How should I optimize my graphics?

I’m trying to create a procedurally generated world. I have all the actual math and stuff worked out, but I noticed that I might not be creating the geometries the correct way. I create 2 * 16 * 16 triangles for each chunk (which then forms a node which I render), and each triangle may or may not have a different color (given through a material) that is dependent on the triangle’s average height.

I can already see a couple problems arising, namely the fact that I make a separate triangle and use different materials for each color. I wish to use only solid colors on the triangles but I haven’t been able to find a way to do it very efficiently. Basically, when I render more than one chunk it creates a bottleneck-like performance, with FPS going down to about 50 (previously from 100).

Are there any optimization techniques that you folks could suggest that I use? Here’s a picture of what exactly is going on:

Thanks for the help!

UPDATE: I figured out to use the GeometryBatchFactory.optimize(node) method and it boosted my FPS.

Forget about using one geometry per triangle. You need to batch it with at least few thousand triangles per geometry. You can use vertex color buffer to change colors of triangles.

1 Like

Yeah, what abies said. I almost choked when I saw your stats. 18,000+ uniforms may be the most I’ve ever seen. :slight_smile:

Note: if color is the only thing different in your materials then you are better off just embedding the color right into the mesh and using one material. Or at the very least if you don’t want to add a color buffer to your mesh then at least use only one material per color and batch those meshes.

[java]
public Tile(AssetManager manager, double h0, double h1, double h2, double h3) {

    assetManager = manager;

    meshOne = new Mesh();
    meshTwo = new Mesh();

    geometryOne = new Geometry("wireframeGeometry", meshOne);
    geometryTwo = new Geometry("wireframeGeometry", meshTwo);
    
    material = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");

    this.h0 = h0;
    this.h1 = h1;
    this.h2 = h2;
    this.h3 = h3;

    calculateDistance();
    calculateColors();
    
    createMesh();
    createGeometry();

    tileNode = new Node("tileNode");
    tileNode.attachChild(geometryOne);
    tileNode.attachChild(geometryTwo);
    
    GeometryBatchFactory.optimize(tileNode);
}

private static void calculateDistance() {
    double diff1 = Math.abs(h0 - h3) * Math.abs(h0 - h3);
    double diff2 = Math.abs(h1 - h2) * Math.abs(h1 - h2);

    double ans1 = Math.sqrt(2 + diff1);
    double ans2 = Math.sqrt(2 + diff2);

    if (ans1 <= ans2) {
        start = 1;
        color = 1;
    } else {
        start = 0;
        color = 1;
    }

    if ((h0 == h1 && h2 == h3) || (h0 == h2 && h1 == h3)) {
        color = 1;
    }
}

