luv

Workshop wiki

gnomes.lisp

luvcraft/birthday/gnomes.lisp

system luvcraft/birthday · 20 definitions · on GitHub

Dancing gnomes: the agent's SDF body, multiplied and set to music.

Contract: (add-dancing-gnomes session gnomes) registers one scene overlay drawing every gnome as an instanced SDF billboard, reusing the gnome part functions from luvcraft/agent/shaders.lisp (gnome-distance and friends live in #:luvcraft.shaders; define any dance variants here rather than editing that file). GNOMES is a list of plists (:x :y :z :scale :phase :hue); each gnome dances -- bouncing, swaying, slowly turning -- driven by the frame clock offset by its phase, and its hue shifts the robe and hat so the troupe reads as individuals. (remove-dancing-gnomes session) releases the overlay.

The CPU's whole contribution is one instance buffer written once: feet, radius, scale, phase and hue per gnome. Everything that moves, moves in the fragment shader, on the bounded frame clock the sky's clouds already drift by. The hop and the sway are rigid motions folded into the ray before the march starts; only the squash-and-stretch touches the field itself, and it pays for that with a shorter, humbler step.

--------------------------------------------------------------------- The dance, as knobs

Like the gnome's proportions, each number below stands in the shader source by name and folds to a literal, so tuning a step rebuilds the troupe's pipeline rather than writing a uniform. The amplitudes are small on purpose: a four-year-old should giggle, and a gnome bouncing his own height stops being a gnome and starts being a hazard.

in-package#:luvcraft.shaders
defparameter*dancing-gnome-tempo*1.4

Hops per second, before each gnome's phase spreads the troupe out.

The frame clock wraps at 3600 seconds; at 1.4 that is a whole number of hops, so the hour turns over without a stumble.

defparameter*dancing-gnome-hop*0.20"How high the hop lifts his feet, in figure units."
defparameter*dancing-gnome-squash*0.16

How much the hop squashes and stretches him: tall near the top of the hop, flat at the landing, cross-section compensating either way so he keeps his volume.

defparameter*dancing-gnome-sway*0.11

The side-to-side lean, in radians, about his own forward axis. It runs at half the hop tempo, so he leans the other way on every second landing.

defparameter*dancing-gnome-turn*0.06

Pirouettes per second: how fast the whole figure yaws. Slow on purpose; the turn is for showing the hat from every side, not for spinning.

macrolet
define-dance-knob
nameplacequantityminimummaximumstep&optionaldocumentation
`
luvcraft:define-knob,name
:group:birthday:quantity
:quantity,quantity:unit,
if
eqquantity:figure-proportion
:one:cell
:documentation,documentation:minimum,minimum:maximum,maximum:step,step
,place
define-dance-knobdancing-gnome-tempo*dancing-gnome-tempo*:figure-proportion0.24.00.05
define-dance-knobdancing-gnome-hop*dancing-gnome-hop*:figure-extent0.00.60.01
define-dance-knobdancing-gnome-squash*dancing-gnome-squash*:figure-proportion0.00.50.01
define-dance-knobdancing-gnome-sway*dancing-gnome-sway*:figure-proportion0.00.40.01
define-dance-knobdancing-gnome-turn*dancing-gnome-turn*:figure-proportion0.00.50.01

--------------------------------------------------------------------- Dance variants of the gnome's field

The originals answer for one upright fellow and will not bend, so the two functions below wrap gnome-distance and its tetrahedron normal in the one non-isometric part of the warp. WIDE and TALL are the squash's axis factors; LIMBER, the smaller of them, scales the answer back to an honest lower bound so the march never oversteps the compressed axis.

define-shader-functiondancing-gnome-distance
pointwidetalllimber

gnome-distance through the squash-and-stretch.

The sample point is divided back into the unsquashed body; the answer is a distance in the warped space only after LIMBER shrinks it.

*
gnome-distance
vec3
/
swizzlepoint:x
wide
/
swizzlepoint:y
tall
/
swizzlepoint:z
wide
limber
define-shader-functiondancing-gnome-warped-normal
pointwidetalllimber

The tetrahedron normal over the squashed field.

Differentiating the warped field directly keeps the normal consistent with the surface the march actually found: the constant LIMBER factor can only shrink the gradient, never turn it, and normalize forgets it.

let*
reach0.0016
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
normalize
+
+
*a
dancing-gnome-distance
+point
*areach
widetalllimber
*b
dancing-gnome-distance
+point
*breach
widetalllimber
+
*c
dancing-gnome-distance
+point
*creach
widetalllimber
*d
dancing-gnome-distance
+point
*dreach
widetalllimber
define-shader-functiondancing-gnome-party-color
hue

A festive colour on the hue wheel, HUE in turns.

Three phases of one cosine: as cheap as a rainbow gets, and it never leaves [0,1].

let*
angle
*6.2831853hue
vec3
+0.5
*0.5
cos
-angle2.0943951
+0.5
*0.5
cos
-angle4.1887902
define-shader-functiondancing-gnome-albedo
pointhue

gnome-albedo with the robe and hat dressed for a party.

The same five parts answer for the same materials -- shape and colour still cannot drift apart -- but the hat wears HUE and the robe its near-complement, toned back toward the garden palette so the troupe is festive rather than fluorescent. Beard, skin and boots keep their own colours: it is a party, not a costume change.

let*
strand
*
-
lattice-noise
*point
vec334.07.034.0
0.5
gnome-grain
weave
*
-
lattice-noise
*point
vec315.015.015.0
0.5
gnome-grain
felt
*
-
lattice-noise
*point
vec324.024.024.0
0.5
gnome-grain
robe-color
*
+
*robe-party0.22
vec30.030.0350.05
+1.0
*weave0.30
skin-color
vec30.720.470.35
beard-color
*
vec30.800.790.755
+1.0
*strand0.22
hat-color
*
+
*hat-party0.44
vec30.050.0150.015
+1.0
*felt0.20
boot-color
*
vec30.1050.0750.055
+1.0
*weave0.35

The running nearest-so-far, folded by hand as in gnome-albedo.

near-skin
stepskinrobe
color-1
mixrobe-colorskin-colornear-skin
distance-1
minrobeskin
near-beard
stepbearddistance-1
color-2
mixcolor-1beard-colornear-beard
distance-2
mindistance-1beard
near-hat
stephatdistance-2
color-3
mixcolor-2hat-colornear-hat
distance-3
mindistance-2hat
near-boots
stepbootsdistance-3
color-4
mixcolor-3boot-colornear-boots
skin-weight
-1.0
smoothstep0.00.035
-skin
mindistance-3boots
gnome-face-detailpointcolor-4skin-weight

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

The vertex stage is the gnome's billboard with one more lane riding through: (scale, phase, hue, spare) per instance, beside the bounding sphere the proxy is sized by.

define-shader-methodshader-specification-fordancing-gnome-sdf-vertex-specification
role
eql:dancing-gnome-sdf
stage
eql:vertex
:stage:vertex:inputs
quad-corner:vec3:location0
sphere-center-radius:vec4:location1
dance-vector:vec4:location2
:outputs
clip-position:vec4:built-in:position
proxy-world-position:vec3:location0
sphere-output:vec4:location1
dance-output:vec4:location2
:resources
frame-state:uniform-block:set0:binding2:members#.*frame-uniform-members*
let*
center
swizzlesphere-center-radius:xyz
radius
swizzlesphere-center-radius: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-outputproxy-world-positionworld-position
set-outputsphere-outputsphere-center-radius
set-outputdance-outputdance-vector
define-shader-methodshader-specification-fordancing-gnome-sdf-fragment-specification
role
eql:dancing-gnome-sdf
stage
eql:fragment
:stage:fragment:inputs
proxy-world-position:vec3:location0
sphere-input:vec4:location1
dance-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
swizzlesphere-input:xyz
radius
swizzlesphere-input:w
figure-scale
max
swizzledance-input:x
0.001
phase
swizzledance-input:y
hue
swizzledance-input:z
elapsed

The beat: the frame clock at the troupe's tempo, this gnome's phase ahead, so the troupe ripples instead of drilling.

beat
*6.2831853
+
*dancing-gnome-tempoelapsed
phase
bounce
abs
sinbeat
hop
*dancing-gnome-hopbounce

Squash and stretch, volume held roughly constant: stretched near the top of the hop, squashed at the landing.

tall
+1.0
*dancing-gnome-squash
-bounce0.35
wide
/1.0
sqrttall
limber
mintallwide
sway
*dancing-gnome-sway
sin
*beat0.5
sway-cos
cossway
sway-sin
sinsway

The pirouette. The stock gnome always faces the camera; this one faces the camera only on average, yawing slowly through the whole circle with his phase deciding where he started.

yaw
*6.2831853
+
*dancing-gnome-turnelapsed
phase
yaw-cos
cosyaw
yaw-sin
sinyaw
ray
normalize
-proxy-world-positioncamera
toward-eye
-cameracenter
facing-camera
normalize
vec3
swizzletoward-eye:x
0.0
swizzletoward-eye:z
sideways-camera
vec3
swizzlefacing-camera:z
0.0
-
swizzlefacing-camera:x

The figure frame, yawed in the ground plane. Both basis vectors are horizontal and unit, so the turned pair stays orthonormal.

facing
+
*facing-camerayaw-cos
*sideways-camerayaw-sin
sideways
-
*sideways-camerayaw-cos
*facing-camerayaw-sin

Per-instance stature: the knob times this gnome's own scale, entering once, exactly as in the stock fragment. He stands with his feet at figure y zero; the instance centre is the middle of his bounding sphere, 0.85 above them (*GNOME-BODY-CENTRE*).

stature
*gnome-staturefigure-scale
local-camera
vec3
/
dottoward-eyesideways
stature
+
/
swizzletoward-eye:y
stature
0.85
/
dottoward-eyefacing
stature
local-ray
vec3
dotraysideways
swizzleray:y
dotrayfacing

Bounding-sphere entry and exit, analytically as ever. The CPU sized the sphere to hold the whole dance -- hop, lean, stretch -- so the march can trust it.

figure-radius
/radiusstature
to-center
-local-camera
vec30.00.850.0
half-way
-
dotto-centerlocal-ray
gap
-
dotto-centerto-center
*figure-radiusfigure-radius
discriminant
-
*half-wayhalf-way
gap
span
sqrt
maxdiscriminant0.0
entry
max
-half-wayspan
0.0
exit
+half-wayspan

The hop and the sway are rigid motions, so their inverses fold into the camera and the ray once -- un-lift, then un-lean about the feet -- and the march runs in the body's own swaying space at no cost per step.

dropped-y
-
swizzlelocal-camera:y
hop
sway-camera
vec3
+
*
swizzlelocal-camera:x
sway-cos
*dropped-ysway-sin
-
*dropped-ysway-cos
*
swizzlelocal-camera:x
sway-sin
swizzlelocal-camera:z
sway-ray
vec3
+
*
swizzlelocal-ray:x
sway-cos
*
swizzlelocal-ray:y
sway-sin
-
*
swizzlelocal-ray:y
sway-cos
*
swizzlelocal-ray:x
sway-sin
swizzlelocal-ray:z
travel
counted-fold
march64.0ray-distanceentry
let*
point
+sway-camera
if
<distance0.0009
ray-distance
ifray-distance

The hat's domain warp plus the squash make this field a little less metric than the stock gnome's; the step keeps a wider margin of honesty.

point
+sway-camera
*sway-raytravel
surface-distance
dancing-gnome-distancepointwidetalllimber
coverage
*
-1.0
step0.0035surface-distance
-1.0
step0.0
-discriminant

The unsquashed body point, where the parts and the face live.

body-point
vec3
/
swizzlepoint:x
wide
/
swizzlepoint:y
tall
/
swizzlepoint:z
wide

The normal, back out through the warp: differentiated in swaying space, rotated forward through the lean, then through the yawed basis into the world.

sway-normal
dancing-gnome-warped-normalpointwidetalllimber
lean-normal
vec3
-
*
swizzlesway-normal:x
sway-cos
*
swizzlesway-normal:y
sway-sin
+
*
swizzlesway-normal:x
sway-sin
*
swizzlesway-normal:y
sway-cos
swizzlesway-normal:z
normal
+
*sideways
swizzlelean-normal:x
+
vec30.0
swizzlelean-normal:y
0.0
*facing
swizzlelean-normal:z
albedo

The lighting is the stock gnome's, verbatim: he is the same fellow, just busier.

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.35
1.35
sky
+0.55
*0.45
swizzlenormal:y
occlusion
clamp
+0.35
*0.65
smoothstep-0.150.35
swizzlebody-point:y
0.351.0
view-facing
max0.0
dotnormal
*ray-1.0
rim
expt
-1.0view-facing
3.5
halfway
normalize
-sun-directionray
specular
*0.12
expt
max0.0
dotnormalhalfway
28.0
illumination
+
*ambient
*skyocclusion
*sun-color
+0.12
*wrapped1.15
radiance
+
+
*albedoillumination
*sun-color
*specularcoverage
*
vec30.950.720.44
*rimgnome-rim-light

Premultiplied, like the scene target: misses leave no rectangular trace of the conservative billboard.

set-outputcolor-output
vec4
*radiancecoverage
coverage

--------------------------------------------------------------------- The troupe overlay: one pipeline, one instance buffer, one draw.

in-package#:luvcraft.birthday
defundancing-gnome-figure-radius

The bounding radius, in figure units, of one dancing gnome mid-move.

gnome-figure-radius answers for the fellow standing still; the dance adds the hop's lift, the sway's sweep of everything up to the hat's tip, and the stretch of the squash, each taken at its extreme. Conservative on purpose: the proxy edge does not blur what it cuts, it slices it flat.

defundancing-gnome-instance-data
gnomes

Two vec4 lanes per gnome: (centre.xyz, radius) and (scale, phase, hue, 0).

The plist's :X :Y :Z place his feet in world cells; the record's centre is the middle of his bounding sphere, *GNOME-BODY-CENTRE* figure units above them at his own stature. Phase and hue default to golden-ratio spreads so an unadorned troupe still arrives as individuals rather than as a drill team in uniform.

let
data
make-array
*8
lengthgnomes
:element-type'single-float
index0
dolist
gnomegnomesdata
destructuring-bind
&key
x0.0
y0.0
z0.0
phase
mod
*0.618034index
1.0
hue
mod
*0.618034
+index2
1.0
&allow-other-keys
gnome
let
setf
arefdata
+base0
floatx1.0
arefdata
+base1
arefdata
+base2
floatz1.0
arefdata
+base3
float
*figure-radiusstature
1.0
arefdata
+base4
arefdata
+base5
floatphase1.0
arefdata
+base6
floathue1.0
arefdata
+base7
0.0
incfindex
defclassdancing-gnome-troupe-overlay
pipeline:initarg:pipeline:accessortroupe-pipeline
vertex-buffer:initarg:vertex-buffer:accessortroupe-vertex-buffer
instance-buffer:initarg:instance-buffer:accessortroupe-instance-buffer
instance-count:initarg:instance-count:readertroupe-instance-count
:documentation

Every dancing gnome in one instanced draw. The instances were written once when the troupe arrived; the dance itself lives in the shader.

defmethodluvcraft:luvcraft-focus-score
declare
ignoreoverlaysession
nil
defmethodluvcraft::luvcraft-overlay-live-shader-pipelines
and
troupe-pipelineoverlay
list
troupe-pipelineoverlay
defmethodluvcraft:encode-luvcraft-overlay
sessionpasssurface-texture
let
luv:set-pipelinepass
luvcraft::live-shader-pipeline-native-pipeline
troupe-pipelineoverlay
luv:set-vertex-bufferpass0
troupe-vertex-bufferoverlay
luv:set-vertex-bufferpass1
troupe-instance-bufferoverlay
luv:set-bind-grouppass0
luvcraft::luvcraft-frame-scene-bind-groupframe
luv:drawpass6
troupe-instance-countoverlay
overlay
defmethodluvcraft:release-luvcraft-overlay
when
troupe-pipelineoverlay
setf
troupe-pipelineoverlay
nil
dolist
resource
list
troupe-instance-bufferoverlay
troupe-vertex-bufferoverlay
whenresource
luv:destroyresource
setf
troupe-instance-bufferoverlay
nil
troupe-vertex-bufferoverlay
nil
values
defunadd-dancing-gnomes
sessiongnomes

Call in the troupe: draw gnomes dancing in session as one overlay.

gnomes is a list of plists (:x :y :z :scale :phase :hue): feet position in world cells, stature multiplier, dance phase in turns, and hue in turns around the party colour wheel; scale, phase and hue may be omitted. Any troupe already dancing in session is relieved first. Returns the overlay, or NIL when gnomes is empty.

whengnomes
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"dancing gnome proxy":size
*4
lengthvertex-data
:usage'
:vertex:copy-dst
instance-buffer
luv:createdevice
luv:make-buffer-descriptor:label"dancing gnome troupe":size
*4
lengthinstance-data
:usage'
:vertex:copy-dst
pipeline
luvcraft::make-live-shader-pipeline:role:dancing-gnome-sdf:vertex-role:dancing-gnome-sdf:label"dancing gnome troupe 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'dancing-gnome-troupe-overlay:pipelinepipeline:vertex-buffervertex-buffer:instance-bufferinstance-buffer:instance-count
lengthgnomes
setfcompleted-pt
overlay
unlesscompleted-p
whenpipeline
wheninstance-buffer
ignore-errors
luv:destroyinstance-buffer
whenvertex-buffer
ignore-errors
luv:destroyvertex-buffer
defunremove-dancing-gnomes
session

Send the troupe home: release session's dancing-gnome overlay, if any.

let
overlay
find-if
lambda
overlay
luvcraft:luvcraft-session-overlayssession
whenoverlay