In the previous tutorial we played animations on a skeleton. Now we'll add visual effects by loading and controlling particle system files (.ps) in the scene.
This tutorial covers:
createInstanceParticle system files (.ps) are loaded exactly like shape files, using UScene::createInstance():
NL3D::UInstance particles = m_Scene->createInstance("snow.ps");
The returned UInstance can be positioned, scaled, and shown/hidden like any other instance:
particles.setPos(NLMISC::CVector(0.f, 0.f, 0.f));
Particle systems are created and edited using the Object Viewer particle editor. The .ps file contains all the emitters, forces, and particle types that make up the effect.
Some particle systems have start/stop capability (one-shot effects like explosions). Check with canStartStop():
if (particles.canStartStop())
{
particles.start();
}
You can later stop and restart:
particles.stop();
// ...
particles.start();
Use isStarted() to query the current state.
Continuous effects (like snow or fire) typically start automatically when created and run until deleted.
Particle systems respond to the instance's scale. This is useful for making bigger or smaller versions of the same effect:
particles.setScale(NLMISC::CVector(2.f, 2.f, 2.f));
For particle-specific control beyond start/stop, cast the UInstance to UParticleSystemInstance. This gives access to user parameters, manual emission, and color modulation:
#include <nel/3d/u_particle_system_instance.h>
NL3D::UParticleSystemInstance psFX;
psFX.cast(particles);
if (!psFX.empty())
{
// User parameters (0-3) control artist-defined behaviors, range 0.0 to 1.0
psFX.setUserParam(0, 0.5f);
// Modulate the color of the entire system
psFX.setUserColor(NLMISC::CRGBA(255, 200, 150));
// Toggle all emitters on/off
psFX.activateEmitters(true);
// Force the system to allocate resources even when off-screen
// (important for gameplay-critical effects like spells)
psFX.forceInstanciate();
}
User parameters are values from 0 to 1 that the particle system artist can wire to any property (emission rate, size, speed, color, etc.) when designing the effect in the Object Viewer.
To attach a particle effect to a bone on a skeleton (for example, a magic spell on a character's hand), use USkeleton::stickObject():
sint boneId = m_Skeleton.getBoneIdByName("Bip01 R Hand");
if (boneId >= 0)
{
m_Skeleton.stickObject(particles, (uint)boneId);
}
The particle system will then follow the bone's position and orientation. To detach:
m_Skeleton.detachSkeletonSon(particles);
Delete particle instances like any other instance:
m_Scene->deleteInstance(particles);
This example creates a scene with a shape and two particle effects: one attached to the scene at a fixed position, and another that can be toggled with the space bar. Since particle system files are created with the Object Viewer tool, this tutorial uses placeholder filenames that you'll need to replace with actual .ps files from the Snowballs data or your own creations.
#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_instance.h>
#include <nel/3d/u_text_context.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::UTextContext *m_TextContext;
NL3D::UInstance m_Entity;
NL3D::UInstance m_AmbientPS;
NL3D::UInstance m_EffectPS;
bool m_EffectActive;
bool m_CloseWindow;
double m_LastTime;
float m_CamAngle;
};
CMyGame::CMyGame()
: m_CloseWindow(false)
, m_Scene(NULL)
, m_TextContext(NULL)
, m_EffectActive(false)
, m_CamAngle(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->setDisplay(NL3D::UDriver::CMode(800, 600, 32));
m_Driver->setWindowTitle("Particle Systems");
CPath::addSearchPath("data", true, false);
CPath::remapExtension("dds", "tga", true);
// Optional text context for HUD
m_TextContext = m_Driver->createTextContext("n019003l.pfb");
if (m_TextContext) m_TextContext->setFontSize(12);
// Create scene with sun lighting
m_Scene = m_Driver->createScene(true);
m_Scene->enableLightingSystem(true);
m_Scene->setSunDiffuse(CRGBA(255, 255, 240));
m_Scene->setSunDirection(CVector(1.f, 1.f, -2.f).normed());
// 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);
// Load a shape for context
m_Entity = m_Scene->createInstance("box02.shape");
if (!m_Entity.empty())
m_Entity.setPos(CVector(0.f, 0.f, 0.f));
// Load an ambient particle system (continuous, like snow)
// Replace with an actual .ps filename from your data
m_AmbientPS = m_Scene->createInstance("snow.ps");
if (!m_AmbientPS.empty())
m_AmbientPS.setPos(CVector(0.f, 0.f, 3.f));
// Load a one-shot effect particle system
// Replace with an actual .ps filename from your data
m_EffectPS = m_Scene->createInstance("appear.ps");
if (!m_EffectPS.empty())
{
m_EffectPS.setPos(CVector(0.f, 0.f, 1.f));
// One-shot effects may need explicit start
if (m_EffectPS.canStartStop())
m_EffectPS.stop();
}
m_LastTime = CTime::ticksToSecond(CTime::getPerformanceTime());
}
CMyGame::~CMyGame()
{
if (!m_EffectPS.empty()) m_Scene->deleteInstance(m_EffectPS);
if (!m_AmbientPS.empty()) m_Scene->deleteInstance(m_AmbientPS);
if (!m_Entity.empty()) m_Scene->deleteInstance(m_Entity);
if (m_TextContext) m_Driver->deleteTextContext(m_TextContext);
if (m_Scene) m_Driver->deleteScene(m_Scene);
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 == KeySPACE && kd.FirstTime)
{
// Toggle the one-shot effect
if (!m_EffectPS.empty() && m_EffectPS.canStartStop())
{
if (m_EffectActive)
m_EffectPS.stop();
else
m_EffectPS.start();
m_EffectActive = !m_EffectActive;
}
}
}
}
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;
m_CamAngle += dt * 0.3f;
CVector eye(cosf(m_CamAngle) * 6.f, sinf(m_CamAngle) * 6.f, 3.f);
m_Scene->getCam().lookAt(eye, CVector(0.f, 0.f, 1.f));
m_Driver->clearBuffers(CRGBA(10, 10, 15));
m_Scene->animate(CTime::getLocalTime() / 1000.0);
m_Scene->render();
// HUD
if (m_TextContext)
{
m_TextContext->setHotSpot(NL3D::UTextContext::TopLeft);
m_TextContext->setColor(CRGBA::White);
m_TextContext->printfAt(0.01f, 0.99f,
"[Space] Effect: %s", m_EffectActive ? "ON" : "OFF");
}
m_Driver->swapBuffers();
}
}
int main(int argc, char *argv[])
{
CApplicationContext applicationContext;
CMyGame myGame;
myGame.run();
return EXIT_SUCCESS;
}
Place shape and particle system files in a data directory next to the executable. The Snowballs data archive contains snow.ps, appear.ps, disappear.ps, and other particle effects you can use.
You now know the core 3D rendering features of NeL: primitives, textures, shapes, lights, animations, and particle systems. The next tutorials will cover game infrastructure topics like config files, Georges data sheets, and networking.