3

So I'm trying to achieve the "Polygon Art/Low Poly" style with LibGDX. I start by constructing a model made of triangles.

1

Then with the vertex shader, I calculate the colors for each vertex based on height.

2

Problem is, the terrain is Gouraud shaded when I want it to be flat shaded like this:

3

I know that with higher versions of OpenGL there's a "flat" keyword in glsl that will disable the interpolation of colors between vertices. From what I read online and in this post: https://i.stack.imgur.com/DrNx9.jpg , I think that I would need to have each triangle in the terrain be separate from each other? I would also need to calculate the normal per triangle? I couldn't understand the code in the other StackOverflow, but this is what I tried to do:

Original

public Model getWorld(){
    returnWorld = new Model();
    modelBuilder = new ModelBuilder();
    modelBuilder.begin();
    worldMeshBuilder = modelBuilder.part("worldPart", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, new Material());
    pieceMeshBuilder = new MeshBuilder();
    meshPiece = new Mesh(false, 3, 3, 
            new VertexAttribute(Usage.Position, 3, "a_position"), 
            new VertexAttribute(Usage.Normal, 3, "a_normal"), 
            new VertexAttribute(Usage.ColorPacked, 4, "a_color"));
    Vector3 vectorCopy = new Vector3();
    for(int i = 0; i < world.length - 1; i++){
        for(int j = 0; j < world[0].length - 1; j++){
            if((i + j) % 2 == 0){
                pieceMeshBuilder.begin(Usage.Position | Usage.Normal, renderType);
                pieceMeshBuilder.triangle(
                    vectorCopy = verticies[i][j],
                    vectorCopy = verticies[i][j + 1],
                    vectorCopy = verticies[i + 1][j + 1]
                );
                worldMeshBuilder.addMesh(pieceMeshBuilder.end());
                pieceMeshBuilder.begin(Usage.Position | Usage.Normal, renderType);
                pieceMeshBuilder.triangle(
                        vectorCopy = verticies[i + 1][j + 1],
                        vectorCopy = verticies[i + 1][j],
                        vectorCopy = verticies[i][j]
                );
                worldMeshBuilder.addMesh(pieceMeshBuilder.end());
            } else {
                pieceMeshBuilder.begin(Usage.Position | Usage.Normal, renderType);
                pieceMeshBuilder.triangle(
                    vectorCopy = verticies[i][j],
                    vectorCopy = verticies[i][j + 1],
                    vectorCopy = verticies[i + 1][j]
                );
                worldMeshBuilder.addMesh(pieceMeshBuilder.end());
                pieceMeshBuilder.begin(Usage.Position | Usage.Normal, renderType);
                pieceMeshBuilder.triangle(
                        vectorCopy = verticies[i + 1][j + 1],
                        vectorCopy = verticies[i + 1][j],
                        vectorCopy = verticies[i][j + 1]
                );
                worldMeshBuilder.addMesh(pieceMeshBuilder.end());
            }
        }
    }
    returnWorld = modelBuilder.end();
    return returnWorld;
}

Now:

