luv

Workshop wiki

fireworks.lisp

luvcraft/birthday/fireworks.lisp

system luvcraft/birthday · 38 definitions · on GitHub

Fireworks: a great big show over the meadow at dusk.

Contract: (add-birthday-fireworks session &key origin) registers one scene overlay running a continuous show: shells launch from near ORIGIN every few seconds, ascend with a sparkling trail, and burst into sphere, ring, and willow patterns of a few hundred sparks each. The spark population is simulated on the CPU in refresh-luvcraft-overlay (gravity, drag, fade) and drawn as one instanced draw of small glowing billboards whose radiance clears the bloom threshold, so the lens makes them blaze. Per-spark colour comes from the shell's palette. (stop-birthday-fireworks session) releases the overlay.

Shader sections live under (in-package #:luvcraft.shaders).

The population lives in preallocated single-float columns inside a plain FIREWORK-SHOW structure, so the whole simulation steps -- and tests -- without a GPU anywhere near it. The per-frame work is one pop pass, one integrate-and-compact pass, and one instance-packing pass, all closed loops over specialized arrays that allocate nothing. Chance never enters: every wobble, tilt, and palette is a hash of a counter, so a replayed show lands every shell in the same place.

in-package#:luvcraft.birthday

--------------------------------------------------------------------- Deterministic dials

The show wants variety, not surprise. CL:RANDOM never appears here: every decision is a hash of the shell or spark counter and a small salt naming which decision is being made, folded down to a dial in [0,1). The arithmetic stays under a 32-bit mask, which SBCL compiles modularly, so no shell number ever conses a bignum.

declaim
defunfirework-mix
word

One 32-bit avalanche round over word.

declare
type
unsigned-byte32
word
optimize
speed3
let*
word
logand
*word2654435761
#xffffffff
word
logxorword
ashword-15
word
logand
*word2246822519
#xffffffff
word
logxorword
ashword-13
word
declaim
ftype
function
fixnum
unsigned-byte32
single-float
firework-dial
defunfirework-dial
indexsalt

A deterministic dial in [0,1) for index, distinct per salt.

declare
typefixnumindex
type
unsigned-byte32
salt
optimize
speed3
/
float
firework-mix
logxor
logand
*
logandindex#xffffffff
2654435761
#xffffffff
firework-mix
logand
+salt1013904223
#xffffffff
1.0
4294967296.0

--------------------------------------------------------------------- The show's state: dense columns behind one semantic owner

A spark has no identity worth an object: it is a row in fifteen parallel single-float lanes owned by the show. Stable in-place compaction keeps the lanes in spawn order, so index zero is always the oldest survivor and "drop the oldest" is one leftward REPLACE per column. Rockets are few enough to sit in eight fixed slots scanned whole.

defconstant+firework-spark-capacity+4096"The most sparks alive at once; past it the oldest are dropped."
defconstant+firework-rocket-capacity+8"The most shells climbing at once, comfortably more than the schedule asks."
defparameter*firework-gravity*5.0

Cells per second squared pulling sparks down. Dreamier than the world's own gravity on purpose: burst blooms should hang, and willows should droop slowly enough to watch.

defparameter*firework-launch-interval*1.8"Seconds between shells before the per-shell spread is added."
defparameter*firework-launch-spread*1.2"How many extra seconds the launch dial can add to the interval."
defparameter*firework-trail-rate*26.0"Trail sparks a climbing rocket sheds per second."
defparameter*firework-grandeur*2.6

Scales every spark's drawn size and glow at pack time, leaving the simulation untouched. One is the show as simulated; from across a meadow the sparks are subpixel at one, and grandeur is what keeps a distant burst legible instead of averaging away to dust.

defstruct
firework-show
:constructor%make-firework-show
:copiernil

One continuous fireworks show: schedule, rockets, and spark columns. Plain CPU state over preallocated arrays; it steps without a GPU.

Where shells rise from.

origin-x0.0:typesingle-float
origin-y0.0:typesingle-float
origin-z0.0:typesingle-float

The schedule and the running counters every dial hangs off.

countdown0.75:typesingle-float
shell-counter0:typefixnum
sparkle-counter0:typefixnum

What has happened so far, for tests and curiosity.

launch-count0:typefixnum
burst-count0:typefixnum
retired-count0:typefixnum

The spark columns.

capacity+firework-spark-capacity+:typefixnum
spark-count0:typefixnum
spark-x
make-array0:element-type'single-float
:type
simple-arraysingle-float
spark-y
make-array0:element-type'single-float
:type
simple-arraysingle-float
spark-z
make-array0:element-type'single-float
:type
simple-arraysingle-float
spark-vx
make-array0:element-type'single-float
:type
simple-arraysingle-float
spark-vy
make-array0:element-type'single-float
:type
simple-arraysingle-float
spark-vz
make-array0:element-type'single-float
:type
simple-arraysingle-float
spark-red
make-array0:element-type'single-float
:type
simple-arraysingle-float
spark-green
make-array0:element-type'single-float
:type
simple-arraysingle-float
spark-blue
make-array0:element-type'single-float
:type
simple-arraysingle-float
spark-size
make-array0:element-type'single-float
:type
simple-arraysingle-float
spark-glow
make-array0:element-type'single-float
:type
simple-arraysingle-float
spark-age
make-array0:element-type'single-float
:type
simple-arraysingle-float
spark-life
make-array0:element-type'single-float
:type
simple-arraysingle-float
spark-drag
make-array0:element-type'single-float
:type
simple-arraysingle-float

Seconds until this spark pops into secondaries; zero means never.

spark-pop
make-array0:element-type'single-float
:type
simple-arraysingle-float

The rocket slots.

rocket-flag
make-array0:element-type'
unsigned-byte8
:type
simple-array
unsigned-byte8
rocket-x
make-array0:element-type'single-float
:type
simple-arraysingle-float
rocket-y
make-array0:element-type'single-float
:type
simple-arraysingle-float
rocket-z
make-array0:element-type'single-float
:type
simple-arraysingle-float
rocket-vx
make-array0:element-type'single-float
:type
simple-arraysingle-float
rocket-vy
make-array0:element-type'single-float
:type
simple-arraysingle-float
rocket-vz
make-array0:element-type'single-float
:type
simple-arraysingle-float
rocket-fuse
make-array0:element-type'single-float
:type
simple-arraysingle-float
rocket-trail
make-array0:element-type'single-float
:type
simple-arraysingle-float
rocket-pattern
make-array0:element-type'
unsigned-byte8
:type
simple-array
unsigned-byte8
rocket-palette
make-array0:element-type'
unsigned-byte8
:type
simple-array
unsigned-byte8
rocket-seed
make-array0:element-type'
unsigned-byte32
:type
simple-array
unsigned-byte32
defunmake-firework-show
&key
origin'
0.00.00.0

Make a show ready to step, launching from near origin, a list (X Y Z).

flet
column
make-arraycapacity:element-type'single-float:initial-element0.0
slots
make-array+firework-rocket-capacity+:element-type'single-float:initial-element0.0
%make-firework-show:origin-x
coerce
eltorigin0
'single-float
:origin-y
coerce
eltorigin1
'single-float
:origin-z
coerce
eltorigin2
'single-float
:capacitycapacity:spark-x
column
:spark-y
column
:spark-z
column
:spark-vx
column
:spark-vy
column
:spark-vz
column
:spark-red
column
:spark-green
column
:spark-blue
column
:spark-size
column
:spark-glow
column
:spark-age
column
:spark-life
column
:spark-drag
column
:spark-pop
column
:rocket-flag
make-array+firework-rocket-capacity+:element-type'
unsigned-byte8
:initial-element0
:rocket-x
slots
:rocket-y
slots
:rocket-z
slots
:rocket-vx
slots
:rocket-vy
slots
:rocket-vz
slots
:rocket-fuse
slots
:rocket-trail
slots
:rocket-pattern
make-array+firework-rocket-capacity+:element-type'
unsigned-byte8
:initial-element0
:rocket-palette
make-array+firework-rocket-capacity+:element-type'
unsigned-byte8
:initial-element0
:rocket-seed
make-array+firework-rocket-capacity+:element-type'
unsigned-byte32
:initial-element0

--------------------------------------------------------------------- Admitting sparks

defunreserve-firework-room
showwanted

Make room for wanted sparks, dropping the oldest when the cap bites. The columns are in spawn order, so dropping is one leftward shift each.

declare
typefixnumwanted
optimize
speed3
let*
count
firework-show-spark-countshow
spill
-
+countwanted
firework-show-capacityshow
declare
typefixnumcountspill
when
pluspspill
macrolet
shift-columns
&restreaders
`
progn,@
loopforreaderinreaderscollect`
let
column
,readershow
replacecolumncolumn:start2spill:end2count
shift-columnsfirework-show-spark-xfirework-show-spark-yfirework-show-spark-zfirework-show-spark-vxfirework-show-spark-vyfirework-show-spark-vzfirework-show-spark-redfirework-show-spark-greenfirework-show-spark-bluefirework-show-spark-sizefirework-show-spark-glowfirework-show-spark-agefirework-show-spark-lifefirework-show-spark-dragfirework-show-spark-pop
incf
firework-show-retired-countshow
spill
setf
firework-show-spark-countshow
-countspill
show
declaim
defunemit-firework-spark
showxyzvxvyvzredgreenbluesizeglowlifedragpop

Append one spark when a slot is free and say whether it was admitted. Volleys call reserve-firework-room first; strays simply yield at the cap.

declare
typesingle-floatxyzvxvyvzredgreenbluesizeglowlifedragpop
optimize
speed3
let
count
firework-show-spark-countshow
declare
typefixnumcount
when
<count
firework-show-capacityshow
setf
aref
firework-show-spark-xshow
count
x
aref
firework-show-spark-yshow
count
y
aref
firework-show-spark-zshow
count
z
aref
firework-show-spark-vxshow
count
vx
aref
firework-show-spark-vyshow
count
vy
aref
firework-show-spark-vzshow
count
vz
aref
firework-show-spark-redshow
count
red
aref
firework-show-spark-greenshow
count
green
aref
firework-show-spark-blueshow
count
blue
aref
firework-show-spark-sizeshow
count
size
aref
firework-show-spark-glowshow
count
glow
aref
firework-show-spark-ageshow
count
0.0
aref
firework-show-spark-lifeshow
count
life
aref
firework-show-spark-dragshow
count
drag
aref
firework-show-spark-popshow
count
pop
firework-show-spark-countshow
1+count
t

--------------------------------------------------------------------- Burst geometry and palettes

defunfirework-sphere-direction
kn

The Kth of N points of a Fibonacci sphere: uniform without chance.

declare
typefixnumkn
optimize
speed3
let*
y
-1.0
/
*2.0
+
floatk1.0
0.5
floatn1.0
ring
sqrt
max0.0
-1.0
*yy
theta
*2.3999632
floatk1.0
values
*ring
costheta
y
*ring
sintheta
defunfirework-spark-color
paletteknseed

The linear rgb the Kth of N sparks wears under palette, dialed by seed.

declare
typefixnumpaletteknseed
optimize
speed3
let
dial
ecasepalette
0

Gold, from ember-orange up toward straw.

values1.0
+0.6
*0.24dial
+0.2
*0.18dial
1

Silver-white, cool and slightly blued.

values
+0.84
*0.1dial
+0.9
*0.06dial
1.0
2

Pink and cyan, alternating around the shell.

if
evenpk
values1.00.380.62
values0.320.881.0
3

Green and violet, likewise.

if
evenpk
values0.421.00.5
values0.720.421.0
4

Rainbow: the whole wheel once around the shell.

let
hue
values
+0.5
*0.5
cos
*6.2831855hue
+0.5
*0.5
cos
*6.2831855
-hue0.33333334
+0.5
*0.5
cos
*6.2831855
-hue0.6666667
defunburst-firework-peony
showxyzvxvyvzpaletteseednspeedpop-p

A sphere of sparks: the classic bloom. With pop-p each spark carries a fuse of its own and pops into a handful of hot secondaries -- the double burst.

declare
typesingle-floatxyzvxvyvzspeed
typefixnumpaletteseedn
optimize
speed3
dotimes
kn
multiple-value-bind
dxdydz
let
v
*speed
+0.85
*0.3
multiple-value-bind
redgreenblue
emit-firework-sparkshowxyz
+
*dxv
*vx0.2
+
*dyv
*vy0.2
+
*dzv
*vz0.2
redgreenblue0.095
+5.0
*2.0
+1.6
*0.8
0.85
ifpop-p
+0.5
*0.3
0.0
defunburst-firework-ring
showxyzvxvyvzpaletteseed

A tilted circle of sparks, evenly spaced, all at one speed.

declare
typesingle-floatxyzvxvyvz
typefixnumpaletteseed
optimize
speed3
let*
n96
tilt
+0.4
spin
*6.2831855

The ring's normal, then an orthonormal basis in its plane. TILT stays well away from zero, so the cross with world up never degenerates.

nx
*
sintilt
cosspin
ny
costilt
nz
*
sintilt
sinspin
len
sqrt
+
*nxnx
*nznz
ux
/
-nz
len
uz
/nxlen
wx
*nyuz
wy
-
*nzux
*nxuz
wz
-
*nyux
dotimes
kn
let*
theta
/
*6.2831855
floatk1.0
floatn1.0
ct
costheta
st
sintheta
v
*7.0
+0.95
*0.1
multiple-value-bind
redgreenblue
emit-firework-sparkshowxyz
+
*
+
*uxct
*wxst
v
*vx0.2
+
*
*wyst
v
*vy0.2
+
*
+
*uzct
*wzst
v
*vz0.2
redgreenblue0.09
+5.5
*1.5
+1.7
*0.5
0.80.0
defunburst-firework-willow
showxyzvxvyvzseed

Fewer, slower, heavier sparks that drag hard and droop long: always gold, because a willow is.

declare
typesingle-floatxyzvxvyvz
typefixnumseed
optimize
speed3
let
n130
dotimes
kn
multiple-value-bind
dxdydz

Lift the sphere toward the sky so the droop has somewhere to fall from.

let*
ly
+
*dy0.55
0.4
len
sqrt
+
*dxdx
*lyly
*dzdz
v
/
*4.6
+0.8
*0.4
len
multiple-value-bind
redgreenblue
emit-firework-sparkshowxyz
+
*dxv
*vx0.2
+
*lyv
*vy0.2
+
*dzv
*vz0.2
redgreenblue0.08
+3.6
*1.2
+3.2
*1.0
1.50.0
defunpop-firework-spark
showi

The delight of a double shell: spark I quietly pops into a handful of tiny hot sparks of its own, whitened past its parent's colour. At the cap the pop simply fizzles; nothing shifts under the caller's sweep.

declare
typefixnumi
optimize
speed3
let*
x
aref
firework-show-spark-xshow
i
y
aref
firework-show-spark-yshow
i
z
aref
firework-show-spark-zshow
i
vx
*
aref
firework-show-spark-vxshow
i
0.35
vy
*
aref
firework-show-spark-vyshow
i
0.35
vz
*
aref
firework-show-spark-vzshow
i
0.35
red
min1.0
+
aref
firework-show-spark-redshow
i
0.3
green
min1.0
+
aref
firework-show-spark-greenshow
i
0.3
blue
min1.0
+
aref
firework-show-spark-blueshow
i
0.3
n
incf
firework-show-sparkle-countershow
dotimes
k5
multiple-value-bind
dxdydz
emit-firework-sparkshowxyz
+vx
*dx2.1
+vy
*dy2.1
+vz
*dz2.1
redgreenblue0.0656.51.80.0

--------------------------------------------------------------------- Shells: launching, climbing, bursting

defunlaunch-firework-shell
showshell

Send shell number shell up from near the origin, its tilt, fuse, pattern, and palette all dialed off its number.

declare
typefixnumshell
optimize
speed3
let
slot
loopforsbelow+firework-rocket-capacity+when
zerop
aref
firework-show-rocket-flagshow
s
returns
whenslot
incf
firework-show-launch-countshow
setf
aref
firework-show-rocket-flagshow
slot
1
aref
firework-show-rocket-xshow
slot
+
firework-show-origin-xshow
*1.5
aref
firework-show-rocket-yshow
slot
firework-show-origin-yshow
aref
firework-show-rocket-zshow
slot
+
firework-show-origin-zshow
*1.5
aref
firework-show-rocket-vxshow
slot
*2.2
aref
firework-show-rocket-vyshow
slot
+9.5
aref
firework-show-rocket-vzshow
slot
*2.2
aref
firework-show-rocket-fuseshow
slot
+1.55
aref
firework-show-rocket-trailshow
slot
0.0

The first shell is a classic peony; after that the dial chooses, leaning peony.

aref
firework-show-rocket-patternshow
slot
if
zeropshell
0
let
cond
<d0.45
0

peony

<d0.65
1

ring

<d0.85
2

willow

t3

double peony Every seventh shell wears the rainbow, finale-ish.

aref
firework-show-rocket-paletteshow
slot
if
=6
modshell7
4
aref
firework-show-rocket-seedshow
slot
firework-mix
logand
*
+shell1
2654435761
#xffffffff
defunemit-firework-trail
showslot

Shed one gold ember behind the rocket in slot.

declare
typefixnumslot
optimize
speed3
let*
n
incf
firework-show-sparkle-countershow
emit-firework-sparkshow
aref
firework-show-rocket-xshow
slot
aref
firework-show-rocket-yshow
slot
aref
firework-show-rocket-zshow
slot
+
*
aref
firework-show-rocket-vxshow
slot
0.3
*jx1.6
+
*
aref
firework-show-rocket-vyshow
slot
0.2
*jy1.6
+
*
aref
firework-show-rocket-vzshow
slot
0.3
*jz1.6
1.00.780.450.052.8
+0.3
2.60.0
defunburst-firework-shell
showslot

The rocket in slot has reached its fuse: bloom, and free the slot.

declare
typefixnumslot
optimize
speed3
let
x
aref
firework-show-rocket-xshow
slot
y
aref
firework-show-rocket-yshow
slot
z
aref
firework-show-rocket-zshow
slot
vx
aref
firework-show-rocket-vxshow
slot
vy
aref
firework-show-rocket-vyshow
slot
vz
aref
firework-show-rocket-vzshow
slot
palette
aref
firework-show-rocket-paletteshow
slot
seed
aref
firework-show-rocket-seedshow
slot
incf
firework-show-burst-countshow
setf
aref
firework-show-rocket-flagshow
slot
0
ecase
aref
firework-show-rocket-patternshow
slot
0
burst-firework-peonyshowxyzvxvyvzpaletteseed2406.2nil
1
2
burst-firework-willowshowxyzvxvyvzseed
3
burst-firework-peonyshowxyzvxvyvzpaletteseed1106.5t

--------------------------------------------------------------------- The step: schedule, rockets, sparks

defunstep-firework-schedule
showdt
declare
typesingle-floatdt
optimize
speed3
let
countdown
-
firework-show-countdownshow
dt
if
pluspcountdown
setf
firework-show-countdownshow
countdown
let
shell
firework-show-shell-countershow
setf
firework-show-shell-countershow
1+shell
firework-show-countdownshow
+
coerce*firework-launch-interval*'single-float
*
coerce*firework-launch-spread*'single-float
defunstep-firework-rockets
showdt
declare
typesingle-floatdt
optimize
speed3
let
gravity
coerce*firework-gravity*'single-float
rate
coerce*firework-trail-rate*'single-float
dotimes
when
plusp
aref
firework-show-rocket-flagshow
slot
let
fuse
-
aref
firework-show-rocket-fuseshow
slot
dt
cond
pluspfuse
let*
decay
max0.0
-1.0
*0.25dt
vx
*
aref
firework-show-rocket-vxshow
slot
decay
vy
-
*
aref
firework-show-rocket-vyshow
slot
decay
vz
*
aref
firework-show-rocket-vzshow
slot
decay
setf
aref
firework-show-rocket-vxshow
slot
vx
aref
firework-show-rocket-vyshow
slot
vy
aref
firework-show-rocket-vzshow
slot
vz
aref
firework-show-rocket-fuseshow
slot
fuse
incf
aref
firework-show-rocket-xshow
slot
*vxdt
incf
aref
firework-show-rocket-yshow
slot
*vydt
incf
aref
firework-show-rocket-zshow
slot
*vzdt

The trail: a fractional emission accumulator, so the sparkle density does not depend on the frame rate.

let
carry
+
aref
firework-show-rocket-trailshow
slot
*ratedt
loopwhile
>=carry1.0
do
decfcarry1.0
setf
aref
firework-show-rocket-trailshow
slot
carry
defunstep-firework-sparks
showdt
declare
typesingle-floatdt
optimize
speed3

Pops first, over the population that began the frame: a popping spark appends its secondaries past COUNT, and the integrator below picks the newborns up in the same sweep. Emission during this pass only ever appends -- pop-firework-spark never shifts -- so the indices hold.

let
count
firework-show-spark-countshow
pops
firework-show-spark-popshow
declare
typefixnumcount
dotimes
icount
let
pop
arefpopsi
when
plusppop
let
left
-popdt
cond
pluspleft
setf
arefpopsi
left
t
setf
arefpopsi
0.0

Age, integrate, and compact in place. Survivors slide left, so the columns stay in spawn order and index zero stays the oldest spark.

let
xs
firework-show-spark-xshow
ys
firework-show-spark-yshow
zs
firework-show-spark-zshow
vxs
firework-show-spark-vxshow
vys
firework-show-spark-vyshow
vzs
firework-show-spark-vzshow
reds
firework-show-spark-redshow
greens
firework-show-spark-greenshow
blues
firework-show-spark-blueshow
sizes
firework-show-spark-sizeshow
glows
firework-show-spark-glowshow
ages
firework-show-spark-ageshow
lives
firework-show-spark-lifeshow
drags
firework-show-spark-dragshow
pops
firework-show-spark-popshow
gravity
coerce*firework-gravity*'single-float
count
firework-show-spark-countshow
write0
declare
typefixnumcountwrite
dotimes
readcount
let
age
+
arefagesread
dt
when
<age
areflivesread
let*
decay
max0.0
-1.0
*
arefdragsread
dt
vx
*
arefvxsread
decay
vy
-
*
arefvysread
decay
vz
*
arefvzsread
decay
setf
arefxswrite
+
arefxsread
*vxdt
arefyswrite
+
arefysread
*vydt
arefzswrite
+
arefzsread
*vzdt
arefvxswrite
vx
arefvyswrite
vy
arefvzswrite
vz
arefredswrite
arefredsread
arefgreenswrite
arefgreensread
arefblueswrite
arefbluesread
arefsizeswrite
arefsizesread
arefglowswrite
arefglowsread
arefageswrite
age
arefliveswrite
areflivesread
arefdragswrite
arefdragsread
arefpopswrite
arefpopsread
incfwrite
incf
firework-show-retired-countshow
-countwrite
setf
firework-show-spark-countshow
write
defunstep-firework-show
showdt

Advance show by dt seconds: schedule, fly, burst, age, retire. Plain arithmetic over the show's own columns; safe without a GPU.

let
dt
coercedt'single-float
show

--------------------------------------------------------------------- Packing the instance lanes

Each instance is two vec4s: (center.xyz, radius) and (colour.rgb, intensity). A young spark's intensity sits well above the lens's bright-pass threshold of 1.5, so the bloom chain does the blazing; the last three tenths of a life fade it out. The climbing rocket heads ride along at the end as bright white-gold comets.

defunfill-firework-instances
showdata

Pack the live population into data, stride eight floats per instance, and return how many instances are ready to draw.

declare
type
simple-arraysingle-float
data
optimize
speed3
let
count
firework-show-spark-countshow
xs
firework-show-spark-xshow
ys
firework-show-spark-yshow
zs
firework-show-spark-zshow
reds
firework-show-spark-redshow
greens
firework-show-spark-greenshow
blues
firework-show-spark-blueshow
sizes
firework-show-spark-sizeshow
glows
firework-show-spark-glowshow
ages
firework-show-spark-ageshow
lives
firework-show-spark-lifeshow
grandeur
coerce*firework-grandeur*'single-float
filled0
declare
typefixnumcountfilled
typesingle-floatgrandeur
dotimes
icount
let*
base
*filled8
worn
/
arefagesi
areflivesi
fade
min1.0
*
-1.0worn
3.3333333
setf
arefdatabase
arefxsi
arefdata
+base1
arefysi
arefdata
+base2
arefzsi
arefdata
+base3
*
arefsizesi
grandeur
arefdata
+base4
arefredsi
arefdata
+base5
arefgreensi
arefdata
+base6
arefbluesi
arefdata
+base7
*
arefglowsi
fadegrandeur
incffilled
dotimes
when
plusp
aref
firework-show-rocket-flagshow
slot
let
base
*filled8
setf
arefdatabase
aref
firework-show-rocket-xshow
slot
arefdata
+base1
aref
firework-show-rocket-yshow
slot
arefdata
+base2
aref
firework-show-rocket-zshow
slot
arefdata
+base3
0.12
arefdata
+base4
1.0
arefdata
+base5
0.92
arefdata
+base6
0.72
arefdata
+base7
7.0
incffilled
filled

--------------------------------------------------------------------- The overlay: the show's seat in the session

The overlay owns the GPU resources and the frame clock; the show inside it owns nothing but arrays. Refresh runs before vertex building, so what it packs is on screen the same frame.

defclassfireworks-overlay
show:initarg:show:readerfireworks-overlay-show
pipeline:initarg:pipeline:accessorfireworks-overlay-pipeline
vertex-buffer:initarg:vertex-buffer:accessorfireworks-overlay-vertex-buffer
instance-buffer:initarg:instance-buffer:accessorfireworks-overlay-instance-buffer
instance-data:initarg:instance-data:readerfireworks-overlay-instance-data
instance-count:initform0:accessorfireworks-overlay-instance-count
last-refresh:initformnil:accessorfireworks-overlay-last-refresh
defmethodluvcraft::luvcraft-overlay-live-shader-pipelines
list
fireworks-overlay-pipelineoverlay
defmethodluvcraft:refresh-luvcraft-overlay
session
declare
ignoresession
let*
now
get-internal-real-time
last
shiftf
fireworks-overlay-last-refreshoverlay
now
seconds
iflast
min0.1
/
float
-nowlast
1.0
internal-time-units-per-second
0.0
show
fireworks-overlay-showoverlay
let
count
fill-firework-instancesshow
fireworks-overlay-instance-dataoverlay
setf
fireworks-overlay-instance-countoverlay
count

One queue write of the whole preallocated lane; the draw below only reads the live prefix.

when
pluspcount
luv:write-buffer
fireworks-overlay-instance-bufferoverlay
fireworks-overlay-instance-dataoverlay
overlay
defmethodluvcraft:encode-luvcraft-overlay
sessionpasssurface-texture
let
count
fireworks-overlay-instance-countoverlay
when
pluspcount
let
luv:set-pipelinepass
luvcraft::live-shader-pipeline-native-pipeline
fireworks-overlay-pipelineoverlay
luv:set-vertex-bufferpass0
fireworks-overlay-vertex-bufferoverlay
luv:set-vertex-bufferpass1
fireworks-overlay-instance-bufferoverlay
luv:set-bind-grouppass0
luvcraft::luvcraft-frame-scene-bind-groupframe
luv:drawpass6count
overlay
defmethodluvcraft:release-luvcraft-overlay
when
fireworks-overlay-pipelineoverlay
luvcraft::release-live-shader-pipeline
fireworks-overlay-pipelineoverlay
setf
fireworks-overlay-pipelineoverlay
nil
dolist
resource
list
fireworks-overlay-instance-bufferoverlay
fireworks-overlay-vertex-bufferoverlay
whenresource
luv:destroyresource
setf
fireworks-overlay-instance-bufferoverlay
nil
fireworks-overlay-vertex-bufferoverlay
nil
values
defunfind-fireworks-overlay
session
find-if
lambda
overlay
typepoverlay'fireworks-overlay
luvcraft:luvcraft-session-overlayssession
defundefault-firework-origin
session

Where the show stands when nobody says: two dozen cells ahead of the camera along the ground, so the first shell rises into view.

let*
camera
luvcraft:luvcraft-session-camerasession
defunadd-birthday-fireworks
session&keyorigin

Start the show: register one scene overlay whose shells rise from near origin, a list (X Y Z), ahead of the camera by default. Returns the overlay; calling again while a show runs returns the running one.

or
let*
device
luvcraft:luvcraft-session-devicesession
instance-data
make-array
*8
+
firework-show-capacityshow
+firework-rocket-capacity+
:element-type'single-float:initial-element0.0
vertex-buffernil
instance-buffernil
pipelinenil
completed-pnil
unwind-protect
progn
setfvertex-buffer
luv:createdevice
luv:make-buffer-descriptor:label"birthday spark quad":size
*4
lengthvertex-data
:usage'
:vertex:copy-dst
instance-buffer
luv:createdevice
luv:make-buffer-descriptor:label"birthday spark instances":size
*4
lengthinstance-data
:usage'
:vertex:copy-dst
pipeline
luvcraft::make-live-shader-pipeline:role:birthday-spark:vertex-role:birthday-spark:label"birthday spark pipeline":devicedevice:layout
luvcraft::live-shader-pipeline-layout
:vertex-buffers'
:array-stride12:attributes
:shader-location0:offset0:format:float32x3
:array-stride32:step-mode:instance:attributes
:shader-location1:offset0:format:float32x4
:shader-location2:offset16:format:float32x4
:target-formatluvcraft::+luvcraft-scene-color-format+:target-blend:premultiplied-alpha:primitive'
:topology:triangle-list
:depth-stencil'
:format:depth32-float:depth-write-enablednil:depth-compare:less
luv:write-buffervertex-buffervertex-data
let
overlay
make-instance'fireworks-overlay:showshow:pipelinepipeline:vertex-buffervertex-buffer:instance-bufferinstance-buffer:instance-datainstance-data
setfcompleted-pt
unlesscompleted-p
whenpipeline
wheninstance-buffer
ignore-errors
luv:destroyinstance-buffer
whenvertex-buffer
ignore-errors
luv:destroyvertex-buffer
defunstop-birthday-fireworks
session

End the show: release the fireworks overlay, returning it, or NIL when none was running.

let
whenoverlay
overlay

--------------------------------------------------------------------- The spark's glow, on the GPU

A spark is not sphere-traced: it is a small camera-facing square whose fragment measures its distance from the billboard's centre and falls off smoothly to nothing at the edge. The colour lanes carry linear radiance already scaled by the spark's intensity, so a young spark's output clears the bloom threshold and the lens blazes it; the alpha stays low so overlapping glows add rather than occlude.

in-package#:luvcraft.shaders
define-shader-methodshader-specification-forbirthday-spark-vertex-specification
role
eql:birthday-spark
stage
eql:vertex
:stage:vertex:inputs
quad-corner:vec3:location0
spark-center-size:vec4:location1
spark-color-glow:vec4:location2
:outputs
clip-position:vec4:built-in:position
glow-coordinate:vec2:location0
glow-color:vec4:location1
:resources
frame-state:uniform-block:set0:binding2:members#.*frame-uniform-members*
let*
center
swizzlespark-center-size:xyz
radius
swizzlespark-center-size:w
camera
representation
swizzlecamera-vector:xyz
right
representation
swizzleright-vector:xyz
up
forward
representation
swizzleforward-vector:xyz
corner-x
-
*
swizzlequad-corner:x
2.0
1.0
corner-y
-
*
swizzlequad-corner:y
2.0
1.0
world-position
+center
+
*right
*corner-xradius
*up
*corner-yradius
relative
view-x
dotrelativeright
view-y
dotrelativeup
view-z
dotrelativeforward
x-scale
representation
swizzleprojection-vector:x
y-scale
representation
swizzleprojection-vector:y
z-scale
representation
swizzleprojection-vector:z
z-offset
representation
swizzleprojection-vector:w
set-outputclip-position
vec4
*view-xx-scale
-
*view-yy-scale
+
*view-zz-scale
z-offset
view-z
set-outputglow-coordinate
vec2corner-xcorner-y
set-outputglow-colorspark-color-glow
define-shader-methodshader-specification-forbirthday-spark-fragment-specification
role
eql:birthday-spark
stage
eql:fragment
:stage:fragment:inputs
glow-coordinate:vec2:location0
glow-color:vec4:location1
:outputs
color-output:vec4:location0

Squared distance from the billboard centre, smoothed to zero at the edge and squared again for a hot little core. Premultiplied output: the colour lanes are the light, the low alpha keeps the glows nearly additive over one another and over the dusk behind them.

let*
separation
dotglow-coordinateglow-coordinate
envelope
-1.0
smoothstep0.01.0separation
falloff
*envelopeenvelope
blaze
*
swizzleglow-color:w
falloff
set-outputcolor-output
vec4
*
swizzleglow-color:xyz
blaze
*falloff0.4