FabricateShaderBoxes.kt
src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricateShaderBoxes.kt · 125 lines
package afield.graphics.scene.fabricated
import afield.document.graph.Box
import afield.app.protocol.Rect
import afield.app.server.SessionInformation
import afield.document.graph.box
import afield.graphics.render.ModulePointer
import afield.graphics.shader.attachShader
import afield.app.server.thisSessionInfo
import java.util.Collections
import java.util.IdentityHashMap
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.
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.shaderModules(): LinkedHashMap<String, ModulePointer> {
val out = LinkedHashMap<String, ModulePointer>()
val seen = Collections.newSetFromMap(IdentityHashMap<ModulePointer, Boolean>())
fun add(name: String, module: ModulePointer) {
if (seen.add(module)) out[name] = module
}
add(FabricateFrameGraph.PASS_GBUFFER, gBufferModule)
add(FabricateFrameGraph.PASS_SHADOW, shadowModule)
add(FabricateFrameGraph.PASS_AO, aoModule)
add(FabricateFrameGraph.PASS_LIGHTING, lightingModule)
add("std-transparent-copy", transparentCopyModule)
add(FabricateFrameGraph.PASS_TRANSPARENCY, transparentModule)
add(FabricateFrameGraph.PASS_MOTION_BLUR, motionBlurModule)
add(FabricateFrameGraph.PASS_ANTI_ALIAS, antiAliasModule)
// Textured g-buffer variants created during glTF import live on the g-buffer target.
gBuffer.modules.forEach { (name, module) ->
if (module !== gBufferModule) add("std-gbuffer/$name", module)
}
// Post-stack effects (tone mapping, bloom, user shader effects). Motion blur / FXAA reuse the
// modules already added above and are skipped by the identity de-dup.
postStack.effects().forEach { effect ->
effect.module?.let { add("std-post/${effect.name}", it) }
}
return out
}
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.shaderSources(): LinkedHashMap<String, Pair<String, String>> {
val out = LinkedHashMap<String, Pair<String, String>>()
shaderModules().forEach { (name, module) ->
val params = module.current.parameters
out[name] = params.vertex to params.fragment
}
return out
}
fun StandardRenderer.Bundle.bindShadersToBoxes
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.
Call this in a live session after the bundle (and any post effects / glTF you want to expose) are
set up; re-calling it picks up newly added modules and leaves existing edits intact. Boxes are laid
out in a simple grid starting at origin. Returns the pass-name -> box map.
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> {
require(columns > 0) { "columns must be positive" }
val boxes = LinkedHashMap<String, Box>()
shaderModules().entries.forEachIndexed { index, (name, module) ->
val id = "$namePrefix/$name"
val b = box(id, root = root)
b.name = "$namePrefix · $name"
val col = index % columns
val row = index / columns
b.frame = Rect(
origin.x + col * (origin.w + gap),
origin.y + row * (origin.h + gap),
origin.w,
origin.h
)
b.attachShader(module)
boxes[name] = b
}
return boxes
}
fun StandardRenderer.Bundle.bindShaderToBox
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).
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 {
val b = box("$namePrefix/$name", root = root)
b.name = "$namePrefix · $name"
b.frame = frame
b.attachShader(module)
return b
}