Examples » Textured Triangle

Importing image data, texturing and custom shaders.


This example extends the basic Triangle example with these features:

  • Working with textures and using the Trade library for importing image data.
  • Creating custom shaders.
  • Storing resources in the executable, so they don't have to be carried as separate files along the application.

Basic skeleton

Compared to the original triangle example, we need extra includes for loading image data and uploading them to a texture:

#include <Corrade/Containers/ArrayView.h>
#include <Corrade/Containers/Optional.h>
#include <Corrade/PluginManager/Manager.h>
#include <Corrade/Utility/Resource.h>
#include <Magnum/ImageView.h>
#include <Magnum/GL/Buffer.h>
#include <Magnum/GL/DefaultFramebuffer.h>
#include <Magnum/GL/Mesh.h>
#include <Magnum/GL/Texture.h>
#include <Magnum/GL/TextureFormat.h>
#include <Magnum/Platform/Sdl2Application.h>
#include <Magnum/Trade/AbstractImporter.h>
#include <Magnum/Trade/ImageData.h>

#include "TexturedTriangleShader.h"

The basic skeleton of main example class is similar to the original, except for a custom shader and added GL::Texture2D.

class TexturedTriangleExample: public Platform::Application {
        explicit TexturedTriangleExample(const Arguments& arguments);

        void drawEvent() override;

        GL::Mesh _mesh;
        TexturedTriangleShader _shader;
        GL::Texture2D _texture;

Textured triangle shader

Let's start with the shader. The shader takes 2D vertex position and 2D texture coordinates. We declare both attributes as Vector2; assign vertex position to location zero and texture coordinates to location one. The locations can be chosen pretty arbitrarily, but location zero should be always occupied. It's also good to make attribute declarations compatible between shaders so you can use a mesh configured for one shader with another shader.

Next to these two attributes it also needs a uniform for the base color and a texture binding. We will provide convenience public API for setting these two parameters. Good practice is to allow method chaining on them.

class TexturedTriangleShader: public GL::AbstractShaderProgram {
        typedef GL::Attribute<0, Vector2> Position;
        typedef GL::Attribute<1, Vector2> TextureCoordinates;

        explicit TexturedTriangleShader();

