Search code examples
javaopenglglslshaderlwjgl

LWJGL Tilerenderer only renders one type of tile/one texture


I'm learning LWJGL and OpenGL by following this tutorial i found, i tried my best to change the code to be compatible with the never versions and hadnt a problem with it so far. But now my Tilerenderer wont render more than one type of tile/one texture for the VBO's and i tried to fix it now since 3 days (in the beginning it didn't render anything at all) but couldn't find anything that fixes this problem.

I'm using Java 9.0.4 and LWJGL 3.2.1 build 12 with JOML 1.9.13, GLFW, OpenGL and stb.

So far i tried changing the entire code involved in this problem and changing different variables for shaders but nothing seemed to work so far.

Here are all classes i figured might have something to do with the problem.

The Main class

import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.opengl.GL11.*;

import org.joml.Matrix4f;
import org.joml.Vector3f;
import org.lwjgl.opengl.GL;

public class Main {



    public static void main(String[] args) {

        int speed = 5;

        Window.setCallbacks();

        if (!glfwInit()) {

            throw new IllegalStateException("Failed to init GLFW");

        }

        glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);

        Window window = new Window();
        window.setSize(640, 480);
        window.setFullscreen(false);
        window.createWindow("GAME");

        GL.createCapabilities();

        Camera camera = new Camera(window.getWidth(), window.getHeight());

        glEnable(GL_TEXTURE_2D);

        TileRenderer tiles = new TileRenderer();

        Shader shader = new Shader("shader");

        World world = new World();

        world.setTile(Tile.test_tile, 0, 0);

        double frameCap = 1.0 / 60.0;
        double frameTime     = 0;
        double time = Timer.getTime();
        double unprocessed = 0;

        int frames = 0;

        while(!window.shouldClose()) {

            boolean canRender = false;

            double time2 = Timer.getTime();
            double passed = time2 - time;
            unprocessed+=passed;

            frameTime += passed;

            time = time2;

            while (unprocessed >= frameCap) {

                canRender = true;

                unprocessed-=frameCap;

                if(window.getInput().isMouseButtonDown(0)) {

                    glfwSetWindowShouldClose(window.getWindow(), true);

                }   

                if (window.getInput().isKeyPressed(GLFW_KEY_ESCAPE)) {

                    glfwSetWindowShouldClose(window.getWindow(), true);

                }

                if(window.getInput().isKeyDown(GLFW_KEY_W)) {

                    camera.addPosition(new Vector3f(0, -speed, 0));

                }

                if(window.getInput().isKeyDown(GLFW_KEY_A)) {

                    camera.addPosition(new Vector3f(speed, 0, 0));

                }

                if(window.getInput().isKeyDown(GLFW_KEY_S)) {

                    camera.addPosition(new Vector3f(0, speed, 0));

                }

                if(window.getInput().isKeyDown(GLFW_KEY_D)) {

                    camera.addPosition(new Vector3f(-speed, 0, 0));

                }

                if(window.getInput().isKeyDown(GLFW_KEY_O)) {

                    speed = 5;

                }

                if(window.getInput().isKeyDown(GLFW_KEY_P)) {

                    speed = 25;

                }

                window.update();

                if (frameTime >= 1.0) {

                    frameTime = 0;
                    System.out.println("FPS:" + frames);
                    frames = 0;

                }
            }

            if (canRender) {

                glClear(GL_COLOR_BUFFER_BIT);

                world.render(tiles, shader, camera);

                window.swapBuffers();

                frames++;

            }
        }

        glfwTerminate();

    }
}

The World class

import org.joml.Matrix4f;
import org.joml.Vector3f;

public class World {

    private byte[] tiles;

    private int width;
    private int height;

    private Matrix4f world;

    public World () {

        width = 16;
        height = 16;

        tiles = new byte [width * height];

        world = new Matrix4f().setTranslation(new Vector3f(0));
        world.scale(32);
    }

    public void render(TileRenderer renderer, Shader shader, Camera camera) {

        for (int x = 0; x < height; x++) {

            for (int y = 0; y < width; y++) {

                renderer.renderTile(tiles[x + y * width], y, -x, shader, world, camera);

            }

        }

    }

    public void setTile (Tile tile, int x, int y) {

        System.err.println(tile.getId());
        tiles[x + y * width] = tile.getId();


    }

}

The Tilerenderer class

import java.util.HashMap;

import org.joml.Matrix4f;
import org.joml.Vector3f;

public class TileRenderer {

        private HashMap<String, Texture> tileTextures;

        private Model tileModel;