private static void createMesh() {
    Vector3f[] verticesOne = new Vector3f[3];
    Vector2f[] texCoordOne = new Vector2f[3];

    Vector3f[] verticesTwo = new Vector3f[3];
    Vector2f[] texCoordTwo = new Vector2f[3];

    int[] indexesOne = new int[3];
    int[] indexesTwo = new int[3];

    float[] normalsOne = new float[9];
    float[] normalsTwo = new float[9];
    
    if (start == 0) {
        verticesOne[0] = new Vector3f(0 * scale, (float) (h0 * scale) / 10, 0 * scale);
        verticesOne[1] = new Vector3f(1 * scale, (float) (h1 * scale) / 10, 0 * scale);
        verticesOne[2] = new Vector3f(0 * scale, (float) (h2 * scale) / 10, 1 * scale);

        verticesTwo[0] = new Vector3f(0 * scale, (float) (h2 * scale) / 10, 1 * scale);
        verticesTwo[1] = new Vector3f(1 * scale, (float) (h3 * scale) / 10, 1 * scale);
        verticesTwo[2] = new Vector3f(1 * scale, (float) (h1 * scale) / 10, 0 * scale);

        texCoordOne[0] = new Vector2f(0, 0);
        texCoordOne[1] = new Vector2f(1, 0);
        texCoordOne[2] = new Vector2f(0, 1);

        texCoordTwo[0] = new Vector2f(0, 1);
        texCoordTwo[1] = new Vector2f(1, 1);
        texCoordTwo[2] = new Vector2f(1, 0);
        
        int[] tempOne = {2, 1, 0};
        int[] tempTwo = {0, 1, 2};
        
        indexesOne = tempOne;
        indexesTwo = tempTwo;
        
        Vector3f edgeOne1 = verticesOne[2].subtract(verticesOne[0]);
        Vector3f edgeOne2 = verticesOne[1].subtract(verticesOne[0]);
        Vector3f normalOne = edgeOne1.cross(edgeOne2);

        Vector3f edgeTwo1 = verticesTwo[0].subtract(verticesTwo[2]);
        Vector3f edgeTwo2 = verticesTwo[1].subtract(verticesTwo[2]);
        Vector3f normalTwo = edgeTwo1.cross(edgeTwo2);

        float getXOne = normalOne.get(0);
        float getYOne = normalOne.get(1);
        float getZOne = normalOne.get(2);

        float getXTwo = normalTwo.get(0);
        float getYTwo = normalTwo.get(1);
        float getZTwo = normalTwo.get(2);

        normalsOne = new float[] {getXOne,getYOne,getZOne, getXOne,getYOne,getZOne, getXOne,getYOne,getZOne};
        normalsTwo = new float[] {getXTwo,getYTwo,getZTwo, getXTwo,getYTwo,getZTwo, getXTwo,getYTwo,getZTwo};
    } else {
        verticesOne[0] = new Vector3f(0 * scale, (float) (h0 * scale) / 10, 0 * scale);
        verticesOne[1] = new Vector3f(0 * scale, (float) (h2 * scale) / 10, 1 * scale);
        verticesOne[2] = new Vector3f(1 * scale, (float) (h3 * scale) / 10, 1 * scale);

        verticesTwo[0] = new Vector3f(1 * scale, (float) (h3 * scale) / 10, 1 * scale);
        verticesTwo[1] = new Vector3f(1 * scale, (float) (h1 * scale) / 10, 0 * scale);
        verticesTwo[2] = new Vector3f(0 * scale, (float) (h0 * scale) / 10, 0 * scale);

        texCoordOne[0] = new Vector2f(0, 0);
        texCoordOne[1] = new Vector2f(0, 1);
        texCoordOne[2] = new Vector2f(1, 1);

        texCoordTwo[0] = new Vector2f(1, 1);
        texCoordTwo[1] = new Vector2f(1, 0);
        texCoordTwo[2] = new Vector2f(0, 0);

        int[] tempOne = {0, 1, 2};
        int[] tempTwo = {0, 1, 2};

        indexesOne = tempOne;
        indexesTwo = tempTwo;
        
        Vector3f edgeOne1 = verticesOne[0].subtract(verticesOne[2]);
        Vector3f edgeOne2 = verticesOne[1].subtract(verticesOne[2]);
        Vector3f normalOne = edgeOne1.cross(edgeOne2);

        Vector3f edgeTwo1 = verticesTwo[0].subtract(verticesTwo[2]);
        Vector3f edgeTwo2 = verticesTwo[1].subtract(verticesTwo[2]);
        Vector3f normalTwo = edgeTwo1.cross(edgeTwo2);

        float getXOne = normalOne.get(0);
        float getYOne = normalOne.get(1);
        float getZOne = normalOne.get(2);

        float getXTwo = normalTwo.get(0);
        float getYTwo = normalTwo.get(1);
        float getZTwo = normalTwo.get(2);

        normalsOne = new float[] {getXOne,getYOne,getZOne, getXOne,getYOne,getZOne, getXOne,getYOne,getZOne};
        normalsTwo = new float[] {getXTwo,getYTwo,getZTwo, getXTwo,getYTwo,getZTwo, getXTwo,getYTwo,getZTwo};
    }
    
    meshOne.setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(verticesOne));
    meshOne.setBuffer(Type.TexCoord, 2, BufferUtils.createFloatBuffer(texCoordOne));
    meshOne.setBuffer(Type.Index, 1, BufferUtils.createIntBuffer(indexesOne));
    meshOne.setBuffer(Type.Normal, 3, BufferUtils.createFloatBuffer(normalsOne));
    meshOne.updateBound();

    meshTwo.setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(verticesTwo));
    meshTwo.setBuffer(Type.TexCoord, 2, BufferUtils.createFloatBuffer(texCoordTwo));
    meshTwo.setBuffer(Type.Index, 1, BufferUtils.createIntBuffer(indexesTwo));
    meshTwo.setBuffer(Type.Normal, 3, BufferUtils.createFloatBuffer(normalsTwo));
    meshTwo.updateBound();
}

