luv

Workshop wiki

shaders.lisp

luvcraft/shaders.lisp

system luvcraft/core · 128 definitions · on GitHub

The visible block-world materials as typed mathematical specifications.

These shader methods are luvcraft's hot-replaceable source of truth: the live pipelines watch their CLOS role/stage coordinates through MOP dependents and rebuild on redefinition. The crosshair vertex stage at the end is a literal SPIR-V module rather than a mathematical specification.

in-package#:luvcraft.shaders

A matrix is representation; this is the meaning of the operation it participates in. The four dense rows arrive through the frame ABI, but applying them is a checked map from an affine cell-valued lattice point to homogeneous light clip. A separate checked projection produces the deliberately packed shadow sample tuple used by the fragment stage.

define-projective-shader-map:world-to-light:domain-type:vec3:domain-quantity:world-position:domain-unit:cell:domain-affine-pt:sample-type:vec3:sample-components
:xy:quantity:shadow-uv:unit:one
:z:quantity:shadow-depth:unit:one
:coordinate-scale
1/21/21
:coordinate-offset
1/21/20

The knobs folded into these shaders. Each is a special this file's source names where it once held a literal; the parser folds the current value in, and the pipeline that folded it rebuilds when it moves (LUVCRAFT/KNOBS.LISP). Turning one is a shader rebuild, not a uniform write, so they are the few literals worth that: the ones art direction keeps asking about.

defparameter*sun-disc-scale*4.0"How many times its true angular radius the sun's disc is drawn at."
defparameter*sun-disc-radiance*39.0"The disc's radiance above display white, which the bloom feeds on."
defparameter*direct-light-gain*0.80"The direct sun's diffuse intensity on a lit block face."
defparameter*screen-curvature*0.40"How far a terminal faceplate's normal bulges toward its rim."
defparameter*scanline-count*240.0"Raster lines across a terminal faceplate's height."
defparameter*scanline-depth*0.20"How dark the raster's gaps go, as a fraction of the picture."
defparameter*screen-effect-ceiling*0.6"The most a faceplate's raster, hum, flicker, and grain may take."
luvcraft:define-knobsun-disc-scale
:group:sun:quantity
:quantity:sun-disc-scale:unit:one
:unit-label"×":minimum1.0:maximum12.0:step0.5
*sun-disc-scale*
luvcraft:define-knobsun-disc-radiance
:group:sun:quantity
:quantity:sun-disc-radiance:unit:one
:minimum0.0:maximum100.0:step1.0
*sun-disc-radiance*
luvcraft:define-knobdirect-light-gain
:group:sun:quantity
:quantity:direct-light-gain:unit:one
:unit-label"×":minimum0.0:maximum6.0:step0.05
*direct-light-gain*
luvcraft:define-knobscreen-curvature
:group:terminal:quantity
:quantity:screen-curvature:unit:one
:minimum0.0:maximum1.5:step0.05
*screen-curvature*
luvcraft:define-knobscanline-count
:group:terminal:quantity
:quantity:scanline-count:unit:one
:minimum60.0:maximum1000.0:step20.0
*scanline-count*
luvcraft:define-knobscanline-depth
:group:terminal:quantity
:quantity:screen-effect-strength:unit:one
:minimum0.0:maximum1.0:step0.05
*scanline-depth*
luvcraft:define-knobscreen-effect-ceiling
:group:terminal:quantity
:quantity:screen-effect-strength:unit:one
:minimum0.0:maximum1.0:step0.05
*screen-effect-ceiling*

The sky's own knobs. Everything else about the sky arrives through the frame environment from the day profile; these four are the art direction the profile has no opinion about.

defparameter*sky-scatter-gain*0.70"How strongly the atmosphere's phase function brightens the sun's quarter."
defparameter*cloud-coverage*1.35"The profile's cloudiness against art direction; one leaves it alone."
defparameter*star-brightness*1.7"The radiance of a first-magnitude star, above display white."
defparameter*moon-radiance*4.6"The full moon's radiance above display white; the bloom feeds on it."
defparameter*cloud-altitude*80.0"The cumulus deck's world height, which both the sky and the ground read."
defparameter*cloud-shadow-depth*0.72"How much of the direct sun a cloud takes from the ground beneath it."
luvcraft:define-knobsky-scatter-gain
:group:sky:quantity
:quantity:scatter-gain:unit:one
:unit-label"×":minimum0.0:maximum3.0:step0.05
*sky-scatter-gain*
luvcraft:define-knobcloud-coverage
:group:sky:quantity
:quantity:cloud-coverage:unit:one
:unit-label"×":minimum0.0:maximum2.0:step0.05
*cloud-coverage*
luvcraft:define-knobstar-brightness
:group:sky:quantity
:quantity:star-brightness:unit:one
:minimum0.0:maximum6.0:step0.1
*star-brightness*
luvcraft:define-knobmoon-radiance
:group:sky:quantity
:quantity:moon-radiance:unit:one
:minimum0.0:maximum20.0:step0.2
*moon-radiance*
luvcraft:define-knobcloud-altitude
:group:sky:quantity
:quantity:cloud-altitude:unit:one
:minimum60.0:maximum900.0:step10.0
*cloud-altitude*
luvcraft:define-knobcloud-shadow-depth
:group:sky:label"cloud shadow":quantity
:quantity:cloud-coverage:unit:one
:minimum0.0:maximum1.0:step0.02
*cloud-shadow-depth*

The surface knobs. A block face is one tile of a sixteen-texel atlas and a thousand of them are in view at once, so how far apart two faces of the same material drift, and how the light they return is split between the sun, the sky, and the ground, are the questions art direction actually asks about the world's materials.

defparameter*surface-detail*0.85"How far the procedural weathering moves one face away from the next."
defparameter*surface-roughness*1.35"The whole palette's micro-roughness against what its relief implies."
defparameter*specular-gain*1.95"How much of the sun's microfacet lobe a block surface returns."
defparameter*ambient-bounce*1.05"How much of the ground's own colour a downward face is lit by."
luvcraft:define-knobsurface-detail
:group:surface:quantity
:quantity:surface-detail:unit:one
:unit-label"×":minimum0.0:maximum3.0:step0.05
*surface-detail*
luvcraft:define-knobsurface-roughness
:group:surface:quantity
:quantity:surface-roughness:unit:one
:unit-label"×":minimum0.3:maximum2.0:step0.05
*surface-roughness*
luvcraft:define-knobspecular-gain
:group:surface:quantity
:quantity:specular-gain:unit:one
:unit-label"×":minimum0.0:maximum4.0:step0.05
*specular-gain*
luvcraft:define-knobambient-bounce
:group:surface:quantity
:quantity:ambient-bounce:unit:one
:minimum0.0:maximum1.5:step0.05
*ambient-bounce*

What presentation does to the graded image after the filmic curve: the two grading controls every colourist reaches for first, and the lens's own small dispersion. They are folded literals rather than uniform lanes because they are art direction, changed by hand and then left.

defparameter*chromatic-aberration*0.70"How far the lens splits red from blue at the corners, in texels."
defparameter*grade-saturation*1.10"Saturation of the presented image, one being the filmic curve's own."
defparameter*grade-contrast*0.16"How far the presented image is pulled toward an S-curve."
luvcraft:define-knobchromatic-aberration
:group:grading:label"aberration":quantity
:quantity:chromatic-aberration:unit:one
:minimum0.0:maximum3.0:step0.05
*chromatic-aberration*
luvcraft:define-knobgrade-saturation
:group:grading:label"saturation":quantity
:quantity:grade-saturation:unit:one
:unit-label"×":minimum0.0:maximum2.0:step0.02
*grade-saturation*
luvcraft:define-knobgrade-contrast
:group:grading:label"contrast":quantity
:quantity:grade-contrast:unit:one
:minimum0.0:maximum1.0:step0.02
*grade-contrast*

Every stage which reads the frame environment declares the same uniform block at binding 2: identical member order and offsets are an ABI requirement, so the member list is written once and spliced at read time.

eval-when
:compile-toplevel:load-toplevel:execute
defparameter*frame-uniform-members*'
camera-vector:vec4

camera position, w unused

:components
:xyz:quantity:world-position:unit:cell
right-vector:vec4:components
:xyz:quantity:world-direction:unit:one
up-vector:vec4:components
:xyz:quantity:world-direction:unit:one
forward-vector:vec4:components
:xyz:quantity:world-direction:unit:one
projection-vector:vec4

x scale, y scale, z scale, z offset

:components
:x:quantity:projection-scale:unit:one
:y:quantity:projection-scale:unit:one
:z:quantity:projection-scale:unit:one
:w:quantity:view-distance:unit:cell
fog-vector:vec4

fog near, fog far, elapsed time, cloudiness

:components
:x:quantity:view-distance:unit:cell
:y:quantity:view-distance:unit:cell
:z:quantity:sky-time:unit:second
:w:quantity:cloudiness:unit:one
sun-vector:vec4

sun direction, day factor

:components
:xyz:quantity:world-direction:unit:one
:w:quantity:day-factor:unit:one
sun-color-vector:vec4

sun colour, angular width

:components
:xyz:quantity:linear-rgb:unit:one
:w:quantity:sun-disc-coordinate:unit:one
zenith-vector:vec4

zenith colour, w target height in pixels

:components
:xyz:quantity:linear-rgb:unit:one
:w:quantity:target-pixel-extent:unit:one
horizon-vector:vec4

horizon colour, w target width in pixels

:components
:xyz:quantity:linear-rgb:unit:one
:w:quantity:target-pixel-extent:unit:one
ambient-vector:vec4

ambient colour, exposure

:components
:xyz:quantity:linear-rgb:unit:one
fog-color-vector:vec4

fog colour, w shadow diagnostic selector

:components
:xyz:quantity:linear-rgb:unit:one
:w:quantity:shadow-diagnostic:unit:one
shadow-control-vector:vec4

texel u/v, base bias, slope bias

:components
:xy:quantity:shadow-uv:unit:one:character:difference
:z:quantity:shadow-depth:unit:one:character:difference
:w:quantity:shadow-depth:unit:one:character:difference
shadow-filter-vector:vec4

depth span, world/texel, min/max radius

:components
:x:quantity:world-distance:unit:cell
:y:quantity:world-distance:unit:cell
:z:quantity:shadow-filter-radius:unit:one
:w:quantity:shadow-filter-radius:unit:one
atlas-vector:vec4

atlas texel width, remaining lanes reserved

:components
:x:quantity:atlas-texel-width:unit:one
shadow-row-x:vec4

light-space clip x from world position

shadow-row-y:vec4

light-space clip y from world position

shadow-row-z:vec4

light-space depth from world position

shadow-row-w:vec4

light-space homogeneous w

"The one frame-environment uniform layout shared by all scene stages."
define-shader-methodshader-specification-forblock-world-vertex-specification
role
eql:block-surface
stage
eql:vertex
:stage:vertex:inputs
world-position:vec3:location0:quantity:world-position:unit:cell
local-uv-shade-input:vec3:location1:components
:xy:quantity:tile-local-uv:unit:one
:z:quantity:ambient-occlusion:unit:one
normal-input:vec3:location2:quantity:world-direction:unit:one
light-input:vec3:location3:components
:x:quantity:sky-light-level:unit:one
:y:quantity:block-light-level:unit:one
:z:quantity:material-emission:unit:one
tile-edge-input:vec2:location4:components
:x:quantity:atlas-tile-offset:unit:one
:y:quantity:packed-edge-shaping:unit:one
:outputs
clip-position:vec4:built-in:position
uv-shade-output:vec3:location0
normal-output:vec3:location1:quantity:world-direction:unit:one
fog-output:float:location2:quantity:fog-amount:unit:one
light-output:vec3:location3:components
:x:quantity:sky-light-level:unit:one
:y:quantity:block-light-level:unit:one
:z:quantity:material-emission:unit:one
shadow-uv-output:vec2:location4:quantity:shadow-uv:unit:one
shadow-depth-output:float:location5:quantity:shadow-depth:unit:one

The fragment stage shapes its own normal from where the fragment sits inside its cell, so it needs the world point rather than only the face's flat normal.

world-position-output:vec3:location6:quantity:world-position:unit:cell
edge-shaping-output:vec4:location7
tile-offset-output:float:location8:quantity:atlas-tile-offset:unit:one:interpolation:flat
:resources
frame-state:uniform-block:set0:binding2:members#.*frame-uniform-members*
let*
camera
swizzlecamera-vector:xyz
right
swizzleright-vector:xyz
up
swizzleup-vector:xyz
forward
swizzleforward-vector:xyz
relative
view-x
dotrelative
swizzleright-vector:xyz
view-y
dotrelative
swizzleup-vector:xyz
view-z
interpret
dotrelative
swizzleforward-vector:xyz
:quantity:view-distance:unit:cell
fog-near
swizzlefog-vector:x
fog-far
swizzlefog-vector:y
x-scale
swizzleprojection-vector:x
y-scale
swizzleprojection-vector:y
z-scale
swizzleprojection-vector:z
z-offset
swizzleprojection-vector:w
clip-x
*view-xx-scale
clip-y
-
*view-yy-scale
clip-z
+
interpret
*view-zz-scale
:quantity:view-distance:unit:cell
z-offset

Homogeneous clip is a heterogeneous GPU representation: XYZ and W do not constitute one semantic vector quantity.

shadow-projection
project-sample
project-point:world-to-lightworld-positionshadow-row-xshadow-row-yshadow-row-zshadow-row-w

Projecting either field lowers the shared homogeneous map once. Keeping depth first preserves the established instruction order.

shadow-depth
swizzleshadow-projection:z
local-uv
representation
swizzlelocal-uv-shade-input:xy
tile-offset
representation
swizzletile-edge-input:x
atlas-width
representation
swizzleatlas-vector:x

Resolve normalized atlas coordinates at vertex execution time. They are recomputed every draw from the renderer-owned width, but retain the old interpolation and exact half-texel endpoint values.

atlas-uv
vec2
/
+tile-offset
swizzlelocal-uv:x
/atlas-width16.0
swizzlelocal-uv:y
uv-shade
vec3
swizzleatlas-uv:x
swizzleatlas-uv:y
representation
swizzlelocal-uv-shade-input:z

Four -1/0/1 edge classifications occupy one exact base-three scalar in the vertex stream. Unpack before interpolation; each face supplies the same code at all of its vertices.

