Unreal Engine 4 Voxel Terrain Tutorial Part 5: Using Multiple Voxel Materials

After quite a long wait, we’re finally back with part 5 of the UE4 Voxel Terrain series! Today we’ll be adding support for multiple voxel materials like stone, dirt, and grass. In the process we’ll also learn how to add the new materials to our terrain generation, which could be extended to add almost any material you want. You could even extend it to easily add caves!

Before we get started you’re going to want to grab the latest version of the tutorial assets from either the project’s GitHub or this link. For this tutorial I’ve added new textures for stone and ore, because we needed something besides dirt and grass to properly show this off. Once you have the new assets, it’s time to begin the tutorial.

Adding Support For Multiple Materials

Before we can even think about how we want to generate our new voxels, we need to make it so the engine can render them. To do this we need to move away from the single TerrainMaterial variable, and replace it with an array of materials. After that, we need to tell our mesh extraction function how to handle the new materials. This is simpler than it sounds, so let’s get started.

Header File

There isn’t much to do here, because all we’re doing is replacing a single member with a new one. Find the TerrainMaterial member in your AVoxelTerrainActor class, it should look something like this:

	// The material to apply to our voxel terrain
	UPROPERTY(Category = "Voxel Terrain", BlueprintReadWrite, EditAnywhere) UMaterialInterface* TerrainMaterial;

Once you find it, go ahead and replace it with this new member:

	// The material to apply to our voxel terrain
	UPROPERTY(Category = "Voxel Terrain", BlueprintReadWrite, EditAnywhere) TArray<UMaterialInterface*> TerrainMaterials;

This will allow us to have a list of many materials instead of only a single material like we had before. Like I said though, not much to do in this file. On to the next one!

Implementation File

The only code that we need to change in this file is inside the AVoxelTerrainActor::BeginPlay() function. What we need to do is fairly simple, but please don’t take this as the best way to handle this. To give a quick overview before we look at the changes though: we need to create a new mesh section for each material we want to apply to our terrain. We need to create multiple mesh sections because each section can only have a single material. The way I’ve decided to do this for the tutorial series is simple: we just loop over the code that we already had in the function from earlier tutorials (With a few modifications!) for each material we would like to use. This isn’t the best solution because we end up looping over a potentially large set of data multiple times, instead of looping over it only once like you would with other methods.

The reason I decided to go with this code is simple though: I’m not trying to make the most optimized code ever, I’m trying to teach you the basics so you can build your own system using what you learned. Because of that reason, I prefer the easier to understand and implement examples over the more optimized ones. Anyways, let’s see the code shall we? As usual I’ll give you the entire function and then go over the changes afterwards.

