Feature guide » Builtin shaders

Overview and basic usage of builtin shaders.

Magnum contains a set of general-purpose shaders for easy prototyping, UI rendering and data visualization/debugging in both 2D and 3D scenes. The following shaders are available:

The essential functionality of builtin shaders can be used even on unextended OpenGL 2.1 and OpenGL ES 2.0 / WebGL 1.0, but the code will try to use the most recent technology available to have them as efficient as possible on every configuration. Some functionality, such as uniform buffers, texture arrays or object ID rendering, requires newer versions or extensions, as noted in documentation of a particular feature.

Usage

Shader usage is divided into two parts: describing vertex attributes in the mesh and setting up the shader itself.

Each shader expects some set of vertex attributes, thus when adding a vertex buffer into the mesh, you need to specify which shader attributes are on which position in the buffer. See GL::Mesh::addVertexBuffer() for details and usage examples. Example mesh configuration for the Shaders::PhongGL shader:

struct Vertex {
    Vector3 position;
    Vector3 normal;
    Vector2 textureCoordinates;
};
Vertex data[60]{
    // ...
};

GL::Buffer vertices;
vertices.setData(data, GL::BufferUsage::StaticDraw);

GL::Mesh mesh;
mesh.addVertexBuffer(vertices, 0,
    Shaders::PhongGL::Position{},
    Shaders::PhongGL::Normal{},
    Shaders::PhongGL::TextureCoordinates{})
     //...
     ;

Each shader then has its own set of configuration functions. Some configuration is static, specified commonly as flags in constructor, directly affecting compiled shader code. Other configuration is specified through uniforms and various binding points, commonly exposed through various setters. For uniforms there's two different workflows — a classical one, where uniforms have immediate setters, and a uniform buffer workflow, where the uniform parameters are saved to a structure and then uploaded to a GPU buffer. Let's compare both approaches:

Using classic uniforms

The most straightforward and portable way, working even on old OpenGL ES 2.0 and WebGL 1.0 platforms, is using classic uniform setters. All shader uniforms have a reasonable defaults so you are able to see at least something when using the shader directly without any further configuration, but in most cases you may want to specify at least the transformation/projection matrices. Example configuration and rendering using Shaders::PhongGL — by default it's just colored and uses a single light, and we set a color of both in addition to transformation, projection and normal matrices:

Matrix4 transformationMatrix{}, projectionMatrix{};

Shaders::PhongGL shader;
shader
    .setTransformationMatrix(transformationMatrix)
    .setProjectionMatrix(projectionMatrix)
    .setNormalMatrix(transformationMatrix.normalMatrix())
    .setDiffuseColor(0x2f83cc_rgbf)
    .setLightColors({0xe9ecae_rgbf})
    .draw(mesh);

Using uniform buffers

Uniform buffers require GL 3.1, OpenGL ES 3.0 or WebGL 2.0 and are more verbose to set up, but when used the right way they can result in greatly reduced driver overhead. Uniform buffers get enabled using the Flag::UniformBuffers flag that's implemented for all builtin shaders, and after that you're not supposed to use most of the set*() APIs anymore, instead you have to fill uniform structures, upload them to GL::Buffer instances and then bind those via various bind*Buffer() APIs. To simplify porting, documentation of each classic uniform setter lists the equivalent uniform buffer APIs.

Because some parameters such as projection, material or light setup don't change every draw, they are organized into buffers based on expected frequency of change. This way you can fill the projection and material buffers just once at the start, light setup only when the camera position changes and with much less to upload for every draw. The separation is also done in a way that makes it possible to reuse projection/transformation data among different shaders, e.g. for a depth pre-pass.

In the following example, projection and transformation parameters are supplied via generic shader-independent Shaders::ProjectionUniform3D and Shaders::TransformationUniform3D structures and Phong-specific parameters then via Shaders::PhongDrawUniform, Shaders::PhongMaterialUniform and Shaders::PhongLightUniform structures. While the structures expose the fields directly, the data layout may be non-trivial and it's thus recommended to use the setters unless they prove to be a performance bottleneck:

GL::Buffer projectionUniform, lightUniform, materialUniform,
    transformationUniform, drawUniform;
