luvcraft/birthday/fireworks.lisp
--------------------------------------------------------------------- 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.
A deterministic dial in [0,1) for index, distinct per salt.
--------------------------------------------------------------------- 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.
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.
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.
One continuous fireworks show: schedule, rockets, and spark columns. Plain CPU state over preallocated arrays; it steps without a GPU.
Where shells rise from.
The schedule and the running counters every dial hangs off.
What has happened so far, for tests and curiosity.
The spark columns.
Seconds until this spark pops into secondaries; zero means never.
The rocket slots.
Make a show ready to step, launching from near origin, a list (X Y Z).
--------------------------------------------------------------------- Admitting sparks
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.
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.
--------------------------------------------------------------------- Burst geometry and palettes
Pink and cyan, alternating around the shell.
Green and violet, likewise.
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.
A tilted circle of sparks, evenly spaced, all at one speed.
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.
Fewer, slower, heavier sparks that drag hard and droop long: always gold, because a willow is.
Lift the sphere toward the sky so the droop has somewhere to fall from.
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.
--------------------------------------------------------------------- Shells: launching, climbing, bursting
Send shell number shell up from near the origin, its tilt, fuse,
pattern, and palette all dialed off its number.
The first shell is a classic peony; after that the dial chooses, leaning peony.
double peony Every seventh shell wears the rainbow, finale-ish.
Shed one gold ember behind the rocket in slot.
The rocket in slot has reached its fuse: bloom, and free the slot.
--------------------------------------------------------------------- The step: schedule, rockets, sparks
The trail: a fractional emission accumulator, so the sparkle density does not depend on the frame rate.
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.
Age, integrate, and compact in place. Survivors slide left, so the columns stay in spawn order and index zero stays the oldest spark.
Advance show by dt seconds: schedule, fly, burst, age, retire.
Plain arithmetic over the show's own columns; safe without a GPU.
--------------------------------------------------------------------- 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.
Pack the live population into data, stride eight floats per instance,
and return how many instances are ready to draw.
--------------------------------------------------------------------- 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.
One queue write of the whole preallocated lane; the draw below only reads the live prefix.
Where the show stands when nobody says: two dozen cells ahead of the camera along the ground, so the first shell rises into view.
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.
End the show: release the fireworks overlay, returning it, or NIL when none was running.
--------------------------------------------------------------------- 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.
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.
(session &key origin)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.
(overlay session)Publish any complete pending render state for OVERLAY at a frame boundary.
(session)End the show: release the fireworks overlay, returning it, or NIL when none was running.
(word)Headings, paragraphs, figures and their IDs, mentions, marks.
Multiplication and scalar scaling.
(index salt)Division of two represented quantities.
Convert one scalar float or unsigned value to a 32-bit float.
Addition over compatible quantities.
The most sparks alive at once; past it the oldest are dropped.
The most shells climbing at once, comfortably more than the schedule asks.
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.
Seconds between shells before the per-shell spread is added.
How many extra seconds the launch dial can add to the interval.
Trail sparks a climbing rocket sheds per second.
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.
(&key (origin '(0.0 0.0 0.0))
(capacity +firework-spark-capacity+))(show wanted)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.
Subtraction or unary negation.
(show x y z vx vy vz red green blue
size glow life drag pop)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.
Test whether one compatible scalar is less than another.
The componentwise square root of a raw value.
The maximum of compatible quantities.
(palette k n seed)(scheme)(show x y z vx vy vz palette seed n speed pop-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.
(show x y z vx vy vz palette seed)(show x y z vx vy vz seed)Fewer, slower, heavier sparks that drag hard and droop long: always gold, because a willow is.
(show i)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.
The minimum of compatible quantities.
(show shell)Send shell number SHELL up from near the origin, its tilt, fuse, pattern, and palette all dialed off its number.
Test whether two compatible scalars are equal.
The non-negative remainder of integer division.
(show slot)(show slot)(show dt)(show dt)Test whether one compatible scalar is at least another.
(show dt)(show dt)Advance SHOW by DT seconds: schedule, fly, burst, age, retire. Plain arithmetic over the show's own columns; safe without a GPU.
(show data)Pack the live population into DATA, stride eight floats per instance, and return how many instances are ready to draw.
(overlay)The live shader pipelines OVERLAY owns, so the session can count them among its own; none by default.
(overlay session pass surface-texture)Encode OVERLAY into SESSION's open scene PASS for SURFACE-TEXTURE.
An explicitly owned AVFrame.
(pass-encoder pipeline)(pass-encoder slot buffer &key (offset 0))(pass-encoder index bind-group)(pass-encoder vertex-count
&optional (instance-count 1) (first-vertex 0) (first-instance 0))(overlay)Release resources owned by an object attached to luvcraft.
(artifact)(handle)Logically invalidate HANDLE immediately. Native teardown may be deferred until submitted work which captured HANDLE has completed.
(session)(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.
(camera)(camera)(camera)Logical disjunction of tests and raw truth values.
(device descriptor)Asks the DEVICE for a handle to newly created instance of some object fulfilling the DESCRIPTOR.
(&key role (stage :fragment) vertex-role label device layout vertex-module
vertex-buffers target-format target-blend primitive depth-stencil)Create and install a live mathematical render pipeline. VERTEX-MODULE is borrowed when VERTEX-ROLE is NIL and STAGE is not :VERTEX. Every module compiled from ROLE or VERTEX-ROLE is owned by the returned artifact and retired on replacement or explicit release.
The linear HDR attachment format shared by every scene-stage pipeline.
(session overlay)Attach OVERLAY at SESSION's next native frame boundary and return it. ADD consumes a newly offered OVERLAY on both success and terminal rejection. Once SESSION begins stopping, the rejected overlay is released exactly once before APPLICATION-ATTACHMENT-CLOSED is signalled.
(session overlay &key (release-p t))Detach OVERLAY at a frame boundary and optionally release it there. The no-release path is intentionally preserved for FUSE-LUVCRAFT-OVERLAY: that path may run inside a frame whose command stream still borrows OVERLAY.
(generic-function name specialized-lambda-list options &body body)Define a shader-producing method with ordinary DEFMETHOD identity. Calling the method reparses its small source form so changes to source-level abstractions participate in live rebuilding. Method replacement remains the role/stage identity watched by the MOP; abstraction revisions are tracked separately by live…
(role stage)Return the current durable shader specification for ROLE and STAGE.
Select and reorder vector components by a designator such as :XYZ or :RGB.
Expose a quantity's raw representation.
The inner product of two vectors.
Produce dimensionless progress across compatible edges.
Fireworks: a great big show over the meadow at dusk.
Contract: (
add-birthday-fireworkssession &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 inrefresh-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-fireworkssession) 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.