The Quad updating a VBO with BufferSubData
From LWJGL
Contents |
Introduction
When we’ve got our VBO set up we can draw it as many times as we like without any need to keep track of the actual data. We have uploaded it to GPU memory and OpenGL knows where that is and how to get it when necessary. However we might want to adjust our model’s vertices to do some animation for example.
Creating our VBO
We already know how to setup a VBO, we call the method “glBufferData” to allocate memory on the graphics card and upload bytes to it. We can also tell OpenGL how we are going to use this data by saying “GL_STATIC_DRAW” for instance. When we know that we are going to update the VBO we could use “GL_STREAM_DRAW” instead. Again, we are just letting OpenGL know how the data will be processed later on.
Creating our VBO looks like this, verticesFloatBuffer is filled with the vertices data:
// Create a new Vertex Buffer Object in memory and select it (bind) vboId = GL15.glGenBuffers(); GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vboId); GL15.glBufferData(GL15.GL_ARRAY_BUFFER, verticesFloatBuffer, GL15.GL_STREAM_DRAW);
Updating our VBO
When updating a VBO we should make sure that OpenGL is not currently using the VBO for rendering therefore we’ll update our VBO before our rendering code. To update the VBO we simply bind it again, create the new vertex definition in bytes and upload it with “glBufferSubData”. Where “glBufferData” allocates new memory, “glBufferSubData” reuses that same memory and overwrites whatever was there in the first place. We are going to adjust our quad vertices by giving it a random offset in the X and Y direction.
// Update vertices in the VBO, first bind the VBO
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vboId);
// Apply and update vertex data
for (int i = 0; i < vertices.length; i++) {
TexturedVertex vertex = vertices[i];
// Define offset
float offsetX = (float) (Math.cos(Math.PI * Math.random()) * 0.1);
float offsetY = (float) (Math.sin(Math.PI * Math.random()) * 0.1);
// Offset the vertex position
float[] xyz = vertex.getXYZ();
vertex.setXYZ(xyz[0] + offsetX, xyz[1] + offsetY, xyz[2]);
// Put the new data in a ByteBuffer (in the view of a FloatBuffer)
FloatBuffer vertexFloatBuffer = vertexByteBuffer.asFloatBuffer();
vertexFloatBuffer.rewind();
vertexFloatBuffer.put(vertex.getElements());
vertexFloatBuffer.flip();
GL15.glBufferSubData(GL15.GL_ARRAY_BUFFER, i * TexturedVertex.stride,
vertexByteBuffer);
// Restore the vertex data
vertex.setXYZ(xyz[0], xyz[1], xyz[2]);
}
// And of course unbind
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
Notice that we update each vertex at a time, “glBufferSubData” is not meant to rewrite the entire set of bytes. Instead in the memory block of our quad (which is 4 times a vertex definition) we’ll use our “stride” (the amount of bytes for one vertex definition) to offset which bytes will be overwritten. The amount of bytes that will get overwritten in our quad buffer is the same amount of bytes that are in a single vertex buffer.
The only portion of the code responsible of updating the VBO is this line:
GL15.glBufferSubData(GL15.GL_ARRAY_BUFFER, i * TexturedVertex.stride, vertexByteBuffer);
Remember that the TexturedVertex class holds all information about a single vertex including the amount of bytes it is made up of (the stride).
The result
The result is a quad which corners are constantly moving in a random fashion (run the application to see the effect):
Complete source code
Vertex shader
#version 150 core
in vec4 in_Position;
in vec4 in_Color;
in vec2 in_TextureCoord;
out vec4 pass_Color;
out vec2 pass_TextureCoord;
void main(void) {
gl_Position = in_Position;
pass_Color = in_Color;
pass_TextureCoord = in_TextureCoord;
}
Fragment shader
#version 150 core
uniform sampler2D texture_diffuse;
in vec4 pass_Color;
in vec2 pass_TextureCoord;
out vec4 out_Color;
void main(void) {
out_Color = pass_Color;
// Override out_Color with our texture pixel
out_Color = texture2D(texture_diffuse, pass_TextureCoord);
}
TexturedVertex class
public class TexturedVertex {
// Vertex data
private float[] xyzw = new float[] {0f, 0f, 0f, 1f};
private float[] rgba = new float[] {1f, 1f, 1f, 1f};
private float[] st = new float[] {0f, 0f};
// The amount of bytes an element has
public static final int elementBytes = 4;
// Elements per parameter
public static final int positionElementCount = 4;
public static final int colorElementCount = 4;
public static final int textureElementCount = 2;
// Bytes per parameter
public static final int positionBytesCount = positionElementCount * elementBytes;
public static final int colorByteCount = colorElementCount * elementBytes;
public static final int textureByteCount = textureElementCount * elementBytes;
// Byte offsets per parameter
public static final int positionByteOffset = 0;
public static final int colorByteOffset = positionByteOffset + positionBytesCount;
public static final int textureByteOffset = colorByteOffset + colorByteCount;
// The amount of elements that a vertex has
public static final int elementCount = positionElementCount +
colorElementCount + textureElementCount;
// The size of a vertex in bytes, like in C/C++: sizeof(Vertex)
public static final int stride = positionBytesCount + colorByteCount +
textureByteCount;
// Setters
public void setXYZ(float x, float y, float z) {
this.setXYZW(x, y, z, 1f);
}
public void setRGB(float r, float g, float b) {
this.setRGBA(r, g, b, 1f);
}
public void setST(float s, float t) {
this.st = new float[] {s, t};
}
public void setXYZW(float x, float y, float z, float w) {
this.xyzw = new float[] {x, y, z, w};
}
public void setRGBA(float r, float g, float b, float a) {
this.rgba = new float[] {r, g, b, 1f};
}
// Getters
public float[] getElements() {
float[] out = new float[TexturedVertex.elementCount];
int i = 0;
// Insert XYZW elements
out[i++] = this.xyzw[0];
out[i++] = this.xyzw[1];
out[i++] = this.xyzw[2];
out[i++] = this.xyzw[3];
// Insert RGBA elements
out[i++] = this.rgba[0];
out[i++] = this.rgba[1];
out[i++] = this.rgba[2];
out[i++] = this.rgba[3];
// Insert ST elements
out[i++] = this.st[0];
out[i++] = this.st[1];
return out;
}
public float[] getXYZW() {
return new float[] {this.xyzw[0], this.xyzw[1], this.xyzw[2], this.xyzw[3]};
}
public float[] getXYZ() {
return new float[] {this.xyzw[0], this.xyzw[1], this.xyzw[2]};
}
public float[] getRGBA() {
return new float[] {this.rgba[0], this.rgba[1], this.rgba[2], this.rgba[3]};
}
public float[] getRGB() {
return new float[] {this.rgba[0], this.rgba[1], this.rgba[2]};
}
public float[] getST() {
return new float[] {this.st[0], this.st[1]};
}
}
Application
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import org.lwjgl.BufferUtils;
import org.lwjgl.LWJGLException;
import org.lwjgl.input.Keyboard;
import org.lwjgl.opengl.ContextAttribs;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.PixelFormat;
import org.lwjgl.util.glu.GLU;
import de.matthiasmann.twl.utils.PNGDecoder;
import de.matthiasmann.twl.utils.PNGDecoder.Format;
public class TheQuadExampleUpdateVBO {
// Entry point for the application
public static void main(String[] args) {
new TheQuadExampleUpdateVBO();
}
// Setup variables
private final String WINDOW_TITLE = "The Quad: Update VBO";
private final int WIDTH = 320;
private final int HEIGHT = 320;
// Quad variables
private int vaoId = 0;
private int vboId = 0;
private int vboiId = 0;
private int indicesCount = 0;
// Shader variables
private int vsId = 0;
private int fsId = 0;
private int pId = 0;
// Texture variables
private int[] texIds = new int[] {0, 0};
private int textureSelector = 0;
// Update VBO variables
private TexturedVertex[] vertices = null;
private ByteBuffer vertexByteBuffer = null;
private ByteBuffer verticesByteBuffer = null;
public TheQuadExampleUpdateVBO() {
// Initialize OpenGL (Display)
this.setupOpenGL();
this.setupQuad();
this.setupShaders();
this.setupTextures();
while (!Display.isCloseRequested()) {
// Do a single loop (logic/render)
this.loopCycle();
// Force a maximum FPS of about 60
Display.sync(60);
// Let the CPU synchronize with the GPU if GPU is tagging behind
Display.update();
}
// Destroy OpenGL (Display)
this.destroyOpenGL();
}
private void setupTextures() {
texIds[0] = this.loadPNGTexture("assets/stGrid1.png", GL13.GL_TEXTURE0);
texIds[1] = this.loadPNGTexture("assets/stGrid2.png", GL13.GL_TEXTURE0);
this.exitOnGLError("setupTexture");
}
private void setupOpenGL() {
// Setup an OpenGL context with API version 3.2
try {
PixelFormat pixelFormat = new PixelFormat();
ContextAttribs contextAtrributes = new ContextAttribs(3, 2)
.withForwardCompatible(true)
.withProfileCore(true);
Display.setDisplayMode(new DisplayMode(WIDTH, HEIGHT));
Display.setTitle(WINDOW_TITLE);
Display.create(pixelFormat, contextAtrributes);
GL11.glViewport(0, 0, WIDTH, HEIGHT);
} catch (LWJGLException e) {
e.printStackTrace();
System.exit(-1);
}
// Setup an XNA like background color
GL11.glClearColor(0.4f, 0.6f, 0.9f, 0f);
// Map the internal OpenGL coordinate system to the entire screen
GL11.glViewport(0, 0, WIDTH, HEIGHT);
this.exitOnGLError("setupOpenGL");
}
private void setupQuad() {
// We'll define our quad using 4 vertices of the custom 'TexturedVertex' class
TexturedVertex v0 = new TexturedVertex();
v0.setXYZ(-0.5f, 0.5f, 0); v0.setRGB(1, 0, 0); v0.setST(0, 0);
TexturedVertex v1 = new TexturedVertex();
v1.setXYZ(-0.5f, -0.5f, 0); v1.setRGB(0, 1, 0); v1.setST(0, 1);
TexturedVertex v2 = new TexturedVertex();
v2.setXYZ(0.5f, -0.5f, 0); v2.setRGB(0, 0, 1); v2.setST(1, 1);
TexturedVertex v3 = new TexturedVertex();
v3.setXYZ(0.5f, 0.5f, 0); v3.setRGB(1, 1, 1); v3.setST(1, 0);
vertices = new TexturedVertex[] {v0, v1, v2, v3};
// Create a FloatBufer of the appropriate size for one vertex
vertexByteBuffer = BufferUtils.createByteBuffer(TexturedVertex.stride);
// Put each 'Vertex' in one FloatBuffer
verticesByteBuffer = BufferUtils.createByteBuffer(vertices.length *
TexturedVertex.stride);
FloatBuffer verticesFloatBuffer = verticesByteBuffer.asFloatBuffer();
for (int i = 0; i < vertices.length; i++) {
// Add position, color and texture floats to the buffer
verticesFloatBuffer.put(vertices[i].getElements());
}
verticesFloatBuffer.flip();
// OpenGL expects to draw vertices in counter clockwise order by default
byte[] indices = {
0, 1, 2,
2, 3, 0
};
indicesCount = indices.length;
ByteBuffer indicesBuffer = BufferUtils.createByteBuffer(indicesCount);
indicesBuffer.put(indices);
indicesBuffer.flip();
// Create a new Vertex Array Object in memory and select it (bind)
vaoId = GL30.glGenVertexArrays();
GL30.glBindVertexArray(vaoId);
// Create a new Vertex Buffer Object in memory and select it (bind)
vboId = GL15.glGenBuffers();
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vboId);
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, verticesFloatBuffer, GL15.GL_STREAM_DRAW);
// Put the position coordinates in attribute list 0
GL20.glVertexAttribPointer(0, TexturedVertex.positionElementCount, GL11.GL_FLOAT,
false, TexturedVertex.stride, TexturedVertex.positionByteOffset);
// Put the color components in attribute list 1
GL20.glVertexAttribPointer(1, TexturedVertex.colorElementCount, GL11.GL_FLOAT,
false, TexturedVertex.stride, TexturedVertex.colorByteOffset);
// Put the texture coordinates in attribute list 2
GL20.glVertexAttribPointer(2, TexturedVertex.textureElementCount, GL11.GL_FLOAT,
false, TexturedVertex.stride, TexturedVertex.textureByteOffset);
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
// Deselect (bind to 0) the VAO
GL30.glBindVertexArray(0);
// Create a new VBO for the indices and select it (bind) - INDICES
vboiId = GL15.glGenBuffers();
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, vboiId);
GL15.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, indicesBuffer, GL15.GL_STATIC_DRAW);
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
this.exitOnGLError("setupQuad");
}
private void setupShaders() {
// Load the vertex shader
vsId = this.loadShader("src/thequad/vertex_textured.glsl", GL20.GL_VERTEX_SHADER);
// Load the fragment shader
fsId = this.loadShader("src/thequad/fragment_textured.glsl", GL20.GL_FRAGMENT_SHADER);
// Create a new shader program that links both shaders
pId = GL20.glCreateProgram();
GL20.glAttachShader(pId, vsId);
GL20.glAttachShader(pId, fsId);
GL20.glLinkProgram(pId);
// Position information will be attribute 0
GL20.glBindAttribLocation(pId, 0, "in_Position");
// Color information will be attribute 1
GL20.glBindAttribLocation(pId, 1, "in_Color");
// Textute information will be attribute 2
GL20.glBindAttribLocation(pId, 2, "in_TextureCoord");
GL20.glValidateProgram(pId);
this.exitOnGLError("setupShaders");
}
private void logicCycle() {
// Texture selection
while(Keyboard.next()) {
// Only listen to events where the key was pressed (down event)
if (!Keyboard.getEventKeyState()) continue;
// Switch textures depending on the key released
switch (Keyboard.getEventKey()) {
case Keyboard.KEY_1:
textureSelector = 0;
break;
case Keyboard.KEY_2:
textureSelector = 1;
break;
}
}
// Update vertices in the VBO, first bind the VBO
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vboId);
// Apply and update vertex data
for (int i = 0; i < vertices.length; i++) {
TexturedVertex vertex = vertices[i];
// Define offset
float offsetX = (float) (Math.cos(Math.PI * Math.random()) * 0.1);
float offsetY = (float) (Math.sin(Math.PI * Math.random()) * 0.1);
// Offset the vertex position
float[] xyz = vertex.getXYZ();
vertex.setXYZ(xyz[0] + offsetX, xyz[1] + offsetY, xyz[2]);
// Put the new data in a ByteBuffer (in the view of a FloatBuffer)
FloatBuffer vertexFloatBuffer = vertexByteBuffer.asFloatBuffer();
vertexFloatBuffer.rewind();
vertexFloatBuffer.put(vertex.getElements());
vertexFloatBuffer.flip();
GL15.glBufferSubData(GL15.GL_ARRAY_BUFFER, i * TexturedVertex.stride,
vertexByteBuffer);
// Restore the vertex data
vertex.setXYZ(xyz[0], xyz[1], xyz[2]);
}
// And of course unbind
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
this.exitOnGLError("logicCycle");
}
private void renderCycle() {
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT);
GL20.glUseProgram(pId);
// Bind the texture
GL13.glActiveTexture(GL13.GL_TEXTURE0);
GL11.glBindTexture(GL11.GL_TEXTURE_2D, texIds[textureSelector]);
// Bind to the VAO that has all the information about the vertices
GL30.glBindVertexArray(vaoId);
GL20.glEnableVertexAttribArray(0);
GL20.glEnableVertexAttribArray(1);
GL20.glEnableVertexAttribArray(2);
// Bind to the index VBO that has all the information about the order of the vertices
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, vboiId);
// Draw the vertices
GL11.glDrawElements(GL11.GL_TRIANGLES, indicesCount, GL11.GL_UNSIGNED_BYTE, 0);
// Put everything back to default (deselect)
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
GL20.glDisableVertexAttribArray(0);
GL20.glDisableVertexAttribArray(1);
GL20.glDisableVertexAttribArray(2);
GL30.glBindVertexArray(0);
GL20.glUseProgram(0);
this.exitOnGLError("renderCycle");
}
private void loopCycle() {
// Update logic
this.logicCycle();
// Update rendered frame
this.renderCycle();
this.exitOnGLError("loopCycle");
}
private void destroyOpenGL() {
// Delete the texture
GL11.glDeleteTextures(texIds[0]);
GL11.glDeleteTextures(texIds[1]);
// Delete the shaders
GL20.glUseProgram(0);
GL20.glDetachShader(pId, vsId);
GL20.glDetachShader(pId, fsId);
GL20.glDeleteShader(vsId);
GL20.glDeleteShader(fsId);
GL20.glDeleteProgram(pId);
// Select the VAO
GL30.glBindVertexArray(vaoId);
// Disable the VBO index from the VAO attributes list
GL20.glDisableVertexAttribArray(0);
GL20.glDisableVertexAttribArray(1);
// Delete the vertex VBO
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
GL15.glDeleteBuffers(vboId);
// Delete the index VBO
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
GL15.glDeleteBuffers(vboiId);
// Delete the VAO
GL30.glBindVertexArray(0);
GL30.glDeleteVertexArrays(vaoId);
this.exitOnGLError("destroyOpenGL");
Display.destroy();
}
private int loadShader(String filename, int type) {
StringBuilder shaderSource = new StringBuilder();
int shaderID = 0;
try {
BufferedReader reader = new BufferedReader(new FileReader(filename));
String line;
while ((line = reader.readLine()) != null) {
shaderSource.append(line).append("\n");
}
reader.close();
} catch (IOException e) {
System.err.println("Could not read file.");
e.printStackTrace();
System.exit(-1);
}
shaderID = GL20.glCreateShader(type);
GL20.glShaderSource(shaderID, shaderSource);
GL20.glCompileShader(shaderID);
if (GL20.glGetShader(shaderID, GL20.GL_COMPILE_STATUS) == GL11.GL_FALSE) {
System.err.println("Could not compile shader.");
System.exit(-1);
}
this.exitOnGLError("loadShader");
return shaderID;
}
private int loadPNGTexture(String filename, int textureUnit) {
ByteBuffer buf = null;
int tWidth = 0;
int tHeight = 0;
try {
// Open the PNG file as an InputStream
InputStream in = new FileInputStream(filename);
// Link the PNG decoder to this stream
PNGDecoder decoder = new PNGDecoder(in);
// Get the width and height of the texture
tWidth = decoder.getWidth();
tHeight = decoder.getHeight();
// Decode the PNG file in a ByteBuffer
buf = ByteBuffer.allocateDirect(
4 * decoder.getWidth() * decoder.getHeight());
decoder.decode(buf, decoder.getWidth() * 4, Format.RGBA);
buf.flip();
in.close();
} catch (IOException e) {
e.printStackTrace();
System.exit(-1);
}
// Create a new texture object in memory and bind it
int texId = GL11.glGenTextures();
GL13.glActiveTexture(textureUnit);
GL11.glBindTexture(GL11.GL_TEXTURE_2D, texId);
// All RGB bytes are aligned to each other and each component is 1 byte
GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1);
// Upload the texture data and generate mip maps (for scaling)
GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGB, tWidth, tHeight, 0,
GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buf);
GL30.glGenerateMipmap(GL11.GL_TEXTURE_2D);
// Setup the ST coordinate system
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT);
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT);
// Setup what to do when the texture has to be scaled
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER,
GL11.GL_LINEAR);
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER,
GL11.GL_LINEAR_MIPMAP_LINEAR);
this.exitOnGLError("loadPNGTexture");
return texId;
}
private void exitOnGLError(String errorMessage) {
int errorValue = GL11.glGetError();
if (errorValue != GL11.GL_NO_ERROR) {
String errorString = GLU.gluErrorString(errorValue);
System.err.println("ERROR - " + errorMessage + ": " + errorString);
if (Display.isCreated()) Display.destroy();
System.exit(-1);
}
}
}
