0

I am trying to develop a game where I am rendering up to 300 cubes on screen. The performance of modelBatch when creating new modelInstance for each cube is terrible. There is no 3d batch that batches all the cubes to one draw call as far as I know. So I am desperately trying to batch them somehow.

This question is directly related to this one: LibGDX 3D increase perfomance

The answer posted batches all the cubes successfully but when environment is added to get some lighting it appears that the cubes have missing sides or something else is wrong with them.

Heres a picture:

enter image description here

Here is my cube class (pretty much copied from answer above)

public class Cube {

  int index;
  int vertexFloatSize;
  int posOffset;
  int norOffset;
  boolean hasColor;
  int colOffset;
  private Vector3 position = new Vector3();
  private Matrix4 rotationTransform = new Matrix4().idt();

  private Color color = new Color();
  public float halfWidth, halfHeight, halfDepth;
  private boolean transformDirty = false;
  private boolean colorDirty = false;

  static final Vector3 CORNER000 = new Vector3();
  static final Vector3 CORNER010 = new Vector3();
  static final Vector3 CORNER100 = new Vector3();
  static final Vector3 CORNER110 = new Vector3();
  static final Vector3 CORNER001 = new Vector3();
  static final Vector3 CORNER011 = new Vector3();
  static final Vector3 CORNER101 = new Vector3();
  static final Vector3 CORNER111 = new Vector3();

  static final Vector3[] FACE0 = {CORNER000, CORNER100, CORNER110, CORNER010};
  static final Vector3[] FACE1 = {CORNER101, CORNER001, CORNER011, CORNER111};
  static final Vector3[] FACE2 = {CORNER000, CORNER010, CORNER011, CORNER001};
  static final Vector3[] FACE3 = {CORNER101, CORNER111, CORNER110, CORNER100};
  static final Vector3[] FACE4 = {CORNER101, CORNER100, CORNER000, CORNER001};
  static final Vector3[] FACE5 = {CORNER110, CORNER111, CORNER011, CORNER010};
  static final Vector3[][] FACES = {FACE0, FACE1, FACE2, FACE3, FACE4, FACE5};

  static final Vector3 NORMAL0 = new Vector3();
  static final Vector3 NORMAL1 = new Vector3();
  static final Vector3 NORMAL2 = new Vector3();
  static final Vector3 NORMAL3 = new Vector3();
  static final Vector3 NORMAL4 = new Vector3();
  static final Vector3 NORMAL5 = new Vector3();
  static final Vector3[] NORMALS = {NORMAL0, NORMAL1, NORMAL2, NORMAL3, NORMAL4, NORMAL5};

  float[] meshVertices;

  public Cube(float x, float y, float z, float width, float height, float depth, int index, 
        VertexAttributes vertexAttributes, float[] meshVertices){

      position.set(x,y,z);
      this.halfWidth = width/2;
      this.halfHeight = height/2;
      this.halfDepth = depth/2;
      this.index = index;
      this.meshVertices = meshVertices;

      NORMAL0.set(0,0,-1);
      NORMAL1.set(0,0,1);
      NORMAL2.set(-1,0,0);
      NORMAL3.set(1,0,0);
      NORMAL4.set(0,-1,0);
      NORMAL5.set(0,1,0);

      vertexFloatSize = vertexAttributes.vertexSize/4; //4 bytes per float
      posOffset = getVertexAttribute(Usage.Position, vertexAttributes).offset/4;
      norOffset = getVertexAttribute(Usage.Normal, vertexAttributes).offset/4;

      VertexAttribute colorAttribute = getVertexAttribute(Usage.Color, vertexAttributes);
      hasColor = colorAttribute!=null;
      if (hasColor){
          colOffset = colorAttribute.offset/4;
          this.setColor(Color.WHITE);
      }
      transformDirty = true;

  }

  public void setDimensions(float x, float y , float z){

      this.halfWidth = x/2;
      this.halfHeight = y/2;
      this.halfDepth = z/2;

  }

  public void setIndex(int index){

      this.index = index;
      transformDirty = true;
      colorDirty = true;

  }

  /**
   * Call this after moving and/or rotating.
   */
  public void update(){

      if (colorDirty && hasColor){
          for (int faceIndex= 0; faceIndex<6; faceIndex++){
              int baseVertexIndex = (index*24 + faceIndex*4)*vertexFloatSize;//24 unique vertices per cube, 4 unique vertices per face
              for (int cornerIndex=0; cornerIndex<4; cornerIndex++){
                  int vertexIndex = baseVertexIndex + cornerIndex*vertexFloatSize + colOffset;
                  meshVertices[vertexIndex] = color.r;
                  meshVertices[++vertexIndex] = color.g;
                  meshVertices[++vertexIndex] = color.b;
                  meshVertices[++vertexIndex] = color.a;
              }
          }
          colorDirty = false;
      }


      if (!transformDirty){
          return;
      }

      transformDirty = false;

      CORNER000.set(-halfWidth,-halfHeight,-halfDepth).rot(rotationTransform).add(position);
      CORNER010.set(-halfWidth,halfHeight,-halfDepth).rot(rotationTransform).add(position);
      CORNER100.set(halfWidth,-halfHeight,-halfDepth).rot(rotationTransform).add(position);
      CORNER110.set(halfWidth,halfHeight,-halfDepth).rot(rotationTransform).add(position);
      CORNER001.set(-halfWidth,-halfHeight,halfDepth).rot(rotationTransform).add(position);
      CORNER011.set(-halfWidth,halfHeight,halfDepth).rot(rotationTransform).add(position);
      CORNER101.set(halfWidth,-halfHeight,halfDepth).rot(rotationTransform).add(position);
    CORNER111.set(halfWidth,halfHeight,halfDepth).rot(rotationTransform).add(position);

      NORMAL0.set(0,0,-1).rot(rotationTransform);
      NORMAL1.set(0,0,1).rot(rotationTransform);
      NORMAL2.set(-1,0,0).rot(rotationTransform);
      NORMAL3.set(1,0,0).rot(rotationTransform);
      NORMAL4.set(0,-1,0).rot(rotationTransform);
      NORMAL5.set(0,1,0).rot(rotationTransform);

      for (int faceIndex= 0; faceIndex<6; faceIndex++){
          int baseVertexIndex = (index*24 + faceIndex*4)*vertexFloatSize;//24 unique vertices per cube, 4 unique vertices per face
          for (int cornerIndex=0; cornerIndex<4; cornerIndex++){
              int vertexIndex = baseVertexIndex + cornerIndex*vertexFloatSize + posOffset;
              meshVertices[vertexIndex] = FACES[faceIndex][cornerIndex].x;
              meshVertices[++vertexIndex] = FACES[faceIndex][cornerIndex].y;
              meshVertices[++vertexIndex] = FACES[faceIndex][cornerIndex].z;

              vertexIndex = baseVertexIndex + cornerIndex*vertexFloatSize + norOffset;
              meshVertices[vertexIndex] = NORMALS[faceIndex].x;
              meshVertices[++vertexIndex] = NORMALS[faceIndex].y;
              meshVertices[++vertexIndex] = NORMALS[faceIndex].z;
          }
      }
  }

  public Cube setColor(Color color){

      if (hasColor){
          this.color.set(color);
          colorDirty = true;
      }
      return this;
  }

  public void setAlpha(float alpha) {

       if (hasColor){

           this.color.set(this.color.r, this.color.g, this.color.b, alpha);
           colorDirty = true;

       }
  }

  public Cube translate(float x, float y, float z){
      position.add(x,y,z);
      transformDirty = true;
      return this;
  }

  public Cube setPosition(float x, float y, float z){
      position.set(x,y,z);
      transformDirty = true;
      return this;
  }

public Cube setPosition(Vector3 position1) {

       position.set(position1);
       transformDirty = true;
       return this;

  }

  public VertexAttribute getVertexAttribute (int usage, VertexAttributes attributes) {
      int len = attributes.size();
      for (int i = 0; i < len; i++)
          if (attributes.get(i).usage == usage) return attributes.get(i);

      return null;
  }



  public Vector3 getPosition() {

      return this.position;
  }

}

And here is a test case that i created to test the cubes.

public class TestCase {

    ModelInstance mBatchedCubesModelInstance;
    Mesh mBatchedCubesMesh;
    float[] mBatchedCubesVertices;
    Array<Cube> mBatchedCubes;

