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 colored cube with ability to change its color and rotate it using mouse.

Setting up and preparing the mesh

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

In our class we now need two buffers instead of one. Additionaly for user interactivity we 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::Buffer _indexBuffer, _vertexBuffer;
        GL::Mesh _mesh;
        Shaders::Phong _shader;

        Matrix4 _transformation, _projection;
        Vector2i _previousMousePosition;
        Color3 _color;
};

Because we are displaying 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);

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 earlier, interleaving the data gives us 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 vertex buffer.

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

    _vertexBuffer.setData(MeshTools::interleave(cube.positions(0), cube.normals(0)), GL::BufferUsage::StaticDraw);

Why do we need 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 buffer with unique vertex data and index buffer telling which data belong to which vertex. The indices are by default just 32-bit integers. But most meshes don't need 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());
    _indexBuffer.setData(indexData, GL::BufferUsage::StaticDraw);

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(_vertexBuffer, 0, Shaders::Phong::Position{}, Shaders::Phong::Normal{})
        .setIndexBuffer(_indexBuffer, 0, indexType, indexStart, indexEnd);

We now specify the initial transformation, color and projection. See Operations with matrices and vectors and 2D and 3D transformations for more thorough introduction to transformations.

    _transformation = Matrix4::rotationX(30.0_degf)*
                      Matrix4::rotationY(40.0_degf);
    _color = Color3::fromHsv(35.0_degf, 1.0f, 1.0f);

    _projection = Matrix4::perspectiveProjection(35.0_degf, Vector2{GL::defaultFramebuffer.viewport().size()}.aspectRatio(), 0.01f, 100.0f)*
                  Matrix4::translation(Vector3::zAxis(-10.0f));
}

Rendering

In the draw event we clear the framebuffer (don't forget to clear also 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.rotationScaling())
        .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;

    _previousMousePosition = event.position();
    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;

    const Vector2 delta = 3.0f*
        Vector2{event.position() - _previousMousePosition}/
        Vector2{GL::defaultFramebuffer.viewport().size()};

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

    _previousMousePosition = event.position();
    event.setAccepted();
    redraw();
}

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

MAGNUM_APPLICATION_MAIN(PrimitivesExample)

Compilation

Compilation is pretty straigtforward. We now need some additional libraries, like MeshTools and Primitives, everything else is the same as previously:

find_package(Magnum REQUIRED
    GL
    MeshTools
    Primitives
    Shaders
    Sdl2Application)

set_directory_properties(PROPERTIES CORRADE_USE_PEDANTIC_FLAGS ON)

add_executable(magnum-primitives PrimitivesExample.cpp)
target_link_libraries(magnum-primitives PRIVATE
    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.