        public TileRenderer() {
            tileTextures = new HashMap<>();
            float[] vertices = new float[]{
                -1f, 1f, 0, // TOP LEFT 0
                1f, 1f, 0,  // TOP RIGHT 1
                1f, -1f, 0, // BOTTOM RIGHT 2
                -1f, -1f, 0,// BOTTOM LEFT 3
            };

            float[] texture = new float[]{0, 0, 1, 0, 1, 1, 0, 1,};

            int[] indices = new int[]{0, 1, 2, 2, 3, 0};

            tileModel = new Model(vertices, texture, indices);

            for (int i = 0; i < Tile.tiles.length; i++) {
                if (Tile.tiles[i] != null) {
                    if (!tileTextures.containsKey(Tile.tiles[i].getTexture())) {
                        String tex = Tile.tiles[i].getTexture();
                        tileTextures.put(tex, new Texture(tex + ".png"));
                    }
                }
            }
        }

        public void renderTile (byte id, int x, int y, Shader shader, Matrix4f world, Camera camera) { 

            shader.bind();

            if (tileTextures.containsKey(Tile.tiles[id].getTexture())) {

                tileTextures.get(Tile.tiles[id].getTexture()).bind(0);

            }

            Matrix4f tilePos = new Matrix4f().translate(new Vector3f(x*2, y*2, 0));
            Matrix4f target = new Matrix4f();

            camera.getProjection().mul(world, target);
            target.mul(tilePos);

            shader.setUniform("sampler", 0);
            shader.setUniform("projection", target);

            tileModel.render();

        }   
}

The Tile class

public class Tile {

    public static Tile tiles[] = new Tile[16];

    public static final Tile testTile = new Tile((byte)0, "Test");
    public static final Tile testTile2 = new Tile((byte)1, "Test2");

    private byte id;
    private String texture;

    public Tile(byte id, String texture) {

        this.id = id;
        this.texture = texture;

        if (tiles[id] != null) {

            throw new IllegalStateException("Tiles at: [" + id + "] is already being used!");

        }

        tiles[id] = this;

    }

    public byte getId () {return id;}
    public String getTexture () {return texture;}

}

The Model class

import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL15.*;
import static org.lwjgl.opengl.GL20.*;

import java.nio.FloatBuffer;
import java.nio.IntBuffer;

import org.lwjgl.BufferUtils;

public class Model {

    private int draw_count;
    private int v_id;
    private int t_id;
    private int i_id;

    public Model (float[] vertices, float[] tex_coords, int[] indices) {

        draw_count = indices.length;

        IntBuffer buffer = BufferUtils.createIntBuffer(indices.length);
        buffer.put(indices);
        buffer.flip();

        v_id = glGenBuffers();

        glBindBuffer(GL_ARRAY_BUFFER, v_id);
        glBufferData(GL_ARRAY_BUFFER, createBuffer(vertices), GL_STATIC_DRAW);

        t_id = glGenBuffers();

        glBindBuffer(GL_ARRAY_BUFFER, t_id);
        glBufferData(GL_ARRAY_BUFFER, createBuffer(tex_coords), GL_STATIC_DRAW);

        i_id = glGenBuffers();

        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, i_id);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, buffer, GL_STATIC_DRAW);

        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

    }

    public void render() {

        glEnableVertexAttribArray(0);
        glEnableVertexAttribArray(1);

        glBindBuffer(GL_ARRAY_BUFFER, v_id);
        glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);

        glBindBuffer(GL_ARRAY_BUFFER, t_id);
        glVertexAttribPointer(1, 2, GL_FLOAT, false, 0, 0);

        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, i_id);
        glDrawElements(GL_TRIANGLES, draw_count, GL_UNSIGNED_INT, 0);

        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

        glDisableVertexAttribArray(0);
        glDisableVertexAttribArray(1);

    }

    private FloatBuffer createBuffer(float[] data) {

        FloatBuffer buffer = BufferUtils.createFloatBuffer(data.length);
        buffer.put(data);
        buffer.flip();
        return buffer;

    }
}

The Texture class

import java.nio.ByteBuffer;
import java.nio.IntBuffer;

import static org.lwjgl.stb.STBImage.*;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL13.*;

import org.lwjgl.BufferUtils;

public class Texture {

    private int id;
    private int width;
    private int heigth;

    public Texture (String filename) {

        IntBuffer width = BufferUtils.createIntBuffer(1);
        IntBuffer heigth = BufferUtils.createIntBuffer(1);
        IntBuffer comp = BufferUtils.createIntBuffer(1);

        ByteBuffer data = stbi_load("./res/" + filename, width, heigth, comp, 4);

        this.width = width.get();
        this.heigth = heigth.get();

        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, this.width, this.heigth, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);   

        stbi_image_free(data);

    }

    public void bind (int sampler) {

        if (sampler >= 0 && sampler <= 31) {

            glActiveTexture(GL_TEXTURE0 + sampler);
            glBindTexture(GL_TEXTURE_2D, sampler);

        }
    }
}

The Shader class

import static org.lwjgl.opengl.GL20.*;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.nio.FloatBuffer;

import org.joml.Matrix4f;
import org.lwjgl.BufferUtils;

public class Shader {

    private int program;
    private int vs;
    private int fs;

