All Classes Namespaces Functions Variables Typedefs Enumerations Enumerator Friends Pages
Widget3DExample.cpp

The Widget3D example shows how to render 3D geometry inside applications using MultiTouch Cornerstone SDK.In this example we demonstrate how to render two different 3D objects: an interactive textured cube and an animated rotating propeller. It is recommended to be familiar with the AdvancedRendering example before proceeding to this example.

Widget3D-screenshot.png
Screenshot of the Widget3D example

The example contains two custom widgets: CubeWidget and PropellerWidget that represent the cube and propeller, respectively, and they both derive from class MultiWidgets::FrameBufferWidget. For those interested, MultiWidgets::FrameBufferWidget is a widget that automatically sets up necessary resources for drawing custom geometry through an off-screen render target. This means that the geometry is rendered to a separate texture and that texture is then used when drawing the widget itself.

Below are the headers for the custom widgets:

CubeWidget.hpp:

/* Copyright (C) 2007-2013 Multi Touch Oy, Finland, http://www.multitaction.com
*
* This file is part of MultiTouch Cornerstone.
*
* All rights reserved. You may use this file only for purposes for which you
* have a specific, written permission from Multi Touch Oy.
*
*/
#ifndef CUBE_WIDGET_HPP
#define CUBE_WIDGET_HPP
#include <MultiWidgets/FrameBufferWidget.hpp>
#include <Luminous/Buffer.hpp>
#include <Luminous/Image.hpp>
#include <Luminous/Program.hpp>
#include <Luminous/VertexArray.hpp>
namespace Examples
{
class CubeWidget : public MultiWidgets::FrameBufferWidget
{
public:
CubeWidget();
virtual void processFingers(MultiWidgets::GrabManager & gm, const MultiWidgets::FingerArray & fa, float dt) OVERRIDE;
enum CubeFace {
FACE_POSX,
FACE_NEGX,
FACE_POSY,
FACE_NEGY,
FACE_POSZ,
FACE_NEGZ
};
void setFaceTexture(CubeFace face, const QString & file);
private:
virtual void render3D(Luminous::RenderContext & r) const OVERRIDE;
private:
Luminous::Buffer m_vbuffer;
Luminous::VertexArray m_vertexArray;
Luminous::Image m_textures[6];
float m_yRotate;
};
}
#endif

PropellerWidget.hpp:

/* Copyright (C) 2007-2013 Multi Touch Oy, Finland, http://www.multitaction.com
*
* This file is part of MultiTouch Cornerstone.
*
* All rights reserved. You may use this file only for purposes for which you
* have a specific, written permission from Multi Touch Oy.
*
*/
#ifndef PROPELLER_WIDGET_HPP
#define PROPELLER_WIDGET_HPP
#include <MultiWidgets/FrameBufferWidget.hpp>
#include <Luminous/Buffer.hpp>
#include <Luminous/Program.hpp>
#include <Luminous/VertexArray.hpp>
namespace Examples
{
class PropellerWidget : public MultiWidgets::FrameBufferWidget
{
public:
PropellerWidget();
private:
void initVertexData();
virtual void update(const MultiWidgets::FrameInfo & frameInfo) OVERRIDE;
virtual void render3D(Luminous::RenderContext & r) const OVERRIDE;
private:
Luminous::Buffer m_vbuffer;
Luminous::VertexArray m_vertexArray;
// The description for our vertex data
struct Vertex
{
Nimble::Vector3f position;
};
std::vector<Vertex> m_vertexData;
float m_rotation;
};
}
#endif

In general for rendering 3D objects we need to do the following things: Load and describe shaders, Create or load and describe the geometry, eg. vertex data, setup transformations and render.

First, let's take a look at CubeWidget. For a cube we need a total of six faces each consisting of four vertices. Each vertex has a position and a texture coordinate. Because a cube is geometrically such a simple object we can just manually define each vertex. First, we define a struct that describes the data of one vertex and then define all vertices of the cube:

