Examples » Primitives

Importing mesh data, 3D transformations and input handling.

Image

This example shows how to create indexed meshes from imported data and do some basic user interaction, introducing these new features:

  • Interleaving vertex data and compressing indices for better performance.
  • Basic 3D transformations and perspective projection.
  • Mouse event handling.

This example displays a colored cube with ability to change its color and rotate it using a mouse.

Setting up

This example makes use of imported 3D mesh data, processes them and renders using a Phong shader.

#include <Magnum/GL/Buffer.h>
#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/Interleave.h>
#include <Magnum/MeshTools/CompressIndices.h>
#include <Magnum/Platform/Sdl2Application.h>
#include <Magnum/Primitives/Cube.h>
#include <Magnum/Shaders/Phong.h>
#include <Magnum/Trade/MeshData3D.h>

This time, for user interactivity we'll need to implement mouse event handlers and store transformation & projection matrices plus current color.

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

    private:
        void drawEvent() override;
        void mousePressEvent(MouseEvent& event) override;
        void mouseReleaseEvent(MouseEvent& event) override;
        void mouseMoveEvent(MouseMoveEvent& event) override;

        GL::Mesh _mesh;
        Shaders::Phong _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. Enabling face culling is not needed for proper rendering, but it will speed things up as only front-facing faces will be rendered.

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

We now use the pre-made cube primitive and create a mesh from it. The mesh is indexed and contains position and normal data. As said in the Triangle example, interleaving the data gives us the best memory access performance. We can do it by hand as in the previous example, but using MeshTools::interleave() is much more convenient. We upload the interleaved data directly to a vertex buffer.

    const Trade::MeshData3D cube = Primitives::cubeSolid();

    GL::Buffer vertices;
    vertices.setData(MeshTools::interleave(cube.positions(0), cube.normals(0)));

Why do we need an indexed mesh and what it actually is? In most meshes the same vertex data are shared among more than one vertex, even a simple square consists of two triangles sharing two adjacent vertices. To save precious GPU memory, the mesh can be indexed, i.e. containing a buffer with unique vertex data and an index buffer telling which data belong to which vertex. The indices are by default just 32-bit integers. But most meshes don't need the full 32-bit range to index vertex data — our mesh has only 36 unique vertices, thus even the smallest possible 8-bit range is large enough. MeshTools::compressIndices() again does all the boring work for us — it checks index range and creates an array consisting of UnsignedByte, UnsignedShort or UnsignedInt indices based on that.

    Containers::Array<char> indexData;
    MeshIndexType indexType;
    UnsignedInt indexStart, indexEnd;
    std::tie(indexData, indexType, indexStart, indexEnd) =
        MeshTools::compressIndices(cube.indices());
    GL::Buffer indices;
    indices.setData(indexData);

Everything is now ready for configuring the mesh. We set the primitive type, index count, add our vertex buffer and specify the index buffer. The indexStart and indexEnd parameters are purely optional, but they might improve memory access performance on desktop GL as the GPU will know what subset of vertex data are used.

    _mesh.setPrimitive(cube.primitive())
        .setCount(cube.indices().size())
        .addVertexBuffer(std::move(vertices), 0, Shaders::Phong::Position{},
                                                 Shaders::Phong::Normal{})
        .setIndexBuffer(std::move(indices), 0, indexType, indexStart, indexEnd);

As a final step in the constructor we specify the initial transformation, projection and color. See Operations with matrices and vectors and 2D and 3D transformations for a more thorough introduction to transformations.

    _transformation =
        Matrix4::rotationX(30.0_degf)*Matrix4::rotationY(40.0_degf);
    _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 (don't forget to clear also the depth buffer), set transformation, normal and projection matrices and all material parameters and draw the mesh.

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

    _shader.setLightPosition({7.0f, 5.0f, 2.5f})
        .setLightColor(Color3{1.0f})
        .setDiffuseColor(_color)
        .setAmbientColor(Color3::fromHsv({_color.hue(), 1.0f, 0.3f}))
        .setTransformationMatrix(_transformation)
        .setNormalMatrix(_transformation.normalMatrix())
        .setProjectionMatrix(_projection);
    _mesh.draw(_shader);

    swapBuffers();
}

Mouse event handling

Event handling is also nothing complicated, on every click (press + release) we change color hue to some other and on mouse drag we rotate the object based on relative mouse position to previous event.

void PrimitivesExample::mousePressEvent(MouseEvent& event) {
    if(event.button() != MouseEvent::Button::Left) return;

    event.setAccepted();
}

void PrimitivesExample::mouseReleaseEvent(MouseEvent& event) {
    _color = Color3::fromHsv({_color.hue() + 50.0_degf, 1.0f, 1.0f});

    event.setAccepted();
    redraw();
}

void PrimitivesExample::mouseMoveEvent(MouseMoveEvent& event) {
    if(!(event.buttons() & MouseMoveEvent::Button::Left)) 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 need some additional libraries, like MeshTools and Primitives, everything else is the same as previously:

find_package(Corrade REQUIRED Main)
find_package(Magnum REQUIRED
    GL
    MeshTools
    Primitives
    Shaders
    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)

You can now try using another primitive from Primitives namespace or render the mesh with 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.