├── .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 |
--------------------------------------------------------------------------------