In the previous tutorials we set up a project, created a game loop, and rendered text. Now we'll draw actual 3D geometry: a spinning colored cube with an orbiting camera, all using the NeL driver's immediate-mode drawing functions.
This tutorial covers:
NeL provides two ways to render 3D content. The scene graph (UScene) manages shapes, lights, and animations automatically. For simple cases, though, you can draw primitives directly through the driver using drawQuad, drawLine, and similar functions. This tutorial uses the direct approach, which requires you to set up the camera matrices yourself.
In NeL's coordinate system, X is right, Y is forward, and Z is up.
A UMaterial controls how geometry is drawn. For unlit colored geometry, call initUnlit(). You also need to configure depth testing so that closer faces draw over farther ones.
Add two materials to the game class:
#include <nel/3d/u_material.h>
private:
NL3D::UMaterial m_CubeMat;
NL3D::UMaterial m_SkyMat;
Create them after the driver is initialized:
// Opaque material for the cube
m_CubeMat = m_Driver->createMaterial();
m_CubeMat.initUnlit();
m_CubeMat.setZWrite(true);
m_CubeMat.setZFunc(NL3D::UMaterial::lessequal);
// Background material: no depth write, always passes
m_SkyMat = m_Driver->createMaterial();
m_SkyMat.initUnlit();
m_SkyMat.setZWrite(false);
m_SkyMat.setZFunc(NL3D::UMaterial::always);
Delete them in the destructor, before releasing the driver:
m_Driver->deleteMaterial(m_CubeMat);
m_Driver->deleteMaterial(m_SkyMat);
To render 3D geometry, you need to configure three things on the driver: a frustum (the projection), a view matrix (where the camera is), and a model matrix (where the object is).
A CFrustum defines the perspective projection. Add the include and initialize it each frame based on the window size:
#include <nel/3d/frustum.h>
uint32 screenW, screenH;
m_Driver->getWindowSize(screenW, screenH);
NL3D::CFrustum frustum;
frustum.initPerspective(float(NLMISC::Pi / 3.0),
float(screenW) / float(screenH), 0.1f, 100.f);
m_Driver->setFrustum(frustum);
The view matrix positions and orients the camera. Build it from an eye position, a target to look at, and an up vector. NeL's CMatrix doesn't have a built-in lookAt, so we construct it manually:
static NLMISC::CMatrix buildViewMatrix(
const NLMISC::CVector &eye,
const NLMISC::CVector &target,
const NLMISC::CVector &up)
{
NLMISC::CVector jj = (target - eye).normed(); // forward
NLMISC::CVector ii = (jj ^ up).normed(); // right
NLMISC::CVector kk = ii ^ jj; // corrected up
NLMISC::CMatrix camWorld;
camWorld.setRot(ii, jj, kk, true);
camWorld.setPos(eye);
NLMISC::CMatrix viewMatrix = camWorld;
viewMatrix.invert();
return viewMatrix;
}
Then each frame:
NLMISC::CVector eye(cosf(m_CamAngle) * m_CamDist,
sinf(m_CamAngle) * m_CamDist, 3.f);
NLMISC::CVector target(0.f, 0.f, 1.f);
NLMISC::CVector up(0.f, 0.f, 1.f);
m_Driver->setViewMatrix(buildViewMatrix(eye, target, up));
The model matrix transforms object vertices into world space. For world-space drawing, set it to identity:
NLMISC::CMatrix modelMatrix;
modelMatrix.identity();
m_Driver->setModelMatrix(modelMatrix);
The driver's drawQuad function takes a CQuadColor with four 3D vertices and four colors. To draw a cube, we draw its six faces:
static void drawFace(NL3D::UDriver *driver, NL3D::UMaterial &mat,
const NLMISC::CVector &v0, const NLMISC::CVector &v1,
const NLMISC::CVector &v2, const NLMISC::CVector &v3,
NLMISC::CRGBA color)
{
NL3D::CQuadColor quad;
quad.V0 = v0; quad.V1 = v1; quad.V2 = v2; quad.V3 = v3;
quad.Color0 = quad.Color1 = quad.Color2 = quad.Color3 = color;
driver->drawQuad(quad, mat);
}
static void drawCube(NL3D::UDriver *driver, NL3D::UMaterial &mat,
const NLMISC::CMatrix &transform, float s)
{
// 8 vertices of a cube (X right, Y forward, Z up)
NLMISC::CVector v[8] = {
transform * NLMISC::CVector(-s, -s, -s),
transform * NLMISC::CVector( s, -s, -s),
transform * NLMISC::CVector( s, s, -s),
transform * NLMISC::CVector(-s, s, -s),
transform * NLMISC::CVector(-s, -s, s),
transform * NLMISC::CVector( s, -s, s),
transform * NLMISC::CVector( s, s, s),
transform * NLMISC::CVector(-s, s, s),
};
drawFace(driver, mat, v[3], v[2], v[1], v[0], NLMISC::CRGBA(200, 50, 50)); // bottom red
drawFace(driver, mat, v[4], v[5], v[6], v[7], NLMISC::CRGBA( 50, 200, 50)); // top green
drawFace(driver, mat, v[0], v[1], v[5], v[4], NLMISC::CRGBA( 50, 50, 200)); // back blue
drawFace(driver, mat, v[2], v[3], v[7], v[6], NLMISC::CRGBA(200, 200, 50)); // front yellow
drawFace(driver, mat, v[3], v[0], v[4], v[7], NLMISC::CRGBA(200, 50, 200)); // left magenta
drawFace(driver, mat, v[1], v[2], v[6], v[5], NLMISC::CRGBA( 50, 200, 200)); // right cyan
}
Call it in the render loop with a spinning transform:
NLMISC::CMatrix cubeTransform;
cubeTransform.identity();
cubeTransform.setPos(NLMISC::CVector(0.f, 0.f, 1.f));
cubeTransform.rotateZ(m_CubeAngle);
cubeTransform.rotateX(m_CubeAngle * 0.7f);
drawCube(m_Driver, m_CubeMat, cubeTransform, 0.8f);
Never tie animation speed to the frame rate. Use CTime::getPerformanceTime() to measure real elapsed time:
#include <nel/misc/time_nl.h>
Add timing state to the class:
double m_LastTime;
float m_CubeAngle;
float m_CamAngle;
float m_CamDist;
Initialize in the constructor:
m_LastTime = NLMISC::CTime::ticksToSecond(NLMISC::CTime::getPerformanceTime());
m_CubeAngle = 0.f;
m_CamAngle = 0.f;
m_CamDist = 6.f;
Compute delta time at the start of each frame:
double now = NLMISC::CTime::ticksToSecond(NLMISC::CTime::getPerformanceTime());
float dt = float(now - m_LastTime);
m_LastTime = now;
m_CubeAngle += dt * 1.0f;
m_CamAngle += dt * 0.3f;
To handle continuous key state (held keys), listen for both EventKeyDownId and EventKeyUpId and track the state in booleans:
m_Driver->EventServer.addListener(EventKeyDownId, this);
m_Driver->EventServer.addListener(EventKeyUpId, this);
void CMyGame::operator()(const CEvent &event)
{
if (event == EventCloseWindowId)
{
m_CloseWindow = true;
}
else if (event == EventKeyDownId)
{
CEventKeyDown &kd = (CEventKeyDown &)event;
if (kd.Key == KeyUP) m_KeyForward = true;
if (kd.Key == KeyDOWN) m_KeyBackward = true;
}
else if (event == EventKeyUpId)
{
CEventKeyUp &ku = (CEventKeyUp &)event;
if (ku.Key == KeyUP) m_KeyForward = false;
if (ku.Key == KeyDOWN) m_KeyBackward = false;
}
}
Apply movement in the game loop:
if (m_KeyForward) m_CamDist -= dt * 4.f;
if (m_KeyBackward) m_CamDist += dt * 4.f;
if (m_CamDist < 2.f) m_CamDist = 2.f;
Without a background, it's hard to perceive depth. Draw a simple ground grid using drawLine:
// Draw a ground grid at Z=0
NLMISC::CRGBA gridColor(60, 60, 60);
for (int i = -5; i <= 5; ++i)
{
m_Driver->drawLine(NLMISC::CVector(float(i), -5.f, 0.f),
NLMISC::CVector(float(i), 5.f, 0.f), gridColor, m_SkyMat);
m_Driver->drawLine(NLMISC::CVector(-5.f, float(i), 0.f),
NLMISC::CVector(5.f, float(i), 0.f), gridColor, m_SkyMat);
}
#include <nel/misc/types_nl.h>
#include <nel/misc/app_context.h>
#include <nel/misc/event_listener.h>
#include <nel/misc/debug.h>
#include <nel/misc/time_nl.h>
#include <nel/3d/u_driver.h>
#include <nel/3d/u_material.h>
#include <nel/3d/u_text_context.h>
#include <nel/3d/frustum.h>
using namespace std;
using namespace NLMISC;
// Build a view matrix from eye position, target, and up vector
static CMatrix buildViewMatrix(const CVector &eye, const CVector &target, const CVector &up)
{
CVector jj = (target - eye).normed();
CVector ii = (jj ^ up).normed();
CVector kk = ii ^ jj;
CMatrix camWorld;
camWorld.setRot(ii, jj, kk, true);
camWorld.setPos(eye);
CMatrix viewMatrix = camWorld;
viewMatrix.invert();
return viewMatrix;
}
// Draw a colored quad face
static void drawFace(NL3D::UDriver *driver, NL3D::UMaterial &mat,
const CVector &v0, const CVector &v1, const CVector &v2, const CVector &v3,
CRGBA color)
{
NL3D::CQuadColor quad;
quad.V0 = v0; quad.V1 = v1; quad.V2 = v2; quad.V3 = v3;
quad.Color0 = quad.Color1 = quad.Color2 = quad.Color3 = color;
driver->drawQuad(quad, mat);
}
// Draw a colored cube centered at origin, transformed by a matrix
static void drawCube(NL3D::UDriver *driver, NL3D::UMaterial &mat,
const CMatrix &transform, float s)
{
CVector v[8] = {
transform * CVector(-s, -s, -s),
transform * CVector( s, -s, -s),
transform * CVector( s, s, -s),
transform * CVector(-s, s, -s),
transform * CVector(-s, -s, s),
transform * CVector( s, -s, s),
transform * CVector( s, s, s),
transform * CVector(-s, s, s),
};
drawFace(driver, mat, v[3], v[2], v[1], v[0], CRGBA(200, 50, 50)); // bottom
drawFace(driver, mat, v[4], v[5], v[6], v[7], CRGBA( 50, 200, 50)); // top
drawFace(driver, mat, v[0], v[1], v[5], v[4], CRGBA( 50, 50, 200)); // back
drawFace(driver, mat, v[2], v[3], v[7], v[6], CRGBA(200, 200, 50)); // front
drawFace(driver, mat, v[3], v[0], v[4], v[7], CRGBA(200, 50, 200)); // left
drawFace(driver, mat, v[1], v[2], v[6], v[5], CRGBA( 50, 200, 200)); // right
}
class CMyGame : public IEventListener
{
public:
CMyGame();
~CMyGame();
void run();
virtual void operator()(const CEvent &event) NL_OVERRIDE;
private:
NL3D::UDriver *m_Driver;
NL3D::UTextContext *m_TextContext;
NL3D::UMaterial m_CubeMat;
NL3D::UMaterial m_SkyMat;
bool m_CloseWindow;
bool m_KeyForward;
bool m_KeyBackward;
double m_LastTime;
float m_CubeAngle;
float m_CamAngle;
float m_CamDist;
};
CMyGame::CMyGame()
: m_CloseWindow(false)
, m_KeyForward(false)
, m_KeyBackward(false)
, m_TextContext(NULL)
, m_CubeAngle(0.f)
, m_CamAngle(0.f)
, m_CamDist(6.f)
{
m_Driver = NL3D::UDriver::createDriver(0, NL3D::UDriver::OpenGl3);
if (!m_Driver)
{
nlerror("Failed to create driver");
return;
}
m_Driver->EventServer.addListener(EventCloseWindowId, this);
m_Driver->EventServer.addListener(EventKeyDownId, this);
m_Driver->EventServer.addListener(EventKeyUpId, this);
m_Driver->setDisplay(NL3D::UDriver::CMode(800, 600, 32));
m_Driver->setWindowTitle("Spinning Cube");
// Optional: text overlay for HUD info
m_TextContext = m_Driver->createTextContext("n019003l.pfb");
if (m_TextContext)
m_TextContext->setFontSize(12);
// Opaque material for the cube
m_CubeMat = m_Driver->createMaterial();
m_CubeMat.initUnlit();
m_CubeMat.setZWrite(true);
m_CubeMat.setZFunc(NL3D::UMaterial::lessequal);
// Background material: no depth write
m_SkyMat = m_Driver->createMaterial();
m_SkyMat.initUnlit();
m_SkyMat.setZWrite(false);
m_SkyMat.setZFunc(NL3D::UMaterial::always);
m_LastTime = CTime::ticksToSecond(CTime::getPerformanceTime());
}
CMyGame::~CMyGame()
{
if (m_TextContext)
m_Driver->deleteTextContext(m_TextContext);
m_Driver->deleteMaterial(m_CubeMat);
m_Driver->deleteMaterial(m_SkyMat);
m_Driver->release();
delete m_Driver;
}
void CMyGame::operator()(const CEvent &event)
{
if (event == EventCloseWindowId)
{
m_CloseWindow = true;
}
else if (event == EventKeyDownId)
{
CEventKeyDown &kd = (CEventKeyDown &)event;
if (kd.Key == KeyUP) m_KeyForward = true;
if (kd.Key == KeyDOWN) m_KeyBackward = true;
}
else if (event == EventKeyUpId)
{
CEventKeyUp &ku = (CEventKeyUp &)event;
if (ku.Key == KeyUP) m_KeyForward = false;
if (ku.Key == KeyDOWN) m_KeyBackward = false;
}
}
void CMyGame::run()
{
while (m_Driver->isActive() && !m_CloseWindow)
{
m_Driver->EventServer.pump();
// Delta time
double now = CTime::ticksToSecond(CTime::getPerformanceTime());
float dt = float(now - m_LastTime);
m_LastTime = now;
// Update animation
m_CubeAngle += dt * 1.0f;
m_CamAngle += dt * 0.3f;
if (m_KeyForward) m_CamDist -= dt * 4.f;
if (m_KeyBackward) m_CamDist += dt * 4.f;
if (m_CamDist < 2.f) m_CamDist = 2.f;
// Set up the camera
uint32 screenW, screenH;
m_Driver->getWindowSize(screenW, screenH);
NL3D::CFrustum frustum;
frustum.initPerspective(float(Pi / 3.0),
float(screenW) / float(screenH), 0.1f, 100.f);
m_Driver->setFrustum(frustum);
CVector eye(cosf(m_CamAngle) * m_CamDist,
sinf(m_CamAngle) * m_CamDist, 3.f);
CVector target(0.f, 0.f, 1.f);
CVector up(0.f, 0.f, 1.f);
m_Driver->setViewMatrix(buildViewMatrix(eye, target, up));
CMatrix modelMatrix;
modelMatrix.identity();
m_Driver->setModelMatrix(modelMatrix);
// Clear and draw
m_Driver->clearBuffers(CRGBA(30, 30, 30));
// Ground grid
CRGBA gridColor(60, 60, 60);
for (int i = -5; i <= 5; ++i)
{
m_Driver->drawLine(CVector(float(i), -5.f, 0.f),
CVector(float(i), 5.f, 0.f), gridColor, m_SkyMat);
m_Driver->drawLine(CVector(-5.f, float(i), 0.f),
CVector(5.f, float(i), 0.f), gridColor, m_SkyMat);
}
// Spinning cube
CMatrix cubeTransform;
cubeTransform.identity();
cubeTransform.setPos(CVector(0.f, 0.f, 1.f));
cubeTransform.rotateZ(m_CubeAngle);
cubeTransform.rotateX(m_CubeAngle * 0.7f);
drawCube(m_Driver, m_CubeMat, cubeTransform, 0.8f);
// HUD text
if (m_TextContext)
{
m_TextContext->setHotSpot(NL3D::UTextContext::TopLeft);
m_TextContext->setColor(CRGBA::White);
m_TextContext->printfAt(0.01f, 0.99f,
"[Up/Down] Camera distance: %.1f", m_CamDist);
}
m_Driver->swapBuffers();
}
}
int main(int argc, char *argv[])
{
CApplicationContext applicationContext;
CMyGame myGame;
myGame.run();
return EXIT_SUCCESS;
}
You now know how to draw 3D geometry, control a camera, and animate with delta time. In the next tutorial, you will learn how to apply textures and use alpha blending.