packed
representation
swizzletile-edge-input:y
u-low-digit
floor
/packed27.0
after-u-low
-packed
*u-low-digit27.0
u-high-digit
floor
/after-u-low9.0
after-u-high
-after-u-low
*u-high-digit9.0
v-low-digit
floor
/after-u-high3.0
v-high-digit
-after-u-high
*v-low-digit3.0
edge-shaping
vec4
-u-low-digit1.0
-u-high-digit1.0
-v-low-digit1.0
-v-high-digit1.0
set-outputclip-positionclip
set-outputuv-shade-outputuv-shade
set-outputnormal-outputnormal-input
set-outputfog-outputfog-amount
set-outputlight-outputlight-input
set-outputshadow-uv-output
swizzleshadow-projection:xy
set-outputshadow-depth-outputshadow-depth
set-outputworld-position-outputworld-position
set-outputedge-shaping-outputedge-shaping
set-outputtile-offset-output
swizzletile-edge-input:x
defunblock-world-camera-uniform-block

The frame uniform block exactly as the vertex specification declares it.

find-if
lambda
resource
typepresource'shader-uniform-block
shader-specification-resources
defunblock-world-vertex-lowering

Compile the camera transform and retain expression-to-SSA provenance.

World text is ordinary scene geometry. Its model transform has already placed each glyph quad in world coordinates; this stage applies the same camera view and perspective projection as block surfaces, while preserving the em-space coordinate and current projected pixel scale Slug consumes.

define-shader-methodshader-specification-forblock-world-text-vertex-specification
role
eql:slug-world-text
stage
eql:vertex
:stage:vertex:inputs
quad-corner:vec3:location0
world-origin:vec3:location1
world-right-edge:vec3:location2
world-up-edge:vec3:location3
outline-low:vec3:location4
outline-high:vec3:location5
atlas-input:vec3:location6
band-low:vec3:location7
band-high:vec3:location8
:outputs
clip-position:vec4:built-in:position
render-coordinate:vec2:location0
render-atlas-base:vec2:location1
render-band-bounds:vec4:location2
render-band-counts:vec2:location3
render-color:vec4:location4
:resources
frame-state:uniform-block:set0:binding2:members#.*frame-uniform-members*

Dynamic dilation, after the reference's vertex stage: the quad grows past the outline by a fixed number of pixels, however large or small the glyph lands on screen, so the pixel filter's half-width is always inside it and no more. A vertex knows its own depth and the frame knows the target's height, which together give the pixel length of each em edge; the corner then slides outward along both edges by the dilation over that length. Em coordinates slide with it, so the outline stays where it was.

let*
camera
swizzlecamera-vector:xyz
right
representation
swizzleright-vector:xyz
up
forward
representation
swizzleforward-vector:xyz
corner
swizzlequad-corner:xy
undilated-position
+world-origin
*world-right-edge
swizzlecorner:x
*world-up-edge
swizzlecorner:y
undilated-relative
-undilated-position
undilated-depth
max
dotundilated-relativeforward
0.001
pixels-per-unit
/
*
representation
swizzleprojection-vector:y
representation
swizzlezenith-vector:w
0.5
undilated-depth
right-edge-pixels
*pixels-per-unit
sqrt
+
*
dotworld-right-edgeright
dotworld-right-edgeright
*
dotworld-right-edgeup
dotworld-right-edgeup
up-edge-pixels
*pixels-per-unit
sqrt
+
*
dotworld-up-edgeright
dotworld-up-edgeright
*
dotworld-up-edgeup
dotworld-up-edgeup
dilated-corner
+corner
*
-
*corner2.0
vec21.01.0
vec2
/dilation
maxright-edge-pixels0.001
/dilation
maxup-edge-pixels0.001
world-position
assume-quantity
+world-origin
*world-right-edge
swizzledilated-corner:x
*world-up-edge
swizzledilated-corner:y
:quantity:world-position:unit:cell
outline-coordinate
+
swizzleoutline-low:xy
*
-
swizzleoutline-high:xy
swizzleoutline-low:xy
dilated-corner
relative
view-x
dotrelative
swizzleright-vector:xyz
view-y
dotrelative
swizzleup-vector:xyz
view-z
interpret
dotrelative
swizzleforward-vector:xyz
:quantity:view-distance:unit:cell
x-scale
swizzleprojection-vector:x
y-scale
swizzleprojection-vector:y
z-scale
swizzleprojection-vector:z
z-offset
swizzleprojection-vector:w
clip-x
*view-xx-scale
clip-y
-
*view-yy-scale
clip-z
+
interpret
*view-zz-scale
:quantity:view-distance:unit:cell
z-offset
set-outputclip-positionclip
set-outputrender-coordinateoutline-coordinate
set-outputrender-atlas-base
swizzleatlas-input:xy
set-outputrender-band-bounds
vec4
swizzleband-low:xy
swizzleband-high:xy
set-outputrender-band-counts
vec2
swizzleoutline-low:z
swizzleoutline-high:z

The instance record's three spare Z lanes carry the linear ink colour.

set-outputrender-color
vec4
swizzleatlas-input:z
swizzleband-low:z
swizzleband-high:z
1.0

Terminal cell backgrounds are analytic world rectangles. The instance carries the exact painted rectangle; the vertex stage inflates the quad by a small margin and hands the fragment stage a rectangle-local coordinate, where the same signed-distance coverage as the GUI round-rects resolves the edge against the screen gradient. They share the text run's frame bind group and premultiplied blending.

define-shader-methodshader-specification-forterminal-cell-vertex-specification
role
eql:terminal-cell
stage
eql:vertex
:stage:vertex:inputs
quad-corner:vec3:location0
world-origin:vec3:location1
world-right-edge:vec3:location2
world-up-edge:vec3:location3
ink-input:vec3:location4:quantity:linear-rgb:unit:one
:outputs
clip-position:vec4:built-in:position
render-coordinate:vec2:location0
render-half-size-radius:vec3:location1
render-color:vec4:location2
:resources
frame-state:uniform-block:set0:binding2:members#.*frame-uniform-members*
let*
width
sqrt
dotworld-right-edgeworld-right-edge
height
sqrt
dotworld-up-edgeworld-up-edge

Inflate by a fraction of the row height on every side so the antialiased edge has room to fall off outside the exact rectangle.

margin
u
-
*
swizzlequad-corner:x
+width
*2.0margin
margin
v
-
*
swizzlequad-corner:y
+height
*2.0margin
margin
world-position
assume-quantity
+world-origin
*world-right-edge
*world-up-edge
:quantity:world-position:unit:cell
camera
swizzlecamera-vector:xyz
right
swizzleright-vector:xyz
up
swizzleup-vector:xyz
forward
swizzleforward-vector:xyz
relative
view-x
dotrelative
swizzleright-vector:xyz
view-y
dotrelative
swizzleup-vector:xyz
view-z
interpret
dotrelative
swizzleforward-vector:xyz
:quantity:view-distance:unit:cell
x-scale
swizzleprojection-vector:x
y-scale
swizzleprojection-vector:y
z-scale
swizzleprojection-vector:z
z-offset
swizzleprojection-vector:w
clip-x
*view-xx-scale
clip-y
-
*view-yy-scale
clip-z
+
interpret
*view-zz-scale
:quantity:view-distance:unit:cell
z-offset
set-outputclip-positionclip
set-outputrender-coordinate
set-outputrender-half-size-radius
set-outputrender-color
vec41.0
define-shader-methodshader-specification-forterminal-cell-fragment-specification
role
eql:terminal-cell
stage
eql:fragment
:stage:fragment:inputs
render-coordinate:vec2:location0
half-size-radius:vec3:location1
color:vec4:location2
:outputs
color-output:vec4:location0
let*
coverage
luv.analytic:roundrect-coveragerender-coordinatehalf-size-radius
set-outputcolor-output
*colorcoverage

The terminal is a tube behind glass, drawn as three coplanar rectangles. This vertex stage serves the two outer ones: the phosphor plate beneath the cell backgrounds and glyphs, and the faceplate glass above them. Both want the same rectangle-local coordinate, the same world position for view-dependent shading, and the surface's own frame, so the stage hands down the normalized right and up edges alongside the outward normal. The instance layout stays the cell run's, with the fourth vector carrying the normal, so one vertex buffer description serves every terminal rectangle.

define-shader-methodshader-specification-forterminal-screen-vertex-specification
role
eql:terminal-screen
stage
eql:vertex
:stage:vertex:inputs
quad-corner:vec3:location0
world-origin:vec3:location1
world-right-edge:vec3:location2
world-up-edge:vec3:location3
normal-input:vec3:location4
:outputs
clip-position:vec4:built-in:position
render-coordinate:vec2:location0
render-half-size:vec2:location1
render-world-position:vec3:location2
render-normal:vec3:location3
render-right:vec3:location4
render-up:vec3:location5
:resources
frame-state:uniform-block:set0:binding2:members#.*frame-uniform-members*
let*
width
sqrt
dotworld-right-edgeworld-right-edge
height
sqrt
dotworld-up-edgeworld-up-edge
u
*
swizzlequad-corner:x
width
v
*
swizzlequad-corner:y
height
world-position
assume-quantity
+world-origin
*world-right-edge
swizzlequad-corner:x
*world-up-edge
swizzlequad-corner:y
:quantity:world-position:unit:cell
camera
swizzlecamera-vector:xyz
right
swizzleright-vector:xyz
up
swizzleup-vector:xyz
forward
swizzleforward-vector:xyz
relative
view-x
dotrelative
swizzleright-vector:xyz
view-y
dotrelative
swizzleup-vector:xyz
view-z
interpret
dotrelative
swizzleforward-vector:xyz
:quantity:view-distance:unit:cell
x-scale
swizzleprojection-vector:x
y-scale
swizzleprojection-vector:y
z-scale
swizzleprojection-vector:z
z-offset
swizzleprojection-vector:w
clip-x
*view-xx-scale
clip-y
-
*view-yy-scale
clip-z
+
interpret
*view-zz-scale
:quantity:view-distance:unit:cell
z-offset
set-outputclip-positionclip
set-outputrender-coordinate
set-outputrender-half-size
set-outputrender-world-position
set-outputrender-normalnormal-input
set-outputrender-right
*world-right-edge
set-outputrender-up
*world-up-edge

Where the tube's active area stops and its bezel begins is a fact about the panel, not about either material, so both stages ask the same two questions instead of repeating a literal that could drift apart.

define-shader-functionterminal-panel-edge-distance
coordinatehalf-size
"World-cell distance from a panel-local point to the nearest panel edge."
min
-
swizzlehalf-size:x
abs
swizzlecoordinate:x
-
swizzlehalf-size:y
abs
swizzlecoordinate:y
define-shader-functionterminal-screen-coverage
edge-distance
"One inside the tube's active area, zero on the bezel, resolved between."
smoothstep0.110.122edge-distance

The phosphor plate is the back of the tube: what the glyphs are painted on, and what remains when the terminal is blank. It is deliberately not the glass -- reflections belong on the faceplate above the text, where they can actually veil it -- so this stage only answers what the tube emits and how its recess and bezel catch the daylight of the world it hangs in.

define-shader-methodshader-specification-forterminal-screen-fragment-specification
role
eql:terminal-screen
stage
eql:fragment
:stage:fragment:inputs
render-coordinate:vec2:location0
half-size:vec2:location1
world-position:vec3:location2
normal-input:vec3:location3
right-input:vec3:location4
up-input:vec3:location5
:outputs
color-output:vec4:location0
:resources
frame-state:uniform-block:set0:binding2:members#.*frame-uniform-members*
let*
camera
representation
swizzlecamera-vector:xyz
sun-direction
representation
swizzlesun-vector:xyz
sun-color
representation
swizzlesun-color-vector:xyz
ambient
representation
swizzleambient-vector:xyz
normal
normalizenormal-input
n-dot-l
max0.0
dotnormalsun-direction
irradiance
+ambient
*sun-color
edge-distance
terminal-panel-edge-distancerender-coordinatehalf-size
screen-mask

Panel-local coordinates normalized to the corners, so the tube's falloff is a property of the picture rather than of its size.

u
/
swizzlerender-coordinate:x
swizzlehalf-size:x
v
/
swizzlerender-coordinate:y
swizzlehalf-size:y
radius-squared
+
*uu
*vv

The screen sits behind the bezel, so shade falls in from the lip, and a tube's own glass is brightest through the middle.

inset-shade
smoothstep0.00.5
-edge-distance0.11
centre-weight
mix1.00.55
clamp
*radius-squared0.7
0.01.0
vignette
*
mix0.261.0inset-shade
centre-weight
glass-albedo
vec30.0090.0090.012

A faint idling phosphor: warm enough to read as a lit tube rather than a hole, dim enough to stay black next to any real glyph.

phosphor
vec30.00900.01120.0104
screen-color
*
+
*glass-albedoirradiance
phosphor
vignette

The bezel is matte dark plastic: a lit inner lip where it turns toward the screen, and a slightly brighter outer chamfer.

lip
smoothstep0.080.11edge-distance
chamfer
smoothstep0.050.0edge-distance
bezel-albedo
mix
mix
vec30.0210.0200.022
vec30.0880.0880.096
lip
vec30.0420.0400.041
chamfer
half-vector
normalize
+sun-directionview
n-dot-h
max0.0
dotnormalhalf-vector
bezel-specular
*sun-color
*
exptn-dot-h42.0
0.09day-factor
bezel-color
+
*bezel-albedoirradiance
bezel-specular
rgb
mixbezel-colorscreen-colorscreen-mask
set-outputcolor-output

The faceplate is the glass in front of the picture, drawn last so that everything it does happens *to* the text rather than behind it. It is the whole analogue argument of the terminal, and every term in it is one physical claim about a tube seen through coated glass:

  • the raster is a fixed line count belonging to the panel, so it holds

still in the world instead of crawling with the camera, and it fades out before it can alias rather than shimmering at a distance;

  • the glass is very slightly convex, expressed only as a bent normal,

so the room wraps around the rim while every glyph stays exactly where the glyph pass drew it -- curvature as light, not as geometry;

  • mains hum and a little grain keep the picture from being perfectly

still, at amplitudes chosen to be felt rather than seen.

Premultiplied alpha makes one draw do both halves of that: the alpha lane attenuates the picture underneath (raster, corners, hum, grain) and the colour lane adds what the glass itself sends back (sky, sun, room).

define-shader-methodshader-specification-forterminal-faceplate-fragment-specification
role
eql:terminal-faceplate
stage
eql:fragment
:stage:fragment:inputs
render-coordinate:vec2:location0
half-size:vec2:location1
world-position:vec3:location2
normal-input:vec3:location3
right-input:vec3:location4
up-input:vec3:location5
:outputs
color-output:vec4:location0
:resources
frame-state:uniform-block:set0:binding2:members#.*frame-uniform-members*
let*
camera
representation
swizzlecamera-vector:xyz
sun-direction
representation
swizzlesun-vector:xyz
sun-color
representation
swizzlesun-color-vector:xyz
zenith
representation
swizzlezenith-vector:xyz
horizon
representation
swizzlehorizon-vector:xyz
ambient
representation
swizzleambient-vector:xyz
elapsed
flat-normal
normalizenormal-input
right
normalizeright-input
up
normalizeup-input
half-x
swizzlehalf-size:x
half-y
swizzlehalf-size:y
u
/
swizzlerender-coordinate:x
half-x
v
/
swizzlerender-coordinate:y
half-y

