1

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.

visualisation of the problem, blue triangle is TestPlayer

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);
    }
}
Community
  • 1
  • 1
ShadowDragon
  • 2,238
  • 5
  • 20
  • 32
  • 1
    Some quick thoughts: **1)** Don't create the shader-programm each time a `TestPlayer`is created; once is enough. **2)** Update buffers and uniforms in the `render` function, and only if the data has changed. **3)** The `2`s used at `glVertexAttribPointer` should be `4` because the VS expects a `vec4` (or you know very well how it works when sizes don't match) **4)** When the keyboard is pressed not only model needs to be updated, but camera as well if you want it to follow the model. – Ripi2 Dec 05 '18 at 18:36
  • **to 2)** Meaning I should move all my glBufferData methods in the render loop and check if the values have changed before uploading it to the GPU? **to 3)** Changing the `2`s to `4`s in `glVertexAttribPointer` causes my triangle to no longer look like a triangle. More like a big line. `size` equals two because I'm sending two values for each vertex (x and y coordinates). The second `2` is a 8 as `Float.BYTES * 2 = 8` in Java. **to 4)** `camera.update` updates the camera. – ShadowDragon Dec 05 '18 at 19:10
  • **2)** Yes. The idea is to avoid unneeded RAM->GPU transfers. **3)** Then use `in vec2 position;` in the VS, but `gl_Position` is a vec4 you must fill with something. **4)** When is it called? – Ripi2 Dec 05 '18 at 19:20
  • **to 4)** It is called in `TestPlayer.update` which is called right before the render method is called in my main class. Normally I wouldn't call it as often as render, but in this minimal example I do. – ShadowDragon Dec 05 '18 at 19:25
  • Speaking of "minimal example"... Post a *compilable* example, you're hiding some parts which may be relevant. – Ripi2 Dec 05 '18 at 19:27
  • @Ripi2 This should now be the complete code required to run the minimal example. – ShadowDragon Dec 05 '18 at 19:38
  • @httpdigest Thx for the note. I changed my code accordingly: `glUniformMatrix4fv(matLocation, false, projection.mul(camera.getView().mul(model)).get(matBuffer));`. One problem less to worry about, thought the main problem isn't fixed. – ShadowDragon Dec 06 '18 at 16:57
  • @httpdigest I don't really understand what's wrong with my approach. `camera.getView().mul(model)` changes the camera matrix and stores the change into the camera matrix which is then passed on to the projection matrix, thus the end result is stored in the projection matrix which is send to the GPU. The only other way I see how I could do this is split the entire thing up into 3 lines (one line per operation). Thought in the end, it still looks the same, except that I've got now 3 lines instead of 1. – ShadowDragon Dec 06 '18 at 18:32
  • Adding `projection.setOrtho(-16.f, 16.f, -9f, 9f, -1.f, 1.f);` to my render method after the object is drawn and resetting the view matrix to it's original position fixes the flickering, but still doesn't allow me to move my player with the camera following him. – ShadowDragon Dec 06 '18 at 19:01

0 Answers0