Hi,
I just reviewed some of my classes using the last LWJGL that now use GLFW instead of it’s internal Display class.
Now, i’m review some of my 2D drawing stuff that used the old OpenGL stuff using glBegin / glEnd by a more modern version using VBO, IBO, etc…
But i have some design questions and i must admit that i have some pain to go on the flexible pipeline…
For some test i done this small classe (i skip all window initialization and openGL configuration like modelview, ortho, …)
i update the code above with the result of discussions on this topic for have the code only 1 time
/** Draft
*/
public class OpenGLSupport {
/**
* Maximum bufferized primitives count
*/
private static final int PRIMITIVES_SIZE = 512;
/** FLOAT size in bytes (for stride and offset computes)
*/
private static final int FLOAT_SIZE = Float.SIZE / 8;
/** INTEGER size in bytes (for stride and offset computes)
*/
private static final int INTEGER_SIZE = Integer.SIZE / 8;
/**
* VERTEX Position size in bytes (for stride and offset computes)
*/
private static final int VERTEX_SIZE = FLOAT_SIZE * 2;
/** COLOR size in bytes (for stride and offset computes)
*/
private static final int COLOR_SIZE = FLOAT_SIZE * 4;
/**
* Maximum vertices buffer size
*/
private static final int VERTICES_SIZE = powerOfTwo( PRIMITIVES_SIZE * ( VERTEX_SIZE + COLOR_SIZE ) * 4 );
/**
* Maximum indices buffer size
*/
private static final int INDICES_SIZE = powerOfTwo( PRIMITIVES_SIZE * 8 );
/** OpenGL bind optimizer
*/
private OpenGLBinder glBinder = null;
private Primitive[] primitives = new Primitive[ PRIMITIVES_SIZE ];
private FloatBuffer verticesBuffer = BufferUtils.createFloatBuffer( VERTICES_SIZE );
private IntBuffer indicesBuffer = BufferUtils.createIntBuffer( INDICES_SIZE );
private int primitiveCount = 0;
/** Our VBO and IBOI handles
*/
private int iVBO = 0;
private int iIBO = 0;
/** Color to use
*/
private Color color = Color.WHITE;
public OpenGLSupport( OpenGLBinder binder ) {
System.out.println("GraphicsOpenGL (primitivesSize=" + PRIMITIVES_SIZE + ", verticesSize=" + VERTICES_SIZE + ", indicesSize=" + INDICES_SIZE );
this.glBinder = binder;
}
public void setColor( Color color ) {
this.color = color;
}
public void drawRect(float x, float y, float width, float height) {
// 1 primitive , 4 vertices, 8 indices
checkBufferSize( 1 , 4 , 8 );
// Get the primitive to draw
Primitive p = getPrimitive( GL11.GL_LINES );
// push vertices and colors
p.pushVertex( x , y );
p.pushVertex( x + width , y );
p.pushVertex( x + width , y + height );
p.pushVertex( x , y + height );
// push indices
indicesBuffer.put( 0 );
indicesBuffer.put( 1 );
indicesBuffer.put( 1 );
indicesBuffer.put( 2 );
indicesBuffer.put( 2 );
indicesBuffer.put( 3 );
indicesBuffer.put( 3 );
indicesBuffer.put( 0 );
p.end();
}
public void drawLine(float x1, float y1, float x2, float y2) {
// 1 primitive , 2 vertices, 2 indices
checkBufferSize( 1 , 2 , 2 );
// Get the primitive to draw
Primitive p = getPrimitive( GL11.GL_LINES );
// push vertices and colors
p.pushVertex( x1 , y1 );
p.pushVertex( x2 , y2 );
// push indices
indicesBuffer.put( 0 );
indicesBuffer.put( 1 );
p.end();
}
public void fillRect(float x, float y, float width, float height) {
// 1 primitive , 4 vertices, 6 indices
checkBufferSize( 1 , 4 , 6 );
// Get the primitive to draw
Primitive p = getPrimitive( GL11.GL_TRIANGLES );
// push vertices and colors
p.pushVertex( x , y );
p.pushVertex( x + width , y );
p.pushVertex( x + width , y + height );
p.pushVertex( x , y + height );
// push indices
indicesBuffer.put( 0 );
indicesBuffer.put( 1 );
indicesBuffer.put( 2 );
indicesBuffer.put( 0 );
indicesBuffer.put( 2 );
indicesBuffer.put( 3 );
p.end();
}
/** Peek a Primitive object in the primitives buffer and reinit it.
* This methode reuse {@link Primitive} instances in order to limit instanciations under the rendering process.
*/
private Primitive getPrimitive( int primitiveType ) {
Primitive p = primitives[ primitiveCount ];
if( p == null ) {
p = new Primitive();
primitives[ primitiveCount ] = p;
}
primitiveCount++;
p.begin( primitiveType );
return p;
}
public void flush() {
// flush our buffers
verticesBuffer.flip();
indicesBuffer.flip();
// VBO binding
boolean gen = ( iVBO == 0 );
{
if( gen ) {
iVBO = GL15.glGenBuffers();
}
// Bind VBO (only if it's not already the case)
glBinder.bindVBO( iVBO );
// make a full buffer for be able to perform dynamic size buffer
if( gen ) {
FloatBuffer empty = BufferUtils.createFloatBuffer( verticesBuffer.capacity() );
for(int i = 0 ; i < empty.capacity() ; i++ ) {
empty.put( 0f );
}
empty.flip();
GL15.glBufferData( GL15.GL_ARRAY_BUFFER, empty , GL15.GL_DYNAMIC_DRAW );
}
GL15.glBufferSubData( GL15.GL_ARRAY_BUFFER, 0 , verticesBuffer );
}
// IBO binding
gen = ( iIBO == 0 );
{
if( gen ) {
iIBO = GL15.glGenBuffers();
}
// Bind IBO (only if it's not already the case)
glBinder.bindIBO( iIBO );
// make a full buffer for be able to perform dynamic size buffer
if( gen ) {
IntBuffer empty = BufferUtils.createIntBuffer( indicesBuffer.capacity() );
for(int i = 0 ; i < empty.capacity() ; i++ ) {
empty.put( 0 );
}
empty.flip();
GL15.glBufferData( GL15.GL_ELEMENT_ARRAY_BUFFER, empty , GL15.GL_DYNAMIC_DRAW );
}
GL15.glBufferSubData( GL15.GL_ELEMENT_ARRAY_BUFFER, 0 , indicesBuffer );
}
// Painting
GL11.glEnableClientState( GL11.GL_VERTEX_ARRAY );
Primitive p = null;
for(int i = 0 ; i < primitiveCount ; i++ ) {
p = primitives[i];
if( p.glColorOffset >= 0 ) {
GL11.glEnableClientState( GL11.GL_COLOR_ARRAY );
GL11.glColorPointer( 4 , GL11.GL_FLOAT , p.stride , p.glColorOffset );
}
GL11.glVertexPointer( 2, GL11.GL_FLOAT, p.stride , p.glVertexOffset );
GL11.glDrawElements( p.type , p.indicesCount , GL11.GL_UNSIGNED_INT , p.indicesOffset );
if( p.glColorOffset >= 0 ) {
GL11.glDisableClientState( GL11.GL_COLOR_ARRAY );
}
}
//Disable vertex arrays
GL11.glDisableClientState( GL11.GL_VERTEX_ARRAY );
// clear the buffer
verticesBuffer.clear();
indicesBuffer.clear();
primitiveCount = 0;
}
public void dispose() {
if( iVBO > 0 ) {
glBinder.unbindVBO( iVBO );
GL15.glDeleteBuffers( iVBO );
iVBO = 0;
}
if( iIBO > 0 ) {
glBinder.unbindIBO( iIBO );
GL15.glDeleteBuffers( iIBO );
iIBO = 0;
}
verticesBuffer.clear();
indicesBuffer.clear();
primitiveCount = 0;
}
/** Check if the need to flush all pending statements before continuing.
*/
private void checkBufferSize( int primitiveCount , int verticesCount , int indicesCount ) {
if( this.primitiveCount + primitiveCount >= PRIMITIVES_SIZE ) {
flush();
return;
}
if( verticesBuffer.position() + ( verticesCount * ( VERTEX_SIZE + COLOR_SIZE ) ) >= VERTICES_SIZE ) {
flush();
return;
}
if( indicesBuffer.position() + ( indicesCount * INTEGER_SIZE ) >= INDICES_SIZE ) {
flush();
}
}
private class Primitive {
private int type;
private int glVertexOffset;
private int glColorOffset;
private int stride;
private int indicesOffset;
private int indicesCount;
private void begin( int type ) {
// init it
this.type = type;
this.glVertexOffset = -1;
this.glColorOffset = -1;
this.stride = -1;
this.indicesOffset = indicesBuffer.position() * INTEGER_SIZE;
this.indicesCount = 0;
}
private void pushVertex( float x , float y ) {
int strideStart = verticesBuffer.position() * FLOAT_SIZE;
pushPaintContext();
if( glVertexOffset < 0 ) glVertexOffset = verticesBuffer.position() * FLOAT_SIZE;
verticesBuffer.put( x );
verticesBuffer.put( y );
if( stride < 0 ) stride = ( verticesBuffer.position() * FLOAT_SIZE ) - strideStart;
}
private void pushPaintContext() {
if( color != null ) {
if( glColorOffset < 0 ) glColorOffset = verticesBuffer.position() * FLOAT_SIZE;
pushColor( color );
}
else {
// TODO : texture impl
}
}
private void pushColor( Color color ) {
float[] rgb = color.getRGBComponents();
for(int i = 0 ; i < 4 ; i++ ) {
if( i < rgb.length ) {
verticesBuffer.put( rgb[i] );
}
else {
verticesBuffer.put( 1f ); // for alpha if missing
}
}
}
private void end() {
indicesCount = ( (indicesBuffer.position() * INTEGER_SIZE) - indicesOffset ) / INTEGER_SIZE;
}
}
/**
* Get the closest greater power of 2 to the fold number.
*/
private static int powerOfTwo(int num) {
if( num != 0 ) {
num--;
num |= (num >> 1); // Or first 2 bits
num |= (num >> 2); // Or next 2 bits
num |= (num >> 4); // Or next 4 bits
num |= (num >> 8); // Or next 8 bits
num |= (num >> 16); // Or next 16 bits
num++;
}
return num;
}
}
and the binder :
public class OpenGLBinder {
private int bindedVBO;
private int bindedIBO;
private int bindedTexure;
public void bindIBO( int ibo ) {
if( bindedIBO == ibo ) return;
GL15.glBindBuffer( GL15.GL_ELEMENT_ARRAY_BUFFER, ibo );
bindedIBO = ibo;
}
public void unbindIBO( int ibo ) {
if( bindedIBO == ibo ) {
GL15.glBindBuffer( GL15.GL_ELEMENT_ARRAY_BUFFER, 0 );
bindedIBO = 0;
}
}
public void bindVBO( int vbo ) {
if( bindedVBO == vbo ) return;
GL15.glBindBuffer( GL15.GL_ARRAY_BUFFER, vbo );
bindedVBO = vbo;
}
public void unbindVBO( int vbo ) {
if( bindedVBO == vbo ) {
GL15.glBindBuffer( GL15.GL_ARRAY_BUFFER, 0 );
bindedVBO = 0;
}
}
}
As is, if i call a fillRect + flush, this code work. (ughh ^^)
So now, i have some reflextion about how to design an efficient drawing back-end if i call by example 2 fillRect and 1 drawLine !!
What i think :
- Fill all vertices for theses 2 rects and the line when i call fillRect and drawLine methods
when flush():
-
bind the IBO, VBO
-
send all vertices to VBO (for 2 rects and 1 line)
-
send all indices to IBO (for 2 rects and 1 line)
-
glVertex (with buffer offset)
-
drawElement for the first rect (with indice offset)
-
glVertex (with buffer offset)
-
drawElement for the first rect (with indice offset)
-
glVertex (with buffer offset)
-
drawElement for the second rect (with indice offset)
-
glVertex (with buffer offset)
-
drawElement for the line (with indice offset)
The advantage with this design, it’s i can merge relatively easily “textcoord” or “colors” in the VBO and use stride concepts for each “entity” since i make a separate drawElement for each one.
I don’t know if i can do a drawElement for all of theses ? i think no because we can have the first fillRect with a color and the second one with a texture. so i must configure the call to the drawElement.
The second things, is the size of the buffer, i could imagine to have a limited size (ie 1024k) and flush() automatically if i call a draw() methods that will overflow the buffer.
PS: i would admit, all was simpler with glBegin and glEnd ^^ but it’s really fun to discover new things.
Some advise is welcome ^^