A cubic bulge leaves the middle of the plate flat and turns only the last part of the way to the rim, which is what a real face does and what keeps the reflection from sliding about.

normal
normalize
+flat-normal
+
*right
*bulge
*u
*uu
*up
*bulge
*v
*vv
n-dot-v
max0.0
dotnormalview

Schlick Fresnel for coated glass: a low floor rising toward grazing, so a frontal view stays dark and an oblique one lights up.

fresnel
+0.021
*0.979
expt
-1.0n-dot-v
5.0

The mirrored view ray samples the same two-colour sky gradient the sky dome draws, so the glass reflects the day it stands in.

reflected
-
*normal
*2.0
dotnormalview
view
sky-height
smoothstep-0.060.5
swizzlereflected:y
sky-reflection
*
mixhorizonzenithsky-height
*fresnel0.72
room-reflection
*ambient
*fresnel0.55

A tight glint plus a broad sheen; the bent normal drags both of them into a curved streak near the rim.

half-vector
normalize
+sun-directionview
n-dot-h
max0.0
dotnormalhalf-vector
glint
*sun-color
*
exptn-dot-h200.0
2.4day-factor
sheen
*sun-color
*
exptn-dot-h26.0
0.10day-factor
edge-distance
terminal-panel-edge-distancerender-coordinatehalf-size
screen-mask

The raster: a fixed number of lines across this panel's height, exactly as a tube has a fixed line count whatever its diagonal.

line-pitch
vertical-dx
derivative-x
swizzlerender-coordinate:y
vertical-dy
derivative-y
swizzlerender-coordinate:y
cells-per-pixel
max
sqrt
+
*vertical-dxvertical-dx
*vertical-dyvertical-dy
1e-6
pixels-per-line
/line-pitchcells-per-pixel

Below two pixels a line cannot be drawn, only aliased, so the raster leaves rather than turning into moire at a distance.

raster-fade
smoothstep1.83.6pixels-per-line
line-phase
/
swizzlerender-coordinate:y
line-pitch
line-profile
+0.5
*0.5
cos
*6.2831855line-phase
scanline
*scanline-depthraster-fade
-1.0line-profile

Attenuating the picture is only half of a raster. A tonemapped frame puts bright text on the shoulder of the filmic curve, where a multiply barely moves it, and puts a dark terminal's background near black, where a multiply moves it not at all. So the tube's own idle glow below carries the beam instead: energy-preserving about one, so the raster shapes that glow without lighting it.

beam
mix1.0
+0.55
*0.9line-profile
raster-fade

A slow bar drifting up the picture and a fast shimmer on top of it: the two ways a tube betrays the mains it is plugged into.

hum-phase
fract
-
*v0.5
*elapsed0.052
hum-band
expt
+0.5
*0.5
cos
*6.2831855hum-phase
4.0
hum
*0.020hum-band
flicker
*0.008
+0.5
*0.5
sin
*elapsed47.3

Grain is resampled a couple of dozen times a second rather than every frame, so it reads as emulsion instead of as a frame rate.

grain
lattice-hash
vec3
*
swizzlerender-coordinate:x
617.0
*
swizzlerender-coordinate:y
613.0
floor
*elapsed26.0
speckle
*0.018grain

The corners of a tube are always darker than its middle, and this is the one such falloff that also reaches the text.

corner-shade
smoothstep0.551.35
+
*uu
*vv
corner
*0.16corner-shade

Glass scatters a little of the room forward instead of reflecting all of it, and a tube idles with a little phosphor of its own. Together they lift the black off the floor -- the difference between a screen and a hole in the wall -- and because the corner falloff multiplies them, this is where the shape of the tube actually becomes visible on an otherwise dark terminal.

haze
*
+
vec30.00720.00840.0104
*ambient0.045
*beam
mix1.00.30corner-shade
attenuation
clamp
+scanlinecornerhumflickerspeckle
0.0screen-effect-ceiling
emission
+sky-reflectionroom-reflectionglintsheenhaze
set-outputcolor-output
vec4
*emissionscreen-mask
*attenuationscreen-mask

The phone's screen is not a tube. It is a sheet of flat glass over a dark panel, and everything the wall's faceplate argues about -- raster, hum, grain, the tube's convex bulge and its darker corners -- is exactly what a retina slab does not have. What it has instead is a rounded edge, a Fresnel reflection of the sky it is held under, one tight glint, and, because someone has been holding it, fingerprints: patches of oil that scatter a little of the room forward where clean glass would not. Both stages below share the wall's vertex stage; only the material is the phone's own.

define-shader-functionphone-glass-distance
coordinatehalf-sizeradius

Signed distance from a panel-local point to the rounded glass edge, negative inside. RADIUS is the corner radius in world cells.

let*
qx
max
-
abs
swizzlecoordinate:x
-
swizzlehalf-size:x
radius
0.0
qy
max
-
abs
swizzlecoordinate:y
-
swizzlehalf-size:y
radius
0.0
-
sqrt
+
*qxqx
*qyqy
radius
define-shader-functionphone-glass-coverage
coordinatehalf-sizeradius
"One on the glass, zero past its rounded edge, resolved over one pixel."
let*
distance
phone-glass-distancecoordinatehalf-sizeradius
dx
dy
pixel
max
sqrt
+
*dxdx
*dydy
1e-5
define-shader-methodshader-specification-forphone-screen-fragment-specification
role
eql:phone-screen
stage
eql:fragment
:stage:fragment:inputs
render-coordinate:vec2:location0
half-size:vec2:location1
world-position:vec3:location2
normal-input:vec3:location3
right-input:vec3:location4
up-input:vec3:location5
:outputs
color-output:vec4:location0
:resources
frame-state:uniform-block:set0:binding2:members#.*frame-uniform-members*
let*
sun-direction
representation
swizzlesun-vector:xyz
sun-color
representation
swizzlesun-color-vector:xyz
ambient
representation
swizzleambient-vector:xyz
normal
normalizenormal-input
n-dot-l
max0.0
dotnormalsun-direction
irradiance
+ambient
*sun-color
coverage
phone-glass-coveragerender-coordinatehalf-size0.02

The panel behind the glass: black, and lit only by what little the room puts into it. Off, it should read as a dark mirror.

panel-albedo
vec30.0050.0050.006
rgb
*panel-albedoirradiance
set-outputcolor-output
vec4
*rgbcoverage
coverage
define-shader-methodshader-specification-forphone-glass-fragment-specification
role
eql:phone-glass
stage
eql:fragment
:stage:fragment:inputs
render-coordinate:vec2:location0
half-size:vec2:location1
world-position:vec3:location2
normal-input:vec3:location3
right-input:vec3:location4
up-input:vec3:location5
:outputs
color-output:vec4:location0
:resources
frame-state:uniform-block:set0:binding2:members#.*frame-uniform-members*
let*
camera
representation
swizzlecamera-vector:xyz
sun-direction
representation
swizzlesun-vector:xyz
sun-color
representation
swizzlesun-color-vector:xyz
zenith
representation
swizzlezenith-vector:xyz
horizon
representation
swizzlehorizon-vector:xyz
ambient
representation
swizzleambient-vector:xyz
normal
normalizenormal-input
n-dot-v
max0.0
dotnormalview

Coated glass: dark seen straight on, a mirror seen at a slant.

fresnel
+0.018
*0.982
expt
-1.0n-dot-v
5.0
reflected
-
*normal
*2.0
dotnormalview
view
sky-height
smoothstep-0.060.5
swizzlereflected:y
sky-reflection
*
mixhorizonzenithsky-height
*fresnel0.8
room-reflection
*ambient
*fresnel0.5
half-vector
normalize
+sun-directionview
n-dot-h
max0.0
dotnormalhalf-vector
glint
*sun-color
*
exptn-dot-h320.0
2.0day-factor

Fingerprints. Oil on the glass scatters light forward over a broad lobe where the clean coating would send it away, so the smudges only appear where there is light to scatter -- near the sun's sheen and at a slant to the sky -- and never as paint.

u
swizzlerender-coordinate:x
v
swizzlerender-coordinate:y
smudge-field
smudge
*
smoothstep0.560.74smudge-field
0.22
sheen
*sun-color
*
exptn-dot-h14.0
0.05day-factor
scatter
*
+sheen
*
+ambient
*horizon0.4
0.035
smudge
coverage
phone-glass-coveragerender-coordinatehalf-size0.02
emission
+sky-reflectionroom-reflectionglintscatter

The glass takes nothing away from the picture under it: alpha zero, and only what it sends back is added.

set-outputcolor-output
vec4
*emissioncoverage
0.0

A video screen is the plainest world rectangle luvcraft draws: the same instance record as a terminal cell, but the fragment stage reads a decoded picture instead of computing a material. The picture is the only thing on it, so there is no shading model here at all -- the texture is sRGB and the hardware decode hands the scene target the linear radiance it wants.

The one non-obvious line is the flipped V. A quad corner runs from zero at the bottom to one at the top, and a decoded frame's first row is its top, so a screen that did not flip would play the film upside down.

define-shader-methodshader-specification-forvideo-screen-vertex-specification
role
eql:video-screen
stage
eql:vertex
:stage:vertex:inputs
quad-corner:vec3:location0
world-origin:vec3:location1
world-right-edge:vec3:location2
world-up-edge:vec3:location3
:outputs
clip-position:vec4:built-in:position
render-uv:vec2:location0
:resources
frame-state:uniform-block:set0:binding2:members#.*frame-uniform-members*
let*
corner-x
swizzlequad-corner:x
corner-y
swizzlequad-corner:y
world-position
assume-quantity
+world-origin
*world-right-edgecorner-x
*world-up-edgecorner-y
:quantity:world-position:unit:cell
camera
swizzlecamera-vector:xyz
right
swizzleright-vector:xyz
up
swizzleup-vector:xyz
forward
swizzleforward-vector:xyz
relative
view-x
dotrelative
swizzleright-vector:xyz
view-y
dotrelative
swizzleup-vector:xyz
view-z
interpret
dotrelative
swizzleforward-vector:xyz
:quantity:view-distance:unit:cell
x-scale
swizzleprojection-vector:x
y-scale
swizzleprojection-vector:y
z-scale
swizzleprojection-vector:z
z-offset
swizzleprojection-vector:w
clip-x
*view-xx-scale
clip-y
-
*view-yy-scale
clip-z
+
interpret
*view-zz-scale
:quantity:view-distance:unit:cell
z-offset
set-outputclip-positionclip
set-outputrender-uv
vec2corner-x
-1.0corner-y
define-shader-methodshader-specification-forvideo-screen-fragment-specification
role
eql:video-screen
stage
eql:fragment
:stage:fragment:inputs
uv-input:vec2:location0:quantity:texture-uv:unit:one
:outputs
color-output:vec4:location0
:resources
video-picture:texture-2d:set0:binding0:sample-transfer:srgb-to-linear:sample-components
:rgb:quantity:linear-rgb:unit:one
video-sampler:sampler:set0:binding1
frame-state:uniform-block:set0:binding2:members#.*frame-uniform-members*

The scene pass writes raw radiance: exposure and the filmic curve belong to the post chain, so a screen that emits its own picture writes the decoded picture and stops there.

let*
picture
samplevideo-picturevideo-sampleruv-input
radiance
swizzlepicture:rgb
set-outputcolor-output

A portal is another game's picture arriving on a wall, and it should read as exactly that: a transmission, not a window and not a movie. The panel is the whole terminal face; the picture sits inside it on a dark matte with the same margin the text grid keeps, so the wall's proportions do not change when the shell becomes a portal. What marks the picture as coming from elsewhere is done to its signal, not to the glass (the faceplate above still supplies raster, hum, and reflections as it does for text):

  • the colour channels do not quite converge -- a radial chromatic

fringe, nothing at the centre and a few texels at the corners;

  • alternate lines are displaced by a texel or so on a slow beat, a

field interlace that never fully locks;

  • a soft sync bar climbs the picture, brighter than the hum bar the

glass adds, so the two are visibly different mechanisms;

  • the picture warms up over half a second when the link opens instead

of appearing, and its edge is a thin phosphor line on the matte.

The uniform carries the picture rectangle in panel UV and the moment the link opened; everything else is the frame environment's clock.

eval-when
:compile-toplevel:load-toplevel:execute
defparameter*portal-uniform-members*'
picture-rect:vec4

picture u0, v0, u1, v1 within the panel

portal-control:vec4

opened-at seconds, texel u, texel v, unused

"The uniform layout of one portal panel."
define-shader-methodshader-specification-forportal-screen-fragment-specification
role
eql:portal-screen
stage
eql:fragment
:stage:fragment:inputs
uv-input:vec2:location0:quantity:texture-uv:unit:one
:outputs
color-output:vec4:location0
:resources
portal-picture:texture-2d:set0:binding0:sample-transfer:srgb-to-linear:sample-components
:rgb:quantity:linear-rgb:unit:one
portal-sampler:sampler:set0:binding1
frame-state:uniform-block:set0:binding2:members#.*frame-uniform-members*
portal-state:uniform-block:set0:binding3:members#.*portal-uniform-members*
let*
elapsed
opened
swizzleportal-control:x
texel
swizzleportal-control:yz
rect-min
swizzlepicture-rect:xy
rect-max
swizzlepicture-rect:zw
rect-size
-rect-maxrect-min

Panel UV to picture UV; the picture is where both lie in [0,1].

uv
picture-uv
/
-uvrect-min
rect-size
px
swizzlepicture-uv:x
py
swizzlepicture-uv:y
inside-x
*
step0.0px
steppx1.0
inside-y
*
step0.0py
steppy1.0
inside
*inside-xinside-y

Warm-up: half a second from black to picture.

age
-elapsedopened
warmth
smoothstep0.00.55age

Field interlace: odd picture lines drift a texel sideways on a slow beat, even lines the other way, never quite locking.

line
floor
/py
*2.0
swizzletexel:y
field
-
*2.0
fract
*line0.5
0.5
weave
*field
*2.5
swizzletexel:x
sin
+
*elapsed1.7
*py9.0

The odd field is a shade dimmer than the even one, so the interlace shows as texture where the weave alone would not.

field-shade
-1.0
*0.10
+0.5field
woven
+picture-uv
vec2weave0.0

Chromatic fringe: red and blue pulled apart radially, growing with the square of the distance from the centre.

centered
-woven
vec20.50.5
radius2
dotcenteredcentered
fringe
*centered
*radius2
*22.0
swizzletexel:x
red
swizzle
representation
sampleportal-pictureportal-sampler
assume-quantity
+wovenfringe
:quantity:texture-uv:unit:one
:x
green
swizzle
representation
sampleportal-pictureportal-sampler
assume-quantitywoven:quantity:texture-uv:unit:one
:y
blue
swizzle
representation
sampleportal-pictureportal-sampler
assume-quantity
-wovenfringe
:quantity:texture-uv:unit:one
:z
picture
vec3redgreenblue

The sync bar: a soft band climbing the picture every few seconds.

bar-phase
fract
-
*py0.7
*elapsed0.11
bar
expt
+0.5
*0.5
cos
*6.2831855bar-phase
6.0
lifted
*picture
*field-shade
+1.0
*0.16bar

A little vignette of the picture's own, inside the frame.

vignette
-1.0
*0.22
smoothstep0.300.95radius2
signal
*lifted
*vignettewarmth

The matte and its phosphor edge. Distance to the picture edge in panel UV; the edge line is a couple of texels wide and glows a little further into the matte.

edge-dx
max
-
swizzlerect-min:x
-
swizzlerect-max:x
edge-dy
max
-
swizzlerect-min:y
-
swizzlerect-max:y
outside-distance
max0.0
maxedge-dxedge-dy
line-width
*2.5
swizzletexel:x
swizzlerect-size:x
edge-line
-1.0
smoothstep0.0line-widthoutside-distance
edge-glow
-1.0
smoothstep0.0
*12.0line-width
outside-distance
phosphor
vec30.160.620.55
matte
vec30.00750.00800.0100
matte-color
+matte
*phosphor
*warmth
+
*0.55edge-line
*0.06edge-glow
rgb
mixmatte-colorsignalinside
set-outputcolor-output
define-shader-methodshader-specification-forvideo-screen-hardware-fragment-specification
role
eql:video-screen-hardware
stage
eql:fragment
:stage:fragment:inputs
uv-input:vec2:location0:quantity:texture-uv:unit:one
:outputs
color-output:vec4:location0
:resources
video-luma:texture-2d:set0:binding0
video-sampler:sampler:set0:binding1
frame-state:uniform-block:set0:binding2:members#.*frame-uniform-members*
video-chroma:texture-2d:set0:binding3

VideoToolbox's ordinary bi-planar output is video-range NV12. Sampling the two CVMetalTextures performs no copy; this matrix is the only colour conversion between the decoder surface and the HDR scene target.

let*
y
-
swizzle
representation
samplevideo-lumavideo-sampleruv-input
:x
/16.0255.0
uv
-
swizzle
representation
samplevideo-chromavideo-sampleruv-input
:xy
vec20.50.5
u
v
r
+
*1.164383y
*1.792741v
g
-
*1.164383y
*0.213249u
*0.532909v
b
+
*1.164383y
*2.112402u
set-outputcolor-output
vec4rgb1.0
defmethodshader-specification-for
role
eql:slug-world-text
stage
eql:fragment
declare
ignorerolestage
define-shader-methodshader-specification-forblock-world-fragment-specification
role
eql:block-surface
stage
eql:fragment
:stage:fragment:inputs
uv-shade-input:vec3:location0:components
:xy:quantity:texture-uv:unit:one
:z:quantity:ambient-occlusion:unit:one
normal-input:vec3:location1:quantity:world-direction:unit:one
fog-input:float:location2:quantity:fog-amount:unit:one
light-input:vec3:location3:components
:x:quantity:sky-light-level:unit:one
:y:quantity:block-light-level:unit:one
:z:quantity:material-emission:unit:one
shadow-uv-input:vec2:location4:quantity:shadow-uv:unit:one
shadow-depth-input:float:location5:quantity:shadow-depth:unit:one
world-position-input:vec3:location6:quantity:world-position:unit:cell
edge-shaping-input:vec4:location7
tile-offset-input:float:location8:quantity:atlas-tile-offset:unit:one:interpolation:flat
:outputs
color-output:vec4:location0
:resources
block-atlas:texture-2d:set0:binding0:sample-transfer:srgb-to-linear:sample-components
:rgb:quantity:linear-rgb:unit:one
block-sampler:sampler:set0:binding1
frame-state:uniform-block:set0:binding2:members#.*frame-uniform-members*
shadow-map:depth-texture-2d:set0:binding3:sample-components
:x:quantity:shadow-depth:unit:one
shadow-sampler:sampler:set0:binding4
shadow-comparison-sampler:sampler:set0:binding5
block-normal-atlas:texture-2d:set0:binding6:sample-transfer:identity:sample-components
:rgb:quantity:surface-normal-sample:unit:one
:a:quantity:surface-relief:unit:one
let*
uv-shadeuv-shade-input
uv
swizzleuv-shade:xy
ao
swizzleuv-shade:z
normalnormal-input

--- the shaped surface ------------------------------------------- A block face is flat geometry, but it should not read as a flat material. Two shapings ride on the same face: a bevel that rounds the last sixth of a cell toward its edge, and a per-texel relief read straight out of the atlas the material is already painted in. Both are pure normal perturbation, so no mesh changes.

flat-normal
representationnormal-input
surface-point
representationworld-position-input
eye
representation
swizzlecamera-vector:xyz
cell
fractsurface-point
axis-x
abs
swizzleflat-normal:x
axis-y
abs
swizzleflat-normal:y

A block face's own normal selects its two in-plane world axes, so one expression serves all six faces without a branch.

tangent-u
vec3
-1.0axis-x
0.0axis-x
tangent-v
vec30.0
-1.0axis-y
axis-y
plane-u
plane-v

The atlas coordinate and its screen derivative size both shapings. The normal itself is already the central difference of the same procedural relief field that paints the colour atlas.

tile-count11.0
tile-texels16.0

Texels per pixel: one cell is TILE-TEXELS of them.

footprint
max
*
*tile-counttile-texels
*tile-texels

Both shapings are sub-block detail, so both have to fade out as soon as they stop resolving on screen; otherwise distant terrain sparkles and shows a lit grid instead of a surface. One measured footprint drives both, at their own scales.

relief-fade
-1.0
smoothstep0.301.10footprint
bevel-fade
-1.0
smoothstep1.605.00footprint

The mesher's edge classification decides what each boundary does. A convex edge rounds the surface over; a concave one fillets it into the inner corner; a flush one -- the middle of an open plain, where the face simply continues into an identically oriented neighbour -- leaves the surface alone. Signs come straight from the vertex lane, so the same ramp expression serves all three.

edgeedge-shaping-input
edge-u-low
swizzleedge:x
edge-u-high
swizzleedge:y
edge-v-low
swizzleedge:z
edge-v-high
swizzleedge:w

A round-over is only legible if it is a few pixels wide. Held to a fixed fraction of a cell it dwindles to nothing a short way off, and every block in the middle distance flattens back into a card; grown with the footprint it keeps its shape until it is genuinely too small to resolve. The upper bound stops it from eating the face it is supposed to be an edge of.

bevel-width
clamp
*0.115footprint
0.1050.185
ramp-u-low
smoothstepbevel-width0.0plane-u
ramp-u-high
smoothstep
-1.0bevel-width
1.0plane-u
ramp-v-low
smoothstepbevel-width0.0plane-v
ramp-v-high
smoothstep
-1.0bevel-width
1.0plane-v
bevel-u
-
*ramp-u-highedge-u-high
*ramp-u-lowedge-u-low
bevel-v
-
*ramp-v-highedge-v-high
*ramp-v-lowedge-v-low
bevel-lean
+
*tangent-ubevel-u
*tangent-vbevel-v

An inner corner is a crevice, and a crevice gathers occlusion. Only the filleted edges contribute; a rounded-over outer edge is more exposed than the face it belongs to, not less.

crease
+
*ramp-u-low
max0.0
-edge-u-low
*ramp-u-high
max0.0
-edge-u-high
*ramp-v-low
max0.0
-edge-v-low
*ramp-v-high
max0.0
-edge-v-high
seam
clampcrease0.01.0

One linear texture read replaces the four height taps formerly used here. The CPU generator clamps those taps within each tile, normalizes the result, and stores the source height in alpha for inspection. Fade the decoded tangent normal back toward the flat face as its texels become sub-pixel.

normal-sample
representation
swizzle
sampleblock-normal-atlasblock-sampleruv
:rgb
tangent-normal
-
*normal-sample2.0
vec31.01.01.0
relief-x
*
swizzletangent-normal:x
relief-fade
relief-y
*
swizzletangent-normal:y
relief-fade
relief-z
mix1.0
swizzletangent-normal:z
relief-fade

A real round-over is an arc, not a leaning plane. Adding a scaled tangent to the face normal can only ever reach the arctangent of that scale, so a strong edge needs an implausible scale and still bunches its shading into the last sliver. Rotating the normal through an angle instead sweeps it evenly from the face toward the edge, which is what a fillet of that radius actually does. The guarded denominator matters: over the flat middle of a face the lean is exactly zero, and the angle is zero with it.

lean-magnitude
sqrt
dotbevel-leanbevel-lean
lean-direction
/bevel-lean
max0.0001lean-magnitude
bevel-turn
*
min1.0lean-magnitude
*0.90bevel-fade
rounded
+
*flat-normal
cosbevel-turn
*lean-direction
sinbevel-turn
seam-occlusion
-1.0
*0.34
*seambevel-fade

--- the weathering ----------------------------------------------- The atlas has one tile per material and the world has thousands of faces per tile, so on its own every grass block is the same card seen again. Three fields fix that and none of them costs a texel: a hash of the cell a face belongs to, constant across that face and different on the next one; value noise over the world point itself, which knows nothing about where faces begin and so drifts across a whole plain the way real ground does; and a grain finer than a cell whose own gradient leans the surface, so the ground is not merely painted unevenly but lit unevenly. The cell is found by stepping half a cell back along the face's own normal, which lands inside the block whichever of the six faces this is. #3AVEKC

face-cell
floor
-surface-point
*flat-normal0.5
face-seed
lattice-hash
+face-cell
*flat-normal0.37
face-hue
lattice-hash
+face-cell
vec35.211.379.13
patch
lattice-fractal-noise
*surface-point0.075
grain-point
*surface-point1.05
grain
lattice-noisegrain-point

Two more taps of the same grain, a step along each of the face's own in-plane axes: their differences are the field's gradient there, which is exactly the tilt a bump of it would give.

grain-u
lattice-noise
+grain-point
*tangent-u0.55
grain-v
lattice-noise
+grain-point
*tangent-v0.55
weathering
clamp
*surface-detail
+
*
-patch0.5
1.05
+
*
-face-seed0.5
0.30
*
-grain0.5
*0.30bevel-fade
-0.420.42

Brighter patches are also warmer and duller ones cooler, and a face's own constant drifts its hue as well as its value: a plain of grass wants to be many greens, not one green at many brightnesses.

hue-drift
clamp
*surface-detail
+
*
-face-hue0.5
0.36
*
-patch0.5
0.44
-0.350.35
weathered-tint
+
vec31.01.01.0
+
*
vec31.121.00.84
weathering
*
vec30.130.02-0.11
hue-drift

The grain is a cell-wide feature, so it survives to the distance a cell does rather than the distance a texel does.

bump-strength
*0.85
*bevel-fadesurface-detail
shaped
normalize
+
+
*roundedrelief-z
*tangent-u
+relief-x
*
-graingrain-u
bump-strength
*tangent-v
+relief-y
*
-graingrain-v
bump-strength
shading-normal
assume-quantityshaped:quantity:world-direction:unit:one

--- the micro-surface -------------------------------------------- The atlas normal is this material's own slope at texel scale, and the best evidence available about what happens below a texel: a steep texel is a rough material. When the relief fades out with distance its variance has to go somewhere, and the honest place is the roughness -- Toksvig's argument -- so a highlight keeps its size across the draw distance instead of sharpening into a sparkle on every distant face at once. #2T8CCH

relief-slope
sqrt
+
*
swizzletangent-normal:x
swizzletangent-normal:x
*
swizzletangent-normal:y
swizzletangent-normal:y
roughness
clamp
*surface-roughness
+
mix0.400.92
clamp
*relief-slope1.7
0.01.0
+
*0.28
-1.0relief-fade
*0.10
-grain0.5
0.160.99
sun-direction
swizzlesun-vector:xyz
n-dot-l
max0.0
dotshading-normalsun-direction
shadow-coordinateshadow-uv-input
shadow-u
swizzleshadow-coordinate:x
shadow-v
swizzleshadow-coordinate:y
shadow-in-bounds
*
step
quantity0.0:quantity:shadow-u:unit:one
shadow-u
stepshadow-u
quantity1.0:quantity:shadow-u:unit:one
step
quantity0.0:quantity:shadow-v:unit:one
shadow-v
stepshadow-v
quantity1.0:quantity:shadow-v:unit:one
shadow-texel-size
swizzleshadow-control-vector:xy
shadow-base-bias
swizzleshadow-control-vector:z
shadow-slope-bias
swizzleshadow-control-vector:w

A PCF tap displaced across a receiver plane must compare against that plane's depth at the tap, not the center fragment depth. With the orthographic shadow rows, the analytical UV depth gradient is the face normal expressed in the light basis and scaled by the world-span/depth-span ratio. Its denominator is bounded only near grazing incidence, where direct sun is negligible. This prevents wide kernels from reading the receiver's own slope as an occluder.

shadow-right
assume-quantity
normalize
swizzleshadow-row-x:xyz
:quantity:world-direction:unit:one
shadow-up
assume-quantity
normalize
swizzleshadow-row-y:xyz
:quantity:world-direction:unit:one
shadow-forward
assume-quantity
normalize
swizzleshadow-row-z:xyz
:quantity:world-direction:unit:one
shadow-normal-forward
min-0.05
dotnormalshadow-forward
shadow-depth-span
swizzleshadow-filter-vector:x
shadow-world-units-per-texel
swizzleshadow-filter-vector:y
shadow-world-span
interpret
/shadow-world-units-per-texel
swizzleshadow-texel-size:x
:quantity:world-distance:unit:cell
shadow-span-ratio
/shadow-world-spanshadow-depth-span
shadow-depth-gradient
interpret
vec2
-
assume-quantity
representation
*
/
dotnormalshadow-right
shadow-normal-forward
shadow-span-ratio
:quantity:shadow-depth-gradient:unit:one
-
assume-quantity
representation
*
/
dotnormalshadow-up
shadow-normal-forward
shadow-span-ratio
:quantity:shadow-depth-gradient:unit:one
:quantity:shadow-depth-gradient:unit:one
shadow-center-depth
swizzle
sampleshadow-mapshadow-samplershadow-coordinate
:x
receiver-depthshadow-depth-input
shadow-blocker-separation
interpret
max
quantity0.0:quantity:shadow-depth:unit:one:character:difference
-
-receiver-depthshadow-bias
shadow-center-depth
:quantity:shadow-depth:unit:one:character:absolute
shadow-minimum-radius
swizzleshadow-filter-vector:z
shadow-maximum-radius
swizzleshadow-filter-vector:w
sun-angular-width
swizzlesun-color-vector:w
shadow-penumbra-world-radius
interpret
*
*shadow-blocker-separationshadow-depth-span
sun-angular-width
:quantity:world-distance:unit:cell
shadow-filter-radius
clamp
+shadow-minimum-radius
interpret
/shadow-penumbra-world-radiusshadow-world-units-per-texel
:quantity:shadow-filter-radius:unit:one
shadow-minimum-radiusshadow-maximum-radius
shadow-sample
shadow-visibilityshadow-mapshadow-comparison-samplershadow-coordinatereceiver-depthshadow-depth-gradientshadow-texel-sizeshadow-biasshadow-filter-radius
sampled-shadow
mix1.0shadow-sampleshadow-in-bounds