    public Shader (String filename) {

        program = glCreateProgram();

        vs = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(vs, readFile(filename + ".vs"));
        glCompileShader(vs);
        if (glGetShaderi(vs, GL_COMPILE_STATUS) != 1) {

            System.err.println(glGetShaderInfoLog(vs));
            System.exit(1);

        }

        fs = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fs, readFile(filename + ".fs"));
        glCompileShader(fs);
        if (glGetShaderi(fs, GL_COMPILE_STATUS) != 1) {

            System.err.println(glGetShaderInfoLog(fs));
            System.exit(1);

        }

        glAttachShader(program, vs);
        glAttachShader(program, fs);

        glBindAttribLocation(program, 0, "vertices");
        glBindAttribLocation(program, 1, "textures");

        glLinkProgram(program);
        if (glGetProgrami(program, GL_LINK_STATUS) != 1) {

            System.err.println(glGetProgramInfoLog(program));
            System.exit(1);

        }

        glValidateProgram(program);
        if (glGetProgrami(program, GL_VALIDATE_STATUS) != 1) {

            System.err.println(glGetProgramInfoLog(program));
            System.exit(1);

        }
    }

    public void bind () {

        glUseProgram(program);

    }

    private String readFile (String filename) {

        StringBuilder string = new StringBuilder();

        BufferedReader br;

        try {

            br = new BufferedReader(new FileReader(new File("./shaders/" + filename)));
            String line;

            while((line = br.readLine()) != null) {

                string.append(line);
                string.append("\n");

            }

        } catch (IOException e ) {e.printStackTrace();}

        return string.toString();

    }

    public void setUniform (String name, int value) {

        int location = glGetUniformLocation(program, name);

        if (location != -1) {

            glUniform1i(location, value);

        }
    }

    public void setUniform (String name, Matrix4f value) {

        int location = glGetUniformLocation(program, name);

        FloatBuffer buffer = BufferUtils.createFloatBuffer(16); 

        value.get(buffer);

        if (location != -1) {

            glUniformMatrix4fv(location, false, buffer);

        }
    }
}

The Fragment Shader

#version 120

uniform sampler2D sampler;

varying vec2 tex_coords;

void main () {
    gl_FragColor = texture2D(sampler, tex_coords);
}

The Vertex Shader

#version 120

attribute vec3 vertices;
attribute vec2 textures;

varying vec2 tex_coords;

uniform mat4 projection;

void main() {
    tex_coords = textures;
    gl_Position = projection*vec4(vertices, 1);
}

So far I'm creating 16x16 tiles all with the same texture but it is supposed to change the tile at 0, 0 (top left corner) to have a different texture


Solution

  • There is a basic misunderstanding, about how texturing works in OpenGL.

    You have to create a separate texture object for each texture by glGenTextures. (See also Java Code Examples for org.lwjgl.opengl.GL11.glTexImage2D()).

    int textureObject = glGenTextures();
    

    This texture object has to be bound before loading the texture and before rendering the mesh. The texture is bound to the active texture unit, which is set by glActiveTexture.

    int textureUnitIndex = 0; // e.g
    glActiveTexture(GL_TEXTURE0 + textureUnitIndex);
    glBindTexture(GL_TEXTURE_2D, textureObject);
    

    The texture unit is a binding point for the shader program. The texture sampler in the shader program has to be associated to the same binding point. This can be done by glUniform1i:

    GLSL:

    uniform sampler2D sampler;
    

    Java:

    int location = glGetUniformLocation(program, "sampler");
    glUniform1i(location, textureUnitIndex);
    

    Sidenote: since GLSL version 4.2 this can be done in the fragment shader by specifying binding points - See OpenGL Shading Language 4.20 Specification - 4.4.4 Opaque-Uniform Layout Qualifiers; page 60:

    #version 420
    
    layout (binding = 0) uniform sampler2D sampler;
    

    In your code never a texture object is generated. So the default texture object 0 is used for all textures. This causes that you code doesn't work.

    Change the class Texture, somehow as follows, to solve the issue:

    public class Texture {
    
        private int textureObject;
        private int width;
        private int heigth;
    
        public Texture (String filename) {
    
            IntBuffer width = BufferUtils.createIntBuffer(1);
            IntBuffer heigth = BufferUtils.createIntBuffer(1);
            IntBuffer comp = BufferUtils.createIntBuffer(1);
    
            ByteBuffer data = stbi_load("./res/" + filename, width, heigth, comp, 4);
    
            this.width = width.get();
            this.heigth = heigth.get();
    
            textureObject = glGenTextures(); // generate texture name
            glBindTexture(GL_TEXTURE_2D, textureObject);
    
            glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, this.width, this.heigth, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);   
    
            stbi_image_free(data);
    
        }
    
        public void bind (int sampler) {
    
            if (sampler >= 0 && sampler <= 31) {
    
                glActiveTexture(GL_TEXTURE0 + sampler);
                glBindTexture(GL_TEXTURE_2D, textureObject); // bind texture object 
            }
        }
    }