Tuesday, September 12, 2017

voxels - Unity block chunk generation pattern


I am trying to do something like Minecraft game in Unity. Currently I am trying to generate a chunk of blocks, given dimensions. I can successfully create a chunk of N x 0 x 0 sized chunk, but due to lack of logic, I have no idea on how to make triangles and vertices match for other blocks.


Basically, what I mean is, I can generate something like this:


enter image description here


But can't do this:


enter image description here


My Block vertices and Triangles:



Vector3[] cVerts = {
new Vector3 (0, 0, 0), //0
new Vector3 (1, 0, 0), //1
new Vector3 (1, 1, 0), //2
new Vector3 (0, 1, 0), //3
new Vector3 (0, 1, 1), //4
new Vector3 (1, 1, 1), //5
new Vector3 (1, 0, 1), //6
new Vector3 (0, 0, 1), //7
};

int[] cTris = {
0, 2, 1, //face front
0, 3, 2,
2, 3, 4, //face top
2, 4, 5,
1, 2, 5, //face right
1, 5, 6,
0, 7, 4, //face left
0, 4, 3,
5, 4, 7, //face back

5, 7, 6,
0, 6, 7, //face bottom
0, 1, 6
};

Chunk creation method:


     void CreateChunk(){
...
for (int x = 0; x < 100; x++)
{

for(int y = 0; y < 10; y++){
for (int z = 0; z < 1; z++)
{
AddBlockAtPosition(new VectorInt(x, y, z), "Grass");
}
}
}

ChunkMesh.vertices = CurrentChunkVerts.ToArray();
ChunkMesh.triangles = CurrentChunkTris.ToArray();


//ChunkMesh.uv = uvs.ToArray();
ChunkMesh.RecalculateNormals();
}

AddBlockAtPosition() method:


    void AddBlockAtPosition(VectorInt pos, string name)
{
int x = pos.x, y = pos.y, z = pos.z;


//create separate cube data
CubeMesh cube = new CubeMesh(vertices.ToArray(), triangles.ToArray(), uvs.ToArray());
//I use MoveCube() to change each Block's vertex and triangle numbers, so they will "match" in a Chunk mesh, when they become part of it.
cube.MoveCube(new Vector3(x, y, z), (x + y + z) * 8);
//cube.MoveUV(new Vector2(x + y + z, x + y + z));

CurrentChunkVerts.AddRange(cube.Vertices());
CurrentChunkTris.AddRange(cube.Triangles());
//CurrentChunkUV.AddRange(cube.UVs());
}


And most importantly, that's where my problems are, MoveCube();


Class Cube{
private Vector3[] vertices;
private int[] triangles;
...
public void MoveCube(Vector3 direction, int n)
{
for(int i=0; i {

vertices[i] += direction;
}
for (int i = 0; i < triangles.Length; i++)
{
triangles[i] = triangles[i] + n;
}
}
}

I know that I am not the first to re-create minecraft, I have found this tutorial https://www.blockstory.net/node/56



But for me as a beginner, some "simple" parts like these I have problems with are skipped. And other tutorials just generalize the idea, which isn't enough for me.



Answer



Remember, the whole point of using a chunk mesh is to try to render just the outside skin, and hide all the internal detail inside the chunk where we can't see it. If we generate a whole cube (all 12 triangles) at every occupied position, then we might as well spawn it as a separate mesh and at least hope GPU instancing saves us some work - otherwise we're both losing instancing AND rendering stuff we don't need, the worst of both worlds.


So, to minimize redundant geometry, we can do something a bit like this...


(Here I'm going to assume your chunks can be made of multiple materials, whose textures are stored as a texture array so we can use UVs over an arbitrary range and they'll tile correctly for us. This way when four faces of the same material meet, we only need 1 vertex, rather than 4 with different UV coordinates. (We can also do this in the shader math if texture arrays aren't available.)


First we define the characteristics we want to be the same at any shared vertex:


struct VertexSignature {
int3 position,
CardinalDirection normal,
BlockMaterial material,

}

Now we can use this to de-duplicate vertices, referring back to the same index anytime our code asks for a vertex with the same characteristics:


Dictionary _vertexLookup = new Dictionary...
int _vertexCount = 0;

int GetVertexIndex(VertexSignature signature) {
int index;
if(!_vertexLookup.TryGetValue(signature, out index) {
index = _vertexCount++;

_vertexLookup.Add(signature, index);
}
return index;
}

Next we can iterate over all the spots in our chunk, to see which blocks need to produce faces (I'm going to be a bit loose & pseudocode-y for brevity):


foreach(int3 position in chunk.Bounds) {
// I assume ultimately you'll have some type of data structure that records
// the current block at each position, whether the product of a terrain generator
// or player mining/building, etc.

var block = World.GetBlockAt(position);

// If this block is empty air, we don't need to draw any faces for it.
if(block.type == BlockType.Empty)
continue;

// Otherwise, we check if this block is exposed on any face.
foreach(CardinalDirection direction in Neighbourhood) {
var neighbour = World.GetBlockAt(position + direction);


// If the block is occluded on this side, we don't need to draw
// the faces in between the two blocks.
if(neighbour.type != BlockType.Empty)
continue;

// Otherwise, we've found an exposed face. Add it to the list.
AddFace(position, direction, block.type);
}
}


To add each face, we just look up the corresponding position offsets we want for each vertex for a face on that side of the cube, request an index for each resulting vertex, and add those indices to our growing list.


List _indices;

void AddFace(int3 position, CardinalDirection direction, BlockMaterial material) {
// Have a table or map that lets you quickly look up the 4 vertex offsets
// from the cube's bottom-left-back corner for a given face direction.
int3[] vertexPositions = GetVertexPositions(direction);

VertexSignature signature;
signature.normal = direction;

signature.material = material;

signature.position = position + vertexPositions[0];
int a = GetVertexIndex(signature);

signature.position = position + vertexPositions[1];
int b = GetVertexIndex(signature);

signature.position = position + vertexPositions[2];
int c = GetVertexIndex(signature);


signature.position = position + vertexPositions[3];
int d = GetVertexIndex(signature);

_indices.Add(a);
_indices.Add(b);
_indices.Add(c);

_indices.Add(a);
_indices.Add(c);

_indices.Add(d);
}

Lastly, once we've visited every block and added all the indices we need, we can bake down our vertex signatures into the corresponding arrays for positions, normals, and UVs...


vertices = new Vector3[_vertexCount];
normals = new Vector3[_vertexCount];
uvs = new Vector3[_vertexCount];

foreach(var pair in _vertexLookup) {
int index = pair.value;

vertices[index] = pair.key.position;
normals[index] = (Vector3)pair.key.normal;

Vector3 uv = ProjectPositionToUV(pair.key.position, pair.key.normal);
uv.z = pair.key.material.index;

uvs[index] = uv;
}

From here you've got all the ingredients you need to build a mesh of arbitrary shape & size, with shared vertices and no internal faces. Note that since we've only been using basic types & math above, all of this can be done on a separate thread, leaving only the final assembly of these buffers into the Mesh instance and uploading to the GPU for the main thread to handle.



The simple method shown here still produces some redundant faces & vertices: a long rectangular plane of the same material could just be two faces instead of length * width * 2, but that's a further optimization we can tackle separately.


No comments:

Post a Comment

Simple past, Present perfect Past perfect

Can you tell me which form of the following sentences is the correct one please? Imagine two friends discussing the gym... I was in a good s...