├── .gitignore ├── .swiftpm └── xcode │ └── xcshareddata │ └── xcschemes │ └── MeshGradient.xcscheme ├── CHANGELOG.md ├── Files └── mesh.gif ├── LICENSE ├── Package.swift ├── README.md └── Sources ├── MeshGradient ├── ControlPoint.swift ├── DummyResources │ └── please_swiftpm ├── Grid.swift ├── MTLBufferPool.swift ├── MTLComputeHermitPatchMatrix.metal ├── MTLComputeHermitPatchMatrixFunction.swift ├── MTLComputeMeshTrianglePrimitives.metal ├── MTLComputeMeshTrianglePrimitivesFunction.swift ├── MTLComputeNoise.metal ├── MTLComputeNoiseFunction.swift ├── MTLComputeShuffleCoefficients.metal ├── MTLComputeShuffleCoefficientsFunction.swift ├── MTLDrawMeshTriangles.metal ├── MTLDrawMeshTrianglesFunction.swift ├── MeshAnimator.swift ├── MeshDataProvider.swift ├── MeshGenerator.swift ├── MeshGradientError.swift ├── MeshRandomizer.swift ├── MetalMeshRenderer.swift └── SwiftUI │ └── LegacyMeshGradient.swift └── MeshGradientCHeaders ├── PleasingSwiftPM.c └── include └── MetalMeshShaderTypes.h /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/MeshGradient.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 67 | 68 | 74 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.0.2 4 | 5 | * Reflect `framesPerSecond` change in `MeshGradient` SwiftUI view. 6 | [Pull request](https://github.com/Nekitosss/MeshGradient/pull/1) 7 | 8 | ## 1.0.1 9 | 10 | * Enable non-uniform mesh gradient dimensions. E.g. 4x8 or 5x12 meshes now possible 11 | * Fix `MeshGradient` view name for macOS 12 | -------------------------------------------------------------------------------- /Files/mesh.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patskovn/MeshGradient/f16b544336b7bdaf7e3eba4ac6cba50e1160220e/Files/mesh.gif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Nikita Patskov and others 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "MeshGradient", 7 | platforms: [ 8 | .macOS(.v10_15), 9 | .iOS(.v13), 10 | .macCatalyst(.v13), 11 | .tvOS(.v13), 12 | ], 13 | products: [ 14 | .library( 15 | name: "MeshGradient", 16 | targets: ["MeshGradient"]), 17 | .library(name: "MeshGradientCHeaders", 18 | targets: ["MeshGradientCHeaders"]), 19 | ], 20 | targets: [ 21 | .target( 22 | name: "MeshGradient", 23 | dependencies: ["MeshGradientCHeaders"], 24 | resources: [.copy("DummyResources/")] 25 | ), 26 | .target(name: "MeshGradientCHeaders"), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MeshGradient 2 | 3 | Metal-based implementation of beautiful mesh gradient. Image worth thousands of words, so just let me show you. 4 | 5 | ![Mesh gradient gif](Files/mesh.gif) 6 | 7 | ## How to generate mesh 8 | 9 | Small notes before I show you the code. This mesh is generated from the constants you provide. To create mesh you have to provide grid with `ControlPoint` matrix that *describe* your gradient (just like with other gradients, huh?): 10 | 11 | * You need to provide `colors` for gradient - that is obvious, right? 12 | * You need to provide `location`s of control points. Locations are in metal coordinate space - that one is a bit scarier. Relax, it is just values in -1...1 13 | * You need to provide how *turbulent* your mesh is. This is `uTangent` and `vTangent` exists for. 14 | 15 | Again, **don't be scared**. You have all the randomizers out of the box and they are pretty easy to use. 16 | 17 | Now, to the code: 18 | 19 | ```swift 20 | typealias MeshColor = SIMD3 21 | 22 | // You can provide custom `locationRandomizer` and `turbulencyRandomizer` for advanced usage 23 | var meshRandomizer = MeshRandomizer(colorRandomizer: MeshRandomizer.arrayBasedColorRandomizer(availableColors: meshColors)) 24 | 25 | private var meshColors: [simd_float3] { 26 | return [ 27 | MeshRandomizer.randomColor(), 28 | MeshRandomizer.randomColor(), 29 | MeshRandomizer.randomColor(), 30 | ] 31 | } 32 | 33 | // This methods prepares the grid model that will be sent to metal for rendering 34 | func generatePlainGrid(size: Int = 6) -> Grid { 35 | let preparationGrid = Grid(repeating: .zero, width: size, height: size) 36 | 37 | // At first we create grid without randomisation. This is smooth mesh gradient without 38 | // any turbulency and overlaps 39 | var result = MeshGenerator.generate(colorDistribution: preparationGrid) 40 | 41 | // And here we shuffle the grid using randomizer that we created 42 | for x in stride(from: 0, to: result.width, by: 1) { 43 | for y in stride(from: 0, to: result.height, by: 1) { 44 | meshRandomizer.locationRandomizer(&result[x, y].location, x, y, result.width, result.height) 45 | meshRandomizer.turbulencyRandomizer(&result[x, y].uTangent, x, y, result.width, result.height) 46 | meshRandomizer.turbulencyRandomizer(&result[x, y].vTangent, x, y, result.width, result.height) 47 | 48 | meshRandomizer.colorRandomizer(&result[x, y].color, result[x, y].color, x, y, result.width, result.height) 49 | } 50 | } 51 | 52 | return result 53 | } 54 | 55 | ``` 56 | 57 | ### SwiftUI 58 | 59 | ```swift 60 | // If you want just show static grid without any animations 61 | struct MyStaticGrid: View { 62 | var body: some View { 63 | MeshGradient(grid: generatePlainGrid()) 64 | } 65 | } 66 | 67 | struct MyAnimatedGrid: View { 68 | 69 | // MeshRandomizer is a plain struct with just the functions. So you can dynamically change it! 70 | @State var meshRandomizer = MeshRandomizer(colorRandomizer: MeshRandomizer.arrayBasedColorRandomizer(availableColors: meshColors)) 71 | 72 | var body: some View { 73 | MeshGradient(initialGrid: generatePlainGrid(), 74 | animatorConfiguration: .init(animationSpeedRange: 2 ... 4, meshRandomizer: meshRandomizer))) 75 | } 76 | } 77 | ``` 78 | 79 | ### UIKit 80 | 81 | Use `MTKView` and `MetalMeshRenderer`. I hope you have minimal knowledge in metal rendering. Good luck ;) 82 | Check source code of SwiftUI `MeshGradient` implementation that just wraps `MTKView`. 83 | -------------------------------------------------------------------------------- /Sources/MeshGradient/ControlPoint.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | import simd 4 | import SwiftUI 5 | 6 | /// Describes one mesh gradient entry in matrix. 7 | public struct ControlPoint { 8 | 9 | /// Color of the control point 10 | public var color: simd_float3 11 | 12 | /// Location of control point. Initially just interpolated from grid size. Change it to have overlapping in mesh 13 | public var location: simd_float2 14 | 15 | /// Defines turbulency in mesh gradient. Change to show more "sinusoidal" look 16 | public var uTangent: simd_float2 17 | 18 | /// Defines turbulency in mesh gradient. Change to show more "sinusoidal" look 19 | public var vTangent: simd_float2 20 | 21 | public init(color: simd_float3 = simd_float3(0, 0, 0), 22 | location: simd_float2 = simd_float2(0, 0), 23 | uTangent: simd_float2 = simd_float2(0, 0), 24 | vTangent: simd_float2 = simd_float2(0, 0)) { 25 | self.color = color 26 | self.location = location 27 | self.uTangent = uTangent 28 | self.vTangent = vTangent 29 | } 30 | 31 | } 32 | 33 | extension ControlPoint: VectorArithmetic, AdditiveArithmetic { 34 | public static func - (lhs: ControlPoint, rhs: ControlPoint) -> ControlPoint { 35 | .init(color: lhs.color - rhs.color, 36 | location: lhs.location - rhs.location, 37 | uTangent: lhs.uTangent - rhs.uTangent, 38 | vTangent: lhs.vTangent - rhs.vTangent) 39 | } 40 | 41 | public static func + (lhs: ControlPoint, rhs: ControlPoint) -> ControlPoint { 42 | .init(color: lhs.color + rhs.color, 43 | location: lhs.location + rhs.location, 44 | uTangent: lhs.uTangent + rhs.uTangent, 45 | vTangent: lhs.vTangent + rhs.vTangent) 46 | } 47 | 48 | public mutating func scale(by rhs: Double) { 49 | color.scale(by: rhs) 50 | location.scale(by: rhs) 51 | uTangent.scale(by: rhs) 52 | vTangent.scale(by: rhs) 53 | } 54 | 55 | public var magnitudeSquared: Double { 56 | color.magnitudeSquared + location.magnitudeSquared + uTangent.magnitudeSquared + vTangent.magnitudeSquared 57 | } 58 | 59 | public static var zero: ControlPoint { 60 | .init(color: .zero, location: .zero, uTangent: .zero, vTangent: .zero) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/MeshGradient/DummyResources/please_swiftpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patskovn/MeshGradient/f16b544336b7bdaf7e3eba4ac6cba50e1160220e/Sources/MeshGradient/DummyResources/please_swiftpm -------------------------------------------------------------------------------- /Sources/MeshGradient/Grid.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftUI 3 | import simd 4 | import Accelerate 5 | 6 | /// A two-dimensional grid of `Element`. 7 | public struct Grid { 8 | public var elements: ContiguousArray 9 | 10 | public var width: Int 11 | 12 | public var height: Int 13 | 14 | public init(repeating element: Element, width: Int, height: Int) { 15 | self.width = width 16 | self.height = height 17 | self.elements = ContiguousArray(repeating: element, count: width * height) 18 | } 19 | 20 | public init(width: Int, array: () -> [Element]) { 21 | let arr = array() 22 | assert(arr.count.isMultiple(of: width)) 23 | self.width = width 24 | self.height = arr.count / width 25 | self.elements = ContiguousArray(arr) 26 | } 27 | 28 | public func index(x: Int, y: Int) -> Int { 29 | x + y * width 30 | } 31 | 32 | public subscript(i: Int) -> Element { 33 | get { 34 | elements[i] 35 | } 36 | set { 37 | elements[i] = newValue 38 | } 39 | } 40 | 41 | public subscript(x: Int, y: Int) -> Element { 42 | get { 43 | self[index(x: x, y: y)] 44 | } 45 | set { 46 | self[index(x: x, y: y)] = newValue 47 | } 48 | } 49 | } 50 | 51 | extension Grid: Equatable where Element: Equatable {} 52 | extension Grid: Hashable where Element: Hashable {} 53 | 54 | extension Collection { 55 | 56 | /// Returns the element at the specified index if it is within bounds, otherwise nil. 57 | subscript (safe index: Index) -> Element? { 58 | return indices.contains(index) ? self[index] : nil 59 | } 60 | } 61 | 62 | extension Grid: VectorArithmetic, AdditiveArithmetic where Element: VectorArithmetic & AdditiveArithmetic { 63 | public static func - (lhs: Grid, rhs: Grid) -> Grid { 64 | var grid = Grid(repeating: .zero, width: lhs.width, height: lhs.height) 65 | for i in lhs.elements.indices { 66 | grid.elements[i] = lhs.elements[i] - (rhs.elements[safe: i] ?? Element.zero) 67 | } 68 | return grid 69 | } 70 | 71 | public static func + (lhs: Grid, rhs: Grid) -> Grid { 72 | var grid = Grid(repeating: .zero, width: lhs.width, height: lhs.height) 73 | 74 | for i in lhs.elements.indices { 75 | grid.elements[i] = lhs.elements[i] + (rhs.elements[safe: i] ?? Element.zero) 76 | } 77 | return grid 78 | } 79 | 80 | public mutating func scale(by rhs: Double) { 81 | for i in elements.indices { 82 | elements[i].scale(by: rhs) 83 | } 84 | } 85 | 86 | public var magnitudeSquared: Double { 87 | elements.reduce(0, { $0 + Double($1.magnitudeSquared) }) 88 | } 89 | 90 | public static var zero: Grid { 91 | .init(repeating: Element.zero, width: 0, height: 0) 92 | } 93 | } 94 | 95 | extension SIMD2: VectorArithmetic, AdditiveArithmetic where Scalar == Float { 96 | public mutating func scale(by rhs: Double) { 97 | self = self * SIMD2(x: Float(rhs), y: Float(rhs)) 98 | } 99 | 100 | public var magnitudeSquared: Double { 101 | Double(x*x + y*y) 102 | } 103 | } 104 | 105 | extension SIMD3: VectorArithmetic, AdditiveArithmetic where Scalar == Float { 106 | public mutating func scale(by rhs: Double) { 107 | self = self * SIMD3(x: Float(rhs), y: Float(rhs), z: Float(rhs)) 108 | } 109 | 110 | public var magnitudeSquared: Double { 111 | Double(x*x+y*y+z*z) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Sources/MeshGradient/MTLBufferPool.swift: -------------------------------------------------------------------------------- 1 | 2 | import Metal 3 | 4 | final class MTLBufferPool { 5 | 6 | private var pool: [Int: [UInt: ContiguousArray]] = [:] 7 | private let device: MTLDevice 8 | private let monitor = NSObject() 9 | 10 | init(device: MTLDevice) { 11 | self.device = device 12 | } 13 | 14 | subscript(length: Int, resourceOptions: MTLResourceOptions) -> MTLBuffer? { 15 | get { 16 | objc_sync_enter(monitor) 17 | defer { objc_sync_exit(monitor) } 18 | 19 | var availableBuffer = pool[length, default: [:]][resourceOptions.rawValue, default: []] 20 | if availableBuffer.isEmpty { 21 | return device.makeBuffer(length: length, options: resourceOptions) 22 | 23 | } else { 24 | defer { pool[length, default: [:]][resourceOptions.rawValue] = availableBuffer } 25 | return availableBuffer.removeLast() 26 | } 27 | } 28 | set { 29 | objc_sync_enter(monitor) 30 | defer { objc_sync_exit(monitor) } 31 | 32 | guard let newValue = newValue else { return } 33 | pool[length, default: [:]][resourceOptions.rawValue, default: []].append(newValue) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/MeshGradient/MTLComputeHermitPatchMatrix.metal: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #import "../MeshGradientCHeaders/include/MetalMeshShaderTypes.h" 4 | 5 | using namespace metal; 6 | 7 | static vector_float2 surfacePoint(float u, 8 | float v, 9 | float4x4 H, 10 | float4x4 H_T, 11 | float4x4 X, 12 | float4x4 Y) { 13 | float4 U = float4 { 14 | u * u * u, u * u, u, 1 15 | }; 16 | float4 V = float4 { 17 | v * v * v, v * v, v, 1 18 | }; 19 | 20 | return float2 { 21 | dot(V, U * H * X * H_T), 22 | dot(V, U * H * Y * H_T) 23 | }; 24 | } 25 | 26 | static vector_float4 colorPoint(float u, 27 | float v, 28 | float4x4 H, 29 | float4x4 H_T, 30 | float4x4 R, 31 | float4x4 G, 32 | float4x4 B) { 33 | 34 | float4 U = float4 { 35 | u * u * u, u * u, u, 1 36 | }; 37 | float4 V = float4 { 38 | v * v * v, v * v, v, 1 39 | }; 40 | 41 | return float4 { 42 | dot(V, U * H * R * H_T), 43 | dot(V, U * H * G * H_T), 44 | dot(V, U * H * B * H_T), 45 | 1 46 | }; 47 | } 48 | 49 | kernel void computeHermitPatchMatrix(device const MeshIntermediateVertex* inVertices [[buffer(ComputeMeshFinalInputIndexVertices)]], 50 | constant uint *width [[buffer(ComputeMeshFinalInputIndexWidth)]], 51 | constant uint *subdivisions [[buffer(ComputeMeshFinalInputIndexSubdivisions)]], 52 | device MeshVertex* result [[buffer(ComputeMeshFinalInputIndexResult)]], 53 | constant vector_uint3 *computeSize [[buffer(ComputeMeshFinalInputIndexCalculationSize)]], 54 | vector_uint3 index [[thread_position_in_grid]]) { 55 | 56 | vector_uint3 computeSizeValue = *computeSize; 57 | 58 | if (index.x >= computeSizeValue.x || index.y >= computeSizeValue.y || index.z >= computeSizeValue.z) { 59 | return; 60 | } 61 | 62 | uint w = *width; 63 | uint s = *subdivisions; 64 | 65 | uint i = index.z; 66 | 67 | uint u_i = index.y; 68 | uint v_i = index.x; 69 | 70 | MeshIntermediateVertex vert = inVertices[i]; 71 | uint resX = vert.x * s + u_i; 72 | uint resY = vert.y * s + v_i; 73 | uint resI = resX + resY * w; 74 | 75 | float u = float(u_i) / float(s-1); 76 | float v = float(v_i) / float(s-1); 77 | 78 | float4x4 H = float4x4 { 79 | { 2, -3, 0, 1 }, 80 | { -2, 3, 0, 0 }, 81 | { 1, -2, 1, 0 }, 82 | { 1, -1, 0, 0 }, 83 | }; 84 | float4x4 H_T = transpose(H); 85 | 86 | MeshVertex resultVertex = { 87 | surfacePoint(u, v, H, H_T, vert.X, vert.Y), 88 | colorPoint(u, v, H, H_T, vert.R, vert.G, vert.B), 89 | }; 90 | result[resI] = resultVertex; 91 | } 92 | -------------------------------------------------------------------------------- /Sources/MeshGradient/MTLComputeHermitPatchMatrixFunction.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | import Metal 4 | import simd 5 | @_implementationOnly import MeshGradientCHeaders 6 | 7 | final class MTLComputeHermitPatchMatrixFunction { 8 | 9 | private let pipelineState: MTLComputePipelineState 10 | 11 | init(device: MTLDevice, library: MTLLibrary) throws { 12 | guard let computeHermitPatchMatrixFunction = library.makeFunction(name: "computeHermitPatchMatrix") 13 | else { throw MeshGradientError.metalFunctionNotFound(name: "computeHermitPatchMatrix") } 14 | 15 | self.pipelineState = try device.makeComputePipelineState(function: computeHermitPatchMatrixFunction) 16 | } 17 | 18 | func call(subdivisions: Int, 19 | resultBuffer: MTLBuffer, 20 | intermediateResultBuffer: MTLBuffer, 21 | gridWidth: Int, 22 | intermediateResultBufferSize: Int, 23 | commandBuffer: MTLCommandBuffer) { 24 | 25 | guard let computeEncoder = commandBuffer.makeComputeCommandEncoder() 26 | else { 27 | assertionFailure() 28 | return 29 | } 30 | computeEncoder.setComputePipelineState(pipelineState) 31 | 32 | computeEncoder.setBuffer(intermediateResultBuffer, 33 | offset: 0, 34 | index: Int(ComputeMeshFinalInputIndexVertices.rawValue)) 35 | 36 | var width = gridWidth 37 | let depth = intermediateResultBufferSize 38 | 39 | computeEncoder.setBytes(&width, 40 | length: MemoryLayout.size(ofValue: width), 41 | index: Int(ComputeMeshFinalInputIndexWidth.rawValue)) 42 | 43 | var inSubdivisions = UInt32(subdivisions) 44 | computeEncoder.setBytes(&inSubdivisions, 45 | length: MemoryLayout.size(ofValue: inSubdivisions), 46 | index: Int(ComputeMeshFinalInputIndexSubdivisions.rawValue)) 47 | 48 | computeEncoder.setBuffer(resultBuffer, 49 | offset: 0, 50 | index: Int(ComputeMeshFinalInputIndexResult.rawValue)) 51 | 52 | let computeSize = MTLSize(width: subdivisions, 53 | height: subdivisions, 54 | depth: depth) 55 | 56 | var calculationSizeParameter = SIMD3(Int32(computeSize.width), 57 | Int32(computeSize.height), 58 | Int32(computeSize.depth)) 59 | 60 | computeEncoder.setBytes(&calculationSizeParameter, 61 | length: MemoryLayout.size(ofValue: calculationSizeParameter), 62 | index: Int(ComputeMeshFinalInputIndexCalculationSize.rawValue)) 63 | 64 | let threadGroupSize = MTLSize(width: min(intermediateResultBufferSize, pipelineState.maxTotalThreadsPerThreadgroup), 65 | height: 1, 66 | depth: 1) 67 | 68 | computeEncoder.dispatchThreadgroups(computeSize, 69 | threadsPerThreadgroup: threadGroupSize) 70 | 71 | computeEncoder.endEncoding() 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /Sources/MeshGradient/MTLComputeMeshTrianglePrimitives.metal: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | 4 | #import "../MeshGradientCHeaders/include/MetalMeshShaderTypes.h" 5 | 6 | using namespace metal; 7 | 8 | MeshVertex meshVertex(device const MeshVertex* inVertices, uint x, uint y, uint width, uint tag) { 9 | return inVertices[x + y * width]; 10 | }; 11 | 12 | kernel void computeMeshTrianglePrimitives(device const MeshVertex* inVertices [[buffer(ComputeMeshTrianglesIndexVertices)]], 13 | constant uint *width [[buffer(ComputeMeshTrianglesIndexWidth)]], 14 | constant uint *height [[buffer(ComputeMeshTrianglesIndexHeight)]], 15 | device MeshVertex* result [[buffer(ComputeMeshTrianglesIndexResult)]], 16 | constant vector_uint2 *computeSize [[buffer(ComputeMeshTrianglesIndexCalculationSize)]], 17 | vector_uint2 index [[thread_position_in_grid]]) { 18 | vector_uint2 computeSizeValue = *computeSize; 19 | 20 | if (index.x >= computeSizeValue.x || index.y >= computeSizeValue.y) { 21 | return; 22 | } 23 | 24 | uint w = *width; 25 | 26 | uint y = index.y; 27 | uint x = index.x; 28 | 29 | uint i = (x + y * (w - 1)) * 6; 30 | 31 | MeshVertex p00 = meshVertex(inVertices, x , y , w, 0); 32 | MeshVertex p10 = meshVertex(inVertices, x + 1, y , w, 10); 33 | MeshVertex p01 = meshVertex(inVertices, x , y + 1, w, 1); 34 | MeshVertex p11 = meshVertex(inVertices, x + 1, y + 1, w, 11); 35 | 36 | result[i+0] = p00; 37 | result[i+1] = p10; 38 | result[i+2] = p11; 39 | 40 | result[i+3] = p11; 41 | result[i+4] = p01; 42 | result[i+5] = p00; 43 | } 44 | 45 | 46 | -------------------------------------------------------------------------------- /Sources/MeshGradient/MTLComputeMeshTrianglePrimitivesFunction.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | import Metal 4 | import simd 5 | @_implementationOnly import MeshGradientCHeaders 6 | 7 | final class MTLComputeMeshTrianglePrimitivesFunction { 8 | 9 | private let pipelineState: MTLComputePipelineState 10 | 11 | init(device: MTLDevice, library: MTLLibrary) throws { 12 | guard let computeMeshTrianglePrimitivesFunction = library.makeFunction(name: "computeMeshTrianglePrimitives") 13 | else { throw MeshGradientError.metalFunctionNotFound(name: "computeMeshTrianglePrimitives") } 14 | 15 | self.pipelineState = try device.makeComputePipelineState(function: computeMeshTrianglePrimitivesFunction) 16 | } 17 | 18 | func call(gridSize: (width: Int, height: Int), 19 | resultTrianglesBuffer: MTLBuffer, 20 | finalVertices: MTLBuffer, 21 | finalVerticesSize: Int, 22 | commandBuffer: MTLCommandBuffer) { 23 | 24 | guard let computeEncoder = commandBuffer.makeComputeCommandEncoder() 25 | else { 26 | assertionFailure() 27 | return 28 | } 29 | computeEncoder.setComputePipelineState(pipelineState) 30 | 31 | computeEncoder.setBuffer(finalVertices, 32 | offset: 0, 33 | index: Int(ComputeMeshTrianglesIndexVertices.rawValue)) 34 | 35 | var (width, height) = gridSize 36 | computeEncoder.setBytes(&width, 37 | length: MemoryLayout.size(ofValue: width), 38 | index: Int(ComputeMeshTrianglesIndexWidth.rawValue)) 39 | 40 | computeEncoder.setBytes(&height, 41 | length: MemoryLayout.size(ofValue: height), 42 | index: Int(ComputeMeshTrianglesIndexHeight.rawValue)) 43 | 44 | computeEncoder.setBuffer(resultTrianglesBuffer, 45 | offset: 0, 46 | index: Int(ComputeMeshTrianglesIndexResult.rawValue)) 47 | 48 | let computeSize = MTLSize(width: width - 1, 49 | height: height - 1, 50 | depth: 1) 51 | 52 | var calculationSizeParameter = SIMD2(Int32(computeSize.width), Int32(computeSize.height)) 53 | computeEncoder.setBytes(&calculationSizeParameter, 54 | length: MemoryLayout.size(ofValue: calculationSizeParameter), 55 | index: Int(ComputeMeshTrianglesIndexCalculationSize.rawValue)) 56 | 57 | let threadGroupSize = MTLSize(width: min(finalVerticesSize, pipelineState.maxTotalThreadsPerThreadgroup), 58 | height: 1, 59 | depth: 1) 60 | 61 | computeEncoder.dispatchThreadgroups(computeSize, 62 | threadsPerThreadgroup: threadGroupSize) 63 | 64 | computeEncoder.endEncoding() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/MeshGradient/MTLComputeNoise.metal: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | 4 | #import "../MeshGradientCHeaders/include/MetalMeshShaderTypes.h" 5 | 6 | using namespace metal; 7 | 8 | 9 | constant uint NOISE_DIM = 512; 10 | 11 | // Noise Declarations 12 | float rand(int x, int y); 13 | float smoothNoise(float x, float y); 14 | 15 | half4 noiseColor(int x, int y, bool isSmooth, float alpha); 16 | 17 | // The Kernel 18 | kernel void computeNoize(texture2d outTexture [[texture(ComputeNoiseInputIndexOutputTexture)]], 19 | constant NoiseUniforms &uniforms [[buffer(ComputeNoiseInputIndexUniforms)]], 20 | uint2 gid [[thread_position_in_grid]]) 21 | { 22 | half4 outColor = noiseColor(gid.x, 23 | gid.y, 24 | uniforms.isSmooth == 1, 25 | uniforms.noiseAlpha); 26 | 27 | outTexture.write(outColor, gid); 28 | } 29 | 30 | // Noise 31 | half4 noiseColor(int x, int y, bool isSmooth, float alpha) 32 | { 33 | float colorVal; 34 | 35 | float xVal = x; 36 | float yVal = y; 37 | 38 | float xZoomed = xVal / 0.5; 39 | float yZoomed = yVal / 0.5; 40 | 41 | colorVal = 256.0 * rand(xZoomed, yZoomed); 42 | 43 | if (isSmooth) { 44 | // colorVal = 256.0 * smoothNoise(xZoomed, yZoomed); 45 | } 46 | 47 | return half4(half(colorVal) / 255.0, half(colorVal) / 255.0, half(colorVal) / 255.0, alpha); 48 | } 49 | 50 | // Noise Functions 51 | float smoothNoise(float x, float y) 52 | { 53 | // Get the truncated x, y, and z values 54 | int intX = x; 55 | int intY = y; 56 | 57 | // Get the fractional reaminder of x, y, and z 58 | float fractX = x - intX; 59 | float fractY = y - intY; 60 | 61 | // Get first whole number before 62 | int x1 = (intX + NOISE_DIM) % NOISE_DIM; 63 | int y1 = (intY + NOISE_DIM) % NOISE_DIM; 64 | 65 | // Get the number after 66 | int x2 = (x1 + NOISE_DIM - 1) % NOISE_DIM; 67 | int y2 = (y1 + NOISE_DIM - 1) % NOISE_DIM; 68 | 69 | // interpolate the noise 70 | float value = 0.0; 71 | value += fractX * fractY * rand(x1,y1); 72 | value += fractX * (1 - fractY) * rand(x1,y2); 73 | value += (1 - fractX) * fractY * rand(x2,y1); 74 | value += (1 - fractX) * (1 - fractY) * rand(x2,y2); 75 | 76 | return value; 77 | } 78 | 79 | // Generate a random float in the range [0.0f, 1.0f] using x, y, and z (based on the xor128 algorithm) 80 | float rand(int x, int y) 81 | { 82 | int seed = x + y * 57 * 241; 83 | seed = (seed<< 13) ^ seed; 84 | return (( 1.0 - ( (seed * (seed * seed * 15731 + 789221) + 1376312589) & 2147483647) / 1073741824.0f) + 1.0f) / 2.0f; 85 | } 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /Sources/MeshGradient/MTLComputeNoiseFunction.swift: -------------------------------------------------------------------------------- 1 | 2 | 3 | import Foundation 4 | import Metal 5 | import simd 6 | @_implementationOnly import MeshGradientCHeaders 7 | 8 | final class MTLComputeNoiseFunction { 9 | 10 | private var _noiseTexture: MTLTexture? 11 | 12 | private let device: MTLDevice 13 | private let pipelineState: MTLComputePipelineState 14 | 15 | init(device: MTLDevice, library: MTLLibrary) throws { 16 | self.device = device 17 | 18 | guard let computeNoiseFunction = library.makeFunction(name: "computeNoize") 19 | else { throw MeshGradientError.metalFunctionNotFound(name: "computeNoize") } 20 | 21 | self.pipelineState = try device.makeComputePipelineState(function: computeNoiseFunction) 22 | } 23 | 24 | func call(viewportSize: simd_float2, pixelFormat: MTLPixelFormat, commandQueue: MTLCommandQueue, uniforms: NoiseUniforms) -> MTLTexture? { 25 | guard uniforms.noiseAlpha > 0 else { return nil } 26 | 27 | guard let commandBuffer = commandQueue.makeCommandBuffer() else { return nil } 28 | let width = Int(viewportSize.x) 29 | let height = Int(viewportSize.y) 30 | 31 | if width == 0 || height == 0 { 32 | return nil 33 | } 34 | 35 | if let noiseTexture = _noiseTexture, noiseTexture.width == width, noiseTexture.height == height { 36 | return noiseTexture 37 | } 38 | let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: pixelFormat, 39 | width: width, 40 | height: height, 41 | mipmapped: false) 42 | textureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget, .pixelFormatView] 43 | guard let noiseTexture = device.makeTexture(descriptor: textureDescriptor), 44 | let encoder = commandBuffer.makeComputeCommandEncoder() 45 | else { return nil } 46 | 47 | let threadgroupCounts = MTLSize(width: 8, height: 8, depth: 1) 48 | let threadgroups = MTLSize(width: noiseTexture.width / threadgroupCounts.width, 49 | height: noiseTexture.height / threadgroupCounts.height, 50 | depth: 1) 51 | 52 | if threadgroups.height == 0 || threadgroups.width == 0 { 53 | encoder.endEncoding() 54 | return nil 55 | } 56 | 57 | encoder.setComputePipelineState(pipelineState) 58 | 59 | encoder.setTexture(noiseTexture, index: Int(ComputeNoiseInputIndexOutputTexture.rawValue)) 60 | 61 | var uniforms = uniforms 62 | encoder.setBytes(&uniforms, 63 | length: MemoryLayout.size(ofValue: uniforms), 64 | index: Int(ComputeNoiseInputIndexUniforms.rawValue)) 65 | 66 | encoder.dispatchThreadgroups(threadgroups, 67 | threadsPerThreadgroup: threadgroupCounts) 68 | 69 | encoder.endEncoding() 70 | 71 | commandBuffer.commit() 72 | commandBuffer.waitUntilCompleted() 73 | 74 | self._noiseTexture = noiseTexture 75 | return noiseTexture 76 | } 77 | 78 | } 79 | extension MTLTexture { 80 | func getPixels(mipmapLevel: Int = 0) -> UnsafeMutablePointer { 81 | let fromRegion = MTLRegionMake2D(0, 0, self.width, self.height) 82 | let bytesPerRow = 4 * self.width 83 | let data = UnsafeMutablePointer.allocate(capacity: bytesPerRow * self.height) 84 | 85 | self.getBytes(data, bytesPerRow: bytesPerRow, from: fromRegion, mipmapLevel: mipmapLevel) 86 | return data 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/MeshGradient/MTLComputeShuffleCoefficients.metal: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | 4 | #import "../MeshGradientCHeaders/include/MetalMeshShaderTypes.h" 5 | 6 | using namespace metal; 7 | 8 | static float4x4 meshXCoefficients(MeshControlPoint p00, 9 | MeshControlPoint p01, 10 | MeshControlPoint p10, 11 | MeshControlPoint p11) { 12 | 13 | return float4x4 { 14 | { p00.location.x, p10.location.x, p00.uTangent.x, p10.uTangent.x }, 15 | { p01.location.x, p11.location.x, p01.uTangent.x, p11.uTangent.x }, 16 | { p00.vTangent.x, p10.vTangent.x, 0, 0 }, 17 | { p01.vTangent.x, p11.vTangent.x, 0, 0 }, 18 | }; 19 | } 20 | 21 | static float4x4 meshYCoefficients(MeshControlPoint p00, 22 | MeshControlPoint p01, 23 | MeshControlPoint p10, 24 | MeshControlPoint p11) { 25 | 26 | 27 | return float4x4 { 28 | { p00.location.y, p10.location.y, p00.uTangent.y, p10.uTangent.y }, 29 | { p01.location.y, p11.location.y, p01.uTangent.y, p11.uTangent.y }, 30 | { p00.vTangent.y, p10.vTangent.y, 0, 0 }, 31 | { p01.vTangent.y, p11.vTangent.y, 0, 0 }, 32 | }; 33 | } 34 | 35 | static float4x4 colorCoefficients(float p00, 36 | float p01, 37 | float p10, 38 | float p11) { 39 | 40 | return float4x4 { 41 | { p00, p10, 0, 0 }, 42 | { p01, p11, 0, 0 }, 43 | { 0, 0, 0, 0 }, 44 | { 0, 0, 0, 0 }, 45 | }; 46 | } 47 | 48 | MeshControlPoint meshControlPoint(device const MeshControlPoint* inVertices, uint x, uint y, uint width) { 49 | return inVertices[x + y * width]; 50 | } 51 | 52 | 53 | kernel void shuffleCoefficients(device const MeshControlPoint* inVertices [[buffer(ComputeMeshVertexInputIndexVertices)]], 54 | constant vector_uint2 *gridSize [[buffer(ComputeMeshVertexInputIndexGridSize)]], 55 | device MeshIntermediateVertex* result [[buffer(ComputeMeshVertexInputIndexResult)]], 56 | constant vector_uint2 *computeSize [[buffer(ComputeMeshVertexInputCalculationSize)]], 57 | vector_uint2 index [[thread_position_in_grid]]) { 58 | vector_uint2 computeSizeValue = *computeSize; 59 | 60 | if (index.x >= computeSizeValue.x || index.y >= computeSizeValue.y) { 61 | return; 62 | } 63 | 64 | uint width = (*gridSize).x; 65 | 66 | uint x = index.x; 67 | uint y = index.y; 68 | 69 | uint i = index.x + index.y * (width - 1); 70 | 71 | MeshControlPoint p00 = meshControlPoint(inVertices, x, y, width); 72 | MeshControlPoint p01 = meshControlPoint(inVertices, x, y + 1, width); 73 | MeshControlPoint p10 = meshControlPoint(inVertices, x + 1, y, width); 74 | MeshControlPoint p11 = meshControlPoint(inVertices, x + 1, y + 1, width); 75 | 76 | float4x4 X = meshXCoefficients(p00, p01, p10, p11); 77 | float4x4 Y = meshYCoefficients(p00, p01, p10, p11); 78 | 79 | float4x4 R = colorCoefficients(p00.color.x, 80 | p01.color.x, 81 | p10.color.x, 82 | p11.color.x); 83 | 84 | float4x4 G = colorCoefficients(p00.color.y, 85 | p01.color.y, 86 | p10.color.y, 87 | p11.color.y); 88 | 89 | float4x4 B = colorCoefficients(p00.color.z, 90 | p01.color.z, 91 | p10.color.z, 92 | p11.color.z); 93 | 94 | MeshIntermediateVertex intermediateVertex = { 95 | X, 96 | Y, 97 | R, 98 | G, 99 | B, 100 | x, 101 | y, 102 | }; 103 | result[i] = intermediateVertex; 104 | } 105 | -------------------------------------------------------------------------------- /Sources/MeshGradient/MTLComputeShuffleCoefficientsFunction.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | import Metal 4 | import simd 5 | @_implementationOnly import MeshGradientCHeaders 6 | 7 | final class MTLComputeShuffleCoefficientsFunction { 8 | 9 | private let pipelineState: MTLComputePipelineState 10 | private let bufferPool: MTLBufferPool 11 | 12 | init(device: MTLDevice, library: MTLLibrary, bufferPool: MTLBufferPool) throws { 13 | guard let shuffleCoefficientsFunction = library.makeFunction(name: "shuffleCoefficients") 14 | else { throw MeshGradientError.metalFunctionNotFound(name: "shuffleCoefficients") } 15 | 16 | self.pipelineState = try device.makeComputePipelineState(function: shuffleCoefficientsFunction) 17 | self.bufferPool = bufferPool 18 | } 19 | 20 | func call(grid: Grid, intermediateResultBuffer: MTLBuffer, commandBuffer: MTLCommandBuffer) { 21 | let buffer = grid.elements.map { 22 | return MeshControlPoint(location: $0.location, 23 | uTangent: $0.uTangent, 24 | vTangent: $0.vTangent, 25 | color: vector_float4($0.color, 1)) 26 | } 27 | 28 | let bufferLength = MemoryLayout.stride * buffer.count 29 | guard 30 | let computeEncoder = commandBuffer.makeComputeCommandEncoder(), 31 | let inputBuffer = bufferPool[bufferLength, .storageModeShared] 32 | else { 33 | assertionFailure() 34 | return 35 | } 36 | computeEncoder.setComputePipelineState(pipelineState) 37 | 38 | commandBuffer.addCompletedHandler { _ in 39 | self.bufferPool[bufferLength, .storageModeShared] = inputBuffer 40 | } 41 | 42 | let rawPointer = inputBuffer.contents() 43 | let typedPointer = rawPointer.bindMemory(to: MeshControlPoint.self, capacity: bufferLength) 44 | let bufferedPointer = UnsafeBufferPointer(start: typedPointer, count: buffer.count) 45 | let mutatingPointer = UnsafeMutableBufferPointer(mutating: bufferedPointer) 46 | 47 | for i in stride(from: 0, to: buffer.count, by: 1) { 48 | mutatingPointer[i] = buffer[i] 49 | } 50 | 51 | computeEncoder.setBuffer(inputBuffer, 52 | offset: 0, 53 | index: Int(ComputeMeshVertexInputIndexVertices.rawValue)) 54 | 55 | var gridSize = vector_uint2(UInt32(grid.width), UInt32(grid.height)) 56 | computeEncoder.setBytes(&gridSize, 57 | length: MemoryLayout.size(ofValue: gridSize), 58 | index: Int(ComputeMeshVertexInputIndexGridSize.rawValue)) 59 | 60 | computeEncoder.setBuffer(intermediateResultBuffer, 61 | offset: 0, 62 | index: Int(ComputeMeshVertexInputIndexResult.rawValue)) 63 | 64 | let computeSize = MTLSize(width: grid.width - 1, 65 | height: grid.height - 1, 66 | depth: 1) 67 | 68 | var calculationSizeParameter = SIMD2(Int32(computeSize.width), Int32(computeSize.height)) 69 | computeEncoder.setBytes(&calculationSizeParameter, 70 | length: MemoryLayout.size(ofValue: calculationSizeParameter), 71 | index: Int(ComputeMeshVertexInputCalculationSize.rawValue)) 72 | 73 | let threadGroupSize = MTLSize(width: min(grid.elements.count, pipelineState.maxTotalThreadsPerThreadgroup), 74 | height: 1, 75 | depth: 1) 76 | 77 | computeEncoder.dispatchThreadgroups(computeSize, 78 | threadsPerThreadgroup: threadGroupSize) 79 | 80 | computeEncoder.endEncoding() 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /Sources/MeshGradient/MTLDrawMeshTriangles.metal: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | using namespace metal; 4 | 5 | #import "../MeshGradientCHeaders/include/MetalMeshShaderTypes.h" 6 | 7 | // Vertex shader outputs and fragment shader inputs 8 | struct RasterizerData 9 | { 10 | // The [[position]] attribute of this member indicates that this value 11 | // is the clip space position of the vertex when this structure is 12 | // returned from the vertex function. 13 | float4 position [[position]]; 14 | 15 | // Since this member does not have a special attribute, the rasterizer 16 | // interpolates its value with the values of the other triangle vertices 17 | // and then passes the interpolated value to the fragment shader for each 18 | // fragment in the triangle. 19 | float4 color; 20 | float2 textureCoordinate; 21 | }; 22 | 23 | vertex RasterizerData 24 | prepareToDrawShader(uint vertexID [[vertex_id]], 25 | constant MeshVertex *vertices [[buffer(0)]]) 26 | { 27 | RasterizerData out; 28 | 29 | out.position = vector_float4(0.0, 0.0, 0.0, 1.0); 30 | out.position.xy = vertices[vertexID].position.xy; 31 | 32 | // Pass the input color directly to the rasterizer. 33 | out.color = vertices[vertexID].color; 34 | 35 | out.textureCoordinate = float2(out.position.x / 2 + 0.5, -out.position.y / 2 + 0.5); 36 | 37 | return out; 38 | } 39 | 40 | fragment float4 drawRasterizedMesh(RasterizerData in [[stage_in]], 41 | texture2d noiseTexture [[ texture(DrawMeshTextureIndexBaseColor) ]]) 42 | { 43 | 44 | 45 | constexpr sampler linear(coord::normalized, 46 | address::clamp_to_edge, 47 | filter::linear); 48 | 49 | // Sample the texture to obtain a color 50 | const float4 colorSample = float4(noiseTexture.sample(linear, in.textureCoordinate)); 51 | 52 | const float3 outRGB = colorSample.w * colorSample.xyz + (1 - colorSample.w) * in.color.xyz; 53 | 54 | return float4(outRGB, in.color.w); 55 | } 56 | -------------------------------------------------------------------------------- /Sources/MeshGradient/MTLDrawMeshTrianglesFunction.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | import Metal 4 | import MetalKit 5 | import simd 6 | @_implementationOnly import MeshGradientCHeaders 7 | 8 | final class MTLDrawMeshTrianglesFunction { 9 | 10 | private let pipelineState: MTLRenderPipelineState 11 | 12 | init(device: MTLDevice, library: MTLLibrary, mtkView: MTKView) throws { 13 | guard let prepareToDrawFunction = library.makeFunction(name: "prepareToDrawShader") 14 | else { throw MeshGradientError.metalFunctionNotFound(name: "prepareToDrawShader") } 15 | 16 | guard let drawMeshFunction = library.makeFunction(name: "drawRasterizedMesh") 17 | else { throw MeshGradientError.metalFunctionNotFound(name: "drawRasterizedMesh") } 18 | 19 | let pipelineDescriptor = MTLRenderPipelineDescriptor() 20 | pipelineDescriptor.label = "Draw mesh pipeline" 21 | pipelineDescriptor.vertexFunction = prepareToDrawFunction 22 | pipelineDescriptor.fragmentFunction = drawMeshFunction 23 | pipelineDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat 24 | 25 | self.pipelineState = try device.makeRenderPipelineState(descriptor: pipelineDescriptor) 26 | } 27 | 28 | func call(meshVertices: MTLBuffer, 29 | noise: MTLTexture?, 30 | meshVerticesCount: Int, 31 | view: MTKView, 32 | commandBuffer: MTLCommandBuffer, 33 | viewportSize: simd_float2) { 34 | 35 | assert(meshVerticesCount.isMultiple(of: 3)) 36 | guard let renderPassDescriptor = view.currentRenderPassDescriptor, 37 | let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) 38 | else { 39 | assertionFailure() 40 | return 41 | } 42 | 43 | renderEncoder.setViewport( 44 | MTLViewport(originX: 0, 45 | originY: 0, 46 | width: Double(viewportSize.x), 47 | height: Double(viewportSize.y), 48 | znear: 0, 49 | zfar: 1) 50 | ) 51 | 52 | renderEncoder.setRenderPipelineState(pipelineState) 53 | 54 | renderEncoder.setFragmentTexture(noise, 55 | index: Int(DrawMeshTextureIndexBaseColor.rawValue)) 56 | 57 | renderEncoder.setVertexBuffer(meshVertices, 58 | offset: 0, 59 | index: 0) 60 | for i in stride(from: 0, to: meshVerticesCount, by: 3) { 61 | renderEncoder.drawPrimitives(type: .triangle, 62 | vertexStart: i, 63 | vertexCount: 3) 64 | } 65 | 66 | renderEncoder.endEncoding() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/MeshGradient/MeshAnimator.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | import simd 4 | 5 | /// Stores current and next state of the mesh. Used for animatable meshes 6 | public final class MeshAnimator: MeshDataProvider { 7 | 8 | public struct Configuration { 9 | 10 | /// - Parameters: 11 | /// - framesPerSecond: Preferred framerate get that from MTKView 12 | /// - animationSpeedRange: Range of animation duration in the mesh, literally. The less the faster 13 | /// - meshRandomizer: Randomisation functions for mesh 14 | public init(framesPerSecond: Int = 60, animationSpeedRange: ClosedRange = 2...5, meshRandomizer: MeshRandomizer) { 15 | self.framesPerSecond = framesPerSecond 16 | self.animationSpeedRange = animationSpeedRange 17 | self.meshRandomizer = meshRandomizer 18 | } 19 | 20 | public var framesPerSecond: Int 21 | public var animationSpeedRange: ClosedRange 22 | public var meshRandomizer: MeshRandomizer 23 | } 24 | 25 | private struct AnimationFrameControlPoint { 26 | let finalControlPoint: ControlPoint 27 | let startPoint: ControlPoint 28 | var completionFactor: Double // 0...1 29 | let scaleFactor: Double 30 | 31 | mutating func bumpNextFrame() -> ControlPoint { 32 | completionFactor += scaleFactor 33 | var step = (finalControlPoint - startPoint) 34 | 35 | let easedCompletionFactor = completionFactor * completionFactor * (3 - 2 * completionFactor); 36 | step.scale(by: easedCompletionFactor) 37 | 38 | return startPoint + step 39 | } 40 | 41 | static var zero: AnimationFrameControlPoint { 42 | .init(finalControlPoint: .zero, startPoint: .zero, completionFactor: .zero, scaleFactor: .zero) 43 | } 44 | } 45 | 46 | private let initialGrid: Grid 47 | public var configuration: Configuration 48 | private var animationParameters: Grid 49 | 50 | public init(grid: Grid, configuration: Configuration) { 51 | self.initialGrid = grid 52 | self.configuration = configuration 53 | 54 | self.animationParameters = Grid(repeating: .zero, width: grid.width, height: grid.height) 55 | 56 | for y in 0 ..< animationParameters.height { 57 | for x in 0 ..< animationParameters.width { 58 | animationParameters[x, y] = generateNextAnimationEndpoint(x: x, y: y, gridWidth: grid.width, gridHeight: grid.height, startPoint: grid[x, y]) 59 | } 60 | } 61 | } 62 | 63 | public var grid: Grid { 64 | var resultGrid = Grid(repeating: .zero, 65 | width: animationParameters.width, 66 | height: animationParameters.height) 67 | 68 | for y in 0 ..< animationParameters.height { 69 | for x in 0 ..< animationParameters.width { 70 | let i = animationParameters.index(x: x, y: y) 71 | resultGrid[i] = animationParameters[i].bumpNextFrame() 72 | if animationParameters[i].completionFactor >= 1 { 73 | animationParameters[i] = generateNextAnimationEndpoint(x: x, y: y, gridWidth: resultGrid.width, gridHeight: resultGrid.height, startPoint: resultGrid[i]) 74 | } 75 | } 76 | } 77 | return resultGrid 78 | } 79 | 80 | private func generateNextAnimationEndpoint(x: Int, y: Int, gridWidth: Int, gridHeight: Int, startPoint: ControlPoint) -> AnimationFrameControlPoint { 81 | let animationDuration = Double.random(in: configuration.animationSpeedRange) 82 | let scaleFactor = (1 / Double(configuration.framesPerSecond)) / animationDuration 83 | var randomizedControlPoint = initialGrid[x, y] 84 | 85 | configuration.meshRandomizer.locationRandomizer(&randomizedControlPoint.location, x, y, gridWidth, gridHeight) 86 | 87 | configuration.meshRandomizer.turbulencyRandomizer(&randomizedControlPoint.uTangent, x, y, gridWidth, gridHeight) 88 | configuration.meshRandomizer.turbulencyRandomizer(&randomizedControlPoint.vTangent, x, y, gridWidth, gridHeight) 89 | 90 | configuration.meshRandomizer.colorRandomizer(&randomizedControlPoint.color, randomizedControlPoint.color, x, y, gridWidth, gridHeight) 91 | 92 | return AnimationFrameControlPoint(finalControlPoint: randomizedControlPoint, 93 | startPoint: startPoint, 94 | completionFactor: 0, 95 | scaleFactor: scaleFactor) 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /Sources/MeshGradient/MeshDataProvider.swift: -------------------------------------------------------------------------------- 1 | 2 | public protocol MeshDataProvider { 3 | var grid: Grid { get } 4 | } 5 | 6 | public class StaticMeshDataProvider: MeshDataProvider { 7 | public var grid: Grid 8 | 9 | public init(grid: Grid) { 10 | self.grid = grid 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/MeshGradient/MeshGenerator.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | import simd 5 | 6 | public enum MeshGenerator { 7 | 8 | public typealias Color = simd_float3 9 | 10 | /// Linear interpolation between `min` and `max`. 11 | private static func lerp(_ f: S, _ min: S, _ max: S) -> S { 12 | min + f * (max - min) 13 | } 14 | 15 | public static func generate(colorDistribution: Grid) -> Grid { 16 | var grid = Grid(repeating: ControlPoint(), 17 | width: colorDistribution.width, 18 | height: colorDistribution.height) 19 | 20 | for y in 0 ..< grid.height { 21 | for x in 0 ..< grid.width { 22 | generateControlPoint(in: &grid, x: x, y: y) 23 | } 24 | } 25 | return grid 26 | } 27 | 28 | private static func generateControlPoint(in grid: inout Grid, x: Int, y: Int) { 29 | grid[x, y].location = simd_float2( 30 | lerp(Float(x) / Float(grid.width - 1), -1, 1), 31 | lerp(Float(y) / Float(grid.height - 1), -1, 1) 32 | ) 33 | 34 | grid[x, y].uTangent.x = 2 / Float(grid.width - 1) 35 | grid[x, y].vTangent.y = 2 / Float(grid.height - 1) 36 | } 37 | } 38 | 39 | #if canImport(UIKit) 40 | import UIKit 41 | typealias NativeColor = UIColor 42 | #elseif canImport(AppKit) 43 | import AppKit 44 | typealias NativeColor = NSColor 45 | #endif 46 | 47 | public extension NativeColor { 48 | var toSIMD3: simd_float3 { 49 | 50 | var r: CGFloat = 0 51 | var g: CGFloat = 0 52 | var b: CGFloat = 0 53 | var o: CGFloat = 0 54 | 55 | getRed(&r, green: &g, blue: &b, alpha: &o) 56 | 57 | return .init(Float(r), Float(g), Float(b)) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/MeshGradient/MeshGradientError.swift: -------------------------------------------------------------------------------- 1 | 2 | enum MeshGradientError: Error { 3 | case metalFunctionNotFound(name: String) 4 | } 5 | -------------------------------------------------------------------------------- /Sources/MeshGradient/MeshRandomizer.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | import simd 4 | 5 | /// Stores randimisation information of the mesh. Data can be dynamically changes in SwiftUI views. 6 | /// Default initialiser provides empirically good random mesh. Free to change for your own needs. 7 | public struct MeshRandomizer { 8 | public init(locationRandomizer: @escaping MeshRandomizer.LocationRandomizer = MeshRandomizer.automaticallyRandomizeLocationYExceptTopAndBottomBasedOnGridSize(), 9 | turbulencyRandomizer: @escaping MeshRandomizer.TangentRandomizer = MeshRandomizer.randomizeTurbulencyExceptEdges(range: -0.25...0.25), 10 | colorRandomizer: @escaping MeshRandomizer.ColorRandomizer = MeshRandomizer.arrayBasedColorRandomizer(availableColors: (0...15).map({ _ in MeshRandomizer.randomColor() }))) { 11 | self.locationRandomizer = locationRandomizer 12 | self.turbulencyRandomizer = turbulencyRandomizer 13 | self.colorRandomizer = colorRandomizer 14 | } 15 | 16 | public typealias LocationRandomizer = (_ location: inout simd_float2, _ x: Int, _ y: Int, _ gridWidth: Int, _ gridHeight: Int) -> Void 17 | public typealias TangentRandomizer = (_ tangent: inout simd_float2, _ x: Int, _ y: Int, _ gridWidth: Int, _ gridHeight: Int) -> Void 18 | public typealias ColorRandomizer = (_ color: inout simd_float3, _ initialColor: simd_float3, _ x: Int, _ y: Int, _ gridWidth: Int, _ gridHeight: Int) -> Void 19 | 20 | 21 | public var locationRandomizer: LocationRandomizer 22 | public var turbulencyRandomizer: TangentRandomizer 23 | public var colorRandomizer: ColorRandomizer 24 | 25 | 26 | public static func randomColor() -> simd_float3 { 27 | .init(.random(in: 0...1), .random(in: 0...1), .random(in: 0...1)) 28 | } 29 | 30 | /// Will randomly choose colors from the provided array 31 | /// - Parameter availableColors: List of colors that will be used for randomisation 32 | public static func arrayBasedColorRandomizer(availableColors: [simd_float3]) -> ColorRandomizer { 33 | assert(!availableColors.isEmpty, "Available colors can not be empty") 34 | return { color, _, _, _, _, _ in 35 | color = availableColors.randomElement()! 36 | } 37 | } 38 | 39 | /// Slightly changes Y location of the control points based on grid width and height. 40 | /// Grid is drawed from top to bottom, and because of that changing of X locations can create visual glitches 41 | public static func automaticallyRandomizeLocationYExceptTopAndBottomBasedOnGridSize() -> LocationRandomizer { 42 | return { location, x, y, gridWidth, gridHeight in 43 | let locationVariationRange = 1.2 * 1 / Float(gridHeight) 44 | if y != 0 && y != gridHeight - 1 { 45 | location.y += .random(in: -locationVariationRange...locationVariationRange) 46 | } 47 | } 48 | } 49 | 50 | /// Applies random values from range to each control point location. Avoids randomisation of control points on the edges 51 | /// - Parameter range: Range for randomisation 52 | public static func randomizeLocationExceptEdges(range: ClosedRange) -> LocationRandomizer { 53 | return { location, x, y, gridWidth, gridHeight in 54 | if x != 0 && x != gridWidth - 1 { 55 | location.x += .random(in: range) 56 | } 57 | if y != 0 && y != gridHeight - 1 { 58 | location.y += .random(in: range) 59 | } 60 | } 61 | } 62 | 63 | /// Applies random values from range for each control point tangents. Avoids randomisation of control points on the edges 64 | /// - Parameter range: Range for randomisation 65 | public static func randomizeTurbulencyExceptEdges(range: ClosedRange) -> TangentRandomizer { 66 | return { tangent, x, y, width, height in 67 | if (x != 0 && y != 0) && (x != width - 1 && y != height - 1) { 68 | tangent.x += .random(in: range) 69 | tangent.y += .random(in: range) 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/MeshGradient/MetalMeshRenderer.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | import Metal 4 | import MetalKit 5 | @_implementationOnly import MeshGradientCHeaders 6 | 7 | public final class MetalMeshRenderer: NSObject, MTKViewDelegate { 8 | 9 | var commandQueue: MTLCommandQueue? 10 | var bufferPool: MTLBufferPool? 11 | 12 | var computeNoiseFunction: MTLComputeNoiseFunction? 13 | var computeShuffleCoefficients: MTLComputeShuffleCoefficientsFunction? 14 | var computeHermitPatchMatrix: MTLComputeHermitPatchMatrixFunction? 15 | var computeMeshTrianglePrimitives: MTLComputeMeshTrianglePrimitivesFunction? 16 | var drawMesh: MTLDrawMeshTrianglesFunction? 17 | 18 | var viewportSize: vector_float2 = .zero 19 | 20 | var subdivisions: Int 21 | let meshDataProvider: MeshDataProvider 22 | let grainAlpha: Float 23 | 24 | public init(metalKitView mtkView: MTKView, meshDataProvider: MeshDataProvider, grainAlpha: Float, subdivisions: Int = 18) { 25 | self.subdivisions = subdivisions 26 | self.meshDataProvider = meshDataProvider 27 | self.grainAlpha = grainAlpha 28 | 29 | guard let device = mtkView.device, 30 | let defaultLibrary = try? device.makeDefaultLibrary(bundle: .module) 31 | else { 32 | assertionFailure() 33 | return 34 | } 35 | 36 | 37 | let bufferPool = MTLBufferPool(device: device) 38 | 39 | do { 40 | computeNoiseFunction = try .init(device: device, library: defaultLibrary) 41 | computeShuffleCoefficients = try .init(device: device, library: defaultLibrary, bufferPool: bufferPool) 42 | computeHermitPatchMatrix = try .init(device: device, library: defaultLibrary) 43 | computeMeshTrianglePrimitives = try .init(device: device, library: defaultLibrary) 44 | drawMesh = try .init(device: device, library: defaultLibrary, mtkView: mtkView) 45 | 46 | } catch { 47 | assertionFailure(error.localizedDescription) 48 | } 49 | commandQueue = device.makeCommandQueue() 50 | self.bufferPool = bufferPool 51 | } 52 | 53 | public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { 54 | viewportSize.x = Float(size.width) 55 | viewportSize.y = Float(size.height) 56 | } 57 | 58 | private func calculateTriangles(grid: Grid, subdivisions: Int, commandBuffer: MTLCommandBuffer) -> (buffer: MTLBuffer, length: Int, elementsCount: Int)? { 59 | let resultVerticesSize = getResultVerticesSize(grid: grid, subdivisions: subdivisions) 60 | 61 | let resultTrianglesSize = (resultVerticesSize.width - 1) * (resultVerticesSize.height - 1) * 6 62 | let resultTrianglesBufferSize = MemoryLayout.stride * resultTrianglesSize 63 | 64 | guard let (triangleStripBuf, _, triangleStripCount) = calculateMeshTriangles(grid: grid, subdivisions: subdivisions, commandBuffer: commandBuffer), 65 | let resultTrianglesBuffer = bufferPool?[resultTrianglesBufferSize, .storageModePrivate], 66 | let computeMeshTrianglePrimitives = self.computeMeshTrianglePrimitives 67 | else { return nil } 68 | commandBuffer.addCompletedHandler { _ in 69 | self.bufferPool?[resultTrianglesBufferSize, .storageModePrivate] = resultTrianglesBuffer 70 | } 71 | 72 | computeMeshTrianglePrimitives.call(gridSize: resultVerticesSize, 73 | resultTrianglesBuffer: resultTrianglesBuffer, 74 | finalVertices: triangleStripBuf, 75 | finalVerticesSize: triangleStripCount, 76 | commandBuffer: commandBuffer) 77 | return (resultTrianglesBuffer, resultTrianglesBufferSize, resultTrianglesSize) 78 | } 79 | 80 | private func getResultVerticesSize(grid: Grid, subdivisions: Int) -> (width: Int, height: Int) { 81 | return (width: (grid.width - 1) * subdivisions, height: (grid.height - 1) * subdivisions) 82 | } 83 | 84 | private func calculateMeshTriangles(grid: Grid, subdivisions: Int, commandBuffer: MTLCommandBuffer) -> (buffer: MTLBuffer, length: Int, elementsCount: Int)? { 85 | 86 | let resultVerticesSize = getResultVerticesSize(grid: grid, subdivisions: subdivisions) 87 | 88 | let intermediateSize = (grid.width - 1) * (grid.height - 1) 89 | let intermediateBufferSize = intermediateSize * MemoryLayout.stride 90 | 91 | let finalVerticesSize = resultVerticesSize.width * resultVerticesSize.height 92 | let finalVerticesBufferSize = MemoryLayout.stride * finalVerticesSize 93 | 94 | guard let intermediateResultBuffer = bufferPool?[intermediateBufferSize, .storageModePrivate], 95 | let finalVerticesBuffer = bufferPool?[finalVerticesBufferSize, .storageModePrivate], 96 | let computeShuffleCoefficients = self.computeShuffleCoefficients, 97 | let computeHermitPatchMatrix = self.computeHermitPatchMatrix 98 | 99 | else { 100 | assertionFailure() 101 | return nil 102 | } 103 | commandBuffer.addCompletedHandler { _ in 104 | self.bufferPool?[intermediateBufferSize, .storageModePrivate] = intermediateResultBuffer 105 | self.bufferPool?[finalVerticesBufferSize, .storageModePrivate] = finalVerticesBuffer 106 | } 107 | 108 | commandBuffer.label = "Show Mesh Buffer" 109 | 110 | computeShuffleCoefficients.call(grid: grid, 111 | intermediateResultBuffer: intermediateResultBuffer, 112 | commandBuffer: commandBuffer) 113 | 114 | 115 | computeHermitPatchMatrix.call(subdivisions: subdivisions, 116 | resultBuffer: finalVerticesBuffer, 117 | intermediateResultBuffer: intermediateResultBuffer, 118 | gridWidth: resultVerticesSize.width, 119 | intermediateResultBufferSize: intermediateSize, 120 | commandBuffer: commandBuffer) 121 | 122 | return (finalVerticesBuffer, finalVerticesBufferSize, finalVerticesSize) 123 | } 124 | 125 | public func draw(in view: MTKView) { 126 | guard let commandQueue = commandQueue, 127 | let commandBuffer = commandQueue.makeCommandBuffer(), 128 | let computeNoise = self.computeNoiseFunction 129 | else { return } 130 | let grid = meshDataProvider.grid 131 | 132 | let noiseTexture = computeNoise.call(viewportSize: viewportSize, 133 | pixelFormat: view.colorPixelFormat, 134 | commandQueue: commandQueue, 135 | uniforms: .init(isSmooth: 1, 136 | color1: 94, 137 | color2: 168, 138 | color3: 147, 139 | noiseAlpha: grainAlpha)) 140 | 141 | guard let (resultBuffer, _, resultElementsCount) = calculateTriangles(grid: grid, subdivisions: subdivisions, commandBuffer: commandBuffer), 142 | let drawMesh = self.drawMesh 143 | else { assertionFailure(); return } 144 | 145 | drawMesh.call(meshVertices: resultBuffer, 146 | noise: noiseTexture, 147 | meshVerticesCount: resultElementsCount, 148 | view: view, 149 | commandBuffer: commandBuffer, 150 | viewportSize: viewportSize) 151 | 152 | if let drawable = view.currentDrawable { 153 | commandBuffer.present(drawable) 154 | } 155 | 156 | commandBuffer.commit() 157 | } 158 | 159 | func unwrap(buffer: MTLBuffer, length: Int? = nil, elementsCount: Int) -> [Element] { 160 | let rawPointer = buffer.contents() 161 | let length = length ?? MemoryLayout.stride * elementsCount 162 | let typedPointer = rawPointer.bindMemory(to: Element.self, capacity: length) 163 | let bufferedPointer = UnsafeBufferPointer(start: typedPointer, count: length) 164 | 165 | var result: [Element] = [] 166 | for i in 0.., animatorConfiguration: MeshAnimator.Configuration) 13 | case `static`(grid: Grid) 14 | } 15 | 16 | #if canImport(SwiftUI) 17 | import SwiftUI 18 | 19 | #if canImport(UIKit) 20 | import UIKit 21 | 22 | public struct LegacyMeshGradient: UIViewRepresentable { 23 | 24 | private let state: MeshGradientState 25 | private let subdivisions: Int 26 | private let grainAlpha: Float 27 | 28 | public init(initialGrid: Grid, 29 | animatorConfiguration: MeshAnimator.Configuration, 30 | grainAlpha: Float = MeshGradientDefaults.grainAlpha, 31 | subdivisions: Int = MeshGradientDefaults.subdivisions) { 32 | self.state = .animated(initial: initialGrid, animatorConfiguration: animatorConfiguration) 33 | self.grainAlpha = grainAlpha 34 | self.subdivisions = subdivisions 35 | } 36 | 37 | public init(grid: Grid, 38 | grainAlpha: Float = MeshGradientDefaults.grainAlpha, 39 | subdivisions: Int = MeshGradientDefaults.subdivisions) { 40 | self.state = .static(grid: grid) 41 | self.grainAlpha = grainAlpha 42 | self.subdivisions = subdivisions 43 | } 44 | 45 | public func makeUIView(context: Context) -> MTKView { 46 | let view = MTKView(frame: .zero, device: MTLCreateSystemDefaultDevice()) 47 | context.coordinator.renderer = .init(metalKitView: view, meshDataProvider: createDataProvider(), grainAlpha: grainAlpha, subdivisions: subdivisions) 48 | 49 | switch state { 50 | case .animated(_, let configuration): 51 | view.isPaused = false 52 | view.enableSetNeedsDisplay = false 53 | view.preferredFramesPerSecond = configuration.framesPerSecond 54 | case .static: 55 | view.isPaused = true 56 | view.enableSetNeedsDisplay = true 57 | view.preferredFramesPerSecond = 60 58 | } 59 | 60 | view.delegate = context.coordinator.renderer 61 | return view 62 | } 63 | 64 | private func createDataProvider() -> MeshDataProvider { 65 | switch state { 66 | case .animated(let initial, let animatorConfiguration): 67 | return MeshAnimator(grid: initial, configuration: animatorConfiguration) 68 | case .static(let grid): 69 | return StaticMeshDataProvider(grid: grid) 70 | } 71 | } 72 | 73 | public func updateUIView(_ view: MTKView, context: Context) { 74 | switch state { 75 | case .animated(_, let animatorConfiguration): 76 | guard let animator = context.coordinator.renderer.meshDataProvider as? MeshAnimator else { 77 | fatalError("Incorrect mesh data provider type. Expected \(MeshAnimator.self), got \(type(of: context.coordinator.renderer.meshDataProvider))") 78 | } 79 | animator.configuration = animatorConfiguration 80 | animator.configuration.framesPerSecond = min(animatorConfiguration.framesPerSecond, view.preferredFramesPerSecond) 81 | case .static(let grid): 82 | guard let staticMesh = context.coordinator.renderer.meshDataProvider as? StaticMeshDataProvider else { 83 | fatalError("Incorrect mesh data provider type. Expected \(StaticMeshDataProvider.self), got \(type(of: context.coordinator.renderer.meshDataProvider))") 84 | } 85 | staticMesh.grid = grid 86 | view.setNeedsDisplay() 87 | } 88 | context.coordinator.renderer.mtkView(view, drawableSizeWillChange: view.drawableSize) 89 | context.coordinator.renderer.subdivisions = subdivisions 90 | } 91 | 92 | public func makeCoordinator() -> Coordinator { 93 | return .init() 94 | } 95 | 96 | public final class Coordinator { 97 | var renderer: MetalMeshRenderer! 98 | } 99 | 100 | } 101 | 102 | #elseif canImport(AppKit) // canImport(UIKit) 103 | 104 | import AppKit 105 | 106 | public struct MeshGradient: NSViewRepresentable { 107 | 108 | private let state: MeshGradientState 109 | private let subdivisions: Int 110 | private let grainAlpha: Float 111 | 112 | public init(initialGrid: Grid, 113 | animatorConfiguration: MeshAnimator.Configuration, 114 | grainAlpha: Float = MeshGradientDefaults.grainAlpha, 115 | subdivisions: Int = MeshGradientDefaults.subdivisions) { 116 | self.state = .animated(initial: initialGrid, animatorConfiguration: animatorConfiguration) 117 | self.grainAlpha = grainAlpha 118 | self.subdivisions = subdivisions 119 | } 120 | 121 | public init(grid: Grid, 122 | grainAlpha: Float = MeshGradientDefaults.grainAlpha, 123 | subdivisions: Int = MeshGradientDefaults.subdivisions) { 124 | self.state = .static(grid: grid) 125 | self.grainAlpha = grainAlpha 126 | self.subdivisions = subdivisions 127 | } 128 | 129 | public func makeNSView(context: Context) -> MTKView { 130 | let view = MTKView(frame: .zero, device: MTLCreateSystemDefaultDevice()) 131 | context.coordinator.renderer = .init(metalKitView: view, meshDataProvider: createDataProvider(), grainAlpha: grainAlpha, subdivisions: subdivisions) 132 | 133 | switch state { 134 | case .animated(_, let configuration): 135 | view.isPaused = false 136 | view.enableSetNeedsDisplay = false 137 | view.preferredFramesPerSecond = configuration.framesPerSecond 138 | case .static: 139 | view.isPaused = true 140 | view.enableSetNeedsDisplay = true 141 | view.preferredFramesPerSecond = 60 142 | } 143 | 144 | view.delegate = context.coordinator.renderer 145 | return view 146 | } 147 | 148 | private func createDataProvider() -> MeshDataProvider { 149 | switch state { 150 | case .animated(let initial, let animatorConfiguration): 151 | return MeshAnimator(grid: initial, configuration: animatorConfiguration) 152 | case .static(let grid): 153 | return StaticMeshDataProvider(grid: grid) 154 | } 155 | } 156 | 157 | public func updateNSView(_ view: MTKView, context: Context) { 158 | switch state { 159 | case .animated(_, let animatorConfiguration): 160 | guard let animator = context.coordinator.renderer.meshDataProvider as? MeshAnimator else { 161 | fatalError("Incorrect mesh data provider type. Expected \(MeshAnimator.self), got \(type(of: context.coordinator.renderer.meshDataProvider))") 162 | } 163 | animator.configuration = animatorConfiguration 164 | animator.configuration.framesPerSecond = min(animatorConfiguration.framesPerSecond, view.preferredFramesPerSecond) 165 | case .static(let grid): 166 | guard let staticMesh = context.coordinator.renderer.meshDataProvider as? StaticMeshDataProvider else { 167 | fatalError("Incorrect mesh data provider type. Expected \(StaticMeshDataProvider.self), got \(type(of: context.coordinator.renderer.meshDataProvider))") 168 | } 169 | staticMesh.grid = grid 170 | view.setNeedsDisplay(view.bounds) 171 | } 172 | context.coordinator.renderer.mtkView(view, drawableSizeWillChange: view.drawableSize) 173 | context.coordinator.renderer.subdivisions = subdivisions 174 | } 175 | 176 | public func makeCoordinator() -> Coordinator { 177 | return .init() 178 | } 179 | 180 | public final class Coordinator { 181 | var renderer: MetalMeshRenderer! 182 | } 183 | 184 | } 185 | #endif // canImport(AppKit) 186 | 187 | #endif // canImport(SwiftUI) 188 | -------------------------------------------------------------------------------- /Sources/MeshGradientCHeaders/PleasingSwiftPM.c: -------------------------------------------------------------------------------- 1 | // 2 | // PleasingSwiftPM.c 3 | // 4 | // 5 | // Created by Nikita Patskov on 08.06.2022. 6 | // 7 | 8 | #include 9 | -------------------------------------------------------------------------------- /Sources/MeshGradientCHeaders/include/MetalMeshShaderTypes.h: -------------------------------------------------------------------------------- 1 | // 2 | // MetalMeshShaderTypes.h 3 | // ConvettiEmitter 4 | // 5 | // Created by Nikita Patskov on 03/06/2022. 6 | // 7 | 8 | #ifndef MetalMeshShaderTypes_h 9 | #define MetalMeshShaderTypes_h 10 | 11 | #include 12 | // Buffer index values shared between shader and C code to ensure Metal shader buffer inputs 13 | // match Metal API buffer set calls. 14 | typedef enum ComputeNoiseInputIndex 15 | { 16 | ComputeNoiseInputIndexOutputTexture = 0, 17 | ComputeNoiseInputIndexUniforms 18 | } AAPLVertexInputIndex; 19 | 20 | typedef enum ComputeMeshVertexInputIndex 21 | { 22 | ComputeMeshVertexInputIndexVertices = 0, 23 | ComputeMeshVertexInputIndexGridSize, 24 | ComputeMeshVertexInputIndexResult, 25 | ComputeMeshVertexInputCalculationSize, 26 | } ComputeMeshVertexInputIndex; 27 | 28 | typedef enum ComputeMeshFinalInputIndex 29 | { 30 | ComputeMeshFinalInputIndexVertices = 0, 31 | ComputeMeshFinalInputIndexWidth, 32 | ComputeMeshFinalInputIndexDepth, 33 | ComputeMeshFinalInputIndexSubdivisions, 34 | ComputeMeshFinalInputIndexResult, 35 | ComputeMeshFinalInputIndexCalculationSize, 36 | 37 | } ComputeMeshFinalInputIndex; 38 | 39 | typedef enum ComputeMeshTrianglesIndex 40 | { 41 | ComputeMeshTrianglesIndexVertices = 0, 42 | ComputeMeshTrianglesIndexWidth, 43 | ComputeMeshTrianglesIndexHeight, 44 | ComputeMeshTrianglesIndexResult, 45 | ComputeMeshTrianglesIndexCalculationSize, 46 | } ComputeMeshTrianglesIndex; 47 | 48 | typedef enum DrawMeshTextureIndex 49 | { 50 | DrawMeshTextureIndexBaseColor = 0, 51 | } DrawMeshTextureIndex; 52 | 53 | struct NoiseUniforms 54 | { 55 | int isSmooth; 56 | float color1; 57 | float color2; 58 | float color3; 59 | float noiseAlpha; 60 | }; 61 | 62 | typedef struct 63 | { 64 | vector_float2 location; 65 | vector_float2 uTangent; 66 | vector_float2 vTangent; 67 | 68 | vector_float4 color; 69 | 70 | } MeshControlPoint; 71 | 72 | typedef struct 73 | { 74 | simd_float4x4 X; 75 | simd_float4x4 Y; 76 | 77 | simd_float4x4 R; 78 | simd_float4x4 G; 79 | simd_float4x4 B; 80 | 81 | unsigned int x; 82 | unsigned int y; 83 | 84 | } MeshIntermediateVertex; 85 | 86 | typedef struct 87 | { 88 | vector_float2 position; 89 | vector_float4 color; 90 | } MeshVertex; 91 | 92 | 93 | #endif /* MetalMeshShaderTypes_h */ 94 | --------------------------------------------------------------------------------