How to create a basic mesh dynamically?

Hello! I am new to Cinder and have gone through the official OpenGL in Cinder tutorials here: https://libcinder.org/docs/guides/opengl/index.html So that is about how much I know about OpenGL in Cinder today.

I would like to learn how to create and draw a basic mesh dynamically, like this:

  1. Define 3 new vertex positions and then define a new triangle polygon from them.
  2. Dynamically define another vertex and dynamically define another triangle,
    using the new vertex and two of the existing vertices.
  3. Delete one or all triangles or vertices in this mesh.

How hard can it be? :yum: In plain C++ I would just create two Lists → Vertices & Triangles and add or remove members to them as I needed.

But what is the right way to do this in OpenGL/Cinder? Shall I create an VboMesh, TriMesh or something else, what is the principle? Where can I find the best up to date information about creating and editing meshes dynamically?

My final goal is to create an experimental modelling/animation tool, so performance and non-linearity is both important for me.

Thanks in advance!
/ Svante

TriMesh is to gl::VboMesh as Surface is to gl::Texture. They are CPU side representations of GPU data, with added conveniences. You’re free to assemble meshes via TriMesh, Source, gl::VboMesh directly, or you can create your own Vbo/Ibos manually if you’ve got exotic layouts. gl::VertBatch emulates old immediate mode / fixed function GL if performance isn’t of the utmost concern. So many ways to go.

If you’re building a modelling tool you might wind up with wacky data structures like halfedges etc which don’t have direct cinder analogues so your hand might be forced to go lower level by that limitation.

As per usual, the best examples are often from the cinder source itself. Here’s how they populate a cube mesh, for example.

2 Likes

Thanks for guiding me here Lithium! :+1:

Yes there are many ways to go, from what you describe I think creating my own Vbo/Ibo:s might be the best option for me.

With lower level do you mean using plain OpenGL instead of Cinder? I do want to create meshes with quads, but I imagine that I always can/must convert quads into triangles when sending the mesh to the GPU?

I have found a really good guide about mesh creation in Cinder that I will read trough, here:
Mesh Creation In Cinder
:nerd_face:

Ok so here is my try to write a small example on creating a basic mesh dynamically.
I have done it by reading what I could find about the topic and a lot of experimenting.
Please let me know if I have done some mistakes in my asumptions here, thanks!

Also, how can I get the camera to mimic the result/view from setMatricesWindow( getWindowSize() ) ?

#include "cinder/app/App.h"
#include "cinder/app/RendererGl.h"
#include "cinder/gl/gl.h"

#include "cinder/CinderImGui.h" 

using namespace ci;
using namespace ci::app;

/*
    Creates a basic Mesh as a TriMesh or a VboMesh.
    -----------------------------------------------

    1)  Defines 3 vertex positions and then defines a triangle polygon from them.

    2)  A Left Mouse Click -> Creates another vertex and another triangle,
        using the new vertex and two of the existing vertices.

    3)  A Right Mouse Click deletes the second triangle and the last vertex in this mesh.
*/

class BasicMeshApp : public App {
public:

    void setup() override;
    void update() override;
    void draw() override;

    void mouseDown(MouseEvent event) override;
    void resize() override;

    void buildTriMesh();
    void buildVboMesh();

    vec2 mousePos;

    std::list <vec3> mousePoints; // List with XYZ positions 

    bool useTriMesh = false;
    bool useVboMesh = false;
    bool useAnimation = true;
    bool useWireframe = false;

    // TriMesh
    TriMesh mTriMesh;  // Handle to the TriMesh 

    /// VboMesh
    gl::VboMeshRef	        mVboMesh;   // Reference to the VboMesh 
    std::vector<vec3>       positions;  // Vector(List) for XYZ positions
    std::vector<ColorA>     colors;     // Vector(List) for colors
    std::vector<uint32_t>   indices;    // Vector(List) for indices (to create polygons)    ( uint32_t is a fixed width integer type )
    gl::VboRef              indexBufferObj; // Reference to a Vertex Buffer to store the indices
    gl::BatchRef	        batchVbo;   // Reference to the Batch

};

void BasicMeshApp::resize() {

    // Rebuild the mesh just to fit the window...
    buildTriMesh();  // Create the TriMesh
    buildVboMesh();  // Create the VboMesh

    update(); // Keep drawing the ImGui interface while resizing
}

void BasicMeshApp::mouseDown(MouseEvent event)
{
    // Add or remove points:
    if (event.isLeft()) { // Left mouse-click?

        mousePoints.clear(); // Clear the list (Just to keep it simple)

        // Add a new point to the list:
        mousePoints.push_front(vec3(event.getPos().x, event.getPos().y, 0));
    }
    else if (event.isRight()) { // Right mouse-click?

        if (mousePoints.size() > 0) {          // If there still points in the List...

            // Remove one point from the list:
            mousePoints.pop_front();
        }
    }

    buildTriMesh(); // Recreate the mesh
    buildVboMesh(); // Recreate the mesh

}

