Well, I have made the move to shaders and have spent the last week or so trying to figure out how to use shaders and how to create working VBOs/VAOs.
I found a good example of how to program in Open GL 4.0 at http://openglbook.com/the-book/chapter-2-vertices-and-shapes/ (most of the following code is modified to fit Java from the examples on that website).
I am posting the following code in hopes that it benefits some lost soul trying to learn the newer (buffer object based) version of Open GL (>=3.0) in Light Weight Java Game Library (LWJGL).
The following code is not perfect or optimized by any stretch of the imagination, but it should at least get a triangle rendered to the screen using VAOs and Shaders without any dependancy libraries other than LWJGL.
Here’s the main class (GLSLExample.java) with the initialization methods for LWJGL and the render loop:
import java.io.IOException;
import java.io.StringReader;
import java.nio.FloatBuffer;
import java.util.Arrays;
import org.lwjgl.BufferUtils;
import org.lwjgl.LWJGLException;
import org.lwjgl.Sys;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
/** Renders a colored triangle using Vertex Buffer Objects
*/
public class GLSLExample {
final int maxFps = 60;
private int fps;
private long nextFps;
private long lastFrame;
private int modelViewMatrixUniformLocation;
private int projectionMatrixUniformLocation;
private VAO vao;
public GLSLExample() {
float[] modelViewMatrix = null;
float[] projectionMatrix = null;
float horzFOV = 90;
float vertFOV = 70;
float zNear = 1.9f;
float zFar = 11f;
float widthRatio = horzFOV/vertFOV;
initGL("Open GL/GLSL Test", 800, 600);
projectionMatrix = frustumMatrix(-widthRatio, widthRatio, -1, 1, zNear, zFar);
//projectionMatrix = orthoMatrix(-1, 1, -1, 1, zNear, zFar);
FloatBuffer projectionBuffer = BufferUtils.createFloatBuffer(16);
projectionBuffer.put(projectionMatrix);
projectionBuffer.flip();
modelViewMatrix = lookAtMatrix(0, 0, 0, 270, 0);
FloatBuffer modelViewBuffer = BufferUtils.createFloatBuffer(16);
modelViewBuffer.put(modelViewMatrix);
modelViewBuffer.flip();
Shader shader = shaderInit();
modelViewMatrixUniformLocation = GL20.glGetUniformLocation(shader.getProgram(), "modelViewMatrix");
projectionMatrixUniformLocation = GL20.glGetUniformLocation(shader.getProgram(), "projectionMatrix");
GL20.glUniformMatrix4(modelViewMatrixUniformLocation, false, modelViewBuffer);
GL20.glUniformMatrix4(projectionMatrixUniformLocation, false, projectionBuffer);
System.out.println("ModelView: " + Arrays.toString(modelViewMatrix) );
System.out.println("Projection: " + Arrays.toString(projectionMatrix) );
this.vao = new VAO();
vao.createVAO();
gameLoop();
vao.destroyVAO();
destroy();
}
/** initGL, initialize Open GL display and create vbo
* @return two ints, the first is the vbo created and the second is the ibo created
*/
private void initGL(String title, int width, int height) {
try {
Display.setDisplayMode(new DisplayMode(width, height));
Display.setTitle(title);
Display.create();
} catch (LWJGLException e) {
e.printStackTrace();
}
}
private Shader shaderInit() {
Shader shader = null;
StringReader vertexShaderSource = new StringReader(
"#version 130
" +
"in vec3 in_Position;
" +
"in vec4 in_Color;
" +
"out vec4 frag_Color;
" +
"uniform mat4 modelViewMatrix;
" +
"uniform mat4 projectionMatrix;
" +
"void main(void) {
" +
"gl_Position = (projectionMatrix * modelViewMatrix) * vec4(in_Position, 1.0);
" +
"frag_Color = in_Color;
" +
"}
");
StringReader fragmentShaderSource = new StringReader(
"#version 130
" +
"in vec4 frag_Color;
" +
"out vec4 out_Color;
" +
"void main(void) {
" +
"out_Color = frag_Color;
" +
"}
");
try {
shader = Shader.createShader(vertexShaderSource, fragmentShaderSource);
Shader.linkUseShader(shader.getProgram());
} catch (IOException e) {
System.err.println("Error loading shader files");
e.printStackTrace();
}
return shader;
}
private void gameLoop() {
nextFps = getTime();
while(!Display.isCloseRequested()) {
updateDelta();
updateFps();
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
render();
Display.update();
Display.sync(maxFps);
}
}
private void render() {
GL30.glBindVertexArray(this.vao.getId());
GL11.glDrawArrays(GL11.GL_TRIANGLES, 0, 3);
GL30.glBindVertexArray(0);
}
/** getDelta, calculate how many milliseconds have passed since last frame.
* @return milliseconds passed since last frame
*/
public long updateDelta() {
long time = getTime();
long delta = time - lastFrame;
lastFrame = time;
return delta;
}
/** getTime
* @return the current time in milliseconds
*/
public long getTime() {
return (Sys.getTime() * 1000) / Sys.getTimerResolution();
}
/** updateFps, calculate the FPS and set it in the title bar
*/
public void updateFps() {
if(getTime() > nextFps) {
Display.setTitle("Open GL/GLSL Test - FPS: " + fps);
fps = 0;
nextFps += 1000;
}
fps++;
}
private void destroy() {
Display.destroy();
}
/** orthoMatrix, generates a projection/orthogonal matrix from the input parameters
* @return an array of 16 floats in column major order for use as an Open GL projection matrix
*/
public static float[] orthoMatrix(float left, float right, float bottom, float top, float near, float far) {
float[] mat = new float[] {
2/(right-left), 0, 0, 0,
0, 2/(top-bottom), 0, 0,
0, 0, -2/(far-near), 0,
-(right+left)/(right-left), -(top+bottom)/(top-bottom), -(far+near)/(far-near), 1,
};
return mat;
}
/** frustumMatrix, generate a projection/frustrum matrix from the input parameters
* @param near - the distance to the near culling plan (should be greater than 0)
* @param far - the distance to the far culling plan (should be greater than 'near')
* @return an array of 16 floats in column major order for use as an Open GL projection matrix
*/
public static float[] frustumMatrix(float left, float right, float bottom, float top, float near, float far) {
float[] mat = new float[] {
(2*near)/(right-left), 0, 0, 0,
0, (2*near)/(top-bottom), 0, 0,
(right+left)/(right-left), (top+bottom)/(top-bottom), -(far+near)/(far-near), -1,
0, 0, -(2*far*near)/(far-near), 0,
};
return mat;
}
/** lookAtMatrix, generates a model-view matrix 'looking at' the specified direction
* @return
*/
public static float[] lookAtMatrix(float xCenter, float yCenter, float zCenter, float horzRotation, float vertRotation) {
float panRadius = 1.0f; // Causes the camera to rotate around the central point
float xLook = (float) Math.cos( (horzRotation)*Math.PI/180 );
float zLook = (float) Math.sin( (horzRotation)*Math.PI/180 );
float yLook = (float) Math.sin( (vertRotation)*Math.PI/180 );
// Because we are creating a 3-dimensional vector, the square of the cosine of the 3rd vector should equal
// the sum of the squares of the first 2 vectors, so we multiple the first 2 vectors by the cosine of the
// 3rd vector to ensure that the sum of the square of all 3 vector parts equal 1
float xzNormalize = (float) Math.cos( (vertRotation)*Math.PI/180 ); // The square of the x and z parts of the vector should equal the square of this cosine value
// If the camera angle is at +-90 than adjust it a very small amount back toward level to prevent the camera view from flipping over
if(xzNormalize < 0.001f) { xzNormalize = 0.001f; }
// Multiple the x and z part of the vector by the cosine value calculated to adjust/average them into the unit vector
float ratioXNormalize = xLook * xzNormalize;
float ratioZNormalize = zLook * xzNormalize;
float xEye = -(ratioXNormalize*panRadius)+xCenter;
float yEye = -(yLook*panRadius)+yCenter;
float zEye = -(ratioZNormalize*panRadius)+zCenter;
float xUp = 0;
float yUp = 1;
float zUp = 0;
float[] eye = new float[] {xEye, yEye, zEye};
float[] f = normalize(xCenter-xEye, yCenter-yEye, zCenter-zEye);
float[] u = normalize(xUp, yUp, zUp);
float[] s = normalize(cross(f, u));
u = cross(s, f);
float[] m = new float[16];
m[0+0] = s[0];
m[4+0] = s[1];
m[8+0] = s[2];
m[0+1] = u[0];
m[4+1] = u[1];
m[8+1] = u[2];
m[0+2] = -f[0];
m[4+2] = -f[1];
m[8+2] = -f[2];
m[12+0] = -dot(s, eye);
m[12+1] = -dot(u, eye);
m[12+2] = dot(f, eye);
m[15] = 1.0f;
return m;
}
public static float[] normalize(float x, float y, float z) {
float length = (float) Math.sqrt(x*x + y*y + z*z);
return new float[] {
x/length,
y/length,
z/length,
};
}
public static float[] normalize(float[] a) {
float length = (float) Math.sqrt(a[0]*a[0] + a[1]*a[1] + a[2]*a[2]);
return new float[] {
a[0]/length,
a[1]/length,
a[2]/length,
};
}
public static float[] cross(float[] a, float[] b) {
return new float[] {
a[1]*b[2] - a[2]*b[1],
a[2]*b[0] - a[0]*b[2],
a[0]*b[1] - a[1]*b[0],
};
}
public static float dot(float[] a, float[] b) {
return a[0]*b[0] + a[1]*b[1] + a[2]*b[2];
}
/** Main method for testing the example
*/
public static void main(String[] args) {
new GLSLExample();
}
}
Here is the Vertex Array Object class (VAO.java) that handles creating and deleting a simple triangle stored as an Open GL VAO.
import java.nio.ByteBuffer;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
import org.lwjgl.util.glu.GLU;
public class VAO {
private int vaoId, vboId;
public int getId() {
return vaoId;
}
public void createVAO() {
float[] vertices = {
-0.8f, -0.8f, -2.0f,
0.0f, 0.8f, -2.0f,
0.8f, -0.8f, -2.0f,
};
byte[] colors = {
127, 0, 0, 127,
0, 127, 0, 127,
0, 0, 127, 127
};
ByteBuffer vertexData = BufferUtils.createByteBuffer(3 * 16);
for(int i = 0; i < 3; i++) {
vertexData.putFloat(vertices[3*i + 0]);
vertexData.putFloat(vertices[3*i + 1]);
vertexData.putFloat(vertices[3*i + 2]);
vertexData.put(colors, i*4, 4);
}
vertexData.rewind();
vaoId = GL30.glGenVertexArrays(); // Vertex/Attribute array to reference buffers
GL30.glBindVertexArray(vaoId);
vboId = GL15.glGenBuffers(); // Buffer to hold vertex/normal/color data
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vboId);
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, vertexData, GL15.GL_STATIC_DRAW); // upload data to Video Card
GL20.glVertexAttribPointer(0, 3, GL11.GL_FLOAT, false, 16, 0); // Vertex attribute
GL20.glVertexAttribPointer(1, 4, GL11.GL_BYTE, true, 16, 12); // Color attribute
GL20.glEnableVertexAttribArray(0);
GL20.glEnableVertexAttribArray(1);
GL30.glBindVertexArray(0);
int errorCheckValue = GL11.glGetError();
if(errorCheckValue != GL11.GL_NO_ERROR) {
System.out.println("ERROR: Could not create a VBO: " + GLU.gluErrorString(errorCheckValue) );
System.exit(0);
}
}
public void destroyVAO() {
GL20.glDisableVertexAttribArray(1);
GL20.glDisableVertexAttribArray(0);
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
GL15.glDeleteBuffers(vboId);
GL30.glBindVertexArray(0);
GL30.glDeleteVertexArrays(vaoId);
int errorCheckValue = GL11.glGetError();
if(errorCheckValue != GL11.GL_NO_ERROR) {
System.out.println("ERROR: Could not destroy the VBO: " + GL11.glGetString(errorCheckValue) );
System.exit(0);
}
}
}
Here is the Shader class (Shader.java) for compiling and linking a shader’s source (file, string, or reader) to an Open GL shader program.
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20;
public class Shader {
public int shaderProgram;
public int vertexShader;
public int fragmentShader;
public Shader(int shaderProgram, int vertexShader, int fragmentShader) {
this.shaderProgram = shaderProgram;
this.vertexShader = vertexShader;
this.fragmentShader = fragmentShader;
}
public int getProgram() {
return shaderProgram;
}
public void setProgram(int program) {
this.shaderProgram = program;
}
public int getVertexShader() {
return vertexShader;
}
public void setVertexShader(int vertexShader) {
this.vertexShader = vertexShader;
}
public int getFragmentShader() {
return fragmentShader;
}
public void setFragmentShader(int fragmentShader) {
this.fragmentShader = fragmentShader;
}
/** delete, delete this shader program from the Open GL context
*/
public void delete() {
// When the user shuts down your program, you should deallocate all your GL resources.
// Unbind shader program
GL20.glUseProgram(0);
// Detach shaders
GL20.glDetachShader(shaderProgram, vertexShader);
GL20.glDetachShader(shaderProgram, fragmentShader);
// Delete the shaders
GL20.glDeleteShader(vertexShader);
GL20.glDeleteShader(fragmentShader);
// Delete the shader program
GL20.glDeleteProgram(shaderProgram);
}
/** loadShader, load the specified files as Open GL shaders
* @param vertexShaderFile - the vertex shader
* @param fragmentShaderFile - the fragment shader
* @throws IOException if there is an error reading the shader files
*/
public static Shader createShader(File vertexShaderFile, File fragmentShaderFile) throws IOException {
FileReader vertexShaderReader = new FileReader(vertexShaderFile);
FileReader fragmentShaderReader = new FileReader(fragmentShaderFile);
return createShader(vertexShaderReader, fragmentShaderReader);
}
/** loadShader, load the specified readers as Open GL shaders
* @param vertexShaderReader - the vertex shader
* @param fragmentShaderReader - the fragment shader
* @throws IOException if there is an error reading the shader readers
*/
public static Shader createShader(Reader vertexShaderReader, Reader fragmentShaderReader) throws IOException {
int shaderProgram = 0;
int vertexShader = 0;
int fragmentShader = 0;
int maxLength = 0;
String infoLog = null;
// Read the shaders into strings to send to the Video Card
String vertexSource = readReader(vertexShaderReader);
String fragmentSource = readReader(fragmentShaderReader);
// Create an empty vertex shader handle, send the vertex shader source code to Open GL and compile it
vertexShader = GL20.glCreateShader(GL20.GL_VERTEX_SHADER);
GL20.glShaderSource(vertexShader, vertexSource);
GL20.glCompileShader(vertexShader);
// Check for errors uploading the source code or compiling the shader
int isCompiled_VS = GL20.glGetShader(vertexShader, GL20.GL_COMPILE_STATUS);
if(isCompiled_VS == GL11.GL_FALSE) {
maxLength = GL20.glGetShader(vertexShader, GL20.GL_INFO_LOG_LENGTH);
infoLog = GL20.glGetShaderInfoLog(vertexShader, maxLength);
throw new ExceptionInInitializerError("Vertex Shader Info Log: " + infoLog);
}
// Create an empty fragment shader handle, send the fragment shader source code to Open GL and compile it
fragmentShader = GL20.glCreateShader(GL20.GL_FRAGMENT_SHADER);
GL20.glShaderSource(fragmentShader, fragmentSource);
GL20.glCompileShader(fragmentShader);
// Check for errors uploading the source code or compiling the shader
int isCompiled_FS = GL20.glGetShader(fragmentShader, GL20.GL_COMPILE_STATUS);
if(isCompiled_FS == GL11.GL_FALSE) {
maxLength = GL20.glGetShader(fragmentShader, GL20.GL_INFO_LOG_LENGTH);
infoLog = GL20.glGetShaderInfoLog(fragmentShader, maxLength);
throw new ExceptionInInitializerError("Fragment Shader Info Log: " + infoLog);
}
// Create the shader program and attached the vertex and fragment shaders to it
shaderProgram = GL20.glCreateProgram();
GL20.glAttachShader(shaderProgram, vertexShader);
GL20.glAttachShader(shaderProgram, fragmentShader);
Shader shader = new Shader(shaderProgram, vertexShader, fragmentShader);
return shader;
}
/** linkShader, links and uses shader program
* @param shaderProgram - the Open GL integer of the shader program
*/
public static void linkUseShader(int shaderProgram) {
int maxLength = 0;
String shaderProgramInfoLog = null;
// Link our program
/* The vertex and fragment shaders are inspected, optimized and converted to binary code by the Video Card. */
GL20.glLinkProgram(shaderProgram);
GL20.glValidateProgram(shaderProgram);
/* Check and make sure that the program linked. If it fails, it would mean either there is a mismatch between the vertex */
/* and fragment shaders. Or the shaders may have surpassed the Video Card's abilities. Perhaps too many ALU operations or */
/* too many texel fetch instructions or too many interpolators or dynamic loops. */
int isLinked = GL20.glGetProgram(shaderProgram, GL20.GL_LINK_STATUS);
if(isLinked == GL11.GL_FALSE) {
// Noticed the glGetProgram and glGetProgramInfoLog calls, instead of glGetShader and glGetShaderInfoLog
maxLength = GL20.glGetProgram(shaderProgram, GL20.GL_INFO_LOG_LENGTH);
shaderProgramInfoLog = GL20.glGetProgramInfoLog(shaderProgram, maxLength);
// Handle the error in an appropriate way such as displaying a message or writing to a log file.
System.out.println("Shader Program Info Log: " + shaderProgramInfoLog);
throw new ExceptionInInitializerError("Shader Program Info Log: " + shaderProgramInfoLog);
}
// Load the shader into the rendering pipeline
// Remember to also call glUniform** to update shader uniforms
GL20.glUseProgram(shaderProgram);
}
/** readReader
* @param reader - the reader to read
* @return the contents of the reader
* @throws IOException if there is an error reading the reader
*/
private static String readReader(Reader reader) throws IOException {
BufferedReader buffer = new BufferedReader(reader);
StringBuilder contents = new StringBuilder();
String line = null;
while((line = buffer.readLine()) != null) {
contents.append(line + "
");
}
buffer.close();
reader.close();
return contents.toString();
}
}