How to design a simple GLSL wrapper for shader use
Asked Answered
D

2

9

UPDATE: Because I needed something right away, I've created a simple shader wrapper that does the sort of thing I need. You can find it here: ShaderManager on GitHub. Note that it's designed for Objective-C / iOS, so may not be useful to everyone. If you have any suggestions for design improvements, please let me know!

Original Problem:

I'm new to using GLSL shaders. I'm familiar enough with the GLSL language and the OpenGL interface, but I'm having trouble designing a simple API through which to use shaders.

OpenGL's C interface to interact with shaders seems cumbersome. I can't seem to find any tutorials on the net that cover the API design of such things.

My question is this: does any one have a good, simple, API design or pattern to wrap the OpenGL shader program API?

Take the following simple example. Say I have one vertex shader that just emulates fixed functionality, and two fragment shaders - one for drawing smooth rectangles and one for drawing smooth circles. I have the following files:

Shader.vsh : Simple vertex shader, with the following inputs/outputs:
    -- Uniforms: mat4 Model, mat4 View, mat4 Projection
    -- Attributes: vec4 Vertex, vec2 TexCoord, vec4 Color
    -- Varying: vec4 vColor, vec2 vTexCoord

Square.fsh : Fragment shader for drawing squares based on tex coord / color
Circle.fsh : Fragment shader for drawing circles based on tex coord / color

Basic Linking

Now what is the standard way to use these? Do I link the above shaders into two OpenGL shader programs? That is:

Shader.vsh + Square.fsh = SquareProgram
Shader.vsh + Circle.fsh = CircleProgram

Or do I instead create one big program where the fragment shaders check some conditional uniform variables and call out to a shader function to generate their result. E.g:

Shader.vsh + Square.fsh + Circle.fsh + Main.fsh = ShaderProgram
//Main.fsh here would simply check whether to call out to square or circle

With two individual programs I would presumably need to call

glUseProgram(CircleProgram); or glUseProgram(SquareProgram);

Before each type of element I want to draw. I would then need to set the uniforms (Model / View / Projection) and attributes of each program before I use it. This seems so unwieldy.

With the single ShaderProgram option I would still need to set some sort of boolean switch (circle or square) in the fragment shader that would be checked before drawing each pixel. This also seems complicated.

As a side note, am I allowed to link two fragment shaders, each with a main() function, into one shader program? How would OpenGL know which one to call?

Setting Variables

The calls:

glUniform*
glVertexAttribPointer

Are used to set uniforms and attribute pointer locations on the current program.

Different classes and structures may need to access and set variables on the current shader (or change the current shader) from different places in the code. I can't think of a nice way to do this that decouples the shader code from the code that wants to use it.

That is, each shape I want to draw will need to set vertex and texture coordinate attributes - requiring the handles to those attributes generated by OpenGL.

The camera will need to set its projection matrix as a uniform in the vertex shader, while the class managing the model matrix stack will need to set its own uniform in the vertex shader.

Changing shaders part-way through drawing a scene would mean that all these classes will need to set their uniforms and attributes again.

How do most people design around this?

A global dictionary of shaders accessed by handle or name, with getters and setters for their parameters?

An OO design with shader objects that each have parameters?

I've looked at the following wrappers:

Jon's Teapot: GLSL Shader Manager - This wraps shaders in C++ classes. It seems like little more than a wrapper that enforces OO principles on a C API, resulting in a C++ API that is much the same.

I am after any sort of design that simplifies the use of Shader programs, and am not concerned about the particular paradigm used (OO, procedural, and so on)

Densitometer answered 29/5, 2011 at 5:58 Comment(2)
One possible starting point: #2795544. It does not attempt to deal with the uniforms, attributes, etc., but at least simplifies compiling and linking.Biting
Thanks Jerry, that's a good starting point and answers my question about using a single shader program over multiple shader programs.Densitometer
F
5

Basic Linking:

There is no standard way here. There are at least 2 general approaches:

  1. Monolithic - one shader covers many cases, using uniform boolean switches. These branches don't hurt performance because the condition result is constant for any fragment group (actually, for all of the fragments).

  2. Multi-object program compositing - main shader declares a set of entry points (like 'get_diffuse', 'get_specular', etc), which are implemented in separate shader objects attached. This implies individual shader for each object, but any kind of caching helps.

Setting Variables: Uniforms

I will just describe the approach I developed.

Each shader program has a list of uniform dictionaries. It's used to fill the uniform source list upon program (re-)linking. When the program is activated, it goes through the uniform list, fetches values from their sources and uploads them to GL. In the result, data is not directly connected with the user shader program, and whatever manages it does not care about the program using it.

One of these dictionaries can be, for example, a core one, containing model,view transformations, camera projection and maybe something else.

Setting Variables: Attributes

