├── .gitignore ├── LICENSE ├── README.md ├── SpatialDynamicMesh ├── SpatialDynamicMesh.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── SpatialDynamicMesh.xcscheme └── SpatialDynamicMesh │ ├── App.swift │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-512@2x 1.png │ │ └── Icon-512@2x.png │ ├── AppIcon.solidimagestack │ │ ├── Back.solidimagestacklayer │ │ │ ├── Content.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── Icon-512@2x.png │ │ │ └── Contents.json │ │ ├── Contents.json │ │ └── Front.solidimagestacklayer │ │ │ ├── Content.imageset │ │ │ ├── Contents.json │ │ │ └── Icon-512-Front@2x.png │ │ │ └── Contents.json │ └── Contents.json │ ├── Bridging.h │ ├── ContentView.swift │ ├── DynamicMesh.swift │ ├── Info.plist │ ├── MetalContext.swift │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── ShaderTypes.h │ ├── Shaders.metal │ └── SpatialDynamicMesh.entitlements └── screenshots └── 01.png /.gitignore: -------------------------------------------------------------------------------- 1 | xcuserdata/ 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dynamic RealityKit Meshes with LowLevelMesh 2 | 3 | This sample is a demonstration of how to use the [`LowLevelMesh`](https://developer.apple.com/documentation/realitykit/lowlevelmesh) class in RealityKit, introduced in visionOS 2 , iOS 18, and macOS 15 Sequoia. **Requires Xcode 16.0 Beta 1 or newer.** 4 | 5 | ![A screenshot of the sample app running on the visionOS Simulator](screenshots/01.png) 6 | 7 | The core idea behind this API is that sometimes you'd like to update the contents of a `MeshResource` without having to recreate it from scratch or pack your data into the prescribed [`MeshBuffers`](https://developer.apple.com/documentation/realitykit/meshbuffers) format. To that end, a `LowLevelMesh` is an alternative to `MeshDescriptor` that allows you to specify your own vertex buffer layout and regenerate mesh contents either on the CPU or on the GPU with a Metal compute shader. 8 | 9 | ## Attributes and Layouts 10 | 11 | A `LowLevelMesh` is constructed from a descriptor, which holds attributes and layouts. Attributes and layouts work together to tell RealityKit where to find all of the data for a given vertex. Attributes and layouts are an abstraction that give you a lot of flexibility over how vertex data is laid out in vertex buffers. 12 | 13 | An _attribute_ is a property of a vertex: position, normal, texture coordinate, etc. All of the data for a single attribute for a given mesh resides in one vertex buffer. Since vertex data might be interleaved in a vertex buffer, each attribute has an _offset_ which tells RealityKit where the data starts relative to the beginning of the buffer. Additionally, each attribute has a _format_ that indicates the type and size of the data. For example, vertex positions are commonly stored as `float3`, and a vertex color might be packed into a `uchar4Normalized`. 14 | 15 | For each vertex buffer, the low-level mesh descriptor contains a _layout_ which mostly exists to indicate the _stride_ or distance between the start of data for one vertex and the start of the next. Although it is possible to completely deinterleave vertex data, using one vertex buffer per attribute, it is common to interleave vertex data into as small a number of buffers as possible (one or maybe two). 16 | 17 | ## Attributes and Layouts in Practice 18 | 19 | To construct a `LowLevelMesh.Descriptor`, we first need to design our vertex layout. In this sample, I use a fully interleaved layout, with one buffer holding all attributes. Again, this isn't necessary, and one of the major selling points of the API is that you can use a completely arbitrary data layout. 20 | 21 | Suppose I have a vertex structure that looks like this in MSL: 22 | 23 | ```metal 24 | struct MeshVertex { 25 | packed_float3 position; 26 | packed_float3 normal; 27 | packed_float2 texCoords; 28 | }; 29 | ``` 30 | 31 | From the use of packed types, we know that the position is at offset 0, the normal is at offset 12, the texture coordinates are at offset 24, and the whole vertex has a size and stride of 32 bytes. This type is difficult to write as a Swift struct, since we don't have control over layout and padding, but that doesn't stop us from working with such vertices, especially if we only manipulate the data from a compute shader. 32 | 33 | Here's how to construct the attributes and layouts needed to represent this vertex structure in RealityKit: 34 | 35 | ```swift 36 | let attributes: [LowLevelMesh.Attribute] = [ 37 | .init(semantic: .position, format: .float3, offset: 0), 38 | .init(semantic: .normal, format: .float3, offset: 12), 39 | .init(semantic: .uv0, format: .float2, offset: 24) 40 | ] 41 | 42 | let layouts: [LowLevelMesh.Layout] = [ 43 | .init(bufferIndex: 0, bufferOffset: 0, bufferStride: 32) 44 | ] 45 | ``` 46 | 47 | (Note that each `LowLevelMesh.Attribute` has a default layout index of 0, corresponding to the first vertex buffer. You can change this if using more than one buffer/layout.) 48 | 49 | (You might object that this is brittle and will break if anything about our vertex struct changes. You'd be right. See the code for an example of how to make this slightly more robust by using Swift's `MemoryLayout` type.) 50 | 51 | ## Creating a `LowLevelMesh` 52 | 53 | One of the most important things to note about this API is that RealityKit manages the actual Metal buffers that store vertex and index data; you don't create these buffers yourself. This is so that RealityKit can perform internal synchronization and efficiently reuse resources over time. 54 | 55 | Since a `LowLevelMesh` can be updated at run-time without being recreated, we need to pick capacities for its vertex and index buffers. If you know that your vertex and index counts will never change, you can just use these as your vertex and index capacities. Otherwise, you need to establish an upper bound representing the maximum amount of data you expect the mesh to need during its whole lifetime. 56 | 57 | This sample builds an animated grid mesh whose resolution can be controlled at runtime. So, we select an upper bound of how finely subdivided the grid can be and calculate our vertex and index capacities based on our knowledge of the mesh topology we'll create: 58 | 59 | ```swift 60 | let vertexCapacity = (maxSegmentCount + 1) * (maxSegmentCount + 1) 61 | let indexCapacity = maxSegmentCount * maxSegmentCount * 6 62 | ``` 63 | 64 | Putting all of this together, we can now construct a `LowLevelMesh` object: 65 | 66 | ```swift 67 | let meshDescriptor = LowLevelMesh.Descriptor(vertexCapacity: vertexCapacity, 68 | vertexAttributes: attributes, 69 | vertexLayouts: layouts, 70 | indexCapacity: indexCapacity, 71 | indexType: .uint32) 72 | lowLevelMesh = try LowLevelMesh(descriptor: meshDescriptor) 73 | ``` 74 | 75 | ## Populating Mesh Data and Parts 76 | 77 | At this point, our mesh contains no data and no geometry. To actually draw anything with RealityKit, we need to populate our vertex buffers and tell our mesh how to group their contents into mesh parts. 78 | 79 | The core API for populating a low-level mesh's vertex buffers is the `replace(bufferIndex: Int, using: MTLCommandBuffer)` method. For each vertex buffer you want to fill with data, you call this method, which returns an `MTLBuffer`. You can then dispatch a compute function to populate the buffer. You have complete control over the work you do to achieve this; the only requisite is that you tell RealityKit which command buffer holds the commands that perform the update. RealityKit can then wait on its completion before using the contents of the buffer to render. This prevents race conditions and allows you to seamlessly update mesh data even while a previous version of the mesh's contents are being rendered. 80 | 81 | Similarly, you use the `replaceIndices(using: MTLCommandBuffer)` method to update mesh indices. 82 | 83 | When first populating a mesh, and whenever you change the number of indices thereafter, you must recreate the _parts_ of the mesh. You can think of a part as a sub-mesh: it has a range of indices and can be assigned a unique material. You are also responsible for calculating the model-space bounding box of the part, so RealityKit can perform frustum culling. 84 | 85 | In the sample, our grid mesh only has one part, and we use a fixed bounding volume that always contains it. Depending on your use case, you might need to do more work. 86 | 87 | Since we want to completely replace the pre-existing mesh part(s), we use the `replaceAll` method, calling it with a single-element array containing our one part. You're welcome to divide your mesh into as many parts as are necessary to achieve your desired effect. 88 | 89 | ```swift 90 | let bounds = BoundingBox(min: SIMD3(-0.5, -1.0, -0.5), 91 | max: SIMD3(0.5, 1.0, 0.5)) 92 | lowLevelMesh.parts.replaceAll([ 93 | LowLevelMesh.Part(indexOffset: 0, 94 | indexCount: indexCount, 95 | topology: .triangle, 96 | materialIndex: 0, 97 | bounds: bounds) 98 | ]) 99 | ``` 100 | 101 | Here's a sketch of how you might dispatch the compute work to perform a mesh update: 102 | 103 | ```swift 104 | let commandBuffer = commandQueue.makeCommandBuffer()! 105 | let threadgroupSize = MTLSize(...) 106 | 107 | let commandEncoder = commandBuffer.makeComputeCommandEncoder()! 108 | let vertexBuffer = lowLevelMesh.replace(bufferIndex: 0, using: commandBuffer) 109 | commandEncoder.setBuffer(vertexBuffer, offset: 0, index: 0) 110 | // ... set compute pipeline and other necessary state ... 111 | commandEncoder.dispatchThreads(..., threadsPerThreadgroup: threadgroupSize) 112 | 113 | let indexBuffer = lowLevelMesh.replaceIndices(using: commandBuffer) 114 | commandEncoder.setBuffer(indexBuffer, offset: 0, index: 0) 115 | // ... set compute pipeline and other necessary state ... 116 | commandEncoder.dispatchThreads(..., threadsPerThreadgroup: threadgroupSize) 117 | commandEncoder.endEncoding() 118 | 119 | commandBuffer.commit() 120 | ``` 121 | 122 | Since this is an introduction to the `LowLevelMesh` API and not a Metal tutorial, I refer you to the source code for the details of how the vertex and index buffers are populated on the GPU using compute shaders. 123 | 124 | ## Displaying a Low-Level Mesh 125 | 126 | The final step of getting a mesh on screen with RealityKit is using it to generate a `MeshResource` and attaching it to a `ModelEntity`. This is our opportunity to determine which `Material` should be used to shade each mesh part. 127 | 128 | ```swift 129 | let meshResource = try MeshResource(from: lowLevelMesh) 130 | 131 | var material = PhysicallyBasedMaterial() 132 | // ... set material properties ... 133 | 134 | let modelEntity = ModelEntity() 135 | modelEntity.model = ModelComponent(mesh: meshResource, materials: [material]) 136 | ``` 137 | 138 | We can then situate the model entity in the world using the ordinary APIs. 139 | 140 | A mesh resource created from a low-level mesh retains it, so that the mesh resource is aware of when its buffers and parts are updated. This allows RealityKit to seamlessly display the mesh whenever it changes (whether by being updated on the CPU or the GPU). 141 | 142 | The sample updates the animated mesh at a cadence of 60 FPS, but you may find that for your use cases, you want to update less frequently, or only in response to user input. 143 | 144 | For a more thorough example of how to use this API, consult Apple's sample code project, [_Creating a spatial drawing app with RealityKit_](https://developer.apple.com/documentation/RealityKit/creating-a-spatial-drawing-app-with-realitykit). 145 | -------------------------------------------------------------------------------- /SpatialDynamicMesh/SpatialDynamicMesh.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 838031B62C17D4CD00BA4DB2 /* MetalContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838031B52C17D4C700BA4DB2 /* MetalContext.swift */; }; 11 | 838031B82C17D4DE00BA4DB2 /* DynamicMesh.swift in Sources */ = {isa = PBXBuildFile; fileRef = 838031B72C17D4DB00BA4DB2 /* DynamicMesh.swift */; }; 12 | 838031BA2C17D4FE00BA4DB2 /* Shaders.metal in Sources */ = {isa = PBXBuildFile; fileRef = 838031B92C17D4FA00BA4DB2 /* Shaders.metal */; }; 13 | 839906F02C17C8EF00DD5712 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 839906EF2C17C8EF00DD5712 /* App.swift */; }; 14 | 839906F22C17C8EF00DD5712 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 839906F12C17C8EF00DD5712 /* ContentView.swift */; }; 15 | 839906F42C17C8F000DD5712 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 839906F32C17C8F000DD5712 /* Assets.xcassets */; }; 16 | 839906F82C17C8F000DD5712 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 839906F72C17C8F000DD5712 /* Preview Assets.xcassets */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | 83657AEA2C18267F00C73727 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 21 | 838031B52C17D4C700BA4DB2 /* MetalContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalContext.swift; sourceTree = ""; }; 22 | 838031B72C17D4DB00BA4DB2 /* DynamicMesh.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicMesh.swift; sourceTree = ""; }; 23 | 838031B92C17D4FA00BA4DB2 /* Shaders.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Shaders.metal; sourceTree = ""; }; 24 | 838031BB2C17D62100BA4DB2 /* ShaderTypes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ShaderTypes.h; sourceTree = ""; }; 25 | 838031BC2C17D6B800BA4DB2 /* Bridging.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Bridging.h; sourceTree = ""; }; 26 | 839906EC2C17C8EF00DD5712 /* SpatialDynamicMesh.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SpatialDynamicMesh.app; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | 839906EF2C17C8EF00DD5712 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; 28 | 839906F12C17C8EF00DD5712 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 29 | 839906F32C17C8F000DD5712 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 30 | 839906F52C17C8F000DD5712 /* SpatialDynamicMesh.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SpatialDynamicMesh.entitlements; sourceTree = ""; }; 31 | 839906F72C17C8F000DD5712 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 32 | /* End PBXFileReference section */ 33 | 34 | /* Begin PBXFrameworksBuildPhase section */ 35 | 839906E92C17C8EF00DD5712 /* Frameworks */ = { 36 | isa = PBXFrameworksBuildPhase; 37 | buildActionMask = 2147483647; 38 | files = ( 39 | ); 40 | runOnlyForDeploymentPostprocessing = 0; 41 | }; 42 | /* End PBXFrameworksBuildPhase section */ 43 | 44 | /* Begin PBXGroup section */ 45 | 839906E32C17C8EF00DD5712 = { 46 | isa = PBXGroup; 47 | children = ( 48 | 839906EE2C17C8EF00DD5712 /* SpatialDynamicMesh */, 49 | 839906ED2C17C8EF00DD5712 /* Products */, 50 | ); 51 | sourceTree = ""; 52 | }; 53 | 839906ED2C17C8EF00DD5712 /* Products */ = { 54 | isa = PBXGroup; 55 | children = ( 56 | 839906EC2C17C8EF00DD5712 /* SpatialDynamicMesh.app */, 57 | ); 58 | name = Products; 59 | sourceTree = ""; 60 | }; 61 | 839906EE2C17C8EF00DD5712 /* SpatialDynamicMesh */ = { 62 | isa = PBXGroup; 63 | children = ( 64 | 838031B52C17D4C700BA4DB2 /* MetalContext.swift */, 65 | 838031B72C17D4DB00BA4DB2 /* DynamicMesh.swift */, 66 | 839906F12C17C8EF00DD5712 /* ContentView.swift */, 67 | 839906EF2C17C8EF00DD5712 /* App.swift */, 68 | 838031B92C17D4FA00BA4DB2 /* Shaders.metal */, 69 | 838031BB2C17D62100BA4DB2 /* ShaderTypes.h */, 70 | 838031BC2C17D6B800BA4DB2 /* Bridging.h */, 71 | 839906F32C17C8F000DD5712 /* Assets.xcassets */, 72 | 83657AEA2C18267F00C73727 /* Info.plist */, 73 | 839906F52C17C8F000DD5712 /* SpatialDynamicMesh.entitlements */, 74 | 839906F62C17C8F000DD5712 /* Preview Content */, 75 | ); 76 | path = SpatialDynamicMesh; 77 | sourceTree = ""; 78 | }; 79 | 839906F62C17C8F000DD5712 /* Preview Content */ = { 80 | isa = PBXGroup; 81 | children = ( 82 | 839906F72C17C8F000DD5712 /* Preview Assets.xcassets */, 83 | ); 84 | path = "Preview Content"; 85 | sourceTree = ""; 86 | }; 87 | /* End PBXGroup section */ 88 | 89 | /* Begin PBXNativeTarget section */ 90 | 839906EB2C17C8EF00DD5712 /* SpatialDynamicMesh */ = { 91 | isa = PBXNativeTarget; 92 | buildConfigurationList = 839906FB2C17C8F000DD5712 /* Build configuration list for PBXNativeTarget "SpatialDynamicMesh" */; 93 | buildPhases = ( 94 | 839906E82C17C8EF00DD5712 /* Sources */, 95 | 839906E92C17C8EF00DD5712 /* Frameworks */, 96 | 839906EA2C17C8EF00DD5712 /* Resources */, 97 | ); 98 | buildRules = ( 99 | ); 100 | dependencies = ( 101 | ); 102 | name = SpatialDynamicMesh; 103 | productName = SpatialDynamicMesh; 104 | productReference = 839906EC2C17C8EF00DD5712 /* SpatialDynamicMesh.app */; 105 | productType = "com.apple.product-type.application"; 106 | }; 107 | /* End PBXNativeTarget section */ 108 | 109 | /* Begin PBXProject section */ 110 | 839906E42C17C8EF00DD5712 /* Project object */ = { 111 | isa = PBXProject; 112 | attributes = { 113 | BuildIndependentTargetsInParallel = 1; 114 | LastSwiftUpdateCheck = 1540; 115 | LastUpgradeCheck = 1600; 116 | TargetAttributes = { 117 | 839906EB2C17C8EF00DD5712 = { 118 | CreatedOnToolsVersion = 15.4; 119 | LastSwiftMigration = 1600; 120 | }; 121 | }; 122 | }; 123 | buildConfigurationList = 839906E72C17C8EF00DD5712 /* Build configuration list for PBXProject "SpatialDynamicMesh" */; 124 | compatibilityVersion = "Xcode 14.0"; 125 | developmentRegion = en; 126 | hasScannedForEncodings = 0; 127 | knownRegions = ( 128 | en, 129 | Base, 130 | ); 131 | mainGroup = 839906E32C17C8EF00DD5712; 132 | productRefGroup = 839906ED2C17C8EF00DD5712 /* Products */; 133 | projectDirPath = ""; 134 | projectRoot = ""; 135 | targets = ( 136 | 839906EB2C17C8EF00DD5712 /* SpatialDynamicMesh */, 137 | ); 138 | }; 139 | /* End PBXProject section */ 140 | 141 | /* Begin PBXResourcesBuildPhase section */ 142 | 839906EA2C17C8EF00DD5712 /* Resources */ = { 143 | isa = PBXResourcesBuildPhase; 144 | buildActionMask = 2147483647; 145 | files = ( 146 | 839906F82C17C8F000DD5712 /* Preview Assets.xcassets in Resources */, 147 | 839906F42C17C8F000DD5712 /* Assets.xcassets in Resources */, 148 | ); 149 | runOnlyForDeploymentPostprocessing = 0; 150 | }; 151 | /* End PBXResourcesBuildPhase section */ 152 | 153 | /* Begin PBXSourcesBuildPhase section */ 154 | 839906E82C17C8EF00DD5712 /* Sources */ = { 155 | isa = PBXSourcesBuildPhase; 156 | buildActionMask = 2147483647; 157 | files = ( 158 | 839906F22C17C8EF00DD5712 /* ContentView.swift in Sources */, 159 | 839906F02C17C8EF00DD5712 /* App.swift in Sources */, 160 | 838031B82C17D4DE00BA4DB2 /* DynamicMesh.swift in Sources */, 161 | 838031B62C17D4CD00BA4DB2 /* MetalContext.swift in Sources */, 162 | 838031BA2C17D4FE00BA4DB2 /* Shaders.metal in Sources */, 163 | ); 164 | runOnlyForDeploymentPostprocessing = 0; 165 | }; 166 | /* End PBXSourcesBuildPhase section */ 167 | 168 | /* Begin XCBuildConfiguration section */ 169 | 839906F92C17C8F000DD5712 /* Debug */ = { 170 | isa = XCBuildConfiguration; 171 | buildSettings = { 172 | ALWAYS_SEARCH_USER_PATHS = NO; 173 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 174 | CLANG_ANALYZER_NONNULL = YES; 175 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 176 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 177 | CLANG_ENABLE_MODULES = YES; 178 | CLANG_ENABLE_OBJC_ARC = YES; 179 | CLANG_ENABLE_OBJC_WEAK = YES; 180 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 181 | CLANG_WARN_BOOL_CONVERSION = YES; 182 | CLANG_WARN_COMMA = YES; 183 | CLANG_WARN_CONSTANT_CONVERSION = YES; 184 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 185 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 186 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 187 | CLANG_WARN_EMPTY_BODY = YES; 188 | CLANG_WARN_ENUM_CONVERSION = YES; 189 | CLANG_WARN_INFINITE_RECURSION = YES; 190 | CLANG_WARN_INT_CONVERSION = YES; 191 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 192 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 193 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 194 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 195 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 196 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 197 | CLANG_WARN_STRICT_PROTOTYPES = YES; 198 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 199 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 200 | CLANG_WARN_UNREACHABLE_CODE = YES; 201 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 202 | COPY_PHASE_STRIP = NO; 203 | DEAD_CODE_STRIPPING = YES; 204 | DEBUG_INFORMATION_FORMAT = dwarf; 205 | ENABLE_STRICT_OBJC_MSGSEND = YES; 206 | ENABLE_TESTABILITY = YES; 207 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 208 | GCC_C_LANGUAGE_STANDARD = gnu17; 209 | GCC_DYNAMIC_NO_PIC = NO; 210 | GCC_NO_COMMON_BLOCKS = YES; 211 | GCC_OPTIMIZATION_LEVEL = 0; 212 | GCC_PREPROCESSOR_DEFINITIONS = ( 213 | "DEBUG=1", 214 | "$(inherited)", 215 | ); 216 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 217 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 218 | GCC_WARN_UNDECLARED_SELECTOR = YES; 219 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 220 | GCC_WARN_UNUSED_FUNCTION = YES; 221 | GCC_WARN_UNUSED_VARIABLE = YES; 222 | IPHONEOS_DEPLOYMENT_TARGET = 18.0; 223 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 224 | MACOSX_DEPLOYMENT_TARGET = 15.0; 225 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 226 | MTL_FAST_MATH = YES; 227 | ONLY_ACTIVE_ARCH = YES; 228 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 229 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 230 | XROS_DEPLOYMENT_TARGET = 2.0; 231 | }; 232 | name = Debug; 233 | }; 234 | 839906FA2C17C8F000DD5712 /* Release */ = { 235 | isa = XCBuildConfiguration; 236 | buildSettings = { 237 | ALWAYS_SEARCH_USER_PATHS = NO; 238 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 239 | CLANG_ANALYZER_NONNULL = YES; 240 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 241 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 242 | CLANG_ENABLE_MODULES = YES; 243 | CLANG_ENABLE_OBJC_ARC = YES; 244 | CLANG_ENABLE_OBJC_WEAK = YES; 245 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 246 | CLANG_WARN_BOOL_CONVERSION = YES; 247 | CLANG_WARN_COMMA = YES; 248 | CLANG_WARN_CONSTANT_CONVERSION = YES; 249 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 250 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 251 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 252 | CLANG_WARN_EMPTY_BODY = YES; 253 | CLANG_WARN_ENUM_CONVERSION = YES; 254 | CLANG_WARN_INFINITE_RECURSION = YES; 255 | CLANG_WARN_INT_CONVERSION = YES; 256 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 257 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 258 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 259 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 260 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 261 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 262 | CLANG_WARN_STRICT_PROTOTYPES = YES; 263 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 264 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 265 | CLANG_WARN_UNREACHABLE_CODE = YES; 266 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 267 | COPY_PHASE_STRIP = NO; 268 | DEAD_CODE_STRIPPING = YES; 269 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 270 | ENABLE_NS_ASSERTIONS = NO; 271 | ENABLE_STRICT_OBJC_MSGSEND = YES; 272 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 273 | GCC_C_LANGUAGE_STANDARD = gnu17; 274 | GCC_NO_COMMON_BLOCKS = YES; 275 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 276 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 277 | GCC_WARN_UNDECLARED_SELECTOR = YES; 278 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 279 | GCC_WARN_UNUSED_FUNCTION = YES; 280 | GCC_WARN_UNUSED_VARIABLE = YES; 281 | IPHONEOS_DEPLOYMENT_TARGET = 18.0; 282 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 283 | MACOSX_DEPLOYMENT_TARGET = 15.0; 284 | MTL_ENABLE_DEBUG_INFO = NO; 285 | MTL_FAST_MATH = YES; 286 | SWIFT_COMPILATION_MODE = wholemodule; 287 | XROS_DEPLOYMENT_TARGET = 2.0; 288 | }; 289 | name = Release; 290 | }; 291 | 839906FC2C17C8F000DD5712 /* Debug */ = { 292 | isa = XCBuildConfiguration; 293 | buildSettings = { 294 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 295 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 296 | CLANG_ENABLE_MODULES = YES; 297 | CODE_SIGN_ENTITLEMENTS = SpatialDynamicMesh/SpatialDynamicMesh.entitlements; 298 | CODE_SIGN_STYLE = Automatic; 299 | CURRENT_PROJECT_VERSION = 1; 300 | DEAD_CODE_STRIPPING = YES; 301 | DEVELOPMENT_ASSET_PATHS = "\"SpatialDynamicMesh/Preview Content\""; 302 | DEVELOPMENT_TEAM = ""; 303 | ENABLE_HARDENED_RUNTIME = NO; 304 | ENABLE_PREVIEWS = YES; 305 | GENERATE_INFOPLIST_FILE = YES; 306 | INFOPLIST_FILE = SpatialDynamicMesh/Info.plist; 307 | INFOPLIST_KEY_CFBundleDisplayName = "Dynamic Mesh"; 308 | INFOPLIST_KEY_LSApplicationCategoryType = ""; 309 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 310 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 311 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 312 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 313 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 314 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 315 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 316 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 317 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 318 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 319 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 320 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 321 | MARKETING_VERSION = 1.0; 322 | PRODUCT_BUNDLE_IDENTIFIER = com.metalbyexample.SpatialDynamicMesh; 323 | PRODUCT_NAME = "$(TARGET_NAME)"; 324 | SDKROOT = auto; 325 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; 326 | SUPPORTS_MACCATALYST = NO; 327 | SWIFT_EMIT_LOC_STRINGS = YES; 328 | SWIFT_OBJC_BRIDGING_HEADER = SpatialDynamicMesh/Bridging.h; 329 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 330 | SWIFT_VERSION = 5.0; 331 | TARGETED_DEVICE_FAMILY = "1,2,7"; 332 | }; 333 | name = Debug; 334 | }; 335 | 839906FD2C17C8F000DD5712 /* Release */ = { 336 | isa = XCBuildConfiguration; 337 | buildSettings = { 338 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 339 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 340 | CLANG_ENABLE_MODULES = YES; 341 | CODE_SIGN_ENTITLEMENTS = SpatialDynamicMesh/SpatialDynamicMesh.entitlements; 342 | CODE_SIGN_STYLE = Automatic; 343 | CURRENT_PROJECT_VERSION = 1; 344 | DEAD_CODE_STRIPPING = YES; 345 | DEVELOPMENT_ASSET_PATHS = "\"SpatialDynamicMesh/Preview Content\""; 346 | DEVELOPMENT_TEAM = ""; 347 | ENABLE_HARDENED_RUNTIME = NO; 348 | ENABLE_PREVIEWS = YES; 349 | GENERATE_INFOPLIST_FILE = YES; 350 | INFOPLIST_FILE = SpatialDynamicMesh/Info.plist; 351 | INFOPLIST_KEY_CFBundleDisplayName = "Dynamic Mesh"; 352 | INFOPLIST_KEY_LSApplicationCategoryType = ""; 353 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 354 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 355 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 356 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 357 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 358 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 359 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 360 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 361 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 362 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 363 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 364 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 365 | MARKETING_VERSION = 1.0; 366 | PRODUCT_BUNDLE_IDENTIFIER = com.metalbyexample.SpatialDynamicMesh; 367 | PRODUCT_NAME = "$(TARGET_NAME)"; 368 | SDKROOT = auto; 369 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; 370 | SUPPORTS_MACCATALYST = NO; 371 | SWIFT_EMIT_LOC_STRINGS = YES; 372 | SWIFT_OBJC_BRIDGING_HEADER = SpatialDynamicMesh/Bridging.h; 373 | SWIFT_VERSION = 5.0; 374 | TARGETED_DEVICE_FAMILY = "1,2,7"; 375 | }; 376 | name = Release; 377 | }; 378 | /* End XCBuildConfiguration section */ 379 | 380 | /* Begin XCConfigurationList section */ 381 | 839906E72C17C8EF00DD5712 /* Build configuration list for PBXProject "SpatialDynamicMesh" */ = { 382 | isa = XCConfigurationList; 383 | buildConfigurations = ( 384 | 839906F92C17C8F000DD5712 /* Debug */, 385 | 839906FA2C17C8F000DD5712 /* Release */, 386 | ); 387 | defaultConfigurationIsVisible = 0; 388 | defaultConfigurationName = Release; 389 | }; 390 | 839906FB2C17C8F000DD5712 /* Build configuration list for PBXNativeTarget "SpatialDynamicMesh" */ = { 391 | isa = XCConfigurationList; 392 | buildConfigurations = ( 393 | 839906FC2C17C8F000DD5712 /* Debug */, 394 | 839906FD2C17C8F000DD5712 /* Release */, 395 | ); 396 | defaultConfigurationIsVisible = 0; 397 | defaultConfigurationName = Release; 398 | }; 399 | /* End XCConfigurationList section */ 400 | }; 401 | rootObject = 839906E42C17C8EF00DD5712 /* Project object */; 402 | } 403 | -------------------------------------------------------------------------------- /SpatialDynamicMesh/SpatialDynamicMesh.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SpatialDynamicMesh/SpatialDynamicMesh.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SpatialDynamicMesh/SpatialDynamicMesh.xcodeproj/xcshareddata/xcschemes/SpatialDynamicMesh.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /SpatialDynamicMesh/SpatialDynamicMesh/App.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct SpatialDynamicMeshApp: App { 5 | 6 | var body: some Scene { 7 | WindowGroup() { 8 | ContentView() 9 | } 10 | #if os(visionOS) 11 | .windowStyle(.volumetric) 12 | .defaultSize(width: 1.0, height: 0.2, depth: 1.0, in: .meters) 13 | #endif 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /SpatialDynamicMesh/SpatialDynamicMesh/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SpatialDynamicMesh/SpatialDynamicMesh/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon-512@2x.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | }, 9 | { 10 | "idiom" : "mac", 11 | "scale" : "1x", 12 | "size" : "16x16" 13 | }, 14 | { 15 | "idiom" : "mac", 16 | "scale" : "2x", 17 | "size" : "16x16" 18 | }, 19 | { 20 | "idiom" : "mac", 21 | "scale" : "1x", 22 | "size" : "32x32" 23 | }, 24 | { 25 | "idiom" : "mac", 26 | "scale" : "2x", 27 | "size" : "32x32" 28 | }, 29 | { 30 | "idiom" : "mac", 31 | "scale" : "1x", 32 | "size" : "128x128" 33 | }, 34 | { 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "idiom" : "mac", 41 | "scale" : "1x", 42 | "size" : "256x256" 43 | }, 44 | { 45 | "idiom" : "mac", 46 | "scale" : "2x", 47 | "size" : "256x256" 48 | }, 49 | { 50 | "idiom" : "mac", 51 | "scale" : "1x", 52 | "size" : "512x512" 53 | }, 54 | { 55 | "filename" : "Icon-512@2x 1.png", 56 | "idiom" : "mac", 57 | "scale" : "2x", 58 | "size" : "512x512" 59 | } 60 | ], 61 | "info" : { 62 | "author" : "xcode", 63 | "version" : 1 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /SpatialDynamicMesh/SpatialDynamicMesh/Assets.xcassets/AppIcon.appiconset/Icon-512@2x 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metal-by-example/metal-spatial-dynamic-mesh/8b04589f51bae9b64d7f4356c14d5a544437c4a8/SpatialDynamicMesh/SpatialDynamicMesh/Assets.xcassets/AppIcon.appiconset/Icon-512@2x 1.png -------------------------------------------------------------------------------- /SpatialDynamicMesh/SpatialDynamicMesh/Assets.xcassets/AppIcon.appiconset/Icon-512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metal-by-example/metal-spatial-dynamic-mesh/8b04589f51bae9b64d7f4356c14d5a544437c4a8/SpatialDynamicMesh/SpatialDynamicMesh/Assets.xcassets/AppIcon.appiconset/Icon-512@2x.png -------------------------------------------------------------------------------- /SpatialDynamicMesh/SpatialDynamicMesh/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon-512@2x.png", 5 | "idiom" : "vision", 6 | "scale" : "2x" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /SpatialDynamicMesh/SpatialDynamicMesh/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/Icon-512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metal-by-example/metal-spatial-dynamic-mesh/8b04589f51bae9b64d7f4356c14d5a544437c4a8/SpatialDynamicMesh/SpatialDynamicMesh/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/Icon-512@2x.png -------------------------------------------------------------------------------- /SpatialDynamicMesh/SpatialDynamicMesh/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SpatialDynamicMesh/SpatialDynamicMesh/Assets.xcassets/AppIcon.solidimagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.solidimagestacklayer" 9 | }, 10 | { 11 | "filename" : "Back.solidimagestacklayer" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /SpatialDynamicMesh/SpatialDynamicMesh/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon-512-Front@2x.png", 5 | "idiom" : "vision", 6 | "scale" : "2x" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /SpatialDynamicMesh/SpatialDynamicMesh/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/Icon-512-Front@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metal-by-example/metal-spatial-dynamic-mesh/8b04589f51bae9b64d7f4356c14d5a544437c4a8/SpatialDynamicMesh/SpatialDynamicMesh/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/Icon-512-Front@2x.png -------------------------------------------------------------------------------- /SpatialDynamicMesh/SpatialDynamicMesh/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SpatialDynamicMesh/SpatialDynamicMesh/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SpatialDynamicMesh/SpatialDynamicMesh/Bridging.h: -------------------------------------------------------------------------------- 1 | #import "ShaderTypes.h" 2 | -------------------------------------------------------------------------------- /SpatialDynamicMesh/SpatialDynamicMesh/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import RealityKit 3 | 4 | @MainActor 5 | class SceneContent { 6 | let context: MetalContext 7 | let modelEntity = ModelEntity() 8 | let wave: AnimatedWaveMesh 9 | 10 | init(context: MetalContext) { 11 | self.context = context 12 | do { 13 | wave = try AnimatedWaveMesh(context: context) 14 | wave.segmentCount = 96 15 | wave.amplitude = 0.02 16 | wave.waveDensity = 5.0 17 | wave.speed = 1.0 18 | 19 | let meshResource = try MeshResource(from: wave.lowLevelMesh) 20 | 21 | var material = PhysicallyBasedMaterial() 22 | material.baseColor.tint = .white 23 | material.roughness.scale = 0.0 24 | material.metallic.scale = 1.0 25 | material.faceCulling = .none 26 | 27 | modelEntity.model = ModelComponent(mesh: meshResource, materials: [material]) 28 | } catch { 29 | fatalError("Failed to intialize scene content") 30 | } 31 | } 32 | 33 | func update(timestep: TimeInterval) { 34 | wave.update(timestep) 35 | } 36 | } 37 | 38 | let animationFrameDuration: TimeInterval = 1.0 / 60.0 39 | 40 | struct ContentView: View { 41 | @State private var sceneContent = SceneContent(context: MetalContext.shared) 42 | @State private var frameDuration: TimeInterval = 0.0 43 | @State private var lastUpdateTime = CACurrentMediaTime() 44 | 45 | private let timer = Timer.publish(every: animationFrameDuration, on: .main, in: .common).autoconnect() 46 | 47 | var body: some View { 48 | RealityView { content in 49 | #if os(visionOS) 50 | content.add(sceneContent.modelEntity) 51 | #else 52 | sceneContent.modelEntity.transform.scale = SIMD3(repeating: 2.0) 53 | let anchorEntity = AnchorEntity(world: SIMD3(0, 0, -0.5)) 54 | anchorEntity.transform.rotation = simd_quatf(angle: .pi / 4, axis: SIMD3(1, 0, 0)) 55 | anchorEntity.addChild(sceneContent.modelEntity) 56 | content.add(anchorEntity) 57 | #endif 58 | } update: { content in 59 | sceneContent.wave.update(frameDuration) 60 | } 61 | .onReceive(timer) { input in 62 | let currentTime = CACurrentMediaTime() 63 | frameDuration = currentTime - lastUpdateTime 64 | lastUpdateTime = currentTime 65 | } 66 | .ignoresSafeArea() 67 | } 68 | } 69 | 70 | #Preview { 71 | ContentView() 72 | } 73 | -------------------------------------------------------------------------------- /SpatialDynamicMesh/SpatialDynamicMesh/DynamicMesh.swift: -------------------------------------------------------------------------------- 1 | import Metal 2 | import RealityKit 3 | 4 | @MainActor 5 | class AnimatedWaveMesh { 6 | let maxSegmentCount = 128 7 | let context: MetalContext 8 | let lowLevelMesh: LowLevelMesh 9 | 10 | var waveDensity: Float = 3.0 11 | var amplitude: Float = 0.1 12 | var speed: Float = 1.0 13 | 14 | var segmentCount = 64 { 15 | didSet { 16 | needsTopologyUpdate = true 17 | } 18 | } 19 | 20 | private var needsTopologyUpdate = true 21 | 22 | private var time: TimeInterval = 0.0 23 | 24 | init(context: MetalContext) throws { 25 | self.context = context 26 | 27 | // Get the memory layout of our vertex type so we can use it below 28 | let vertex = MemoryLayout.self 29 | 30 | // Create an attribute array whose elements match the order, format, and offsets of our vertex type 31 | let attributes: [LowLevelMesh.Attribute] = [ 32 | .init(semantic: .position, format: .float3, offset: vertex.offset(of: \.position)!), 33 | .init(semantic: .normal, format: .float3, offset: vertex.offset(of: \.normal)!), 34 | .init(semantic: .uv0, format: .float2, offset: vertex.offset(of: \.uv)!) 35 | ] 36 | 37 | // Create a layout describing a vertex buffer that holds packed instances of our vertex 38 | let layouts: [LowLevelMesh.Layout] = [ 39 | .init(bufferIndex: 0, bufferOffset: 0, bufferStride: vertex.stride) 40 | ] 41 | 42 | let vertexCapacity = (maxSegmentCount + 1) * (maxSegmentCount + 1) 43 | let indexCapacity = maxSegmentCount * maxSegmentCount * 6 44 | 45 | // Create a mesh descriptor that describes a mesh comprised of vertices as laid out above 46 | let meshDescriptor = LowLevelMesh.Descriptor(vertexCapacity: vertexCapacity, 47 | vertexAttributes: attributes, 48 | vertexLayouts: layouts, 49 | indexCapacity: indexCapacity, 50 | indexType: MTLIndexType.uint32) 51 | 52 | self.lowLevelMesh = try LowLevelMesh(descriptor: meshDescriptor) 53 | 54 | update(0.0) 55 | } 56 | 57 | func update(_ timestep: TimeInterval) { 58 | self.time += timestep 59 | 60 | guard let updateCommandBuffer = context.commandQueue.makeCommandBuffer() else { return } 61 | 62 | let activeSegmentCount = max(0, min(segmentCount, maxSegmentCount)) 63 | let indexCount = activeSegmentCount * activeSegmentCount * 6 64 | 65 | var waveDescriptor = WaveDescriptor(segmentCount: UInt32(activeSegmentCount), 66 | time: Float(time) * speed, 67 | waveDensity: waveDensity, 68 | amplitude: amplitude) 69 | 70 | let threadgroupSize = MTLSize(width: 8, height: 8, depth: 1) 71 | let requiresUniformDispatch = !context.device.supportsFamily(.apple4) 72 | 73 | if let commandEncoder = updateCommandBuffer.makeComputeCommandEncoder() { 74 | commandEncoder.setBytes(&waveDescriptor, length: MemoryLayout.size(ofValue: waveDescriptor), index: 1) 75 | 76 | let vertexBuffer = lowLevelMesh.replace(bufferIndex: 0, using: updateCommandBuffer) 77 | commandEncoder.setComputePipelineState(context.computePipelines[PipelineIndex.waveVertexUpdate.rawValue]) 78 | commandEncoder.setBuffer(vertexBuffer, offset: 0, index: 0) 79 | let vertexThreads = MTLSize(width: activeSegmentCount + 1, height: activeSegmentCount + 1, depth: 1) 80 | if requiresUniformDispatch { 81 | let vertexThreadgroups = MTLSize(width: (vertexThreads.width + threadgroupSize.width - 1) / threadgroupSize.width, 82 | height: (vertexThreads.height + threadgroupSize.height - 1) / threadgroupSize.height, 83 | depth: 1) 84 | commandEncoder.dispatchThreadgroups(vertexThreadgroups, threadsPerThreadgroup: threadgroupSize) 85 | } else { 86 | commandEncoder.dispatchThreads(vertexThreads, threadsPerThreadgroup: threadgroupSize) 87 | } 88 | 89 | if needsTopologyUpdate { 90 | let indexBuffer = lowLevelMesh.replaceIndices(using: updateCommandBuffer) 91 | commandEncoder.setComputePipelineState(context.computePipelines[PipelineIndex.gridIndexUpdate.rawValue]) 92 | commandEncoder.setBuffer(indexBuffer, offset: 0, index: 0) 93 | let indexThreads = MTLSize(width: activeSegmentCount, height: activeSegmentCount, depth: 1) 94 | if requiresUniformDispatch { 95 | let indexThreadgroups = MTLSize(width: (indexThreads.width + threadgroupSize.width - 1) / threadgroupSize.width, 96 | height: (indexThreads.height + threadgroupSize.height - 1) / threadgroupSize.height, 97 | depth: 1) 98 | commandEncoder.dispatchThreadgroups(indexThreadgroups, threadsPerThreadgroup: threadgroupSize) 99 | } else { 100 | commandEncoder.dispatchThreads(indexThreads, threadsPerThreadgroup: threadgroupSize) 101 | } 102 | 103 | let bounds = BoundingBox(min: SIMD3(-0.5, -1.0, -0.5), 104 | max: SIMD3(0.5, 1.0, 0.5)) 105 | lowLevelMesh.parts.replaceAll([ 106 | LowLevelMesh.Part(indexOffset: 0, 107 | indexCount: indexCount, 108 | topology: .triangle, 109 | materialIndex: 0, 110 | bounds: bounds) 111 | ]) 112 | 113 | needsTopologyUpdate = false 114 | } 115 | 116 | commandEncoder.endEncoding() 117 | } 118 | 119 | updateCommandBuffer.commit() 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /SpatialDynamicMesh/SpatialDynamicMesh/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationPreferredDefaultSceneSessionRole 8 | UIWindowSceneSessionRoleVolumetricApplication 9 | UIApplicationSupportsMultipleScenes 10 | 11 | UISceneConfigurations 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /SpatialDynamicMesh/SpatialDynamicMesh/MetalContext.swift: -------------------------------------------------------------------------------- 1 | import Metal 2 | 3 | enum PipelineIndex: Int { 4 | case waveVertexUpdate 5 | case gridIndexUpdate 6 | } 7 | 8 | class MetalContext { 9 | enum Error : Swift.Error { 10 | case libraryNotFound 11 | case functionNotFound(name: String) 12 | } 13 | 14 | static let shared = { 15 | do { 16 | return try MetalContext() 17 | } catch { 18 | fatalError("Error occurred while initializing shared Metal context: \(error)") 19 | } 20 | }() 21 | 22 | let device: MTLDevice 23 | let commandQueue: MTLCommandQueue 24 | 25 | var computePipelines: [MTLComputePipelineState] = [] 26 | 27 | init(device: MTLDevice? = nil, commandQueue: MTLCommandQueue? = nil) throws { 28 | guard let device = device ?? MTLCreateSystemDefaultDevice() else { 29 | fatalError() 30 | } 31 | guard let commandQueue = commandQueue ?? device.makeCommandQueue() else { 32 | fatalError() 33 | } 34 | self.device = device 35 | self.commandQueue = commandQueue 36 | 37 | try makePipelines() 38 | } 39 | 40 | func makePipelines() throws { 41 | guard let library = device.makeDefaultLibrary() else { 42 | throw Error.libraryNotFound 43 | } 44 | 45 | let vertexFunctionName = "update_wave_vertex" 46 | guard let vertexFunction = library.makeFunction(name: vertexFunctionName) else { 47 | throw Error.functionNotFound(name: vertexFunctionName) 48 | } 49 | let vertexPipeline = try device.makeComputePipelineState(function: vertexFunction) 50 | computePipelines.insert(vertexPipeline, at: PipelineIndex.waveVertexUpdate.rawValue) 51 | 52 | let indexFunctionName = "update_grid_indices" 53 | guard let indexFunction = library.makeFunction(name: indexFunctionName) else { 54 | throw Error.functionNotFound(name: indexFunctionName) 55 | } 56 | let indexPipeline = try device.makeComputePipelineState(function: indexFunction) 57 | computePipelines.insert(indexPipeline, at: PipelineIndex.gridIndexUpdate.rawValue) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /SpatialDynamicMesh/SpatialDynamicMesh/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SpatialDynamicMesh/SpatialDynamicMesh/ShaderTypes.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #ifndef __METAL__ 6 | typedef struct { float x; float y; float z; } packed_float3; 7 | #endif 8 | 9 | struct MeshVertex { 10 | packed_float3 position; 11 | packed_float3 normal; 12 | simd_packed_float2 uv; 13 | }; 14 | 15 | struct WaveDescriptor { 16 | unsigned int segmentCount; 17 | float time; 18 | float waveDensity; 19 | float amplitude; 20 | }; 21 | -------------------------------------------------------------------------------- /SpatialDynamicMesh/SpatialDynamicMesh/Shaders.metal: -------------------------------------------------------------------------------- 1 | #include 2 | using namespace metal; 3 | 4 | #include "ShaderTypes.h" 5 | 6 | float wave_height(constant WaveDescriptor &wave, float x, float z) { 7 | float r = sqrt(x * x + z * z); 8 | float y = wave.amplitude * cos(-r * 2.0f * M_PI_F * wave.waveDensity + wave.time); 9 | return y; 10 | } 11 | 12 | [[kernel]] 13 | void update_grid_indices(device uint *indices [[buffer(0)]], 14 | constant WaveDescriptor &wave [[buffer(1)]], 15 | uint2 gridCoords [[thread_position_in_grid]]) 16 | { 17 | if ((gridCoords[0] >= wave.segmentCount) || (gridCoords[1] >= wave.segmentCount)) { 18 | return; // Avoid writing out of bounds 19 | } 20 | 21 | uint widthSegments = wave.segmentCount; 22 | uint widthVertexCount = widthSegments + 1; 23 | uint baseIndex = (gridCoords.y * widthSegments + gridCoords.x) * 6; 24 | uint baseVertex = gridCoords.y * widthVertexCount + gridCoords.x; 25 | // Each grid segment is composed of two triangles wound in counter-clockwise order. 26 | indices[baseIndex + 0] = baseVertex; 27 | indices[baseIndex + 1] = baseVertex + widthVertexCount; 28 | indices[baseIndex + 2] = baseVertex + 1; 29 | indices[baseIndex + 3] = baseVertex + 1; 30 | indices[baseIndex + 4] = baseVertex + widthVertexCount; 31 | indices[baseIndex + 5] = baseVertex + widthVertexCount + 1; 32 | } 33 | 34 | [[kernel]] 35 | void update_wave_vertex(device MeshVertex *vertices [[buffer(0)]], 36 | constant WaveDescriptor &wave [[buffer(1)]], 37 | uint2 gridCoords [[thread_position_in_grid]]) 38 | { 39 | if ((gridCoords[0] > wave.segmentCount) || (gridCoords[1] > wave.segmentCount)) { 40 | return; // Avoid writing out of bounds 41 | } 42 | 43 | // The entire mesh is defined to span from -0.5 to 0.5 along the XZ plane in model space. 44 | const float width = 1.0f; 45 | const float depth = 1.0f; 46 | 47 | // Although we only pass in a single segment count value, for the sake of clarity, here 48 | // we define separate variables for the number of width and depth segments. 49 | const float widthSegments = wave.segmentCount; 50 | const float depthSegments = wave.segmentCount; 51 | 52 | // As a final bit of prelude, we define the model-space width and depth of each segment 53 | const float segmentWidth = width / widthSegments; 54 | const float segmentDepth = depth / depthSegments; 55 | 56 | // Here we convert from the 2D index of the vertex we're handling to the 1D vertex index 57 | // in the array of mesh vertices. 58 | const uint widthVertexCount = widthSegments + 1; 59 | const uint vertexIndex = gridCoords[1] * widthVertexCount + gridCoords[0]; 60 | 61 | // This is the vertex we're responsible for updating 62 | device MeshVertex &vert = vertices[vertexIndex]; 63 | 64 | // The position of the vertex is determined by interpolating 65 | // in grid space then sampling the wave height. 66 | const float x = gridCoords[0] * segmentWidth - (width * 0.5f); 67 | const float z = gridCoords[1] * segmentDepth - (depth * 0.5f); 68 | const float y = wave_height(wave, x, z); 69 | 70 | // To find the surface normal we take additional samples at 71 | // differential offsets in the X and Z directions. 72 | const float eps = 0.01f; 73 | const float dydx = wave_height(wave, x + eps, z) - wave_height(wave, x - eps, z); 74 | const float dydz = wave_height(wave, x, z + eps) - wave_height(wave, x, z - eps); 75 | const float dydy = eps * 2; 76 | const float3 N = normalize(float3(dydx, dydy, dydz)); 77 | 78 | // To find the uv coordinates of the vertex we do a straight 79 | // linear interpolation in grid space, flipping the vertical 80 | // axis to agree with Metal texture space. 81 | const float u = gridCoords[0] / widthSegments; 82 | const float v = 1.0f - (gridCoords[1] / depthSegments); 83 | 84 | // Finally we update our output vertex attributes. 85 | vert.position = float3(x, y, z); 86 | vert.normal = N; 87 | vert.uv = float2(u, v); 88 | } 89 | -------------------------------------------------------------------------------- /SpatialDynamicMesh/SpatialDynamicMesh/SpatialDynamicMesh.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /screenshots/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metal-by-example/metal-spatial-dynamic-mesh/8b04589f51bae9b64d7f4356c14d5a544437c4a8/screenshots/01.png --------------------------------------------------------------------------------