Renderers
Turning a scene into pixels on a Vulkan device. Three renderers live in afield.graphics.scene.fabricated, and they disagree about what a frame costs.
StandardRenderer is the one that runs at frame rate. A small deferred renderer — g-buffer, shadow map, ambient occlusion, lighting composite — built on the Field 4 primitives rather than hiding them, so you feed it meshes and wire a RenderTarget around the passes it hands back. FabricateFrameGraph is that pass list made ordered and named, and it does not hide the underlying Target objects: inserting a pass and manipulating one directly are both available. FabricatePostStack and FabricatePassChain are the two post-processing surfaces, and they differ in one thing: a FabricatePostStack pass inherits its source's resolution and format, while a FabricatePassChain lets every pass declare its own, which is what a half-resolution reduce or a downsample pyramid needs.
PathTracingRenderer is the one that converges. A progressive GPU path tracer with a stackless software BVH that works on every Field Vulkan device and KHR ray queries where the device has them. It accumulates: the image improves while nothing moves, camera motion resets or reprojects that history, and a lighting edit always starts again. Its scene and environment are snapshots — PathTraceScene and PathTraceEnvironment are read once at construction, so editing a material means building a new tracer rather than mutating a live one. Output is a fixed-resolution linear HDR render target, and blitToScreen is a separate call because exposure and tone mapping are a presentation decision. PathTraceGltf loads glTF into it, sRGB-decoding base colour while leaving metallic-roughness linear.
GaussianSplatRenderer is the one that renders something measured rather than modelled. Splats from a capture, sixteen floats each, sorted and blended rather than rasterized as triangles; SplatSequence reads a directory of .compressed.ply frames in any order through a bounded most-recently-used cache, because scrubbing a shot is random access and a forward-only reader would be no use for working.
Underneath all three sits FabricateScene, a Three.js-shaped scene graph that owns no Vulkan resources at all: nodes carry transforms, renderables carry material and rendering intent, and FabricateScenePlanner turns that into the opaque, alpha-masked and back-to-front transparent draw queues a renderer consumes. That independence is what lets the scene graph, the pass graph and the render plan be exercised without a GPU: FabricateSceneGraphTestMain, FabricatePassGraphTestMain and FabricateRenderPlanTestMain are plain mains that do, carrying their own assertions. They are not in the FIELD_TEST smoke catalog, so run them by hand.
Two constraints apply to the renderers themselves. A renderer is constructed, installed and closed on Field's render thread, like any other Field 4 module — installing under a name already in use retires the previous occupant, so there is no coroutine lifetime bookkeeping to do. And a device is required: nothing here that draws does so into an FLine, or in a headless test, the way the Math page's contents do. For the same ground with pictures, see The Fabricate renderer, Path tracing and Field 4 graphics.
46 source file(s), 797 public symbol(s), 427 of them carrying KDoc. Everything below is the documentation that lives beside the code — the same text the completion documentation panel shows in the editor. A blank summary means the declaration has no KDoc yet, not that this page dropped it.
FabricateDirectLighting.kt
What connects StandardRenderer's raster bundle to the ray-traced DirectLighting pass.
That pass needs a G-buffer and a ray-visibility scene that agree about where the surfaces are.
registerOpaqueTriangles fills the raster G-buffer from the same indexed OpaqueTriangles
snapshot the ray acceleration structure keeps, expanding it once for the pulling shader instead
of uploading geometry every frame. directLighting then hands the bundle's G-buffer images and
its post-stack output to the pass, and rtxdiSettings supplies the camera contract the
optional RTXDI path wants, read from the bundle's own raster projection.
Opaque diffuse surfaces only: transparency, textured emission and latent pigments want an adapter nothing here provides yet.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricateDirectLighting.kt — read the source
Used in: Direct lighting
Top-level functions
| Declaration | Summary |
|---|---|
fun StandardRenderer.Bundle.directLighting(visibility: OpaqueRayScene, name: String = "direct-light"): DirectLighting |
Build the experiment from Fabricate's existing G-buffer. Call bundle.attachTo first, then lighting.attach; present lighting.output. SOURCE reads the unchanged post-stack output. Opaque diffuse only: transparency, textured emission and latent pigments require a future adapter. |
fun StandardRenderer.Bundle.rtxdiSettings() |
Camera contract for the optional RTXDI pass, matching this bundle's raster projection. |
fun StandardRenderer.Bundle.registerOpaqueTriangles(name: String, geometry: OpaqueTriangles, color: Vector3f = Vector3f(0.7f), transform: Matrix4f = Matrix4f()): StandardRenderer.Bundle.PullingMeshPair |
Populate the existing raster G-buffer from the SAME indexed snapshot used for ray visibility. Fabricate's current pulling shader consumes expanded vec4 triangles; the adapter expands once. The ray BLAS keeps indexed XYZ data. No per-frame geometry upload or silent alpha approximation. |
FabricateFrameGraph.kt
The renderer's off-screen passes as a named, ordered list, and the one place that order is
written into a RenderTarget.
A StandardRenderer.Bundle holds its five built-in passes as five separate fields, which is a
set rather than a sequence. FabricateFrameGraph takes a bundle and makes it a sequence:
g-buffer, shadow map, ambient occlusion, lighting, transparent composite, then one entry for
each effect currently in the bundle's FabricatePostStack. Every built-in name is a constant on
the companion, and passNameFor supplies the two that arrive as post effects rather than as
bundle fields, motion blur and anti-aliasing; anything else is named after its effect. So
insertAfter("my-pass", PASS_LIGHTING, target) works without knowing how the bundle was built.
attachTo is where the order stops being advisory. A RenderTarget renders its preTargets
before its own pass and in map order, so attaching removes and re-adds every pass by name, which
leaves preTargets iterating in the graph's order. It also remembers, per target, which names
this graph put there last time and drops the ones that have since gone, while a pass some other
code added under a name this graph never used is left alone. Attaching a second time is
therefore a resync rather than a duplicate.
The graph is a view. StandardRenderer.Bundle.frameGraph() builds a fresh one on each call, it
does not own the targets it names, and it does not stop a box reaching past it to a
RenderToTexture directly.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricateFrameGraph.kt — read the source
FabricateFrameGraph
class FabricateFrameGraph private constructor(private val graph: FabricatePassGraph<Target>)
Ordered, inspectable pass contract for StandardRenderer.
FabricateGltfBridge.kt
The glTF side of the fabricated scene model: one call that turns a loaded GltfSupport.GltfModel
into a FabricateScene, and two conversions beside it.
There are two routes from a loaded model into the PBR renderer and they differ in what they
touch. FabricateGltfBridge.fromModel builds a scene graph and stops, so a caller can read or
rewrite the imported nodes before any GPU resource exists. StandardRenderer.Bundle.registerGltf
does that same import and then registers a pulling mesh per node, which is what the renderer
draws from and what a tool inspecting node names does not need.
Nothing here loads a file. GltfSupport.loadGltfBasic is the loader and MeshFromDisk.kt
describes the route in; this is the conversion at the end of it.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricateGltfBridge.kt — read the source
Top-level functions
| Declaration | Summary |
|---|---|
fun GltfSupport.MaterialTextured.toFabricateMaterial(): FabricateMaterial |
FabricateMaterial.fromGltf as an extension, for a glTF material already in hand. |
fun Int.toFabricateTextureSemantic(): String |
An Assimp texture-type constant as the glTF name for that slot: baseColor, normal, metallicRoughness and so on, with an unrecognised type rendered as texture- and its number rather than dropped. |
FabricateGltfBridge
Converts the existing Assimp/GLTF loader model into the fabricated scene/material data model.
| Declaration | Summary |
|---|---|
fun fromModel(model: GltfSupport.GltfModel, name: String = "gltf"): FabricateScene |
A FabricateScene holding model's meshes, with no renderer and no GPU resources attached. |
FabricateMaterialLibrary.kt
The flattened per-material record a render queue carries, and the table that hands out its ids.
FabricateMaterial is the authoring shape: nineteen fields, six of them texture handles.
FabricateMaterialRecord is nine fields wide, and toRecord is the conversion. It keeps
base colour, metallic, roughness, alpha cutoff, alpha mode and double-sidedness as they are,
turns receiveAo into a 0-or-1 occlusion scale, and leaves the textures, clearcoat, the UV
transform, receiveShadow and sortOrder behind. Two of the record's fields are placeholders
rather than conversions: emissive is written as zero instead of the material's
emissiveFactor, and textureMask as 0.
FabricateMaterialLibrary keys materials by identity rather than by equality, so a material
shared between nodes gets one id and two materials with matching values get two. An id is a
position in a dense list, which is why record(id) is a plain index and clear() invalidates
every id handed out before it. idFor converts a material the first time it sees one and
returns the existing id afterwards without re-reading it; refresh re-reads into the same slot.
FabricateScenePlanner keeps a library and calls refresh for each visible renderable as it
plans, so FabricateRenderPlan's materials list is current for the frame it was planned in,
and each item's materialId indexes it.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricateMaterialLibrary.kt — read the source
Top-level functions
| Declaration | Summary |
|---|---|
fun FabricateMaterial.toRecord(): FabricateMaterialRecord |
FabricateMaterialRecord
One material flattened to nine fields, addressed by id from a render queue.
FabricateMaterialLibrary
class FabricateMaterialLibrary
Stable CPU-side material table. This is the handoff shape for the future GPU material buffer: scene nodes carry material objects, while render queues address compact material records by id.
FabricatePassChain.kt
The vocabulary for declaring a chain of post passes that are not all the same shape, and the pure planner that resolves one.
A chain is a list of FabricatePassSpec. Each spec names its pass, carries its fragment shader,
and says how big it wants to be (FabricatePassSize), in what format, which earlier pass feeds
its primary sampler, and which other earlier passes it wants bound to named samplers. A size may
be absolute, a fraction of the pass's own input, or a fraction of the texture entering the chain,
which is what lets one chain hold a full-resolution pass, a quarter-resolution blur and a
half-float intermediate.
FabricatePassChain.plan turns that list into a FabricatePassChainPlan without making a Vulkan
call: it resolves every target shape and every sampler binding, works out which pass last reads
each pass's output, and puts two passes on the same physical target when their outputs never
overlap in time and their shapes match. Being arithmetic and reachability rather than GPU state
is what lets FabricatePassChainTest exercise all of it on the CPU, which this layer needs more
than most: a resolution resolved one texel wrong renders a picture that looks entirely plausible.
FabricatePassChainPlan.report prints the resolution it arrived at — every pass with its shape,
samplers and target slot, and the memory total against one full-resolution target per pass.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricatePassChain.kt — read the source
FabricatePassChain
Declarative, heterogeneous pass chains for FabricatePostStack — every pass may have its own resolution, its own format, and its own named inputs.
| Declaration | Summary |
|---|---|
const val CHAIN_INPUT |
Reference name for the texture entering the chain — the stack input, not a pass. |
const val GBUFFER_MOTION |
Reference name for the renderer's per-pixel motion vectors — g-buffer attachment 3, not a pass in this chain. |
const val GBUFFER_DEPTH |
Reference name for the renderer's depth buffer — the g-buffer's sampled depth, not a pass. |
val EXTERNAL_REFERENCES: Set<String> |
The reference names that resolve outside the chain rather than to one of its passes. |
const val PRIMARY_SAMPLER |
The sampler name a pass's primary input is always bound to. |
const val PERSISTENT_IMAGE_COUNT |
Images a persistent pass's target holds — the ping-pong depth. |
fun plan(passes: List<FabricatePassSpec>, inputWidth: Int, inputHeight: Int, inputFormat: Int): FabricatePassChainPlan |
Plan passes against a chain input of inputWidth x inputHeight in inputFormat. |
FabricatePassShape
The resolved shape of a pass target: what FabricatePassSize and the format request came to.
| Declaration | Summary |
|---|---|
val bytes: Long |
Bytes of color attachment this shape occupies, mip chain included. |
override fun toString(): String |
FabricatePassSize
sealed interface FabricatePassSize
How a pass's target is sized.
| Declaration | Summary |
|---|---|
fun resolve(source: FabricatePassShape, chainInput: FabricatePassShape): Pair<Int, Int> |
|
val independentOfSource: Boolean |
True if resolve ignores its source argument. |
data object MatchSource : FabricatePassSize |
The size of the pass's own primary input — the historical, and still the default, behaviour. |
data object MatchChainInput : FabricatePassSize |
The size of the texture entering the chain, whatever the pass immediately before did. |
data class Absolute(val width: Int, val height: Int) : FabricatePassSize |
A fixed size in texels, independent of the chain. |
data class FractionOfSource(val numerator: Int, val denominator: Int) : FabricatePassSize |
numerator/denominator of the pass's primary input. Chain these to build a pyramid. |
data class FractionOfChainInput(val numerator: Int, val denominator: Int) : FabricatePassSize |
numerator/denominator of the chain input — a stable absolute rung, whatever precedes it. |
FabricatePassRef
sealed interface FabricatePassRef
Which texture a pass sampler is bound to.
| Declaration | Summary |
|---|---|
data object ChainInput : FabricatePassRef |
The texture entering the chain. |
data class Pass(val index: Int, val name: String) : FabricatePassRef |
The output of an earlier pass. |
data class PreviousFrame(val index: Int, val name: String) : FabricatePassRef |
A FabricatePassSpec.persistent pass's output as of the end of last frame — the other half of its ping-pong pair. |
data class External(val reference: String) : FabricatePassRef |
A texture the renderer owns rather than the chain — FabricatePassChain.GBUFFER_MOTION or FabricatePassChain.GBUFFER_DEPTH. |
FabricatePassInput
An extra named sampler a pass wants bound to another pass's output.
| Declaration | Summary |
|---|---|
val samplerName: String |
|
val fromPass: String |
|
val previousFrame: Boolean |
FabricateResolvedInput
data class FabricateResolvedInput(val samplerName: String, val from: FabricatePassRef)
A resolved FabricatePassInput — sampler name plus the texture the planner bound it to.
FabricatePassSpec
One declared pass.
FabricatePlannedPass
A pass target after planning: its shape, its bound inputs, its lifetime and the slot it shares.
| Declaration | Summary |
|---|---|
val index: Int |
|
val spec: FabricatePassSpec |
|
val shape: FabricatePassShape |
|
val inputs: List<FabricateResolvedInput> |
|
val lastConsumer: Int |
|
val slot: Int |
|
val name: String |
|
val persistent: Boolean |
True if this pass's target is a ping-pong pair rather than a single image. |
val frameHistory: Boolean |
True if this pass asked for the chain's frame history. |
FabricateTargetSlot
A physical target, and every pass that takes a turn writing to it.
| Declaration | Summary |
|---|---|
val index: Int |
|
val shape: FabricatePassShape |
|
val passIndices: MutableList<Int> |
|
val persistent: Boolean |
|
val imageCount: Int |
Images this slot allocates — two for a ping-pong pair, one otherwise. |
val bytes: Long |
Color-attachment bytes this slot occupies, ping-pong pair included. |
FabricatePassChainPlan
A resolved chain: what to allocate, in what order to run it, and what it costs.
| Declaration | Summary |
|---|---|
val passes: List<FabricatePlannedPass> |
|
val slots: List<FabricateTargetSlot> |
|
val input: FabricatePassShape |
|
val targetCount: Int |
Physical targets this chain needs. |
val passCount: Int |
Targets a one-target-per-pass allocator would need. |
val plannedBytes: Long |
Color-attachment bytes this chain needs. |
val naiveBytes: Long |
Color-attachment bytes one full-resolution, chain-input-format target per pass would need. |
val unsharedBytes: Long |
Bytes the multi-resolution shapes alone save, before any slot sharing. |
val persistentPasses: List<FabricatePlannedPass> |
Passes whose target is a ping-pong pair — the chain's state, if it has any. |
val historyPasses: List<FabricatePlannedPass> |
Passes that asked for the chain's frame history — the chain's reprojecting passes. |
val output: FabricatePlannedPass |
The chain's final pass. |
operator fun get(name: String): FabricatePlannedPass? |
|
fun passesOn(slot: Int): List<FabricatePlannedPass> |
Passes sharing slot's target, in the order they write it. |
fun report(): String |
A human-readable audit of the plan: every pass with its resolved shape, its bound samplers, the slot it writes and when its output dies, then the memory totals against the counterfactual. |
FabricatePassChainBuilder.kt
Materializes a planned pass chain onto the GPU, through the post stack that already exists.
The division of labour is strict. FabricatePassChain.plan decides everything: every
resolution, every format, which texture every sampler reads, which passes can share a target.
This file decides nothing. It walks the plan and calls
StandardRenderer.Bundle.addShaderEffect with sizes already resolved to
FabricatePassSize.Absolute, so there is no second, subtly different implementation of the
arithmetic sitting where only a GPU can reach it. That matters more here than it usually would,
because a resolution bug in a pass chain renders a picture that looks fine.
The passes land in the same FabricatePostStack as everything else, so FabricateFrameGraph
schedules them, the stack's output() is the chain's output, insertBefore and remove still
work on them by name, and a chain composes with addBloom and setToneMapping rather than
replacing them.
FabricateBuiltChain comes back holding the plan, the built effects and the chain input, and is
how a push constant is retuned afterwards without a rebuild or a reallocation.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricatePassChainBuilder.kt — read the source
Top-level functions
| Declaration | Summary |
|---|---|
fun StandardRenderer.Bundle.addPassChain(passes: List<FabricatePassSpec>, renderer: Renderer = fromContext(), printReport: Boolean = false): FabricateBuiltChain |
Materialize passes as a new chain or replace their existing named passes. |
fun FabricatePostEffect.setPush(name: String, value: Any): Boolean |
Set one push constant on a post effect's fullscreen quad. |
fun FabricatePostEffect.setPushBinding(name: String, value: () -> Any): Boolean |
Bind one push constant on a post effect's fullscreen quad to a supplier re-read every frame. |
Top-level values
| Declaration | Summary |
|---|---|
const val GBUFFER_MOTION_ATTACHMENT |
The g-buffer colour attachment holding per-pixel motion vectors. |
FabricateBuiltChain
A chain that has been planned and built: the plan, the effects, and live push-constant access.
| Declaration | Summary |
|---|---|
val plan: FabricatePassChainPlan |
|
val effects: List<FabricatePostEffect> |
|
val input: RenderToTexture |
|
val history: FrameHistory |
The chain's frame history — shared by every pass that declared FabricatePassSpec.frameHistory, and inert if none did. |
val output: RenderToTexture |
The chain's final image. |
fun names(): List<String> |
Pass names, in run order. |
fun pass(name: String): FabricatePostEffect? |
The named pass, or null if this chain has no such pass. |
operator fun set(pushName: String, value: Any) |
Set a push constant on every pass in the chain that declares it. |
fun targetOf(name: String): RenderToTexture? |
The target a named pass renders into, or null if there is no such pass. |
fun stateOf(name: String): PingPongRenderToTexture? |
The ping-pong target behind a FabricatePassSpec.persistent pass, or null if name is not one. |
fun stateNames(): List<String> |
Persistent pass names, in run order — the chain's state fields. |
fun historyNames(): List<String> |
Names of the passes that receive frame history — the chain's reprojecting passes. |
fun setPush(passName: String, pushName: String, value: Any): Boolean |
Retune a pass's push constant in place, with no rebuild and no reallocation. |
val allocatedBytes: Long |
Sum of every pass target's color-attachment bytes, as actually allocated. |
override fun toString(): String |
FabricatePassGraph.kt
The ordering primitive the Fabricate renderer's pass collections are built on.
Both FabricatePostStack and FabricateFrameGraph hold a FabricatePassGraph, so what a pass
name means, and where a new pass may go relative to one already there, is settled once here
rather than once per collection. The same file carries the rule that decides where a
replacement chain picks up its input: the output of the pass before the first one it replaces,
or the chain's original input when that pass comes first.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricatePassGraph.kt — read the source
FabricatePassGraph
class FabricatePassGraph<T>(initial: Iterable<Pair<String, T>> = emptyList())
The ordered, name-keyed map both of this package's pass collections are built on.
FabricatePassGraphTestMain.kt
src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricatePassGraphTestMain.kt — read the source
FabricatePassGraphTestMain
object FabricatePassGraphTestMain
Tiny JVM smoke test for the pure ordered-pass data structure behind FabricateFrameGraph. Run with the backdoor's run-jvm-main or any JVM launcher that includes jvmMain classes.
| Declaration | Summary |
|---|---|
fun main(args: Array<String>) |
FabricatePassLibrary.kt
Worked multi-resolution passes, as lists of FabricatePassSpec ready to hand to a chain.
Six recipes and two chains assembled from them: a downsample pyramid, a separable Gaussian blur, a
disc aperture blur, an upsample-and-add, a bright pass and a composite, then
FabricatePassLibrary.apertureChain and FabricatePassLibrary.bloomPyramid. Every one returns
specs rather than touching the GPU, so a chain can be composed from several of them, planned,
printed and asserted on before anything is allocated, and a caller can splice a rung, reorder one
or drop one.
Four of the fragment shaders live here as string constants; the separable blur reuses the nine-tap
kernel the bloom chain already ships rather than keeping a second copy in step with it. All of
them follow the post-stack conventions: a fullscreen triangle, vUv texel-aligned so v = 0 is
image row 0, the primary input bound as sourceTex, and every offset scaled by the source's texel
size, so a pass reads its input's texels rather than assuming its own resolution matches.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricatePassLibrary.kt — read the source
FabricatePassLibrary
A library of worked multi-resolution passes, expressed as FabricatePassSpec fragments.
| Declaration | Summary |
|---|---|
const val MAX_APERTURE_TAPS |
Hard cap on aperture taps, so the disc loop is bounded at compile time. |
const val MAX_HALF_EXACT_RESOLUTION |
Largest jumpFloodDistance resolution whose texel coordinates a half-float holds exactly. |
fun downsamplePyramid(prefix: String = "reduce", levels: Int = 3, source: String? = null, format: Int? = null): List<FabricatePassSpec> |
A downsample pyramid: levels halvings of the source, each a 13-tap filtered reduce. |
fun separableBlur(prefix: String = "blur", radius: Float = 2.0f, source: String? = null, size: FabricatePassSize = FabricatePassSize.MatchSource, format: Int? = null): List<FabricatePassSpec> |
A separable Gaussian blur: a horizontal pass then a vertical one, both at the source's size. |
fun aperture(name: String = "aperture", radiusTexels: Float = 8f, taps: Int = 32, source: String? = null, size: FabricatePassSize = FabricatePassSize.MatchSource, format: Int? = null): FabricatePassSpec |
A disc (aperture) blur of radiusTexels, the shape a real lens makes out of a point of light. |
fun upsampleAdd(name: String, source: String, over: String, weight: Float = 1f, radiusTexels: Float = 1f, format: Int? = null): FabricatePassSpec |
A tent upsample of source added onto over at over's resolution, weighted by weight. |
fun composite(name: String = "composite", blurred: String, sharp: String, blend: Float = 0.5f, gain: Float = 1f, add: Float = 0f, format: Int? = null): FabricatePassSpec |
A composite: blurred blended over sharp at sharp's resolution. |
fun brightPass(name: String = "bright", threshold: Float = 1f, source: String? = null, size: FabricatePassSize = FabricatePassSize.MatchSource, format: Int? = VK_FORMAT_R16G16B16A16_SFLOAT): FabricatePassSpec |
A soft-knee bright pass, reusing the kernel addBloom already ships. |
fun apertureChain(nearRadius: Float = 4f, farRadius: Float = 12f, taps: Int = 32, blend: Float = 0.7f, format: Int? = null): List<FabricatePassSpec> |
The chain the hand-wired version is made of: source, reduce ½, reduce ¼, two aperture blurs at different radii on the cheap ¼ image, and a full-resolution composite. |
fun bloomPyramid(levels: Int = 4, threshold: Float = 1f, intensity: Float = 0.7f, format: Int? = VK_FORMAT_R16G16B16A16_SFLOAT): List<FabricatePassSpec> |
A multi-resolution bloom: bright pass, a pyramid, then the pyramid walked back up with upsampleAdd, then composited additively. |
fun stableFluid(prefix: String = "fluid", resolution: Int = 256, iterations: Int = 20, timestep: Float = 1f / 60f, velocityDissipation: Float = 0.999f, dyeDissipation: Float = 0.997f, vorticity: Float = 12f, format: Int = VK_FORMAT_R16G16B16A16_SFLOAT): List<FabricatePassSpec> |
A Stam-style stable fluid: a velocity field and a dye field that persist across frames, with a splat you paint into either of them. |
fun jumpFloodDistance(prefix: String = "jfa", resolution: Int = 512, source: String = FabricatePassChain.CHAIN_INPUT, threshold: Float = 0.5f, channel: Int = 3, signed: Boolean = false, plusOne: Boolean = true, scale: Float = 1f, format: Int = VK_FORMAT_R16G16B16A16_SFLOAT): List<FabricatePassSpec> |
A jump flood distance transform: a mask in, a distance field out, in 1 + log2(resolution) passes. |
fun distanceBands(name: String = "distance-bands", source: String? = null, spacing: Float = 16f, inside: Vector3f = Vector3f(0.15f, 0.45f, 0.95f), outside: Vector3f = Vector3f(0.95f, 0.55f, 0.15f), format: Int? = null): FabricatePassSpec |
Bands and an isoline over a distance field, so a chain that ends in one shows something. |
const val DOWNSAMPLE_13TAP_FRAGMENT |
Jimenez 13-tap downsample. Four inner taps at ±1 source texel carry half the weight; four overlapping 2x2 blocks on the ±2 ring carry an eighth each. |
val APERTURE_FRAGMENT |
Disc (aperture) blur. Taps on a golden-angle spiral with r = sqrt(t) so they are uniform over the disc's area, weighted equally so the result is flat-topped with a hard rim — the bokeh shape, not a Gaussian. |
const val UPSAMPLE_ADD_FRAGMENT |
3x3 tent upsample of lowTex added onto sourceTex. Offsets are in lowTex texels, so the tent is a genuine reconstruction filter for the small image rather than a blur of the big one. |
const val COMPOSITE_FRAGMENT |
Join blurTex onto sourceTex at sourceTex's resolution: crossfade by blend, then lay add of the blurred image over the top. Depth of field is blend near one; bloom is blend zero and add positive. |
val FLUID_ADVECT_VELOCITY_FRAGMENT |
Semi-Lagrangian advection of the velocity field by itself. |
val FLUID_CURL_FRAGMENT |
∇×v of the velocity field, one channel, for the vorticity confinement force. |
fun fluidForceFragment(vorticity: Boolean): String |
The force stage: vorticity confinement, then your splat. |
val FLUID_DIVERGENCE_FRAGMENT |
∇·v, the right-hand side of the pressure equation. One channel, hence its own format. |
const val FLUID_ZERO_FRAGMENT |
Writes zero. |
const val FLUID_JACOBI_FRAGMENT |
One Jacobi iteration of ∇²p = ∇·v. |
val FLUID_PROJECT_FRAGMENT |
The projection: subtract ∇p from the velocity field, leaving it (approximately) divergence-free. The pass that writes the fluid's persistent velocity state. |
val FLUID_ADVECT_DYE_FRAGMENT |
Advects the dye by this frame's projected velocity and adds the mass splat — the fluid's other persistent field, and the one you actually look at. |
const val JUMP_FLOOD_SEED_FRAGMENT |
Jump flood seeding. Writes the texel's own centre as a seed coordinate where the mask says so, and a z = 0 "no seed" sentinel where it does not. |
const val JUMP_FLOOD_FRAGMENT |
One jump flood round: keep whichever of the nine candidates at ±stride carries the seed closest to this texel. |
const val JUMP_FLOOD_DISTANCE_FRAGMENT |
Turns a flooded seed field into a distance: r is the distance, gb the seed coordinate it came from, a the mask. |
const val DISTANCE_BANDS_FRAGMENT |
Even bands and a dark isoline over a signed or unsigned distance field, with the two sides coloured differently. |
const val MAX_MOTION_BLUR_TAPS |
Hard cap on cameraMotionBlur taps, so the streak loop is bounded at compile time. |
const val MOTION_DISPLAY_MAX_MODE |
Highest temporalDebug mode — availability. |
const val FRAME_HISTORY_GLSL |
The GLSL every FabricatePassSpec.frameHistory pass shares: clip/UV conversion and the reprojection lookup. |
fun temporalAccumulate(name: String = "taa", source: String? = null, blend: Float = 0.9f, rejection: Float = 0f, ndcDepth: Float = 0.5f, perPixelMotion: Boolean = true, resolution: Pair<Int, Int>, format: Int = VK_FORMAT_R16G16B16A16_SFLOAT): List<FabricatePassSpec> |
A TAA-style temporal accumulation: blend this frame into a reprojected history, with a rejection rule. |
fun cameraMotionBlur(name: String = "camera-motion-blur", source: String? = null, strength: Float = 1f, taps: Int = 9, ndcDepth: Float = 0.5f, format: Int? = null): List<FabricatePassSpec> |
Camera motion blur: smear each pixel along the direction it came from. |
fun temporalDebug(name: String = "motion-display", source: String? = null, mode: Int = 0, motionScale: Float = 8f, ndcDepth: Float = 0.5f, perPixelMotion: Boolean = true, format: Int? = null): List<FabricatePassSpec> |
The debug view, in four modes — and it is not garnish. |
fun temporalAccumulateFragment(perPixelMotion: Boolean = true) |
Neighbourhood-clamped temporal accumulation against a reprojected history. |
fun temporalDebugFragment(perPixelMotion: Boolean = true) |
The debug view's four modes, sharing one shader because they share one reprojection. |
fun cameraMotionBlurFragment(taps: Int): String |
Camera motion blur over taps samples along the backward-motion direction. |
enum class SummaryStatistics |
How a cell's value statistics are carried up the pyramid. |
const val SUMMARY_VALUE |
Plane carrying (weight, value mean, value variance, occupancy) — see SummaryStatistics. |
const val SUMMARY_MOMENT |
Plane carrying the first and second spatial raw moments, (Σwx, Σwy, Σwx², Σwy²). |
const val SUMMARY_EXTENT |
Plane carrying (Σwxy, min value, max value, ·) — the cross moment and the two extremes. |
const val SUMMARY_RANGE |
Plane carrying (first, count, prefix, ·) — a node's range in the sorted array. |
val SUMMARY_PLANES: List<String> |
The three planes both producers write, in the order summaryPyramid emits them. |
const val MAX_EXACT_MORTON_BITS |
Largest Morton code an fp32 channel holds exactly, as a bit count. |
const val MAX_POINT_QUADTREE_POINTS |
Largest point count mortonPointQuadtree accepts. |
fun summaryPlane(prefix: String, plane: String, depth: Int): String |
The pass name a plane's level lands on: $prefix-$plane-$depth. |
fun pointSearchSteps(count: Int): Int |
Binary-search steps needed to bracket count sorted keys — 1 + ⌈log₂ count⌉. |
fun summaryPyramid(prefix: String = "summary", resolution: Int = 256, source: String = FabricatePassChain.CHAIN_INPUT, coarsest: Int = 0, weightChannel: Int = -1, valueChannel: Int = -1, statistics: SummaryStatistics = SummaryStatistics.POOLED, format: Int = VK_FORMAT_R32G32B32A32_SFLOAT): List<FabricatePassSpec> |
A summarised quadtree over a raster: every 2×2 block reduced to its parent, all the way up, carrying mergeable sufficient statistics instead of a colour. |
fun mortonPointQuadtree(prefix: String = "quadtree", points: String, pointGrid: Int = 32, depth: Int = 4, coarsest: Int = 0, densityScale: Float = 1f, statistics: SummaryStatistics = SummaryStatistics.POOLED, format: Int = VK_FORMAT_R32G32B32A32_SFLOAT): List<FabricatePassSpec> |
A summarised quadtree over a point set: Morton codes, a sort, one node level found by bracketing prefixes in the sorted array, and then the same reduction ladder as summaryPyramid. |
fun summaryEllipses(name: String = "summary-ellipses", over: String = FabricatePassChain.CHAIN_INPUT, prefix: String = "summary", depth: Int = 4, sigma: Float = 1f, weightGain: Float = 1f, underGain: Float = 0.35f, tint: Vector3f = Vector3f(0.35f, 0.9f, 1f), format: Int? = null): FabricatePassSpec |
Draws a level of a summarised quadtree as geometry: a centroid, a 1σ covariance ellipse and a weight tint per cell, over the image it summarises. |
fun summaryCovariance(name: String = "summary-covariance", prefix: String = "summary", depth: Int = 4, gain: Float = 1f, format: Int? = null): FabricatePassSpec |
The spatial covariance of every cell of one level, as (cxx, cyy, cxy) — the statistic the moment planes exist to make available, computed once. |
enum class SubdivisionCriterion |
What makes a cell worth splitting — the question a summarised quadtree exists to answer. |
fun adaptiveSummaryCells(prefix: String = "summary", name: String = "adaptive", over: String = FabricatePassChain.CHAIN_INPUT, coarsest: Int = 1, finest: Int = 5, criterion: SubdivisionCriterion = SubdivisionCriterion.VALUE_VARIANCE, threshold: Float = 0.02f, weightFloor: Float = 1e-4f, statistics: SummaryStatistics = SummaryStatistics.POOLED, sigma: Float = 1f, weightGain: Float = 1f, underGain: Float = 0.45f, tint: Vector3f = Vector3f(0.35f, 0.9f, 1f), format: Int? = null): List<FabricatePassSpec> |
An adaptive cut of a summarised quadtree: descend from the root, stop where the cell is a good enough description of its contents, and draw the cells you stopped at. |
fun summaryReadout(name: String = "summary-readout", from: String? = null, channel: Int = 0, gain: Float = 1f, bias: Float = 0f, format: Int? = null): FabricatePassSpec |
One channel of one plane, scaled into 0..1 — the instrument every assertion about a summary is made through. |
fun summaryVariance(name: String = "summary-variance", from: String? = null, statistics: SummaryStatistics = SummaryStatistics.POOLED, gain: Float = 1f, format: Int? = null): FabricatePassSpec |
The cell variance of a SUMMARY_VALUE plane, scaled — which takes different arithmetic for the two SummaryStatistics, and is the only consumer that has to know which it is looking at. |
const val SUMMARY_CHANNEL_GLSL |
Channel selection and the leaf's weight/value pair, shared by every seed pass. |
val SUMMARY_VALUE_SEED_POOLED_FRAGMENT |
Seeds (weight, mean, variance, occupancy) from one texel. |
val SUMMARY_VALUE_SEED_RAW_FRAGMENT |
Seeds (weight, Σwv, Σwv², occupancy) — the raw-moment alternative. |
val SUMMARY_MOMENT_SEED_FRAGMENT |
Seeds the spatial first and second moments, (Σwx, Σwy, Σwx², Σwy²), from one texel. |
val SUMMARY_EXTENT_SEED_FRAGMENT |
Seeds (Σwxy, min v, max v, 0). |
const val SUMMARY_CHILDREN_GLSL |
The 2×2 gather every reduction does: the four children of the texel being written. |
val SUMMARY_MEAN_REDUCE_FRAGMENT |
Reduce by averaging: correct for every channel that is a per-area sum. |
val SUMMARY_VALUE_REDUCE_POOLED_FRAGMENT |
Reduce (weight, mean, variance, occupancy) by the weighted pooled-variance identity. |
val SUMMARY_EXTENT_REDUCE_FRAGMENT |
Reduce (Σwxy, min, max, ·): the cross moment averages, the extremes take their own operator. |
val SUMMARY_RANGE_REDUCE_FRAGMENT |
Reduce (first, count, prefix, ·): the parent's range is its children's. |
val SUMMARY_READOUT_FRAGMENT |
One channel, scaled and biased into something an 8-bit readback can carry. |
val SUMMARY_COVARIANCE_FRAGMENT |
(cxx, cyy, cxy) from the three planes, scaled — and cxy biased, because it is signed. |
fun summaryVarianceFragment(statistics: SummaryStatistics): String |
The variance of a SUMMARY_VALUE cell, however that plane happens to carry it. |
const val MORTON_GLSL |
Bit interleave, and the reason a Morton code is a tree path rather than a hash. |
fun mortonCodeFragment(depth: Int, count: Int, width: Int): String |
Quantise a point into the leaf grid and interleave — (code, index, ·, ·), both normalised. |
fun pointRankFragment(count: Int, width: Int): String |
Each element counts the elements that precede it — the gather half of a sort. |
fun pointGatherFragment(count: Int, width: Int): String |
Each slot picks up the element whose rank it is — the other half of the sort. |
fun pointNodesFragment(depth: Int, count: Int, width: Int, steps: Int): String |
One node per texel: its Morton prefix, and the range of the sorted array holding its points. |
fun pointLeafValueFragment(count: Int, width: Int, pooled: Boolean): String |
Seeds the value plane for a node, by Welford over its points. |
fun pointLeafMomentFragment(count: Int, width: Int): String |
Seeds (Σwx, Σwy, Σwx², Σwy²) for a node, from the points in its range. |
fun pointLeafExtentFragment(count: Int, width: Int): String |
Seeds (Σwxy, min v, max v, 0) for a node, with the same empty-cell sentinels as the raster. |
fun summarySelectFragment(criterion: SubdivisionCriterion, statistics: SummaryStatistics, hasParent: Boolean): String |
One level of the descent: does this cell stop here, or do its children get a turn? |
fun summaryAdaptiveDrawFragment(cells: Int): String |
Composites one level of the cut over the levels above it: a cell outline always, an ellipse and a centroid where there is mass. |
fun summaryEllipsesFragment(cells: Int): String |
A centroid, a 1σ ellipse and a weight tint per cell, over the image underneath. |
FabricatePostEffects.kt
High-level post-processing helpers for StandardRenderer.Bundle, layered on top of the ordered
FabricatePostStack. These are the "make HDR look right" and "make emissive glow" effects that
Three.js users expect from an EffectComposer: tone mapping + exposure, and bloom.
All passes are built with StandardRenderer.Bundle.addShaderEffect, so they chain automatically
(each samples the previous stack output as sourceTex). Build the post stack once, then call
bundle.attachTo(renderer) so the new passes are synced into the window's frame graph.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricatePostEffects.kt — read the source
Used in: Fabricate Renderer
Top-level functions
| Declaration | Summary |
|---|---|
fun StandardRenderer.Bundle.setToneMapping(mode: FabricateToneMap = FabricateToneMap.ACES, exposure: Float = 1f, renderer: Renderer = fromContext()): FabricatePostEffect? |
Set (or replace) the final tone-mapping pass. Tone mapping always runs last in the post stack, so it maps HDR lighting into a displayable range after every other effect. Re-invoking this replaces the previous tone-map pass; FabricateToneMap.NONE removes it. Remember to re-attach the frame graph afterwards so the pass is scheduled. |
fun StandardRenderer.Bundle.addBloom(threshold: Float = 1.0f, intensity: Float = 0.6f, radius: Float = 2.5f, renderer: Renderer = fromContext()): List<FabricatePostEffect> |
Add a bloom chain: a bright-pass extract, a separable Gaussian blur, and an additive composite back over the pre-bloom image. Bloom runs on HDR values, so it is inserted before any tone-mapping pass. Calling this again rebuilds the chain. Re-attach the frame graph afterwards. |
Top-level values
| Declaration | Summary |
|---|---|
const val TONEMAP_EFFECT_NAME |
FabricateToneMap
Tone-mapping operator passed to setToneMapping.
Constants: NONE, REINHARD, FILMIC, ACES, AGX.
FabricatePostStack.kt
The ordered post-processing stack a Fabricate pass lands in, and the record describing one pass.
One source texture enters the stack, named passes run in order, and the last one's target is the
image to present; a stack with no passes presents its input unchanged. Ordering is delegated to
FabricatePassGraph, so passes are addressed by name: a feature can insert before or after a pass
it did not add, and two features that each append a pass compose rather than overwriting one
another.
FabricatePostEffect is what the stack stores: a name, the target the pass renders into, the
module holding its pipeline, and a kind tag. That tag is how FabricateFrameGraph recognises the
two passes with a fixed place in the standard pass order, motion blur and anti-aliasing; every
other pass is named after its effect.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricatePostStack.kt — read the source
FabricatePostStack
Ordered post-processing stack for Fabricate, following Three.js' EffectComposer model: one source texture enters the stack, named passes run in order, and the final pass is the image to present.
FabricatePostEffect
One pass in a FabricatePostStack: its name, output target, optional shader module and kind. Shader effects own a module. Native effects record compute from their target and leave module null. FabricateFrameGraph gives motion blur and anti-aliasing their reserved names in the standard pass order; other effects use their own name. Native effects are excluded from shader inspection and reload.
| Declaration | Summary |
|---|---|
val name: String |
|
val target: RenderToTexture |
|
val module: ModulePointer? |
|
val kind: Kind |
|
enum class Kind |
Which reserved slot in the standard pass order this pass claims, if any. |
FabricateRenderPlan.kt
Render queue data structures and the FabricateScenePlanner that builds them from a FabricateScene.
The planner walks the scene graph, assigns each visible renderable to one of three queues based
on its material's alphaMode (Opaque, AlphaMask, or Transparent), and sorts each queue
for correct draw ordering: opaque and masked items front-to-back by sort order, transparent
items back-to-front by camera distance. Shadow casters are collected from the opaque and masked
sets. FabricateScene.planScene calls the planner; StandardRenderer.Bundle.applyScenePlan
consumes the resulting FabricateRenderPlan.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricateRenderPlan.kt — read the source
Used in: Fabricate Renderer
Top-level functions
| Declaration | Summary |
|---|---|
fun FabricateNode.isEffectivelyVisible(): Boolean |
Top-level values
| Declaration | Summary |
|---|---|
val FabricateMaterial.renderQueue: FabricateRenderQueue |
FabricateRenderItem
One visible renderable node, resolved with its material ID, world transforms, and camera distance.
FabricateRenderPlan
| Declaration | Summary |
|---|---|
val opaque: List<FabricateRenderItem> |
|
val alphaMask: List<FabricateRenderItem> |
|
val transparent: List<FabricateRenderItem> |
|
val shadowCasters: List<FabricateRenderItem> |
|
val materials: List<FabricateMaterialRecord> |
|
val allDrawItems: List<FabricateRenderItem> |
FabricateRenderQueue
enum class FabricateRenderQueue
Constants: Opaque, AlphaMask, Transparent.
FabricateScenePlanner
Builds deterministic opaque/masked/transparent queues from a FabricateScene.
| Declaration | Summary |
|---|---|
fun plan(scene: FabricateScene, cameraPosition: Vector3f? = null): FabricateRenderPlan |
|
fun plan(root: FabricateNode, cameraPosition: Vector3f? = null): FabricateRenderPlan |
FabricateRenderPlanTestMain.kt
src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricateRenderPlanTestMain.kt — read the source
FabricateRenderPlanTestMain
object FabricateRenderPlanTestMain
Pure JVM smoke test for material records and opaque/masked/transparent queue planning.
| Declaration | Summary |
|---|---|
fun main(args: Array<String>) |
FabricateScene.kt
The scene graph: nodes, transforms, materials, lights, animation playback, and the glTF import that fills them.
This file owns no Vulkan resources and imports no device type. A FabricateNode carries a
transform and children; a renderable carries a FabricateMaterial and the intent to be drawn;
updateWorldTransforms composes the hierarchy into world matrices. None of that needs a GPU, so
the scene graph, the animation sampler and the draw-order planning in FabricateRenderPlan.kt
are all exercisable from a plain main, which FabricateSceneGraphTestMain and
FabricateSceneTestMain do without a device.
FabricateTransform is a position/rotation/scale triple rather than a matrix, so handing the
graph a composed Matrix4f is not enough on its own: FabricateTransform.set(Matrix4f)
decomposes it back into the triple. A transform that cannot be expressed as one, such as a
shear, will not survive the round trip.
Lights are a list, and an empty list is not the same as no lighting: it keeps the single default
directional light, so a scene that never touches lights is still lit. One shadow-casting
directional light is treated as the key light and drives the shadow map; the rest reach the
deferred lighting pass through a lights buffer.
FabricateAlphaMode is the field that decides which of the three draw queues a renderable joins,
and it is the same OPAQUE/MASK/BLEND distinction glTF makes, carried through rather than
collapsed.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricateScene.kt — read the source
Used in: Fabricate Renderer, Geometry from disk
Top-level functions
FabricateScene
class FabricateScene(val root: FabricateNode = FabricateNode("root"))
A small Three.js-like scene layer for Fabricate, independent of Vulkan resources: nodes own transforms, renderables own material/rendering intent, and renderer bindings consume the ordered draw lists.
FabricateNode
class FabricateNode(val name: String)
FabricateTransform
| Declaration | Summary |
|---|---|
val position: Vector3f |
|
val rotation: Quaternionf |
|
val scale: Vector3f |
|
var matrixOverride: Matrix4f? |
The exact matrix set was handed, kept only when the TRS triple above cannot reproduce it. |
fun clearMatrix(): FabricateTransform |
Discard matrixOverride and go back to composing the TRS fields. |
fun matrix(): Matrix4f |
|
fun set(matrix: Matrix4f): FabricateTransform |
|
fun set(position: Vector3f, rotation: Quaternionf = Quaternionf(), scale: Vector3f = Vector3f(1f, 1f, 1f)): FabricateTransform |
FabricateAlphaMode
Constants: OPAQUE, MASK, BLEND.
FabricateMaterial
| Declaration | Summary |
|---|---|
val baseColor: Vector4f |
|
var metallic: Float |
|
var roughness: Float |
|
var baseColorTexture: LoadedTexture? |
|
var normalTexture: LoadedTexture? |
|
var metallicRoughnessTexture: LoadedTexture? |
|
var occlusionTexture: LoadedTexture? |
|
var emissiveTexture: LoadedTexture? |
|
var clearcoatTexture: LoadedTexture? |
|
var emissiveFactor: Vector3f |
|
var clearcoatFactor: Float |
|
var clearcoatRoughnessFactor: Float |
|
var uvTransform: Vector4f |
UV offset (xy) and scale (zw), applied to the maps in uvTransformMaps. |
var uvTransformMaps: Int |
Which maps uvTransform applies to, UV_BASE_COLOR … UV_CLEARCOAT; the rest are sampled at their own UVs. |
var alphaMode: FabricateAlphaMode |
|
var alphaCutoff: Float |
|
var doubleSided: Boolean |
|
var receiveShadow: Boolean |
|
var receiveAo: Boolean |
|
var sortOrder: Int |
|
fun transparent(opacity: Float = baseColor.w): FabricateMaterial |
FabricateRenderable
| Declaration | Summary |
|---|---|
val material: FabricateMaterial |
|
var castShadow: Boolean |
|
var boundsCenter: Vector3f |
|
var boundsRadius: Float |
|
var userData: Any? |
|
fun sortKey(node: FabricateNode): Int |
FabricateDraw
| Declaration | Summary |
|---|---|
val node: FabricateNode |
|
val renderable: FabricateRenderable |
|
val worldMatrix: Matrix4f |
|
val worldCenter: Vector3f |
|
val distanceToCamera: Float |
|
val sortKey: Int |
FabricateDrawLists
| Declaration | Summary |
|---|---|
val opaque: List<FabricateDraw> |
|
val transparent: List<FabricateDraw> |
|
val shadowCasters: List<FabricateDraw> |
|
val allVisible: List<FabricateDraw> |
FabricateRenderProfile
| Declaration | Summary |
|---|---|
var ambientOcclusion: Boolean |
|
var aoStrength: Float |
|
var aoRadius: Float |
|
var softShadows: Boolean |
|
var shadowStrength: Float |
|
var shadowSoftness: Float |
|
var motionBlur: Boolean |
|
var antiAlias: Boolean |
|
fun applyTo(bundle: StandardRenderer.Bundle): StandardRenderer.Bundle |
FabricateLightType
Constants: DIRECTIONAL, POINT, SPOT.
FabricateLight
A light source. Directional lights model the sun (parallel rays, no falloff); point lights radiate from a position with inverse-square falloff clamped by range; spot lights add a cone defined by innerConeDegrees/outerConeDegrees. Only one directional light with castShadow can drive the shadow map at a time (the first such light wins); the rest are shaded without shadows.
FabricateRegisteredGltf
| Declaration | Summary |
|---|---|
val nodes: List<FabricateNode> |
|
val opaqueMeshes: List<StandardRenderer.Bundle.PullingMeshPair> |
|
val transparentMeshes: List<StandardRenderer.PullingMesh> |
|
val meshBindings: List<FabricateGltfMeshBinding> |
|
fun animate(time: Float): FabricateRegisteredGltf |
|
val meshes: List<StandardRenderer.Bundle.PullingMeshPair> |
FabricateGltfMeshBinding
| Declaration | Summary |
|---|---|
val index: Int |
|
val node: FabricateNode |
|
val opaque: StandardRenderer.Bundle.PullingMeshPair? |
|
val transparent: StandardRenderer.PullingMesh? |
|
fun applyMaterial(material: FabricateMaterial): FabricateGltfMeshBinding |
FabricateSceneGraphTestMain.kt
src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricateSceneGraphTestMain.kt — read the source
FabricateSceneGraphTestMain
object FabricateSceneGraphTestMain
Pure JVM smoke test for hierarchical transform composition.
| Declaration | Summary |
|---|---|
fun main(args: Array<String>) |
FabricateSceneTestMain.kt
src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricateSceneTestMain.kt — read the source
FabricateSceneTestMain
JVM smoke test for Fabricate's pure scene graph: transform hierarchy, animation playback, and opaque/transparent draw ordering. This intentionally avoids a Vulkan device.
| Declaration | Summary |
|---|---|
fun main(args: Array<String>) |
FabricateShaderBoxes.kt
Expose Fabricate's built-in shaders the same way user-authored shaders work: each pass becomes an
editable box with vertex/fragment GLSL tabs, live introspection, and alt r hot reload.
Nothing here is new low-level machinery — it reuses Box.attachShader, which installs the reload
actions, copies the module's source into the box's editable properties, and (through reloadShader)
rebuilds by copying the module's existing Parameters, so the renderer's own textureResolver,
attachment formats, and push-constant seeding survive every edit. Only the GLSL source changes.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricateShaderBoxes.kt — read the source
Used in: Fabricate Renderer
Top-level functions
| Declaration | Summary |
|---|---|
fun StandardRenderer.Bundle.shaderModules(): LinkedHashMap<String, ModulePointer> |
All built-in graphics ModulePointers in this bundle, keyed by a stable pass name. Includes the deferred passes, the post-stack effects (motion blur, FXAA, plus any tone-map/bloom/user effects added so far), and any per-material textured g-buffer variants registered by glTF import. Modules are de-duplicated by identity, so passes that also appear in the post stack are listed once. |
fun StandardRenderer.Bundle.shaderSources(): LinkedHashMap<String, Pair<String, String>> |
CPU-only view of the built-in shader sources, for inspection/diffing without a session or GPU. Returns pass name -> (vertex source, fragment source). |
fun StandardRenderer.Bundle.bindShadersToBoxes(namePrefix: String = "fabricate", root: SessionInformation = thisSessionInfo.get(), origin: Rect = Rect(40.0, 40.0, 340.0, 200.0), columns: Int = 3, gap: Double = 24.0): LinkedHashMap<String, Box> |
Bind every built-in shader in this bundle to its own editable box (see shaderModules). Each box is named "$namePrefix/$passName", gets the pass's GLSL in its vertex/fragment tabs, and can be edited and hot-reloaded with alt r (or ABI-preserving alt shift enter) like any user shader. |
fun StandardRenderer.Bundle.bindShaderToBox(name: String, module: ModulePointer, namePrefix: String = "fabricate", root: SessionInformation = thisSessionInfo.get(), frame: Rect = Rect(40.0, 40.0, 340.0, 200.0)): Box |
Bind a single built-in module to a named box. Useful when you only want to hack on one pass, e.g. bundle.bindShaderToBox("std-lighting", bundle.lightingModule). |
GaussianSplatRenderer.kt
Rendering a Gaussian splat capture: PLY reading, packing, depth sorting and the built-in splat shaders.
Splats are not triangles and the differences drive the whole file. Each one is an anisotropic
Gaussian of sixteen floats (position, a covariance, colour and opacity) drawn as a
screen-aligned quad whose falloff the fragment shader evaluates, and the result is only correct
if the quads arrive in depth order, because alpha blending does not commute. So a frame here is
a sort followed by a draw, and most of the machinery below exists to make the sort affordable:
SortMode.ExactRadix runs a GPU radix sort over per-splat depth keys, DepthBuckets trades
exactness for a cheaper bucketed approximation, and None builds none of it, which is the right
answer when the data was written back-to-front already or the blend is order-independent.
A Cloud is sized by capacity rather than by the data it was built from, so one cloud can be
rewritten every frame from different files. That is what makes a splat sequence playable at
all, with the frame cache on the disk side in SplatSequence.kt, and it is why a
fifteen-source frame is one upload into one pre-sized buffer instead of fifteen allocations.
configureModule sets the raster state the built-in shaders need and does not set the shader
source, because a box using moduleHere owns its vertex and fragment text through its own
properties. simpleGaussianSplatSupport.kt uses that to hand a box an editable copy of the
shaders.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/GaussianSplatRenderer.kt — read the source
Top-level functions
GaussianSplatRenderer
PathTraceEmitterGpuSmoke.kt
Real GPU checks for PathTracingRenderer.emitterSampling, the path tracer's direct sampling of
emissive triangles: four scenes, each measured against a number that exists outside this file.
Unbiasedness is checked before variance. A variance win from a biased estimator reduces noise and changes the image, which is what a wrong area-to-solid-angle Jacobian, a missing cosine and a double-counted emission arm all do as well, so the noise reduction on its own says nothing about which of the two happened.
Which scene a check uses decides what it can resolve. A small bright lamp in the dark is where BSDF sampling barely works, so its own converged image stays noisy, and a reference that noisy cannot resolve a two-percent bias at any tolerance. Three of the four arms below are therefore uniform-radiance scenes, where both sides converge and the comparison is sharp:
inertWithoutEmitters— a scene with nothing emissive in it, where on and off must agree bit for bit. Which means the two runs have to stop on the same sample, and a polled sample count cannot be made to land there, so the second run targets whatever the first reached and the pair is retried until they match.enclosureAgainstEnvironment— a sphere inside a uniformly emissive cube, one bounce. Every direction carries radiance one, so this is the same integral as the same sphere under a constant unit environment map, which the tracer solves through a separate, independently checked next-event path. One bounce means neither run applies an MIS weight at all, so what is under test is the new pdf alone: its normalisation and its Jacobian. Both furnace means are also asserted against 1.0, so the arm is absolute and not a self-comparison.enclosureComposition— the same enclosure with six differently coloured faces and two bounces, emitter sampling on against off. Both arms fire and both MIS weights matter, and a selection probability that has come off its triangle tints the answer. Uniform near-field radiance again means the off run converges, so this resolves a fraction of a percent.- the lamp box, driven from
run— two small lamps in the dark, which is the scene the variance claim is measured in. Its bias residual is reported at two reference budgets: a residual that falls like one over the square root of the budget is the reference's own noise, and one that holds is bias. That decay is asserted, because a fixed tolerance there would be a tolerance on noise.
The lamp box is built around the three failures the uniform scenes cannot show. Its two lamps differ in size, colour and power, so a table whose probability and triangle index have come apart shows as a colour shift rather than as noise. One of them is one-sided and faces down, because a back face culled on a BSDF hit has to be dark to the direct sampler too. And a sphere sits on the floor, where its shadow is a shadow ray: the integrator's occlusion query stops a thousandth short of the sampled point, so that a lamp cannot occlude its own connection, and a query that ran the whole way would discard every direct contribution and leave a correct image with no variance improvement in it. The RMSE ratio asserted here is what would notice.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceEmitterGpuSmoke.kt — read the source
PathTraceEmitterGpuSmoke
object PathTraceEmitterGpuSmoke
Four arms measured at 96x72 on each enabled backend, then three figures at 480x360. Each arm prints a PASS line naming the population it measured. Run through RayTracingGpuAudit's --stage=emitters or dev_docs/agents/demos/path_tracing_emitters/verify.yaml in a live box.
| Declaration | Summary |
|---|---|
suspend fun run(renderer: Renderer, outputDirectory: String) |
Drives every check and writes the before/after captures under outputDirectory. Call from a suspend body in a box below a live renderer, as with the other path-trace smokes. |
PathTraceFrameOutput.kt
The path tracer's per-frame image set, for reconstruction rather than for display.
PathTracingRenderer normally accumulates samples into one progressively cleaner picture. A
denoiser wants the opposite: this frame's noisy radiance on its own, together with the buffers
that say what each pixel is looking at. enableFrameOutput on the tracer builds a
PathTraceFrameOutput, which converts the packed per-pixel records the tracer writes into
seven attachments: radiance, depth, motion, diffuse and specular albedo, normal and roughness,
and specular hit distance. inputs and guides hand those over in the shape
ReconstructionInputs and RayReconstructionGuides expect.
The images stay on the device and nothing is read back, so a consumer is a Target recorded
after the tracer's own render. Opaque materials only, which is why the tracer refuses the call
on a scene with glass in it.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceFrameOutput.kt — read the source
PathTraceFrameOutput
GPU-resident output of a single path-tracing dispatch, before progressive accumulation. Six vec4s per pixel (96 bytes). No readback or native library is needed to produce these images. The tracer owns this object; consumers borrow the images until the tracer closes. Jitter is projection displacement, positive right/down: primary rays sample pixel-jitter. Depth is RH perspective Vulkan [0,1], clear=1, using near/far. Motion is previous-current in input pixels without jitter; geometry is the tracer's immutable scene snapshot.
| Declaration | Summary |
|---|---|
val near |
|
val far |
|
var jitter |
|
val images |
|
fun inputs() |
|
fun guides() |
|
override fun render(command: CommandBuffer,index: Int) |
Record after tracer.render, before any reconstruction consumer. |
override fun close() |
PathTraceGlassDemo.kt
All four of the path tracer's glass materials in one scene: clear, absorbing, frosted, and a thin pane.
public/docs/path_tracing.md prints this scene as its glass figure and
dev_docs/agents/transcripts/path_tracing_glass.yaml launches it, so what the file builds is
what those two are written about. Left to right: a sphere of PathTraceMaterial.glass(), one
given an attenuationColor, one given a roughness of 0.32, and a quad of thinGlass.
Most of the geometry here is background, since what a transparent material shows is mostly
whatever is behind it. The floor is a checkerboard of 0.55-unit tiles and the wall behind the
spheres is eighteen horizontal stripes, both built from the same two diffuse greys, one ten times
the reflectance of the other. Each material moves that pattern its own way: refraction displaces
the stripes, absorption tints them by how far the ray travelled inside the solid, roughness
scatters them, and the thin pane leaves their positions where they are, because thinGlass
transmits per crossing and has no thickness to refract across.
The returned renderer is set to ten bounces rather than the default five, because a ray that
enters and leaves a sphere has spent two of them before reaching anything else, and to an
exposure of 1.5 for a floor this dark. With no environmentPath the light is a constant white
environment of radiance one; a path is read by PathTraceEnvironment.fromFile, which takes EXR,
Radiance and ordinary image files.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceGlassDemo.kt — read the source
PathTraceGlassDemo
Clear glass, absorbing green glass, frosted glass and a thin blue pane, from left to right.
| Declaration | Summary |
|---|---|
fun install(renderer: Renderer, environmentPath: String? = null, width: Int = 640, height: Int = 360): PathTracingRenderer |
Builds the scene, points a camera at it and installs a PathTracingRenderer under renderer, returning it so a box can blitToScreen or reconfigure it. |
PathTraceGltf.kt
glTF and GLB into a PathTraceScene: hierarchy baked to world space, PBR parameters mapped onto
PathTraceMaterial, and an extension check that refuses rather than renders the wrong thing.
The tracer has no scene graph and holds world-space triangles, so this importer flattens the
glTF node hierarchy as it reads, composing each node's transform with the caller's. Textures are
copied out and every Assimp resource is released before the call returns, which is why no
renderer or device is involved and an import is testable from a plain main.
Two behaviours are easier to find here than to rediscover from a wrong picture. The first is that
the extension set is read from the JSON directly, ahead of Assimp: an extension in
extensionsRequired that this importer does not implement fails the call, while one in
extensionsUsed produces a warning and an import of the base material. Declining to load is the
right answer for the former because a required extension means the asset is wrong without it.
The second is that the import is all-or-nothing: geometry accumulates into a staging scene and
is appended only on success, so a failure leaves the target scene exactly as it was rather than
half-populated.
Colour space is the detail that most often looks like a renderer bug. Base colour and emission
textures are sRGB and are decoded; metallic-roughness and normal maps are linear data and are
not. Vertex colours and the base colour factor multiply after decoding, alpha is never squared,
and PathTraceGltfGpuSmoke pins all of it against known byte values through real GPU traversal.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceGltf.kt — read the source
Top-level functions
| Declaration | Summary |
|---|---|
fun PathTraceScene.importGltf(path: String, transform: Matrix4f = Matrix4f(), material: (name: String, value: PathTraceMaterial) -> PathTraceMaterial = { _, value -> value }, warning: (String) -> Unit = { println("glTF: $it") }, pose: GltfSkinPose? = null): PathTraceGltfImport |
Append a static glTF/GLB scene, baking hierarchy and transform into world-space geometry. Copies textures and releases Assimp resources before returning; no renderer is required. material can replace or amend imported PBR parameters, for example to choose solid glass. An import failure leaves this scene unchanged. |
GltfSkinPose
data class GltfSkinPose(val clip: String? = null, val seconds: Float = 0f)
Which pose a skinned mesh is traced in.
PathTraceGltfImport
Details of one static import. Material IDs also select materials in shadeMaterial().
| Declaration | Summary |
|---|---|
val boundsMin: Vector3f, val boundsMax: Vector3f, val warnings: List<String>) |
PathTraceGltfGpuSmoke.kt
src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceGltfGpuSmoke.kt — read the source
PathTraceGltfGpuSmoke
Known texture values checked through actual GPU traversal, material evaluation and accumulation.
| Declaration | Summary |
|---|---|
suspend fun run(renderer: Renderer, backend: PathTracingRenderer.Backend) |
PathTraceGltfTestMain.kt
src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceGltfTestMain.kt — read the source
PathTraceGltfTestMain
| Declaration | Summary |
|---|---|
fun main(args: Array<String>) |
PathTraceGpuSmoke.kt
GPU smoke tests and a demo entry point for PathTracingRenderer.
PathTraceGpuSmoke.run sets up known scenes (a constant HDR environment, emissive quads, a
white-furnace diffuse surface, and glass slabs) and reads back linear pixels from real GPU
dispatches to assert accumulation behaviour, lighting and camera resets, HDR material
correctness, Beer-Lambert attenuation, Fresnel, indirect clamping, and transparent-background
compositing. Where the device supports ray queries, both the compute-BVH and ray-query backends
run, and their pixel outputs are compared.
PathTraceGpuSmoke.runCamera covers the thin-lens model: pinhole sharpness, aperture blur,
focus-distance changes, pixel jitter, and accumulation invalidation on target or parameter change.
PathTraceDemo.install builds a three-sphere scene (diffuse, metallic, rough metal over a floor)
and returns a live PathTracingRenderer for interactive use from a box.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceGpuSmoke.kt — read the source
Used in: Progressive path tracing
PathTraceGpuSmoke
Real GPU readback assertions. Run from a Field box below a live renderer; no placeholder captures.
| Declaration | Summary |
|---|---|
suspend fun runSeams(renderer: Renderer) |
A closed quad must have no pinholes along either diagonal, from either side. The camera-centred rays land exactly on shared edges where u/v rounding used to miss both triangles on NVIDIA. Black misses make this visible without temporal reuse. |
suspend fun run(renderer: Renderer) |
|
suspend fun runCamera(renderer: Renderer) |
Thin-lens focus, coverage, jitter, and automatic reset checks on real GPU pixels. |
suspend fun awaitSamples(tracer: PathTracingRenderer, count: Int) |
PathTraceDemo
Small reusable material study; the environment file is optional, with a constant white map as fallback.
PathTraceInstrument.kt
The path tracer's instrument: what it is doing, why it restarted, and how fast it is converging.
Answering any of that from a box by hand means a frames { repeat(180) { frame() } } loop with a
println inside: it costs frames of its own, it misses a reset that lands between frame
boundaries, and it cannot see the camera basis the tracer actually built. What is here instead is
a .h printer, a .h2 drill-down and PathTraceConvergence, which the tracer appends to from
its own render. Recompiling the integrator in place, the other thing such a loop gets written
for, is PathTracingRenderer.reloadIntegrator.
The two renderings
registerPathTraceInstrument gives PathTracingRenderer both an afield.ui.visualization.registerValuePrinter
and an afield.ui.visualization.registerH2Value entry, because the class is not a data class and reflection alone
serves the two panels unevenly. H2Reflect reflects any type, so .h2 reaches every public
tuning property without help; .h with no printer falls through to ValueVisualizer's
formatFallbackObject, which prints the class name and an identity hash and nothing else — while
the Field 4 types the tracer is built out of (Renderer, Camera,
ComputeModulePointer, RenderToTexture, VulkanBuffer, …) all have printers in
FourVisualizers.kt.
So the two renderings do different jobs, and the split is why the .h panel stays short:
.his the glance. One header line and a handful of rows, with the rows that are at their defaults dropped — so a plain tracer shows five rows, and each of the five optional groups (a non-agreeing up, a lens, temporal reuse, fog, an attached convergence log) costs one more, to a maximum of ten. It is meant to fit in the gap above the next box..h2is the drill-down. A strict superset of what reflection reaches on its own: the same values, grouped rather than flat, with the derived facts beside them and the sub-objects still reachable so each one lands on its own printer.
The up vector the tracer actually uses
The tracer does not use camera.up. updateFrame builds its own up as right × forward, i.e.
the part of the declared up that is perpendicular to the view direction, normalised, and that is
what reaches the Frame uniform. A declared up of (0, 3, 0), or one tilted 20° out of the view
plane, gives a different vector from the one the image is built with, and no property tree can
show it, because the value is a local inside a private method. PathTraceCameraBasis computes
both and reports the angle between them, and the .h row appears only when they disagree.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceInstrument.kt — read the source
Used in: Progressive path tracing
Top-level functions
| Declaration | Summary |
|---|---|
fun PathTracingRenderer.status(): PathTraceStatus |
Read this tracer's PathTraceStatus now. |
fun PathTracingRenderer.recordConvergence(maxPoints: Int = 4096): PathTraceConvergence |
Start logging convergence on this tracer, replacing any existing log, and hand it back. |
fun Box.showConvergence(log: PathTraceConvergence, title: String? = "path tracer convergence", channel: String = "output", marks: Int = 12): Plot? |
Plot a PathTraceConvergence — samples against wall seconds, with a labelled dashed rule wherever accumulation restarted and why. |
fun registerPathTraceInstrument() |
Register both renderings. Discovered by di.kt's ClassGraph scan of @RunsAt, like registerFourVisualizers and registerH2DomainAdapters. |
PathTraceCameraBasis
The camera as the tracer resolves it, declared and effective side by side.
| Declaration | Summary |
|---|---|
val at: Vector3f |
|
val target: Vector3f |
|
val declaredUp: Vector3f |
|
val effectiveUp: Vector3f |
|
val right: Vector3f |
|
val forward: Vector3f |
|
val fovRadians: Double |
|
val distanceToTarget: Float |
|
val upTiltDegrees: Double |
Angle between declaredUp and effectiveUp, in degrees, or NaN when either is degenerate. |
val upAgrees: Boolean |
Whether the declared up is the one the image is built with, to within a twentieth of a degree. |
PathTraceConvergenceSummary
Whatever a PathTraceConvergence has recorded, reduced to the five numbers worth a row.
| Declaration | Summary |
|---|---|
val points: Int |
|
val seconds: Double |
|
val samples: Int |
|
val resets: Int |
|
val samplesPerSecond: Double |
Fresh samples per wall second over the recorded window, or NaN when nothing has elapsed. |
PathTraceStatus
An immutable snapshot of everything the instrument shows, read once off a live tracer.
PathTraceConvergence
class PathTraceConvergence(val maxPoints: Int = 4096)
A per-frame convergence log: samples against wall time, plus every reset and why it happened.
| Declaration | Summary |
|---|---|
val seconds: List<Double> |
Wall seconds since the first recorded frame, one per retained point. |
val samples: List<Int> |
Fresh samples per pixel, one per retained point, aligned with seconds. |
val points: Int |
How many points are retained. Less than the number of frames once decimate has run. |
val framesPerPoint: Int |
The sampling stride: one point per this many frames. Doubles each time the log fills. |
fun resetMarks(limit: Int = 12): List<Pair<String, Double>> |
Reset marks as "reason" to seconds, the shape showSignals wants for marks. |
val resets: List<Pair<String, Double>> |
Every reset kept, newest last, as "reason" to seconds. resetMarks is the plottable form. |
val summary: PathTraceConvergenceSummary? |
Five numbers for the instrument, or null before the second point (one point spans no time). |
fun clear() |
Forget everything and start again, keeping the attachment. |
override fun toString(): String |
IntegratorReload
What PathTracingRenderer.reloadIntegrator did: three outcomes, not a boolean.
| Declaration | Summary |
|---|---|
val status: Status |
|
val sourceLength: Int |
Characters of source offered. Not the source itself: an integrator is ~15 KB and this ends up in logs. |
val errors: List<String> |
|
enum class Status |
|
val reloaded: Boolean |
True only for Status.RELOADED — i.e. the image is about to change. |
val ok: Boolean |
True unless it failed to compile, so check(result.ok) does not fire on a no-op. |
override fun toString(): String |
PathTraceMediumGpuSmoke.kt
Real GPU checks for the path tracer's participating medium: PathTracingRenderer.mediumDensity
and everything that is sampled along a ray rather than at a surface.
A medium adds three probability densities to an integrator: a distance density, a phase function, and the transmittance that multiplies a light connection. Each has a failure that darkens or brightens the image without otherwise distorting it, so a render cannot be told from a correct one by looking at it — a distance pdf missing its extinction factor, a phase function normalised to 1 instead of 1/4π, a transmittance applied twice, once analytically on the connection and once through the collision event that already estimated it. So each arm is checked against a number that exists independently of this code:
inertWithoutExtinction— extinction zero, with albedo, anisotropy and bounds all set to something conspicuous, must reach the previous integrator bit for bit. The distance sample has no Sobol dimension of its own, it shares the BSDF lobe selector's and the escape branch remaps it back to uniform, so "the medium is off" and "the sample stream is untouched" are two different claims and this is the second one.absorptionAgainstClosedForm— the absolute anchor. A purely absorbing medium (albedo zero) in front of an emissive plane must attenuate by exactlyexp(-σt·d), per pixel, withdthe camera-to-plane distance. That is a closed form, not a converged reference, so no common-mode error can hide in it. Run at two densities an order of magnitude apart in transmittance, and in a bounded configuration too, where the exponent is the slab-crossing length and what is under test is the shader's ray-to-bounds clip rather than the exponential.absorptionAgainstDepthFog— the same scene and the same closed form, reached throughfogDensity, which computes its ownexp(-σ·primaryDepth)and shares nothing with the medium but the scene. It is also the arm that tests the closed form itself: the analyticdis a Kotlin reconstruction of the ray the shader builds, and a wrong reconstruction fails here instead of agreeing with itself.scatteringFurnace— energy conservation, asserted at 1.0. An albedo-one medium embedded in a uniform unit radiance field is in exact equilibrium, sinceL = 1everywhere solves the transport equation. The scene has no geometry in it, so no BSDF, no area pdf and no surface reflectance can contribute to the answer or account for it. Repeated across six anisotropies, which extends it from a normalisation check to a check thatPathTracingRenderer.mediumAnisotropy's density and its sampling routine describe the same distribution: were they to disagree, the phase arm's implicit weight of one would be wrong and the furnace would not close.furnaceBounceSweep— the same furnace at four times the density, where the truncation deficit is large enough to characterise. The mean rises monotonically with the bounce budget and never passes 1.0, which is the signature of truncation; a normalisation error would sit at a fixed offset and a double-counted arm would overshoot.singleScatteringAgainstQuadrature— the arm that shares no code and no scene with anything else. A slab of medium under a constant environment, one bounce, is a one-dimensional integral of the radiative transfer equation that a Kotlin quadrature can evaluate to eight figures. It pins the distance pdf, the albedo weight, the phase normalisation and the transmittance on the environment connection simultaneously and absolutely, against numerics written from the equation rather than from the shader.emitterConnectionIsInvisible— the cross-mechanism arm for the one mechanism the furnace and the quadrature cannot reach: the connection from a scattering point to emissive geometry, with its area-to-solid-angle Jacobian and its own transmittance. A uniformly emissive enclosure is a radiance field in which an albedo-one medium is invisible, so the reference is the same scene rendered with the medium off, and the medium's emitter pdf appears in one of the two arms only. That premise is exact only for a non-reflecting emitter, and every dielectric this BSDF can express returns a few percent of what it is lit by, so the arm runs three configurations — a black metal, a dielectric, and the dielectric pair with the connection under test switched off in both — and ends in a bound on that connection's error rather than in an identity.lampInFog— two small lamps in fog, which is the scene the variance claim is measured in. Its bias residual is reported at two reference budgets, because a fixed tolerance on a residual is a tolerance on noise; and its variance ratio is asserted at a floor, not printed.
Every arm then runs again on each traversal backend the device exposes. The medium touches no
traversal code and the analytic anchors are backend-independent, so a backend that misses them
when the other does not has put the disagreement in traversal rather than in the estimator, which
is a distinction no single pass can draw. Two further arms render the figures, and the
numbers are written to a report.txt per backend after every arm as well as printed, because a
box's println reaches the server log through a buffer a backdoor teardown does not wait for.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceMediumGpuSmoke.kt — read the source
PathTraceMediumGpuSmoke
object PathTraceMediumGpuSmoke
Eight arms measured at 96x72 and two that render figures, all repeated on each backend. A live renderer is required throughout, so this carries no FIELD_TEST annotation and is driven from a box instead: dev_docs/agents/demos/path_tracing_medium/verify.yaml is the transcript that does it.
| Declaration | Summary |
|---|---|
fun godRays(): PathTraceScene |
The demo scene, kept here so its picture is rendered by the same code that measures everything else: a bright strip light with four slats under it, the whole room hazed, viewed down the axis everything is extruded along so the shafts are seen in cross-section. |
fun godRaysCamera(renderer: Renderer) |
Level, and square on to the x-y plane the shafts fan out in, so the slats are seen nearly edge-on and the shafts across rather than down their length. |
val GOD_RAYS_BOUNDS |
Bounds for godRays, in the form mediumBounds takes: the floor's full width, the z extent inset a tenth of a unit from the ends the room is extruded to, and a ceiling at 5.2, above the strip light and below the top of the back wall. |
const val GOD_RAYS_DENSITY |
The demo's extinction, and a figure that wants a medium you can see through rather than the densest one the integrator handles. At 0.30 the haze is about two and a third optical depths along the view and obscures itself, so the wall, the floor and the slats' shadow wedges wash into one grey. |
suspend fun run(renderer: Renderer, outputDirectory: String) |
Every arm, on every backend this device exposes. |
PathTraceScene.kt
What the path tracer traces: triangles, materials, textures and an environment map, all as plain Kotlin with no device attached.
Nothing in this file needs a GPU, which is why it is separate from
PathTracingRenderer.kt. A scene is assembled by calling triangle, quad, mesh or sphere,
each of which appends to flat lists and returns the scene so calls chain; material and
texture intern a value and hand back the integer index the triangles refer to it by. pack
then flattens the result into the float and int arrays the shader expects, and building a BVH
over them is the renderer's job, not this file's. The consequence for a test is that scene
construction, indexing and packing are all checkable headlessly, which is what
PathTraceTestMain does.
PathTraceMaterial is linear-light throughout, with no sRGB anywhere in the struct, and carries
the transmission parameters as well as the metallic-roughness ones, so glass is a material rather
than a separate path. It carries one geometric precondition, and breaking it shows up as a dark
result: solid (non-thin-walled) glass assumes closed, outward-wound, non-overlapping meshes,
because the attenuation distance is measured between surface crossings.
PathTraceEnvironment is both the sky and, usually, the main light. It builds the importance
distribution the sampler draws from, which is why an environment is a snapshot like the geometry
is: changing it changes a precomputed table. PathTraceTexture decodes through stb_image and
TinyEXR and keeps its pixels linear, sRGB-decoding base colour while leaving
metallic-roughness alone. That asymmetry is what glTF requires, and PathTraceGltfGpuSmoke
checks it against known byte values.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceScene.kt — read the source
Used in: Progressive path tracing, A polytope, cut open, and traced
PathTraceMaterial
Linear-light surface materials. Solid glass requires closed, outward-wound, non-overlapping meshes.
| Declaration | Summary |
|---|---|
val red: Float |
|
val metallic: Float |
|
val emissionRed: Float |
|
val transmission: Boolean |
|
val attenuationRed: Float |
|
val attenuationDistance: Float |
|
val baseColorTexture: Int |
|
val emissionTexture: Int |
|
val opacity: Float |
|
val alphaMode: AlphaMode |
|
val normalScale: Float |
|
enum class AlphaMode |
PathTraceScene
Build a static world-space triangle scene, then pass it to PathTracingRenderer.
PathTraceEnvironment
class PathTraceEnvironment(val width: Int, val height: Int, rgba: FloatArray)
Linear RGBA equirectangular image, top row = north pole; longitude atan2(z,x).
PathTraceTexture
CPU-owned linear RGBA texture. Wrap modes: 0 repeat, 1 clamp, 2 mirrored repeat.
| Declaration | Summary |
|---|---|
val wrapU: Int |
PathTraceShaderBoxes.kt
pathTracingHere: a path tracer whose material function is the box's own editable compute
shader, authored and hot-reloaded exactly like any other Field shader.
The integrator, trace.comp, calls one GLSL function at every surface hit,
shadeMaterial(Material, PathTraceSurface), and the packaged copy of it returns its argument
unchanged. This file puts an editable copy of that function in the box, so a material is written
where shader code is normally written and reloads through the ordinary Reload Compute Shader
action. Replacing the whole integrator instead is the other direction, and that is
PathTracingRenderer.reloadIntegrator.
Two comment markers in the packaged source are what make that possible. pathTraceTemplates
cuts it at // MATERIAL_BEGIN and // MATERIAL_END and puts the halves into the box's
shaderTemplate map as pathTracing.preamble and pathTracing.integrator, so the shader the
macro seeds is those two template slots with an identity shadeMaterial between them to edit.
Everything such a function needs is already in the preamble: the Material and
PathTraceSurface structs, the frame uniform, and the buffer references the scene is read
through.
The material shader is compiled twice, once at expansion and once on the first frame, and which
source each of those sees is the thing to understand before reading _pathTracingHere. A macro
compiles at expansion time, because the reflected interface is what the generated Kotlin
accessors are built from, and at that moment no renderer has been resolved and no device asked
whether it supports ray queries. So the templates are seeded from the portable integrator, whose
reflection differs from the ray-query one only by an optional acceleration-structure binding;
then _pathTracingHere re-seeds them for the device it actually found and edits the
HARDWARE_QUERY define in the already-seeded _compute source, rather than recompiling through
the macro. Every later reload goes through the normal Field path and gets the device-specific
template.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceShaderBoxes.kt — read the source
Top-level functions
PathTraceTemporalGpuSmoke.kt
src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceTemporalGpuSmoke.kt — read the source
PathTraceTemporalGpuSmoke
object PathTraceTemporalGpuSmoke
Real GPU history/visibility checks, including a measured moving-camera noise comparison.
| Declaration | Summary |
|---|---|
suspend fun runFog(renderer: Renderer) |
|
suspend fun run(renderer: Renderer, verifyKeyboard: Boolean = true) |
PathTraceTestMain.kt
src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceTestMain.kt — read the source
PathTraceTestMain
Pure scene/importance-distribution contracts plus compilation/reflection of BOTH GPU variants.
| Declaration | Summary |
|---|---|
fun main(args: Array<String>) |
PathTraceTriMesh.kt
TriMesh into PathTraceScene, so that anything in Field which produces a mesh can be traced.
PathTraceScene.mesh already takes indexed positions, so the arithmetic here is small. The
difference is what it does with the triangles a real producer emits and
PathTraceScene.triangle rejects: zero-area slivers where two vertices coincide, and vertex
normals that came out of a degenerate face as zero-length. A Conway operator chain, a
marching-cubes surface, a Manifold Boolean and a cloth simulation all produce some, and one of
them aborts the whole append. Here they are counted and skipped, and the count comes back in
PathTraceTriMeshImport.degenerateCount.
The producers whose output lands here:
| source | route |
|---|---|
| a polytope, including Conway chains | afield.geometry.polytope.triangulate .mesh |
| a Manifold solid, after Booleans | afield.geometry.solid.manifold.Solid.toTriMesh |
| a scalar field | afield.geometry.mesh.marchingCubes, or afield.geometry.solid.manifold.Solid.levelSet |
| a mesh off disk | afield.geometry.mesh.readObj and the rest of mesh_io.kt |
| a heightfield, a tube, a revolve | mesh_generate.kt |
Normals decide whether it reads as faceted or smooth
TriMesh.normals is optional, and when it is absent every triangle gets its own face normal, so
the surface reads faceted. That is the right look for a polyhedron and the wrong one for a
sphere. Both Manifold and Field's polytope triangulator can supply either: toTriMesh(normals =
true) and triangulate(normals = NormalPolicy.Crease(30.0)) split a vertex per face at a hard
edge and average across a soft one.
Splitting vertices for shading also breaks index-sharing, so the mesh that shades well is not the
one to measure topology on. Take two: the split mesh for the scene, the plain one for
afield.geometry.mesh.genus and afield.geometry.mesh.isWatertight.
A material per triangle
The overload taking a (triangle: Int) -> Int selector is handed each triangle's index in mesh.
That numbering does not close up over a skipped triangle, so it stays lined up with the
per-triangle side tables a producer hands
out: afield.geometry.polytope.TriangulatedGeometry.sourceFace names the polygon a triangle came
from, and its triangleAttributes carry whatever the triangulator was asked to record. So a
Conway chain's lineage can pick the material, and a face's provenance becomes something you can
see in the render.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceTriMesh.kt — read the source
Top-level functions
| Declaration | Summary |
|---|---|
fun PathTraceScene.importTriMesh(mesh: TriMesh, material: Int, transform: Matrix4f = Matrix4f(), vertexColors: Boolean = false, warning: (String) -> Unit = { println("TriMesh: $it") }): PathTraceTriMeshImport |
Append mesh to this scene under material, baking transform into world-space coordinates. |
fun PathTraceScene.importTriMesh(mesh: TriMesh, material: (triangle: Int) -> Int, transform: Matrix4f = Matrix4f(), vertexColors: Boolean = false, warning: (String) -> Unit = { println("TriMesh: $it") }): PathTraceTriMeshImport |
importTriMesh with the material chosen per triangle. |
PathTraceTriMeshImport
What one importTriMesh call did: the triangles that reached the scene, and the ones that did not.
| Declaration | Summary |
|---|---|
val triangleCount: Int |
Triangles appended to the scene. |
val degenerateCount: Int |
Triangles skipped for zero area or a non-finite coordinate. |
val boundsMin: Vector3f |
Corner of the appended geometry's world-space bounding box. Both corners are zero when nothing was appended. |
val boundsMax: Vector3f |
The opposite corner. |
val warnings: List<String> |
One line per distinct reason something was skipped or substituted. |
val size: Vector3f |
Extent of the appended geometry, which is what a camera distance is usually derived from. |
PathTracingRenderer.kt
The path tracer itself: BVH upload, the two traversal backends, accumulation, and the history reprojection that keeps an image usable while a camera moves.
This is the device-facing half of the path tracing stack, and the only file in it that owns
Vulkan resources. The scene it traces is built elsewhere, by PathTraceScene.kt, which holds
the geometry, materials and environment as plain arrays, and PathTraceGltf.kt, which fills
them from a glTF file. It arrives here as an immutable snapshot that is packed into device
buffers once. That split is why editing a material means constructing a new tracer: there is no
path from a changed Kotlin value back into the buffers the shader reads.
Two traversal backends compile from the same source. COMPUTE_BVH walks a stackless software
BVH and runs on any Vulkan device Field supports, MoltenVK included; RAY_QUERY uses
VK_KHR_ray_query hardware traversal where the device advertises it. They share the lighting,
material evaluation and accumulation code, so an image should not depend on which one ran, and
PathTraceGpuSmoke checks that it does not.
The output is a fixed-resolution linear HDR render target rather than something presentable.
Exposure and Reinhard tone mapping happen in a separate blitToScreen pass, which keeps the
accumulated radiance readable: readLinear is what the smoke tests measure, and a tone-mapped
buffer would have nothing useful in it.
For the same material with pictures, see public/docs/path_tracing.md.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTracingRenderer.kt — read the source
PathTracingRenderer
Progressive GPU path tracer: stackless software BVH on every Field Vulkan device, optional KHR ray queries. Construct/install/close on Field's render thread, like other Field 4 modules. Scene and environment are snapshots; recreate the tracer to edit them. Camera motion resets or optionally reprojects history; lighting edits always reset accumulation. Output is a fixed-resolution linear HDR RTT. Call blitToScreen separately for exposure + Reinhard presentation. Installing under the same name retires the previous tracer; no coroutine lifetime bookkeeping is needed.
| Declaration | Summary |
|---|---|
val renderer: Renderer |
|
val width: Int |
|
val height: Int |
|
var camera: Camera |
|
val name: String |
|
enum class Backend |
|
val backend: Backend |
|
var samplesPerFrame: Int |
|
var pixelJitter: Float |
Subpixel sample width in pixels: one covers the pixel, zero always samples its centre. |
var apertureRadius: Float |
Circular thin-lens radius in world units. Zero gives a pinhole camera with everything in focus. |
var focusDistance: Float? |
Focus-plane distance along camera.forward in world units; null follows camera.target. |
var temporalReuse: Boolean |
Reproject compatible surface history on camera motion. Approximate, bounded, and off by default. |
var temporalMaxSamples: Int |
Maximum old samples carried across a camera move; glossy surfaces or a changing view may retain fewer. |
var temporalMotionSensitivity: Float |
Camera-angle penalty: one uses conservative view rejection; zero ignores view-angle changes. |
var temporalRoughnessSensitivity: Float |
Roughness penalty: one rejects polished surfaces and reduces glossy history; zero removes both restrictions. |
var temporalMaxBlurRadius: Float |
Largest estimated defocus radius in pixels allowed to retain surface history during motion. |
var fogDensity: Float |
Exponential depth-fog density per world unit. Zero disables fog. |
var fogStartDistance: Float |
Clear distance from the camera, in world units, before depth fog starts. |
var fogColor: Vector3f |
Linear HDR fog colour; returned by copy. Depth fog preserves image alpha. |
var fogBackground: Boolean |
Treat background misses as infinitely distant when fog is enabled; transparent backgrounds stay transparent. |
var mediumDensity: Float |
Extinction of the participating medium, per world unit. Zero disables it, and is the default. |
var mediumAlbedo: Vector3f |
Single-scattering albedo: the fraction of mediumDensity that scatters rather than absorbs, per channel. Returned by copy. |
var mediumAnisotropy: Float |
Henyey–Greenstein anisotropy of the phase function, in (-1, 1). Zero scatters isotropically; positive is forward scattering, which is what turns a light source in haze into a halo rather than an even glow; negative is backscattering, which is what a headlight in fog does to the driver behind it. |
var mediumBounds: Pair<Vector3f, Vector3f>? |
The axis-aligned box the medium occupies, as low to high, or null to fill all space. Returned by copy. |
var maxBounces: Int |
|
var environmentIntensity: Float |
|
var environmentRotation: Float |
|
var showEnvironment: Boolean |
Hide only primary-ray misses. Lighting, reflections and refraction still use the environment. |
var background: Vector4f |
Linear, straight RGBA background; alpha zero gives a transparent background for compositing. |
var indirectClamp: Float |
Maximum summed indirect luminance per path sample, in linear HDR units before exposure. Zero disables this biased firefly filter (the default). RGB is scaled together; camera-visible emission, background and first-surface lighting are preserved. Deeper glass transmission and caustics can darken. Changing this value resets accumulation. |
var emitterSampling: Boolean |
Sample emissive triangles directly, in addition to the environment, and combine that estimate with the emission a BSDF sample finds using the same power heuristic. This is unbiased: a converged image matches one rendered with it off. It costs one extra shadow ray per non-delta surface interaction, and it is what makes a small, bright lamp converge at all. Emitters behind stochastic alpha coverage, and emissive glass, stay BSDF-only. Turning it off reaches the previous estimator exactly. Changing this value resets accumulation. |
val emitterCount: Int |
Emissive triangles this scene offers the direct sampler; zero leaves emission to BSDF hits. |
var rayEpsilon: Float |
|
var exposure: Float |
Exposure affects display only and never discards samples. |
var samples: Int |
Fresh samples per pixel since the last view change/reset, excluding reprojected history. |
var paused: Boolean |
|
var convergence: PathTraceConvergence? |
Optional per-frame convergence log. Attach one and every render appends a (wall time, samples) point plus any reset it observes, which is what a frames { repeat(n) { frame() } } loop with a println in it was doing by hand. Detach by setting it to null; the tracer holds no other reference to it. |
val materialModule: ComputeModulePointer |
|
val materialMesh: ComputeMesh |
|
val output: RenderToTexture |
Premultiplied linear HDR RGBA; alpha is primary visibility coverage, not sample count. |
val triangleCount: Int |
|
var frameOutput: PathTraceFrameOutput? |
Optional unaccumulated radiance and matching primary guides. The tracer owns their lifetime. |
fun enableFrameOutput(): PathTraceFrameOutput |
Create this tracer's frameOutput, or return the one it already has; the first call resets accumulation. Reconstruction guides currently require opaque, non-transmissive materials, so a scene with glass or partial coverage has to stay on the progressive output. |
fun install(): PathTracingRenderer |
Register compute and RTT passes. Reusing a name retires the previous tracer. |
override fun render(command: CommandBuffer, index: Int) |
|
fun blitToScreen(blend: Boolean = true): ModulePointer |
Optional second pass: sample the RTT, apply exposure/Reinhard, and composite on screen. |
fun reset(reason: String = "property edit") |
Discard accumulated samples and start the estimate again. |
val resetGeneration: Long |
How many times accumulation has been discarded. samples counts within one generation, so a generation that has not advanced is the proof that a sample count is comparable to an earlier one — which four hand-written printf loops in one live session were trying to establish. |
val lastResetReason: String |
Why accumulation was last discarded. See reset. |
fun integratorSource(): String |
The integrator source this tracer compiles: the packaged trace.comp with whatever HARDWARE_QUERY state this backend needs. The starting point for reloadIntegrator. |
fun integratorSource(file: File): String |
integratorSource rebuilt from file — the working-copy trace.comp you are editing, which the packaged resource will not reflect until the next Gradle build. The backend define is applied for you, so a file that already carries one, or lacks one, both come out right. |
fun reloadIntegrator(source: String = integratorSource()): IntegratorReload |
Recompile the integrator on this running tracer, keeping the scene, the environment, the camera pose and every tuning property. |
fun reloadIntegrator(file: File): IntegratorReload |
reloadIntegrator from a working-copy trace.comp; see integratorSource. |
fun readLinear(): FloatArray |
Synchronous GPU readback for export/verification. Call on the render thread, outside frame recording. |
override fun close() |
Detach now; retire GPU resources through Field's frames-in-flight cleanup queue. |
ProceduralSplats.kt
Six Gaussian-splat clouds built from code, for a splat shader to deform: a shell, a fern, a nautilus, coral, a bloom and a braid.
Each builder returns a SplatSet of a few thousand flat, oriented splats and uses a fixed hash
where it needs variation, so a call gives the same cloud every time and index in an insert
means the same splat on every run. all maps the six under the demo's names, in its order.
The helpers below them are ports rather than conveniences. hsl is three.js's Color.setHSL
down to the sRGB-to-linear conversion its colour management applies by default,
quaternionFromUnitVectors is its Quaternion.setFromUnitVectors down to the axis it chooses
when the two vectors are opposite, and previewHash is the fract(sin(x) * 43758.5453) hash
the builders draw their variation from.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/ProceduralSplats.kt — read the source
ProceduralSplats
The six procedural clouds of the madp splat-shader demo, as SplatSets: built from code, deterministic, and the same splat for splat as the demo's (same order, so index in an insert means the same splat in both).
| Declaration | Summary |
|---|---|
val all: Map<String, () -> SplatSet> |
The six clouds by the demo's names, in the demo's order. |
fun icosphereShell(radius: Double = 1.05, depth: Int = 4, pointRadius: Double = 0.024, pointThickness: Double = 0.003): SplatSet |
Points on a subdivided octahedron pushed out onto a sphere, each a thin disc facing out and coloured by longitude: Spark's constructSpherePoints with radius 1.05, depth 4. 1 026 splats. |
fun fernPlume(): SplatSet |
The Barnsley fern played as the chaos game for 7 200 steps, each point after the first 28 a small leaf-shaped splat, arched and given depth, darker green at the stem. |
fun nautilusShell(): SplatSet |
A logarithmic spiral tube rising 2.8 turns, fourteen plates around each of 240 rings, warm ochre fading lighter toward the open end. 3 360 splats. |
fun coralBranches(): SplatSet |
A branching coral grown recursively four levels deep from one trunk, each branch a wavy row of discs facing along it, thinning toward the tips, red-orange shading to coral. |
fun phyllotaxisBloom(count: Int = 2800): SplatSet |
2 800 petals on the golden angle, rising and opening like a flower head, warm yellow-green at the base to gold at the rim. |
fun torusBraid(): SplatSet |
Ten strands of beads braided around a (2, 3) torus knot, cyan through violet. 3 400 splats. |
fun previewHash(seed: Double): Double |
The demo's previewHash: the classic fract(sin(x) * 43758.5453) hash, 0..1. |
fun quaternionFromUnitVectors(fx: Double, fy: Double, fz: Double, tx: Double, ty: Double, tz: Double): DoubleArray |
The rotation taking unit vector from to unit vector to, as (x, y, z, w) — three.js's Quaternion.setFromUnitVectors, including its choice of axis when the two are opposite. |
fun hsl(h: Double, s: Double, l: Double): DoubleArray |
three.js's Color.setHSL: h wraps, s and l clamp to 0..1, and the result is converted from sRGB to linear, as three.js's colour management does by default. |
RayTracingDocsHarness.kt
The figures on Field's ray tracing pages: the StandardRendererDocsHarness shape, over the ray
tracing pipeline instead of the rasterizer.
Every public render method runs a deterministic ray tracing example and captures the output to a PNG. If ray tracing is unavailable, or the module fails to build, the method throws rather than writing a stand-in image, so a page's figure is either a render or nothing. A placeholder is a valid PNG, and a build that accepts one cannot tell a capture from a picture of its own failure.
tools/check_doc_assets.py guards the other side of that: it rejects a published figure whose
pixels match the "Ray Tracing / Not Available" card a harness could once write in place of a
render on a device without VK_KHR_ray_tracing_pipeline.
From a box connected beneath a renderer, a figure is three lines:
val image = RayTracingDocsHarness.renderUvGradient("/tmp/rt-uv.png")
display("render", RayTracingDocsHarness.htmlForImage(image, "UV gradient"))
println("ray tracing example ready")
src/jvmMain/kotlin/afield/graphics/scene/fabricated/RayTracingDocsHarness.kt — read the source
RayTracingDocsHarness
Renders the ray tracing documentation's examples offscreen, or throws if the device cannot.
RayTracingGpuAudit.kt
The ray-tracing audit: one JVM entrypoint that drives Field's path-tracing and ray-pipeline smokes against a real GPU and writes down what it found.
This is the four.raytracing.gpu-audit catalog test. There is no document on disk and no Field
server: the audit opens a window, builds a Renderer, makes a bare Box for the smokes to run
under, and steps the frame loop itself, resuming each check only between complete frames,
because the renderer records with a runBlocking of its own. Every stage writes its verdict and
elapsed time into report.json as it finishes, so a run that dies part way through still says
where it got to.
The stages selected by default are seams, base, camera, temporal, fog and pipeline;
--all adds emitters, medium for participating media, and benchmark, and --stage=NAME
runs a single one. A device
with no VK_KHR_ray_tracing_pipeline records the pipeline stage as SKIPPED and its reason,
unless --require-pipeline says to fail there instead, and --fallback turns the optional
ray-tracing negotiation off so the ordinary compute path is what gets measured.
The pipeline stage reads pixels rather than checking that a file was written: a UV gradient against the value its dispatch should have computed, a barycentric triangle that has to reach all three vertex colours, checker geometry that has to come out both dark and light, and a sphere whose traced image has to change when its instance transform moves and come back identical when the transform is put back.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/RayTracingGpuAudit.kt — read the source
RayTracingGpuAudit
Runs the existing Field renderer and readback smokes on one thread, without a saved document. Default: seams, base/glTF/glass, camera, temporal, fog, pipeline captures. --all adds emitters, medium and GPU benchmarks. --stage=NAME selects one stage. --require-ray-query and --require-pipeline fail closed on an unsuitable device; otherwise unsupported pipeline captures are explicitly SKIPPED in report.json. --fallback disables optional RT negotiation to exercise ordinary compute rendering.
| Declaration | Summary |
|---|---|
fun main(args: Array<String>) |
|
suspend fun capturePipeline(renderer: Renderer, out: File) |
Fresh files plus assertions on shader output: a screenshot of an empty box cannot pass. |
RectSLGalleryHarness.kt
The RectSL gallery on the docs site, rendered offscreen: the StackSLGalleryHarness shape,
applied to programs that describe a whole scene rather than one fullscreen fragment.
A RectSL program carries a wall, a duration, an fps, and its rects, tracks, groups and StackSL
materials, so the render path here is the one afield.lang.rectsl.RectSLRenderRunner already
uses: compile the program, lower each sampled frame to a triangle mesh, and draw it through
RECT_ANIMATION_VERTEX_SHADER plus a StackSL-compiled material fragment into an offscreen
RenderToTexture.
Both entry points hold one renderer and one graphics context open and walk the program list, which is the "one Field document, many Field 4 renders" pattern. A fresh target is built per example, because each program's wall has its own aspect ratio; the StackSL gallery can reuse one fixed 512² target instead.
renderGallery reads back a single representative still per program. renderAnimations sweeps
the program's own timeline frame by frame and encodes each animate: true program to an mp4,
since the docs runner can screenshot the native GPU window but never record it, which leaves the
readback and the ffmpeg encode to the harness. The programs themselves are editable Field
content under this harness's WORKSPACE_DIR, not literals in this file.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/RectSLGalleryHarness.kt — read the source
RectSLGalleryHarness
Renders the RectSL gallery stills and animations offscreen, one scene program at a time.
| Declaration | Summary |
|---|---|
const val WORKSPACE_DIR |
Repo-relative workspace whose *_rectsl_.rectsl files are the examples. |
data class Example(val name: String, val source: String, val stillTime: Double? = null, val animate: Boolean = false) |
One gallery entry loaded from the workspace: name is the box slug (and the PNG/mp4 stem rectsl-<name>), source its RectSL program. stillTime is the timeline moment sampled for the still. animate marks programs that also get an mp4. |
fun loadExamples(workspaceDir: File = defaultWorkspaceDir()): List<Example> |
Load every example from the workspace: one box per <name>_rectsl_.rectsl file, ordered by the docOrder in the sibling <name>_metadata_.yaml (which also carries optional animate: / stillTime:). The programs live in the workspace, not in this file. |
suspend fun renderGallery(outputDir: File, renderer: Renderer): File |
Render every workspace example into <outputDir>/rectsl-<name>.png (a single representative frame), reusing a single renderer. Returns the hero PNG. |
suspend fun renderAnimations(outputDir: File, renderer: Renderer): List<File> |
Render an mp4 for every animate: true workspace example into <outputDir>/rectsl-<name>.mp4, reusing one renderer. The scene is sampled over its own timeline; each frame is read back and handed to ffmpeg. |
suspend fun renderFitToTarget(outputDir: File, renderer: Renderer): File |
A dedicated illustration of the hardest bridge idea (#5): a RectSL distribution is a Functions graph, so the host can fit it to a target and RectSL just samples the winner. We fit a two-lobe target with fitBest, bind the seed/target/fitted curves by name into a real RectSL program, and render three arrays — target (reference), seed (before), fitted (after) — so the gold "after" row visibly lands on the target silhouette that the muddy "before" row misses. The layout is the ordinary array + sample path; only the curve was searched for by the host. |
val FIT_SCENE_SOURCE: String |
The scene rendered by renderFitToTarget; target/seed/fit are host-bound curves. |
val SDF_SCENE_SOURCE: String |
The scene the SDF looks are demonstrated on. |
val SDF_PATTERN_SOURCE: String |
A scene whose motion comes from the pattern language, so the differential covers the two tracks emitSdf cannot lower symbolically and samples instead (both pattern queries; ->degrade's 64-bit hash lowers exactly now). |
val SDF_CARVE_SOURCE: String |
The field that gets subtracted by the carve look. |
data class SdfAgreement(val label: String, val comparisons: Int, val maxAbsError: Double, val meanAbsError: Double, val quantisation: Double, val coordinateResidual: Double) |
What measureSdfAgreement found, so the caller can print it and assert on it. |
suspend fun renderSdf(outputDir: File, renderer: Renderer): File |
Everything the SDF page needs, in one graphics context: a still per look, an animated clip, the raster-vs-SDF counterfactual, and the GPU↔CPU differential. Returns the hero still. |
suspend fun measureSdfAgreement(renderer: Renderer, program: SceneProgram, times: List<Double> = listOf(0.0, 0.37, 0.95, 2.05, 3.6), ranges: List<Pair<String, Double>> = listOf("near" to PROBE_NEAR_RANGE, "far" to PROBE_FAR_RANGE)): List<SdfAgreement> |
Ask the compiled shader what the field is, and compare it against the CPU sampler that the raster path draws from. |
suspend fun measureGallerySdfAgreement(renderer: Renderer, maxScenes: Int = 8, times: List<Double> = listOf(0.0, 0.41, 1.7)): List<SdfAgreement> |
The same differential, over a diverse subset of the real gallery rather than the two scenes this page draws. |
SplatSequence.kt
Playing a splat capture over time: the frame reader, the play head, and the box controls that drive it.
A splat sequence is a directory of per-frame .compressed.ply files, and the reason this file
exists separately from GaussianSplatRenderer.kt is that the interesting problem is on the disk
side rather than the GPU side. SplatSequence reads frames on demand into a bounded
most-recently-used cache, sized in frames rather than bytes, because a working session scrubs:
a 124-frame capture is 44 MB and would fit whole, a five-minute one will not, and a
forward-only reader would make the short case pleasant and the long case impossible. Nothing
prefetches, which is a decision and not an omission — read-ahead only pays during playback, and
doing it properly costs a thread and a drop policy.
SplatTransport is the play head and holds no data at all: a frame index, a rate, and the
playing/looping state. Keeping it separate from the reader is what lets the same transport drive
a sequence on disk or a frame set already resident, and its source fps is independent of the
display rate, so a 12 fps capture shown at 60 does not become a 60 fps capture.
Box.transportControls wires it to a control group, and the ToHTML rendering puts the
play head in a box's output where it can be read at a glance.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/SplatSequence.kt — read the source
Top-level functions
| Declaration | Summary |
|---|---|
fun Box.transportControls(transport: SplatTransport, channel: String = "transport") |
A transport you can drive from an output area: scrub, speed, play/pause, loop and single steps. |
SplatSequence
class SplatSequence(val files: List<File>, val cacheFrames: Int = 96)
A directory of .compressed.ply frames, readable in any order.
| Declaration | Summary |
|---|---|
val files: List<File> |
|
val cacheFrames: Int |
How many decoded frames to keep. Each is roughly the file's size again as ints. |
val frameCount: Int |
|
val stats: String |
Cache hits and misses since construction, for deciding whether cacheFrames is right. |
fun raw(frame: Int): CompressedSplatPly.RawSplats |
The payload for frame, read if it is not already cached. Indices outside the sequence wrap, so a caller doing arithmetic on slot offsets does not have to. |
fun prime(): SplatSequence |
Read every frame now. Only sensible when the whole sequence fits in cacheFrames. |
fun peakWindowSplats(window: Int): Int |
The largest total splat count any window consecutive frames can reach, which is what a GaussianSplatRenderer.streaming cloud's capacity has to be. |
SplatTransport
class SplatTransport(val frameCount: Int, var fps: Double = 12.0) : ToHTML
A play head over frameCount frames: play, pause, scrub, loop, and run backwards.
| Declaration | Summary |
|---|---|
val frameCount: Int |
|
var fps: Double |
Source frames per second. Independent of the display rate. |
var playing: Boolean |
|
var presentationPaced: Boolean |
When true, advance only services clock bookkeeping; a frame-set presenter moves time with commitPresented. This keeps the public playhead identical to the complete asset set actually shown instead of letting it run several frames ahead during an I/O underrun. |
var speed: Double |
1.0 is forward at fps; negative runs backwards; 0.0 is a pause that keeps playing true. |
var loop: Boolean |
Wrap at the ends. When false the play head clamps and playing is left alone. |
var seekGeneration: Long |
Increases on an explicit scrub/seek, not on ordinary wall-clock advancement. |
var position: Double |
|
val frame: Int |
The frame position resolves to. |
var normalized: Double |
position as 0..1 over the sequence, for a slider. |
fun advance(nowNanos: Long, maxCatchUpSeconds: Double = 0.25): Boolean |
Advance the play head to nowNanos and report whether frame changed. |
fun seek(target: Double): Boolean |
Jump to target frames, fractional. Reports whether frame changed. |
fun scrubBy(delta: Double): Boolean |
Jump by delta frames, fractional — the scrub-by-a-drag case. |
fun invalidate() |
Force the next advance to report a change, after something else invalidated the upload. |
fun commitPresented(frame: Int, nowNanos: Long) |
Move the playhead after one complete externally-paced frame set has committed. Not a seek. |
fun resetClock() |
Drop the clock, so the next advance does not count the time since the last one. |
override fun toString(): String |
|
override fun toHTML(): String |
A play head you can read at a glance: the track, where the head sits on it, the frame count, and the state that decides what happens next. |
SplatSet.kt
The CPU side of a splat cloud: how one is built, where it can be loaded from, and how it is framed before a camera looks at it.
SplatSet keeps the splats in a flat FloatArray laid out the way the GPU reads them, and
grows as push appends. Its companion loads a cloud instead of building one: fromPly reads a
Gaussian-splat capture, decoding the log scales, logit opacity and spherical-harmonic colour
those files store, and fromGltf turns the vertices of a mesh into discs facing along their
normals, sized from how densely they cover the surface.
fit is the framing a SplatShader uses: a median centre and a 90th-percentile radius, so a
handful of stray splats cannot decide where the camera points. A cloud that arrives in metres
needs fitted before an insert written for a unit-sized cloud reads on it as it does on
ProceduralSplats.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/SplatSet.kt — read the source
SplatSet
class SplatSet(initialCapacity: Int = 1024)
Gaussian splats on the CPU in their parametric form — centre, radii, rotation, colour and opacity — rather than the covariance GaussianSplatRenderer draws from.
| Declaration | Summary |
|---|---|
var floats: FloatArray |
The packed splats; only the first count × 16 floats are live. |
var count: Int |
How many splats have been pushed. |
fun push(x: Double, y: Double, z: Double, sx: Double, sy: Double, sz: Double, qx: Double, qy: Double, qz: Double, qw: Double, opacity: Double, r: Double, g: Double, b: Double): SplatSet |
Append one splat. qx..qw need not be normalised; a zero quaternion means no rotation. |
fun centre(index: Int): Vector3f |
The centre of splat index. |
fun bounds(): Pair<Vector3f, Vector3f> |
The smallest box holding every splat centre; both corners are zero for an empty set. |
fun fit(): Fit |
Where the cloud is and how big, measured so that outliers do not count: Fit.centre is the per-axis median of the splat centres and Fit.radius90 the distance from it that nine splats in ten fall within. Non-finite centres are skipped. An empty set fits the unit ball. |
fun fitted(radius: Double = 1.0): SplatSet |
A copy moved and scaled so fit comes out centred on the origin with a 90th-percentile radius of radius: centres and radii both scale, rotations and colours stay. |
data class Fit(val centre: Vector3f, val radius90: Float) |
A robust centre and radius for a cloud; see SplatSet.fit. |
SplatShader.kt
The splat-shader box: a cloud of Gaussian splats uploaded once, a few lines of GLSL that rewrite every splat each frame, and Field's Vulkan splat renderer drawing the result.
SplatShader does the work. It uploads the parametric splats, binds them to the box's compute
module as splatsIn and the renderer's cloud as splatsOut, dispatches the insert once per
splat before the sort runs, and installs the cloud in an accumulation target, so a trail is a
setting rather than a second code path. SplatShaderSettings is everything a viewer can change
while it runs, from the camera and the turn rate to the opacity and gamma the splats are
blended with, and showControls puts the demo's sliders in the box's controls output.
The clouds come from SplatSet and ProceduralSplats; the sort, the packed covariance form
and the splat shaders themselves are GaussianSplatRenderer's.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/SplatShader.kt — read the source
Top-level functions
| Declaration | Summary |
|---|---|
fun Box.showControls(shader: SplatShader, channel: String = "controls") |
The demo's viewer toolbar for shader, in this box's controls output: the auto-rotate speed, the size, a splat-size multiplier, play/pause for t and the turn, and a button that starts t again from zero. The panel opens, as the demo's toolbar is always in view. |
SplatShader
A Gaussian-splat cloud whose splats are rewritten every frame by a few lines of GLSL in a box: the madp splat-shader demo's "insert", on Field's Vulkan splat renderer.
| Declaration | Summary |
|---|---|
val module: ComputeModulePointer |
The compute module holding the insert, usually computeModuleHere {}. |
val splats: SplatSet |
The splats the insert starts from each frame. |
val renderer: Renderer |
The renderer to draw on; by default the one this box is running in. |
val settings |
The settings after configure ran; the framing, rotation and splat look can change live. |
val source: FrameBufferGroup |
The parametric splats, as uploaded: what the insert reads. |
val cloud: GaussianSplatRenderer.Cloud |
The renderer's cloud: what the insert writes, and what is sorted and drawn. |
val trail: PointAccumulation |
What the cloud is drawn into, and what leaves the trail. |
val mesh: ComputeMesh |
The dispatch in module that runs the insert, one invocation per splat. |
val fit: SplatSet.Fit |
The SplatSet.fit the framing uses. |
var t: Double |
Seconds since this was made: the insert's t. Set it to scrub. |
var angle: Double |
The cloud's current turn about +y, in radians. |
var playing: Boolean |
When false, t and the rotation stand still and the insert sees the same t each frame. |
fun model(): Matrix4f |
The model matrix this frame: the fit, the size, then the turn about +y. |
fun applyLook() |
Push settings' camera and splat look onto the cloud; called once a frame, so edits show. |
fun remove() |
Take the cloud and its trail off the renderer. Launching another SplatShader with the same name does this too. |
SplatShaderSettings
What a SplatShader can be told; see each property.
| Declaration | Summary |
|---|---|
var name: String |
The renderer module name. Two SplatShaders with one name replace each other. |
var autoRotate: Double |
Turn about +y, in the demo's slider units: 0.32 is its default, 0 stands still. |
var size: Double |
The demo's size slider: a multiplier on the fitted size. |
var trail: Boolean |
The demo's trail mode: lay each frame over a fading copy of the last, so moving splats leave streaks. fade is how much of the old picture goes each frame. |
var fade: Double |
With trail on, the part of the previous frame that fades away each frame: the demo's 0.12. |
var fit: Boolean |
Frame the cloud by its median and 90th-percentile radius; false draws it as it is. |
var splatSize: Float |
Multiplies every splat's screen size. The default, 1/√2.2, is what makes a splat look the size it does in the demo. Spark blends in display space, so a Gaussian's faint tails stay dark; Field blends in linear light and the window encodes the result to sRGB, which lifts those tails, and at the same radius the visible dot comes out about twice as wide. For a splat over black, encode(c · a') = c · a wants a' ≈ a^2.2, which is the same Gaussian narrowed by √2.2. Set 1 for the splats' true size. |
var alphaScale: Float |
Multiplies every splat's opacity. |
var colorGamma: Float |
See GaussianSplatRenderer.Config.colorGamma; 2.2 is right for Field's window. |
var cameraAt: Vector3f |
|
var cameraTarget: Vector3f |
|
var fovDegrees: Double |
Vertical field of view. |
var sortMode: GaussianSplatRenderer.SortMode |
GaussianSplatRenderer.SortMode.ExactRadix by default: the keys are view-depth float bits. |
SplatShaderSortSmoke.kt
src/jvmMain/kotlin/afield/graphics/scene/fabricated/SplatShaderSortSmoke.kt — read the source
SplatShaderSortSmoke
Does the splat depth sort see where a SplatShader insert put the splats? Measured on the GPU.
| Declaration | Summary |
|---|---|
fun main(args: Array<String>) |
StackSLGalleryHarness.kt
The StackSL gallery on the docs site: one fullscreen fragment per example program, rendered
offscreen into a fixed 512² (OUTPUT_DIM) target.
It holds one renderer, one offscreen RenderToTexture and one fragment slot open, then walks
the StackSL programs in a canonical_graphics-style workspace (WORKSPACE_DIR). Each program is
injected through the ordinary {{ stack.auto.program }} template path, the same live-coding
mechanism the StackSL pages describe, and the fullscreen module is rebuilt under a single name,
so a new module replaces the previous one in the target's module map. The resulting texture is
then read back.
renderGallery reads back a single still per program. renderAnimations drives a time push
constant across frames and encodes each animate: true program to an mp4, since the docs runner
can screenshot the native GPU window but never record it, which leaves the readback and the
ffmpeg encode to the harness. Both are "one Field document, many Field 4 renders": a whole
page's assets come out of a single graphics context rather than a JVM restart apiece. The
example programs are editable Field content in the workspace, not literals in this file.
Both targets use GALLERY_FORMAT, an sRGB format, for the same reason the window's swapchain is
B8G8R8A8_SRGB: the fragment writes linear colour and the attachment encodes it on write, so
the bytes read back are the bytes a viewer sees in a live window. A UNORM attachment stores
the linear values unencoded, and reads back markedly darker than the same program looks in a
window. The feedback programs sample their own previous frame, and an sRGB ping-pong hands that
sample back decoded, so they still compute in linear light; that is also
PingPongRenderToTexture's default.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/StackSLGalleryHarness.kt — read the source
StackSLGalleryHarness
Renders the StackSL gallery stills and animations offscreen, one fullscreen program at a time.
| Declaration | Summary |
|---|---|
const val OUTPUT_DIM |
|
const val GALLERY_FORMAT |
The colour format of every gallery target: the swapchain's encoding, in RGBA order so TexturePreviewCache.capture copies it straight to the PNG without a blit or a swap. |
const val WORKSPACE_DIR |
Repo-relative workspace whose *_stackSL_.stacksl files are the examples. |
data class Example(val name: String, val program: String, val time: Float = 1.75f, val animate: Boolean = false, val timeSpeed: Float = 1.0f, val feedback: Boolean = false) |
One gallery entry loaded from the workspace: name is the box slug (and the PNG/mp4 stem stacksl-<name>), program its StackSL source, time is baked into input.w for the still. animate marks time-driven programs that also get an mp4, and timeSpeed scales how fast input.w sweeps in that clip. |
fun loadExamples(workspaceDir: File = defaultWorkspaceDir()): List<Example> |
Load every example from the canonical_graphics-style workspace: one box per <name>_stackSL_.stacksl file, ordered by the docOrder in the sibling <name>_metadata_.yaml (which also carries optional animate:/timeSpeed:). The programs live in the workspace, not in this file. |
suspend fun renderGallery(outputDir: File, renderer: Renderer): File |
Render every workspace example into <outputDir>/stacksl-<name>.png, reusing a single renderer + target. Returns the hero PNG for the docs fence. |
suspend fun renderAnimations(outputDir: File, renderer: Renderer): List<File> |
Render an mp4 for every animate: true workspace example into <outputDir>/stacksl-<name>.mp4, reusing one renderer + target. The shader is built once per program; a time push constant sweeps over ANIM_FRAMES frames, each read back and handed to ffmpeg. |
StandardRendererDocsHarness.kt
The offscreen renders behind Field's graphics documentation: deterministic StandardRenderer
scenes captured to PNG, plus the HTML wrapper that puts one in a box output.
A page gets its figure by calling one of the render functions from a box, passing the returned
file to htmlForImage, and letting the docs build screenshot the result through the ordinary
backdoor path. The scenes take the renderer's features one at a time: metalness, shadow and
ambient-occlusion comparisons, a camera and depth study, glTF loading, blended transparency, and
the post-stack vignette and tone-map passes. Each takes its renderer from the box's context by
default, so a box connected beneath a renderer needs to pass only an output path.
renderStackSLGallery and renderRectSLGallery, with their animation counterparts, are
wrappers rather than scenes of their own: the rendering belongs to StackSLGalleryHarness and
RectSLGalleryHarness, and the wrapper copies the hero image that harness produced to the path
the docs fence checks.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/StandardRendererDocsHarness.kt — read the source
StandardRendererDocsHarness
object StandardRendererDocsHarness
Renders the graphics documentation's StandardRenderer figures offscreen, one scene per call.
simpleGaussianSplatSupport.kt
A Gaussian-splat shader pair seeded into a box as editable starter source, so the splat shader is
live Field code rather than source hidden inside GaussianSplatRenderer.
The two entry points are call-shaped macros, so their Kotlin bodies only throw and the work is in
the expansion: it writes the renderer's starter vertex and fragment source into the box's
vertex and fragment properties, compiles them, reflects their interface into generated
bindings, and rewrites the call to _moduleHere. What comes back is an ordinary graphics
module, with the reload actions and shader-error reporting every other shader box has, and
GaussianSplatRenderer.Cloud takes it as suppliedRenderModule.
There are two of them because multiview is a different vertex shader and not a flag.
gaussianSplatMultiviewModuleHere seeds the variant that requires GL_EXT_multiview and selects
a model-view and projection matrix per eye from gl_ViewIndex; use it when the cloud draws into
a RenderToTexture whose multiviewViewMask is non-zero, and the mono one otherwise. The
fragment shader is the same in both cases.
The seeding happens once. macro_moduleHereWithDefaultShaders writes the starter source only
where the box has no shader source of its own, so an edited shader survives re-running the macro.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/simpleGaussianSplatSupport.kt — read the source
Top-level functions
| Declaration | Summary |
|---|---|
fun Box.gaussianSplatModuleHere(p: Parameters.() -> Unit): ModulePointer |
A box-owned Gaussian-splat graphics module, initially seeded with Field's built-in mono shader. |
fun Box.gaussianSplatMultiviewModuleHere(p: Parameters.() -> Unit): ModulePointer |
The multiview counterpart to gaussianSplatModuleHere. Use this for a RenderToTexture whose multiviewViewMask is non-zero; the seeded vertex shader exposes left/right model-view and projection matrices and selects them with gl_ViewIndex. |
fun Box.macro_gaussianSplatModuleHere(args: List<String>): Insert? |
The macro expansion behind gaussianSplatModuleHere: the ordinary graphics-module expansion, with GaussianSplatRenderer's mono splat shader pair as the source to seed. |
fun Box.macro_gaussianSplatMultiviewModuleHere(args: List<String>): Insert? |
macro_gaussianSplatModuleHere with the multiview vertex shader, behind gaussianSplatMultiviewModuleHere. |
fun registerGaussianSplatModuleMacros() |
Puts both splat macros into MacroRegistry.callRewrite, under the names box code writes. |
standardRenderer.kt
The deferred renderer the rest of Fabricate is arranged around: the pass bundle, its shaders, and the configuration that sizes them.
This is the file to read first, because the objects it returns are what almost everything else
here consumes. StandardRenderer.build(...) builds seven passes (g-buffer, shadow map, ambient
occlusion, lighting composite, transparent composite, motion blur, anti-aliasing) and hands back
a Bundle holding each as a RenderToTexture, along with the scene defaults, the scene graph
and the post stack. Nothing is hidden behind that: FabricateFrameGraph wraps the same targets
to give them names and an order, FabricatePostStack appends to them, and a box that wants to
reach past all of it and manipulate a RenderToTexture directly still can. Throughout, the
existing Field 4 primitives are composed rather than replaced.
Deferred is the choice that explains the pass list. Geometry is rasterized once into a g-buffer
of positions, normals and material parameters, and lighting is then evaluated per screen pixel,
so cost scales with lights rather than with lights times triangles, which is what lets
FabricateScene's light list be arbitrary. It is also why transparency needs a separate pass at
the end: a g-buffer holds one surface per pixel, and a blended surface is a second one.
Scenes are not built here. FabricateScene owns the graph and FabricateScenePlanner produces
the draw queues; this file renders what it is given. For the renderer working, with captures of
each pass, see public/docs/fabricate_renderer.md.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/standardRenderer.kt — read the source
Top-level functions
| Declaration | Summary |
|---|---|
fun postEffectTargetParameters(name: String, shape: FabricatePassShape, rtt: RTTParameters.() -> Unit = {}): RTTParameters.() -> Unit |
The RTTParameters every StandardRenderer.Bundle.addShaderEffect pass target is built with. |
StandardRenderer
One-stop shop for a small deferred renderer: g-buffer, shadow map, AO, and a lighting composite. Everything here is built on top of the existing Field 4 primitives so users can just feed meshes in and wire a RenderTarget around the passes.
standardShapes.kt
Ten par_shapes primitives, written straight into vertex-pulling buffers as triangle soup.
StandardShapes is the layer under StandardRenderer.Bundle.registerCube, registerSphere and
the eight other register* shape helpers, one per function here. Each builds a par_shapes mesh,
scales it, translates it if the call gave a centre, and writes it into two buffers the caller
supplies, one for positions and one for normals. The write is non-indexed: the source mesh's
triangle list is expanded, so three vertices go out per triangle and a vertex-pulling shader
reads a position and its normal at the same index with no indirection. ShapeWriteResult comes
back carrying the source mesh's vertex and triangle counts alongside what was actually written,
which is how a caller sizes its mesh afterwards instead of predicting the triangle count up
front.
WriteConfig covers the two decisions a caller usually has to make. padVec3To4 writes each
vector as four floats for std430 alignment and is on by default; append chooses between
continuing after whatever is already in the buffer and starting at its beginning, and all ten
register* helpers pass append = false. Normals come from par_shapes, computed there if the
mesh arrived without any, with an index that falls outside the normal buffer reading the
triangle's face normal instead. Each call frees its par_shapes mesh before returning.
src/jvmMain/kotlin/afield/graphics/scene/fabricated/standardShapes.kt — read the source
StandardShapes
Thin Kotlin helpers around par_shapes that emit non-indexed positions + normals into caller-provided DynamicallyWriteable buffers (useful for vertex-pulling tests).