private static void createGeometry() {
    material.setBoolean("UseMaterialColors", true);
    
    if (color == 0) {
        material.setColor("Diffuse", colorRGB1);
        geometryOne.setMaterial(material);
        
        material.setColor("Diffuse", colorRGB2);
        geometryTwo.setMaterial(material);
        
    } else {
        material.setColor("Diffuse", colorRGB1);
        geometryOne.setMaterial(material);
        
        material.setColor("Diffuse", colorRGB2);
        geometryTwo.setMaterial(material);
    }
}

private static void calculateColors() {
    double totalOne = 0;
    double totalTwo = 0;
    
    double[] valOne = new double[3];
    double[] valTwo = new double[3];
    
    if (color == 1) {
        totalOne = (h0 + h1 + h2 + h3) / 4;
        totalTwo = totalOne; 
    } else if (start == 1) {
        totalOne = ((h0 * 45) + (h2 * 90) + (h3 * 45)) / 180;
        totalTwo = ((h0 * 45) + (h1 * 90) + (h3 * 45)) / 180;
    } else if (start == 0) {
        totalOne = ((h1 * 45) + (h0 * 90) + (h2 * 45)) / 180;
        totalTwo = ((h1 * 45) + (h3 * 90) + (h2 * 45)) / 180;
    }
    
    valOne[0] = (140 - (totalOne % 32)) / 255;
    valOne[1] = (155 - (totalOne % 16)) / 255;
    valOne[2] = (140 - (totalOne % 8)) / 255;

    valTwo[0] = (140 - (totalTwo % 32)) / 255;
    valTwo[1] = (155 - (totalTwo % 16)) / 255;
    valTwo[2] = (140 - (totalTwo % 8)) / 255;
    
    colorRGB1 = new ColorRGBA((float) valOne[0], (float) valOne[1], (float) valOne[2], 1);
    colorRGB2 = new ColorRGBA((float) valTwo[0], (float) valTwo[1], (float) valTwo[2], 1);
}

[/java]

Take a look at the materials just in case I’m not describing it all correctly. This is the tile class, which is what makes up a chunk; the chunk optimizes the geometry.

I know it’s been mentioned already, but setting the vertex color will allow you to use a single material for all tiles.

The number of traingles/vertices in the stats of the scene look like approximately a single terrain tile, however there are 2000+ objects in your scene even after accounting for the statistics panel in the hud.

I’m not exactly sure how you are determining the terrain vertex placement, but perhaps a more traditional approach using a noise function would help.

Another thing you should probably get into the habit of doing right away, is to reuse objects like Vector3f’s. example:

[java]
Vector3f tempV3 = new Vector3f();

// Meanwhile, back in the bat cave
tempV3.set(5,5,5);

// As appossed to:
Vector3f anotherInstanceOfVector3f = new Vector3f(5,5,5);
[/java]

And this section here:

[java]
if (color == 0) {
material.setColor(“Diffuse”, colorRGB1);
geometryOne.setMaterial(material);

        material.setColor(“Diffuse”, colorRGB2);
        geometryTwo.setMaterial(material);

    } else {
        material.setColor(“Diffuse”, colorRGB1);
        geometryOne.setMaterial(material);

        material.setColor(“Diffuse”, colorRGB2);
        geometryTwo.setMaterial(material);
    }

[/java]

I’m surprised this works… When you update the uniform on the material, this should be visible on all meshes using this material as you are not cloning it.