struct Vertex
{
Nimble::Vector3f position;
};
static const Vertex vertexData[] = {
// Front
{ Nimble::Vector3f(-1, 1, 1), Nimble::Vector2f(0,1) },
{ Nimble::Vector3f( 1, 1, 1), Nimble::Vector2f(1,1) },
{ Nimble::Vector3f(-1,-1, 1), Nimble::Vector2f(0,0) },
{ Nimble::Vector3f( 1,-1, 1), Nimble::Vector2f(1,0) },
// Left
{ Nimble::Vector3f(-1, 1,-1), Nimble::Vector2f(0,1) },
{ Nimble::Vector3f(-1, 1, 1), Nimble::Vector2f(1,1) },
{ Nimble::Vector3f(-1,-1,-1), Nimble::Vector2f(0,0) },
{ Nimble::Vector3f(-1,-1, 1), Nimble::Vector2f(1,0) },
// Right
{ Nimble::Vector3f( 1, 1,-1), Nimble::Vector2f(0,1) },
{ Nimble::Vector3f( 1, 1, 1), Nimble::Vector2f(1,1) },
{ Nimble::Vector3f( 1,-1,-1), Nimble::Vector2f(0,0) },
{ Nimble::Vector3f( 1,-1, 1), Nimble::Vector2f(1,0) },
// Back
{ Nimble::Vector3f( 1, 1,-1), Nimble::Vector2f(0,1) },
{ Nimble::Vector3f(-1, 1,-1), Nimble::Vector2f(1,1) },
{ Nimble::Vector3f( 1,-1,-1), Nimble::Vector2f(0,0) },
{ Nimble::Vector3f(-1,-1,-1), Nimble::Vector2f(1,0) },
// Bottom
{ Nimble::Vector3f(-1, 1,-1), Nimble::Vector2f(0,1) },
{ Nimble::Vector3f( 1, 1,-1), Nimble::Vector2f(1,1) },
{ Nimble::Vector3f(-1, 1, 1), Nimble::Vector2f(0,0) },
{ Nimble::Vector3f( 1, 1, 1), Nimble::Vector2f(1,0) },
// Top
{ Nimble::Vector3f(-1,-1, 1), Nimble::Vector2f(0,1) },
{ Nimble::Vector3f( 1,-1, 1), Nimble::Vector2f(1,1) },
{ Nimble::Vector3f(-1,-1,-1), Nimble::Vector2f(0,0) },
{ Nimble::Vector3f( 1,-1,-1), Nimble::Vector2f(1,0) },
};

For rendering the cube we also need two matrices, i.e. the projection and the model-view transformation matrices. These are passed to the shader as uniforms and for that we need to describe the uniform buffer:

struct Uniforms
{
Nimble::Matrix4f projMatrix;
Nimble::Matrix4f modelMatrix;
};

In the constructor we need to load the shaders that are used to render the geometry and create a description for the vertex attributes.

const QString vs("cube.vs");
const QString fs("cube.fs");
m_shader.loadShader(vs, Luminous::Shader::Vertex);
m_shader.loadShader(fs, Luminous::Shader::Fragment);
desc.addAttribute<Nimble::Vector3f>("vertex_position");
desc.addAttribute<Nimble::Vector2f>("vertex_uv");
m_shader.setVertexDescription(desc);

The shaders themselves are very simple. The vertex shader simply transforms the vertices from object space to screen space and the fragment shader just assigns a color to the fragment based on the texture. The shaders are included below for reference:

The vertex shader:

#version 150
layout(std140, column_major) uniform BaseBlock
{
mat4 projMatrix;
mat4 modelMatrix;
};
in vec3 vertex_position;
in vec2 vertex_uv;
out vec2 fs_vertex_uv;
void main()
{
fs_vertex_uv = vertex_uv;
gl_Position = projMatrix * modelMatrix * vec4(vertex_position, 1);
}

