Examples » Textured Quad

Indexed meshes, importing image data, texturing and custom shaders.

Image

This example expands on the basic Triangle example with indexed meshes, loading images into textures, creating custom shaders and embedding resources into the executable.

Basic skeleton

Compared to the previous example, we need extra includes for image loading, textures and compiled-in resources:

#include <Corrade/Containers/Optional.h>
#include <Corrade/Containers/StringView.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 "TexturedQuadShader.h"

Basic skeleton of the main example class is similar to the previous one, except for a custom shader and added GL::Texture2D.

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

    private:
        void drawEvent() override;

        GL::Mesh _mesh;
        TexturedQuadShader _shader;
        GL::Texture2D _texture;
};

Textured shader

Let's look at the shader sources first, and then see how to expose their interface to the C++ code. The TexturedQuadShader.vert shader just sets the position and passes texture coordinates through to the fragment shader. Note the explicit attribute locations, which we will refer to later:

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

out vec2 interpolatedTextureCoordinates;

void main() {
    interpolatedTextureCoordinates = textureCoordinates;

    gl_Position = position;
}

TexturedQuadShader.frag loads a color from the texture and multiplies it with a color coming from a uniform. It also sets a reasonable default in case the uniform isn't set from user code.

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;
}

We'll store the GLSL code as compiled-in resources because that's the most convenient way. Having them as a string literal in the C++ code would make them lose syntax highlighting but also mess up with line numbering in case the GLSL compiler fails with an error. And having them as separate files would cause additional complications when running the executable. The resource data will be compiled into the binary using CMake at the end of the example.

On the C++ side, we subclass a GL::AbstractShaderProgram and define the two vertex inputs using a GL::Attribute typedef, with matching types and locations. Next to these two attributes we also expose a uniform setter for the base color, and a texture binding function. Good practice is to allow method chaining on them.

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

        explicit TexturedQuadShader();

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

        TexturedQuadShader& bindTexture(GL::Texture2D& texture) {
            texture.bind(TextureUnit);
            return *this;
        }

    private:
        enum: Int { TextureUnit = 0 };

        Int _colorUniform;
};

In the constructor we load the above-mentioned embedded GLSL sources via Utility::Resource, compile and attach them and link the program together. We then retrieve location for the base color uniform and set the texture layer uniform to the same index we used in the bindTexture() implementation above. As we conservatively rely on OpenGL 3.3, only the attribute locations could be set directly in the shader source with layout(location = N). With newer OpenGL versions we could also explicitly set uniform locations and texture binding slots, here we have to set them up from the C++ side using uniformLocation() and setUniform(). See Binding attribute and fragment data location, Uniform locations and Specifying texture and image binding units for more information.

TexturedQuadShader::TexturedQuadShader() {
    MAGNUM_ASSERT_GL_VERSION_SUPPORTED(GL::Version::GL330);

    const Utility::Resource rs{"texturedquad-data"};

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

    vert.addSource(rs.getString("TexturedQuadShader.vert"));
    frag.addSource(rs.getString("TexturedQuadShader.frag"));

    CORRADE_INTERNAL_ASSERT_OUTPUT(vert.compile() && frag.compile());

    attachShaders({vert, frag});

    CORRADE_INTERNAL_ASSERT_OUTPUT(link());

    _colorUniform = uniformLocation("color");
    setUniform(uniformLocation("textureData"), TextureUnit);
}

Setting up an indexed mesh

To match the expectations of our shader, we'll use a Vector2 for both 2D vertex positions and 2D texture coordinates. Instead of a single triangle however, we'll draw a quad using an index buffer — one vertex for each corner, but composed of two triangles in a counterclockwise order, resulting in six indices in total.