void BasicMeshApp::buildTriMesh()
{
    // Remove old data
    mTriMesh.clear();

    // Setup the TriMesh format: (Choose the attributes that you will need)
    mTriMesh = TriMesh(

        TriMesh::Format()   // This TriMesh shall contain:
        .positions()        // Position
        .colors(3)          // RGB Color      ??  Couldn't use .colors(4)  Why...?  
    );

    // *** Build the first triangle polygon ***
    
    // Vertex 1
    mTriMesh.appendPosition(vec3(getWindowCenter().x, getWindowCenter().y -20, 0)); // Position
    mTriMesh.appendColorRgb(Color(0, 0, 1));                                        // Color      ??  Couldn't use  ColorA(0, 0, 1, 0.5) Why...? 

    // Vertex 2
    mTriMesh.appendPosition(vec3(0, 0, 0));
    mTriMesh.appendColorRgb(Color(0, 1, 1));

    // Vertex 3
    mTriMesh.appendPosition(vec3(getWindowWidth(), 0, 0));
    mTriMesh.appendColorRgb(Color(1, 0, 1));

    // Create the first triangle polygon (from the 3 first vertices/indicies)
    mTriMesh.appendTriangle(0, 1, 2); // Use indices/vertices 1,2,3

    // *** Build the second triangle polygon ***

    if (mousePoints.size() > 0) { // If there have been drawn an extra vertex... 

        // Vertex 4 (Takes the position from the list with mouse-clicks)
        mTriMesh.appendPosition(vec3(mousePoints.front().x, mousePoints.front().y, 0)); 
        mTriMesh.appendColorRgb(Color(0.5, 0.5, 0.5));

        // Create the second triangle polygon (from the 2 first vertices and the new 4:th vertex)
        mTriMesh.appendTriangle(0, 1, 3); // Use indices/vertices 1,2 and 4
    }
}

void BasicMeshApp::buildVboMesh() {

    // Clear all previous mesh data: <-- (Just to keep it simple)
    positions.clear();
    colors.clear();
    indices.clear();

    // Define a VboMesh Layout: (Choose the attributes that you will need)
    std::vector<gl::VboMesh::Layout> layout = {
        gl::VboMesh::Layout().usage(GL_DYNAMIC_DRAW).attrib(geom::Attrib::POSITION, 3), // *Dynamic* positions (Will be animated)
        gl::VboMesh::Layout().usage(GL_STATIC_DRAW).attrib(geom::Attrib::COLOR, 4)      // *Static* Colors     (Will not animate)
                                                // .attrib(geom::TEX_COORD_0, 2)
                                                // .attrib(geom::NORMAL, 3)
    };

    // *** Build the first triangle polygon ***
   
    // Vertex 1
    positions.push_back(vec3(getWindowCenter().x, getWindowCenter().y +20, 0)); // Add a vertex position element to the positions vector/List
    colors.push_back(ColorA(1, 0, 0, 1));                                       // Add a vertex color element to the color vector/List
    indices.push_back(0); // Add first index for the first triangle polygon
    
    // Vertex 2
    positions.push_back(vec3(ci::app::getWindowWidth(), ci::app::getWindowHeight(), 0));
    colors.push_back(ColorA(0.5, 0.5, 0, 1));
    indices.push_back(1); // Second index for the first triangle polygon

    // Vertex 3
    positions.push_back(vec3(0, ci::app::getWindowHeight(), 0));
    colors.push_back(ColorA(0, 0.5, 0.5, 1));
    indices.push_back(2); // Last index for the first triangle polygon

    // *** Build the second triangle polygon ***
    
    if (mousePoints.size() > 0) { // If there have been drawn an extra vertex

        // Vertex 4 (Takes the position from the list with mouse-clicks)
        positions.push_back( vec3( mousePoints.front().x, mousePoints.front().y, 0));  
        colors.push_back(ColorA(0.5, 0.5, 0.5, 1));

        // Create the second triangle polygon from these indices:
        indices.push_back(0); // First index for the second polygon/triangle (Vertex 1) (Using same position as vertex 1)
        indices.push_back(1); // Second index for the second polygon/triangle (Vertex 2) (Using same position as vertex 2)
        indices.push_back(3); // Last index for the second polygon/triangle (Vertex 4) (Using same position as vertex 4, wich was the mouse click)
    }
 
    // Allocate a Vertex Buffer to store the indices on the GPU (So the Vbo-mesh knows from wich vertices it should build its triagles) 
    indexBufferObj = gl::Vbo::create(GL_ELEMENT_ARRAY_BUFFER, indices, GL_STATIC_DRAW); //            ?  GL_STATIC_DRAW  ? GL_DYNAMIC_DRAW        GL_ARRAY_BUFFER

    // Create the Vbo-mesh and pass the index-BufferObject  
    mVboMesh = gl::VboMesh::create(positions.size(), GL_TRIANGLES, { layout }, indices.size(), GL_UNSIGNED_INT, indexBufferObj);  
                                                  // GL_LINES GL_TRIANGLES GL_POINTS             GL_UNSIGNED_SHORT

    mVboMesh->bufferAttrib<vec3>(geom::POSITION, positions); // Hook up the positions vector/List to the VBO-mesh
    mVboMesh->bufferAttrib<ColorA>(geom::COLOR, colors);     // Hook up the color vector/List to the VBO-mesh

    batchVbo = gl::Batch::create(mVboMesh, gl::getStockShader(gl::ShaderDef().color())); // Create a (faster) Batch object from the Vbo-mesh (A Batch combines the mesh with the shader)
    //batchVbo = gl::Batch::create(mVboMesh, gl::getStockShader( gl::ShaderDef().lambert().color()) ); // Alternative batch that use the built in Lambert shader
}

