
Last week I created the marching cubes algorithm to create a cave type terrain using a noise generator. I decided to take it a step further and allow the player to walk on this terrain and have the ability to edit it, creating their own caves and bridges.
Before I continue, the following link helped me to translate my C# code into a compute shader:
https://polycoding.net/marching-cubes/part-3/
The first step was to create a script that handles raycasting out from the camera, and detecting when we hit a mesh. I called my script 'TerrainEditor'. In the update function, I simply raycast out from the the main camera being used (player camera), and get the hit position on the mesh.
public class TerrainEditor : MonoBehaviour
{
void Update()
{
RaycastHit hit;
if (Physics.Raycast(camTransform.position, camTransform.forward, out hit, Mathf.Infinity))
{
m_isHitting = true;
hitPosition = hit.point;
currentHitObject = hit.transform.gameObject;
m_sweeper.transform.position = hitPosition;
}
else
{
m_isHitting = false;
currentHitObject = null;
}
}
}
I also added a variable called 'BrushSize', and drew a gizmo ball at the hit point of the raycast. This would act as the brush which the user could edit the terrain with.

Now that we have the hitPoint on the mesh, the nest step was to find a way to edit the points of a single chunk. To do this, I went into the 'Chunk.cs' script and added the following code, which gave bunch of information to the compute shader that controls the marching cubes algorithm.
public void EditWeights(Vector3 hitPosition, float brushSize, bool add)
{
int kernal = marchingShader.FindKernel("UpdateWeights");
_weightsBuffer.SetData(_weights);
marchingShader.SetBuffer(kernal, "_Weights", _weightsBuffer);
marchingShader.SetInt("_ChunkSize", GridMetrics.PointsPerChunk);
marchingShader.SetVector("_HitPosition", CalculateHitPosition(hitPosition));
marchingShader.SetFloat("_BrushSize", brushSize);
marchingShader.SetFloat("_TerraformingStrength", add ? 1f : -1f);
marchingShader.SetVector("_ChunkNumber", ChunkCoordinates);
marchingShader.Dispatch(kernal,
GridMetrics.PointsPerChunk / GridMetrics.NumThreads,
GridMetrics.PointsPerChunk / GridMetrics.NumThreads,
GridMetrics.PointsPerChunk / GridMetrics.NumThreads);
_weightsBuffer.GetData(_weights);
UpdateMesh();
}
Chunk Size - The size of the chunk.
Hit Position - The position that the raycaster hit.
Brush Size - Size of the brush.
Terraforming Strength - The strength which we want to add or take away from the points in the grid. (In this case, it would be 1).
Now that the shader has this information, we can now do all the calculations inside the compute shader. First, I made a new kernel and called it 'Update Weights' and gave it the same amount of threads as the march cubes kernel. The actual function is quite simple.
[numthreads(numThreads, numThreads, numThreads)]
void UpdateWeights(uint3 id : SV_DISPATCHTHREADID)
{
if (distance(id, _HitPosition) <= _BrushSize)
{
_Weights[IndexFromCoord(id.x, id.y, id.z)] += _TerraformingStrength;
}
}
And that is basically it. With some simple mouse click logic, we can now edit the points of the terrain. Although there is a slight problem... This only works for a single chunk. Meaning if I go to edit on the edge of 2 chunks, the following results happen...

Here, you can see a giant hole in the mesh, because we are only editing the currently selected mesh from the ray cast. A simple solution would be to use a sweeper and check all the mesh's inside the sweeping object, and only edit those mesh's based on the position of the hit point.
This works well, although it only works for chunks that already have a mesh generated, so it wouldn't edit the points of a chunk that is empty. An expensive but simple way of fixing this is to add a box collider to each chunk, (even the empty ones), and use the sweeper to check ALL the chunks that are within range, and edit those points.
Sweeper.cs
public class Sweeper : MonoBehaviour
{
List<Chunk> chunkList = new List<Chunk>();
List<Collider> colliderList = new List<Collider>();
public List<Chunk> GetChunks()
{
chunkList.Clear();
for (int i = 0; i < colliderList.Count; i++)
{
//NOTE: The chunk script is attachted to the parent object
Chunk chunk = colliderList[i].GetComponentInParent<Chunk>();
chunkList.Add(chunk);
}
return chunkList;
}
private void OnTriggerEnter(Collider other)
{
if (other.GetType() == typeof(BoxCollider))
{
colliderList.Add(other);
}
}
private void OnTriggerExit(Collider other)
{
colliderList.Remove(other);
}
}
In the sweeper.cs class, every time the sweeper enters a chunk, it adds it to a list, and when it exits the chunk, it removes it from the list. This way, the chunks inside the sweeping object are always available. I also made a GetChunks() function that returns all the chunks in the sweeper.
Then when I go to edit the terrain, instead of just modifying the points of the current chunk, I edit the points of all the chunks inside the sweeper, like in the code below...
chunks.Clear();
chunks = m_sweeper.GetChunks();
for (int i = 0; i < chunks.Count; i++)
{
chunks[i].EditWeights(hitPosition, brushSize, add);
}
And that is basically it. Now I can create bridges across empty spaces like in the image below...

A video of the final product...