Marching Cubes Part 3: Terraforming a custom terrain mesh with marching cubes
In the previous part we introduced a simple static mesh, made by applying marching cubes to a 3D grid of weights. To make the mesh a little more interesting, let’s allow our users to interact with it by clicking somewhere on the mesh and lowering or raising the mesh inside a radius.
1. Interactivity and Collision
1.1 Casting a ray from the camera
We will start with the ability to click on our mesh and displaying a brush with a given radius at the precise location that was clicked. To do this, create a new C# script, call it TerraformingCamera. Attach the newly created script to the main camera in the scene hierarchy.
The terraformingCamera is responsible for casting a ray from the mouse onto the mesh.
Start by defining 2 private fields: hit point and camera.
On awake we get and assign the reference to our camera.
To interact with the mesh, we have to shoot a ray from the mouse position towards the mesh. We can use Physics.Raycast for this.
Create a new method called terraform. This method requires a bool parameter called “add”. This parameter is responsible for telling us whether to raise/add to the terrain or lower/substract from it.
The above code casts a ray from the mouse into the scene. When a collider is hit, we get the Chunk monobehaviour from the hit gameobject.
Note: Currently there is only a single type of collider we can hit, that being the chunk. In case you intent to have multiple non-chunk colliders in your scene, it would be best to specify a layermask for your raycast.
We also set the _hitPoint, this we can use to visualise where we hit our mesh.
To shoot this ray and start terraforming, call the Terraform method inside of the update method. When pressing the left mouse button, we want to raise the mesh. Right mouse will lower it.
To finish our terraforming script, create a new public field, “BrushSize”. The brush size is responsible for telling that what we want to terraform (chunk) how large the radius to terraform must be.
To visualise where we hit and what the radius of our hit is, draw a wiresphere at the hitpoint with radius brushsize.
Make sure Gizmos is enabled inside the scene viewport!
1.1 Collision detection when casting a ray
If we now start the game, we see that, even if we click on the mesh: nothing happens. This is because we have not yet enabled collision on our mesh. To do this we have to add a mesh collider to the Chunk object in the scene hierarchy.
Now, open the Chunk script. Create a new field for the MeshCollider.
Don’t forget to assign the mesh collider in the scene view to the Chunk component.
Let’s rewrite some of our code. Whenever we call the ConstructMesh method, we create a new mesh, instead we are going to reuse the mesh each time we call the method.
To do this, once again, create a Mesh field and initialize it on start.
In the CreateMeshFromTriangles, instead of creating a mesh, clear our mesh field and assign the vertices and triangles to this field.
Create a new method called UpdateMesh, this method is responsible for recreating the mesh and assigning the newly created mesh to the collider and filter.
Call UpdateMesh on start.
When entering play mode, we should now be able to see clicked positions printed to the console, aswell as a yellow sphere on the mesh (make sure Gizmos is enabled).
2.1 Update weights in the chunk
The only thing now left to do is ofcourse actually raising and lowering terrain.
Inside the chunk script add a new method, EditWeights.
This method accepts a position, a brush size and a boolean add.
The hit position is the point we hit the mesh with our mouse, this is where we want to edit the mesh. The brush size is simply the radius we want to edit (in other words, we have a spherical brush that is the same as being drawn on screen by the OnDrawGizmos inside TerraformingCamera). And the “add” tells us whether to lower or raise the mesh.
The idea behind lowering and raising is simple: all we need to do is increase or decrease our weights (stored in the _weights property inside the chunk). In doing so, the new weights will be either above the iso threshold or below it. If you are unsure what an iso threshold is, have a look at the first part: Understanding the algorithm
We are going to reuse the marching cubes compute shader to edit the weights. So we are going to need a new kernel to pass data to and dispatch to. For now, assume there is a kernel (≈ method) called “UpdateWeights”.
We can find the index of the kernel by using the .FindKernel(“UpdateWeights”) method. This returns an int.
Before moving on, we should also do this for the ConstructMesh method, this will avoid us headaches if we ever without knowing change the indexes of the kernels.
To update the weights, the compute shader should be aware of a few things, namely: the current weights, the size of a chunk, where inside the chunk we are updating and how large the radius we are updating in is, whether to substract or add to the terrain.
Start by placing the current weights onto the weightsbuffer that we created last part. And binding the buffer to the UpdateWeights kernel.
Now, we set the other parameters.
Not to forget, we need to tell the shader to add or substract from the terrain. In this case I opted to use a very simple method of simple passing a single float: -1f or 1f. Every grid points weight that is inside of the brush will be increased or decreased with this “TerraformStrength” value.
Dispatch the shader like we did last part and take the now updated weights from the weights buffer. Update the mesh with our new weights.
2.2 Updating weights via compute shader
Now that the compute shader is dispatched, let’s actually implement it. Open the MarchingCubesCompute.compute. Create a new kernel called “UpdateWeights”.
Below the includes, add the parameters we passed along from inside the C# chunk script.
At the bottom of the compute file, create the UpdateWeights kernel method.
Just like in the March method, first check if the id is actually within the valid chunk size range.
Next, we want to check if the distance between the hit position and the position we are currently evaluating in the compute shader execution (which is defined by the id parameter) is inside the radius of our brush.
For this, we use the already built-in function: distance. If the distance is smaller or equal to the brush size, we are currently editing a weight inside the brush, and so we have to add or substract from the weight.
2.3 Finishing touch
Return to the TerraformingCamera and call the EditWeights function.
That is it! Return to Unity and start the game. You should now be able edit the terrain by clicking on it.