First of all, shader program is an attribute consumer, so it is what has to extract these attributes from a mesh (or any other data storage) and upload them to GL in a way it needs. It should also make sure that types of provided attributes match the requested types.

When using with monolithic shader approach, there is a possible unpleasant situation when one the disabled branch ways requires a vertex attribute that is not provided. I would advice using another attribute's data to supply the missing one, because we don't care about the actual values in this case.

P.S. You can find an actual implementation of these ideas here: http://code.google.com/p/kri/

Federalize answered 29/5, 2011 at 16:50 Comment(2)
Thank you, that's incredibly helpful. I didn't realize constant branches in monolithic shaders would be optimized away - it makes sense when you explain it. Does the glUniform* family of functions have much performance overhead?Densitometer
@simeon. No, in my experiments calling glUniform for each variable for each shader activation costs nothing compared to a cached variant.Federalize
P
8

I see this is tagged with iOS, so if you're partial to Objective-C, I'd take a good look at Jeff LaMarche's GLProgram wrapper class, which he describes here and has source available here. I've used it within my own applications to simplify some of the shader program setup, and to make the code a little cleaner.

For example, you can set up a shader and its attributes and uniforms using code like the following:

sphereDepthProgram = [[GLProgram alloc] initWithVertexShaderFilename:@"SphereDepth" fragmentShaderFilename:@"SphereDepth"];
[sphereDepthProgram addAttribute:@"position"];
[sphereDepthProgram addAttribute:@"inputImpostorSpaceCoordinate"];
if (![sphereDepthProgram link])
{
    NSLog(@"Depth shader link failed");
    NSString *progLog = [sphereDepthProgram programLog];
    NSLog(@"Program Log: %@", progLog); 
    NSString *fragLog = [sphereDepthProgram fragmentShaderLog];
    NSLog(@"Frag Log: %@", fragLog);
    NSString *vertLog = [sphereDepthProgram vertexShaderLog];
    NSLog(@"Vert Log: %@", vertLog);
    [sphereDepthProgram release];
    sphereDepthProgram = nil;
}

sphereDepthPositionAttribute = [sphereDepthProgram attributeIndex:@"position"];
sphereDepthImpostorSpaceAttribute = [sphereDepthProgram attributeIndex:@"inputImpostorSpaceCoordinate"];
sphereDepthModelViewMatrix = [sphereDepthProgram uniformIndex:@"modelViewProjMatrix"];
sphereDepthRadius = [sphereDepthProgram uniformIndex:@"sphereRadius"];

When you need to use the shader program, you then do something like the following:

[sphereDepthProgram use];

This doesn't address the issues of branching vs. individual shaders that you bring up above, but Jeff's implementation does provide a nice encapsulation of some of the OpenGL ES boilerplate shader setup code.

Prostomium answered 29/5, 2011 at 17:5 Comment(1)
Thank you for pointing me to this - it seems I have reinvented the wheel with my class above. If I had only found this one a few hours earlier!Densitometer
F
5

Basic Linking:

There is no standard way here. There are at least 2 general approaches:

  1. Monolithic - one shader covers many cases, using uniform boolean switches. These branches don't hurt performance because the condition result is constant for any fragment group (actually, for all of the fragments).

  2. Multi-object program compositing - main shader declares a set of entry points (like 'get_diffuse', 'get_specular', etc), which are implemented in separate shader objects attached. This implies individual shader for each object, but any kind of caching helps.

Setting Variables: Uniforms

I will just describe the approach I developed.

Each shader program has a list of uniform dictionaries. It's used to fill the uniform source list upon program (re-)linking. When the program is activated, it goes through the uniform list, fetches values from their sources and uploads them to GL. In the result, data is not directly connected with the user shader program, and whatever manages it does not care about the program using it.

One of these dictionaries can be, for example, a core one, containing model,view transformations, camera projection and maybe something else.

Setting Variables: Attributes

First of all, shader program is an attribute consumer, so it is what has to extract these attributes from a mesh (or any other data storage) and upload them to GL in a way it needs. It should also make sure that types of provided attributes match the requested types.

When using with monolithic shader approach, there is a possible unpleasant situation when one the disabled branch ways requires a vertex attribute that is not provided. I would advice using another attribute's data to supply the missing one, because we don't care about the actual values in this case.

P.S. You can find an actual implementation of these ideas here: http://code.google.com/p/kri/

Federalize answered 29/5, 2011 at 16:50 Comment(2)
Thank you, that's incredibly helpful. I didn't realize constant branches in monolithic shaders would be optimized away - it makes sense when you explain it. Does the glUniform* family of functions have much performance overhead?Densitometer
@simeon. No, in my experiments calling glUniform for each variable for each shader activation costs nothing compared to a cached variant.Federalize

© 2022 - 2024 — McMap. All rights reserved.