Echo: five laps where your past laps are the traffic

· 5 min read · game-dev, three.js, webgpu, racing, performance

A race car far ahead on a wet straight, trailing violet exhaust flames, with the low sun flaring beside a grandstand

Echo is a mode of Endurance Racing with a simple rule: you race five laps, and every lap you finish becomes traffic. It comes back as a hologram of your own car, replaying that lap on the circuit, and you have to pass it without touching it. A touch costs 3 seconds, and the score is the total time of the five laps plus the penalties. It is desktop only.

Lap 1 is the recording lap, with nothing in front of you. From lap 2, the last valid laps, at most four, replay as echoes.

Racing yourself does not work by default #

The first problem is not code, it is geometry. If an echo replays your lap at the speed you drove it, in the same direction, it never meets you. You would see holograms up the road and never reach one.

So an echo replays its lap at 0.75 times the speed it was driven. It is slower than you, so you catch it. I modelled how many echoes you meet per lap with a constant speed, where you pass the echo that starts off meters ahead if off ≤ (1 − pace) · lap length:

Pace Echoes met on laps 2 to 5
0.60 1, 2, 3, 4
0.75 1, 2, 3, 3
0.90 1, 1, 1, 1
1.00 0, 0, 0, 0

I then measured it in the browser with the scripted pilot I use for filming, which follows the same line every lap and never avoids anything. At 0.75 it met 0, 2, 3, 4 and 4 echoes per lap and touched 0, 1, 2, 3 and 3 of them. When the pilot alternated 3 meters to one side lap by lap, so that an echo was never on its line, the touches fell to 0, 0, 1, 1, 1. Passing 6 meters apart is never a touch.

Those are a robot's numbers. A person is the only real playtest of the pace, and I have left it as a knob: ?echopace=0.9 changes it for a session.

Recording a lap #

A lap is a list of poses. The recorder is the one that Photo Run uses: one pose per 1/60 s of simulation, written into an array allocated once. It records the car's root position and rotation and the pivots of the wheels, so the hologram has its suspension and its spinning wheels. It samples by simulation time, not by frame, so a 60 Hz and a 144 Hz screen record the same lap. A lap is about 0.9 MB.

A lap is not kept if it had a respawn (Backspace, or a fall) or lasted under 20 seconds.

Putting echoes ahead of you #

At every crossing of the line, the echoes are placed ahead of you, the newest nearest:

startOffsets: [ 300, 700, 1100, 1500 ], // meters ahead of the line
pace: 0.75,
penalty: 3,
grace: 2.5,    // no touches in the first seconds of a lap: they are fading in
lockout: 2,    // one contact is one penalty

Each lap start gives the echoes a new id, and the hologram pool fades a new id in over 700 ms and hides an old one at once. That one rule hides the fact that the whole set was moved.

An echo that reaches the line starts its lap again. The end of a lap and its start differ by a few meters sideways and a little in heading, so I carry that difference as an offset that fades out over 0.4 seconds. There is no teleport.

What counts as a touch #

Two oriented boxes overlapping: the chassis box of your car, shrunk by 12 cm, against the echo's, tested with the separating axis theorem on the ground plane. A hologram's edge is not a wall, hence the shrink. Only echoes within 30 meters are tested at all. After a touch that echo cannot be touched again for 2 seconds, and the first 2.5 seconds of a lap are free.

Holograms have no physics. Nothing pushes you, the penalty is the whole consequence.

One pool of holograms #

This is the part I like most. The echoes are drawn by the same pool of ghost cars that the public rooms use for other drivers. It holds at most four cars, each a baked copy of the real car of about 30,000 triangles, all sharing one additive material.

The pool is built and compiled during the normal startup, and the real car's hologram is baked during lap 1, three milliseconds per frame. So when the first echo appears there is no new program to compile and no new mesh to make. I measured it by wrapping the device's createShaderModule, createRenderPipeline and createComputePipeline: zero, zero and zero from the first lap to the result screen. On a page where building a shader at the wrong moment is a hitch, and on an older iPhone can end the page, that was the requirement that shaped everything else.

Echo and the online rooms never run together, because they would use the same pool. Entering a room turns Echo off.

What it costs #

While What runs Cost
Driving one pose per step, at most 4 pose interpolations, at most 4 box tests (only within 30 m) the recorder is Photo Run's: 0.09 µs a pose, about 5.5 KB per second
Drawing at most 4 meshes from the shared pool 4 echoes drawn against hidden, lap 5, 960×540: median 16.7 ms both, p95 18.5 against 18.6 ms
GPU objects while playing none made 0 modules, 0 render pipelines, 0 compute pipelines from the first lap to the result

The frame times are vsync-bound, so the difference is smaller than what the measurement can show.

What is missing #

  • A real playtest of the pace and of the 3 second penalty.
  • The echoes are always your current car. If you swap cars between runs, laps from the other car are not kept.
  • A set of echoes is replaced at every lap start, so an echo you were passing vanishes at the line.
  • I thought about an echo coming back toward you. It would be more dramatic and much harsher on blind corners, so I left it out.
  • There are no online records. The best time is saved in your browser only, because the room server bills by message.

Echo is in Endurance Racing, and the project page is here.

Related projects