A surface turned away from the light has no shadow decision to make: its own geometry already excludes the sun, the receiver plane's depth runs almost parallel to the light so every filter tap lands a large and increasingly ill-conditioned distance away, and the map's answer degenerates into noise. Near noon that is every vertical face in the world at once, which is why the shimmer was worst there. Fading the decision out toward "lit" exactly where it stops meaning anything costs nothing visually -- the same cosine multiplies the direct term to zero -- and gives the specular lobe and the diagnostic a stable value instead of the noise.

The lower edge is where the receiver-plane denominator above starts being clamped: SHADOW-FORWARD is the negated sun, so that dot product is exactly -N-DOT-L, and the clamp engages precisely below 0.05. The two mechanisms therefore hand over rather than overlap, and the clamp never has to carry a visible decision.

shadow-relevance
smoothstep0.050.22n-dot-l
direct-shadow
mix1.0sampled-shadowshadow-relevance

The mesh carries normalized raw light readings; every response curve and balance below is an art parameter editable live without remeshing the world.

sky-input
swizzlelight-input:x
block-input
swizzlelight-input:y
emission-input
swizzlelight-input:z
sky-level
*sky-inputsky-input
block-level
*block-inputblock-input
day-factor
swizzlesun-vector:w

Lateral skylight gives ambient visibility but not a hard sun beam; the shadow map gates only the direct solar term.

sun-visibility
smoothstep
quantity0.90:quantity:sky-light-level:unit:one
quantity1.0:quantity:sky-light-level:unit:one
sky-input
ambient
swizzleambient-vector:xyz
sun-color
swizzlesun-color-vector:xyz

--- the deck's shadow -------------------------------------------- The sky draws a cumulus deck on a plane at a fixed world height, and the ground under it should know. Following the sun up from this point to that plane and asking the very same field the very same coverage question is the whole of it: the deck's own shadow sweeping across the world, which is what keeps a plain at noon from being one flat sheet of light. The threshold is wider than the sky's, because a shadow thrown from that far off is blurred by the sun's own angular width long before it lands. #RSGLTL

elapsed
sun-climb
max0.15
representation
swizzlesun-direction:y
deck-reach
/
max8.0
-cloud-altitude
swizzlesurface-point:y
sun-climb
deck-meeting
+
+surface-point
*
representationsun-direction
deck-reach
vec3
*elapsed2.2
0.0
*elapsed1.2
deck-cover
cloud-fractal-noise
*deck-meeting0.0044
deck-coverage
cloud-shadow
assume-quantity
-1.0
*cloud-shadow-depth
smoothstepdeck-coverage
+deck-coverage0.20
deck-cover
:quantity:cloud-shadow:unit:one

Occlusion bites harder than the raw mesh reading: the corner where three blocks meet is what tells the eye these are solid volumes.

occlusion
interpret
*
exptao1.8
assume-quantityseam-occlusion:quantity:ambient-occlusion:unit:one
:quantity:ambient-occlusion:unit:one

--- what is not the sun ------------------------------------------ Everything above a face that is not the sun is still the sky, and the sky is not one colour: a face turned up sees the zenith, a face turned sideways sees the horizon, and a face turned down sees what the ground bounced. One interpolation over the normal's own vertical component is the whole of it, and it is the difference between blocks standing in a world and blocks floating in a grey room. The profile's ambient colour stays in the mixture: it is the art direction's say over light the geometry cannot explain.

facing-up
*0.5
+1.0
representation
swizzleshading-normal:y
dome-color
mix
representation
swizzlehorizon-vector:xyz
representation
swizzlezenith-vector:xyz
*facing-upfacing-up
bounce-color
environment
assume-quantity
mixbounce-color
mixdome-color0.62
facing-up
:quantity:linear-rgb:unit:one

A small floor keeps unlit geometry barely readable rather than a void; caves stay dark for the right reason. The ambient term is deliberately weaker, and the sun correspondingly stronger, than a display-referred renderer could afford: the filmic curve on presentation is what brings the sunlit half back down.

sky-light
interpret
*environment
+0.030
*0.86sky-level
occlusion
:quantity:linear-rgb:unit:one
sun-light
interpret
*sun-color
*direct-light-gainn-dot-lsun-visibilityday-factordirect-shadowcloud-shadow
mix0.551.0ao
:quantity:linear-rgb:unit:one
torch-color
quantity
vec31.00.820.58
:quantity:linear-rgb:unit:one
local-light
interpret
*torch-colorblock-level
:quantity:linear-rgb:unit:one
albedo
assume-quantity
*
representation
swizzle
sampleblock-atlasblock-sampleruv
:rgb
weathered-tint
:quantity:linear-rgb:unit:one
reflected
interpret
*albedo
+sky-lightsun-lightlocal-light
:quantity:linear-rgb:unit:one

--- the microfacet lobe ------------------------------------------ One GGX lobe off the shaped normal, with Smith's height-correlated visibility and a dielectric's Fresnel: blocks are not metal, so four per cent at normal incidence and the whole of it at a grazing one. The roughness above decides everything about its shape, so a polished face and a tufted one differ here without differing anywhere else.

view-direction
assume-quantity
normalize
-eyesurface-point
:quantity:world-direction:unit:one
half-vector
assume-quantity
normalize
+
normalize
-eyesurface-point
representationsun-direction
:quantity:world-direction:unit:one
n-dot-h
max0.0
dotshading-normalhalf-vector
n-dot-v
max0.0
dotshading-normalview-direction
v-dot-h
max0.0
dotview-directionhalf-vector
cosine-light
cosine-view
cosine-half
alpha
*roughnessroughness
alpha-squared
*alphaalpha
distribution-denominator
+
*
*cosine-halfcosine-half
-alpha-squared1.0
1.0
distribution
/alpha-squared
max0.0001
*3.14159265
*distribution-denominatordistribution-denominator

Smith's height-correlated visibility already carries the 1/(4 cos cos) the microfacet specular would otherwise divide by.

visibility-light
*cosine-view
sqrt
+
*
*cosine-lightcosine-light
-1.0alpha-squared
alpha-squared
visibility-view
*cosine-light
sqrt
+
*
*cosine-viewcosine-view
-1.0alpha-squared
alpha-squared
visibility
/0.5
max0.0001
+visibility-lightvisibility-view
fresnel
+0.04
*0.96
expt
-1.0
5.0

The cosine belongs in the specular lobe as much as in the diffuse one: without it a highlight can appear on a face the sun cannot reach, carrying whatever the shadow map happened to say there.

sun-specular
*
*
*distribution
*visibilityfresnel

The same sky again, in the direction the surface actually reflects: the sheen that tells a smooth face from a rough one out of the sun, and the one term that makes a block look like it is standing under this weather rather than under a light bulb.

reflection-direction
-
*
representationshading-normal
*2.0cosine-view
representationview-direction
reflection-up
clamp
*0.5
+1.0
swizzlereflection-direction:y
0.01.0
reflected-sky
mix
representation
swizzlehorizon-vector:xyz
representation
swizzlezenith-vector:xyz
*reflection-upreflection-up
sheen
*
-1.0roughness
+0.035
*0.32
expt
-1.0cosine-view
5.0
ambient-specular
*reflected-sky
*sheen
specular
assume-quantity
+sun-specularambient-specular
:quantity:linear-rgb:unit:one
radiance
+reflectedspecular
interpret
*albedoemission-input
:quantity:linear-rgb:unit:one

Distant terrain fades into exactly the colour the sky's own ground half arrives at, warmed where the ray runs toward a low sun: the aerial perspective that puts a mile of air between here and the horizon.

look-direction
normalize
-surface-pointeye
sun-elevation
representation
swizzlesun-direction:y
low-sun
*
-1.0
smoothstep0.020.45sun-elevation
fog-color
assume-quantity:quantity:linear-rgb:unit:one
fog-amountfog-input
fogged
mixradiancefog-colorfog-amount
normal-rgba
assume-quantity
vec4
representation
quantity1.0:quantity:opacity:unit:one
:quantity:linear-rgba:unit:one
shadow-diagnostic
swizzlefog-color-vector:w
shadow-rgba
assume-quantity
vec4
representation
vec3direct-shadowdirect-shadowdirect-shadow
representation
quantity1.0:quantity:opacity:unit:one
:quantity:linear-rgba:unit:one
rgba
mixnormal-rgbashadow-rgbashadow-diagnostic
set-outputcolor-outputrgba
defunblock-world-fragment-lowering

Compile the block material and retain its expression-to-SSA provenance.

defunblock-world-fragment-module

Lower the readable block material to luv's structured SPIR-V module.

spv:shader-lowering-module

The sky is a fullscreen triangle drawn before block geometry with depth writes disabled. Its vertex stage reconstructs a per-corner view ray from the camera basis; its fragment stage is pure image mathematics over the interpolated ray and the frame environment lanes.

define-shader-methodshader-specification-forblock-world-sky-vertex-specification
role
eql:sky
stage
eql:vertex
:stage:vertex:inputs
corner-position:vec3:location0:quantity:clip-coordinate:unit:one
:outputs
clip-position:vec4:built-in:position
ray-output:vec3:location0:quantity:world-direction:unit:one
:resources
frame-state:uniform-block:set0:binding2:members#.*frame-uniform-members*
let*
cornercorner-position
x
swizzlecorner:x
y
swizzlecorner:y
z
swizzlecorner:z
right
swizzleright-vector:xyz
up
swizzleup-vector:xyz
forward
swizzleforward-vector:xyz
x-scale
swizzleprojection-vector:x
y-scale
swizzleprojection-vector:y

Invert the block vertex projection: clip x,y back to view-space slopes, including its y flip, so the ray agrees with the world. These coordinates are measured from clip-space's zero origin. Representation makes that origin choice explicit and erased: no runtime subtraction is needed to treat either coordinate as a displacement before scaling it back into view space.

clip-x-displacement
assume-quantity:quantity:clip-x-coordinate:unit:one:character:difference
clip-y-displacement
assume-quantity:quantity:clip-y-coordinate:unit:one:character:difference
view-x
/clip-x-displacementx-scale
view-y
-
/clip-y-displacementy-scale
ray
+
*rightview-x
*upview-y
forward
set-outputclip-positionclip
set-outputray-outputray

Lattice value noise is the whole procedural vocabulary this file needs: a hash of an integer lattice index, trilinear smoothing between its corners, and a few octaves with the domain rotated between them so the cubic grain never reads as a grid. It is written once here and reused by the cloud decks, the star field, the moon's maria, and the weathering that makes one block face differ from the next.

define-shader-functionlattice-hash

Hash one integer lattice site into the unit interval.

Deliberately trigonometry-free, and deliberately a function of the whole site rather than of a collapsed scalar index. Hashing x + 57y + 113z through a sine makes every plane of constant index share a hash, and at star-field frequencies those planes are plainly visible as streaks across the sky. Folding the site's own components against each other has no preferred direction, and costs less than a sine besides.

let*
scattered
shift
dotscattered
+
swizzlescattered:zyx
vec331.3231.3231.32
folded
+scattered
vec3shiftshiftshift
fract
*
+
swizzlefolded:x
swizzlefolded:y
swizzlefolded:z
define-shader-functionlattice-noise
point
"Smoothly interpolated value noise over an integer lattice."
let*
lattice
floorpoint
offset
fractpoint

The classic smoothstep weight: continuous first derivative at the cell boundaries, which is what keeps the clouds from creasing.

weight
*offset
*offset
-
vec33.03.03.0
*offset2.0
u
swizzleweight:x
v
swizzleweight:y
w
swizzleweight:z
near-low
mix
lattice-hash
+lattice
vec31.00.00.0
u
near-high
mix
lattice-hash
+lattice
vec30.01.00.0
lattice-hash
+lattice
vec31.01.00.0
u
far-low
mix
lattice-hash
+lattice
vec30.00.01.0
lattice-hash
+lattice
vec31.00.01.0
u
far-high
mix
lattice-hash
+lattice
vec30.01.01.0
lattice-hash
+lattice
vec31.01.01.0
u
mix
mixnear-lownear-highv
mixfar-lowfar-highv
w
define-shader-functionlattice-fractal-noise
point

Three octaves of value noise; the domain rotates to hide the lattice.

The fold's state carries the rotating sample point and the running sum together, so the noise body is emitted once and executed three times rather than inlined three times.

let*
accumulated
counted-fold
octave3.0state
vec4point0.0
let*
sample-point
swizzlestate:xyz
x
swizzlesample-point:x
y
swizzlesample-point:y
z
swizzlesample-point:z
gain
expt0.5
+octave1.0
total
+
swizzlestate:w
*
lattice-noisesample-point
gain
rotated
*
vec3
+
*0.8x
*0.6z
+y7.31
+
*-0.6x
*0.8z
2.03
vec4rotatedtotal
swizzleaccumulated:w
define-shader-functioncloud-fractal-noise
point

Four octaves of the same noise: a cloud deck's shape down to its wisps.

A deck's silhouette is decided by the first octave and its edges by the last, so this is the one place in the sky worth paying for a fourth. The weights are the same halving series, renormalized, so the field still spans the unit interval and a coverage threshold means the same thing it does with three.

let*
accumulated
counted-fold
octave4.0state
vec4point0.0
let*
sample-point
swizzlestate:xyz
x
swizzlesample-point:x
y
swizzlesample-point:y
z
swizzlesample-point:z
gain
expt0.5
+octave1.0
total
+
swizzlestate:w
*
lattice-noisesample-point
gain
rotated
*
vec3
+
*0.78x
*0.63z
+y11.17
+
*-0.63x
*0.78z
2.11
vec4rotatedtotal
*
swizzleaccumulated:w
1.067
define-shader-functionhenyey-greenstein
cosineasymmetry

The Henyey-Greenstein phase function: how much light a haze sends on at COSINE off its original direction, for a medium of the given ASYMMETRY.

One expression replaces the two fitted powers the sky used to brighten around the sun. A phase function's shape is the reason a low sun has a huge soft glow and a high one a tight one: the same medium, the same number, a different angle.

