Senin, 14 Oktober 2013

Unity Voxel Tutorial Part 5: Building a 3d Voxel



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
templates-office.com Tutorial, Unity, Voxel Tut, Voxels

Unity Voxel Tutorial Part 6: Building 3d Voxels


You may have noticed how I cleverly named the last part 3d voxel and this one 3d voxels. That it because in this part we'll be making our chunk show an actual chunk of blocks. So picking up from last tile where we had one block rendering what we'll do now is make a way to store the information of the chunk.

This is an interesting point because there are two ways to do this, you can store the information in each chunk so that every chunk has the data for the blocks it contains or you can use a big array for all the level data that each chunk refers to. I think we'll use the big array because it's easier later on. This will mean that our world has a fixed size though but making an infinitely generating world will have to remain outside of the scope of this tutorial.

So to store our level data make a new script and call it World .Also create a gameobject and call it world as well and put the script on it. The core of this script will be the data array, start by setting up the variables:
public byte[,,] data;
public int worldX=16;
public int worldY=16;
public int worldZ=16;

We'll initiate the byte array with the world* variables as the sizes of the array. Use the start array for this:
void Start () {

data = new byte[worldX,worldY,worldZ];

}

With the default variables our world is only one chunk in size but we'll change that later. This array is pretty useless when it's all empty so let's at least fill it with some placeholder data, similar to how we filled the 2d array we'll cycle through all the dimensions of this array and turn anything lower than 8 to stone. Remember that in our data array 0 is air, 1 is stone and 2 is dirt.
void Start () {

data = new byte[worldX,worldY,worldZ];

for (int x=0; x<worldX; x++){
for (int y=0; y<worldY; y++){
for (int z=0; z<worldZ; z++){

if(y<=8){
data[x,y,z]=1;
}

}
}
}

}

Later on we'll use this script to also generate all our chunk game objects but for now we're just going to use it to store the data while we finish setting up the chunk script so we'll jump back to that chunk script. Go ahead and add a new public variable to the chunk for the world object and a private one for the world script. This is temporary so that we can access the world data.
public GameObject worldGO;
private World world;

Go into unity and set the worldGO variable to the world gameobject by dragging it from the heirarchy window to the variable in the inspector on the Chunk game object. In the start function add the following line:
world=worldGO.GetComponent("World") as World;

Now we can access the world data with world.data but this isn't ideal so jump over to the world script and add a new function called Block:
public byte Block(int x, int y, int z){

if( x>=worldX || x<0 || y>=worldY || y<0 || z>=worldZ || z<0){
return (byte) 1;
}

return data[x,y,z];
}

Now in the chunk script we'll use world.block(0,0,0) to get block data. This function just returns the data from the array but it includes an if to check that the requested block is within the boundaries of the array and otherwise it returns stone.

Back in the chunk script let's get to generating. Create a new function in the chunk script and call it Generate Mesh, with this chunk we're going to cycle through every block in the chunk and based on data from the world.data array generate a mesh. We can't just create a face for every block though because that would be far too many faces that the player actually can't see, instead we're going to only make faces that are exposed to air. This means that blocks buried and surrounded by other blocks aren't rendered at all saving us time. Create a function like this:
Edit: as ratnushock pointed out we need to add this variable to the script first, this defines the chunk size:
public int chunkSize=16;


void GenerateMesh(){

for (int x=0; x<chunkSize; x++){
for (int y=0; y<chunkSize; y++){
for (int z=0; z<chunkSize; z++){
//This code will run for every block in the chunk

if(world.Block(x,y,z)!=0){
//If the block is solid

if(world.Block(x,y+1,z)==0){
//Block above is air
CubeTop(x,y,z,world.Block(x,y,z));
}

if(world.Block(x,y-1,z)==0){
//Block below is air
CubeBot(x,y,z,world.Block(x,y,z));

}

if(world.Block(x+1,y,z)==0){
//Block east is air
CubeEast(x,y,z,world.Block(x,y,z));

}

if(world.Block(x-1,y,z)==0){
//Block west is air
CubeWest(x,y,z,world.Block(x,y,z));

}

if(world.Block(x,y,z+1)==0){
//Block north is air
CubeNorth(x,y,z,world.Block(x,y,z));

}

if(world.Block(x,y,z-1)==0){
//Block south is air
CubeSouth(x,y,z,world.Block(x,y,z));

}

}

}
}
}

UpdateMesh ();
}