public Model getWorld(){
    returnWorld = new Model();
    modelBuilder = new ModelBuilder();
    modelBuilder.begin();
    worldMeshBuilder = modelBuilder.part("worldPart", GL20.GL_LINES, Usage.Position | Usage.Normal, new Material());

    for(int i = 0; i < world.length - 1; i++){
        for(int j = 0; j < world[0].length - 1; j++){

            Vector3 normal1 = calcNormal(verticies[i][j], verticies[i + 1][j], verticies[i + 1][j + 1]);
            Vector3 normal2 = calcNormal(verticies[i][j], verticies[i + 1][j + 1], verticies[i][j + 1]);

            if((i + j) % 2 == 0){
                meshPiece = new Mesh(false, 18, 3, 
                        new VertexAttribute(Usage.Position, 3, "a_position"), 
                        new VertexAttribute(Usage.Normal, 3, "a_normal")//, 
                        //new VertexAttribute(Usage.ColorPacked, 4, "a_color")
                        );
                worldMeshBuilder.addMesh(meshPiece.setVertices(new float[] {
                        verticies[i][j].x, verticies[i][j].y, verticies[i][j].z, normal1.x, normal1.y, normal1.z,
                        verticies[i + 1][j].x, verticies[i + 1][j].y, verticies[i + 1][j].z, normal1.x, normal1.y, normal1.z, 
                        verticies[i + 1][j + 1].x, verticies[i + 1][j + 1].y, verticies[i + 1][j + 1].z, normal1.x, normal1.y, normal1.z,
                    }));
                meshPiece = new Mesh(false, 18, 3, 
                        new VertexAttribute(Usage.Position, 3, "a_position"), 
                        new VertexAttribute(Usage.Normal, 3, "a_normal")//, 
                        //new VertexAttribute(Usage.ColorPacked, 4, "a_color")
                        );
                worldMeshBuilder.addMesh(meshPiece.setVertices(new float[] {
                        verticies[i][j].x, verticies[i][j].y, verticies[i][j].z, normal2.x, normal2.y, normal2.z,
                        verticies[i + 1][j + 1].x, verticies[i + 1][j + 1].y, verticies[i + 1][j + 1].z, normal2.x, normal2.y, normal2.z, 
                        verticies[i][j + 1].x, verticies[i][j + 1].y, verticies[i][j + 1].z, normal2.x, normal2.y, normal2.z,
                    }));
            } else {
                meshPiece = new Mesh(false, 18, 3, 
                        new VertexAttribute(Usage.Position, 3, "a_position"), 
                        new VertexAttribute(Usage.Normal, 3, "a_normal")//, 
                        //new VertexAttribute(Usage.ColorPacked, 4, "a_color")
                        );
                worldMeshBuilder.addMesh(meshPiece.setVertices(new float[] {
                        verticies[i][j].x, verticies[i][j].y, verticies[i][j].z, normal1.x, normal1.y, normal1.z,
                        verticies[i + 1][j].x, verticies[i + 1][j].y, verticies[i + 1][j].z, normal1.x, normal1.y, normal1.z, 
                        verticies[i][j + 1].x, verticies[i][j + 1].y, verticies[i][j + 1].z, normal1.x, normal1.y, normal1.z,
                    }));
                meshPiece = new Mesh(false, 18, 3, 
                        new VertexAttribute(Usage.Position, 3, "a_position"), 
                        new VertexAttribute(Usage.Normal, 3, "a_normal")//, 
                        //new VertexAttribute(Usage.ColorPacked, 4, "a_color")
                        );
                worldMeshBuilder.addMesh(meshPiece.setVertices(new float[] {
                        verticies[i + 1][j].x, verticies[i + 1][j].y, verticies[i + 1][j].z, normal2.x, normal2.y, normal2.z,
                        verticies[i + 1][j + 1].x, verticies[i + 1][j + 1].y, verticies[i + 1][j + 1].z, normal2.x, normal2.y, normal2.z, 
                        verticies[i][j + 1].x, verticies[i][j + 1].y, verticies[i][j + 1].z, normal2.x, normal2.y, normal2.z,
                    }));
            }
        }
    }
    returnWorld = modelBuilder.end();
    return returnWorld;
}

Problem is the new code isn't rendering anything... I have looked at the API for ModelBuilder, MeshBuilder, Mesh, and VertexAttribute/s but I can't figure out why it isn't working. Any help would be great as this has been a very frustrating day. Thank so much!

Emil Laine
  • 41,598
  • 9
  • 101
  • 157
jrdzha
  • 161
  • 2
  • 12
  • Have a look at https://github.com/libgdx/libgdx/blob/master/tests/gdx-tests/src/com/badlogic/gdx/tests/g3d/HeightMapTest.java which uses https://github.com/libgdx/libgdx/blob/master/tests/gdx-tests/src/com/badlogic/gdx/tests/g3d/HeightField.java, which looks like is what you're trying to do (it has the (boolean) option to be smooth or flat) – Xoppa Oct 13 '15 at 22:16
  • Ok thanks so much. I took a look at these examples, but it'll take me some time to dissect them. If I have any questions, could you answer them? – jrdzha Oct 17 '15 at 16:50

