TriMesh
Construct a triangle mesh from vec3
and Color
objects.
Namespace | Include |
---|---|
ci::TriMesh |
#include "cinder/gl/gl.h" |
Description
Superseded by
gl::Batch
.
Use the TriMesh
when explicitly defining each position and color, but take care about performance; the TriMesh
is implicitly converted to a VboMesh
which in turn converts to a gl::Batch
.
When performance is important, always use gl::Batch
.
VboMesh versus TriMesh
Source: VboMesh vs TriMesh
The difference between TriMesh
and gl::VboMesh
is similar to the difference between Surface
and gl::Texture
. The TriMesh
is designed to be an in-memory representation for software, and gl::VboMesh
is an OpenGL-specific hardware representation. The VboMesh
is the fastest option, particularly if you are not modifying your geometry.
It can be a bit tricky to get it right however - check out the VboMesh sample.
Comparing TriMesh and VboMesh
Source: VboMesh vs TriMesh
This section is not yet compatible with Cinder 0.9.x
Comparing the TriMesh and VboMesh in Cinder 16 August 2014 I’ve been playing around with Cinder trying to make a simple app for drawing isometric images with triangles. I’m still learning my way around OpenGL and the wrapper classes Cinder offers but I like what I’ve seen so far. It’s been a lot of fun doing C++ again after a long time with Ruby and JavaScript.
I kept running into a random segfault when trying to draw anything using the VboMesh so I went looking for some super basic sample code. The closest thing I found was this slightly broken example. So I spent a little time cleaning it up, then got it drawing equilateral triangles:
I started wondering if I should be using the TriMesh instead, so I decided to try implementing the drawing that way so I could compare the code. It ended up a little cleaner, but since my goal is to have colors change when you touch a triangle, I’ll probably stick with the VboMesh so I can use dynamic colors.
Here’s the code in case it’s helpful for anyone else:
// Simple demo to compare drawing 2D triangles with Cinder's VBO and TriMesh wrappers.
// https://drewish.com/2014/08/16/comparing-the-trimesh-and-vbomesh-in-cinder/
#include "cinder/app/AppBasic.h"
#include "cinder/gl/gl.h"
#include "cinder/gl/Vbo.h"
#include "cinder/Trimesh.h"
#include "cinder/params/Params.h"
using namespace ci;
using namespace ci::app;
using namespace std;
// sqrt(3) / 2
#define M_SQRT_3_2 0.8660254037844386
class MeshTestApp : public AppBasic {
public:
void prepareSettings( Settings *settings ) override;
void setup();
void recalculate();
void buildVBOMesh();
void buildTriMesh();
void draw();
gl::VboMesh mMesh;
TriMesh mTriangles;
params::InterfaceGl mParams;
int mCols = 10;
int mRows = 10;
int mScale = 30;
bool mWireframe = false;
bool useTriMesh = false;
};
void MeshTestApp::prepareSettings( Settings *settings )
{
settings->enableHighDensityDisplay();
settings->enableMultiTouch( false );
}
void MeshTestApp::setup()
{
mParams = params::InterfaceGl( "Parameters", Vec2i( 220, 170 ) );
mParams.addParam( "Scale", &mScale )
.keyIncr( "s" ).keyDecr( "S" )
.min( 1 ).max( 100 ).step( 1 )
.updateFn( [this] { recalculate(); } );
mParams.addParam( "Columns", &mCols )
.keyIncr( "d" ).keyDecr( "D" )
.min( 1 ).max( 100 ).step( 1 )
.updateFn( [this] { recalculate(); } );
mParams.addParam( "Rows", &mRows )
.keyIncr( "f" ).keyDecr( "F" )
.min( 1 ).max( 100 ).step( 1 )
.updateFn( [this] { recalculate(); } );
mParams.addParam( "Wireframe", &mWireframe )
.key( "w" );
mParams.addParam( "TriMesh/VBO", &useTriMesh )
.updateFn( [this] { recalculate(); } )
.key( "t" );
recalculate();
}
void MeshTestApp::recalculate()
{
if (useTriMesh) buildTriMesh();
else buildVBOMesh();
}
void MeshTestApp::buildVBOMesh()
{
gl::VboMesh::Layout layout;
layout.setStaticIndices();
layout.setStaticPositions();
layout.setStaticColorsRGB();
mMesh = gl::VboMesh(mCols * mRows * 3, mCols * mRows * 3, layout, GL_TRIANGLES);
vector<uint32_t> indices;
vector<Vec3f> positions;
vector<Color> colors;
float w = M_SQRT_3_2 * mScale;
float h = 1.0 * mScale;
int index = -1;
Color8u color;
for (int col = 0; col < mCols; col++) {
float x = col * 2 * w;
int direction = (col % 2) ? 1 : -1;
for (int row = 0; row < mRows; row++) {
direction *= -1;
float y = row * h;
positions.push_back( Vec3f( x - w * direction, y - h, 0 ) );
positions.push_back( Vec3f( x + w * direction, y + 0, 0 ) );
positions.push_back( Vec3f( x - w * direction, y + h, 0 ) );
// #69D2E7 and #A7DBD8
color = (direction > 0) ? Color8u( 105,210,231 ) : Color8u( 224,228,204 );
colors.push_back( color );
colors.push_back( color );
colors.push_back( color );
index += 3;
indices.push_back( index - 2 );
indices.push_back( index - 1 );
indices.push_back( index );
}
}
mMesh.bufferIndices( indices );
mMesh.bufferPositions( positions );
mMesh.bufferColorsRGB( colors );
}
void MeshTestApp::buildTriMesh()
{
float w = M_SQRT_3_2 * mScale;
float h = 1.0 * mScale;
int index = -1;
Color color;
mTriangles.clear();
for (int col = 0; col < mCols; col++) {
float x = col * 2 * w;
int direction = (col % 2) ? 1 : -1;
for (int row = 0; row < mRows; row++) {
direction *= -1;
float y = row * h;
mTriangles.appendVertex( Vec3f( x - w * direction, y - h, 0 ) );
mTriangles.appendVertex( Vec3f( x + w * direction, y + 0, 0 ) );
mTriangles.appendVertex( Vec3f( x - w * direction, y + h, 0 ) );
// #69D2E7 and #A7DBD8
color = (direction > 0) ? Color8u( 250,105,0 ) : Color8u( 105,210,231 );
mTriangles.appendColorRgb( color );
mTriangles.appendColorRgb( color );
mTriangles.appendColorRgb( color );
index += 3;
mTriangles.appendTriangle( index - 2, index - 1, index );
}
}
}
void MeshTestApp::draw()
{
gl::pushModelView();
gl::clear( Color::white() );
if (mWireframe) gl::enableWireframe();
if (useTriMesh)
gl::draw( mTriangles );
else
gl::draw( mMesh );
if (mWireframe) gl::disableWireframe();
gl::popModelView();
mParams.draw();
}
CINDER_APP_BASIC( MeshTestApp, RendererGl )
Optimisation
Source: Are Vbo binds expensive?
A TriMesh
is stored in system memory (closer to CPU) and a VboMesh is stored in device memory (closer to GPU), a VboMesh will always render faster than a TriMesh
. What's more, in order to draw a TriMesh
it is converted to VboMesh
under the hood by Cinder anyway. To avoid overhead, it's better to do this only once instead of every frame.
Binding a VboMesh
is relatively expensive because OpenGL is a state machine and it involves quite a lot of states to change. Compared to using a TriMesh
, however, it is trivial.
Step 1 of your optimizations should be to have all objects in device memory by creating a VboMesh from them. It's even better to create a gl::Batch
which combines the mesh with the shader you use to draw it. Batches are faster than a VboMesh, for the same reason a VboMesh
is faster than a TriMesh
: the VboMesh
is converted to a gl::Batch
under the hood.
Next, you might want to have a look at instancing. Are you drawing 100's of instances of the same object? It's a lot faster to render all your objects with a single draw call.
Next, make sure to apply some form of culling. Objects that are not visible (e.g. behind the camera) should not be sent to the GPU at all. Look at the frustum culling sample for more information.
Next, and a bit harder to implement, is sorting. Sort the objects from front (close to the camera) to back (further away). This avoids overdraw, where the same pixel is changed several times in a single frame.
Finally, optimize your shader to run as fast as possible. And try to do as much in the shader as possible. You say you're using a dynamic mesh. Are you sure you have to update it on the CPU? You could also use a vertex shader to deform a static mesh, which in general is much faster.
Public Members
Output | Member |
---|---|
void | appendPosition (const vec3 &position) |
void | appendPositions (const vec3 *positions, size_t num) |
void | appendColorRgb (const Color &color) |
void | appendColors (const Color *rgbs, size_t num) |
const VECDIM< DIM, float >::TYPE * | getPositions () const |
std::vector< vec3 > & | getNormals () |
std::vector< vec3 > & | getTangents () |
std::vector< vec3 > & | getBitangents () |
COLORDIM< DIM, float >::TYPE * | getColors () |
void | subdivide (int division=2, bool normalize=false) |
Examples
Basic
The shortest possible example.
#include "cinder/app/App.h"
#include "cinder/app/RendererGl.h"
#include "cinder/gl/gl.h"
using namespace ci;
using namespace ci::app;
class MyApp : public App {
public:
void setup() override;
void draw() override;
TriMesh mesh;
};
void MyApp::setup()
{
mesh = TriMesh(
TriMesh::Format()
.positions()
.colors(3)
);
mesh.appendPosition(vec3(10, 100, 0));
mesh.appendColorRgb(Color(0, 0, 0));
mesh.appendPosition(vec3(100, 100, 0));
mesh.appendColorRgb(Color(1, 0, 0));
mesh.appendPosition(vec3(100, 10, 0));
mesh.appendColorRgb(Color(1, 1, 0));
mesh.appendPosition(vec3(10, 10, 0));
mesh.appendColorRgb(Color(0, 1, 0));
mesh.appendTriangle(0, 1, 2);
mesh.appendTriangle(0, 2, 3);
}
void MyApp::draw()
{
gl::clear();
gl::draw(mesh);
}
CINDER_APP(MyApp, RendererGl, [](App::Settings *settings) {
settings->setWindowSize(240, 160);
})
With camera
The above example assumed a default camera, which only renderes triangles at z=0
. To render vertices in depth, you will need to establish a point of view.
#include "cinder/app/App.h"
#include "cinder/app/RendererGl.h"
#include "cinder/gl/gl.h"
using namespace ci;
using namespace ci::app;
using namespace std;
class MyApp : public App {
public:
void setup();
void draw();
CameraPersp mCam;
TriMesh mMesh;
};
void MyApp::setup()
{
mCam.lookAt(vec3(0, 0, 500), vec3(0));
}
void MyApp::draw()
{
gl::clear();
mMesh = TriMesh(
TriMesh::Format()
.positions()
.colors(3)
);
mMesh.appendPosition(vec3(-100, 100, 0));
mMesh.appendColorRgb(Color(0, 0, 0));
mMesh.appendPosition(vec3(100, 100, 0));
mMesh.appendColorRgb(Color(1, 0, 0));
mMesh.appendPosition(vec3(100, -100, 0));
mMesh.appendColorRgb(Color(1, 1, 0));
mMesh.appendPosition(vec3(-100, -100, 0));
mMesh.appendColorRgb(Color(0, 1, 0));
mMesh.appendTriangle(0, 1, 2);
mMesh.appendTriangle(0, 2, 3);
gl::setMatrices(mCam);
gl::pushModelView();
gl::draw(mMesh);
gl::popModelView();
}
CINDER_APP(MyApp, RendererGl, [](App::Settings *settings) {
settings->setWindowSize(360, 240);
})
Sierpinski Triangle
Source: TriMesh HelloWorld
A more interesting example involving time.
#include "cinder/app/App.h"
#include "cinder/app/RendererGl.h"
#include "cinder/gl/gl.h"
using namespace ci;
using namespace ci::app;
using namespace std;
int sierpinski(TriMesh& mesh,
const vec3& centre,
float size,
int tri_mesh_index,
int num_iterations);
class MyApp : public App {
public:
void setup();
void draw();
CameraPersp mCam;
};
void MyApp::setup()
{
mCam.lookAt(vec3(0, 0, 200), vec3(0), vec3(0, 1, 0));
}
void MyApp::draw()
{
gl::clear();
TriMesh mesh;
int iterations = (int)((1 + sin(getElapsedSeconds())) * 4);
sierpinski(mesh, vec3(0), 50, 0, iterations);
gl::setMatrices(mCam);
gl::pushModelView();
gl::draw(mesh);
gl::popModelView();
}
CINDER_APP(MyApp, RendererGl, [](App::Settings *settings) {
settings->setWindowSize(640, 480);
})
/** Sierpinski Triangle
*
* Reference:
* https://en.wikipedia.org/wiki/Sierpinski_triangle
*
*/
int sierpinski(TriMesh& mesh,
const vec3& centre,
float size,
int tri_mesh_index,
int num_iterations)
{
if(num_iterations > 0)
{
num_iterations -= 1;
float new_size = size * 0.5f;
tri_mesh_index = sierpinski(mesh,
centre + vec3(0, 1, 0) * new_size,
new_size,
tri_mesh_index,
num_iterations);
tri_mesh_index = sierpinski(mesh,
centre + vec3(-1, -1, 0) * new_size,
new_size,
tri_mesh_index,
num_iterations);
tri_mesh_index = sierpinski(mesh,
centre + vec3(1, -1, 0) * new_size,
new_size,
tri_mesh_index,
num_iterations);
return tri_mesh_index;
}
else
{
mesh.appendPosition(centre + vec3(0, 1, 0) * size);
mesh.appendPosition(centre + vec3(-1, -1, 0) * size);
mesh.appendPosition(centre + vec3(1, -1, 0) * size);
mesh.appendTriangle(tri_mesh_index,
tri_mesh_index + 1,
tri_mesh_index + 2);
return tri_mesh_index + 3;
}
}
3D
This is how you can draw triangles with depth.
#include "cinder/app/App.h"
#include "cinder/app/RendererGl.h"
#include "cinder/gl/gl.h"
using namespace ci;
using namespace ci::app;
using namespace std;
class MyApp : public App {
public:
void setup();
void draw();
CameraPersp mCam;
TriMesh mMesh;
};
void MyApp::setup()
{
gl::enableDepthWrite();
gl::enableDepthRead();
mCam.lookAt(vec3(250, 200, 500), vec3(0));
}
void MyApp::draw()
{
gl::clear();
mMesh = TriMesh(
TriMesh::Format()
.positions()
.colors(3)
);
// Create the points of our cube
vec3 v0 { -100, -100, -100 };
vec3 v1 { 100, -100, -100 };
vec3 v2 { 100, 100, -100 };
vec3 v3 { -100, 100, -100 };
vec3 v4 { -100, -100, 100 };
vec3 v5 { 100, -100, 100 };
vec3 v6 { 100, 100, 100 };
vec3 v7 { -100, 100, 100 };
// Create the colors for each vertex
Color c0 { 0, 0, 0 };
Color c1 { 1, 0, 0 };
Color c2 { 1, 1, 0 };
Color c3 { 0, 1, 0 };
Color c4 { 0, 0, 1 };
Color c5 { 1, 0, 1 };
Color c6 { 1, 1, 1 };
Color c7 { 0, 1, 1 };
vec3 faces[6][4] = { /* Vertices for the 6 faces of a cube. */
{v0, v1, v2, v3}, {v3, v2, v6, v7}, {v7, v6, v5, v4},
{v4, v5, v1, v0}, {v5, v6, v2, v1}, {v7, v4, v0, v3}
};
Color colors[6][4] = { /* colors for each vertex of the cube. */
{c0, c1, c2, c3}, {c3, c2, c6, c7}, {c7, c6, c5, c4},
{c4, c5, c1, c0}, {c5, c6, c2, c1}, {c7, c4, c0, c3}
};
for (int i = 0; i < 6; i++)
{
mMesh.appendPosition(faces[i][0]);
mMesh.appendColorRgb(colors[i][0]);
mMesh.appendPosition(faces[i][1]);
mMesh.appendColorRgb(colors[i][1]);
mMesh.appendPosition(faces[i][2]);
mMesh.appendColorRgb(colors[i][2]);
mMesh.appendPosition(faces[i][3]);
mMesh.appendColorRgb(colors[i][3]);
int numberVertices = mMesh.getNumVertices();
mMesh.appendTriangle(numberVertices - 4,
numberVertices - 3,
numberVertices - 2);
mMesh.appendTriangle(numberVertices - 4,
numberVertices - 2,
numberVertices - 1);
}
gl::setMatrices(mCam);
gl::pushModelView();
gl::draw(mMesh);
gl::popModelView();
}
CINDER_APP(MyApp, RendererGl, [](App::Settings *settings) {
settings->setWindowSize(360, 240);
})
Textures
#include "cinder/app/App.h"
#include "cinder/app/RendererGl.h"
#include "cinder/gl/gl.h"
using namespace ci;
using namespace ci::app;
class MyApp : public App {
public:
void setup();
void draw();
CameraPersp mCam;
TriMesh mMesh;
gl::Texture2dRef mTex;
gl::GlslProgRef mGlsl;
};
void MyApp::setup()
{
gl::enableDepthWrite();
gl::enableDepthRead();
auto image = loadImage(loadUrl("https://www.cs.cmu.edu/~chuck/lennapg/len_std.jpg"));
mTex = gl::Texture2d::create(image);
mCam.lookAt(vec3(250, 200, 500), vec3(0));
}
void MyApp::draw()
{
gl::clear();
mMesh = TriMesh(
TriMesh::Format()
.positions()
.texCoords(2)
);
// Create the points of our cube
vec3 v0 { -100, -100, -100 };
vec3 v1 { 100, -100, -100 };
vec3 v2 { 100, 100, -100 };
vec3 v3 { -100, 100, -100 };
vec3 v4 { -100, -100, 100 };
vec3 v5 { 100, -100, 100 };
vec3 v6 { 100, 100, 100 };
vec3 v7 { -100, 100, 100 };
// Create the colors for each vertex
Color c0 { 0, 0, 0 };
Color c1 { 1, 0, 0 };
Color c2 { 1, 1, 0 };
Color c3 { 0, 1, 0 };
Color c4 { 0, 0, 1 };
Color c5 { 1, 0, 1 };
Color c6 { 1, 1, 1 };
Color c7 { 0, 1, 1 };
// Create the texture coordinates for each vertex
vec2 t0 { 0, 0 };
vec2 t1 { 1, 0 };
vec2 t2 { 1, 1 };
vec2 t3 { 0, 1 };
vec3 faces[6][4] = { /* Vertices for the 6 faces of a cube. */
{v0, v1, v2, v3}, {v3, v2, v6, v7}, {v7, v6, v5, v4},
{v4, v5, v1, v0}, {v5, v6, v2, v1}, {v7, v4, v0, v3}
};
for (int i = 0; i < 6; i++)
{
mMesh.appendPosition(faces[i][0]);
mMesh.appendTexCoord(t0);
mMesh.appendPosition(faces[i][1]);
mMesh.appendTexCoord(t1);
mMesh.appendPosition(faces[i][2]);
mMesh.appendTexCoord(t2);
mMesh.appendPosition(faces[i][3]);
mMesh.appendTexCoord(t3);
int numberVertices = mMesh.getNumVertices();
mMesh.appendTriangle(numberVertices - 4,
numberVertices - 3,
numberVertices - 2);
mMesh.appendTriangle(numberVertices - 4,
numberVertices - 2,
numberVertices - 1);
}
gl::setMatrices(mCam);
// Apply shader and texture
gl::ScopedGlslProg glslScope { gl::getStockShader(gl::ShaderDef().texture()) };
gl::ScopedTextureBind texScope { mTex };
gl::draw(mMesh);
}
CINDER_APP(MyApp, RendererGl, [](App::Settings *settings) {
settings->setWindowSize(640, 480);
})