luv

Workshop wiki

shaders.lisp

luvcraft/agent/shaders.lisp

system luvcraft/agent-bodies · 36 definitions · on GitHub

The first genuinely volumetric body for an embodied agent: a little fellow assembled out of signed distance fields.

A camera-facing square is only the conservative rasterization proxy. Each fragment sends its world-space camera ray through the signed distance field and shades the point where sphere tracing converges. The proxy's depth remains an approximation for now; the shape and its lighting are genuinely three-dimensional.

The figure is built in its own upright frame: feet at local y zero, +y up, +z along the embodied agent's retained heading. The proxy still faces the camera for conservative rasterization, but the gnome inside it looks where he is actually going. Every part is a small closed form -- a round cone for the robe and the hat, ellipsoids for the beard and the brim, spheres for head, nose, mittens and boots -- and the parts are welded with a polynomial smooth minimum so the joins read as one body rather than as a pile of primitives. The same five part functions answer twice: once for the surface the ray finds, once for the colour it wears.

in-package#:luvcraft.shaders

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

Every number below stands in the shader source by name and folds to a literal when the source is parsed, so turning one rebuilds his pipeline rather than writing a uniform (LUVCRAFT/KNOBS.LISP). For a figure that is drawn once in the frame that is the right trade: the knobs pay for themselves in how fast a proportion can be tried, and nothing pays per pixel. What is here is what an argument about how a gnome should look actually turns out to be about -- the hat, the nose, the beard, the belly, and how far apart his eyes are set.

defparameter*gnome-stature*0.80

The whole figure's scale, in world cells per figure unit.

The marcher works in figure units and this is the only place the world's scale enters, so his bounding sphere follows from it too. At four fifths he stands about a cell and a quarter: shorter than a player, which is rather the point of him.

defparameter*gnome-hat-height*1.56"How high the hat's tip stands above his feet."
defparameter*gnome-hat-flare*0.240"The radius of the hat's cone where it meets the brim."
defparameter*gnome-hat-lean*0.42"How far forward the hat's tip leans. Zero is a traffic cone."
defparameter*gnome-brim-width*0.400"The brim's radius. Wider than about four tenths and it eats his face."
defparameter*gnome-head-size*0.265"The radius of the head the face is laid out on."
defparameter*gnome-nose-size*0.120"The radius of the nose, the one feature of him nothing hides."
defparameter*gnome-beard-width*0.305"How broad the beard hangs at the cheeks."
defparameter*gnome-beard-length*0.450"The height of the beard's blunt point above his feet: lower is longer."
defparameter*gnome-belly*0.255"The robe's radius at the hem."
defparameter*gnome-shoulder*0.160"The robe's radius at the shoulders."
defparameter*gnome-mitten-reach*0.230"How far out from the body his mitten hands rest."
defparameter*gnome-eye-spread*0.36"How far to the side the eyes sit on the head, as a direction component."
defparameter*gnome-eye-height*0.10"How high the eyes sit on the head. Too high and the brim hides them."
defparameter*gnome-eye-size*0.120"The angular radius of a pupil on the head."
defparameter*gnome-blush*0.35"How much colour there is in his cheeks."
defparameter*gnome-grain*2.50

How strongly the beard's strands, the robe's weave and the hat's felt show. Zero is flawless vinyl.

