Overview
Properly handling coordinate spaces is essential for effects to playback properly in the final game-engine. An effect might appear to work in the PopcornFX editor because it is instantiated at the world origin by default, but can completely break when brought ingame and spawned away from the origin if transforms are not properly handled.
You will mainly be using two coordinate spaces when making effects:
- worldspace: This is the coordinate system of the ingame world.
- localspace: This is the coordinate system of an effect instance.
Worldspace
Rendering happens in worldspace. Collisions happen in worldspace. Particle simulation usually also happens in worldspace.
float3(0,0,0) in worldspace is the world origin.
float3(12,0,5) in worldspace is the point located 12 units along the world’s X axis, and 5 units along the world Z axis.
Localspace
Particle spawn & initialization usually happens in localspace. Simulation also sometimes needs to happen in localspace, albeit more rarely.
float3(0,0,0) in localspace is the center of the effect instance.
If the effect is spawned at the location float3(12,0,5) in worldspace, particles spawned at localspace coordinate float3(0,0,0) will appear at worldspace coordinate float3(12,0,5).
float3suf(0,1,0) in localspace is the point 1 unit away from the effect instance position along the effect instance UP axis. This won’t match the world UP axis if the effect instance is rotated.
Testing transform correctness
By default the PopcornFX editor will spawn one effect instance at the world origin, this makes it easy to miss transform issues when you are not used to it.
There are various tools you can use to make sure your effect handles transforms properly.
Move the current effect instance around
- Select the ‘Root’ node in the effect treeview
- Enable the transform gizmo using the None–Translate–Rotate–Scale dropdown in the viewport toolbar, or the Q–W–E–R keys with the viewport focused
- Move, rotate, or scale the effect instance using the gizmo
![]() | ![]() |
Without transform nodes | With transform nodes |
Spawning multiple effect instances on the backdrop
- Enable the mesh or grid backdrop
- Focus the viewport, and move the mouse cursor over a location on the grid
- Press the spacebar to spawn a new instance of the effect at that location
![]() | ![]() |
Without transform nodes | With transform nodes |
Transform nodes
Particle graphs expose a ‘transform’ node (abbreviated ‘xform’). You can use the xform node to convert 3d coordinates from local to world, or vice-versa.
A set of helper xform templates are also available in the core template library.
Transforming positions (particle position, collision point, …) or directions (particle velocity, collision surface normal, forward axis, …) require different treatment.
Positions need to be transformed taking into account the full effect instance transforms (position+orientation), whereas directions need to be transformed using the effect orientation only.
The position or direction helper templates take care of that:
- Local position to world: Takes a position in localspace coordinates, outputs that same position in worldspace coordinates.
- Local direction to world: Takes a direction in localspace coordinates, outputs that same direction in worldspace coordinates.
- World position to local: Takes a position in worldspace coordinates, outputs that same position in localspace coordinates.
- World direction to local: Takes a direction in worldspace coordinates, outputs that same direction in localspace coordinates.
If you use the raw xform node, you will need to specify the transform mode manually in the node properties.
You usually don’t need to specify an explicit simulation state (spawn or evolve) for transform nodes.
IMPORTANT NOTE: Having to manually handle transforms in the simulation graphs is a temporary necessity, we have an upcoming feature to allow the nodegraph compiler to automatically detect and insert most of the needed coordinate-space conversions.
Examples
Here is a test case to illustrate transform nodes. The test rig is a simple effect spawning 500 particles per second. The particle graph initializes particle positions on a sphere using the “vrand” node. It then sends those positions to a physics node with a small downwards acceleration force (gravity). Finally the particle graph draws the particles to the screen using a simple billboard renderer node.
Base effect without transform nodes:
![]() | ![]() |
Moving the effect instance in the editor viewport has no effect.
Particles spawn at the world origin, because there is no local to world xform node. The vrand node returns point on a sphere around location {0,0,0}, therefore particles appear on a world-origin centered sphere.
Effect using the “local position to world” node at spawn:
![]() | ![]() |
Particles spawn relative to the effect instance position, leaving a trail of particles behind when the effect moves.
The “vrand” node picks a random position at spawn on a sphere. The “local position to world” node transforms them to worldspace (still at spawn time), then sends those worldspace positions to the physics node. The physics node updates worldspace positions each frame.
Effect using the “local position to world” node at evolve:
![]() | ![]() |
Particles spawn relative to the effect instance and move along with it.
The physics node updates the localspace positions straight out of the “vrand()” node. The local position to world node converts those local positions to worldspace just before wiring them into the renderer. Here the entire simulation effectively runs in the effect instance localspace, and is converted to worldspace just before rendering.
This should be avoided if possible, as some functions will only work properly with worldspace positions (such as scene intersections, collisions, or spatial layers).
To get the same behavior, you can use the “localspace” node.
Effect using the “localspace” node:
![]() | ![]() |
Particles spawn relative to the effect instance and move along with it.
The local spawn positions are converted to worldspace.
The simulation happens in worldspace, and the simulated worldspace position flows into the localspace node, which outputs the new worldspace position after taking into account the effect instance transforms changes in the current simulation frame. Consequently, it is identical to the effect running the simulation in localspace and converting to worldspace before rendering. Except now, as the simulation runs in worldspace, you can use all the scene intersection functions, collision, or spatial layers.
Transforms through events
When you trigger an event, you can append a float3 payload with a “PayloadKind” of “XFormsPosition” or “XFormsOrientation”. This will treat this float3 value as containing actual transform information for the child particles.
Using a transform node at spawn in the child particles will use the parent payload’s xforms if available, otherwise it will use the effect instance xforms.
Using a transform node at evolve in the child particles will always use the effect instance xforms.
You only care about the payload kind when you manually append a 3D position or orientation payload that should be used as the parent transforms in the child layer.
Most of the event helper templates of the core template library will allow you to plug a position and orientation (or forward/up axis) in an input pin, and will properly setup the event so any child particle who uses xform nodes picks up those transforms.
This is the case for example for the Trail, Collision, OnDeath, SimpleTrigger, SimpleTriggerOnce templates, and all placement templates.
Example: collisions
The collision node exposes an “OnCollide” event pin. It triggers an event every time the particle collides with the scene. The collision node will setup xform payloads at the contact point.
Using xform nodes in any child layer will use the transforms information in the event payload, and consequently spawn the child particles at the location where the collision happened.
![]() | ![]() |
Without transform nodes in child | With transform nodes in child |
General guidelines
A general good rule of thumb to follow is:
- For particles that do not need to follow the effect instance:
Setup their spawn position, velocity, etc. in localspace, then insert a local->world xform node, and do the evolve stuff in worldspace.
Of course there are exceptions if you initialize the position from something which is already in worldspace, for example a world position you send from the game-engine via an attribute, or a 3d coordinate sent as a payload in the parent event, and manually extracted in the child layers. - For particles that need to follow the effect instance:
If their evolve is simple and can run in localspace (no collisions, no spatial layers, no trail, no scene queries, etc…), use a local to world xform node at evolve right before the renderer
Otherwise, the safest is to wire the evolve position, velocity, etc, into a localspace node. For example between spawn and a physics node, or between physics and render, both should work.
Another good rule of thumb is:
If there’s no xform node in a layer, there’s probably something wrong.