raylib icon indicating copy to clipboard operation
raylib copied to clipboard

[rlgl] Custom shaders called with `rlLoadDrawQuad()` cannot fetch data from texture uniforms.

Open mikeemm opened this issue 1 year ago • 4 comments

Issue description

TL;DR: Shaders made with rlLoadDrawQuad() can't seem to reliably access sampler2D uniforms via texture or texelFetch.

In my program I render a 3D scene into a RenderTexture. I then call rlLoadDrawQuad with a custom shader to load the previous texture, filter it and render to another RenderTexture, which I then output to the screen. However, it seems rlLoadDrawQuad cannot access uniform texture data when it is being called after BeginTextureMode(); removing that and outputting directly into the screen fixes this, but then I can't do further operations with that RenderTexture.

However, when attempting to create a small code sample to share here, I couldn't get rlLoadDrawQuad() to load a texture at all, even when outputting directly to the screen. Everything else in the shaders work correctly, the uniform location is found and the texture coordinates display fine onscreen, but all shader data is black no matter what texture it tries to access despite the image being loaded. I'm not sure where the problem is at this point to be honest.

Environment

Desktop (GLFW) Windows 10 64-bit GPU NVIDIA GeForce RTX 3050 Laptop GPU/PCIe/SSE2 OpenGL: 3.3.0 NVIDIA 537.13 GLSL: 3.30 NVIDIA via Cg compiler

Code Example

The code below should output whatever image you load up as example.png, but instead draws blank.

main.c:

#include "raylib.h"
#include "rlgl.h"

int main()
{
    InitWindow(1600, 900, "Test");

    Texture testTexture = LoadTexture("example.png");
    Shader testShader = LoadShader("rendertexture_VP.glsl", "rendertexture_FP.glsl");
    
    SetTargetFPS(60);
    while (!WindowShouldClose())
    {
        BeginDrawing();
            ClearBackground(BLACK);
            int loc = GetShaderLocation(testShader, "source");
            SetShaderValueTexture(testShader, loc, testTexture);
            rlEnableShader(testShader.id);
            rlLoadDrawQuad();
            rlDisableShader();
        EndDrawing();
    }

    CloseWindow();
}

rendertexture_VP.glsl:

#version 330

layout (location = 0) in vec3 vertexPosition;
layout (location = 1) in vec2 vertexTexCoord;

out vec2 fragTexCoord;

void main()
{
    fragTexCoord = vertexTexCoord;
    gl_Position = vec4(vertexPosition, 1.0f);
}

rendertexture_FP.glsl:

#version 330

in vec2 fragTexCoord;

uniform sampler2D source;

out vec4 finalColor;

void main()
{
    vec3 texColor = texture(source, fragTexCoord).xyz;
    finalColor = vec4(texColor, 1.0f);
}

mikeemm avatar Dec 02 '24 21:12 mikeemm

@mikeemm Considering you are using rlgl, you should use rlSetShader(unsigned int id, int *locs) to enable shader locations, or do the process manually.

Note that rlgl is a lower level API, just a thin layer over OpenGL, but still has some custom functionality.

raysan5 avatar Dec 02 '24 21:12 raysan5

@raysan5 I tried that before, and neither rlSetShader() nor BeginShaderMode() (which just calls rlSetShader() internally) worked. Adding either of these in the example I provided does not fix the issue for me, as the shader still refuses to access the texture. Please don't close this issue quite yet.

mikeemm avatar Dec 03 '24 08:12 mikeemm

@mikeemm Ok, reopening it.

raysan5 avatar Dec 03 '24 11:12 raysan5

Turns out before you set the sampler uniform you have to bind the texture using rlEnableTexture(); I assume this worked at first in my personal project because at some point during the rendering phase something in my code already bound the texture and left it bound.

However, the other reason this doesn't work is because SetShaderValueTexture() calls rlSetUniformSampler(), which calls glUniform1i(locIndex, 1 + i). This assumes that there was already one texture bound at texture unit 0 before binding the one you'd actually want to use; again, I assume this worked for me at first because a previous operation left a texture bound on unit 0 without adding its id to the RLGL state. Using SetShaderValue() instead with an explicit texture unit solves this.

So this doesn't work:

BeginDrawing();
    rlEnableTexture(testTexture.id);
    int texLoc = GetShaderLocation(testShader, "source");
    SetShaderValueTexture(testShader, texLoc, testTexture);
    rlLoadDrawQuad();
    rlDisableShader();
EndDrawing();

But this does:

BeginDrawing();
    rlEnableTexture(testTexture.id);
    int texUnit = 0;
    int texLoc = GetShaderLocation(testShader, "source");
    SetShaderValue(testShader, texLoc, &texUnit, SHADER_UNIFORM_SAMPLER2D);
    rlLoadDrawQuad();
    rlDisableShader();
EndDrawing();

This behavior must be intentional so that it works together with other drawing functions, which seem to set a default texture to texture unit 0 - my question is, where and why is this texture added? It seems incoherent that SetShaderValueTexture() would work or not depending on where in the program it was being used, with no way to know whether it would work until runtime. Is the reasoning for this behavior already documented somewhere perhaps?

mikeemm avatar Dec 04 '24 10:12 mikeemm

any news on this?

CodingMadness avatar Jan 05 '25 04:01 CodingMadness

Hi, I'm having issues due to what @mikeemm is describing; since updating to raylib 5.5, my project (which uses multiple uniform samplers in post processing) no longer renders correctly. If I revert this change, though, things work fine again.

I'm not sure if this change introduced a bug, or if I just don't understand things correctly and the way I was doing things was actually incorrect.

Worth noting that the deferred render example is also broken by this change.

TadCordle avatar Jan 08 '25 03:01 TadCordle

SetShaderValueTexture() sets a uniform to point to a texture slot (whichever it decides) and remembers what texture it should bind to that slot. The actual texture binding happens when a batch is being drawn. rlLoadDrawQuad() doesn't use batch system so the textures are never bound to the slots. (It can work accidentally because textures will be left bound from previous draw calls). Here's how to do it manually:

BeginDrawing();
int textureSlot = 7;              //Choose whatever slot you want (16 texture units are guaranteed on OpenGL3)
rlActiveTextureSlot(textureSlot); //Select this slot
rlEnableTexture(testTexture.id);  //Bind texture to currently selected slot

rlEnableShader(testShader.id);    //This is not strictly needed because SetShaderValue() enables shader for us
int texLoc = GetShaderLocation(testShader, "source");
SetShaderValue(testShader, texLoc, &textureSlot, SHADER_UNIFORM_SAMPLER2D); // Assign sampler to use your slot
rlLoadDrawQuad();
EndDrawing();

veins1 avatar Jan 08 '25 13:01 veins1

Adding SetShaderValue calls seems to fix the deferred render example, at least. My project still seems to have weird issues, but I'll assume user error for now.

TadCordle avatar Jan 08 '25 19:01 TadCordle

Thank you @veins1 for the explanation. Ideally I think two functions named so similarly (SetShaderValue() and SetShaderValueTexture()) should be able to be used interchangeably under the same conditions; however changing how they work now would surely break too much to be worth it. I'll be closing this issue, and hopefully it will remain a useful reference to anybody who happens to find themselves in a similar situation.

mikeemm avatar Feb 18 '25 13:02 mikeemm