Anyways… look into extending the mesh class, using a noise function and ensure that you can pass in a “chunk size” for the tile to easily change the tile size so u can find the “sweet spot” between number of visible tiles/vertices per tile and visible distance trade-off.

Reason being–performance will very based on: Number of verts per object, the number of objects being rendered… so 12 x 12 tiles with 40 verts each may perform terrible where 6 x 6 tiles with 160 verts each might perform great.

@t0neg0d

My program sort of works like this: upon initialization, it creates a Chunk (from the Chunk class); the Chunk class goes through a for loop that creates a 4 x 4 pseudo grid of Tiles. What you were seeing above was the majority of the Tile code. The Chunk collects the Node from each tile, offsets it by the width and height to patch them together, does a geometry batch optimization, and creates a Node for the Chunk. The Chunk’sNode is returned and is rendered.

I was planning on extending either Mesh or Node for both Tile and Chunk but wasn’t sure how much it would improve it. I’m going to do it regardless but I was unsure as to which one i should extend because I am unfamiliar with how I would go about combining meshes with different colors, etc.

The Chunk constrictor actually takes in a float array for height vales that are created from a SimplexNoise generator. That’s how I’ve been getting the vertexes and such.

Would there be a better way to create the Tile other than just sort of gluing them together into one Node?

You actually should not extend Geometry (or Node or Spatial) at all and just make a Control that updates it.

@t0neg0d

You mentioned that I should only be using one Material. Would only instancing one material like I did in the code count as doing that?

I don’t even have to see the code to know that you are doing something wrong with mesh generation. This was enough:

This says you are rendering 2080 objects. That’s a lot of objects.

It also says that each of those objects (on average) only has 1.2 triangles.

It also implies that each triangle has its own Material with its own set of uniforms. About 9 uniforms per object.

This is the worst way to construct a scene. It’s the equivalent of mailing individual pennies one day at a time to pay your power bill versus just sending actual money.

Bottom line:
batch your meshes into more than one triangle per mesh. Preferably thousands of triangles per mesh.

Material m1 = new Material(…)
Material m2 = new Material(…)
…is creating two different materials. Not the same. Two different material instances each with their own sets of uniforms.

Material m1 = new Material(…)
Material m2 = m1;
…is reusing a material.

But if you are only instantiating different materials to set the color… that’s the least efficient way. Put the color in the mesh and make bigger meshes and only have one material.

I think that about covers everything, repeating again as necessary.

I now understand the magnatude of my errors. You mentioned that I have an immense amount of objects; would batching the meshes have any impact on the object count? Nonetheless, thanks a ton for your help. You got my head fastened on for the time being.

I have a follow-up question. Would combining the meshes into one big mesh have any interference in the future implementation of frustrum culling?

Once again, thanks so much for your guidance!

@mjbmitch said: I now understand the magnatude of my errors. You mentioned that I have an immense amount of objects; would batching the meshes have any impact on the object count? Nonetheless, thanks a ton for your help. You got my head fastened on for the time being.

I have a follow-up question. Would combining the meshes into one big mesh have any interference in the future implementation of frustrum culling?

Once again, thanks so much for your guidance!

You want to batch… but you don’t want to go totally crazy about it either. It’s a balancing act.

Something to keep in mind, though: frustum culling is done on the CPU… on every object in the scene graph. So if you don’t have that many vertexes overall (and in this case you don’t have many) then it may make sense to just batch everything. You will save CPU time and your GPU won’t even break a sweat. Still, a spatially organized scene graph may have other benefits so you probably want to pick a reasonable “chunk” size and batch everything in the chunk. Where “reasonable” may have to be determined through experimentation.

<cite>@pspeed said:</cite> You want to batch... but you don't want to go totally crazy about it either. It's a balancing act.

Something to keep in mind, though: frustum culling is done on the CPU… on every object in the scene graph. So if you don’t have that many vertexes overall (and in this case you don’t have many) then it may make sense to just batch everything. You will save CPU time and your GPU won’t even break a sweat. Still, a spatially organized scene graph may have other benefits so you probably want to pick a reasonable “chunk” size and batch everything in the chunk. Where “reasonable” may have to be determined through experimentation.

