Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Improving a Basic Voxel Engine

Discussion in 'Scripting' started by DevMerlin, Apr 15, 2014.

  1. DevMerlin

    DevMerlin

    Joined:
    Dec 21, 2011
    Posts:
    96
    I've been working on a basic engine that can effectively generate a Voxel mesh of cubes. At the moment, that's all it can really do. It's just a singular chunk generator that builds a large box that can be deformed at will. It's all coded in C#.

    I am curious if anyone would be willing to take a look at the code and see... not if, but how it could be improved drastically. One such area would be figuring out a more efficient way of building the cubes. For each type of cube, each face has to be repeated in code, and I KNOW there should be better ways to do that. Due to the size, even with just a single cube represented, I'm posting the link in Pastebin:

    http://pastebin.com/UHuLn0ji

    This is how a single chunk is initialized, it doesn't have a form of map management at the moment either:

     
    Last edited: Apr 15, 2014
  2. rrh

    rrh

    Joined:
    Jul 12, 2012
    Posts:
    331
    I couldn't try it out because I don't know the definition of the Block class.


    EDIT: Okay, on reading more thoroughly, you're not using any properties of the Block class other than block.blockType, so as far as I can tell I could just replace that with an integer.

    I see right now for uvs, you're just assigning one square to the top, one to the east, one to the west, etc. What would be your plan if you were to assign different colours to different blocks? I suppose for example, you could have block.blockType = 2 or 3 instead of just 1 and 0 and further subdivide the 6 sections of the texture?
     
    Last edited: Apr 16, 2014
  3. DevMerlin

    DevMerlin

    Joined:
    Dec 21, 2011
    Posts:
    96
    Essentially, yes. And that pretty much is the exact definition of blockType, a temporarily stored integer. Although instead of subdividing, new UV's are assigned pointing to locations on a 1024 texture with 128 tiles based on the block type - the if (blockType == 1 (2, 3, 4, 5, 6, 7 and so on). The rest of it actually repeats - a lot. I tried to create a function per face, using ref to reference the arrays. That, however, resulted in so many errors I couldn't copy/paste all of them. In theory, the lists could be made public and not specific to the scope of the chunk builder function, and the vertex int counter could also be moved there as well without disrupting it, but I'm not sure if that's a good idea although it MIGHT be a requirement in order to create a save system.
     
    Last edited: Apr 16, 2014
  4. bigmisterb

    bigmisterb

    Joined:
    Nov 6, 2010
    Posts:
    4,221
    Since, that is the exact thing I am working on at the moment I will tell you the steps that I did.

    First, I made the block class, with only the class and the two variables you need. isActive and block type. (which is an int)

    I then made a definition class. This is the class that holds all the work horse stuff. It includes all of the block definitions and the mesh builder.

    Next I built the chunk class, this is the one that just manages each chunk of data. It is based on a MonoBehaviour and it's initialization process does the initial build of the block data. much like you have it. All of the building components and stuff like that are called from the data class though.

    Lastly, I built a world script, this is placed on an object and it builds all of the chunks as you need them.

    The differences:

    I worked specifically on building the data class to be very simple, so where you have 1 piece of code that is 220 lines of code to build your render, I have may be 50 or so. I do this by sending references to functions rather than brute force building the mesh. I also created static UV and Vertex placement for each block. So consider that you have a Vector3[6,4] that contains 6 sides at 4 verts each. Then all you have to do is to match sides to that vector. (I could have, but didn't create an enum block for the side names, but I figured that I would just keep them in order.)

    OK, now all I had to do was run something like this:
    Code (csharp):
    1.  
    2. for (int i=0; i<6; i++) {
    3.     if(bools[i]) AddFace(ref verts, ref faces, ref uvs, ref norms, i, position, blockType);
    4. }
    5.  
    So AddFace then looks like this:
    Code (csharp):
    1.  
    2.     private static void AddFace(ref List<Vector3> verts,
    3.             ref List<int> faces,
    4.             ref List<Vector2> uvs,
    5.             ref List<Vector3> norms,
    6.             int direction,
    7.             Vector3 position,
    8.             int blockType
    9.         ){
    10.         int index = verts.Count;
    11.         AddVerts(ref verts, direction, position);
    12.         AddFaces(ref faces, index);
    13.         AddUVs(ref uvs, direction, blockType);
    14.         AddNormals(ref norms, direction);
    15.     }
    16.  
    It makes the code very simple in the end and breaks it all down to easy to read functions rather than... switches or long lines of ifs.

    I don't know if that is what you are looking for, but is an observation.


    Edit: Oh, if you haven't checked it out, look at this:
    https://sites.google.com/site/letsmakeavoxelengine/home
     
    Last edited: Apr 16, 2014
  5. rrh

    rrh

    Joined:
    Jul 12, 2012
    Posts:
    331
    Code (csharp):
    1.  
    2.                                         if (block.blockType == 1  top == 0)
    3.                                         {
    4.  
    5. //it seems to me everything from HERE
    6.                                                 vertexIndex = vertices.Count;
    7.                                                 vertices.Add(new Vector3(x, y + 1, z));
    8.                                                 vertices.Add(new Vector3(x, y + 1, z + 1));
    9.                                                 vertices.Add(new Vector3(x + 1, y + 1, z + 1));
    10.                                                 vertices.Add(new Vector3(x + 1, y + 1, z));
    11.                                                 // first triangle for the block top
    12.                                                 triangles.Add(vertexIndex);
    13.                                                 triangles.Add(vertexIndex + 1);
    14.                                                 triangles.Add(vertexIndex + 2);
    15.                                                 // second triangle for the block top
    16.                                                 triangles.Add(vertexIndex + 2);
    17.                                                 triangles.Add(vertexIndex + 3);
    18.                                                 triangles.Add(vertexIndex);
    19. //to HERE is going to be the same for all the block types, so it seems redundant to copy it to a bunch of if statements for the different block types
    20.  
    21.                                                 // add UV
    22.                                                 uvs.Add(new Vector2 (0.125f, 0.0f));
    23.                                                 uvs.Add(new Vector2 (0.25f, 0.0f));
    24.                                                 uvs.Add(new Vector2 (0.25f, 0.125f));
    25.                                                 uvs.Add(new Vector2 (0.125f, 0.125f));
    26.  
    27.                                         }

    What I meant by subdivide with the UVs was for the simplest example if you have 2 block types, instead of going from 0.125 to 0.25, you make it half as large and go from 0.125 to 0.1875, and then you have an offset which can be 0 or 0.1875.
    Code (csharp):
    1.  
    2.                                                 uvs.Add(new Vector2 (0.125f+offsetx, 0.0f));
    3.                                                 uvs.Add(new Vector2 (0.25f+offsetx, 0.0f));
    So then you can set that offset completely separately from creating the face.

    bigmisterb has some good ideas, that's a very good way to break the process down into functions.
     
  6. bigmisterb

    bigmisterb

    Joined:
    Nov 6, 2010
    Posts:
    4,221
    in my reference I use a texture atlas for all my textures to reduce draw calls. Because of that, I simply have a number system that keeps track of the placement of those blocks. uv's for all 6 directions. they are whole numbers, so in my math, I simply divide them by the width and height of the texture atlas count.

    Later on, I will simply change this whole process to OOP so each map adds its self to the atlas, and the atlas draws its way down the mip maps. Right now, when the texture level changes surrounding textures mess with it. I think this is the only real way to get clean textures from it.