DEV Community

Hiroshi TK
Hiroshi TK

Posted on

Wall-impact sparks: test the normal before tuning the glow

A laser impact needs two inputs from the game: a hit position and an outward surface normal. The particle effect should consume those inputs. It should not invent a collision result from a bright spot on a wall.

This small Three.js demo uses a fixed planar wall facing positive Z and a synthetic hit at its center. It emits 32 unlit particles and pauses after 18 fixed simulation steps so the direction is easy to inspect. The screenshot shows that constructed test, not a raycast from a shipped game.

Start with an axis you can name

The authored velocity range points outward along positive Z: X and Y each range from -0.8 to 0.8, while Z ranges from 0.4 to 1.1. Lifetime is 1.2 seconds. Continuous emission is zero. A small positive offset keeps the burst origin just in front of the wall.

In a real integration, transform a mesh-local hit normal into world space correctly before using it. A normal is a direction, not a position; translating it as though it were a point is a common orientation bug. Nonuniform scale requires the proper normal transformation.

const outward = hitNormal.clone().normalize();
const orientation = new THREE.Quaternion().setFromUnitVectors(
  new THREE.Vector3(0, 0, 1), outward
);
const angles = new THREE.Euler().setFromQuaternion(orientation);
const origin = hitPoint.clone().addScaledVector(outward, 0.02);
const burst = vfx.createEffect(effect, {
  position: origin.toArray(),
  rotation: [angles.x, angles.y, angles.z],
  seed: 14
});
burst.emitBurst(effect.emitters[0].id, {count: 32});
Enter fullscreen mode Exit fullscreen mode

effect here is an already prepared definition with the stated zero-rate emitter. Load a validated runtime bundle in a game. The fixed-wall demo uses an authoring definition to keep the experiment self-contained.

Verify the simple case before rotating the wall

On the test wall the outward normal is [0,0,1], so the quaternion is the identity. This is a useful baseline: particles should move into positive Z from an origin at Z=0.02. It is not evidence that every rotated or scaled wall has been tested.

The live lab prints the first debug particle position and active count. Inspect those alongside the screenshot. If the particle motion is correct but the picture looks wrong, investigate camera framing, transparency, depth, and scale before changing the collision normal.

The source below gives the critical emitter settings. Build them on a default effect, mount a ThreeVfxRenderer in a scene, and update it once per frame in seconds.

const e = effect.emitters[0];
e.spawn.rate = 0;
e.spawn.rateValue = scalar(0);
e.spawn.shape = 'point';
e.initializeParticle.lifetime = scalar(1.2);
e.initializeParticle.velocity.mode = 'vector';
e.initializeParticle.velocity.min = [-0.8, -0.8, 0.4];
e.initializeParticle.velocity.max = [0.8, 0.8, 1.1];
Enter fullscreen mode Exit fullscreen mode

Make teardown part of the demonstration

Remove the effect after its particles finish, or reuse it through an explicit ownership policy. An active looping emitter with a zero continuous rate can still exist after the visible burst is gone; invisibility is not the same as disposal. The lab's Destroy instances button removes the created instances and reports the remaining count.

When the scene itself leaves, destroy the VFX renderer, release scene-owned GPU resources, and remove any event listeners or animation-loop callbacks owned by that scene. Do not keep the old renderer alive behind a newly mounted view.

A web game VFX editor can provide the authored appearance, while the host owns contact detection and lifecycle. The NixieFX Three.js runtime reference documents the adapter calls used in this boundary.

Limits

The demonstration does not implement raycasting, ricochet, damage, decals, surface-material selection, or collision response for the emitted particles. It verifies one known normal and a visible burst. Test rotated walls and transformed parents separately before generalizing the result.

Preview bloom is not exported host postprocessing. The recipe is unlit and does not need mesh-surface emission. Those choices also avoid features that PixiJS does not support, but the screenshot is still a Three.js result rather than a cross-renderer visual comparison. GPU frame time and mobile performance remain separate measurements.

Three.js wall-impact burst paused at frame 18, with 32 active particles and an outward positive-Z position

Top comments (0)