1
\$\begingroup\$

I am making a voxel based terrain system (Yes, I know, very original...), anyway...

Each voxel should have it's own color so the player can distinguish which type of material it is. For example: dirt would be brown; grass, green and stone would be gray.

I don't really understand marching cubes, so I come to you guys with my issue. I want to be able to color each vertex based on which voxel it would visually make sense to belong to. Edit: To clear things up a little. The voxel contains the color data, I do not wish to generate the colors (like how some systems automatically apply a stone texture based on the height/slope of the face)

How would I go about doing this?


Edit: Marching Cubes Code

Here is the code I am using for marching cubes, as I have said. I don't understand marching cubes so this code wasn't written by me.

Original Source of the Code Here

(Some of the data structures (like the lookup tables) have been omitted from the code)

Also note that the code below has been edited to pass in the color data for each Voxel.

        public virtual void Generate(IList<float> voxels, IList<Voxel> voxelData, int width, int height, int depth, IList<Vector3> verts, IList<int> indices)
        {

            if (Surface > 0.0f)
            {
                WindingOrder[0] = 0;
                WindingOrder[1] = 1;
                WindingOrder[2] = 2;
            }
            else
            {
                WindingOrder[0] = 2;
                WindingOrder[1] = 1;
                WindingOrder[2] = 0;
            }

            int x, y, z, i;
            int ix, iy, iz;
            for (x = 0; x < width - 1; x++)
            {
                for (y = 0; y < height - 1; y++)
                {
                    for (z = 0; z < depth - 1; z++)
                    {
                        //Get the values in the 8 neighbours which make up a cube
                        for (i = 0; i < 8; i++)
                        {
                            ix = x + VertexOffset[i, 0];
                            iy = y + VertexOffset[i, 1];
                            iz = z + VertexOffset[i, 2];

                            Cube[i] = voxels[ix + iy * width + iz * width * height];
                        }

                        //Perform algorithm
                        March(x, y, z, Cube, verts, indices);
                    }
                }
            }

        }

        protected virtual float GetOffset(float v1, float v2)
        {
            float delta = v2 - v1;
            return (delta == 0.0f) ? Surface : (Surface - v1) / delta;
        }
        protected override void March(float x, float y, float z, float[] cube, IList<Vector3> vertList, IList<int> indexList)
        {
            int i, j, vert, idx;
            int flagIndex = 0;
            float offset = 0.0f;

            //Find which vertices are inside of the surface and which are outside
            for (i = 0; i < 8; i++) if (cube[i] <= Surface) flagIndex |= 1 << i;

            //Find which edges are intersected by the surface
            int edgeFlags = CubeEdgeFlags[flagIndex];

            //If the cube is entirely inside or outside of the surface, then there will be no intersections
            if (edgeFlags == 0) return;

            //Find the point of intersection of the surface with each edge
            for (i = 0; i < 12; i++)
            {
                //if there is an intersection on this edge
                if ((edgeFlags & (1 << i)) != 0)
                {
                    offset = GetOffset(cube[EdgeConnection[i, 0]], cube[EdgeConnection[i, 1]]);

                    EdgeVertex[i].x = x + (VertexOffset[EdgeConnection[i, 0], 0] + offset * EdgeDirection[i, 0]);
                    EdgeVertex[i].y = y + (VertexOffset[EdgeConnection[i, 0], 1] + offset * EdgeDirection[i, 1]);
                    EdgeVertex[i].z = z + (VertexOffset[EdgeConnection[i, 0], 2] + offset * EdgeDirection[i, 2]);
                }
            }

            //Save the triangles that were found. There can be up to five per cube
            for (i = 0; i < 5; i++)
            {
                if (TriangleConnectionTable[flagIndex, 3 * i] < 0) break;

                idx = vertList.Count;

                for (j = 0; j < 3; j++)
                {
                    vert = TriangleConnectionTable[flagIndex, 3 * i + j];
                    indexList.Add(idx + WindingOrder[j]);
                    vertList.Add(EdgeVertex[vert]);
                }
            }
        }
/// VoxelData
[Serializable]
public struct Voxel
{
    public Color32 color;
    public long materialId;
}

Example of what I tried

I have already tried assigning the color based on the current cube being processed, it did not give the result I wanted, take a look at the image below, notice that the corner of some voxels are white instead of gray, this is because of the way I implemented colors the first time, those vertices are technically generated from an air block but should visually belong to a stone block.

(Ignore the weird glitches in the terrain, that is something I have already fixed, it was with the way I was generating the terrain)

enter image description here


After a few hours of searching:

I found what seems promising in an answer here

private static final byte[] BLOCK_PICK_BITS_BY_VERT_INDEX = new byte[]{
    (byte)0x67,
    (byte)0x26,
    (byte)0x23,
    (byte)0x37,
    (byte)0x45,
    (byte)0x04,
    (byte)0x01,
    (byte)0x15,
    (byte)0x57,
    (byte)0x46,
    (byte)0x02,
    (byte)0x13
};

private static final int pickBlockIdForVertex(long blockIds, int vertIndex)
{
    int bits = BLOCK_PICK_BITS_BY_VERT_INDEX[vertIndex];
    int blockIndexA = bits >> 4;
    int blockIndexB = bits & 0xf;
    int blockIdA = (int)(blockIds >> 8 * blockIndexA) & 0xff;
    int blockIdB = (int)(blockIds >> 8 * blockIndexB) & 0xff;

    //assert (blockIdA == airBlockId) != (blockIdB == airBlockId);
    //return blockIdA - nonZeroAirBlockId + blockIdB; 
    return blockIdA + blockIdB;
}

However the answer doesn't explain how blockIds work or how to get vertIndex.