Now this will cycle through every block in the chunk, if the block is not air it will run through all the faces of the block and check adjacent blocks. For each adjacent air block it will create a face for that side. We just check if the block at x,y,z + 1 in whatever direction we're checking is air and if so we run the function for that face. At the end of the function we call UpdateMesh() to set the mesh to the new one.

Replace the code that generated a block in the start function (all the CubeTop, CubeNorth, etc. and the UpdateMesh() ) with GenerateMesh();

If you run now you should get a flat mesh showing the tops of 16x16 blocks.

You should see this!
So this is pretty cool, it's terrain with a collision mesh and you can update it at any time by calling GenerateMesh(); You can also change the creation of the data array contents to get something more interesting than a plane. The problem here is that we've written the code so far just to make this one chunk but ideally we would have lots of chunks for more advanced terrain. Why don't we do that now.

What we'll do is just use the world gameobject for initialization and it will generate the chunk gameobjects as needed. Take your chunk gameobject in unity and drag it into the project window (into a prefabs folder if you want to keep it neat) and this will make a prefab of it. Now you can delete it from the scene. Switch to the world script and we can get going.

First add some variables to the world script:
public GameObject chunk;
public GameObject[,,] chunks;
public int chunkSize=16;

And go into unity and set the chunk gameobject to be the prefab we just created.

After we generate the contents of the data array we'll generate and array of chunk gameobjects:
chunks=new GameObject[Mathf.FloorToInt(worldX/chunkSize),
Mathf.FloorToInt(worldY/chunkSize),
Mathf.FloorToInt(worldZ/chunkSize)];

