Skip to content

RayReconstructionDemo.kt

src/jvmMain/kotlin/afield/media/reconstruction/RayReconstructionDemo.kt · 130 lines

A direct-lighting scene lit by RTXDI whose output can be switched between raw, spatially denoised and DLSS Ray Reconstruction paths.

RayReconstructionDemo reuses DirectLightingDemo for the G-buffer and lighting, builds the guide images ray reconstruction needs, and runs two DlssReconstruction instances at native resolution. Setting path resets history and changes which image is shown.

package afield.media.reconstruction

import afield.graphics.vulkan.Renderer
import afield.graphics.vulkan.Target
import afield.graphics.vulkan.device.CommandBuffer
import afield.graphics.render.lighting.*
import afield.document.context.*
import afield.graphics.render.*
import afield.graphics.scene.*
import afield.graphics.shader.*
import afield.graphics.render.Module
import afield.graphics.vulkan.targets.RenderToTexture
import afield.graphics.texture.TextureSampler
import afield.graphics.shader.fromViews
import org.lwjgl.vulkan.VK13.*

class RayReconstructionDemo(val renderer: Renderer,val size: ReconstructionSize=ReconstructionSize(1920,1080),

Existing Fabricate G-buffer and RTXDI, with interchangeable reconstruction at native resolution. The direct-lighting bridge is Lambertian: specular albedo=0 and roughness=1 describe that BRDF. Do not reuse these guides for a glossy/path-traced signal. All editing happens between frames.

class RayReconstructionDemo(val renderer: Renderer,val size: ReconstructionSize=ReconstructionSize(1920,1080),
    val name: String="ray-reconstruction") : Target,AutoCloseable {

enum class Path { RAW, SPATIAL, SPATIAL_DLAA, RAY_RECONSTRUCTION }

val scene=DirectLightingDemo(renderer,size.width,size.height)

val settings

    val settings=RtxdiSettings({scene.bundle.scene.camera.modelView()},{scene.bundle.scene.projection.evaluateUniform()})
        .apply { temporal=true;spatialSamples=4;denoisePasses=0 }

val rr=DlssReconstruction(renderer,size,size,ReconstructionMode.DLAA,rayReconstruction=true)

val sr=DlssReconstruction(renderer,size,size,ReconstructionMode.DLAA)

var path

    var path=Path.RAY_RECONSTRUCTION
        set(value) { if(field!=value){field=value;reset("pipeline switch")};showOutput() }

var animate=true

var cameraMotion=true

var time=0.0

val raw get()=scene.lighting.output

val output get()

    val output get()=when(path){Path.RAY_RECONSTRUCTION->rr.output;Path.SPATIAL_DLAA->sr.output;else->raw}
    private val guides=RenderToTexture(renderer) {
        width=size.width;height=size.height;colorAttachments=5;useDepth=false;debugName="$name-guides"
        colorFormats=listOf(VK_FORMAT_R32_SFLOAT,VK_FORMAT_R32G32_SFLOAT,VK_FORMAT_R16G16B16A16_SFLOAT,VK_FORMAT_R16G16B16A16_SFLOAT,VK_FORMAT_R16G16B16A16_SFLOAT)
    }

val depth get()=guides.imageViewArray[0]

val motion get()=guides.imageViewArray[1]

val diffuseAlbedo get()=guides.imageViewArray[2]

val specularAlbedo get()=guides.imageViewArray[3]

val normalRoughness get()=guides.imageViewArray[4]

    private val sampler=TextureSampler(renderer,1,false,filter=VK_FILTER_NEAREST)
    private val convert=Module(parameters(renderer) {
        this.name="$name-guides";+guides;depthTest=false;depthWrite=false;blend=false
        vertex=VERTEX;fragment=FRAGMENT
        val sources=mapOf("positions" to scene.lighting.surfaces.position,"normals" to scene.lighting.surfaces.normal,
            "albedo" to scene.lighting.surfaces.albedo,"velocity" to scene.lighting.surfaces.motion)
        textureResolver={ tex -> sources[tex.name]?.let { fromViews(listOf(it),tex.name,sampler) } }
    })
    private var frame=0L
    private var revision=0L
    private var closed=false

init

    init {
        scene.lighting.enableRtxdi(RtxdiProgram.load(),settings).getOrThrow()
        scene.lighting.mode=DirectLighting.Mode.RTXDI
        scene.lighting.samples=8
        check(convert.valid)
        val p=convert.pointer();val mesh=Mesh(renderer,p);mesh.resize(3,0,p);convert.meshes["quad"]=mesh;guides.modules["convert"]=p
        // Own the ordering of these existing passes, rather than running Fabricate's unrelated post effects.
        scene.bundle.frameGraph().passNames().forEach { renderer.renderList.preTargets.remove(it) }
        renderer.renderList.preTargets.remove(scene.lighting.name)
        renderer.renderList.modules.remove("direct-light-demo")
        renderer.renderList.preTargets[name]=this;showOutput()
    }
    private fun showOutput() { output.blitToScreen(name=name) }

fun reset(reason: String="camera cut or edit")

    fun reset(reason: String="camera cut or edit") {
        revision++;rr.reset(reason);sr.reset(reason);settings.resetHistory(reason);scene.bundle.invalidateFrameHistory()
    }

override fun render(command: CommandBuffer,index: Int)

    override fun render(command: CommandBuffer,index: Int) {
        if(closed)return
        val jitter=if(path==Path.RAW || path==Path.SPATIAL)ReconstructionJitter(0f,0f) else ReconstructionJitter.halton(frame,size,size)
        scene.bundle.scene.rasterJitterNdc.set(2*jitter.x/size.width,-2*jitter.y/size.height)
        if(animate)scene.animate(time)
        if(animate || cameraMotion)time+=1.0/60
        if(cameraMotion)scene.bundle.scene.camera.at.x=(5+kotlin.math.sin(time*.4)*1.5).toFloat()
        settings.commitLights(listOf(IdentifiedAreaLight(1,scene.lighting.emitter)))
        settings.denoisePasses=if(path==Path.SPATIAL || path==Path.SPATIAL_DLAA)3 else 0
        scene.bundle.gBuffer.render(command,index);scene.lighting.render(command,index);guides.render(command,index)
        val inputs=ReconstructionInputs(raw,depth,motion)
        val state=ReconstructionFrame(frame,jitter,revision=revision)
        when(path) {
            Path.RAY_RECONSTRUCTION->rr.evaluate(command,inputs,state,RayReconstructionGuides(diffuseAlbedo,specularAlbedo,normalRoughness,
                scene.bundle.scene.camera.modelView(),scene.bundle.scene.projection.evaluateUniform()))
            Path.SPATIAL_DLAA->sr.evaluate(command,inputs,state)
            else->Unit
        }
        frame++
    }

override fun close()

    override fun close() {
        if(closed)return;closed=true
        renderer.renderList.preTargets.remove(name,this);renderer.renderList.modules.remove(name)
        rr.close();sr.close();scene.close()
        renderer.cleanup+=Runnable { convert.cleanup();sampler.cleanup();guides.imageViewArray.forEach { it.imageView.cleanup() }
            guides.colorImageRefs.forEach { it?.cleanup() };guides.defaultSampler?.cleanup() }
    }

companion object

    companion object {
        private const val VERTEX="""#version 460
        void main(){vec2 p=vec2((gl_VertexIndex<<1)&2,gl_VertexIndex&2);gl_Position=vec4(p*2-1,0,1);}
        """
        private const val FRAGMENT="""#version 460
        layout(set=0,binding=0) uniform sampler2D positions;
        layout(set=0,binding=1) uniform sampler2D normals;
        layout(set=0,binding=2) uniform sampler2D albedo;
        layout(set=0,binding=3) uniform sampler2D velocity;
        layout(location=0) out float depth;
        layout(location=1) out vec2 motion;
        layout(location=2) out vec4 diffuse;
        layout(location=3) out vec4 specular;
        layout(location=4) out vec4 normalRoughness;
        void main(){ivec2 p=ivec2(gl_FragCoord.xy);vec4 pos=texelFetch(positions,p,0);
            depth=pos.w;motion=-texelFetch(velocity,p,0).xy*vec2(textureSize(velocity,0));
            vec3 n=texelFetch(normals,p,0).xyz;bool valid=pos.w<1 && dot(n,n)>.01;
            diffuse=vec4(valid?texelFetch(albedo,p,0).rgb:vec3(0),1);
            specular=vec4(0,0,0,1);normalRoughness=vec4(valid?normalize(n):vec3(0,0,1),1);
        }
        """
    }
}