defparameter*gnome-rim-light*0.14"The warm outline that separates him from whatever he stands in front of."
macrolet
define-figure-knob
nameplacequantityminimummaximumstep&optionaldocumentation
`
luvcraft:define-knob,name
:group:gnome:quantity
:quantity,quantity:unit,
if
eqquantity:figure-proportion
:one:cell
:documentation,documentation:minimum,minimum:maximum,maximum:step,step
,place
define-figure-knobgnome-stature*gnome-stature*:figure-proportion0.42.50.05
define-figure-knobgnome-hat-height*gnome-hat-height*:figure-extent1.152.600.02
define-figure-knobgnome-hat-flare*gnome-hat-flare*:figure-extent0.160.440.005
define-figure-knobgnome-hat-lean*gnome-hat-lean*:figure-offset-0.150.600.01
define-figure-knobgnome-brim-width*gnome-brim-width*:figure-extent0.240.500.005
define-figure-knobgnome-head-size*gnome-head-size*:figure-extent0.170.330.005
define-figure-knobgnome-nose-size*gnome-nose-size*:figure-extent0.030.160.005
define-figure-knobgnome-beard-width*gnome-beard-width*:figure-extent0.150.350.005
define-figure-knobgnome-beard-length*gnome-beard-length*:figure-extent0.250.720.01
define-figure-knobgnome-belly*gnome-belly*:figure-extent0.180.440.005
define-figure-knobgnome-shoulder*gnome-shoulder*:figure-extent0.120.320.005
define-figure-knobgnome-mitten-reach*gnome-mitten-reach*:figure-extent0.200.420.005
define-figure-knobgnome-eye-spread*gnome-eye-spread*:figure-proportion0.250.750.01
define-figure-knobgnome-eye-height*gnome-eye-height*:figure-proportion-0.200.350.01
define-figure-knobgnome-eye-size*gnome-eye-size*:figure-proportion0.040.220.005
define-figure-knobgnome-blush*gnome-blush*:figure-proportion0.01.00.05
define-figure-knobgnome-grain*gnome-grain*:figure-proportion0.02.50.05
define-figure-knobgnome-rim-light*gnome-rim-light*:figure-proportion0.01.00.02

--------------------------------------------------------------------- Distance-field vocabulary

define-shader-functiongnome-length
vector
"Euclidean length, floored away from the origin's derivative singularity."
sqrt
max
dotvectorvector
1e-12
define-shader-functiongnome-smooth-union
first-distancesecond-distanceradius

The polynomial smooth minimum: a union with a fillet of RADIUS.

This is what makes a nose grow out of a face instead of intersecting it.

let*
blend
clamp
+0.5
*0.5
/
-second-distancefirst-distance
radius
0.01.0
-
mixsecond-distancefirst-distanceblend
*radius
*blend
-1.0blend
define-shader-functiongnome-sphere-distance
pointcenterradius
"Signed distance to a sphere."
-
gnome-length
-pointcenter
radius
define-shader-functiongnome-ellipsoid-distance
pointcenterradii

The usual bounded approximation to an ellipsoid's signed distance.

Exact would need a quartic root; this under-estimates, which is all sphere tracing asks of it.

let*
offset
/
-pointcenter
radii
outer
gradient
gnome-length
/offsetradii
/
*outer
-outer1.0
maxgradient1e-6
define-shader-functiongnome-round-cone-distance
pointbase-heighttop-heightbase-radiustop-radius

A cone capped by a sphere at each end: the shape of a robe, and of a hat.

The lateral surface is tangent to both caps, so the silhouette has no crease where the cone meets its rounding.

let*
height
max
-top-heightbase-height
1e-4
radial
axial
-
swizzlepoint:y
base-height
slope
clamp
/
-base-radiustop-radius
height
-0.9990.999
run
sqrt
max
-1.0
*slopeslope
1e-6
side
+
*radial
-slope
*axialrun
if
-
gnome-length
vec2radialaxial
base-radius
if
-top-radius
-
+
*radialrun
*axialslope
base-radius

--------------------------------------------------------------------- The parts of a gnome

Five functions, each answering for one material. They are called by the marcher to find the surface and again at the hit point to decide which material owns it, so shape and colour can never drift apart.

define-shader-functiongnome-hat-point
point

The hat's own frame: a domain warp that leans the cone's tip forward.

A straight cone is a traffic cone. The lean is what makes it a hat someone is wearing, and it costs one smoothstep.

let*
rise
smoothstep1.02gnome-hat-height
swizzlepoint:y
lean
*riserise
vec3
-
swizzlepoint:x
*
*gnome-hat-lean0.20
lean
swizzlepoint:y
-
swizzlepoint:z
*gnome-hat-leanlean
define-shader-functiongnome-hat-distance
point

The tall cone and the brim it sits on.

The brim rides high, near the crown rather than at eye level. A brim low enough to be practical would put his whole face in shadow, and a gnome whose eyes you cannot find is not a friendly little fellow but a bollard.

let*
cone
gnome-round-cone-distance1.04gnome-hat-heightgnome-hat-flare0.018
brim
gnome-ellipsoid-distancepoint
vec30.01.060.0
vec3gnome-brim-width0.042gnome-brim-width
gnome-smooth-unionconebrim0.045
define-shader-functiongnome-skin-distance
point

Head and nose.

Most of the head is under the hat or behind the beard; the nose is the part of him you actually see, so it is a frank sphere.

let*
head
gnome-sphere-distancepoint
vec30.00.900.0
gnome-head-size

The nose rides on whatever head it was given, so resizing the head does not leave it hanging in front of his face.

nose
gnome-sphere-distancepoint
vec30.00.855
*gnome-head-size0.90
gnome-nose-size
gnome-smooth-unionheadnose0.045
define-shader-functiongnome-beard-distance
point

A broad beard with a blunt point: the mass that gives him his gravity.

Its top edge is the lower bound of his face, so it sits just under the nose: enough beard to be venerable, not so much that there is nobody behind it.

let*
mass
gnome-ellipsoid-distancepoint
vec30.00.6050.04
vec3gnome-beard-width0.23
*gnome-beard-width0.90
tip
gnome-sphere-distancepoint
vec30.0gnome-beard-length0.115
0.095
moustache
gnome-ellipsoid-distancepoint
vec30.00.7750.155
vec30.1550.0450.145
gnome-smooth-unionmoustache0.05
define-shader-functiongnome-robe-distance
point

The robe, flaring to a hem that rests on the ground, and two mitten hands.

The hem stops just above his boots. An earlier version ran the cone's lower cap down past the feet, and the terrain cut it off in a dead straight line: he looked less like he was standing on the world than pasted onto it.

let*
robe
gnome-round-cone-distancepoint0.300.80gnome-bellygnome-shoulder
mirrored
vec3
abs
swizzlepoint:x
swizzlepoint:y
swizzlepoint:z
mitten
gnome-sphere-distancemirrored
vec3gnome-mitten-reach0.500.115
0.09
gnome-smooth-unionrobemitten0.05
define-shader-functiongnome-boot-distance
point
"Two small boots, toes forward, mirrored across the body's plane."
let*
mirrored
vec3
abs
swizzlepoint:x
swizzlepoint:y
swizzlepoint:z
gnome-ellipsoid-distancemirrored
vec30.130.0550.115
vec30.0950.0550.145
define-shader-functiongnome-distance
point

The whole fellow: five parts welded with fillets of different sizes.

The robe-to-head weld is generous because a gnome has no neck to speak of; the hat sits on the brim with almost no fillet at all, because a hat is worn rather than grown.

define-shader-functiongnome-normal
point
"The gradient by the tetrahedron trick: four samples rather than six."
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

--------------------------------------------------------------------- What he is made of

define-shader-functiongnome-face-detail
pointalbedoskin-weight

Eyes, brows and a little colour in the cheeks, painted on the skin only.

The features are placed by *direction* from the centre of the head rather than by distance to a point in space. Placing them by point failed: the welded surface stands a fillet's width outside the head sphere the markers were measured against, so every feature sat just under the skin and the gnome came out blank-faced. A direction does not care how far out the surface ended up.

The eyes are small and dark and the brows are heavy. Resisting the urge to enlarge the eyes is the whole difference between a friendly little fellow and a cartoon: he should look like he is thinking about you, not begging for something.

let*
mirrored
vec3
abs
swizzlepoint:x
swizzlepoint:y
swizzlepoint:z
direction
normalize
-mirrored
vec30.00.900.0

Glint and brow are stated relative to the eye, so moving the eyes moves the whole expression rather than taking it apart.

eye-direction
normalize
vec3gnome-eye-spreadgnome-eye-height0.85
eye
gnome-length
-directioneye-direction
pupil
-1.0
smoothstepgnome-eye-size
+gnome-eye-size0.035
eye
socket
-1.0
smoothstep
+gnome-eye-size0.035
+gnome-eye-size0.210
eye
glint
-1.0
smoothstep0.0350.060
gnome-length
-direction
normalize
vec3
-gnome-eye-spread0.05
+gnome-eye-height0.08
0.87

The brow is measured in a squashed metric: wide, low and shallow, which is a brow, where an unsquashed disc would be a wart.

brow
-1.0
smoothstep0.1150.190
gnome-length
*
-direction
normalize
vec3
+gnome-eye-spread0.01
+gnome-eye-height0.17
0.82
vec30.451.90.9
cheek
-1.0
smoothstep0.1800.400
gnome-length
-direction
normalize
vec30.80-0.220.56
shaded
mixalbedo
*albedo
vec30.620.550.52
*socket
*skin-weight0.32
blushed
mixshaded
vec30.820.400.33
*cheek
*skin-weightgnome-blush
eyed
mixblushed
vec30.0350.0300.042
*pupilskin-weight
browed
mixeyed
vec30.800.790.76
*brow
*skin-weight0.9
lit
mixbrowed
vec30.950.950.95
*glint
*skin-weight0.9
lit
define-shader-functiongnome-albedo
point

The nearest part decides the colour.

A muted, slightly earthy palette on purpose: a scarlet hat over a slate robe reads as someone who has been standing in a garden for a long time, where saturated primaries would read as a toy.

let*

Three grains, each sampled in a domain stretched along the way that material actually runs: the beard downward in strands, the robe in a coarse weave, the hat in fine felt. Without them everything the sphere tracer returns is the same flawless vinyl, and a gnome who has stood in a garden for two hundred years should not look like he came out of a mould this morning.

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
*
vec30.0550.1450.245
+1.0
*weave0.30
skin-color
vec30.720.470.35
beard-color
*
vec30.800.790.755
+1.0
*strand0.22
hat-color
*
vec30.520.0750.085
+1.0
*felt0.20
boot-color
*
vec30.1050.0750.055
+1.0
*weave0.35

A running nearest-so-far, folded by hand: step answers one when the candidate is at least as near, and mix takes it.

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

How much of this point is face, for the details painted on it.

skin-weight
-1.0
smoothstep0.00.035
-skin
mindistance-3boots
gnome-face-detailpointcolor-4skin-weight
define-shader-functiongnome-shaded-color
pointraysidewaysfacingsun-directionsun-colorambientcoverage

Shade one confirmed gnome surface hit.

Keeping the expensive normal and material field evaluations behind the fragment statement's hit guard means conservative billboard misses pay only for the march, not another five whole-body field evaluations.

let*
local-normal
normal
+
*sideways
swizzlelocal-normal:x
+
vec30.0
swizzlelocal-normal:y
0.0
*facing
swizzlelocal-normal:z
albedo
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
swizzlepoint: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
vec4
*radiancecoverage
coverage

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

define-shader-methodshader-specification-forgnome-sdf-vertex-specification
role
eql:gnome-sdf
stage
eql:vertex
:stage:vertex:inputs
quad-corner:vec3:location0
sphere-center-radius:vec4:location1
figure-facing:vec4:location2
:outputs
clip-position:vec4:built-in:position
proxy-world-position:vec3:location0
sphere-output:vec4:location1
facing-output:vec3: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-outputfacing-output
swizzlefigure-facing:xyz
define-shader-methodshader-specification-forgnome-sdf-fragment-specification
role
eql:gnome-sdf
stage
eql:fragment
:stage:fragment:inputs
proxy-world-position:vec3:location0
sphere-input:vec4:location1
facing-input:vec3: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
ray
normalize
-proxy-world-positioncamera

The figure's own upright frame: world up stays up and +z is the body's retained forward direction. RIGHT is the cross product of world up with FACING, written out by hand.

toward-eye
-cameracenter
facing
normalizefacing-input
sideways
vec3
swizzlefacing:z
0.0
-
swizzlefacing:x

The whole march happens in figure units, where the gnome is one and three quarters tall whatever his stature: dividing the camera by the stature once here is the entire cost of the scale knob, and every proportion below can be written as itself. He stands with his feet at figure y zero; the instance centre is the middle of his bounding sphere, 0.85 above them.

local-camera
vec3
/
dottoward-eyesideways
gnome-stature
+0.85
/
dottoward-eyefacing
gnome-stature
local-ray
vec3
dotraysideways
swizzleray:y
dotrayfacing

Enter and leave through the bounding sphere analytically, so the fixed step count is spent on the body rather than on the approach.

figure-radius
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
initial-point
+local-camera
*local-rayentry
initial-distance
gnome-distanceinitial-point
initial-state
vec3entryinitial-distance
max
-1.0
step0.0009initial-distance
step0.000001
-entryexit

Carry distance and termination explicitly. COUNTED-FOLD lowers :UNTIL to a real loop break on every backend, so hits and sphere exits no longer spend the rest of the fixed budget re-evaluating the whole body field.

march-state
counted-fold
march64.0stateinitial-state:until
>
swizzlestate:z
0.5
let*

The hat's domain warp makes the field slightly non-metric; a shortened step keeps the march honest.

next-distance
next-point
+local-camera
*local-raynext-distance
next-surface-distance
gnome-distancenext-point
vec3next-distancenext-surface-distance
max
-1.0
step0.0009next-surface-distance
step0.000001
-next-distanceexit
travel
swizzlemarch-state:x
point
+local-camera
*local-raytravel
surface-distance
swizzlemarch-state:y
coverage
*
-1.0
step0.0035surface-distance
-1.0
step0.0
-discriminant

The scene target uses premultiplied alpha. Misses leave no rectangular trace of the conservative billboard. Set the cheap miss first; the structured WHEN keeps normal and albedo evaluation inside the hit arm.

set-outputcolor-output
vec40.00.00.00.0
when
>coverage0.0
set-outputcolor-output
gnome-shaded-colorpointraysidewaysfacing
representation
swizzlesun-vector:xyz
representation
swizzlesun-color-vector:xyz
representation
swizzleambient-vector:xyz
coverage