The fragment shader:

#version 150
layout(std140, column_major) uniform BaseBlock
{
mat4 projMatrix;
mat4 modelMatrix;
};
uniform sampler2D tex;
in vec2 fs_vertex_uv;
out vec4 frag_color;
void main()
{
frag_color = texture(tex, fs_vertex_uv);
}

We then assign the vertex data to a vertex buffer and bind the buffer to a vertex array. By using a vertex array we can render the whole geometry instead of manually having to specify each vertex in every render call.

// update the vertex buffer
m_vbuffer.setData(vertexData, sizeof(vertexData), Luminous::Buffer::STATIC_DRAW);
m_vertexArray.addBinding(m_vbuffer, desc);

To assign the texture for each face we define a function that reads a texture from the disk and stores the data in an array of textures:

void CubeWidget::setFaceTexture(CubeFace face, const QString & file)
{
m_textures[face].read(file);
}

In the render3D function we first setup the projection and model-view matrices. The projection matrix is a simple perspective projection. The model-view matrix first rotates the cube around the y-axis by m_yRotate degrees and then translates along the -z-axis. Then we tell the render context to use this transformation.

// Create projection matrix
const Nimble::Matrix4f matPerspective = Nimble::Matrix4f::perspectiveProjection(90, 1, 1, 1024);
Luminous::ViewTransformGuard viewGuard(r, matPerspective);
// Create transformation matrix
const auto matModelView =
Nimble::Matrix4::makeTranslation(Nimble::Vector3f(0.f, 0.f, -4.f))
{
Luminous::TransformGuard modelViewGuard(r, matModelView);

Next we render the cube. We do this by rendering each of the six faces separately by calculating the correct index to the vertex array and selecting the corresponding texture. Lastly, we update the uniform values to the shader. Note that we transpose the matrices because Cornerstone uses row-major ordering while the shader expects column-major ordering.

// Clear the frame buffer
r.clear(Luminous::CLEARMASK_COLOR_DEPTH, backgroundColor());
std::map<QByteArray, const Luminous::Texture *> textures;
for (int i = 0; i < 6; ++i) {
// Render all the faces
textures["tex"] = &m_textures[i].texture();
auto b = r.render<Vertex, Uniforms>(false,
i * 4, 4, 1.f,
m_vertexArray,
m_shader, &textures);
// Fill the uniform data
b.uniform->projMatrix = r.viewTransform().transposed();
b.uniform->modelMatrix = r.transform().transposed();
}
}

After rendering don't forget to restore the transform!

This is enough to render a static 3D cube on the screen. However, we still want to interact with the cube. We do this by overriding the processFingers function and calculating the rotation amount of the cube based on how much the input moves horizontally. If more than one finger is interacting with the widget we fall back to the default behaviour.

void CubeWidget::processFingers(MultiWidgets::GrabManager & gm, const MultiWidgets::FingerArray & fa, float dt)
{
if(fa.size() == 1) {
float rotate, scale;
Nimble::Vector2 translate, screenP;
// Compute motion in screen coordinates
calculateMotion(fa, translate, scale, rotate, screenP);
// Convert translation to local coordinates
Nimble::Matrix3 transform = gm.transform3();
translate = (transform * Nimble::Vector3(translate, 1) - transform * Nimble::Vector3(0, 0, 1)).vector2();
m_yRotate += translate.x;
}
else
FrameBufferWidget::processFingers(gm, fa, dt);
}

The PropellerWidget is very similar in all aspects. The main difference is that we procedurally generate the vertex data instead of manually defining each vertex. We do this by creating a set of blades, each consisting of several sections. The whole geometry is created as one single triangle strip so we can draw the whole mesh in one render call.

