b3d-trail

A ribbon left behind by anything that moves: contrails, missile smoke, the wake of a fast enemy. Nest it in the thing that moves; it attaches to that thing's mesh, offset in its local space, and draws a TrailMesh from there.

Lifted from manta-recon's engine contrails (tosijs-3d#4, board #186), revived from the 2010 game: very translucent (a contrail is vapour catching light, not a light source), hidden when slow, and tinted differently under water.

Demo

A drone circles with two engine trails, dipping under the water and back. The trails change tint as they cross the surface.

import { b3d, b3dBox, b3dTrail, b3dWater, b3dLight, b3dSkybox, b3dGround, sceneDelta } from 'tosijs-3d'
import { orbitCam } from 'tosijs-3d/demo-utils'

const drone = b3dBox(
  { x: 12, y: 3, z: 0, width: 1.2, height: 0.3, depth: 1.6, color: '#3388dd' },
  b3dTrail({ x: 0.45, y: 0, z: -0.8, diameter: 0.12, length: 60, alpha: 0.35, minSpeed: 2, underwaterColor: '#60e0ff' }),
  b3dTrail({ x: -0.45, y: 0, z: -0.8, diameter: 0.12, length: 60, alpha: 0.35, minSpeed: 2, underwaterColor: '#60e0ff' }),
)

preview.append(
  b3d(
    {
      sceneCreated(el) {
        orbitCam(el, { alpha: -Math.PI / 2.2, beta: Math.PI / 2.6, radius: 34, target: [0, 1, 0] })
        let a = 0
        el.scene.onBeforeRenderObservable.add(() => {
          a += sceneDelta(el.scene)
          drone.x = Math.cos(a * 0.6) * 12
          drone.z = Math.sin(a * 0.6) * 12
          drone.y = 1 + Math.sin(a * 0.9) * 3 // under the water and out again
          drone.ry = (-a * 0.6 * 180) / Math.PI
        })
      },
    },
    b3dLight({ y: 1, intensity: 0.9 }),
    b3dSkybox({ timeOfDay: 16, realtimeScale: 0 }),
    b3dGround({ width: 80, height: 80, y: -6, color: '#4a5a44' }),
    b3dWater({ y: 0, waterSize: 120, twoSided: true }),
    drone,
  )
)
tosi-b3d { width: 100%; height: 100%; }

Attributes

Attribute Default Description
x y z 0 Where the ribbon starts, in the HOST's local space (an engine pod, a nozzle)
diameter 0.06 Ribbon width (m)
length 45 Ribbon length, in segments (one a frame, so length ≈ how many frames of history)
color '#ffffff' Its colour (emissive, unlit)
underwaterColor '' The colour below a <tosi-b3d-water> surface; empty = same as color
alpha 0.16 Opacity. Low on purpose: you should see through a contrail
minSpeed 8 Below this speed (m/s, measured at the emitter) the ribbon fades out, so a hover does not smear

In code

attachTrail(node, scene, options) does the same for a node you hold, such as a missile you spawned yourself, and returns { update(dt), reset(), dispose() }.

It never throws inside the render loop: an exception in a Babylon observer skips every observer after it, which manta found the hard way when a trail error silently killed the aircraft's camera follow.