let*
squared
*asymmetryasymmetry
denominator
max0.0001
+
+1.0squared
*-2.0
*asymmetrycosine
/
-1.0squared
*12.566371
exptdenominator1.5
define-shader-functionaerial-perspective-color
fog-colordirectionsun-directionlow-sunday-factor

The colour something arbitrarily far away takes, seen along DIRECTION.

Distant terrain and the sky's own ground half must agree exactly, or the edge of the resident world draws itself as a line. They agree by asking this: the profile's fog colour, warmed where the ray runs toward a low sun and brightened by the same haze the sky's halo is made of, so a ridge in the east at dawn glows and one in the west stays cool. #8X33G2

let*
alignment
dotdirectionsun-direction
toward
max0.0alignment
glow
henyey-greensteinalignment0.66
warm
mix
vec31.080.740.46
vec31.250.520.27
low-sun
+
*fog-color
-1.0
*0.18
*low-suntoward
*warm
*day-factor
+
*glow0.055
*low-sun
*
*towardtoward
0.22
define-shader-functionstar-light
directionelapsed

One star per lattice cell of the sky, at a hashed place in its cell.

Value noise raised to a power -- what the star field used to be -- makes smooth blobs the size of its own lattice, and they smear into streaks wherever the interpolation runs along a cell edge. A star is a point, so this hashes the cell to a position, a magnitude, and a twinkling phase, and draws a small gaussian around it. Magnitude is a steep power of its hash: a few bright stars and a great many faint ones, which is the actual distribution overhead.

let*
point
*direction74.0
local
fractpoint
place-x
place-y
place-z

Keep the star off its cell's boundary so no star is ever cut in half by the next cell's gaussian falling off first.

centre
+
vec30.250.250.25
*
vec3place-xplace-yplace-z
0.5
offset
-localcentre
radius
dotoffsetoffset
magnitude
exptplace-z9.0
twinkle
+0.74
*0.26
sin
+
*elapsed2.3
*place-x43.0
spread
exp
*-230.0radius
*magnitude
*twinklespread

The sky is image mathematics over a view ray and the frame environment. What it argues is that everything up there is one atmosphere seen at different depths:

  • the vertical gradient runs the whole way from horizon to zenith

rather than resolving in the first few degrees, because that is what the optical depth along a ray actually does;

  • haze thickens toward the horizon, and the sunrise band it carries

belongs to the sun's own quarter of the compass, not all the way round;

  • one Henyey-Greenstein phase function, evaluated at two asymmetries,

is the whole glow around the sun;

  • two cloud decks, each a plane at a fixed height, so the ray meets

them farther out the closer it runs to level: thin cirrus far above, and the cumulus sheet whose own shadow, sampled once along the deck toward the sun, gives it a lit face and a dark underside;

  • at night, points for stars, a band for the galaxy, and the moon

opposite the sun;

  • a compact solar disc pushed well past display white so the lens

chain has something real to bloom.

Everything below the horizon arrives at the exact fog colour distant terrain fades to, so silhouettes meet the sky without a seam, and only then darkens -- gently, because a stronger falloff shows up as a step where the last resident chunk ends. #9SSXDJ records why an HDR path wants a sky with something genuinely bright in it, and #8X33G2 what each of these terms replaced.

define-shader-methodshader-specification-forblock-world-sky-fragment-specification
role
eql:sky
stage
eql:fragment
:stage:fragment:inputs
ray-input:vec3:location0:quantity:world-direction:unit:one
:outputs
color-output:vec4:location0
:resources
frame-state:uniform-block:set0:binding2:members#.*frame-uniform-members*
let*
direction
elevation
swizzledirection:y
above
maxelevation0.0
eye
representation
swizzlecamera-vector:xyz
sun-direction
representation
swizzlesun-vector:xyz
sun-color
representation
swizzlesun-color-vector:xyz
sun-width
representation
swizzlesun-color-vector:w
zenith
representation
swizzlezenith-vector:xyz
horizon
representation
swizzlehorizon-vector:xyz
fog-color
representation
swizzlefog-color-vector:xyz
elapsed

One number decides how much of the sky is sunrise: a sun near the horizon warms the haze, the scatter, and the cloud faces together.

sun-elevation
swizzlesun-direction:y
low-sun
*day-factor
-1.0
smoothstep0.020.45sun-elevation
alignment
dotdirectionsun-direction
toward-sun
max0.0alignment

The sun's half of the sky, measured on the ground plane: a sunrise band belongs in the east, and the west should stay blue.

level-ray
vec3
swizzledirection:x
0.0
swizzledirection:z
level-sun
vec3
swizzlesun-direction:x
0.0
swizzlesun-direction:z
azimuth
max0.0
/
dotlevel-raylevel-sun
max0.001
*
sqrt
dotlevel-raylevel-ray
sqrt
dotlevel-sunlevel-sun

--- the atmosphere --------------------------------------------- The vertical gradient is the sky's own colour at depth; a small exponent spreads it across the whole hemisphere instead of resolving it in the first few degrees, which is what an optical depth along the ray actually does.

gradient
expt
clampelevation0.01.0
0.42
base
mixhorizonzenithgradient
haze
exp
*-9.0above

A sunrise is sunlight that has come the long way through the atmosphere, so the band it paints is the sun's own colour at that hour, laid exactly where the ray runs both low and toward it. Away from the sun's quarter of the compass the sky stays its own colour, which is what keeps the west blue while the east burns.

warm-color
*sun-color0.82
warm-band
*low-sun
*haze
+0.12
*0.88
*azimuthazimuth
hazed
mixbasewarm-color
clamp
*0.92warm-band
0.01.0
broad
henyey-greensteinalignment0.62
tight
henyey-greensteinalignment0.90
halo-tint
mix
vec31.00.970.90
vec31.00.520.24
low-sun
halo-depth
mix0.701.60haze
halo
*
+
*broad
+0.030
*0.055low-sun
*tight
+0.014
*0.055low-sun
scattered
+hazed
*halo-tinthalo

--- night ------------------------------------------------------

night

The galaxy is a great circle of the sky, so one fixed axis and the ray's distance from its plane is the whole band.

galaxy-axis
vec30.420.55-0.72
galaxy-distance
dotdirectiongalaxy-axis
galaxy-band
exp
*-20.0
*galaxy-distancegalaxy-distance
galaxy-structure
+
*
lattice-noise
*direction15.0
0.58
*
lattice-noise
*direction44.0
0.42
galaxy
*galaxy-band
*
+0.10
*1.25galaxy-structure

A dust lane is the band's own darkness, not an absence of stars, so it multiplies rather than subtracts.

-1.0
*0.55
smoothstep0.420.66
lattice-noise
*direction7.0
star-visibility
*night
smoothstep-0.020.14elevation
stars
*
star-lightdirectionelapsed
*star-brightness
*star-visibility
+1.0
*1.6galaxy-band
starred
+
*
vec30.600.660.94
*galaxy
*star-visibility0.17
*
vec30.920.941.0
stars

The moon rides opposite the sun, which puts it up for exactly the hours the sun is not, and always full: the simple sky this world wants, and the one that lights its nights.

moon-direction
*sun-direction-1.0
moon-alignment
dotdirectionmoon-direction
moon-radius
*sun-width1.7
moon-limb
*0.5
*moon-radiusmoon-radius
moon-disc
smoothstep
-1.0moon-limb
-1.0
*0.80moon-limb
moon-alignment

The component of the ray across the moon's own direction, in units of its radius: the disc's face, which the maria are painted on.

moon-face
/
-direction
*moon-directionmoon-alignment
max0.0001moon-radius
moon-maria
lattice-noise
+
*moon-face1.8
vec313.05.09.0
moon-shape
*
+0.70
*0.50moon-maria
-1.0
*0.40
clamp
dotmoon-facemoon-face
0.01.0
moon-glow
*
expt
max0.0moon-alignment
220.0
0.30
lunar
*
vec30.940.951.0
*night
+
*moon-disc
*moon-radiancemoon-shape
moon-glow
nightly
+scattered
+starredlunar

--- the cloud decks --------------------------------------------

cirrus-point
+
*direction
/2400.0
maxelevation0.02
vec3
*elapsed6.0
0.0
*elapsed2.0
cirrus-field
lattice-fractal-noise
*cirrus-point
vec30.001050.001050.00225
cirrus
*
smoothstep0.560.88cirrus-field
*
smoothstep0.020.26elevation
*
-1.0
0.45
cirrus-color
*
mix
vec31.101.121.18
vec31.380.840.56
*low-sunlow-sun
with-cirrus
mixnightlycirrus-colorcirrus

The deck is a plane at a fixed height in the world, not a fixed height above the camera: the ground has to be able to find the same plane along the sun and read the same field there, or the shadows sweeping over it would belong to some other sky. #RSGLTL

deck-rise
deck-point
+
+eye
*direction
/deck-rise
maxelevation0.014
vec3
*elapsed2.2
0.0
*elapsed1.2
deck-scale0.0044
cloud-field
cloud-fractal-noise
*deck-pointdeck-scale

Coverage is the profile's cloudiness against the knob; the edge softens toward the horizon, where a deck's detail is far smaller than a pixel and a hard edge could only shimmer.

coverage
softness
mix0.260.055
smoothstep0.0120.34elevation
deck-mask
smoothstep0.0060.055elevation
cloud-density
*deck-mask
smoothstepcoverage
+coveragesoftness
cloud-field
core
clampcloud-density0.01.0

What lies between this piece of deck and the sun, sampled along the deck itself: the shape's own shadow, and so its lit face.

shadow-field
cloud-fractal-noise
*
+deck-point
*level-sun300.0
deck-scale
shadow-density
smoothstepcoverage
+coveragesoftness
shadow-field
cloud-light
-1.0
*0.70shadow-density
cloud-lit
mix
vec31.221.201.16
vec31.400.740.36
*low-sunlow-sun
cloud-dark
mix
vec30.500.570.74
vec30.440.380.52
*low-sunlow-sun
cloud-body
mixcloud-darkcloud-lit
*cloud-light
-1.0
*0.45core

Silver lining: thin edges facing the sun glow, dense cores do not.

silver
*cloud-lit
*
expttoward-sun14.0
*
-1.0core
+0.30
*0.85low-sun

After sunset a deck is lit by the sky alone, so it takes the sky's own colour; a neutral grey at one twentieth reads warm against a night zenith and the eye calls it dust.

night-tint
mix
vec30.440.520.80
vec31.01.01.0
cloud-color
*
*
+cloud-bodysilver
night-tint

A deck recedes into the same haze the sky's own horizon does.

cloud-reach
smoothstep0.0100.11elevation
clouded
mixwith-cirrus
mixhazedcloud-colorcloud-reach
*cloud-density0.94

--- the ground half -------------------------------------------- Below the horizon this shader stands in for terrain too far off to be resident, so it must arrive at exactly the colour distant terrain fades to. It may still say where the land is: the ray's own meeting with the ground plane gives a point to sample, and a wide, weak relief over it keeps the lower half from being one flat wall of fog. The relief fades out at the horizon, where the plane runs away faster than a pixel can resolve it.

aerial
aerial-perspective-colorfog-colordirectionsun-directionlow-sunday-factor
descent
max0.004
-elevation
ground-point
+eye
*direction
/
max1.0
swizzleeye:y
descent
land
lattice-fractal-noise
*ground-point0.0021
land-relief
*
-land0.5
smoothstep0.0-0.22elevation
depth-below
smoothstep0.0-0.35elevation
ground
*aerial
+
-1.0
*0.26depth-below
*0.44land-relief
grounded
mixcloudedground
smoothstep0.060-0.006elevation

--- the sun ---------------------------------------------------- The disc is drawn at a few times the sun's true angular radius, the way every game sun is, and deliberately far above display white: the floating point attachment keeps it, the bright pass feeds on it, and the filmic curve rolls it into a hot core instead of a flat clipped patch. For a small angle the cosine threshold of an angular radius is one less half its square.

disc-radius
disc-limb
*0.5
*disc-radiusdisc-radius
disc
smoothstep
-1.0disc-limb
-1.0
*0.56disc-limb
alignment

A star's disc is brightest at its centre; without the limb darkening a sun drawn this large reads as a sticker.

disc-radial
clamp
/
-1.0alignment
max0.000001disc-limb
0.01.0
limb-darkening
-1.0
*0.45
*disc-radialdisc-radial
corona
+
*
expttoward-sun900.0
0.8
*
expttoward-sun130.0
0.22
occlusion
-1.0
*core0.94
solar
*sun-color
*day-factor
+
*disc
*sun-disc-radiance
*occlusionlimb-darkening
*corona
*2.0occlusion
rgb
+groundedsolar
set-outputcolor-output

Presentation is where linear scene radiance becomes a display image. The scene attachment is floating point, so a sun disc or a specular glint arrives far above display white; the lens stack composes in that linear space, exposure scales it, and a filmic curve rolls it off. Screen center lies on the framed terminal, so its depth is the focus plane; only farther fragments receive the wide nine-tap blur. The HUD is drawn afterward, already graded, straight onto the presentation image.

The presentation stages share one small uniform block, written once per frame by luvcraft-post-uniform-data. Member order is an ABI requirement exactly as it is for the frame environment, so it is written once here.

eval-when
:compile-toplevel:load-toplevel:execute
defparameter*post-uniform-members*'
post-control:vec4

texel u, texel v, focus blur, exposure

lens-control:vec4

bloom gain, shaft gain, vignette, threshold

sun-screen:vec4

sun screen u, v, on-screen weight, diagnostic

bloom-control:vec4

chain texel u, v, shaft decay, elapsed time

presentation-control:vec4

output texel u, v, reserved, reserved

"The presentation uniform layout shared by the post and bloom stages."

Narkowicz's fitted ACES curve. Its toe keeps shadows from turning to mud, its shoulder compresses the sun and the crystal glow into a hot core instead of a flat clipped patch, and it desaturates highlights the way film does.

define-shader-functionaces-filmic
radiance
"Map linear scene radiance to display-linear [0,1] through the ACES fit."
let*
numerator
*radiance
+
*radiance2.51
vec30.030.030.03
denominator
+
*radiance
+
*radiance2.43
vec30.590.590.59
vec30.140.140.14
clamp
/numeratordenominator
vec30.00.00.0
vec31.01.01.0
define-shader-methodshader-specification-forfocus-post-vertex-specification
role
eql:focus-post
stage
eql:vertex
:stage:vertex:inputs
corner-position:vec3:location0
:outputs
clip-position:vec4:built-in:position
uv-output:vec2:location0
let*
x
swizzlecorner-position:x
y
swizzlecorner-position:y
set-outputclip-position
vec4xy
swizzlecorner-position:z
1.0
set-outputuv-output
vec2
*
+x1.0
0.5
*
+y1.0
0.5

