2
\$\begingroup\$

I've already asked similar but a bit unclear question here but this time I will be very specific and to the point.

Suppose I have an actor which grabs a power up. He starts to glow using bloom shader and after 10 seconds back to normal attaching the default shader again. The question basically boils down to:

How to use different shaders on the same model at runtime?

Consider following very simple example: Default shader:

attribute vec4 Position;
uniform mat4 ModelViewProjMatrix;

void main(void)
{
    gl_Position = ModelViewProjMatrix * Position;
}

Render code inside RendererGLES20 will be:

void RendererGLES20::render(Model * model)
{
    glUniformMatrix4fv(mvpUniform, 1, 0, &mvpMatrix);
    GLuint positionSlot = glGetAttribLocation(_program, "Position");
    glEnableVertexAttribArray(positionSlot);

    // interleaved data, But for now we are ONLY using the positions, ignoring texture, normals and colours.
    const GLvoid* pCoords = &(model->vertexArray[0].Position[0]);
    glVertexAttribPointer(positionSlot, 2, GL_FLOAT, GL_FALSE, stride, pCoords);

    glDrawArrays(GL_TRIANGLES, 0, model->vertexCount);

    glDisableVertexAttribArray(positionSlot);
}

Simple enough! Now imagine that the actor got some power up and following crazy shader is applied:

Crazy Shader:

attribute vec4 Position;
attribute vec4 SourceColor;
attribute vec2 Texture;
attribute vec4 Normal;
attribute vec2 tempAttrib0;
attribute vec2 tempAttrib1;

// A bunch of varying but we don't need to worry about these for now                                           
varying vec4 .........;
varying .........;

uniform mat4 MVPMatrix;
uniform vec2 BloomAmount;
uniform vec2 BloomQuality;
uniform vec2 BloomSize;
uniform vec2 RippleSize;
uniform vec2 RippleAmmount;
uniform vec2 RippleLocation;
uniform vec2 deltaTime;
uniform vec2 RippleMaxIterations;

void main(void)
{
    // Some crazy voodoo source code here...
    // .........
    gl_Position = ..............;
}

As you can clearly see, in order to attach this shader to the model I would need to modify the actual renderer source code to following:

void RendererGLES20::render(Model * model)
{
    glUniformMatrix4fv(mvpUniform, 1, 0, ....);
    glUniformMatrix4fv(bloomAmountUniform, 1, 0, ....);
    glUniformMatrix4fv(bloomQualityUniform, 1, 0, ....);
    glUniformMatrix4fv(bloomSizeUniform, 1, 0, ....);
    glUniformMatrix4fv(rippleSizeUniform, 1, 0, ....);
    glUniformMatrix4fv(rippleAmountUniform, 1, 0, ....);
    glUniformMatrix4fv(rippleLocationUniform, 1, 0, ....);
    glUniformMatrix4fv(rippleMaxIterationsUniform, 1, 0, ....);
    glUniformMatrix4fv(deltaTimeUniform, 1, 0, ....);

    GLuint positionSlot = glGetAttribLocation(_program, "Position");
    GLuint sourceColorSlot = glGetAttribLocation(_program, "SourceColor");
    GLuint textureSlot = glGetAttribLocation(_program, "Texture");
    GLuint normalSlot = glGetAttribLocation(_program, "Normal");
    GLuint tempAttrib0Slot = glGetAttribLocation(_program, "TempAttrib0");
    GLuint tempAttrib1Slot = glGetAttribLocation(_program, "TempAttrib1");

    glEnableVertexAttribArray(positionSlot);
    glEnableVertexAttribArray(sourceColorSlot);
    glEnableVertexAttribArray(textureSlot);
    glEnableVertexAttribArray(normalSlot);
    glEnableVertexAttribArray(tempAttrib0Slot);
    glEnableVertexAttribArray(tempAttrib1Slot);

    // interleaved data
    const GLvoid* pCoords = &(model->vertexArray[0].Position[0]);
    const GLvoid* sCoords = &(model->vertexArray[0].SourceColor[0]);
    const GLvoid* tCoords = &(model->vertexArray[0].Texture[0]);
    const GLvoid* nCoords = &(model->vertexArray[0].Normal[0]);
    const GLvoid* t0Coords = &(model->vertexArray[0].TempAttrib0[0]);
    const GLvoid* t1Coords = &(model->vertexArray[0].TempAttrib1[0]);

    glVertexAttribPointer(positionSlot, 3, GL_FLOAT, GL_FALSE, stride, pCoords);
    glVertexAttribPointer(sourceColorSlot, 4, GL_FLOAT, GL_FALSE, stride, sCoords);
    glVertexAttribPointer(textureSlot, 2, GL_FLOAT, GL_FALSE, stride, tCoords);
    glVertexAttribPointer(normalSlot, 4, GL_FLOAT, GL_FALSE, stride, nCoords);
    glVertexAttribPointer(tempAttrib0Slot, 3, GL_FLOAT, GL_FALSE, stride, t0Coords);
    glVertexAttribPointer(tempAttrib1Slot, 2, GL_FLOAT, GL_FALSE, stride, t1Coords);

    glDrawArrays(GL_TRIANGLES, 0, model->vertexCount);

    glDisableVertexAttribArray(positionSlot);
    glDisableVertexAttribArray(sourceColorSlot);
    glDisableVertexAttribArray(textureSlot);
    glDisableVertexAttribArray(normalSlot);
    glDisableVertexAttribArray(tempAttrib0Slot);
    glDisableVertexAttribArray(tempAttrib1Slot);

}

