0
\$\begingroup\$

I'm making a little OpenGL renderer. Currently I use VAOs/VBOs and batch all the entities with the same VAO. I then draw using glDrawElementsInstanced. I am wondering how can I send a different textures in the uniform for the Sampler2D since each entities might have a different texture ?

Here the batch Java side building the uniform buffer:

public class ContainerBatch extends Batch<VisualContainer> {
    private VertexArray vao;
    private Camera perspective;
    private UniformBuffer buffer;

    public ContainerBatch(ShaderProgram program, Camera perspective) {
        super(program);
        this.vao = new RectangleVertexArray();
        this.perspective = perspective;
        this.buffer = new UniformBuffer()
                .setBinding(0)
                .addMatrix4fEntry("quadsTransforms", true)
                .addVector2fEntry("quadsSizes", true)
                .addIntegerEntry("quadsTextures", true)
                .addBooleanEntry("quadsHasTexture", true)
                .addVector3fEntry("quadsFillColors", true)
                .addFloatEntry("quadsOpacities", true)
                .allocate();
    }

    public void prepare() {
        if(this.size() > UniformBuffer.ARRAY_SIZE)
            throw new IllegalStateException("Batch cannot contain more than " + UniformBuffer.ARRAY_SIZE + " models.");

        this.program.use();
        this.program.sendMatrix4f("perspective", this.perspective.getOrthographicView());
    }

    public void fillBuffer() {
        IntStream.range(0, this.size()).parallel().forEach(i -> {
            VisualContainer visualContainer = this.get(i);

            if(this.perspective.contains(visualContainer) && visualContainer.isVisible()) {
                buffer.insertMatrix4fAt("quadsTransforms", visualContainer.getWorldTransformation(), i);
                buffer.insertVector2fAt("quadsSizes", visualContainer.getScale(), i);

                if(visualContainer.getTexture() != null) {
                    buffer.insertIntegerAt("quadsTextures", visualContainer.getTexture().getId(), i);
                    buffer.insertBooleanAt("quadsHasTexture", true, i);
                } else {
                    buffer.insertIntegerAt("quadsTextures", 0, i);
                    buffer.insertBooleanAt("quadsHasTexture", false, i);
                }

                buffer.insertVector3fAt("quadsFillColors", visualContainer.getFillColor(), i);
                buffer.insertFloatAt("quadsOpacities", visualContainer.getOpacity(), i);
            }
        });
    }

    public void draw(float depth) {
        this.fillBuffer();

        this.program.use();
        this.program.sendUniformBuffer(this.buffer);
        this.program.sendFloat("depth", depth);

        if(this.perspective.isDirty()) {
            this.program.sendMatrix4f("perspective", this.perspective.getOrthographicView());
            this.program.sendVector4f("viewport", this.perspective.getViewport());
        }

        this.vao.bind();
        this.vao.getIndexBuffer().bind();
        glDrawElementsInstanced(GL_TRIANGLES, this.vao.getIndexBuffer().getCount(), GL_UNSIGNED_INT, 0, this.size());
    }
}

Here's my vertex shader:

#version 460 core

const int MAX_QUADS_PER_SCENE = _[MAX_QUADS];

uniform mat4 perspective;
uniform vec4 viewport;
uniform float depth;

layout (location = 0) in vec3 vertex;
layout (location = 1) in vec2 texCoord;

layout(std140, binding=0) uniform quads {
    mat4 quadsTransforms[MAX_QUADS_PER_SCENE];
    vec2 quadsSizes[MAX_QUADS_PER_SCENE];
    int quadsTextures[MAX_QUADS_PER_SCENE];
    int quadsHasTexture[MAX_QUADS_PER_SCENE];
    vec3 quadsFillColors[MAX_QUADS_PER_SCENE];
    float quadsOpacities[MAX_QUADS_PER_SCENE];
};

out vec2 quadTexCoord;
out vec2 quadSize;
out vec3 quadColor;
out float quadOpacity;

out int textureSampler;
out int hasTexture;

void main() {
    quadTexCoord = texCoord;
    quadSize = quadsSizes[gl_InstanceID];
    textureSampler = quadsTextures[gl_InstanceID];
    quadColor = quadsFillColors[gl_InstanceID];
    quadOpacity = quadsOpacities[gl_InstanceID];

    float ndcDepth = (depth / 500) - 1;
    mat4 quadMatrice = quadsTransforms[gl_InstanceID];
    gl_Position = perspective * quadMatrice * vec4(vertex.xy, ndcDepth, 1.0);
}