// Generate the propeller vertex data
void PropellerWidget::initVertexData()
{
m_vertexData.clear();
int blades = 18;
int sections = 40;
float radius = 3.0f;
float height = 0.3f;
float twist = 1.f;
for(int i = 0; i < blades; i++) {
float rel = i / (float) blades;
float angle = rel * Nimble::Math::TWO_PI;
Nimble::Vector3 dir(cosf(angle), 0, sinf(angle));
dir.normalize(radius);
Nimble::Vector3 topcolor(0.6, 0.9, 0.6+sinf(angle)*0.4f);
Nimble::Vector3 botcolor(0.0, 0.0, 0.0);
for(int j = 0; j <= sections; j++) {
Vertex vertex0, vertex1;
float jrel = j / (float) sections;
float h = height * cosf(jrel * Nimble::Math::HALF_PI);
m.rotateAroundAxis(dir, jrel * twist);
Nimble::Vector3 offset = dir * (jrel + 0.1f);
Nimble::Vector3 turn = m * Nimble::Vector3(0, h, 0);
Nimble::Vector3 n = cross(dir, turn);
n.normalize();
vertex0.normal = n;
vertex0.color = topcolor;
vertex0.position = offset + turn;
vertex1.normal = n;
vertex1.color = botcolor;
vertex1.position = offset - turn;
// make degenerated triangles between blades
if (i > 0 && j == 0)
m_vertexData.push_back(vertex0);
m_vertexData.push_back(vertex0);
m_vertexData.push_back(vertex1);
if (i != blades-1 && j == sections)
m_vertexData.push_back(vertex1);
}
}
}

Vertex data and uniforms are described in the same way as before. Vertices now have normal and color attributes instead of texture coordinates because we shade the mesh using lighting. We also need a separate transformation matrix for normals because direction vectors need to be transformed differently from position vectors.

// The description for our vertex data
struct Vertex
{
Nimble::Vector3f position;
};
// The description for our uniform buffer
struct PropellerUniforms
{
Nimble::Matrix4f projMatrix;
Nimble::Matrix4f modelMatrix;
Nimble::Matrix4f normalMatrix;
};

In the constructor we load and initialize the data the same way as before (except we procedurally generate the vertex data instead of using a static array):

// Generate vertex data
initVertexData();
// Update the vertex buffer
m_vbuffer.setData(m_vertexData.data(), sizeof(Vertex) * m_vertexData.size(), Luminous::Buffer::STATIC_DRAW);
m_vertexArray.addBinding(m_vbuffer, desc);

Rendering is very similar as well. Instead of rendering the six faces separately we can draw the whole geometry as a single triangle strip:

void PropellerWidget::render3D(Luminous::RenderContext & r) const
{
// Create projection matrix
const Nimble::Matrix4f matPerspective = Nimble::Matrix4f::perspectiveProjection(90, 1, 1, 1024);
Luminous::ViewTransformGuard viewGuard(r, matPerspective);
// Create transformation matrix
const auto matModelView = Nimble::Matrix4::makeTranslation(Nimble::Vector3f(0.f, 0.f, -4.f))
{
Luminous::TransformGuard modelViewGuard(r, matModelView);
// Clear the frame buffer
r.clear(Luminous::CLEARMASK_COLOR_DEPTH, backgroundColor());
// Render the propeller mesh
auto b = r.render<Vertex, PropellerUniforms>(false,
0, m_vertexData.size(), 1.f,
m_vertexArray, m_shader);
// Fill the uniform data
b.uniform->projMatrix = r.viewTransform().transposed();
b.uniform->modelMatrix = r.transform().transposed();
b.uniform->normalMatrix = r.transform().inverse();
}
}

The shaders for the propeller are a bit more complex because we use diffuse lighting to shade the primitives. In the vertex shader we simply transform the vertex position and normals. In the fragment shader we then calculate a diffuse coefficient based on the angle between the normal of the surface and the light direction and apply the diffuse color to those parts that are facing the light.

The vertex shader:

