luv

Workshop wiki

balloons.lisp

luvcraft/birthday/balloons.lisp

system luvcraft/birthday · 21 definitions · on GitHub

Balloons: bright SDF spheres-with-knots bobbing on their strings.

Contract: (add-birthday-balloons session balloons) registers one scene overlay drawing every balloon as an instanced, camera-facing, sphere-traced billboard in the manner of the gnome's body overlay. BALLOONS is a list of plists (:x :y :z :radius :hue :phase); the overlay owns its instance buffer and pipelines. Balloons bob and sway gently on the frame clock with per-instance phase, and each wears its own bright glossy colour. (remove-birthday-balloons session) releases the overlay.

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

A balloon is much simpler than a gnome and the shader below leans on that: it has no face to keep toward the camera -- a balloon is round about its string, so the field is marched straight in world axes -- and the whole dance is done in the vertex stage. Each frame the vertex shader reads the frame clock out of the fog vector, bobs and sways the balloon's centre by its instance phase, and hands the *animated* centre to the fragment stage; the CPU writes the instance buffer once, when the balloons are tied on, and never again. The billboard is centred on the animated bounding sphere, so nothing can slide out of its own proxy and be sliced flat at the edge -- the bound follows the balloon around rather than trying to be wide enough for everywhere it might go.

in-package#:luvcraft.shaders

--------------------------------------------------------------------- The balloon's proportions, as knobs

As with the gnome, each number stands in the shader source by name and folds to a literal when the source is parsed, so turning one rebuilds the pipeline. The shape works in balloon units -- one unit is the balloon's radius at its widest -- and only the two amplitudes speak in world cells, because a big balloon and a small one hang from the same kind of breeze.

defparameter*balloon-elongation*1.16

How much taller than wide a balloon stands, in balloon radii. One would be a ball, which is a different toy.

defparameter*balloon-string-length*2.2"How far the string hangs below the knot, in balloon radii."
defparameter*balloon-bob-amplitude*0.15"How high a balloon rides its bob, in world cells."
defparameter*balloon-sway-amplitude*0.08"How far a balloon drifts sideways, in world cells."
defparameter*balloon-gloss*0.9

The strength of the latex highlight. Zero is a matte balloon, which is a balloon that has given up.

defparameter*balloon-rim-light*0.18"The pale outline that lifts a balloon off whatever stands behind it."
defparameter*balloon-translucency*0.30"How much sunlight leaks through the latex when the sun is behind it."
macrolet
define-balloon-knob
nameplacequantityminimummaximumstep&optionaldocumentation
`
luvcraft:define-knob,name
:group:balloon:quantity
:quantity,quantity:unit,
if
eqquantity:figure-extent
:cell:one
:documentation,documentation:minimum,minimum:maximum,maximum:step,step
,place
define-balloon-knobballoon-elongation*balloon-elongation*:figure-proportion1.01.60.01
define-balloon-knobballoon-string-length*balloon-string-length*:figure-proportion0.24.00.1
define-balloon-knobballoon-bob-amplitude*balloon-bob-amplitude*:figure-extent0.00.50.01
define-balloon-knobballoon-sway-amplitude*balloon-sway-amplitude*:figure-extent0.00.40.01
define-balloon-knobballoon-gloss*balloon-gloss*:figure-proportion0.02.00.05
define-balloon-knobballoon-rim-light*balloon-rim-light*:figure-proportion0.01.00.02
define-balloon-knobballoon-translucency*balloon-translucency*:figure-proportion0.01.00.02

--------------------------------------------------------------------- The shape

Three primitives in balloon units: the latex is an upright ellipsoid eased toward the knot by a small sphere so the bottom tapers the way a filled balloon actually hangs, the knot is a nub poking out under the taper, and the string is a thin capsule dropping from the knot. The latex parts weld with the gnome's smooth minimum; the string joins by plain min, because a string is tied on, not grown.

define-shader-functionballoon-body-distance
point

The latex: an ellipsoid eased into a teardrop, with a tied-off knot.

The taper sphere sits low inside the ellipsoid and the generous fillet between them is what turns two primitives into one skin; the knot keeps a small fillet so it still reads as a knot.

let*
body
gnome-ellipsoid-distancepoint
vec30.00.00.0
vec31.0balloon-elongation1.0
taper
gnome-sphere-distancepoint
vec30.0-1.050.0
0.30
knot
gnome-sphere-distancepoint
vec30.0-1.280.0
0.14
define-shader-functionballoon-string-distance
point
"The string: a thin vertical capsule hanging from the knot."
let*
drop
clamp
/
--1.30
swizzlepoint:y
balloon-string-length
0.01.0
nearest-y
--1.30
*dropballoon-string-length
-
gnome-length
vec3
swizzlepoint:x
-
swizzlepoint:y
nearest-y
swizzlepoint:z
0.035
define-shader-functionballoon-distance
point
"The whole balloon: latex welded, string tied on."
define-shader-functionballoon-normal
point
"The gradient by the tetrahedron trick, exactly as the gnome takes it."
let*
reach0.002
a
vec31.0-1.0-1.0
b
vec3-1.0-1.01.0
c
vec3-1.01.0-1.0
d
vec31.01.01.0
define-shader-functionballoon-albedo
hue

Six party colours on a ring; HUE picks one or blends between neighbours.

Saturated primaries on purpose -- the very thing the gnome's palette refuses. A balloon is a toy, and should read as one from across the meadow: red, orange, sunny yellow, green, sky blue, pink, and around again to red.

let*
red
vec30.850.070.09
color-1
mixred
vec30.950.380.05
clampturn0.01.0
color-2
mixcolor-1
vec31.00.780.06
clamp0.01.0
color-3
mixcolor-2
vec30.100.620.15
clamp0.01.0
color-4
mixcolor-3
vec30.150.450.95
clamp0.01.0
color-5
mixcolor-4
vec30.930.300.55
clamp0.01.0
mixcolor-5red
clamp0.01.0

--------------------------------------------------------------------- The pipeline stages

The shape spans balloon-unit y from the string's tail up to the crown, so its bounding sphere is centred well below the balloon's own centre. Both stages state the bound the same way from the same knobs -- the vertex to size the proxy, the fragment to enter and leave the march analytically -- so they cannot drift apart. The margin of three tenths covers the string's thickness and anything a smooth union swells.

define-shader-methodshader-specification-forballoon-sdf-vertex-specification
role
eql:balloon-sdf
stage
eql:vertex
:stage:vertex:inputs
quad-corner:vec3:location0
balloon-center-radius:vec4:location1
balloon-tint-phase:vec4:location2
:outputs
clip-position:vec4:built-in:position
proxy-world-position:vec3:location0
balloon-output:vec4:location1
tint-output:vec4:location2
:resources
frame-state:uniform-block:set0:binding2:members#.*frame-uniform-members*
let*
center
swizzleballoon-center-radius:xyz
radius
swizzleballoon-center-radius:w
phase
swizzleballoon-tint-phase:y

The dance, entirely here: a bob and two sways at incommensurate rates, each offset by the instance phase, so a row of balloons never marches in step. The fragment receives the moved centre and needs no clock of its own.

elapsed
turn
*phase6.28318
bob
*balloon-bob-amplitude
sin
+
*elapsed2.1
turn
sway-x
*balloon-sway-amplitude
cos
+
*elapsed1.3
turn
sway-z
*balloon-sway-amplitude
cos
+
*elapsed1.7
bobbed
+center
vec3sway-xbobsway-z
camera
representation
swizzlecamera-vector:xyz
right
representation
swizzleright-vector:xyz
up
forward
representation
swizzleforward-vector:xyz

The bound, in balloon units, then scaled by this instance.

bound-bottom
--1.35balloon-string-length
bound-center-y
*0.5
+balloon-elongationbound-bottom
bound-radius
+
*0.5
-balloon-elongationbound-bottom
0.3
extent
*radius
*bound-radius1.02
billboard-center
+bobbed
vec30.0
*radiusbound-center-y
0.0
corner-x
-
*
swizzlequad-corner:x
2.0
1.0
corner-y
-
*
swizzlequad-corner:y
2.0
1.0
world-position
+billboard-center
+
*right
*corner-xextent
*up
*corner-yextent
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-outputproxy-world-positionworld-position
set-outputballoon-output
vec4bobbedradius
set-outputtint-outputballoon-tint-phase
define-shader-methodshader-specification-forballoon-sdf-fragment-specification
role
eql:balloon-sdf
stage
eql:fragment
:stage:fragment:inputs
proxy-world-position:vec3:location0
balloon-input:vec4:location1
tint-input:vec4:location2
:outputs
color-output:vec4:location0
:resources
frame-state:uniform-block:set0:binding2:members#.*frame-uniform-members*
let*
camera
representation
swizzlecamera-vector:xyz
center
swizzleballoon-input:xyz
radius
swizzleballoon-input:w
hue
swizzletint-input:x
ray
normalize
-proxy-world-positioncamera

Balloon units: world over the instance radius. No facing frame -- a balloon is round about its string -- so the ray marches unrotated and only the camera moves into the balloon's frame.

local-camera
/
-cameracenter
radius

The same bound the vertex stage stated.

bound-bottom
--1.35balloon-string-length
bound-center
vec30.0
*0.5
+balloon-elongationbound-bottom
0.0
bound-radius
+
*0.5
-balloon-elongationbound-bottom
0.3
to-center
-local-camerabound-center
half-way
-
dotto-centerray
gap
-
dotto-centerto-center
*bound-radiusbound-radius
discriminant
-
*half-wayhalf-way
gap
span
sqrt
maxdiscriminant0.0
entry
max
-half-wayspan
0.0
exit
+half-wayspan

Half the gnome's steps: three round primitives converge fast, and the ellipsoid's bounded estimate already understates, so the step scale stays close to one.

travel
point
+local-camera
*raytravel
surface-distance

A feathered edge rather than the gnome's hard step: latex against sky benefits from half a pixel of mercy.

coverage
*
-1.0
smoothstep0.0040.02surface-distance
-1.0
step0.0
-discriminant

Which material owns the hit, by the nearer field.

string-weight
stepstringbody
albedo
mixlatex
vec30.920.920.88
string-weight

The string dissolves over its lower half, so the balloon reads as tied to the air rather than planted in the ground.

along
clamp
/
--1.30
swizzlepoint:y
balloon-string-length
0.01.0
string-fade
-1.0
smoothstep0.551.0along
faded-coverage
*coverage
mix1.0string-fadestring-weight
normal
sun-direction
representation
swizzlesun-vector:xyz
sun-color
representation
swizzlesun-color-vector:xyz
ambient
representation
swizzleambient-vector:xyz
lambert
dotnormalsun-direction
wrapped
max0.0
/
+lambert0.4
1.4
sky
+0.55
*0.45
swizzlenormal:y
view-facing
max0.0
dotnormal
*ray-1.0
rim
expt
-1.0view-facing
3.0
halfway
normalize
-sun-directionray

Latex is two highlights: the tight glint that says glossy and a broad sheen that says rubber rather than glass. Neither belongs on the string.

facing-half
max0.0
dotnormalhalfway
glint
*balloon-gloss
exptfacing-half90.0
sheen
*0.10
exptfacing-half8.0

Sunlight through the far side: the lift that makes a back-lit balloon glow its own colour instead of going dark.

glow
*balloon-translucency
max0.0
-lambert
illumination
+
*ambientsky
*sun-color
+0.15
*wrapped1.1
radiance
+
+
*albedoillumination
*sun-color
*
+glintsheen
-1.0string-weight
+
*
*albedosun-color
glow
*
vec31.00.980.92
*rim
*balloon-rim-light
-1.0string-weight

Premultiplied alpha, as the scene target expects: a miss leaves no rectangular trace of the proxy.

set-outputcolor-output
vec4
*radiancefaded-coverage
faded-coverage
in-package#:luvcraft.birthday

--------------------------------------------------------------------- The overlay: one pipeline, one quad, one instanced draw.

Structured after the gnome's body overlay, minus its every-frame heartbeat: the instance buffer is written once when the balloons are tied on, and the vertex shader animates from the frame clock, so encode only binds and draws.

defclassbirthday-balloon-overlay
pipeline:initarg:pipeline:accessorballoon-overlay-pipeline
vertex-buffer:initarg:vertex-buffer:accessorballoon-overlay-vertex-buffer
instance-buffer:initarg:instance-buffer:accessorballoon-overlay-instance-buffer
instance-count:initarg:instance-count:readerballoon-overlay-instance-count
defmethodluvcraft::luvcraft-overlay-live-shader-pipelines
list
balloon-overlay-pipelineoverlay
defmethodluvcraft:encode-luvcraft-overlay
sessionpasssurface-texture
let
luv:set-pipelinepass
luvcraft::live-shader-pipeline-native-pipeline
balloon-overlay-pipelineoverlay
luv:set-vertex-bufferpass0
balloon-overlay-vertex-bufferoverlay
luv:set-vertex-bufferpass1
balloon-overlay-instance-bufferoverlay
luv:set-bind-grouppass0
luvcraft::luvcraft-frame-scene-bind-groupframe
luv:drawpass6
balloon-overlay-instance-countoverlay
overlay
defmethodluvcraft:release-luvcraft-overlay
when
balloon-overlay-pipelineoverlay
luvcraft::release-live-shader-pipeline
balloon-overlay-pipelineoverlay
setf
balloon-overlay-pipelineoverlay
nil
dolist
resource
list
balloon-overlay-instance-bufferoverlay
balloon-overlay-vertex-bufferoverlay
whenresource
luv:destroyresource
setf
balloon-overlay-instance-bufferoverlay
nil
balloon-overlay-vertex-bufferoverlay
nil
values
defunballoon-instance-data
balloons

Pack balloons into the two-vec4 instance records the shader reads.

Each record is (centre.xyz, radius) then (hue, phase, spare, spare). An unstated hue walks the palette ring by the golden ratio and an unstated phase spreads similarly, so a bag of plain (:x :y :z) plists still comes out as a mixed bunch that never bobs in unison.

let
data
make-array
*8
lengthballoons
:element-type'single-float:initial-element0.0
loopforballooninballoonsforcountfrom0forbasefrom0by8do
flet
put
offsetvalue
setf
arefdata
+baseoffset
coercevalue'single-float
put0
getfballoon:x0.0
put1
getfballoon:y0.0
put2
getfballoon:z0.0
put3
getfballoon:radius0.35
put4
or
getfballoon:hue
mod
*count0.618034
1.0
put5
or
getfballoon:phase
mod
*count0.377
1.0
data
defunadd-birthday-balloons
sessionballoons

Tie balloons -- plists of (:x :y :z :radius :hue :phase) -- into session.

One overlay, one instanced draw. Any balloons already tied on are taken down first, so calling this twice redecorates rather than crowds. Returns the overlay, or NIL when balloons is empty.

whenballoons
let*
device
luvcraft:luvcraft-session-devicesession
instance-data
vertex-buffernil
instance-buffernil
pipelinenil
completed-pnil
unwind-protect
progn
setfvertex-buffer
luv:createdevice
luv:make-buffer-descriptor:label"balloon SDF proxy":size
*4
lengthvertex-data
:usage'
:vertex:copy-dst
instance-buffer
luv:createdevice
luv:make-buffer-descriptor:label"balloon SDF instances":size
*4
lengthinstance-data
:usage'
:vertex:copy-dst
pipeline
luvcraft::make-live-shader-pipeline:role:balloon-sdf:vertex-role:balloon-sdf:label"balloon sphere SDF 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
luv:write-bufferinstance-bufferinstance-data
let
overlay
make-instance'birthday-balloon-overlay:pipelinepipeline:vertex-buffervertex-buffer:instance-bufferinstance-buffer:instance-count
lengthballoons
setfcompleted-pt
unlesscompleted-p
whenpipeline
wheninstance-buffer
ignore-errors
luv:destroyinstance-buffer
whenvertex-buffer
ignore-errors
luv:destroyvertex-buffer
defunremove-birthday-balloons
session

Take down every balloon overlay tied to session, releasing its GPU state.

dolist
overlay
remove-if-not
lambda
overlay
luvcraft:luvcraft-session-overlayssession
values