void BasicMeshApp::setup()
{
    ImGui::Initialize();

    gl::pointSize(6);

    buildTriMesh();  // Create the TriMesh
    buildVboMesh();  // Create the VboMesh
}

void BasicMeshApp::update() 
{
    // ImGui interface
    ImGui::Begin("Settings - Mesh");
    ImGui::SetWindowPos(ImVec2(10, 10), ImGuiCond_FirstUseEver); 
    ImGui::SetWindowSize(ImVec2(190, 140), ImGuiCond_FirstUseEver);
    static int e = 0;
    ImGui::RadioButton("Use TriMesh (CPU-mem)", &e, 0);
    ImGui::RadioButton("Use VboMesh (GPU-mem)", &e, 1);
    if (e == 0)
    {
        useTriMesh = true;
        useVboMesh = false;
    }
    else 
    {
        useTriMesh = false;
        useVboMesh = true;
    }
    ImGui::NewLine();
    ImGui::Checkbox("Animate Vbo", &useAnimation);
    ImGui::Checkbox("Wireframe", &useWireframe);
    ImGui::End();

}

void BasicMeshApp::draw()
{
    gl::clear();

    // reset the matrices
    gl::setMatricesWindow(getWindowSize());

   /* CameraPersp cam; 
      cam.lookAt(vec3(0, 0, -900), vec3(0));  // ?? How to "flip/mirror" the cam to mimic the result from gl::setMatricesWindow(getWindowSize()) ??
      gl::setMatrices(cam); */

    if (useWireframe) gl::enableWireframe();
    else gl::disableWireframe();

    // Draw the TriMesh 
    if (useTriMesh)
    {
        gl::draw(mTriMesh); // Draw the TriMesh
    }

    // Draw the VboMesh 
    if (useVboMesh)
    {
        if (useAnimation) // First animate the Vbo mesh vertex positions 
        {
            // Get a Position Attribute reference to the GPU buffer
            auto mappedPosAttrib = mVboMesh->mapAttrib3f(geom::Attrib::POSITION, false);

            // Add animated displacement to the positionAttribute for the first vertex position in the buffer
            mappedPosAttrib->y = getWindowCenter().y + sin(getElapsedSeconds() *8) *10; 
            mappedPosAttrib->x = getWindowCenter().x + cos(getElapsedSeconds() *8) *10;

            mappedPosAttrib.unmap();
        }

        // gl::draw(mVboMesh); // Draw the VboMesh 
        batchVbo->draw();      // Draw the VboMesh (faster) with a "Batch"
    }

    // Draw text
    if (useWireframe) gl::disableWireframe(); // (Turn off wireframe now while drawing text)
    if (mousePoints.size() == 0) gl::drawString("Left click to: \nAdd a vertex", vec3(20, getWindowCenter().y -20, 0), ColorA(1, 1, 1, 0.5), Font("verdana", 20.f));
    if (mousePoints.size() >= 1) gl::drawString("Left click to: \nSet a new position", vec3(20, getWindowCenter().y - 20, 0), ColorA(1, 1, 1, 0.5), Font("verdana", 20.f));
    if (mousePoints.size() >= 1)gl::drawStringCentered("Right click to: \nRemove the vertex", vec3(getWindowWidth()-100, getWindowCenter().y - 20, 0), ColorA(1, 1, 1, 0.5), Font("verdana", 20.f));
    if (useTriMesh) gl::drawStringCentered("TriMesh", vec3(getWindowCenter().x, 20, 0), Color(1, 1, 1), Font("verdana", 50.f));
    if (useVboMesh) gl::drawStringCentered("VboMesh", vec3(getWindowCenter().x, getWindowHeight() -50, 0), Color(1, 1, 1), Font("verdana", 50.f));

    gl::color(Color(1, 1, 0.0)); // Set a yellow draw color

    // Draw Points (from the mousePoints list)
    gl::begin( GL_POINTS );

    for each( vec3 point in mousePoints ) {

        gl::vertex( point ); // Draw each point in the list
    }
     
    /*
    // Draw all points in the TriMesh
    if (useTriMesh) 
    {
        vec3* allVertexPositions;
        allVertexPositions = mTriMesh.getPositions<3>();
        for (int i = 0; i < mTriMesh.getBufferPositions().size() / 3; i++)
        {
            gl::vertex(allVertexPositions->x, allVertexPositions->y, 0); // Draw this vertex position as a Point
            allVertexPositions++;
        }
    }

    // Draw all points in the VboMesh
    if (useVboMesh)
    {
        gl::VboMesh::MappedAttrib allVertexPositions = mVboMesh->mapAttrib3f(geom::Attrib::POSITION, false);
        for (int i = 0; i < mVboMesh->getNumVertices() ; i++) 
        { 
            gl::vertex(allVertexPositions->x, allVertexPositions->y, 0); // Draw this vertex position as a Point
            allVertexPositions++;
        }
        allVertexPositions.unmap();
    }*/

    gl::end();

}