// Called when the actor has begun playing in the level
void AVoxelTerrainActor::BeginPlay()
{
	// Extract the voxel mesh from PolyVox
	PolyVox::Region ToExtract(Vector3DInt32(0, 0, 0), Vector3DInt32(127, 127, 63));
	auto ExtractedMesh = extractCubicMesh(VoxelVolume.Get(), ToExtract);
	auto DecodedMesh = decodeMesh(ExtractedMesh);

	// This isn't the most efficient way to handle this, but it works.
	// To improve the performance of this code, you'll want to modify 
	// the code so that you only run this section of code once.
	for (int Material = 0; Material < TerrainMaterials.Num; Material++)
	{
		// Define variables to pass into the CreateMeshSection function
		auto Vertices = TArray<FVector>();
		auto Indices = TArray<int32>();
		auto Normals = TArray<FVector>();
		auto UV0 = TArray<FVector2D>();
		auto Colors = TArray<FColor>();
		auto Tangents = TArray<FProcMeshTangent>();

		// Loop over all of the triangle vertex indices
		for (uint32 i = 0; i < DecodedMesh.getNoOfIndices() - 2; i += 3)
		{
			// We need to add the vertices of each triangle in reverse or the mesh will be upside down
			auto Index = DecodedMesh.getIndex(i + 2);
			auto Vertex2 = DecodedMesh.getVertex(Index);
			auto TriangleMaterial = Vertex2.data.getMaterial();

			// Before we continue, we need to be sure that the triangle is the right material; we don't want to use verticies from other materials
			if (TriangleMaterial == (Material + 1))
			{
				// If it is of the same material, then we need to add the correct indices now
				Indices.Add(Vertices.Add(FPolyVoxVector(Vertex2.position) * 100.f));

				Index = DecodedMesh.getIndex(i + 1);
				auto Vertex1 = DecodedMesh.getVertex(Index);
				Indices.Add(Vertices.Add(FPolyVoxVector(Vertex1.position) * 100.f));

				Index = DecodedMesh.getIndex(i);
				auto Vertex0 = DecodedMesh.getVertex(Index);
				Indices.Add(Vertices.Add(FPolyVoxVector(Vertex0.position) * 100.f));

				// Calculate the tangents of our triangle
				const FVector Edge01 = FPolyVoxVector(Vertex1.position - Vertex0.position);
				const FVector Edge02 = FPolyVoxVector(Vertex2.position - Vertex0.position);

				const FVector TangentX = Edge01.GetSafeNormal();
				FVector TangentZ = (Edge01 ^ Edge02).GetSafeNormal();

				for (int32 i = 0; i < 3; i++)
				{
					Tangents.Add(FProcMeshTangent(TangentX, false));
					Normals.Add(TangentZ);
				}
			}
		}

		// Finally create the mesh
		Mesh->CreateMeshSection(Material, Vertices, Indices, Normals, UV0, Colors, Tangents, true);
		Mesh->SetMaterial(Material, TerrainMaterials[Material]);
	}
}

As you can see the changes are pretty small, and for the most part should be self-explanatory after the overview of what I changed. I’ll go over everything in detail anyways though, just in case you missed a change or didn’t understand why I changed something.

	// Extract the voxel mesh from PolyVox
	PolyVox::Region ToExtract(Vector3DInt32(0, 0, 0), Vector3DInt32(127, 127, 63));
	auto ExtractedMesh = extractCubicMesh(VoxelVolume.Get(), ToExtract);
	auto DecodedMesh = decodeMesh(ExtractedMesh);

	// This isn't the most efficient way to handle this, but it works.
	// To improve the performance of this code, you'll want to modify 
	// the code so that you only run this section of code once.
	for (int Material = 0; Material < TerrainMaterials.Num; Material++)
	{
            // ...
	}

First up you’ll see that I’ve wrapped the entirety of the mesh creation inside of a for loop, which loops over all of the materials in the TerrainMaterials array. As I said before, we need to do this because each mesh section can only have a single material assigned to it, so we need multiple mesh sections.

		// Loop over all of the triangle vertex indices
		for (uint32 i = 0; i < DecodedMesh.getNoOfIndices() - 2; i += 3)
		{
			// We need to add the vertices of each triangle in reverse or the mesh will be upside down
			auto Index = DecodedMesh.getIndex(i + 2);
			auto Vertex2 = DecodedMesh.getVertex(Index);
			auto TriangleMaterial = Vertex2.data.getMaterial();

			// Before we continue, we need to be sure that the triangle is the right material; we don't want to use verticies from other materials
			if (TriangleMaterial == (Material + 1))
			{
				// If it is of the same material, then we need to add the correct indices now
				Indices.Add(Vertices.Add(FPolyVoxVector(Vertex2.position) * 100.f));

				// ...
			}
		}

Inside of the next loop you can see that I’ve wrapped the bulk of the code in an if statement, this checks to make sure that we only add triangles that have the correct material assigned to them into the mesh section. To do this it uses the decoded data from the first vertex of the triangle, which contains the MaterialDensityPair44 that corresponds to the voxel that the triangle represents, and therefore also contains the material of the voxel. If the material id of the voxel is the same as the iterator of the first loop then it adds the triangle to the new mesh section, otherwise it does nothing and just continues the loop looking for valid triangles.

		// Finally create the mesh
		Mesh->CreateMeshSection(Material, Vertices, Indices, Normals, UV0, Colors, Tangents, true);
		Mesh->SetMaterial(Material, TerrainMaterials[Material]);

