Examples » Textured triangle

Importing image data, texturing and custom shaders.

Image

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/PluginManager/Manager.h>
#include <Magnum/Buffer.h>
#include <Magnum/DefaultFramebuffer.h>
#include <Magnum/Mesh.h>
#include <Magnum/Texture.h>
#include <Magnum/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 Texture2D.

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

    private:
        void drawEvent() override;

        Buffer _buffer;
        Mesh _mesh;
        TexturedTriangleShader _shader;
        Texture2D _texture;
};

Textured triangle shader

Let's start with the shader. It's practically a simplified version of textured Shaders::Flat2D, written for OpenGL 3.3 without bothering about backwards compatibility. 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 AbstractShaderProgram {
    public:
        typedef Attribute<0, Vector2> Position;
        typedef Attribute<1, Vector2> TextureCoordinates;

        explicit TexturedTriangleShader();

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

        TexturedTriangleShader& bindTexture(Texture2D& texture) {
            texture.bind(TextureLayer);
            return *this;
        }

    private:
        enum: Int { TextureLayer = 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() {
    MAGNUM_ASSERT_VERSION_SUPPORTED(Version::GL330);

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

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

    vert.addSource(rs.get("TexturedTriangleShader.vert"));
    frag.addSource(rs.get("TexturedTriangleShader.frag"));

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

    attachShaders({vert, frag});

    CORRADE_INTERNAL_ASSERT_OUTPUT(link());

    _colorUniform = uniformLocation("color");

    setUniform(uniformLocation("textureData"), TextureLayer);
}

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 vertex position and texture coordinate */
        {{ 0.5f, -0.5f}, {1.0f, 0.0f}}, /* Right vertex position and texture coordinate */
        {{ 0.0f,  0.5f}, {0.5f, 1.0f}}  /* Top vertex 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:

    _buffer.setData(data, BufferUsage::StaticDraw);
    _mesh.setPrimitive(MeshPrimitive::Triangles)
        .setCount(3)
        .addVertexBuffer(_buffer, 0,
            TexturedTriangleShader::Position{},
            TexturedTriangleShader::TextureCoordinates{});

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;
    std::unique_ptr<Trade::AbstractImporter> importer = manager.loadAndInstantiate("TgaImporter");
    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"};
    if(!importer->openData(rs.getRaw("stone.tga")))
        std::exit(2);

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.

    Containers::Optional<Trade::ImageData2D> image = importer->image2D(0);
    CORRADE_INTERNAL_ASSERT(image);
    _texture.setWrapping(Sampler::Wrapping::ClampToEdge)
        .setMagnificationFilter(Sampler::Filter::Linear)
        .setMinificationFilter(Sampler::Filter::Linear)
        .setStorage(1, TextureFormat::RGB8, 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() {
    defaultFramebuffer.clear(FramebufferClear::Color);

    using namespace Math::Literals;

    _shader.setColor(0xffb2b2_rgbf)
        .bindTexture(_texture);
    _mesh.draw(_shader);

    swapBuffers();
}

And, don't forget the main function:

MAGNUM_APPLICATION_MAIN(TexturedTriangleExample)

Compilation

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. 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.

group=textured-triangle-data

[file]
filename=TexturedTriangleShader.frag

[file]
filename=TexturedTriangleShader.vert

[file]
filename=stone.tga

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

find_package(Magnum REQUIRED Trade Sdl2Application)

set_directory_properties(PROPERTIES CORRADE_USE_PEDANTIC_FLAGS ON)

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 at all.

corrade_add_resource(TexturedTriangle_RESOURCES resources.conf)

add_executable(magnum-textured-triangle
    TexturedTriangleExample.cpp
    TexturedTriangleShader.cpp
    TexturedTriangleShader.h
    ${TexturedTriangle_RESOURCES})
target_link_libraries(magnum-textured-triangle PRIVATE
    Magnum::Application
    Magnum::Magnum
    Magnum::Trade)

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.