And here's my fragment shader:

#version 460 core

out vec4 outColor;

in vec2 quadTexCoord;
in vec2 quadSize;
in vec3 quadColor;
in float quadOpacity;

in flat int textureSampler;
in flat int hasTexture;

void main() {
    if(hasTexture == 1) {
        vec4 texturePixelColor = texture(sampler2D(textureSampler), quadTexCoord);
        texturePixelColor.w = min(texturePixelColor.w, quadOpacity);
        outColor = texturePixelColor;
    } else {
        outColor = vec4(quadColor, quadOpacity);
    }
}

As you can see for the shader, I tried sending the texture id directly and using it as a sampler to no avail. To my understanding Sampler2Ds needs to be dynamic uniforms which means I can only send one with my draw call. Of course I could use an atlas here and change the texture coordinates. I'm wondering if this is possible using multiple images.

Thank you.

\$\endgroup\$
3
  • \$\begingroup\$ Your solution using an atlas (or array texture, to avoid having to change UV coordinates — just add in a per-instance index in the shader) is the way this is usually done. Want to write that up as an Answer below? \$\endgroup\$
    – DMGregory
    Commented Apr 26, 2023 at 18:36
  • \$\begingroup\$ How are array texture done ? I found some code relating to those in my searches but don't quite understand how to make it fit with my current impl. \$\endgroup\$ Commented Apr 26, 2023 at 18:37
  • 1
    \$\begingroup\$ You can find the documentation for OpenGL Array Textures here. If you need help converting your current solution to use them, consider editing your question to ask "How to use Array Textures to select different textures per instance?" and folks can answer that refined question. \$\endgroup\$
    – DMGregory
    Commented Apr 26, 2023 at 19:04

1 Answer 1

0
\$\begingroup\$

I have opted for simply utilising the many texture slots provided by OpenGL. When I load a texture I set its ID into an array where the index is the sampler index. In my fragment shader, the sampler2D is now an array.

Texture code:

public Texture(String filePath) {
        this.path = "src/main/resources/texture/" + filePath;
        this.id = glGenTextures();
        registry[currentIndex++] = this.id;

        this.bind();
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);  // Horizontal Wrap
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); // Vertical Wrap

        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); // Stretch algorithm
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); // Shrink algorithm

        int[] width = new int[1];
        int[] height = new int[1];
        int[] channels = new int[1];
        ByteBuffer image = stbi_load(this.path, width, height, channels, 0);

        if(image != null) {
            this.width = width[0];
            this.height = height[0];
            this.channels = channels[0] == 3 ? GL_RGB : GL_RGBA;
            System.out.println(this.width + ", " + this.height);

            glTexImage2D(GL_TEXTURE_2D, 0, this.channels, this.width, this.height,
                    0, this.channels, GL_UNSIGNED_BYTE, image);
            stbi_image_free(image);
        }
    }

Then I bind the entries where the id > -1 (i initialise the registry array to -1 everywhere) like so:

for(int i = 0; i < Texture.registry.length; i++) {
            if(Texture.registry[i] > -1) {
                glActiveTexture(GL_TEXTURE0 + i);
                glBindTexture(GL_TEXTURE_2D, Texture.registry[i]);
                this.program.sendTexture("textureSamplers", i); // Transform the string to add [i] at the end
            }
        }

And finally in my fragment shader I pass a texture index for that entity:

#version 460 core

out vec4 outColor;

in vec2 quadTexCoord;
in vec2 quadSize;
in vec3 quadColor;
in float quadOpacity;

uniform sampler2D textureSamplers[32];
in flat int textureIndex;

void main() {
    if(textureIndex >= 0) {
        vec4 texturePixelColor = texture(textureSamplers[textureIndex], quadTexCoord);
        texturePixelColor.w = min(texturePixelColor.w, quadOpacity);
        outColor = texturePixelColor;
    } else {
        outColor = vec4(quadColor, quadOpacity);
    }
}

Unsure if this is the best/right way but it fixes my problem. Of course I will never use all 32 slots and since the amount of slots changes based on hardware, I might make the size of the array dynamic by expanding it before compiling.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .