Search code examples
c#texturesfragment-shaderopentk

What is the correct way to bind Multiple Textures on OpenTK?


I tried to implement a simple test program following the OpenTK official tutorial (here) but I can't achieve any result. Following the exact coding sequence proposed my output is just a white square. I tried several patches such as avoiding the fragment shader mix() function and what I found is that the tutorial-proposed code transferred to the fragment shader only the last loaded texture.

In particular, the proposed code lets you generate a texture and assign attributes without binding the texture, while I found some answers that suggest the binding should happen before assigning any attribute (for example here).

So I tried any combination of GL function calls, and I still don't have a working code, but some insights (my latest code below):

  • if I load a texture by activating the Texture Unit before, it consistently assigns texture regardless the texture object creation order, however only TextureUnit.Texture0 seems to reach the fragment shader and be assigned to any declared sampler2D
  • calling the GL.GetUniformLocation and subsequent GL.Uniform1() to assign the uniform indexes doesn't produce any visible change in the behaviour
  • changing the position where I call GL.UseProgram(shader.Handle); doesn't make any difference
  • as mentioned above, the fragment shader function texture(tex0, texCoord) shows the first texture, and so the texture(tex1, texCoord) or any other declared sampler
  • the mix(texture(tex0, TexCoord), texture(tex1, TexCoord), 0.2); fragment shader function shows always white pixels, even when mixing the same texture calling mix(texture(tex0, TexCoord), texture(tex0, TexCoord), 0.2); (note that such texture show up instead when I call texture(tex0, texCoord))

I run out of ideas and I can't find any resources. Thanks for helping.

Fragment Shader Source:

#version 330 core

in vec2 texCoord;
out vec4 outputColor;

uniform sampler2D tex0;
uniform sampler2D tex1;
uniform sampler2D tex2;

void main() {
    //outputColor = mix(texture(tex0, TexCoord), texture(tex1, TexCoord), 0.2);
    outputColor = texture(tex0, texCoord);
    //outputColor = vec4(1.0, 0.0, 0.0, 1.0);
}

Vertex Shader Source:

#version 330 core

layout(location = 0) in vec3 aPosition;
layout(location = 1) in vec2 aTexCoord;

out vec2 texCoord;

void main(void)
{
    texCoord = aTexCoord;
    gl_Position = vec4(aPosition, 1.0);
}

Texture Class Source:

internal class Texture {

public int Handle { get; private set; }

public Texture(string sourceFile, TextureUnit unit = TextureUnit.Texture0)
{

    Image<Rgba32> image = Image.Load<Rgba32>(sourceFile);
    image.Mutate(x => x.Flip(FlipMode.Vertical));
    var pixels = new byte[4 * image.Width * image.Height];
    image.CopyPixelDataTo(pixels);

    GL.ActiveTexture(unit);

    this.Handle = GL.GenTexture();
    GL.BindTexture(TextureTarget.Texture2D, this.Handle);

    GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
    GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
    GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
    GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);

    GL.TexImage2D(
            TextureTarget.Texture2D,     // Texture type
            0,                           // Level of detail
            PixelInternalFormat.Rgba,    // GPU storing format
            image.Width, image.Height,   // Width, Height
            0,                           // Border of the image, legacy param
            PixelFormat.Rgba,            // Format: ImgSharp use this
            PixelType.UnsignedByte,      // Pixel type
            pixels);                     // Actual pixels
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Use()
{
    GL.BindTexture(TextureTarget.Texture2D, this.Handle);
}

//[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Use(TextureUnit unit)
{
    // GL.ActiveTexture(unit);
    // GL.BindTexture(TextureTarget.Texture2D, this.Handle);
}

} // Texture Class

Shader Class Source (only relevant methods):

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Use()
{
    GL.UseProgram(program);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetInt(string name, int value)
{
    int location = GL.GetUniformLocation(program, name);
    GL.Uniform1(location, value);
}

Loading method of the game class:

    protected override void OnLoad()
    {
        GL.ClearColor(0.2f, 0.2f, 0.2f, 1.0f);

        VertexArrayObject = GL.GenVertexArray();
        VertexBufferObject = GL.GenBuffer();
        ElementBufferObject = GL.GenBuffer();

        // 1. bind Vertex Array Object
        GL.BindVertexArray(VertexArrayObject);

        // 2. copy our vertices array in a buffer for OpenGL to use
        GL.BindBuffer(BufferTarget.ArrayBuffer, VertexBufferObject);
        GL.BufferData(BufferTarget.ArrayBuffer, vertices.Length * sizeof(float), vertices, BufferUsageHint.StaticDraw);

        // 3. then set our vertex attributes pointers
        GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 5 * sizeof(float), 0);
        GL.EnableVertexAttribArray(0);
        GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, 5 * sizeof(float), 3 * sizeof(float));
        GL.EnableVertexAttribArray(1);

        // Indices to ElementBufferObject
        GL.BindBuffer(BufferTarget.ElementArrayBuffer, ElementBufferObject);
        GL.BufferData(BufferTarget.ElementArrayBuffer, indices.Length * sizeof(uint), indices, BufferUsageHint.StaticDraw);

        shader = new Shader("./ShadersSrc/vshader6_textures2.glsl", "./ShadersSrc/fshader6_textures2.glsl");

        texture0 = new Texture("./Textures/src/container.jpg", TextureUnit.Texture0);
        shader.SetInt("tex0", 0);

        texture1 = new Texture("./Textures/src/awesomeface.png", TextureUnit.Texture1);
        shader.SetInt("tex1", 1);
        
        texture2 = new Texture("./Textures/src/wall.jpg", TextureUnit.Texture2);
        shader.SetInt("tex1", 2);

        base.OnLoad();
    }

Frame drawing method of the game class:

    protected override void OnRenderFrame(FrameEventArgs e)
    {
        GL.Clear(ClearBufferMask.ColorBufferBit);

        GL.BindVertexArray(VertexArrayObject);

        shader.Use();
        //texture0.Use(TextureUnit.Texture0);
        //texture1.Use(TextureUnit.Texture1);

        GL.DrawElements(PrimitiveType.Triangles, indices.Length, DrawElementsType.UnsignedInt, 0);

        Context.SwapBuffers();

        base.OnRenderFrame(e);
    }

Solution

  • GL.Uniform1() sets the value of a uniform variable in the standard uniform block of the current program object. Therefore, you must install the program with GL.UseProgram before you can set a uniform:

    shader.Use();
    shader.SetInt("tex0", 0);
    shader.SetInt("tex1", 1);