I am really at a lost on how to solve this problem, I'd really like someones help. I'm sure there are probably other people with the same questions as me, I am honestly surprised there is absolutely no direct documentation on this (at least, to my knowledge there isn't).

I will be reading up on marching cubes and trying to figure out how to maybe implement colors.


I think I'm almost there!

So the answer I linked to in the previous edit, where I said it looked promising, I finally was able to understand some of it. And from what I understood I made the code below:

    // Because the Marching Cubes implementation I use has some of the least to greatest significant vertex indices swapped, I modified this below to account for that. 
    /// Mainly any 7s in the array below were swapped with 6s and the same for 4s and 5s.
    private static byte[] MAT_CHOOSE_BITS = new byte[]{
        0x76,
        0x27,
        0x23,
        0x36,
        0x54,
        0x05,
        0x01,
        0x14,
        0x46,
        0x57,
        0x02,
        0x13
    };

    public Color32 GetMaterialColor(int[] mats, int vert)
    {
        int bits = MAT_CHOOSE_BITS[vert];

        // Because I am indexing an array and not bit-shifting a long I must use `7 - bits`
        int indexA = 7 - (bits >> 4); 
        int indexB = 7 - (bits & 0xf);

        return Material.materials[(mats[indexA] + mats[indexB]) - 1].color;
    }

The code above ALMOST works, it's very frustrating. When I run the code, it runs through the loop about 8 times before it encounters an Argument Out Of Bounds Exception, I debugged it and apparently in one of the situations the "mats" array doesn't contain any zeros, which from what I understand shouldn't be possible. The theory behind the code above is that one of the indices (either indexA or indexB) should point to a zero in the mats array, because in theory one side of the equation is always zero (or air).

I don't understand what I did wrong... I know at this point it has to be the way I am generating the voxels, it might also still be that code but I need to first solve the issue where none of the ints passed into the mats array is 0.

Here is the code I am using to generate the terrain:

        for (int i = 0; i < 16; i++)
        {
            for(int j = 0; j < 256; j++)
            {
                for (int k = 0; k < 16; k++)
                { 
                    Voxel v;
                    float d = terrain.noise.Value.GetSimplex(i, j, k);

                    if (d > 0 || Mathf.Approximately(d, 0f)) v.materialId = UnityEngine.Random.Range(1, 3);
                    else v.materialId = 0;

                    v.density = d;

                    data.voxels[i + j * 16 + k * 16 * 256] = v;
                }
            }
        }

FAILURE

So I wasn't able to fix this issue, and it seems nobody else on this forum is going to as well. So I decided to go with a much simpler solution, basically, I dropped smoothing the terrain to just binary smoothing. Either a cube is off, or on and there are sharp slopes between that. it makes the terrain look blocky but it's the best solution for how I fixed the vertex colors. I fixed the vertex colors by first checking if the current cube is air, if it is it chooses the material of the first neighbor that isn't air.

If anyone has an answer to this question in the future, I am all ears.

\$\endgroup\$
4
  • \$\begingroup\$ How do you currently generate the mesh? There should be some moment in that code where you instantiate a new vertex based on your evaluation of a particular voxel site in your grid. That would be the appropriate place to look up/compute the corresponding colour and assign it to that vertex, but having never seen your code it's difficult for us to speculate exactly what this will look like in your implementation. \$\endgroup\$
    – DMGregory
    Jan 10, 2020 at 13:03
  • \$\begingroup\$ @DMGregory I have already tried setting the colors based on the xyz index of the current cube being processed, unfortunately that wont work because when the mesh gets smoothed some vertices lie inside of air tiles and don't get colored properly. \$\endgroup\$ Jan 10, 2020 at 16:10
  • \$\begingroup\$ That's a great detail and visual example to add to your question. It may be a good idea to paint-over this example with a rough guideline of what you want the output to be instead, so it's clear how you want colours to be assigned in the case of smoothed vertices between two different materials. \$\endgroup\$
    – DMGregory
    Jan 10, 2020 at 16:59
  • \$\begingroup\$ @DMGregory I have edited my question to include an image, also I don't know if you had seen the edit I did before that, but I also provided code. \$\endgroup\$ Jan 10, 2020 at 17:41

1 Answer 1

1
\$\begingroup\$

I gave this issue another shot, and I found a solution!

I figure that this will probably be helpful to someone in the future, so I will describe exactly how I did it, I will provide minimal code though, because my solutions code is a tiny bit implementation specific.


Credit Where it is Due

This answer was made possible by this answer here. In fact, my answer is basically just going to explain the stuff I was confused about in the that answer and what I did to fix it for my implementation.

The code I ended up using

    Color SeletColor(Color[] options, int vertIndex)
    {
        int bits = ColorSelectBits[vertIndex];
        int indexA = 7 - (bits >> 4);
        int indexB = 7 - (bits & 0xF);

        Color a = options[indexA], b = options[indexB];

        Color c = new Color
        {
            r = a.r + b.r,
            g = a.g + b.g,
            b = a.b + b.b,
            a = a.a + b.a
        };

        return c;
    }

    private static readonly int[] ColorSelectBits = new int[]
    {
        0x67,
        0x74,
        0x45,
        0x56,
        0x23,
        0x30,
        0x01,
        0x12,
        0x62,
        0x73,
        0x40,
        0x51
    };

An explanation of how I fixed it

So in the answer I linked to, his least to most significant vertex order was different than the Marching Cubes implementation I used.

I was able to fix this thanks to the presence of 2 arrays.

    protected static readonly int[,] VertexOffset = new int[,]
    {
        {0, 0, 0},{1, 0, 0},{1, 1, 0},{0, 1, 0},
        {0, 0, 1},{1, 0, 1},{1, 1, 1},{0, 1, 1}
    };

    /// <summary>
    /// EdgeConnection lists the index of the endpoint vertices for each 
    /// of the 12 edges of the cube.
    /// edgeConnection[12][2]
    /// </summary>
    private static readonly int[,] EdgeConnection = new int[,]
    {
            {0,1}, {1,2}, {2,3}, {3,0},
            {4,5}, {5,6}, {6,7}, {7,4},
            {0,4}, {1,5}, {2,6}, {3,7}
    };

I used EdgeConnection to get an index to VertexOffset, I then found an index to the opposite of whatever "Vector" (The three floats in the second dimension) that EdgeConnection pointed me to, and the index to the opposite is what the first/last 4 bits of an entry of ColorSelectBits are.

Here is an example:

EdgeConnection[0] is {0, 1}

VertexOffset[0] is {0, 0, 0} the opposite of that is {1, 1, 1} so the index I needed was 6

VertexOffset[1] is {1, 0, 0} the opposite of that is {0, 1, 1} so the index I needed was 7

That means the value I needed for ColorSelectBits[0] is 0x67

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .