Inheriting Batch class


#1

Hi, I’m trying to wrap my head around the proper usage of the Batch object. I am rendering a 3x3 matrix of cubes currently with the following code.

class CipherApp : public App {
  public:
	void setup() override;
	void mouseDown( MouseEvent event ) override;
	void update() override;
	void draw() override;
    
    static const int MAP_SIZE = 6;
    
    CameraPersp mCam;
    gl::BatchRef mBlocks[MAP_SIZE][MAP_SIZE][MAP_SIZE];
};

void CipherApp::setup(){

    auto lambert = gl::ShaderDef().lambert().color();
    gl::GlslProgRef	shader = gl::getStockShader( lambert );
    
    for(int i = 0; i < MAP_SIZE; i++) {
        for(int j = 0; j < MAP_SIZE; j++) {
            for(int k = 0; k < MAP_SIZE; k++) {
                auto cube = geom::Cube();
                mBlocks[i][j][k] = gl::Batch::create( cube, shader );
            }
        }
    }

    mCam.lookAt( vec3( 10, 20, 10 ), vec3( 0 ) );
}

void CipherApp::draw() {
    gl::clear();
    gl::enableDepthRead();
    gl::enableDepthWrite();
    
    gl::setMatrices( mCam );
    
    for( int i = 0; i < MAP_SIZE; i++ ) {
        for( int j = 0; j < MAP_SIZE; j++ ) {
            for(int k = 0; k < MAP_SIZE; k++) {
                gl::ScopedModelMatrix scpModelMatrix;
                gl::translate( i, j, k );
                gl::color( i / (float)MAP_SIZE, j / (float)MAP_SIZE, k / (float)MAP_SIZE );
                mBlocks[i][j][k]->draw();
            }
        }
    }
}

I have read that a batch is a bundle of a mesh and shader. If my chunk of cubes will all utilize the same shader is it overkill to use a Batch for each cube?

My next confusion is inheriting the Batch type so I can specify a specific Cube class. I am looking for each block to store some parameters beyond what is specified in a Batch so I set out to make a Cube class with parameters such as scale and location that inherited a Batch object but was confused when I could not define my new Cubes with

Cube newCube = gl::Batch::create(...)

How can I make Cubes inherit a batch type and create a cube mesh with geom::Cube();


#2

hello!

Hopefully I don’t botch this explanation but here goes haha.

To answer your first question, it really depends on how many cubes as cubes are not terribly complicated things to draw; so for the most part, using a Batch for each cube shouldn’t be overkill. Offhand though, I’d say you don’t need to make so many geom::Cube()s but could just move

auto cube = geom::Cube()

outside of the nested for loops.

There are optimizations that can be done with methods like Instancing that can make things more performant if you’re drawing a bunch of similar geometry but probably not something I’d worry about too much since it sounds like you’re just starting out(but check out the InstancedTeapots example if you’re curious).

As to your second question, and hopefully someone more familiar with the internals can better explain this, but the Batch class essentially acts like a engine of sorts, handling all of the different OpenGL calls between the shader and mesh necessary to draw something to the screen, it’s not the class that defines what your object is, that’s the responsibility of the mesh(in this case, the VboMesh or TriMesh classes).

when you call

auto cube = geom::Cube()

that creates a Source object, which, when passed to gl::Batch::create, gets turned into a mesh object.

To do what you’re trying to do, you need to create a custom mesh and there are a couple ways to go about it.

a. I actually learned something while reading up on this but the easiest way(though fyi, I have not tried this before) might be to just follow the VboMesh example as it sticks with using Source objects. Look at the bufferLayout variable in particular to see how you can add additional attributes. To add data for those attributes, take a look at the setupGeometry function in the PickingFbo example for how you could add the data.

b. Another way that might be good would be to just directly inherit geom::Cube. Based on past experience it might be a tad complicated to add more attributes like scale, etc though having refreshed my knowledge on the matter, I imagine it’d be similar to method a.

c. I personally would just create a custom class that doesn’t really inherit from anything and construct a VboMesh from scratch. It’s a bit of extra work, I just like building things from scratch cause it’s very straightforward with regular geometry and helps re-enforce knowledge of the overall process which helps when trying to build more complicated things. Cubes are simple objects, you can find all of the necessary data for the vertices and indices in the GeomIo.cpp file in the source, the same setupGeometry function in the PickingFbo sample should provide more clues as to how to go about this as well.

I’m sure I’m blanking on other ways but that should hopefully be enough to get you going in the right direction.


#3

Hi,

your Batch contains both the mesh and the shader. For efficient rendering, OpenGL needs to know how to access the mesh data (vertices, normals, texture coordinates, etc.) from the shader. A Batch assembles all this information (a.k.a. buffer layout) once and can then reuse it every time it is drawn.

In your example, you’re drawing the same cube 9 times. But you’re creating 9 different batches, which really isn’t necessary and defeats the whole purpose of a Batch. Why not create it once and draw it 9 times using the same Batch?

Next up: inheritance. In my personal opinion, it would be bad design to inherit from Batch just to store a model matrix (or position, rotation, scale). As said, a Batch represents a 3D object. You can draw the same 3D object thousands of times in different positions and scales. So position and scale are not properties of a Batch and should be stored separately.

It would be better to create your own class, e.g. a Model class, that encapsulates a ci::gl::BatchRef and a ci::mat4. A bit like this:

class Model {
  public:
    Model( const ci::gl::BatchRef &batch ) : mBatch( batch ), mIsDirty( true ) {}
    virtual ~Model() {}

    void setPosition( const ci::vec3 &position ) {
        mIsDirty |= ( mPosition != position ); 
        mPosition = position; 
    }
    void setOrientation( const ci::quat &orientation ) { 
        mIsDirty |= ( mOrientation != orientation ); 
        mOrientation = orientation; 
    }
    void setScale( const ci::vec3 &scale ) { 
        mIsDirty |= ( mScale != scale ); 
        mScale = scale;
    }

    const ci::gl::BatchRef &getBatch() const { return mBatch; }

    const ci::mat4 &getModelMatrix() const {
        if( mIsDirty ) {
            mModelMatrix = glm::translate( mPosition );
            mModelMarix *= glm::toMat4( mOrientation );
            mModelMatrix *= glm::scale( mScale );
            
            mIsDirty = false;
        }

        return mModelMatrix;
    }

    void draw() {
        gl::ScopedModelMatrix scpModel;
        gl::setModelMatrix( getModelMatrix() );
        
        bindTextures();
        updateUniforms();

        mBatch->draw();
    }

  protected:
    virtual void bindTextures() {
        // Override this method to bind your textures.
    }

    virtual void updateUniforms() {
        // Override this method to update your shader uniforms.
        auto &glsl = getBatch()->getGlslProg();
        gl::ScopedGlslProg scpGlslProg( glsl );
        glsl->uniform( "name", value );
    }

  private:
    ci::vec3 mPosition;
    ci::quat mOrientation;
    ci::vec3 mScale;
    ci::gl::BatchRef mBatch;
    mutable ci::mat4 mModelMatrix;
    mutable bool mIsDirty;
};

, and you can also create an InstancedModel class that allows you to render hundreds of copies of the same batch. In that case, you should store a bunch of ci::mat4's in a std::vector and use instanced drawing for maximum efficiency. See the samples that come with Cinder for more information.

-Paul