The description
I'm quite new to OpenGL and started working on a camera system using OpenGL 4.5. I've got an orthogonal camera which should follow the player entity around (Bird's-eye view) through a 2D level made out of other entities. I know that in OpenGL, there is no real camera and instead you move everything else in the opposite direction, thought I'm struggling implementing a orthogonal camera in code. The code below is drawing the player as a 2D triangle while the level itself is made out of other 2D entities with or without textures.
Libraries and languages used to create the code below:
- Java 9
- GLSL 4.5
- OpenGL 4.5 core profile only
- LWJGL 3
- GLFW (part of LWJGL 3)
- JOML math library
The problem
My camera doesn't follow the player and whenever I press a button, for example D, OpenGL renders two flickering player entities on the screen instead of moving the player with it's camera to the right.
My current code
Below is the minimal implementation of a test player and the camera. All other entities can be described as a player without a camera instance. As this is the minimal working example of my player and camera (without the main class to initialize LWJGL 3) I replaced my custom methods with direct OpenGL calls.
The Player:
public class TestPlayer {
private int x = 0, y = 0;
private float size = 1.f;
private OrthoCamera camera;
private int matLocation = 0;
private FloatBuffer matBuffer = BufferUtils.createFloatBuffer(16);
private int shaderProgram = 0;
private IntBuffer vertexArray = BufferUtils.createIntBuffer(1);
private IntBuffer vertexBuffer = BufferUtils.createIntBuffer(1);
private IntBuffer indexBuffer = BufferUtils.createIntBuffer(1);
Matrix4f projection = new Matrix4f().ortho(-16.f, 16.f, -9f, 9f, -1.f, 1.f);
Matrix4f model = new Matrix4f().identity().translate(new Vector3f(0, 0, 0));
private String[] vertexShaderSource = {
"#version 450 core\n",
"\n",
"layout (location = 0) in vec4 position;\n",
"\n",
"uniform mat4 u_MVP;\n",
"\n",
"void main() {\n",
" gl_Position = u_MVP * position;\n",
"}\n"
};
private String[] fragmentShaderSource = {
"#version 450 core\n",
"\n",
"layout (location = 0) out vec4 colour;\n",
"layout (location = 1) uniform vec4 u_Colour;\n",
"\n",
"void main() {\n",
" colour = u_Colour;\n",
"}"
};
public TestPlayer(Vector3f position, Vector3f lookat) {
this.camera = new OrthoCamera(position, lookat);
this.updatePositions();
this.shaderProgram = this.createShader();
glUseProgram(this.shaderProgram);
matBuffer.clear();
matLocation = glGetUniformLocation(this.shaderProgram, "u_MVP");
glUniformMatrix4fv(matLocation, false, projection.get(matBuffer));
glUseProgram(0);
glBindVertexArray(0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
private void updatePositions() {
if(vertexArray.hasRemaining()) {
glDeleteVertexArrays(this.vertexArray);
vertexArray.clear();
}
if(indexBuffer.hasRemaining()) {
glDeleteBuffers(this.indexBuffer);
indexBuffer.clear();
}
if(vertexBuffer.hasRemaining()) {
glDeleteBuffers(this.vertexBuffer);
vertexBuffer.clear();
}
float[] positions = {
x-size, y-size,
x , y+size,
x+size, y-size
};
int[] indices = {
0, 1, 2
};
glGenVertexArrays(this.vertexArray);
FloatBuffer vertexData = BufferUtils.createFloatBuffer(3 * 2);
vertexData.put(positions);
vertexData.flip();
glGenBuffers(this.vertexBuffer);
glBindBuffer(GL_ARRAY_BUFFER, this.vertexBuffer.get(0));
glBufferData(GL_ARRAY_BUFFER, vertexData, GL_STATIC_DRAW);
glBindVertexArray(this.vertexArray.get(0));
glBindBuffer(GL_ARRAY_BUFFER, this.vertexBuffer.get(0));
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, false, Float.BYTES * 2, 0);
IntBuffer indexData = BufferUtils.createIntBuffer(3);
indexData.put(indices);
indexData.flip();
glGenBuffers(this.indexBuffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this.indexBuffer.get(0));
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexData, GL_STATIC_DRAW);
}
public void update() {
this.camera.update(new Vector3f(x, y, 1));
}
public void keyUpdate(int key) {
if(key == GLFW.GLFW_KEY_W)
y++;
if(key == GLFW.GLFW_KEY_S)
y--;
if(key == GLFW.GLFW_KEY_A)
x--;
if(key == GLFW.GLFW_KEY_D)
x++;
}
public void render(double currentTime) {
matBuffer.clear();
this.updatePositions();
glUseProgram(this.shaderProgram);
glBindVertexArray(this.vertexArray.get(0));
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this.indexBuffer.get(0));
// colour
glUniform4f(1, 0.f, .8f, 1.f, 1.f);
Matrix4f matrix = projection.mul(camera.getView().mul(model));
glUniformMatrix4fv(matLocation, false, matrix.get(matBuffer));
glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, 0);
}
public void dispose() {
glBindVertexArray(0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glDeleteVertexArrays(this.vertexArray);
glDeleteBuffers(this.indexBuffer);
glDeleteBuffers(this.vertexBuffer);
glUseProgram(0);
glDeleteProgram(this.shaderProgram);
}
private int createShader() {
int program = glCreateProgram();
int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, vertexShaderSource);
glCompileShader(vertexShader);
if(glGetShaderi(vertexShader, GL_COMPILE_STATUS) == GL_FALSE) {
System.err.println("ERROR: Compiling vertex shader");
System.err.println(glGetShaderInfoLog(vertexShader));
glDeleteShader(vertexShader);
return -1;
}
int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, fragmentShaderSource);
glCompileShader(fragmentShader);
if(glGetShaderi(fragmentShader, GL_COMPILE_STATUS) == GL_FALSE) {
System.err.println("ERROR: Compiling fragment shader");
System.err.println(glGetShaderInfoLog(fragmentShader));
glDeleteShader(fragmentShader);
return -1;
}
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
glLinkProgram(program);
glValidateProgram(program);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
return program;
}
}
The camera: Do note that this is a barebone camera class. The goal is to get this very simple version of the camera working before adding more features to it.
public class OrthoCamera {
private Vector3f position;
private Matrix4f view = new Matrix4f().identity();
public OrthoCamera(Vector3f position, Vector3f lookAt) {
this.position = position;
this.view.lookAt(position, lookAt, new Vector3f(0, 1, 0));
}
public void update(Vector3f lookAt) {
this.view.identity().lookAt(position, lookAt, new Vector3f(0, 1, 0));
}
public Matrix4f getView() {
return this.view;
}
}
EDIT:
I've just created a gif of the problem. The blue triangle is the TestPlayer while the other triangle is a non-moving entity. The D
key is the only key which has been pressed (once) in the gif below.
EDIT 2: The main class of the minimal example:
public class HelloWorld {
// The window handle
private long window;
private TestPlayer player;
private TestPlayer2 player2;
private Callback glErrorCallback;
private static final IntBuffer SCREEN_WIDTH = BufferUtils.createIntBuffer(1);
private static final IntBuffer SCREEN_HEIGHT = BufferUtils.createIntBuffer(1);
public void run() {
// Output in my case: Hello LWJGL 3.2.0 build 12!
System.out.println("Hello LWJGL " + Version.getVersion() + "!");
init();
loop();
if(glErrorCallback != null)
this.glErrorCallback.free();
// Free the window callbacks and destroy the window
glfwFreeCallbacks(window);
glfwDestroyWindow(window);
GL.setCapabilities(null);
// Terminate GLFW and free the error callback
glfwTerminate();
glfwSetErrorCallback(null).free();
}
private void init() {
GLFWErrorCallback.createPrint(System.err).set();
if ( !glfwInit() )
throw new IllegalStateException("Unable to initialize GLFW");
// Configure GLFW
glfwDefaultWindowHints();
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 5);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GLFW_TRUE);
// Create the window
window = glfwCreateWindow(1280, 720, "Hello World!", NULL, NULL);
if ( window == NULL )
throw new RuntimeException("Failed to create the GLFW window");
glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
if ( key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE )
glfwSetWindowShouldClose(window, true);
if ( action == GLFW.GLFW_PRESS)
if(player != null)
player.keyUpdate(key);
});
// Get the thread stack and push a new frame
try ( MemoryStack stack = stackPush() ) {
IntBuffer pWidth = stack.mallocInt(1); // int*
IntBuffer pHeight = stack.mallocInt(1); // int*
// Get the window size passed to glfwCreateWindow
glfwGetWindowSize(window, pWidth, pHeight);
// Get the resolution of the primary monitor
GLFWVidMode vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor());
// Center the window
glfwSetWindowPos(
window,
(vidmode.width() - pWidth.get(0)) / 2,
(vidmode.height() - pHeight.get(0)) / 2
);
} // the stack frame is popped automatically
glfwMakeContextCurrent(window);
// Enable v-sync
glfwSwapInterval(1);
// Make the window visible
glfwShowWindow(window);
}
private void loop() {
GL.createCapabilities();
glfwGetFramebufferSize(this.window, SCREEN_WIDTH, SCREEN_HEIGHT);
glViewport(0, 0, SCREEN_WIDTH.get(), SCREEN_HEIGHT.get());
this.glErrorCallback = GLUtil.setupDebugMessageCallback();
// Set the clear color
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
player = new TestPlayer(new Vector3f(0, 0, 0), new Vector3f(0, 0, 0));
player2 = new TestPlayer2();
// Run the rendering loop until the user has attempted to close
// the window or has pressed the ESCAPE key.
while ( !glfwWindowShouldClose(window) ) {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // clear the framebuffer
// normally, update would not be called as often as render
player.update();
player2.update();
player2.render(glfwGetTime());
// the player with the camera
player.render(glfwGetTime());
glfwSwapBuffers(window); // swap the color buffers
// Poll for window events. The key callback above will only be
// invoked during this call.
glfwPollEvents();
}
player.dispose();
player2.dispose();
}
public static void main(String[] args) {
new HelloWorld().run();
}
}
The second static entity:
public class TestPlayer2 {
private int x = 3, y = 3;
private float size = 1.f;
private int shaderProgram = 0;
private IntBuffer vertexArray = BufferUtils.createIntBuffer(1);
private IntBuffer vertexBuffer = BufferUtils.createIntBuffer(1);
private IntBuffer indexBuffer = BufferUtils.createIntBuffer(1);
private String[] vertexShaderSource = {
"#version 450 core\n",
"\n",
"layout (location = 0) in vec4 position;\n",
"\n",
"uniform mat4 u_MVP;\n",
"\n",
"void main() {\n",
" gl_Position = u_MVP * position;\n",
"}\n"
};
private String[] fragmentShaderSource = {
"#version 450 core\n",
"\n",
"layout (location = 0) out vec4 colour;\n",
"layout (location = 1) uniform vec4 u_Colour;\n",
"\n",
"void main() {\n",
" colour = u_Colour;\n",
"}"
};
public TestPlayer2() {
float[] positions = {
x-size, y-size,
x , y+size,
x+size, y-size
};
int[] indices = {
0, 1, 2
};
glGenVertexArrays(this.vertexArray);
FloatBuffer vertexData = BufferUtils.createFloatBuffer(3 * 2);
vertexData.put(positions);
vertexData.flip();
glGenBuffers(this.vertexBuffer);
glBindBuffer(GL_ARRAY_BUFFER, this.vertexBuffer.get(0));
glBufferData(GL_ARRAY_BUFFER, vertexData, GL_STATIC_DRAW);
glBindVertexArray(this.vertexArray.get(0));
glBindBuffer(GL_ARRAY_BUFFER, this.vertexBuffer.get(0));
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, false, Float.BYTES * 2, 0);
IntBuffer indexData = BufferUtils.createIntBuffer(3);
indexData.put(indices);
indexData.flip();
glGenBuffers(this.indexBuffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this.indexBuffer.get(0));
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexData, GL_STATIC_DRAW);
Matrix4f projection = new Matrix4f().ortho(-16.f, 16.f, -9f, 9f, -1.f, 1.f);
this.shaderProgram = this.createShader();
glUseProgram(this.shaderProgram);
int location = glGetUniformLocation(this.shaderProgram, "u_MVP");
FloatBuffer buffer = BufferUtils.createFloatBuffer(16);
glUniformMatrix4fv(location, false, projection.get(buffer));
glUseProgram(0);
glBindVertexArray(0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
private int createShader() {
int program = glCreateProgram();
int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, vertexShaderSource);
glCompileShader(vertexShader);
if(glGetShaderi(vertexShader, GL_COMPILE_STATUS) == GL_FALSE) {
System.err.println("ERROR: Compiling vertex shader");
System.err.println(glGetShaderInfoLog(vertexShader));
glDeleteShader(vertexShader);
return -1;
}
int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, fragmentShaderSource);
glCompileShader(fragmentShader);
if(glGetShaderi(fragmentShader, GL_COMPILE_STATUS) == GL_FALSE) {
System.err.println("ERROR: Compiling fragment shader");
System.err.println(glGetShaderInfoLog(fragmentShader));
glDeleteShader(fragmentShader);
return -1;
}
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
glLinkProgram(program);
glValidateProgram(program);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
return program;
}
public void update() {
// Nothing in here, static entity
}
public void render(double currentTime) {
glUseProgram(this.shaderProgram);
glBindVertexArray(this.vertexArray.get(0));
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this.indexBuffer.get(0));
glUniform4f(1, 8.f, .8f, 1.f, 1.f);
glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, 0);
}
public void dispose() {
glBindVertexArray(0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glDeleteVertexArrays(this.vertexArray);
glDeleteBuffers(this.indexBuffer);
glDeleteBuffers(this.vertexBuffer);
glUseProgram(0);
glDeleteProgram(this.shaderProgram);
}
}