    TestCase(){

            int width = 5;
            int height = 5;
            int length = 5;
            int numCubes = width*height*length;

            ModelBuilder mb = new ModelBuilder();
            mb.begin();
            MeshPartBuilder mpb = mb.part("cubes", GL20.GL_TRIANGLES, (Usage.Position | Usage.Normal | Usage.Color), new Material());
            for (int i=0; i<numCubes; i++){
                mpb.box(1, 1, 1);
            }
            Model model = mb.end();
            mBatchedCubesModelInstance = new ModelInstance(model);

            mBatchedCubesMesh = model.meshes.get(0);
            VertexAttributes vertexAttributes = mBatchedCubesMesh.getVertexAttributes();
            int vertexFloatSize = vertexAttributes .vertexSize / 4; //4 bytes per float
            mBatchedCubesVertices = new float[numCubes * 24 * vertexFloatSize]; //24 unique vertices per cube
            mBatchedCubesMesh.getVertices(mBatchedCubesVertices);

            mBatchedCubes = new Array<Cube>(numCubes);
            int cubeNum = 0;
            for (int x = 0; x < width; x++) {
                    for (int y = 0; y < height; y++) {
                            for (int z = 0; z < length; z++) {
                                    mBatchedCubes.add(new Cube((x-(width/2f))*1.5f, -((y-(height/2f)) * 1.5f), -(z-(length/2f))*1.5f, 1,1,1, cubeNum++, vertexAttributes, mBatchedCubesVertices ).setColor(Colors.getMixedColor()));
                            }
                    }
            }

    }

    public void render(ModelBatch batch, Environment environment){

            for (Cube cube : mBatchedCubes){ //must update any changed cubes.
                cube.update();
            }
            mBatchedCubesMesh.setVertices(mBatchedCubesVertices); //apply changes to mesh

            if(environment!=null) batch.render(mBatchedCubesModelInstance,environment);
            else batch.render(this.mBatchedCubesModelInstance);

    }

}

The cubes don't even move in my game so I don't even need the transformation. I just need to set the colors (including alpha channel) every frame. The batched mesh should work with libgdx shadow mapping also (I hope it automatically works when you have a properly batched modelInstance).

Could anyone tell me what is wrong with my code and why some faces don't show up. I understand I might be asking much so I will put a bounty on this question (50 points) after two days.

EDIT: After the answer from Tenfour04 things got a lot better. Alpha channel seems to work, strange face problem seems to have disappeared. However When I applied the changes to my real game I noticed that sometimes flour draws on top of in game terrain. I updated the test to illustrate the problem by adding big plane in the middle of cubes. Made a video: https://www.youtube.com/watch?v=LQhSMJfuyZY.

I would also like to clarify that I am not using any custom shader. Using only ModelBatch.begin() and .end() methods with no additional openGl calls.

Community
  • 1
  • 1
  • Assuming the floor is supposed to be transparent, you need to sort it into the middle of your cubes. Since it has different dimensions, it would probably be easiest to just make separate Models for the cubes above and below the floor. Then submit the three models in order from far to near. But if your floor is opaque, then you just need to draw it before you draw the transparent stuff. The trouble with transparent stuff is that you can never rely on depth testing...you have to draw all of it from far to near. Can't go back and insert something in the middle. – Tenfour04 Jul 15 '14 at 21:18
  • If the floor is intersecting multiple layers of cubes, you might need to break it up into six individual rectangles and break up the cube layers into more models so you can adequately draw them in order. – Tenfour04 Jul 15 '14 at 21:19
  • Lets forget the transparency for a bit now. All the cubes in the video are fully opaque. They are sorted every frame just the way you said to. If I understand depth testing at least a little bit correctly I think that no depth testing is happening somehow. In the video some cubes should go half way through the big plane. And you can't see that happening they are either drawn fully in front of the big plane or aren't drawn at all. – Justas Sakalauskas Jul 15 '14 at 21:37
  • This line I gave you: `new DepthTestAttribute(false)` turns off depth testing. You have to turn it off if drawing transparent cubes. – Tenfour04 Jul 15 '14 at 23:50
  • Ahh totally missed that. Finally fixed things in my game to look the way they were before i tried to batch things up... yay!I turned the depth testing on. Without depth testing things looked good but when with shadows they messed up again a little bit. Depth testing plus sorting worked great for my games case. Thank you very much! You helped so much. – Justas Sakalauskas Jul 16 '14 at 21:25

1 Answers1

1

I see a couple problems:

  1. You have blending off and/or your shader isn't setting the alpha value

  2. You aren't culling back faces.

So when you create your new material, instead of just using new Material(), use:

