This example demonstrates how to create a flat plane from triangles and then oscillate its vertices to obtain a dynamic surface. Also, the color of vertices will depend on the oscillation amplitude.
The example is based on the emptyExample
project in openFrameworks. Begin with adding the declaration and definition of the setNormals()
function, as described in the Computing normals using the setNormals() function section. Then in the testApp.h
file, in the testApp
class declaration, add definitions of mesh
and light
:
ofMesh mesh; //Mesh ofLight light; //Light
In the beginning of the testApp.cpp
file, add constants with vertex grid size:
int W = 100; //Grid size int H = 100;
The setup()
function adds vertices and triangles to the mesh and enables lighting:
void testApp::setup(){ //Set up vertices and colors for (int y=0; y<H; y++) { for (int x=0; x<W; x++) { mesh.addVertex( ofPoint( (x - W/2) * 6, (y - H/2) * 6, 0 ) ); mesh.addColor( ofColor( 0, 0, 0 ) ); } } //Set up triangles' indices for (int y=0; y<H-1; y++) { for (int x=0; x<W-1; x++) { int i1 = x + W * y; int i2 = x+1 + W * y; int i3 = x + W * (y+1); int i4 = x+1 + W * (y+1); mesh.addTriangle( i1, i2, i3 ); mesh.addTriangle( i2, i4, i3 ); } } setNormals( mesh ); //Set normals light.enable(); //Enable lighting }
The update()
function changes the z coordinate of each vertex using Perlin noise (refer to Appendix B, Perlin Noise) and also sets its color between the range blue to white:
void testApp::update(){ float time = ofGetElapsedTimef(); //Get time //Change vertices for (int y=0; y<H; y++) { for (int x=0; x<W; x++) { int i = x + W * y; //Vertex index ofPoint p = mesh.getVertex( i ); //Get Perlin noise value float value = ofNoise( x * 0.05, y * 0.05, time * 0.5 ); //Change z-coordinate of vertex p.z = value * 100; mesh.setVertex( i, p ); //Change color of vertex mesh.setColor( i, ofColor( value*255, value * 255, 255 ) ); } } setNormals( mesh ); //Update the normals }
The draw()
function draws the surface and slowly rotates it:
void testApp::draw(){ ofEnableDepthTest(); //Enable z-buffering //Set a gradient background from white to gray ofBackgroundGradient( ofColor( 255 ), ofColor( 128 ) ); ofPushMatrix(); //Store the coordinate system //Move the coordinate center to screen's center ofTranslate( ofGetWidth()/2, ofGetHeight()/2, 0 ); //Calculate the rotation angle float time = ofGetElapsedTimef(); //Get time in seconds float angle = time * 20; //Compute angle. We rotate at speed //20 degrees per second ofRotate( 30, 1, 0, 0 ); //Rotate coordinate system ofRotate( angle, 0, 0, 1 ); //Draw mesh //Here ofSetColor() does not affects the result of drawing, //because the mesh has its own vertices' colors mesh.draw(); ofPopMatrix(); //Restore the coordinate system }
Run the example and you will see a pulsating surface that slowly rotates on the screen:
Now replace in the testApp::draw()
function in the line mesh.draw();
by the following line:
mesh.drawWireframe();
Now, run the project and you will see the wireframe structure of the surface.
Until now you knew how to create simple animated smooth surfaces and disconnected clouds of primitives. Let's consider an advanced example of constructing a smooth surface that grows and twists in space.
In this example we will create a tube-like surface, that is formed from a number of deformed circles. At each update()
call, we will generate one circle and connect it with the previous circle by adding triangles to the surface. At each step the circle will slowly move, rotate, and deform in space. As result, we will see a growing and twisting 3D knot.
The example is based on the emptyExample
project in openFrameworks. Begin with adding declaration and definition of the setNormals()
function, as is described in the Computing normals using the setNormals() function section. Then in the testApp.h
file, in the testApp
class declaration, add definitions of the mesh
, light
, and addRandomCircle()
function:
ofMesh mesh; //Mesh ofLight light; //Light void addRandomCircle( ofMesh &mesh ); //Main function which //moves circle and adds triangles to the object
In the beginning of the testApp.cpp
file, add the constants and the variables for the circle that will be used for knot generation:
//The circle parameters float Rad = 25; //Radius of circle float circleStep = 3; //Step size for circle motion int circleN = 40; //Number of points on the circle //Current circle state ofPoint pos; //Circle center ofPoint axeX, axyY, axyZ; //Circle's coordinate system
The setup()
function sets the initial values of the circle's position and also enables lighting with light
, using its default settings:
void testApp::setup(){ pos = ofPoint( 0, 0, 0 ); //Start from center of coordinate axeX = ofPoint( 1, 0, 0 ); //Set initial coordinate system axyY = ofPoint( 0, 1, 0 ); axyZ = ofPoint( 0, 0, 1 ); light.enable(); //Enable lighting ofSetFrameRate( 60 ); //Set the rate of screen redrawing }
The update()
function just calls the addRandomCircle()
function, which adds one more circle to the knot:
void testApp::update(){ addRandomCircle( mesh ); }
The draw()
function draws the mesh on the screen. Note that we use the mesh.getCentroid()
function, which returns the center of mass of mesh's vertex array. In other words, we apply it for the shift coordinate system ofTranslate( -mesh.getCentroid() )
, which helps us to draw our object positioned in the center :
void testApp::draw(){ ofEnableDepthTest(); //Enable z-buffering //Set a gradient background from white to gray ofBackgroundGradient( ofColor( 255 ), ofColor( 128 ) ); ofPushMatrix(); //Store the coordinate system //Move the coordinate center to screen's center ofTranslate( ofGetWidth()/2, ofGetHeight()/2, 0 ); //Calculate the rotation angle float time = ofGetElapsedTimef(); //Get time in seconds float angle = time * 20; //Compute the angle. //We rotate at speed 20 degrees per second ofRotate( angle, 0, 1, 0 ); //Rotate the coordinate system //along y-axe //Shift the coordinate center so the mesh //will be drawn in the screen center ofTranslate( -mesh.getCentroid() ); //Draw the mesh //Here ofSetColor() does not affects the result of drawing, //because the mesh has its own vertices' colors mesh.draw(); ofPopMatrix(); //Restore the coordinate system }
The most important function in the example is addRandomCircle()
. It pseudorandomly moves the circle, adds new vertices from the circle to the object's vertex array, and adds corresponding triangles to the object. It also sets colors for the new vertices.
void testApp::addRandomCircle( ofMesh &mesh ){ float time = ofGetElapsedTimef(); //Time //Parameters – twisting and rotating angles and color float twistAngle = 5.0 * ofSignedNoise( time * 0.3 + 332.4 ); float rotateAngle = 1.5; ofFloatColor color( ofNoise( time * 0.05 ), ofNoise( time * 0.1 ), ofNoise( time * 0.15 )); color.setSaturation( 1.0 ); //Make the color maximally //colorful //Rotate the coordinate system of the circle axeX.rotate( twistAngle, axyZ ); axyY.rotate( twistAngle, axyZ ); axeX.rotate( rotateAngle, axyY ); axyZ.rotate( rotateAngle, axyY ); //Move the circle on a step ofPoint move = axyZ * circleStep; pos += move; //Add vertices for (int i=0; i<circleN; i++) { float angle = float(i) / circleN * TWO_PI; float x = Rad * cos( angle ); float y = Rad * sin( angle ); //We would like to distort this point //to make the knot's surface embossed float distort = ofNoise( x * 0.2, y * 0.2, time * 0.2 + 30 ); distort = ofMap( distort, 0.2, 0.8, 0.8, 1.2 ); x *= distort; y *= distort; ofPoint p = axeX * x + axyY * y + pos; mesh.addVertex( p ); mesh.addColor( color ); } //Add the triangles int base = mesh.getNumVertices() - 2 * circleN; if ( base >= 0 ) { //Check if it is not the first step //and we really need to add the triangles for (int i=0; i<circleN; i++) { int a = base + i; int b = base + (i + 1) % circleN; int c = circleN + a; int d = circleN + b; mesh.addTriangle( a, b, d ); //Clock-wise mesh.addTriangle( a, d, c ); } //Update the normals setNormals( mesh ); } }
Run the example and you will see a growing and twisting knot, as shown in the following screenshot:
Note that we control the rate of testApp::update()
callings (and hence the addRandomCircle()
rate) using the ofSetFrameRate( 60 )
call in testApp::setup()
. If you change the rate, say to ofSetFrameRate( 30 )
, you will obtain a differently shaped knot. To make the resultant shape independent of frame rate, you should make the circleStep
parameter dependent on the time between current and previous frames.
At each update()
call, the application constantly adds new vertices and triangles to the object. Then it recalculates all the normals, though many of the triangles did not change. So application performance will degrade with time because the setNormals()
function will take more and more computing power. To solve this problem, you can optimize the setNormals()
function so it does not recalculate the unchanged normals and does not check the old triangles at all.