scene_shapes - ryzom/ryzomcore GitHub Wiki
title: Loading and rendering 3D shapes description: Use the NeL scene graph to load shape files, create instances, set up lights, and render a scene published: true date: 2026-03-14T00:00:00.000Z tags: editor: markdown dateCreated: 2026-03-14T00:00:00.000Z
So far we've drawn geometry manually with drawQuad. This works for simple cases, but real games use the scene graph to manage shapes, instances, lights, cameras, and animations. In this tutorial we'll load a .shape file, create an instance of it in a UScene, set up a directional light, and render the scene with a camera.
This tutorial covers:
- Creating a
UScene - Loading
.shapefiles withcreateInstance - Setting up a
UCamerawithlookAt - Configuring a directional light with
ULight - The animate/render loop
- Positioning and rotating instances with
UTransformable - Using
CPathto locate data files
When you call UDriver::createScene(), NeL creates a scene graph that handles visibility culling, LOD, lighting, and animation for you. You add objects to the scene by creating instances of shapes. Each instance has its own position, rotation, and scale, but shares the mesh and material data from the shape.
The scene renders in two steps: animate() updates animations and time-dependent state, and render() draws everything through the driver.
Shape files reference textures by filename. NeL's CPath system must be able to find both the shape and its textures. Register your data directories early:
#include <nel/misc/path.h>
// Add the data directory to the search path (recursive)
NLMISC::CPath::addSearchPath("data", true, false);
// Allow DDS files to be used in place of TGA
NLMISC::CPath::remapExtension("dds", "tga", true);
The remapExtension call means if a shape references wood.tga but only wood.dds exists in the search path, NeL will load the DDS file automatically.
#include <nel/3d/u_scene.h>
m_Scene = m_Driver->createScene(true);
The true parameter indicates a small scene (optimized for fewer objects). Delete it in the destructor before releasing the driver:
m_Driver->deleteScene(m_Scene);
Every scene has a default camera, retrieved with getCam(). Use lookAt() for a quick camera setup with eye position and target:
#include <nel/3d/u_camera.h>
UCamera cam = m_Scene->getCam();
cam.setTransformMode(NL3D::UTransformable::DirectMatrix);
cam.setPerspective(float(NLMISC::Pi / 3.0), 1.33f, 0.1f, 1000.f);
cam.lookAt(NLMISC::CVector(0.f, -5.f, 2.f), NLMISC::CVector(0.f, 0.f, 0.f));
setTransformMode(DirectMatrix) is required before calling lookAt or setMatrix on a camera. setPerspective takes field-of-view in radians, aspect ratio, near plane, and far plane.
To orbit the camera each frame, call lookAt again with an updated eye position:
CVector eye(cosf(m_CamAngle) * m_CamDist,
sinf(m_CamAngle) * m_CamDist, 3.f);
m_Scene->getCam().lookAt(eye, CVector(0.f, 0.f, 0.f));
NeL has two kinds of lights. ULight is a standalone light object set on the driver (used for fixed-function and immediate-mode rendering). UPointLight is a scene-graph light created through the scene (used for dynamic per-object lighting). For a basic directional light (like the sun), use ULight:
#include <nel/3d/u_light.h>
NL3D::ULight *light = NL3D::ULight::createLight();
light->setupDirectional(
NLMISC::CRGBA(60, 60, 60), // ambient
NLMISC::CRGBA(255, 255, 255), // diffuse
NLMISC::CRGBA(255, 255, 255), // specular
NLMISC::CVector(0.5f, 0.5f, -1.f).normed() // direction
);
m_Driver->setLight(0, *light);
m_Driver->enableLight(0);
The scene also has global lighting settings:
m_Scene->enableLightingSystem(true);
m_Scene->setAmbientGlobal(NLMISC::CRGBA(60, 60, 60));
m_Scene->setSunDiffuse(NLMISC::CRGBA(255, 255, 255));
m_Scene->setSunSpecular(NLMISC::CRGBA(255, 255, 255));
m_Scene->setSunDirection(NLMISC::CVector(0.5f, 0.5f, -1.f).normed());
Delete the standalone light with delete light after you no longer need it.
Create an instance by passing the shape filename to UScene::createInstance(). The shape file must be findable through CPath:
#include <nel/3d/u_instance.h>
NL3D::UInstance entity = m_Scene->createInstance("box02.shape");
if (entity.empty())
{
nlwarning("Failed to create instance");
}
The empty() method checks whether the instance was created successfully. The shape is loaded from disk on first use and cached in the shape bank.
By default, instances use the RotQuat transform mode. You can set position, rotation, and scale independently:
entity.setPos(NLMISC::CVector(0.f, 0.f, 0.f));
entity.setScale(NLMISC::CVector(1.f, 1.f, 1.f));
To rotate by Euler angles, switch to RotEuler mode:
entity.setTransformMode(NL3D::UTransformable::RotEuler);
entity.setRotEuler(0.f, 0.f, angle); // rotate around Z
Each frame, call animate() with the current time in seconds, then render():
// Animate the scene (updates animations, LOD, etc.)
m_Scene->animate(NLMISC::CTime::getLocalTime() / 1000.0);
// Render the scene
m_Scene->render();
The driver's clearBuffers should be called before rendering, and swapBuffers after.
This example loads a shape file from the data subdirectory of the working directory, lights it with a directional light, and lets the camera orbit around it.
To run this, copy a .shape file (for example box02.shape from nel/samples/3d/cluster_viewer/shapes/) into a data directory next to the executable.
#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/misc/path.h>
#include <nel/3d/u_driver.h>
#include <nel/3d/u_scene.h>
#include <nel/3d/u_camera.h>
#include <nel/3d/u_light.h>
#include <nel/3d/u_instance.h>
using namespace std;
using namespace NLMISC;
class CMyGame : public IEventListener
{
public:
CMyGame();
~CMyGame();
void run();
virtual void operator()(const CEvent &event) NL_OVERRIDE;
private:
NL3D::UDriver *m_Driver;
NL3D::UScene *m_Scene;
NL3D::ULight *m_Light;
NL3D::UInstance m_Entity;
bool m_CloseWindow;
bool m_KeyForward;
bool m_KeyBackward;
double m_LastTime;
float m_CamAngle;
float m_CamDist;
float m_EntityAngle;
};
CMyGame::CMyGame()
: m_CloseWindow(false)
, m_KeyForward(false)
, m_KeyBackward(false)
, m_Scene(NULL)
, m_Light(NULL)
, m_CamAngle(0.f)
, m_CamDist(6.f)
, m_EntityAngle(0.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("Loading Shapes");
// Register data search paths
CPath::addSearchPath("data", true, false);
CPath::remapExtension("dds", "tga", true);
// Create a directional light
m_Light = NL3D::ULight::createLight();
m_Light->setupDirectional(
CRGBA(60, 60, 60),
CRGBA(255, 255, 255),
CRGBA(255, 255, 255),
CVector(0.5f, 0.5f, -1.f).normed());
m_Driver->setLight(0, *m_Light);
m_Driver->enableLight(0);
// Create the scene
m_Scene = m_Driver->createScene(true);
if (!m_Scene)
{
nlerror("Failed to create scene");
return;
}
// Set up the camera
NL3D::UCamera cam = m_Scene->getCam();
cam.setTransformMode(NL3D::UTransformable::DirectMatrix);
cam.setPerspective(float(Pi / 3.0), 800.f / 600.f, 0.1f, 1000.f);
cam.lookAt(CVector(0.f, -6.f, 3.f), CVector(0.f, 0.f, 0.f));
// Load a shape instance
m_Entity = m_Scene->createInstance("box02.shape");
if (m_Entity.empty())
{
nlwarning("Failed to load box02.shape - place it in the data/ directory");
}
else
{
m_Entity.setTransformMode(NL3D::UTransformable::RotEuler);
m_Entity.setPos(CVector(0.f, 0.f, 0.f));
}
m_LastTime = CTime::ticksToSecond(CTime::getPerformanceTime());
}
CMyGame::~CMyGame()
{
if (!m_Entity.empty())
m_Scene->deleteInstance(m_Entity);
if (m_Scene)
m_Driver->deleteScene(m_Scene);
delete m_Light;
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();
double now = CTime::ticksToSecond(CTime::getPerformanceTime());
float dt = float(now - m_LastTime);
m_LastTime = now;
// Update animation state
m_CamAngle += dt * 0.3f;
m_EntityAngle += dt * 0.5f;
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;
// Update camera orbit
CVector eye(cosf(m_CamAngle) * m_CamDist,
sinf(m_CamAngle) * m_CamDist, 3.f);
m_Scene->getCam().lookAt(eye, CVector(0.f, 0.f, 0.f));
// Rotate the entity
if (!m_Entity.empty())
m_Entity.setRotEuler(0.f, 0.f, m_EntityAngle);
// Clear, animate, render
m_Driver->clearBuffers(CRGBA(40, 40, 50));
m_Scene->animate(CTime::getLocalTime() / 1000.0);
m_Scene->render();
m_Driver->swapBuffers();
}
}
int main(int argc, char *argv[])
{
CApplicationContext applicationContext;
CMyGame myGame;
myGame.run();
return EXIT_SUCCESS;
}
You now have a working scene graph with shapes, lights, and a camera. In the next tutorial, you will learn how to add dynamic point lights to a scene.
- Have a look at the complete list of NeL tutorials.