├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── LICENSE.txt ├── Package.swift ├── README.md ├── Sources └── MeshKit │ ├── Mesh+Helper │ ├── Grid.swift │ ├── Interpolation.swift │ ├── SCNGeometrySource+Colors.swift │ └── SCNNode+Convenience.swift │ ├── MeshNode.swift │ ├── MeshScene.swift │ └── MeshView.swift └── Tests └── MeshKitTests └── MeshKitTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2021 Scott Chacon 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. -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "MeshKit", 8 | platforms: [.iOS(.v12), .tvOS(.v12)], 9 | products: [ 10 | // Products define the executables and libraries a package produces, and make them visible to other packages. 11 | .library( 12 | name: "MeshKit", 13 | targets: ["MeshKit"]), 14 | ], 15 | dependencies: [ 16 | // Dependencies declare other packages that this package depends on. 17 | // .package(url: /* package url */, from: "1.0.0"), 18 | ], 19 | targets: [ 20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 21 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 22 | .target( 23 | name: "MeshKit", 24 | dependencies: []), 25 | .testTarget( 26 | name: "MeshKitTests", 27 | dependencies: ["MeshKit"]), 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MeshKit 2 | 3 | A powerful and easy to use live mesh gradient renderer for iOS. 4 | 5 | **This project wouldn't be possible without the awesome work from [Moving Parts](https://movingparts.io/gradient-meshes) and their [Swift Playground](https://github.com/movingparts-io/Gradient-Meshes-with-SceneKit)** 6 | 7 | ## What is MeshKit? 8 | 9 | MeshKit is an easy to use live mesh gradient renderer for iOS. In just a few lines of code, you can create a mesh gradient. 10 | 11 | ## Usage 12 | 13 | You can use the `MeshView` to render the view. This basically contains a SCNView with some extra magic. 14 | 15 | ##### To generate a mesh with the MeshView, you call the create method after initializing it. 16 | 17 | ```swift 18 | let meshView = MeshView() 19 | view.addSubview(meshView) 20 | 21 | meshView.create([ 22 | .init(point: (0, 0), location: (0, 0), color: UIColor(red: 0.149, green: 0.275, blue: 0.325, alpha: 1.000)), 23 | .init(point: (0, 1), location: (0, 1), color: UIColor(red: 0.157, green: 0.447, blue: 0.443, alpha: 1.000)), 24 | .init(point: (0, 2), location: (0, 2), color: UIColor(red: 0.165, green: 0.616, blue: 0.561, alpha: 1.000)), 25 | 26 | .init(point: (1, 0), location: (1, 0), color: UIColor(red: 0.541, green: 0.694, blue: 0.490, alpha: 1.000)), 27 | .init(point: (1, 1), location: (Float.random(in: 0.3...1.8), Float.random(in: 0.3...1.5)), color: UIColor(red: 0.541, green: 0.694, blue: 0.490, alpha: 1.000)), 28 | .init(point: (1, 2), location: (1, 2), color: UIColor(red: 0.914, green: 0.769, blue: 0.416, alpha: 1.000)), 29 | 30 | .init(point: (2, 0), location: (2, 0), color: UIColor(red: 0.957, green: 0.635, blue: 0.380, alpha: 1.000)), 31 | .init(point: (2, 1), location: (2, 1), color: UIColor(red: 0.933, green: 0.537, blue: 0.349, alpha: 1.000)), 32 | .init(point: (2, 2), location: (2, 2), color: UIColor(red: 0.906, green: 0.435, blue: 0.318, alpha: 1.000)), 33 | ]) 34 | ``` 35 | 36 | This can be called as many times as you want if you ever want to change the gradient. 37 | 38 | The `MeshView.create` method needs a `MeshNode.Color` array. These are simple ways to interface with the points on the mesh gradient. 39 | 40 | ##### The `Color` struct has 3 parts. `point`, `location`, and `color`. 41 | 42 | `point` – Where the color should exist on the gradient. This is different from `location` as this is meant for where on the square it should exist. For example, `0, 0` is one of the corners. No interpolation is involved here. 43 | 44 | *No two colors should have the same point.* 45 | 46 | `location` – Two floats on the x and y axis that will move the color and interpolate it's neighboring colors. What this basically means is where you actually want the color to go on the gradient. 47 | 48 | *Don't change the location for the edges of the gradient or it will have a weird shape.* 49 | 50 | `color` – A UIColor for the point. Be sure to choose colors that will interpolate with each other well. 51 | 52 | *Alphas are not used and will be ignored.* 53 | 54 | ##### You can also set the `width` and `height` when creating the mesh. 55 | 56 | For simplicity, the width and height should be the same. By default both are set to `3`. If you increase/decrease it, then you will need to supply the width/height **squared**. So if you set it to `2` then you will need to give it `4` colors. It would look like this 57 | 58 | ```swift 59 | meshView.create([ 60 | .init(point: (0, 0), location: (0, 0), color: UIColor(red: 0.149, green: 0.275, blue: 0.325, alpha: 1.000)), 61 | .init(point: (0, 1), location: (0, 1), color: UIColor(red: 0.157, green: 0.447, blue: 0.443, alpha: 1.000)), 62 | 63 | .init(point: (1, 0), location: (1, 0), color: UIColor(red: 0.541, green: 0.694, blue: 0.490, alpha: 1.000)), 64 | .init(point: (1, 1), location: (Float.random(in: 0.3...1.8), Float.random(in: 0.3...1.5)), color: UIColor(red: 0.541, green: 0.694, blue: 0.490, alpha: 1.000)), 65 | ] 66 | ``` 67 | 68 | If you set it to `4` then you would need to give it `16` colors and so on. 69 | 70 | ##### As well as setting the width and height, you can also change the subdivisions. 71 | 72 | This is the easiest setting to change. It changes the "resolution" of the wireframe. By default it is set to `18`. Raising it will exponentially decrease performance. 73 | 74 | ## Plans 75 | 76 | * More shape options 77 | * Easier methods to animate location changes 78 | * Color generating 79 | * Display P3 and other color profiles rendering/exporting 80 | * HDR support 81 | * More efficient rendering (Metal?) 82 | * macOS support 83 | * XCTests 84 | 85 | ## Acknowledgments 86 | 87 | * [Moving Parts](https://movingparts.io/gradient-meshes) 88 | -------------------------------------------------------------------------------- /Sources/MeshKit/Mesh+Helper/Grid.swift: -------------------------------------------------------------------------------- 1 | /// https://github.com/movingparts-io/Gradient-Meshes-with-SceneKit 2 | import Foundation 3 | 4 | /// A two-dimensional grid of `Element`. 5 | public struct Grid { 6 | public var elements: ContiguousArray 7 | 8 | public var width: Int 9 | 10 | public var height: Int 11 | 12 | public init(repeating element: Element, width: Int, height: Int) { 13 | self.width = width 14 | self.height = height 15 | self.elements = ContiguousArray(repeating: element, count: width * height) 16 | } 17 | 18 | public subscript(x: Int, y: Int) -> Element { 19 | get { 20 | elements[x + y * width] 21 | } 22 | set { 23 | elements[x + y * width] = newValue 24 | } 25 | } 26 | } 27 | 28 | extension Grid: Equatable where Element: Equatable {} 29 | 30 | extension Grid: Hashable where Element: Hashable {} 31 | -------------------------------------------------------------------------------- /Sources/MeshKit/Mesh+Helper/Interpolation.swift: -------------------------------------------------------------------------------- 1 | /// https://github.com/movingparts-io/Gradient-Meshes-with-SceneKit 2 | import Foundation 3 | 4 | /// Linear interpolation between `min` and `max`. 5 | public func lerp(_ f: S, _ min: S, _ max: S) -> S { 6 | min + f * (max - min) 7 | } 8 | -------------------------------------------------------------------------------- /Sources/MeshKit/Mesh+Helper/SCNGeometrySource+Colors.swift: -------------------------------------------------------------------------------- 1 | /// https://github.com/movingparts-io/Gradient-Meshes-with-SceneKit 2 | import SceneKit 3 | 4 | public extension SCNGeometrySource { 5 | /// Initializes a `SCNGeometrySource` with a list of colors as 6 | /// `SCNVector3`s`. 7 | convenience init(colors: [SCNVector3]) { 8 | let colorData = Data(bytes: colors, count: MemoryLayout.size * colors.count) 9 | 10 | self.init( 11 | data: colorData, 12 | semantic: .color, 13 | vectorCount: colors.count, 14 | usesFloatComponents: true, 15 | componentsPerVector: 3, 16 | bytesPerComponent: MemoryLayout.size, 17 | dataOffset: 0, 18 | dataStride: MemoryLayout.size 19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/MeshKit/Mesh+Helper/SCNNode+Convenience.swift: -------------------------------------------------------------------------------- 1 | /// https://github.com/movingparts-io/Gradient-Meshes-with-SceneKit 2 | import SceneKit 3 | 4 | public extension SCNNode { 5 | /// Creates a `SCNNode` based on a `Grid` of 2D points and a `Grid` of 6 | /// colors. 7 | convenience init(points: Grid, colors: Grid) { 8 | 9 | 10 | self.init( 11 | geometry: Self.get(points: points, colors: colors) 12 | ) 13 | } 14 | } 15 | 16 | public extension SCNNode { 17 | static func `get`(points: Grid, colors: Grid) -> SCNGeometry { 18 | precondition(points.width == colors.width && points.height == colors.height, "Grids must be of the same size.") 19 | 20 | var vertexList: [simd_float3] = [] 21 | var colorList: [simd_float3] = [] 22 | 23 | for x in 0 ..< points.width - 1 { 24 | for y in 0 ..< points.height - 1 { 25 | let p00 = points[x , y ] 26 | let p10 = points[x + 1, y ] 27 | let p01 = points[x , y + 1] 28 | let p11 = points[x + 1, y + 1] 29 | 30 | let v00 = simd_float3(p00.x, p00.y, 0) 31 | let v10 = simd_float3(p10.x, p10.y, 0) 32 | let v01 = simd_float3(p01.x, p01.y, 0) 33 | let v11 = simd_float3(p11.x, p11.y, 0) 34 | 35 | let c1 = colors[x , y ] 36 | let c2 = colors[x + 1, y ] 37 | let c3 = colors[x , y + 1] 38 | let c4 = colors[x + 1, y + 1] 39 | 40 | vertexList.append(contentsOf: [ 41 | v00, v10, v11, 42 | 43 | v11, v01, v00 44 | ]) 45 | 46 | colorList.append(contentsOf: [ 47 | c1, c2, c4, 48 | 49 | c4, c3, c1 50 | ]) 51 | } 52 | } 53 | 54 | let indices = vertexList.indices.map(Int32.init) 55 | 56 | let elements = SCNGeometryElement(indices: indices, primitiveType: .triangles) 57 | 58 | return SCNGeometry( 59 | sources: [ 60 | SCNGeometrySource(vertices: vertexList.map { SCNVector3($0) }), 61 | SCNGeometrySource(colors: colorList.map { SCNVector3($0) }) 62 | ], 63 | elements: [elements] 64 | ) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/MeshKit/MeshNode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MeshNode.swift 3 | // Acrylic 4 | // 5 | // Created by Ethan Lipnik on 10/23/21. 6 | // 7 | 8 | import UIKit 9 | import SceneKit 10 | 11 | public class MeshNode { 12 | 13 | private let linearSRGCColorSpace = CGColorSpace(name: CGColorSpace.linearSRGB)! 14 | 15 | public struct ControlPoint { 16 | public init(color: simd_float3 = simd_float3(0, 0, 0), location: simd_float2 = simd_float2(0, 0), uTangent: simd_float2 = simd_float2(0, 0), vTangent: simd_float2 = simd_float2(0, 0)) { 17 | self.color = color 18 | self.location = location 19 | self.uTangent = uTangent 20 | self.vTangent = vTangent 21 | } 22 | 23 | public var color: simd_float3 = simd_float3(0, 0, 0) 24 | public var location: simd_float2 = simd_float2(0, 0) 25 | public var uTangent: simd_float2 = simd_float2(0, 0) 26 | public var vTangent: simd_float2 = simd_float2(0, 0) 27 | } 28 | 29 | public struct Elements { 30 | public init(points: Grid, colors: Grid) { 31 | self.points = points 32 | self.colors = colors 33 | } 34 | 35 | public var points: Grid 36 | public var colors: Grid 37 | } 38 | 39 | public struct Color: Identifiable, Equatable, Codable { 40 | public static func == (lhs: MeshNode.Color, rhs: MeshNode.Color) -> Bool { 41 | return lhs.id == rhs.id && lhs.color == rhs.color 42 | } 43 | 44 | public var id: String { 45 | return "\(point.x)\(point.y)" 46 | } 47 | 48 | public init(point: (x: Int, y: Int), location: (x: Float, y: Float), color: UIColor, tangent: (u: Float, v: Float)) { 49 | self.point = point 50 | self.location = location 51 | self.color = color 52 | self.tangent = tangent 53 | } 54 | 55 | public var point: (x: Int, y: Int) 56 | public var location: (x: Float, y: Float) 57 | public var color: UIColor 58 | public var tangent: (u: Float, v: Float) 59 | 60 | public func colorToSimd() -> simd_float3 { 61 | var red: CGFloat = 0 62 | var green: CGFloat = 0 63 | var blue: CGFloat = 0 64 | var alpha: CGFloat = 0 65 | 66 | color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) 67 | 68 | return simd_float3(Float(red), Float(green), Float(blue)) 69 | } 70 | 71 | enum CodingKeys: String, CodingKey { 72 | case pointX 73 | case pointY 74 | 75 | case locationX 76 | case locationY 77 | 78 | case color 79 | case tangentU 80 | case tangentV 81 | } 82 | 83 | public init(from decoder: Decoder) throws { 84 | let container = try decoder.container(keyedBy: CodingKeys.self) 85 | 86 | let pointX = try container.decode(Int.self, forKey: .pointX) 87 | let pointY = try container.decode(Int.self, forKey: .pointY) 88 | self.point = (pointX, pointY) 89 | 90 | let locationX = try container.decode(Float.self, forKey: .locationX) 91 | let locationY = try container.decode(Float.self, forKey: .locationY) 92 | self.location = (locationX, locationY) 93 | 94 | self.color = try container.decode(CodableColor.self, forKey: .color).uiColor 95 | 96 | let tangentU = try container.decode(Float.self, forKey: .tangentU) 97 | let tangentV = try container.decode(Float.self, forKey: .tangentV) 98 | self.tangent = (tangentU, tangentV) 99 | } 100 | 101 | public func encode(to encoder: Encoder) throws { 102 | var container = encoder.container(keyedBy: CodingKeys.self) 103 | 104 | try container.encode(point.x, forKey: .pointX) 105 | try container.encode(point.y, forKey: .pointY) 106 | 107 | try container.encode(location.x, forKey: .locationX) 108 | try container.encode(location.y, forKey: .locationY) 109 | 110 | try container.encode(CodableColor(uiColor: color), forKey: .color) 111 | 112 | try container.encode(tangent.u, forKey: .tangentU) 113 | try container.encode(tangent.v, forKey: .tangentV) 114 | } 115 | 116 | struct CodableColor : Codable { 117 | var red : CGFloat = 0.0, green: CGFloat = 0.0, blue: CGFloat = 0.0, alpha: CGFloat = 0.0 118 | 119 | var uiColor : UIColor { 120 | return UIColor(red: red, green: green, blue: blue, alpha: alpha) 121 | } 122 | 123 | init(uiColor : UIColor) { 124 | uiColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha) 125 | } 126 | } 127 | } 128 | 129 | public static func node(elements: Elements) -> SCNNode { 130 | return SCNNode(points: elements.points, colors: elements.colors) 131 | } 132 | 133 | public static func generateElements(width: Int, height: Int, colors: [Color], subdivisions: Int) -> Elements { 134 | /// This math is from https://github.com/movingparts-io/Gradient-Meshes-with-SceneKit 135 | let H = simd_float4x4(rows: [ 136 | simd_float4( 2, -2, 1, 1), 137 | simd_float4(-3, 3, -2, -1), 138 | simd_float4( 0, 0, 1, 0), 139 | simd_float4( 1, 0, 0, 0) 140 | ]) 141 | 142 | let H_T = H.transpose 143 | 144 | func surfacePoint(u: Float, v: Float, X: simd_float4x4, Y: simd_float4x4) -> simd_float2 { 145 | let U = simd_float4(u * u * u, u * u, u, 1) 146 | let V = simd_float4(v * v * v, v * v, v, 1) 147 | 148 | return simd_float2( 149 | dot(V, U * H * X * H_T), 150 | dot(V, U * H * Y * H_T) 151 | ) 152 | } 153 | 154 | func meshCoefficients(_ p00: ControlPoint, _ p01: ControlPoint, _ p10: ControlPoint, _ p11: ControlPoint, axis: KeyPath) -> simd_float4x4 { 155 | func l(_ controlPoint: ControlPoint) -> Float { 156 | controlPoint.location[keyPath: axis] 157 | } 158 | 159 | func u(_ controlPoint: ControlPoint) -> Float { 160 | controlPoint.uTangent[keyPath: axis] 161 | } 162 | 163 | func v(_ controlPoint: ControlPoint) -> Float { 164 | controlPoint.vTangent[keyPath: axis] 165 | } 166 | 167 | return simd_float4x4(rows: [ 168 | simd_float4(l(p00), l(p01), v(p00), v(p01)), 169 | simd_float4(l(p10), l(p11), v(p10), v(p11)), 170 | simd_float4(u(p00), u(p01), 0, 0), 171 | simd_float4(u(p10), u(p11), 0, 0) 172 | ]) 173 | } 174 | 175 | func colorCoefficients(_ p00: ControlPoint, _ p01: ControlPoint, _ p10: ControlPoint, _ p11: ControlPoint, axis: KeyPath) -> simd_float4x4 { 176 | func l(_ point: ControlPoint) -> Float { 177 | point.color[keyPath: axis] 178 | } 179 | 180 | return simd_float4x4(rows: [ 181 | simd_float4(l(p00), l(p01), 0, 0), 182 | simd_float4(l(p10), l(p11), 0, 0), 183 | simd_float4( 0, 0, 0, 0), 184 | simd_float4( 0, 0, 0, 0) 185 | ]) 186 | } 187 | 188 | func colorPoint(u: Float, v: Float, R: simd_float4x4, G: simd_float4x4, B: simd_float4x4) -> simd_float3 { 189 | let U = simd_float4(u * u * u, u * u, u, 1) 190 | let V = simd_float4(v * v * v, v * v, v, 1) 191 | 192 | return simd_float3( 193 | dot(V, U * H * R * H_T), 194 | dot(V, U * H * G * H_T), 195 | dot(V, U * H * B * H_T) 196 | ) 197 | } 198 | 199 | func bilinearInterpolation(u: Float, v: Float, _ c00: ControlPoint, _ c01: ControlPoint, _ c10: ControlPoint, _ c11: ControlPoint) -> simd_float3 { 200 | let r = simd_float2x2(rows: [ 201 | simd_float2(c00.color.x, c01.color.x), 202 | simd_float2(c10.color.x, c11.color.x), 203 | ]) 204 | 205 | let g = simd_float2x2(rows: [ 206 | simd_float2(c00.color.y, c01.color.y), 207 | simd_float2(c10.color.y, c11.color.y), 208 | ]) 209 | 210 | let b = simd_float2x2(rows: [ 211 | simd_float2(c00.color.z, c01.color.z), 212 | simd_float2(c10.color.z, c11.color.z), 213 | ]) 214 | 215 | let r_ = dot(simd_float2(1 - u, u), r * simd_float2(1 - v, v)) 216 | let g_ = dot(simd_float2(1 - u, u), g * simd_float2(1 - v, v)) 217 | let b_ = dot(simd_float2(1 - u, u), b * simd_float2(1 - v, v)) 218 | 219 | return simd_float3(r_, g_, b_) 220 | } 221 | 222 | var grid = Grid(repeating: ControlPoint(), width: width, height: height) 223 | for color in colors { 224 | grid[color.point.x, color.point.y].color = color.colorToSimd() 225 | } 226 | 227 | for y in 0 ..< grid.height { 228 | for x in 0 ..< grid.width { 229 | 230 | if let color = colors.first(where: { $0.point.x == x && $0.point.y == y }) { 231 | grid[x, y].uTangent.x = color.tangent.u / Float(grid.width - 1) 232 | grid[x, y].vTangent.y = color.tangent.v / Float(grid.height - 1) 233 | 234 | grid[x, y].location = simd_float2( 235 | lerp(color.location.x / Float(grid.width - 1), -1, 1), 236 | lerp(color.location.y / Float(grid.height - 1), -1, 1) 237 | ) 238 | } else { 239 | grid[x, y].uTangent.x = 2 / Float(grid.width - 1) 240 | grid[x, y].vTangent.y = 2 / Float(grid.height - 1) 241 | 242 | grid[x, y].location = simd_float2( 243 | lerp(Float(x) / Float(grid.width - 1), -1, 1), 244 | lerp(Float(y) / Float(grid.height - 1), -1, 1) 245 | ) 246 | } 247 | } 248 | } 249 | 250 | var points = Grid( 251 | repeating: simd_float2(0, 0), 252 | width: (grid.width - 1) * subdivisions, 253 | height: (grid.height - 1) * subdivisions 254 | ) 255 | 256 | var colors = Grid( 257 | repeating: simd_float3(0, 0 , 0), 258 | width: (grid.width - 1) * subdivisions, 259 | height: (grid.height - 1) * subdivisions 260 | ) 261 | 262 | for x in 0 ..< grid.width - 1 { 263 | for y in 0 ..< grid.height - 1 { 264 | // The four control points in the corners of the current patch. 265 | let p00 = grid[ x, y] 266 | let p01 = grid[ x, y + 1] 267 | let p10 = grid[x + 1, y] 268 | let p11 = grid[x + 1, y + 1] 269 | 270 | // The X and Y coefficient matrices for the current Hermite patch. 271 | let X = meshCoefficients(p00, p01, p10, p11, axis: \.x) 272 | let Y = meshCoefficients(p00, p01, p10, p11, axis: \.y) 273 | 274 | // The coefficients matrices for the current hermite patch in RGB 275 | // space 276 | let R = colorCoefficients(p00, p01, p10, p11, axis: \.x) 277 | let G = colorCoefficients(p00, p01, p10, p11, axis: \.y) 278 | let B = colorCoefficients(p00, p01, p10, p11, axis: \.z) 279 | 280 | for u in 0 ..< subdivisions { 281 | for v in 0 ..< subdivisions { 282 | points[x * subdivisions + u, y * subdivisions + v] = 283 | surfacePoint( 284 | u: Float(u) / Float(subdivisions - 1), 285 | v: Float(v) / Float(subdivisions - 1), 286 | X: X, 287 | Y: Y 288 | ) 289 | 290 | // Compare against bilinear interpolation here by using 291 | // 292 | // bilinearInterpolation( 293 | // u: Float(u) / Float(subdivisions - 1), 294 | // v: Float(v) / Float(subdivisions - 1), 295 | // c00, c01, c10, c11) 296 | // 297 | colors[x * subdivisions + u, y * subdivisions + v] = 298 | colorPoint( 299 | u: Float(u) / Float(subdivisions - 1), 300 | v: Float(v) / Float(subdivisions - 1), 301 | R: R, G: G, B: B 302 | ) 303 | } 304 | } 305 | } 306 | } 307 | 308 | return .init(points: points, colors: colors) 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /Sources/MeshKit/MeshScene.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MeshScene.swift 3 | // 4 | // 5 | // Created by Ethan Lipnik on 3/23/22. 6 | // 7 | 8 | import SceneKit 9 | 10 | open class MeshScene: SCNScene { 11 | public final func create(_ colors: [MeshNode.Color], width: Int = 3, height: Int = 3, subdivisions: Int = 18) { 12 | let elements = MeshNode.generateElements(width:width, 13 | height: height, 14 | colors: colors, 15 | subdivisions: subdivisions) 16 | 17 | if let node = rootNode.childNode(withName: "meshNode", 18 | recursively: false) { 19 | node.geometry = SCNNode.get(points: elements.points, 20 | colors: elements.colors) 21 | } else { 22 | let node = MeshNode.node(elements: elements) 23 | node.name = "meshNode" 24 | 25 | rootNode.childNodes.forEach({ $0.removeFromParentNode() }) 26 | rootNode.addChildNode(node) 27 | } 28 | } 29 | 30 | public final func generate(size: CGSize) -> UIImage { 31 | let renderer = SCNRenderer(device: MTLCreateSystemDefaultDevice()) 32 | renderer.scene = self 33 | return renderer.snapshot(atTime: .zero, with: size, antialiasingMode: .none) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/MeshKit/MeshView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MeshView.swift 3 | // Acrylic 4 | // 5 | // Created by Ethan Lipnik on 10/23/21. 6 | // 7 | 8 | import UIKit 9 | import SceneKit 10 | 11 | public class MeshView: UIView { 12 | 13 | // MARK: - Views 14 | public lazy var scene: MeshScene = { 15 | let scene = MeshScene() 16 | 17 | return scene 18 | }() 19 | public lazy var sceneView: SCNView = { 20 | let view = SCNView() 21 | 22 | view.scene = scene 23 | view.allowsCameraControl = false 24 | view.debugOptions = debugOptions 25 | view.isUserInteractionEnabled = false 26 | 27 | view.translatesAutoresizingMaskIntoConstraints = false 28 | 29 | return view 30 | }() 31 | 32 | // MARK: - Variables 33 | public lazy var debugOptions: SCNDebugOptions = [] { 34 | didSet { 35 | sceneView.debugOptions = debugOptions 36 | } 37 | } 38 | 39 | public lazy var scaleFactor: CGFloat = contentScaleFactor { 40 | didSet { 41 | sceneView.contentScaleFactor = contentScaleFactor 42 | } 43 | } 44 | 45 | // MARK: - Setup 46 | public init() { 47 | super.init(frame: .zero) 48 | 49 | setup() 50 | } 51 | 52 | public override init(frame: CGRect) { 53 | super.init(frame: .zero) 54 | 55 | setup() 56 | } 57 | 58 | public required init?(coder: NSCoder) { 59 | super.init(coder: coder) 60 | 61 | setup() 62 | } 63 | 64 | private final func setup() { 65 | addSubview(sceneView) 66 | 67 | NSLayoutConstraint.activate([ 68 | sceneView.topAnchor.constraint(equalTo: topAnchor), 69 | sceneView.leadingAnchor.constraint(equalTo: leadingAnchor), 70 | sceneView.trailingAnchor.constraint(equalTo: trailingAnchor), 71 | sceneView.bottomAnchor.constraint(equalTo: bottomAnchor) 72 | ]) 73 | } 74 | 75 | public final func create(_ colors: [MeshNode.Color], width: Int = 3, height: Int = 3, subdivisions: Int = 18) { 76 | scene.create(colors, width: width, height: height, subdivisions: subdivisions) 77 | } 78 | 79 | public final func snapshot() -> UIImage { 80 | return sceneView.snapshot() 81 | } 82 | 83 | public final func generate(size: CGSize) -> UIImage { 84 | return scene.generate(size: size) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Tests/MeshKitTests/MeshKitTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import MeshKit 3 | 4 | final class MeshKitTests: XCTestCase { 5 | } 6 | --------------------------------------------------------------------------------