AlexStv

Unity voxel block tutorial pt. 2

Welcome back! Last time we made functions to set up the mesh with vertices and triangles. In this part we'll add textures and collision meshes to our blocks. We'll start with textures, first of all we need a tile sheet to use for textures. Since we have many blocks on the same mesh the mesh needs one image with all the textures.

I'll be using this tilesheet in the tutorial. Feel free to use it as well non-comercially.

You can use your own if you want or save this image to the project's assets folder.

Open up the import settings for this texture because we'll need to change them a bit, Set the texture type to advanced, disable all the options down to wrap mode, set that to repeat and set filter mode to point. This will stop it from filtering the texture and blurring the lines between pixels which causes bleeding between tiles where you can see the edge of the tile next to the one a block is supposed to be using. Then set the max size large enough to accommodate your texture and automatic compressed should work fine.

Take the chunk game object we created in the last part by putting the chunk script on an empty game object. Now add the tilesheet texture to the chunk by dragging the texture onto it (make sure it appears added in the inspector). Now the chunk will use this texture but we still need to assign uv coordinates for each face and apply them to the mesh.

Start with the block script where first of all we'll give it a function that can easily be overridden to change the texture.

We'll create a struct for tile positions first:

     public struct Tile { public int x; public int y;}

And then a function to return the tile positions based on direction:

     public virtual Tile TexturePosition(Direction direction)
     {
         Tile tile = new Tile();
         tile.x = 0;
         tile.y = 0;
         return tile;
     }

This function will let us use the direction of the face and return which tile to use. This function always returns 0,0 since it's the base class but later we'll create a block with more interesting texturing. First though we'll use the positions to create uv coordinates for the mesh with a new function and add a new variable:

variable:

     const float tileSize = 0.25f;

function:

     public virtual Vector2[] FaceUVs(Direction direction)
     {
         Vector2[] UVs = new Vector2[4];
         Tile tilePos = TexturePosition(direction);
         UVs[0] = new Vector2(tileSize * tilePos.x + tileSize,
             tileSize * tilePos.y);
         UVs[1] = new Vector2(tileSize * tilePos.x + tileSize,
             tileSize * tilePos.y + tileSize);
         UVs[2] = new Vector2(tileSize * tilePos.x,
             tileSize * tilePos.y + tileSize);
         UVs[3] = new Vector2(tileSize * tilePos.x,
             tileSize * tilePos.y);
         return UVs;
     } 

The variable tileSize is equal to 1 divided by the number of tiles per side, in the case of our example texture 1/4 (0.25). The function creates an array of vector2 and gets the tile position with the direction and populates the array based on the tile position. Every UV coordinate corresponds to a vertex and the triangle made of those vertices uses the corresponding texture coordinates.

Now we have to call the function for every face adding the resulting UV coordinates to the mesh data so for every FaceData function add the resulting uv array to the mesh data uv list:

     protected virtual MeshData FaceDataUp
         (Chunk chunk, int x, int y, int z, MeshData meshData)
     {
         meshData.vertices.Add(new Vector3(x - 0.5f, y + 0.5f, z + 0.5f));
         meshData.vertices.Add(new Vector3(x + 0.5f, y + 0.5f, z + 0.5f));
         meshData.vertices.Add(new Vector3(x + 0.5f, y + 0.5f, z - 0.5f));
         meshData.vertices.Add(new Vector3(x - 0.5f, y + 0.5f, z - 0.5f));
         meshData.AddQuadTriangles();
         //Add the following line to every FaceData function with the direction of the face
         meshData.uv.AddRange(FaceUVs(Direction.up));
         return meshData;
     }

Lastly use the uv list in the chunk to create the mesh:

     // Sends the calculated mesh information
     // to the mesh and collision components
     void RenderMesh(MeshData meshData)
     {
         filter.mesh.Clear();
         filter.mesh.vertices = meshData.vertices.ToArray();
         filter.mesh.triangles = meshData.triangles.ToArray();
         //Add the following two lines
         filter.mesh.uv = meshData.uv.ToArray();
         filter.mesh.RecalculateNormals();
     }

Firstly just send the uvs to the mesh and then we need to recalculate the normals of our new mesh or all the lighting will be off.

Now we can test our code in unity, if you're using the tiles I provided the base block should show up as stone!

It doesn't seem super impressive but let's take a crack at another block type. Grass blocks often have a texture for the sides, top and bottom so we'll try that. Create a new script and call it BlockGrass and we'll override the TexturePositions function.

using UnityEngine;
using System.Collections;
public class BlockGrass : Block
{
     public BlockGrass()
         : base()
     {
     }
     public override Tile TexturePosition(Direction direction)
     {
         Tile tile = new Tile();
         switch (direction)
         {
             case Direction.up:
                 tile.x = 2;
                 tile.y = 0;
                 return tile;
             case Direction.down:
                 tile.x = 1;
                 tile.y = 0;
                 return tile;
         }
         tile.x = 3;
         tile.y = 0;
         return tile;
     }
}