You see how vastly different code you need to write in order to attach a different shader. Now what if I want to re-attach the default shader back? (this is attaching and detaching of shaders has to happen at run-time, e.g.: actor collected power up).

Any ideas how can I efficiently and easily implement this to allow a model to change shaders at run-time? I am just looking forward to a nice implementation/idea. How would you guys handle the above problem?

\$\endgroup\$

3 Answers 3

5
\$\begingroup\$

You compile your shaders and bind them into programs. You can use the same VBOs again and again but with different uniforms or even programs, in the same frame, without problems.

For each program, your calls to glGetAttribLocation will return -1 if that attribute is not in the program, or has been optimised-out (e.g. you declare it but don't use it). glGetUniformLocation likewise.

My draw_mesh code usually checks for all the attribute and uniform names I've standardised on, and sets the field if its in the program.

To take a real-world example from my code, I often use a model format called Glest G3D which is a keyframe-based animation format. I have two shaders; one will draw a single frame, the other will interpolate two frames. In my G3D.mesh.draw function I see if the program has a tween uniform; if so, I set vertex1 and normal1 attributes and the tween uniform. I always set vertex0 and normal1 attributes. So one shader gets two frames to interpolate between, and the other just gets one frame, and the standardisation of uniform and attribute names is the contract between my custom shaders and my utility mesh drawing code.

\$\endgroup\$
4
  • \$\begingroup\$ Can you explain how? Every shader has possibly different uniforms/attribs. So different code has to run for different shaders (as shown above). Shaders are not like attaching different textures to the same model! \$\endgroup\$
    – fakhir
    Commented Jul 4, 2013 at 6:17
  • 1
    \$\begingroup\$ The cost of doing business. Either you write your shaders to use a common set of parameters (which is how ubershaders became popular) or you set more parameters when you change. The speed difference is not huge unless you change a LOT, in which case you need to optimize why you are changing a lot and not the parameters thing =) \$\endgroup\$ Commented Jul 4, 2013 at 16:58
  • \$\begingroup\$ Just a side thing I've been curious about for a while. Is there any overhead to glGetAttribLocation? If there was I guess you could always cache the locations CPU-side, but just wondering. \$\endgroup\$ Commented Jul 4, 2013 at 22:43
  • \$\begingroup\$ @LewisWakeford I do keep a cache. I have not profiled if its necessary. I am in a webGL world, and the cost in openGL is most likely much cheaper anyway. \$\endgroup\$
    – Will
    Commented Jul 4, 2013 at 23:03
0
\$\begingroup\$

Are you worried about runtime overhead of switching shaders or are you worried about redundant code? Don't worry about the first one until you need to. Avoid premature optimization.

If you are worried about the second, put the code for each shader program attachment in a separate function so you can choose which to call at runtime. If there are many overlapping uniforms, group the common set uniform calls into one function.

You might also find this page helpful: https://stackoverflow.com/questions/7235929/glsl-problem-multiple-shaders-in-one-program

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Too often, this premature optimization is not bad, but a sign of: "can I do things better, design my architecture to be more flexible and robust straight from the start so that I don't return and refactor a truckload of logic".. let the people worry so that they're aware at least - switching shaders is a problem if considered within a big, serious engine.. \$\endgroup\$
    – teodron
    Commented Jul 4, 2013 at 15:48
0
\$\begingroup\$

A bit late, but if I understood correctly, you're worried about making your render code general without having to worry about which shader the object being draw has currently, right?

I think you could implement the idea of materials and shaders in your renderable objects, and make it related to the shader somehow. Then it's a matter of enforcing that such variables EXIST for that given material-shader combination.

The code would be something like this:

void RendererGLES20::render(Model * model)
{
    model->Material.ActivateMaterial(model->shader);
    //This model HAS a specific shader, or if you're playing with effects
    //then each effect also has a specific shader

    for(unsigned int i = 0; i < model.Effects.count; i++)
    {
        //Activate it as well            
    }

    //generic renderable code for all models
    //here you'll bind your vertexes, normals, uvs, everything else all models share        
}

Note: This is NOT OPTIMAL! Just get the idea.

And in Material.ActivateMaterial(Shader* shader), something like:

void Material::ActivateMaterial(Shader* shader)
{
    //Here you know the shader, know the material, and can get your variables correctly
    glUniformMatrix4fv(paramLocation, 1, GL_TRUE, &paramValue[0][0]);
    //Other variables
}

For effects:

void Effect::ActivateEffect(Shader* shader)
{        
    glUniform3fv(0, 1, &thisEffectAlphaValue);
    //etc

    //for better control, you could
    glUniform3fv(shader->AlphaLocation, 1, &alphaValue);

    //and for even better
    shader->SendParameter("locationName", &alphaValue); //alphaValue being a vec3 and this being a overloaded SendParameter for various types

    //With this last one you could also keep track of which shader failed to get the location of a variable and save the error somewhere with your Shader class
}

So by the time you call glDrawElements(), all your variables were sent accordingly.

In my case, my effects have a specific shader each, and I just attach them to objects. Objects will keep rendering themselves since they all do the same thing: receive a MVP matrix, vec3 position, normal, etc, and output a vec4 color in the fragment shader.

For the optimal part, the best way would be to sort objects by shader before rendering. Switching shaders is a big performance issue, so if you can, you should sort objects by shader, then it's effects, and then draw.

\$\endgroup\$

You must log in to answer this question.

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