Overview
I built a full particle and VFX authoring system in C++ integrated directly into our custom engine (TGE). The system covers everything from the data layer — a serialisable ParticleEmitterData struct with 7 spawn shapes, burst emission, sub-emitters, and world collision — through to a live ImGui editor where artists can edit over-lifetime curves for colour (R/G/B/A independently), size, velocity per axis, and rotation, and see results in-engine immediately. Particle parameters flow end-to-end from the editor into the runtime update loop and out through the pixel and vertex shaders.
My Role & Responsibilities
I designed and implemented the entire system — data architecture, serialisation, runtime particle update, ImGui editor UI, sub-emitter support, PhysX collision integration, and shader wiring. The goal was to make it production-usable: artists and designers could open the editor, tune an effect, and see it running in the scene without touching code or waiting for a rebuild.
C++
ImGui
Particle Systems
Curve Editor
JSON Serialisation
Sub-Emitters
PhysX Raycasting
Shader Integration
VFX Workflow
Custom Engine (TGE)
Technical Highlights
The core design challenge was keeping the editor and runtime in perfect sync. Every over-lifetime curve — colour channels R, G, B, A, size, velocity X/Y/Z, and rotation — is stored as a std::vector<CurveKey> inside ParticleEmitterData, serialised to JSON, and evaluated identically in both the editor preview and the runtime particle update. The entire data structure is wrapped in a CopyOnWriteWrapper so edits in the ImGui panel don't cause unnecessary copies until a value actually changes. World collision is handled by casting a PhysX ray from each particle's previous position to its current one every frame — on hit, a configurable decal TGO is spawned at the contact point and the particle can be set to die immediately or continue.
Development Breakdown
Step 01 — First Particle
One Particle, End-to-End
Before any systems existed, I proved the full pipeline end-to-end with a single hardcoded particle. The first non-obvious discovery: sprite size must be written directly into mySize on the instance data every frame — transform matrix scale has no effect on sprites. Getting this right on one particle meant everything built after it stood on a correct foundation.
Step 02 — Live Editor Preview
Particles Playing Inside the Editor
With one particle working, I integrated live preview into the TGE editor — first in the ObjectDefinitionDocument for single-asset preview, then in the SceneDocument so emitters play at their correct world-space positions. A critical bug I hit early: storing only the VFXInstance shared_ptr lets the owning GameObject go out of scope, leaving an internal pointer dangling and crashing on UpdateTransform(). Storing both together in a PreviewVFXEntry solved it.
Step 03 — Data Architecture
ParticleEmitterData — The Foundation of Everything
The entire system is driven by a single ParticleEmitterData struct — spawn shape, rate, burst schedules, gravity, colour ranges, direction spread, collision flags, and sub-emitters. The struct serialises fully to and from JSON, so emitter setups save, reload, and share between scenes without any code changes.
Step 04 — Curve System
11 Independent Over-Lifetime Channels
Rather than simple min/max scalars, I built 11 independent over-lifetime curve channels: colour R, G, B, A, size, velocity X/Y/Z, and rotation. Velocity curves are baked at spawn time against the particle's initial direction vector — so direction spread and spawn shape interact correctly without recomputing direction every frame.
Step 05 — World Collision & Decals
Per-Particle PhysX Raycasting with Decal Spawning
When collision is enabled, every live particle casts a PhysX ray from its previous position to its current one each frame. On hit, the particle can die immediately or continue — and if Spawn Decal on Collision is enabled, a configurable TGO asset spawns at the exact contact point. Collision TGO name and scale are both exposed in the ImGui editor and serialise with the emitter data.