TexturedQuadExample::TexturedQuadExample(const Arguments& arguments):
    Platform::Application{arguments, Configuration{}
        .setTitle("Magnum Textured Quad Example")}
{
    struct QuadVertex {
        Vector2 position;
        Vector2 textureCoordinates;
    };
    const QuadVertex vertices[]{
        {{ 0.5f, -0.5f}, {1.0f, 0.0f}}, /* Bottom right */
        {{ 0.5f,  0.5f}, {1.0f, 1.0f}}, /* Top right */
        {{-0.5f, -0.5f}, {0.0f, 0.0f}}, /* Bottom left */
        {{-0.5f,  0.5f}, {0.0f, 1.0f}}  /* Top left */
    };
    const UnsignedInt indices[]{        /* 3--1 1 */
        0, 1, 2,                        /* | / /| */
        2, 1, 3                         /* |/ / | */
    };                                  /* 2 2--0 */

Because it's an indexed mesh, we pass index count instead of vertex count to GL::Mesh::setCount(), and in addition to adding the vertex buffer we call also GL::Mesh::setIndexBuffer() with an index type that matches our data. The buffers are again directly moved to the mesh so we don't need to worry about them later.

    _mesh.setCount(Containers::arraySize(indices))
        .addVertexBuffer(GL::Buffer{vertices}, 0,
            TexturedQuadShader::Position{},
            TexturedQuadShader::TextureCoordinates{})
        .setIndexBuffer(GL::Buffer{indices}, 0,
            GL::MeshIndexType::UnsignedInt);

Loading an image and populating the texture

Magnum implements support for various file formats using dynamically loaded plugins. This way the core libraries don't need to implicitly depend on a variety of 3rd party libraries and there's also a possibility to choose among alternative implementations for certain formats. See Loading and using plugins for more information about how plugins work and File format support for a list of common formats and plugins implementing them.

For simplicity we'll use the TGA format through the TgaImporter plugin, which is available directly in the core Magnum repository. The image will also be embedded as a resource, similarly to the GLSL shader code. If the plugin fails to load or the image file can't be opened, we exit — no need to print anything in this case, the manager or the plugin itself already print an error message on their own.

    PluginManager::Manager<Trade::AbstractImporter> manager;
    Containers::Pointer<Trade::AbstractImporter> importer =
        manager.loadAndInstantiate("TgaImporter");
    const Utility::Resource rs{"texturedquad-data"};
    if(!importer || !importer->openData(rs.getRaw("stone.tga")))
        std::exit(1);

After the image is loaded, we create a GL::Texture2D 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);
    CORRADE_INTERNAL_ASSERT(image);
    _texture.setWrapping(GL::SamplerWrapping::ClampToEdge)
        .setMagnificationFilter(GL::SamplerFilter::Linear)
        .setMinificationFilter(GL::SamplerFilter::Linear)
        .setStorage(1, GL::textureFormat(image->format()), image->size())
        .setSubImage(0, {}, *image);
}

Drawing

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

void TexturedQuadExample::drawEvent() {
    GL::defaultFramebuffer.clear(GL::FramebufferClear::Color);

    using namespace Math::Literals;

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

    swapBuffers();
}

Finally, there's the main function macro:

MAGNUM_APPLICATION_MAIN(TexturedQuadExample)

Compilation

Compilation is slightly more complicated compared to the previous example, because we're embedding resources into the executable.

The resources.conf file lists all resources which need to be compiled into the executable, which is our GLSL shader sources and the texture image (the one used in the above screenshot is stone.tga). A resource group has an identifier through which it gets accessed, we use texturedquad-data to match the C++ code above. The resource compilation process is explained thoroughly in Corrade's resource management tutorial.

group=texturedquad-data

[file]
filename=TexturedQuadShader.frag

[file]
filename=TexturedQuadShader.vert

[file]
filename=stone.tga

In the CMakeLists.txt file first we find the required Magnum package. We don't need the Shaders library for anything anymore, however we use Trade to load our image:

find_package(Corrade REQUIRED Main)
find_package(Magnum REQUIRED GL 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 in any way.

corrade_add_resource(TexturedQuad_RESOURCES resources.conf)

add_executable(magnum-texturedquad WIN32
    TexturedQuadExample.cpp
    TexturedQuadShader.cpp
    TexturedQuadShader.h
    ${TexturedQuad_RESOURCES})
target_link_libraries(magnum-texturedquad PRIVATE
    Corrade::Main
    Magnum::Application
    Magnum::GL
    Magnum::Magnum
    Magnum::Trade)

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 =
    manager.loadAndInstantiate("AnyImageImporter");
if(!importer || !importer->openFile("path/to/my/file.jpg"))
    std::exit(1);

The Model Viewer tutorial expands further on this topic and also shows how to open files passed via command-line arguments.