PathTraceShaderBoxes.kt
src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceShaderBoxes.kt · 119 lines
package afield.graphics.scene.fabricated
import afield.document.graph.Box
import afield.runtime.lifecycle.RunsAt
import afield.runtime.lifecycle.macroRegistration
import afield.runtime.macros.MacroRegistry
import afield.graphics.vulkan.Renderer
import afield.document.context.*
import afield.graphics.render.*
import afield.graphics.scene.*
import afield.graphics.shader.*
import afield.graphics.vulkan.targets.RenderToTexture
import afield.graphics.vulkan.util.Camera
import afield.graphics.shader._compute
import afield.graphics.shader._computeModuleHere
import afield.graphics.shader.macro_computeModuleHereWithDefaultShader
import afield.graphics.shader.shaderTemplate
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.
fun Box.pathTracingHere
A named tracer whose material function lives in this box's normal compute shader property.
@Suppress("UNUSED_PARAMETER")
fun Box.pathTracingHere(
scene: PathTraceScene, environment: PathTraceEnvironment,
width: Int = 640, height: Int = 360, camera: Camera? = null,
name: String = "path-tracer", target: RenderToTexture? = null,
backend: PathTracingRenderer.Backend = PathTracingRenderer.Backend.AUTO,
configure: Parameters.() -> Unit = {}
): PathTracingRenderer = throw MacroRegistry.MacroNotExpandedException("pathTracingHere must be expanded as a macro")
private fun Box.pathTraceTemplates(hardware: Boolean) {
val source = PathTracingRenderer.shader(hardware)
shaderTemplate["pathTracing.preamble"] = source.substringBefore("// MATERIAL_BEGIN")
shaderTemplate["pathTracing.integrator"] = source.substringAfter("// MATERIAL_END")
}
fun Box._pathTracingHere
What pathTracingHere rewrites to, and where the device is finally consulted: the Renderer
comes from the ambient context, hardware traversal is taken whenever the device advertises ray
queries and backend has not pinned the software BVH, and the tracer is built with this box's
compute module as its material module.
It edits the prepared source rather than the property: _compute is the box's compute text
after template expansion, which is what makes flipping HARDWARE_QUERY a string edit on real
GLSL. A box over which no prepare pass has run has no _compute to edit, so this is called from
the expansion the macro writes and not by hand.
fun Box._pathTracingHere(
scene: PathTraceScene, environment: PathTraceEnvironment,
width: Int = 640, height: Int = 360, camera: Camera? = null,
name: String = "path-tracer", target: RenderToTexture? = null,
backend: PathTracingRenderer.Backend = PathTracingRenderer.Backend.AUTO,
configure: Parameters.() -> Unit = {}
): PathTracingRenderer {
val renderer: Renderer = fromContext()
val hardware = backend != PathTracingRenderer.Backend.COMPUTE_BVH && renderer.device.supportsRayQuery
pathTraceTemplates(hardware)
// Initial macro reflection is portable; only the optional AS binding differs on a query device.
// Subsequent reloads expand the device-specific template above through the ordinary Field path.
_compute = _compute!!.replace("#define HARDWARE_QUERY", "").let {
if (hardware) it.replace("#version 460", "#version 460\n#define HARDWARE_QUERY") else it
}
return PathTracingRenderer(renderer, scene, environment, width, height, camera ?: Camera(renderer), backend, name, target) { p ->
p.configure()
_computeModuleHere(p)
}.install()
}
fun registerPathTraceShaderBoxes()
Registers the pathTracingHere rewrite, once per new root. The default shader it plants is the
preamble slot, a shadeMaterial that returns its material unchanged, and the integrator slot,
with a comment saying where push constants and includes go.
@RunsAt(macroRegistration)
fun registerPathTraceShaderBoxes() {
MacroRegistry.callRewrite["pathTracingHere"] = { box, _, args ->
box.pathTraceTemplates(false)
box.macro_computeModuleHereWithDefaultShader(args, """
{{ pathTracing.preamble }}
// Add push constants, shared includes, and material functions here.
Material shadeMaterial(Material material, PathTraceSurface surface) {
return material;
}
{{ pathTracing.integrator }}
""".trimIndent(), "_pathTracingHere")
}
}