new Material(
    IntAttribute.createCullFace(GL20.GL_FRONT),//For some reason, libgdx ModelBuilder makes boxes with faces wound in reverse, so cull FRONT
    new BlendingAttribute(1f), //opaque since multiplied by vertex color
    new DepthTestAttribute(false), //don't want depth mask or rear cubes might not show through
    ColorAttribute.createDiffuse(Color.WHITE) //white since multiplied by vertex color
    );

You will also need to sort the cubes by distance from camera to get their alphas to layer correctly. Here's an updated Cube class that supports sorting. It has to be able to track color independently from the vertices array in case its index changes:

public class Cube implements Comparable<Cube>{

    private int index;
    int vertexFloatSize;
    int posOffset;
    int norOffset;
    boolean hasColor;
    int colOffset;
    private Vector3 position = new Vector3();
    private Matrix4 rotationTransform = new Matrix4().idt();
    public float halfWidth, halfHeight, halfDepth;
    private boolean transformDirty = false;
    private boolean colorDirty = false;
    private Color color = new Color();
    float camDistSquared;

    static final Vector3 CORNER000 = new Vector3();
    static final Vector3 CORNER010 = new Vector3();
    static final Vector3 CORNER100 = new Vector3();
    static final Vector3 CORNER110 = new Vector3();
    static final Vector3 CORNER001 = new Vector3();
    static final Vector3 CORNER011 = new Vector3();
    static final Vector3 CORNER101 = new Vector3();
    static final Vector3 CORNER111 = new Vector3();

    static final Vector3[] FACE0 = {CORNER000, CORNER100, CORNER110, CORNER010};
    static final Vector3[] FACE1 = {CORNER101, CORNER001, CORNER011, CORNER111};
    static final Vector3[] FACE2 = {CORNER000, CORNER010, CORNER011, CORNER001};
    static final Vector3[] FACE3 = {CORNER101, CORNER111, CORNER110, CORNER100};
    static final Vector3[] FACE4 = {CORNER101, CORNER100, CORNER000, CORNER001};
    static final Vector3[] FACE5 = {CORNER110, CORNER111, CORNER011, CORNER010};
    static final Vector3[][] FACES = {FACE0, FACE1, FACE2, FACE3, FACE4, FACE5};

    static final Vector3 NORMAL0 = new Vector3();
    static final Vector3 NORMAL1 = new Vector3();
    static final Vector3 NORMAL2 = new Vector3();
    static final Vector3 NORMAL3 = new Vector3();
    static final Vector3 NORMAL4 = new Vector3();
    static final Vector3 NORMAL5 = new Vector3();
    static final Vector3[] NORMALS = {NORMAL0, NORMAL1, NORMAL2, NORMAL3, NORMAL4, NORMAL5};

    public Cube(float x, float y, float z, float width, float height, float depth, int index, 
        VertexAttributes vertexAttributes, float[] meshVertices){
    position.set(x,y,z);
    this.halfWidth = width/2;
    this.halfHeight = height/2;
    this.halfDepth = depth/2;
    this.index = index;


    vertexFloatSize = vertexAttributes.vertexSize/4; //4 bytes per float
    posOffset = getVertexAttribute(Usage.Position, vertexAttributes).offset/4;
    norOffset = getVertexAttribute(Usage.Normal, vertexAttributes).offset/4;

    VertexAttribute colorAttribute = getVertexAttribute(Usage.Color, vertexAttributes);
    hasColor = colorAttribute!=null;
    if (hasColor){
        colOffset = colorAttribute.offset/4;
        this.setColor(Color.WHITE, meshVertices);
    }
    transformDirty = true;
    }

    public void updateCameraDistance(Camera cam){
    camDistSquared = cam.position.dst2(position);
    }

