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 .shape files with createInstance
  • Setting up a UCamera with lookAt
  • Configuring a directional light with ULight
  • The animate/render loop
  • Positioning and rotating instances with UTransformable
  • Using CPath to locate data files

The scene graph

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.

Setting up search paths

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.

Creating a scene

#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);

Setting up the camera

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));

Setting up a light

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.

Loading a shape instance

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.

Positioning and rotating instances

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

The animate/render loop

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.

Complete source code

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;
}

What's next

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.

⚠️ **GitHub.com Fallback** ⚠️