1
votes

TL/DR: How do you specify a (default) texture unit for unused samplers in a sampler array?

I have the following scene in my OpenGL 4.1 application:

scene

As you can see, there are 2 lights. Each light has its own shadow map. So my fragment shader looks like this:

#define MAX_LIGHTS 16

uniform samplerCube uShadowMaps[MAX_LIGHTS];

// sample the shadow map of the light
float Shadow(int light) {
    vec3 direction = Position - uLightPositions[light];
    float sampledDepth = 1.0;

    switch (light) {
        case 0: sampledDepth = texture(uShadowMaps[0], direction).r; break;
        case 1: sampledDepth = texture(uShadowMaps[1], direction).r; break;
    }

    // ... the rest of the calculations ...
}

This code works because there are exactly 2 lights and each binds it's own shadow map. The code every light runs looks somewhat like this:

// make the name of the uniform
std::stringstream shadowMapName;
shadowMapName << "uShadowMaps[" << index << "]";

// bind the shadow map texture and bind the uniform
glActiveTexture(GL_TEXTURE3 + index);  // index is the light number, 3 because there are 3 other textures already bound
glBindTexture(GL_TEXTURE_CUBE_MAP, shadowMapTexture);
auto position = glGetUniformLocation(shaderProgram, shadowMapName.str().c_str());
glUniform1i(position, index + 3);

However, I want to have multiple lights in the scene eventually, so I want to write my shaders so they work for more lights. I added the next switch case in the shader like this:

// sample the shadow map for the light
float Shadow(int light) {
    // ...

    switch (light) {
        case 0: sampledDepth = texture(uShadowMaps[0], direction).r; break;
        case 1: sampledDepth = texture(uShadowMaps[1], direction).r; break;
        case 2: sampledDepth = texture(uShadowMaps[2], direction).r; break;
    }

    // ...
}

This, unfortunately, results in a black window.

black window

I assume this is because there's nothing bound to mShaderMaps[2]. And sure enough, if I add another light to the scene, it renders again.

So my question is: How do you specify a (default) texture unit for unused samplers?


Note: why switch? This does not work:

// sample the shadow map for the light
float Shadow(int light) {
    // ...

    sampledDepth = texture(uShadowMaps[light], direction).r;

    // ...
}

Because light is not a dynamically uniform expression. See GLSL, Array of textures of differing size.


EDIT

The Shadow function is called like this:

// calculate diffuse color for the fragment
for (auto i = 0; i < min(uLightCount, MAX_LIGHTS); ++i) {
    // ... calculate diffuse for the light

    diffuseColor += diffuse * Shadow(i);

}

// ... some more stuff ...

// calculate specular color for the fragment
for (auto i = 0; i < min(uLightCount, MAX_LIGHTS); ++i) {
    // ... calculate specular for the light

    specularColor += specular * Shadow(i);

}
1
Couldn't you just pass a uniform telling how many lights there are and skip not-existing ones? - BDL
I do that. The Shadow function is called in a for loop for every light in the scene like this: for (int i = 0; i < min(uLightCount, MAX_LIGHTS); ++i) { // ... calculate color and call the Shadow } where uLightCount is a uniform equal to the number of lights. - martindzejky
For some reason, it doesn't matter that the switch case 2: is never called. As soon as it's in the code, I get black window. - martindzejky
@chuckeles: "Because light is not a dynamically uniform expression." Is that really true? Is the value you're passing selected based on a shader stage input or a texture access or something? Just because the Shadow function doesn't declare it to be const doesn't mean it isn't dynamically uniform. Dynamically uniform is about the value the expression evaluates to. - Nicol Bolas
See the updated question. I think it should be dynamically uniform expression because it's in the for loop, which uses dynamically uniform variables (upper limit and the offset). But this is the only way I got it working. - martindzejky

1 Answers

0
votes

Well, it turns out it is really easy. I just create an empty shadow map and bind it to every shadow map in the array, each to its own texture unit. The code looks like this:

auto empty = ShadowMap::CreateEmpty(); // make an empty shadow map

for (auto i = 0; i < 12; ++i) { // 12 is the shadow map array size
    std::stringstream shadowMapName;
    shadowMapName << "uShadowMaps[" << i << "]";

    glActiveTexture(GL_TEXTURE3 + i);  // bind to a separate texture unit
    glBindTexture(GL_TEXTURE_CUBE_MAP, empty->GetId());
    auto position = glGetUniformLocation(shaderProgram, shadowMapName.str().c_str());
    glUniform1i(position, i + 3);
}

The reason I couldn't get it working before was that I was binding a simple texture to unused shadow maps. But the bound shadow maps use cube maps, so I was binding a different texture types to the array, which does not work.