And then at the end I’ve changed the mesh section creation code so it uses the material index as the mesh section index. I’ve also changed the Mesh->SetMaterial call so that it uses the correct material from our TerrainMaterials array from earlier. That wraps up the changes to this function.

Generating The New Materials

With those changes in place you’re now setup to apply multiple materials to your terrain. Now I’m going to give you an example of a terrain generation function that makes use of the changes we made above. You’ll probably want to expand on my examples to add extra materials and other features, such as foliage, biomes, or anything else that comes to mind. All of the changes in this section take place in the VoxelTerrainPager::pageIn function, so go and find that in your implementation file. Here is the entire function, as usual there will be a breakdown afterwards.

// Called when a new chunk is paged in
// This function will automatically generate our voxel-based terrain from simplex noise
void VoxelTerrainPager::pageIn(const PolyVox::Region& region, PagedVolume<MaterialDensityPair44>::Chunk* Chunk)
{
	// This is our kernel. It is responsible for generating our noise.
	CKernel NoiseKernel;

	// Commonly used constants
	auto Zero = NoiseKernel.constant(0);
	auto One = NoiseKernel.constant(1);
	auto VerticalHeight = NoiseKernel.constant(TerrainHeight);
	auto HalfVerticalHeight = NoiseKernel.constant(TerrainHeight / 2.f);

	// Create a gradient on the vertical axis to form our ground plane.
	auto VerticalGradient = NoiseKernel.divide(NoiseKernel.clamp(NoiseKernel.subtract(VerticalHeight, NoiseKernel.z()), Zero, VerticalHeight), VerticalHeight);

	// Turn our gradient into two solids that represent the ground and air. This prevents floating terrain from forming later.
	auto VerticalSelect = NoiseKernel.select(Zero, One, VerticalGradient, NoiseKernel.constant(0.5), Zero);

	// This is the actual noise generator we'll be using.
	// In this case I've gone with a simple fBm generator, which will create terrain that looks like smooth, rolling hills.
	auto TerrainFractal = NoiseKernel.simplefBm(BasisTypes::BASIS_SIMPLEX, InterpolationTypes::INTERP_LINEAR, NoiseOctaves, NoiseFrequency, Seed);

	// Scale and offset the generated noise value. 
	// Scaling the noise makes the features bigger or smaller, and offsetting it will move the terrain up and down.
	auto TerrainScale = NoiseKernel.scaleOffset(TerrainFractal, NoiseScale, NoiseOffset);

	// Setting the Z scale of the fractal to 0 will effectively turn the fractal into a heightmap.
	auto TerrainZScale = NoiseKernel.scaleZ(TerrainScale, Zero);

	// Finally, apply the Z offset we just calculated from the fractal to our ground plane.
	auto PerturbGradient = NoiseKernel.translateZ(VerticalSelect, TerrainZScale);

	// Now we want to determine different materials based on a variety of factors.
	// This is made easier by the fact that we're basically generating a heightmap.

	// For now our grass is always going to appear at the top level, so we don't need to do anything fancy.
	auto GrassZ = NoiseKernel.subtract(HalfVerticalHeight, TerrainZScale);

	// To generate pockets of ore we're going to need another noise generator.
	auto OreFractal = NoiseKernel.simpleRidgedMultifractal(BasisTypes::BASIS_SIMPLEX, InterpolationTypes::INTERP_LINEAR, 2, 5 * NoiseFrequency, Seed);

	CNoiseExecutor TerrainExecutor(NoiseKernel);

	// Now that we have our noise setup, let's loop over our chunk and apply it.
	for (int x = region.getLowerX(); x <= region.getUpperX(); x++)
	{
		for (int y = region.getLowerY(); y <= region.getUpperY(); y++)
		{
			for (int z = region.getLowerZ(); z <= region.getUpperZ(); z++)
			{
				// Evaluate the noise
				auto EvaluatedNoise = TerrainExecutor.evaluateScalar(x, y, z, PerturbGradient);
				MaterialDensityPair44 Voxel;

				bool bSolid = EvaluatedNoise > 0.5;
				Voxel.setDensity(bSolid ? 255 : 0);

				// Determine what material should be set on the voxel
				// Air = 0
				// Stone = 1
				// Dirt = 2
				// Grass = 3
				// Ore = 4

				int ActualGrassZ = FMath::FloorToInt(TerrainExecutor.evaluateScalar(x, y, z, GrassZ));
				int DirtZ = ActualGrassZ - 1;
				int DirtThickness = 3;

				if (bSolid)
				{
					if (z >= ActualGrassZ)
					{
						Voxel.setMaterial(3);
					}
					else if (z <= DirtZ && z > (DirtZ - DirtThickness))
					{
						Voxel.setMaterial(2);
					}
					else
					{
						auto EvaluatedOreFractal = TerrainExecutor.evaluateScalar(x, y, z, OreFractal);

						if (EvaluatedOreFractal > 1.95)
							Voxel.setMaterial(4);
						else
							Voxel.setMaterial(1);
					}
				}
				else
				{
					Voxel.setMaterial(0);
				}

				// Voxel position within a chunk always start from zero. So if a chunk represents region (4, 8, 12) to (11, 19, 15)
				// then the valid chunk voxels are from (0, 0, 0) to (7, 11, 3). Hence we subtract the lower corner position of the
				// region from the volume space position in order to get the chunk space position.
				Chunk->setVoxel(x - region.getLowerX(), y - region.getLowerY(), z - region.getLowerZ(), Voxel);
			}
		}
	}
}

There are two main additions in this code, the first being a new noise generator and the second being an if statement that decides what material the generated voxels will use. Let’s look at the new code in detail:

auto HalfVerticalHeight = NoiseKernel.constant(TerrainHeight / 2.f);

Up first we have a new constant, which is set to half of the maximum terrain height. While not strictly required, it is going to be useful for calculating the distance from the surface.

	// Now we want to determine different materials based on a variety of factors.
	// This is made easier by the fact that we're basically generating a heightmap.

	// For now our grass is always going to appear at the top level, so we don't need to do anything fancy.
	auto GrassZ = NoiseKernel.subtract(HalfVerticalHeight, TerrainZScale);

	// To generate pockets of ore we're going to need another noise generator.
	auto OreFractal = NoiseKernel.simpleRidgedMultifractal(BasisTypes::BASIS_SIMPLEX, InterpolationTypes::INTERP_LINEAR, 2, 5 * NoiseFrequency, Seed);

The next chunk of code that I’ve added defines a few new variables that we need to figure out where we should put different materials. The GrassZ variable uses the heightmap-like noise that we use to translate our ground plane to figure out what blocks should be grass. It is also used to place three blocks of dirt under the grass. Then there is the OreFractal variable, this one defines another fractal noise generator for us to use. In the next section of code this variable is used to place veins of ore throughout the terrain.

				// Determine what material should be set on the voxel
				// Air = 0
				// Stone = 1
				// Dirt = 2
				// Grass = 3
				// Ore = 4

				int ActualGrassZ = FMath::FloorToInt(TerrainExecutor.evaluateScalar(x, y, z, GrassZ));
				int DirtZ = ActualGrassZ - 1;
				int DirtThickness = 3;

				if (bSolid)
				{
					if (z >= ActualGrassZ)
					{
						Voxel.setMaterial(3);
					}
					else if (z <= DirtZ && z > (DirtZ - DirtThickness))
					{
						Voxel.setMaterial(2);
					}
					else
					{
						auto EvaluatedOreFractal = TerrainExecutor.evaluateScalar(x, y, z, OreFractal);

						if (EvaluatedOreFractal > 1.95)
							Voxel.setMaterial(4);
						else
							Voxel.setMaterial(1);
					}
				}
				else
				{
					Voxel.setMaterial(0);
				}

Finally we have this section of code, which is what actually applies the materials to the terrain. In short, this code checks several values, and based on those checks it chooses a material for the voxel that it’s working with. Because this is something of a lengthy addition, and I know some of the people following this tutorial series don’t do much programming, I’m going to break it up and explain each part.

				// Determine what material should be set on the voxel
				// Air = 0
				// Stone = 1
				// Dirt = 2
				// Grass = 3
				// Ore = 4

				int ActualGrassZ = FMath::FloorToInt(TerrainExecutor.evaluateScalar(x, y, z, GrassZ));
				int DirtZ = ActualGrassZ - 1;
				int DirtThickness = 3;

The first thing we do in this code is prepare a couple of variables to make our code shorter. The first variable, ActualGrassZ, uses the VM to evaluate GrassZ instruction we created earlier to figure out where we should place grass. The second variable, DirtZ, takes that first variable and subtracts 1 from it. We subtract 1 because we want the dirt layer to begin on the next layer of voxels after the grass. The third variable, DirtThickness, is used later to tell the terrain generator how thick we want our dirt layer to be.

				if (bSolid)
				{
					// ...
				}
				else
				{
					Voxel.setMaterial(0);
				}

Next up we have this if statement, which has the same functionality as the material-determining code that we wrote in Part 2 of the tutorial series. That functionality being: if the voxel we’re working with is solid, then we should give it a material, but if it’s not solid then the material should be set to 0 (air).

					if (z >= ActualGrassZ)
					{
						Voxel.setMaterial(3);
					}
					else if (z <= DirtZ && z > (DirtZ - DirtThickness))
					{
						Voxel.setMaterial(2);
					}
					else
					{
						// ...
					}

Then we have this section, which determines what material the voxel should have if it was solid. First it checks if the current Z position is greater than or equal to the grass level, and sets the material to 3 (grass) if it is. If that wasn’t the case, it then checks if the Z position is within the range of our dirt layer. If it is, then it sets the material to 2 (dirt), but otherwise it moves on to the final section of code.

						auto EvaluatedOreFractal = TerrainExecutor.evaluateScalar(x, y, z, OreFractal);

						if (EvaluatedOreFractal > 1.95)
							Voxel.setMaterial(4);
						else
							Voxel.setMaterial(1);

Finally, if the voxel wasn’t dirt or grass, we decide if the voxel should be stone or ore. To do this we evaluate the OreFractal instruction we created earlier, and check it’s value at the current position. If the value is greater than 1.95 then we set the material to 4 (ore), but if it isn’t, then we set the material to 1 (stone). The reason we check if the value is greater than 1.95, instead of something like 0.95, is simple: not all of the fractals have values in the same range. The value of the simpleRidgedMultifractal that we used for our new fractal is usually in the range of 1 to 2, but it can go outside of that range!

So It Compiles, But…

If you were to add the changes above and compile your code, you would probably not notice any difference at all when you play your level. This is because our mesh extractor only shows what it thinks will be visible, you’re not usually going to be seeing all of the blocks underground unless there is a cave or some type of cliff, so it doesn’t create a mesh for the underground voxels. Thankfully, there are a few things we can do about this!

The easiest way to check if the code is working is to simply slice the terrain up so you can see the entire vertical section of the terrain. To do this go to your VoxelTerrainPager::pageIn again, and find the section that loops over the chunk. Find the following line of code:

for (int x = region.getLowerX(); x <= region.getUpperX(); x++)

You’ll want to change this to look something like this:

for (int x = region.getLowerX(); x <= region.getUpperX() / 2; x++)

If you compile that change and go back into the engine, you should now see that your terrain has had a slice cut off of it, revealing the entire vertical layer. If you’re seeing a layer of grass, followed by dirt, and then a large layer of stone and ore, then you’ll know that your code is working. With that we’ve completed this part of the tutorial, but stick around, because in the next part we’re going to cover “infinite” terrain generation like what you see in Minecraft.