#version 150
layout(std140, column_major) uniform BaseBlock
{
mat4 projMatrix;
mat4 modelMatrix;
mat3 normalMatrix;
};
// light direction
vec3 L = vec3(-1.0, -1.0, 1.0);
in vec3 vertex;
in vec3 normal;
in vec3 diffuse;
out vec3 vs_normal;
out vec3 vs_diffuse;
out vec3 vs_lightDir;
void main()
{
vs_normal = normalize(normalMatrix * normal);
vs_lightDir = normalize(L);
vs_diffuse = diffuse;
gl_Position = projMatrix * modelMatrix * vec4(vertex, 1);
}

And the fragment shader:

#version 150
layout(std140, column_major) uniform BaseBlock
{
mat4 projMatrix;
mat4 modelMatrix;
mat3 normalMatrix;
};
in vec3 vs_diffuse;
in vec3 vs_normal;
in vec3 vs_lightDir;
vec4 ambient = vec4(0, 0.1, 0, 1);
out vec4 fs_fragColor;
void main()
{
vec4 color = ambient;
// Make sure that these are normalized
vec3 n = normalize(vs_normal);
vec3 l = normalize(vs_lightDir);
// Calculate angle between normal and light direction
float n_dot_l = max(dot(n, l), 0.0);
// Shade based on angle
color.xyz += vs_diffuse * n_dot_l;
fs_fragColor.xyz = color.xyz;
fs_fragColor.w = 1;
}

More information about diffuse (or Lambertian) shading can be found online in several places.

Instead of having some kind of interaction we want the propeller to spin at a constant speed. For this we override the update function and increment the rotation amount each function call. We multiply our speed by the elapsed frame time so that the speed remains constant over time and is unaffected by changes in the speed at which the update function is called.

void PropellerWidget::update(const MultiWidgets::FrameInfo & frameInfo)
{
FrameBufferWidget::update(frameInfo);
m_rotation += m_speed * frameInfo.dt();
}

Lastly, we of course need to create the widgets and add them to the application.

Creating the propeller widget is simple:

// Create a propeller widget
{
auto w = MultiWidgets::create<Examples::PropellerWidget>();
app.mainLayer()->addChild(w);
w->setLocation(Nimble::Vector2(200, 200));
w->setSize(Nimble::SizeF(400, 400));
w->setResolution(Nimble::Size(1024,1024));
w->setBackgroundColor(0.f, 0.f, 0.f, 0.7f);
}

For creating the cube we also need to load and assign the textures:

{
// Create a cube widget
auto w = MultiWidgets::create<Examples::CubeWidget>();
app.mainLayer()->addChild(w);
w->setBorderWidth(10);
w->setLocation(Nimble::Vector2(500, 500));
w->setSize(Nimble::SizeF(400, 400));
w->setResolution(Nimble::Size(1024,1024));
w->setBackgroundColor(0.f, 0.f, 0.f, 0.7f);
const QString img = "logo.png";
// Set texture for each face
w->setFaceTexture(Examples::CubeWidget::FACE_POSX, img);
w->setFaceTexture(Examples::CubeWidget::FACE_NEGX, img);
w->setFaceTexture(Examples::CubeWidget::FACE_POSZ, img);
w->setFaceTexture(Examples::CubeWidget::FACE_NEGZ, img);
w->setFaceTexture(Examples::CubeWidget::FACE_POSY, img);
w->setFaceTexture(Examples::CubeWidget::FACE_NEGY, img);
}

The full source code is shown below for both the widget implementation files and the main source file.

CubeWidget.cpp:

/* Copyright (C) 2007-2013 Multi Touch Oy, Finland, http://www.multitaction.com
*
* This file is part of MultiTouch Cornerstone.
*
* All rights reserved. You may use this file only for purposes for which you
* have a specific, written permission from Multi Touch Oy.
*
*/
#include "CubeWidget.hpp"
namespace Examples
{
struct Vertex
{
Nimble::Vector3f position;
};
static const Vertex vertexData[] = {
// Front
{ Nimble::Vector3f(-1, 1, 1), Nimble::Vector2f(0,1) },
{ Nimble::Vector3f( 1, 1, 1), Nimble::Vector2f(1,1) },
{ Nimble::Vector3f(-1,-1, 1), Nimble::Vector2f(0,0) },
{ Nimble::Vector3f( 1,-1, 1), Nimble::Vector2f(1,0) },
// Left
{ Nimble::Vector3f(-1, 1,-1), Nimble::Vector2f(0,1) },
{ Nimble::Vector3f(-1, 1, 1), Nimble::Vector2f(1,1) },
{ Nimble::Vector3f(-1,-1,-1), Nimble::Vector2f(0,0) },
{ Nimble::Vector3f(-1,-1, 1), Nimble::Vector2f(1,0) },
// Right
{ Nimble::Vector3f( 1, 1,-1), Nimble::Vector2f(0,1) },
{ Nimble::Vector3f( 1, 1, 1), Nimble::Vector2f(1,1) },
{ Nimble::Vector3f( 1,-1,-1), Nimble::Vector2f(0,0) },
{ Nimble::Vector3f( 1,-1, 1), Nimble::Vector2f(1,0) },
// Back
{ Nimble::Vector3f( 1, 1,-1), Nimble::Vector2f(0,1) },
{ Nimble::Vector3f(-1, 1,-1), Nimble::Vector2f(1,1) },
{ Nimble::Vector3f( 1,-1,-1), Nimble::Vector2f(0,0) },
{ Nimble::Vector3f(-1,-1,-1), Nimble::Vector2f(1,0) },
// Bottom
{ Nimble::Vector3f(-1, 1,-1), Nimble::Vector2f(0,1) },
{ Nimble::Vector3f( 1, 1,-1), Nimble::Vector2f(1,1) },
{ Nimble::Vector3f(-1, 1, 1), Nimble::Vector2f(0,0) },
{ Nimble::Vector3f( 1, 1, 1), Nimble::Vector2f(1,0) },
// Top
{ Nimble::Vector3f(-1,-1, 1), Nimble::Vector2f(0,1) },
{ Nimble::Vector3f( 1,-1, 1), Nimble::Vector2f(1,1) },
{ Nimble::Vector3f(-1,-1,-1), Nimble::Vector2f(0,0) },
{ Nimble::Vector3f( 1,-1,-1), Nimble::Vector2f(1,0) },
};
struct Uniforms
{
Nimble::Matrix4f projMatrix;
Nimble::Matrix4f modelMatrix;
};
CubeWidget::CubeWidget()
: FrameBufferWidget(),
m_yRotate(0)
{
const QString vs("cube.vs");
const QString fs("cube.fs");
m_shader.loadShader(vs, Luminous::Shader::Vertex);
m_shader.loadShader(fs, Luminous::Shader::Fragment);
desc.addAttribute<Nimble::Vector3f>("vertex_position");
desc.addAttribute<Nimble::Vector2f>("vertex_uv");
m_shader.setVertexDescription(desc);
// update the vertex buffer
m_vbuffer.setData(vertexData, sizeof(vertexData), Luminous::Buffer::STATIC_DRAW);
m_vertexArray.addBinding(m_vbuffer, desc);
}
void CubeWidget::render3D(Luminous::RenderContext & r) const
{
// Create projection matrix
const Nimble::Matrix4f matPerspective = Nimble::Matrix4f::perspectiveProjection(90, 1, 1, 1024);
Luminous::ViewTransformGuard viewGuard(r, matPerspective);
// Create transformation matrix
const auto matModelView =
Nimble::Matrix4::makeTranslation(Nimble::Vector3f(0.f, 0.f, -4.f))
{
Luminous::TransformGuard modelViewGuard(r, matModelView);
// Clear the frame buffer
r.clear(Luminous::CLEARMASK_COLOR_DEPTH, backgroundColor());
std::map<QByteArray, const Luminous::Texture *> textures;
for (int i = 0; i < 6; ++i) {
// Render all the faces
textures["tex"] = &m_textures[i].texture();
auto b = r.render<Vertex, Uniforms>(false,
i * 4, 4, 1.f,
m_vertexArray,
m_shader, &textures);
// Fill the uniform data
b.uniform->projMatrix = r.viewTransform().transposed();
b.uniform->modelMatrix = r.transform().transposed();
}
}
}
void CubeWidget::processFingers(MultiWidgets::GrabManager & gm, const MultiWidgets::FingerArray & fa, float dt)
{
if(fa.size() == 1) {
float rotate, scale;
Nimble::Vector2 translate, screenP;
// Compute motion in screen coordinates
calculateMotion(fa, translate, scale, rotate, screenP);
// Convert translation to local coordinates
Nimble::Matrix3 transform = gm.transform3();
translate = (transform * Nimble::Vector3(translate, 1) - transform * Nimble::Vector3(0, 0, 1)).vector2();
m_yRotate += translate.x;
}
else
FrameBufferWidget::processFingers(gm, fa, dt);
}
void CubeWidget::setFaceTexture(CubeFace face, const QString & file)
{
m_textures[face].read(file);
}
}

