diff --git a/README.md b/README.md index 44c227c..f524d6e 100644 --- a/README.md +++ b/README.md @@ -22,14 +22,17 @@ https://github.com/li3zhen1/Grape/assets/45376537/6a1c9510-8af6-4967-9c05-c304b2 #### Features -| Feature | Status | -| --- | --- | -| LinkForce | ✅ | -| ManyBodyForce | ✅ | -| CenterForce | ✅ | -| CollideForce | ✅ | -| PositionForce | | -| RadialForce | ✅ | +| | Status (2D) | Status (3D) | Metal | +| --- | --- | --- | --- | +| **NdTree** | ✅ | 🚧 | | +| **Simulation** | ✅ | 🚧 | 🚧 | +|  LinkForce | ✅ | | | +|  ManyBodyForce | ✅ | | | +|  CenterForce | ✅ | | | +|  CollideForce | ✅ | | | +|  PositionForce | ✅ | | | +|  RadialForce | ✅ | | | +| **SwiftUI View** | 🚧 | | | #### Usage diff --git a/Sources/ForceSimulation/forces/CollideForce.swift b/Sources/ForceSimulation/forces/CollideForce.swift index a03f425..2a54870 100644 --- a/Sources/ForceSimulation/forces/CollideForce.swift +++ b/Sources/ForceSimulation/forces/CollideForce.swift @@ -118,14 +118,13 @@ extension CollideForce: Force { nodes: sim.simulationNodes.map { ($0, $0.position) }, getQuadDelegate: { MaxRadiusQuadTreeDelegate { - // switch self.radius { - // case .constant(let r): - // return r - // case .varied(_): - // return self.calculatedRadius[$0, default: 0.0] - // } - return self.calculatedRadius[$0, default: 0.0] - // return self.calculatedRadius[$0]! + switch self.radius { + case .constant(let r): + return r + case .varied(_): + return self.calculatedRadius[$0, default: 0.0] + } + // return self.calculatedRadius[$0, default: 0.0] } } ) diff --git a/Sources/ForceSimulation/forces/LinkForce.swift b/Sources/ForceSimulation/forces/LinkForce.swift index d6cc69c..cbd319f 100644 --- a/Sources/ForceSimulation/forces/LinkForce.swift +++ b/Sources/ForceSimulation/forces/LinkForce.swift @@ -76,7 +76,6 @@ final public class LinkForce: Force where N: Identifiable { var calculatedBias: [Float] = [] weak var simulation: Simulation? - var iterations: Int internal init( diff --git a/Sources/ForceSimulation/forces/ManyBodyForce.swift b/Sources/ForceSimulation/forces/ManyBodyForce.swift index 74a4c9c..647907b 100644 --- a/Sources/ForceSimulation/forces/ManyBodyForce.swift +++ b/Sources/ForceSimulation/forces/ManyBodyForce.swift @@ -195,9 +195,7 @@ final public class ManyBodyForce: Force where N: Identifiable { let quad = try QuadTree2( nodes: sim.simulationNodes.map { ($0, $0.position) } ) { - // this switch is only called on root init - // but it significantly slows down the performance - // + // this switch is only called on root init return switch self.mass { case .constant(let m): MassQuadTreeDelegate> { _ in m } diff --git a/Sources/ForceSimulation/forces/PositionForce.swift b/Sources/ForceSimulation/forces/PositionForce.swift index 35947e0..b3cb8de 100644 --- a/Sources/ForceSimulation/forces/PositionForce.swift +++ b/Sources/ForceSimulation/forces/PositionForce.swift @@ -5,22 +5,88 @@ // Created by li3zhen1 on 10/1/23. // - final public class PositionForce: Force where N: Identifiable { - var x: Float - var y: Float - var strength: Float - init(x: Float, y: Float, strength: Float = 0.1) { - self.x = x - self.y = y + public enum Direction { + case x + case y + } + public enum TargetOnDirection { + case constant(Float) + case varied([N.ID: Float]) + } + public enum Strength { + case constant(Float) + case varied([N.ID: Float]) + } + public var strength: Strength + public var direction: Direction + public var calculatedStrength: [N.ID: Float] = [:] + public var targetOnDirection: TargetOnDirection + public var calculatedTargetOnDirection: [N.ID: Float] = [:] + + internal init(direction: Direction, targetOnDirection: TargetOnDirection, strength: Strength = .constant(1.0)) { self.strength = strength + self.direction = direction + self.targetOnDirection = targetOnDirection } - weak var simulation: Simulation? + weak var simulation: Simulation? { + didSet { + guard let sim = self.simulation else { return } + self.calculatedStrength = strength.calculated(sim.simulationNodes) + self.calculatedTargetOnDirection = targetOnDirection.calculated(sim.simulationNodes) + } + } public func apply(alpha: Float) { + guard let sim = self.simulation else { return } + let vectorIndex = self.direction == .x ? 0 : 1 + for i in sim.simulationNodes.indices { + let nodeId = sim.simulationNodes[i].id + sim.simulationNodes[i].velocity += ( + self.calculatedTargetOnDirection[nodeId, default: 0.0] - sim.simulationNodes[i].position[vectorIndex] + ) * self.calculatedStrength[nodeId, default: 0.0] * alpha + } + } +} +extension PositionForce.Strength { + func calculated(_ nodes: [SimNode]) -> [N.ID: Float] where SimNode: Identifiable, SimNode.ID == N.ID { + switch self { + case .constant(let value): + return nodes.reduce(into: [:]) { $0[$1.id] = value } + case .varied(let dict): + return dict + } } +} +extension PositionForce.TargetOnDirection { + func calculated(_ nodes: [SimNode]) -> [N.ID: Float] where SimNode: Identifiable, SimNode.ID == N.ID { + switch self { + case .constant(let value): + return nodes.reduce(into: [:]) { $0[$1.id] = value } + case .varied(let dict): + return dict + } + } } + + +public extension Simulation { + func createPositionForce( + direction: PositionForce.Direction, + targetOnDirection: PositionForce.TargetOnDirection, + strength: PositionForce.Strength = .constant(1.0) + ) -> PositionForce { + let force = PositionForce( + direction: direction, + targetOnDirection: targetOnDirection, + strength: strength + ) + force.simulation = self + self.forces.append(force) + return force + } +} \ No newline at end of file diff --git a/Sources/ForceSimulation/mpsforces/CenterForce.metal b/Sources/ForceSimulation/mpsforces/CenterForce.metal new file mode 100644 index 0000000..87462fe --- /dev/null +++ b/Sources/ForceSimulation/mpsforces/CenterForce.metal @@ -0,0 +1,25 @@ +#include +using namespace metal; + +struct Node { + float2 position; + float2 velocity; + float2 fixation; +}; + +kernel void applyCenterForce( + device Node* nodes [[ buffer(0) ]], + constant float2& center [[ buffer(1) ]], + constant float& strength [[ buffer(2) ]], + uint id [[ thread_position_in_grid ]], + uint nodeCount [[ threads_per_grid ]]) +{ + float2 meanPosition = float2(0.0, 0.0); + for (int i = 0; i < nodeCount; ++i) { + meanPosition += nodes[i].position; + } + meanPosition /= float(nodeCount); + + float2 delta = (meanPosition - center) * strength; + nodes[id].position -= delta; +} diff --git a/Sources/ForceSimulation/mpsforces/MPSSimulation.swift b/Sources/ForceSimulation/mpsforces/MPSSimulation.swift new file mode 100644 index 0000000..17aab8a --- /dev/null +++ b/Sources/ForceSimulation/mpsforces/MPSSimulation.swift @@ -0,0 +1,28 @@ +import Foundation +import Metal +import simd +import MetalPerformanceShaders + + +final public class MPSSimulation { + init() { + guard let device = MTLCreateSystemDefaultDevice() else { + fatalError("") + } + + guard let commandQueue = device.makeCommandQueue() else { + fatalError("") + } + + let library = device.makeDefaultLibrary() + + let function = library?.makeFunction(name: "") + + var pipelineState: MTLComputePipelineState + do { + pipelineState = try device.makeComputePipelineState(function: function!) + } catch { + fatalError("") + } + } +} diff --git a/Sources/QuadTree/NdTree.swift b/Sources/QuadTree/NdTree.swift new file mode 100644 index 0000000..623124b --- /dev/null +++ b/Sources/QuadTree/NdTree.swift @@ -0,0 +1,158 @@ +// +// File.swift +// +// +// Created by li3zhen1 on 10/10/23. +// + +import Foundation + +public protocol ComponentComparable { + @inlinable static func <(lhs: Self, rhs: Self) -> Bool + @inlinable static func <=(lhs: Self, rhs: Self) -> Bool + @inlinable static func >(lhs: Self, rhs: Self) -> Bool + @inlinable static func >=(lhs: Self, rhs: Self) -> Bool +} + +public struct NdBox where Coordinate: SIMD, Coordinate: ComponentComparable { + public var p0: Coordinate + public var p1: Coordinate + + @inlinable public init(p0:Coordinate, p1:Coordinate) { + var p0 = p0 + var p1 = p1 + for i in p0.indices { + if p1[i] < p0[i] { + swap(&p0[i], &p1[i]) + } + } + self.p0 = p0 + self.p1 = p1 + } + + public static var unit: Self { .init(p0: .zero, p1: .one) } +} + +extension NdBox { + var area: Float { + var result: Float = 1 + let delta = p1 - p0 + for i in delta.indices { + result *= delta[i] + } + return result + } + + var vec: Coordinate { p1 - p0 } + + var center: Coordinate { (p1+p0) / 2 } + + @inlinable public func getCorner( + of direction: Direction + ) -> Coordinate where Direction: NdDirection, Direction.Coordinate == Coordinate { + var result = Coordinate() + var value = direction.rawValue + + // starting from the last element + for i in (0.. Bool { + return (p0 <= point) && (point < p1) + } +} + +/// Reversed bit representation for nth dimension +/// e.g. for 3d: 0b001 => (x:0, y:0, z:1) +public protocol NdDirection: RawRepresentable { + associatedtype Coordinate: SIMD + var rawValue: Int { get } + var reversed: Self { get } + static var entryCount: Int { get } +} + +//public extension SIMD { +// @inlinable func direction(originalPoint point: Self) -> Direction where Direction: NdDirection, Direction.Coordinate == Self { +// +// } +//} + +struct OctDirection: NdDirection { + typealias Coordinate = SIMD3 + let rawValue: Int + var reversed: OctDirection { + return OctDirection(rawValue: 7-rawValue) + } + static let entryCount: Int = 8 +} + +public struct NdChildren where Coordinate: SIMD { + @usableFromInline internal let children: [T] + + @inlinable public subscript( + at direction: Direction + ) -> T where Direction: NdDirection, Direction.Coordinate == Coordinate { + return children[direction.rawValue] + } +} + + +public protocol TreeNodeDelegate { + associatedtype Node + associatedtype Index: Hashable + mutating func didAddNode(_ node: Index, at position: Vector2f) + mutating func didRemoveNode(_ node: Index, at position: Vector2f) + func copy() -> Self + func createNew() -> Self +} + + +public final class NdTree where C:SIMD, C:ComponentComparable, TD: TreeNodeDelegate { + + public typealias Box = NdBox + public typealias Children = NdChildren, C> + public typealias Index = Int + public typealias Direction = Int + + public private(set) var box: NdBox + + public var nodes: [Index] = [] + public private(set) var children: Children? + public let clusterDistance: Float + public var delegate: TD + + init( + box: Box, + clusterDistance: Float, + rootNodeDelegate: TD + ) { + self.box = box + self.clusterDistance = clusterDistance + self.delegate = rootNodeDelegate.createNew() + } + + public func add(_ nodeIndex: Index, at point: C) { + + } + + + private func cover(_ point: C) { + if box.contains(point) { return } + + repeat { + +// let direction: Direction = +// +// expand(towards: direction) + + } while !box.contains(point) + } + + private func expand(towards direction: Direction) { + + } +} diff --git a/Sources/QuadTree/Octad.swift b/Sources/QuadTree/Octad.swift new file mode 100644 index 0000000..7e86537 --- /dev/null +++ b/Sources/QuadTree/Octad.swift @@ -0,0 +1,177 @@ +// +// Quad.swift +// +// +// Created by li3zhen1 on 9/26/23. +// +// #if arch(wasm32) +// import SimdPolyfill +// #else +import simd + +// #endif + +public typealias Vector3f = simd_float3 + +extension Vector3f: AdditiveArithmetic { + + @inlinable public func lengthSquared() -> Float { + return x * x + y * y + z * z + } + + @inlinable public func length() -> Float { + return (x * x + y * y + z * z).squareRoot() + } + + @inlinable public func squaredDistanceTo(_ point: Self) -> Float { + return (self - point).lengthSquared() + } + + @inlinable public func distanceTo(_ point: Self) -> Float { + return (self - point).length() + } +} + +public struct Octad { + public typealias Coordinate = simd_float3 + + private var x0y0z0: Coordinate + private var x1y1z1: Coordinate + + public var x0: Float { + get { + x0y0z0.x + } + set { + x0y0z0.x = newValue + } + } + + public var x1: Float { + get { + x1y1z1.x + } + set { + x1y1z1.x = newValue + } + } + + public var y0: Float { + get { + x0y0z0.y + } + set { + x0y0z0.y = newValue + } + } + + public var y1: Float { + get { + x1y1z1.y + } + set { + x1y1z1.y = newValue + } + } + + public var z0: Float { + get { + x0y0z0.z + } + set { + x0y0z0.z = newValue + } + } + + public var z1: Float { + get { + x1y1z1.z + } + set { + x1y1z1.z = newValue + } + } + + private init(x0y0z0: Coordinate, x1y1z1: Coordinate) { + self.x0y0z0 = x0y0z0 + self.x1y1z1 = x1y1z1 + } + + public init(corner: Coordinate, oppositeCorner: Coordinate) { + let x0 = corner.x + let x1 = oppositeCorner.x + let y0 = corner.y + let y1 = oppositeCorner.y + let z0 = corner.z + let z1 = oppositeCorner.z + self.init(x0: x0, x1: x1, y0: y0, y1: y1, z0: z0, z1: z1) + } + + public init(x0: Float, x1: Float, y0: Float, y1: Float, z0: Float, z1: Float) { + switch (x1 < x0, y1 < y0, z1 < z0) { + case (true, true, true): + self.x0y0z0 = Coordinate(x: x1, y: y1, z: z1) + self.x1y1z1 = Coordinate(x: x0, y: y0, z: z0) + case (true, true, false): + self.x0y0z0 = Coordinate(x: x1, y: y1, z: z0) + self.x1y1z1 = Coordinate(x: x0, y: y0, z: z1) + case (true, false, true): + self.x0y0z0 = Coordinate(x: x1, y: y0, z: z1) + self.x1y1z1 = Coordinate(x: x0, y: y1, z: z0) + case (true, false, false): + self.x0y0z0 = Coordinate(x: x1, y: y0, z: z0) + self.x1y1z1 = Coordinate(x: x0, y: y1, z: z1) + case (false, true, true): + self.x0y0z0 = Coordinate(x: x0, y: y1, z: z1) + self.x1y1z1 = Coordinate(x: x1, y: y0, z: z0) + case (false, true, false): + self.x0y0z0 = Coordinate(x: x0, y: y1, z: z0) + self.x1y1z1 = Coordinate(x: x1, y: y0, z: z1) + case (false, false, true): + self.x0y0z0 = Coordinate(x: x0, y: y0, z: z1) + self.x1y1z1 = Coordinate(x: x1, y: y1, z: z0) + case (false, false, false): + self.x0y0z0 = Coordinate(x: x0, y: y0, z: z0) + self.x1y1z1 = Coordinate(x: x1, y: y1, z: z1) + + } + } + + public static let placeholder = Self(x0: 0, x1: 1, y0: 0, y1: 1, z0: 0, z1: 1) +} + +public enum Octant: Int { + case northWestForward = 0 + case northEastForward = 1 + case southWestForward = 2 + case southEastForward = 3 + case northWestBackward = 4 + case northEastBackward = 5 + case southWestBackward = 6 + case southEastBackward = 7 + + var reversed: Self { + return Octant(rawValue: 7 - self.rawValue)! + } + + static let allValues: [Self] = [ + .northWestForward, + .northEastForward, + .southWestForward, + .southEastForward, + .northWestBackward, + .northEastBackward, + .southWestBackward, + .southEastBackward, + ] +} + +public struct OctadChildren { + @usableFromInline let children: [T] + + @inlinable public subscript(at octant: Octant) -> T { + get { + return children[octant.rawValue] + } + } +} diff --git a/Sources/QuadTree/QuadTree2.swift b/Sources/QuadTree/QuadTree2.swift index 6065067..103378d 100644 --- a/Sources/QuadTree/QuadTree2.swift +++ b/Sources/QuadTree/QuadTree2.swift @@ -9,7 +9,7 @@ import simd // #endif // TODO: https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&ved=2ahUKEwjoh_vKttuBAxUunokEHdchDZAQFnoECBkQAQ&url=https%3A%2F%2Fosf.io%2Fdu6gq%2Fdownload%2F%3Fversion%3D1%26displayName%3Dgove-2018-updating-tree-approximations-2018-06-13T02%253A16%253A17.463Z.pdf&usg=AOvVaw3KFAE5U8cnhTDMN_qrzV6a&opi=89978449 -public class QuadTreeNode2 where N: Identifiable, QD: QuadDelegate, QD.Node == N { +public final class QuadTreeNode2 where N: Identifiable, QD: QuadDelegate, QD.Node == N { public private(set) var quad: Quad diff --git a/Tests/QuadTreeTests/NdTreeTests.swift b/Tests/QuadTreeTests/NdTreeTests.swift new file mode 100644 index 0000000..1cd2f48 --- /dev/null +++ b/Tests/QuadTreeTests/NdTreeTests.swift @@ -0,0 +1,44 @@ +import XCTest +@testable import QuadTree + +extension SIMD3: ComponentComparable { + public static func < (lhs: SIMD3, rhs: SIMD3) -> Bool { + return lhs.x < rhs.x && lhs.y < rhs.y && lhs.z < rhs.z + } + + public static func > (lhs: SIMD3, rhs: SIMD3) -> Bool { + return lhs.x > rhs.x && lhs.y > rhs.y && lhs.z > rhs.z + } + + public static func <= (lhs: SIMD3, rhs: SIMD3) -> Bool { + return lhs.x <= rhs.x && lhs.y <= rhs.y && lhs.z <= rhs.z + } + + public static func >= (lhs: SIMD3, rhs: SIMD3) -> Bool { + return lhs.x >= rhs.x && lhs.y >= rhs.y && lhs.z >= rhs.z + } + +} + +final class NdTreeTests: XCTestCase { + + func testNdBoxSwap() { + let box = NdBox(p0: SIMD3.one, p1: SIMD3.zero) + assert(box.p0 == .zero) + } + + func testNdBoxGetCorner() { + let box = NdBox(p0: SIMD3.one, p1: SIMD3.zero) + + + + let direction = OctDirection(rawValue: 3) + assert(box.getCorner(of: direction) == .init(0, 1, 1)) + + + let direction2 = OctDirection(rawValue: 4) + assert(box.getCorner(of: direction2) == .init(1, 0, 0)) + + + } +}