1
\$\begingroup\$

I've flattened my 3D chunk array(Got nice 20ms speed up), modified my VertexData struct (that I use in Dictionary as a key) to have custom hashcode(another 20ms and -2.7mb GC.alloc data speedup). But I'm not sure what to do next, since 40ms still feels long for such a small chunk(In fact, its twice smaller the size of Minecrafts 16x256x16 and still managing to be slower.).

My biggest bottleneck still seems to be the dictionary.

This is current performance profile:

enter image description here

The code:

public void MakeChunk()
    {

        vertexCount = 0;
        //these are all Lists except verticesDictionary
        vertices.Clear();
        normals.Clear();
        verticesDict.Clear();
        uvs.Clear();
        triangles.Clear();

        int len = 32 * 32 * 32;

        for (int i = 0; i < len; i++)
        {
            int cubeType = data.raw[i];
            if (cubeType != 0)
            {
                int x = i % 32;
                int y = (i / 32) % 32;
                int z = i / (32 * 32);
                MakeCube(x, y, z, cubeType);
                blocksGenerated++;
            }
        }
    }
    void MakeCube(int x, int y, int z, int cubeType)
    {
        for (int i = 0; i < 6; i++)
        {
            Direction dir = (Direction)i;
            if (data.GetNeighbor(x, y, z, dir) == 0)
            {
                MakeFace(dir, x, y, z, cubeType);
            }
        }
    }
    void MakeFace(Direction dir, int x, int y, int z, int cubeType)
    {
        VertexSignature signature;
        signature.normal = dir;

        Vector3[] faceVertices = CubeMeshData.faceVertices((int)dir, x, y, z);
        int[] triangleIndices = new int[4];
        signature.cubeType = cubeType;

        for (int i = 0; i < 4; i++)
        {
            signature.position = faceVertices[i];
            Vector3 uv = CubeMeshData.ProjectPositionToUV(signature.position, dir);
            uv.z = signature.cubeType;

            int vertex = GetVertexIndex(signature);
            triangleIndices[i] = vertex;
        }

        triangles.AddRange(new int[6] {
            triangleIndices[0],
            triangleIndices[1],
            triangleIndices[2],
            triangleIndices[0],
            triangleIndices[2],
            triangleIndices[3]
            }
        );


    }
    void PrepareMeshData()
    {
        foreach (var pair in verticesDict)
        {
            int index = pair.Value;
            vertices.Add(pair.Key.position);
            CubeMeshData.DataCoordinate coord = CubeMeshData.offsets[(int)pair.Key.normal];
            normals.Add(new Vector3(coord.x, coord.y, coord.z));

            Vector3 uv = CubeMeshData.ProjectPositionToUV(pair.Key.position, pair.Key.normal);
            uv.z = pair.Key.cubeType;
            uvs.Add(uv);
        }
    }

Hashing:

 struct VertexSignature
    {
        public Vector3 position;
        public Direction normal;
        public int cubeType;

        public override int GetHashCode()
        {
            uint h = 0x811c9dc5;
            h = (h ^ (uint)position.x) * 0x01000193;
            h = (h ^ (uint)position.y) * 0x01000193;
            h = (h ^ (uint)position.z) * 0x01000193;
            h = (h ^ (uint)normal) * 0x01000193;
            h = (h ^ (uint)cubeType) * 0x01000193;

            return (int)h;
        }
    };

Then I do this:

    Vector3[] vertexArr = vertices.ToArray();
    int[] triArr = triangles.ToArray();
    Vector3[] normalsArr = normals.ToArray();

I've really run out of ideas on how to improve performance further.

Any help is welcome.

\$\endgroup\$
  • \$\begingroup\$ Comments are not for extended discussion; this conversation has been moved to chat. \$\endgroup\$ – DMGregory Mar 15 at 18:49
1
\$\begingroup\$

Nested method calls

Method calls in loops should be inlined (not sure C# /CLI's internal rules for this), as every such causes a JMP in the generated assembly (same for MakeCube calling MakeDir):

for (int i = 0; i < len; i++) { int cubeType = data.raw[i]; if (cubeType != 0) { ... MakeCube(x, y, z, cubeType); ... } } In C we would just macro this, but you are stuck with C# which may mean copy-pasting blocks of code, or using a code generator of some sort to do that for you.

Even worse is that you call MakeDir inside MakeCube inside a loop.

"Fiddly branchless logic"

FBL - a coin termed on a question I once asked on the OpenGL forums for shader code - is the best thing you can do, where possible, to avoid conditionals, especially inside loops.

For example, you are saying if (cubeType != 0) { /*do some stuff*/ } What if you eliminated that and did that stuff anyway, but made it invisible for null voxels? For example, creating blocks that exist but have zero dimensions and thus are not visible? This can be done by for example, doing the following:

1. factor = 1 if any bit is set in cube.type (without using a conditional), else 0 (if no bits are set, i.e. = positive zero)
2. multiply your x, y, z dimensions of cube generated by this factor

I know that C# has some restrictions in this regard (getting factor without triggering a full conditional)... You'll need to do some more (re)search on this. And this may also increase the number of vertices sent to the GPU... but if you are doing custom shaders, that "full set" is then also easier to index into without search (for example if you want to generate your voxels using a geometry shader). But at the end of the day, it all depends on what you want to speed up.

This is not trivial, but FBL is a valuable tool indeed for data-intensive applications like this.

Hashing inefficiency

Also, your hash function is very heavy on ops count. I would try to reduce that. We can talk about this further if you wish.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ This'd break my mesh since I'm already skipping duplicate vertices and I'm also planning on implementing greedy meshing. So I'm afraid I'll have to check whether cube is 0 or not right? \$\endgroup\$ – Nick Mar 15 at 16:20
  • \$\begingroup\$ See update. Greedy meshing... yep can't help ya there ;) Best you can do is try for the ternary operator ? : over if and see if compiler shortcuts it or not. \$\endgroup\$ – Engineer Mar 15 at 16:21
  • \$\begingroup\$ With ternary operator I'd be unnecessarily counting x,y,z coordinates(doing division and modulo calculations) for blocks that are 0 \$\endgroup\$ – Nick Mar 15 at 16:23
  • \$\begingroup\$ about hashing, If there are any improvements, I'd gladly hear and do them \$\endgroup\$ – Nick Mar 15 at 16:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.