        TexturedTriangleShader& setColor(const Color3& color) {
            setUniform(_colorUniform, color);
            return *this;

        TexturedTriangleShader& bindTexture(GL::Texture2D& texture) {
            return *this;

        enum: Int { TextureUnit = 0 };

        Int _colorUniform;

We store GLSL sources as compiled-in resources because that's the most convenient way — by storing them in a string directly in the source we would lose syntax highlighting and line numbering in case the GLSL compiler fails with an error, whereas by storing them as separate files we would need to carry these along the executable. The resource data will be compiled into the binary using CMake later. You can read more about compiled-in resources in Corrade's resource management tutorial.

In the constructor we load the GLSL sources from compiled-in resources, compile and attach them and link the program together. Note that we explicitly check for compilation and link status — it's better to exit the program immediately instead of leaving it in some unexpected state. We then retrieve location for the base color uniform. Then we set the texture layer uniform to fixed value, so it doesn't have to be set manually when using the shader for rendering. We require OpenGL 3.3 in the shader, so the attribute locations can be conveniently set directly in the shader source. With newer OpenGL versions we could also explicitly set uniform locations and texture layers itself, see Uniform locations and Specifying texture and image binding units. However, in this example we will keep compatibility with OpenGL 3.3.

TexturedTriangleShader::TexturedTriangleShader() {

    const Utility::Resource rs{"textured-triangle-data"};

    GL::Shader vert{GL::Version::GL330, GL::Shader::Type::Vertex};
    GL::Shader frag{GL::Version::GL330, GL::Shader::Type::Fragment};


    CORRADE_INTERNAL_ASSERT_OUTPUT(GL::Shader::compile({vert, frag}));

    attachShaders({vert, frag});


    _colorUniform = uniformLocation("color");

    setUniform(uniformLocation("textureData"), TextureUnit);

The TexturedTriangleShader.vert shader just sets the position and passes texture coordinates through to fragment shader. Note the explicit attribute locations:

layout(location = 0) in vec4 position;
layout(location = 1) in vec2 textureCoordinates;

out vec2 interpolatedTextureCoordinates;

void main() {
    interpolatedTextureCoordinates = textureCoordinates;

    gl_Position = position;

TexturedTriangleShader.frag loads color from the texture and multiplies it with color we specified in the uniform:

uniform vec3 color = vec3(1.0, 1.0, 1.0);
uniform sampler2D textureData;

in vec2 interpolatedTextureCoordinates;

out vec4 fragmentColor;

void main() {
    fragmentColor.rgb = color*texture(textureData, interpolatedTextureCoordinates).rgb;
    fragmentColor.a = 1.0;

Setting up the mesh and texture

As specified in the shader above, we use Vector2 for both 2D vertex positions and 2D texture coordinates:

TexturedTriangleExample::TexturedTriangleExample(const Arguments& arguments):
    Platform::Application{arguments, Configuration{}
        .setTitle("Magnum Textured Triangle Example")}
    struct TriangleVertex {
        Vector2 position;
        Vector2 textureCoordinates;
    const TriangleVertex data[]{
        {{-0.5f, -0.5f}, {0.0f, 0.0f}}, /* Left position and texture coordinate */
        {{ 0.5f, -0.5f}, {1.0f, 0.0f}}, /* Right position and texture coordinate */
        {{ 0.0f,  0.5f}, {0.5f, 1.0f}}  /* Top position and texture coordinate */

We then fill the buffer, configure mesh primitive and vertex count and specify attribute locations in the buffer for use with our shader:

    GL::Buffer buffer;
        .addVertexBuffer(std::move(buffer), 0,

Now we will instantiate the plugin manager and try to load the TgaImporter plugin. If the plugin cannot be loaded, we exit immediately. You can read more about plugin directory locations and plugin loading in Loading and using plugins.

    PluginManager::Manager<Trade::AbstractImporter> manager;
    Containers::Pointer<Trade::AbstractImporter> importer =
    if(!importer) std::exit(1);

Now we need to load the texture. Similarly to shader sources, the texture is also stored as resource in the executable.

    const Utility::Resource rs{"textured-triangle-data"};

After the image is loaded, we create a texture from it. Note that we have to explicitly set all required texture parameters, otherwise the texture will be incomplete. GL::textureFormat() is a convenience utility that picks up GL::TextureFormat::RGB8, GL::TextureFormat::RGBA8 or any other based on image's PixelFormat.

    Containers::Optional<Trade::ImageData2D> image = importer->image2D(0);
        .setStorage(1, GL::textureFormat(image->format()), image->size())
        .setSubImage(0, {}, *image);

The drawing function is again fairly simple. We clear the buffer, set base color to light red, set the texture and perform the drawing. Last thing is again buffer swap.

void TexturedTriangleExample::drawEvent() {

    using namespace Math::Literals;



And, don't forget the main function:



Compilation is slightly more complicated compared to previous examples, because we need to compile our resources into the executable.

The resources.conf file lists all resources which need to be compiled into the executable — that is our GLSL shader sources and the texture image (the one used in the above screenshot is stone.tga). All resource groups need to have unique identifier by which they are accessed, we will use textured-triangle-data as above. As said above, the resource compilation process is explained thoroughly in Corrade's resource management tutorial.





In the CMakeLists.txt file first we find the required Magnum package, now asking for the Trade library as well:

find_package(Corrade REQUIRED Main)
find_package(Magnum REQUIRED GL Trade Sdl2Application)


After that we compile the resources using the corrade_add_resource() macro and create an executable from the result and other source files. Last step is linking to all needed libraries. The plugin is loaded dynamically, so it's not handled by CMake in any way.

corrade_add_resource(TexturedTriangle_RESOURCES resources.conf)

add_executable(magnum-textured-triangle WIN32
target_link_libraries(magnum-textured-triangle PRIVATE

Once the application builds and starts, you can now try playing around with the shader source, modifying texture coordinates or adding other effects. The full file content is linked below. Full source code is also available in the magnum-examples GitHub repository.

The ports branch contains additional patches for iOS, Android and Emscripten support that aren't present in master in order to keep the example code as simple as possible.

PNG, JPEG and other file formats

In an effort to keep the core Magnum repository small, it has support just for the TGA format. But it doesn't stop there, the Magnum Plugins repository provides support for many more file formats. Let's try loading arbitrary files instead — the AnyImageImporter plugin autodetects a file format and then delegates to a plugin that knows how to load PNGs. All you need to do is load it instead of "TgaImporter" and then bundle a different file type — or rewrite the example to take files from the filesystem instead:

Containers::Pointer<Trade::AbstractImporter> importer =
if(!importer) std::exit(1);


See Loading and using plugins for more information about how plugin loading and selection works. The next tutorial, Model Viewer, expands further on this topic and also shows how to open files passed via command-line arguments.