Textured Quad
Indexed meshes, importing image data, texturing and custom shaders.
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::
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::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::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(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::
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_
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.
- CMakeLists.txt
- resources.conf
- TexturedQuadExample.cpp
- TexturedQuadShader.cpp
- TexturedQuadShader.frag
- TexturedQuadShader.h
- TexturedQuadShader.vert
- stone.tga
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.