projectionUniform.setData({
    Shaders::ProjectionUniform3D{}
        .setProjectionMatrix(projectionMatrix)
});
lightUniform.setData({
    Shaders::PhongLightUniform{}
        .setColor(0xe9ecae_rgbf)
});
materialUniform.setData({
    Shaders::PhongMaterialUniform{}
        .setDiffuseColor(0x2f83cc_rgbf)
});
transformationUniform.setData({
    Shaders::TransformationUniform3D{}
        .setTransformationMatrix(transformationMatrix)
});
drawUniform.setData({
    Shaders::PhongDrawUniform{}
        .setNormalMatrix(transformationMatrix.normalMatrix())
});

Shaders::PhongGL shader{Shaders::PhongGL::Configuration{}
    .setFlags(Shaders::PhongGL::Flag::UniformBuffers)};
shader
    .bindProjectionBuffer(projectionUniform)
    .bindLightBuffer(lightUniform)
    .bindMaterialBuffer(materialUniform)
    .bindTransformationBuffer(transformationUniform)
    .bindDrawBuffer(drawUniform)
    .draw(mesh);

Altogether, this results in the same output as in the classic uniform case shown above. Similarly to the classic uniforms, default-constructed structures have reasonable defaults to make the shader render at least something, but note that you have to bind the buffer to get the defaults, without a buffer bound you'll get a fully black mesh at best and nothing rendered at all in the worst cases.

Multidraw and reducing driver overhead

The main advantage of uniform buffers is the ability to specify data for multiple draws together — after all, having to reupload three or four buffers for every draw like shown above wouldn't be really faster or easier than setting the uniforms directly. On the other hand, uploading everything first and binding a different subrange each time would avoid the reupload, but since most drivers have uniform buffer alignment requirement as high as 256 bytes (GL::Buffer::uniformOffsetAlignment()), the per-draw buffers would have to be very sparse.

Instead, it's possible to construct the shaders with a statically defined draw count, fill the buffers with data for that many draws at once and then use setDrawOffset() to pick concrete per-draw parameters. Since material parameters are commonly shared among multiple draws, the desired usage is to upload unique materials and then reference them via a Shaders::*DrawUniform::materialId. The following snippet shows drawing three different meshes, where two of them share the same material definition. The projection and light buffer is the same as above:

GL::Mesh redCone{}, yellowCube{}, redSphere{};
Matrix4 redConeTransformation{},
    yellowCubeTransformation{},
    redSphereTransformation{};

materialUniform.setData({
    Shaders::PhongMaterialUniform{}
        .setDiffuseColor(0xcd3431_rgbf),
    Shaders::PhongMaterialUniform{}
        .setDiffuseColor(0xc7cf2f_rgbf),
});
transformationUniform.setData({
    Shaders::TransformationUniform3D{}
        .setTransformationMatrix(redConeTransformation),
    Shaders::TransformationUniform3D{}
        .setTransformationMatrix(yellowCubeTransformation),
    Shaders::TransformationUniform3D{}
        .setTransformationMatrix(redSphereTransformation),
});
drawUniform.setData({
    Shaders::PhongDrawUniform{}
        .setNormalMatrix(redConeTransformation.normalMatrix())
        .setMaterialId(0),
    Shaders::PhongDrawUniform{}
        .setNormalMatrix(yellowCubeTransformation.normalMatrix())
        .setMaterialId(1),
    Shaders::PhongDrawUniform{}
        .setNormalMatrix(redSphereTransformation.normalMatrix())
        .setMaterialId(0),
});

Shaders::PhongGL shader{Shaders::PhongGL::Configuration{}
    .setFlags(Shaders::PhongGL::Flag::UniformBuffers)
    .setLightCount(1)
    .setMaterialCount(2)
    .setDrawCount(3)};
shader
    .bindProjectionBuffer(projectionUniform)
    .bindTransformationBuffer(transformationUniform)
    .bindDrawBuffer(drawUniform)
    .bindLightBuffer(lightUniform)
    .bindMaterialBuffer(materialUniform)
    .setDrawOffset(0)
    .draw(redCone)
    .setDrawOffset(1)
    .draw(yellowCube)
    .setDrawOffset(2)
    .draw(redSphere);

While this minimizes the state changes to just a single immediate uniform being changed between draws, it's possible to go even further by using GL::MeshView instances onto a single GL::Mesh instead of several different GL::Mesh objects — that way the attribute layout doesn't need to be updated and it's just submitting draws with different offsets and counts.

Finally, with mesh views and on platforms that support ARB_shader_draw_parameters from OpenGL 4.6 or the ANGLE_multi_draw / WEBGL_multi_draw ES and WebGL extension, it's possible to directly submit a multi-draw command. The shader needs to have Flag::MultiDraw enabled, which will make it use the gl_DrawID builtin to pick the per-draw parameters on its own. The above snippet modified for multidraw would then look like this, uniform upload and binding is the same as before:

GL::MeshView redConeView{}, yellowCubeView{}, redSphereView{};


Shaders::PhongGL shader{Shaders::PhongGL::Configuration{}
    .setFlags(Shaders::PhongGL::Flag::MultiDraw)
    .setLightCount(1)
    .setMaterialCount(2)
    .setDrawCount(3)};
shader
    
    .draw({redConeView, yellowCubeView, redSphereView});

Instancing

Shaders::FlatGL and Shaders::PhongGL support instancing, which allows them to render the same mesh several times but with different transformation and material applied. It can be thought of as a more constrained variant of the multidraw mentioned above, but instead of uniform buffers the per-instance parameters are passed through instanced mesh attributes.

No uniform buffer requirement means this feature can be used even on OpenGL ES 2.0 and WebGL 1.0 targets if corresponding instancing extensions are available. Using attributes instead of uniform buffers also means there's no limitation on how many instances can be drawn at once, on the other hand a mesh can have only a certain amount of attribute bindings and thus only the basic properties can be specified per-instance such as the transformation matrix or base color.

The following snippet shows a setup similar to the multidraw above, except that it's just the same sphere drawn three times in different locations and with a different material applied. Note that the per-instance color is achieved by using the usual vertex color attribute, only instanced:

Matrix4 redSphereTransformation{},
    yellowSphereTransformation{},
    greenSphereTransformation{};

struct {
    Matrix4 transformationMatrix;
    Matrix3x3 normalMatrix;
    Color3 color;
} instanceData[]{
    {redSphereTransformation,
     redSphereTransformation.normalMatrix(),
     0xcd3431_rgbf},
    {yellowSphereTransformation,
     yellowSphereTransformation.normalMatrix(),
     0xc7cf2f_rgbf},
    {greenSphereTransformation,
     greenSphereTransformation.normalMatrix(),
     0x3bd267_rgbf},
};

GL::Mesh sphereInstanced{};
sphereInstanced.addVertexBufferInstanced(GL::Buffer{instanceData}, 1, 0,
    Shaders::PhongGL::TransformationMatrix{},
    Shaders::PhongGL::NormalMatrix{},
    Shaders::PhongGL::Color3{});
sphereInstanced.setInstanceCount(3);

Shaders::PhongGL shader{Shaders::PhongGL::Configuration{}
    .setFlags(Shaders::PhongGL::Flag::InstancedTransformation|
              Shaders::PhongGL::Flag::VertexColor)};
shader
    .setProjectionMatrix(projectionMatrix)
    
    .draw(sphereInstanced);

Skinning

Shaders::FlatGL, Shaders::MeshVisualizerGL*D and Shaders::PhongGL are capable of rendering skinned meshes. Such meshes are commonly imported from files such as glTF or FBX together with the skeleton hierarchy and associated animations. The following snippet shows compiling a Trade::MeshData to a GL::Mesh using MeshTools::compile(), using Trade::SkinData and MeshTools::compiledPerVertexJointCount() to set up shader parameters and finally uploading calculated joint matrices to perform the skinning animation:

/* Import and compile the mesh */
Trade::MeshData meshData = ;
GL::Mesh mesh = MeshTools::compile(meshData);
Containers::Pair<UnsignedInt, UnsignedInt> meshPerVertexJointCount =
    MeshTools::compiledPerVertexJointCount(meshData);

/* Import the skin associated with the mesh */
Trade::SkinData3D skin = ;

/* Set up a skinned shader */
Shaders::PhongGL shader{Shaders::PhongGL::Configuration{}
    .setJointCount(skin.joints().size(), meshPerVertexJointCount.first(),
                                         meshPerVertexJointCount.second())};



/* Absolute transformations for all nodes in the scene, possibly animated */
Containers::Array<Matrix4> absoluteTransformations{};


