├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── Assets ├── Images │ └── vertexkit-particle-noise.png ├── Pixels-3D_logo_1k.png ├── Pixels-3D_logo_1k_bg.png ├── Pixels-3D_logo_4k.png └── Pixels-3D_logo_4k_bg.png ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Source ├── Shaders │ └── Color3DPIX.metal ├── VTXs │ └── Content │ │ └── Generator │ │ ├── Particles │ │ ├── ParticlesPIX.swift │ │ ├── ParticlesPixelModel.swift │ │ └── ParticlesVTX.metal │ │ └── UVParticles │ │ ├── UVParticlesPIX.swift │ │ ├── UVParticlesPixelModel.swift │ │ └── UVParticlesVTX.metal └── VertexKit.swift └── VertexKit.xcworkspace ├── contents.xcworkspacedata └── xcshareddata ├── IDEWorkspaceChecks.plist └── swiftpm └── Package.resolved /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | xcuserdata/ 4 | 5 | build/* 6 | docs/* 7 | 8 | Pods/* -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Assets/Images/vertexkit-particle-noise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heestand-xyz/VertexKit/2be56472a4fb43ce851dd7875864772b6b5c7fe3/Assets/Images/vertexkit-particle-noise.png -------------------------------------------------------------------------------- /Assets/Pixels-3D_logo_1k.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heestand-xyz/VertexKit/2be56472a4fb43ce851dd7875864772b6b5c7fe3/Assets/Pixels-3D_logo_1k.png -------------------------------------------------------------------------------- /Assets/Pixels-3D_logo_1k_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heestand-xyz/VertexKit/2be56472a4fb43ce851dd7875864772b6b5c7fe3/Assets/Pixels-3D_logo_1k_bg.png -------------------------------------------------------------------------------- /Assets/Pixels-3D_logo_4k.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heestand-xyz/VertexKit/2be56472a4fb43ce851dd7875864772b6b5c7fe3/Assets/Pixels-3D_logo_4k.png -------------------------------------------------------------------------------- /Assets/Pixels-3D_logo_4k_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heestand-xyz/VertexKit/2be56472a4fb43ce851dd7875864772b6b5c7fe3/Assets/Pixels-3D_logo_4k_bg.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 ⬢ Hexagons 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CoreGraphicsExtensions", 6 | "repositoryURL": "https://github.com/heestand-xyz/CoreGraphicsExtensions", 7 | "state": { 8 | "branch": null, 9 | "revision": "89b30426f001988a878905d9cdcec0647dc17281", 10 | "version": "1.3.1" 11 | } 12 | }, 13 | { 14 | "package": "PixelColor", 15 | "repositoryURL": "https://github.com/heestand-xyz/PixelColor", 16 | "state": { 17 | "branch": null, 18 | "revision": "04d2444fc288edc5fa5cbfd85bbbc9128b63e6db", 19 | "version": "1.3.4" 20 | } 21 | }, 22 | { 23 | "package": "PixelKit", 24 | "repositoryURL": "https://github.com/heestand-xyz/PixelKit", 25 | "state": { 26 | "branch": null, 27 | "revision": "f663ac4951ac04138797dfc5d819c875b7010325", 28 | "version": "3.0.3" 29 | } 30 | }, 31 | { 32 | "package": "RenderKit", 33 | "repositoryURL": "https://github.com/heestand-xyz/RenderKit", 34 | "state": { 35 | "branch": null, 36 | "revision": "71c1363f8ea93405209b4d20303b3caa1058501a", 37 | "version": "2.0.2" 38 | } 39 | }, 40 | { 41 | "package": "Resolution", 42 | "repositoryURL": "https://github.com/heestand-xyz/Resolution", 43 | "state": { 44 | "branch": null, 45 | "revision": "0fdee03d7b312f075897cdac437f366cc6631d6a", 46 | "version": "1.0.4" 47 | } 48 | }, 49 | { 50 | "package": "TextureMap", 51 | "repositoryURL": "https://github.com/heestand-xyz/TextureMap", 52 | "state": { 53 | "branch": null, 54 | "revision": "dd04b7c1bf64853afa4c045a511bbca7f5b12f54", 55 | "version": "0.5.0" 56 | } 57 | } 58 | ] 59 | }, 60 | "version": 1 61 | } 62 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "VertexKit", 7 | platforms: [ 8 | .iOS(.v13), 9 | .macOS(.v10_15), 10 | .tvOS(.v13) 11 | ], 12 | products: [ 13 | .library(name: "VertexKit", targets: ["VertexKit"]), 14 | ], 15 | dependencies: [ 16 | .package(url: "https://github.com/heestand-xyz/PixelKit", from: "3.0.1"), 17 | .package(url: "https://github.com/heestand-xyz/PixelColor", from: "1.3.2"), 18 | .package(url: "https://github.com/heestand-xyz/Resolution", from: "1.0.4"), 19 | .package(url: "https://github.com/heestand-xyz/CoreGraphicsExtensions", from: "1.3.2"), 20 | ], 21 | targets: [ 22 | .target(name: "VertexKit", dependencies: ["PixelKit", "PixelColor", "Resolution", "CoreGraphicsExtensions"], path: "Source"), 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # VertexKit 4 | 5 | a Framework for iOS & macOS
6 | written in Swift & Metal
7 | an extension of [PixelKit](https://github.com/heestand-xyz/pixelkit)
8 | 9 | ## Particles Example 10 | 11 | 12 | 13 | ```swift 14 | view.wantsLayer = true 15 | view.layer!.backgroundColor = .black 16 | 17 | PixelKit.main.render.bits = ._16 18 | 19 | let pres: Resolution = .square(Int(sqrt(1_000_000))) 20 | 21 | let noise = NoisePIX(at: pres) 22 | noise.colored = true 23 | noise.octaves = 5 24 | noise.zPosition = .live * 0.1 25 | 26 | let particles = UVParticlesPIX(at: .size(view.bounds.size) * 2) 27 | particles.vtxPixIn = noise - 0.5 28 | particles.color = LiveColor(lum: 1.0, a: 0.1) 29 | 30 | let finalPix: PIX = particles 31 | finalPix.view.frame = view.bounds 32 | finalPix.view.checker = false 33 | view.addSubview(finalPix.view) 34 | ``` 35 | -------------------------------------------------------------------------------- /Source/Shaders/Color3DPIX.metal: -------------------------------------------------------------------------------- 1 | // 2 | // Color3DPIX.metal 3 | // VertexKit 4 | // 5 | // Created by Hexagons on 2018-09-22. 6 | // Copyright © 2018 Hexagons. All rights reserved. 7 | // 8 | 9 | #include 10 | using namespace metal; 11 | 12 | struct VertexOut{ 13 | float4 position [[position]]; 14 | float2 texCoord; 15 | float4 color; 16 | }; 17 | 18 | struct Uniforms{ 19 | float r; 20 | float g; 21 | float b; 22 | float a; 23 | }; 24 | 25 | fragment float4 color3DPIX(VertexOut out [[stage_in]], 26 | const device Uniforms& in [[ buffer(0) ]], 27 | sampler s [[ sampler(0) ]]) { 28 | return float4(in.r * out.color.r, 29 | in.g * out.color.g, 30 | in.b * out.color.b, 31 | in.a * out.color.a); 32 | } 33 | 34 | -------------------------------------------------------------------------------- /Source/VTXs/Content/Generator/Particles/ParticlesPIX.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UVParticlesPIX.swift 3 | // VertexKit 4 | // 5 | // Created by Anton Heestand on 2019-05-02. 6 | // Copyright © 2019 Hexagons. All rights reserved. 7 | // 8 | 9 | import CoreGraphics 10 | import CoreGraphicsExtensions 11 | import Metal 12 | import RenderKit 13 | import PixelKit 14 | import Resolution 15 | import PixelColor 16 | 17 | public class ParticlesPIX: PIXGenerator, CustomGeometryDelegate { 18 | 19 | public typealias Model = ParticlesPixelModel 20 | 21 | private var model: Model { 22 | get { generatorModel as! Model } 23 | set { generatorModel = newValue } 24 | } 25 | 26 | open override var customMetalLibrary: MTLLibrary { VertexKit.metalLibrary } 27 | open override var customVertexShaderName: String? { "particlesVTX" } 28 | open override var shaderName: String { "color3DPIX" } 29 | 30 | public override var additiveVertexBlending: Bool { true } 31 | 32 | struct Particle { 33 | var position: CGPoint 34 | var velocity: CGPoint 35 | var lifeTime: Double = 0.0 36 | } 37 | private var particles: [Particle] = [] 38 | 39 | // MARK: Properties 40 | 41 | @LiveColor("clearBackgroundColor") public var clearBackgroundColor: PixelColor = .black 42 | @LiveFloat("lifeTime") public var lifeTime: CGFloat = 1.0 43 | @LiveInt("emitCount", range: 0...10) public var emitCount: Int = 1 44 | @LivePoint("emitPosition") public var emitPosition: CGPoint = .zero 45 | @LiveSize("emitSize") public var emitSize: CGSize = .zero 46 | @LivePoint("direction") public var direction: CGPoint = CGPoint(x: 0.0, y: 0.0) 47 | @LiveFloat("randomDirection") public var randomDirection: CGFloat = 1.0 48 | @LiveFloat("velocity", range: 0.0...0.01, increment: 0.001) public var velocity: CGFloat = 0.005 49 | @LiveFloat("randomVelocity", range: 0.0...0.01, increment: 0.001) public var randomVelocity: CGFloat = 0.0 50 | @LiveFloat("particleSize", range: 0.0...2.0, increment: 1.0) public var particleSize: CGFloat = 1.0 51 | 52 | // MARK: Property Helpers 53 | 54 | public override var liveList: [LiveWrap] { 55 | super.liveList.filter({ liveWrap in 56 | liveWrap.typeName != "backgroundColor" 57 | }) + [_clearBackgroundColor, _lifeTime, _emitCount, _emitPosition, _emitSize, _direction, _randomDirection, _velocity, _randomVelocity, _particleSize] 58 | } 59 | 60 | public override var uniforms: [CGFloat] { 61 | color.components 62 | } 63 | 64 | open override var vertexUniforms: [CGFloat] { 65 | [particleSize] 66 | } 67 | 68 | // MARK: - Life Cycle - 69 | 70 | public init(model: Model) { 71 | super.init(model: model) 72 | setup() 73 | } 74 | 75 | public required init(at resolution: Resolution = .auto) { 76 | let model = Model(resolution: resolution) 77 | super.init(model: model) 78 | setup() 79 | } 80 | 81 | // MARK: - Setup 82 | 83 | private func setup() { 84 | 85 | customGeometryActive = true 86 | customGeometryDelegate = self 87 | 88 | clearColor = clearBackgroundColor 89 | _clearBackgroundColor.didSetValue = { [weak self] in 90 | self?.clearColor = self?.clearBackgroundColor ?? .clear 91 | } 92 | 93 | PixelKit.main.render.listenToFrames(id: id) { [weak self] in 94 | self?.particleLoop() 95 | } 96 | 97 | } 98 | 99 | public override func destroy() { 100 | super.destroy() 101 | 102 | PixelKit.main.render.unlistenToFrames(for: id) 103 | } 104 | 105 | // MARK: - Live Model 106 | 107 | public override func modelUpdateLive() { 108 | super.modelUpdateLive() 109 | 110 | clearBackgroundColor = model.clearBackgroundColor 111 | lifeTime = model.lifeTime 112 | emitCount = model.emitCount 113 | emitPosition = model.emitPosition 114 | emitSize = model.emitSize 115 | direction = model.direction 116 | randomDirection = model.randomDirection 117 | velocity = model.velocity 118 | randomVelocity = model.randomVelocity 119 | particleSize = model.particleSize 120 | 121 | super.modelUpdateLiveDone() 122 | } 123 | 124 | public override func liveUpdateModel() { 125 | super.liveUpdateModel() 126 | 127 | model.clearBackgroundColor = clearBackgroundColor 128 | model.lifeTime = lifeTime 129 | model.emitCount = emitCount 130 | model.emitPosition = emitPosition 131 | model.emitSize = emitSize 132 | model.direction = direction 133 | model.randomDirection = randomDirection 134 | model.velocity = velocity 135 | model.randomVelocity = randomVelocity 136 | model.particleSize = particleSize 137 | 138 | super.liveUpdateModelDone() 139 | } 140 | 141 | // MARK: - Particle Loop 142 | 143 | private func particleLoop() { 144 | addParticles() 145 | moveParticles() 146 | removeParticles() 147 | render() 148 | } 149 | 150 | private func addParticles() { 151 | guard emitCount > 0 else { return } 152 | for _ in 0.. 0.0 { 159 | velocity += CGPoint(x: .random(in: -1.0...1.0) * randomDirection * self.velocity, 160 | y: .random(in: -1.0...1.0) * randomDirection * self.velocity) 161 | } 162 | let particle = Particle(position: position, velocity: velocity) 163 | particles.append(particle) 164 | } 165 | } 166 | 167 | private func moveParticles() { 168 | for (index, particle) in particles.enumerated() { 169 | var velocity = particle.velocity 170 | if randomVelocity > 0.0 { 171 | velocity += CGPoint(x: .random(in: -1.0...1.0) * randomVelocity, 172 | y: .random(in: -1.0...1.0) * randomVelocity) 173 | } 174 | particles[index].position += velocity 175 | particles[index].lifeTime += PixelKit.main.render.secondsPerFrame 176 | } 177 | } 178 | 179 | private func removeParticles() { 180 | for (index, particle) in particles.enumerated().reversed() { 181 | if particle.lifeTime >= lifeTime { 182 | particles.remove(at: index) 183 | } 184 | } 185 | } 186 | 187 | public func removeAllParticles() { 188 | particles = [] 189 | render() 190 | } 191 | 192 | // MARK: - Custom Vertices 193 | 194 | public func customVertices() -> RenderKit.Vertices? { 195 | 196 | let count = particles.count 197 | var vertices: [RenderKit.Vertex] = [] 198 | for particle in particles { 199 | vertices.append(Vertex(x: particle.position.x, 200 | y: particle.position.y)) 201 | } 202 | 203 | let vertexBuffer = vertices.flatMap(\.buffer3d) 204 | let vertexBufferSize = max(vertexBuffer.count, 1) * MemoryLayout.size 205 | let vertexBufferBuffer = PixelKit.main.render.metalDevice.makeBuffer(bytes: !vertexBuffer.isEmpty ? vertexBuffer : [0.0], length: vertexBufferSize, options: [])! 206 | 207 | return RenderKit.Vertices(buffer: vertexBufferBuffer, vertexCount: count, type: .point, wireframe: false) 208 | } 209 | 210 | } 211 | -------------------------------------------------------------------------------- /Source/VTXs/Content/Generator/Particles/ParticlesPixelModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Anton Heestand on 2022-01-15. 3 | // 4 | 5 | import Foundation 6 | import CoreGraphics 7 | import RenderKit 8 | import PixelKit 9 | import Resolution 10 | import PixelColor 11 | 12 | public struct ParticlesPixelModel: PixelGeneratorModel { 13 | 14 | // MARK: Global 15 | 16 | public var id: UUID = UUID() 17 | public var name: String = "Particles" 18 | public var typeName: String = "vtx-pix-content-generator-particles" 19 | public var bypass: Bool = false 20 | 21 | public var outputNodeReferences: [NodeReference] = [] 22 | 23 | public var viewInterpolation: ViewInterpolation = .linear 24 | public var interpolation: PixelInterpolation = .linear 25 | public var extend: ExtendMode = .zero 26 | 27 | public var premultiply: Bool = true 28 | public var resolution: Resolution = .auto 29 | 30 | public var backgroundColor: PixelColor = .black 31 | public var color: PixelColor = .white 32 | 33 | // MARK: Local 34 | 35 | public var clearBackgroundColor: PixelColor = .black 36 | public var lifeTime: CGFloat = 1.0 37 | public var emitCount: Int = 1 38 | public var emitPosition: CGPoint = .zero 39 | public var emitSize: CGSize = .zero 40 | public var direction: CGPoint = .zero 41 | public var randomDirection: CGFloat = 1.0 42 | public var velocity: CGFloat = 0.005 43 | public var randomVelocity: CGFloat = 0.0 44 | public var particleSize: CGFloat = 0.1 45 | } 46 | 47 | extension ParticlesPixelModel { 48 | 49 | enum LocalCodingKeys: String, CodingKey, CaseIterable { 50 | case clearBackgroundColor 51 | case lifeTime 52 | case emitCount 53 | case emitPosition 54 | case emitSize 55 | case direction 56 | case randomDirection 57 | case velocity 58 | case randomVelocity 59 | case particleSize 60 | } 61 | 62 | public init(from decoder: Decoder) throws { 63 | 64 | self = try PixelGeneratorModelDecoder.decode(from: decoder, model: self) as! Self 65 | 66 | let container = try decoder.container(keyedBy: LocalCodingKeys.self) 67 | 68 | if try PixelModelDecoder.isLiveListCodable(decoder: decoder) { 69 | let liveList: [LiveWrap] = try PixelModelDecoder.liveListDecode(from: decoder) 70 | for codingKey in LocalCodingKeys.allCases { 71 | guard let liveWrap: LiveWrap = liveList.first(where: { $0.typeName == codingKey.rawValue }) else { continue } 72 | 73 | switch codingKey { 74 | case .clearBackgroundColor: 75 | guard let live = liveWrap as? LiveColor else { continue } 76 | clearBackgroundColor = live.wrappedValue 77 | case .lifeTime: 78 | guard let live = liveWrap as? LiveFloat else { continue } 79 | lifeTime = live.wrappedValue 80 | case .emitCount: 81 | guard let live = liveWrap as? LiveInt else { continue } 82 | emitCount = live.wrappedValue 83 | case .emitPosition: 84 | guard let live = liveWrap as? LivePoint else { continue } 85 | emitPosition = live.wrappedValue 86 | case .emitSize: 87 | guard let live = liveWrap as? LiveSize else { continue } 88 | emitSize = live.wrappedValue 89 | case .direction: 90 | guard let live = liveWrap as? LivePoint else { continue } 91 | direction = live.wrappedValue 92 | case .randomDirection: 93 | guard let live = liveWrap as? LiveFloat else { continue } 94 | randomDirection = live.wrappedValue 95 | case .velocity: 96 | guard let live = liveWrap as? LiveFloat else { continue } 97 | velocity = live.wrappedValue 98 | case .randomVelocity: 99 | guard let live = liveWrap as? LiveFloat else { continue } 100 | randomVelocity = live.wrappedValue 101 | case .particleSize: 102 | guard let live = liveWrap as? LiveFloat else { continue } 103 | particleSize = live.wrappedValue 104 | } 105 | } 106 | return 107 | } 108 | 109 | clearBackgroundColor = try container.decode(PixelColor.self, forKey: .clearBackgroundColor) 110 | lifeTime = try container.decode(CGFloat.self, forKey: .lifeTime) 111 | emitCount = try container.decode(Int.self, forKey: .emitCount) 112 | emitPosition = try container.decode(CGPoint.self, forKey: .emitPosition) 113 | emitSize = try container.decode(CGSize.self, forKey: .emitSize) 114 | direction = try container.decode(CGPoint.self, forKey: .direction) 115 | randomDirection = try container.decode(CGFloat.self, forKey: .randomDirection) 116 | velocity = try container.decode(CGFloat.self, forKey: .velocity) 117 | randomVelocity = try container.decode(CGFloat.self, forKey: .randomVelocity) 118 | particleSize = try container.decode(CGFloat.self, forKey: .particleSize) 119 | } 120 | } 121 | 122 | extension ParticlesPixelModel { 123 | 124 | public func isEqual(to nodeModel: NodeModel) -> Bool { 125 | guard let pixelModel = nodeModel as? Self else { return false } 126 | guard isPixelGeneratorEqual(to: pixelModel) else { return false } 127 | guard clearBackgroundColor == pixelModel.clearBackgroundColor else { return false } 128 | guard lifeTime == pixelModel.lifeTime else { return false } 129 | guard emitCount == pixelModel.emitCount else { return false } 130 | guard emitPosition == pixelModel.emitPosition else { return false } 131 | guard emitSize == pixelModel.emitSize else { return false } 132 | guard direction == pixelModel.direction else { return false } 133 | guard randomDirection == pixelModel.randomDirection else { return false } 134 | guard velocity == pixelModel.velocity else { return false } 135 | guard randomVelocity == pixelModel.randomVelocity else { return false } 136 | guard particleSize == pixelModel.particleSize else { return false } 137 | return true 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Source/VTXs/Content/Generator/Particles/ParticlesVTX.metal: -------------------------------------------------------------------------------- 1 | // 2 | // ParticleUV3DVTX.metal 3 | // VertexKitShaders 4 | // 5 | // Created by Anton Heestand on 2019-05-02. 6 | // Copyright © 2019 Hexagons. All rights reserved. 7 | // 8 | 9 | #include 10 | using namespace metal; 11 | 12 | struct VertexIn{ 13 | packed_float3 position; 14 | packed_float2 texCoord; 15 | }; 16 | 17 | struct VertexOut{ 18 | float4 position [[position]]; 19 | float pointSize [[point_size]]; 20 | float4 color; 21 | }; 22 | 23 | struct Uniforms { 24 | float size; 25 | }; 26 | 27 | vertex VertexOut particlesVTX(const device VertexIn* vertices [[ buffer(0) ]], 28 | unsigned int vid [[ vertex_id ]], 29 | const device Uniforms& in [[ buffer(1) ]], 30 | sampler s [[ sampler(0) ]]) { 31 | 32 | int index = vid; 33 | 34 | float x = vertices[index].position.x * 2; 35 | float y = vertices[index].position.y * 2; 36 | float z = 0.0; 37 | 38 | float4 color = float4(1, 1, 1, 1); 39 | 40 | VertexOut vtxOut; 41 | vtxOut.position = float4(x, y, z, 1); 42 | vtxOut.pointSize = in.size; 43 | vtxOut.color = color; 44 | 45 | return vtxOut; 46 | } 47 | -------------------------------------------------------------------------------- /Source/VTXs/Content/Generator/UVParticles/UVParticlesPIX.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UVParticlesPIX.swift 3 | // VertexKit 4 | // 5 | // Created by Anton Heestand on 2019-05-02. 6 | // Copyright © 2019 Hexagons. All rights reserved. 7 | // 8 | 9 | import CoreGraphics 10 | import Metal 11 | import RenderKit 12 | import PixelKit 13 | import Resolution 14 | import PixelColor 15 | 16 | public class UVParticlesPIX: PIXGenerator, CustomGeometryDelegate { 17 | 18 | public typealias Model = UVParticlesPixelModel 19 | 20 | private var model: Model { 21 | get { generatorModel as! Model } 22 | set { generatorModel = newValue } 23 | } 24 | 25 | open override var customMetalLibrary: MTLLibrary { VertexKit.metalLibrary } 26 | open override var customVertexShaderName: String? { "uvParticlesVTX" } 27 | open override var shaderName: String { "color3DPIX" } 28 | 29 | public override var customVertexTextureActive: Bool { true } 30 | public override var customVertexNodeIn: (NODE & NODEOut)? { 31 | particlesInput 32 | } 33 | 34 | public override var additiveVertexBlending: Bool { true } 35 | 36 | @LiveColor("clearBackgroundColor") public var clearBackgroundColor: PixelColor = .black 37 | 38 | @LiveFloat("particleSize", range: 0.0...2.0, increment: 1.0) public var particleSize: CGFloat = 1.0 39 | /// Map Size of each particle from the blue channel 40 | @LiveBool("hasSize") public var hasSize: Bool = false 41 | /// Map Alpha of each particle from the alpha channel 42 | @LiveBool("hasAlpha") public var hasAlpha: Bool = false 43 | /// `hasAlpha` needs to be `true`. The clips any alpha below 1.0. 44 | @LiveBool("hasAlphaClip") public var hasAlphaClip: Bool = false 45 | 46 | @available(*, deprecated, renamed: "particlesInput") 47 | public var vtxPixIn: (PIX & NODEOut)? { 48 | get { particlesInput } 49 | set { particlesInput = newValue } 50 | } 51 | public var particlesInput: (PIX & NODEOut)? { didSet { render() } } 52 | 53 | public override var liveList: [LiveWrap] { 54 | super.liveList.filter({ liveWrap in 55 | liveWrap.typeName != "backgroundColor" 56 | }) + [_particleSize, _hasSize, _hasAlpha, _hasAlphaClip] 57 | } 58 | 59 | public override var uniforms: [CGFloat] { 60 | color.components 61 | } 62 | open override var vertexUniforms: [CGFloat] { 63 | [particleSize, particlesInput?.finalResolution.width ?? 1, particlesInput?.finalResolution.height ?? 1, hasSize ? 1 : 0, hasAlpha ? 1 : 0, hasAlphaClip ? 1 : 0, resolution.aspect] 64 | } 65 | 66 | // MARK: - Life Cycle - 67 | 68 | public init(model: Model) { 69 | super.init(model: model) 70 | setup() 71 | } 72 | 73 | public required init(at resolution: Resolution = .auto) { 74 | let model = Model(resolution: resolution) 75 | super.init(model: model) 76 | setup() 77 | } 78 | 79 | // MARK: - Setup 80 | 81 | private func setup() { 82 | customGeometryActive = true 83 | customGeometryDelegate = self 84 | clearColor = clearBackgroundColor 85 | _clearBackgroundColor.didSetValue = { [weak self] in 86 | self?.clearColor = self?.clearBackgroundColor ?? .clear 87 | } 88 | } 89 | 90 | // MARK: - Live Model 91 | 92 | public override func modelUpdateLive() { 93 | super.modelUpdateLive() 94 | 95 | clearBackgroundColor = model.clearBackgroundColor 96 | particleSize = model.particleSize 97 | hasSize = model.hasSize 98 | hasAlpha = model.hasAlpha 99 | hasAlphaClip = model.hasAlphaClip 100 | 101 | super.modelUpdateLiveDone() 102 | } 103 | 104 | public override func liveUpdateModel() { 105 | super.liveUpdateModel() 106 | 107 | model.clearBackgroundColor = clearBackgroundColor 108 | model.particleSize = particleSize 109 | model.hasSize = hasSize 110 | model.hasAlpha = hasAlpha 111 | model.hasAlphaClip = hasAlphaClip 112 | 113 | super.liveUpdateModelDone() 114 | } 115 | 116 | // MARK: Custom Geometry 117 | 118 | public func customVertices() -> RenderKit.Vertices? { 119 | 120 | let count = (particlesInput?.finalResolution.w ?? 1) * (particlesInput?.finalResolution.h ?? 1) 121 | let vertexBuffers: [Float] = [Float](repeating: 0.0, count: 1)//count) 122 | 123 | let vertexBuffersSize = vertexBuffers.count * MemoryLayout.size 124 | let verticesBuffer = PixelKit.main.render.metalDevice.makeBuffer(bytes: vertexBuffers, length: vertexBuffersSize, options: [])! 125 | 126 | return RenderKit.Vertices(buffer: verticesBuffer, vertexCount: count, type: .point, wireframe: false) 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /Source/VTXs/Content/Generator/UVParticles/UVParticlesPixelModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Anton Heestand on 2022-01-15. 3 | // 4 | 5 | import Foundation 6 | import CoreGraphics 7 | import RenderKit 8 | import PixelKit 9 | import Resolution 10 | import PixelColor 11 | 12 | public struct UVParticlesPixelModel: PixelGeneratorModel { 13 | 14 | // MARK: Global 15 | 16 | public var id: UUID = UUID() 17 | public var name: String = "UV Particles" 18 | public var typeName: String = "vtx-pix-content-generator-uv-particles" 19 | public var bypass: Bool = false 20 | 21 | public var outputNodeReferences: [NodeReference] = [] 22 | 23 | public var viewInterpolation: ViewInterpolation = .linear 24 | public var interpolation: PixelInterpolation = .linear 25 | public var extend: ExtendMode = .zero 26 | 27 | public var premultiply: Bool = true 28 | public var resolution: Resolution = .auto 29 | 30 | public var backgroundColor: PixelColor = .black 31 | public var color: PixelColor = .white 32 | 33 | // MARK: Local 34 | 35 | public var clearBackgroundColor: PixelColor = .black 36 | public var particleSize: CGFloat = 0.1 37 | public var hasSize: Bool = false 38 | public var hasAlpha: Bool = false 39 | public var hasAlphaClip: Bool = false 40 | } 41 | 42 | extension UVParticlesPixelModel { 43 | 44 | enum LocalCodingKeys: String, CodingKey, CaseIterable { 45 | case clearBackgroundColor 46 | case particleSize 47 | case hasSize 48 | case hasAlpha 49 | case hasAlphaClip 50 | } 51 | 52 | public init(from decoder: Decoder) throws { 53 | 54 | self = try PixelGeneratorModelDecoder.decode(from: decoder, model: self) as! Self 55 | 56 | let container = try decoder.container(keyedBy: LocalCodingKeys.self) 57 | 58 | if try PixelModelDecoder.isLiveListCodable(decoder: decoder) { 59 | let liveList: [LiveWrap] = try PixelModelDecoder.liveListDecode(from: decoder) 60 | for codingKey in LocalCodingKeys.allCases { 61 | guard let liveWrap: LiveWrap = liveList.first(where: { $0.typeName == codingKey.rawValue }) else { continue } 62 | 63 | switch codingKey { 64 | case .clearBackgroundColor: 65 | guard let live = liveWrap as? LiveColor else { continue } 66 | clearBackgroundColor = live.wrappedValue 67 | case .particleSize: 68 | guard let live = liveWrap as? LiveFloat else { continue } 69 | particleSize = live.wrappedValue 70 | case .hasSize: 71 | guard let live = liveWrap as? LiveBool else { continue } 72 | hasSize = live.wrappedValue 73 | case .hasAlpha: 74 | guard let live = liveWrap as? LiveBool else { continue } 75 | hasAlpha = live.wrappedValue 76 | case .hasAlphaClip: 77 | guard let live = liveWrap as? LiveBool else { continue } 78 | hasAlphaClip = live.wrappedValue 79 | } 80 | } 81 | return 82 | } 83 | 84 | clearBackgroundColor = try container.decode(PixelColor.self, forKey: .clearBackgroundColor) 85 | particleSize = try container.decode(CGFloat.self, forKey: .particleSize) 86 | hasSize = try container.decode(Bool.self, forKey: .hasSize) 87 | hasAlpha = try container.decode(Bool.self, forKey: .hasAlpha) 88 | hasAlphaClip = try container.decode(Bool.self, forKey: .hasAlphaClip) 89 | } 90 | } 91 | 92 | extension UVParticlesPixelModel { 93 | 94 | public func isEqual(to nodeModel: NodeModel) -> Bool { 95 | guard let pixelModel = nodeModel as? Self else { return false } 96 | guard isPixelGeneratorEqual(to: pixelModel) else { return false } 97 | guard clearBackgroundColor == pixelModel.clearBackgroundColor else { return false } 98 | guard particleSize == pixelModel.particleSize else { return false } 99 | guard hasSize == pixelModel.hasSize else { return false } 100 | guard hasAlpha == pixelModel.hasAlpha else { return false } 101 | guard hasAlphaClip == pixelModel.hasAlphaClip else { return false } 102 | return true 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Source/VTXs/Content/Generator/UVParticles/UVParticlesVTX.metal: -------------------------------------------------------------------------------- 1 | // 2 | // ParticleUV3DVTX.metal 3 | // VertexKitShaders 4 | // 5 | // Created by Anton Heestand on 2019-05-02. 6 | // Copyright © 2019 Hexagons. All rights reserved. 7 | // 8 | 9 | #include 10 | using namespace metal; 11 | 12 | struct VertexIn { 13 | packed_float3 position; 14 | packed_float2 texCoord; 15 | }; 16 | 17 | struct VertexOut { 18 | float4 position [[position]]; 19 | float pointSize [[point_size]]; 20 | float4 color; 21 | }; 22 | 23 | struct Uniforms { 24 | float size; 25 | float resx; 26 | float resy; 27 | float mapSize; 28 | float mapAlpha; 29 | float mapAlphaClip; 30 | float aspect; 31 | }; 32 | 33 | vertex VertexOut uvParticlesVTX(unsigned int vid [[ vertex_id ]], 34 | const device Uniforms& in [[ buffer(1) ]], 35 | texture2d inTex [[ texture(0) ]], 36 | sampler s [[ sampler(0) ]]) { 37 | 38 | int ux = vid % int(in.resx); 39 | int vy = int(float(vid) / in.resx); 40 | float u = float(ux) / in.resx; 41 | float v = float(vy) / in.resy; 42 | float2 uv = float2(u, v); 43 | float4 c = inTex.sample(s, uv); 44 | float x = (c.r / in.aspect) * 2; 45 | float y = c.g * 2; 46 | float z = 0.0; 47 | 48 | VertexOut vtxOut; 49 | vtxOut.position = float4(x, y, z, 1); 50 | vtxOut.pointSize = in.mapSize > 0 ? in.size * c.b : in.size; 51 | vtxOut.color = in.mapAlpha > 0 ? float4(1, 1, 1, in.mapAlphaClip ? c.a == 1.0 : c.a) : float4(1, 1, 1, 1); 52 | 53 | return vtxOut; 54 | } 55 | -------------------------------------------------------------------------------- /Source/VertexKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VertexKit.swift 3 | // VertexKit 4 | // 5 | // Created by Hexagons on 2018-08-02. 6 | // Copyright © 2018 Hexagons. All rights reserved. 7 | // 8 | 9 | import CoreGraphics 10 | import Metal 11 | import RenderKit 12 | import PixelKit 13 | import simd 14 | import Resolution 15 | 16 | public class VertexKit { 17 | 18 | public static let main = VertexKit() 19 | 20 | let pixelKit = PixelKit.main 21 | 22 | // MARK: - Life Cycle 23 | 24 | init() { 25 | print("VertexKit", "ready to render.") 26 | } 27 | 28 | // MARK: - Log 29 | 30 | public static func log(pix: PIX? = nil, _ level: Logger.LogLevel, _ category: Logger.LogCategory?, _ message: String, loop: Bool = false, clean: Bool = false, e error: Error? = nil, _ file: String = #file, _ function: String = #function, _ line: Int = #line) { 31 | PixelKit.main.logger.log(prefix: "VertexKit", level, category, message, loop: loop, clean: clean, e: error, file, function, line) 32 | } 33 | 34 | // MARK: - Setup 35 | 36 | // MARK: Shaders 37 | 38 | enum MetalLibraryError: Error { 39 | case runtimeERROR(String) 40 | } 41 | 42 | static let metalLibrary: MTLLibrary = { 43 | do { 44 | return try PixelKit.main.render.metalDevice.makeDefaultLibrary(bundle: Bundle.module) 45 | } catch { 46 | fatalError("Loading Metal Library Failed: \(error.localizedDescription)") 47 | } 48 | }() 49 | 50 | } 51 | -------------------------------------------------------------------------------- /VertexKit.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /VertexKit.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /VertexKit.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CoreGraphicsExtensions", 6 | "repositoryURL": "https://github.com/heestand-xyz/CoreGraphicsExtensions", 7 | "state": { 8 | "branch": null, 9 | "revision": "a40dc230306daacb410f9a2f36f63a811ea2e88e", 10 | "version": "1.2.1" 11 | } 12 | }, 13 | { 14 | "package": "PixelColor", 15 | "repositoryURL": "https://github.com/heestand-xyz/PixelColor", 16 | "state": { 17 | "branch": null, 18 | "revision": "23851e7ea5f2dc844dd6d9a4d730864199583921", 19 | "version": "1.3.2" 20 | } 21 | }, 22 | { 23 | "package": "Resolution", 24 | "repositoryURL": "https://github.com/heestand-xyz/Resolution", 25 | "state": { 26 | "branch": null, 27 | "revision": "0fdee03d7b312f075897cdac437f366cc6631d6a", 28 | "version": "1.0.4" 29 | } 30 | }, 31 | { 32 | "package": "TextureMap", 33 | "repositoryURL": "https://github.com/heestand-xyz/TextureMap", 34 | "state": { 35 | "branch": null, 36 | "revision": "34781125ed9bb07744ca7d63fc4ce75334d09aa0", 37 | "version": "0.3.0" 38 | } 39 | } 40 | ] 41 | }, 42 | "version": 1 43 | } 44 | --------------------------------------------------------------------------------