The bloom and light-shaft chain runs on a quarter-resolution pair of floating-point attachments. Bright fragments are extracted once, blurred separably, and then swept radially away from the solar disc; presentation adds both back into linear scene radiance before the filmic curve. Every stage sees the same bind group shape -- source texture, linear sampler, presentation uniforms -- so one layout serves the whole chain, and the stage that reads it is chosen by CLOS role rather than by a mode lane. #IC14P3 records the shape this stack took and where it left its plan.

define-shader-methodshader-specification-forbloom-bright-fragment-specification
role
eql:bloom-bright
stage
eql:fragment
:stage:fragment:inputs
uv-input:vec2:location0
:outputs
color-output:vec4:location0
:resources
source-color:texture-2d:set0:binding0:sample-transfer:identity
source-sampler:sampler:set0:binding1
post-state:uniform-block:set0:binding2:members#.*post-uniform-members*
let*
texel
swizzlepost-control:xy
exposure
swizzlepost-control:w
threshold
swizzlelens-control:w
dx
*texel
vec21.00.0
dy
*texel
vec20.01.0

Four scene taps per chain texel. The chain is a quarter of the frame's width, so a single tap would alias exactly the highlights this pass exists to smear.

box
*
+
swizzle
samplesource-colorsource-sampler
+uv-input
+dxdy
:xyz
swizzle
samplesource-colorsource-sampler
+uv-input
-dxdy
:xyz
swizzle
samplesource-colorsource-sampler
-uv-input
-dxdy
:xyz
swizzle
samplesource-colorsource-sampler
-uv-input
+dxdy
:xyz
0.25

The chain works in exposed units, so its contribution stays in step with the scene when exposure moves.

radiance
luminance
dotradiance
vec30.21260.71520.0722
knee
smoothstepthreshold
+threshold0.75
luminance
set-outputcolor-output
vec4
*radianceknee
1.0

A thirteen-tap gaussian expressed as seven linearly filtered samples: each offset lands between the two texels whose weights it combines. The host runs the pair twice, which convolves the kernel with itself and puts the standing deviation at about four chain texels -- a wide, soft glow rather than a halo tight enough to read as an outline. The two directions are separate roles rather than a uniform lane, so each pipeline is exactly the code it runs.

define-shader-methodshader-specification-forbloom-horizontal-fragment-specification
role
eql:bloom-horizontal
stage
eql:fragment
:stage:fragment:inputs
uv-input:vec2:location0
:outputs
color-output:vec4:location0
:resources
source-color:texture-2d:set0:binding0:sample-transfer:identity
source-sampler:sampler:set0:binding1
post-state:uniform-block:set0:binding2:members#.*post-uniform-members*
let*
span
vec2
swizzlebloom-control:x
0.0
near-offset
*span1.4585
mid-offset
*span3.4038
far-offset
*span5.3510
center
*
swizzle
samplesource-colorsource-sampleruv-input
:xyz
0.1370
near
*
+
swizzle
samplesource-colorsource-sampler
+uv-inputnear-offset
:xyz
swizzle
samplesource-colorsource-sampler
-uv-inputnear-offset
:xyz
0.2393
mid
*
+
swizzle
samplesource-colorsource-sampler
+uv-inputmid-offset
:xyz
swizzle
samplesource-colorsource-sampler
-uv-inputmid-offset
:xyz
0.1394
far
*
+
swizzle
samplesource-colorsource-sampler
+uv-inputfar-offset
:xyz
swizzle
samplesource-colorsource-sampler
-uv-inputfar-offset
:xyz
0.0527
set-outputcolor-output
vec4
+
+centernear
+midfar
1.0
define-shader-methodshader-specification-forbloom-vertical-fragment-specification
role
eql:bloom-vertical
stage
eql:fragment
:stage:fragment:inputs
uv-input:vec2:location0
:outputs
color-output:vec4:location0
:resources
source-color:texture-2d:set0:binding0:sample-transfer:identity
source-sampler:sampler:set0:binding1
post-state:uniform-block:set0:binding2:members#.*post-uniform-members*
let*
span
vec20.0
swizzlebloom-control:y
near-offset
*span1.4585
mid-offset
*span3.4038
far-offset
*span5.3510
center
*
swizzle
samplesource-colorsource-sampleruv-input
:xyz
0.1370
near
*
+
swizzle
samplesource-colorsource-sampler
+uv-inputnear-offset
:xyz
swizzle
samplesource-colorsource-sampler
-uv-inputnear-offset
:xyz
0.2393
mid
*
+
swizzle
samplesource-colorsource-sampler
+uv-inputmid-offset
:xyz
swizzle
samplesource-colorsource-sampler
-uv-inputmid-offset
:xyz
0.1394
far
*
+
swizzle
samplesource-colorsource-sampler
+uv-inputfar-offset
:xyz
swizzle
samplesource-colorsource-sampler
-uv-inputfar-offset
:xyz
0.0527
set-outputcolor-output
vec4
+
+centernear
+midfar
1.0

Crepuscular rays as a screen-space gather: march the blurred bright image toward the solar disc and accumulate what is still lit, attenuating with distance. Terrain that occludes the sun is dark in the bright image, so the rays break into beams around leaves and rooftops for free. The host fades SUN-SCREEN's weight out as the disc leaves the frame.

define-shader-methodshader-specification-forsun-shaft-fragment-specification
role
eql:sun-shafts
stage
eql:fragment
:stage:fragment:inputs
uv-input:vec2:location0
:outputs
color-output:vec4:location0
:resources
source-color:texture-2d:set0:binding0:sample-transfer:identity
source-sampler:sampler:set0:binding1
post-state:uniform-block:set0:binding2:members#.*post-uniform-members*
let*
sun-uv
swizzlesun-screen:xy
weight
swizzlesun-screen:z
decay
swizzlebloom-control:z
delta
*
-sun-uvuv-input
0.03125
gathered
counted-fold
tap32.0total
vec30.00.00.0
+total
*
swizzle
samplesource-colorsource-sampler
+uv-input
*deltatap
:xyz
exptdecaytap
set-outputcolor-output
vec4
*gathered
*weight0.03125
1.0
define-shader-methodshader-specification-forfocus-post-fragment-specification
role
eql:focus-post
stage
eql:fragment
:stage:fragment:inputs
uv-input:vec2:location0
:outputs
color-output:vec4:location0
:resources
scene-color:texture-2d:set0:binding0:sample-transfer:identity
scene-sampler:sampler:set0:binding1
scene-depth:depth-texture-2d:set0:binding2
post-state:uniform-block:set0:binding3:members#.*post-uniform-members*
bloom-color:texture-2d:set0:binding4:sample-transfer:identity
shaft-color:texture-2d:set0:binding5:sample-transfer:identity
depth-sampler:sampler:set0:binding6

World-space application panels keep their semantic GPU geometry at native presentation density. Their depth is compared with the deliberately lower-density game depth below, without asking either attachment domain to masquerade as the other.

world-panel-color:texture-2d:set0:binding7:sample-transfer:identity
world-panel-depth:depth-texture-2d:set0:binding8
let*
texel
swizzlepost-control:xy
presentation-texel
swizzlepresentation-control:xy
active
swizzlepost-control:z
exposure
swizzlepost-control:w
centered
-uv-input
vec20.50.5
radial
dotcenteredcentered

A lens does not focus every wavelength on the same circle, and the error grows with the distance off axis. Three taps a fraction of a texel apart is the whole of it: nothing at the centre of the frame, a hair of colour at its corners.

dispersion
*centered
sharp
vec4
swizzle
samplescene-colorscene-sampler
+uv-inputdispersion
:x
swizzle
samplescene-colorscene-sampleruv-input
:y
swizzle
samplescene-colorscene-sampler
-uv-inputdispersion
:z
1.0
depth
swizzle
samplescene-depthdepth-sampleruv-input
:x
focus-depth
swizzle
samplescene-depthdepth-sampler
vec20.50.5
:x
blur-amount
*active
smoothstep0.00150.018
-depthfocus-depth
dx
*texel
vec25.00.0
dy
*texel
vec20.05.0
blurred
*
+
*sharp4.0
*
samplescene-colorscene-sampler
+uv-inputdx
2.0
*
samplescene-colorscene-sampler
-uv-inputdx
2.0
*
samplescene-colorscene-sampler
+uv-inputdy
2.0
*
samplescene-colorscene-sampler
-uv-inputdy
2.0
samplescene-colorscene-sampler
+
+uv-inputdx
dy
samplescene-colorscene-sampler
+
-uv-inputdx
dy
samplescene-colorscene-sampler
-
+uv-inputdx
dy
samplescene-colorscene-sampler
-
-uv-inputdx
dy
0.0625
scene-radiance
swizzle
mixsharpblurredblur-amount
:xyz
panel
sampleworld-panel-colorscene-sampleruv-input
panel-depth
swizzle
sampleworld-panel-depthdepth-sampleruv-input
:x

A transparent clear has alpha zero, but the strict comparison also keeps the depth clear from becoming a surface in front of sky.

panel-visible
if
<panel-depthdepth
1.00.0
panel-alpha
*
swizzlepanel:w
panel-visible
radiance
+
*
swizzlepanel:xyz
panel-visible
*scene-radiance
-1.0panel-alpha

The chain already carries exposed units; the scene does not.

bloom
swizzle
samplebloom-colorscene-sampleruv-input
:xyz
shafts
swizzle
sampleshaft-colorscene-sampleruv-input
:xyz
exposed
+
*radianceexposure

A panel in front of the scene also occludes scene-produced bloom and shafts. The panel itself stays out of the reduced lens chain, preserving its analytic native-density edges.

*
+
*bloom
swizzlelens-control:x
*shafts
swizzlelens-control:y
-1.0panel-alpha
graded

The two grading controls a colourist reaches for first, in the order they belong in: saturation about the image's own luminance, then contrast as a blend toward a smoothstep, which steepens the midtones without ever clipping either end the way a gain about a pivot would.

luminance
dotgraded
vec30.21260.71520.0722
grey
vec3luminanceluminanceluminance
saturated
+grey
curved
*saturated
*saturated
-
vec33.03.03.0
*saturated2.0
contrasted
mixsaturatedcurvedgrade-contrast

A restrained corner falloff; the frame should feel photographed, not port-holed.

vignette
-1.0
*
swizzlelens-control:z
smoothstep0.100.75radial

Eight bits per channel cannot hold a sky gradient: a wide, slowly changing surface crosses a quantization step every few dozen pixels and the eye reads the step as a contour. Interleaved gradient noise, scaled so it is about half a step wherever the image sits on the transfer curve, turns the contour into a grain too fine to see.

pixel
/uv-inputpresentation-texel
dither-phase
+
*
swizzlepixel:x
0.06711056
*
swizzlepixel:y
0.00583715
dither
-
fract
*52.9829189
fractdither-phase
0.5
dither-scale
*0.0060
expt
+luminance0.0025
0.55
presented
mix
+
*contrastedvignette
vec3
*ditherdither-scale
*ditherdither-scale
*ditherdither-scale
radiance

The shadow diagnostic is a measurement, not a picture: when the scene pass writes raw visibility instead of radiance, exposure, the filmic curve, the lens chain, the grade, and the vignette would each distort exactly the quantity being measured, so presentation hands the value straight through.

swizzlesun-screen:w
set-outputcolor-output
vec4presented1.0
defunfocus-post-uniform-block

The presentation uniform block exactly as the post stage declares it.

find-if
lambda
resource
typepresource'shader-uniform-block
shader-specification-resources

The shadow-map pass renders the same block mesh into stored light-space depth. The block fragment material samples that product with explicit percentage-closer filtering and receiver bias above.

define-shader-methodshader-specification-forblock-world-shadow-vertex-specification
role
eql:block-shadow
stage
eql:vertex
:stage:vertex:inputs
world-position:vec3:location0:quantity:world-position:unit:cell
:outputs
clip-position:vec4:built-in:position
:resources
frame-state:uniform-block:set0:binding2:members#.*frame-uniform-members*
let*
clip
project-point:world-to-lightworld-positionshadow-row-xshadow-row-yshadow-row-zshadow-row-w
set-outputclip-positionclip

The crosshair is deliberately another tiny mathematical material rather than a magic fixed-function colour. Its positions and ink are dense vertex data, so the shared graph needs no backend-specific vertex-index operation.

define-shader-methodshader-specification-forblock-world-crosshair-vertex-specification
role
eql:block-crosshair
stage
eql:vertex
:stage:vertex:inputs
screen-position:vec3:location0:quantity:clip-coordinate:unit:one
ink-input:vec3:location1:quantity:linear-rgb:unit:one
:outputs
clip-position:vec4:built-in:position
ink-output:vec3:location0:quantity:linear-rgb:unit:one
let*
clip
vec4
representation
swizzlescreen-position:x
representation
swizzlescreen-position:y
representation
swizzlescreen-position:z
1.0
set-outputclip-positionclip
set-outputink-outputink-input
define-shader-methodshader-specification-forblock-world-crosshair-fragment-specification
role
eql:block-crosshair
stage
eql:fragment
:stage:fragment:inputs
ink-input:vec3:location0:quantity:linear-rgb:unit:one
:outputs
color-output:vec4:location0
let*
inkink-input
rgba
assume-quantity
vec4
representation
quantity1.0:quantity:opacity:unit:one
:quantity:linear-rgba:unit:one
set-outputcolor-outputrgba

The software cursor is one quad carrying a cursor-local coordinate; the arrow itself is a signed distance field the fragment stage evaluates. A pointer is nearly all long diagonals, so triangle soup stairsteps badly at any size, and its silhouette, its outline, its rounded corners and its drop shadow all fall out of thresholds of the one distance.

defconstant+luvcraft-cursor-corner-radius+1.3"How far the pointer's convex corners are rounded, in design units."
defconstant+luvcraft-cursor-outline-width+0.8

How wide the pointer's dark border is, in design units.

Chosen against the corner radius rather than on its own: the white body is the silhouette inset by this much, so its corners keep whatever rounding the difference between the two leaves them.

defunluvcraft-cursor-outline

The pointer's visible corners, tip first, in its own design grid.

The tip is the hotspot, so it sits at the origin exactly. Rounding does not move any of these: a corner's arc is tangent to both its edges and reaches back out to the corner itself, which is why this table can be read as the shape you see rather than as a construction for it.

A the tip, and the hotspot B the heel, foot of the vertical left edge C where the tail leaves the head D the tail's outer corner E the tail's inner corner F where the tail rejoins the head G foot of the diagonal right edge

'
0.00.0
0.016.4
3.413.8
6.320.6
10.118.9
7.612.4
12.411.0
defunluvcraft-cursor-extent

The design-grid rectangle the pointer is drawn inside, as width and height.

let
list
reduce#'maxoutline:key#'first
reduce#'maxoutline:key#'second
define-shader-abstractioncursor-arrow-height
"How far down the design grid the pointer reaches."
defuncursor-outline-edge-normal
fromto

