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:
- Shaders::
FlatGL*D — flat shading using single color or texture - Shaders::
VectorGL*D — colored vector graphics - Shaders::
DistanceFieldVectorGL*D – colored and outlined vector graphics - Shaders::
VertexColorGL*D — vertex-colored meshes - Shaders::
PhongGL — Phong shading using colors or textures, 3D only - Shaders::
MeshVisualizerGL2D / Shaders:: MeshVisualizerGL3D — wireframe visualization
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::
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::
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::set*()
APIs anymore, instead you have to fill uniform structures, upload them to GL::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::
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::
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::
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::
Finally, with mesh views and on platforms that support ARB_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::
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::
/* 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::
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::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::
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:
- Upload the textures to subrectangles of a larger GL::
Texture2D and then specify Shaders:: TextureTransformationUniform:: offset and rotationScaling for each draw, or in case of an instanced draw supply an instanced TextureOffset attribute and have a global scale set for all instanced via setTextureMatrix(). - Enable Flag::
TextureArrays in the shader (not available on OpenGL ES 2.0 or WebGL 1.0), upload the textures to slices of a GL:: Texture2DArray and specify Shaders:: TextureTransformationUniform:: layer for each draw, or in case of an instanced draw supply a layer in an instanced TextureOffsetLayer attribute.
While with a GL::
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::
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::
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::true
, and the final construction will stall if it happens before a (potentially async) compilation is finished. See also the GL::
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::
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::
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::
Besides vertex attributes, Shaders::
framebuffer.mapForDraw({ {Shaders::GenericGL3D::ColorOutput, GL::Framebuffer::ColorAttachment{0}}, {Shaders::GenericGL3D::ObjectIdOutput, GL::Framebuffer::ColorAttachment{1}}});