Examples and tutorials » Primitives

Importing mesh data, 3D transformations and input handling.

Image

This example shows how to easily create a GL::Mesh from arbitrary external data and do basic mouse or touch interaction in a 3D scene.

Setting up

The new includes are for a Primitives::cubeSolid(), which returns a Trade::MeshData, which we then turn into a GL mesh using MeshTools::compile(). And because we have a 3D scene now, we'll want to draw it with Shaders::PhongGL instead of plain flat shading.

#include <Magnum/GL/DefaultFramebuffer.h>
#include <Magnum/GL/Mesh.h>
#include <Magnum/GL/Renderer.h>
#include <Magnum/Math/Color.h>
#include <Magnum/Math/Matrix4.h>
#include <Magnum/MeshTools/Compile.h>
#include <Magnum/Platform/Sdl2Application.h>
#include <Magnum/Primitives/Cube.h>
#include <Magnum/Shaders/PhongGL.h>
#include <Magnum/Trade/MeshData.h>

This time, for user interactivity we'll need to implement pointer event handlers and store transformation & projection Matrix4 plus the current Color3.

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

    private:
        void drawEvent() override;
        void pointerReleaseEvent(PointerEvent& event) override;
        void pointerMoveEvent(PointerMoveEvent& event) override;

        GL::Mesh _mesh;
        Shaders::PhongGL _shader;

        Matrix4 _transformation, _projection;
        Color3 _color;
};

Because we are displaying a 3D scene, we need to enable depth test to have the cube rendered in proper Z order. The GL::Renderer::Feature::FaceCulling is not strictly needed, but for closed meshes it can save a lot of rasterization work by discarding triangles that face away from camera.

PrimitivesExample::PrimitivesExample(const Arguments& arguments):
    Platform::Application{arguments, Configuration{}
        .setTitle("Magnum Primitives Example")}
{
    GL::Renderer::enable(GL::Renderer::Feature::DepthTest);
    GL::Renderer::enable(GL::Renderer::Feature::FaceCulling);

Preparing the mesh

Perhaps the biggest leap from previous examples is that setting up the mesh is now just a simple oneliner:

    _mesh = MeshTools::compile(Primitives::cubeSolid());

The main responsibility here lies on Trade::MeshData, which is a class capable of describing arbitrary meshes in arbitrary layouts. It's used as the backbone of the Primitives library as well as of Magnum's asset import pipeline, which will be shown in the next Model Viewer example.

The Primitives::cubeSolid() gives us an indexed triangle mesh with positions and normals. MeshTools::compile() then takes the Trade::MeshData, uploads it to the GPU, and pops out a GL::Mesh that owns the index and vertex buffers and has all attributes configured to be rendered with builtin shaders.

While this admittedly skipped quite a few intermediate steps compared to manual mesh setup from the previous examples, in practice you'll mostly either deal with raw attribute arrays or already have a Trade::MeshData to begin with. Nevertheless, it's not an opaque container — be sure to go through its documentation to understand how the data is represented, how the attributes can be accessed and how it all maps to a GL::Mesh.

Transformation and projection

As a final step in the constructor we specify the initial cube transformation, rotating it slightly for a 3D look. Earlier we used color literals from Math::Literals, here we use angle literals. Magnum has strongly-typed angles, which allows us to conveniently use degrees even though APIs such as Matrix4::rotationX() accept radians:

    _transformation =
        Matrix4::rotationX(30.0_degf)*Matrix4::rotationY(40.0_degf);

Then we set up a perspective projection with an aspect ratio matching the window size and an initial color with an orange hue. For a more thorough introduction to Magnum's vector math library please see Operations with matrices and vectors and 2D and 3D transformations.

    _projection =
        Matrix4::perspectiveProjection(
            35.0_degf, Vector2{windowSize()}.aspectRatio(), 0.01f, 100.0f)*
        Matrix4::translation(Vector3::zAxis(-10.0f));
    _color = Color3::fromHsv({35.0_degf, 1.0f, 1.0f});
}

Rendering

In the draw event we clear the framebuffer, now also clearing the depth buffer as we need it for the depth test. For the shader we hardcode a directional light source, set the cached color, transformation and projection matrices including a normal matrix and draw the mesh.

void PrimitivesExample::drawEvent() {
    GL::defaultFramebuffer.clear(
        GL::FramebufferClear::Color|GL::FramebufferClear::Depth);

    _shader.setLightPositions({{1.4f, 1.0f, 0.75f, 0.0f}})
        .setDiffuseColor(_color)
        .setAmbientColor(Color3::fromHsv({_color.hue(), 1.0f, 0.3f}))
        .setTransformationMatrix(_transformation)
        .setNormalMatrix(_transformation.normalMatrix())
        .setProjectionMatrix(_projection)
        .draw(_mesh);

    swapBuffers();
}

Pointer event handling

On a click or tap (pointer press + release) we want to shift the color hue and on drag we'll rotate the object using a bunch of vector and matrix operations. Platform::Sdl2Application supports both mouse and touch input, to make the code touch-aware we allow Pointer::Finger to be used in addition to Pointer::MouseLeft. We won't handle any multi-touch gestures and thus ignore all but the PointerEvent::isPrimary() events.

To save resources, the application doesn't redraw constantly by default, which means we have to trigger a redraw() in order to see any effect from the events. In case you'd want to draw constantly — for example in order to perform an animation — you can call redraw() directly in the drawEvent().

void PrimitivesExample::pointerReleaseEvent(PointerEvent& event) {
    if(!event.isPrimary() ||
       !(event.pointer() & (Pointer::MouseLeft|Pointer::Finger)))
        return;

    _color = Color3::fromHsv({_color.hue() + 50.0_degf, 1.0f, 1.0f});

    event.setAccepted();
    redraw();
}

void PrimitivesExample::pointerMoveEvent(PointerMoveEvent& event) {
    if(!event.isPrimary() ||
       !(event.pointers() & (Pointer::MouseLeft|Pointer::Finger)))
        return;

    Vector2 delta = 3.0f*Vector2{event.relativePosition()}/Vector2{windowSize()};

    _transformation =
        Matrix4::rotationX(Rad{delta.y()})*
        _transformation*
        Matrix4::rotationY(Rad{delta.x()});

    event.setAccepted();
    redraw();
}

The main function is just the macro call, as previously.

MAGNUM_APPLICATION_MAIN(PrimitivesExample)

Compilation

Compilation is pretty straigtforward, similar to the Triangle example. Again omitting the basic setup described in the Getting Started Guide, we now use also the MeshTools, Primitives and Trade libraries:

find_package(Corrade REQUIRED Main)
find_package(Magnum REQUIRED
    GL
    MeshTools
    Primitives
    Shaders
    Trade
    Sdl2Application)

set_directory_properties(PROPERTIES CORRADE_USE_PEDANTIC_FLAGS ON)

add_executable(magnum-primitives WIN32 PrimitivesExample.cpp)
target_link_libraries(magnum-primitives PRIVATE
    Corrade::Main
    Magnum::Application
    Magnum::GL
    Magnum::Magnum
    Magnum::MeshTools
    Magnum::Primitives
    Magnum::Shaders
    Magnum::Trade)

You can now try using different primitives from the Primitives namespace or render the mesh with a different shader from Shaders namespace. 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.