The outward unit normal of the outline edge running from to to.

let*
ex
-
firstto
firstfrom
length
max
sqrt
+
*exex
*eyey
1e-6
defuninset-cursor-outline
outlineradius

Move every edge of outline inward by radius and meet the corners again.

Growing the distance to this back by radius restores outline with each convex corner replaced by an arc of that radius. A reflex corner offsets outward instead and so stays crisp, which is what the notch between head and tail wants.

loopwithcount=
lengthoutline
forindexbelowcountforcorner=
nthindexoutline
forprevious=
nth
mod
-index1
count
outline
fornext=
nth
mod
+index1
count
outline
forentering=forleaving=fordeterminant=
-
*
firstentering
secondleaving
*
secondentering
firstleaving
collect
if
<
absdeterminant
1e-6

A straight-through corner has no bisector to solve for; both edges want the same offset, so take it directly.

list
-
firstcorner
*radius
firstleaving
-
secondcorner
*radius
secondleaving
list
+
firstcorner
/
*
-radius
-
secondleaving
secondentering
determinant
+
secondcorner
/
*
-radius
-
firstentering
firstleaving
determinant
defuncursor-outline-distance-form
pointcorners

Source for the exact signed distance from point to the polygon corners.

The pointer's notch is reflex, so the distance is taken the general way -- nearest point over every edge, sign from a crossing count -- rather than by intersecting half planes, which would quietly shave the notch off.

flet
edges
operator
loopwithcount=
lengthcorners
forindexbelowcountforcorner=
nthindexcorners
forprevious=
nth
mod
-index1
count
corners
collect`
,operator,point,
firstcorner
,
secondcorner
,
firstprevious
,
secondprevious
`
define-shader-functioncursor-edge-squared-distance
pointpxpyqxqy
"Squared distance from POINT to the outline edge between P and Q."
let*
ex
-qxpx
ey
-qypy
wx
-
swizzlepoint:x
px
wy
-
swizzlepoint:y
py
along
clamp
/
+
*wxex
*wyey
max
+
*exex
*eyey
1e-6
0.01.0
bx
-wx
*exalong
by
-wy
*eyalong
+
*bxbx
*byby
define-shader-functioncursor-edge-crossing
pointpxpyqxqy

-1 when a ray from POINT crosses the outline edge between P and Q, else 1.

Multiplying one of these per edge counts the crossings, so the product is negative exactly inside the outline however it winds.

let*
ex
-qxpx
ey
-qypy
wx
-
swizzlepoint:x
px
wy
-
swizzlepoint:y
py
past-p
steppy
swizzlepoint:y
before-q
-1.0
stepqy
swizzlepoint:y
left-of
step
*eywx
*exwy
crossing
+
*past-pbefore-qleft-of
*
-1.0past-p
-1.0before-q
-1.0left-of
-1.0
*2.0crossing
define-shader-abstractioncursor-arrow-distance
point

Signed distance from POINT to the pointer, negative inside.

The corners are rounded off by LUVCRAFT-CURSOR-CORNER-RADIUS, which is what keeps the heel, the shoulder and the tail from reading as the barbs of a fir tree, and what leaves the white body still curved once its dark outline has disappeared into a dark background.

define-shader-methodshader-specification-forluvcraft-cursor-vertex-specification
role
eql:cursor
stage
eql:vertex
:stage:vertex:inputs
screen-position:vec3:location0:quantity:clip-coordinate:unit:one
cursor-input:vec2:location1:quantity:cursor-coordinate:unit:one
:outputs
clip-position:vec4:built-in:position
cursor-output:vec2:location0
let*
clip
vec4
representation
swizzlescreen-position:x
representation
swizzlescreen-position:y
representation
swizzlescreen-position:z
1.0
set-outputclip-positionclip
set-outputcursor-output
representationcursor-input
define-shader-methodshader-specification-forluvcraft-cursor-fragment-specification
role
eql:cursor
stage
eql:fragment
:stage:fragment:inputs
cursor-input:vec2:location0
:outputs
color-output:vec4:location0
let*

One pixel measured in the cursor's own units, so the same source resolves its edges wherever the arrow is scaled to.

dx
derivative-x
swizzlecursor-input:x
dy
derivative-y
swizzlecursor-input:x
pixel
max
sqrt
+
*dxdx
*dydy
1e-4

A soft shadow down and to the right lifts the arrow off bright sky and pale inventory panels alike without a second silhouette.

shadow-point
-cursor-input
vec21.11.6
shadow-distance
shadow
*0.40
-1.0
smoothstep-0.42.8shadow-distance

The silhouette, and the white body inset from it by the outline. The body keeps whatever rounding the outline width leaves it, so the two widths are chosen together.

body
outline-ink
vec30.0450.0500.060

The body is not flat white but carries a little light along its length: warm and bright at the tip, where the eye is meant to go, cooling as it falls back into the tail.

sheen
body-ink
mix
vec30.9850.9760.952
vec30.8550.8680.900
sheen
ink
mixoutline-inkbody-inkcore

Premultiplied: the arrow covers its own share and the shadow only darkens whatever the arrow itself left uncovered.

alpha
+body
*shadow
-1.0body
rgba
assume-quantity
vec4
*inkbody
alpha
:quantity:linear-rgba:unit:one
set-outputcolor-outputrgba

--------------------------------------------------------------------- The physics bodies as spheres, drawn the honest way.

Every ball, marble, drop, and gobbet is really a sphere in the physics, and here it finally looks like one: each body is a camera-facing quad whose fragments send an exact ray at the analytic sphere -- one closed quadratic, no marching -- and shade the true surface point with the true normal. The instance stream carries the body's centre and radius, its orientation quaternion, an albedo and emission the CPU resolved from the body-kind palette, and the sampled voxel light levels. Patterns are painted in the body's own frame: the fragment carries the world normal back through the quaternion's conjugate, so a ball's band, a beach ball's panels, and a smiley's face all tumble as the body rolls.

The lowering has no fragment-depth output, so the quad cannot write the sphere's own depth. Instead the vertex stage slides the proxy toward the eye by one radius: its depth is then conservatively near for every point on the sphere, and a ball resting in a hollow is not eaten by the terrain in front of its centre plane. The draw itself tests depth without writing it and blends premultiplied, with the CPU handing over instances farthest first.

define-shader-functionphysics-sphere-length
vector
"Euclidean length, floored away from the origin's derivative singularity."
sqrt
max
dotvectorvector
1e-12
define-shader-functionphysics-sphere-cross
ab
"The cross product, spelled out; the shader language keeps no such operator."
define-shader-functionphysics-sphere-unspin
vectorspin

VECTOR carried into the body's own frame: a turn by SPIN's conjugate.

The physics keeps a unit quaternion (x y z w) purely for the eye. A pattern painted on the body's own axes therefore visibly turns with the body when the world-space surface normal is unspun through the quaternion before the pattern reads it.

define-shader-functionphysics-sphere-band-color
albedolocal

The classic ball's pale band: a latitude ring in the pattern frame, so it sweeps visibly over the ball as it rolls.

let*
band
-1.0
smoothstep0.100.22
abs
swizzlelocal:y
mixalbedo
vec30.890.840.75
*band0.85
define-shader-functionphysics-sphere-marble-color
albedolocal
"Glass with a vein through it: value noise in the pattern frame."
let*
vein
smoothstep0.420.72
mixalbedo
vec30.940.971.0
*vein0.55
define-shader-functionphysics-sphere-beach-color
local

Six panels meeting at the poles, without an arc tangent.

Three half-planes at sixty degrees classify the pattern-frame longitude: one sector satisfies exactly one of them and its neighbour exactly two, so the count alternates white with colour around the ball, and which pair holds names the colour. A small white cap covers each pole, where every panel converges the way a real beach ball's cap does.

let*
x
swizzlelocal:x
z
swizzlelocal:z
s0
step0.0x
s1
step0.0
+
*-0.5x
*0.8660254z
s2
step0.0
-
*-0.5x
*0.8660254z
white
clamp
-2.0
+s0
+s1s2
0.01.0
red
*s0
*s1
-1.0s2
yellow
*
-1.0s0
*s1s2
blue
*s0
*
-1.0s1
s2
panel
+
*
vec30.970.950.92
white
+
*
vec30.920.160.13
red
+
*
vec30.990.800.12
yellow
*
vec30.160.360.90
blue
cap
smoothstep0.920.97
abs
swizzlelocal:y
mixpanel
vec30.970.950.92
cap
define-shader-functionphysics-sphere-smiley-color
albedolocal

Two eyes and a smile on the pattern frame's +z half, so the face tumbles.

The features are placed by direction from the centre, the same trick the gnome's face uses: a direction does not care what radius the ball was drawn at. The smile is the lower arc of a ring around a point above the mouth, masked to the face's lower half.

let*
mirrored
vec3
abs
swizzlelocal:x
swizzlelocal:y
swizzlelocal:z
eye
-1.0
smoothstep0.130.19
physics-sphere-length
-mirrored
vec30.300.320.90
ring
smile
*
-1.0
smoothstep0.050.10
abs
-ring0.62
smoothstep0.020.12
-0.0
swizzlelocal:y
ink
clamp
+eyesmile
0.01.0
mixalbedo
vec30.160.100.05
ink
define-shader-methodshader-specification-forphysics-sphere-vertex-specification
role
eql:physics-sphere
stage
eql:vertex
:stage:vertex:inputs
quad-corner:vec3:location0
sphere-instance:vec4:location1
spin-instance:vec4:location2
tint-instance:vec4:location3
shine-instance:vec4:location4
:outputs
clip-position:vec4:built-in:position
proxy-world-position:vec3:location0
sphere-output:vec4:location1
spin-output:vec4:location2
tint-output:vec4:location3
light-fog-output:vec4:location4
:resources
frame-state:uniform-block:set0:binding2:members#.*frame-uniform-members*
let*
center
swizzlesphere-instance:xyz
radius
max
swizzlesphere-instance:w
1e-4
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
to-camera
-cameracenter
camera-distance

The proxy plane slides toward the eye by one radius, stopping short of the eye itself: its interpolated depth is then nearer than any point of the sphere, so the depth test against terrain can only err by letting a hidden ball show through a sliver, not by eating a resting ball at its contact circle.

slide
minradius
max0.0
-camera-distance0.25
proxy-center
+center
*
/to-cameracamera-distance
slide

Exactly the half-extent the silhouette needs from the slid plane, capped so a ball against the eye stays finite.

silhouette-run
sqrt
max
-
*camera-distancecamera-distance
*radiusradius
0.001
half-extent
min
*radius4.0
*radius
/
-camera-distanceslide
silhouette-run
world-position
+proxy-center
+
*right
*corner-xhalf-extent
*up
*corner-yhalf-extent
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

Fog is measured once at the centre; a body is small enough to take one answer, and the quadratic law matches the terrain's.

center-view-z
dot
-centercamera
forward
fog-near
fog-far
fog-progress
clamp
/
-center-view-zfog-near
max1.0
-fog-farfog-near
0.01.0
fog-amount
*fog-progressfog-progress
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-instance
set-outputspin-outputspin-instance
set-outputtint-outputtint-instance
set-outputlight-fog-output
vec4
swizzleshine-instance:x
swizzleshine-instance:y
swizzleshine-instance:z
fog-amount
define-shader-methodshader-specification-forphysics-sphere-fragment-specification
role
eql:physics-sphere
stage
eql:fragment
:stage:fragment:inputs
proxy-world-position:vec3:location0
sphere-input:vec4:location1
spin-input:vec4:location2
tint-input:vec4:location3
light-fog-input:vec4:location4
: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
max
swizzlesphere-input:w
1e-4
ray
normalize
-proxy-world-positioncamera

The closed-form ray/sphere meeting: no march for a quadric.

to-center
-cameracenter
half-b
dotto-centerray
gap
-
dotto-centerto-center
*radiusradius
discriminant
-
*half-bhalf-b
gap
coverage
step0.0discriminant
span
sqrt
maxdiscriminant0.0
travel
max
-
-half-b
span
0.0
point
+camera
*raytravel
normal
normalize
-pointcenter

The pattern frame: the normal unspun by the body's quaternion.

local
physics-sphere-unspinnormalspin-input
pattern
swizzlelight-fog-input:z
base
swizzletint-input:xyz
is-band
*
step0.5pattern
steppattern1.5
is-marble
*
step1.5pattern
steppattern2.5
is-beach
*
step2.5pattern
steppattern3.5
is-smiley
step3.5pattern

The block world's light, in miniature: the sampled voxel levels response-curved the same way, the same sun gate, the same dome and torch colours, a wrapped diffuse in place of the flat one.

sky-input
swizzlelight-fog-input:x
block-input
swizzlelight-fog-input:y
sky-level
*sky-inputsky-input
block-level
*block-inputblock-input
sun-direction
representation
swizzlesun-vector:xyz
sun-color
representation
swizzlesun-color-vector:xyz
ambient
representation
swizzleambient-vector:xyz
sun-visibility
smoothstep0.901.0sky-input
lambert
dotnormalsun-direction
wrapped
max0.0
/
+lambert0.30
1.30
facing-up
*0.5
+1.0
swizzlenormal:y
dome
mix
representation
swizzlehorizon-vector:xyz
representation
swizzlezenith-vector:xyz
*facing-upfacing-up
bounce
*
representation
swizzlefog-color-vector:xyz
0.9
environment
mixbounce
mixambientdome0.62
facing-up
sky-light
*environment
+0.030
*0.86sky-level
sun-light
*sun-color
*direct-light-gain
*wrapped
*sun-visibilityday-factor
torch
*
vec31.00.820.58
block-level
emission
swizzletint-input:w

One soft lobe for rubber, a tight bright one for glass, and a glassy rim only the marble wears.

halfway
normalize
-sun-directionray
gloss
+18.0
*46.0is-marble
shine
+0.16
*0.40is-marble
specular
*sun-color
*shine
*
expt
max0.0
dotnormalhalfway
gloss
*sun-visibility
*day-factor
step0.0lambert
view-facing
max0.0
dotnormal
*ray-1.0
rim
*is-marble
*0.22
expt
-1.0view-facing
3.0
radiance
+
*albedo
+sky-light
+sun-lighttorch
+specular
+
vec3rimrimrim
*albedoemission

The same aerial perspective the terrain fades into.

sun-elevation
swizzlesun-direction:y
low-sun
*day-factor
-1.0
smoothstep0.020.45sun-elevation
fog-color
aerial-perspective-color
representation
swizzlefog-color-vector:xyz
raysun-directionlow-sunday-factor
fogged
mixradiancefog-color
swizzlelight-fog-input:w

Premultiplied, so a missed fragment leaves no trace of the quad.

set-outputcolor-output
vec4
*foggedcoverage
coverage