PropellerWidget.cpp:

/* Copyright (C) 2007-2013 Multi Touch Oy, Finland, http://www.multitaction.com
*
* This file is part of MultiTouch Cornerstone.
*
* All rights reserved. You may use this file only for purposes for which you
* have a specific, written permission from Multi Touch Oy.
*
*/
#include "PropellerWidget.hpp"
#include <Luminous/ProgramGL.hpp>
namespace Examples
{
// The description for our uniform buffer
struct PropellerUniforms
{
Nimble::Matrix4f projMatrix;
Nimble::Matrix4f modelMatrix;
Nimble::Matrix4f normalMatrix;
};
// Generate the propeller vertex data
void PropellerWidget::initVertexData()
{
m_vertexData.clear();
int blades = 18;
int sections = 40;
float radius = 3.0f;
float height = 0.3f;
float twist = 1.f;
for(int i = 0; i < blades; i++) {
float rel = i / (float) blades;
float angle = rel * Nimble::Math::TWO_PI;
Nimble::Vector3 dir(cosf(angle), 0, sinf(angle));
dir.normalize(radius);
Nimble::Vector3 topcolor(0.6, 0.9, 0.6+sinf(angle)*0.4f);
Nimble::Vector3 botcolor(0.0, 0.0, 0.0);
for(int j = 0; j <= sections; j++) {
Vertex vertex0, vertex1;
float jrel = j / (float) sections;
float h = height * cosf(jrel * Nimble::Math::HALF_PI);
m.rotateAroundAxis(dir, jrel * twist);
Nimble::Vector3 offset = dir * (jrel + 0.1f);
Nimble::Vector3 turn = m * Nimble::Vector3(0, h, 0);
Nimble::Vector3 n = cross(dir, turn);
n.normalize();
vertex0.normal = n;
vertex0.color = topcolor;
vertex0.position = offset + turn;
vertex1.normal = n;
vertex1.color = botcolor;
vertex1.position = offset - turn;
// make degenerated triangles between blades
if (i > 0 && j == 0)
m_vertexData.push_back(vertex0);
m_vertexData.push_back(vertex0);
m_vertexData.push_back(vertex1);
if (i != blades-1 && j == sections)
m_vertexData.push_back(vertex1);
}
}
}
PropellerWidget::PropellerWidget()
: FrameBufferWidget(),
m_speed(this, "speed", 50.0f),
m_rotation(0)
{
// Load the shaders
const QString vs("propeller.vs");
const QString fs("propeller.fs");
m_shader.loadShader(vs, Luminous::Shader::Vertex);
m_shader.loadShader(fs, Luminous::Shader::Fragment);
// Describe the vertex buffer
desc.addAttribute<Nimble::Vector3f>("vertex");
desc.addAttribute<Nimble::Vector3f>("normal");
desc.addAttribute<Nimble::Vector3f>("diffuse");
m_shader.setVertexDescription(desc);
// Generate vertex data
initVertexData();
// Update the vertex buffer
m_vbuffer.setData(m_vertexData.data(), sizeof(Vertex) * m_vertexData.size(), Luminous::Buffer::STATIC_DRAW);
m_vertexArray.addBinding(m_vbuffer, desc);
}
void PropellerWidget::update(const MultiWidgets::FrameInfo & frameInfo)
{
FrameBufferWidget::update(frameInfo);
m_rotation += m_speed * frameInfo.dt();
}
void PropellerWidget::render3D(Luminous::RenderContext & r) const
{
// Create projection matrix
const Nimble::Matrix4f matPerspective = Nimble::Matrix4f::perspectiveProjection(90, 1, 1, 1024);
Luminous::ViewTransformGuard viewGuard(r, matPerspective);
// Create transformation matrix
const auto matModelView = Nimble::Matrix4::makeTranslation(Nimble::Vector3f(0.f, 0.f, -4.f))
{
Luminous::TransformGuard modelViewGuard(r, matModelView);
// Clear the frame buffer
r.clear(Luminous::CLEARMASK_COLOR_DEPTH, backgroundColor());
// Render the propeller mesh
auto b = r.render<Vertex, PropellerUniforms>(false,
0, m_vertexData.size(), 1.f,
m_vertexArray, m_shader);
// Fill the uniform data
b.uniform->projMatrix = r.viewTransform().transposed();
b.uniform->modelMatrix = r.transform().transposed();
b.uniform->normalMatrix = r.transform().inverse();
}
}
}

