Unity Voxel Tutorial Part 5: 3d Voxel



Hello everyone following the voxel tutorial, it's been a long time since an update. In this time I've written a new updated tutorial on voxel terrain that supports infinite terrain and saving/loading of chunks. Try it out on my new site: AlexStv.com


Ok, I took a long break since the last part. I'm pretty bad at being active especially with a lot of work to focus on but without further adieu we will start building our meshes to be viewed in 3d. This is probably what a lot of people had in mind when they started this tutorial series but I promise, with the understanding gained from the earlier tutorials this part will come a lot easier. However users that haven't followed the last 4 parts are welcome to start here but I won't be going into as much detail about how meshes are created.

Also this tutorial and the next one I'm writing as I go instead of using parts of my finished code (of course still using that as reference) so I'm hoping that will help stop me from making as many mistakes as I shouldn't skip things or have to make changes to things as I past them to the tutorial as much.

Let's start by making a new scene in unity with an empty game object. Name that object "Chunk", this will be the mesh for a 16x16x16 area (Or larger) of our world. Also make a new script and call it "Chunk" as well because it goes on the chunk object. Let's make our code create one cube to start.

First of all set up the variables:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class Chunk : MonoBehaviour {
 
 private List<Vector3> newVertices = new List<Vector3>();
    private List<int> newTriangles = new List<int>();
  private List<Vector2> newUV = new List<Vector2>();
 
 private float tUnit = 0.25f;
 private Vector2 tStone = new Vector2 (1, 0);
 private Vector2 tGrass = new Vector2 (0, 1);
 
 private Mesh mesh;
 private MeshCollider col;
 
 private int faceCount;

When you're adding these remember to include the line "using System.Collections.Generic;" from line 3 up there. Now, this should look pretty familiar, same lists of verticies, tris and UVs, a tUnit and texture coordinates, our mesh filter and mesh collider and lastly a faceCount which is just a new name for the squareCount we used in the other script.

Set up the start function to set some of our variables:
mesh = GetComponent<MeshFilter> ().mesh;
col = GetComponent<MeshCollider> ();

After you've done this go to unity and add a Mesh Filter, a Mesh Renderer and a Mesh Collider to our Chunk game object.

Now create two functions:
void CubeTop (int x, int y, int z, byte block) {
 
}

void UpdateMesh ()
{

}

And call them in the start loop:
void Start () { 
 
 mesh = GetComponent<MeshFilter> ().mesh;
 col = GetComponent<MeshCollider> ();
 
 CubeTop(0,0,0,0);
 UpdateMesh ();
}

CubeTop will be one of six functions that generate a side of the cube and update mesh will commit the verticies and things to the mesh filter. We'll just start with the top to get it working.

Because this is actually very similar to the 2d example I'll take this in some larger steps than normal now, here is what we need in the CubeTop function:
void CubeTop (int x, int y, int z, byte block) {
 
 newVertices.Add(new Vector3 (x,  y,  z + 1));
 newVertices.Add(new Vector3 (x + 1, y,  z + 1));
 newVertices.Add(new Vector3 (x + 1, y,  z ));
 newVertices.Add(new Vector3 (x,  y,  z ));
 
 newTriangles.Add(faceCount * 4  ); //1
 newTriangles.Add(faceCount * 4 + 1 ); //2
 newTriangles.Add(faceCount * 4 + 2 ); //3
 newTriangles.Add(faceCount * 4  ); //1
 newTriangles.Add(faceCount * 4 + 2 ); //3
 newTriangles.Add(faceCount * 4 + 3 ); //4
 
 Vector2 texturePos;
 
 texturePos=tStone;
 
 newUV.Add(new Vector2 (tUnit * texturePos.x + tUnit, tUnit * texturePos.y));
 newUV.Add(new Vector2 (tUnit * texturePos.x + tUnit, tUnit * texturePos.y + tUnit));
 newUV.Add(new Vector2 (tUnit * texturePos.x, tUnit * texturePos.y + tUnit));
 newUV.Add(new Vector2 (tUnit * texturePos.x, tUnit * texturePos.y));
 
}

And here is the UpdateMesh Function:
void UpdateMesh ()
{
 
 mesh.Clear ();
 mesh.vertices = newVertices.ToArray();
 mesh.uv = newUV.ToArray();
 mesh.triangles = newTriangles.ToArray();
 mesh.Optimize ();
 mesh.RecalculateNormals ();
 
 //col.sharedMesh=null;
 //col.sharedMesh=mesh;
 
 newVertices.Clear();
 newUV.Clear();
 newTriangles.Clear();
 
 faceCount=0; //Fixed: Added this thanks to a bug pointed out by ratnushock!

}

What's happening here? Well CubeTop runs first and it creates verticies for a square facing upwards using the x,y,z coordinates, then it creates numbers for the triangles using the faceCount and lastly it applies the texture at the coordinates to the face. For now though texturePos is just set to tStone. We'll add some ifs to set the texture once we have more than one cube.

Now lets hop over to unity and place our gameobjects so that we can run. Put the Chunk at 0,0,0 and set the camera's position y to 10 and rotation x to 45. This should put it dead center.

This is what you should see when you run
So as you can see there are no textures yet and it's just the top face so let's add the materials first of all. Just drag the tilesheet texture onto the chunk gameobject (for those of you who haven't done the previous tutorials, the tilesheet is a 128x128 size image with 4x4 tiles. Here's the one I'm using: Link!).

For the rest of the faces the functions will be quite similar, all we'll be doing is adjusting the verticies. Pretty much the rest of the function will be the same for every face so what we'll do is remove the common parts of the functions and put them in a separate function instead of having it written out for each face.

So create a new function called Cube with a Vector2 called texturePos as a parameter, this function will be called for every face and run all the code common to all faces. Move the newTriangles lines to it and the newUV lines to it. Then add "faceCount++" to the end.
void Cube (Vector2 texturePos) {
 
 newTriangles.Add(faceCount * 4  ); //1
 newTriangles.Add(faceCount * 4 + 1 ); //2
 newTriangles.Add(faceCount * 4 + 2 ); //3
 newTriangles.Add(faceCount * 4  ); //1
 newTriangles.Add(faceCount * 4 + 2 ); //3
 newTriangles.Add(faceCount * 4 + 3 ); //4
 
 newUV.Add(new Vector2 (tUnit * texturePos.x + tUnit, tUnit * texturePos.y));
 newUV.Add(new Vector2 (tUnit * texturePos.x + tUnit, tUnit * texturePos.y + tUnit));
 newUV.Add(new Vector2 (tUnit * texturePos.x, tUnit * texturePos.y + tUnit));
 newUV.Add(new Vector2 (tUnit * texturePos.x, tUnit * texturePos.y));
 
 faceCount++; // Add this line
}

Now your CubeTop function should be a little shorter but call Cube(texturePos); at the end of the function. We decide the texture coordinates in the function unique to each side because the Cube function doesn't have any block data to decide what texture to use and because textures might be based on which face of the cube we're making. Your CubeTop function should look like this now:
void CubeTop (int x, int y, int z, byte block) {
  
  newVertices.Add(new Vector3 (x,  y,  z + 1));
  newVertices.Add(new Vector3 (x + 1, y,  z + 1));
  newVertices.Add(new Vector3 (x + 1, y,  z ));
  newVertices.Add(new Vector3 (x,  y,  z ));
  
  Vector2 texturePos;
  
  texturePos=tStone;
  
  Cube (texturePos);
  
 }

Now we can make the other five functions, they'll look just the same as this one except that they'll use different coordinates for the verticies. Later on they will also decide what textures to use in the unique functions but for now just keep using texturePos=tStone.

Now create 5 new functions with the same content as CubeTop called CubeNorth, CubeEast, CubeSouth, CubeWest and CubeBot but change out the newVerticies lines with these:
//CubeNorth
newVertices.Add(new Vector3 (x + 1, y-1, z + 1));
newVertices.Add(new Vector3 (x + 1, y, z + 1));
newVertices.Add(new Vector3 (x, y, z + 1));
newVertices.Add(new Vector3 (x, y-1, z + 1));

//CubeEast
newVertices.Add(new Vector3 (x + 1, y - 1, z));
newVertices.Add(new Vector3 (x + 1, y, z));
newVertices.Add(new Vector3 (x + 1, y, z + 1));
newVertices.Add(new Vector3 (x + 1, y - 1, z + 1));

//CubeSouth
newVertices.Add(new Vector3 (x, y - 1, z));
newVertices.Add(new Vector3 (x, y, z));
newVertices.Add(new Vector3 (x + 1, y, z));
newVertices.Add(new Vector3 (x + 1, y - 1, z));

//CubeWest
newVertices.Add(new Vector3 (x, y- 1, z + 1));
newVertices.Add(new Vector3 (x, y, z + 1));
newVertices.Add(new Vector3 (x, y, z));
newVertices.Add(new Vector3 (x, y - 1, z));

//CubeBot
newVertices.Add(new Vector3 (x,  y-1,  z ));
newVertices.Add(new Vector3 (x + 1, y-1,  z ));
newVertices.Add(new Vector3 (x + 1, y-1,  z + 1));
newVertices.Add(new Vector3 (x,  y-1,  z + 1));

Now you should have 6 functions and one common function for the faces of the cube. Go to the start function and add the five new functions after CubeTop with the parameters 0,0,0,0 for all of them. If you run it in unity now you should see a cube, not so visible in the camera view but if you look around in the scene view you'll see it.

You should see this
Lets have a look at the collision model generation as well, in the 2d example we generated a different collision mesh after the mesh to be rendered but here we'll use the same mesh for both. The commented lines in the UpdateMesh function do just this. First of all we reset the collision mesh and then we set the collision mesh to mesh so we use the same one we've already made for the mesh renderer. So, uncomment these lines:
col.sharedMesh=null;
col.sharedMesh=mesh;

And you'll have a cube with a collision mesh!

I'll end this part here, it seems like a logical point to stop but what we have now is more than a cube, what we have is a system to create individual faces which will come in very handy when we're creating a surface that resembles cubes but is actually made of just the visible faces. But that's for next time.

Until then please message me with any problems you find and follow me on twitter or google plus to get updated!

Part 6

19 comments:

  1. Hey Alexandros,

    There's something you should fix.

    Quote: " After you've done this go to unity and add a Mesh Filter and a Mesh Collider to our Chunk game object. "

    A "Mesh Renderer" is also required otherwise we get nothing on screen when you told us to place the camera at pos=(0,10,-10) rot=(45,0,0).

    If I find anything else I'll send another comment.

    ReplyDelete
    Replies
    1. Thanks so much ratnushock, I added this and the faceCount=0; line that you mentioned in another comment.

      Delete
  2. Awesome, works well. That really simplifies making a box. Its really not that hard when I understand how it works. Thanks heaps I'm learning lots.

    ReplyDelete
  3. Haven't quite finished this one yet, but just saw this which looks like a problem:

    > "Now lets hop over to unity and place our gameobjects so that we can run. Put the Chunk at 0,0,0 and set the camera's position y to 10 and rotation x to 45. This should put it dead center."

    However, when I did that it wasn't in view of the camera. I had to set the camera's X rotation to 90 and that put it dead center. Maybe I did something wrong?

    ReplyDelete
    Replies
    1. You didn't do anything wrong. When you add gameobjects to the scene there position can vary. If you have Chunk at 0, 0, 0 and the camera at 0, 10, -10 with x rotation at 45 degrees you should be able to see it exactly like the tutorial.

      Delete
    2. Change the code "Vector2 texturePos;" to "Vector2 texturePos = new Vector2();"

      Delete
  4. Hey, can we get download for this file as well? I ran into an error in part 6 with copying/pasting the noise script and I need to go back to this part and do part 6 over.

    ReplyDelete
    Replies
    1. Also in case you're wondering, this is the error:


      NullReferenceException: Object reference not set to an instance of an object
      (wrapper managed-to-managed) object:ElementAddr (object,int,int,int)
      World.Block (Int32 x, Int32 y, Int32 z) (at Assets/Scripts/Level/World.cs:82)
      Chunk.Block (Int32 x, Int32 y, Int32 z) (at Assets/Scripts/Level/Chunk.cs:100)
      Chunk.GenerateMesh () (at Assets/Scripts/Level/Chunk.cs:54)
      Chunk.Start () (at Assets/Scripts/Level/Chunk.cs:37)

      Delete
  5. I'm having an issue with the UpdateMesh function, I am constantly getting an array out of bounds error for the mesh.uv = newUV.ToArray();

    My error keeps telling me that it needs to be the same size as mesh.vertices = newVertices.ToArray();

    I don't know where I went wrong, and I even went so far as to copy and past everything at least 3 times, and I cant figure it out.

    ReplyDelete
    Replies
    1. Nevermind, I figured it out, by closing unity and opening it again.

      Delete
    2. Happened to me as well. For me the error was that I had made changes to the array size in the inspector when testing. Setting it back to 0 fixed it

      Delete
  6. ok so i found an error where you set the Directional faces you need to add

    Vector2 texturePos;
    texturePos=tStone;
    Cube (texturePos);

    this is inside the CubeFace Function like in CubeTop

    ReplyDelete
  7. I have a problem, my cube faces are all rendering one way, so you cant see the bottom or the back two sides.

    Thanks for the awesome tutorial though.

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
    2. I used a double sided shader so that I can see the faces, however there is still some dodgy lighting problems. Is there any way to fix this?

      Delete
    3. 2017 and i had the same problem, some fix?

      Delete
  8. 8elo boi8ia sas parakalo kanteme add sto facebook ΣεμπιςχΧχ me lene

    ReplyDelete
  9. I'm kind of getting the image in the picture, the problem is that when I rotate the cube the faces disappear at random, they disappear more as the cube rotates, I don't know if it's a unity bug or a problem with my code, but I'm pretty sure that I followed the code exactly, if anyone has a similar problem or can help I'd really appreciate it, as I'm pretty new to unity.

    Thanks

    ReplyDelete
  10. Hi, nice tutorial.
    You could add MeshRenderer, -Filter and -Collider automatically instead, by adding:

    [RequireComponent(typeof(MeshCollider)), RequireComponent(typeof(MeshFilter)), RequireComponent(typeof(MeshRenderer))]
    public class Chunk : MonoBehaviour {

    ReplyDelete