41 pinball lamps in one material, with an ID map
· 1 min read · shaders, tsl, game-dev, pinball, textures
The playfield of Graveyard Pinball has 41 individually lit lamps that glow as the ball scores. They are not 41 objects. They are one material and three small maps, all generated by a script in world coordinates, so a lamp sits exactly where it is in the 3D table.
The three maps #
| map | what it holds |
|---|---|
| board art | the playfield with every insert OFF, dark glass |
| lit map | every insert LIT: color plus a tight halo, black elsewhere |
| id map | an 8-bit lamp id per pixel, 0 for none, sampled with NEAREST and no mipmaps |
A JSON manifest lists every lamp: id, name, group, color and position, plus bind, which ties a lamp to a reactive prop on the table.
About ten lines of shader #
Read the id at this pixel. If it is not zero, look up that lamp's level in a one-row texture with one float per lamp, and add lit(uv) * level * gain to the board's own emissive. One material, one draw call, any number of lamps, each with its own brightness. A level above 1 pushes the color into the bloom.
With WebGPU, the same math is a TSL emissive node: the ids texture is read with an integer texel fetch so there is no filtering.
The API a game needs #
lamps.set('letter_3_A', 1) // sustained, >1 overdrive
lamps.flash('bumper_1', { peak: 1.5 })
lamps.chase('letters', { step: 0.07 })
lamps.bindFlash('cauldron_01') // flash every lamp bound to that prop
The rules, what lights what, belong to the game: cauldron hits spell GRAVEYARD, three lanterns make the jackpot, a bank of tombs advances the multiplier.
Changing the playfield #
Because the art and the maps are generated, editing the layout means editing the generator, then re-running it. Lamp shapes are small paths: circle, ring, tomb, diamond, star, chevron, pill and skull.
The display that complements them is in the DMD post. The project is on its project page.
Related projects
- Graveyard Pinball — Free Halloween game