CINDER_APP( BasicMeshApp, RendererGl( RendererGl::Options().msaa( 0 ) ) )



Assuming you mean 2D, the best way is to use a CameraOrtho as opposed to a CameraPersp. If you mean you want a 3d perspective camera that is placed such that it’s 1 to 1 between camera and screen space, then you could use something like:

float w = app::getWindowWidth ( );
float h = app::getWindowHeight ( ));
float cameraDistance = std::sqrtf( h * h - 0.25f * h * h );
            
camera = CameraPersp  ( w, h, 60.0f, 0.01f, 1000.0f );
camera.lookAt ( vec3 ( 0, 0, cameraDistance ), vec3 ( 0 ) );

1 Like

Thanks again for your help Lithium! :sunglasses: :+1:

I have modified your example slightly to fit my scene and it works very well.

Though I still think I do something wrong here: everything gets ‘flipped and mirrored’ :upside_down_face: when I use setMatrices(camera) so then I have to invert the FOV to make it look right. That is most likely not the correct way to do it? :blush:The ‘flipping problem’ comes right after using use setMatrices(camera), so maybe I just should use the camera in another way?

This is what I have now, it works great! But what basic problem am I working my way around when I inverting the FOV ?:

    float w = app::getWindowWidth();
    float h = app::getWindowHeight();
    float cameraDistance = std::sqrtf(h * h - 0.25f * h * h); // sqrtf((Height*Height - 0.25) * (Height*Height)) ? Some clever way to move back proportional to the screen height

    CameraPersp camera = CameraPersp(w, h, -60.0f, 0.01f, -1000.0f);  
    camera.lookAt(vec3(getWindowCenter().x, getWindowCenter().y, -cameraDistance), vec3(getWindowCenter().x, getWindowCenter().y, 0));

    // Makes everything 'flipped, mirrored and upside down'? 
    // So I have to invert the FOV, farPlane and the cameraDistance to "fix" it, flipping the FOV seems wrong so I most likely do something wrong?
    gl::setMatrices(camera); // I guess.. this is how.. I can "select the camera"? (to view the scene through it?) 

    // Rotating the whole scene on Y
    gl::rotate( sin(getElapsedSeconds())-1 , vec3(0, 1, 0));

You shouldn’t need to do that, you can provide an up vector to the lookAt function.

static vec3 kUp = vec3 ( 0, 1, 0 );
CameraPersp camera = CameraPersp ( w, h, 60.0f, 0.01f, 1000.0f );
camera.lookAt ( vec3 ( 0, 0, cameraDistance ), vec3 ( 0 ), kUp );

Useful information! :+1: I still have to use a negative Y up-vector, and a negative farPlane I also have to invert the cameraDistance to make the scene look like the initial setMatricesWindow( getWindowSize() ). But I guess that is normal, right?

    static vec3 kUp = vec3(0, -1, 0);
    CameraPersp camera = CameraPersp(w, h, 60.0f, 0.01f, -1000.0f );
    camera.lookAt(vec3(getWindowCenter().x, getWindowCenter().y, -cameraDistance), vec3(getWindowCenter().x, getWindowCenter().y, 100), kUp );