for (int x=0; x<chunks.GetLength(0); x++){
for (int y=0; y<chunks.GetLength(1); y++){
for (int z=0; z<chunks.GetLength(2); z++){

chunks[x,y,z]= Instantiate(chunk,
new Vector3(x*chunkSize,y*chunkSize,z*chunkSize),
new Quaternion(0,0,0,0)) as GameObject;

Chunk newChunkScript= chunks[x,y,z].GetComponent(\"Chunk\") as Chunk;

newChunkScript.worldGO=gameObject;
newChunkScript.chunkSize=chunkSize;
newChunkScript.chunkX=x*chunkSize;
newChunkScript.chunkY=y*chunkSize;
newChunkScript.chunkZ=z*chunkSize;

}
}
}

What this does is generate enough chunks to display the contents of the data array and then sets them up with the variables needed. First we initialize the chunks array taking the size of the level and dividing each dimension by the chunk size so that we get the right amount of chunks based on the size of our world and we turn them into ints rounding down so that if they don't match up we get fewer chunks rather than chunks that aren't filled. This means that we never have to define how many chunks we want instead we just define the size of the level and the size of the chunks to represent them.

Then we go through and for each slot in the chunks array we instantiate a new chunk prefab. They are positioned with a new one every chunksize so we get their position with dimension*chunksize. Then we get access to their Chunk script and then set some variables. The worldGO, the chunk size and then some new variables; the coordinates of the chunk so that it can know which blocks it represents.

Go into the chunk script because we have to make some changes here too. Add these new variables for the position that we'll use to access the right blocks from the data array.
public int chunkX;
public int chunkY;
public int chunkZ;

Secondly we're going to add yet another step to our getting block data method, create a new function called Block:
byte Block(int x, int y, int z){
return world.Block(x+chunkX,y+chunkY,z+chunkZ);
}

All this does is add on the position of the chunk to the position of the block we're accessing so that the chunk further to the center of the level accesses the blocks at it's location.

To use this function we have to change the way we accessed the blocks before so find are replace occurrences of world.Block with just Block. However don't replace the one in the function we just added.
byte Block(int x, int y, int z){
return world.Block(x+chunkX,y+chunkY,z+chunkZ); // Don't replace the world.Block in this line!
}

So you can either click replace for each instance and skip the one return line we want to keep or you can replace all then remember to change back that one line.
You open this menu by pressing ctrl+f and then pressing the down arrow on the left to get the replace box
Now we can set the size of the world in the world object and leave the chunk size at 16. If you set the size of the world to 64x64 it should generate 4x4 chunks!

Now we're getting somewhere
But the problem now is that this terrain is super boring, why don't we fix that? To the World Script!
This is very similar to the way we added noise to the 2d level, we'll be generating perlin noise for each column.

First of all we now need more advanced noise than the standard unity perlin noise can offer as far as I know so we'll be using someone else's implementation of Perlin's Simplex noise in c#: Perlin Simplex Noise for C# and XNA. What you'll need to do is create a new C# script, call it "Noise" and remove everything but:
using UnityEngine;
using System.Collections;

Then after that paste in the code from simplex noise implementation linked as Download at the bottom of the post I just linked. Just save this file and we can access it from the World script.

Now we'll create a new function in the world script to retrieve the noise:
int PerlinNoise(int x,int y, int z, float scale, float height, float power){
float rValue;
rValue=Noise.GetNoise (((double)x) / scale, ((double)y)/ scale, ((double)z) / scale);
rValue*=height;

if(power!=0){
rValue=Mathf.Pow( rValue, power);
}

return (int) rValue;
}

Now we can call Perlin noise with some extra variables like scale and height and power like in the 2d version but now we have a whole new dimension. Up in the start function where we created a floor of stone let's replace that with something a little more interesting.
for (int x=0; x<worldX; x++){
for (int z=0; z<worldZ; z++){
int stone=PerlinNoise(x,0,z,10,3,1.2f);
stone+= PerlinNoise(x,300,z,20,4,0)+10;
int dirt=PerlinNoise(x,100,z,50,2,0) +1; //Added +1 to make sure minimum grass height is 1

for (int y=0; y<worldY; y++){
if(y<=stone){
data[x,y,z]=1;
} else if(y<=dirt+stone){ //Changed this line thanks to a comment
data[x,y,z]=2;
}

}
}
}

This should add a little noise to the surface and in addition add a new block. If you run now the surface should be noisy but everything will be the same block. What you can do now is add different textures for blocks.

Back in the chunk script in the code for each face we decided what texture to use but just set it to stone, now let's change that.

I created a new variable called tGrassTop to set a texture based on the face of the block:
private Vector2 tGrassTop = new Vector2 (1, 1);

Then in the CubeTop function I use this:
Vector2 texturePos=new Vector2(0,0);

if(Block(x,y,z)==1){
texturePos=tStone;
} else if(Block(x,y,z)==2){
texturePos=tGrassTop;
}

Now the sides of the blocks are often going to be the same so if you want you can set up a common function to set the textures for the sides of the blocks but what I've done is used this for all of them and the bottom:
if(Block(x,y,z)==1){
texturePos=tStone;
} else if(Block(x,y,z)==2){
texturePos=tGrass;
}

If you run this you should get some ugly terrain with our placeholder textures but it should work as a prototype:


Now it needs some remove and place block functions and some more block types but you can see how it's done. Next time I think we'll be able to place and remove blocks and come up with a better way to generate the chunks than before the game loads.

Here's an example of what you could do with what we've made so far, with some new textures and directional lights it's starting to look professional.



Until then though good luck, even with just this I think you have a good start to work from if you feel like you don't want to rely on my tutorial. Feel free to follow me on twitter or g+ and as always of you have a problem please let me know and I'll do my best to fix it.


Part 7: Modifying the terrain
templates-office.com Terrain Generation, Tutorial, Unity, Voxel Tut, Voxels