Now that we've gotten comfortable with transformations and basic drawing, let's look at how to work in 3D. If you explore some of the
Let's see what happens if we simply swap in a call to
#include "cinder/app/App.h"
#include "cinder/app/RendererGl.h"
#include "cinder/gl/gl.h"
using namespace ci;
using namespace ci::app;
class BasicApp : public App {
public:
void draw() override;
};
void BasicApp::draw()
{
gl::clear();
// this will work, right?
gl::drawCube( vec3(), vec3( 2 ) );
}
CINDER_APP( BasicApp, RendererGl )
Not quite what we'd hoped for. As you may recall, by default Cinder sets things up so that the coordinate systems match the (3, 3, 3)
and have it look at the origin (0, 0, 0)
, we'll use
A very valuable tool for understanding the
After creating a Model
matrix discussed previously, Cinder maintains a View
matrix and a Projection
matrix. While the Model
matrix is used to position an object in the world (more formally the object to world-space transformation), the View
matrix is used to position our virtual "eye" in the world (the world to view-space transformation). We are manipulating a View matrix when we call routines like View
and Projection
matrices are set.
void BasicApp::draw()
{
gl::clear();
CameraPersp cam;
cam.lookAt( vec3( 3, 3, 3 ), vec3( 0 ) );
gl::setMatrices( cam );
gl::drawCube( vec3(), vec3( 2 ) );
}
While it lacks the shading that we normally associate with 3D, the result is the distinctive outline of a cube. We'll look at how to improve its look soon, and some of the details of how these matrices interact with convenience methods like
console() << gl::getProjectionMatrix() << std::endl;
Output:
[[ 1.299, 0.000, 0.000, 0.000]
[ 0.000, 1.732, 0.000, 0.000]
[ 0.000, 0.000, -1.002, -2.002]
[ 0.000, 0.000, -1.000, 0.000]]
Let's add some simple Lambert shading to the example. We'll get into shaders in more detail later, but for the time being we'll use
#include "cinder/gl/Shader.h"
…
void BasicApp::draw()
{
gl::clear();
CameraPersp cam;
cam.lookAt( vec3( 3, 3, 3 ), vec3( 0 ) );
gl::setMatrices( cam );
auto lambert = gl::ShaderDef().lambert();
auto shader = gl::getStockShader( lambert );
shader->bind();
// draw sphere at the origin, radius 1
gl::drawSphere( vec3(), 1.0f );
}
This is a start, but has a couple of visible problems. We've switched to
void BasicApp::draw()
{
gl::clear();
// turn on z-buffering
gl::enableDepthRead();
gl::enableDepthWrite();
CameraPersp cam;
cam.lookAt( vec3( 3, 3, 3 ), vec3( 0 ) );
gl::setMatrices( cam );
auto lambert = gl::ShaderDef().lambert();
auto shader = gl::getStockShader( lambert );
shader->bind();
gl::drawSphere( vec3(), 1.0f, 40 );
}
Much better. First, our black triangle artifact is gone, because we've enabled the Z-Buffer (also known as the depth buffer). If you're not familiar, Z-Buffering is the standard technique for preventing fragments from drawing on top of each other incorrectly. It uses a per-pixel depth value to ensure that no pixel draws on top of a pixel it is "behind." Various effects can be achieved by variously enabling and disabling depth buffer read and write, as well as its testing function, which we won't be exploring here. Just keep in mind that in the general 3D case, we want to call
Let's extend the use of transformations into 3D:
void BasicApp::draw()
{
gl::clear();
gl::enableDepthRead();
gl::enableDepthWrite();
CameraPersp cam;
cam.lookAt( vec3( 5, 2, 5 ), vec3( 0, 1, 0 ) );
gl::setMatrices( cam );
auto lambert = gl::ShaderDef().lambert().color();
auto shader = gl::getStockShader( lambert );
shader->bind();
int numSpheres = 64;
float maxAngle = M_PI * 7;
float spiralRadius = 1;
float height = 3;
for( int s = 0; s < numSpheres; ++s ) {
float rel = s / (float)numSpheres;
float angle = rel * maxAngle;
float y = rel * height;
float r = rel * spiralRadius * spiralRadius;
vec3 offset( r * cos( angle ), y, r * sin( angle ) );
gl::pushModelMatrix();
gl::translate( offset );
gl::color( Color( CM_HSV, rel, 1, 1 ) );
gl::drawSphere( vec3(), 0.1f, 30 );
gl::popModelMatrix();
}
}
One change this snippet highlights is the addition of
void BasicApp::draw()
{
gl::clear();
gl::enableDepthRead();
gl::enableDepthWrite();
CameraPersp cam;
cam.lookAt( vec3( 3, 4.5, 4.5 ), vec3( 0, 1, 0 ) );
gl::setMatrices( cam );
auto lambert = gl::ShaderDef().lambert().color();
auto shader = gl::getStockShader( lambert );
shader->bind();
int numSpheres = 64;
float maxAngle = M_PI * 7;
float spiralRadius = 1;
float height = 2;
float anim = getElapsedFrames() / 30.0f;
for( int s = 0; s < numSpheres; ++s ) {
float rel = s / (float)numSpheres;
float angle = rel * maxAngle;
float y = fabs( cos( rel * M_PI + anim ) ) * height;
float r = rel * spiralRadius;
vec3 offset( r * cos( angle ), y / 2, r * sin( angle ) );
gl::pushModelMatrix();
gl::translate( offset );
gl::scale( vec3( 0.05f, y, 0.05f ) );
gl::color( Color( CM_HSV, rel, 1, 1 ) );
gl::drawCube( vec3(), vec3( 1 ) );
gl::popModelMatrix();
}
}
Here we're creating the anim variable as a function of
In the next section we'll graduate from the GL convenience methods and look at using