    /**
     * Call this after moving and/or rotating.
     */
    public void update(float[] meshVertices){

    if (transformDirty){
        transformDirty = false;

        CORNER000.set(-halfWidth,-halfHeight,-halfDepth).rot(rotationTransform).add(position);
        CORNER010.set(-halfWidth,halfHeight,-halfDepth).rot(rotationTransform).add(position);
        CORNER100.set(halfWidth,-halfHeight,-halfDepth).rot(rotationTransform).add(position);
        CORNER110.set(halfWidth,halfHeight,-halfDepth).rot(rotationTransform).add(position);
        CORNER001.set(-halfWidth,-halfHeight,halfDepth).rot(rotationTransform).add(position);
        CORNER011.set(-halfWidth,halfHeight,halfDepth).rot(rotationTransform).add(position);
        CORNER101.set(halfWidth,-halfHeight,halfDepth).rot(rotationTransform).add(position);
        CORNER111.set(halfWidth,halfHeight,halfDepth).rot(rotationTransform).add(position);

        NORMAL0.set(0,0,-1).rot(rotationTransform);
        NORMAL1.set(0,0,1).rot(rotationTransform);
        NORMAL2.set(-1,0,0).rot(rotationTransform);
        NORMAL3.set(1,0,0).rot(rotationTransform);
        NORMAL4.set(0,-1,0).rot(rotationTransform);
        NORMAL5.set(0,1,0).rot(rotationTransform);

        for (int faceIndex= 0; faceIndex<6; faceIndex++){
        int baseVertexIndex = (index*24 + faceIndex*4)*vertexFloatSize;//24 unique vertices per cube, 4 unique vertices per face
        for (int cornerIndex=0; cornerIndex<4; cornerIndex++){
            int vertexIndex = baseVertexIndex + cornerIndex*vertexFloatSize + posOffset;
            meshVertices[vertexIndex] = FACES[faceIndex][cornerIndex].x;
            meshVertices[++vertexIndex] = FACES[faceIndex][cornerIndex].y;
            meshVertices[++vertexIndex] = FACES[faceIndex][cornerIndex].z;

            vertexIndex = baseVertexIndex + cornerIndex*vertexFloatSize + norOffset;
            meshVertices[vertexIndex] = NORMALS[faceIndex].x;
            meshVertices[++vertexIndex] = NORMALS[faceIndex].y;
            meshVertices[++vertexIndex] = NORMALS[faceIndex].z;
        }
        }
    }

    if (colorDirty){
        colorDirty = false;

        for (int faceIndex= 0; faceIndex<6; faceIndex++){
        int baseVertexIndex = (index*24 + faceIndex*4)*vertexFloatSize;//24 unique vertices per cube, 4 unique vertices per face
        for (int cornerIndex=0; cornerIndex<4; cornerIndex++){
            int vertexIndex = baseVertexIndex + cornerIndex*vertexFloatSize + colOffset;
            meshVertices[vertexIndex] = color.r;
            meshVertices[++vertexIndex] = color.g;
            meshVertices[++vertexIndex] = color.b;
            meshVertices[++vertexIndex] = color.a;
        }
        }
    }
    }

    public Cube setColor(Color color, float[] meshVertices){
    if (hasColor){
        this.color.set(color);
        colorDirty = true;

    }
    return this;
    }

    public void setIndex(int index){
    if (this.index != index){
        transformDirty = true;
        colorDirty = true;
        this.index = index;
    }
    }

    public Cube translate(float x, float y, float z){
    position.add(x,y,z);
    transformDirty = true;
    return this;
    }

    public Cube translateTo(float x, float y, float z){
    position.set(x,y,z);
    transformDirty = true;
    return this;
    }

    public Cube rotate(float axisX, float axisY, float axisZ, float degrees){
    rotationTransform.rotate(axisX, axisY, axisZ, degrees);
    transformDirty = true;
    return this;
    }

    public Cube rotateTo(float axisX, float axisY, float axisZ, float degrees){
    rotationTransform.idt();
    rotationTransform.rotate(axisX, axisY, axisZ, degrees);
    transformDirty = true;
    return this;
    }

    public VertexAttribute getVertexAttribute (int usage, VertexAttributes attributes) {
    int len = attributes.size();
    for (int i = 0; i < len; i++)
        if (attributes.get(i).usage == usage) return attributes.get(i);

    return null;
    }

    @Override
    public int compareTo(Cube other) {
    //the cube has a lower index than a cube that is closer to the camera
    if (camDistSquared>other.camDistSquared)
        return -1;
    return camDistSquared<other.camDistSquared ? 1 : 0;
    }
}

And you would sort it like this:

for (Cube cube : mBatchedCubes){
    cube.updateCameraDistance(camera);
}
mBatchedCubes.sort();
int index = 0;
for (Cube cube : mBatchedCubes){
    cube.setIndex(index++);
}
Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • Thank you very much for your answer. Things got a lot better. I noticed one more problem though. I updated my question. Could you take a look? Also would you like me to add a bounty before accepting your answer? – Justas Sakalauskas Jul 15 '14 at 21:13