/* Gather joint transformations for this skin, upload and draw */
Containers::Array<Matrix4> jointTransformations{NoInit, skin.joints().size()};
for(std::size_t i = 0; i != jointTransformations.size(); ++i)
    jointTransformations[i] = absoluteTransformations[skin.joints()[i]]*
                              skin.inverseBindMatrices()[i];
shader
    .setJointMatrices(jointTransformations)
    
    .draw(mesh);

The above hardcodes the joint counts in the shader, which makes it the most optimal for rendering given mesh. However, with multiple skinned meshes it'd mean having a dedicated shader instance tailored for each. To avoid that, you can set the joint count and per-vertex joint count to the maximum that the meshes would need, enable Flag::DynamicPerVertexJointCount, for a particular draw upload just a subset of joint matrices the mesh would reference and set the count of actually used per-vertex joints via Shaders::PhongGL::setPerVertexJointCount(). Thus, compared to above:

Shaders::PhongGL shader{Shaders::PhongGL::Configuration{}
    .setFlags(Shaders::PhongGL::Flag::DynamicPerVertexJointCount)
    .setJointCount(maxSkinJointCount, 4, 4)};



shader
    .setJointMatrices(jointTransformations)
    .setPerVertexJointCount(meshPerVertexJointCount.first(),
                            meshPerVertexJointCount.second())
    
    .draw(mesh);

Using textures

Unless the shader requires a texture to work (which is the case of Shaders::VectorGL and Shaders::DistanceFieldVectorGL), by default all shaders are just colored. Enabling a texture is done via a flag (such as Shaders::PhongGL::Flag::DiffuseTexture) and then the texture is bound via an appropriate bind*Texture() call. In most cases the texture value is multiplied with the corresponding color uniform.

GL::Texture2D diffuseTexture;


Shaders::PhongGL shader{Shaders::PhongGL::Configuration{}
    .setFlags(Shaders::PhongGL::Flag::DiffuseTexture)};
shader.bindDiffuseTexture(diffuseTexture)
    
    .draw(mesh);

All shaders that support textures are also able to apply arbitrary transformation to the texture coordinate attribute by enabling Flag::TextureTransformation on a particular shader. Desired transformation is then supplied via setTextureMatrix() (or a Shaders::TextureTransformationUniform in case uniform buffers are used). This can be useful for animations, when you have a larger atlas with switchable texture variations for a single mesh, or when you have texture coordinates quantized in some nontrivial way.

Texture transformation is also useful in the multidraw or instancing scenarios mentioned above, since each draw will most likely require a different texture. There are two options:

While with a GL::Texture2D you may hit texture size limits (not to mention you possible issues with materials that relied on a certain wrapping mode), GL::Texture2DArray is generally able to contain a lot more data, however all slices have to be of the same size. You can also combine the two approaches and pack differently sized textures to slices of a texture array and then set both offset/scale and a layer per-draw.

The following snippet shows a multi-draw setup with a different texture array layer used by each draw. While the projection, transformation, draw material and light buffers are the same as before, there's a new per-draw Shaders::TextureTransformationUniform buffer supplying the layer information:

ImageView2D coneDiffuse{}, cubeDiffuse{}, sphereDiffuse{};

GL::Texture2DArray diffuseTexture;
diffuseTexture
    
    /* Assuming all images have the same format and size */
    .setStorage(1, GL::textureFormat(coneDiffuse.format()),
        {coneDiffuse.size(), 3})
    .setSubImage(0, {}, coneDiffuse)
    .setSubImage(1, {}, cubeDiffuse)
    .setSubImage(2, {}, sphereDiffuse);

GL::Buffer textureTransformationUniform;
textureTransformationUniform.setData({
    Shaders::TextureTransformationUniform{}
        .setLayer(0),
    Shaders::TextureTransformationUniform{}
        .setLayer(1),
    Shaders::TextureTransformationUniform{}
        .setLayer(2),
});

Shaders::PhongGL shader{Shaders::PhongGL::Configuration{}
    .setFlags(Shaders::PhongGL::Flag::MultiDraw|
              Shaders::PhongGL::Flag::DiffuseTexture|
              Shaders::PhongGL::Flag::TextureArrays)
    .setLightCount(1)
    .setMaterialCount(2)
    .setDrawCount(3)};
shader
    
    .bindDiffuseTexture(diffuseTexture)
    .bindTextureTransformationBuffer(textureTransformationUniform)
    .draw({redConeView, yellowCubeView, redSphereView});

While the primary use case of texture arrays is with uniform buffers and multidraw, they work in the classic uniform workflow as well — use setTextureLayer() there instead.

Async shader compilation and linking

By default, shaders are compiled and linked directly in their constructor. While that's convenient and easy to use, applications using heavier shaders, many shader combinations or running on platforms that translate GLSL to other APIs such as HLSL or MSL, may spend a significant portion of their startup time just on shader compilation and linking.

To mitigate this problem, shaders can be compiled in an asynchronous way. Depending on the driver and system, this can mean that for example eight shaders get compiled at the same time in eight parallel threads, instead of sequentially one after another. To achieve such parallelism, the construction needs to be broken into two parts — first submitting compilation of all shaders using Shaders::*GL::compile(), forming temporary Shaders::*GL::CompileState instances, then possibly doing other work until it's completed, and finally constructing final shader instances out of the temporary state:

Shaders::FlatGL3D::CompileState flatState =
    Shaders::FlatGL3D::compile();
Shaders::FlatGL3D::CompileState flatTexturedState =
    Shaders::FlatGL3D::compile(
        Shaders::FlatGL3D::Configuration{}
            .setFlags(Shaders::FlatGL3D::Flag::Textured));
Shaders::MeshVisualizerGL3D::CompileState meshVisualizerState =
    Shaders::MeshVisualizerGL3D::compile();

while(!flatState.isLinkFinished() ||
      !flatTexturedState.isLinkFinished() ||
      !meshVisualizerState.isLinkFinished()) {
    // Do other work ...
}

Shaders::FlatGL3D flat{std::move(flatState)};
Shaders::FlatGL3D flatTextured{std::move(flatTexturedState)};
Shaders::MeshVisualizerGL3D meshVisualizer{std::move(meshVisualizerState)};

The above code will work correctly also on drivers that implement async compilation partially or not at all — there GL::AbstractShaderProgram::isLinkFinished() will implicitly return true, and the final construction will stall if it happens before a (potentially async) compilation is finished. See also the GL::AbstractShaderProgram documentation for more information.

Generic vertex attributes and framebuffer attachments

Many shaders share the same vertex attribute definitions, such as positions, normals, texture coordinates etc. It's thus possible to configure the mesh for a generic shader and then render it with any compatible shader. Definition of all generic attributes is available in the Shaders::GenericGL class. Setup of the mesh shown above using generic attributes could then look like this:

mesh.addVertexBuffer(vertices, 0,
    Shaders::GenericGL3D::Position{},
    Shaders::GenericGL3D::Normal{},
    Shaders::GenericGL3D::TextureCoordinates{});

Note that in this particular case both setups are equivalent, because Shaders::PhongGL attribute definitions are just aliases to the generic ones. Then you can render the mesh using the Shaders::PhongGL shader like above, or use for example Shaders::FlatGL3D or even Shaders::MeshVisualizerGL3D with the same mesh reconfiguration. The unused attributes will be simply ignored.

Shaders::MeshVisualizerGL3D shader{Shaders::MeshVisualizerGL3D::Configuration{}
    .setFlags(Shaders::MeshVisualizerGL3D::Flag::Wireframe)};
shader
    .setColor(0x2f83cc_rgbf)
    .setWireframeColor(0xdcdcdc_rgbf)
    .setViewportSize(Vector2{GL::defaultFramebuffer.viewport().size()})
    .setTransformationMatrix(transformationMatrix)
    .setProjectionMatrix(projectionMatrix)
    .draw(mesh);

The MeshTools::compile() utility configures meshes using generic vertex attribute definitions to make them usable with any builtin shader.

Besides vertex attributes, Shaders::GenericGL contains generic definitions for framebuffer outputs as well — in many cases a shader has just one (color) output, but some shaders such as Shaders::FlatGL or Shaders::PhongGL offer an object ID output as well. A setup equivalent to what's done in Flat shader's Object ID output but using the generic definitions would look like this:

framebuffer.mapForDraw({
    {Shaders::GenericGL3D::ColorOutput, GL::Framebuffer::ColorAttachment{0}},
    {Shaders::GenericGL3D::ObjectIdOutput, GL::Framebuffer::ColorAttachment{1}}});