SceneKit: Deformable Terrain
For one of my first projects at Apple, I developed a significant amount of prototype 3D UI with Apple's SceneKit framework, and I was impressed by the balance it struck between power and simplicity. SceneKit makes it very easy to set up a 3D object graph and provides surprisingly powerful control over lighting, cameras, and object materials.
SceneKit has continued to grow more impressive with each update, and I recently dived back into it over the weekend to play around with a simple concept I'd been wanting to experiment with for a while.
There are a variety of possible approaches for this, the one described here has been implemented in a custom SCNNode subclass called TerrainMesh. The class creates a mesh of arbitrary size which can then be deformed or customized at the level of individual vertices. In discussing how this example class works, and how to use it, several concepts will be touched on:
Finally, once all of this has been set-up, the easy part is plugging it into a SCNNode's -geometry property. Within TerrainMesh this happens as part of the -configureGeometry call.
By default, the vertices TerrainMesh builds are oriented so that the mesh extends along the X and Y planes, and the Z value of each vertex controls its depth, defining the 'height' of the terrain for that point.
These vertices form the foundation of our custom SceneKit geometry. Creating the SCNGeometrySource for them is rather simple using one of SceneKit's factory methods: +[SCNGeometrySource geometrySourceWithVertices:count:].
The advantage with this approach is that we can reuse the same vertex for multiple triangles, which prevents us from having to unnecessarily define the same point repeteadly. In the example mesh image, vertex 4 is actually used as part of 6 different triangles.
The triangles are used to create our SCNGeometryElement object. An example of how the SCNGeometryElement is created from the data:
We create an NSData based on our array of index sets, and pass that into the SCNGeometryElement factory method, along with some basic parameters detailing how we'd like the element to be created.
So now that we've created the points of the mesh, and the triangles that make up the mesh, we should be done, right? Almost!
The normals allow SceneKit to understand the surface of our geometry and how it should be rendered under light. For a more detailed definition on normals this is a great and concise explanation. Like our previously created vertex source, the SCNGeometrySource for our normals is created fairly simply using one of SceneKit's convenience methods: +[SCNGeometrySource geometrySourceWithNormals:count:].
(Image source: Blender 3D.)
For our purposes we just create an X,Y pair of values for each vertex. The X and Y have values in the range of 0.0 - 1.0 to define what portion of the material texture should be applied at that point.
Again, the example code handles all this with a set of reasonable default values. If you wish to create a custom source for this, you can do so in a similar manner to how we created our other sources, except you use one of the SCNGeometrySource methods which allow you to specify the SCNGeometrySourceSemanticTexcoord as the semantic for your geometry source data.
- (void)derformTerrainAt:(CGPoint)point brushRadius:(double)brushRadius intensity:(double)intensity;
Under the hood, TerrainMesh is simply recomputing the vertices and other geometry (disclaimer: not in the most efficient way, I should add) and then updating the TerrainMesh SCNNode. The result is a flexible and morphable terrain which we can paint hills or valleys into:
Any questions, please feel free to get in touch.
SceneKit has continued to grow more impressive with each update, and I recently dived back into it over the weekend to play around with a simple concept I'd been wanting to experiment with for a while.
Deformable Terrain
Get the source code for this article on GitHub: /matthewreagan/TerrainMesh3D
There are a variety of possible approaches for this, the one described here has been implemented in a custom SCNNode subclass called TerrainMesh. The class creates a mesh of arbitrary size which can then be deformed or customized at the level of individual vertices. In discussing how this example class works, and how to use it, several concepts will be touched on:
- Creating custom SceneKit geometry
- Calculating various components of that geometry (normals, texture coordinates, etc.)
- Deforming and manipulating that geometry
Creating a Mesh
To create our deformable terrain we will build a mesh of triangles which can then be manipulated. Creating this geometry for SceneKit is handled automatically by TerrainMesh, but under the hood that geometry is comprised of several components:- Vertices to define each point on the mesh
- Sets of indices to define each triangle
- Normals to allow for proper lighting / shading
- Texture coordinates to specify how image textures should be mapped
SceneKit Custom Geometry
In SceneKit, our geometry will be built from several closely-related components. We will create a SCNGeometry object from a set of SCNGeometrySources. These sources will encapsulate the data for the vertices, triangles, normals, and texture coordinates. Our SCNGeometry is also supplied with a SCNGeometryElement, which defines the overall terrain element built from the triangles making up the mesh.Finally, once all of this has been set-up, the easy part is plugging it into a SCNNode's -geometry property. Within TerrainMesh this happens as part of the -configureGeometry call.
Geometry: Vertices
The vertices of our terrain are the actual points in 3D object space which define the mesh. In the above image they correspond to the dots at each corner. By default, the TerrainMesh example creates a 2D array of vertices where the 0th vertex begins at 0.0, 0.0 in object space, and where the X and Y values incrementally increase with the vertex index. The final index is the 'topright' point of the mesh, with an X, Y value of whatever unit side length is passed in. Example: for a 2x2 mesh with a side length of 5.0, the 0th vertex would be 0.0, 0.0 and the final vertex at index 3 would be 5.0, 5.0.By default, the vertices TerrainMesh builds are oriented so that the mesh extends along the X and Y planes, and the Z value of each vertex controls its depth, defining the 'height' of the terrain for that point.
These vertices form the foundation of our custom SceneKit geometry. Creating the SCNGeometrySource for them is rather simple using one of SceneKit's factory methods: +[SCNGeometrySource geometrySourceWithVertices:count:].
Geometry: Triangles
The triangles of the mesh are defined using our previous vertices. The TerrainMesh class does this automatically, but in short the triangles are defined by creating sets of index values which map to our vertices. In the example mesh image, the 1st triangle of the mesh is defined using an index set of 4, 3, 0.The advantage with this approach is that we can reuse the same vertex for multiple triangles, which prevents us from having to unnecessarily define the same point repeteadly. In the example mesh image, vertex 4 is actually used as part of 6 different triangles.
The triangles are used to create our SCNGeometryElement object. An example of how the SCNGeometryElement is created from the data:
SCNGeometryElement *element = [SCNGeometryElement geometryElementWithData:[NSData dataWithBytes:indices length:length] primitiveType:SCNGeometryPrimitiveTypeTriangles primitiveCount:totalTriangles bytesPerIndex:sizeof(int)];
We create an NSData based on our array of index sets, and pass that into the SCNGeometryElement factory method, along with some basic parameters detailing how we'd like the element to be created.
So now that we've created the points of the mesh, and the triangles that make up the mesh, we should be done, right? Almost!
Geometry: Normals
SceneKit needs some additional information besides the vertices and our SCNGeometryElement built with our triangles. In order to properly shade and light the surface of our mesh, we need to provide the normals for each vertex.The normals allow SceneKit to understand the surface of our geometry and how it should be rendered under light. For a more detailed definition on normals this is a great and concise explanation. Like our previously created vertex source, the SCNGeometrySource for our normals is created fairly simply using one of SceneKit's convenience methods: +[SCNGeometrySource geometrySourceWithNormals:count:].
(Image source: Blender 3D.)
Geometry: Texture Coordinates
Finally, if we wish to texture our terrain to make it nice and terrain-like, we're going to need to define the texture coordinates. This information allows us to flexibly control how any SCNMaterials are mapped onto our custom 3D geometry.For our purposes we just create an X,Y pair of values for each vertex. The X and Y have values in the range of 0.0 - 1.0 to define what portion of the material texture should be applied at that point.
Again, the example code handles all this with a set of reasonable default values. If you wish to create a custom source for this, you can do so in a similar manner to how we created our other sources, except you use one of the SCNGeometrySource methods which allow you to specify the SCNGeometrySourceSemanticTexcoord as the semantic for your geometry source data.
Putting It Together
Building custom geometry in SceneKit can seem daunting initially, but it's comprised of several individual components which by themselves are relatively simple. The end result in the example code is a nice flat plane of triangles. By itself, it doesn't look very interesting right now. But because with our custom-defined geometry we have granular control over each individual vertex, we can do some fun things with our mesh.Deformation
In the TerrainMesh demo app, one example of this is built into the TerrainMesh API and provides a paintbrush-style effect to raise or lower the terrain:- (void)derformTerrainAt:(CGPoint)point brushRadius:(double)brushRadius intensity:(double)intensity;
Under the hood, TerrainMesh is simply recomputing the vertices and other geometry (disclaimer: not in the most efficient way, I should add) and then updating the TerrainMesh SCNNode. The result is a flexible and morphable terrain which we can paint hills or valleys into:
Animation
If you want to animate your geometry changes, to bring your mountains and valleys to life, take a look at Apple's SCNMorpher class, which makes it very easy to do.Wrapping Up
This example was thrown together quickly for fun and for demonstration purposes, but hopefully it has shown how flexible SceneKit can be, especially when creating custom geometry.Any questions, please feel free to get in touch.