In addition there will be a part 5.5, which will contain some examples of different variations of the terrain generator that you can copy into your project to get a new look. So even if you’re not interested in the “infinite” terrain generation, hopefully you’ll get something out of this. See you in the next part!

24 comments on “Unreal Engine 4 Voxel Terrain Tutorial Part 5: Using Multiple Voxel MaterialsAdd yours →

  1. Hello, this code doesn’t really compile for me. I had to make a small change to get it going.

    from
    for (int Material = 0; Material < TerrainMaterials.Num; Material++)

    to
    for (int Material = 0; Material < TerrainMaterials.Num(); Material++)

    1. That would be the correct code, I really have no idea why it was compiling fine on my end before I published this tutorial. I’ll make sure the post gets updated at some point (I’ve been super busy lately, haven’t had much time to read comments).

  2. Great work on the tutorial so far. It’s so hard to find information about voxel engines. Everything worked fine for me except part 5 of this tutorial. When loading it in the editor i get random crashes with access code violations. I tried using your files from github and had the same results. I tried many variations, including UE 4.13, and the latest version of polyvox. It just keeps crashing. Any ideas?

    1. I think I fixed the bug! The last line of code in the tutorial is what breaks the program. This line causes problems because every voxel needs to be initialized (setVoxel).

      ” for (int x = region.getLowerX(); x <= region.getUpperX() / 2; x++) "

      So instead of that, we can do a check on line 180 of VoxelTerrainActor.cpp,

      if (bSolid && x < region.getUpperX() )

      This seems to fix it! we can also see where each chunk ends, which is pretty neat.

  3. I have successfully gotten everything to build. Dragged the Voxel Terrain Actor from the class list in Unreal Editor into onto the map, where all the properties then show up on the right. However, I cannot get it to actually generate and show the voxel terrain. If I click “Play”, I just get a blank map. If I click “Launch”, UE goes through all the building and validating, and then the launched executable promptly crashes without displaying anything. I am using UE 4.13 and Visual Studio 2015. I’m very new to UE, so it is possible I am doing something horribly wrong, but I’m fairly sure I followed through the tutorial precisely. I even downloaded the code from you GIT repository to validate that everything I had done matched. I can post the output log from the crash if it would be helpful, but don’t want to clutter up your comments with something that long unless you want it.

  4. I have not taken time to read this tutorial series because I would like to ask somthing before I do. Is it possible to take this same voxel terrain method and turn it into a sort of poly gon looking terrain? I want to make somthing like this video here https://www.youtube.com/watch?v=syDXn-Ig6U8. With the same destruction looking thing.

  5. Excellent material thus far, thank you kindly for putting this out there. I am left wondering how to get the pageOut method to fire for something like serialization. Right now I have hacked in serialization, but I’d prefer to do it the idiomatic way if possible.

  6. I’ve experienced a few crashes and I think it’s related to reusing the i variable in two loops, here and here:

    // Loop over all of the triangle vertex indices
    for (uint32 i = 0; i < DecodedMesh.getNoOfIndices() – 2; i += 3)

    // Calculate the tangents of our triangle

    for (int32 i = 0; i < 3; i++)

    By changing the inner loop variable name to n the crashes stopped.

  7. Hello,

    I followed the tutorials and I fixed a lot of problems but when I click on play the field does not materialize.

    Could you help me ?

    1. Hello,

      I know it may be a little late but i had the same problem as you. The problem was that i forgot to add materials in the VoxelTerrainActor. I had to use the search tool to find where it was in all the variables.

      Once i added all the materials in the list (you may have to change the order of materials to have the desired output) the Terrain was generated.

      This comes from the fact that the PageIn function loops through assigned materials. Don’t know if you have the same error, but this helped me.

  8. 增大壮阳、丰乳缩阴、泡妞把妹,价值十万资料无限下。

    逆向推荐,上线替下线赚钱,什么不用干,坐收二百万。

    增大网:

    111.zhuan.in

    抓紧来!

Leave a Reply