Widget3DExample.cpp:

/* Copyright (C) 2007-2013 Multi Touch Oy, Finland, http://www.multitaction.com
*
* This file is part of MultiTouch Cornerstone.
*
* All rights reserved. You may use this file only for purposes for which you
* have a specific, written permission from Multi Touch Oy.
*
*/
#include <MultiWidgets/Application.hpp>
#include "PropellerWidget.hpp"
#include "CubeWidget.hpp"
int main(int argc, char ** argv)
{
if(!app.init(argc, argv))
return 1;
// Create a propeller widget
{
auto w = MultiWidgets::create<Examples::PropellerWidget>();
app.mainLayer()->addChild(w);
w->setLocation(Nimble::Vector2(200, 200));
w->setSize(Nimble::SizeF(400, 400));
w->setResolution(Nimble::Size(1024,1024));
w->setBackgroundColor(0.f, 0.f, 0.f, 0.7f);
}
{
// Create a cube widget
auto w = MultiWidgets::create<Examples::CubeWidget>();
app.mainLayer()->addChild(w);
w->setBorderWidth(10);
w->setLocation(Nimble::Vector2(500, 500));
w->setSize(Nimble::SizeF(400, 400));
w->setResolution(Nimble::Size(1024,1024));
w->setBackgroundColor(0.f, 0.f, 0.f, 0.7f);
const QString img = "logo.png";
// Set texture for each face
w->setFaceTexture(Examples::CubeWidget::FACE_POSX, img);
w->setFaceTexture(Examples::CubeWidget::FACE_NEGX, img);
w->setFaceTexture(Examples::CubeWidget::FACE_POSZ, img);
w->setFaceTexture(Examples::CubeWidget::FACE_NEGZ, img);
w->setFaceTexture(Examples::CubeWidget::FACE_POSY, img);
w->setFaceTexture(Examples::CubeWidget::FACE_NEGY, img);
}
// Run the application:
return app.run();
}