The up and down faces return specific tiles and the rest return another. This gives us a simple way to retexture a block for a new bock type. Lets see it in game, add this line just before update chunk in the chunk's Update function:

         blocks[4, 5, 2] = new BlockGrass();

And you'll see this:

Collision mesh

Now let's take a look at generating the collision mesh, the last voxel tutorial I wrote used the same mesh for rendering as for the collision mesh. This works for the standard cube voxel but a lot of games end up having more and more complex blocks where you would prefer to have a more simplified collision mesh or none at all.

Updating the mesh the collision mesh is one of the longest factors in chunk update time, because of this it's smart to keep your collision mesh as simple as possible. It is also possible (if you don't intend to use unity physics) to script your own collisions without a mesh. Here's a good explanation of a technique you could use for your own collisions: Stack Overflow

Because most of the time the collision mesh will match the render mesh the first thing we'll do is make an easy way to use the same data for both. We'll do this by copying all additions to vertices and triangles in the mesh data object.

First add a new boolean to MeshData:

     public bool useRenderDataForCol;

When this is true all triangles and vertices added to the render mesh get added to the collision mesh as well.

Then we'll change the existing AddQuadTriangles function:

     public void AddQuadTriangles()
     {
         triangles.Add(vertices.Count - 4);
         triangles.Add(vertices.Count - 3);
         triangles.Add(vertices.Count - 2);
         triangles.Add(vertices.Count - 4);
         triangles.Add(vertices.Count - 2);
         triangles.Add(vertices.Count - 1);
         if (useRenderDataForCol)
         {
             colTriangles.Add(colVertices.Count - 4);
             colTriangles.Add(colVertices.Count - 3);
             colTriangles.Add(colVertices.Count - 2);
             colTriangles.Add(colVertices.Count - 4);
             colTriangles.Add(colVertices.Count - 2);
             colTriangles.Add(colVertices.Count - 1);
         }
     }

This way we carry out the same function but for the collision mesh when useRenderDataForCol is true but we use colVertices.Count as the offset for the collision list.

Next add a function to add to MeshData:

     public void AddVertex(Vector3 vertex)
     {
         vertices.Add(vertex);
         if (useRenderDataForCol)
         {
             colVertices.Add(vertex);
         }
     }

Currently over in the block script we're adding to the vertices list directly so we'll have to change over to calling this function to add vertices. Go to the block class and change all the lines like this:

         meshData.vertices.Add(new Vector3(...));

to this:

         meshData.AddVertex(new Vector3(...));

This way they're added to vertices and added to colVertices if useRenderDataForCol is true.

One last function we need in the MeshData script adds to the triangles list. Although right now we're only using AddQuadTriangles() we might need the functionality later:

     public void AddTriangle(int tri)
     {
         triangles.Add(tri);
         if (useRenderDataForCol)
         {
             colTriangles.Add(tri - (vertices.Count - colVertices.Count));
         }
     }

Firstly it just adds to the triangle list, then if useRenderDataForCol is true we add it to the colTriangles list but we need to adjust the value by the difference between the count of the vertices and colVertices lists since the triangles list entries correspond to indexes in the vertices lists we have to adjust their values to match the differences in the lists.

Now that we have the capability to add to both lists we have to enable it by setting useRenderDataForCol to true at the start of the BlockData function in the block script:

 public virtual MeshData BlockData(Chunk chunk, int x, int y, int z, MeshData meshData){
         meshData.useRenderDataForCol = true;
         ...

Remember to set this to true or false at the start BlockData whenever you override it or else you'll continue using the setting of the last block.

That should give you valid collision mesh data in the meshData class. Now we have to apply it to out Mesh Collider component:

     void RenderMesh(MeshData meshData)
     {
         filter.mesh.Clear();
         filter.mesh.vertices = meshData.vertices.ToArray();
         filter.mesh.triangles = meshData.triangles.ToArray();
         filter.mesh.uv = meshData.uv.ToArray();
         filter.mesh.RecalculateNormals();
 
         //additions:
         coll.sharedMesh = null;
         Mesh mesh = new Mesh();
         mesh.vertices = meshData.colVertices.ToArray();
         mesh.triangles = meshData.colTriangles.ToArray();
         mesh.RecalculateNormals();
 
         coll.sharedMesh = mesh;
     }

The additions to the RenderMesh function remove the current collider mesh and then create a new mesh to apply the vertices and triangles to and then use the new mesh as the collision mesh. Running that in unity should give you a collision mesh exactly where your rendered mesh is. You can see that it's working by enabling Gizmos > MeshCollider and disabling then enabling the collider. The collider should show up as a green wireframe when the chunk is selected right on top of the blue outline that was there. You should see the green wireframe turn on and off by disabling and enabling the mesh collider component.

[image]../../images/voxcol.png[/image]

If your project isn't working at this point try downloading the scripts from this tutorial below and see if yours match. If you can't solve it then post below with your problem and any errors you're receiving. If you find any errors in my tutorial or have suggestions I would be grateful to hear them as well.

Tutorial part 1Download scripts so far Tutorial part 3

26th Dec, 2014
Alex