1 Answers1

6

Flat and smooth shading is usually determined by vertex normals and how colors are interpolated across the face.

Typically in a smooth model the normals at each vertex are averaged for each face point at the same location. The normal at each shared vertex location is the same for each face. This is what makes the lighting smooth because there are no abrupt changes at edges.

In a flat shaded model vertex normals do not match adjacent faces and instead each vertex normals is the same as face normal. This creates abrupt changes in the normal value at the edge.

This can typically be modified in whatever 3D modeling package you use or by slightly modifying your normal generation code if the terrain is generated procedurally.

The following picture shows the difference between flat and smooth normals. The darker blue lines represent the average smoothed normals and the lighter (cyan) lines represent the harsh face normal.

vertex vs faces

You should be able to draw all the triangles together in one draw call. You do not need to split the mesh.

Justin Meiners
  • 10,754
  • 6
  • 50
  • 92
  • 1
    If each vertex's normal needs to match the normal of the face, how can a vertex that is shared by multiple faces share a normal with all of them? Thanks for your quick response :) – jrdzha Oct 13 '15 at 01:24
  • 2
    @JahrudZ That is a good question, you are correct. If you use a glDrawElements style data structure then it is organized to share vertex data and you cannot specifiy a unique normal at each vertex, however with this model you also cannot specify unique UV coordinates which usually is required for most 3D models. You can instead duplicate vertex and normal data for each face and draw with glDrawArrays. If this doesn't make sense I can elaborate more. – Justin Meiners Oct 13 '15 at 01:45
  • @JahrudZ In your example it looks like you are already using a non-indexed glDrawArrays style data structure so you should just be able to modify the CalcNormal function. – Justin Meiners Oct 13 '15 at 02:00
  • The CalcNorm function just returns a Vector3 for 3 points (the 3 points on the triangle). Problem is that nothing is rendering with my "new" code :( Its just a blank screen – jrdzha Oct 13 '15 at 02:05
  • Yeah that is perfect you just set each of those three points to the face normal (your calcnormal probably already does that before averaging ) – Justin Meiners Oct 13 '15 at 02:48
  • Thank you so much Justin. This helped clear up alot of my questions. I just have one last thing: how would I set the normal of the vertex in libgdx? What is it that I'm doing wrong in the code titled "new"? – jrdzha Oct 13 '15 at 03:59
  • I will have to take a closer look at it tomorrow but I would not create separate vertex attribs for each triangle. If the original one works normally it looks like it should be able to do this. – Justin Meiners Oct 13 '15 at 05:49
  • Quick question, what is that screenshot from? – tobloef Oct 13 '15 at 11:47
  • Justin, thanks so much. It would help alot since I'm new to 3D and I dont have that much experience. TobLoef, the first 2 screenshots are from a game I'm working on atm. It's more of a proof of concept right now though as I'm still learning. The third screenshot is from google haha.. – jrdzha Oct 13 '15 at 15:25
  • @TobLoef I mocked it up in Blender, it doesn't quite show flat vertex normals the way I wanted, but its ok. – Justin Meiners Oct 13 '15 at 16:04
  • 1
    @JahrudZ Yeah no problem. Let me re-explain the shared vertex triangle concept more clearly. Right now in your code you define 3 vertices for each triangle. That means you actually have multiple vertices at the same locations (one for each adjacent triangle) along with multiple normals at each vertex. With averaged smooth normals the normal is the same for all vertices at a shared location. With flat normals they will be different for each face. – Justin Meiners Oct 13 '15 at 16:07
  • I think I understand what you are saying. Thats what I tried to implement in the new code. But since I'm very new to LibGDX, I'm not sure how to implement it in code. Thanks so much for your help, I cant begin to show how much I appreciate it :) – jrdzha Oct 13 '15 at 17:25