I’ve definitely made the mistake on not letting culling do it job… and this is super solid advice.

Batching is one approach, however, if you are looking for smooth terrain, you will want to share triangle verts and average the normals using the surrounding verts on the one you are calculating. This would look like:

...
!!!
!!!

The center vert is not repeated 4 times, it is referenced 4 times in the index buffer for the 4 triangles that use the vertex. Calculating the normal for the single vert uses the surrounding 8 verts and averages/normalizes their positions to find a common normal between all positions.

Now, to set the color of the triangles, you attach a vertex color buffer to the mesh defining the color of each vertex (based on height I assume) and let the shader interpolate the blend to the next vertex. This will allow you to use a single material and actually, you won’t have to set any uniforms for color information. You can also use the vertex color blend to apply height based texture splatting… if that’s what you want… though, I think your terrain looks cool the way you have it.

Like @normen said, the docs say don’t extend Node, etc… reason being, is it is impossible for the devs to support core code if you modify it. However, extending Mesh is common and I “believe” even used as an example in the tutorials.

You entire approach (in concept) is spot on for tiled terrain… however, JME (like all other engines) run best under certain circumstances… in this case fewer objects containing larger numbers of vertices will perform better than lots of objects containing smaller numbers of vertices.

THANKS A TON FOR THE HELP!

Here’s the progress I’ve made so far in terms of optimization:

http://i.imgur.com/bydwrHw.png

1 Like
<cite>@mjbmitch said:</cite> THANKS A TON FOR THE HELP!

Here’s the progress I’ve made so far in terms of optimization:

http://i.imgur.com/bydwrHw.png

Complete awesomeness!

@mjbmitch
For future reference, if you turn this into an endless terrain generator… you’ll probably get to the point of defining levels of detail as you generate each tile. Since terrain is grid based (most of the time), it makes more sense to do this yourself. Anyways… point being, @pspeed mentioned something to me when I was playing around with this as well and it worked out very well for hiding gaps between LODs. Do a forum search on terrain skirts (or skirting or something of the like)… really cool, simple way of removing the inevitable gaps between tiles.

<cite>@t0neg0d said:</cite> @mjbmitch For future reference, if you turn this into an endless terrain generator.... you'll probably get to the point of defining levels of detail as you generate each tile. Since terrain is grid based (most of the time), it makes more sense to do this yourself. Anyways... point being, @pspeed mentioned something to me when I was playing around with this as well and it worked out very well for hiding gaps between LODs. Do a forum search on terrain skirts (or skirting or something of the like)... really cool, simple way of removing the inevitable gaps between tiles.

Thanks a ton for mentioning this! I thought about doing LoDs but realized that there would be some gaps and wasn’t sure how to fix them (and didn’t have the thought to Google “terrain skirting”). Thanks!

Unfortunately, it seems I’m going to have to get a grasp on LoDs before I can implement terrain skirting.

<cite>@mjbmitch said:</cite> Thanks a ton for mentioning this! I thought about doing LoDs but realized that there would be some gaps and wasn't sure how to fix them (and didn't have the thought to Google "terrain skirting"). Thanks!

Unfortunately, it seems I’m going to have to get a grasp on LoDs before I can implement terrain skirting.

LoDs for terrain tiles are pretty simple:

Let’s say you had a 41 x 41 vert tile size with a space of 1 unit (or 1f) between each vert. (the vert count needs to be odd… keep this in mind while proceeding)
To define a new LoD you could simply define a secondary index buffer using every other vertex.
For the next level, you would use every 4th or 5th vertex

To change level of detail, you just swap out the index buffer and whooohooo… simpler version of the same tile.

For skirting… the easiest aproach is to duplicate the outter edge vertices and pull them straight down along the y axis. The only real hurdle this creates is remembering that calculating the normals needs to ignore these extra verts.

If you are generating the height map via noise function, you can just set up the params to include vert positions outside of the area you are using for the current tile (a border of height info basically) and use these to calc the normal for the borders of the tile.

1 Like