Skip to content

Particles: allow motion in 3D space #1696

Description

@obiot

Summary

Particles are placed in 3D but move in 2D. A particle keeps the depth it was born at, for its whole life, so no particle effect can expand along Z or trail away from the camera.

Current state

  • emitter.ts:517-523 propagates the emitter's depth onto every new particle via addChild(child, z), and particle.ts:451 builds its absolute position as (pos.x, pos.y, depth). So a burst emitted at an enemy's depth projects and sorts correctly under a Camera3d: 3D placement works today.
  • particle.ts:27 declares vel: Vector2d (pooled at line 75), the launch is vel.set(speed * cos(angle), -speed * sin(angle)) (line 189), and integration touches pos.x/pos.y only (lines 284-285).
  • settings.ts has no Z term anywhere: no Z velocity, no Z gravity, no Z spread.

The consequence is that an explosion under a perspective camera is a flat disc facing the viewer rather than a sphere, and a trail cannot recede.

Evidence this bites

The AfterBurner example hand-rolls its vapour trail, and its own comment explains why:

Hand-rolled trail of additive Sprites, NOT a ParticleEmitter. Each node spawns at the engine outlet, then each frame its depth is increased so the node recedes away from the camera in world space.

That is roughly forty lines of bespoke particle system in an example whose job is to showcase engine features, written because the feature does not exist. Trail is no help either: addPoint(x, y) is 2D as well.

Proposal

Extend the existing class rather than adding a Particle3d sibling. Renderable.pos is already a 3D vector at runtime, the split would duplicate most of a 480 line class, need its own pool (particlePool is createPool<Particle, [emitter]>) and leave two integration loops to keep in step. It would also sit badly next to how the same 2D/3D question was settled for shaders and for Renderable.pos (#1510).

  1. Promote vel to Vector3d and integrate pos.z += vel.z * skew.
  2. Add an elevation angle (plus variation) to the settings, defaulting to 0. angle becomes azimuth. At elevation 0 all motion stays in the XY plane, so every existing effect is unchanged.
  3. Leave gravity and wind 2D for now. They are screen space stylings and a gravityZ has no obvious meaning until someone asks for one.

Performance

The 2D path must remain today's path, selected by a flag, not the 3D path with zeros in it:

  • Integration is two extra float ops per particle per frame, dwarfed by the alpha and scale interpolation each particle already pays. Memory is one more double per particle.
  • Spawn trig is the only real arithmetic: a 3D launch needs two more trig calls per particle spawned. Gate it, so an emitter with zero elevation and zero elevation variation runs exactly the two call path it runs now.
  • Sorting is the one that can bite. emitter.ts:263 sets autoSort = false with the comment "particle z-sort would be wasted work", which is true because every particle in an emitter shares the emitter's depth. Independent Z breaks that invariant. Additive blending is commutative so it needs no ordering, but settings.blendMode defaults to "normal" and supersedes the additive flag (particle.ts:173-177), and those modes do need back to front order. The sort should therefore key off the blend mode, not off the 3D flag.

Culling needs no work: the emitter already re-asserts inViewport for its children (emitter.ts:651) because its bounds do not cover them.

Relationship to #1404

#1404 moves particle physics into the vertex shader, where a Z component is free. That does not make this ticket redundant, it makes the ordering matter:

  • Store the launch as (azimuth, elevation, speed) rather than baking a velocity at spawn, so both paths derive from the same inputs and the CPU integration becomes the reference implementation the shader can be tested against.
  • GPU-instanced particle rendering #1404's instance layout should be vec3 position for the same reason (noted there).

This one is small and self contained and should not wait on that one.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions