From 262494b8ad0cab3465d5a5125c64493401ea0137 Mon Sep 17 00:00:00 2001 From: Max Thiele Date: Wed, 13 May 2026 23:47:07 +0200 Subject: [PATCH 01/27] Initial test pipeline with somewhat working temporal filtering --- .../kool/demo/deferred2/Deferred2Pipeline.kt | 113 ++++++++ .../kool/demo/deferred2/Deferred2Test.kt | 170 +++++++++++ .../fabmax/kool/demo/deferred2/GbufferPass.kt | 273 ++++++++++++++++++ .../kool/demo/deferred2/LightingPass.kt | 156 ++++++++++ .../demo/deferred2/ProjectionFunctions.kt | 78 +++++ .../kool/demo/deferred2/TemporalFilterPass.kt | 217 ++++++++++++++ 6 files changed, 1007 insertions(+) create mode 100644 kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt create mode 100644 kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt create mode 100644 kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt create mode 100644 kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt create mode 100644 kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/ProjectionFunctions.kt create mode 100644 kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt new file mode 100644 index 000000000..6d72e73de --- /dev/null +++ b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt @@ -0,0 +1,113 @@ +package de.fabmax.kool.demo.deferred2 + +import de.fabmax.kool.demo.deferred2.GbufferPass.Companion.TSAA_PATTERN_4 +import de.fabmax.kool.math.Vec2i +import de.fabmax.kool.pipeline.BufferedImageData2d +import de.fabmax.kool.pipeline.TexFormat +import de.fabmax.kool.pipeline.Texture2d +import de.fabmax.kool.scene.Lighting +import de.fabmax.kool.scene.Node +import de.fabmax.kool.scene.PerspectiveCamera +import de.fabmax.kool.scene.Scene +import de.fabmax.kool.util.KoolDispatchers +import de.fabmax.kool.util.Uint8Buffer +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.coroutines.yield + +private const val renderScale = 0.75f +val tsaa = TSAA_PATTERN_4 + +class Deferred2Pipeline( + val content: Node, + val lighting: Lighting = Lighting(), + scene: Scene, +) { + val camera = PerspectiveCamera() + + val gbuffers = AlternatingPair { + GbufferPass(content, camera, scene.renderSize) + } + + // fixme: think of something better for providing a camera + //val sceneCam get() = gbufferPass.camera + + val lightingPass = LightingPass( + gbuffers = gbuffers, + camera = camera, + lighting = lighting, + size = scene.renderSize, + ) + val filterPass = TemporalFilterPass( + lightingOutput = lightingPass.lightingOutput, + gbuffers = gbuffers, + camera = camera, + size = scene.renderSize, + ) + + init { + scene.addOffscreenPass(gbuffers.a) + scene.addOffscreenPass(gbuffers.b) + scene.addOffscreenPass(lightingPass) + scene.addComputePass(filterPass) + + var size = scene.renderSize + scene.onRenderScene += { + val newSize = scene.renderSize + if (size != newSize) { + size = newSize + gbuffers.a.setSize(size.x, size.y) + gbuffers.b.setSize(size.x, size.y) + lightingPass.setSize(size.x, size.y) + filterPass.resize(size) + } + + + } + + scene.coroutineScope.launch { + withContext(KoolDispatchers.Synced) { + while (true) { + gbuffers.newVal.isEnabled = true + gbuffers.oldVal.isEnabled = false + lightingPass.swapBuffers() + filterPass.swapBuffers() + yield() + } + } + } + } + + private val Scene.renderSize: Vec2i get() = Vec2i( + (mainRenderPass.viewport.width * renderScale).toInt().coerceAtLeast(1), + (mainRenderPass.viewport.height * renderScale).toInt().coerceAtLeast(1) + ) +} + +fun makeDitherPattern(): Texture2d { + val buf = Uint8Buffer(16) + fun u(i: Int): UByte = (255f * i.toFloat() / (buf.capacity - 1)).toInt().toUByte() + + buf[0] = u(0) + buf[1] = u(9) + buf[2] = u(3) + buf[3] = u(11) + + buf[4] = u(13) + buf[5] = u(5) + buf[6] = u(15) + buf[7] = u(7) + + buf[8] = u(4) + buf[9] = u(12) + buf[10] = u(2) + buf[11] = u(10) + + buf[12] = u(16) + buf[13] = u(8) + buf[14] = u(14) + buf[15] = u(6) + + val data = BufferedImageData2d(buf, 4, 4, TexFormat.R) + return Texture2d(data) +} diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt new file mode 100644 index 000000000..31725765f --- /dev/null +++ b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt @@ -0,0 +1,170 @@ +package de.fabmax.kool.demo.deferred2 + +import de.fabmax.kool.Assets +import de.fabmax.kool.KoolApplication +import de.fabmax.kool.addScene +import de.fabmax.kool.loadTexture2d +import de.fabmax.kool.math.Vec3f +import de.fabmax.kool.math.deg +import de.fabmax.kool.modules.ksl.KslShader +import de.fabmax.kool.modules.ksl.blocks.ColorSpaceConversion +import de.fabmax.kool.modules.ksl.blocks.convertColorSpace +import de.fabmax.kool.modules.ksl.lang.* +import de.fabmax.kool.pipeline.FullscreenShaderUtil.fullscreenQuadVertexStage +import de.fabmax.kool.pipeline.FullscreenShaderUtil.generateFullscreenQuad +import de.fabmax.kool.pipeline.swapPipelineDataCapturing +import de.fabmax.kool.scene.* +import de.fabmax.kool.util.Color +import de.fabmax.kool.util.MdColor +import de.fabmax.kool.util.Time +import de.fabmax.kool.util.debugOverlay + +suspend fun KoolApplication.deferred2Test() { + val texAlbedo = Assets.loadTexture2d("materials/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_COL_2K_METALNESS.jpg").getOrThrow() + val texNormal = Assets.loadTexture2d("materials/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_NRM_2K_METALNESS.jpg").getOrThrow() + + addScene { + val content = Node().apply { + val gShader = GbufferShader(GbufferShaderConfig.Builder().apply { color { vertexColor() } }.build()) + addColorMesh { + generate { + color = Color.WHITE + cube { origin.set(-2.5f, 0f, 0f)} + } + onUpdate { +// transform.rotate(360f.deg * Time.deltaT, Vec3f.Y_AXIS) + } + shader = gShader + } + addTextureMesh(isNormalMapped = true) { + generate { + cube { origin.set(0f, 0f, 0f) } + } + onUpdate { + transform + .setIdentity() + .rotate(90f.deg * Time.gameTime.toFloat(), Vec3f.Y_AXIS) + .translate(2.5f, 0f, 0f) + } + shader = GbufferShader(GbufferShaderConfig.Builder().apply { + color { + textureColor(texAlbedo) + } + normalMapping { + useNormalMap(texNormal) + } + }.build()).apply { + bindTexture2d("tbaseColor", texAlbedo) + bindTexture2d("tNormalMap", texNormal) + } + } + addColorMesh { + generate { + cube { colored() } + } + onUpdate { + transform.rotate(90f.deg * Time.deltaT, Vec3f.Y_AXIS) + } + shader = gShader + } + addColorMesh { + generate { + translate(0f, -2f, 0f) + color = MdColor.PURPLE //Color.WHITE + grid { + sizeX = 50f + sizeY = 50f + } + } + shader = gShader + } + } + + val lighting = Lighting().apply { + singlePointLight { + //setup(Vec3f(3f, 4f, 2f)) + //setColor(MdColor.AMBER.toLinear(), intensity = 30f) + setup(Vec3f(1.2f, 1.2f, 2f)) + setColor(Color.WHITE, intensity = 5f) + } + } + val deferred2Pipeline = Deferred2Pipeline(content, lighting, this) + + content.apply { + val orbitCam = orbitCamera(deferred2Pipeline.camera) { } + addNode(orbitCam) + } + +// val matPass = GbufferPass(content) +// addOffscreenPass(matPass) +// content.apply { +// orbitCamera(matPass.defaultView) { } +// } +// +// val lighting = Lighting().apply { +// singlePointLight { +// //setup(Vec3f(3f, 4f, 2f)) +// //setColor(MdColor.AMBER.toLinear(), intensity = 30f) +// setup(Vec3f(1.2f, 1.2f, 2f)) +// setColor(Color.WHITE, intensity = 5f) +// } +// } +// +// val lightingPass = DeferredLightingPass( +// depth = matPass.depth, +// encodedNormalsMeta = matPass.encodedNormalsMeta, +// albedoEmission = matPass.albedoEmission, +// metalRoughnessAo = matPass.metalRoughnessAo, +// sceneCam = matPass.camera, +// lighting = lighting, +// ) +// addComputePass(lightingPass) + + addTextureMesh { + generate { + generateFullscreenQuad() + } + val outShader = KslShader("deferred2-output") { + val uv = interStageFloat2() + fullscreenQuadVertexStage(uv) + fragmentStage { + main { + val output = texture2d("deferredOutput") + val uvi = (uv.output * output.size().toFloat2()).toInt2() + val color by output.load(uvi).rgb + + + val ditherTex = texture2d("ditherPattern") + //val ditherC by baseCoord % ditherTex.size() + val ditherNoise by ditherTex.load(uvi).r + val srgb by convertColorSpace(color, ColorSpaceConversion.LinearToSrgbHdr()) + (ditherNoise - 0.5f.const) / 255f.const + + + //color set convertColorSpace(color, ColorSpaceConversion.LinearToSrgbHdr()) + colorOutput(srgb) + } + } + } + + outShader.bindTexture2d("ditherPattern", makeDitherPattern()) + var inputTex by outShader.bindTexture2d("deferredOutput", deferred2Pipeline.lightingPass.lightingOutput) + onUpdate { + outShader.swapPipelineDataCapturing(deferred2Pipeline.filterPass.filterOutput.newVal) { + inputTex = deferred2Pipeline.filterPass.filterOutput.newVal + } + } + + shader = outShader + } + } + + ctx.scenes += debugOverlay() +} + +class AlternatingPair(factory: () -> T) { + val a: T = factory() + val b: T = factory() + + val newVal: T get() = if (Time.frameCount % 2 == 0) a else b + val oldVal: T get() = if (Time.frameCount % 2 == 0) b else a +} \ No newline at end of file diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt new file mode 100644 index 000000000..b1a1ccd93 --- /dev/null +++ b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt @@ -0,0 +1,273 @@ +package de.fabmax.kool.demo.deferred2 + +import de.fabmax.kool.math.MutableMat4f +import de.fabmax.kool.math.Vec2f +import de.fabmax.kool.math.Vec2i +import de.fabmax.kool.modules.ksl.BasicVertexConfig +import de.fabmax.kool.modules.ksl.KslShader +import de.fabmax.kool.modules.ksl.LightingConfig +import de.fabmax.kool.modules.ksl.blocks.* +import de.fabmax.kool.modules.ksl.lang.* +import de.fabmax.kool.pipeline.* +import de.fabmax.kool.pipeline.shading.AlphaMode +import de.fabmax.kool.scene.Camera +import de.fabmax.kool.scene.Node +import de.fabmax.kool.scene.VertexLayouts +import de.fabmax.kool.scene.vertexAttrib +import de.fabmax.kool.util.Color +import de.fabmax.kool.util.Time + +class GbufferPass(content: Node, camera: Camera, initialSize: Vec2i) : OffscreenPass2d( + drawNode = content, + attachmentConfig = AttachmentConfig { + addColor(TexFormat.R_I32, filterMethod = FilterMethod.NEAREST) // meta data + addColor(TexFormat.R_I32, filterMethod = FilterMethod.NEAREST) // encoded normals + addColor(TexFormat.RGBA, filterMethod = FilterMethod.NEAREST) // albedo, a * 255 = emission strength + addColor(TexFormat.RGBA, filterMethod = FilterMethod.NEAREST) // metal, roughness, ao + defaultDepth() + }, + initialSize = initialSize, + name = "deferred2-gbuffer-pass" +) { + val meta get() = colorTextures[0] + val encodedNormals get() = colorTextures[1] + val albedoEmission get() = colorTextures[2] + val metalRoughnessAo get() = colorTextures[3] + + val depth get() = depthTexture!! + + init { + this.camera = camera + + val offsetMat = MutableMat4f() + camera.onCameraUpdated += { + val offset = tsaa[Time.frameCount % tsaa.size] + offsetMat.setIdentity().translate(offset.x / width, offset.y / height, 0f).mul(camera.proj) + camera.proj.set(offsetMat) + } + } + + companion object { + private val s = 1f/16f + val TSAA_PATTERN_NONE = listOf(Vec2f.ZERO) + val TSAA_PATTERN_4 = listOf( + Vec2f(-2 * s, -6 * s), + Vec2f(6 * s, -2 * s), + Vec2f(-6 * s, -2 * s), + Vec2f(2 * s, 6 * s), + ) + val TSAA_PATTERN_8 = listOf( + Vec2f(1 * s, -3 * s), + Vec2f(7 * s, -7 * s), + Vec2f(-1 * s, 3 * s), + Vec2f(5 * s, 1 * s), + Vec2f(3 * s, 7 * s), + Vec2f(-3 * s, -5 * s), + Vec2f(-7 * s, -1 * s), + Vec2f(-5 * s, -5 * s), + ) + val TSAA_PATTERN_16 = listOf( + Vec2f(1 * s, 1 * s), + Vec2f(-5 * s, -2 * s), + Vec2f(-2 * s, 6 * s), + Vec2f(-8 * s, 0 * s), + + Vec2f(-1 * s, -3 * s), + Vec2f(2 * s, 5 * s), + Vec2f(0 * s, -7 * s), + Vec2f(7 * s, -4 * s), + + Vec2f(-3 * s, 2 * s), + Vec2f(5 * s, 3 * s), + Vec2f(-4 * s, -6 * s), + Vec2f(6 * s, 7 * s), + + Vec2f(4 * s, -1 * s), + Vec2f(3 * s, -5 * s), + Vec2f(-6 * s, 4 * s), + Vec2f(-7 * s, -8 * s), + ) + } +} + +class GbufferShader(val config: GbufferShaderConfig) : KslShader("deferred2-gbuffer-shader") { + init { + pipelineConfig = PipelineConfig(blendMode = BlendMode.DISABLED, cullMethod = config.cullMethod) + program.program() + config.modelCustomizer?.invoke(program) + } + + private fun KslProgram.program() { + val camData = cameraData() + val positionViewSpace = interStageFloat3("positionWorldSpace") + val normalViewSpace = interStageFloat3("normalWorldSpace") + var tangentViewSpace: KslInterStageVector? = null + + val texCoordBlock: TexCoordAttributeBlock + + vertexStage { + main { + val vertexBlock = vertexTransformBlock(config.vertexCfg) { + inLocalPos(vertexAttrib(VertexLayouts.Position.position)) + inLocalNormal(vertexAttrib(VertexLayouts.Normal.normal)) + + if (config.normalMapCfg.isNormalMapped) { + // if normal mapping is enabled, the input vertex data is expected to have a tangent attribute + inLocalTangent(vertexAttrib(VertexLayouts.Tangent.tangent)) + } + } + + // world position and normal are made available via ports for custom models to modify them + val worldPos = float3Port("worldPos", vertexBlock.outWorldPos) + val worldNormal = float3Port("worldNormal", vertexBlock.outWorldNormal) + + val viewPos by camData.viewMat * float4Value(worldPos, 1f) + outPosition set camData.projMat * viewPos + + positionViewSpace.input set viewPos.xyz + normalViewSpace.input set (camData.viewMat * float4Value(worldNormal, 0f)).xyz + + if (config.normalMapCfg.isNormalMapped) { + tangentViewSpace = interStageFloat4().apply { + input.xyz set (camData.viewMat * float4Value(vertexBlock.outWorldTangent.xyz, 0f)).xyz + input.w set vertexBlock.outWorldTangent.w + } + } + texCoordBlock = texCoordAttributeBlock() + } + } + fragmentStage { + main { + // determine main color (albedo) + val colorBlock = fragmentColorBlock(config.colorCfg) + val baseColor = float4Port("baseColor", colorBlock.outColor) + (config.alphaMode as? AlphaMode.Mask)?.let { + `if`(baseColor.a lt it.cutOff.const) { + discard() + } + } + val emissionBlock = fragmentPropertyBlock(config.emissionCfg) + val emissionStrength = float1Port("emissionStrength", emissionBlock.outProperty) + + val vertexNormal = float3Var(normalize(normalViewSpace.output)) + if (config.cullMethod.isBackVisible && config.vertexCfg.isFlipBacksideNormals) { + `if`(!inIsFrontFacing) { + vertexNormal *= (-1f).const3 + } + } + + // do normal map computations (if enabled) and adjust material block input normal accordingly + val bumpedNormal = if (config.normalMapCfg.isNormalMapped) { + val normalMapStrength = fragmentPropertyBlock(config.normalMapCfg.strengthCfg).outProperty + normalMapBlock(config.normalMapCfg) { + inTangentWorldSpace(tangentViewSpace!!.output) + inNormalWorldSpace(vertexNormal) + inStrength(normalMapStrength) + inTexCoords(texCoordBlock.getTextureCoords()) + }.outBumpNormal + } else { + vertexNormal + } + + val normal = float3Port("normal", bumpedNormal) + val roughness = float1Port("roughness", fragmentPropertyBlock(config.roughnessCfg).outProperty) + val metallic = float1Port("metallic", fragmentPropertyBlock(config.metallicCfg).outProperty) + val aoFactor = float1Port("aoFactor", fragmentPropertyBlock(config.aoCfg).outProperty) + + val normalHashX by clamp(((sqrt(abs(normal.x)) * sign(normal.x) + 1f.const) * 8f.const).toInt1(), 0.const, 15.const) + val normalHashY by clamp(((sqrt(abs(normal.y)) * sign(normal.y) + 1f.const) * 8f.const).toInt1(), 0.const, 15.const) + + intOutput(int4Value(normalHashX or (normalHashY shl 4.const), 0.const, 0.const, 0.const), location = 0) + intOutput(int4Value(encodeNormal(normal), 0.const, 0.const, 0.const), location = 1) + colorOutput(float4Value(baseColor.rgb, emissionStrength), location = 2) + colorOutput(float4Value(metallic, roughness, aoFactor, 0f.const), location = 3) + } + } + } +} + +class GbufferShaderConfig(builder: Builder) { + val vertexCfg: BasicVertexConfig = builder.vertexCfg.build() + val colorCfg: ColorBlockConfig = builder.colorCfg.build() + val emissionCfg: PropertyBlockConfig = builder.emissionCfg.build() + val normalMapCfg: NormalMapConfig = builder.normalMapCfg.build() + val metallicCfg: PropertyBlockConfig = builder.metallicCfg.build() + val roughnessCfg: PropertyBlockConfig = builder.roughnessCfg.build() + val aoCfg: PropertyBlockConfig = builder.aoCfg.build() + // todo val parallaxCfg: ParallaxMapConfig = builder.parallaxCfg.build() + val lightingCfg: LightingConfig = builder.lightingCfg.build() + + val alphaMode: AlphaMode = builder.alphaMode + val cullMethod: CullMethod = builder.cullMethod + val materialFlags: Int = builder.materialFlags + + val modelCustomizer: (KslProgram.() -> Unit)? = builder.modelCustomizer + + open class Builder { + val vertexCfg = BasicVertexConfig.Builder() + val colorCfg = ColorBlockConfig.Builder("baseColor").constColor(Color.GRAY) + val emissionCfg = PropertyBlockConfig.Builder("emissionStrength").apply { constProperty(0f) } + val normalMapCfg = NormalMapConfig.Builder() + val metallicCfg = PropertyBlockConfig.Builder("metallic").apply { constProperty(0f) } + val roughnessCfg = PropertyBlockConfig.Builder("roughness").apply { constProperty(0.5f) } + val aoCfg = PropertyBlockConfig.Builder("ao").apply { constProperty(1f) } + // todo val parallaxCfg = ParallaxMapConfig.Builder() + val lightingCfg = LightingConfig.Builder() + + var alphaMode: AlphaMode = AlphaMode.Blend + var cullMethod: CullMethod = CullMethod.CULL_BACK_FACES + + var materialFlags = 0 + + var modelCustomizer: (KslProgram.() -> Unit)? = null + + fun enableSsao(ssaoMap: Texture2d? = null): Builder { + lightingCfg.enableSsao(ssaoMap) + return this + } + + inline fun metallic(block: PropertyBlockConfig.Builder.() -> Unit) { + metallicCfg.block() + } + + inline fun roughness(block: PropertyBlockConfig.Builder.() -> Unit) { + roughnessCfg.block() + } + + inline fun ao(block: PropertyBlockConfig.Builder.() -> Unit) { + aoCfg.block() + } + + inline fun color(block: ColorBlockConfig.Builder.() -> Unit) { + colorCfg.colorSources.clear() + colorCfg.block() + } + + inline fun emission(block: PropertyBlockConfig.Builder.() -> Unit) { + emissionCfg.block() + } + + inline fun lighting(block: LightingConfig.Builder.() -> Unit) { + lightingCfg.block() + } + + inline fun normalMapping(block: NormalMapConfig.Builder.() -> Unit) { + normalMapCfg.block() + } + +// inline fun parallaxMapping(block: ParallaxMapConfig.Builder.() -> Unit) { +// parallaxCfg.block() +// } + + inline fun vertices(block: BasicVertexConfig.Builder.() -> Unit) { + vertexCfg.block() + } + + open fun build() = GbufferShaderConfig(this) + } + + companion object { + const val MATERIAL_FLAG_ALWAYS_LIT = 1 + const val MATERIAL_FLAG_IS_MOVING = 2 + } +} diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt new file mode 100644 index 000000000..56814c868 --- /dev/null +++ b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt @@ -0,0 +1,156 @@ +package de.fabmax.kool.demo.deferred2 + +import de.fabmax.kool.math.Vec2i +import de.fabmax.kool.math.Vec4f +import de.fabmax.kool.modules.ksl.KslShader +import de.fabmax.kool.modules.ksl.blocks.LightDataStruct +import de.fabmax.kool.modules.ksl.blocks.getLightDirectionFromFragPos +import de.fabmax.kool.modules.ksl.blocks.getLightRadiance +import de.fabmax.kool.modules.ksl.blocks.setLightData +import de.fabmax.kool.modules.ksl.lang.* +import de.fabmax.kool.pipeline.* +import de.fabmax.kool.pipeline.FullscreenShaderUtil.fullscreenQuadVertexStage +import de.fabmax.kool.pipeline.FullscreenShaderUtil.generateFullscreenQuad +import de.fabmax.kool.scene.* +import de.fabmax.kool.util.MemoryLayout +import de.fabmax.kool.util.Struct + +class LightingPass( + val gbuffers: AlternatingPair, +// val depth: Texture2d, +// val encodedNormalsMeta: Texture2d, +// val albedoEmission: Texture2d, +// val metalRoughnessAo: Texture2d, + camera: Camera, + lighting: Lighting?, + size: Vec2i, +) : OffscreenPass2d( + drawNode = Node(), + attachmentConfig = AttachmentConfig { + addColor(TexFormat.RG11B10_F, filterMethod = FilterMethod.NEAREST) // metal, roughness, ao + transientDepth() + }, + initialSize = size, + name = "deferred2-lighting-pass" +) { +// val lightingOutput = AlternatingPair { +// StorageTexture2d(size.x, size.y, TexFormat.RG11B10_F, samplerSettings = SamplerSettings().clamped().nearest()) +// } + + val lightingOutput: Texture2d get() = colorTexture!! + + private val lightingShader = DeferredLightingShader( +// depth = depth, +// encodedNormalsMeta = encodedNormalsMeta, +// albedoEmission = albedoEmission, +// metalRoughnessAo = metalRoughnessAo, + ) + + init { + this.camera = camera + this.lighting = lighting + + val outputMesh = Mesh(VertexLayouts.PositionTexCoord).apply { + generateFullscreenQuad() + shader = lightingShader + } + drawNode.addNode(outputMesh) + } + + fun swapBuffers() { + val newGbuffer = gbuffers.oldVal + lightingShader.swapPipelineDataCapturing(newGbuffer) { + depthTex = newGbuffer.depth + encodedNormals = newGbuffer.encodedNormals + albedoEmissionTex = newGbuffer.albedoEmission + metalRoughnessAoTex = newGbuffer.metalRoughnessAo + + camData.set { + set(it.proj, camera.proj) + set(it.invProj, camera.invProj) + set(it.invView, camera.invView) + set(it.viewport, Vec4f(0f, 0f, width.toFloat(), height.toFloat())) + set(it.position, camera.globalPos) + set(it.camNear, camera.clipNear) + } + lightData.set { + setLightData(lighting, maxLightCount = 4, it) + } + } + } +} + +class DeferredLightingShader : KslShader("deferred2-lighting") { + var depthTex by bindTexture2d("depth") + var encodedNormals by bindTexture2d("encodedNormals") + var albedoEmissionTex by bindTexture2d("albedoEmission") + var metalRoughnessAoTex by bindTexture2d("metalRoughnessAo") + + val camData = bindUniformStruct("deferredCamData", DeferredCamDataStruct) + private val lightDataStruct = LightDataStruct(4) + val lightData = bindUniformStruct("lightData", lightDataStruct) + + init { + pipelineConfig = PipelineConfig( + blendMode = BlendMode.DISABLED, + cullMethod = CullMethod.NO_CULLING, + depthTest = DepthCompareOp.ALWAYS + ) + program.program() + } + + private fun KslProgram.program() { + val uv = interStageFloat2() + fullscreenQuadVertexStage(uv) + fragmentStage { + val depth = texture2d("depth", isUnfilterable = true) + val encodedNormals = texture2dInt("encodedNormals") + val albedoEmission = texture2d("albedoEmission") + val metalRoughnessAo = texture2d("metalRoughnessAo") + + val camData = uniformStruct("deferredCamData", DeferredCamDataStruct) + val lightData = uniformStruct("lightData", lightDataStruct) + + main { + val baseCoord = (uv.output * depth.size().toFloat2()).toInt2() + val camNear = camData[DeferredCamDataStruct.camNear] + val invProj = camData[DeferredCamDataStruct.invProj] + val invView = camData[DeferredCamDataStruct.invView] + val worldPos by unprojectBaseCoord(depth, baseCoord, camNear, invProj, invView).xyz + + val encNormalMeta by encodedNormals.load(baseCoord, lod = 0.const).xy + val viewNormal by decodeNormal(encNormalMeta.x) + val worldNormal by (camData[DeferredCamDataStruct.invView] * float4Value(viewNormal, 0f)).xyz + + val albedoEmission = float4Var(albedoEmission.load(baseCoord, lod = 0.const)) + val albedo = albedoEmission.xyz + val emission = albedoEmission.w + + val metalRoughnessAo by metalRoughnessAo.load(baseCoord, lod = 0.const).xyz + val metallic = metalRoughnessAo.x + val roughness = metalRoughnessAo.y + val ao = metalRoughnessAo.z + + val lightPos by lightData[lightDataStruct.encodedPositions][0.const] + val lightDir by lightData[lightDataStruct.encodedDirections][0.const] + val lightColor by lightData[lightDataStruct.encodedColors][0.const] + val dirToLight by normalize(getLightDirectionFromFragPos(worldPos, lightPos)) + val radiance by getLightRadiance(worldPos, lightPos, lightDir, lightColor) + + val ambient by 0.04f.const + val diffuse by albedo * ambient + albedo * radiance * saturate(dot(dirToLight, worldNormal)) + + colorOutput(diffuse) + } + } + } +} + +object DeferredCamDataStruct : Struct("DeferredCameraData", MemoryLayout.Std140) { + val proj = mat4("projMat") + val invProj = mat4("invProjMat") + val invView = mat4("invView") + val viewport = float4("viewport") + val position = float3("position") + val camNear = float1("camClipNear") +} diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/ProjectionFunctions.kt b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/ProjectionFunctions.kt new file mode 100644 index 000000000..a11c4c5a3 --- /dev/null +++ b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/ProjectionFunctions.kt @@ -0,0 +1,78 @@ +package de.fabmax.kool.demo.deferred2 + +import de.fabmax.kool.modules.ksl.blocks.getLinearDepthReversed +import de.fabmax.kool.modules.ksl.lang.* + +context(scope: KslScopeBuilder) +fun baseCoordToUv(baseCoord: KslExprInt2, viewSize: KslExprInt2): KslExprFloat2 { + val uv = float2Var((baseCoord.toFloat2() + 0.5f.const2) / viewSize.toFloat2()) + uv.y set 1f.const - uv.y + return uv +} + +class UnprojectBaseCoord( + depth: KslUniform, + parentScope: KslScopeBuilder +) : KslFunction("fnUnprojectBaseCoord", KslFloat4, parentScope.parentStage) { + init { + val baseCoord = paramInt2("baseCoord") + val camNear = paramFloat1("camNear") + val invProj = paramMat4("invProj") + val invView = paramMat4("invView") + + body { + val uv by baseCoordToUv(baseCoord, depth.size()) + val viewDepth by getLinearDepthReversed(depth.load(baseCoord, lod = 0.const).x, camNear) + val viewProjXy by (uv * 2f.const - 1f.const) * viewDepth + val viewProjPos by float4Value(viewProjXy, camNear, viewDepth) + val viewPos by invProj * viewProjPos + val worldPos by invView * float4Value(viewPos.xyz, 1f) + worldPos + } + } +} + +fun KslScopeBuilder.unprojectBaseCoord( + depth: KslUniform, + baseCoord: KslExprInt2, + camNear: KslExprFloat1, + invProj: KslExprMat4, + invView: KslExprMat4, +): KslExprFloat4 { + val func = parentStage.getOrCreateFunction("fnUnprojectBaseCoord") { UnprojectBaseCoord(depth, this) } + return func(baseCoord, camNear, invProj, invView) +} + + +class UnprojectBUv( + depth: KslUniform, + parentScope: KslScopeBuilder +) : KslFunction("fnUnprojectUv", KslFloat4, parentScope.parentStage) { + init { + val uv = paramFloat2("uv") + val camNear = paramFloat1("camNear") + val invProj = paramMat4("invProj") + val invView = paramMat4("invView") + + body { + // todo: need to invert uv.y? + val viewDepth by getLinearDepthReversed(depth.sample(uv, lod = 0f.const).x, camNear) + val viewProjXy by (uv * 2f.const - 1f.const) * viewDepth + val viewProjPos by float4Value(viewProjXy, camNear, viewDepth) + val viewPos by invProj * viewProjPos + val worldPos by invView * float4Value(viewPos.xyz, 1f) + worldPos + } + } +} + +fun KslScopeBuilder.unprojectUv( + depth: KslUniform, + uv: KslExprFloat2, + camNear: KslExprFloat1, + invProj: KslExprMat4, + invView: KslExprMat4, +): KslExprFloat4 { + val func = parentStage.getOrCreateFunction("fnUnprojectUv") { UnprojectBUv(depth, this) } + return func(uv, camNear, invProj, invView) +} \ No newline at end of file diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt new file mode 100644 index 000000000..8d93672a2 --- /dev/null +++ b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt @@ -0,0 +1,217 @@ +package de.fabmax.kool.demo.deferred2 + +import de.fabmax.kool.math.Vec2i +import de.fabmax.kool.math.Vec3i +import de.fabmax.kool.modules.ksl.KslComputeShader +import de.fabmax.kool.modules.ksl.blocks.ColorSpaceConversion +import de.fabmax.kool.modules.ksl.blocks.convertColorSpace +import de.fabmax.kool.modules.ksl.lang.* +import de.fabmax.kool.pipeline.* +import de.fabmax.kool.scene.Camera +import de.fabmax.kool.util.MemoryLayout +import de.fabmax.kool.util.Struct +import de.fabmax.kool.util.Time + +class TemporalFilterPass( + val lightingOutput: Texture2d, + val gbuffers: AlternatingPair, + val camera: Camera, + private var size: Vec2i, + filterStorageFmt: TexFormat = TexFormat.RGBA_F16, +) : ComputePass("deferred2-lighting-pass") { + val filterOutput = AlternatingPair { + StorageTexture2d(size.x, size.y, filterStorageFmt, samplerSettings = SamplerSettings().clamped().nearest()) + } + private val filterState = StorageTexture2d(size.x, size.y, TexFormat.R, samplerSettings = SamplerSettings().clamped().nearest()) + + private val temporalShader = TemporalFilterShader(filterStorageFmt, lightingOutput, filterState) + + init { + setupPasses() + } + + fun resize(size: Vec2i) { + this.size = size + filterOutput.a.resize(size.x, size.y) + filterOutput.b.resize(size.x, size.y) + filterState.resize(size.x, size.y) + setupPasses() + } + + private fun setupPasses() { + clearAndReleaseTasks() + val groupsX = (size.x + 7) / 8 + val groupsY = (size.y + 7) / 8 + addTask(temporalShader, Vec3i(groupsX, groupsY, 1)) + } + + fun swapBuffers() { + temporalShader.swapPipelineDataCapturing(filterOutput.newVal) { + oldAlbedo = gbuffers.oldVal.albedoEmission + newAlbedo = gbuffers.newVal.albedoEmission + oldMeta = gbuffers.oldVal.meta + newMeta = gbuffers.newVal.meta + oldFilter = filterOutput.oldVal + newFilter = filterOutput.newVal + frameI = Time.frameCount + } + } +} + +class TemporalFilterShader( + val filterStorageFmt: TexFormat, + lightingOutput: Texture2d, + filterState: StorageTexture2d, +) : KslComputeShader("deferred2-temporal-filter") { + var lightingOutput by bindTexture2d("lightingOutput", lightingOutput) + var oldAlbedo by bindTexture2d("oldAlbedo") + var newAlbedo by bindTexture2d("newAlbedo") + var oldMeta by bindTexture2d("oldMeta") + var newMeta by bindTexture2d("newMeta") + +// var oldDepth by bindTexture2d("oldDepth") +// var newDepth by bindTexture2d("newDepth") +// var camData = bindUniformStruct("camData", NewOldCamDataStruct) + + var oldFilter by bindStorageTexture2d("oldFilter") + var newFilter by bindStorageTexture2d("newFilter") + var filterState by bindStorageTexture2d("filterState", filterState) + + var frameI by bindUniformInt1("frameI") + + init { + program.program() + } + + private fun KslProgram.program() { + computeStage(workGroupSizeX = 8, workGroupSizeY = 8) { + val lightingOutput = texture2d("lightingOutput") + val oldAlbedo = texture2d("oldAlbedo") + val newAlbedo = texture2d("newAlbedo") + val oldMeta = texture2dInt("oldMeta") + val newMeta = texture2dInt("newMeta") + + val oldFilter = if (filterStorageFmt.channels == 3) { + storageTexture2d("oldFilter", filterStorageFmt) + } else { + storageTexture2d("oldFilter", filterStorageFmt) + } + val newFilter = if (filterStorageFmt.channels == 3) { + storageTexture2d("newFilter", filterStorageFmt) + } else { + storageTexture2d("newFilter", filterStorageFmt) + } + + val filterState = storageTexture2d("filterState", TexFormat.R) + + val frameI = uniformInt1("frameI") + +// val oldDepth = texture2d("oldDepth", isUnfilterable = true) +// val newDepth = texture2d("newDepth", isUnfilterable = true) +// val camData = uniformStruct("camData", NewOldCamDataStruct) + + val metaEqual = functionBool1("fnMetaEqual") { + val meta1 = paramInt1() + val meta2 = paramInt1() + + body { + val x1 by meta1 and 0xf.const + val y1 by (meta1 shr 4.const) and 0xf.const + val x2 by meta2 and 0xf.const + val y2 by (meta2 shr 4.const) and 0xf.const + (abs(x1 - x2) le 3.const) and (abs(y1 - y2) le 3.const) + } + } + + main { + val baseCoord by inGlobalInvocationId.xy.toInt2() +// val uv by baseCoordToUv(baseCoord, lightingOutput.size()) + val state by (filterState.load(baseCoord).r * 255f.const).toInt1() +// val filterNoise by noise31(uint3Value(inGlobalInvocationId.xy, frameI.toUint1())) + +// val camNear = camData[NewOldCamDataStruct.newCam][DeferredCamDataStruct.camNear] +// val invProj = camData[NewOldCamDataStruct.newCam][DeferredCamDataStruct.invProj] +// val invView = camData[NewOldCamDataStruct.newCam][DeferredCamDataStruct.invView] +// val newWorldPos by unprojectUv(newDepth, baseCoord, camNear, invProj, invView).xyz + + val curAlbedo by newAlbedo.load(baseCoord).rgb + val curMeta by newMeta.load(baseCoord).r + val colorDiff by length(curAlbedo - oldAlbedo.load(baseCoord).rgb) + val sameColorThresh = 0.08f.const //+ filterNoise * 0.01f.const + val sameColor by colorDiff lt sameColorThresh + val sameMeta by metaEqual(curMeta, oldMeta.load(baseCoord).r) + val filterHit by sameColor and sameMeta + + val wasEdge by state and 1.const gt 0.const + val isEdge by false.const + `if`(!filterHit or wasEdge) { + val da by length(curAlbedo - oldAlbedo.load(baseCoord + int2Value(-1, -1)).rgb) + val db by length(curAlbedo - oldAlbedo.load(baseCoord + int2Value(1, 1)).rgb) + val dc by length(curAlbedo - oldAlbedo.load(baseCoord + int2Value(1, -1)).rgb) + val dd by length(curAlbedo - oldAlbedo.load(baseCoord + int2Value(-1, 1)).rgb) + + val minD = min(colorDiff, min(min(da, db), min(dc, dd))) + val maxD = max(colorDiff, max(max(da, db), max(dc, dd))) + isEdge set ((minD lt sameColorThresh) and (maxD gt sameColorThresh)) + + val ma by metaEqual(curMeta, oldMeta.load(baseCoord + int2Value(-1, -1)).r) + val mb by metaEqual(curMeta, oldMeta.load(baseCoord + int2Value(1, 1)).r) + val mc by metaEqual(curMeta, oldMeta.load(baseCoord + int2Value(1, -1)).r) + val md by metaEqual(curMeta, oldMeta.load(baseCoord + int2Value(-1, 1)).r) + + val anyMtrue by ma or mb or mc or md + val anyMfalse by !(ma and mb and mc and md) + isEdge set (isEdge or (anyMtrue and anyMfalse)) + } + state set isEdge.toInt1() + filterState.store(baseCoord, float4Value(state.toFloat1() / 255f.const, 0f.const, 0f.const, 0f.const)) + +// val w by 4f.const + val w by 8f.const //- filterNoise * filterNoise * filterNoise * 4f.const +// val w by 16f.const - filterNoise * filterNoise * filterNoise * 14f.const +// val w by 32f.const - filterNoise * filterNoise * filterNoise * 16f.const +// val w by 100f.const - filterNoise * filterNoise * filterNoise * 75f.const + + `if`((!filterHit and !isEdge) or (wasEdge and !isEdge)) { + w set 0f.const + } + + val curColor by lightingOutput.load(baseCoord).rgb + val curSrgb by convertColorSpace(curColor, ColorSpaceConversion.LinearToSrgb()) + val oldSrgb by convertColorSpace(oldFilter.load(baseCoord).rgb, ColorSpaceConversion.LinearToSrgb()) + val weighted by (oldSrgb * w + curSrgb) / (w + 1f.const) + val filtered by convertColorSpace(weighted, ColorSpaceConversion.SrgbToLinear()) + + `if`(any(isNan(filtered))) { + filtered set curSrgb + } + +// val filteredColor by (old * w + curColor) / (w + 1f.const) + newFilter[baseCoord] = float4Value(filtered, 1f) +// newFilter[baseCoord] = float4Value(current, 1f) + +// val x1 by curMeta and 0xf.const +// val y1 by (curMeta shr 4.const) and 0xf.const +// newFilter[baseCoord] = float4Value(x1.toFloat1() / 15f.const, y1.toFloat1() / 15f.const, 0f.const, 1f.const) + +// newFilter[baseCoord] = float4Value(mix(curColor, curAlbedo, 0.5f.const), 1f) + +// newFilter[baseCoord] = float4Value(colorDiff, colorDiff, colorDiff, 1f.const) +// `if`(wasEdge and !isEdge) { +// newFilter[baseCoord] = Color.RED.const +// } +// `if`(isEdge) { +// newFilter[baseCoord] = Color.YELLOW.const +// } +// `if`(w eq 0f.const) { +// newFilter[baseCoord] = Color.CYAN.const +// } + } + } + } +} + +object NewOldCamDataStruct : Struct("NewOldCamData", MemoryLayout.Std140) { + val oldCam = struct(DeferredCamDataStruct) + val newCam = struct(DeferredCamDataStruct) +} From d07b990b2eebb057de728c5f55f90e4a90762c85 Mon Sep 17 00:00:00 2001 From: Max Thiele Date: Thu, 14 May 2026 13:00:36 +0200 Subject: [PATCH 02/27] Use rgba texture for normals --- .../kool/demo/deferred2/Deferred2Pipeline.kt | 2 +- .../kool/demo/deferred2/Deferred2Test.kt | 32 +++------------ .../fabmax/kool/demo/deferred2/GbufferPass.kt | 40 +++++++++---------- .../kool/demo/deferred2/LightingPass.kt | 7 ++-- .../kool/demo/deferred2/TemporalFilterPass.kt | 18 ++++----- 5 files changed, 38 insertions(+), 61 deletions(-) diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt index 6d72e73de..ad9f14031 100644 --- a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt +++ b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.yield -private const val renderScale = 0.75f +private const val renderScale = 1f val tsaa = TSAA_PATTERN_4 class Deferred2Pipeline( diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt index 31725765f..b421565c8 100644 --- a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt +++ b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt @@ -30,6 +30,10 @@ suspend fun KoolApplication.deferred2Test() { generate { color = Color.WHITE cube { origin.set(-2.5f, 0f, 0f)} + icoSphere { + center.set(-2.5f, 0f, 2.5f) + steps = 4 + } } onUpdate { // transform.rotate(360f.deg * Time.deltaT, Vec3f.Y_AXIS) @@ -43,7 +47,8 @@ suspend fun KoolApplication.deferred2Test() { onUpdate { transform .setIdentity() - .rotate(90f.deg * Time.gameTime.toFloat(), Vec3f.Y_AXIS) + .rotate(10f.deg * Time.gameTime.toFloat(), Vec3f.Y_AXIS) + //.rotate(20f.deg, Vec3f.Y_AXIS) .translate(2.5f, 0f, 0f) } shader = GbufferShader(GbufferShaderConfig.Builder().apply { @@ -95,31 +100,6 @@ suspend fun KoolApplication.deferred2Test() { addNode(orbitCam) } -// val matPass = GbufferPass(content) -// addOffscreenPass(matPass) -// content.apply { -// orbitCamera(matPass.defaultView) { } -// } -// -// val lighting = Lighting().apply { -// singlePointLight { -// //setup(Vec3f(3f, 4f, 2f)) -// //setColor(MdColor.AMBER.toLinear(), intensity = 30f) -// setup(Vec3f(1.2f, 1.2f, 2f)) -// setColor(Color.WHITE, intensity = 5f) -// } -// } -// -// val lightingPass = DeferredLightingPass( -// depth = matPass.depth, -// encodedNormalsMeta = matPass.encodedNormalsMeta, -// albedoEmission = matPass.albedoEmission, -// metalRoughnessAo = matPass.metalRoughnessAo, -// sceneCam = matPass.camera, -// lighting = lighting, -// ) -// addComputePass(lightingPass) - addTextureMesh { generate { generateFullscreenQuad() diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt index b1a1ccd93..135c5efd9 100644 --- a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt +++ b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt @@ -20,19 +20,23 @@ import de.fabmax.kool.util.Time class GbufferPass(content: Node, camera: Camera, initialSize: Vec2i) : OffscreenPass2d( drawNode = content, attachmentConfig = AttachmentConfig { - addColor(TexFormat.R_I32, filterMethod = FilterMethod.NEAREST) // meta data - addColor(TexFormat.R_I32, filterMethod = FilterMethod.NEAREST) // encoded normals - addColor(TexFormat.RGBA, filterMethod = FilterMethod.NEAREST) // albedo, a * 255 = emission strength - addColor(TexFormat.RGBA, filterMethod = FilterMethod.NEAREST) // metal, roughness, ao + // albedo, a * 255 = emission strength + addColor(TexFormat.RGBA, filterMethod = FilterMethod.NEAREST) + // metal, roughness, ao, [empty: a, flags maybe?] + addColor(TexFormat.RGBA, filterMethod = FilterMethod.NEAREST) + // encoded normals, alpha almost free + addColor(TexFormat.RGBA, filterMethod = FilterMethod.NEAREST, clearColor = ClearColorFill(Color.ZERO)) + // object-ids, meta + addColor(TexFormat.R_I32, filterMethod = FilterMethod.NEAREST) defaultDepth() }, initialSize = initialSize, name = "deferred2-gbuffer-pass" ) { - val meta get() = colorTextures[0] - val encodedNormals get() = colorTextures[1] - val albedoEmission get() = colorTextures[2] - val metalRoughnessAo get() = colorTextures[3] + val albedoEmission get() = colorTextures[0] + val metalRoughnessAo get() = colorTextures[1] + val encodedNormals get() = colorTextures[2] + val objectIds get() = colorTextures[3] val depth get() = depthTexture!! @@ -99,7 +103,6 @@ class GbufferShader(val config: GbufferShaderConfig) : KslShader("deferred2-gbuf private fun KslProgram.program() { val camData = cameraData() - val positionViewSpace = interStageFloat3("positionWorldSpace") val normalViewSpace = interStageFloat3("normalWorldSpace") var tangentViewSpace: KslInterStageVector? = null @@ -124,7 +127,6 @@ class GbufferShader(val config: GbufferShaderConfig) : KslShader("deferred2-gbuf val viewPos by camData.viewMat * float4Value(worldPos, 1f) outPosition set camData.projMat * viewPos - positionViewSpace.input set viewPos.xyz normalViewSpace.input set (camData.viewMat * float4Value(worldNormal, 0f)).xyz if (config.normalMapCfg.isNormalMapped) { @@ -174,13 +176,14 @@ class GbufferShader(val config: GbufferShaderConfig) : KslShader("deferred2-gbuf val metallic = float1Port("metallic", fragmentPropertyBlock(config.metallicCfg).outProperty) val aoFactor = float1Port("aoFactor", fragmentPropertyBlock(config.aoCfg).outProperty) - val normalHashX by clamp(((sqrt(abs(normal.x)) * sign(normal.x) + 1f.const) * 8f.const).toInt1(), 0.const, 15.const) - val normalHashY by clamp(((sqrt(abs(normal.y)) * sign(normal.y) + 1f.const) * 8f.const).toInt1(), 0.const, 15.const) + val normalHashX by clamp(((normal.x + 1f.const) * 8f.const).toInt1(), 0.const, 15.const) + val normalHashY by clamp(((normal.y + 1f.const) * 8f.const).toInt1(), 0.const, 15.const) + val hash by (normalHashX shl 28.const) or (normalHashY shl 24.const) - intOutput(int4Value(normalHashX or (normalHashY shl 4.const), 0.const, 0.const, 0.const), location = 0) - intOutput(int4Value(encodeNormal(normal), 0.const, 0.const, 0.const), location = 1) - colorOutput(float4Value(baseColor.rgb, emissionStrength), location = 2) - colorOutput(float4Value(metallic, roughness, aoFactor, 0f.const), location = 3) + colorOutput(float4Value(baseColor.rgb, emissionStrength), location = 0) + colorOutput(float4Value(metallic, roughness, aoFactor, 0f.const), location = 1) + colorOutput(encodeNormalRgb(normal), location = 2) + intOutput(int4Value(hash, 0.const, 0.const, 0.const), location = 3) } } } @@ -265,9 +268,4 @@ class GbufferShaderConfig(builder: Builder) { open fun build() = GbufferShaderConfig(this) } - - companion object { - const val MATERIAL_FLAG_ALWAYS_LIT = 1 - const val MATERIAL_FLAG_IS_MOVING = 2 - } } diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt index 56814c868..ea9cbedf3 100644 --- a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt +++ b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt @@ -104,7 +104,7 @@ class DeferredLightingShader : KslShader("deferred2-lighting") { fullscreenQuadVertexStage(uv) fragmentStage { val depth = texture2d("depth", isUnfilterable = true) - val encodedNormals = texture2dInt("encodedNormals") + val encodedNormals = texture2d("encodedNormals") val albedoEmission = texture2d("albedoEmission") val metalRoughnessAo = texture2d("metalRoughnessAo") @@ -118,9 +118,8 @@ class DeferredLightingShader : KslShader("deferred2-lighting") { val invView = camData[DeferredCamDataStruct.invView] val worldPos by unprojectBaseCoord(depth, baseCoord, camNear, invProj, invView).xyz - val encNormalMeta by encodedNormals.load(baseCoord, lod = 0.const).xy - val viewNormal by decodeNormal(encNormalMeta.x) - val worldNormal by (camData[DeferredCamDataStruct.invView] * float4Value(viewNormal, 0f)).xyz + val viewNormal by decodeNormalRgb(encodedNormals.load(baseCoord, lod = 0.const).xyz) + val worldNormal by (invView * float4Value(viewNormal, 0f.const)).xyz val albedoEmission = float4Var(albedoEmission.load(baseCoord, lod = 0.const)) val albedo = albedoEmission.xyz diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt index 8d93672a2..7f04745e5 100644 --- a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt +++ b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt @@ -49,8 +49,8 @@ class TemporalFilterPass( temporalShader.swapPipelineDataCapturing(filterOutput.newVal) { oldAlbedo = gbuffers.oldVal.albedoEmission newAlbedo = gbuffers.newVal.albedoEmission - oldMeta = gbuffers.oldVal.meta - newMeta = gbuffers.newVal.meta + oldMeta = gbuffers.oldVal.objectIds + newMeta = gbuffers.newVal.objectIds oldFilter = filterOutput.oldVal newFilter = filterOutput.newVal frameI = Time.frameCount @@ -115,10 +115,10 @@ class TemporalFilterShader( val meta2 = paramInt1() body { - val x1 by meta1 and 0xf.const - val y1 by (meta1 shr 4.const) and 0xf.const - val x2 by meta2 and 0xf.const - val y2 by (meta2 shr 4.const) and 0xf.const + val x1 by (meta1 shr 24.const) and 0xf.const + val y1 by (meta1 shr 28.const) and 0xf.const + val x2 by (meta2 shr 24.const) and 0xf.const + val y2 by (meta2 shr 28.const) and 0xf.const (abs(x1 - x2) le 3.const) and (abs(y1 - y2) le 3.const) } } @@ -190,9 +190,9 @@ class TemporalFilterShader( newFilter[baseCoord] = float4Value(filtered, 1f) // newFilter[baseCoord] = float4Value(current, 1f) -// val x1 by curMeta and 0xf.const -// val y1 by (curMeta shr 4.const) and 0xf.const -// newFilter[baseCoord] = float4Value(x1.toFloat1() / 15f.const, y1.toFloat1() / 15f.const, 0f.const, 1f.const) +// val x1 by (curMeta shr 16.const) and 0xff.const +// val y1 by (curMeta shr 24.const) and 0xff.const +// newFilter[baseCoord] = float4Value(x1.toFloat1() / 127f.const, y1.toFloat1() / 127f.const, 0f.const, 1f.const) // newFilter[baseCoord] = float4Value(mix(curColor, curAlbedo, 0.5f.const), 1f) From 5921aad256744690078695fb0a042590b0f0dd0f Mon Sep 17 00:00:00 2001 From: Max Thiele Date: Thu, 14 May 2026 20:22:02 +0200 Subject: [PATCH 03/27] Pretty good temporal filter --- .../kool/demo/deferred2/Deferred2Pipeline.kt | 20 ++- .../kool/demo/deferred2/Deferred2Test.kt | 142 +++++++++-------- .../fabmax/kool/demo/deferred2/GbufferPass.kt | 47 +++++- .../kool/demo/deferred2/LightingPass.kt | 36 ++--- .../demo/deferred2/ProjectionFunctions.kt | 15 +- .../kool/demo/deferred2/TemporalFilterPass.kt | 147 ++++++++++-------- 6 files changed, 237 insertions(+), 170 deletions(-) diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt index ad9f14031..527dc5608 100644 --- a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt +++ b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt @@ -1,6 +1,5 @@ package de.fabmax.kool.demo.deferred2 -import de.fabmax.kool.demo.deferred2.GbufferPass.Companion.TSAA_PATTERN_4 import de.fabmax.kool.math.Vec2i import de.fabmax.kool.pipeline.BufferedImageData2d import de.fabmax.kool.pipeline.TexFormat @@ -15,8 +14,8 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.yield -private const val renderScale = 1f -val tsaa = TSAA_PATTERN_4 +private const val renderScale = 0.5f +val tsaa = GbufferPass.TSAA_PATTERN_4 class Deferred2Pipeline( val content: Node, @@ -26,7 +25,8 @@ class Deferred2Pipeline( val camera = PerspectiveCamera() val gbuffers = AlternatingPair { - GbufferPass(content, camera, scene.renderSize) + val suff = if (it) "A" else "B" + GbufferPass(content, camera, scene.renderSize, "deferred2-gbuffer-pass-$suff") } // fixme: think of something better for providing a camera @@ -61,17 +61,21 @@ class Deferred2Pipeline( lightingPass.setSize(size.x, size.y) filterPass.resize(size) } - - } scene.coroutineScope.launch { withContext(KoolDispatchers.Synced) { while (true) { - gbuffers.newVal.isEnabled = true - gbuffers.oldVal.isEnabled = false lightingPass.swapBuffers() filterPass.swapBuffers() + + + gbuffers.newVal.objModelMatsGpu.uploadData(gbuffers.newVal.objModelMats) +// gbuffers.oldVal.objModelMatsGpu.uploadData(gbuffers.oldVal.objModelMats) + + // this is called after update, newVal was enabled and updated, disable it and enable oldVal for next frame + gbuffers.newVal.isEnabled = false + gbuffers.oldVal.isEnabled = true yield() } } diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt index b421565c8..0da6c0015 100644 --- a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt +++ b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt @@ -19,78 +19,100 @@ import de.fabmax.kool.util.MdColor import de.fabmax.kool.util.Time import de.fabmax.kool.util.debugOverlay +fun gbufferShader(objectId: Int, block: GbufferShaderConfig.Builder.() -> Unit): GbufferShader { + val cfg = GbufferShaderConfig.Builder().apply{ + this.objectId = objectId + block() + }.build() + return GbufferShader(cfg) +} + suspend fun KoolApplication.deferred2Test() { val texAlbedo = Assets.loadTexture2d("materials/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_COL_2K_METALNESS.jpg").getOrThrow() val texNormal = Assets.loadTexture2d("materials/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_NRM_2K_METALNESS.jpg").getOrThrow() + val uvChecker = Assets.loadTexture2d("materials/uv_checker_map.jpg").getOrThrow() addScene { val content = Node().apply { - val gShader = GbufferShader(GbufferShaderConfig.Builder().apply { color { vertexColor() } }.build()) - addColorMesh { - generate { - color = Color.WHITE - cube { origin.set(-2.5f, 0f, 0f)} - icoSphere { - center.set(-2.5f, 0f, 2.5f) - steps = 4 - } - } - onUpdate { -// transform.rotate(360f.deg * Time.deltaT, Vec3f.Y_AXIS) - } - shader = gShader - } - addTextureMesh(isNormalMapped = true) { - generate { - cube { origin.set(0f, 0f, 0f) } - } + + addGroup { onUpdate { - transform - .setIdentity() - .rotate(10f.deg * Time.gameTime.toFloat(), Vec3f.Y_AXIS) - //.rotate(20f.deg, Vec3f.Y_AXIS) - .translate(2.5f, 0f, 0f) +// transform.rotate(-30f.deg * Time.deltaT, Vec3f.Y_AXIS) } - shader = GbufferShader(GbufferShaderConfig.Builder().apply { - color { - textureColor(texAlbedo) + + addColorMesh { + generate { + color = MdColor.PINK.toLinear() + cube { origin.set(-2.5f, 0f, 0f)} + color = MdColor.AMBER.toLinear() + icoSphere { + center.set(-2.5f, 0f, 2.5f) + steps = 4 + radius = 0.5f + } + } + onUpdate { +// transform.rotate(360f.deg * Time.deltaT, Vec3f.Y_AXIS) } - normalMapping { - useNormalMap(texNormal) + shader = gbufferShader(objectId = 1) { + color { vertexColor() } } - }.build()).apply { - bindTexture2d("tbaseColor", texAlbedo) - bindTexture2d("tNormalMap", texNormal) } - } - addColorMesh { - generate { - cube { colored() } + addTextureMesh(isNormalMapped = true) { + generate { + cube { } + } + onUpdate { + transform + .setIdentity() +// .rotate(90f.deg * Time.gameTime.toFloat(), Vec3f.Y_AXIS) + //.rotate(20f.deg, Vec3f.Y_AXIS) + .translate(2.5f, 0f, 0f) + } + shader = gbufferShader(objectId = 2) { + color { + textureColor(texAlbedo) + } + normalMapping { + useNormalMap(texNormal) + } + }.apply { +// bindTexture2d("tbaseColor", uvChecker) + bindTexture2d("tbaseColor", texAlbedo) + bindTexture2d("tNormalMap", texNormal) + } } - onUpdate { - transform.rotate(90f.deg * Time.deltaT, Vec3f.Y_AXIS) + addColorMesh { + generate { + cube { colored() } + } + onUpdate { + transform.rotate(90f.deg * Time.deltaT, Vec3f.Y_AXIS) + } + shader = gbufferShader(objectId = 3) { + color { vertexColor() } + } } - shader = gShader - } - addColorMesh { - generate { - translate(0f, -2f, 0f) - color = MdColor.PURPLE //Color.WHITE - grid { - sizeX = 50f - sizeY = 50f + addColorMesh { + generate { + translate(0f, -1.5f, 0f) + color = Color.WHITE + grid { + sizeX = 50f + sizeY = 50f + } + } + shader = gbufferShader(objectId = 4) { + color { vertexColor() } } } - shader = gShader } } val lighting = Lighting().apply { singlePointLight { - //setup(Vec3f(3f, 4f, 2f)) - //setColor(MdColor.AMBER.toLinear(), intensity = 30f) setup(Vec3f(1.2f, 1.2f, 2f)) - setColor(Color.WHITE, intensity = 5f) + setColor(Color.WHITE, intensity = 25f) } } val deferred2Pipeline = Deferred2Pipeline(content, lighting, this) @@ -110,24 +132,22 @@ suspend fun KoolApplication.deferred2Test() { fragmentStage { main { val output = texture2d("deferredOutput") - val uvi = (uv.output * output.size().toFloat2()).toInt2() + val uvi = (uv.output * output.size().toFloat2() + 0.5f.const2).toInt2() val color by output.load(uvi).rgb val ditherTex = texture2d("ditherPattern") - //val ditherC by baseCoord % ditherTex.size() - val ditherNoise by ditherTex.load(uvi).r + val ditherC by uvi % ditherTex.size() + val ditherNoise by ditherTex.load(ditherC).r val srgb by convertColorSpace(color, ColorSpaceConversion.LinearToSrgbHdr()) + (ditherNoise - 0.5f.const) / 255f.const - - - //color set convertColorSpace(color, ColorSpaceConversion.LinearToSrgbHdr()) colorOutput(srgb) +// colorOutput(color) } } } outShader.bindTexture2d("ditherPattern", makeDitherPattern()) - var inputTex by outShader.bindTexture2d("deferredOutput", deferred2Pipeline.lightingPass.lightingOutput) + var inputTex by outShader.bindTexture2d("deferredOutput", deferred2Pipeline.gbuffers.a.encodedNormals) onUpdate { outShader.swapPipelineDataCapturing(deferred2Pipeline.filterPass.filterOutput.newVal) { inputTex = deferred2Pipeline.filterPass.filterOutput.newVal @@ -141,9 +161,9 @@ suspend fun KoolApplication.deferred2Test() { ctx.scenes += debugOverlay() } -class AlternatingPair(factory: () -> T) { - val a: T = factory() - val b: T = factory() +class AlternatingPair(factory: (Boolean) -> T) { + val a: T = factory(true) + val b: T = factory(false) val newVal: T get() = if (Time.frameCount % 2 == 0) a else b val oldVal: T get() = if (Time.frameCount % 2 == 0) b else a diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt index 135c5efd9..2c5e5e2ed 100644 --- a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt +++ b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt @@ -14,10 +14,15 @@ import de.fabmax.kool.scene.Camera import de.fabmax.kool.scene.Node import de.fabmax.kool.scene.VertexLayouts import de.fabmax.kool.scene.vertexAttrib -import de.fabmax.kool.util.Color -import de.fabmax.kool.util.Time +import de.fabmax.kool.util.* -class GbufferPass(content: Node, camera: Camera, initialSize: Vec2i) : OffscreenPass2d( +object ObjModelMatLayout : Struct("obj_model_mat", MemoryLayout.Std140) { + val reprojectMat = mat4("reprojectMat") +} + +private val prevViewProjMats = List(1024) { MutableMat4f() } + +class GbufferPass(content: Node, camera: Camera, initialSize: Vec2i, name: String) : OffscreenPass2d( drawNode = content, attachmentConfig = AttachmentConfig { // albedo, a * 255 = emission strength @@ -31,7 +36,7 @@ class GbufferPass(content: Node, camera: Camera, initialSize: Vec2i) : Offscreen defaultDepth() }, initialSize = initialSize, - name = "deferred2-gbuffer-pass" + name = name ) { val albedoEmission get() = colorTextures[0] val metalRoughnessAo get() = colorTextures[1] @@ -40,6 +45,9 @@ class GbufferPass(content: Node, camera: Camera, initialSize: Vec2i) : Offscreen val depth get() = depthTexture!! + val objModelMats = StructBuffer(ObjModelMatLayout, 1024) + val objModelMatsGpu = objModelMats.asStorageBuffer() + init { this.camera = camera @@ -49,6 +57,23 @@ class GbufferPass(content: Node, camera: Camera, initialSize: Vec2i) : Offscreen offsetMat.setIdentity().translate(offset.x / width, offset.y / height, 0f).mul(camera.proj) camera.proj.set(offsetMat) } + + val inverseBuf = MutableMat4f() + val reprojectBuf = MutableMat4f() + onAfterCollectDrawCommands += { viewData -> + viewData.drawQueue.forEach { cmd -> + (cmd.mesh.shader as? GbufferShader)?.let { gbufferShader -> + val id = gbufferShader.objectId + objModelMats.set(id) { + val prevViewProj = prevViewProjMats[id] + inverseBuf.set(cmd.modelMatF).invert() + reprojectBuf.set(prevViewProj).mul(inverseBuf) + set(it.reprojectMat, reprojectBuf) + prevViewProj.set(viewData.drawQueue.viewProjMatF).mul(cmd.modelMatF) + } + } + } + } } companion object { @@ -95,6 +120,8 @@ class GbufferPass(content: Node, camera: Camera, initialSize: Vec2i) : Offscreen } class GbufferShader(val config: GbufferShaderConfig) : KslShader("deferred2-gbuffer-shader") { + var objectId: Int by bindUniformInt1("uObjectId", config.objectId) + init { pipelineConfig = PipelineConfig(blendMode = BlendMode.DISABLED, cullMethod = config.cullMethod) program.program() @@ -103,6 +130,7 @@ class GbufferShader(val config: GbufferShaderConfig) : KslShader("deferred2-gbuf private fun KslProgram.program() { val camData = cameraData() + val objectId = interStageInt1("objectId") val normalViewSpace = interStageFloat3("normalWorldSpace") var tangentViewSpace: KslInterStageVector? = null @@ -110,6 +138,9 @@ class GbufferShader(val config: GbufferShaderConfig) : KslShader("deferred2-gbuf vertexStage { main { + val uObjectId = uniformInt1("uObjectId") + objectId.input set uObjectId + (inInstanceIndex.toInt1() shl 10.const) + val vertexBlock = vertexTransformBlock(config.vertexCfg) { inLocalPos(vertexAttrib(VertexLayouts.Position.position)) inLocalNormal(vertexAttrib(VertexLayouts.Normal.normal)) @@ -178,12 +209,12 @@ class GbufferShader(val config: GbufferShaderConfig) : KslShader("deferred2-gbuf val normalHashX by clamp(((normal.x + 1f.const) * 8f.const).toInt1(), 0.const, 15.const) val normalHashY by clamp(((normal.y + 1f.const) * 8f.const).toInt1(), 0.const, 15.const) - val hash by (normalHashX shl 28.const) or (normalHashY shl 24.const) + val meta by (normalHashX shl 28.const) or (normalHashY shl 24.const) or objectId.output colorOutput(float4Value(baseColor.rgb, emissionStrength), location = 0) colorOutput(float4Value(metallic, roughness, aoFactor, 0f.const), location = 1) colorOutput(encodeNormalRgb(normal), location = 2) - intOutput(int4Value(hash, 0.const, 0.const, 0.const), location = 3) + intOutput(int4Value(meta, 0.const, 0.const, 0.const), location = 3) } } } @@ -202,7 +233,7 @@ class GbufferShaderConfig(builder: Builder) { val alphaMode: AlphaMode = builder.alphaMode val cullMethod: CullMethod = builder.cullMethod - val materialFlags: Int = builder.materialFlags + val objectId: Int = builder.objectId val modelCustomizer: (KslProgram.() -> Unit)? = builder.modelCustomizer @@ -220,7 +251,7 @@ class GbufferShaderConfig(builder: Builder) { var alphaMode: AlphaMode = AlphaMode.Blend var cullMethod: CullMethod = CullMethod.CULL_BACK_FACES - var materialFlags = 0 + var objectId = 0 var modelCustomizer: (KslProgram.() -> Unit)? = null diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt index ea9cbedf3..8ed058cca 100644 --- a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt +++ b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt @@ -14,13 +14,10 @@ import de.fabmax.kool.pipeline.FullscreenShaderUtil.generateFullscreenQuad import de.fabmax.kool.scene.* import de.fabmax.kool.util.MemoryLayout import de.fabmax.kool.util.Struct +import de.fabmax.kool.util.Time class LightingPass( val gbuffers: AlternatingPair, -// val depth: Texture2d, -// val encodedNormalsMeta: Texture2d, -// val albedoEmission: Texture2d, -// val metalRoughnessAo: Texture2d, camera: Camera, lighting: Lighting?, size: Vec2i, @@ -33,18 +30,9 @@ class LightingPass( initialSize = size, name = "deferred2-lighting-pass" ) { -// val lightingOutput = AlternatingPair { -// StorageTexture2d(size.x, size.y, TexFormat.RG11B10_F, samplerSettings = SamplerSettings().clamped().nearest()) -// } - val lightingOutput: Texture2d get() = colorTexture!! - private val lightingShader = DeferredLightingShader( -// depth = depth, -// encodedNormalsMeta = encodedNormalsMeta, -// albedoEmission = albedoEmission, -// metalRoughnessAo = metalRoughnessAo, - ) + private val lightingShader = DeferredLightingShader() init { this.camera = camera @@ -58,17 +46,18 @@ class LightingPass( } fun swapBuffers() { - val newGbuffer = gbuffers.oldVal + val newGbuffer = gbuffers.newVal lightingShader.swapPipelineDataCapturing(newGbuffer) { depthTex = newGbuffer.depth encodedNormals = newGbuffer.encodedNormals albedoEmissionTex = newGbuffer.albedoEmission metalRoughnessAoTex = newGbuffer.metalRoughnessAo + frameI = Time.frameCount camData.set { set(it.proj, camera.proj) - set(it.invProj, camera.invProj) set(it.invView, camera.invView) + set(it.invViewProj, camera.invViewProj) set(it.viewport, Vec4f(0f, 0f, width.toFloat(), height.toFloat())) set(it.position, camera.globalPos) set(it.camNear, camera.clipNear) @@ -90,6 +79,8 @@ class DeferredLightingShader : KslShader("deferred2-lighting") { private val lightDataStruct = LightDataStruct(4) val lightData = bindUniformStruct("lightData", lightDataStruct) + var frameI by bindUniformInt1("frameI") + init { pipelineConfig = PipelineConfig( blendMode = BlendMode.DISABLED, @@ -111,12 +102,14 @@ class DeferredLightingShader : KslShader("deferred2-lighting") { val camData = uniformStruct("deferredCamData", DeferredCamDataStruct) val lightData = uniformStruct("lightData", lightDataStruct) + val frameI = uniformInt1("frameI") + main { val baseCoord = (uv.output * depth.size().toFloat2()).toInt2() val camNear = camData[DeferredCamDataStruct.camNear] - val invProj = camData[DeferredCamDataStruct.invProj] val invView = camData[DeferredCamDataStruct.invView] - val worldPos by unprojectBaseCoord(depth, baseCoord, camNear, invProj, invView).xyz + val invViewProj = camData[DeferredCamDataStruct.invViewProj] + val worldPos by unprojectBaseCoord(depth, baseCoord, camNear, invViewProj).xyz val viewNormal by decodeNormalRgb(encodedNormals.load(baseCoord, lod = 0.const).xyz) val worldNormal by (invView * float4Value(viewNormal, 0f.const)).xyz @@ -138,8 +131,10 @@ class DeferredLightingShader : KslShader("deferred2-lighting") { val ambient by 0.04f.const val diffuse by albedo * ambient + albedo * radiance * saturate(dot(dirToLight, worldNormal)) - colorOutput(diffuse) + +// val filterNoise by noise31(float3Value(uv.output, frameI.toFloat1())) * 0.7f.const + 0.3f.const +// colorOutput(diffuse * filterNoise) } } } @@ -147,8 +142,9 @@ class DeferredLightingShader : KslShader("deferred2-lighting") { object DeferredCamDataStruct : Struct("DeferredCameraData", MemoryLayout.Std140) { val proj = mat4("projMat") - val invProj = mat4("invProjMat") +// val invProj = mat4("invProjMat") val invView = mat4("invView") + val invViewProj = mat4("invViewProjMat") val viewport = float4("viewport") val position = float3("position") val camNear = float1("camClipNear") diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/ProjectionFunctions.kt b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/ProjectionFunctions.kt index a11c4c5a3..3e6d1bb13 100644 --- a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/ProjectionFunctions.kt +++ b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/ProjectionFunctions.kt @@ -17,16 +17,14 @@ class UnprojectBaseCoord( init { val baseCoord = paramInt2("baseCoord") val camNear = paramFloat1("camNear") - val invProj = paramMat4("invProj") - val invView = paramMat4("invView") + val invViewProj = paramMat4("invProj") body { val uv by baseCoordToUv(baseCoord, depth.size()) val viewDepth by getLinearDepthReversed(depth.load(baseCoord, lod = 0.const).x, camNear) val viewProjXy by (uv * 2f.const - 1f.const) * viewDepth val viewProjPos by float4Value(viewProjXy, camNear, viewDepth) - val viewPos by invProj * viewProjPos - val worldPos by invView * float4Value(viewPos.xyz, 1f) + val worldPos by invViewProj * viewProjPos worldPos } } @@ -36,15 +34,14 @@ fun KslScopeBuilder.unprojectBaseCoord( depth: KslUniform, baseCoord: KslExprInt2, camNear: KslExprFloat1, - invProj: KslExprMat4, - invView: KslExprMat4, + invViewProj: KslExprMat4, ): KslExprFloat4 { val func = parentStage.getOrCreateFunction("fnUnprojectBaseCoord") { UnprojectBaseCoord(depth, this) } - return func(baseCoord, camNear, invProj, invView) + return func(baseCoord, camNear, invViewProj) } -class UnprojectBUv( +class UnprojectUv( depth: KslUniform, parentScope: KslScopeBuilder ) : KslFunction("fnUnprojectUv", KslFloat4, parentScope.parentStage) { @@ -73,6 +70,6 @@ fun KslScopeBuilder.unprojectUv( invProj: KslExprMat4, invView: KslExprMat4, ): KslExprFloat4 { - val func = parentStage.getOrCreateFunction("fnUnprojectUv") { UnprojectBUv(depth, this) } + val func = parentStage.getOrCreateFunction("fnUnprojectUv") { UnprojectUv(depth, this) } return func(uv, camNear, invProj, invView) } \ No newline at end of file diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt index 7f04745e5..a55f2de8a 100644 --- a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt +++ b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt @@ -20,7 +20,7 @@ class TemporalFilterPass( filterStorageFmt: TexFormat = TexFormat.RGBA_F16, ) : ComputePass("deferred2-lighting-pass") { val filterOutput = AlternatingPair { - StorageTexture2d(size.x, size.y, filterStorageFmt, samplerSettings = SamplerSettings().clamped().nearest()) + StorageTexture2d(size.x, size.y, filterStorageFmt, samplerSettings = SamplerSettings().clamped()) } private val filterState = StorageTexture2d(size.x, size.y, TexFormat.R, samplerSettings = SamplerSettings().clamped().nearest()) @@ -47,13 +47,23 @@ class TemporalFilterPass( fun swapBuffers() { temporalShader.swapPipelineDataCapturing(filterOutput.newVal) { - oldAlbedo = gbuffers.oldVal.albedoEmission - newAlbedo = gbuffers.newVal.albedoEmission - oldMeta = gbuffers.oldVal.objectIds - newMeta = gbuffers.newVal.objectIds + val newGbuffer = gbuffers.newVal + val oldGbuffer = gbuffers.oldVal + + newDepth = newGbuffer.depth + oldAlbedo = oldGbuffer.albedoEmission + newAlbedo = newGbuffer.albedoEmission + oldMeta = oldGbuffer.objectIds + newMeta = newGbuffer.objectIds oldFilter = filterOutput.oldVal newFilter = filterOutput.newVal frameI = Time.frameCount + + objModelMats = newGbuffer.objModelMatsGpu + camData.set { + set(it.invViewProj, camera.invViewProj) + set(it.camNear, camera.clipNear) + } } } } @@ -68,12 +78,12 @@ class TemporalFilterShader( var newAlbedo by bindTexture2d("newAlbedo") var oldMeta by bindTexture2d("oldMeta") var newMeta by bindTexture2d("newMeta") + var newDepth by bindTexture2d("newDepth") -// var oldDepth by bindTexture2d("oldDepth") -// var newDepth by bindTexture2d("newDepth") -// var camData = bindUniformStruct("camData", NewOldCamDataStruct) + var objModelMats by bindStorage("objModelMats") + var camData = bindUniformStruct("camData", FilterCamDataStruct) - var oldFilter by bindStorageTexture2d("oldFilter") + var oldFilter by bindTexture2d("oldFilter", defaultSampler = SamplerSettings().clamped().linear()) var newFilter by bindStorageTexture2d("newFilter") var filterState by bindStorageTexture2d("filterState", filterState) @@ -90,12 +100,13 @@ class TemporalFilterShader( val newAlbedo = texture2d("newAlbedo") val oldMeta = texture2dInt("oldMeta") val newMeta = texture2dInt("newMeta") + val newDepth = texture2d("newDepth", isUnfilterable = true) - val oldFilter = if (filterStorageFmt.channels == 3) { - storageTexture2d("oldFilter", filterStorageFmt) - } else { - storageTexture2d("oldFilter", filterStorageFmt) - } + val invModelMatStruct = struct(ObjModelMatLayout) + val objModelMats = storage("objModelMats", invModelMatStruct) +// val oldObjModelMats = storage("oldObjModelMats", invModelMatStruct) + + val oldFilter = texture2d("oldFilter") val newFilter = if (filterStorageFmt.channels == 3) { storageTexture2d("newFilter", filterStorageFmt) } else { @@ -106,9 +117,7 @@ class TemporalFilterShader( val frameI = uniformInt1("frameI") -// val oldDepth = texture2d("oldDepth", isUnfilterable = true) -// val newDepth = texture2d("newDepth", isUnfilterable = true) -// val camData = uniformStruct("camData", NewOldCamDataStruct) + val camData = uniformStruct("camData", FilterCamDataStruct) val metaEqual = functionBool1("fnMetaEqual") { val meta1 = paramInt1() @@ -125,50 +134,63 @@ class TemporalFilterShader( main { val baseCoord by inGlobalInvocationId.xy.toInt2() -// val uv by baseCoordToUv(baseCoord, lightingOutput.size()) - val state by (filterState.load(baseCoord).r * 255f.const).toInt1() -// val filterNoise by noise31(uint3Value(inGlobalInvocationId.xy, frameI.toUint1())) - -// val camNear = camData[NewOldCamDataStruct.newCam][DeferredCamDataStruct.camNear] -// val invProj = camData[NewOldCamDataStruct.newCam][DeferredCamDataStruct.invProj] -// val invView = camData[NewOldCamDataStruct.newCam][DeferredCamDataStruct.invView] -// val newWorldPos by unprojectUv(newDepth, baseCoord, camNear, invProj, invView).xyz - - val curAlbedo by newAlbedo.load(baseCoord).rgb val curMeta by newMeta.load(baseCoord).r - val colorDiff by length(curAlbedo - oldAlbedo.load(baseCoord).rgb) - val sameColorThresh = 0.08f.const //+ filterNoise * 0.01f.const - val sameColor by colorDiff lt sameColorThresh - val sameMeta by metaEqual(curMeta, oldMeta.load(baseCoord).r) - val filterHit by sameColor and sameMeta + val id by curMeta and 0xffffff.const + + val oldUv by (baseCoord.toFloat2() + 0.5f.const2) / oldFilter.size().toFloat2() + val oldBaseCoord by baseCoord + `if`(id ne 0.const) { + val camNear = camData[FilterCamDataStruct.camNear] + val invViewProj = camData[FilterCamDataStruct.invViewProj] + val worldPos by unprojectBaseCoord(newDepth, baseCoord, camNear, invViewProj) + val objModelMat = structVar(objModelMats[id]) + val oldProj by objModelMat[ObjModelMatLayout.reprojectMat] * worldPos + oldUv set oldProj.xy / oldProj.w * float2Value(0.5f, -0.5f) + 0.5f.const + oldBaseCoord set (oldUv * oldFilter.size().toFloat2()).toInt2() + } - val wasEdge by state and 1.const gt 0.const + val oldStateCa by (oldUv * oldFilter.size().toFloat2() + float2Value(0.5f, 0.5f)).toInt2() + val oldStateCb by (oldUv * oldFilter.size().toFloat2() + float2Value(0.5f, -0.5f)).toInt2() + val oldStateCc by (oldUv * oldFilter.size().toFloat2() + float2Value(-0.5f, -0.5f)).toInt2() + val oldStateCd by (oldUv * oldFilter.size().toFloat2() + float2Value(-0.5f, 0.5f)).toInt2() + val oldState by (filterState.load(oldStateCa).r * 255f.const).toInt1() or + (filterState.load(oldStateCb).r * 255f.const).toInt1() or + (filterState.load(oldStateCc).r * 255f.const).toInt1() or + (filterState.load(oldStateCd).r * 255f.const).toInt1() + val wasEdge by oldState and 1.const gt 0.const + + val sameColorThresh = 0.25f.const + val curAlbedo by float4Var(newAlbedo.load(baseCoord)) + val colorDiff by length(curAlbedo - oldAlbedo.sample(oldUv)) + val sameColor by colorDiff lt sameColorThresh + val filterHit by sameColor val isEdge by false.const `if`(!filterHit or wasEdge) { - val da by length(curAlbedo - oldAlbedo.load(baseCoord + int2Value(-1, -1)).rgb) - val db by length(curAlbedo - oldAlbedo.load(baseCoord + int2Value(1, 1)).rgb) - val dc by length(curAlbedo - oldAlbedo.load(baseCoord + int2Value(1, -1)).rgb) - val dd by length(curAlbedo - oldAlbedo.load(baseCoord + int2Value(-1, 1)).rgb) +// val depth0 by 1f.const / newDepth.load(baseCoord, lod = 0.const).x +// val da by depthEdge(depth0, newDepth.load(baseCoord + int2Value(-1, -1)).x) +// val db by depthEdge(depth0, newDepth.load(baseCoord + int2Value(1, 1)).x) +// val dc by depthEdge(depth0, newDepth.load(baseCoord + int2Value(1, -1)).x) +// val dd by depthEdge(depth0, newDepth.load(baseCoord + int2Value(-1, 1)).x) +// isEdge set (da or db or dc or dd) + + val oldPxSz by 1f.const2 / oldFilter.size().toFloat2() + val da by length(curAlbedo - oldAlbedo.sample(oldUv + float2Value(-1f, -1f) * oldPxSz)) + val db by length(curAlbedo - oldAlbedo.sample(oldUv + float2Value(1f, 1f) * oldPxSz)) + val dc by length(curAlbedo - oldAlbedo.sample(oldUv + float2Value(1f, -1f) * oldPxSz)) + val dd by length(curAlbedo - oldAlbedo.sample(oldUv + float2Value(-1f, 1f) * oldPxSz)) val minD = min(colorDiff, min(min(da, db), min(dc, dd))) val maxD = max(colorDiff, max(max(da, db), max(dc, dd))) isEdge set ((minD lt sameColorThresh) and (maxD gt sameColorThresh)) - - val ma by metaEqual(curMeta, oldMeta.load(baseCoord + int2Value(-1, -1)).r) - val mb by metaEqual(curMeta, oldMeta.load(baseCoord + int2Value(1, 1)).r) - val mc by metaEqual(curMeta, oldMeta.load(baseCoord + int2Value(1, -1)).r) - val md by metaEqual(curMeta, oldMeta.load(baseCoord + int2Value(-1, 1)).r) - - val anyMtrue by ma or mb or mc or md - val anyMfalse by !(ma and mb and mc and md) - isEdge set (isEdge or (anyMtrue and anyMfalse)) } - state set isEdge.toInt1() - filterState.store(baseCoord, float4Value(state.toFloat1() / 255f.const, 0f.const, 0f.const, 0f.const)) + oldState set isEdge.toInt1() + filterState.store(baseCoord, float4Value(oldState.toFloat1() / 255f.const, 0f.const, 0f.const, 0f.const)) + +// val filterNoise by noise31(uint3Value(inGlobalInvocationId.xy, frameI.toUint1())) // val w by 4f.const - val w by 8f.const //- filterNoise * filterNoise * filterNoise * 4f.const -// val w by 16f.const - filterNoise * filterNoise * filterNoise * 14f.const +// val w by 8f.const //- filterNoise * filterNoise * filterNoise * 4f.const + val w by 16f.const //- filterNoise * filterNoise * filterNoise * 14f.const // val w by 32f.const - filterNoise * filterNoise * filterNoise * 16f.const // val w by 100f.const - filterNoise * filterNoise * filterNoise * 75f.const @@ -177,26 +199,17 @@ class TemporalFilterShader( } val curColor by lightingOutput.load(baseCoord).rgb + val oldColor by oldFilter.sample(oldUv).rgb val curSrgb by convertColorSpace(curColor, ColorSpaceConversion.LinearToSrgb()) - val oldSrgb by convertColorSpace(oldFilter.load(baseCoord).rgb, ColorSpaceConversion.LinearToSrgb()) + val oldSrgb by convertColorSpace(oldColor, ColorSpaceConversion.LinearToSrgb()) val weighted by (oldSrgb * w + curSrgb) / (w + 1f.const) val filtered by convertColorSpace(weighted, ColorSpaceConversion.SrgbToLinear()) `if`(any(isNan(filtered))) { filtered set curSrgb } - -// val filteredColor by (old * w + curColor) / (w + 1f.const) newFilter[baseCoord] = float4Value(filtered, 1f) -// newFilter[baseCoord] = float4Value(current, 1f) - -// val x1 by (curMeta shr 16.const) and 0xff.const -// val y1 by (curMeta shr 24.const) and 0xff.const -// newFilter[baseCoord] = float4Value(x1.toFloat1() / 127f.const, y1.toFloat1() / 127f.const, 0f.const, 1f.const) - -// newFilter[baseCoord] = float4Value(mix(curColor, curAlbedo, 0.5f.const), 1f) -// newFilter[baseCoord] = float4Value(colorDiff, colorDiff, colorDiff, 1f.const) // `if`(wasEdge and !isEdge) { // newFilter[baseCoord] = Color.RED.const // } @@ -211,7 +224,13 @@ class TemporalFilterShader( } } -object NewOldCamDataStruct : Struct("NewOldCamData", MemoryLayout.Std140) { - val oldCam = struct(DeferredCamDataStruct) - val newCam = struct(DeferredCamDataStruct) +context(scope: KslScopeBuilder) +private fun depthEdge(baseDepthLin: KslExprFloat1, cmpDepth: KslExprFloat1): KslExprBool1 { + val cmpDepthLin = float1Var(1f.const / cmpDepth) + return min(baseDepthLin, cmpDepthLin) / max(baseDepthLin, cmpDepthLin) lt 0.99f.const +} + +object FilterCamDataStruct : Struct("FilterCamData", MemoryLayout.Std140) { + val invViewProj = mat4("invViewProj") + val camNear = float1("camClipNear") } From ac04acae48fbfeaa9637aea252953a764b547052 Mon Sep 17 00:00:00 2001 From: Max Thiele Date: Sat, 16 May 2026 20:05:37 +0200 Subject: [PATCH 04/27] Integrate bloom into deferred2 pipeline --- .../de/fabmax/kool/pipeline/BloomPass.kt | 74 +++++++++++++------ .../kool/demo/deferred2/Deferred2Pipeline.kt | 12 ++- .../kool/demo/deferred2/Deferred2Test.kt | 22 ++++-- 3 files changed, 75 insertions(+), 33 deletions(-) diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/BloomPass.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/BloomPass.kt index a392ac347..70cb728f3 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/BloomPass.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/BloomPass.kt @@ -7,17 +7,26 @@ import de.fabmax.kool.math.Vec3i import de.fabmax.kool.math.Vec4f import de.fabmax.kool.modules.ksl.KslComputeShader import de.fabmax.kool.modules.ksl.lang.* +import de.fabmax.kool.util.SyncedScope import de.fabmax.kool.util.logD import de.fabmax.kool.util.releaseWith +import kotlinx.coroutines.launch import kotlin.math.max class BloomPass( - val inputTexture: Texture2d, + inputTexture: Texture2d, val inPlace: Boolean = KoolSystem.requireContext().backend.features.readWriteStorageTextures ) : ComputePass("Bloom Pass") { - private val idealWidth: Int get() = (inputTexture.width / 2).coerceAtLeast(1) - private val idealHeight: Int get() = (inputTexture.height / 2).coerceAtLeast(1) + val inputShader = downSamplingShader() + var inputTexture by inputShader.bindTexture2d( + textureName = "sampleInput", + defaultVal = inputTexture, + defaultSampler = SamplerSettings().clamped().copy(baseMipLevel = 0, numMipLevels = 1), + ) + + private val idealWidth: Int get() = (checkNotNull(inputTexture).width / 2).coerceAtLeast(1) + private val idealHeight: Int get() = (checkNotNull(inputTexture).height / 2).coerceAtLeast(1) private var levels: Int = levelsForSize(idealWidth, idealHeight) var threshold = 1f @@ -34,7 +43,7 @@ class BloomPass( name = "downSampleTex" ) - private val downSampleShader = downSamplingShader() + private val downSampleShaderLower = downSamplingShader() private val upSampleShader = upSamplingShader() val width: Int get() = bloomMap.width @@ -42,7 +51,7 @@ class BloomPass( init { logD { "Using $levels bloom levels, in place: $inPlace" } - makeDownSamplePasses() + makeDownSampleLowerPasses() makeUpSamplePasses() bloomMap.releaseWith(this) if (!inPlace) { @@ -53,40 +62,57 @@ class BloomPass( val requiredWidth = idealWidth val requiredHeight = idealHeight if (requiredWidth != width || requiredHeight != height) { - levels = levelsForSize(requiredWidth, requiredHeight) - logD { "Resizing bloom pass to $requiredWidth x $requiredHeight ($levels levels)" } - bloomMap.resize(requiredWidth, requiredHeight, MipMapping.Limited(levels)) - if (!inPlace) { - downSampleTex.resize(requiredWidth, requiredHeight, MipMapping.Limited(levels)) - } + SyncedScope.launch { + levels = levelsForSize(requiredWidth, requiredHeight) + logD { "Resizing bloom pass to $requiredWidth x $requiredHeight ($levels levels)" } + bloomMap.resize(requiredWidth, requiredHeight, MipMapping.Limited(levels)) + if (!inPlace) { + downSampleTex.resize(requiredWidth, requiredHeight, MipMapping.Limited(levels)) + } - clearAndReleaseTasks() - makeDownSamplePasses() - makeUpSamplePasses() + clearAndReleaseTasks() + makeDownSampleFirstPass() + makeDownSampleLowerPasses() + makeUpSamplePasses() + } } } } - private fun makeDownSamplePasses() { - val sampleInput = downSampleShader.bindTexture2d("sampleInput") - val downSampled = downSampleShader.bindStorageTexture2d("downSampled") - var uThreshold by downSampleShader.bindUniformFloat4("threshold") - var uInputTexelSize by downSampleShader.bindUniformFloat2("inputTexelSize") + private fun makeDownSampleFirstPass() { + val groupsX = (width + 7) / 8 + val groupsY = (height + 7) / 8 + val task = addTask(inputShader, Vec3i(groupsX, groupsY, 1)) + + var uThreshold by inputShader.bindUniformFloat4("threshold") + val inputTexelSize = Vec2f(1f / (2 * width), 1f / (2 * height)) + inputShader.bindUniformFloat2("inputTexelSize").set(inputTexelSize) + inputShader.bindStorageTexture2d("downSampled", downSampleTex, 0) + task.onBeforeDispatch { + uThreshold = Vec4f(thresholdLuminanceFactors, threshold) + } + } + + private fun makeDownSampleLowerPasses() { + val sampleInput = downSampleShaderLower.bindTexture2d("sampleInput") + val downSampled = downSampleShaderLower.bindStorageTexture2d("downSampled") + var uThreshold by downSampleShaderLower.bindUniformFloat4("threshold") + var uInputTexelSize by downSampleShaderLower.bindUniformFloat2("inputTexelSize") - for (level in 0 until levels) { + for (level in 1 until levels) { val groupsX = ((width shr level) + 7) / 8 val groupsY = ((height shr level) + 7) / 8 - val task = addTask(downSampleShader, Vec3i(groupsX, groupsY, 1)) - val input = if (level == 0) inputTexture else downSampleTex + val task = addTask(downSampleShaderLower, Vec3i(groupsX, groupsY, 1)) + val input = downSampleTex val inputSampler = SamplerSettings().clamped().copy(baseMipLevel = (level - 1).coerceAtLeast(0), numMipLevels = 1) val inputTexelSize = Vec2f(1f / ((2 * width) shr level), 1f / ((2 * height) shr level)) val key = "$level" task.onBeforeDispatch { - downSampleShader.createdPipeline?.swapPipelineDataCapturing(key) { + downSampleShaderLower.swapPipelineDataCapturing(key) { sampleInput.set(input, inputSampler) downSampled.set(downSampleTex, level) - uThreshold = if (level == 0) Vec4f(thresholdLuminanceFactors, threshold) else Vec4f.ZERO + uThreshold = Vec4f.ZERO uInputTexelSize = inputTexelSize } } diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt index 527dc5608..7ce6377fb 100644 --- a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt +++ b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt @@ -8,8 +8,10 @@ import de.fabmax.kool.scene.Lighting import de.fabmax.kool.scene.Node import de.fabmax.kool.scene.PerspectiveCamera import de.fabmax.kool.scene.Scene +import de.fabmax.kool.util.BufferedList import de.fabmax.kool.util.KoolDispatchers import de.fabmax.kool.util.Uint8Buffer +import de.fabmax.kool.util.forEachUpdated import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.yield @@ -29,8 +31,7 @@ class Deferred2Pipeline( GbufferPass(content, camera, scene.renderSize, "deferred2-gbuffer-pass-$suff") } - // fixme: think of something better for providing a camera - //val sceneCam get() = gbufferPass.camera + private val swapListeners = BufferedList<() -> Unit>() val lightingPass = LightingPass( gbuffers = gbuffers, @@ -68,10 +69,9 @@ class Deferred2Pipeline( while (true) { lightingPass.swapBuffers() filterPass.swapBuffers() - + swapListeners.forEachUpdated { it() } gbuffers.newVal.objModelMatsGpu.uploadData(gbuffers.newVal.objModelMats) -// gbuffers.oldVal.objModelMatsGpu.uploadData(gbuffers.oldVal.objModelMats) // this is called after update, newVal was enabled and updated, disable it and enable oldVal for next frame gbuffers.newVal.isEnabled = false @@ -82,6 +82,10 @@ class Deferred2Pipeline( } } + fun onSwap(block: () -> Unit) { + swapListeners += block + } + private val Scene.renderSize: Vec2i get() = Vec2i( (mainRenderPass.viewport.width * renderScale).toInt().coerceAtLeast(1), (mainRenderPass.viewport.height * renderScale).toInt().coerceAtLeast(1) diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt index 0da6c0015..c1824d09c 100644 --- a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt +++ b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt @@ -10,6 +10,7 @@ import de.fabmax.kool.modules.ksl.KslShader import de.fabmax.kool.modules.ksl.blocks.ColorSpaceConversion import de.fabmax.kool.modules.ksl.blocks.convertColorSpace import de.fabmax.kool.modules.ksl.lang.* +import de.fabmax.kool.pipeline.BloomPass import de.fabmax.kool.pipeline.FullscreenShaderUtil.fullscreenQuadVertexStage import de.fabmax.kool.pipeline.FullscreenShaderUtil.generateFullscreenQuad import de.fabmax.kool.pipeline.swapPipelineDataCapturing @@ -112,7 +113,7 @@ suspend fun KoolApplication.deferred2Test() { val lighting = Lighting().apply { singlePointLight { setup(Vec3f(1.2f, 1.2f, 2f)) - setColor(Color.WHITE, intensity = 25f) + setColor(Color.WHITE, intensity = 50f) } } val deferred2Pipeline = Deferred2Pipeline(content, lighting, this) @@ -122,6 +123,15 @@ suspend fun KoolApplication.deferred2Test() { addNode(orbitCam) } + val bloomPass = BloomPass(deferred2Pipeline.filterPass.filterOutput.newVal) + addComputePass(bloomPass) + deferred2Pipeline.onSwap { + val filterOutput = deferred2Pipeline.filterPass.filterOutput.newVal + bloomPass.inputShader.swapPipelineDataCapturing(filterOutput) { + bloomPass.inputTexture = filterOutput + } + } + addTextureMesh { generate { generateFullscreenQuad() @@ -132,9 +142,9 @@ suspend fun KoolApplication.deferred2Test() { fragmentStage { main { val output = texture2d("deferredOutput") + val bloom = texture2d("bloomOutput") val uvi = (uv.output * output.size().toFloat2() + 0.5f.const2).toInt2() - val color by output.load(uvi).rgb - + val color by output.load(uvi).rgb + bloom.sample(uv.output).rgb val ditherTex = texture2d("ditherPattern") val ditherC by uvi % ditherTex.size() @@ -147,10 +157,12 @@ suspend fun KoolApplication.deferred2Test() { } outShader.bindTexture2d("ditherPattern", makeDitherPattern()) + outShader.bindTexture2d("bloomOutput", bloomPass.bloomMap) var inputTex by outShader.bindTexture2d("deferredOutput", deferred2Pipeline.gbuffers.a.encodedNormals) onUpdate { - outShader.swapPipelineDataCapturing(deferred2Pipeline.filterPass.filterOutput.newVal) { - inputTex = deferred2Pipeline.filterPass.filterOutput.newVal + val filterOutput = deferred2Pipeline.filterPass.filterOutput.newVal + outShader.swapPipelineDataCapturing(filterOutput) { + inputTex = filterOutput } } From 48af0fdd4ce1b87e408c77005e81a0f04e503673 Mon Sep 17 00:00:00 2001 From: Max Thiele Date: Sun, 17 May 2026 11:51:22 +0200 Subject: [PATCH 05/27] Object ID + depth based temporal filter --- .../de/fabmax/kool/pipeline/BloomPass.kt | 9 +- .../kool/demo/deferred2/Deferred2Pipeline.kt | 15 ++- .../kool/demo/deferred2/Deferred2Test.kt | 14 +- .../fabmax/kool/demo/deferred2/GbufferPass.kt | 4 +- .../kool/demo/deferred2/LightingPass.kt | 3 +- .../kool/demo/deferred2/TemporalFilterPass.kt | 121 ++++++++---------- 6 files changed, 79 insertions(+), 87 deletions(-) diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/BloomPass.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/BloomPass.kt index 70cb728f3..4d141078a 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/BloomPass.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/BloomPass.kt @@ -34,7 +34,14 @@ class BloomPass( var radius = 2f var strength = 1f - val bloomMap = StorageTexture2d(idealWidth, idealHeight, TexFormat.RG11B10_F, MipMapping.Limited(levels), name = "bloomMap") + val bloomMap = StorageTexture2d( + width = idealWidth, + height = idealHeight, + format = TexFormat.RG11B10_F, + mipMapping = MipMapping.Limited(levels), + name = "bloomMap", + samplerSettings = SamplerSettings().clamped() + ) val downSampleTex = if (inPlace) bloomMap else StorageTexture2d( width = idealWidth, height = idealHeight, diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt index 7ce6377fb..9c0a6084d 100644 --- a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt +++ b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt @@ -8,16 +8,13 @@ import de.fabmax.kool.scene.Lighting import de.fabmax.kool.scene.Node import de.fabmax.kool.scene.PerspectiveCamera import de.fabmax.kool.scene.Scene -import de.fabmax.kool.util.BufferedList -import de.fabmax.kool.util.KoolDispatchers -import de.fabmax.kool.util.Uint8Buffer -import de.fabmax.kool.util.forEachUpdated +import de.fabmax.kool.util.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.yield private const val renderScale = 0.5f -val tsaa = GbufferPass.TSAA_PATTERN_4 +val tsaa = GbufferPass.TSAA_PATTERN_8 class Deferred2Pipeline( val content: Node, @@ -119,3 +116,11 @@ fun makeDitherPattern(): Texture2d { val data = BufferedImageData2d(buf, 4, 4, TexFormat.R) return Texture2d(data) } + +class AlternatingPair(factory: (Boolean) -> T) { + val a: T = factory(true) + val b: T = factory(false) + + val newVal: T get() = if (Time.frameCount % 2 == 0) a else b + val oldVal: T get() = if (Time.frameCount % 2 == 0) b else a +} \ No newline at end of file diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt index c1824d09c..06b4e22a0 100644 --- a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt +++ b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt @@ -96,7 +96,7 @@ suspend fun KoolApplication.deferred2Test() { } addColorMesh { generate { - translate(0f, -1.5f, 0f) + translate(0f, -0.5f, 0f) color = Color.WHITE grid { sizeX = 50f @@ -112,7 +112,7 @@ suspend fun KoolApplication.deferred2Test() { val lighting = Lighting().apply { singlePointLight { - setup(Vec3f(1.2f, 1.2f, 2f)) + setup(Vec3f(1.2f, 3.2f, 2f)) setColor(Color.WHITE, intensity = 50f) } } @@ -158,7 +158,7 @@ suspend fun KoolApplication.deferred2Test() { outShader.bindTexture2d("ditherPattern", makeDitherPattern()) outShader.bindTexture2d("bloomOutput", bloomPass.bloomMap) - var inputTex by outShader.bindTexture2d("deferredOutput", deferred2Pipeline.gbuffers.a.encodedNormals) + var inputTex by outShader.bindTexture2d("deferredOutput") onUpdate { val filterOutput = deferred2Pipeline.filterPass.filterOutput.newVal outShader.swapPipelineDataCapturing(filterOutput) { @@ -172,11 +172,3 @@ suspend fun KoolApplication.deferred2Test() { ctx.scenes += debugOverlay() } - -class AlternatingPair(factory: (Boolean) -> T) { - val a: T = factory(true) - val b: T = factory(false) - - val newVal: T get() = if (Time.frameCount % 2 == 0) a else b - val oldVal: T get() = if (Time.frameCount % 2 == 0) b else a -} \ No newline at end of file diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt index 2c5e5e2ed..9f61b981e 100644 --- a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt +++ b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt @@ -29,7 +29,7 @@ class GbufferPass(content: Node, camera: Camera, initialSize: Vec2i, name: Strin addColor(TexFormat.RGBA, filterMethod = FilterMethod.NEAREST) // metal, roughness, ao, [empty: a, flags maybe?] addColor(TexFormat.RGBA, filterMethod = FilterMethod.NEAREST) - // encoded normals, alpha almost free + // normals (view space), alpha almost free addColor(TexFormat.RGBA, filterMethod = FilterMethod.NEAREST, clearColor = ClearColorFill(Color.ZERO)) // object-ids, meta addColor(TexFormat.R_I32, filterMethod = FilterMethod.NEAREST) @@ -40,7 +40,7 @@ class GbufferPass(content: Node, camera: Camera, initialSize: Vec2i, name: Strin ) { val albedoEmission get() = colorTextures[0] val metalRoughnessAo get() = colorTextures[1] - val encodedNormals get() = colorTextures[2] + val normals get() = colorTextures[2] val objectIds get() = colorTextures[3] val depth get() = depthTexture!! diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt index 8ed058cca..f58036143 100644 --- a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt +++ b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt @@ -49,7 +49,7 @@ class LightingPass( val newGbuffer = gbuffers.newVal lightingShader.swapPipelineDataCapturing(newGbuffer) { depthTex = newGbuffer.depth - encodedNormals = newGbuffer.encodedNormals + encodedNormals = newGbuffer.normals albedoEmissionTex = newGbuffer.albedoEmission metalRoughnessAoTex = newGbuffer.metalRoughnessAo frameI = Time.frameCount @@ -142,7 +142,6 @@ class DeferredLightingShader : KslShader("deferred2-lighting") { object DeferredCamDataStruct : Struct("DeferredCameraData", MemoryLayout.Std140) { val proj = mat4("projMat") -// val invProj = mat4("invProjMat") val invView = mat4("invView") val invViewProj = mat4("invViewProjMat") val viewport = float4("viewport") diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt index a55f2de8a..8d8a218df 100644 --- a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt +++ b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt @@ -5,6 +5,7 @@ import de.fabmax.kool.math.Vec3i import de.fabmax.kool.modules.ksl.KslComputeShader import de.fabmax.kool.modules.ksl.blocks.ColorSpaceConversion import de.fabmax.kool.modules.ksl.blocks.convertColorSpace +import de.fabmax.kool.modules.ksl.blocks.getLinearDepthReversed import de.fabmax.kool.modules.ksl.lang.* import de.fabmax.kool.pipeline.* import de.fabmax.kool.scene.Camera @@ -22,7 +23,7 @@ class TemporalFilterPass( val filterOutput = AlternatingPair { StorageTexture2d(size.x, size.y, filterStorageFmt, samplerSettings = SamplerSettings().clamped()) } - private val filterState = StorageTexture2d(size.x, size.y, TexFormat.R, samplerSettings = SamplerSettings().clamped().nearest()) + val filterState = StorageTexture2d(size.x, size.y, TexFormat.R, samplerSettings = SamplerSettings().clamped().nearest()) private val temporalShader = TemporalFilterShader(filterStorageFmt, lightingOutput, filterState) @@ -51,8 +52,7 @@ class TemporalFilterPass( val oldGbuffer = gbuffers.oldVal newDepth = newGbuffer.depth - oldAlbedo = oldGbuffer.albedoEmission - newAlbedo = newGbuffer.albedoEmission + oldDepth = oldGbuffer.depth oldMeta = oldGbuffer.objectIds newMeta = newGbuffer.objectIds oldFilter = filterOutput.oldVal @@ -74,11 +74,10 @@ class TemporalFilterShader( filterState: StorageTexture2d, ) : KslComputeShader("deferred2-temporal-filter") { var lightingOutput by bindTexture2d("lightingOutput", lightingOutput) - var oldAlbedo by bindTexture2d("oldAlbedo") - var newAlbedo by bindTexture2d("newAlbedo") var oldMeta by bindTexture2d("oldMeta") var newMeta by bindTexture2d("newMeta") var newDepth by bindTexture2d("newDepth") + var oldDepth by bindTexture2d("oldDepth") var objModelMats by bindStorage("objModelMats") var camData = bindUniformStruct("camData", FilterCamDataStruct) @@ -96,15 +95,13 @@ class TemporalFilterShader( private fun KslProgram.program() { computeStage(workGroupSizeX = 8, workGroupSizeY = 8) { val lightingOutput = texture2d("lightingOutput") - val oldAlbedo = texture2d("oldAlbedo") - val newAlbedo = texture2d("newAlbedo") val oldMeta = texture2dInt("oldMeta") val newMeta = texture2dInt("newMeta") val newDepth = texture2d("newDepth", isUnfilterable = true) + val oldDepth = texture2d("oldDepth", isUnfilterable = true) val invModelMatStruct = struct(ObjModelMatLayout) val objModelMats = storage("objModelMats", invModelMatStruct) -// val oldObjModelMats = storage("oldObjModelMats", invModelMatStruct) val oldFilter = texture2d("oldFilter") val newFilter = if (filterStorageFmt.channels == 3) { @@ -114,74 +111,69 @@ class TemporalFilterShader( } val filterState = storageTexture2d("filterState", TexFormat.R) - val frameI = uniformInt1("frameI") - val camData = uniformStruct("camData", FilterCamDataStruct) - val metaEqual = functionBool1("fnMetaEqual") { - val meta1 = paramInt1() - val meta2 = paramInt1() - - body { - val x1 by (meta1 shr 24.const) and 0xf.const - val y1 by (meta1 shr 28.const) and 0xf.const - val x2 by (meta2 shr 24.const) and 0xf.const - val y2 by (meta2 shr 28.const) and 0xf.const - (abs(x1 - x2) le 3.const) and (abs(y1 - y2) le 3.const) - } - } - main { val baseCoord by inGlobalInvocationId.xy.toInt2() val curMeta by newMeta.load(baseCoord).r val id by curMeta and 0xffffff.const + val oldSize by oldFilter.size().toFloat2() - val oldUv by (baseCoord.toFloat2() + 0.5f.const2) / oldFilter.size().toFloat2() + val camNear = camData[FilterCamDataStruct.camNear] + val invViewProj = camData[FilterCamDataStruct.invViewProj] + val worldPos by unprojectBaseCoord(newDepth, baseCoord, camNear, invViewProj) + + val oldUv by (baseCoord.toFloat2() + 0.5f.const2) / oldSize val oldBaseCoord by baseCoord `if`(id ne 0.const) { - val camNear = camData[FilterCamDataStruct.camNear] - val invViewProj = camData[FilterCamDataStruct.invViewProj] - val worldPos by unprojectBaseCoord(newDepth, baseCoord, camNear, invViewProj) val objModelMat = structVar(objModelMats[id]) val oldProj by objModelMat[ObjModelMatLayout.reprojectMat] * worldPos oldUv set oldProj.xy / oldProj.w * float2Value(0.5f, -0.5f) + 0.5f.const - oldBaseCoord set (oldUv * oldFilter.size().toFloat2()).toInt2() + oldBaseCoord set (oldUv * oldSize).toInt2() } - val oldStateCa by (oldUv * oldFilter.size().toFloat2() + float2Value(0.5f, 0.5f)).toInt2() - val oldStateCb by (oldUv * oldFilter.size().toFloat2() + float2Value(0.5f, -0.5f)).toInt2() - val oldStateCc by (oldUv * oldFilter.size().toFloat2() + float2Value(-0.5f, -0.5f)).toInt2() - val oldStateCd by (oldUv * oldFilter.size().toFloat2() + float2Value(-0.5f, 0.5f)).toInt2() - val oldState by (filterState.load(oldStateCa).r * 255f.const).toInt1() or - (filterState.load(oldStateCb).r * 255f.const).toInt1() or - (filterState.load(oldStateCc).r * 255f.const).toInt1() or - (filterState.load(oldStateCd).r * 255f.const).toInt1() + val oldStateBaseUv by oldUv * oldSize + val oldState by + (filterState.load((oldStateBaseUv + float2Value(0.5f, 0.5f)).toInt2()).r * 255f.const).toInt1() or + (filterState.load((oldStateBaseUv + float2Value(0.5f, -0.5f)).toInt2()).r * 255f.const).toInt1() or + (filterState.load((oldStateBaseUv + float2Value(-0.5f, -0.5f)).toInt2()).r * 255f.const).toInt1() or + (filterState.load((oldStateBaseUv + float2Value(-0.5f, 0.5f)).toInt2()).r * 255f.const).toInt1() val wasEdge by oldState and 1.const gt 0.const - val sameColorThresh = 0.25f.const - val curAlbedo by float4Var(newAlbedo.load(baseCoord)) - val colorDiff by length(curAlbedo - oldAlbedo.sample(oldUv)) - val sameColor by colorDiff lt sameColorThresh - val filterHit by sameColor + val near by camData[FilterCamDataStruct.camNear] + val refDepth by getLinearDepthReversed(newDepth.load(baseCoord).x, near) + val depthA by getLinearDepthReversed(newDepth.load(baseCoord + int2Value(1, 1)).x, near) + val depthB by getLinearDepthReversed(newDepth.load(baseCoord + int2Value(-1, -1)).x, near) + val depthC by getLinearDepthReversed(newDepth.load(baseCoord + int2Value(-1, 0)).x, near) + val depthD by getLinearDepthReversed(newDepth.load(baseCoord + int2Value(1, 0)).x, near) + val depthDab by min(abs(refDepth - depthA), abs(refDepth - depthB)) + (refDepth * 0.01f.const) + val depthDcd by min(abs(refDepth - depthC), abs(refDepth - depthD)) + (refDepth * 0.01f.const) + + val oldDepth by getLinearDepthReversed(oldDepth.load(oldBaseCoord).x, near) + val depthHit by abs(refDepth - oldDepth) lt (max(depthDab, depthDcd) * 2f.const) + val idHit by id eq (oldMeta.load(oldBaseCoord).r and 0xffffff.const) + val filterHit by idHit and depthHit val isEdge by false.const `if`(!filterHit or wasEdge) { -// val depth0 by 1f.const / newDepth.load(baseCoord, lod = 0.const).x -// val da by depthEdge(depth0, newDepth.load(baseCoord + int2Value(-1, -1)).x) -// val db by depthEdge(depth0, newDepth.load(baseCoord + int2Value(1, 1)).x) -// val dc by depthEdge(depth0, newDepth.load(baseCoord + int2Value(1, -1)).x) -// val dd by depthEdge(depth0, newDepth.load(baseCoord + int2Value(-1, 1)).x) -// isEdge set (da or db or dc or dd) - - val oldPxSz by 1f.const2 / oldFilter.size().toFloat2() - val da by length(curAlbedo - oldAlbedo.sample(oldUv + float2Value(-1f, -1f) * oldPxSz)) - val db by length(curAlbedo - oldAlbedo.sample(oldUv + float2Value(1f, 1f) * oldPxSz)) - val dc by length(curAlbedo - oldAlbedo.sample(oldUv + float2Value(1f, -1f) * oldPxSz)) - val dd by length(curAlbedo - oldAlbedo.sample(oldUv + float2Value(-1f, 1f) * oldPxSz)) - - val minD = min(colorDiff, min(min(da, db), min(dc, dd))) - val maxD = max(colorDiff, max(max(da, db), max(dc, dd))) - isEdge set ((minD lt sameColorThresh) and (maxD gt sameColorThresh)) + val hitA by abs(refDepth - depthA) lt depthDab * 2f.const + val hitB by abs(refDepth - depthB) lt depthDab * 2f.const + val hitC by abs(refDepth - depthC) lt depthDcd * 2f.const + val hitD by abs(refDepth - depthD) lt depthDcd * 2f.const + val anyYes by hitA or hitB or hitC or hitD + val anyNo by !hitA or !hitB or !hitC or !hitD + val depthEdge by anyYes and anyNo + + val borderCoords = listOf(Vec2i(-1, -1), Vec2i(1, 1), Vec2i(1, -1), Vec2i(-1, 1)) + val anyEq by false.const + val anyNe by false.const + borderCoords.forEach { bc -> + val sampleId = int1Var(newMeta.load(baseCoord + bc.const).r and 0xffffff.const) + anyEq set (anyEq or (sampleId eq id)) + anyNe set (anyNe or (sampleId ne id)) + } + val idEdge by anyEq and anyNe + isEdge set (depthEdge or idEdge) } oldState set isEdge.toInt1() @@ -208,28 +200,25 @@ class TemporalFilterShader( `if`(any(isNan(filtered))) { filtered set curSrgb } - newFilter[baseCoord] = float4Value(filtered, 1f) // `if`(wasEdge and !isEdge) { -// newFilter[baseCoord] = Color.RED.const +// filtered set Color.RED.const.rgb // } // `if`(isEdge) { -// newFilter[baseCoord] = Color.YELLOW.const +// filtered set Color.YELLOW.const.rgb +// } +// `if`(!filterHit) { +// filtered set Color.CYAN.const.rgb // } // `if`(w eq 0f.const) { // newFilter[baseCoord] = Color.CYAN.const // } + newFilter[baseCoord] = float4Value(filtered, 1f) } } } } -context(scope: KslScopeBuilder) -private fun depthEdge(baseDepthLin: KslExprFloat1, cmpDepth: KslExprFloat1): KslExprBool1 { - val cmpDepthLin = float1Var(1f.const / cmpDepth) - return min(baseDepthLin, cmpDepthLin) / max(baseDepthLin, cmpDepthLin) lt 0.99f.const -} - object FilterCamDataStruct : Struct("FilterCamData", MemoryLayout.Std140) { val invViewProj = mat4("invViewProj") val camNear = float1("camClipNear") From c12e5bcc8237af6bc3cd041ce94817f60bad1a8d Mon Sep 17 00:00:00 2001 From: Max Thiele Date: Sun, 17 May 2026 12:10:44 +0200 Subject: [PATCH 06/27] Moved deferred2 to a demo --- .../kotlin/de/fabmax/kool/demo/Demos.kt | 2 + .../kool/demo/deferred2/Deferred2Demo.kt | 162 ++++++++++++++++ .../kool/demo/deferred2/Deferred2Pipeline.kt | 0 .../fabmax/kool/demo/deferred2/GbufferPass.kt | 8 + .../kool/demo/deferred2/LightingPass.kt | 0 .../demo/deferred2/ProjectionFunctions.kt | 0 .../kool/demo/deferred2/TemporalFilterPass.kt | 0 .../kool/demo/deferred2/Deferred2Test.kt | 174 ------------------ 8 files changed, 172 insertions(+), 174 deletions(-) create mode 100644 kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt rename kool-demo/src/{desktopMain => commonMain}/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt (100%) rename kool-demo/src/{desktopMain => commonMain}/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt (98%) rename kool-demo/src/{desktopMain => commonMain}/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt (100%) rename kool-demo/src/{desktopMain => commonMain}/kotlin/de/fabmax/kool/demo/deferred2/ProjectionFunctions.kt (100%) rename kool-demo/src/{desktopMain => commonMain}/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt (100%) delete mode 100644 kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/Demos.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/Demos.kt index 65c68dd9c..b30e70ddb 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/Demos.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/Demos.kt @@ -5,6 +5,7 @@ import de.fabmax.kool.KoolSystem import de.fabmax.kool.Platform import de.fabmax.kool.demo.bees.BeeDemo import de.fabmax.kool.demo.creativecoding.CreativeCodingDemo +import de.fabmax.kool.demo.deferred2.Deferred2Demo import de.fabmax.kool.demo.helloworld.* import de.fabmax.kool.demo.pathtracing.PathTracingDemo import de.fabmax.kool.demo.pbr.PbrDemo @@ -65,6 +66,7 @@ object Demos { entry("gltf", "glTF Models") { GltfDemo() } entry("ssr", "Reflections") { ReflectionDemo() } entry("deferred", "Deferred Shading", NeedsComputeShaders) { DeferredDemo() } + entry("deferred2", "Deferred Shading 2", NeedsComputeShaders) { Deferred2Demo() } entry("procedural", "Procedural Roses", NeedsComputeShaders) { ProceduralDemo() } entry("pbr", "PBR Materials") { PbrDemo() } } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt new file mode 100644 index 000000000..7214af35f --- /dev/null +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt @@ -0,0 +1,162 @@ +package de.fabmax.kool.demo.deferred2 + +import de.fabmax.kool.KoolContext +import de.fabmax.kool.demo.DemoScene +import de.fabmax.kool.math.Vec3f +import de.fabmax.kool.math.deg +import de.fabmax.kool.modules.ksl.KslShader +import de.fabmax.kool.modules.ksl.blocks.ColorSpaceConversion +import de.fabmax.kool.modules.ksl.blocks.convertColorSpace +import de.fabmax.kool.modules.ksl.lang.* +import de.fabmax.kool.pipeline.BloomPass +import de.fabmax.kool.pipeline.FullscreenShaderUtil.fullscreenQuadVertexStage +import de.fabmax.kool.pipeline.FullscreenShaderUtil.generateFullscreenQuad +import de.fabmax.kool.pipeline.swapPipelineDataCapturing +import de.fabmax.kool.scene.* +import de.fabmax.kool.util.Color +import de.fabmax.kool.util.MdColor +import de.fabmax.kool.util.Time + +class Deferred2Demo : DemoScene("Deferred2 Demo") { + + private val albedoMap by texture2d("materials/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_COL_2K_METALNESS.jpg") + private val normalMap by texture2d("materials/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_NRM_2K_METALNESS.jpg") + //private val uvChecker by texture2d("materials/uv_checker_map.jpg") + + override fun Scene.setupMainScene(ctx: KoolContext) { + val content = deferredContent() + val lighting = Lighting().apply { + singlePointLight { + setup(Vec3f(1.2f, 3.2f, 2f)) + setColor(Color.WHITE, intensity = 50f) + } + } + val deferred2Pipeline = Deferred2Pipeline(content, lighting, this) + + content.apply { + val orbitCam = orbitCamera(deferred2Pipeline.camera) { } + addNode(orbitCam) + } + + val bloomPass = BloomPass(deferred2Pipeline.filterPass.filterOutput.newVal) + addComputePass(bloomPass) + deferred2Pipeline.onSwap { + val filterOutput = deferred2Pipeline.filterPass.filterOutput.newVal + bloomPass.inputShader.swapPipelineDataCapturing(filterOutput) { + bloomPass.inputTexture = filterOutput + } + } + + addTextureMesh { + generate { + generateFullscreenQuad() + } + val outShader = KslShader("deferred2-output") { + val uv = interStageFloat2() + fullscreenQuadVertexStage(uv) + fragmentStage { + main { + val output = de.fabmax.kool.modules.ksl.lang.texture2d("deferredOutput") + val bloom = de.fabmax.kool.modules.ksl.lang.texture2d("bloomOutput") + val uvi = (uv.output * output.size().toFloat2() + 0.5f.const2).toInt2() + val color by output.load(uvi).rgb + bloom.sample(uv.output).rgb + + val ditherTex = de.fabmax.kool.modules.ksl.lang.texture2d("ditherPattern") + val ditherC by uvi % ditherTex.size() + val ditherNoise by ditherTex.load(ditherC).r + val srgb by convertColorSpace(color, ColorSpaceConversion.LinearToSrgbHdr()) + (ditherNoise - 0.5f.const) / 255f.const + colorOutput(srgb) +// colorOutput(color) + } + } + } + + outShader.bindTexture2d("ditherPattern", makeDitherPattern()) + outShader.bindTexture2d("bloomOutput", bloomPass.bloomMap) + var inputTex by outShader.bindTexture2d("deferredOutput") + onUpdate { + val filterOutput = deferred2Pipeline.filterPass.filterOutput.newVal + outShader.swapPipelineDataCapturing(filterOutput) { + inputTex = filterOutput + } + } + + shader = outShader + } + } + + private fun deferredContent() = Node("deferred content").apply { + addGroup { + onUpdate { +// transform.rotate(-30f.deg * Time.deltaT, Vec3f.Y_AXIS) + } + + addColorMesh { + generate { + color = MdColor.PINK.toLinear() + cube { origin.set(-2.5f, 0f, 0f)} + color = MdColor.AMBER.toLinear() + icoSphere { + center.set(-2.5f, 0f, 2.5f) + steps = 4 + radius = 0.5f + } + } + onUpdate { +// transform.rotate(360f.deg * Time.deltaT, Vec3f.Y_AXIS) + } + shader = gbufferShader(objectId = 1) { + color { vertexColor() } + } + } + addTextureMesh(isNormalMapped = true) { + generate { + cube { } + } + onUpdate { + transform + .setIdentity() +// .rotate(90f.deg * Time.gameTime.toFloat(), Vec3f.Y_AXIS) + //.rotate(20f.deg, Vec3f.Y_AXIS) + .translate(2.5f, 0f, 0f) + } + shader = gbufferShader(objectId = 2) { + color { + textureColor(albedoMap) + } + normalMapping { + useNormalMap(normalMap) + } + }.apply { +// bindTexture2d("tbaseColor", uvChecker) + bindTexture2d("tbaseColor", albedoMap) + bindTexture2d("tNormalMap", normalMap) + } + } + addColorMesh { + generate { + cube { colored() } + } + onUpdate { + transform.rotate(90f.deg * Time.deltaT, Vec3f.Y_AXIS) + } + shader = gbufferShader(objectId = 3) { + color { vertexColor() } + } + } + addColorMesh { + generate { + translate(0f, -0.5f, 0f) + color = Color.WHITE + grid { + sizeX = 50f + sizeY = 50f + } + } + shader = gbufferShader(objectId = 4) { + color { vertexColor() } + } + } + } + } +} diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt similarity index 100% rename from kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt rename to kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt similarity index 98% rename from kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt rename to kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt index 9f61b981e..e4bb936d3 100644 --- a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt @@ -300,3 +300,11 @@ class GbufferShaderConfig(builder: Builder) { open fun build() = GbufferShaderConfig(this) } } + +fun gbufferShader(objectId: Int, block: GbufferShaderConfig.Builder.() -> Unit): GbufferShader { + val cfg = GbufferShaderConfig.Builder().apply{ + this.objectId = objectId + block() + }.build() + return GbufferShader(cfg) +} diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt similarity index 100% rename from kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt rename to kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/ProjectionFunctions.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ProjectionFunctions.kt similarity index 100% rename from kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/ProjectionFunctions.kt rename to kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ProjectionFunctions.kt diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt similarity index 100% rename from kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt rename to kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt deleted file mode 100644 index 06b4e22a0..000000000 --- a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt +++ /dev/null @@ -1,174 +0,0 @@ -package de.fabmax.kool.demo.deferred2 - -import de.fabmax.kool.Assets -import de.fabmax.kool.KoolApplication -import de.fabmax.kool.addScene -import de.fabmax.kool.loadTexture2d -import de.fabmax.kool.math.Vec3f -import de.fabmax.kool.math.deg -import de.fabmax.kool.modules.ksl.KslShader -import de.fabmax.kool.modules.ksl.blocks.ColorSpaceConversion -import de.fabmax.kool.modules.ksl.blocks.convertColorSpace -import de.fabmax.kool.modules.ksl.lang.* -import de.fabmax.kool.pipeline.BloomPass -import de.fabmax.kool.pipeline.FullscreenShaderUtil.fullscreenQuadVertexStage -import de.fabmax.kool.pipeline.FullscreenShaderUtil.generateFullscreenQuad -import de.fabmax.kool.pipeline.swapPipelineDataCapturing -import de.fabmax.kool.scene.* -import de.fabmax.kool.util.Color -import de.fabmax.kool.util.MdColor -import de.fabmax.kool.util.Time -import de.fabmax.kool.util.debugOverlay - -fun gbufferShader(objectId: Int, block: GbufferShaderConfig.Builder.() -> Unit): GbufferShader { - val cfg = GbufferShaderConfig.Builder().apply{ - this.objectId = objectId - block() - }.build() - return GbufferShader(cfg) -} - -suspend fun KoolApplication.deferred2Test() { - val texAlbedo = Assets.loadTexture2d("materials/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_COL_2K_METALNESS.jpg").getOrThrow() - val texNormal = Assets.loadTexture2d("materials/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_NRM_2K_METALNESS.jpg").getOrThrow() - val uvChecker = Assets.loadTexture2d("materials/uv_checker_map.jpg").getOrThrow() - - addScene { - val content = Node().apply { - - addGroup { - onUpdate { -// transform.rotate(-30f.deg * Time.deltaT, Vec3f.Y_AXIS) - } - - addColorMesh { - generate { - color = MdColor.PINK.toLinear() - cube { origin.set(-2.5f, 0f, 0f)} - color = MdColor.AMBER.toLinear() - icoSphere { - center.set(-2.5f, 0f, 2.5f) - steps = 4 - radius = 0.5f - } - } - onUpdate { -// transform.rotate(360f.deg * Time.deltaT, Vec3f.Y_AXIS) - } - shader = gbufferShader(objectId = 1) { - color { vertexColor() } - } - } - addTextureMesh(isNormalMapped = true) { - generate { - cube { } - } - onUpdate { - transform - .setIdentity() -// .rotate(90f.deg * Time.gameTime.toFloat(), Vec3f.Y_AXIS) - //.rotate(20f.deg, Vec3f.Y_AXIS) - .translate(2.5f, 0f, 0f) - } - shader = gbufferShader(objectId = 2) { - color { - textureColor(texAlbedo) - } - normalMapping { - useNormalMap(texNormal) - } - }.apply { -// bindTexture2d("tbaseColor", uvChecker) - bindTexture2d("tbaseColor", texAlbedo) - bindTexture2d("tNormalMap", texNormal) - } - } - addColorMesh { - generate { - cube { colored() } - } - onUpdate { - transform.rotate(90f.deg * Time.deltaT, Vec3f.Y_AXIS) - } - shader = gbufferShader(objectId = 3) { - color { vertexColor() } - } - } - addColorMesh { - generate { - translate(0f, -0.5f, 0f) - color = Color.WHITE - grid { - sizeX = 50f - sizeY = 50f - } - } - shader = gbufferShader(objectId = 4) { - color { vertexColor() } - } - } - } - } - - val lighting = Lighting().apply { - singlePointLight { - setup(Vec3f(1.2f, 3.2f, 2f)) - setColor(Color.WHITE, intensity = 50f) - } - } - val deferred2Pipeline = Deferred2Pipeline(content, lighting, this) - - content.apply { - val orbitCam = orbitCamera(deferred2Pipeline.camera) { } - addNode(orbitCam) - } - - val bloomPass = BloomPass(deferred2Pipeline.filterPass.filterOutput.newVal) - addComputePass(bloomPass) - deferred2Pipeline.onSwap { - val filterOutput = deferred2Pipeline.filterPass.filterOutput.newVal - bloomPass.inputShader.swapPipelineDataCapturing(filterOutput) { - bloomPass.inputTexture = filterOutput - } - } - - addTextureMesh { - generate { - generateFullscreenQuad() - } - val outShader = KslShader("deferred2-output") { - val uv = interStageFloat2() - fullscreenQuadVertexStage(uv) - fragmentStage { - main { - val output = texture2d("deferredOutput") - val bloom = texture2d("bloomOutput") - val uvi = (uv.output * output.size().toFloat2() + 0.5f.const2).toInt2() - val color by output.load(uvi).rgb + bloom.sample(uv.output).rgb - - val ditherTex = texture2d("ditherPattern") - val ditherC by uvi % ditherTex.size() - val ditherNoise by ditherTex.load(ditherC).r - val srgb by convertColorSpace(color, ColorSpaceConversion.LinearToSrgbHdr()) + (ditherNoise - 0.5f.const) / 255f.const - colorOutput(srgb) -// colorOutput(color) - } - } - } - - outShader.bindTexture2d("ditherPattern", makeDitherPattern()) - outShader.bindTexture2d("bloomOutput", bloomPass.bloomMap) - var inputTex by outShader.bindTexture2d("deferredOutput") - onUpdate { - val filterOutput = deferred2Pipeline.filterPass.filterOutput.newVal - outShader.swapPipelineDataCapturing(filterOutput) { - inputTex = filterOutput - } - } - - shader = outShader - } - } - - ctx.scenes += debugOverlay() -} From 04f20fb4c8f373a99e21c671bda2bdf30ba27261 Mon Sep 17 00:00:00 2001 From: Max Thiele Date: Sun, 17 May 2026 13:43:15 +0200 Subject: [PATCH 07/27] Settings and stuff --- .../kool/demo/deferred2/Deferred2Demo.kt | 150 ++++++++++++++---- .../kool/demo/deferred2/Deferred2Pipeline.kt | 65 +++++++- .../fabmax/kool/demo/deferred2/GbufferPass.kt | 75 +++------ .../kool/demo/deferred2/TemporalFilterPass.kt | 14 +- 4 files changed, 208 insertions(+), 96 deletions(-) diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt index 7214af35f..12427037e 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt @@ -1,21 +1,23 @@ package de.fabmax.kool.demo.deferred2 import de.fabmax.kool.KoolContext -import de.fabmax.kool.demo.DemoScene +import de.fabmax.kool.demo.* +import de.fabmax.kool.demo.menu.DemoMenu +import de.fabmax.kool.math.Vec2f import de.fabmax.kool.math.Vec3f import de.fabmax.kool.math.deg import de.fabmax.kool.modules.ksl.KslShader import de.fabmax.kool.modules.ksl.blocks.ColorSpaceConversion import de.fabmax.kool.modules.ksl.blocks.convertColorSpace import de.fabmax.kool.modules.ksl.lang.* +import de.fabmax.kool.modules.ui2.* import de.fabmax.kool.pipeline.BloomPass import de.fabmax.kool.pipeline.FullscreenShaderUtil.fullscreenQuadVertexStage import de.fabmax.kool.pipeline.FullscreenShaderUtil.generateFullscreenQuad +import de.fabmax.kool.pipeline.SingleColorTexture import de.fabmax.kool.pipeline.swapPipelineDataCapturing import de.fabmax.kool.scene.* -import de.fabmax.kool.util.Color -import de.fabmax.kool.util.MdColor -import de.fabmax.kool.util.Time +import de.fabmax.kool.util.* class Deferred2Demo : DemoScene("Deferred2 Demo") { @@ -23,6 +25,10 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { private val normalMap by texture2d("materials/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_NRM_2K_METALNESS.jpg") //private val uvChecker by texture2d("materials/uv_checker_map.jpg") + private lateinit var pipeline: Deferred2Pipeline + private val filterWeight = mutableStateOf(16) + private val bloom = mutableStateOf(true) + override fun Scene.setupMainScene(ctx: KoolContext) { val content = deferredContent() val lighting = Lighting().apply { @@ -31,17 +37,21 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { setColor(Color.WHITE, intensity = 50f) } } - val deferred2Pipeline = Deferred2Pipeline(content, lighting, this) + pipeline = Deferred2Pipeline(content, lighting, this) + pipeline.renderScale = 0.5f + filterWeight.value = pipeline.filterPass.filterWeight.toInt() + filterWeight.onChange { _, value -> pipeline.filterPass.filterWeight = value.toFloat() } content.apply { - val orbitCam = orbitCamera(deferred2Pipeline.camera) { } + val orbitCam = orbitCamera(pipeline.camera) { } addNode(orbitCam) } - val bloomPass = BloomPass(deferred2Pipeline.filterPass.filterOutput.newVal) + val bloomPass = BloomPass(pipeline.filterPass.filterOutput.newVal) addComputePass(bloomPass) - deferred2Pipeline.onSwap { - val filterOutput = deferred2Pipeline.filterPass.filterOutput.newVal + bloom.onChange { _, value -> bloomPass.isEnabled = value } + pipeline.onSwap { + val filterOutput = pipeline.filterPass.filterOutput.newVal bloomPass.inputShader.swapPipelineDataCapturing(filterOutput) { bloomPass.inputTexture = filterOutput } @@ -51,38 +61,86 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { generate { generateFullscreenQuad() } - val outShader = KslShader("deferred2-output") { - val uv = interStageFloat2() - fullscreenQuadVertexStage(uv) - fragmentStage { - main { - val output = de.fabmax.kool.modules.ksl.lang.texture2d("deferredOutput") - val bloom = de.fabmax.kool.modules.ksl.lang.texture2d("bloomOutput") - val uvi = (uv.output * output.size().toFloat2() + 0.5f.const2).toInt2() - val color by output.load(uvi).rgb + bloom.sample(uv.output).rgb - - val ditherTex = de.fabmax.kool.modules.ksl.lang.texture2d("ditherPattern") - val ditherC by uvi % ditherTex.size() - val ditherNoise by ditherTex.load(ditherC).r - val srgb by convertColorSpace(color, ColorSpaceConversion.LinearToSrgbHdr()) + (ditherNoise - 0.5f.const) / 255f.const - colorOutput(srgb) -// colorOutput(color) + shader = deferredOutputShader(pipeline, bloomPass) + } + } + + override fun createMenu(menu: DemoMenu, ctx: KoolContext): UiSurface = menuSurface { + LabeledSwitch("Bloom", bloom) { } + MenuSlider1("Filter", filterWeight.use().toFloat(), 0f, 32f, { "${it.toInt()}" }) { + filterWeight.set(it.toInt()) + } + MenuRow { + var tsaaIndex by remember(2) + Text("Temporal AA".l) { labelStyle(120.dp) } + ComboBox { + modifier + .width(Grow.Std) + .margin(start = sizes.largeGap) + .items(TsaaItem.items.map { it.label }) + .selectedIndex(tsaaIndex) + .onItemSelected { + tsaaIndex = it + pipeline.tsaa = TsaaItem.items[it].tsaa } - } } + } + MenuRow { + var scaleIndex by remember(2) + Text("Render scale".l) { labelStyle(120.dp) } + ComboBox { + modifier + .width(Grow.Std) + .margin(start = sizes.largeGap) + .items(ScaleItem.items.map { it.label }) + .selectedIndex(scaleIndex) + .onItemSelected { + scaleIndex = it + pipeline.renderScale = ScaleItem.items[it].scale + } + } + } + } - outShader.bindTexture2d("ditherPattern", makeDitherPattern()) - outShader.bindTexture2d("bloomOutput", bloomPass.bloomMap) - var inputTex by outShader.bindTexture2d("deferredOutput") - onUpdate { - val filterOutput = deferred2Pipeline.filterPass.filterOutput.newVal - outShader.swapPipelineDataCapturing(filterOutput) { - inputTex = filterOutput + private fun deferredOutputShader( + deferred2Pipeline: Deferred2Pipeline, + bloomPass: BloomPass + ): KslShader { + val outputShader = KslShader("deferred2-output") { + val uv = interStageFloat2() + fullscreenQuadVertexStage(uv) + fragmentStage { + main { + val output = de.fabmax.kool.modules.ksl.lang.texture2d("deferredOutput") + val bloom = de.fabmax.kool.modules.ksl.lang.texture2d("bloomOutput") + val uvi = (uv.output * output.size().toFloat2() + 0.5f.const2).toInt2() + val color by output.load(uvi).rgb + bloom.sample(uv.output).rgb + + val ditherTex = de.fabmax.kool.modules.ksl.lang.texture2d("ditherPattern") + val ditherC by uvi % ditherTex.size() + val ditherNoise by ditherTex.load(ditherC).r + val srgb by convertColorSpace(color, ColorSpaceConversion.LinearToSrgbHdr()) + (ditherNoise - 0.5f.const) / 255f.const + colorOutput(srgb) +// colorOutput(color) } } + } - shader = outShader + val ditherTex = makeDitherPattern() + ditherTex.releaseWith(mainScene) + + outputShader.bindTexture2d("ditherPattern", ditherTex) + var bloomTex by outputShader.bindTexture2d("bloomOutput", bloomPass.bloomMap) + var inputTex by outputShader.bindTexture2d("deferredOutput") + val noBloom = SingleColorTexture(Color.BLACK) + deferred2Pipeline.onSwap { + val filterOutput = deferred2Pipeline.filterPass.filterOutput.newVal + outputShader.swapPipelineDataCapturing(filterOutput) { + inputTex = filterOutput + bloomTex = if (bloom.value) bloomPass.bloomMap else noBloom + } } + return outputShader } private fun deferredContent() = Node("deferred content").apply { @@ -160,3 +218,27 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { } } } + +private data class TsaaItem(val label: String, val tsaa: List) { + companion object { + val items = listOf( + TsaaItem("None", Deferred2Pipeline.TSAA_NONE), + TsaaItem("2x", Deferred2Pipeline.TSAA_2), + TsaaItem("4x", Deferred2Pipeline.TSAA_4), + TsaaItem("8x", Deferred2Pipeline.TSAA_8), + TsaaItem("16x", Deferred2Pipeline.TSAA_16) + ) + } +} + +private data class ScaleItem(val label: String, val scale: Float) { + companion object { + val items = listOf( + ScaleItem("0.1x", 0.1f), + ScaleItem("0.25x", 0.25f), + ScaleItem("0.5x", 0.5f), + ScaleItem("0.75x", 0.75f), + ScaleItem("1x", 1f), + ) + } +} diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt index 9c0a6084d..412e2b660 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt @@ -1,5 +1,7 @@ package de.fabmax.kool.demo.deferred2 +import de.fabmax.kool.math.MutableMat4f +import de.fabmax.kool.math.Vec2f import de.fabmax.kool.math.Vec2i import de.fabmax.kool.pipeline.BufferedImageData2d import de.fabmax.kool.pipeline.TexFormat @@ -13,19 +15,18 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.yield -private const val renderScale = 0.5f -val tsaa = GbufferPass.TSAA_PATTERN_8 - class Deferred2Pipeline( val content: Node, val lighting: Lighting = Lighting(), scene: Scene, + var renderScale: Float = 1f, + var tsaa: List = TSAA_4, ) { val camera = PerspectiveCamera() val gbuffers = AlternatingPair { val suff = if (it) "A" else "B" - GbufferPass(content, camera, scene.renderSize, "deferred2-gbuffer-pass-$suff") + GbufferPass(content, camera, scene.renderSize, "deferred2-gbuffer-pass-$suff", this) } private val swapListeners = BufferedList<() -> Unit>() @@ -43,6 +44,9 @@ class Deferred2Pipeline( size = scene.renderSize, ) + internal val prevViewProjMats = List(1024) { MutableMat4f() } + + init { scene.addOffscreenPass(gbuffers.a) scene.addOffscreenPass(gbuffers.b) @@ -53,6 +57,7 @@ class Deferred2Pipeline( scene.onRenderScene += { val newSize = scene.renderSize if (size != newSize) { + logD { "Resizing to ${newSize.x}x${newSize.y}" } size = newSize gbuffers.a.setSize(size.x, size.y) gbuffers.b.setSize(size.x, size.y) @@ -87,6 +92,52 @@ class Deferred2Pipeline( (mainRenderPass.viewport.width * renderScale).toInt().coerceAtLeast(1), (mainRenderPass.viewport.height * renderScale).toInt().coerceAtLeast(1) ) + + companion object { + private val s = 1f/16f + val TSAA_NONE = listOf(Vec2f.ZERO) + val TSAA_2 = listOf( + Vec2f(4 * s, 4 * s), + Vec2f(-4 * s, -4 * s), + ) + val TSAA_4 = listOf( + Vec2f(-2 * s, -6 * s), + Vec2f(6 * s, -2 * s), + Vec2f(-6 * s, -2 * s), + Vec2f(2 * s, 6 * s), + ) + val TSAA_8 = listOf( + Vec2f(1 * s, -3 * s), + Vec2f(7 * s, -7 * s), + Vec2f(-1 * s, 3 * s), + Vec2f(5 * s, 1 * s), + Vec2f(3 * s, 7 * s), + Vec2f(-3 * s, -5 * s), + Vec2f(-7 * s, -1 * s), + Vec2f(-5 * s, -5 * s), + ) + val TSAA_16 = listOf( + Vec2f(1 * s, 1 * s), + Vec2f(-5 * s, -2 * s), + Vec2f(-2 * s, 6 * s), + Vec2f(-8 * s, 0 * s), + + Vec2f(-1 * s, -3 * s), + Vec2f(2 * s, 5 * s), + Vec2f(0 * s, -7 * s), + Vec2f(7 * s, -4 * s), + + Vec2f(-3 * s, 2 * s), + Vec2f(5 * s, 3 * s), + Vec2f(-4 * s, -6 * s), + Vec2f(6 * s, 7 * s), + + Vec2f(4 * s, -1 * s), + Vec2f(3 * s, -5 * s), + Vec2f(-6 * s, 4 * s), + Vec2f(-7 * s, -8 * s), + ) + } } fun makeDitherPattern(): Texture2d { @@ -123,4 +174,8 @@ class AlternatingPair(factory: (Boolean) -> T) { val newVal: T get() = if (Time.frameCount % 2 == 0) a else b val oldVal: T get() = if (Time.frameCount % 2 == 0) b else a -} \ No newline at end of file +} + +object ObjModelMatLayout : Struct("obj_model_mat", MemoryLayout.Std140) { + val reprojectMat = mat4("reprojectMat") +} diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt index e4bb936d3..1b4b3b000 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt @@ -1,7 +1,6 @@ package de.fabmax.kool.demo.deferred2 import de.fabmax.kool.math.MutableMat4f -import de.fabmax.kool.math.Vec2f import de.fabmax.kool.math.Vec2i import de.fabmax.kool.modules.ksl.BasicVertexConfig import de.fabmax.kool.modules.ksl.KslShader @@ -14,15 +13,18 @@ import de.fabmax.kool.scene.Camera import de.fabmax.kool.scene.Node import de.fabmax.kool.scene.VertexLayouts import de.fabmax.kool.scene.vertexAttrib -import de.fabmax.kool.util.* - -object ObjModelMatLayout : Struct("obj_model_mat", MemoryLayout.Std140) { - val reprojectMat = mat4("reprojectMat") -} - -private val prevViewProjMats = List(1024) { MutableMat4f() } - -class GbufferPass(content: Node, camera: Camera, initialSize: Vec2i, name: String) : OffscreenPass2d( +import de.fabmax.kool.util.Color +import de.fabmax.kool.util.StructBuffer +import de.fabmax.kool.util.Time +import de.fabmax.kool.util.asStorageBuffer + +class GbufferPass( + content: Node, + camera: Camera, + initialSize: Vec2i, + name: String, + val pipeline: Deferred2Pipeline, +) : OffscreenPass2d( drawNode = content, attachmentConfig = AttachmentConfig { // albedo, a * 255 = emission strength @@ -53,9 +55,12 @@ class GbufferPass(content: Node, camera: Camera, initialSize: Vec2i, name: Strin val offsetMat = MutableMat4f() camera.onCameraUpdated += { - val offset = tsaa[Time.frameCount % tsaa.size] - offsetMat.setIdentity().translate(offset.x / width, offset.y / height, 0f).mul(camera.proj) - camera.proj.set(offsetMat) + val tsaa = pipeline.tsaa + if (tsaa.isNotEmpty()) { + val offset = tsaa[Time.frameCount % tsaa.size] + offsetMat.setIdentity().translate(offset.x / width, offset.y / height, 0f).mul(camera.proj) + camera.proj.set(offsetMat) + } } val inverseBuf = MutableMat4f() @@ -65,7 +70,7 @@ class GbufferPass(content: Node, camera: Camera, initialSize: Vec2i, name: Strin (cmd.mesh.shader as? GbufferShader)?.let { gbufferShader -> val id = gbufferShader.objectId objModelMats.set(id) { - val prevViewProj = prevViewProjMats[id] + val prevViewProj = pipeline.prevViewProjMats[id] inverseBuf.set(cmd.modelMatF).invert() reprojectBuf.set(prevViewProj).mul(inverseBuf) set(it.reprojectMat, reprojectBuf) @@ -74,48 +79,8 @@ class GbufferPass(content: Node, camera: Camera, initialSize: Vec2i, name: Strin } } } - } - companion object { - private val s = 1f/16f - val TSAA_PATTERN_NONE = listOf(Vec2f.ZERO) - val TSAA_PATTERN_4 = listOf( - Vec2f(-2 * s, -6 * s), - Vec2f(6 * s, -2 * s), - Vec2f(-6 * s, -2 * s), - Vec2f(2 * s, 6 * s), - ) - val TSAA_PATTERN_8 = listOf( - Vec2f(1 * s, -3 * s), - Vec2f(7 * s, -7 * s), - Vec2f(-1 * s, 3 * s), - Vec2f(5 * s, 1 * s), - Vec2f(3 * s, 7 * s), - Vec2f(-3 * s, -5 * s), - Vec2f(-7 * s, -1 * s), - Vec2f(-5 * s, -5 * s), - ) - val TSAA_PATTERN_16 = listOf( - Vec2f(1 * s, 1 * s), - Vec2f(-5 * s, -2 * s), - Vec2f(-2 * s, 6 * s), - Vec2f(-8 * s, 0 * s), - - Vec2f(-1 * s, -3 * s), - Vec2f(2 * s, 5 * s), - Vec2f(0 * s, -7 * s), - Vec2f(7 * s, -4 * s), - - Vec2f(-3 * s, 2 * s), - Vec2f(5 * s, 3 * s), - Vec2f(-4 * s, -6 * s), - Vec2f(6 * s, 7 * s), - - Vec2f(4 * s, -1 * s), - Vec2f(3 * s, -5 * s), - Vec2f(-6 * s, 4 * s), - Vec2f(-7 * s, -8 * s), - ) + onRelease { objModelMatsGpu.release() } } } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt index 8d8a218df..cf5ee6fca 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt @@ -27,8 +27,15 @@ class TemporalFilterPass( private val temporalShader = TemporalFilterShader(filterStorageFmt, lightingOutput, filterState) + var filterWeight by temporalShader.bindUniformFloat1("uFilterWeight", 8f) + init { setupPasses() + onRelease { + filterOutput.a.release() + filterOutput.b.release() + filterState.release() + } } fun resize(size: Vec2i) { @@ -111,9 +118,11 @@ class TemporalFilterShader( } val filterState = storageTexture2d("filterState", TexFormat.R) - val frameI = uniformInt1("frameI") +// val frameI = uniformInt1("frameI") val camData = uniformStruct("camData", FilterCamDataStruct) + val filterWeight = uniformFloat1("uFilterWeight") + main { val baseCoord by inGlobalInvocationId.xy.toInt2() val curMeta by newMeta.load(baseCoord).r @@ -182,10 +191,11 @@ class TemporalFilterShader( // val filterNoise by noise31(uint3Value(inGlobalInvocationId.xy, frameI.toUint1())) // val w by 4f.const // val w by 8f.const //- filterNoise * filterNoise * filterNoise * 4f.const - val w by 16f.const //- filterNoise * filterNoise * filterNoise * 14f.const +// val w by 16f.const //- filterNoise * filterNoise * filterNoise * 14f.const // val w by 32f.const - filterNoise * filterNoise * filterNoise * 16f.const // val w by 100f.const - filterNoise * filterNoise * filterNoise * 75f.const + val w by filterWeight `if`((!filterHit and !isEdge) or (wasEdge and !isEdge)) { w set 0f.const } From 29bdb92cb1a35aced1f4071a3857dbde6bee1987 Mon Sep 17 00:00:00 2001 From: Max Thiele Date: Sun, 17 May 2026 20:03:31 +0200 Subject: [PATCH 08/27] Integrate ao into deferred 2 --- .../kool/pipeline/NormalDepthMapPass.kt | 3 +- .../kool/pipeline/ao/AmbientOcclusionPass.kt | 2 +- .../de/fabmax/kool/pipeline/ao/AoPipeline.kt | 53 +++-- .../kool/pipeline/ao/ComputeAoPipeline.kt | 200 ++++++++++-------- .../kool/pipeline/ao/LegacyAoPipeline.kt | 8 +- .../kotlin/de/fabmax/kool/scene/Camera.kt | 2 +- .../kotlin/de/fabmax/kool/demo/AoDemo.kt | 9 +- .../kotlin/de/fabmax/kool/demo/GltfDemo.kt | 5 +- .../kool/demo/deferred2/Deferred2Demo.kt | 105 ++++----- .../kool/demo/deferred2/Deferred2Pipeline.kt | 85 ++++++-- .../fabmax/kool/demo/deferred2/GbufferPass.kt | 12 -- .../kool/demo/deferred2/LightingPass.kt | 26 ++- .../demo/deferred2/ProjectionFunctions.kt | 14 +- .../kool/demo/deferred2/TemporalFilterPass.kt | 25 ++- .../kool/demo/physics/ragdoll/RagdollDemo.kt | 3 +- .../kool/demo/physics/terrain/TerrainDemo.kt | 3 +- .../kool/demo/procedural/ProceduralDemo.kt | 3 +- 17 files changed, 343 insertions(+), 215 deletions(-) diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/NormalDepthMapPass.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/NormalDepthMapPass.kt index f9c4b51b7..ffd6d85f6 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/NormalDepthMapPass.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/NormalDepthMapPass.kt @@ -27,7 +27,8 @@ class NormalDepthMapPass( name: String = UniqueId.nextId("normal-depth-map-pass") ) : OffscreenPass2d(drawNode, attachmentConfig, initialSize, name) { - val encodedNormalMap: Texture2d get() = colorTexture!! + val depth: Texture2d get() = depthTexture!! + val viewSpaceNormals: Texture2d get() = colorTexture!! var cullMethod: CullMethod? = null diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/ao/AmbientOcclusionPass.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/ao/AmbientOcclusionPass.kt index b17ab0b51..dcd40acd4 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/ao/AmbientOcclusionPass.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/ao/AmbientOcclusionPass.kt @@ -77,7 +77,7 @@ class AmbientOcclusionPass(val aoSetup: AoSetup, width: Int, height: Int) : private fun generateKernels(nKernels: Int) { val n = min(nKernels, MAX_KERNEL_SIZE) - generateAoSampleDirs(n).forEachIndexed { i, k -> + AoPipeline.generateAoSampleDirs(n).forEachIndexed { i, k -> aoPassShader.uKernel[i] = k } aoPassShader.uKernelSize = n diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/ao/AoPipeline.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/ao/AoPipeline.kt index 137de0488..fb529da9c 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/ao/AoPipeline.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/ao/AoPipeline.kt @@ -11,6 +11,7 @@ import de.fabmax.kool.scene.PerspectiveCamera import de.fabmax.kool.scene.Scene import de.fabmax.kool.util.Releasable import de.fabmax.kool.util.Uint8Buffer +import kotlin.jvm.JvmInline import kotlin.math.* import kotlin.random.Random @@ -18,7 +19,7 @@ interface AoPipeline : Releasable { val aoMap: Texture2d var isEnabled: Boolean - var radius: Float + var radius: AoRadius var strength: Float var falloff: Float var kernelSize: Int @@ -47,21 +48,27 @@ interface AoPipeline : Releasable { } fun createDeferred(deferredPipeline: DeferredPipeline) = DeferredAoPipeline(deferredPipeline) - } -} - -internal fun generateAoSampleDirs(numDirs: Int): List { - val scales = (0 until numDirs).map { lerp(0.1f, 1f, (it.toFloat() / (numDirs-1)).pow(2)) } - - return buildList { - repeat(numDirs) { i -> - val xi = hammersley(i, numDirs) - val phi = 2f * PI.toFloat() * xi.x - val cosTheta = sqrt((1f - xi.y)) - val sinTheta = sqrt(1f - cosTheta * cosTheta) - val k = MutableVec3f(sinTheta * cos(phi), sinTheta * sin(phi), cosTheta) - add(k.norm().mul(scales[i])) + fun generateAoSampleDirs(numDirs: Int, numTemporal: Int = 1): List { + val scales = (0 until numDirs).map { lerp(0.1f, 1f, (it.toFloat() / (numDirs-1)).pow(2)) } + val r = Random(12345) + val dirs = (0 until numDirs * numTemporal).map { i -> + hammersley(i, numDirs * numTemporal) + }.shuffled(r) + + return buildList { + repeat(numTemporal) { t -> + repeat(numDirs) { i -> + val xi = dirs[i + t * numDirs] + val phi = 2f * PI.toFloat() * xi.x + val cosTheta = sqrt((1f - xi.y)) + val sinTheta = sqrt(1f - cosTheta * cosTheta) + + val k = MutableVec3f(sinTheta * cos(phi), sinTheta * sin(phi), cosTheta + 0.1f).norm() + add(k.mul(scales[i])) + } + } + } } } } @@ -100,4 +107,20 @@ internal fun generateFilterNoiseTex(size: Int): Texture2d { val data = BufferedImageData2d(buf, size, size, TexFormat.RG) return Texture2d(TexFormat.RG, MipMapping.Off, SamplerSettings().nearest(), "ao_noise_tex") { data } +} + +@JvmInline +value class AoRadius(val radius: Float) { + companion object { + /** + * Absolute radius in world space units. + */ + fun absoluteRadius(radius: Float) = AoRadius(abs(radius)) + + /** + * Relative radius depending on screen depth. E.g., a relative radius of 0.05 resolves to a world space radius + * of 1 unit in 20 units distance. + */ + fun relativeRadius(radius: Float) = AoRadius(-abs(radius)) + } } \ No newline at end of file diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/ao/ComputeAoPipeline.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/ao/ComputeAoPipeline.kt index e7cf40f9d..f4e7322e3 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/ao/ComputeAoPipeline.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/ao/ComputeAoPipeline.kt @@ -9,10 +9,7 @@ import de.fabmax.kool.modules.ksl.blocks.getLinearDepthReversed import de.fabmax.kool.modules.ksl.lang.* import de.fabmax.kool.pipeline.* import de.fabmax.kool.scene.* -import de.fabmax.kool.util.BaseReleasable -import de.fabmax.kool.util.SyncedScope -import de.fabmax.kool.util.logD -import de.fabmax.kool.util.releaseWith +import de.fabmax.kool.util.* import kotlinx.coroutines.launch class ComputeAoPipeline(val scene: Scene, camera: PerspectiveCamera, drawNode: Node) : AoPipeline, BaseReleasable() { @@ -20,25 +17,29 @@ class ComputeAoPipeline(val scene: Scene, camera: PerspectiveCamera, drawNode: N val depthPass = NormalDepthMapPass( drawNode = drawNode, initialSize = Vec2i( - scene.mainRenderPass.viewport.width.coerceAtLeast(1), - scene.mainRenderPass.viewport.height.coerceAtLeast(1) + scene.mainRenderPass.viewport.width.coerceAtLeast(16), + scene.mainRenderPass.viewport.height.coerceAtLeast(16) ) ) val computePass: ComputeAoPass = ComputeAoPass( - normalDepthPass = depthPass, camera = proxyCamera, + inputDepth = depthPass.depth, + inputNormals = depthPass.viewSpaceNormals, distFormat = TexFormat.R_F16, + initialSize = Vec2i( + scene.mainRenderPass.viewport.width.coerceAtLeast(16), + scene.mainRenderPass.viewport.height.coerceAtLeast(16) + ), ) - override val aoMap: Texture2d - get() = computePass.finalAo + override val aoMap: Texture2d get() = computePass.aoMap override var isEnabled: Boolean = true set(value) { field = value applyEnabled(value) } - override var radius: Float by computePass::radius + override var radius: AoRadius by computePass::radius override var strength: Float by computePass::strength override var falloff: Float by computePass::falloff override var kernelSize: Int by computePass::kernelSize @@ -63,12 +64,11 @@ class ComputeAoPipeline(val scene: Scene, camera: PerspectiveCamera, drawNode: N } private fun onRenderScene() { - val mapW = scene.mainRenderPass.viewport.width - val mapH = scene.mainRenderPass.viewport.height - - if (mapW > 0 && mapH > 0) { - depthPass.setSize(mapW, mapH) - computePass.applySizeFromDepthPass() + val width = scene.mainRenderPass.viewport.width + val height = scene.mainRenderPass.viewport.height + if (width > 0 && height > 0) { + depthPass.setSize(width, height) + computePass.resize(width, height) } } @@ -82,26 +82,31 @@ class ComputeAoPipeline(val scene: Scene, camera: PerspectiveCamera, drawNode: N } class ComputeAoPass( - val normalDepthPass: NormalDepthMapPass, val camera: Camera, - val distFormat: TexFormat, + inputDepth: Texture2d, + inputNormals: Texture2d, + initialSize: Vec2i, + val distFormat: TexFormat = TexFormat.R_F16, ) : ComputePass("AO Pass") { - private val inputDepth: Texture2d get() = normalDepthPass.depthTexture!! - private val inputNormals: Texture2d get() = normalDepthPass.colorTexture!! - - private val fullWidth: Int get() = normalDepthPass.width.coerceAtLeast(1) - private val fullHeight: Int get() = normalDepthPass.height.coerceAtLeast(1) - private val scaledWidth: Int get() = (fullWidth / 2).coerceAtLeast(1) - private val scaledHeight: Int get() = (fullHeight / 2).coerceAtLeast(1) - - val scaledNormals = StorageTexture2d(scaledWidth, scaledHeight, TexFormat.RGBA, mipMapping = MipMapping.Limited(SCALE_LEVELS), name = "normalOutput") - val scaledDists = StorageTexture2d(scaledWidth, scaledHeight, distFormat, mipMapping = MipMapping.Limited(SCALE_LEVELS), name = "distOutput") - val aoNoisy = StorageTexture2d(scaledWidth, scaledHeight, TexFormat.R, name = "aoOutputNoisy") - val filteredAo = StorageTexture2d(scaledWidth, scaledHeight, TexFormat.R, name = "filteredAo") - val finalAo = StorageTexture2d(fullWidth, fullHeight, TexFormat.R, name = "finalAo") + val aoMap = StorageTexture2d(initialSize.x, initialSize.y, TexFormat.R, name = "finalAo") + val width: Int get() = aoMap.width + val height: Int get() = aoMap.height + + private val halfWidth: Int get() = (width / 2).coerceAtLeast(1) + private val halfHeight: Int get() = (height / 2).coerceAtLeast(1) + + val scaledNormals = StorageTexture2d(halfWidth, halfHeight, TexFormat.RGBA, mipMapping = MipMapping.Limited(SCALE_LEVELS), name = "normalOutput") + val scaledDists = StorageTexture2d(halfWidth, halfHeight, distFormat, mipMapping = MipMapping.Limited(SCALE_LEVELS), name = "distOutput") + val aoNoisy = StorageTexture2d(halfWidth, halfHeight, TexFormat.R, name = "aoOutputNoisy") + val filteredAo = StorageTexture2d(halfWidth, halfHeight, TexFormat.R, name = "filteredAo") private val noiseTex = generateFilterNoiseTex(NOISE_TEX_SZ) + private val kernelBuffer = StructBuffer(KernelStruct, KERNEL_BUF_SIZE) + private val kernelBufferGpu = kernelBuffer.asStorageBuffer() + + val inputShader = initDownSamplePass(inputNormals, inputDepth) + var inputDepth by inputShader.bindTexture2d("distInput", inputDepth, SamplerSettings().nearest().clamped()) + var inputNormals by inputShader.bindTexture2d("normalInput", inputNormals, SamplerSettings().nearest().clamped()) - private val downSampleShader = initDownSamplePass() private val downSampleLowerShader = initDownSampleLowerPass() private val aoShader = initAoPass() private val denoiseShader = initDenoisePass() @@ -111,24 +116,32 @@ class ComputeAoPass( private var uProj by aoShader.bindUniformMat4("uProj") private var uInvProj by aoShader.bindUniformMat4("uInvProj") private var uCamNear by aoShader.bindUniformFloat1("uCamNear") + private var uFrameI by aoShader.bindUniformInt1("uFrameI") var kernelSize = 16 set(value) { field = value.coerceIn(1, MAX_KERNEL_SIZE) - generateAoSampleDirs(kernelSize).forEachIndexed { i, kernel -> uKernels[i] = kernel } + updateKernels(field, temporalKernels) uKernelSize = field } - private var uKernelSize by aoShader.bindUniformInt1("uKernelRange", kernelSize) - private val uKernels = aoShader.bindUniformFloat3Array("uKernel", MAX_KERNEL_SIZE) + var temporalKernels = 1 + set(value) { + field = value.coerceIn(1, MAX_KERNEL_TERMPORAL_SIZE) + updateKernels(kernelSize, field) + uKernelTemporalSize = field + } - var radius: Float = 1f + private var uKernelSize by aoShader.bindUniformInt1("uKernelSize", kernelSize) + private var uKernelTemporalSize by aoShader.bindUniformInt1("uKernelTemporalSize", temporalKernels) + + var radius: AoRadius = AoRadius.absoluteRadius(1f) set(value) { field = value - uAoRadius = value - uDenoiseRadius = value + uAoRadius = value.radius + uDenoiseRadius = value.radius } - private var uAoRadius by aoShader.bindUniformFloat1("uRadius", radius) - private var uDenoiseRadius by denoiseShader.bindUniformFloat1("uRadius", radius) + private var uAoRadius by aoShader.bindUniformFloat1("uRadius", radius.radius) + private var uDenoiseRadius by denoiseShader.bindUniformFloat1("uRadius", radius.radius) var strength by aoShader.bindUniformFloat1("uStrength", 1.25f) var falloff by aoShader.bindUniformFloat1("uFalloff", 1.5f) @@ -138,22 +151,23 @@ class ComputeAoPass( companion object { const val MAX_KERNEL_SIZE = 64 + const val MAX_KERNEL_TERMPORAL_SIZE = 16 + + private const val KERNEL_BUF_SIZE = MAX_KERNEL_SIZE * MAX_KERNEL_TERMPORAL_SIZE private const val NOISE_TEX_SZ = 4 private const val SCALE_LEVELS = 4 } init { - scaledNormals.releaseWith(this) - scaledDists.releaseWith(this) - noiseTex.releaseWith(this) - - generateAoSampleDirs(kernelSize).forEachIndexed { i, kernel -> uKernels[i] = kernel } + updateKernels(kernelSize, temporalKernels) + onUpdate { captureCamera() } + } - onUpdate { - uProj = camera.proj - uInvProj = camera.invProj - uCamNear = camera.clipNear - } + fun captureCamera() { + uProj = camera.proj + uInvProj = camera.invProj + uCamNear = camera.clipNear + uFrameI = Time.frameCount } override fun doRelease() { @@ -162,32 +176,31 @@ class ComputeAoPass( scaledDists.release() aoNoisy.release() filteredAo.release() - finalAo.release() + aoMap.release() noiseTex.release() + kernelBufferGpu.release() } - internal fun applySizeFromDepthPass() { + fun resize(width: Int, height: Int) { if (!isEnabled) { return } if (doClear) { - if (!isConfigured || finalAo.width != 8 || finalAo.height != 8) { - finalAo.resize(8, 8) + if (!isConfigured || aoMap.width != 8 || aoMap.height != 8) { + aoMap.resize(8, 8) SyncedScope.launch { clearAndReleaseTasks() makeClearPass() } } - } else if (!isConfigured || fullWidth != finalAo.width || fullHeight != finalAo.height) { - val requiredWidth = scaledWidth - val requiredHeight = scaledHeight - logD { "Resizing AO pass to $requiredWidth x $requiredHeight, ao output size: $fullWidth x $fullHeight" } - scaledNormals.resize(requiredWidth, requiredHeight) - scaledDists.resize(requiredWidth, requiredHeight) - aoNoisy.resize(requiredWidth, requiredHeight) - filteredAo.resize(requiredWidth, requiredHeight) - finalAo.resize(fullWidth, fullHeight) + } else if (!isConfigured || width != aoMap.width || height != aoMap.height) { + aoMap.resize(width, height) + scaledNormals.resize(halfWidth, halfHeight) + scaledDists.resize(halfWidth, halfHeight) + aoNoisy.resize(halfWidth, halfHeight) + filteredAo.resize(halfWidth, halfHeight) + logD { "Resized AO pass to $halfWidth x $halfHeight, ao output size: $width x $height" } SyncedScope.launch { clearAndReleaseTasks() @@ -211,7 +224,19 @@ class ComputeAoPass( isConfigured = false } - private fun initDownSamplePass(): KslComputeShader { + private fun updateKernels(numKernels: Int, numTemporal: Int) { + AoPipeline.generateAoSampleDirs( + numDirs = numKernels, + numTemporal = numTemporal + ).forEachIndexed { i, kernel -> + kernelBuffer.set(i) { + set(it.kernel, kernel) + } + } + kernelBufferGpu.uploadData(kernelBuffer) + } + + private fun initDownSamplePass(inputNormals: Texture2d, inputDepth: Texture2d): KslComputeShader { val downSampleShader = downSamplingShader() downSampleShader.bindTexture2d("normalInput", inputNormals, SamplerSettings().nearest()) downSampleShader.bindTexture2d("distInput", inputDepth, SamplerSettings().nearest()) @@ -231,6 +256,7 @@ class ComputeAoPass( aoShader.bindTexture2d("distInput", scaledDists, SamplerSettings().nearest().clamped()) aoShader.bindTexture2d("noiseTex", noiseTex, SamplerSettings().nearest()) aoShader.bindStorageTexture2d("aoOutput", aoNoisy) + aoShader.bindStorage("kernelBuffer", kernelBufferGpu) return aoShader } @@ -249,20 +275,20 @@ class ComputeAoPass( upsampleShader.bindTexture2d("distInput", inputDepth, SamplerSettings().nearest()) upsampleShader.bindTexture2d("scaledDistInput", scaledDists, SamplerSettings().nearest()) upsampleShader.bindUniformFloat1("camNear", camera.clipNear) - upsampleShader.bindStorageTexture2d("finalAo", finalAo) + upsampleShader.bindStorageTexture2d("finalAo", aoMap) return upsampleShader } private fun initClearPass(): KslComputeShader { val clearShader = clearShader() - clearShader.bindStorageTexture2d("finalAo", finalAo) + clearShader.bindStorageTexture2d("finalAo", aoMap) return clearShader } private fun makeDownSamplePass() { - val groupsX = (scaledWidth + 7) / 8 - val groupsY = (scaledHeight + 7) / 8 - addTask(downSampleShader, Vec3i(groupsX, groupsY, 1)) + val groupsX = (halfWidth + 7) / 8 + val groupsY = (halfHeight + 7) / 8 + addTask(inputShader, Vec3i(groupsX, groupsY, 1)) } private fun makeDownSampleLowerPasses() { @@ -271,8 +297,8 @@ class ComputeAoPass( val distOutput = downSampleLowerShader.bindStorageTexture2d("distOutput") for (level in 1 until SCALE_LEVELS) { - val groupsX = ((scaledWidth shr level) + 7) / 8 - val groupsY = ((scaledHeight shr level) + 7) / 8 + val groupsX = ((halfWidth shr level) + 7) / 8 + val groupsY = ((halfHeight shr level) + 7) / 8 val task = addTask(downSampleLowerShader, Vec3i(groupsX, groupsY, 1)) val key = "$level" @@ -287,20 +313,20 @@ class ComputeAoPass( } private fun makeAoPass() { - val groupsX = (scaledWidth + 7) / 8 - val groupsY = (scaledHeight + 7) / 8 + val groupsX = (halfWidth + 7) / 8 + val groupsY = (halfHeight + 7) / 8 addTask(aoShader, Vec3i(groupsX, groupsY, 1)) } private fun makeDenoisePass() { - val groupsX = (scaledWidth + 7) / 8 - val groupsY = (scaledHeight + 7) / 8 + val groupsX = (halfWidth + 7) / 8 + val groupsY = (halfHeight + 7) / 8 addTask(denoiseShader, Vec3i(groupsX, groupsY, 1)) } private fun makeUpsamplePass() { - val groupsX = (fullWidth + 7) / 8 - val groupsY = (fullHeight + 7) / 8 + val groupsX = (width + 7) / 8 + val groupsY = (height + 7) / 8 addTask(upsampleShader, Vec3i(groupsX, groupsY, 1)) } @@ -368,12 +394,15 @@ class ComputeAoPass( val distInput = texture2d("distInput") val noiseTex = texture2d("noiseTex") val aoOutput = storageTexture2d("aoOutput", aoNoisy.format) + val kernelStruct = struct(KernelStruct) + val kernelBuffer = storage("kernelBuffer", kernelStruct) val uProj = uniformMat4("uProj") val uInvProj = uniformMat4("uInvProj") val uCamNear = uniformFloat1("uCamNear") - val uKernel = uniformFloat3Array("uKernel", MAX_KERNEL_SIZE) - val uKernelSize = uniformInt1("uKernelRange") + val uKernelSize = uniformInt1("uKernelSize") + val uKernelTemporalSize = uniformInt1("uKernelTemporalSize") + val uFrameI = uniformInt1("uFrameI") val uRadius = uniformFloat1("uRadius") val uStrength = uniformFloat1("uStrength") val uFalloff = uniformFloat1("uFalloff") @@ -408,8 +437,9 @@ class ComputeAoPass( val tbn by mat3Value(tan1Rot, tan2Rot, normal) val occlusion by 0f.const + val kernelOffset by (uFrameI % uKernelTemporalSize) * uKernelSize fori(0.const, uKernelSize) { i -> - val kernel by tbn * uKernel[i] + val kernel by tbn * kernelBuffer[kernelOffset + i][KernelStruct.kernel] if (KoolSystem.requireContext().backend.isInvertedNdcY) { kernel.y *= (-1f).const } @@ -421,7 +451,7 @@ class ComputeAoPass( sampleLod set clamp(sampleLod, 0f.const, (SCALE_LEVELS-1).toFloat().const) val sampleScreenDepth by distInput.sample(sampleUv, sampleLod).x - val occlusionDistance by clamp((sampleDepth - sampleScreenDepth) * 10f.const, 0f.const, 1f.const) + val occlusionDistance by clamp((sampleDepth - sampleScreenDepth - 0.05f.const) * 10f.const, 0f.const, 1f.const) val occlusionFalloff by 1f.const - smoothStep(0f.const, 1f.const, abs(depth - sampleScreenDepth) / (4f.const * sampleR)) occlusion += occlusionDistance * occlusionFalloff } @@ -481,7 +511,7 @@ class ComputeAoPass( val distInput = texture2d("distInput", isUnfilterable = true) val scaledDistInput = texture2d("scaledDistInput") val camNear = uniformFloat1("camNear") - val finalAo = storageTexture2d("finalAo", finalAo.format) + val finalAo = storageTexture2d("finalAo", aoMap.format) computeStage(8, 8) { main { @@ -510,7 +540,7 @@ class ComputeAoPass( } private fun clearShader() = KslComputeShader("ao-clear-shader") { - val finalAo = storageTexture2d("finalAo", finalAo.format) + val finalAo = storageTexture2d("finalAo", aoMap.format) computeStage(8, 8) { main { val baseCoord by inGlobalInvocationId.xy.toInt2() @@ -518,4 +548,8 @@ class ComputeAoPass( } } } +} + +private object KernelStruct : Struct("KernelStruct", MemoryLayout.Std140) { + val kernel = float3("kernelDir") } \ No newline at end of file diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/ao/LegacyAoPipeline.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/ao/LegacyAoPipeline.kt index 28dd04fd6..ca4a61b0d 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/ao/LegacyAoPipeline.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/ao/LegacyAoPipeline.kt @@ -20,11 +20,11 @@ abstract class LegacyAoPipeline : AoPipeline, BaseReleasable() { override val aoMap: Texture2d get() = denoisePass.colorTexture!! - override var radius: Float - get() = aoPass.radius + override var radius: AoRadius + get() = AoRadius(aoPass.radius) set(value) { - aoPass.radius = value - denoisePass.radius = value + aoPass.radius = value.radius + denoisePass.radius = value.radius } override var strength: Float diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/Camera.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/Camera.kt index 44518cf8a..2c4a1131d 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/Camera.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/Camera.kt @@ -58,7 +58,7 @@ abstract class Camera(name: String = "camera") : Node(name) { protected set val proj = MutableMat4f() - private val lazyInvProj = LazyMat4f { proj.invert(it) } + val lazyInvProj = LazyMat4f { proj.invert(it) } val invProj: Mat4f get() = lazyInvProj.get() val dataF = DataF() diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/AoDemo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/AoDemo.kt index 71bddb9be..a2aef6119 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/AoDemo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/AoDemo.kt @@ -13,10 +13,7 @@ import de.fabmax.kool.modules.ksl.lang.* import de.fabmax.kool.modules.ui2.* import de.fabmax.kool.pipeline.ClearColorDontCare import de.fabmax.kool.pipeline.DepthCompareOp -import de.fabmax.kool.pipeline.ao.AoPipeline -import de.fabmax.kool.pipeline.ao.ComputeAoPipeline -import de.fabmax.kool.pipeline.ao.ForwardAoPipeline -import de.fabmax.kool.pipeline.ao.LegacyAoPipeline +import de.fabmax.kool.pipeline.ao.* import de.fabmax.kool.scene.* import de.fabmax.kool.scene.geometry.RectProps import de.fabmax.kool.toString @@ -48,7 +45,7 @@ class AoDemo : DemoScene("Ambient Occlusion") { private val showAoMapValues = listOf("None", "Filtered", "Noisy") private val showAoMapIndex = mutableStateOf(0) - private val aoRadius = mutableStateOf(1f).onChange { _, new -> aoPipeline.radius = new } + private val aoRadius = mutableStateOf(1f).onChange { _, new -> aoPipeline.radius = AoRadius.absoluteRadius(new) } private val aoFalloff = mutableStateOf(1f).onChange { _, new -> aoPipeline.falloff = new } private val aoStrength = mutableStateOf(1f).onChange { _, new -> aoPipeline.strength = new } private val aoSamples = mutableStateOf(16).onChange { _, new -> aoPipeline.kernelSize = new } @@ -79,7 +76,7 @@ class AoDemo : DemoScene("Ambient Occlusion") { aoPipeline = AoPipeline.createForward(mainScene) aoMapSize.set((aoPipeline as? ForwardAoPipeline)?.mapSize ?: 0.5f) - aoRadius.set(aoPipeline.radius) + aoRadius.set(aoPipeline.radius.radius) aoFalloff.set(aoPipeline.falloff) aoStrength.set(aoPipeline.strength) aoSamples.set(aoPipeline.kernelSize) diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/GltfDemo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/GltfDemo.kt index a3bc6561f..4f6d5e802 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/GltfDemo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/GltfDemo.kt @@ -10,6 +10,7 @@ import de.fabmax.kool.modules.gltf.loadGltfModel import de.fabmax.kool.modules.ksl.KslPbrShader import de.fabmax.kool.modules.ui2.* import de.fabmax.kool.pipeline.ao.AoPipeline +import de.fabmax.kool.pipeline.ao.AoRadius import de.fabmax.kool.pipeline.deferred.DeferredOutputShader import de.fabmax.kool.pipeline.deferred.DeferredPipeline import de.fabmax.kool.pipeline.deferred.DeferredPipelineConfig @@ -119,13 +120,13 @@ class GltfDemo : DemoScene("glTF Models") { } deferredPipeline = DeferredPipeline(mainScene, defCfg) deferredPipeline.aoPipeline?.apply { - radius = 0.2f + radius = AoRadius.absoluteRadius(0.2f) } ssrMapSize.set(deferredPipeline.reflectionMapSize) // create forward pipeline aoPipelineForward = AoPipeline.createForward(mainScene).apply { - radius = 0.2f + radius = AoRadius.absoluteRadius(0.2f) } shadowsForward += listOf( SimpleShadowMap(mainScene, mainScene.lighting.lights[0], contentGroupForward, mapSize = 2048), diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt index 12427037e..ed22ae9fb 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt @@ -21,9 +21,9 @@ import de.fabmax.kool.util.* class Deferred2Demo : DemoScene("Deferred2 Demo") { + val ibl by hdriImage("hdri/newport_loft.rgbe.png") private val albedoMap by texture2d("materials/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_COL_2K_METALNESS.jpg") private val normalMap by texture2d("materials/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_NRM_2K_METALNESS.jpg") - //private val uvChecker by texture2d("materials/uv_checker_map.jpg") private lateinit var pipeline: Deferred2Pipeline private val filterWeight = mutableStateOf(16) @@ -34,10 +34,11 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { val lighting = Lighting().apply { singlePointLight { setup(Vec3f(1.2f, 3.2f, 2f)) - setColor(Color.WHITE, intensity = 50f) + setColor(Color.WHITE, intensity = 5f) } } - pipeline = Deferred2Pipeline(content, lighting, this) + + pipeline = Deferred2Pipeline(content, scene = this, ibl, lighting) pipeline.renderScale = 0.5f filterWeight.value = pipeline.filterPass.filterWeight.toInt() filterWeight.onChange { _, value -> pipeline.filterPass.filterWeight = value.toFloat() } @@ -61,7 +62,7 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { generate { generateFullscreenQuad() } - shader = deferredOutputShader(pipeline, bloomPass) + shader = deferredOutputShader(this@Deferred2Demo, pipeline, bloomPass) } } @@ -82,6 +83,7 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { .onItemSelected { tsaaIndex = it pipeline.tsaa = TsaaItem.items[it].tsaa + pipeline.aoPass.temporalKernels = TsaaItem.items[it].numSamples } } } @@ -102,47 +104,6 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { } } - private fun deferredOutputShader( - deferred2Pipeline: Deferred2Pipeline, - bloomPass: BloomPass - ): KslShader { - val outputShader = KslShader("deferred2-output") { - val uv = interStageFloat2() - fullscreenQuadVertexStage(uv) - fragmentStage { - main { - val output = de.fabmax.kool.modules.ksl.lang.texture2d("deferredOutput") - val bloom = de.fabmax.kool.modules.ksl.lang.texture2d("bloomOutput") - val uvi = (uv.output * output.size().toFloat2() + 0.5f.const2).toInt2() - val color by output.load(uvi).rgb + bloom.sample(uv.output).rgb - - val ditherTex = de.fabmax.kool.modules.ksl.lang.texture2d("ditherPattern") - val ditherC by uvi % ditherTex.size() - val ditherNoise by ditherTex.load(ditherC).r - val srgb by convertColorSpace(color, ColorSpaceConversion.LinearToSrgbHdr()) + (ditherNoise - 0.5f.const) / 255f.const - colorOutput(srgb) -// colorOutput(color) - } - } - } - - val ditherTex = makeDitherPattern() - ditherTex.releaseWith(mainScene) - - outputShader.bindTexture2d("ditherPattern", ditherTex) - var bloomTex by outputShader.bindTexture2d("bloomOutput", bloomPass.bloomMap) - var inputTex by outputShader.bindTexture2d("deferredOutput") - val noBloom = SingleColorTexture(Color.BLACK) - deferred2Pipeline.onSwap { - val filterOutput = deferred2Pipeline.filterPass.filterOutput.newVal - outputShader.swapPipelineDataCapturing(filterOutput) { - inputTex = filterOutput - bloomTex = if (bloom.value) bloomPass.bloomMap else noBloom - } - } - return outputShader - } - private fun deferredContent() = Node("deferred content").apply { addGroup { onUpdate { @@ -219,14 +180,56 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { } } -private data class TsaaItem(val label: String, val tsaa: List) { +private fun deferredOutputShader( + demo: Deferred2Demo, + deferred2Pipeline: Deferred2Pipeline, + bloomPass: BloomPass +): KslShader { + val outputShader = KslShader("deferred2-output") { + val uv = interStageFloat2() + fullscreenQuadVertexStage(uv) + fragmentStage { + main { + val output = texture2d("deferredOutput") + val bloom = texture2d("bloomOutput") + val uvi = (uv.output * output.size().toFloat2()).toInt2() + val color by output.load(uvi).rgb + bloom.sample(uv.output).rgb + + val ditherTex = texture2d("ditherPattern") + val ditherC by uvi % ditherTex.size() + val ditherNoise by ditherTex.load(ditherC).r + val srgb by convertColorSpace(color, ColorSpaceConversion.LinearToSrgbHdr()) + (ditherNoise - 0.5f.const) / 255f.const + colorOutput(srgb) +// colorOutput(color) + } + } + } + + val ditherTex = makeDitherPattern() + ditherTex.releaseWith(demo.mainScene) + + outputShader.bindTexture2d("ditherPattern", ditherTex) + var bloomTex by outputShader.bindTexture2d("bloomOutput", bloomPass.bloomMap) + var inputTex by outputShader.bindTexture2d("deferredOutput") + val noBloom = SingleColorTexture(Color.BLACK) + deferred2Pipeline.onSwap { + val filterOutput = deferred2Pipeline.filterPass.filterOutput.newVal + outputShader.swapPipelineDataCapturing(filterOutput) { + inputTex = filterOutput + bloomTex = if (bloomPass.isEnabled) bloomPass.bloomMap else noBloom + } + } + return outputShader +} + +private data class TsaaItem(val label: String, val tsaa: List, val numSamples: Int) { companion object { val items = listOf( - TsaaItem("None", Deferred2Pipeline.TSAA_NONE), - TsaaItem("2x", Deferred2Pipeline.TSAA_2), - TsaaItem("4x", Deferred2Pipeline.TSAA_4), - TsaaItem("8x", Deferred2Pipeline.TSAA_8), - TsaaItem("16x", Deferred2Pipeline.TSAA_16) + TsaaItem("None", Deferred2Pipeline.TSAA_NONE, 1), + TsaaItem("2x", Deferred2Pipeline.TSAA_2, 2), + TsaaItem("4x", Deferred2Pipeline.TSAA_4, 4), + TsaaItem("8x", Deferred2Pipeline.TSAA_8, 8), + TsaaItem("16x", Deferred2Pipeline.TSAA_16, 16) ) } } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt index 412e2b660..3240877db 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt @@ -6,6 +6,10 @@ import de.fabmax.kool.math.Vec2i import de.fabmax.kool.pipeline.BufferedImageData2d import de.fabmax.kool.pipeline.TexFormat import de.fabmax.kool.pipeline.Texture2d +import de.fabmax.kool.pipeline.ao.AoRadius +import de.fabmax.kool.pipeline.ao.ComputeAoPass +import de.fabmax.kool.pipeline.ibl.EnvironmentMap +import de.fabmax.kool.pipeline.swapPipelineDataCapturing import de.fabmax.kool.scene.Lighting import de.fabmax.kool.scene.Node import de.fabmax.kool.scene.PerspectiveCamera @@ -17,60 +21,108 @@ import kotlinx.coroutines.yield class Deferred2Pipeline( val content: Node, + private val scene: Scene, + val ibl: EnvironmentMap, val lighting: Lighting = Lighting(), - scene: Scene, var renderScale: Float = 1f, var tsaa: List = TSAA_4, ) { val camera = PerspectiveCamera() + val size: Vec2i get() = Vec2i( + (scene.mainRenderPass.viewport.width * renderScale).toInt().coerceAtLeast(16), + (scene.mainRenderPass.viewport.height * renderScale).toInt().coerceAtLeast(16) + ) val gbuffers = AlternatingPair { val suff = if (it) "A" else "B" - GbufferPass(content, camera, scene.renderSize, "deferred2-gbuffer-pass-$suff", this) + GbufferPass(content, camera, size, "deferred2-gbuffer-pass-$suff", this) } + val aoPass: ComputeAoPass = ComputeAoPass( + camera = camera, + inputDepth = gbuffers.a.depth, + inputNormals = gbuffers.a.normals, + initialSize = size + ) + private val swapListeners = BufferedList<() -> Unit>() + private val resizeListeners = BufferedList<(Vec2i) -> Unit>() val lightingPass = LightingPass( gbuffers = gbuffers, camera = camera, lighting = lighting, - size = scene.renderSize, + size = size, + ssaoMap = aoPass.aoMap, + ibl = ibl, ) val filterPass = TemporalFilterPass( lightingOutput = lightingPass.lightingOutput, gbuffers = gbuffers, camera = camera, - size = scene.renderSize, + size = size, + pipeline = this, ) internal val prevViewProjMats = List(1024) { MutableMat4f() } - + internal val oldViewProj = MutableMat4f() + private val prevViewProj = MutableMat4f() init { + aoPass.kernelSize = 16 + aoPass.radius = AoRadius.relativeRadius(0.05f) + aoPass.temporalKernels = tsaa.size + scene.addOffscreenPass(gbuffers.a) scene.addOffscreenPass(gbuffers.b) + scene.addComputePass(aoPass) scene.addOffscreenPass(lightingPass) scene.addComputePass(filterPass) - var size = scene.renderSize + val offsetMat = MutableMat4f() + camera.onCameraUpdated += { + val tsaa = tsaa + if (tsaa.isNotEmpty()) { + val offset = tsaa[Time.frameCount % tsaa.size] + val width = it.viewport.width + val height = it.viewport.height + offsetMat.setIdentity().translate(offset.x / width, offset.y / height, 0f).mul(camera.proj) + camera.proj.set(offsetMat) + camera.lazyInvProj.isDirty = true + } + } + + var oldSize = size scene.onRenderScene += { - val newSize = scene.renderSize - if (size != newSize) { + val newSize = size + if (oldSize != newSize) { logD { "Resizing to ${newSize.x}x${newSize.y}" } - size = newSize + oldSize = newSize gbuffers.a.setSize(size.x, size.y) gbuffers.b.setSize(size.x, size.y) + aoPass.resize(size.x, size.y) lightingPass.setSize(size.x, size.y) filterPass.resize(size) + resizeListeners.forEachUpdated { it(size) } } } scene.coroutineScope.launch { withContext(KoolDispatchers.Synced) { while (true) { + oldViewProj.set(prevViewProj) + prevViewProj.set(camera.viewProj) + lightingPass.swapBuffers() filterPass.swapBuffers() + + val currentGbuffer = gbuffers.newVal + aoPass.inputShader.swapPipelineDataCapturing(currentGbuffer) { + aoPass.inputDepth = currentGbuffer.depth + aoPass.inputNormals = currentGbuffer.normals + //aoPass.captureCamera() + } + swapListeners.forEachUpdated { it() } gbuffers.newVal.objModelMatsGpu.uploadData(gbuffers.newVal.objModelMats) @@ -84,17 +136,16 @@ class Deferred2Pipeline( } } + fun onResize(block: (Vec2i) -> Unit) { + resizeListeners += block + } + fun onSwap(block: () -> Unit) { swapListeners += block } - private val Scene.renderSize: Vec2i get() = Vec2i( - (mainRenderPass.viewport.width * renderScale).toInt().coerceAtLeast(1), - (mainRenderPass.viewport.height * renderScale).toInt().coerceAtLeast(1) - ) - companion object { - private val s = 1f/16f + private val s = 1f/8f val TSAA_NONE = listOf(Vec2f.ZERO) val TSAA_2 = listOf( Vec2f(4 * s, 4 * s), @@ -109,10 +160,10 @@ class Deferred2Pipeline( val TSAA_8 = listOf( Vec2f(1 * s, -3 * s), Vec2f(7 * s, -7 * s), - Vec2f(-1 * s, 3 * s), - Vec2f(5 * s, 1 * s), Vec2f(3 * s, 7 * s), Vec2f(-3 * s, -5 * s), + Vec2f(-1 * s, 3 * s), + Vec2f(5 * s, 1 * s), Vec2f(-7 * s, -1 * s), Vec2f(-5 * s, -5 * s), ) diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt index 1b4b3b000..214499a84 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt @@ -15,7 +15,6 @@ import de.fabmax.kool.scene.VertexLayouts import de.fabmax.kool.scene.vertexAttrib import de.fabmax.kool.util.Color import de.fabmax.kool.util.StructBuffer -import de.fabmax.kool.util.Time import de.fabmax.kool.util.asStorageBuffer class GbufferPass( @@ -52,17 +51,6 @@ class GbufferPass( init { this.camera = camera - - val offsetMat = MutableMat4f() - camera.onCameraUpdated += { - val tsaa = pipeline.tsaa - if (tsaa.isNotEmpty()) { - val offset = tsaa[Time.frameCount % tsaa.size] - offsetMat.setIdentity().translate(offset.x / width, offset.y / height, 0f).mul(camera.proj) - camera.proj.set(offsetMat) - } - } - val inverseBuf = MutableMat4f() val reprojectBuf = MutableMat4f() onAfterCollectDrawCommands += { viewData -> diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt index f58036143..21a895334 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt @@ -3,14 +3,12 @@ package de.fabmax.kool.demo.deferred2 import de.fabmax.kool.math.Vec2i import de.fabmax.kool.math.Vec4f import de.fabmax.kool.modules.ksl.KslShader -import de.fabmax.kool.modules.ksl.blocks.LightDataStruct -import de.fabmax.kool.modules.ksl.blocks.getLightDirectionFromFragPos -import de.fabmax.kool.modules.ksl.blocks.getLightRadiance -import de.fabmax.kool.modules.ksl.blocks.setLightData +import de.fabmax.kool.modules.ksl.blocks.* import de.fabmax.kool.modules.ksl.lang.* import de.fabmax.kool.pipeline.* import de.fabmax.kool.pipeline.FullscreenShaderUtil.fullscreenQuadVertexStage import de.fabmax.kool.pipeline.FullscreenShaderUtil.generateFullscreenQuad +import de.fabmax.kool.pipeline.ibl.EnvironmentMap import de.fabmax.kool.scene.* import de.fabmax.kool.util.MemoryLayout import de.fabmax.kool.util.Struct @@ -21,6 +19,8 @@ class LightingPass( camera: Camera, lighting: Lighting?, size: Vec2i, + var ssaoMap: Texture2d, + private val ibl: EnvironmentMap, ) : OffscreenPass2d( drawNode = Node(), attachmentConfig = AttachmentConfig { @@ -43,6 +43,8 @@ class LightingPass( shader = lightingShader } drawNode.addNode(outputMesh) + + drawNode.addNode(Skybox.cube(ibl.reflectionMap, 2f, colorSpaceConversion = ColorSpaceConversion.AsIs)) } fun swapBuffers() { @@ -52,6 +54,8 @@ class LightingPass( encodedNormals = newGbuffer.normals albedoEmissionTex = newGbuffer.albedoEmission metalRoughnessAoTex = newGbuffer.metalRoughnessAo + irradianceMap = ibl.irradianceMap + aoMap = ssaoMap frameI = Time.frameCount camData.set { @@ -74,6 +78,8 @@ class DeferredLightingShader : KslShader("deferred2-lighting") { var encodedNormals by bindTexture2d("encodedNormals") var albedoEmissionTex by bindTexture2d("albedoEmission") var metalRoughnessAoTex by bindTexture2d("metalRoughnessAo") + var irradianceMap by bindTextureCube("irradiance") + var aoMap by bindTexture2d("aoMap") val camData = bindUniformStruct("deferredCamData", DeferredCamDataStruct) private val lightDataStruct = LightDataStruct(4) @@ -98,6 +104,8 @@ class DeferredLightingShader : KslShader("deferred2-lighting") { val encodedNormals = texture2d("encodedNormals") val albedoEmission = texture2d("albedoEmission") val metalRoughnessAo = texture2d("metalRoughnessAo") + val irradiance = textureCube("irradiance") + val aoMap = texture2d("aoMap") val camData = uniformStruct("deferredCamData", DeferredCamDataStruct) val lightData = uniformStruct("lightData", lightDataStruct) @@ -109,7 +117,10 @@ class DeferredLightingShader : KslShader("deferred2-lighting") { val camNear = camData[DeferredCamDataStruct.camNear] val invView = camData[DeferredCamDataStruct.invView] val invViewProj = camData[DeferredCamDataStruct.invViewProj] - val worldPos by unprojectBaseCoord(depth, baseCoord, camNear, invViewProj).xyz + val depthSample by depth.load(baseCoord, lod = 0.const).x + val size by depth.size() + val worldPos by unprojectBaseCoord(depthSample, baseCoord, size, camNear, invViewProj).xyz + val ssao by aoMap.load(baseCoord, lod = 0.const).x val viewNormal by decodeNormalRgb(encodedNormals.load(baseCoord, lod = 0.const).xyz) val worldNormal by (invView * float4Value(viewNormal, 0f.const)).xyz @@ -129,9 +140,12 @@ class DeferredLightingShader : KslShader("deferred2-lighting") { val dirToLight by normalize(getLightDirectionFromFragPos(worldPos, lightPos)) val radiance by getLightRadiance(worldPos, lightPos, lightDir, lightColor) - val ambient by 0.04f.const + //val ambient by 0.04f.const + val ambient by irradiance.sample(worldNormal).rgb * ssao val diffuse by albedo * ambient + albedo * radiance * saturate(dot(dirToLight, worldNormal)) colorOutput(diffuse) +// colorOutput(float3Value(ssao, ssao, ssao)) + outDepth set depthSample // val filterNoise by noise31(float3Value(uv.output, frameI.toFloat1())) * 0.7f.const + 0.3f.const // colorOutput(diffuse * filterNoise) diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ProjectionFunctions.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ProjectionFunctions.kt index 3e6d1bb13..53fe0a472 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ProjectionFunctions.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ProjectionFunctions.kt @@ -11,17 +11,18 @@ fun baseCoordToUv(baseCoord: KslExprInt2, viewSize: KslExprInt2): KslExprFloat2 } class UnprojectBaseCoord( - depth: KslUniform, parentScope: KslScopeBuilder ) : KslFunction("fnUnprojectBaseCoord", KslFloat4, parentScope.parentStage) { init { + val depth = paramFloat1("depth") val baseCoord = paramInt2("baseCoord") + val size = paramInt2("size") val camNear = paramFloat1("camNear") val invViewProj = paramMat4("invProj") body { - val uv by baseCoordToUv(baseCoord, depth.size()) - val viewDepth by getLinearDepthReversed(depth.load(baseCoord, lod = 0.const).x, camNear) + val uv by baseCoordToUv(baseCoord, size) + val viewDepth by getLinearDepthReversed(depth, camNear) val viewProjXy by (uv * 2f.const - 1f.const) * viewDepth val viewProjPos by float4Value(viewProjXy, camNear, viewDepth) val worldPos by invViewProj * viewProjPos @@ -31,13 +32,14 @@ class UnprojectBaseCoord( } fun KslScopeBuilder.unprojectBaseCoord( - depth: KslUniform, + depth: KslExprFloat1, baseCoord: KslExprInt2, + size: KslExprInt2, camNear: KslExprFloat1, invViewProj: KslExprMat4, ): KslExprFloat4 { - val func = parentStage.getOrCreateFunction("fnUnprojectBaseCoord") { UnprojectBaseCoord(depth, this) } - return func(baseCoord, camNear, invViewProj) + val func = parentStage.getOrCreateFunction("fnUnprojectBaseCoord") { UnprojectBaseCoord(this) } + return func(depth, baseCoord, size, camNear, invViewProj) } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt index cf5ee6fca..43d44173e 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt @@ -17,6 +17,7 @@ class TemporalFilterPass( val lightingOutput: Texture2d, val gbuffers: AlternatingPair, val camera: Camera, + val pipeline: Deferred2Pipeline, private var size: Vec2i, filterStorageFmt: TexFormat = TexFormat.RGBA_F16, ) : ComputePass("deferred2-lighting-pass") { @@ -27,7 +28,7 @@ class TemporalFilterPass( private val temporalShader = TemporalFilterShader(filterStorageFmt, lightingOutput, filterState) - var filterWeight by temporalShader.bindUniformFloat1("uFilterWeight", 8f) + var filterWeight = 8f init { setupPasses() @@ -65,11 +66,13 @@ class TemporalFilterPass( oldFilter = filterOutput.oldVal newFilter = filterOutput.newVal frameI = Time.frameCount + filterW = filterWeight objModelMats = newGbuffer.objModelMatsGpu camData.set { set(it.invViewProj, camera.invViewProj) set(it.camNear, camera.clipNear) + set(it.oldViewProj, pipeline.oldViewProj) } } } @@ -94,6 +97,7 @@ class TemporalFilterShader( var filterState by bindStorageTexture2d("filterState", filterState) var frameI by bindUniformInt1("frameI") + var filterW by bindUniformFloat1("uFilterWeight") init { program.program() @@ -127,22 +131,28 @@ class TemporalFilterShader( val baseCoord by inGlobalInvocationId.xy.toInt2() val curMeta by newMeta.load(baseCoord).r val id by curMeta and 0xffffff.const - val oldSize by oldFilter.size().toFloat2() + val size by newDepth.size() + val sizeF by size.toFloat2() val camNear = camData[FilterCamDataStruct.camNear] val invViewProj = camData[FilterCamDataStruct.invViewProj] - val worldPos by unprojectBaseCoord(newDepth, baseCoord, camNear, invViewProj) + val depth by newDepth.load(baseCoord).x + val worldPos by unprojectBaseCoord(depth, baseCoord, size, camNear, invViewProj) - val oldUv by (baseCoord.toFloat2() + 0.5f.const2) / oldSize + val oldUv by 0f.const2 val oldBaseCoord by baseCoord `if`(id ne 0.const) { val objModelMat = structVar(objModelMats[id]) val oldProj by objModelMat[ObjModelMatLayout.reprojectMat] * worldPos oldUv set oldProj.xy / oldProj.w * float2Value(0.5f, -0.5f) + 0.5f.const - oldBaseCoord set (oldUv * oldSize).toInt2() + oldBaseCoord set (oldUv * sizeF).toInt2() + }.`else` { + val oldProj by camData[FilterCamDataStruct.oldViewProj] * worldPos + oldUv set oldProj.xy / oldProj.w * float2Value(0.5f, -0.5f) + 0.5f.const + oldBaseCoord set (oldUv * sizeF).toInt2() } - val oldStateBaseUv by oldUv * oldSize + val oldStateBaseUv by oldUv * sizeF val oldState by (filterState.load((oldStateBaseUv + float2Value(0.5f, 0.5f)).toInt2()).r * 255f.const).toInt1() or (filterState.load((oldStateBaseUv + float2Value(0.5f, -0.5f)).toInt2()).r * 255f.const).toInt1() or @@ -151,7 +161,7 @@ class TemporalFilterShader( val wasEdge by oldState and 1.const gt 0.const val near by camData[FilterCamDataStruct.camNear] - val refDepth by getLinearDepthReversed(newDepth.load(baseCoord).x, near) + val refDepth by getLinearDepthReversed(depth, near) val depthA by getLinearDepthReversed(newDepth.load(baseCoord + int2Value(1, 1)).x, near) val depthB by getLinearDepthReversed(newDepth.load(baseCoord + int2Value(-1, -1)).x, near) val depthC by getLinearDepthReversed(newDepth.load(baseCoord + int2Value(-1, 0)).x, near) @@ -231,5 +241,6 @@ class TemporalFilterShader( object FilterCamDataStruct : Struct("FilterCamData", MemoryLayout.Std140) { val invViewProj = mat4("invViewProj") + val oldViewProj = mat4("oldViewProj") val camNear = float1("camClipNear") } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/ragdoll/RagdollDemo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/ragdoll/RagdollDemo.kt index dca0d4e4a..5966d6ce9 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/ragdoll/RagdollDemo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/ragdoll/RagdollDemo.kt @@ -21,6 +21,7 @@ import de.fabmax.kool.physics.geometry.BoxGeometry import de.fabmax.kool.physics.geometry.CylinderGeometry import de.fabmax.kool.physics.geometry.PlaneGeometry import de.fabmax.kool.pipeline.ao.AoPipeline +import de.fabmax.kool.pipeline.ao.AoRadius import de.fabmax.kool.scene.* import de.fabmax.kool.toString import de.fabmax.kool.util.* @@ -57,7 +58,7 @@ class RagdollDemo : DemoScene("Ragdoll Demo") { override suspend fun loadResources(ctx: KoolContext) { ao = AoPipeline.createForward(mainScene).apply { - radius = 0.5f + radius = AoRadius.absoluteRadius(0.5f) } shadows += SimpleShadowMap(mainScene, mainScene.lighting.lights[0], mapSize = 4096).apply { setDefaultDepthOffset(true) diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/terrain/TerrainDemo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/terrain/TerrainDemo.kt index 37cf08d51..b15338a86 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/terrain/TerrainDemo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/terrain/TerrainDemo.kt @@ -20,6 +20,7 @@ import de.fabmax.kool.physics.util.CharacterTrackingCamRig import de.fabmax.kool.pipeline.GradientTexture import de.fabmax.kool.pipeline.SamplerSettings import de.fabmax.kool.pipeline.ao.AoPipeline +import de.fabmax.kool.pipeline.ao.AoRadius import de.fabmax.kool.pipeline.ao.ForwardAoPipeline import de.fabmax.kool.pipeline.shading.DepthShader import de.fabmax.kool.scene.* @@ -229,7 +230,7 @@ class TerrainDemo : DemoScene("Terrain Demo") { ssao = AoPipeline.createForwardLegacy(this).apply { // negative radius is used to set radius relative to camera distance - radius = -0.05f + radius = AoRadius.relativeRadius(0.05f) isEnabled = isSsao.value kernelSize = 8 } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/procedural/ProceduralDemo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/procedural/ProceduralDemo.kt index 6d07587b6..3a8e0a4eb 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/procedural/ProceduralDemo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/procedural/ProceduralDemo.kt @@ -9,6 +9,7 @@ import de.fabmax.kool.math.spatial.BoundingBoxF import de.fabmax.kool.modules.ksl.blocks.ColorSpaceConversion import de.fabmax.kool.modules.ui2.* import de.fabmax.kool.pipeline.DepthCompareOp +import de.fabmax.kool.pipeline.ao.AoRadius import de.fabmax.kool.pipeline.deferred.DeferredPipeline import de.fabmax.kool.pipeline.deferred.DeferredPipelineConfig import de.fabmax.kool.scene.Scene @@ -62,7 +63,7 @@ class ProceduralDemo : DemoScene("Procedural Geometry") { outputDepthTest = DepthCompareOp.ALWAYS } val deferredPipeline = DeferredPipeline(this@setupMainScene, deferredCfg).apply { - aoPipeline?.radius = 0.6f + aoPipeline?.radius = AoRadius.absoluteRadius(0.6f) sceneContent.apply { addNode(Glass(ibl, shadowMap).also { onSwap += it }) From 376dcfa294e40999b2039de9ca7c6a95141ff85b Mon Sep 17 00:00:00 2001 From: Max Thiele Date: Sun, 17 May 2026 22:33:24 +0200 Subject: [PATCH 09/27] Back to int encoded normals for higher precision --- .../kool/modules/ksl/blocks/PbrFunctions.kt | 2 +- .../kool/pipeline/NormalDepthMapPass.kt | 22 ++++- .../kool/pipeline/ao/ComputeAoPipeline.kt | 20 ++-- .../kool/demo/deferred2/Deferred2Demo.kt | 36 ++++--- .../kool/demo/deferred2/Deferred2Pipeline.kt | 26 ++--- .../fabmax/kool/demo/deferred2/GbufferPass.kt | 12 +-- .../kool/demo/deferred2/LightingPass.kt | 99 +++++++++++-------- .../kool/demo/deferred2/TemporalFilterPass.kt | 3 +- 8 files changed, 129 insertions(+), 91 deletions(-) diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/PbrFunctions.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/PbrFunctions.kt index 16d7d5476..594a108b8 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/PbrFunctions.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/PbrFunctions.kt @@ -18,7 +18,7 @@ class DistributionGgx(parentScope: KslScopeBuilder) : val nDotH = float1Var(max(dot(n, h), 0f.const)) val nDotH2 = float1Var(nDotH * nDotH) - val denom = float1Var(nDotH2 * (a2 - 1f.const) + 1f.const) + val denom = float1Var(max(nDotH2 * (a2 - 1f.const) + 1f.const, 0.001f.const)) denom set PI.const * denom * denom return@body a2 / denom diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/NormalDepthMapPass.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/NormalDepthMapPass.kt index ffd6d85f6..e0562b952 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/NormalDepthMapPass.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/NormalDepthMapPass.kt @@ -20,7 +20,7 @@ import de.fabmax.kool.util.releaseDelayed class NormalDepthMapPass( drawNode: Node, attachmentConfig: AttachmentConfig = AttachmentConfig.singleColorDefaultDepth( - texFormat = TexFormat.RGBA, + texFormat = TexFormat.R_I32, clearColor = ClearColorFill(Color.ZERO) ), initialSize: Vec2i = Vec2i(128, 128), @@ -134,8 +134,9 @@ class NormalDepthShader( discard() } } - val encoded by encodeNormalRgb(normalize(viewNormal.output)) - colorOutput(encoded, 1f.const) + val viewNormal by normalize(viewNormal.output) + val encoded by encodeNormalInt(viewNormal) + intOutput(int4Value(encoded, 0.const, 0.const, 0.const)) } } } @@ -161,5 +162,16 @@ fun encodeNormalRgb(normal: KslExprFloat3): KslExprFloat3 = float3Var(normal * 0 context(scope: KslScopeBuilder) fun decodeNormalRgb(encoded: KslExprFloat3): KslExprFloat3 = float3Var(encoded * 2f.const - 1f.const) -fun KslScopeBuilder.isValidEncodedNormal(encoded: KslExprInt1): KslExprBool1 = - encoded and 0x80000000.toInt().const ne 0.const \ No newline at end of file +context(scope: KslScopeBuilder) +fun encodeNormalInt(normal: KslExprFloat3): KslExprInt1 = int1Var( + (clamp(((normal.z + 1f.const) * 512f.const).toInt1(), 0.const, 1023.const) shl 22.const) or + (clamp(((normal.y + 1f.const) * 1024f.const).toInt1(), 0.const, 2047.const) shl 11.const) or + clamp(((normal.x + 1f.const) * 1024f.const).toInt1(), 0.const, 2047.const) +) + +context(scope: KslScopeBuilder) +fun decodeNormalInt(encoded: KslExprInt1): KslExprFloat3 = float3Var( + (encoded and 0x7ff.const).toFloat1() / 1024f.const - 1f.const, + ((encoded shr 11.const) and 0x7ff.const).toFloat1() / 1024f.const - 1f.const, + ((encoded shr 22.const) and 0x3ff.const).toFloat1() / 512f.const - 1f.const, +) diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/ao/ComputeAoPipeline.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/ao/ComputeAoPipeline.kt index f4e7322e3..df9d7cac8 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/ao/ComputeAoPipeline.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/ao/ComputeAoPipeline.kt @@ -95,7 +95,7 @@ class ComputeAoPass( private val halfWidth: Int get() = (width / 2).coerceAtLeast(1) private val halfHeight: Int get() = (height / 2).coerceAtLeast(1) - val scaledNormals = StorageTexture2d(halfWidth, halfHeight, TexFormat.RGBA, mipMapping = MipMapping.Limited(SCALE_LEVELS), name = "normalOutput") + val scaledNormals = StorageTexture2d(halfWidth, halfHeight, TexFormat.R_I32, mipMapping = MipMapping.Limited(SCALE_LEVELS), name = "normalOutput") val scaledDists = StorageTexture2d(halfWidth, halfHeight, distFormat, mipMapping = MipMapping.Limited(SCALE_LEVELS), name = "distOutput") val aoNoisy = StorageTexture2d(halfWidth, halfHeight, TexFormat.R, name = "aoOutputNoisy") val filteredAo = StorageTexture2d(halfWidth, halfHeight, TexFormat.R, name = "filteredAo") @@ -340,27 +340,27 @@ class ComputeAoPass( private fun downSamplingShader() = KslComputeShader("down-sample-shader") { computeStage(8, 8) { - val normalInput = texture2d("normalInput") + val normalInput = texture2dInt("normalInput") val distInput = texture2d("distInput", isUnfilterable = true) - val normalOutput = storageTexture2d("normalOutput", TexFormat.RGBA) + val normalOutput = storageTexture2d("normalOutput", TexFormat.R_I32) val distOutput = storageTexture2d("distOutput", distFormat) val camNear = uniformFloat1("camNear") main { val baseCoord by inGlobalInvocationId.xy.toInt2() val loadCoord by baseCoord * 2.const - val normal by 0f.const4 + val normal by 0.const val maxDepth by 1f.const val samplePos = listOf(Vec2i(0, 0), Vec2i(1, 0), Vec2i(0, 1), Vec2i(1, 1)) samplePos.forEach { sample -> val sampleDepth = float1Var(distInput.load(loadCoord + sample.const).x) `if`(sampleDepth lt maxDepth) { maxDepth set sampleDepth - normal set normalInput.load(loadCoord + sample.const) + normal set normalInput.load(loadCoord + sample.const).x } } val linearDepth by getLinearDepthReversed(maxDepth, camNear) - normalOutput.store(baseCoord, normal) + normalOutput.store(baseCoord, int4Value(normal, 0.const, 0.const, 0.const)) distOutput.store(baseCoord, float4Value(linearDepth, 0f.const, 0f.const, 0f.const)) } } @@ -390,7 +390,7 @@ class ComputeAoPass( private fun aoShader() = KslComputeShader("ao-shader") { computeStage(8, 8) { - val normalInput = texture2d("normalInput") + val normalInput = texture2dInt("normalInput") val distInput = texture2d("distInput") val noiseTex = texture2d("noiseTex") val aoOutput = storageTexture2d("aoOutput", aoNoisy.format) @@ -410,13 +410,13 @@ class ComputeAoPass( main { val baseCoord by inGlobalInvocationId.xy.toInt2() val uv by (baseCoord.toFloat2() + 0.5f.const) / normalInput.size().toFloat2() - val encodedNormal = float4Var(normalInput.load(baseCoord)) + val encodedNormal by normalInput.load(baseCoord).x + val normal by decodeNormalInt(encodedNormal) val occlFac by 1f.const - val isValid = encodedNormal.a gt 0f.const + val isValid by dot(normal, normal) gt 0.5f.const `if`(isValid) { val depth by distInput.load(baseCoord).x - val normal by decodeNormalRgb(encodedNormal.rgb) val sampleR by uRadius `if`(sampleR lt 0f.const) { diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt index ed22ae9fb..d941f5182 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt @@ -15,15 +15,22 @@ import de.fabmax.kool.pipeline.BloomPass import de.fabmax.kool.pipeline.FullscreenShaderUtil.fullscreenQuadVertexStage import de.fabmax.kool.pipeline.FullscreenShaderUtil.generateFullscreenQuad import de.fabmax.kool.pipeline.SingleColorTexture +import de.fabmax.kool.pipeline.ao.AoRadius import de.fabmax.kool.pipeline.swapPipelineDataCapturing import de.fabmax.kool.scene.* import de.fabmax.kool.util.* class Deferred2Demo : DemoScene("Deferred2 Demo") { - val ibl by hdriImage("hdri/newport_loft.rgbe.png") + //val ibl by hdriImage("hdri/newport_loft.rgbe.png") + //val ibl by hdriImage("hdri/shanghai_bund_1k.rgbe.png") + val ibl by hdriImage("hdri/circus_arena_1k.rgbe.png") + private val albedoMap by texture2d("materials/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_COL_2K_METALNESS.jpg") private val normalMap by texture2d("materials/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_NRM_2K_METALNESS.jpg") + private val metallicMap by texture2d("materials/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_METALNESS_2K_METALNESS.jpg") + private val roughnessMap by texture2d("materials/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_ROUGHNESS_2K_METALNESS.jpg") + private val aoMap by texture2d("materials/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_AO_2K_METALNESS.jpg") private lateinit var pipeline: Deferred2Pipeline private val filterWeight = mutableStateOf(16) @@ -40,6 +47,7 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { pipeline = Deferred2Pipeline(content, scene = this, ibl, lighting) pipeline.renderScale = 0.5f + pipeline.aoPass.radius = AoRadius.relativeRadius(1/20f) filterWeight.value = pipeline.filterPass.filterWeight.toInt() filterWeight.onChange { _, value -> pipeline.filterPass.filterWeight = value.toFloat() } @@ -126,6 +134,7 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { } shader = gbufferShader(objectId = 1) { color { vertexColor() } + roughness { constProperty(0.15f) } } } addTextureMesh(isNormalMapped = true) { @@ -140,16 +149,17 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { .translate(2.5f, 0f, 0f) } shader = gbufferShader(objectId = 2) { - color { - textureColor(albedoMap) - } - normalMapping { - useNormalMap(normalMap) - } + color { textureColor(albedoMap) } + normalMapping { useNormalMap(normalMap) } + metallic { textureProperty(metallicMap) } + roughness { textureProperty(roughnessMap) } + ao { textureProperty(aoMap) } }.apply { -// bindTexture2d("tbaseColor", uvChecker) bindTexture2d("tbaseColor", albedoMap) bindTexture2d("tNormalMap", normalMap) + bindTexture2d("tmetallic", metallicMap) + bindTexture2d("troughness", roughnessMap) + bindTexture2d("tao", aoMap) } } addColorMesh { @@ -237,11 +247,11 @@ private data class TsaaItem(val label: String, val tsaa: List, val numSam private data class ScaleItem(val label: String, val scale: Float) { companion object { val items = listOf( - ScaleItem("0.1x", 0.1f), - ScaleItem("0.25x", 0.25f), - ScaleItem("0.5x", 0.5f), - ScaleItem("0.75x", 0.75f), - ScaleItem("1x", 1f), + ScaleItem("10 %", 0.1f), + ScaleItem("25 %", 0.25f), + ScaleItem("50 %", 0.5f), + ScaleItem("75 %", 0.75f), + ScaleItem("100 %", 1f), ) } } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt index 3240877db..785b760ec 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt @@ -69,8 +69,8 @@ class Deferred2Pipeline( private val prevViewProj = MutableMat4f() init { - aoPass.kernelSize = 16 - aoPass.radius = AoRadius.relativeRadius(0.05f) + aoPass.kernelSize = 8 + aoPass.radius = AoRadius.relativeRadius(1 / 20f) aoPass.temporalKernels = tsaa.size scene.addOffscreenPass(gbuffers.a) @@ -169,24 +169,24 @@ class Deferred2Pipeline( ) val TSAA_16 = listOf( Vec2f(1 * s, 1 * s), - Vec2f(-5 * s, -2 * s), + Vec2f(-7 * s, -8 * s), + Vec2f(4 * s, -1 * s), + Vec2f(7 * s, -4 * s), + Vec2f(-2 * s, 6 * s), Vec2f(-8 * s, 0 * s), - Vec2f(-1 * s, -3 * s), - Vec2f(2 * s, 5 * s), - Vec2f(0 * s, -7 * s), - Vec2f(7 * s, -4 * s), - - Vec2f(-3 * s, 2 * s), - Vec2f(5 * s, 3 * s), - Vec2f(-4 * s, -6 * s), Vec2f(6 * s, 7 * s), - Vec2f(4 * s, -1 * s), + Vec2f(-3 * s, 2 * s), Vec2f(3 * s, -5 * s), + Vec2f(-5 * s, -2 * s), + Vec2f(2 * s, 5 * s), + Vec2f(-6 * s, 4 * s), - Vec2f(-7 * s, -8 * s), + Vec2f(0 * s, -7 * s), + Vec2f(-4 * s, -6 * s), + Vec2f(5 * s, 3 * s), ) } } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt index 214499a84..e4ef624af 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt @@ -30,8 +30,8 @@ class GbufferPass( addColor(TexFormat.RGBA, filterMethod = FilterMethod.NEAREST) // metal, roughness, ao, [empty: a, flags maybe?] addColor(TexFormat.RGBA, filterMethod = FilterMethod.NEAREST) - // normals (view space), alpha almost free - addColor(TexFormat.RGBA, filterMethod = FilterMethod.NEAREST, clearColor = ClearColorFill(Color.ZERO)) + // encoded normals (view space) + addColor(TexFormat.R_I32, filterMethod = FilterMethod.NEAREST, clearColor = ClearColorFill(Color.ZERO)) // object-ids, meta addColor(TexFormat.R_I32, filterMethod = FilterMethod.NEAREST) defaultDepth() @@ -160,14 +160,10 @@ class GbufferShader(val config: GbufferShaderConfig) : KslShader("deferred2-gbuf val metallic = float1Port("metallic", fragmentPropertyBlock(config.metallicCfg).outProperty) val aoFactor = float1Port("aoFactor", fragmentPropertyBlock(config.aoCfg).outProperty) - val normalHashX by clamp(((normal.x + 1f.const) * 8f.const).toInt1(), 0.const, 15.const) - val normalHashY by clamp(((normal.y + 1f.const) * 8f.const).toInt1(), 0.const, 15.const) - val meta by (normalHashX shl 28.const) or (normalHashY shl 24.const) or objectId.output - colorOutput(float4Value(baseColor.rgb, emissionStrength), location = 0) colorOutput(float4Value(metallic, roughness, aoFactor, 0f.const), location = 1) - colorOutput(encodeNormalRgb(normal), location = 2) - intOutput(int4Value(meta, 0.const, 0.const, 0.const), location = 3) + intOutput(int4Value(encodeNormalInt(normal), 0.const, 0.const, 0.const), location = 2) + intOutput(int4Value(objectId.output, 0.const, 0.const, 0.const), location = 3) } } } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt index 21a895334..f59efc2df 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt @@ -1,9 +1,14 @@ package de.fabmax.kool.demo.deferred2 +import de.fabmax.kool.KoolSystem +import de.fabmax.kool.math.Mat3f import de.fabmax.kool.math.Vec2i import de.fabmax.kool.math.Vec4f import de.fabmax.kool.modules.ksl.KslShader -import de.fabmax.kool.modules.ksl.blocks.* +import de.fabmax.kool.modules.ksl.NormalLightRange +import de.fabmax.kool.modules.ksl.blocks.ColorSpaceConversion +import de.fabmax.kool.modules.ksl.blocks.pbrMaterialBlock +import de.fabmax.kool.modules.ksl.blocks.sceneLightData import de.fabmax.kool.modules.ksl.lang.* import de.fabmax.kool.pipeline.* import de.fabmax.kool.pipeline.FullscreenShaderUtil.fullscreenQuadVertexStage @@ -12,7 +17,6 @@ import de.fabmax.kool.pipeline.ibl.EnvironmentMap import de.fabmax.kool.scene.* import de.fabmax.kool.util.MemoryLayout import de.fabmax.kool.util.Struct -import de.fabmax.kool.util.Time class LightingPass( val gbuffers: AlternatingPair, @@ -43,7 +47,6 @@ class LightingPass( shader = lightingShader } drawNode.addNode(outputMesh) - drawNode.addNode(Skybox.cube(ibl.reflectionMap, 2f, colorSpaceConversion = ColorSpaceConversion.AsIs)) } @@ -55,8 +58,8 @@ class LightingPass( albedoEmissionTex = newGbuffer.albedoEmission metalRoughnessAoTex = newGbuffer.metalRoughnessAo irradianceMap = ibl.irradianceMap + reflectionMap = ibl.reflectionMap aoMap = ssaoMap - frameI = Time.frameCount camData.set { set(it.proj, camera.proj) @@ -66,9 +69,6 @@ class LightingPass( set(it.position, camera.globalPos) set(it.camNear, camera.clipNear) } - lightData.set { - setLightData(lighting, maxLightCount = 4, it) - } } } } @@ -79,13 +79,12 @@ class DeferredLightingShader : KslShader("deferred2-lighting") { var albedoEmissionTex by bindTexture2d("albedoEmission") var metalRoughnessAoTex by bindTexture2d("metalRoughnessAo") var irradianceMap by bindTextureCube("irradiance") + var reflectionMap by bindTextureCube("reflection") + var brdf by bindTexture2d("brdf", KoolSystem.requireContext().defaultPbrBrdfLut) var aoMap by bindTexture2d("aoMap") val camData = bindUniformStruct("deferredCamData", DeferredCamDataStruct) - private val lightDataStruct = LightDataStruct(4) - val lightData = bindUniformStruct("lightData", lightDataStruct) - - var frameI by bindUniformInt1("frameI") + var ambientMapOrientation: Mat3f by bindUniformMat3("uAmbientTextureOri", Mat3f.IDENTITY) init { pipelineConfig = PipelineConfig( @@ -101,54 +100,74 @@ class DeferredLightingShader : KslShader("deferred2-lighting") { fullscreenQuadVertexStage(uv) fragmentStage { val depth = texture2d("depth", isUnfilterable = true) - val encodedNormals = texture2d("encodedNormals") + val encodedNormals = texture2dInt("encodedNormals") val albedoEmission = texture2d("albedoEmission") val metalRoughnessAo = texture2d("metalRoughnessAo") + val reflection = textureCube("reflection") val irradiance = textureCube("irradiance") + val brdf = texture2d("brdf") val aoMap = texture2d("aoMap") + val ambientOri = uniformMat3("uAmbientTextureOri") val camData = uniformStruct("deferredCamData", DeferredCamDataStruct) - val lightData = uniformStruct("lightData", lightDataStruct) - - val frameI = uniformInt1("frameI") main { - val baseCoord = (uv.output * depth.size().toFloat2()).toInt2() - val camNear = camData[DeferredCamDataStruct.camNear] - val invView = camData[DeferredCamDataStruct.invView] - val invViewProj = camData[DeferredCamDataStruct.invViewProj] + val baseCoord by (uv.output * depth.size().toFloat2()).toInt2() val depthSample by depth.load(baseCoord, lod = 0.const).x + `if` (depthSample eq 0f.const) { + discard() + } + + val camNear by camData[DeferredCamDataStruct.camNear] + val camPos by camData[DeferredCamDataStruct.position] + val invView by camData[DeferredCamDataStruct.invView] + val invViewProj by camData[DeferredCamDataStruct.invViewProj] val size by depth.size() val worldPos by unprojectBaseCoord(depthSample, baseCoord, size, camNear, invViewProj).xyz val ssao by aoMap.load(baseCoord, lod = 0.const).x - val viewNormal by decodeNormalRgb(encodedNormals.load(baseCoord, lod = 0.const).xyz) - val worldNormal by (invView * float4Value(viewNormal, 0f.const)).xyz + val encodedNormal by encodedNormals.load(baseCoord, lod = 0.const).x + val viewNormal by decodeNormalInt(encodedNormal) + val worldNormal by normalize((invView * float4Value(viewNormal, 0f.const)).xyz) - val albedoEmission = float4Var(albedoEmission.load(baseCoord, lod = 0.const)) - val albedo = albedoEmission.xyz - val emission = albedoEmission.w + val albedoEmission by float4Var(albedoEmission.load(baseCoord, lod = 0.const)) + val albedo by albedoEmission.xyz + val emission by albedoEmission.w val metalRoughnessAo by metalRoughnessAo.load(baseCoord, lod = 0.const).xyz - val metallic = metalRoughnessAo.x - val roughness = metalRoughnessAo.y - val ao = metalRoughnessAo.z - - val lightPos by lightData[lightDataStruct.encodedPositions][0.const] - val lightDir by lightData[lightDataStruct.encodedDirections][0.const] - val lightColor by lightData[lightDataStruct.encodedColors][0.const] - val dirToLight by normalize(getLightDirectionFromFragPos(worldPos, lightPos)) - val radiance by getLightRadiance(worldPos, lightPos, lightDir, lightColor) + val metallic by metalRoughnessAo.x + val roughness by metalRoughnessAo.y + val ao by metalRoughnessAo.z - //val ambient by 0.04f.const val ambient by irradiance.sample(worldNormal).rgb * ssao - val diffuse by albedo * ambient + albedo * radiance * saturate(dot(dirToLight, worldNormal)) - colorOutput(diffuse) -// colorOutput(float3Value(ssao, ssao, ssao)) - outDepth set depthSample -// val filterNoise by noise31(float3Value(uv.output, frameI.toFloat1())) * 0.7f.const + 0.3f.const -// colorOutput(diffuse * filterNoise) + // todo: config max lights + val maxNumberOfLights = 4 + val normalLightRange = NormalLightRange.ZeroToOne + val shadowFactors = float1Array(maxNumberOfLights, 1f.const) + val lightData = sceneLightData(maxNumberOfLights) + val material = pbrMaterialBlock(maxNumberOfLights, listOf(reflection), brdf, normalLightRange) { + inCamPos(camPos) + inNormal(worldNormal) + inFragmentPos(worldPos) + inBaseColor(float4Value(albedo, 1f.const)) + + inRoughness(roughness) + inMetallic(metallic) + + inIrradiance(ambient) + inAoFactor(ao) + inAmbientOrientation(ambientOri) + + inReflectionMapWeights(float2Value(1f, 0f)) + inReflectionStrength(1f.const3) + + setLightData(lightData, shadowFactors, 1f.const) + } + colorOutput(material.outColor) + outDepth set depthSample +// colorOutput(viewNormal * 0.5f.const + 0.5f.const) +// colorOutput(float3Value(ssao, ssao, ssao)) } } } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt index 43d44173e..04e222456 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt @@ -206,7 +206,8 @@ class TemporalFilterShader( // val w by 100f.const - filterNoise * filterNoise * filterNoise * 75f.const val w by filterWeight - `if`((!filterHit and !isEdge) or (wasEdge and !isEdge)) { + val isReprojectOutOfScreen by (oldUv.x lt 0f.const) or (oldUv.y lt 0f.const) or (oldUv.x gt 1f.const) or (oldUv.y gt 1f.const) + `if`((!filterHit and !isEdge) or (wasEdge and !isEdge) or isReprojectOutOfScreen) { w set 0f.const } From 1c53b4ce58a13157a538f6a4eb8d6dd3e23f4ab4 Mon Sep 17 00:00:00 2001 From: Max Thiele Date: Wed, 20 May 2026 19:03:03 +0200 Subject: [PATCH 10/27] Screen space reflections --- .../fabmax/kool/modules/ksl/KslPbrShader.kt | 2 +- .../kool/modules/ksl/KslPbrSplatShader.kt | 2 +- .../kool/modules/ksl/blocks/CameraData.kt | 11 +- .../modules/ksl/blocks/PbrMaterialBlock.kt | 26 +-- .../modules/ksl/blocks/RandomFunctions.kt | 2 +- .../ksl/lang/KslScopeBuilderFunctions.kt | 8 +- .../de/fabmax/kool/pipeline/DrawQueue.kt | 7 + .../de/fabmax/kool/pipeline/PipelineBase.kt | 7 + .../kool/pipeline/deferred/PbrSceneShader.kt | 8 +- .../kotlin/de/fabmax/kool/scene/Lighting.kt | 13 +- .../kool/demo/deferred2/Deferred2Demo.kt | 211 +++++++++++++----- .../kool/demo/deferred2/Deferred2Pipeline.kt | 62 +++-- .../kool/demo/deferred2/LightingPass.kt | 182 +++++++++++---- .../kool/demo/deferred2/TemporalFilterPass.kt | 2 +- 14 files changed, 388 insertions(+), 155 deletions(-) diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/KslPbrShader.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/KslPbrShader.kt index 5c0e43cd7..5e63c7252 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/KslPbrShader.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/KslPbrShader.kt @@ -135,7 +135,7 @@ open class KslPbrShader(cfg: Config, model: KslProgram = Model(cfg)) : KslLitSha val reflectionMaps = if (cfg.isTextureReflection) { List(2) { textureCube("tReflectionMap_$it") } } else { - null + emptyList() } val material = pbrMaterialBlock(cfg.lightingCfg.maxNumberOfLights, reflectionMaps, brdfLut, cfg.lightingCfg.normalLightRange) { diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/KslPbrSplatShader.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/KslPbrSplatShader.kt index cebee4689..4ef3f7706 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/KslPbrSplatShader.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/KslPbrSplatShader.kt @@ -228,7 +228,7 @@ class KslPbrSplatShader(val cfg: Config) : KslShader("KslPbrSplatShader") { val reflectionMaps = if (cfg.isTextureReflection) { List(2) { textureCube("tReflectionMap_$it") } } else { - null + emptyList() } val material = pbrMaterialBlock(cfg.lightingCfg.maxNumberOfLights, reflectionMaps, brdfLut, cfg.lightingCfg.normalLightRange) { diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/CameraData.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/CameraData.kt index eba79202e..edb452771 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/CameraData.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/CameraData.kt @@ -10,7 +10,7 @@ import de.fabmax.kool.util.Time context(program: KslProgram) fun cameraData(): CameraData { - return (program.dataBlocks.find { it is CameraData } as? CameraData) ?: CameraData(program) + return program.dataBlocks.filterIsInstance().firstOrNull() ?: CameraData(program) } fun KslScopeBuilder.depthToViewSpacePos(linearDepth: KslExprFloat1, clipSpaceXy: KslExprFloat2, camData: CameraData): KslExprFloat3 { @@ -25,8 +25,11 @@ class CameraData(program: KslProgram) : KslDataBlock("CameraData", program), Ksl private val camUniform = uniformStruct("uCameraData", CamDataStruct, BindGroupScope.VIEW) val viewProjMat: KslExprMat4 get() = camUniform[CamDataStruct.viewProj] + val invViewProjMat: KslExprMat4 get() = camUniform[CamDataStruct.invViewProj] val viewMat: KslExprMat4 get() = camUniform[CamDataStruct.view] + val invViewMat: KslExprMat4 get() = camUniform[CamDataStruct.invView] val projMat: KslExprMat4 get() = camUniform[CamDataStruct.proj] + val invProjMat: KslExprMat4 get() = camUniform[CamDataStruct.invProj] val viewport: KslExprFloat4 get() = camUniform[CamDataStruct.viewport] val viewParams: KslExprFloat4 get() = camUniform[CamDataStruct.viewParams] @@ -67,8 +70,11 @@ class CameraData(program: KslProgram) : KslDataBlock("CameraData", program), Ksl val cam = q.view.camera binding.set { set(it.viewProj, q.viewProjMatF) + set(it.invViewProj, q.invViewProjMatF) set(it.view, q.viewMatF) + set(it.invView, q.invViewMatF) set(it.proj, q.projMat) + set(it.invProj, q.invProjMat) set(it.viewport, viewportVec.set(vp.x.toFloat(), vp.y.toFloat(), vp.width.toFloat(), vp.height.toFloat())) set(it.viewParams, cam.viewParams) set(it.position, cam.globalPos) @@ -81,8 +87,11 @@ class CameraData(program: KslProgram) : KslDataBlock("CameraData", program), Ksl object CamDataStruct : Struct("CameraData", MemoryLayout.Std140) { val viewProj = mat4("viewProjMat") + val invViewProj = mat4("invViewProjMat") val view = mat4("viewMat") + val invView = mat4("invViewMat") val proj = mat4("projMat") + val invProj = mat4("invProjMat") val viewport = float4("viewport") val viewParams = float4("viewParams") val position = float3("position") diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/PbrMaterialBlock.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/PbrMaterialBlock.kt index f3563da0c..fd05523a1 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/PbrMaterialBlock.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/PbrMaterialBlock.kt @@ -9,7 +9,7 @@ import kotlin.math.PI context(builder: KslScopeBuilder) fun pbrMaterialBlock( maxNumberOfLights: Int, - reflectionMaps: List>?, + reflectionMaps: List>, brdfLut: KslExpression, normalLightRange: NormalLightRange, block: PbrMaterialBlock.() -> Unit @@ -29,7 +29,7 @@ fun pbrMaterialBlock( class PbrMaterialBlock( maxNumberOfLights: Int, name: String, - reflectionMaps: List>?, + reflectionMaps: List>, brdfLut: KslExpression, normalLightRange: NormalLightRange, parentScope: KslScopeBuilder, @@ -41,14 +41,16 @@ class PbrMaterialBlock( // environment reflection map(s) val inReflectionMapWeights = inFloat2("inReflectionMapWeights", float2Value(1f, 0f)) val inReflectionStrength = inFloat3("inReflectionStrength", float3Value(1f, 1f, 1f)) - // screen-space reflection - val inReflectionColor = inFloat3("inReflectionColor", float3Value(1f, 1f, 1f)) - val inReflectionWeight = inFloat1("inReflectionWeight", 0f.const) val inAmbientOrientation = inMat3("inAmbientOrientation") val inIrradiance = inFloat3("inIrradiance") val inAoFactor = inFloat1("inAoFactor", 0f.const) + val outSpecular = outFloat3("outSpecular") + val outSpecularFactor = outFloat3("outSpecularFactor") + val outAmbient = outFloat3("outAmbient") + val outLight = outFloat3("outLight") + init { body.apply { val baseColorRgb = inBaseColor.rgb @@ -86,7 +88,7 @@ class PbrMaterialBlock( // use irradiance / ambient color as fallback reflection color in case no reflection map is used // ambient color is supposed to be uniform in this case because reflection direction is not considered val reflectionColor by inIrradiance - if (reflectionMaps != null) { + if (reflectionMaps.isNotEmpty()) { // sample reflection map in reflection direction val r = inAmbientOrientation * reflect(-viewDir, inNormal) val mipLevel by (1f.const - pow(1f.const - roughness, 1.25f.const)) * (ReflectionMapPass.REFLECTION_MIP_LEVELS - 1).toFloat().const @@ -99,14 +101,12 @@ class PbrMaterialBlock( } reflectionColor set reflectionColor * inReflectionStrength - // screen-space reflection - reflectionColor set mix(reflectionColor, clamp(inReflectionColor, Vec3f.ZERO.const, Vec3f(5f).const), inReflectionWeight) - val brdf by brdfLut.sample(float2Value(normalDotView, roughness)).rg - val specular by reflectionColor * (fAmbient * brdf.r + brdf.g) / inBaseColor.a - val ambient by kDAmbient * diffuse * inAoFactor - val reflection by specular * inAoFactor - outColor set ambient + lo + reflection + outSpecular set reflectionColor + outSpecularFactor set (fAmbient * brdf.r + brdf.g) / inBaseColor.a * inAoFactor + outAmbient set kDAmbient * diffuse * inAoFactor + outLight set lo + outColor set outAmbient + outLight + outSpecular * outSpecularFactor } } } diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/RandomFunctions.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/RandomFunctions.kt index 97878dbbe..d70b4306b 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/RandomFunctions.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/RandomFunctions.kt @@ -76,7 +76,7 @@ fun KslScopeBuilder.noise41(p: KslExprFloat4): KslExprFloat1 = noise41(p.toUintB @JvmName("noise42f") fun KslScopeBuilder.noise42(p: KslExprFloat4): KslExprFloat2 = noise42(p.toUintBits()) @JvmName("noise43f") -fun KslScopeBuilder.noise423(p: KslExprFloat4): KslExprFloat3 = noise43(p.toUintBits()) +fun KslScopeBuilder.noise43(p: KslExprFloat4): KslExprFloat3 = noise43(p.toUintBits()) @JvmName("noise44f") fun KslScopeBuilder.noise44(p: KslExprFloat4): KslExprFloat4 = noise44(p.toUintBits()) diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslScopeBuilderFunctions.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslScopeBuilderFunctions.kt index 4ed9ce686..157fcf9a6 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslScopeBuilderFunctions.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslScopeBuilderFunctions.kt @@ -202,11 +202,11 @@ context(_: KslScopeBuilder) fun radians(vec: KslVectorExpression) where V: KslFloatType, V: KslVector = KslBuiltinRadiansVector(vec) context(_: KslScopeBuilder) -fun reflect(a: KslVectorExpression, b: KslVectorExpression) - where V: KslFloatType, V: KslVector = KslBuiltinReflect(a, b) +fun reflect(incident: KslVectorExpression, normal: KslVectorExpression) + where V: KslFloatType, V: KslVector = KslBuiltinReflect(incident, normal) context(_: KslScopeBuilder) -fun refract(a: KslVectorExpression, b: KslVectorExpression, i: KslScalarExpression) - where V: KslFloatType, V: KslVector = KslBuiltinRefract(a, b, i) +fun refract(incident: KslVectorExpression, normal: KslVectorExpression, eta: KslScalarExpression) + where V: KslFloatType, V: KslVector = KslBuiltinRefract(incident, normal, eta) context(_: KslScopeBuilder) fun round(value: KslScalarExpression) = KslBuiltinRoundScalar(value) diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/DrawQueue.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/DrawQueue.kt index 465b5a83e..5a9198e85 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/DrawQueue.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/DrawQueue.kt @@ -46,6 +46,8 @@ class DrawQueue() { private val lazyInvProj = LazyMat4f { projMat.invert(it) } private val lazyInvViewF = LazyMat4f { viewMatF.invert(it) } private val lazyInvViewD = LazyMat4d { viewMatD.invert(it) } + private val lazyInvViewProjF = LazyMat4f { viewProjMatF.invert(it) } + private val lazyInvViewProjD = LazyMat4d { viewProjMatD.invert(it) } val invProjMat: Mat4f get() = lazyInvProj.get() @@ -53,6 +55,9 @@ class DrawQueue() { val invViewMatD: Mat4d get() = lazyInvViewD.get() + val invViewProjMatF: Mat4f get() = lazyInvViewProjF.get() + val invViewProjMatD: Mat4d get() = lazyInvViewProjD.get() + private val commandPool = mutableListOf() @PublishedApi @@ -99,6 +104,8 @@ class DrawQueue() { lazyInvProj.isDirty = true lazyInvViewF.isDirty = true lazyInvViewD.isDirty = true + lazyInvViewProjF.isDirty = true + lazyInvViewProjD.isDirty = true } private fun getOrderedQueue(): OrderedQueue { diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/PipelineBase.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/PipelineBase.kt index 9e36683a3..294961cdb 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/PipelineBase.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/PipelineBase.kt @@ -74,6 +74,13 @@ abstract class PipelineBase(val name: String, val bindGroupLayouts: BindGroupLay } } +inline fun > T.swapPipelineData(key: Any?, block: T.() -> Unit) { + createdPipeline?.let { + it.swapPipelineData(key) + block() + } +} + inline fun PipelineBase.swapPipelineDataCapturing(key: Any?, block: () -> Unit) { swapPipelineData(key) block() diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred/PbrSceneShader.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred/PbrSceneShader.kt index f2981ac8a..825408d2e 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred/PbrSceneShader.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred/PbrSceneShader.kt @@ -199,7 +199,7 @@ open class PbrSceneShader(cfg: DeferredPbrConfig, model: Model = Model(cfg)) : val reflectionMaps = if (cfg.isTextureReflection) { List(2) { textureCube("tReflectionMap_$it") } } else { - null + emptyList() } val material = pbrMaterialBlock(cfg.lightingConfig.maxNumberOfLights, reflectionMaps, brdfLut, cfg.lightingConfig.normalLightRange) { @@ -217,12 +217,12 @@ open class PbrSceneShader(cfg: DeferredPbrConfig, model: Model = Model(cfg)) : inReflectionMapWeights(uniformFloat2("uReflectionWeights")) inReflectionStrength(reflectionStrength) - inReflectionColor(reflectionColor) - inReflectionWeight(reflectionWeight) setLightData(lightData, shadowFactors, cfg.lightingConfig.lightStrength.const) } - colorOutput(material.outColor + emissive) + val finalReflectionColor by mix(material.outSpecular, clamp(reflectionColor, Vec3f.ZERO.const, Vec3f(5f).const), reflectionWeight) + val finalColor by material.outAmbient + material.outLight + finalReflectionColor * material.outSpecularFactor + emissive + colorOutput(finalColor) outDepth set texture2d("depth", isUnfilterable = true).sample(uv).r } } diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/Lighting.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/Lighting.kt index f9277491b..f61033372 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/Lighting.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/Lighting.kt @@ -9,9 +9,8 @@ import de.fabmax.kool.util.logW * @author fabmax */ class Lighting { - private val _lights = mutableListOf() val lights: List - get() = _lights + field = mutableListOf() var maxNumberOfLights = 4 @@ -31,16 +30,16 @@ class Lighting { } fun addLight(light: Light) { - if (light in _lights) { + if (light in lights) { logW { "light is already present in lights list" } return } - if (_lights.size >= maxNumberOfLights) { + if (lights.size >= maxNumberOfLights) { logW { "Unable to add light: Maximum number of lights (${maxNumberOfLights}) reached. Consider increasing Scene.lighting.maxNumberOfLights" } return } light.lightIndex = lights.size - _lights += light + lights += light } inline fun addDirectionalLight(block: Light.Directional.() -> Unit): Light.Directional { @@ -65,7 +64,7 @@ class Lighting { } fun removeLight(light: Light) { - _lights -= light + lights -= light light.lightIndex = -1 lights.forEachIndexed { i, it -> it.lightIndex = i @@ -74,7 +73,7 @@ class Lighting { fun clear() { lights.forEach { it.lightIndex = -1 } - _lights.clear() + lights.clear() } fun singleDirectionalLight(block: Light.Directional.() -> Unit): Light.Directional { diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt index d941f5182..2e14c8c16 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt @@ -6,6 +6,7 @@ import de.fabmax.kool.demo.menu.DemoMenu import de.fabmax.kool.math.Vec2f import de.fabmax.kool.math.Vec3f import de.fabmax.kool.math.deg +import de.fabmax.kool.modules.gltf.GltfLoadConfig import de.fabmax.kool.modules.ksl.KslShader import de.fabmax.kool.modules.ksl.blocks.ColorSpaceConversion import de.fabmax.kool.modules.ksl.blocks.convertColorSpace @@ -14,38 +15,54 @@ import de.fabmax.kool.modules.ui2.* import de.fabmax.kool.pipeline.BloomPass import de.fabmax.kool.pipeline.FullscreenShaderUtil.fullscreenQuadVertexStage import de.fabmax.kool.pipeline.FullscreenShaderUtil.generateFullscreenQuad +import de.fabmax.kool.pipeline.SamplerSettings import de.fabmax.kool.pipeline.SingleColorTexture import de.fabmax.kool.pipeline.ao.AoRadius -import de.fabmax.kool.pipeline.swapPipelineDataCapturing +import de.fabmax.kool.pipeline.swapPipelineData import de.fabmax.kool.scene.* +import de.fabmax.kool.toString import de.fabmax.kool.util.* class Deferred2Demo : DemoScene("Deferred2 Demo") { - //val ibl by hdriImage("hdri/newport_loft.rgbe.png") - //val ibl by hdriImage("hdri/shanghai_bund_1k.rgbe.png") - val ibl by hdriImage("hdri/circus_arena_1k.rgbe.png") + val ibl by hdriImage("hdri/newport_loft.rgbe.png") +// val ibl by hdriImage("hdri/shanghai_bund_1k.rgbe.png") +// val ibl by hdriImage("hdri/circus_arena_1k.rgbe.png") +// val ibl by hdriImage("hdri/syferfontein_0d_clear_1k.rgbe.png") +// val ibl by hdriImage("hdri/colorful_studio_1k.rgbe.png") +// val ibl by hdriImage("hdri/spruit_sunrise_1k.rgbe.png") +// val ibl by hdriImage("hdri/mossy_forest_1k.rgbe.png") + + val teapot by model("models/teapot.gltf.gz", GltfLoadConfig(applyMaterials = false)) private val albedoMap by texture2d("materials/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_COL_2K_METALNESS.jpg") private val normalMap by texture2d("materials/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_NRM_2K_METALNESS.jpg") private val metallicMap by texture2d("materials/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_METALNESS_2K_METALNESS.jpg") private val roughnessMap by texture2d("materials/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_ROUGHNESS_2K_METALNESS.jpg") private val aoMap by texture2d("materials/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_AO_2K_METALNESS.jpg") + //private val uvChecker by texture2d("materials/uv_checker_map.jpg") + private val uvChecker by texture2d("materials/kool-test-tex.png") private lateinit var pipeline: Deferred2Pipeline + private lateinit var bloomPass: BloomPass private val filterWeight = mutableStateOf(16) + private val groundRoughness = mutableStateOf(0.5f) private val bloom = mutableStateOf(true) + private val gpuTimes = mutableStateOf(GpuTimes(0.0, 0.0, 0.0, 0.0, 0.0)) + private var gpuTimesAccu = GpuTimes(0.0, 0.0, 0.0, 0.0, 0.0) + override fun Scene.setupMainScene(ctx: KoolContext) { val content = deferredContent() val lighting = Lighting().apply { - singlePointLight { - setup(Vec3f(1.2f, 3.2f, 2f)) - setColor(Color.WHITE, intensity = 5f) - } + clear() +// singlePointLight { +// setup(Vec3f(1.2f, 3.2f, 2f)) +// setColor(Color.WHITE, intensity = 5f) +// } } - pipeline = Deferred2Pipeline(content, scene = this, ibl, lighting) + pipeline = Deferred2Pipeline(content, scene = this, ibl, isScreenSpaceReflections = true, lighting) pipeline.renderScale = 0.5f pipeline.aoPass.radius = AoRadius.relativeRadius(1/20f) filterWeight.value = pipeline.filterPass.filterWeight.toInt() @@ -56,12 +73,13 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { addNode(orbitCam) } - val bloomPass = BloomPass(pipeline.filterPass.filterOutput.newVal) + bloomPass = BloomPass(pipeline.filterPass.filterOutput.newVal) + bloomPass.isProfileGpu = true addComputePass(bloomPass) bloom.onChange { _, value -> bloomPass.isEnabled = value } pipeline.onSwap { val filterOutput = pipeline.filterPass.filterOutput.newVal - bloomPass.inputShader.swapPipelineDataCapturing(filterOutput) { + bloomPass.inputShader.swapPipelineData(filterOutput) { bloomPass.inputTexture = filterOutput } } @@ -72,56 +90,44 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { } shader = deferredOutputShader(this@Deferred2Demo, pipeline, bloomPass) } - } - override fun createMenu(menu: DemoMenu, ctx: KoolContext): UiSurface = menuSurface { - LabeledSwitch("Bloom", bloom) { } - MenuSlider1("Filter", filterWeight.use().toFloat(), 0f, 32f, { "${it.toInt()}" }) { - filterWeight.set(it.toInt()) - } - MenuRow { - var tsaaIndex by remember(2) - Text("Temporal AA".l) { labelStyle(120.dp) } - ComboBox { - modifier - .width(Grow.Std) - .margin(start = sizes.largeGap) - .items(TsaaItem.items.map { it.label }) - .selectedIndex(tsaaIndex) - .onItemSelected { - tsaaIndex = it - pipeline.tsaa = TsaaItem.items[it].tsaa - pipeline.aoPass.temporalKernels = TsaaItem.items[it].numSamples - } - } - } - MenuRow { - var scaleIndex by remember(2) - Text("Render scale".l) { labelStyle(120.dp) } - ComboBox { - modifier - .width(Grow.Std) - .margin(start = sizes.largeGap) - .items(ScaleItem.items.map { it.label }) - .selectedIndex(scaleIndex) - .onItemSelected { - scaleIndex = it - pipeline.renderScale = ScaleItem.items[it].scale - } + val nAccu = 50 + onUpdate { + gpuTimesAccu = GpuTimes( + gpuTimesAccu.gbuffer + pipeline.gbuffers.a.tGpu.inWholeMicroseconds / 1000.0 / nAccu, + gpuTimesAccu.ao + pipeline.aoPass.tGpu.inWholeMicroseconds / 1000.0 / nAccu, + gpuTimesAccu.lighting + pipeline.lightingPass.tGpu.inWholeMicroseconds / 1000.0 / nAccu, + gpuTimesAccu.filter + pipeline.filterPass.tGpu.inWholeMicroseconds / 1000.0 / nAccu, + gpuTimesAccu.bloom + bloomPass.tGpu.inWholeMicroseconds / 1000.0 / nAccu, + ) + if (Time.frameCount % nAccu == 0) { + gpuTimes.set(gpuTimesAccu) + gpuTimesAccu = GpuTimes(0.0, 0.0, 0.0, 0.0, 0.0) } } } private fun deferredContent() = Node("deferred content").apply { addGroup { - onUpdate { -// transform.rotate(-30f.deg * Time.deltaT, Vec3f.Y_AXIS) - } + transform.rotate(45f.deg, Vec3f.Y_AXIS) addColorMesh { generate { color = MdColor.PINK.toLinear() cube { origin.set(-2.5f, 0f, 0f)} + color = MdColor.LIGHT_BLUE.toLinear() + cube { + origin.set(-2.5f, 1f, 0f) + size.set(0.25f, 1f, 0.25f) + } + } + shader = gbufferShader(objectId = 1) { + color { vertexColor() } + roughness { constProperty(0.15f) } + } + } + addColorMesh { + generate { color = MdColor.AMBER.toLinear() icoSphere { center.set(-2.5f, 0f, 2.5f) @@ -129,12 +135,10 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { radius = 0.5f } } - onUpdate { -// transform.rotate(360f.deg * Time.deltaT, Vec3f.Y_AXIS) - } - shader = gbufferShader(objectId = 1) { + shader = gbufferShader(objectId = 6) { color { vertexColor() } - roughness { constProperty(0.15f) } + metallic { constProperty(1f) } + roughness { constProperty(0f) } } } addTextureMesh(isNormalMapped = true) { @@ -154,6 +158,10 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { metallic { textureProperty(metallicMap) } roughness { textureProperty(roughnessMap) } ao { textureProperty(aoMap) } + +// color { constColor(Color.WHITE) } +// metallic { constProperty(1f) } +// roughness { constProperty(0f) } }.apply { bindTexture2d("tbaseColor", albedoMap) bindTexture2d("tNormalMap", normalMap) @@ -173,19 +181,100 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { color { vertexColor() } } } - addColorMesh { + addTextureMesh { generate { translate(0f, -0.5f, 0f) - color = Color.WHITE grid { sizeX = 50f sizeY = 50f + texCoordScale.set(10f, 10f) } } shader = gbufferShader(objectId = 4) { - color { vertexColor() } + color { + textureColor(uvChecker) + } + roughness { uniformProperty(groundRoughness.value, "uRough") } +// metallic { constProperty(1f) } + + }.apply { + bindTexture2d("tbaseColor", uvChecker) + + var rough by bindUniformFloat1("uRough", groundRoughness.value) + groundRoughness.onChange { _, newValue -> rough = newValue } + } + } + + val modelMesh = teapot.meshes.values.first().apply { + transform.translate(0f, -0.5f, 5f).scale(0.5f) + shader = gbufferShader(objectId = 5) { + color { + constColor(MdColor.LIME toneLin 500) +// constColor(Color.WHITE) + } + roughness { constProperty(0.1f) } +// metallic { constProperty(1f) } } } + addNode(modelMesh) + } + } + + override fun createMenu(menu: DemoMenu, ctx: KoolContext): UiSurface = menuSurface { + LabeledSwitch("Bloom", bloom) { } + MenuSlider1("Filter", filterWeight.use().toFloat(), 0f, 32f, { "${it.toInt()}" }) { + filterWeight.set(it.toInt()) + } + MenuRow { + var tsaaIndex by remember(2) + Text("Temporal AA".l) { labelStyle(120.dp) } + ComboBox { + modifier + .width(Grow.Std) + .margin(start = sizes.largeGap) + .items(TsaaItem.items.map { it.label }) + .selectedIndex(tsaaIndex) + .onItemSelected { + tsaaIndex = it + pipeline.tsaa = TsaaItem.items[it].tsaa + pipeline.aoPass.temporalKernels = TsaaItem.items[it].numSamples + } + } + } + MenuRow { + var scaleIndex by remember(2) + Text("Render scale".l) { labelStyle(120.dp) } + ComboBox { + modifier + .width(Grow.Std) + .margin(start = sizes.largeGap) + .items(ScaleItem.items.map { it.label }) + .selectedIndex(scaleIndex) + .onItemSelected { + scaleIndex = it + pipeline.renderScale = ScaleItem.items[it].scale + } + } + } + MenuSlider1("Roughness", groundRoughness.use(), 0f, 1f) { + groundRoughness.set(it) + } + MenuRow { + Column(width = Grow.Std) { + Text("G-Buffer:") { } + Text("Ambient Occlusion:") { } + Text("Lighting:") { } + Text("Filter:") { } + Text("Bloom:") { } + } + Column { + val t = gpuTimes.use() + Text("${t.gbuffer.toString(2)} ms") { } + Text("${t.ao.toString(2)} ms") { } + Text("${t.lighting.toString(2)} ms") { } + Text("${t.filter.toString(2)} ms") { } + Text("${t.bloom.toString(2)} ms") { } + } } } } @@ -203,7 +292,7 @@ private fun deferredOutputShader( val output = texture2d("deferredOutput") val bloom = texture2d("bloomOutput") val uvi = (uv.output * output.size().toFloat2()).toInt2() - val color by output.load(uvi).rgb + bloom.sample(uv.output).rgb + val color by output.sample(uv.output).rgb + bloom.sample(uv.output).rgb val ditherTex = texture2d("ditherPattern") val ditherC by uvi % ditherTex.size() @@ -220,11 +309,11 @@ private fun deferredOutputShader( outputShader.bindTexture2d("ditherPattern", ditherTex) var bloomTex by outputShader.bindTexture2d("bloomOutput", bloomPass.bloomMap) - var inputTex by outputShader.bindTexture2d("deferredOutput") + var inputTex by outputShader.bindTexture2d("deferredOutput", defaultSampler = SamplerSettings().nearest().clamped()) val noBloom = SingleColorTexture(Color.BLACK) deferred2Pipeline.onSwap { val filterOutput = deferred2Pipeline.filterPass.filterOutput.newVal - outputShader.swapPipelineDataCapturing(filterOutput) { + outputShader.swapPipelineData(filterOutput) { inputTex = filterOutput bloomTex = if (bloomPass.isEnabled) bloomPass.bloomMap else noBloom } @@ -255,3 +344,5 @@ private data class ScaleItem(val label: String, val scale: Float) { ) } } + +private data class GpuTimes(val gbuffer: Double, val ao: Double, val lighting: Double, val filter: Double, val bloom: Double) diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt index 785b760ec..f48834409 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt @@ -9,7 +9,7 @@ import de.fabmax.kool.pipeline.Texture2d import de.fabmax.kool.pipeline.ao.AoRadius import de.fabmax.kool.pipeline.ao.ComputeAoPass import de.fabmax.kool.pipeline.ibl.EnvironmentMap -import de.fabmax.kool.pipeline.swapPipelineDataCapturing +import de.fabmax.kool.pipeline.swapPipelineData import de.fabmax.kool.scene.Lighting import de.fabmax.kool.scene.Node import de.fabmax.kool.scene.PerspectiveCamera @@ -23,6 +23,7 @@ class Deferred2Pipeline( val content: Node, private val scene: Scene, val ibl: EnvironmentMap, + val isScreenSpaceReflections: Boolean, val lighting: Lighting = Lighting(), var renderScale: Float = 1f, var tsaa: List = TSAA_4, @@ -42,7 +43,8 @@ class Deferred2Pipeline( camera = camera, inputDepth = gbuffers.a.depth, inputNormals = gbuffers.a.normals, - initialSize = size + initialSize = size, + distFormat = TexFormat.R_F32, ) private val swapListeners = BufferedList<() -> Unit>() @@ -55,6 +57,7 @@ class Deferred2Pipeline( size = size, ssaoMap = aoPass.aoMap, ibl = ibl, + pipeline = this, ) val filterPass = TemporalFilterPass( lightingOutput = lightingPass.lightingOutput, @@ -76,9 +79,17 @@ class Deferred2Pipeline( scene.addOffscreenPass(gbuffers.a) scene.addOffscreenPass(gbuffers.b) scene.addComputePass(aoPass) +// reflectionPass?.let { scene.addComputePass(it) } scene.addOffscreenPass(lightingPass) scene.addComputePass(filterPass) + + gbuffers.a.isProfileGpu = true + gbuffers.b.isProfileGpu = true + lightingPass.isProfileGpu = true + filterPass.isProfileGpu = true + aoPass.isProfileGpu = true + val offsetMat = MutableMat4f() camera.onCameraUpdated += { val tsaa = tsaa @@ -101,6 +112,7 @@ class Deferred2Pipeline( gbuffers.a.setSize(size.x, size.y) gbuffers.b.setSize(size.x, size.y) aoPass.resize(size.x, size.y) +// reflectionPass?.resize(size) lightingPass.setSize(size.x, size.y) filterPass.resize(size) resizeListeners.forEachUpdated { it(size) } @@ -110,30 +122,34 @@ class Deferred2Pipeline( scene.coroutineScope.launch { withContext(KoolDispatchers.Synced) { while (true) { - oldViewProj.set(prevViewProj) - prevViewProj.set(camera.viewProj) + swapBuffers() + yield() + } + } + } + } - lightingPass.swapBuffers() - filterPass.swapBuffers() + private fun swapBuffers() { + oldViewProj.set(prevViewProj) + prevViewProj.set(camera.viewProj) - val currentGbuffer = gbuffers.newVal - aoPass.inputShader.swapPipelineDataCapturing(currentGbuffer) { - aoPass.inputDepth = currentGbuffer.depth - aoPass.inputNormals = currentGbuffer.normals - //aoPass.captureCamera() - } + lightingPass.swapBuffers() + filterPass.swapBuffers() +// reflectionPass?.swapBuffers() - swapListeners.forEachUpdated { it() } + val currentGbuffer = gbuffers.newVal + aoPass.inputShader.swapPipelineData(currentGbuffer) { + aoPass.inputDepth = currentGbuffer.depth + aoPass.inputNormals = currentGbuffer.normals + } - gbuffers.newVal.objModelMatsGpu.uploadData(gbuffers.newVal.objModelMats) + swapListeners.forEachUpdated { it() } - // this is called after update, newVal was enabled and updated, disable it and enable oldVal for next frame - gbuffers.newVal.isEnabled = false - gbuffers.oldVal.isEnabled = true - yield() - } - } - } + gbuffers.newVal.objModelMatsGpu.uploadData(gbuffers.newVal.objModelMats) + + // this is called after update, newVal was enabled and updated, disable it and enable oldVal for next frame + gbuffers.newVal.isEnabled = false + gbuffers.oldVal.isEnabled = true } fun onResize(block: (Vec2i) -> Unit) { @@ -193,9 +209,9 @@ class Deferred2Pipeline( fun makeDitherPattern(): Texture2d { val buf = Uint8Buffer(16) - fun u(i: Int): UByte = (255f * i.toFloat() / (buf.capacity - 1)).toInt().toUByte() + fun u(i: Int): UByte = (255f * (i-1).toFloat() / (buf.capacity - 1)).toInt().toUByte() - buf[0] = u(0) + buf[0] = u(1) buf[1] = u(9) buf[2] = u(3) buf[3] = u(11) diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt index f59efc2df..cad8cc2d0 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt @@ -3,20 +3,16 @@ package de.fabmax.kool.demo.deferred2 import de.fabmax.kool.KoolSystem import de.fabmax.kool.math.Mat3f import de.fabmax.kool.math.Vec2i -import de.fabmax.kool.math.Vec4f import de.fabmax.kool.modules.ksl.KslShader import de.fabmax.kool.modules.ksl.NormalLightRange -import de.fabmax.kool.modules.ksl.blocks.ColorSpaceConversion -import de.fabmax.kool.modules.ksl.blocks.pbrMaterialBlock -import de.fabmax.kool.modules.ksl.blocks.sceneLightData +import de.fabmax.kool.modules.ksl.blocks.* import de.fabmax.kool.modules.ksl.lang.* import de.fabmax.kool.pipeline.* import de.fabmax.kool.pipeline.FullscreenShaderUtil.fullscreenQuadVertexStage import de.fabmax.kool.pipeline.FullscreenShaderUtil.generateFullscreenQuad import de.fabmax.kool.pipeline.ibl.EnvironmentMap import de.fabmax.kool.scene.* -import de.fabmax.kool.util.MemoryLayout -import de.fabmax.kool.util.Struct +import de.fabmax.kool.util.ColorGradient class LightingPass( val gbuffers: AlternatingPair, @@ -25,6 +21,7 @@ class LightingPass( size: Vec2i, var ssaoMap: Texture2d, private val ibl: EnvironmentMap, + val pipeline: Deferred2Pipeline, ) : OffscreenPass2d( drawNode = Node(), attachmentConfig = AttachmentConfig { @@ -36,7 +33,7 @@ class LightingPass( ) { val lightingOutput: Texture2d get() = colorTexture!! - private val lightingShader = DeferredLightingShader() + private val lightingShader = DeferredLightingShader(pipeline.isScreenSpaceReflections) init { this.camera = camera @@ -52,29 +49,23 @@ class LightingPass( fun swapBuffers() { val newGbuffer = gbuffers.newVal - lightingShader.swapPipelineDataCapturing(newGbuffer) { + lightingShader.swapPipelineData(newGbuffer) { depthTex = newGbuffer.depth + depthSmall = pipeline.aoPass.scaledDists encodedNormals = newGbuffer.normals albedoEmissionTex = newGbuffer.albedoEmission metalRoughnessAoTex = newGbuffer.metalRoughnessAo irradianceMap = ibl.irradianceMap reflectionMap = ibl.reflectionMap aoMap = ssaoMap - - camData.set { - set(it.proj, camera.proj) - set(it.invView, camera.invView) - set(it.invViewProj, camera.invViewProj) - set(it.viewport, Vec4f(0f, 0f, width.toFloat(), height.toFloat())) - set(it.position, camera.globalPos) - set(it.camNear, camera.clipNear) - } + oldColor = pipeline.filterPass.filterOutput.oldVal } } } -class DeferredLightingShader : KslShader("deferred2-lighting") { +class DeferredLightingShader(isScreenSpaceReflections: Boolean) : KslShader("deferred2-lighting") { var depthTex by bindTexture2d("depth") + var depthSmall by bindTexture2d("depthSmall") var encodedNormals by bindTexture2d("encodedNormals") var albedoEmissionTex by bindTexture2d("albedoEmission") var metalRoughnessAoTex by bindTexture2d("metalRoughnessAo") @@ -83,7 +74,8 @@ class DeferredLightingShader : KslShader("deferred2-lighting") { var brdf by bindTexture2d("brdf", KoolSystem.requireContext().defaultPbrBrdfLut) var aoMap by bindTexture2d("aoMap") - val camData = bindUniformStruct("deferredCamData", DeferredCamDataStruct) + var oldColor by bindTexture2d("oldColor") + var ambientMapOrientation: Mat3f by bindUniformMat3("uAmbientTextureOri", Mat3f.IDENTITY) init { @@ -92,14 +84,17 @@ class DeferredLightingShader : KslShader("deferred2-lighting") { cullMethod = CullMethod.NO_CULLING, depthTest = DepthCompareOp.ALWAYS ) - program.program() + program.program(isScreenSpaceReflections) + + bindTexture1d("tgradient", GradientTexture(ColorGradient.ROCKET)) } - private fun KslProgram.program() { + private fun KslProgram.program(isScreenSpaceReflections: Boolean) { val uv = interStageFloat2() fullscreenQuadVertexStage(uv) fragmentStage { val depth = texture2d("depth", isUnfilterable = true) + val depthSmall = texture2d("depthSmall", isUnfilterable = true) val encodedNormals = texture2dInt("encodedNormals") val albedoEmission = texture2d("albedoEmission") val metalRoughnessAo = texture2d("metalRoughnessAo") @@ -109,7 +104,7 @@ class DeferredLightingShader : KslShader("deferred2-lighting") { val aoMap = texture2d("aoMap") val ambientOri = uniformMat3("uAmbientTextureOri") - val camData = uniformStruct("deferredCamData", DeferredCamDataStruct) + val camData = cameraData() main { val baseCoord by (uv.output * depth.size().toFloat2()).toInt2() @@ -118,10 +113,10 @@ class DeferredLightingShader : KslShader("deferred2-lighting") { discard() } - val camNear by camData[DeferredCamDataStruct.camNear] - val camPos by camData[DeferredCamDataStruct.position] - val invView by camData[DeferredCamDataStruct.invView] - val invViewProj by camData[DeferredCamDataStruct.invViewProj] + val camNear by camData.clipNear + val camPos by camData.position + val invView by camData.invViewMat + val invViewProj by camData.invViewProjMat val size by depth.size() val worldPos by unprojectBaseCoord(depthSample, baseCoord, size, camNear, invViewProj).xyz val ssao by aoMap.load(baseCoord, lod = 0.const).x @@ -159,25 +154,134 @@ class DeferredLightingShader : KslShader("deferred2-lighting") { inAoFactor(ao) inAmbientOrientation(ambientOri) - inReflectionMapWeights(float2Value(1f, 0f)) - inReflectionStrength(1f.const3) - setLightData(lightData, shadowFactors, 1f.const) } - colorOutput(material.outColor) + + if (isScreenSpaceReflections) { + val oldColor = texture2d("oldColor") + val screenReflection by screenReflect(material, viewNormal, depthSmall, oldColor) + val finalColor by material.outAmbient + material.outLight + screenReflection //+ emissive + colorOutput(finalColor) +// colorOutput(screenReflection) + } else { + colorOutput(material.outColor) + } + outDepth set depthSample -// colorOutput(viewNormal * 0.5f.const + 0.5f.const) -// colorOutput(float3Value(ssao, ssao, ssao)) } } } } -object DeferredCamDataStruct : Struct("DeferredCameraData", MemoryLayout.Std140) { - val proj = mat4("projMat") - val invView = mat4("invView") - val invViewProj = mat4("invViewProjMat") - val viewport = float4("viewport") - val position = float3("position") - val camNear = float1("camClipNear") +context(_: KslProgram, _: KslShaderStage) +fun KslScopeBuilder.screenReflect( + material: PbrMaterialBlock, + viewNormal: KslExprFloat3, + viewDists: KslUniform, + oldColor: KslUniform, +): KslExprFloat3 { + val camData = cameraData() + + val fnProjViewPos = functionFloat2("fnProiViewPos") { + val viewPos = paramFloat3("viewPos") + body { + val p by camData.projMat * float4Value(viewPos, 1f) + p.xy / p.w * float2Value(0.5f, -0.5f) + 0.5f.const + } + } + + val fnDepthDelta = functionFloat1("fnDepthDelta") { + val uv = paramFloat2("uv") + val refDepth = paramFloat1("refDepth") + body { + val texSz by viewDists.size().toFloat2() + val uvi by (uv * texSz).toInt2() + viewDists.load(uvi, lod = 0.const).x - refDepth + } + } + + val fnCastRay = functionFloat3("fnCastRay") { + val origin = paramFloat3("origin") + val rayDir = paramFloat3("rayDir") + val noise = paramFloat3("noise") + + body { + val baseDist by -origin.z + val dError by 0f.const + val stepUv by 0f.const2 + val isHit by false.const + val step by baseDist * 0.025f.const + noise.x * 0.01f.const + val prevStep by 0f.const + val stepScale by 1f.const + val maxIncrease by 4f.const + val directionFac by abs(dot(rayDir, normalize(origin))) + + val numSteps by 0.const + + repeat(16.const) { + numSteps += 1.const + val prevStepSize by abs(step - prevStep) + val stepPos by origin + rayDir * step + stepUv set fnProjViewPos(stepPos) + dError set fnDepthDelta(stepUv, -stepPos.z) * stepScale + `if`(abs(dError) lt step / 50f.const) { + isHit set true.const + `break`() + } + `if`((stepUv.x lt 0f.const) or (stepUv.x gt 1f.const) or (stepUv.y lt 0f.const) or (stepUv.y gt 1f.const)) { + `break`() + } + + val nextStep by clamp(dError * (0.75f.const + noise.y * 0.5f.const), -prevStepSize * maxIncrease, prevStepSize * maxIncrease) + val foregroundObjThresh by max(prevStepSize, baseDist * 0.05f.const / directionFac) + `if`(-nextStep gt foregroundObjThresh) { + prevStep set step + step += prevStepSize + }.elseIf(step + nextStep lt prevStep) { + stepScale *= 0.5f.const + }.`else` { + prevStep set step + step += nextStep + } + } + + val result by 0f.const3 + `if`(isHit) { + result set float3Value(stepUv, 1f.const) + } + result + +// texture1d("tgradient").sample(numSteps.toFloat1() / 16f.const).rgb + } + } + + val reflectionColorOut by material.outSpecular + `if`(material.inRoughness lt 0.9f.const) { + val viewPos by (camData.viewMat * float4Value(material.inFragmentPos, 1f)).xyz + val noise1 by noise43(float4Value(viewPos, camData.frameIndex.toFloat1())) + val noise2 by noise13(noise1.x) + val reflectionColor by 0f.const3 + val reflectionWeight by 0f.const + val rayDir by reflect(normalize(viewPos), viewNormal) + + val rayDir1 by normalize(rayDir + (noise1 - 0.5f.const) * material.inRoughness * 0.35f.const) + val rayResult1 by fnCastRay(viewPos, rayDir1, noise1) + `if`(rayResult1.z gt 0f.const) { + reflectionColor += oldColor.sample(rayResult1.xy).rgb * rayResult1.z + reflectionWeight += rayResult1.z + } + val rayDir2 by normalize(rayDir + (noise2 - 0.5f.const) * material.inRoughness * 0.35f.const) + val rayResult2 by fnCastRay(viewPos, rayDir2, noise2) + `if`(rayResult2.z gt 0f.const) { + reflectionColor += oldColor.sample(rayResult2.xy).rgb * rayResult2.z + reflectionWeight += rayResult2.z + } + + val roughWeight by 1f.const - smoothStep(0.85f.const, 0.9f.const, material.inRoughness) + `if`(reflectionWeight gt 0f.const) { + reflectionColorOut set mix(material.outSpecular, reflectionColor / reflectionWeight, saturate(reflectionWeight) * roughWeight) + } +// reflectionColorOut set rayResult1 + } + return reflectionColorOut * material.outSpecularFactor } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt index 04e222456..92bf145d1 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt @@ -55,7 +55,7 @@ class TemporalFilterPass( } fun swapBuffers() { - temporalShader.swapPipelineDataCapturing(filterOutput.newVal) { + temporalShader.swapPipelineData(filterOutput.newVal) { val newGbuffer = gbuffers.newVal val oldGbuffer = gbuffers.oldVal From 1486b551230bb05d427df98a18ca2b6b62996178 Mon Sep 17 00:00:00 2001 From: Max Thiele Date: Fri, 22 May 2026 19:44:21 +0200 Subject: [PATCH 11/27] Emissive materials --- .../kool/demo/deferred2/Deferred2Demo.kt | 30 +++++++-- .../fabmax/kool/demo/deferred2/GbufferPass.kt | 4 +- .../kool/demo/deferred2/LightingPass.kt | 64 +++++++++++++------ 3 files changed, 72 insertions(+), 26 deletions(-) diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt index 2e14c8c16..dbe1e4821 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt @@ -8,6 +8,7 @@ import de.fabmax.kool.math.Vec3f import de.fabmax.kool.math.deg import de.fabmax.kool.modules.gltf.GltfLoadConfig import de.fabmax.kool.modules.ksl.KslShader +import de.fabmax.kool.modules.ksl.blocks.ColorBlockConfig import de.fabmax.kool.modules.ksl.blocks.ColorSpaceConversion import de.fabmax.kool.modules.ksl.blocks.convertColorSpace import de.fabmax.kool.modules.ksl.lang.* @@ -22,6 +23,8 @@ import de.fabmax.kool.pipeline.swapPipelineData import de.fabmax.kool.scene.* import de.fabmax.kool.toString import de.fabmax.kool.util.* +import kotlin.math.ceil +import kotlin.math.round class Deferred2Demo : DemoScene("Deferred2 Demo") { @@ -69,7 +72,9 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { filterWeight.onChange { _, value -> pipeline.filterPass.filterWeight = value.toFloat() } content.apply { - val orbitCam = orbitCamera(pipeline.camera) { } + val orbitCam = orbitCamera(pipeline.camera) { + setRotation(100f, -7f) + } addNode(orbitCam) } @@ -114,6 +119,7 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { addColorMesh { generate { color = MdColor.PINK.toLinear() +// color = Color.WHITE cube { origin.set(-2.5f, 0f, 0f)} color = MdColor.LIGHT_BLUE.toLinear() cube { @@ -122,8 +128,23 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { } } shader = gbufferShader(objectId = 1) { - color { vertexColor() } + color { + vertexColor() + uniformColor(uniformName = "uBaseCol", blendMode = ColorBlockConfig.BlendMode.Multiply) + } roughness { constProperty(0.15f) } + emission { uniformProperty(10f, "uEmi") } + }.apply { + var baseColFac by bindUniformColor("uBaseCol") + var emi by bindUniformFloat1("uEmi") + onUpdate { + val str = 10f //(sin(Time.gameTime.toFloat() * 4f) + 1f) * 16f + var e = str + e = ceil(e * 4f) / 4f + val b = if (e > 0f) round(str / e * 255f) / 255f else 0f + baseColFac = Color(b, b, b) + emi = e + } } } addColorMesh { @@ -206,7 +227,7 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { } val modelMesh = teapot.meshes.values.first().apply { - transform.translate(0f, -0.5f, 5f).scale(0.5f) + transform.translate(0f, -0.5f, 5f).scale(0.5f).rotate(20f.deg, Vec3f.Y_AXIS) shader = gbufferShader(objectId = 5) { color { constColor(MdColor.LIME toneLin 500) @@ -259,11 +280,12 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { MenuSlider1("Roughness", groundRoughness.use(), 0f, 1f) { groundRoughness.set(it) } + Text("Timings".l) { sectionTitleStyle() } MenuRow { Column(width = Grow.Std) { Text("G-Buffer:") { } Text("Ambient Occlusion:") { } - Text("Lighting:") { } + Text("Lighting + Reflections:") { } Text("Filter:") { } Text("Bloom:") { } } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt index e4ef624af..2874d1876 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt @@ -26,7 +26,7 @@ class GbufferPass( ) : OffscreenPass2d( drawNode = content, attachmentConfig = AttachmentConfig { - // albedo, a * 255 = emission strength + // albedo, a * 64 = emission strength addColor(TexFormat.RGBA, filterMethod = FilterMethod.NEAREST) // metal, roughness, ao, [empty: a, flags maybe?] addColor(TexFormat.RGBA, filterMethod = FilterMethod.NEAREST) @@ -160,7 +160,7 @@ class GbufferShader(val config: GbufferShaderConfig) : KslShader("deferred2-gbuf val metallic = float1Port("metallic", fragmentPropertyBlock(config.metallicCfg).outProperty) val aoFactor = float1Port("aoFactor", fragmentPropertyBlock(config.aoCfg).outProperty) - colorOutput(float4Value(baseColor.rgb, emissionStrength), location = 0) + colorOutput(float4Value(baseColor.rgb, emissionStrength / 64f.const), location = 0) colorOutput(float4Value(metallic, roughness, aoFactor, 0f.const), location = 1) intOutput(int4Value(encodeNormalInt(normal), 0.const, 0.const, 0.const), location = 2) intOutput(int4Value(objectId.output, 0.const, 0.const, 0.const), location = 3) diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt index cad8cc2d0..bd542d5a3 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt @@ -127,7 +127,7 @@ class DeferredLightingShader(isScreenSpaceReflections: Boolean) : KslShader("def val albedoEmission by float4Var(albedoEmission.load(baseCoord, lod = 0.const)) val albedo by albedoEmission.xyz - val emission by albedoEmission.w + val emissiveStrength by albedoEmission.w * 64f.const val metalRoughnessAo by metalRoughnessAo.load(baseCoord, lod = 0.const).xyz val metallic by metalRoughnessAo.x @@ -160,7 +160,7 @@ class DeferredLightingShader(isScreenSpaceReflections: Boolean) : KslShader("def if (isScreenSpaceReflections) { val oldColor = texture2d("oldColor") val screenReflection by screenReflect(material, viewNormal, depthSmall, oldColor) - val finalColor by material.outAmbient + material.outLight + screenReflection //+ emissive + val finalColor by material.outAmbient + material.outLight + screenReflection + albedo * emissiveStrength colorOutput(finalColor) // colorOutput(screenReflection) } else { @@ -250,38 +250,62 @@ fun KslScopeBuilder.screenReflect( result set float3Value(stepUv, 1f.const) } result - -// texture1d("tgradient").sample(numSteps.toFloat1() / 16f.const).rgb } } - val reflectionColorOut by material.outSpecular + val reflectionColorOut by material.outSpecular * material.outSpecularFactor * material.inAoFactor `if`(material.inRoughness lt 0.9f.const) { val viewPos by (camData.viewMat * float4Value(material.inFragmentPos, 1f)).xyz - val noise1 by noise43(float4Value(viewPos, camData.frameIndex.toFloat1())) - val noise2 by noise13(noise1.x) val reflectionColor by 0f.const3 val reflectionWeight by 0f.const + val numHits by 0f.const + val numRays by 0f.const val rayDir by reflect(normalize(viewPos), viewNormal) - val rayDir1 by normalize(rayDir + (noise1 - 0.5f.const) * material.inRoughness * 0.35f.const) - val rayResult1 by fnCastRay(viewPos, rayDir1, noise1) - `if`(rayResult1.z gt 0f.const) { - reflectionColor += oldColor.sample(rayResult1.xy).rgb * rayResult1.z - reflectionWeight += rayResult1.z + val minColor by 1000f.const3 + val maxColor by 0f.const3 + + val noise by noise33(viewPos * (camData.frameIndex % 32.const + 1.const).toFloat1()) + repeat(3.const) { + val scatterOffset by (noise - 0.5f.const) * material.inRoughness * 0.5f.const + val scatteredRayDir by normalize(rayDir + scatterOffset) + val rayResult by fnCastRay(viewPos, scatteredRayDir, noise) + `if`(rayResult.z gt 0f.const) { + val sampleColor by oldColor.sample(rayResult.xy).rgb * rayResult.z * material.outSpecularFactor + reflectionColor += sampleColor + reflectionWeight += rayResult.z + numHits += 1f.const + minColor set min(minColor, sampleColor) + maxColor set max(maxColor, sampleColor) + }.`else` { + minColor set min(minColor, reflectionColorOut) + maxColor set max(maxColor, reflectionColorOut) + } + noise set noise13(noise.x) + numRays += 1f.const } - val rayDir2 by normalize(rayDir + (noise2 - 0.5f.const) * material.inRoughness * 0.35f.const) - val rayResult2 by fnCastRay(viewPos, rayDir2, noise2) - `if`(rayResult2.z gt 0f.const) { - reflectionColor += oldColor.sample(rayResult2.xy).rgb * rayResult2.z - reflectionWeight += rayResult2.z + + `if`(numHits gt 0f.const) { + val thresh by length(maxColor - minColor) + `while`((thresh gt 0.1f.const) and (numRays lt 6f.const)) { + val scatterOffset by (noise - 0.5f.const) * material.inRoughness * 0.5f.const + val scatteredRayDir by normalize(rayDir + scatterOffset) + val rayResult by fnCastRay(viewPos, scatteredRayDir, noise) + `if`(rayResult.z gt 0f.const) { + reflectionColor += oldColor.sample(rayResult.xy).rgb * rayResult.z * material.outSpecularFactor + reflectionWeight += rayResult.z + numHits += 1f.const + thresh -= 0.1f.const + } + noise set noise13(noise.x) + numRays += 1f.const + } } val roughWeight by 1f.const - smoothStep(0.85f.const, 0.9f.const, material.inRoughness) `if`(reflectionWeight gt 0f.const) { - reflectionColorOut set mix(material.outSpecular, reflectionColor / reflectionWeight, saturate(reflectionWeight) * roughWeight) + reflectionColorOut set mix(reflectionColorOut, reflectionColor / reflectionWeight, saturate(reflectionWeight / numRays) * roughWeight) } -// reflectionColorOut set rayResult1 } - return reflectionColorOut * material.outSpecularFactor + return reflectionColorOut } From 513864f84a00b108e56c3817d6aba976cfe46394 Mon Sep 17 00:00:00 2001 From: fabmax Date: Fri, 22 May 2026 20:24:30 +0200 Subject: [PATCH 12/27] Use demo loader resource paths --- .../kool/demo/deferred2/Deferred2Demo.kt | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt index dbe1e4821..274bf8edd 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt @@ -28,23 +28,23 @@ import kotlin.math.round class Deferred2Demo : DemoScene("Deferred2 Demo") { - val ibl by hdriImage("hdri/newport_loft.rgbe.png") -// val ibl by hdriImage("hdri/shanghai_bund_1k.rgbe.png") -// val ibl by hdriImage("hdri/circus_arena_1k.rgbe.png") -// val ibl by hdriImage("hdri/syferfontein_0d_clear_1k.rgbe.png") -// val ibl by hdriImage("hdri/colorful_studio_1k.rgbe.png") -// val ibl by hdriImage("hdri/spruit_sunrise_1k.rgbe.png") -// val ibl by hdriImage("hdri/mossy_forest_1k.rgbe.png") + val ibl by hdriImage("${DemoLoader.hdriPath}/newport_loft.rgbe.png") +// val ibl by hdriImage("${DemoLoader.hdriPath}/shanghai_bund_1k.rgbe.png") +// val ibl by hdriImage("${DemoLoader.hdriPath}/circus_arena_1k.rgbe.png") +// val ibl by hdriImage("${DemoLoader.hdriPath}/syferfontein_0d_clear_1k.rgbe.png") +// val ibl by hdriImage("${DemoLoader.hdriPath}/colorful_studio_1k.rgbe.png") +// val ibl by hdriImage("${DemoLoader.hdriPath}/spruit_sunrise_1k.rgbe.png") +// val ibl by hdriImage("${DemoLoader.hdriPath}/mossy_forest_1k.rgbe.png") - val teapot by model("models/teapot.gltf.gz", GltfLoadConfig(applyMaterials = false)) + val teapot by model("${DemoLoader.modelPath}/teapot.gltf.gz", GltfLoadConfig(applyMaterials = false)) - private val albedoMap by texture2d("materials/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_COL_2K_METALNESS.jpg") - private val normalMap by texture2d("materials/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_NRM_2K_METALNESS.jpg") - private val metallicMap by texture2d("materials/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_METALNESS_2K_METALNESS.jpg") - private val roughnessMap by texture2d("materials/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_ROUGHNESS_2K_METALNESS.jpg") - private val aoMap by texture2d("materials/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_AO_2K_METALNESS.jpg") - //private val uvChecker by texture2d("materials/uv_checker_map.jpg") - private val uvChecker by texture2d("materials/kool-test-tex.png") + private val albedoMap by texture2d("${DemoLoader.materialPath}/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_COL_2K_METALNESS.jpg") + private val normalMap by texture2d("${DemoLoader.materialPath}/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_NRM_2K_METALNESS.jpg") + private val metallicMap by texture2d("${DemoLoader.materialPath}/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_METALNESS_2K_METALNESS.jpg") + private val roughnessMap by texture2d("${DemoLoader.materialPath}/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_ROUGHNESS_2K_METALNESS.jpg") + private val aoMap by texture2d("${DemoLoader.materialPath}/MetalDesignerWeaveSteel002/MetalDesignerWeaveSteel002_AO_2K_METALNESS.jpg") +// private val uvChecker by texture2d("${DemoLoader.materialPath}/uv_checker_map.jpg") + private val uvChecker by texture2d("${DemoLoader.materialPath}/kool-test-tex.png") private lateinit var pipeline: Deferred2Pipeline private lateinit var bloomPass: BloomPass From c8af4aa5fd0a1e24002ee8b151f0781a25b1d021 Mon Sep 17 00:00:00 2001 From: Max Thiele Date: Sat, 23 May 2026 00:14:49 +0200 Subject: [PATCH 13/27] More ssr optimizations --- .../modules/ksl/blocks/PbrMaterialBlock.kt | 4 +- .../kool/demo/deferred2/Deferred2Pipeline.kt | 3 - .../kool/demo/deferred2/LightingPass.kt | 110 ++++++++---------- 3 files changed, 52 insertions(+), 65 deletions(-) diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/PbrMaterialBlock.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/PbrMaterialBlock.kt index fd05523a1..67b801eb3 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/PbrMaterialBlock.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/PbrMaterialBlock.kt @@ -103,10 +103,10 @@ class PbrMaterialBlock( val brdf by brdfLut.sample(float2Value(normalDotView, roughness)).rg outSpecular set reflectionColor - outSpecularFactor set (fAmbient * brdf.r + brdf.g) / inBaseColor.a * inAoFactor + outSpecularFactor set (fAmbient * brdf.r + brdf.g) outAmbient set kDAmbient * diffuse * inAoFactor outLight set lo - outColor set outAmbient + outLight + outSpecular * outSpecularFactor + outColor set outAmbient + outLight + outSpecular * outSpecularFactor * inAoFactor / inBaseColor.a } } } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt index f48834409..8e4991cf0 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt @@ -79,7 +79,6 @@ class Deferred2Pipeline( scene.addOffscreenPass(gbuffers.a) scene.addOffscreenPass(gbuffers.b) scene.addComputePass(aoPass) -// reflectionPass?.let { scene.addComputePass(it) } scene.addOffscreenPass(lightingPass) scene.addComputePass(filterPass) @@ -112,7 +111,6 @@ class Deferred2Pipeline( gbuffers.a.setSize(size.x, size.y) gbuffers.b.setSize(size.x, size.y) aoPass.resize(size.x, size.y) -// reflectionPass?.resize(size) lightingPass.setSize(size.x, size.y) filterPass.resize(size) resizeListeners.forEachUpdated { it(size) } @@ -135,7 +133,6 @@ class Deferred2Pipeline( lightingPass.swapBuffers() filterPass.swapBuffers() -// reflectionPass?.swapBuffers() val currentGbuffer = gbuffers.newVal aoPass.inputShader.swapPipelineData(currentGbuffer) { diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt index bd542d5a3..d7bdd6d72 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt @@ -21,7 +21,7 @@ class LightingPass( size: Vec2i, var ssaoMap: Texture2d, private val ibl: EnvironmentMap, - val pipeline: Deferred2Pipeline, + private val pipeline: Deferred2Pipeline, ) : OffscreenPass2d( drawNode = Node(), attachmentConfig = AttachmentConfig { @@ -216,10 +216,7 @@ fun KslScopeBuilder.screenReflect( val maxIncrease by 4f.const val directionFac by abs(dot(rayDir, normalize(origin))) - val numSteps by 0.const - repeat(16.const) { - numSteps += 1.const val prevStepSize by abs(step - prevStep) val stepPos by origin + rayDir * step stepUv set fnProjViewPos(stepPos) @@ -244,68 +241,61 @@ fun KslScopeBuilder.screenReflect( step += nextStep } } - - val result by 0f.const3 - `if`(isHit) { - result set float3Value(stepUv, 1f.const) - } - result + float3Value(stepUv, isHit.toFloat1()) } } - val reflectionColorOut by material.outSpecular * material.outSpecularFactor * material.inAoFactor - `if`(material.inRoughness lt 0.9f.const) { - val viewPos by (camData.viewMat * float4Value(material.inFragmentPos, 1f)).xyz - val reflectionColor by 0f.const3 - val reflectionWeight by 0f.const - val numHits by 0f.const - val numRays by 0f.const - val rayDir by reflect(normalize(viewPos), viewNormal) - - val minColor by 1000f.const3 - val maxColor by 0f.const3 - - val noise by noise33(viewPos * (camData.frameIndex % 32.const + 1.const).toFloat1()) - repeat(3.const) { - val scatterOffset by (noise - 0.5f.const) * material.inRoughness * 0.5f.const - val scatteredRayDir by normalize(rayDir + scatterOffset) - val rayResult by fnCastRay(viewPos, scatteredRayDir, noise) - `if`(rayResult.z gt 0f.const) { - val sampleColor by oldColor.sample(rayResult.xy).rgb * rayResult.z * material.outSpecularFactor - reflectionColor += sampleColor - reflectionWeight += rayResult.z - numHits += 1f.const - minColor set min(minColor, sampleColor) - maxColor set max(maxColor, sampleColor) - }.`else` { - minColor set min(minColor, reflectionColorOut) - maxColor set max(maxColor, reflectionColorOut) - } - noise set noise13(noise.x) - numRays += 1f.const - } - - `if`(numHits gt 0f.const) { - val thresh by length(maxColor - minColor) - `while`((thresh gt 0.1f.const) and (numRays lt 6f.const)) { - val scatterOffset by (noise - 0.5f.const) * material.inRoughness * 0.5f.const - val scatteredRayDir by normalize(rayDir + scatterOffset) - val rayResult by fnCastRay(viewPos, scatteredRayDir, noise) - `if`(rayResult.z gt 0f.const) { - reflectionColor += oldColor.sample(rayResult.xy).rgb * rayResult.z * material.outSpecularFactor - reflectionWeight += rayResult.z - numHits += 1f.const - thresh -= 0.1f.const - } - noise set noise13(noise.x) - numRays += 1f.const - } + val specFactor by material.outSpecularFactor + val roughFactor by material.inRoughness + val envReflectionColor by material.outSpecular * specFactor * material.inAoFactor + + val viewPos by (camData.viewMat * float4Value(material.inFragmentPos, 1f)).xyz + val reflectionWeight by 0f.const + val numRays by 0f.const + val rayDir by reflect(normalize(viewPos), viewNormal) + val noise by noise33(viewPos * (camData.frameIndex % 32.const + 1.const).toFloat1()) + + val reflectionColorOut by 0f.const3 + val minColor by 1000f.const3 + val maxColor by 0f.const3 + val initialRays by clamp((roughFactor * length(specFactor) * 20f.const).toInt1(), 1.const, 4.const) + repeat(initialRays) { + numRays += 1f.const + val scatterOffset by (noise - 0.5f.const) * roughFactor * 0.5f.const + val scatteredRayDir by normalize(rayDir + scatterOffset) + val rayResult by fnCastRay(viewPos, scatteredRayDir, noise) + `if`(rayResult.z gt 0f.const) { + val sampleColor by oldColor.sample(rayResult.xy).rgb * rayResult.z * specFactor + reflectionColorOut += sampleColor + reflectionWeight += rayResult.z + minColor set min(minColor, sampleColor) + maxColor set max(maxColor, sampleColor) + }.`else` { + reflectionColorOut += envReflectionColor + reflectionWeight += 1f.const + minColor set min(minColor, envReflectionColor) + maxColor set max(maxColor, envReflectionColor) } + noise set noise13(noise.x) + } - val roughWeight by 1f.const - smoothStep(0.85f.const, 0.9f.const, material.inRoughness) - `if`(reflectionWeight gt 0f.const) { - reflectionColorOut set mix(reflectionColorOut, reflectionColor / reflectionWeight, saturate(reflectionWeight / numRays) * roughWeight) + val thresh by length(maxColor - minColor) + `while`((thresh gt 0.1f.const) and (numRays lt 6f.const)) { + numRays += 1f.const + val scatterOffset by (noise - 0.5f.const) * roughFactor * 0.5f.const + val scatteredRayDir by normalize(rayDir + scatterOffset) + val rayResult by fnCastRay(viewPos, scatteredRayDir, noise) + `if`(rayResult.z gt 0f.const) { + reflectionColorOut += oldColor.sample(rayResult.xy).rgb * rayResult.z * specFactor + reflectionWeight += rayResult.z + thresh -= 0.1f.const + }.`else` { + reflectionColorOut += envReflectionColor + reflectionWeight += 1f.const } + noise set noise13(noise.x) } + + reflectionColorOut set reflectionColorOut / reflectionWeight return reflectionColorOut } From 295cc2841496bc1e4a9acebb37a2575b17a6a18d Mon Sep 17 00:00:00 2001 From: Max Thiele Date: Sat, 23 May 2026 19:29:56 +0200 Subject: [PATCH 14/27] Cleanup stuff --- .../kool/demo/deferred2/Deferred2Demo.kt | 6 +- .../kool/demo/deferred2/Deferred2Pipeline.kt | 73 ++++++++++++------- .../fabmax/kool/demo/deferred2/GbufferPass.kt | 8 +- .../kool/demo/deferred2/LightingPass.kt | 49 ++++++------- .../demo/deferred2/ProjectionFunctions.kt | 11 --- .../kool/demo/deferred2/TemporalFilterPass.kt | 46 +++--------- .../kool/editor/components/SsaoComponent.kt | 3 +- 7 files changed, 93 insertions(+), 103 deletions(-) diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt index 274bf8edd..aa19d8156 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt @@ -23,6 +23,7 @@ import de.fabmax.kool.pipeline.swapPipelineData import de.fabmax.kool.scene.* import de.fabmax.kool.toString import de.fabmax.kool.util.* +import kotlin.math.abs import kotlin.math.ceil import kotlin.math.round @@ -263,7 +264,10 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { } } MenuRow { - var scaleIndex by remember(2) + var scaleIndex by remember { + val initialScale = ScaleItem.items.minBy { abs(1f - it.scale * UiScale.windowScale.value) } + mutableStateOf(ScaleItem.items.indexOf(initialScale)) + } Text("Render scale".l) { labelStyle(120.dp) } ComboBox { modifier diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt index 8e4991cf0..d35715605 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt @@ -3,6 +3,7 @@ package de.fabmax.kool.demo.deferred2 import de.fabmax.kool.math.MutableMat4f import de.fabmax.kool.math.Vec2f import de.fabmax.kool.math.Vec2i +import de.fabmax.kool.modules.ksl.lang.* import de.fabmax.kool.pipeline.BufferedImageData2d import de.fabmax.kool.pipeline.TexFormat import de.fabmax.kool.pipeline.Texture2d @@ -28,17 +29,18 @@ class Deferred2Pipeline( var renderScale: Float = 1f, var tsaa: List = TSAA_4, ) { - val camera = PerspectiveCamera() val size: Vec2i get() = Vec2i( (scene.mainRenderPass.viewport.width * renderScale).toInt().coerceAtLeast(16), (scene.mainRenderPass.viewport.height * renderScale).toInt().coerceAtLeast(16) ) + val camera = PerspectiveCamera() + private val camDataBuffer = StructBuffer(DeferredCamDataLayout, 1) + val camData = camDataBuffer.asStorageBuffer() val gbuffers = AlternatingPair { val suff = if (it) "A" else "B" - GbufferPass(content, camera, size, "deferred2-gbuffer-pass-$suff", this) + GbufferPass(size, "deferred2-gbuffer-pass-$suff", this) } - val aoPass: ComputeAoPass = ComputeAoPass( camera = camera, inputDepth = gbuffers.a.depth, @@ -46,30 +48,14 @@ class Deferred2Pipeline( initialSize = size, distFormat = TexFormat.R_F32, ) + val lightingPass = LightingPass(size = size, pipeline = this) + val filterPass = TemporalFilterPass(size = size, pipeline = this) private val swapListeners = BufferedList<() -> Unit>() private val resizeListeners = BufferedList<(Vec2i) -> Unit>() - val lightingPass = LightingPass( - gbuffers = gbuffers, - camera = camera, - lighting = lighting, - size = size, - ssaoMap = aoPass.aoMap, - ibl = ibl, - pipeline = this, - ) - val filterPass = TemporalFilterPass( - lightingOutput = lightingPass.lightingOutput, - gbuffers = gbuffers, - camera = camera, - size = size, - pipeline = this, - ) - internal val prevViewProjMats = List(1024) { MutableMat4f() } - internal val oldViewProj = MutableMat4f() - private val prevViewProj = MutableMat4f() + private val oldViewProj = MutableMat4f() init { aoPass.kernelSize = 8 @@ -82,7 +68,6 @@ class Deferred2Pipeline( scene.addOffscreenPass(lightingPass) scene.addComputePass(filterPass) - gbuffers.a.isProfileGpu = true gbuffers.b.isProfileGpu = true lightingPass.isProfileGpu = true @@ -128,8 +113,18 @@ class Deferred2Pipeline( } private fun swapBuffers() { - oldViewProj.set(prevViewProj) - prevViewProj.set(camera.viewProj) + camDataBuffer.set(0) { + set(it.proj, camera.proj) + set(it.view, camera.view) + set(it.invView, camera.invView) + set(it.invViewProj, camera.invViewProj) + set(it.oldViewProj, oldViewProj) + set(it.camPosition, camera.globalPos) + set(it.camNear, camera.clipNear) + set(it.frameIdx, Time.frameCount) + } + camData.uploadData(camDataBuffer) + oldViewProj.set(camera.viewProj) lightingPass.swapBuffers() filterPass.swapBuffers() @@ -243,3 +238,31 @@ class AlternatingPair(factory: (Boolean) -> T) { object ObjModelMatLayout : Struct("obj_model_mat", MemoryLayout.Std140) { val reprojectMat = mat4("reprojectMat") } + +object DeferredCamDataLayout : Struct("deferred_cam_data", MemoryLayout.Std140) { + val proj = mat4("proj") + val view = mat4("view") + val invView = mat4("invView") + val invViewProj = mat4("invViewProj") + val oldViewProj = mat4("oldViewProj") + val camPosition = float3("camPosition") + val camNear = float1("camClipNear") + val frameIdx = int1("frameIdx") +} + +context(_: KslScopeBuilder) +val KslStructStorage.proj: KslExprMat4 get() = this[0.const][DeferredCamDataLayout.proj] +context(_: KslScopeBuilder) +val KslStructStorage.view: KslExprMat4 get() = this[0.const][DeferredCamDataLayout.view] +context(_: KslScopeBuilder) +val KslStructStorage.invView: KslExprMat4 get() = this[0.const][DeferredCamDataLayout.invView] +context(_: KslScopeBuilder) +val KslStructStorage.invViewProj: KslExprMat4 get() = this[0.const][DeferredCamDataLayout.invViewProj] +context(_: KslScopeBuilder) +val KslStructStorage.oldViewProj: KslExprMat4 get() = this[0.const][DeferredCamDataLayout.oldViewProj] +context(_: KslScopeBuilder) +val KslStructStorage.camPosition: KslExprFloat3 get() = this[0.const][DeferredCamDataLayout.camPosition] +context(_: KslScopeBuilder) +val KslStructStorage.camNear: KslExprFloat1 get() = this[0.const][DeferredCamDataLayout.camNear] +context(_: KslScopeBuilder) +val KslStructStorage.frameIdx: KslExprInt1 get() = this[0.const][DeferredCamDataLayout.frameIdx] diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt index 2874d1876..59762caa0 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt @@ -9,8 +9,6 @@ import de.fabmax.kool.modules.ksl.blocks.* import de.fabmax.kool.modules.ksl.lang.* import de.fabmax.kool.pipeline.* import de.fabmax.kool.pipeline.shading.AlphaMode -import de.fabmax.kool.scene.Camera -import de.fabmax.kool.scene.Node import de.fabmax.kool.scene.VertexLayouts import de.fabmax.kool.scene.vertexAttrib import de.fabmax.kool.util.Color @@ -18,13 +16,11 @@ import de.fabmax.kool.util.StructBuffer import de.fabmax.kool.util.asStorageBuffer class GbufferPass( - content: Node, - camera: Camera, initialSize: Vec2i, name: String, val pipeline: Deferred2Pipeline, ) : OffscreenPass2d( - drawNode = content, + drawNode = pipeline.content, attachmentConfig = AttachmentConfig { // albedo, a * 64 = emission strength addColor(TexFormat.RGBA, filterMethod = FilterMethod.NEAREST) @@ -50,7 +46,7 @@ class GbufferPass( val objModelMatsGpu = objModelMats.asStorageBuffer() init { - this.camera = camera + camera = pipeline.camera val inverseBuf = MutableMat4f() val reprojectBuf = MutableMat4f() onAfterCollectDrawCommands += { viewData -> diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt index d7bdd6d72..1009cd6c7 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt @@ -10,17 +10,14 @@ import de.fabmax.kool.modules.ksl.lang.* import de.fabmax.kool.pipeline.* import de.fabmax.kool.pipeline.FullscreenShaderUtil.fullscreenQuadVertexStage import de.fabmax.kool.pipeline.FullscreenShaderUtil.generateFullscreenQuad -import de.fabmax.kool.pipeline.ibl.EnvironmentMap -import de.fabmax.kool.scene.* +import de.fabmax.kool.scene.Mesh +import de.fabmax.kool.scene.Node +import de.fabmax.kool.scene.Skybox +import de.fabmax.kool.scene.VertexLayouts import de.fabmax.kool.util.ColorGradient class LightingPass( - val gbuffers: AlternatingPair, - camera: Camera, - lighting: Lighting?, size: Vec2i, - var ssaoMap: Texture2d, - private val ibl: EnvironmentMap, private val pipeline: Deferred2Pipeline, ) : OffscreenPass2d( drawNode = Node(), @@ -36,28 +33,29 @@ class LightingPass( private val lightingShader = DeferredLightingShader(pipeline.isScreenSpaceReflections) init { - this.camera = camera - this.lighting = lighting + camera = pipeline.camera + lighting = pipeline.lighting val outputMesh = Mesh(VertexLayouts.PositionTexCoord).apply { generateFullscreenQuad() shader = lightingShader } drawNode.addNode(outputMesh) - drawNode.addNode(Skybox.cube(ibl.reflectionMap, 2f, colorSpaceConversion = ColorSpaceConversion.AsIs)) + drawNode.addNode(Skybox.cube(pipeline.ibl.reflectionMap, 2f, colorSpaceConversion = ColorSpaceConversion.AsIs)) } fun swapBuffers() { - val newGbuffer = gbuffers.newVal + val newGbuffer = pipeline.gbuffers.newVal lightingShader.swapPipelineData(newGbuffer) { depthTex = newGbuffer.depth depthSmall = pipeline.aoPass.scaledDists encodedNormals = newGbuffer.normals albedoEmissionTex = newGbuffer.albedoEmission metalRoughnessAoTex = newGbuffer.metalRoughnessAo - irradianceMap = ibl.irradianceMap - reflectionMap = ibl.reflectionMap - aoMap = ssaoMap + irradianceMap = pipeline.ibl.irradianceMap + reflectionMap = pipeline.ibl.reflectionMap + aoMap = pipeline.aoPass.aoMap + camData = pipeline.camData oldColor = pipeline.filterPass.filterOutput.oldVal } } @@ -73,6 +71,7 @@ class DeferredLightingShader(isScreenSpaceReflections: Boolean) : KslShader("def var reflectionMap by bindTextureCube("reflection") var brdf by bindTexture2d("brdf", KoolSystem.requireContext().defaultPbrBrdfLut) var aoMap by bindTexture2d("aoMap") + var camData by bindStorage("camData") var oldColor by bindTexture2d("oldColor") @@ -104,7 +103,8 @@ class DeferredLightingShader(isScreenSpaceReflections: Boolean) : KslShader("def val aoMap = texture2d("aoMap") val ambientOri = uniformMat3("uAmbientTextureOri") - val camData = cameraData() + val camDataLayout = struct(DeferredCamDataLayout) + val camData = storage("camData", camDataLayout) main { val baseCoord by (uv.output * depth.size().toFloat2()).toInt2() @@ -113,10 +113,10 @@ class DeferredLightingShader(isScreenSpaceReflections: Boolean) : KslShader("def discard() } - val camNear by camData.clipNear - val camPos by camData.position - val invView by camData.invViewMat - val invViewProj by camData.invViewProjMat + val camNear by camData.camNear + val camPos by camData.camPosition + val invView by camData.invView + val invViewProj by camData.invViewProj val size by depth.size() val worldPos by unprojectBaseCoord(depthSample, baseCoord, size, camNear, invViewProj).xyz val ssao by aoMap.load(baseCoord, lod = 0.const).x @@ -159,7 +159,7 @@ class DeferredLightingShader(isScreenSpaceReflections: Boolean) : KslShader("def if (isScreenSpaceReflections) { val oldColor = texture2d("oldColor") - val screenReflection by screenReflect(material, viewNormal, depthSmall, oldColor) + val screenReflection by screenReflect(material, viewNormal, depthSmall, oldColor, camData) val finalColor by material.outAmbient + material.outLight + screenReflection + albedo * emissiveStrength colorOutput(finalColor) // colorOutput(screenReflection) @@ -179,13 +179,12 @@ fun KslScopeBuilder.screenReflect( viewNormal: KslExprFloat3, viewDists: KslUniform, oldColor: KslUniform, + camData: KslStructStorage ): KslExprFloat3 { - val camData = cameraData() - val fnProjViewPos = functionFloat2("fnProiViewPos") { val viewPos = paramFloat3("viewPos") body { - val p by camData.projMat * float4Value(viewPos, 1f) + val p by camData.proj * float4Value(viewPos, 1f) p.xy / p.w * float2Value(0.5f, -0.5f) + 0.5f.const } } @@ -249,11 +248,11 @@ fun KslScopeBuilder.screenReflect( val roughFactor by material.inRoughness val envReflectionColor by material.outSpecular * specFactor * material.inAoFactor - val viewPos by (camData.viewMat * float4Value(material.inFragmentPos, 1f)).xyz + val viewPos by (camData.view * float4Value(material.inFragmentPos, 1f)).xyz val reflectionWeight by 0f.const val numRays by 0f.const val rayDir by reflect(normalize(viewPos), viewNormal) - val noise by noise33(viewPos * (camData.frameIndex % 32.const + 1.const).toFloat1()) + val noise by noise33(viewPos * (camData.frameIdx % 64.const + 1.const).toFloat1()) val reflectionColorOut by 0f.const3 val minColor by 1000f.const3 diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ProjectionFunctions.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ProjectionFunctions.kt index 53fe0a472..dae312b28 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ProjectionFunctions.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ProjectionFunctions.kt @@ -64,14 +64,3 @@ class UnprojectUv( } } } - -fun KslScopeBuilder.unprojectUv( - depth: KslUniform, - uv: KslExprFloat2, - camNear: KslExprFloat1, - invProj: KslExprMat4, - invView: KslExprMat4, -): KslExprFloat4 { - val func = parentStage.getOrCreateFunction("fnUnprojectUv") { UnprojectUv(depth, this) } - return func(uv, camNear, invProj, invView) -} \ No newline at end of file diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt index 92bf145d1..78fa2ba70 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt @@ -8,15 +8,8 @@ import de.fabmax.kool.modules.ksl.blocks.convertColorSpace import de.fabmax.kool.modules.ksl.blocks.getLinearDepthReversed import de.fabmax.kool.modules.ksl.lang.* import de.fabmax.kool.pipeline.* -import de.fabmax.kool.scene.Camera -import de.fabmax.kool.util.MemoryLayout -import de.fabmax.kool.util.Struct -import de.fabmax.kool.util.Time class TemporalFilterPass( - val lightingOutput: Texture2d, - val gbuffers: AlternatingPair, - val camera: Camera, val pipeline: Deferred2Pipeline, private var size: Vec2i, filterStorageFmt: TexFormat = TexFormat.RGBA_F16, @@ -26,7 +19,7 @@ class TemporalFilterPass( } val filterState = StorageTexture2d(size.x, size.y, TexFormat.R, samplerSettings = SamplerSettings().clamped().nearest()) - private val temporalShader = TemporalFilterShader(filterStorageFmt, lightingOutput, filterState) + private val temporalShader = TemporalFilterShader(filterStorageFmt, pipeline.lightingPass.lightingOutput, filterState) var filterWeight = 8f @@ -56,8 +49,8 @@ class TemporalFilterPass( fun swapBuffers() { temporalShader.swapPipelineData(filterOutput.newVal) { - val newGbuffer = gbuffers.newVal - val oldGbuffer = gbuffers.oldVal + val newGbuffer = pipeline.gbuffers.newVal + val oldGbuffer = pipeline.gbuffers.oldVal newDepth = newGbuffer.depth oldDepth = oldGbuffer.depth @@ -65,15 +58,10 @@ class TemporalFilterPass( newMeta = newGbuffer.objectIds oldFilter = filterOutput.oldVal newFilter = filterOutput.newVal - frameI = Time.frameCount filterW = filterWeight objModelMats = newGbuffer.objModelMatsGpu - camData.set { - set(it.invViewProj, camera.invViewProj) - set(it.camNear, camera.clipNear) - set(it.oldViewProj, pipeline.oldViewProj) - } + camData = pipeline.camData } } } @@ -88,15 +76,12 @@ class TemporalFilterShader( var newMeta by bindTexture2d("newMeta") var newDepth by bindTexture2d("newDepth") var oldDepth by bindTexture2d("oldDepth") - - var objModelMats by bindStorage("objModelMats") - var camData = bindUniformStruct("camData", FilterCamDataStruct) - var oldFilter by bindTexture2d("oldFilter", defaultSampler = SamplerSettings().clamped().linear()) var newFilter by bindStorageTexture2d("newFilter") var filterState by bindStorageTexture2d("filterState", filterState) - var frameI by bindUniformInt1("frameI") + var objModelMats by bindStorage("objModelMats") + var camData by bindStorage("camData") var filterW by bindUniformFloat1("uFilterWeight") init { @@ -122,8 +107,8 @@ class TemporalFilterShader( } val filterState = storageTexture2d("filterState", TexFormat.R) -// val frameI = uniformInt1("frameI") - val camData = uniformStruct("camData", FilterCamDataStruct) + val camDataLayout = struct(DeferredCamDataLayout) + val camData = storage("camData", camDataLayout) val filterWeight = uniformFloat1("uFilterWeight") @@ -134,10 +119,10 @@ class TemporalFilterShader( val size by newDepth.size() val sizeF by size.toFloat2() - val camNear = camData[FilterCamDataStruct.camNear] - val invViewProj = camData[FilterCamDataStruct.invViewProj] + val near by camData.camNear + val invViewProj by camData.invViewProj val depth by newDepth.load(baseCoord).x - val worldPos by unprojectBaseCoord(depth, baseCoord, size, camNear, invViewProj) + val worldPos by unprojectBaseCoord(depth, baseCoord, size, near, invViewProj) val oldUv by 0f.const2 val oldBaseCoord by baseCoord @@ -147,7 +132,7 @@ class TemporalFilterShader( oldUv set oldProj.xy / oldProj.w * float2Value(0.5f, -0.5f) + 0.5f.const oldBaseCoord set (oldUv * sizeF).toInt2() }.`else` { - val oldProj by camData[FilterCamDataStruct.oldViewProj] * worldPos + val oldProj by camData.oldViewProj * worldPos oldUv set oldProj.xy / oldProj.w * float2Value(0.5f, -0.5f) + 0.5f.const oldBaseCoord set (oldUv * sizeF).toInt2() } @@ -160,7 +145,6 @@ class TemporalFilterShader( (filterState.load((oldStateBaseUv + float2Value(-0.5f, 0.5f)).toInt2()).r * 255f.const).toInt1() val wasEdge by oldState and 1.const gt 0.const - val near by camData[FilterCamDataStruct.camNear] val refDepth by getLinearDepthReversed(depth, near) val depthA by getLinearDepthReversed(newDepth.load(baseCoord + int2Value(1, 1)).x, near) val depthB by getLinearDepthReversed(newDepth.load(baseCoord + int2Value(-1, -1)).x, near) @@ -239,9 +223,3 @@ class TemporalFilterShader( } } } - -object FilterCamDataStruct : Struct("FilterCamData", MemoryLayout.Std140) { - val invViewProj = mat4("invViewProj") - val oldViewProj = mat4("oldViewProj") - val camNear = float1("camClipNear") -} diff --git a/kool-editor-model/src/commonMain/kotlin/de/fabmax/kool/editor/components/SsaoComponent.kt b/kool-editor-model/src/commonMain/kotlin/de/fabmax/kool/editor/components/SsaoComponent.kt index 11827870c..29f78ec30 100644 --- a/kool-editor-model/src/commonMain/kotlin/de/fabmax/kool/editor/components/SsaoComponent.kt +++ b/kool-editor-model/src/commonMain/kotlin/de/fabmax/kool/editor/components/SsaoComponent.kt @@ -6,6 +6,7 @@ import de.fabmax.kool.editor.data.ComponentInfo import de.fabmax.kool.editor.data.SsaoComponentData import de.fabmax.kool.pipeline.Texture2d import de.fabmax.kool.pipeline.ao.AoPipeline +import de.fabmax.kool.pipeline.ao.AoRadius import de.fabmax.kool.pipeline.ao.ForwardAoPipeline import de.fabmax.kool.pipeline.ao.LegacyAoPipeline import de.fabmax.kool.scene.Camera @@ -61,7 +62,7 @@ class SsaoComponent( aoPipeline?.apply { (this as? LegacyAoPipeline)?.mapSize = ssaoSettings.mapSize kernelSize = ssaoSettings.samples - radius = ssaoSettings.radius * radiusSign + radius = AoRadius(ssaoSettings.radius * radiusSign) strength = ssaoSettings.strength falloff = ssaoSettings.power } From 38b7460d02a2b61d7d5b778a0c9fd3fbb1d38707 Mon Sep 17 00:00:00 2001 From: Max Thiele Date: Sun, 24 May 2026 13:18:02 +0200 Subject: [PATCH 15/27] Do reprojection matrix computations in a compute pass --- .../kool/demo/deferred2/Deferred2Demo.kt | 26 ++- .../kool/demo/deferred2/Deferred2Pipeline.kt | 21 +-- .../fabmax/kool/demo/deferred2/GbufferPass.kt | 24 +-- .../kool/demo/deferred2/LightingPass.kt | 14 +- .../demo/deferred2/ReprojectComputePass.kt | 148 ++++++++++++++++++ .../kool/demo/deferred2/TemporalFilterPass.kt | 36 +---- 6 files changed, 193 insertions(+), 76 deletions(-) create mode 100644 kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ReprojectComputePass.kt diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt index aa19d8156..d0d3c0900 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt @@ -53,8 +53,8 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { private val groundRoughness = mutableStateOf(0.5f) private val bloom = mutableStateOf(true) - private val gpuTimes = mutableStateOf(GpuTimes(0.0, 0.0, 0.0, 0.0, 0.0)) - private var gpuTimesAccu = GpuTimes(0.0, 0.0, 0.0, 0.0, 0.0) + private val gpuTimes = mutableStateOf(GpuTimes()) + private var gpuTimesAccu = GpuTimes() override fun Scene.setupMainScene(ctx: KoolContext) { val content = deferredContent() @@ -100,11 +100,12 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { val nAccu = 50 onUpdate { gpuTimesAccu = GpuTimes( - gpuTimesAccu.gbuffer + pipeline.gbuffers.a.tGpu.inWholeMicroseconds / 1000.0 / nAccu, - gpuTimesAccu.ao + pipeline.aoPass.tGpu.inWholeMicroseconds / 1000.0 / nAccu, - gpuTimesAccu.lighting + pipeline.lightingPass.tGpu.inWholeMicroseconds / 1000.0 / nAccu, - gpuTimesAccu.filter + pipeline.filterPass.tGpu.inWholeMicroseconds / 1000.0 / nAccu, - gpuTimesAccu.bloom + bloomPass.tGpu.inWholeMicroseconds / 1000.0 / nAccu, + reproj = gpuTimesAccu.reproj + pipeline.reprojectMatrixComputePass.tGpu.inWholeMicroseconds / 1000.0 / nAccu, + gbuffer = gpuTimesAccu.gbuffer + pipeline.gbuffers.a.tGpu.inWholeMicroseconds / 1000.0 / nAccu, + ao = gpuTimesAccu.ao + pipeline.aoPass.tGpu.inWholeMicroseconds / 1000.0 / nAccu, + lighting = gpuTimesAccu.lighting + pipeline.lightingPass.tGpu.inWholeMicroseconds / 1000.0 / nAccu, + filter = gpuTimesAccu.filter + pipeline.filterPass.tGpu.inWholeMicroseconds / 1000.0 / nAccu, + bloom = gpuTimesAccu.bloom + bloomPass.tGpu.inWholeMicroseconds / 1000.0 / nAccu, ) if (Time.frameCount % nAccu == 0) { gpuTimes.set(gpuTimesAccu) @@ -287,6 +288,7 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { Text("Timings".l) { sectionTitleStyle() } MenuRow { Column(width = Grow.Std) { + Text("Reproject Matrices:") { } Text("G-Buffer:") { } Text("Ambient Occlusion:") { } Text("Lighting + Reflections:") { } @@ -295,6 +297,7 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { } Column { val t = gpuTimes.use() + Text("${t.reproj.toString(2)} ms") { } Text("${t.gbuffer.toString(2)} ms") { } Text("${t.ao.toString(2)} ms") { } Text("${t.lighting.toString(2)} ms") { } @@ -371,4 +374,11 @@ private data class ScaleItem(val label: String, val scale: Float) { } } -private data class GpuTimes(val gbuffer: Double, val ao: Double, val lighting: Double, val filter: Double, val bloom: Double) +private data class GpuTimes( + val reproj: Double = 0.0, + val gbuffer: Double = 0.0, + val ao: Double = 0.0, + val lighting: Double = 0.0, + val filter: Double = 0.0, + val bloom: Double = 0.0, +) diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt index d35715605..067591e18 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt @@ -37,6 +37,8 @@ class Deferred2Pipeline( private val camDataBuffer = StructBuffer(DeferredCamDataLayout, 1) val camData = camDataBuffer.asStorageBuffer() + val reprojectMatrixComputePass = ReprojectComputePass() + val gbuffers = AlternatingPair { val suff = if (it) "A" else "B" GbufferPass(size, "deferred2-gbuffer-pass-$suff", this) @@ -54,26 +56,27 @@ class Deferred2Pipeline( private val swapListeners = BufferedList<() -> Unit>() private val resizeListeners = BufferedList<(Vec2i) -> Unit>() - internal val prevViewProjMats = List(1024) { MutableMat4f() } - private val oldViewProj = MutableMat4f() - init { aoPass.kernelSize = 8 aoPass.radius = AoRadius.relativeRadius(1 / 20f) aoPass.temporalKernels = tsaa.size + scene.addComputePass(reprojectMatrixComputePass) scene.addOffscreenPass(gbuffers.a) scene.addOffscreenPass(gbuffers.b) scene.addComputePass(aoPass) scene.addOffscreenPass(lightingPass) scene.addComputePass(filterPass) + reprojectMatrixComputePass.isProfileGpu = true gbuffers.a.isProfileGpu = true gbuffers.b.isProfileGpu = true lightingPass.isProfileGpu = true filterPass.isProfileGpu = true aoPass.isProfileGpu = true + lightingPass.onRelease { camData.release() } + val offsetMat = MutableMat4f() camera.onCameraUpdated += { val tsaa = tsaa @@ -118,27 +121,23 @@ class Deferred2Pipeline( set(it.view, camera.view) set(it.invView, camera.invView) set(it.invViewProj, camera.invViewProj) - set(it.oldViewProj, oldViewProj) + set(it.oldViewProj, reprojectMatrixComputePass.uploadData.oldVal.viewProjMat) set(it.camPosition, camera.globalPos) set(it.camNear, camera.clipNear) set(it.frameIdx, Time.frameCount) } camData.uploadData(camDataBuffer) - oldViewProj.set(camera.viewProj) + reprojectMatrixComputePass.swapBuffers() lightingPass.swapBuffers() filterPass.swapBuffers() - val currentGbuffer = gbuffers.newVal aoPass.inputShader.swapPipelineData(currentGbuffer) { aoPass.inputDepth = currentGbuffer.depth aoPass.inputNormals = currentGbuffer.normals } - swapListeners.forEachUpdated { it() } - gbuffers.newVal.objModelMatsGpu.uploadData(gbuffers.newVal.objModelMats) - // this is called after update, newVal was enabled and updated, disable it and enable oldVal for next frame gbuffers.newVal.isEnabled = false gbuffers.oldVal.isEnabled = true @@ -235,10 +234,6 @@ class AlternatingPair(factory: (Boolean) -> T) { val oldVal: T get() = if (Time.frameCount % 2 == 0) b else a } -object ObjModelMatLayout : Struct("obj_model_mat", MemoryLayout.Std140) { - val reprojectMat = mat4("reprojectMat") -} - object DeferredCamDataLayout : Struct("deferred_cam_data", MemoryLayout.Std140) { val proj = mat4("proj") val view = mat4("view") diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt index 59762caa0..fbe597fd0 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt @@ -1,6 +1,5 @@ package de.fabmax.kool.demo.deferred2 -import de.fabmax.kool.math.MutableMat4f import de.fabmax.kool.math.Vec2i import de.fabmax.kool.modules.ksl.BasicVertexConfig import de.fabmax.kool.modules.ksl.KslShader @@ -12,8 +11,6 @@ import de.fabmax.kool.pipeline.shading.AlphaMode import de.fabmax.kool.scene.VertexLayouts import de.fabmax.kool.scene.vertexAttrib import de.fabmax.kool.util.Color -import de.fabmax.kool.util.StructBuffer -import de.fabmax.kool.util.asStorageBuffer class GbufferPass( initialSize: Vec2i, @@ -39,32 +36,21 @@ class GbufferPass( val metalRoughnessAo get() = colorTextures[1] val normals get() = colorTextures[2] val objectIds get() = colorTextures[3] - val depth get() = depthTexture!! - val objModelMats = StructBuffer(ObjModelMatLayout, 1024) - val objModelMatsGpu = objModelMats.asStorageBuffer() - init { camera = pipeline.camera - val inverseBuf = MutableMat4f() - val reprojectBuf = MutableMat4f() onAfterCollectDrawCommands += { viewData -> + val upload = pipeline.reprojectMatrixComputePass.uploadData.newVal + upload.viewProjMat.set(viewData.drawQueue.viewProjMatF) + viewData.drawQueue.forEach { cmd -> (cmd.mesh.shader as? GbufferShader)?.let { gbufferShader -> - val id = gbufferShader.objectId - objModelMats.set(id) { - val prevViewProj = pipeline.prevViewProjMats[id] - inverseBuf.set(cmd.modelMatF).invert() - reprojectBuf.set(prevViewProj).mul(inverseBuf) - set(it.reprojectMat, reprojectBuf) - prevViewProj.set(viewData.drawQueue.viewProjMatF).mul(cmd.modelMatF) - } + upload.modelMats.position = gbufferShader.objectId * 16 + cmd.modelMatF.putTo(upload.modelMats) } } } - - onRelease { objModelMatsGpu.release() } } } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt index 1009cd6c7..d9f0a9a7d 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt @@ -48,7 +48,7 @@ class LightingPass( val newGbuffer = pipeline.gbuffers.newVal lightingShader.swapPipelineData(newGbuffer) { depthTex = newGbuffer.depth - depthSmall = pipeline.aoPass.scaledDists + scaledViewZ = pipeline.aoPass.scaledDists encodedNormals = newGbuffer.normals albedoEmissionTex = newGbuffer.albedoEmission metalRoughnessAoTex = newGbuffer.metalRoughnessAo @@ -63,7 +63,7 @@ class LightingPass( class DeferredLightingShader(isScreenSpaceReflections: Boolean) : KslShader("deferred2-lighting") { var depthTex by bindTexture2d("depth") - var depthSmall by bindTexture2d("depthSmall") + var scaledViewZ by bindTexture2d("scaledViewZ") var encodedNormals by bindTexture2d("encodedNormals") var albedoEmissionTex by bindTexture2d("albedoEmission") var metalRoughnessAoTex by bindTexture2d("metalRoughnessAo") @@ -93,7 +93,7 @@ class DeferredLightingShader(isScreenSpaceReflections: Boolean) : KslShader("def fullscreenQuadVertexStage(uv) fragmentStage { val depth = texture2d("depth", isUnfilterable = true) - val depthSmall = texture2d("depthSmall", isUnfilterable = true) + val scaledViewZ = texture2d("scaledViewZ", isUnfilterable = true) val encodedNormals = texture2dInt("encodedNormals") val albedoEmission = texture2d("albedoEmission") val metalRoughnessAo = texture2d("metalRoughnessAo") @@ -159,7 +159,7 @@ class DeferredLightingShader(isScreenSpaceReflections: Boolean) : KslShader("def if (isScreenSpaceReflections) { val oldColor = texture2d("oldColor") - val screenReflection by screenReflect(material, viewNormal, depthSmall, oldColor, camData) + val screenReflection by screenReflect(material, viewNormal, scaledViewZ, oldColor, camData) val finalColor by material.outAmbient + material.outLight + screenReflection + albedo * emissiveStrength colorOutput(finalColor) // colorOutput(screenReflection) @@ -177,7 +177,7 @@ context(_: KslProgram, _: KslShaderStage) fun KslScopeBuilder.screenReflect( material: PbrMaterialBlock, viewNormal: KslExprFloat3, - viewDists: KslUniform, + viewZ: KslUniform, oldColor: KslUniform, camData: KslStructStorage ): KslExprFloat3 { @@ -193,9 +193,9 @@ fun KslScopeBuilder.screenReflect( val uv = paramFloat2("uv") val refDepth = paramFloat1("refDepth") body { - val texSz by viewDists.size().toFloat2() + val texSz by viewZ.size().toFloat2() val uvi by (uv * texSz).toInt2() - viewDists.load(uvi, lod = 0.const).x - refDepth + viewZ.load(uvi, lod = 0.const).x - refDepth } } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ReprojectComputePass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ReprojectComputePass.kt new file mode 100644 index 000000000..86dad464c --- /dev/null +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ReprojectComputePass.kt @@ -0,0 +1,148 @@ +package de.fabmax.kool.demo.deferred2 + +import de.fabmax.kool.math.MutableMat4f +import de.fabmax.kool.math.Vec3i +import de.fabmax.kool.modules.ksl.KslComputeShader +import de.fabmax.kool.modules.ksl.lang.* +import de.fabmax.kool.pipeline.* +import de.fabmax.kool.util.Float32Buffer +import de.fabmax.kool.util.MemoryLayout +import de.fabmax.kool.util.Struct + +class ReprojectComputePass( + val maxObjects: Int = 65536 +) : ComputePass("deferred2-reproject-compute-pass") { + + val uploadData = AlternatingPair { + UploadData(maxObjects) + } + + val modelMats = AlternatingPair { + StorageBuffer(GpuType.Mat4, maxObjects) + } + val reprojectMats = StorageBuffer(GpuType.Mat4, maxObjects) + + private val shader = ReprojectComputeShader(reprojectMats) + + init { + val groupsX = (maxObjects + 63) / 64 + addTask(shader, Vec3i(groupsX, 1, 1)) + onRelease { + modelMats.a.release() + modelMats.b.release() + reprojectMats.release() + } + } + + fun swapBuffers() { + val newUpload = uploadData.newVal + modelMats.newVal.uploadData(newUpload.modelMats) + shader.swapPipelineData(newUpload) { + shader.inputOldViewProj = uploadData.oldVal.viewProjMat + shader.oldModelMats = modelMats.oldVal + shader.newModelMats = modelMats.newVal + } + } +} + +private fun UploadData(size: Int) = UploadData(Float32Buffer(size * 16), MutableMat4f()) + +class UploadData(val modelMats: Float32Buffer, val viewProjMat: MutableMat4f) + +private class ReprojectComputeShader( + reprojectMats: GpuBuffer, +) : KslComputeShader("reproject-compute") { + + var inputOldViewProj by bindUniformMat4("oldViewProj") + var oldModelMats by bindStorage("oldModelMats") + var newModelMats by bindStorage("newModelMats") + + init { + bindStorage("reprojectMats", reprojectMats) + program.program() + } + + private fun KslProgram.program() { + dumpCode = true + computeStage(workGroupSizeX = 64) { + val oldViewProj = uniformMat4("oldViewProj") + val matStruct = struct(StorageMatLayout) + val oldModelMats = storage("oldModelMats", matStruct) + val newModelMats = storage("newModelMats", matStruct) + val reprojectMats = storage("reprojectMats", matStruct) + + main { + val idx by inGlobalInvocationId.x.toInt1() + + val model = mat4Var(newModelMats[idx][StorageMatLayout.mat]) + val det by model.run { + m03*m12*m21*m30 - m02*m13*m21*m30 - m03*m11*m22*m30 + m01*m13*m22*m30 + + m02*m11*m23*m30 - m01*m12*m23*m30 - m03*m12*m20*m31 + m02*m13*m20*m31 + + m03*m10*m22*m31 - m00*m13*m22*m31 - m02*m10*m23*m31 + m00*m12*m23*m31 + + m03*m11*m20*m32 - m01*m13*m20*m32 - m03*m10*m21*m32 + m00*m13*m21*m32 + + m01*m10*m23*m32 - m00*m11*m23*m32 - m02*m11*m20*m33 + m01*m12*m20*m33 + + m02*m10*m21*m33 - m00*m12*m21*m33 - m01*m10*m22*m33 + m00*m11*m22*m33 + } + `if`(det eq 0f.const) { + `return`() + } + + val inverseModelMat by model.run { + val r00 by m12*m23*m31 - m13*m22*m31 + m13*m21*m32 - m11*m23*m32 - m12*m21*m33 + m11*m22*m33 + val r01 by m03*m22*m31 - m02*m23*m31 - m03*m21*m32 + m01*m23*m32 + m02*m21*m33 - m01*m22*m33 + val r02 by m02*m13*m31 - m03*m12*m31 + m03*m11*m32 - m01*m13*m32 - m02*m11*m33 + m01*m12*m33 + val r03 by m03*m12*m21 - m02*m13*m21 - m03*m11*m22 + m01*m13*m22 + m02*m11*m23 - m01*m12*m23 + val r10 by m13*m22*m30 - m12*m23*m30 - m13*m20*m32 + m10*m23*m32 + m12*m20*m33 - m10*m22*m33 + val r11 by m02*m23*m30 - m03*m22*m30 + m03*m20*m32 - m00*m23*m32 - m02*m20*m33 + m00*m22*m33 + val r12 by m03*m12*m30 - m02*m13*m30 - m03*m10*m32 + m00*m13*m32 + m02*m10*m33 - m00*m12*m33 + val r13 by m02*m13*m20 - m03*m12*m20 + m03*m10*m22 - m00*m13*m22 - m02*m10*m23 + m00*m12*m23 + val r20 by m11*m23*m30 - m13*m21*m30 + m13*m20*m31 - m10*m23*m31 - m11*m20*m33 + m10*m21*m33 + val r21 by m03*m21*m30 - m01*m23*m30 - m03*m20*m31 + m00*m23*m31 + m01*m20*m33 - m00*m21*m33 + val r22 by m01*m13*m30 - m03*m11*m30 + m03*m10*m31 - m00*m13*m31 - m01*m10*m33 + m00*m11*m33 + val r23 by m03*m11*m20 - m01*m13*m20 - m03*m10*m21 + m00*m13*m21 + m01*m10*m23 - m00*m11*m23 + val r30 by m12*m21*m30 - m11*m22*m30 - m12*m20*m31 + m10*m22*m31 + m11*m20*m32 - m10*m21*m32 + val r31 by m01*m22*m30 - m02*m21*m30 + m02*m20*m31 - m00*m22*m31 - m01*m20*m32 + m00*m21*m32 + val r32 by m02*m11*m30 - m01*m12*m30 - m02*m10*m31 + m00*m12*m31 + m01*m10*m32 - m00*m11*m32 + val r33 by m01*m12*m20 - m02*m11*m20 + m02*m10*m21 - m00*m12*m21 - m01*m10*m22 + m00*m11*m22 + + val s by 1f.const / det + mat4Value( + col0 = float4Value(r00 * s, r10 * s, r20 * s, r30 * s), + col1 = float4Value(r01 * s, r11 * s, r21 * s, r31 * s), + col2 = float4Value(r02 * s, r12 * s, r22 * s, r32 * s), + col3 = float4Value(r03 * s, r13 * s, r23 * s, r33 * s), + ) + } + + val oldMvp by oldViewProj * oldModelMats[idx][StorageMatLayout.mat] + val r = structVar(matStruct) + r[StorageMatLayout.mat] set oldMvp * inverseModelMat + reprojectMats[idx] = r + } + } + } +} + +private val KslExprMat4.m00: KslExprFloat1 get() = this[0].x +private val KslExprMat4.m10: KslExprFloat1 get() = this[0].y +private val KslExprMat4.m20: KslExprFloat1 get() = this[0].z +private val KslExprMat4.m30: KslExprFloat1 get() = this[0].w + +private val KslExprMat4.m01: KslExprFloat1 get() = this[1].x +private val KslExprMat4.m11: KslExprFloat1 get() = this[1].y +private val KslExprMat4.m21: KslExprFloat1 get() = this[1].z +private val KslExprMat4.m31: KslExprFloat1 get() = this[1].w + +private val KslExprMat4.m02: KslExprFloat1 get() = this[2].x +private val KslExprMat4.m12: KslExprFloat1 get() = this[2].y +private val KslExprMat4.m22: KslExprFloat1 get() = this[2].z +private val KslExprMat4.m32: KslExprFloat1 get() = this[2].w + +private val KslExprMat4.m03: KslExprFloat1 get() = this[3].x +private val KslExprMat4.m13: KslExprFloat1 get() = this[3].y +private val KslExprMat4.m23: KslExprFloat1 get() = this[3].z +private val KslExprMat4.m33: KslExprFloat1 get() = this[3].w + +object StorageMatLayout : Struct("mat_storage", MemoryLayout.Std140) { + val mat = mat4("mat") +} diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt index 78fa2ba70..6cbea515f 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt @@ -60,7 +60,7 @@ class TemporalFilterPass( newFilter = filterOutput.newVal filterW = filterWeight - objModelMats = newGbuffer.objModelMatsGpu + reprojectMats = pipeline.reprojectMatrixComputePass.reprojectMats camData = pipeline.camData } } @@ -80,7 +80,7 @@ class TemporalFilterShader( var newFilter by bindStorageTexture2d("newFilter") var filterState by bindStorageTexture2d("filterState", filterState) - var objModelMats by bindStorage("objModelMats") + var reprojectMats by bindStorage("reprojectMats") var camData by bindStorage("camData") var filterW by bindUniformFloat1("uFilterWeight") @@ -95,18 +95,16 @@ class TemporalFilterShader( val newMeta = texture2dInt("newMeta") val newDepth = texture2d("newDepth", isUnfilterable = true) val oldDepth = texture2d("oldDepth", isUnfilterable = true) - - val invModelMatStruct = struct(ObjModelMatLayout) - val objModelMats = storage("objModelMats", invModelMatStruct) - val oldFilter = texture2d("oldFilter") val newFilter = if (filterStorageFmt.channels == 3) { storageTexture2d("newFilter", filterStorageFmt) } else { storageTexture2d("newFilter", filterStorageFmt) } - val filterState = storageTexture2d("filterState", TexFormat.R) + + val matStruct = struct(StorageMatLayout) + val reprojectMats = storage("reprojectMats", matStruct) val camDataLayout = struct(DeferredCamDataLayout) val camData = storage("camData", camDataLayout) @@ -127,8 +125,8 @@ class TemporalFilterShader( val oldUv by 0f.const2 val oldBaseCoord by baseCoord `if`(id ne 0.const) { - val objModelMat = structVar(objModelMats[id]) - val oldProj by objModelMat[ObjModelMatLayout.reprojectMat] * worldPos + val reprojectMat = mat4Var(reprojectMats[id][StorageMatLayout.mat]) + val oldProj by reprojectMat * worldPos oldUv set oldProj.xy / oldProj.w * float2Value(0.5f, -0.5f) + 0.5f.const oldBaseCoord set (oldUv * sizeF).toInt2() }.`else` { @@ -182,13 +180,6 @@ class TemporalFilterShader( oldState set isEdge.toInt1() filterState.store(baseCoord, float4Value(oldState.toFloat1() / 255f.const, 0f.const, 0f.const, 0f.const)) -// val filterNoise by noise31(uint3Value(inGlobalInvocationId.xy, frameI.toUint1())) -// val w by 4f.const -// val w by 8f.const //- filterNoise * filterNoise * filterNoise * 4f.const -// val w by 16f.const //- filterNoise * filterNoise * filterNoise * 14f.const -// val w by 32f.const - filterNoise * filterNoise * filterNoise * 16f.const -// val w by 100f.const - filterNoise * filterNoise * filterNoise * 75f.const - val w by filterWeight val isReprojectOutOfScreen by (oldUv.x lt 0f.const) or (oldUv.y lt 0f.const) or (oldUv.x gt 1f.const) or (oldUv.y gt 1f.const) `if`((!filterHit and !isEdge) or (wasEdge and !isEdge) or isReprojectOutOfScreen) { @@ -205,19 +196,6 @@ class TemporalFilterShader( `if`(any(isNan(filtered))) { filtered set curSrgb } - -// `if`(wasEdge and !isEdge) { -// filtered set Color.RED.const.rgb -// } -// `if`(isEdge) { -// filtered set Color.YELLOW.const.rgb -// } -// `if`(!filterHit) { -// filtered set Color.CYAN.const.rgb -// } -// `if`(w eq 0f.const) { -// newFilter[baseCoord] = Color.CYAN.const -// } newFilter[baseCoord] = float4Value(filtered, 1f) } } From afb662804563d6c1bb1d5173c07c31b19d3edd30 Mon Sep 17 00:00:00 2001 From: Max Thiele Date: Sun, 24 May 2026 15:19:38 +0200 Subject: [PATCH 16/27] Auto assign object IDs incl instanced mesh support --- .../kool/demo/deferred2/Deferred2Demo.kt | 58 +++-- .../kool/demo/deferred2/Deferred2Pipeline.kt | 6 +- .../fabmax/kool/demo/deferred2/GbufferPass.kt | 232 +++--------------- .../kool/demo/deferred2/GbufferShader.kt | 203 +++++++++++++++ .../kool/demo/deferred2/ObjectIdAllocator.kt | 80 ++++++ .../demo/deferred2/ReprojectComputePass.kt | 16 +- 6 files changed, 371 insertions(+), 224 deletions(-) create mode 100644 kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferShader.kt create mode 100644 kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ObjectIdAllocator.kt diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt index d0d3c0900..964d9f755 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt @@ -3,9 +3,7 @@ package de.fabmax.kool.demo.deferred2 import de.fabmax.kool.KoolContext import de.fabmax.kool.demo.* import de.fabmax.kool.demo.menu.DemoMenu -import de.fabmax.kool.math.Vec2f -import de.fabmax.kool.math.Vec3f -import de.fabmax.kool.math.deg +import de.fabmax.kool.math.* import de.fabmax.kool.modules.gltf.GltfLoadConfig import de.fabmax.kool.modules.ksl.KslShader import de.fabmax.kool.modules.ksl.blocks.ColorBlockConfig @@ -26,6 +24,7 @@ import de.fabmax.kool.util.* import kotlin.math.abs import kotlin.math.ceil import kotlin.math.round +import kotlin.math.sin class Deferred2Demo : DemoScene("Deferred2 Demo") { @@ -121,7 +120,6 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { addColorMesh { generate { color = MdColor.PINK.toLinear() -// color = Color.WHITE cube { origin.set(-2.5f, 0f, 0f)} color = MdColor.LIGHT_BLUE.toLinear() cube { @@ -129,7 +127,7 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { size.set(0.25f, 1f, 0.25f) } } - shader = gbufferShader(objectId = 1) { + shader = gbufferShader { color { vertexColor() uniformColor(uniformName = "uBaseCol", blendMode = ColorBlockConfig.BlendMode.Multiply) @@ -158,7 +156,7 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { radius = 0.5f } } - shader = gbufferShader(objectId = 6) { + shader = gbufferShader { color { vertexColor() } metallic { constProperty(1f) } roughness { constProperty(0f) } @@ -171,20 +169,15 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { onUpdate { transform .setIdentity() -// .rotate(90f.deg * Time.gameTime.toFloat(), Vec3f.Y_AXIS) - //.rotate(20f.deg, Vec3f.Y_AXIS) + .rotate(90f.deg * Time.gameTime.toFloat(), Vec3f.Y_AXIS) .translate(2.5f, 0f, 0f) } - shader = gbufferShader(objectId = 2) { + shader = gbufferShader { color { textureColor(albedoMap) } normalMapping { useNormalMap(normalMap) } metallic { textureProperty(metallicMap) } roughness { textureProperty(roughnessMap) } ao { textureProperty(aoMap) } - -// color { constColor(Color.WHITE) } -// metallic { constProperty(1f) } -// roughness { constProperty(0f) } }.apply { bindTexture2d("tbaseColor", albedoMap) bindTexture2d("tNormalMap", normalMap) @@ -193,16 +186,37 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { bindTexture2d("tao", aoMap) } } - addColorMesh { + + val colorCubeInstances = MeshInstanceList(InstanceLayouts.ModelMat) + addColorMesh(instances = colorCubeInstances) { generate { cube { colored() } } - onUpdate { - transform.rotate(90f.deg * Time.deltaT, Vec3f.Y_AXIS) - } - shader = gbufferShader(objectId = 3) { + shader = gbufferShader { + vertices { instancedModelMatrix() } color { vertexColor() } } + transform.translate(0f, 0f, -5f) + val modelMat = MutableMat4f() + onUpdate { + colorCubeInstances.clear() + colorCubeInstances.addInstances(9) { buffer -> + var i = 0 + for (x in -1..1) { + for (y in -1..1) { + buffer.set(i++) { + val xRot = sin(Time.gameTime + i * 31).toFloat().rad * 2.7f + val yRot = sin(Time.gameTime * 0.73 + i * 17).toFloat().rad * 2.7f + modelMat.setIdentity() + .translate(x * 2f, y * 2f + 3f, 0f) + .rotate(xRot, Vec3f.X_AXIS) + .rotate(yRot, Vec3f.Y_AXIS) + set(it.modelMat, modelMat) + } + } + } + } + } } addTextureMesh { generate { @@ -213,16 +227,14 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { texCoordScale.set(10f, 10f) } } - shader = gbufferShader(objectId = 4) { + shader = gbufferShader { color { textureColor(uvChecker) } roughness { uniformProperty(groundRoughness.value, "uRough") } -// metallic { constProperty(1f) } }.apply { bindTexture2d("tbaseColor", uvChecker) - var rough by bindUniformFloat1("uRough", groundRoughness.value) groundRoughness.onChange { _, newValue -> rough = newValue } } @@ -230,13 +242,11 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { val modelMesh = teapot.meshes.values.first().apply { transform.translate(0f, -0.5f, 5f).scale(0.5f).rotate(20f.deg, Vec3f.Y_AXIS) - shader = gbufferShader(objectId = 5) { + shader = gbufferShader { color { constColor(MdColor.LIME toneLin 500) -// constColor(Color.WHITE) } roughness { constProperty(0.1f) } -// metallic { constProperty(1f) } } } addNode(modelMesh) diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt index 067591e18..3edd76520 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt @@ -28,16 +28,18 @@ class Deferred2Pipeline( val lighting: Lighting = Lighting(), var renderScale: Float = 1f, var tsaa: List = TSAA_4, + maxObjects: Int = 16384 ) { val size: Vec2i get() = Vec2i( (scene.mainRenderPass.viewport.width * renderScale).toInt().coerceAtLeast(16), (scene.mainRenderPass.viewport.height * renderScale).toInt().coerceAtLeast(16) ) val camera = PerspectiveCamera() + val idAllocator: ObjectIdAllocator = DefaultObjectIdAllocator(maxObjects) private val camDataBuffer = StructBuffer(DeferredCamDataLayout, 1) val camData = camDataBuffer.asStorageBuffer() - val reprojectMatrixComputePass = ReprojectComputePass() + val reprojectMatrixComputePass = ReprojectComputePass(maxObjects, this) val gbuffers = AlternatingPair { val suff = if (it) "A" else "B" @@ -57,7 +59,7 @@ class Deferred2Pipeline( private val resizeListeners = BufferedList<(Vec2i) -> Unit>() init { - aoPass.kernelSize = 8 + aoPass.kernelSize = 4 aoPass.radius = AoRadius.relativeRadius(1 / 20f) aoPass.temporalKernels = tsaa.size diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt index fbe597fd0..a1386a524 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt @@ -1,16 +1,12 @@ package de.fabmax.kool.demo.deferred2 +import de.fabmax.kool.math.MutableMat4f import de.fabmax.kool.math.Vec2i -import de.fabmax.kool.modules.ksl.BasicVertexConfig -import de.fabmax.kool.modules.ksl.KslShader -import de.fabmax.kool.modules.ksl.LightingConfig -import de.fabmax.kool.modules.ksl.blocks.* -import de.fabmax.kool.modules.ksl.lang.* import de.fabmax.kool.pipeline.* -import de.fabmax.kool.pipeline.shading.AlphaMode -import de.fabmax.kool.scene.VertexLayouts -import de.fabmax.kool.scene.vertexAttrib -import de.fabmax.kool.util.Color +import de.fabmax.kool.scene.InstanceLayouts +import de.fabmax.kool.scene.Mesh +import de.fabmax.kool.util.* +import kotlin.math.max class GbufferPass( initialSize: Vec2i, @@ -44,198 +40,48 @@ class GbufferPass( val upload = pipeline.reprojectMatrixComputePass.uploadData.newVal upload.viewProjMat.set(viewData.drawQueue.viewProjMatF) + upload.modelMats.limit = 0 viewData.drawQueue.forEach { cmd -> - (cmd.mesh.shader as? GbufferShader)?.let { gbufferShader -> - upload.modelMats.position = gbufferShader.objectId * 16 - cmd.modelMatF.putTo(upload.modelMats) - } - } - } - } -} - -class GbufferShader(val config: GbufferShaderConfig) : KslShader("deferred2-gbuffer-shader") { - var objectId: Int by bindUniformInt1("uObjectId", config.objectId) - - init { - pipelineConfig = PipelineConfig(blendMode = BlendMode.DISABLED, cullMethod = config.cullMethod) - program.program() - config.modelCustomizer?.invoke(program) - } - - private fun KslProgram.program() { - val camData = cameraData() - val objectId = interStageInt1("objectId") - val normalViewSpace = interStageFloat3("normalWorldSpace") - var tangentViewSpace: KslInterStageVector? = null - - val texCoordBlock: TexCoordAttributeBlock - - vertexStage { - main { - val uObjectId = uniformInt1("uObjectId") - objectId.input set uObjectId + (inInstanceIndex.toInt1() shl 10.const) - - val vertexBlock = vertexTransformBlock(config.vertexCfg) { - inLocalPos(vertexAttrib(VertexLayouts.Position.position)) - inLocalNormal(vertexAttrib(VertexLayouts.Normal.normal)) - - if (config.normalMapCfg.isNormalMapped) { - // if normal mapping is enabled, the input vertex data is expected to have a tangent attribute - inLocalTangent(vertexAttrib(VertexLayouts.Tangent.tangent)) + val mesh = cmd.mesh + (mesh.shader as? GbufferShader)?.let { gbufferShader -> + val idRange = pipeline.idAllocator.getIdRange(cmd.mesh) + val bufferPos = idRange.from * 16 + gbufferShader.objectId = idRange.from + upload.modelMats.limit = max(upload.modelMats.limit, bufferPos + idRange.size * 16) + + val instances = mesh.instances + if (instances != null) { + val matrixExtractor = gbufferShader.config.instanceModelMatExtractor ?: DefaultInstanceModelMatrixExtractor + if (instances.numInstances > idRange.size) { + logE { "Mesh ${mesh.name} number of instances exceeds ID range: ${instances.numInstances} > ${idRange.size}" } + } + for (i in 0 until instances.numInstances.coerceAtMost(idRange.size)) { + upload.modelMats.position = bufferPos + i * 16 + matrixExtractor.getModelMatrix(i, mesh, upload.modelMats) + } + } else { + upload.modelMats.position = bufferPos + cmd.modelMatF.putTo(upload.modelMats) } } - - // world position and normal are made available via ports for custom models to modify them - val worldPos = float3Port("worldPos", vertexBlock.outWorldPos) - val worldNormal = float3Port("worldNormal", vertexBlock.outWorldNormal) - - val viewPos by camData.viewMat * float4Value(worldPos, 1f) - outPosition set camData.projMat * viewPos - - normalViewSpace.input set (camData.viewMat * float4Value(worldNormal, 0f)).xyz - - if (config.normalMapCfg.isNormalMapped) { - tangentViewSpace = interStageFloat4().apply { - input.xyz set (camData.viewMat * float4Value(vertexBlock.outWorldTangent.xyz, 0f)).xyz - input.w set vertexBlock.outWorldTangent.w - } - } - texCoordBlock = texCoordAttributeBlock() - } - } - fragmentStage { - main { - // determine main color (albedo) - val colorBlock = fragmentColorBlock(config.colorCfg) - val baseColor = float4Port("baseColor", colorBlock.outColor) - (config.alphaMode as? AlphaMode.Mask)?.let { - `if`(baseColor.a lt it.cutOff.const) { - discard() - } - } - val emissionBlock = fragmentPropertyBlock(config.emissionCfg) - val emissionStrength = float1Port("emissionStrength", emissionBlock.outProperty) - - val vertexNormal = float3Var(normalize(normalViewSpace.output)) - if (config.cullMethod.isBackVisible && config.vertexCfg.isFlipBacksideNormals) { - `if`(!inIsFrontFacing) { - vertexNormal *= (-1f).const3 - } - } - - // do normal map computations (if enabled) and adjust material block input normal accordingly - val bumpedNormal = if (config.normalMapCfg.isNormalMapped) { - val normalMapStrength = fragmentPropertyBlock(config.normalMapCfg.strengthCfg).outProperty - normalMapBlock(config.normalMapCfg) { - inTangentWorldSpace(tangentViewSpace!!.output) - inNormalWorldSpace(vertexNormal) - inStrength(normalMapStrength) - inTexCoords(texCoordBlock.getTextureCoords()) - }.outBumpNormal - } else { - vertexNormal - } - - val normal = float3Port("normal", bumpedNormal) - val roughness = float1Port("roughness", fragmentPropertyBlock(config.roughnessCfg).outProperty) - val metallic = float1Port("metallic", fragmentPropertyBlock(config.metallicCfg).outProperty) - val aoFactor = float1Port("aoFactor", fragmentPropertyBlock(config.aoCfg).outProperty) - - colorOutput(float4Value(baseColor.rgb, emissionStrength / 64f.const), location = 0) - colorOutput(float4Value(metallic, roughness, aoFactor, 0f.const), location = 1) - intOutput(int4Value(encodeNormalInt(normal), 0.const, 0.const, 0.const), location = 2) - intOutput(int4Value(objectId.output, 0.const, 0.const, 0.const), location = 3) } } } } -class GbufferShaderConfig(builder: Builder) { - val vertexCfg: BasicVertexConfig = builder.vertexCfg.build() - val colorCfg: ColorBlockConfig = builder.colorCfg.build() - val emissionCfg: PropertyBlockConfig = builder.emissionCfg.build() - val normalMapCfg: NormalMapConfig = builder.normalMapCfg.build() - val metallicCfg: PropertyBlockConfig = builder.metallicCfg.build() - val roughnessCfg: PropertyBlockConfig = builder.roughnessCfg.build() - val aoCfg: PropertyBlockConfig = builder.aoCfg.build() - // todo val parallaxCfg: ParallaxMapConfig = builder.parallaxCfg.build() - val lightingCfg: LightingConfig = builder.lightingCfg.build() - - val alphaMode: AlphaMode = builder.alphaMode - val cullMethod: CullMethod = builder.cullMethod - val objectId: Int = builder.objectId - - val modelCustomizer: (KslProgram.() -> Unit)? = builder.modelCustomizer - - open class Builder { - val vertexCfg = BasicVertexConfig.Builder() - val colorCfg = ColorBlockConfig.Builder("baseColor").constColor(Color.GRAY) - val emissionCfg = PropertyBlockConfig.Builder("emissionStrength").apply { constProperty(0f) } - val normalMapCfg = NormalMapConfig.Builder() - val metallicCfg = PropertyBlockConfig.Builder("metallic").apply { constProperty(0f) } - val roughnessCfg = PropertyBlockConfig.Builder("roughness").apply { constProperty(0.5f) } - val aoCfg = PropertyBlockConfig.Builder("ao").apply { constProperty(1f) } - // todo val parallaxCfg = ParallaxMapConfig.Builder() - val lightingCfg = LightingConfig.Builder() - - var alphaMode: AlphaMode = AlphaMode.Blend - var cullMethod: CullMethod = CullMethod.CULL_BACK_FACES - - var objectId = 0 - - var modelCustomizer: (KslProgram.() -> Unit)? = null - - fun enableSsao(ssaoMap: Texture2d? = null): Builder { - lightingCfg.enableSsao(ssaoMap) - return this - } - - inline fun metallic(block: PropertyBlockConfig.Builder.() -> Unit) { - metallicCfg.block() - } - - inline fun roughness(block: PropertyBlockConfig.Builder.() -> Unit) { - roughnessCfg.block() - } - - inline fun ao(block: PropertyBlockConfig.Builder.() -> Unit) { - aoCfg.block() +private object DefaultInstanceModelMatrixExtractor : InstanceModelMatrixExtractor { + private val insModelMatBuf = MutableMat4f() + private val modelMatBuf = MutableMat4f() + + @Suppress("UNCHECKED_CAST") + override fun getModelMatrix(instanceIndex: Int, mesh: Mesh<*>, target: Float32Buffer) { + val insts = requireNotNull(mesh.instances) + val modelMat = insts.layout.members.first { it.name == InstanceLayouts.ModelMat.modelMat.name } as Mat4Member + insts.instanceData.get(instanceIndex) { + this as StructBufferView + get(modelMat, insModelMatBuf) + modelMatBuf.set(mesh.modelMatF).mul(insModelMatBuf) + modelMatBuf.putTo(target) } - - inline fun color(block: ColorBlockConfig.Builder.() -> Unit) { - colorCfg.colorSources.clear() - colorCfg.block() - } - - inline fun emission(block: PropertyBlockConfig.Builder.() -> Unit) { - emissionCfg.block() - } - - inline fun lighting(block: LightingConfig.Builder.() -> Unit) { - lightingCfg.block() - } - - inline fun normalMapping(block: NormalMapConfig.Builder.() -> Unit) { - normalMapCfg.block() - } - -// inline fun parallaxMapping(block: ParallaxMapConfig.Builder.() -> Unit) { -// parallaxCfg.block() -// } - - inline fun vertices(block: BasicVertexConfig.Builder.() -> Unit) { - vertexCfg.block() - } - - open fun build() = GbufferShaderConfig(this) } } - -fun gbufferShader(objectId: Int, block: GbufferShaderConfig.Builder.() -> Unit): GbufferShader { - val cfg = GbufferShaderConfig.Builder().apply{ - this.objectId = objectId - block() - }.build() - return GbufferShader(cfg) -} diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferShader.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferShader.kt new file mode 100644 index 000000000..e4a37ce37 --- /dev/null +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferShader.kt @@ -0,0 +1,203 @@ +package de.fabmax.kool.demo.deferred2 + +import de.fabmax.kool.modules.ksl.BasicVertexConfig +import de.fabmax.kool.modules.ksl.KslShader +import de.fabmax.kool.modules.ksl.LightingConfig +import de.fabmax.kool.modules.ksl.blocks.* +import de.fabmax.kool.modules.ksl.lang.* +import de.fabmax.kool.pipeline.* +import de.fabmax.kool.pipeline.shading.AlphaMode +import de.fabmax.kool.scene.Mesh +import de.fabmax.kool.scene.VertexLayouts +import de.fabmax.kool.scene.vertexAttrib +import de.fabmax.kool.util.Color +import de.fabmax.kool.util.Float32Buffer + +fun gbufferShader(block: GbufferShaderConfig.Builder.() -> Unit): GbufferShader { + val cfg = GbufferShaderConfig.Builder().apply{ + block() + }.build() + return GbufferShader(cfg) +} + +class GbufferShader(val config: GbufferShaderConfig) : KslShader("deferred2-gbuffer-shader") { + var objectId: Int by bindUniformInt1("uObjectId") + + init { + pipelineConfig = PipelineConfig(blendMode = BlendMode.DISABLED, cullMethod = config.cullMethod) + program.program() + config.modelCustomizer?.invoke(program) + } + + private fun KslProgram.program() { + val camData = cameraData() + val objectId = interStageInt1("objectId") + val normalViewSpace = interStageFloat3("normalWorldSpace") + var tangentViewSpace: KslInterStageVector? = null + + val texCoordBlock: TexCoordAttributeBlock + + vertexStage { + main { + val uObjectId = uniformInt1("uObjectId") + objectId.input set uObjectId + inInstanceIndex.toInt1() + + val vertexBlock = vertexTransformBlock(config.vertexCfg) { + inLocalPos(vertexAttrib(VertexLayouts.Position.position)) + inLocalNormal(vertexAttrib(VertexLayouts.Normal.normal)) + if (config.normalMapCfg.isNormalMapped) { + // if normal mapping is enabled, the input vertex data is expected to have a tangent attribute + inLocalTangent(vertexAttrib(VertexLayouts.Tangent.tangent)) + } + } + + // world position and normal are made available via ports for custom models to modify them + val worldPos = float3Port("worldPos", vertexBlock.outWorldPos) + val worldNormal = float3Port("worldNormal", vertexBlock.outWorldNormal) + val viewPos by camData.viewMat * float4Value(worldPos, 1f) + outPosition set camData.projMat * viewPos + + normalViewSpace.input set (camData.viewMat * float4Value(worldNormal, 0f)).xyz + if (config.normalMapCfg.isNormalMapped) { + tangentViewSpace = interStageFloat4().apply { + input.xyz set (camData.viewMat * float4Value(vertexBlock.outWorldTangent.xyz, 0f)).xyz + input.w set vertexBlock.outWorldTangent.w + } + } + texCoordBlock = texCoordAttributeBlock() + } + } + fragmentStage { + main { + // determine main color (albedo) + val colorBlock = fragmentColorBlock(config.colorCfg) + val baseColor = float4Port("baseColor", colorBlock.outColor) + (config.alphaMode as? AlphaMode.Mask)?.let { + `if`(baseColor.a lt it.cutOff.const) { + discard() + } + } + val emissionBlock = fragmentPropertyBlock(config.emissionCfg) + val emissionStrength = float1Port("emissionStrength", emissionBlock.outProperty) + + val vertexNormal = float3Var(normalize(normalViewSpace.output)) + if (config.cullMethod.isBackVisible && config.vertexCfg.isFlipBacksideNormals) { + `if`(!inIsFrontFacing) { + vertexNormal *= (-1f).const3 + } + } + + // do normal map computations (if enabled) and adjust material block input normal accordingly + val bumpedNormal = if (config.normalMapCfg.isNormalMapped) { + val normalMapStrength = fragmentPropertyBlock(config.normalMapCfg.strengthCfg).outProperty + normalMapBlock(config.normalMapCfg) { + inTangentWorldSpace(tangentViewSpace!!.output) + inNormalWorldSpace(vertexNormal) + inStrength(normalMapStrength) + inTexCoords(texCoordBlock.getTextureCoords()) + }.outBumpNormal + } else { + vertexNormal + } + + val normal = float3Port("normal", bumpedNormal) + val roughness = float1Port("roughness", fragmentPropertyBlock(config.roughnessCfg).outProperty) + val metallic = float1Port("metallic", fragmentPropertyBlock(config.metallicCfg).outProperty) + val aoFactor = float1Port("aoFactor", fragmentPropertyBlock(config.aoCfg).outProperty) + + colorOutput(float4Value(baseColor.rgb, emissionStrength / 64f.const), location = 0) + colorOutput(float4Value(metallic, roughness, aoFactor, 0f.const), location = 1) + intOutput(int4Value(encodeNormalInt(normal), 0.const, 0.const, 0.const), location = 2) + intOutput(int4Value(objectId.output, 0.const, 0.const, 0.const), location = 3) + } + } + } +} + +class GbufferShaderConfig(builder: Builder) { + val vertexCfg: BasicVertexConfig = builder.vertexCfg.build() + val colorCfg: ColorBlockConfig = builder.colorCfg.build() + val emissionCfg: PropertyBlockConfig = builder.emissionCfg.build() + val normalMapCfg: NormalMapConfig = builder.normalMapCfg.build() + val metallicCfg: PropertyBlockConfig = builder.metallicCfg.build() + val roughnessCfg: PropertyBlockConfig = builder.roughnessCfg.build() + val aoCfg: PropertyBlockConfig = builder.aoCfg.build() + // todo val parallaxCfg: ParallaxMapConfig = builder.parallaxCfg.build() + val lightingCfg: LightingConfig = builder.lightingCfg.build() + val instanceModelMatExtractor: InstanceModelMatrixExtractor? = builder.instanceModelMatExtractor + + val alphaMode: AlphaMode = builder.alphaMode + val cullMethod: CullMethod = builder.cullMethod + + val modelCustomizer: (KslProgram.() -> Unit)? = builder.modelCustomizer + + open class Builder { + val vertexCfg = BasicVertexConfig.Builder() + val colorCfg = ColorBlockConfig.Builder("baseColor").constColor(Color.GRAY) + val emissionCfg = PropertyBlockConfig.Builder("emissionStrength").apply { constProperty(0f) } + val normalMapCfg = NormalMapConfig.Builder() + val metallicCfg = PropertyBlockConfig.Builder("metallic").apply { constProperty(0f) } + val roughnessCfg = PropertyBlockConfig.Builder("roughness").apply { constProperty(0.5f) } + val aoCfg = PropertyBlockConfig.Builder("ao").apply { constProperty(1f) } + // todo val parallaxCfg = ParallaxMapConfig.Builder() + val lightingCfg = LightingConfig.Builder() + var instanceModelMatExtractor: InstanceModelMatrixExtractor? = null + + var alphaMode: AlphaMode = AlphaMode.Blend + var cullMethod: CullMethod = CullMethod.CULL_BACK_FACES + + var modelCustomizer: (KslProgram.() -> Unit)? = null + + fun enableSsao(ssaoMap: Texture2d? = null): Builder { + lightingCfg.enableSsao(ssaoMap) + return this + } + + inline fun metallic(block: PropertyBlockConfig.Builder.() -> Unit) { + metallicCfg.block() + } + + inline fun roughness(block: PropertyBlockConfig.Builder.() -> Unit) { + roughnessCfg.block() + } + + inline fun ao(block: PropertyBlockConfig.Builder.() -> Unit) { + aoCfg.block() + } + + inline fun color(block: ColorBlockConfig.Builder.() -> Unit) { + colorCfg.colorSources.clear() + colorCfg.block() + } + + inline fun emission(block: PropertyBlockConfig.Builder.() -> Unit) { + emissionCfg.block() + } + + inline fun lighting(block: LightingConfig.Builder.() -> Unit) { + lightingCfg.block() + } + + inline fun normalMapping(block: NormalMapConfig.Builder.() -> Unit) { + normalMapCfg.block() + } + +// inline fun parallaxMapping(block: ParallaxMapConfig.Builder.() -> Unit) { +// parallaxCfg.block() +// } + + inline fun vertices(block: BasicVertexConfig.Builder.() -> Unit) { + vertexCfg.block() + } + + fun instanceModelMatExtractor(extractor: InstanceModelMatrixExtractor) { + instanceModelMatExtractor = extractor + } + + open fun build() = GbufferShaderConfig(this) + } +} + +fun interface InstanceModelMatrixExtractor { + fun getModelMatrix(instanceIndex: Int, mesh: Mesh<*>, target: Float32Buffer) +} diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ObjectIdAllocator.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ObjectIdAllocator.kt new file mode 100644 index 000000000..688c3fcfb --- /dev/null +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ObjectIdAllocator.kt @@ -0,0 +1,80 @@ +package de.fabmax.kool.demo.deferred2 + +import de.fabmax.kool.scene.Mesh +import de.fabmax.kool.scene.NodeId +import de.fabmax.kool.util.logD +import de.fabmax.kool.util.logW +import kotlin.math.max + +interface ObjectIdAllocator { + val size: Int + fun getIdRange(mesh: Mesh<*>): ObjectIdRange + fun removeObject(mesh: Mesh<*>) +} + +class DefaultObjectIdAllocator(val maxObjects: Int) : ObjectIdAllocator { + override var size: Int = 0 + private set + + private val objectIdRanges = mutableMapOf() + private val slots = Array(maxObjects) { null } + + override fun getIdRange(mesh: Mesh<*>): ObjectIdRange { + return objectIdRanges.getOrPut(mesh.id) { + val numInstances = mesh.instances?.maxInstances ?: 1 + var range = ObjectIdRange(size, size + numInstances) + if (!checkRangeFree(range)) { + var rng = searchFreeRange(numInstances) + if (rng == null) { + logW { "Failed to find free object ID range for ${mesh.name} (size: $numInstances)" } + rng = ObjectIdRange(maxObjects-1, maxObjects) + } + range = rng + } + logD { "Allocated object ID range for mesh ${mesh.name}: $range" } + size = max(size, range.to) + range + } + } + + override fun removeObject(mesh: Mesh<*>) { + val range = objectIdRanges.remove(mesh.id) + if (range != null) { + slots.fill(null, range.from, range.to) + } + } + + private fun checkRangeFree(range: ObjectIdRange): Boolean { + if (range.from >= maxObjects) { + return false + } + for (i in range.from until range.to) { + if (slots[i] != null) { + return false + } + } + return true + } + + private fun searchFreeRange(size: Int): ObjectIdRange? { + var from = 0 + while (from < maxObjects - size) { + while (slots[from] != null) { + from++ + } + var range = 1 + while (range < size && slots[from + range] == null) { + range++ + } + if (range == size) { + return ObjectIdRange(from, from + range) + } + from += range + } + return null + } +} + +data class ObjectIdRange(val from: Int, val to: Int) { + val size: Int get() = to - from +} diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ReprojectComputePass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ReprojectComputePass.kt index 86dad464c..5771905cb 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ReprojectComputePass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ReprojectComputePass.kt @@ -10,7 +10,8 @@ import de.fabmax.kool.util.MemoryLayout import de.fabmax.kool.util.Struct class ReprojectComputePass( - val maxObjects: Int = 65536 + val maxObjects: Int = 65536, + private val pipeline: Deferred2Pipeline ) : ComputePass("deferred2-reproject-compute-pass") { val uploadData = AlternatingPair { @@ -38,9 +39,10 @@ class ReprojectComputePass( val newUpload = uploadData.newVal modelMats.newVal.uploadData(newUpload.modelMats) shader.swapPipelineData(newUpload) { - shader.inputOldViewProj = uploadData.oldVal.viewProjMat - shader.oldModelMats = modelMats.oldVal - shader.newModelMats = modelMats.newVal + inputOldViewProj = uploadData.oldVal.viewProjMat + oldModelMats = modelMats.oldVal + newModelMats = modelMats.newVal + numMatrices = pipeline.idAllocator.size } } } @@ -53,6 +55,7 @@ private class ReprojectComputeShader( reprojectMats: GpuBuffer, ) : KslComputeShader("reproject-compute") { + var numMatrices by bindUniformInt1("numMatrices") var inputOldViewProj by bindUniformMat4("oldViewProj") var oldModelMats by bindStorage("oldModelMats") var newModelMats by bindStorage("newModelMats") @@ -63,8 +66,8 @@ private class ReprojectComputeShader( } private fun KslProgram.program() { - dumpCode = true computeStage(workGroupSizeX = 64) { + val numMatrices = uniformInt1("numMatrices") val oldViewProj = uniformMat4("oldViewProj") val matStruct = struct(StorageMatLayout) val oldModelMats = storage("oldModelMats", matStruct) @@ -73,6 +76,9 @@ private class ReprojectComputeShader( main { val idx by inGlobalInvocationId.x.toInt1() + `if`(idx ge numMatrices) { + `return`() + } val model = mat4Var(newModelMats[idx][StorageMatLayout.mat]) val det by model.run { From b858e42dbf22698f96ecedc803087d1f9abf0ca3 Mon Sep 17 00:00:00 2001 From: Max Thiele Date: Sun, 24 May 2026 20:06:46 +0200 Subject: [PATCH 17/27] Deferred dynamic lighting --- .../modules/ksl/blocks/GetLightRadiance.kt | 17 +- .../modules/ksl/lang/KslVectorAccessorB2.kt | 3 + .../modules/ksl/lang/KslVectorAccessorB3.kt | 4 + .../modules/ksl/lang/KslVectorAccessorB4.kt | 5 + .../modules/ksl/lang/KslVectorAccessorF2.kt | 3 + .../modules/ksl/lang/KslVectorAccessorF3.kt | 4 + .../modules/ksl/lang/KslVectorAccessorF4.kt | 5 + .../modules/ksl/lang/KslVectorAccessorI2.kt | 3 + .../modules/ksl/lang/KslVectorAccessorI3.kt | 4 + .../modules/ksl/lang/KslVectorAccessorI4.kt | 5 + .../modules/ksl/lang/KslVectorAccessorU2.kt | 3 + .../modules/ksl/lang/KslVectorAccessorU3.kt | 4 + .../modules/ksl/lang/KslVectorAccessorU4.kt | 5 + .../kool/demo/deferred2/Deferred2Demo.kt | 41 ++- .../kool/demo/deferred2/Deferred2Pipeline.kt | 4 + .../demo/deferred2/DeferredLightShader.kt | 119 ++++++++ .../kool/demo/deferred2/DeferredLights.kt | 270 ++++++++++++++++++ .../fabmax/kool/demo/deferred2/GbufferPass.kt | 58 ++-- .../kool/demo/deferred2/LightingPass.kt | 15 +- 19 files changed, 534 insertions(+), 38 deletions(-) create mode 100644 kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/DeferredLightShader.kt create mode 100644 kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/DeferredLights.kt diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/GetLightRadiance.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/GetLightRadiance.kt index 2e30f8086..19eda06bf 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/GetLightRadiance.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/GetLightRadiance.kt @@ -20,8 +20,9 @@ class GetLightRadiance(parentScope: KslScopeBuilder, isFiniteSoi: Boolean) : }.`else` { // spot or point light - val dist = float1Var(length(fragPos - encLightPos.xyz)) - val strength = float1Var(1f.const / (dist * dist + 1f.const)) + val lightToFrag by fragPos - encLightPos.xyz + val dist by length(lightToFrag) + val strength by 1f.const / (dist * dist + 1f.const) if (isFiniteSoi) { strength *= clamp((lightRadius - dist) / lightRadius, 0f.const, 1f.const) } @@ -30,12 +31,12 @@ class GetLightRadiance(parentScope: KslScopeBuilder, isFiniteSoi: Boolean) : radiance set encLightColor.rgb * strength }.`else` { // spot light - val lightDirToFrag = float3Var((fragPos - encLightPos.xyz) / dist) - val outerAngle = encLightDir.w - val innerFac = encLightColor.w - val innerAngle = float1Var(outerAngle + (1f.const - outerAngle) * (1f.const - innerFac)) - val angle = float1Var(dot(lightDirToFrag, encLightDir.xyz)) - val angleStrength = 1f.const - smoothStep(innerAngle, outerAngle, angle) + val lightDirToFrag by lightToFrag / dist + val outerAngle by encLightDir.w + val innerFac by encLightColor.w + val innerAngle by outerAngle + (1f.const - outerAngle) * (1f.const - innerFac) + val angle by dot(lightDirToFrag, encLightDir.xyz) + val angleStrength by 1f.const - smoothStep(innerAngle, outerAngle, angle) radiance set encLightColor.rgb * strength * angleStrength } } diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorB2.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorB2.kt index 4517a8677..d5ce6256b 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorB2.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorB2.kt @@ -15,3 +15,6 @@ val KslExpression.yx get() = bool2("yx") val KslExpression.rr get() = bool2("rr") val KslExpression.gg get() = bool2("gg") val KslExpression.gr get() = bool2("gr") + +operator fun KslExprBool2.component1(): KslExprBool1 = x +operator fun KslExprBool2.component2(): KslExprBool1 = y \ No newline at end of file diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorB3.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorB3.kt index 38bb4e035..dc7c83e63 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorB3.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorB3.kt @@ -17,3 +17,7 @@ val KslExpression.yz get() = bool2("yz") val KslExpression.rg get() = bool2("rg") val KslExpression.rb get() = bool2("rb") val KslExpression.gb get() = bool2("gb") + +operator fun KslExprBool3.component1(): KslExprBool1 = x +operator fun KslExprBool3.component2(): KslExprBool1 = y +operator fun KslExprBool3.component3(): KslExprBool1 = z \ No newline at end of file diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorB4.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorB4.kt index e0fd2aeb1..cf628c807 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorB4.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorB4.kt @@ -22,3 +22,8 @@ val KslExpression.gb get() = bool2("gb") val KslExpression.xyz get() = bool3("xyz") val KslExpression.rgb get() = bool3("rgb") + +operator fun KslExprBool4.component1(): KslExprBool1 = x +operator fun KslExprBool4.component2(): KslExprBool1 = y +operator fun KslExprBool4.component3(): KslExprBool1 = z +operator fun KslExprBool4.component4(): KslExprBool1 = w \ No newline at end of file diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorF2.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorF2.kt index 8214e2e62..d44d4ce21 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorF2.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorF2.kt @@ -15,3 +15,6 @@ val KslExpression.yx get() = float2("yx") val KslExpression.rr get() = float2("rr") val KslExpression.gg get() = float2("gg") val KslExpression.gr get() = float2("gr") + +operator fun KslExprFloat2.component1(): KslExprFloat1 = x +operator fun KslExprFloat2.component2(): KslExprFloat1 = y \ No newline at end of file diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorF3.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorF3.kt index 844f619f0..6a9777646 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorF3.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorF3.kt @@ -17,3 +17,7 @@ val KslExpression.yz get() = float2("yz") val KslExpression.rg get() = float2("rg") val KslExpression.rb get() = float2("rb") val KslExpression.gb get() = float2("gb") + +operator fun KslExprFloat3.component1(): KslExprFloat1 = x +operator fun KslExprFloat3.component2(): KslExprFloat1 = y +operator fun KslExprFloat3.component3(): KslExprFloat1 = z diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorF4.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorF4.kt index c07646265..123c877d5 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorF4.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorF4.kt @@ -25,3 +25,8 @@ val KslExpression.gb get() = float2("gb") val KslExpression.xyz get() = float3("xyz") val KslExpression.rgb get() = float3("rgb") + +operator fun KslExprFloat4.component1(): KslExprFloat1 = x +operator fun KslExprFloat4.component2(): KslExprFloat1 = y +operator fun KslExprFloat4.component3(): KslExprFloat1 = z +operator fun KslExprFloat4.component4(): KslExprFloat1 = w \ No newline at end of file diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorI2.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorI2.kt index 2b20b742e..997be8cbc 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorI2.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorI2.kt @@ -15,3 +15,6 @@ val KslExpression.yx get() = int2("yx") val KslExpression.rr get() = int2("rr") val KslExpression.gg get() = int2("gg") val KslExpression.gr get() = int2("gr") + +operator fun KslExprInt2.component1(): KslExprInt1 = x +operator fun KslExprInt2.component2(): KslExprInt1 = y \ No newline at end of file diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorI3.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorI3.kt index 5baf0c731..fbdc5a05c 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorI3.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorI3.kt @@ -17,3 +17,7 @@ val KslExpression.yz get() = int2("yz") val KslExpression.rg get() = int2("rg") val KslExpression.rb get() = int2("rb") val KslExpression.gb get() = int2("gb") + +operator fun KslExprInt3.component1(): KslExprInt1 = x +operator fun KslExprInt3.component2(): KslExprInt1 = y +operator fun KslExprInt3.component3(): KslExprInt1 = z \ No newline at end of file diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorI4.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorI4.kt index dd289c4f0..3db888ad1 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorI4.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorI4.kt @@ -22,3 +22,8 @@ val KslExpression.gb get() = int2("gb") val KslExpression.xyz get() = int3("xyz") val KslExpression.rgb get() = int3("rgb") + +operator fun KslExprInt4.component1(): KslExprInt1 = x +operator fun KslExprInt4.component2(): KslExprInt1 = y +operator fun KslExprInt4.component3(): KslExprInt1 = z +operator fun KslExprInt4.component4(): KslExprInt1 = w \ No newline at end of file diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorU2.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorU2.kt index 1ef7fac22..6111bbb08 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorU2.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorU2.kt @@ -15,3 +15,6 @@ val KslExpression.yx get() = uint2("yx") val KslExpression.rr get() = uint2("rr") val KslExpression.gg get() = uint2("gg") val KslExpression.gr get() = uint2("gr") + +operator fun KslExprUint2.component1(): KslExprUint1 = x +operator fun KslExprUint2.component2(): KslExprUint1 = y \ No newline at end of file diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorU3.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorU3.kt index dcbe74ac4..9cb465412 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorU3.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorU3.kt @@ -17,3 +17,7 @@ val KslExpression.yz get() = uint2("yz") val KslExpression.rg get() = uint2("rg") val KslExpression.rb get() = uint2("rb") val KslExpression.gb get() = uint2("gb") + +operator fun KslExprUint3.component1(): KslExprUint1 = x +operator fun KslExprUint3.component2(): KslExprUint1 = y +operator fun KslExprUint3.component3(): KslExprUint1 = z \ No newline at end of file diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorU4.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorU4.kt index cbcea19a6..ad83abbf0 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorU4.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslVectorAccessorU4.kt @@ -22,3 +22,8 @@ val KslExpression.gb get() = uint2("gb") val KslExpression.xyz get() = uint3("xyz") val KslExpression.rgb get() = uint3("rgb") + +operator fun KslExprUint4.component1(): KslExprUint1 = x +operator fun KslExprUint4.component2(): KslExprUint1 = y +operator fun KslExprUint4.component3(): KslExprUint1 = z +operator fun KslExprUint4.component4(): KslExprUint1 = w \ No newline at end of file diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt index 964d9f755..369eca455 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt @@ -25,6 +25,7 @@ import kotlin.math.abs import kotlin.math.ceil import kotlin.math.round import kotlin.math.sin +import kotlin.random.Random class Deferred2Demo : DemoScene("Deferred2 Demo") { @@ -56,7 +57,8 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { private var gpuTimesAccu = GpuTimes() override fun Scene.setupMainScene(ctx: KoolContext) { - val content = deferredContent() + val deferredLights = DeferredLights() + val content = deferredContent(deferredLights) val lighting = Lighting().apply { clear() // singlePointLight { @@ -71,13 +73,6 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { filterWeight.value = pipeline.filterPass.filterWeight.toInt() filterWeight.onChange { _, value -> pipeline.filterPass.filterWeight = value.toFloat() } - content.apply { - val orbitCam = orbitCamera(pipeline.camera) { - setRotation(100f, -7f) - } - addNode(orbitCam) - } - bloomPass = BloomPass(pipeline.filterPass.filterOutput.newVal) bloomPass.isProfileGpu = true addComputePass(bloomPass) @@ -89,6 +84,32 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { } } + content.apply { + val orbitCam = orbitCamera(pipeline.camera) { + setRotation(100f, -7f) + } + addNode(orbitCam) + //addNode(pointLights) + } + + pipeline.onSwap { + deferredLights.swapPipelineData(pipeline) + } + val r = Random(1234) + repeat(50) { + deferredLights.addPointLight { + color.set(MdColor.PALETTE.random(r).toLinear()) + position.set(Vec3f(r.randomF(-15f, 15f), 1.5f, r.randomF(-15f, 15f))) + strengthByIntensity(r.randomF(5f, 20f)) + } + deferredLights.addSpotLight { + color.set(MdColor.PALETTE.random(r).toLinear()) + position.set(Vec3f(r.randomF(-15f, 15f), r.randomF(2f, 5f), r.randomF(-15f, 15f))) + setDirection(Vec3f(r.randomF(-1f, 1f), -1f, r.randomF(-1f, 1f))) + strengthByIntensity(r.randomF(30f, 50f)) + } + } + addTextureMesh { generate { generateFullscreenQuad() @@ -113,10 +134,10 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { } } - private fun deferredContent() = Node("deferred content").apply { + private fun deferredContent(pointLights: DeferredLights) = Node("deferred content").apply { addGroup { transform.rotate(45f.deg, Vec3f.Y_AXIS) - + addNode(pointLights) addColorMesh { generate { color = MdColor.PINK.toLinear() diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt index 3edd76520..e9bb219bb 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt @@ -121,6 +121,7 @@ class Deferred2Pipeline( camDataBuffer.set(0) { set(it.proj, camera.proj) set(it.view, camera.view) + set(it.viewProj, camera.viewProj) set(it.invView, camera.invView) set(it.invViewProj, camera.invViewProj) set(it.oldViewProj, reprojectMatrixComputePass.uploadData.oldVal.viewProjMat) @@ -239,6 +240,7 @@ class AlternatingPair(factory: (Boolean) -> T) { object DeferredCamDataLayout : Struct("deferred_cam_data", MemoryLayout.Std140) { val proj = mat4("proj") val view = mat4("view") + val viewProj = mat4("viewProj") val invView = mat4("invView") val invViewProj = mat4("invViewProj") val oldViewProj = mat4("oldViewProj") @@ -252,6 +254,8 @@ val KslStructStorage.proj: KslExprMat4 get() = this[0.con context(_: KslScopeBuilder) val KslStructStorage.view: KslExprMat4 get() = this[0.const][DeferredCamDataLayout.view] context(_: KslScopeBuilder) +val KslStructStorage.viewProj: KslExprMat4 get() = this[0.const][DeferredCamDataLayout.viewProj] +context(_: KslScopeBuilder) val KslStructStorage.invView: KslExprMat4 get() = this[0.const][DeferredCamDataLayout.invView] context(_: KslScopeBuilder) val KslStructStorage.invViewProj: KslExprMat4 get() = this[0.const][DeferredCamDataLayout.invViewProj] diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/DeferredLightShader.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/DeferredLightShader.kt new file mode 100644 index 000000000..a1453dca3 --- /dev/null +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/DeferredLightShader.kt @@ -0,0 +1,119 @@ +package de.fabmax.kool.demo.deferred2 + +import de.fabmax.kool.KoolSystem +import de.fabmax.kool.math.Vec4f +import de.fabmax.kool.modules.ksl.KslShader +import de.fabmax.kool.modules.ksl.NormalLightRange +import de.fabmax.kool.modules.ksl.blocks.modelMatrix +import de.fabmax.kool.modules.ksl.blocks.pbrLightBlock +import de.fabmax.kool.modules.ksl.lang.* +import de.fabmax.kool.pipeline.* +import de.fabmax.kool.scene.InstanceLayouts +import de.fabmax.kool.scene.VertexLayouts +import de.fabmax.kool.scene.instanceAttrib +import de.fabmax.kool.scene.vertexAttrib + +class DeferredLightShader : KslShader("deferred-light-shader") { + var depth by bindTexture2d("depth") + var normals by bindTexture2d("normals") + var albedoEmission by bindTexture2d("albedoEmission") + var metalRoughAo by bindTexture2d("metalRoughAo") + var camData by bindStorage("camData") + + init { + pipelineConfig = PipelineConfig( + blendMode = BlendMode.BLEND_ADDITIVE, + cullMethod = CullMethod.CULL_FRONT_FACES, + depthTest = DepthCompareOp.ALWAYS, + isWriteDepth = false + ) + program.program() + } + + private fun KslProgram.program() { + val projPos = interStageFloat4() + + val lightPosType = interStageFloat4(interpolation = KslInterStageInterpolation.Flat) + val lightDir = interStageFloat4(interpolation = KslInterStageInterpolation.Flat) + val lightColor = interStageFloat4(interpolation = KslInterStageInterpolation.Flat) + val lightRadius = interStageFloat1(interpolation = KslInterStageInterpolation.Flat) + + val camDataLayout = struct(DeferredCamDataLayout) + val camData = storage("camData", camDataLayout) + + vertexStage { + main { + val model by modelMatrix().matrix * instanceAttrib(InstanceLayouts.ModelMat.modelMat) + val mvp by camData.viewProj * model + outPosition set mvp * float4Value(vertexAttrib(VertexLayouts.Position.position), 1f.const) + projPos.input set outPosition + + val lightType by instanceAttrib(DeferredLightInstanceLayout.lightType) + val lightPos by (model * float4Value(0f, 0f, 0f, 1f)).xyz + val spotAngle by instanceAttrib(DeferredLightInstanceLayout.encodedSpotAngle) + val dir by normalize((model * float4Value(1f, 0f, 0f, 0f)).xyz) + + lightRadius.input set length(model * Vec4f.X_AXIS.const) + lightColor.input set instanceAttrib(DeferredLightInstanceLayout.lightColor) + lightPosType.input set float4Value(lightPos, lightType) + lightDir.input set float4Value(dir, spotAngle) + } + } + + fragmentStage { + val depth = texture2d("depth", isUnfilterable = true) + val metalRoughAo = texture2d("metalRoughAo") + val normals = texture2dInt("normals") + val albedoEmission = texture2d("albedoEmission") + + main { + val uv = float2Var(projPos.output.xy / projPos.output.w * 0.5.const + 0.5.const) + if (KoolSystem.requireContext().backend.isInvertedNdcY) { + uv.y set 1f.const - uv.y + } + val size by depth.size() + val baseCoord by (uv * size.toFloat2()).toInt2() + val camNear by camData.camNear + val invViewProj by camData.invViewProj + val depthSample by depth.load(baseCoord, lod = 0.const).x + val worldPos by unprojectBaseCoord(depthSample, baseCoord, size, camNear, invViewProj).xyz + val lightOrigin by lightPosType.output.xyz + val lightToFrag by lightOrigin - worldPos + val lightDist by length(lightToFrag) + val dotDir by dot(lightDir.output.xyz, -lightToFrag / lightDist) + val spotAngle by lightDir.output.w + + `if`((lightDist gt lightRadius.output) or (dotDir lt spotAngle)) { + discard() + } + + val encodedNormal by normals.load(baseCoord).x + val viewNormal by decodeNormalInt(encodedNormal) + val baseColor by albedoEmission.load(baseCoord).rgb + val mra by metalRoughAo.load(baseCoord).xyz + val (metallic, roughness, ao) = mra + val worldNrm by (camData.invView * float4Value(viewNormal, 0f.const)).xyz + val viewDir by normalize(camData.camPosition - worldPos) + val f0 = mix(0.04f.const3, baseColor, metallic) + val lightBlock = pbrLightBlock(false, normalLightRange = NormalLightRange.ZeroToOne) { + inViewDir(viewDir) + inNormalLight(worldNrm) + inFragmentPosLight(worldPos) + inBaseColorRgb(baseColor) + + inRoughnessLight(roughness) + inMetallicLight(metallic) + inF0(f0) + + inEncodedLightPos(lightPosType.output) + inEncodedLightDir(lightDir.output) + inEncodedLightColor(lightColor.output) + inLightRadius(lightRadius.output) + inLightStr(1f.const) + inShadowFac(ao) + } + colorOutput(lightBlock.outRadiance) + } + } + } +} \ No newline at end of file diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/DeferredLights.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/DeferredLights.kt new file mode 100644 index 000000000..8663da8ff --- /dev/null +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/DeferredLights.kt @@ -0,0 +1,270 @@ +package de.fabmax.kool.demo.deferred2 + +import de.fabmax.kool.math.* +import de.fabmax.kool.pipeline.swapPipelineData +import de.fabmax.kool.scene.* +import de.fabmax.kool.scene.geometry.MeshBuilder +import de.fabmax.kool.util.* +import kotlin.math.* + +class DeferredLights(val isDynamic: Boolean = true, name: String = "deferred-lights") : Node(name) { + val pointLights: List + field = mutableListOf() + val spotLights: Map> + field = mutableMapOf>() + + private val pointLightInstances = MeshInstanceList(DeferredLightInstanceLayout) + private val modelMat = MutableMat4f() + + val lightShader = DeferredLightShader() + + private val spotLightMeshes = mutableMapOf() + private val pointLightMesh = Mesh( + layout = VertexLayouts.Position, + instances = pointLightInstances, + name = "DeferredPointLights" + ).apply { + generate { makePointLightMesh() } + shader = lightShader + } + + init { + addNode(pointLightMesh) + if (isDynamic) { + onUpdate { updateLightData() } + } + } + + fun updateLightData() { + pointLightInstances.clear() + pointLightInstances.addInstances(pointLights.size) { buf -> + for (i in 0 until pointLights.size) { + buf.put { encodePoint(pointLights[i]) } + } + } + if (spotLightMeshes.isNotEmpty()) { + for (lights in spotLightMeshes.values) { + lights.instances.clear() + lights.instances.addInstances(lights.lights.size) { buf -> + for (i in 0 until lights.lights.size) { + buf.put { encodeSpot(lights.lights[i]) } + } + } + } + } + } + + private fun MutableStructBufferView.encodePoint(light: DynamicPointLight) { + modelMat + .setIdentity() + .translate(light.position) + .scale(light.radius) + set(DeferredLightInstanceLayout.modelMat, modelMat) + set(DeferredLightInstanceLayout.lightType, Light.Point.ENCODING) + set(DeferredLightInstanceLayout.encodedSpotAngle, -1.01f) + set(DeferredLightInstanceLayout.lightColor, + light.color.r * light.intensity, + light.color.g * light.intensity, + light.color.b * light.intensity, + 1f, + ) + } + + private fun MutableStructBufferView.encodeSpot(light: DynamicSpotLight) { + modelMat + .setIdentity() + .translate(light.position) + .rotate(light.rotation) + modelMat.scale(light.radius) + set(DeferredLightInstanceLayout.modelMat, modelMat) + + //println(cos(min(light.maxSpotAngle.rad, light.spotAngle.rad) / 2f)) + + set(DeferredLightInstanceLayout.lightType, Light.Spot.ENCODING) + set(DeferredLightInstanceLayout.encodedSpotAngle, cos(min(light.maxSpotAngle.rad, light.spotAngle.rad) / 2f)) + set(DeferredLightInstanceLayout.lightColor, + light.color.r * light.intensity, + light.color.g * light.intensity, + light.color.b * light.intensity, + light.coreRatio, + ) + } + + fun addPointLight(pointLight: DynamicPointLight) { + pointLights += pointLight + } + + inline fun addPointLight(block: DynamicPointLight.() -> Unit): DynamicPointLight { + val light = DynamicPointLight() + light.block() + addPointLight(light) + return light + } + + fun removePointLight(light: DynamicPointLight) { + pointLights -= light + } + + fun addSpotLight(spotLight: DynamicSpotLight) { + spotLights.getOrPut(spotLight.maxSpotAngle) { mutableListOf() } += spotLight + val mesh = spotLightMeshes.getOrPut(spotLight.maxSpotAngle) { + SpotLightMesh(spotLight.maxSpotAngle).also { addNode(it.mesh) } + } + mesh.lights += spotLight + } + + inline fun addSpotLight(maxAngle: AngleF = 60f.deg, block: DynamicSpotLight.() -> Unit): DynamicSpotLight { + val light = DynamicSpotLight(maxAngle) + light.block() + addSpotLight(light) + return light + } + + fun removeSpotLight(light: DynamicSpotLight) { + spotLights[light.maxSpotAngle]?.remove(light) + } + + fun swapPipelineData(pipeline: Deferred2Pipeline) { + val gbuffer = pipeline.gbuffers.newVal + lightShader.swapPipelineData(gbuffer) { + depth = gbuffer.depth + normals = gbuffer.normals + albedoEmission = gbuffer.albedoEmission + metalRoughAo = gbuffer.metalRoughnessAo + camData = pipeline.camData + } + } + + private inner class SpotLightMesh(angle: AngleF) { + val lights = mutableListOf() + val instances = MeshInstanceList(DeferredLightInstanceLayout) + val mesh = Mesh( + layout = VertexLayouts.Position, + instances = instances, + name = "DeferredPointLights" + ).apply { + generate { makeSpotLightMesh(angle) } + shader = lightShader + } + } +} + +class DynamicPointLight( + val position: MutableVec3f = MutableVec3f(), + val color: MutableColor = MutableColor(Color.WHITE), + var radius: Float = 1f, + var intensity: Float = 1f, +) { + fun strengthByRadius(radius: Float) { + this.radius = radius + intensity = radius * radius + } + + fun strengthByIntensity(intensity: Float) { + this.intensity = intensity + radius = sqrt(intensity) + } +} + +class DynamicSpotLight(val maxSpotAngle: AngleF) { + val position = MutableVec3f() + val rotation = MutableQuatF() + var spotAngle = maxSpotAngle + var coreRatio = 0.5f + val color = MutableColor(Color.WHITE) + var radius = 1f + var intensity = 1f + + private val tmpDir = MutableVec3f() + + fun setDirection(direction: Vec3f) { + direction.normed(tmpDir) + val v = if (abs(tmpDir.dot(Vec3f.Y_AXIS)) > 0.9f) Vec3f.X_AXIS else Vec3f.Y_AXIS + val b = tmpDir.cross(v, MutableVec3f()) + val c = tmpDir.cross(b, MutableVec3f()) + Mat3f(tmpDir, b, c).getRotation(rotation) + } + + fun strengthByRadius(radius: Float) { + this.radius = radius + intensity = radius * radius + } + + fun strengthByIntensity(intensity: Float) { + this.intensity = intensity + radius = sqrt(intensity) + } +} + +object DeferredLightInstanceLayout : Struct("PointLightInstanceLayout", MemoryLayout.TightlyPacked) { + val modelMat = include(InstanceLayouts.ModelMat.modelMat) + val lightColor = float4("lightColor") + val encodedSpotAngle = float1("encodedSpotAngle") + val lightType = float1("lightType") +} + +private fun MeshBuilder<*>.makePointLightMesh() { + icoSphere { + steps = 0 + radius = 1.176f // required radius to fully include unit sphere at 0 subdivisions + } +} + +private fun MeshBuilder<*>.makeSpotLightMesh(angle: AngleF) { + val radius = 1.17f + val cAng = angle.deg.clamp(0f, 360f) / 2f + val steps = 8 + val belts = (steps / 2 * cAng / 180).toInt() + + // far cap + val iCenter = vertex(Vec3f(radius, 0f, 0f), Vec3f.X_AXIS) + for (i in 0 until steps) { + val a = min((360f / steps).toRad(), cAng.toRad()) + val x = cos(a) * radius + val r = sin(a) * radius + val y = sin(2 * PI * i / steps).toFloat() * r + val z = cos(2 * PI * i / steps).toFloat() * r + + val vp = Vec3f(x, y, z) + val iv = vertex(vp, vp) + if (i > 0) { + geometry.addTriIndices(iCenter, iv, iv - 1) + } + if (i == steps - 1) { + geometry.addTriIndices(iCenter, iv - steps + 1, iv) + } + } + + // belts + for (b in 0 until belts) { + val a = if (b < belts-1) { (360f / steps * (b + 2)).toRad() } else { cAng.toRad() } + for (i in 0 until steps) { + val x = cos(a) * radius + val r = sin(a) * radius + val y = sin(2 * PI * i / steps).toFloat() * r + val z = cos(2 * PI * i / steps).toFloat() * r + + val vp = Vec3f(x, y, z) + val iv = vertex(vp, vp) + if (i > 0) { + geometry.addTriIndices(iv, iv - 1, iv - steps) + geometry.addTriIndices(iv - 1, iv - steps - 1, iv - steps) + } + if (i == steps-1) { + geometry.addTriIndices(iv, iv - steps, iv - steps * 2 + 1) + geometry.addTriIndices(iv - steps + 1, iv, iv - steps * 2 + 1) + } + } + } + + // cone + val iOri = vertex(Vec3f(-0.1f, 0f, 0f), Vec3f.NEG_X_AXIS) + for (i in 0 until steps) { + if (i < steps - 1) { + geometry.addTriIndices(iOri, iOri - steps + i, iOri - steps + i + 1) + } else { + geometry.addTriIndices(iOri, iOri - 1, iOri - steps) + } + } + +} diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt index a1386a524..0c2f1adcb 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt @@ -34,34 +34,56 @@ class GbufferPass( val objectIds get() = colorTextures[3] val depth get() = depthTexture!! + internal val alphaMeshes = mutableListOf>() + internal val lightMeshes = mutableListOf>() + init { camera = pipeline.camera onAfterCollectDrawCommands += { viewData -> val upload = pipeline.reprojectMatrixComputePass.uploadData.newVal upload.viewProjMat.set(viewData.drawQueue.viewProjMatF) - upload.modelMats.limit = 0 - viewData.drawQueue.forEach { cmd -> + alphaMeshes.clear() + lightMeshes.clear() + + val drawQueue = viewData.drawQueue + val it = drawQueue.iterator() + while (it.hasNext()) { + val cmd = it.next() val mesh = cmd.mesh - (mesh.shader as? GbufferShader)?.let { gbufferShader -> - val idRange = pipeline.idAllocator.getIdRange(cmd.mesh) - val bufferPos = idRange.from * 16 - gbufferShader.objectId = idRange.from - upload.modelMats.limit = max(upload.modelMats.limit, bufferPos + idRange.size * 16) + if (!mesh.isOpaque) { + alphaMeshes += cmd.mesh + it.remove() + drawQueue.recycleDrawCommand(cmd) + + } else { + when (val shader = mesh.shader) { + is GbufferShader -> { + val idRange = pipeline.idAllocator.getIdRange(cmd.mesh) + val bufferPos = idRange.from * 16 + shader.objectId = idRange.from + upload.modelMats.limit = max(upload.modelMats.limit, bufferPos + idRange.size * 16) - val instances = mesh.instances - if (instances != null) { - val matrixExtractor = gbufferShader.config.instanceModelMatExtractor ?: DefaultInstanceModelMatrixExtractor - if (instances.numInstances > idRange.size) { - logE { "Mesh ${mesh.name} number of instances exceeds ID range: ${instances.numInstances} > ${idRange.size}" } + val instances = mesh.instances + if (instances != null) { + val matrixExtractor = shader.config.instanceModelMatExtractor ?: DefaultInstanceModelMatrixExtractor + if (instances.numInstances > idRange.size) { + logE { "Mesh ${mesh.name} number of instances exceeds ID range: ${instances.numInstances} > ${idRange.size}" } + } + for (i in 0 until instances.numInstances.coerceAtMost(idRange.size)) { + upload.modelMats.position = bufferPos + i * 16 + matrixExtractor.getModelMatrix(i, mesh, upload.modelMats) + } + } else { + upload.modelMats.position = bufferPos + cmd.modelMatF.putTo(upload.modelMats) + } } - for (i in 0 until instances.numInstances.coerceAtMost(idRange.size)) { - upload.modelMats.position = bufferPos + i * 16 - matrixExtractor.getModelMatrix(i, mesh, upload.modelMats) + is DeferredLightShader -> { + lightMeshes += cmd.mesh + it.remove() + drawQueue.recycleDrawCommand(cmd) } - } else { - upload.modelMats.position = bufferPos - cmd.modelMatF.putTo(upload.modelMats) } } } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt index d9f0a9a7d..e638a4524 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt @@ -42,6 +42,17 @@ class LightingPass( } drawNode.addNode(outputMesh) drawNode.addNode(Skybox.cube(pipeline.ibl.reflectionMap, 2f, colorSpaceConversion = ColorSpaceConversion.AsIs)) + + onAfterCollectDrawCommands += { viewData -> + val ctx = KoolSystem.requireContext() + val gbuffer = pipeline.gbuffers.newVal + for (i in gbuffer.lightMeshes.indices) { + val mesh = gbuffer.lightMeshes[i] + mesh.getOrCreatePipeline(ctx)?.let { pipeline -> + viewData.drawQueue.addMesh(mesh, pipeline) + } + } + } } fun swapBuffers() { @@ -107,7 +118,8 @@ class DeferredLightingShader(isScreenSpaceReflections: Boolean) : KslShader("def val camData = storage("camData", camDataLayout) main { - val baseCoord by (uv.output * depth.size().toFloat2()).toInt2() + val size by depth.size() + val baseCoord by (uv.output * size.toFloat2()).toInt2() val depthSample by depth.load(baseCoord, lod = 0.const).x `if` (depthSample eq 0f.const) { discard() @@ -117,7 +129,6 @@ class DeferredLightingShader(isScreenSpaceReflections: Boolean) : KslShader("def val camPos by camData.camPosition val invView by camData.invView val invViewProj by camData.invViewProj - val size by depth.size() val worldPos by unprojectBaseCoord(depthSample, baseCoord, size, camNear, invViewProj).xyz val ssao by aoMap.load(baseCoord, lod = 0.const).x From 2e1e418cf74e50b6a32ce6c6874de8a5832bc5f4 Mon Sep 17 00:00:00 2001 From: Max Thiele Date: Sun, 24 May 2026 22:03:45 +0200 Subject: [PATCH 18/27] Deferred pipeline helpers --- .../kotlin/de/fabmax/kool/demo/Demos.kt | 4 +- .../kool/demo/deferred2/Deferred2Pipeline.kt | 78 ++++++++++-- .../{Deferred2Demo.kt => Deferred2Test.kt} | 114 +++--------------- .../kool/demo/deferred2/DeferredLights.kt | 32 +++-- .../fabmax/kool/demo/deferred2/GbufferPass.kt | 29 +++-- .../kool/demo/deferred2/GbufferShader.kt | 21 ++++ .../kool/demo/deferred2/LightingPass.kt | 25 ++-- .../kool/demo/deferred2/TemporalFilterPass.kt | 9 +- 8 files changed, 171 insertions(+), 141 deletions(-) rename kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/{Deferred2Demo.kt => Deferred2Test.kt} (75%) diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/Demos.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/Demos.kt index b30e70ddb..d3bc19f8a 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/Demos.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/Demos.kt @@ -5,7 +5,7 @@ import de.fabmax.kool.KoolSystem import de.fabmax.kool.Platform import de.fabmax.kool.demo.bees.BeeDemo import de.fabmax.kool.demo.creativecoding.CreativeCodingDemo -import de.fabmax.kool.demo.deferred2.Deferred2Demo +import de.fabmax.kool.demo.deferred2.Deferred2Test import de.fabmax.kool.demo.helloworld.* import de.fabmax.kool.demo.pathtracing.PathTracingDemo import de.fabmax.kool.demo.pbr.PbrDemo @@ -66,7 +66,7 @@ object Demos { entry("gltf", "glTF Models") { GltfDemo() } entry("ssr", "Reflections") { ReflectionDemo() } entry("deferred", "Deferred Shading", NeedsComputeShaders) { DeferredDemo() } - entry("deferred2", "Deferred Shading 2", NeedsComputeShaders) { Deferred2Demo() } + entry("deferred2test", "Deferred 2 Test", NeedsComputeShaders) { Deferred2Test() } entry("procedural", "Procedural Roses", NeedsComputeShaders) { ProceduralDemo() } entry("pbr", "PBR Materials") { PbrDemo() } } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt index e9bb219bb..eebb8e59c 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt @@ -3,18 +3,17 @@ package de.fabmax.kool.demo.deferred2 import de.fabmax.kool.math.MutableMat4f import de.fabmax.kool.math.Vec2f import de.fabmax.kool.math.Vec2i +import de.fabmax.kool.modules.ksl.KslShader +import de.fabmax.kool.modules.ksl.blocks.ColorSpaceConversion +import de.fabmax.kool.modules.ksl.blocks.convertColorSpace import de.fabmax.kool.modules.ksl.lang.* -import de.fabmax.kool.pipeline.BufferedImageData2d -import de.fabmax.kool.pipeline.TexFormat -import de.fabmax.kool.pipeline.Texture2d +import de.fabmax.kool.pipeline.* +import de.fabmax.kool.pipeline.FullscreenShaderUtil.fullscreenQuadVertexStage +import de.fabmax.kool.pipeline.FullscreenShaderUtil.generateFullscreenQuad import de.fabmax.kool.pipeline.ao.AoRadius import de.fabmax.kool.pipeline.ao.ComputeAoPass import de.fabmax.kool.pipeline.ibl.EnvironmentMap -import de.fabmax.kool.pipeline.swapPipelineData -import de.fabmax.kool.scene.Lighting -import de.fabmax.kool.scene.Node -import de.fabmax.kool.scene.PerspectiveCamera -import de.fabmax.kool.scene.Scene +import de.fabmax.kool.scene.* import de.fabmax.kool.util.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -22,10 +21,11 @@ import kotlinx.coroutines.yield class Deferred2Pipeline( val content: Node, - private val scene: Scene, + val scene: Scene, val ibl: EnvironmentMap, val isScreenSpaceReflections: Boolean, val lighting: Lighting = Lighting(), + val maxGlobalLights: Int = 1, var renderScale: Float = 1f, var tsaa: List = TSAA_4, maxObjects: Int = 16384 @@ -267,3 +267,63 @@ context(_: KslScopeBuilder) val KslStructStorage.camNear: KslExprFloat1 get() = this[0.const][DeferredCamDataLayout.camNear] context(_: KslScopeBuilder) val KslStructStorage.frameIdx: KslExprInt1 get() = this[0.const][DeferredCamDataLayout.frameIdx] + +fun Deferred2Pipeline.installBloomPass(): BloomPass { + val bloomPass = BloomPass(filterPass.filterOutput.newVal) + bloomPass.isProfileGpu = true + scene.addComputePass(bloomPass) + onSwap { + val filterOutput = filterPass.filterOutput.newVal + bloomPass.inputShader.swapPipelineData(filterOutput) { + bloomPass.inputTexture = filterOutput + } + } + return bloomPass +} + +fun Deferred2Pipeline.defaultOutputQuad(bloomPass: BloomPass?): Mesh<*> { + val outputShader = defaultOutputShader(bloomPass) + return TextureMesh().apply { + generate { + generateFullscreenQuad() + } + shader = outputShader + } +} + +fun Deferred2Pipeline.defaultOutputShader( + bloomPass: BloomPass?, +): KslShader { + val outputShader = KslShader("deferred2-output") { + val uv = interStageFloat2() + fullscreenQuadVertexStage(uv) + fragmentStage { + val output = texture2d("deferredOutput") + val bloom = texture2d("bloomOutput") + val ditherTex = texture2d("ditherPattern") + + main { + val uvi = (uv.output * output.size().toFloat2()).toInt2() + val color by output.sample(uv.output).rgb + bloom.sample(uv.output).rgb + val ditherC by uvi % ditherTex.size() + val ditherNoise by ditherTex.load(ditherC).r + val srgb by convertColorSpace(color, ColorSpaceConversion.LinearToSrgbHdr()) + (ditherNoise - 0.5f.const) / 255f.const + colorOutput(srgb) + } + } + } + + val ditherTex = makeDitherPattern() + ditherTex.releaseWith(filterPass) + outputShader.bindTexture2d("ditherPattern", ditherTex) + val bloomMap = bloomPass?.bloomMap ?: SingleColorTexture(Color.BLACK) + outputShader.bindTexture2d("bloomOutput", bloomMap) + var inputTex by outputShader.bindTexture2d("deferredOutput", defaultSampler = SamplerSettings().nearest().clamped()) + onSwap { + val filterOutput = filterPass.filterOutput.newVal + outputShader.swapPipelineData(filterOutput) { + inputTex = filterOutput + } + } + return outputShader +} diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt similarity index 75% rename from kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt rename to kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt index 369eca455..ad46a6cbd 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Demo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt @@ -5,29 +5,23 @@ import de.fabmax.kool.demo.* import de.fabmax.kool.demo.menu.DemoMenu import de.fabmax.kool.math.* import de.fabmax.kool.modules.gltf.GltfLoadConfig -import de.fabmax.kool.modules.ksl.KslShader import de.fabmax.kool.modules.ksl.blocks.ColorBlockConfig -import de.fabmax.kool.modules.ksl.blocks.ColorSpaceConversion -import de.fabmax.kool.modules.ksl.blocks.convertColorSpace -import de.fabmax.kool.modules.ksl.lang.* import de.fabmax.kool.modules.ui2.* import de.fabmax.kool.pipeline.BloomPass -import de.fabmax.kool.pipeline.FullscreenShaderUtil.fullscreenQuadVertexStage -import de.fabmax.kool.pipeline.FullscreenShaderUtil.generateFullscreenQuad -import de.fabmax.kool.pipeline.SamplerSettings -import de.fabmax.kool.pipeline.SingleColorTexture import de.fabmax.kool.pipeline.ao.AoRadius -import de.fabmax.kool.pipeline.swapPipelineData import de.fabmax.kool.scene.* import de.fabmax.kool.toString -import de.fabmax.kool.util.* +import de.fabmax.kool.util.Color +import de.fabmax.kool.util.MdColor +import de.fabmax.kool.util.Time +import de.fabmax.kool.util.l import kotlin.math.abs import kotlin.math.ceil import kotlin.math.round import kotlin.math.sin import kotlin.random.Random -class Deferred2Demo : DemoScene("Deferred2 Demo") { +class Deferred2Test : DemoScene("Deferred2 Test") { val ibl by hdriImage("${DemoLoader.hdriPath}/newport_loft.rgbe.png") // val ibl by hdriImage("${DemoLoader.hdriPath}/shanghai_bund_1k.rgbe.png") @@ -57,44 +51,30 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { private var gpuTimesAccu = GpuTimes() override fun Scene.setupMainScene(ctx: KoolContext) { - val deferredLights = DeferredLights() - val content = deferredContent(deferredLights) + val content = deferredContent() val lighting = Lighting().apply { clear() -// singlePointLight { -// setup(Vec3f(1.2f, 3.2f, 2f)) -// setColor(Color.WHITE, intensity = 5f) -// } } - pipeline = Deferred2Pipeline(content, scene = this, ibl, isScreenSpaceReflections = true, lighting) + pipeline = Deferred2Pipeline(content, scene = this, ibl, isScreenSpaceReflections = true, lighting = lighting) pipeline.renderScale = 0.5f pipeline.aoPass.radius = AoRadius.relativeRadius(1/20f) filterWeight.value = pipeline.filterPass.filterWeight.toInt() filterWeight.onChange { _, value -> pipeline.filterPass.filterWeight = value.toFloat() } - bloomPass = BloomPass(pipeline.filterPass.filterOutput.newVal) + bloomPass = pipeline.installBloomPass() bloomPass.isProfileGpu = true - addComputePass(bloomPass) - bloom.onChange { _, value -> bloomPass.isEnabled = value } - pipeline.onSwap { - val filterOutput = pipeline.filterPass.filterOutput.newVal - bloomPass.inputShader.swapPipelineData(filterOutput) { - bloomPass.inputTexture = filterOutput - } - } + addNode(pipeline.defaultOutputQuad(bloomPass)) + val deferredLights = DeferredLights(pipeline) content.apply { val orbitCam = orbitCamera(pipeline.camera) { setRotation(100f, -7f) } addNode(orbitCam) - //addNode(pointLights) + addNode(deferredLights) } - pipeline.onSwap { - deferredLights.swapPipelineData(pipeline) - } val r = Random(1234) repeat(50) { deferredLights.addPointLight { @@ -110,13 +90,6 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { } } - addTextureMesh { - generate { - generateFullscreenQuad() - } - shader = deferredOutputShader(this@Deferred2Demo, pipeline, bloomPass) - } - val nAccu = 50 onUpdate { gpuTimesAccu = GpuTimes( @@ -134,10 +107,9 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { } } - private fun deferredContent(pointLights: DeferredLights) = Node("deferred content").apply { + private fun deferredContent() = Node("deferred content").apply { addGroup { transform.rotate(45f.deg, Vec3f.Y_AXIS) - addNode(pointLights) addColorMesh { generate { color = MdColor.PINK.toLinear() @@ -154,17 +126,15 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { uniformColor(uniformName = "uBaseCol", blendMode = ColorBlockConfig.BlendMode.Multiply) } roughness { constProperty(0.15f) } - emission { uniformProperty(10f, "uEmi") } + emission { uniformProperty(10f) } }.apply { - var baseColFac by bindUniformColor("uBaseCol") - var emi by bindUniformFloat1("uEmi") onUpdate { val str = 10f //(sin(Time.gameTime.toFloat() * 4f) + 1f) * 16f var e = str e = ceil(e * 4f) / 4f val b = if (e > 0f) round(str / e * 255f) / 255f else 0f - baseColFac = Color(b, b, b) - emi = e + color = Color(b, b, b) + emission = e } } } @@ -199,12 +169,6 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { metallic { textureProperty(metallicMap) } roughness { textureProperty(roughnessMap) } ao { textureProperty(aoMap) } - }.apply { - bindTexture2d("tbaseColor", albedoMap) - bindTexture2d("tNormalMap", normalMap) - bindTexture2d("tmetallic", metallicMap) - bindTexture2d("troughness", roughnessMap) - bindTexture2d("tao", aoMap) } } @@ -252,12 +216,10 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { color { textureColor(uvChecker) } - roughness { uniformProperty(groundRoughness.value, "uRough") } + roughness { uniformProperty(groundRoughness.value) } }.apply { - bindTexture2d("tbaseColor", uvChecker) - var rough by bindUniformFloat1("uRough", groundRoughness.value) - groundRoughness.onChange { _, newValue -> rough = newValue } + groundRoughness.onChange { _, newValue -> roughness = newValue } } } @@ -339,48 +301,6 @@ class Deferred2Demo : DemoScene("Deferred2 Demo") { } } -private fun deferredOutputShader( - demo: Deferred2Demo, - deferred2Pipeline: Deferred2Pipeline, - bloomPass: BloomPass -): KslShader { - val outputShader = KslShader("deferred2-output") { - val uv = interStageFloat2() - fullscreenQuadVertexStage(uv) - fragmentStage { - main { - val output = texture2d("deferredOutput") - val bloom = texture2d("bloomOutput") - val uvi = (uv.output * output.size().toFloat2()).toInt2() - val color by output.sample(uv.output).rgb + bloom.sample(uv.output).rgb - - val ditherTex = texture2d("ditherPattern") - val ditherC by uvi % ditherTex.size() - val ditherNoise by ditherTex.load(ditherC).r - val srgb by convertColorSpace(color, ColorSpaceConversion.LinearToSrgbHdr()) + (ditherNoise - 0.5f.const) / 255f.const - colorOutput(srgb) -// colorOutput(color) - } - } - } - - val ditherTex = makeDitherPattern() - ditherTex.releaseWith(demo.mainScene) - - outputShader.bindTexture2d("ditherPattern", ditherTex) - var bloomTex by outputShader.bindTexture2d("bloomOutput", bloomPass.bloomMap) - var inputTex by outputShader.bindTexture2d("deferredOutput", defaultSampler = SamplerSettings().nearest().clamped()) - val noBloom = SingleColorTexture(Color.BLACK) - deferred2Pipeline.onSwap { - val filterOutput = deferred2Pipeline.filterPass.filterOutput.newVal - outputShader.swapPipelineData(filterOutput) { - inputTex = filterOutput - bloomTex = if (bloomPass.isEnabled) bloomPass.bloomMap else noBloom - } - } - return outputShader -} - private data class TsaaItem(val label: String, val tsaa: List, val numSamples: Int) { companion object { val items = listOf( diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/DeferredLights.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/DeferredLights.kt index 8663da8ff..4b75d5bc3 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/DeferredLights.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/DeferredLights.kt @@ -7,7 +7,11 @@ import de.fabmax.kool.scene.geometry.MeshBuilder import de.fabmax.kool.util.* import kotlin.math.* -class DeferredLights(val isDynamic: Boolean = true, name: String = "deferred-lights") : Node(name) { +class DeferredLights( + pipeline: Deferred2Pipeline, + val isDynamic: Boolean = true, + name: String = "deferred-lights", +) : Node(name) { val pointLights: List field = mutableListOf() val spotLights: Map> @@ -18,8 +22,8 @@ class DeferredLights(val isDynamic: Boolean = true, name: String = "deferred-lig val lightShader = DeferredLightShader() - private val spotLightMeshes = mutableMapOf() - private val pointLightMesh = Mesh( + val spotLightMeshes = mutableMapOf() + val pointLightMesh = Mesh( layout = VertexLayouts.Position, instances = pointLightInstances, name = "DeferredPointLights" @@ -29,13 +33,24 @@ class DeferredLights(val isDynamic: Boolean = true, name: String = "deferred-lig } init { + pipeline.onSwap { swapPipelineData(pipeline) } addNode(pointLightMesh) if (isDynamic) { - onUpdate { updateLightData() } + onUpdate { updateLightInstanceData() } } } - fun updateLightData() { + fun clear() { + pointLights.clear() + pointLightInstances.clear() + spotLights.clear() + spotLightMeshes.values.forEach { + it.lights.clear() + it.instances.clear() + } + } + + fun updateLightInstanceData() { pointLightInstances.clear() pointLightInstances.addInstances(pointLights.size) { buf -> for (i in 0 until pointLights.size) { @@ -78,8 +93,6 @@ class DeferredLights(val isDynamic: Boolean = true, name: String = "deferred-lig modelMat.scale(light.radius) set(DeferredLightInstanceLayout.modelMat, modelMat) - //println(cos(min(light.maxSpotAngle.rad, light.spotAngle.rad) / 2f)) - set(DeferredLightInstanceLayout.lightType, Light.Spot.ENCODING) set(DeferredLightInstanceLayout.encodedSpotAngle, cos(min(light.maxSpotAngle.rad, light.spotAngle.rad) / 2f)) set(DeferredLightInstanceLayout.lightColor, @@ -102,6 +115,7 @@ class DeferredLights(val isDynamic: Boolean = true, name: String = "deferred-lig } fun removePointLight(light: DynamicPointLight) { + // fixme this is very slow for many lights pointLights -= light } @@ -121,7 +135,9 @@ class DeferredLights(val isDynamic: Boolean = true, name: String = "deferred-lig } fun removeSpotLight(light: DynamicSpotLight) { + // fixme this is very slow for many lights spotLights[light.maxSpotAngle]?.remove(light) + spotLightMeshes[light.maxSpotAngle]?.lights?.remove(light) } fun swapPipelineData(pipeline: Deferred2Pipeline) { @@ -135,7 +151,7 @@ class DeferredLights(val isDynamic: Boolean = true, name: String = "deferred-lig } } - private inner class SpotLightMesh(angle: AngleF) { + inner class SpotLightMesh(angle: AngleF) { val lights = mutableListOf() val instances = MeshInstanceList(DeferredLightInstanceLayout) val mesh = Mesh( diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt index 0c2f1adcb..66a5a2a35 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt @@ -64,19 +64,24 @@ class GbufferPass( shader.objectId = idRange.from upload.modelMats.limit = max(upload.modelMats.limit, bufferPos + idRange.size * 16) - val instances = mesh.instances - if (instances != null) { - val matrixExtractor = shader.config.instanceModelMatExtractor ?: DefaultInstanceModelMatrixExtractor - if (instances.numInstances > idRange.size) { - logE { "Mesh ${mesh.name} number of instances exceeds ID range: ${instances.numInstances} > ${idRange.size}" } + try { + val instances = mesh.instances + if (instances != null) { + val matrixExtractor = shader.config.instanceModelMatExtractor ?: DefaultInstanceModelMatrixExtractor + if (instances.numInstances > idRange.size) { + logE { "Mesh ${mesh.name} number of instances exceeds ID range: ${instances.numInstances} > ${idRange.size}" } + } + for (i in 0 until instances.numInstances.coerceAtMost(idRange.size)) { + upload.modelMats.position = bufferPos + i * 16 + matrixExtractor.getModelMatrix(i, mesh, upload.modelMats) + } + } else { + upload.modelMats.position = bufferPos + cmd.modelMatF.putTo(upload.modelMats) } - for (i in 0 until instances.numInstances.coerceAtMost(idRange.size)) { - upload.modelMats.position = bufferPos + i * 16 - matrixExtractor.getModelMatrix(i, mesh, upload.modelMats) - } - } else { - upload.modelMats.position = bufferPos - cmd.modelMatF.putTo(upload.modelMats) + } catch (e: Exception) { + logE { "Error updating model matrices" } + e.printStackTrace() } } is DeferredLightShader -> { diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferShader.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferShader.kt index e4a37ce37..f1cb31669 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferShader.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferShader.kt @@ -21,6 +21,27 @@ fun gbufferShader(block: GbufferShaderConfig.Builder.() -> Unit): GbufferShader } class GbufferShader(val config: GbufferShaderConfig) : KslShader("deferred2-gbuffer-shader") { + var color: Color by bindColorUniform(config.colorCfg) + var colorMap: Texture2d? by bindColorTexture(config.colorCfg) + + var normalMap: Texture2d? by bindTexture2d(config.normalMapCfg.textureName, config.normalMapCfg.defaultNormalMap) + var normalMapStrength: Float by bindPropertyUniform(config.normalMapCfg.strengthCfg) + + var displacement: Float by bindPropertyUniform(config.vertexCfg.displacementCfg) + var displacementMap: Texture2d? by bindPropertyTexture(config.vertexCfg.displacementCfg) + + var emission: Float by bindPropertyUniform(config.emissionCfg) + var emissionMap: Texture2d? by bindPropertyTexture(config.emissionCfg) + + var materialAo: Float by bindPropertyUniform(config.aoCfg) + var materialAoMap: Texture2d? by bindPropertyTexture(config.aoCfg) + + var metallic: Float by bindPropertyUniform(config.metallicCfg) + var metallicMap: Texture2d? by bindPropertyTexture(config.metallicCfg) + + var roughness: Float by bindPropertyUniform(config.roughnessCfg) + var roughnessMap: Texture2d? by bindPropertyTexture(config.roughnessCfg) + var objectId: Int by bindUniformInt1("uObjectId") init { diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt index e638a4524..916bdf19e 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt @@ -30,7 +30,7 @@ class LightingPass( ) { val lightingOutput: Texture2d get() = colorTexture!! - private val lightingShader = DeferredLightingShader(pipeline.isScreenSpaceReflections) + private val lightingShader = DeferredLightingShader(pipeline.isScreenSpaceReflections, pipeline.maxGlobalLights) init { camera = pipeline.camera @@ -72,7 +72,10 @@ class LightingPass( } } -class DeferredLightingShader(isScreenSpaceReflections: Boolean) : KslShader("deferred2-lighting") { +class DeferredLightingShader( + isScreenSpaceReflections: Boolean, + maxGlobalLights: Int, +) : KslShader("deferred2-lighting") { var depthTex by bindTexture2d("depth") var scaledViewZ by bindTexture2d("scaledViewZ") var encodedNormals by bindTexture2d("encodedNormals") @@ -94,12 +97,15 @@ class DeferredLightingShader(isScreenSpaceReflections: Boolean) : KslShader("def cullMethod = CullMethod.NO_CULLING, depthTest = DepthCompareOp.ALWAYS ) - program.program(isScreenSpaceReflections) + program.program(isScreenSpaceReflections, maxGlobalLights) bindTexture1d("tgradient", GradientTexture(ColorGradient.ROCKET)) } - private fun KslProgram.program(isScreenSpaceReflections: Boolean) { + private fun KslProgram.program( + isScreenSpaceReflections: Boolean, + maxGlobalLights: Int, + ) { val uv = interStageFloat2() fullscreenQuadVertexStage(uv) fragmentStage { @@ -147,12 +153,10 @@ class DeferredLightingShader(isScreenSpaceReflections: Boolean) : KslShader("def val ambient by irradiance.sample(worldNormal).rgb * ssao - // todo: config max lights - val maxNumberOfLights = 4 val normalLightRange = NormalLightRange.ZeroToOne - val shadowFactors = float1Array(maxNumberOfLights, 1f.const) - val lightData = sceneLightData(maxNumberOfLights) - val material = pbrMaterialBlock(maxNumberOfLights, listOf(reflection), brdf, normalLightRange) { + val shadowFactors = float1Array(maxGlobalLights, 1f.const) + val lightData = sceneLightData(maxGlobalLights) + val material = pbrMaterialBlock(maxGlobalLights, listOf(reflection), brdf, normalLightRange) { inCamPos(camPos) inNormal(worldNormal) inFragmentPos(worldPos) @@ -173,9 +177,8 @@ class DeferredLightingShader(isScreenSpaceReflections: Boolean) : KslShader("def val screenReflection by screenReflect(material, viewNormal, scaledViewZ, oldColor, camData) val finalColor by material.outAmbient + material.outLight + screenReflection + albedo * emissiveStrength colorOutput(finalColor) -// colorOutput(screenReflection) } else { - colorOutput(material.outColor) + colorOutput(material.outColor + albedo * emissiveStrength) } outDepth set depthSample diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt index 6cbea515f..1637015e5 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt @@ -112,6 +112,12 @@ class TemporalFilterShader( main { val baseCoord by inGlobalInvocationId.xy.toInt2() + val newColor by lightingOutput.load(baseCoord).rgb + `if` (filterWeight eq 0f.const) { + newFilter[baseCoord] = float4Value(newColor, 1f) + `return`() + } + val curMeta by newMeta.load(baseCoord).r val id by curMeta and 0xffffff.const val size by newDepth.size() @@ -186,9 +192,8 @@ class TemporalFilterShader( w set 0f.const } - val curColor by lightingOutput.load(baseCoord).rgb val oldColor by oldFilter.sample(oldUv).rgb - val curSrgb by convertColorSpace(curColor, ColorSpaceConversion.LinearToSrgb()) + val curSrgb by convertColorSpace(newColor, ColorSpaceConversion.LinearToSrgb()) val oldSrgb by convertColorSpace(oldColor, ColorSpaceConversion.LinearToSrgb()) val weighted by (oldSrgb * w + curSrgb) / (w + 1f.const) val filtered by convertColorSpace(weighted, ColorSpaceConversion.SrgbToLinear()) From dfae51ad902079898e5dd6b21b35e01ea264b6d0 Mon Sep 17 00:00:00 2001 From: Max Thiele Date: Sun, 24 May 2026 22:38:26 +0200 Subject: [PATCH 19/27] Use new deferred pipeline in demos --- .../de/fabmax/kool/modules/gltf/GltfConfig.kt | 7 +- .../kool/modules/ksl/blocks/NormalMapBlock.kt | 9 + .../de/fabmax/kool/scene/ModelTemplate.kt | 13 +- .../de/fabmax/kool/util/Buffer.desktop.kt | 11 + .../de/fabmax/kool/demo/DeferredDemo.kt | 268 +++++++++--------- .../kotlin/de/fabmax/kool/demo/GltfDemo.kt | 95 ++++--- 6 files changed, 218 insertions(+), 185 deletions(-) diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/gltf/GltfConfig.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/gltf/GltfConfig.kt index bc68db0c1..99fc41f69 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/gltf/GltfConfig.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/gltf/GltfConfig.kt @@ -2,8 +2,10 @@ package de.fabmax.kool.modules.gltf import de.fabmax.kool.AssetLoader import de.fabmax.kool.modules.ksl.KslPbrShader +import de.fabmax.kool.modules.ksl.KslShader import de.fabmax.kool.modules.ksl.ModelMatrixComposition import de.fabmax.kool.pipeline.Texture2d +import de.fabmax.kool.pipeline.deferred.DeferredKslPbrShader import de.fabmax.kool.pipeline.ibl.EnvironmentMap import de.fabmax.kool.util.ShadowMap import de.fabmax.kool.util.Struct @@ -22,7 +24,7 @@ data class GltfLoadConfig( val sortNodesByAlpha: Boolean = true, val instanceLayout: Struct? = null, val assetLoader: AssetLoader? = null, - val pbrBlock: (KslPbrShader.Config.Builder.(GltfMesh.Primitive) -> Unit)? = null + val pbrBlock: (KslPbrShader.Config.Builder.(GltfMesh.Primitive) -> Unit)? = null, ) data class GltfMaterialConfig( @@ -32,5 +34,6 @@ data class GltfMaterialConfig( val isDeferredShading: Boolean = false, val maxNumberOfLights: Int = 4, val fixedNumberOfJoints: Int = 0, - val modelMatrixComposition: List = emptyList() + val modelMatrixComposition: List = emptyList(), + val shaderFactory: ((DeferredKslPbrShader.Config) -> KslShader)? = null, ) diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/NormalMapBlock.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/NormalMapBlock.kt index 5aba874da..e4ecb1435 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/NormalMapBlock.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/NormalMapBlock.kt @@ -77,6 +77,15 @@ data class NormalMapConfig( var arrayIndex = -1 val strengthCfg: PropertyBlockConfig.Builder = PropertyBlockConfig.Builder("${normalMapName}_strength").constProperty(1f) + fun set(other: NormalMapConfig) { + isNormalMapped = other.isNormalMapped + defaultNormalMap = other.defaultNormalMap + defaultArrayNormalMap = other.defaultArrayNormalMap + arrayIndex = other.normalMapArrayIndex + strengthCfg.propertySources.clear() + strengthCfg.propertySources.addAll(other.strengthCfg.propertySources) + } + fun clearNormalMap(): Builder { isNormalMapped = false defaultNormalMap = null diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/ModelTemplate.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/ModelTemplate.kt index fc961c306..373706e03 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/ModelTemplate.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/ModelTemplate.kt @@ -549,11 +549,14 @@ class ModelTemplate(val scene: GltfScene, val gltfFile: GltfFile) : BaseReleasab vertexCfg.displacementCfg.primaryTexture?.defaultTexture?.let { textures[it.name] = it } } - val shader = if (isDeferred) { - pbrConfig.pipelineCfg.blendMode = BlendMode.DISABLED - DeferredKslPbrShader(pbrConfig.build()) - } else { - KslPbrShader(pbrConfig.build()) + val shaderFactory = cfg.materialConfig.shaderFactory + val shader = when { + shaderFactory != null -> shaderFactory(pbrConfig.build()) + isDeferred -> { + pbrConfig.pipelineCfg.blendMode = BlendMode.DISABLED + DeferredKslPbrShader(pbrConfig.build()) + } + else -> KslPbrShader(pbrConfig.build()) } val depthShader = if (pbrConfig.alphaMode is AlphaMode.Mask) { DepthShader.Config.forMesh( diff --git a/kool-core/src/desktopMain/kotlin/de/fabmax/kool/util/Buffer.desktop.kt b/kool-core/src/desktopMain/kotlin/de/fabmax/kool/util/Buffer.desktop.kt index ddb575aec..fe4055488 100644 --- a/kool-core/src/desktopMain/kotlin/de/fabmax/kool/util/Buffer.desktop.kt +++ b/kool-core/src/desktopMain/kotlin/de/fabmax/kool/util/Buffer.desktop.kt @@ -24,6 +24,8 @@ abstract class GenericBuffer( protected val buffer: B, isAutoLimit: Boolean ) : Buffer { + @PublishedApi + internal var modCount = 0 override var isAutoLimit: Boolean = isAutoLimit set(value) { @@ -70,8 +72,13 @@ abstract class GenericBuffer( } inline fun useRaw(block: (B) -> R): R { + val modBefore = modCount val result = block(getRawBuffer()) finishRawBuffer() + val modAfter = modCount + if (modBefore != modAfter) { + logE { "Buffer was modified while used raw" } + } return result } } @@ -203,17 +210,20 @@ class Float32BufferImpl(buffer: FloatBuffer, isAutoLimit: Boolean = false) : override fun set(i: Int, value: Float) { buffer.put(i, value) + modCount++ } override fun put(value: Float): Float32Buffer { buffer.put(value) pos++ + modCount++ return this } override fun put(data: FloatArray, offset: Int, len: Int): Float32Buffer { buffer.put(data, offset, len) pos += len + modCount++ return this } @@ -222,6 +232,7 @@ class Float32BufferImpl(buffer: FloatBuffer, isAutoLimit: Boolean = false) : buffer.put(it) pos += data.limit } + modCount++ return this } } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/DeferredDemo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/DeferredDemo.kt index a9efb6f83..ec14a3a20 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/DeferredDemo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/DeferredDemo.kt @@ -1,16 +1,17 @@ package de.fabmax.kool.demo import de.fabmax.kool.KoolContext +import de.fabmax.kool.demo.deferred2.* import de.fabmax.kool.demo.menu.DemoMenu import de.fabmax.kool.math.* import de.fabmax.kool.modules.ksl.KslUnlitShader import de.fabmax.kool.modules.ksl.UnlitShaderConfig -import de.fabmax.kool.modules.ksl.blocks.ColorBlockConfig import de.fabmax.kool.modules.ksl.blocks.ColorSpaceConversion import de.fabmax.kool.modules.ksl.lang.* import de.fabmax.kool.modules.ui2.* +import de.fabmax.kool.pipeline.BloomPass import de.fabmax.kool.pipeline.DepthCompareOp -import de.fabmax.kool.pipeline.deferred.* +import de.fabmax.kool.pipeline.ao.AoRadius import de.fabmax.kool.scene.* import de.fabmax.kool.scene.geometry.IndexedVertexList import de.fabmax.kool.scene.geometry.MeshBuilder @@ -23,10 +24,11 @@ import kotlin.random.Random class DeferredDemo : DemoScene("Deferred Shading") { - private lateinit var deferredPipeline: DeferredPipeline + private lateinit var pipeline: Deferred2Pipeline + private lateinit var bloomPass: BloomPass private lateinit var objects: ColorMesh - private lateinit var objectShader: DeferredKslPbrShader + private lateinit var objectShader: GbufferShader private lateinit var lightPositionMesh: Mesh private lateinit var lightVolumeMesh: LineMesh @@ -37,24 +39,22 @@ class DeferredDemo : DemoScene("Deferred Shading") { private val isShowMaps = mutableStateOf(false) private val isAutoRotate = mutableStateOf(true) - private val lightCount = mutableStateOf(2000) - private val lightPower = mutableStateOf(1f) - private val lightRadius = mutableStateOf(1f) + private val lightCount = mutableStateOf(1250) + private val lightPower = mutableStateOf(1.5f) + private val lightRadius = mutableStateOf(2f) private val isObjects = mutableStateOf(true).onChange { _, new -> objects.isVisible = new } private val isLightBodies = mutableStateOf(true).onChange { _, new -> lightPositionMesh.isVisible = new } private val isLightVolumes = mutableStateOf(false).onChange { _, new -> lightVolumeMesh.isVisible = new } private val roughness = mutableStateOf(0.15f).onChange { _, new -> objectShader.roughness = new } - private val bloomStrength = mutableStateOf(1f).onChange { _, new -> deferredPipeline.bloomStrength = new } - private val bloomRadius = mutableStateOf(1f).onChange { _, new -> deferredPipeline.bloomRadius = new } - private val bloomThreshold = mutableStateOf(0.5f).onChange { _, new -> deferredPipeline.bloomThreshold = new } - private val ibl by hdriSingleColor(Color(0.22f, 0.22f, 0.22f)) + private val ibl by hdriSingleColor(Color(0.18f, 0.18f, 0.18f)) private val groundColor by texture2d("${DemoLoader.materialPath}/futuristic-panels1/futuristic-panels1-albedo1.jpg") private val groundNormals by texture2d("${DemoLoader.materialPath}/futuristic-panels1/futuristic-panels1-normal.jpg") private val groundRoughness by texture2d("${DemoLoader.materialPath}/futuristic-panels1/futuristic-panels1-roughness.jpg") private val groundMetallic by texture2d("${DemoLoader.materialPath}/futuristic-panels1/futuristic-panels1-metallic.jpg") private val groundAo by texture2d("${DemoLoader.materialPath}/futuristic-panels1/futuristic-panels1-ao.jpg") + private lateinit var deferredLights: DeferredLights private val lights = mutableListOf() private val colorMap = listOf( @@ -76,7 +76,26 @@ class DeferredDemo : DemoScene("Deferred Shading") { } override fun Scene.setupMainScene(ctx: KoolContext) { - orbitCamera { + // don't use any global lights + lighting.clear() + + val content = Node() + content.makeContent() + + pipeline = Deferred2Pipeline(content, scene = this, ibl, isScreenSpaceReflections = false, lighting = lighting) + pipeline.renderScale = 1f / UiScale.windowScale.value + //pipeline.filterPass.filterWeight = 0f + pipeline.aoPass.radius = AoRadius.absoluteRadius(1.5f) + + bloomPass = pipeline.installBloomPass() + bloomPass.isProfileGpu = true + + deferredLights = DeferredLights(pipeline) + content.addNode(deferredLights) + + addNode(pipeline.defaultOutputQuad(bloomPass)) + + val cam = orbitCamera(pipeline.camera) { // Set some initial rotation so that we look down on the scene setRotation(0f, -40f) setZoom(28.0, max = 50.0) @@ -88,32 +107,8 @@ class DeferredDemo : DemoScene("Deferred Shading") { } } } + content.addNode(cam) - // don't use any global lights - lighting.clear() - - val defCfg = DeferredPipelineConfig().apply { - maxGlobalLights = 1 - isWithAmbientOcclusion = true - isWithScreenSpaceReflections = false - isWithImageBasedLighting = false - isWithBloom = true - isWithVignette = true - isWithChromaticAberration = true - - // set output depth compare op to ALWAYS, so that the skybox with maximum depth value is drawn - outputDepthTest = DepthCompareOp.ALWAYS - } - deferredPipeline = DeferredPipeline(this, defCfg) - deferredPipeline.apply { - bloomRadius = this@DeferredDemo.bloomRadius.value - bloomStrength = this@DeferredDemo.bloomStrength.value - bloomThreshold = this@DeferredDemo.bloomThreshold.value - - lightingPassContent += Skybox.cube(ibl.reflectionMap, 1f, colorSpaceConversion = ColorSpaceConversion.AsIs) - } - deferredPipeline.sceneContent.makeContent() - addNode(deferredPipeline.createDefaultOutputQuad()) makeLightOverlays() onUpdate += { @@ -123,7 +118,7 @@ class DeferredDemo : DemoScene("Deferred Shading") { private fun Scene.makeLightOverlays() { apply { - lightVolumeMesh = addWireframeMesh(deferredPipeline.dynamicPointLights.mesh.geometry, instances = lightVolInsts).apply { + lightVolumeMesh = pipeline.lightingPass.drawNode.addWireframeMesh(deferredLights.pointLightMesh.geometry, instances = lightVolInsts).apply { isFrustumChecked = false isVisible = false isCastingShadow = false @@ -140,7 +135,7 @@ class DeferredDemo : DemoScene("Deferred Shading") { lightPosInsts.clear() lightVolInsts.clear() - deferredPipeline.dynamicPointLights.lightInstances.forEach { light -> + deferredLights.pointLights.forEach { light -> lightModelMat.setIdentity() lightModelMat.translate(light.position) @@ -200,17 +195,14 @@ class DeferredDemo : DemoScene("Deferred Shading") { } } } - objectShader = deferredKslPbrShader { + objectShader = gbufferShader { color { vertexColor() } - roughness(0.15f) + roughness { uniformProperty(roughness.value) } } shader = objectShader } lightPositionMesh = addMesh(VertexLayouts.PositionNormal, instances = lightPosInsts) { - isFrustumChecked = false - isVisible = true - isCastingShadow = false generate { icoSphere { steps = 1 @@ -218,12 +210,10 @@ class DeferredDemo : DemoScene("Deferred Shading") { center.set(Vec3f.ZERO) } } - shader = deferredKslPbrShader { + shader = gbufferShader { vertices { instancedModelMatrix() } - emission { - instanceColor(InstanceLayouts.ModelMatColor.color) - constColor(Color(6f, 6f, 6f), blendMode = ColorBlockConfig.BlendMode.Multiply) - } + color { instanceColor(InstanceLayouts.ModelMatColor.color) } + emission { constProperty(6f) } } } @@ -237,7 +227,7 @@ class DeferredDemo : DemoScene("Deferred Shading") { } } - shader = deferredKslPbrShader { + shader = gbufferShader { color { textureColor(groundColor) } normalMapping { useNormalMap(groundNormals) } roughness { textureProperty(groundRoughness) } @@ -263,18 +253,23 @@ class DeferredDemo : DemoScene("Deferred Shading") { if (forced) { lights.clear() - deferredPipeline.dynamicPointLights.lightInstances.clear() + deferredLights.clear() } else { while (lights.size > lightCount.value) { - lights.removeAt(lights.lastIndex) - deferredPipeline.dynamicPointLights.lightInstances.removeAt(deferredPipeline.dynamicPointLights.lightInstances.lastIndex) + val l = lights.removeAt(lights.lastIndex) + deferredLights.removePointLight(l.light) } } while (lights.size < lightCount.value) { val grp = lightGroups[rand.randomI(lightGroups.indices)] val x = rand.randomI(0 until grp.rows) - val light = deferredPipeline.dynamicPointLights.addPointLight { } + val r = Random(1337) + val light = deferredLights.addPointLight { + color.set(MdColor.PALETTE.random(r).toLinear()) + position.set(Vec3f(r.randomF(-15f, 15f), 1.5f, r.randomF(-15f, 15f))) + strengthByIntensity(r.randomF(5f, 20f)) + } val animLight = AnimatedLight(light).apply { startColor = colorMap[colorMapIdx.value].getColor(lights.size).toLinear() desiredColor = startColor @@ -285,7 +280,7 @@ class DeferredDemo : DemoScene("Deferred Shading") { } updateLightColors() - deferredPipeline.dynamicPointLights.lightInstances.forEach { + deferredLights.pointLights.forEach { it.radius = lightRadius.value it.intensity = lightPower.value } @@ -334,20 +329,19 @@ class DeferredDemo : DemoScene("Deferred Shading") { } } - Text("Bloom".l) { sectionTitleStyle() } - MenuRow { - Text("Strength".l) { labelStyle(lblSize) } - MenuSlider(bloomStrength.use(), 0f, 5f, txtWidth = txtSize) { bloomStrength.set(it) } - } - MenuRow { - Text("Radius".l) { labelStyle(lblSize) } - MenuSlider(bloomRadius.use(), 0f, 2.5f, txtWidth = txtSize) { bloomRadius.set(it) } - } - MenuRow { - Text("Threshold".l) { labelStyle(lblSize) } - MenuSlider(bloomThreshold.use(), 0f, 2f, txtWidth = txtSize) { bloomThreshold.set(it) } - } - +// Text("Bloom".l) { sectionTitleStyle() } +// MenuRow { +// Text("Strength".l) { labelStyle(lblSize) } +// MenuSlider(bloomStrength.use(), 0f, 5f, txtWidth = txtSize) { bloomStrength.set(it) } +// } +// MenuRow { +// Text("Radius".l) { labelStyle(lblSize) } +// MenuSlider(bloomRadius.use(), 0f, 2.5f, txtWidth = txtSize) { bloomRadius.set(it) } +// } +// MenuRow { +// Text("Threshold".l) { labelStyle(lblSize) } +// MenuSlider(bloomThreshold.use(), 0f, 2f, txtWidth = txtSize) { bloomThreshold.set(it) } +// } Text("Objects".l) { sectionTitleStyle() } LabeledSwitch("Show objects".l, isObjects) @@ -363,73 +357,73 @@ class DeferredDemo : DemoScene("Deferred Shading") { .align(AlignmentX.Start, AlignmentY.Bottom) .layout(ColumnLayout) - val albedoMetal = deferredPipeline.activePass.materialPass.albedoMetal - val normalRough = deferredPipeline.activePass.materialPass.normalRoughness - val positionFlags = deferredPipeline.activePass.materialPass.positionFlags - val bloom = deferredPipeline.activePass.bloomPass?.bloomMap - val ao = deferredPipeline.aoPipeline?.aoMap - - Row { - modifier.margin(vertical = sizes.gap) - Image { - modifier - .imageSize(ImageSize.FixedScale(0.3f)) - .imageProvider(FlatImageProvider(albedoMetal, true).mirrorY()) - .margin(horizontal = sizes.gap) - .customShader(albedoMapShader.apply { colorMap = albedoMetal }) - Text("Albedo".l) { imageLabelStyle() } - } - Image { - modifier - .imageSize(ImageSize.FixedScale(0.3f)) - .imageProvider(FlatImageProvider(normalRough, true).mirrorY()) - .margin(horizontal = sizes.gap) - .customShader(normalMapShader.apply { colorMap = normalRough }) - Text("Normals".l) { imageLabelStyle() } - } - } - Row { - modifier.margin(vertical = sizes.gap) - Image { - modifier - .imageSize(ImageSize.FixedScale(0.3f)) - .imageProvider(FlatImageProvider(positionFlags, true).mirrorY()) - .margin(horizontal = sizes.gap) - .customShader(positionMapShader.apply { colorMap = positionFlags }) - Text("Position".l) { imageLabelStyle() } - } - Image(ao) { - modifier - .imageSize(ImageSize.FixedScale(0.3f / deferredPipeline.aoMapSize)) - .imageProvider(FlatImageProvider(ao, true).mirrorY()) - .margin(horizontal = sizes.gap) - .customShader(AoDemo.aoMapShader.apply { colorMap = ao }) - Text("Ambient occlusion".l) { imageLabelStyle() } - } - } - Row { - modifier.margin(vertical = sizes.gap) - Image(positionFlags) { - modifier - .imageSize(ImageSize.FixedScale(0.3f)) - .imageProvider(FlatImageProvider(positionFlags, true).mirrorY()) - .margin(horizontal = sizes.gap) - .customShader(metalRoughFlagsShader.apply { - metal = albedoMetal - rough = normalRough - flags = deferredPipeline.activePass.materialPass.positionFlags - }) - Text("Metal (r), roughness (g), flags (b)".l) { imageLabelStyle() } - } - Image(bloom) { - modifier - .imageSize(ImageSize.FixedScale(0.6f * ((positionFlags.height) / bloom!!.width))) - .imageProvider(FlatImageProvider(bloom, true).mirrorY()) - .margin(horizontal = sizes.gap) - .customShader(bloomMapShader.apply { colorMap = bloom }) - Text("Bloom".l) { imageLabelStyle() } - } - } +// val albedoMetal = deferredPipeline.activePass.materialPass.albedoMetal +// val normalRough = deferredPipeline.activePass.materialPass.normalRoughness +// val positionFlags = deferredPipeline.activePass.materialPass.positionFlags +// val bloom = deferredPipeline.activePass.bloomPass?.bloomMap +// val ao = deferredPipeline.aoPipeline?.aoMap + +// Row { +// modifier.margin(vertical = sizes.gap) +// Image { +// modifier +// .imageSize(ImageSize.FixedScale(0.3f)) +// .imageProvider(FlatImageProvider(albedoMetal, true).mirrorY()) +// .margin(horizontal = sizes.gap) +// .customShader(albedoMapShader.apply { colorMap = albedoMetal }) +// Text("Albedo".l) { imageLabelStyle() } +// } +// Image { +// modifier +// .imageSize(ImageSize.FixedScale(0.3f)) +// .imageProvider(FlatImageProvider(normalRough, true).mirrorY()) +// .margin(horizontal = sizes.gap) +// .customShader(normalMapShader.apply { colorMap = normalRough }) +// Text("Normals".l) { imageLabelStyle() } +// } +// } +// Row { +// modifier.margin(vertical = sizes.gap) +// Image { +// modifier +// .imageSize(ImageSize.FixedScale(0.3f)) +// .imageProvider(FlatImageProvider(positionFlags, true).mirrorY()) +// .margin(horizontal = sizes.gap) +// .customShader(positionMapShader.apply { colorMap = positionFlags }) +// Text("Position".l) { imageLabelStyle() } +// } +// Image(ao) { +// modifier +// .imageSize(ImageSize.FixedScale(0.3f / deferredPipeline.aoMapSize)) +// .imageProvider(FlatImageProvider(ao, true).mirrorY()) +// .margin(horizontal = sizes.gap) +// .customShader(AoDemo.aoMapShader.apply { colorMap = ao }) +// Text("Ambient occlusion".l) { imageLabelStyle() } +// } +// } +// Row { +// modifier.margin(vertical = sizes.gap) +// Image(positionFlags) { +// modifier +// .imageSize(ImageSize.FixedScale(0.3f)) +// .imageProvider(FlatImageProvider(positionFlags, true).mirrorY()) +// .margin(horizontal = sizes.gap) +// .customShader(metalRoughFlagsShader.apply { +// metal = albedoMetal +// rough = normalRough +// flags = deferredPipeline.activePass.materialPass.positionFlags +// }) +// Text("Metal (r), roughness (g), flags (b)".l) { imageLabelStyle() } +// } +// Image(bloom) { +// modifier +// .imageSize(ImageSize.FixedScale(0.6f * ((positionFlags.height) / bloom!!.width))) +// .imageProvider(FlatImageProvider(bloom, true).mirrorY()) +// .margin(horizontal = sizes.gap) +// .customShader(bloomMapShader.apply { colorMap = bloom }) +// Text("Bloom".l) { imageLabelStyle() } +// } +// } } } } @@ -452,7 +446,7 @@ class DeferredDemo : DemoScene("Deferred Shading") { } } - private class AnimatedLight(val light: DeferredPointLights.PointLight) { + private class AnimatedLight(val light: DynamicPointLight) { val startPos = MutableVec3f() val dir = MutableVec3f() var speed = 1.5f diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/GltfDemo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/GltfDemo.kt index 4f6d5e802..c896f5103 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/GltfDemo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/GltfDemo.kt @@ -2,6 +2,9 @@ package de.fabmax.kool.demo import de.fabmax.kool.Assets import de.fabmax.kool.KoolContext +import de.fabmax.kool.demo.deferred2.Deferred2Pipeline +import de.fabmax.kool.demo.deferred2.defaultOutputQuad +import de.fabmax.kool.demo.deferred2.gbufferShader import de.fabmax.kool.demo.menu.DemoMenu import de.fabmax.kool.math.* import de.fabmax.kool.modules.gltf.GltfLoadConfig @@ -11,10 +14,6 @@ import de.fabmax.kool.modules.ksl.KslPbrShader import de.fabmax.kool.modules.ui2.* import de.fabmax.kool.pipeline.ao.AoPipeline import de.fabmax.kool.pipeline.ao.AoRadius -import de.fabmax.kool.pipeline.deferred.DeferredOutputShader -import de.fabmax.kool.pipeline.deferred.DeferredPipeline -import de.fabmax.kool.pipeline.deferred.DeferredPipelineConfig -import de.fabmax.kool.pipeline.deferred.deferredKslPbrShader import de.fabmax.kool.scene.* import de.fabmax.kool.scene.geometry.MeshBuilder import de.fabmax.kool.scene.geometry.generateNormals @@ -74,7 +73,7 @@ class GltfDemo : DemoScene("glTF Models") { private val envMap by hdriImage("${DemoLoader.hdriPath}/shanghai_bund_1k.rgbe.png") - private lateinit var orbitTransform: OrbitInputTransform +// private lateinit var orbitTransform: OrbitInputTransform private var camTranslationTarget: Vec3d? = null private var trackModel = false @@ -82,7 +81,7 @@ class GltfDemo : DemoScene("glTF Models") { private var aoPipelineForward: AoPipeline? = null private val contentGroupForward = Node() - private lateinit var deferredPipeline: DeferredPipeline + private lateinit var deferredPipeline: Deferred2Pipeline private val contentGroupDeferred = Node() private var animationDeltaTime = 0f @@ -96,10 +95,10 @@ class GltfDemo : DemoScene("glTF Models") { setupPipelines(isDeferredShading.value, new) } private val isSsr: MutableStateValue = mutableStateOf(true).onChange { _, new -> - deferredPipeline.isSsrEnabled = new + //deferredPipeline.isSsrEnabled = new setupPipelines(isDeferredShading.value, isAo.value) } - private val ssrMapSize = mutableStateOf(0.5f).onChange { _, new -> deferredPipeline.reflectionMapSize = new } + private val ssrMapSize = mutableStateOf(0.5f)//.onChange { _, new -> deferredPipeline.reflectionMapSize = new } override fun lateInit(ctx: KoolContext) { currentModel.isVisible = true @@ -110,19 +109,18 @@ class GltfDemo : DemoScene("glTF Models") { mainScene.setupLighting() // create deferred pipeline - val defCfg = DeferredPipelineConfig().apply { - isWithAmbientOcclusion = true - isWithScreenSpaceReflections = true - baseReflectionStep = 0.02f - maxGlobalLights = 2 - isWithVignette = true - useImageBasedLighting(envMap) - } - deferredPipeline = DeferredPipeline(mainScene, defCfg) - deferredPipeline.aoPipeline?.apply { - radius = AoRadius.absoluteRadius(0.2f) - } - ssrMapSize.set(deferredPipeline.reflectionMapSize) +// val defCfg = DeferredPipelineConfig().apply { +// isWithAmbientOcclusion = true +// isWithScreenSpaceReflections = true +// baseReflectionStep = 0.02f +// maxGlobalLights = 2 +// isWithVignette = true +// useImageBasedLighting(envMap) +// } + deferredPipeline = Deferred2Pipeline(Node(), mainScene, envMap, isScreenSpaceReflections = true) + deferredPipeline.renderScale = 1f / UiScale.windowScale.value + deferredPipeline.aoPass.radius = AoRadius.absoluteRadius(0.2f) + //ssrMapSize.set(deferredPipeline.reflectionMapSize) // create forward pipeline aoPipelineForward = AoPipeline.createForward(mainScene).apply { @@ -168,8 +166,8 @@ class GltfDemo : DemoScene("glTF Models") { aoPipelineForward?.isEnabled = fwdState && isAo contentGroupDeferred.isVisible = isDeferred - deferredPipeline.isEnabled = isDeferred - deferredPipeline.isAoEnabled = isAo + //deferredPipeline.isEnabled = isDeferred + //deferredPipeline.isAoEnabled = isAo } private fun Scene.makeForwardContent() { @@ -178,20 +176,21 @@ class GltfDemo : DemoScene("glTF Models") { } private fun Scene.makeDeferredContent() { - deferredPipeline.sceneContent.setupContentGroup(true) + deferredPipeline.content.setupContentGroup(true) // main scene only contains a quad used to draw the deferred shading output contentGroupDeferred.apply { isFrustumChecked = false - val outputMesh = deferredPipeline.createDefaultOutputQuad() - (outputMesh.shader as? DeferredOutputShader)?.setupVignette(0f) + val outputMesh = deferredPipeline.defaultOutputQuad(null) + //(outputMesh.shader as? DeferredOutputShader)?.setupVignette(0f) addNode(outputMesh) } addNode(contentGroupDeferred) } private fun Scene.setupCamera() { - orbitTransform = orbitCamera { + //orbitTransform = orbitCamera { + val cam = orbitCamera(deferredPipeline.camera) { setRotation(0f, -30f) zoom = currentModel.zoom translation.set(currentModel.lookAt) @@ -217,6 +216,7 @@ class GltfDemo : DemoScene("glTF Models") { } } } + deferredPipeline.content.addNode(cam) } private fun Scene.setupLighting() { @@ -249,20 +249,19 @@ class GltfDemo : DemoScene("glTF Models") { roundCylinder(4.1f, 0.2f) } - fun KslPbrShader.Config.Builder.materialConfig() { - color { textureColor(colorMap) } - normalMapping { useNormalMap(normalMap) } - ao { textureProperty(aoMap) } - roughness { textureProperty(roughnessMap) } - } - shader = if (isDeferredShading) { - deferredKslPbrShader { - materialConfig() + gbufferShader { + color { textureColor(colorMap) } + normalMapping { useNormalMap(normalMap) } + ao { textureProperty(aoMap) } + roughness { textureProperty(roughnessMap) } } } else { KslPbrShader { - materialConfig() + color { textureColor(colorMap) } + normalMapping { useNormalMap(normalMap) } + ao { textureProperty(aoMap) } + roughness { textureProperty(roughnessMap) } lighting { enableSsao(aoPipelineForward?.aoMap) addShadowMaps(shadowsForward) @@ -287,7 +286,7 @@ class GltfDemo : DemoScene("glTF Models") { prevModel.isVisible = false newModel.isVisible = true - orbitTransform.zoom = newModel.zoom + //orbitTransform.zoom = newModel.zoom camTranslationTarget = newModel.lookAt trackModel = newModel.trackModel } @@ -387,10 +386,24 @@ class GltfDemo : DemoScene("glTF Models") { suspend fun load(isDeferredShading: Boolean): Model { val materialCfg = GltfMaterialConfig( - shadowMaps = if (isDeferredShading) deferredPipeline.shadowMaps else shadowsForward, - scrSpcAmbientOcclusionMap = if (isDeferredShading) deferredPipeline.aoPipeline?.aoMap else aoPipelineForward?.aoMap, +// shadowMaps = if (isDeferredShading) deferredPipeline.shadowMaps else shadowsForward, +// scrSpcAmbientOcclusionMap = if (isDeferredShading) deferredPipeline.aoPipeline?.aoMap else aoPipelineForward?.aoMap, environmentMap = envMap, - isDeferredShading = isDeferredShading + //isDeferredShading = isDeferredShading + shaderFactory = { pbrConfig -> + if (isDeferredShading) { + gbufferShader { + vertexCfg.modelMatrixComposition = pbrConfig.vertexCfg.modelMatrixComposition + colorCfg.colorSources.addAll(pbrConfig.colorCfg.colorSources) + normalMapCfg.set(pbrConfig.normalMapCfg) + roughnessCfg.propertySources.addAll(pbrConfig.roughnessCfg.propertySources) + metallicCfg.propertySources.addAll(pbrConfig.metallicCfg.propertySources) + aoCfg.propertySources.addAll(pbrConfig.aoCfg.propertySources) + } + } else { + KslPbrShader(pbrConfig) + } + } ) val modelCfg = GltfLoadConfig( generateNormals = generateNormals, From beb2c232a96be3ccbd4fd6fbda5ccc4d82d7f686 Mon Sep 17 00:00:00 2001 From: Max Thiele Date: Mon, 25 May 2026 10:35:51 +0200 Subject: [PATCH 20/27] Slightly improved temporal filtering --- .../kotlin/de/fabmax/kool/demo/GltfDemo.kt | 12 +++-- .../kool/demo/deferred2/Deferred2Pipeline.kt | 9 +++- .../kool/demo/deferred2/Deferred2Test.kt | 7 +-- .../kool/demo/deferred2/LightingPass.kt | 7 +-- .../kool/demo/deferred2/ObjectIdAllocator.kt | 4 +- .../demo/deferred2/ReprojectComputePass.kt | 7 +-- .../kool/demo/deferred2/TemporalFilterPass.kt | 47 ++++++++++--------- 7 files changed, 52 insertions(+), 41 deletions(-) diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/GltfDemo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/GltfDemo.kt index c896f5103..7e682376c 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/GltfDemo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/GltfDemo.kt @@ -117,7 +117,14 @@ class GltfDemo : DemoScene("glTF Models") { // isWithVignette = true // useImageBasedLighting(envMap) // } - deferredPipeline = Deferred2Pipeline(Node(), mainScene, envMap, isScreenSpaceReflections = true) + deferredPipeline = Deferred2Pipeline( + content = Node(), + scene = mainScene, + ibl = envMap, + isScreenSpaceReflections = true, + maxGlobalLights = 2, + lighting = mainScene.lighting, + ) deferredPipeline.renderScale = 1f / UiScale.windowScale.value deferredPipeline.aoPass.radius = AoRadius.absoluteRadius(0.2f) //ssrMapSize.set(deferredPipeline.reflectionMapSize) @@ -188,8 +195,7 @@ class GltfDemo : DemoScene("glTF Models") { addNode(contentGroupDeferred) } - private fun Scene.setupCamera() { - //orbitTransform = orbitCamera { + private fun setupCamera() { val cam = orbitCamera(deferredPipeline.camera) { setRotation(0f, -30f) zoom = currentModel.zoom diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt index eebb8e59c..54b1327f0 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt @@ -77,7 +77,7 @@ class Deferred2Pipeline( filterPass.isProfileGpu = true aoPass.isProfileGpu = true - lightingPass.onRelease { camData.release() } + lightingPass.onRelease { camData.releaseDelayed(1) } val offsetMat = MutableMat4f() camera.onCameraUpdated += { @@ -294,7 +294,12 @@ fun Deferred2Pipeline.defaultOutputQuad(bloomPass: BloomPass?): Mesh<*> { fun Deferred2Pipeline.defaultOutputShader( bloomPass: BloomPass?, ): KslShader { - val outputShader = KslShader("deferred2-output") { + val pipelineConfig = PipelineConfig( + blendMode = BlendMode.DISABLED, + depthTest = DepthCompareOp.ALWAYS, + isWriteDepth = false, + ) + val outputShader = KslShader("deferred2-output", pipelineConfig) { val uv = interStageFloat2() fullscreenQuadVertexStage(uv) fragmentStage { diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt index ad46a6cbd..12be47903 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt @@ -24,12 +24,6 @@ import kotlin.random.Random class Deferred2Test : DemoScene("Deferred2 Test") { val ibl by hdriImage("${DemoLoader.hdriPath}/newport_loft.rgbe.png") -// val ibl by hdriImage("${DemoLoader.hdriPath}/shanghai_bund_1k.rgbe.png") -// val ibl by hdriImage("${DemoLoader.hdriPath}/circus_arena_1k.rgbe.png") -// val ibl by hdriImage("${DemoLoader.hdriPath}/syferfontein_0d_clear_1k.rgbe.png") -// val ibl by hdriImage("${DemoLoader.hdriPath}/colorful_studio_1k.rgbe.png") -// val ibl by hdriImage("${DemoLoader.hdriPath}/spruit_sunrise_1k.rgbe.png") -// val ibl by hdriImage("${DemoLoader.hdriPath}/mossy_forest_1k.rgbe.png") val teapot by model("${DemoLoader.modelPath}/teapot.gltf.gz", GltfLoadConfig(applyMaterials = false)) @@ -56,6 +50,7 @@ class Deferred2Test : DemoScene("Deferred2 Test") { clear() } + //val ibl = EnvironmentMap.fromSingleColor(Color.BLACK) pipeline = Deferred2Pipeline(content, scene = this, ibl, isScreenSpaceReflections = true, lighting = lighting) pipeline.renderScale = 0.5f pipeline.aoPass.radius = AoRadius.relativeRadius(1/20f) diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt index 916bdf19e..c24ae60fd 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt @@ -226,7 +226,7 @@ fun KslScopeBuilder.screenReflect( val step by baseDist * 0.025f.const + noise.x * 0.01f.const val prevStep by 0f.const val stepScale by 1f.const - val maxIncrease by 4f.const + val maxIncrease by 1.5f.const val directionFac by abs(dot(rayDir, normalize(origin))) repeat(16.const) { @@ -268,13 +268,14 @@ fun KslScopeBuilder.screenReflect( val rayDir by reflect(normalize(viewPos), viewNormal) val noise by noise33(viewPos * (camData.frameIdx % 64.const + 1.const).toFloat1()) + val scatteringCoeff by 0.4f.const val reflectionColorOut by 0f.const3 val minColor by 1000f.const3 val maxColor by 0f.const3 val initialRays by clamp((roughFactor * length(specFactor) * 20f.const).toInt1(), 1.const, 4.const) repeat(initialRays) { numRays += 1f.const - val scatterOffset by (noise - 0.5f.const) * roughFactor * 0.5f.const + val scatterOffset by (noise - 0.5f.const) * roughFactor * scatteringCoeff val scatteredRayDir by normalize(rayDir + scatterOffset) val rayResult by fnCastRay(viewPos, scatteredRayDir, noise) `if`(rayResult.z gt 0f.const) { @@ -295,7 +296,7 @@ fun KslScopeBuilder.screenReflect( val thresh by length(maxColor - minColor) `while`((thresh gt 0.1f.const) and (numRays lt 6f.const)) { numRays += 1f.const - val scatterOffset by (noise - 0.5f.const) * roughFactor * 0.5f.const + val scatterOffset by (noise - 0.5f.const) * roughFactor * scatteringCoeff val scatteredRayDir by normalize(rayDir + scatterOffset) val rayResult by fnCastRay(viewPos, scatteredRayDir, noise) `if`(rayResult.z gt 0f.const) { diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ObjectIdAllocator.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ObjectIdAllocator.kt index 688c3fcfb..7097c7041 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ObjectIdAllocator.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ObjectIdAllocator.kt @@ -13,7 +13,7 @@ interface ObjectIdAllocator { } class DefaultObjectIdAllocator(val maxObjects: Int) : ObjectIdAllocator { - override var size: Int = 0 + override var size: Int = 1 private set private val objectIdRanges = mutableMapOf() @@ -27,7 +27,7 @@ class DefaultObjectIdAllocator(val maxObjects: Int) : ObjectIdAllocator { var rng = searchFreeRange(numInstances) if (rng == null) { logW { "Failed to find free object ID range for ${mesh.name} (size: $numInstances)" } - rng = ObjectIdRange(maxObjects-1, maxObjects) + rng = ObjectIdRange(0, 1) } range = rng } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ReprojectComputePass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ReprojectComputePass.kt index 5771905cb..b2ca6aee3 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ReprojectComputePass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ReprojectComputePass.kt @@ -8,6 +8,7 @@ import de.fabmax.kool.pipeline.* import de.fabmax.kool.util.Float32Buffer import de.fabmax.kool.util.MemoryLayout import de.fabmax.kool.util.Struct +import de.fabmax.kool.util.releaseDelayed class ReprojectComputePass( val maxObjects: Int = 65536, @@ -29,9 +30,9 @@ class ReprojectComputePass( val groupsX = (maxObjects + 63) / 64 addTask(shader, Vec3i(groupsX, 1, 1)) onRelease { - modelMats.a.release() - modelMats.b.release() - reprojectMats.release() + modelMats.a.releaseDelayed(1) + modelMats.b.releaseDelayed(1) + reprojectMats.releaseDelayed(1) } } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt index 1637015e5..b06664cfc 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt @@ -17,9 +17,11 @@ class TemporalFilterPass( val filterOutput = AlternatingPair { StorageTexture2d(size.x, size.y, filterStorageFmt, samplerSettings = SamplerSettings().clamped()) } - val filterState = StorageTexture2d(size.x, size.y, TexFormat.R, samplerSettings = SamplerSettings().clamped().nearest()) + val filterState = AlternatingPair { + StorageTexture2d(size.x, size.y, TexFormat.R, samplerSettings = SamplerSettings().clamped().nearest()) + } - private val temporalShader = TemporalFilterShader(filterStorageFmt, pipeline.lightingPass.lightingOutput, filterState) + private val temporalShader = TemporalFilterShader(filterStorageFmt, pipeline.lightingPass.lightingOutput) var filterWeight = 8f @@ -28,7 +30,8 @@ class TemporalFilterPass( onRelease { filterOutput.a.release() filterOutput.b.release() - filterState.release() + filterState.a.release() + filterState.b.release() } } @@ -36,7 +39,8 @@ class TemporalFilterPass( this.size = size filterOutput.a.resize(size.x, size.y) filterOutput.b.resize(size.x, size.y) - filterState.resize(size.x, size.y) + filterState.a.resize(size.x, size.y) + filterState.b.resize(size.x, size.y) setupPasses() } @@ -59,6 +63,8 @@ class TemporalFilterPass( oldFilter = filterOutput.oldVal newFilter = filterOutput.newVal filterW = filterWeight + filterStateRd = filterState.oldVal + filterStateWr = filterState.newVal reprojectMats = pipeline.reprojectMatrixComputePass.reprojectMats camData = pipeline.camData @@ -69,7 +75,6 @@ class TemporalFilterPass( class TemporalFilterShader( val filterStorageFmt: TexFormat, lightingOutput: Texture2d, - filterState: StorageTexture2d, ) : KslComputeShader("deferred2-temporal-filter") { var lightingOutput by bindTexture2d("lightingOutput", lightingOutput) var oldMeta by bindTexture2d("oldMeta") @@ -78,7 +83,8 @@ class TemporalFilterShader( var oldDepth by bindTexture2d("oldDepth") var oldFilter by bindTexture2d("oldFilter", defaultSampler = SamplerSettings().clamped().linear()) var newFilter by bindStorageTexture2d("newFilter") - var filterState by bindStorageTexture2d("filterState", filterState) + var filterStateRd by bindTexture2d("filterStateRd") + var filterStateWr by bindStorageTexture2d("filterStateWr") var reprojectMats by bindStorage("reprojectMats") var camData by bindStorage("camData") @@ -101,7 +107,8 @@ class TemporalFilterShader( } else { storageTexture2d("newFilter", filterStorageFmt) } - val filterState = storageTexture2d("filterState", TexFormat.R) + val filterStateRd = texture2d("filterStateRd") + val filterStateWr = storageTexture2d("filterStateWr", TexFormat.R) val matStruct = struct(StorageMatLayout) val reprojectMats = storage("reprojectMats", matStruct) @@ -128,26 +135,23 @@ class TemporalFilterShader( val depth by newDepth.load(baseCoord).x val worldPos by unprojectBaseCoord(depth, baseCoord, size, near, invViewProj) - val oldUv by 0f.const2 - val oldBaseCoord by baseCoord + val oldProj by 0f.const4 `if`(id ne 0.const) { val reprojectMat = mat4Var(reprojectMats[id][StorageMatLayout.mat]) - val oldProj by reprojectMat * worldPos - oldUv set oldProj.xy / oldProj.w * float2Value(0.5f, -0.5f) + 0.5f.const - oldBaseCoord set (oldUv * sizeF).toInt2() + oldProj set reprojectMat * worldPos }.`else` { - val oldProj by camData.oldViewProj * worldPos - oldUv set oldProj.xy / oldProj.w * float2Value(0.5f, -0.5f) + 0.5f.const - oldBaseCoord set (oldUv * sizeF).toInt2() + oldProj set camData.oldViewProj * worldPos } + val oldUv by oldProj.xy / oldProj.w * float2Value(0.5f, -0.5f) + 0.5f.const + val oldBaseCoord by (oldUv * sizeF).toInt2() val oldStateBaseUv by oldUv * sizeF val oldState by - (filterState.load((oldStateBaseUv + float2Value(0.5f, 0.5f)).toInt2()).r * 255f.const).toInt1() or - (filterState.load((oldStateBaseUv + float2Value(0.5f, -0.5f)).toInt2()).r * 255f.const).toInt1() or - (filterState.load((oldStateBaseUv + float2Value(-0.5f, -0.5f)).toInt2()).r * 255f.const).toInt1() or - (filterState.load((oldStateBaseUv + float2Value(-0.5f, 0.5f)).toInt2()).r * 255f.const).toInt1() - val wasEdge by oldState and 1.const gt 0.const + (filterStateRd.load((oldStateBaseUv + float2Value(0.49f, 0.49f)).toInt2()).r * 255f.const).toInt1() or + (filterStateRd.load((oldStateBaseUv + float2Value(0.49f, -0.49f)).toInt2()).r * 255f.const).toInt1() or + (filterStateRd.load((oldStateBaseUv + float2Value(-0.49f, -0.49f)).toInt2()).r * 255f.const).toInt1() or + (filterStateRd.load((oldStateBaseUv + float2Value(-0.49f, 0.49f)).toInt2()).r * 255f.const).toInt1() + val wasEdge by oldState ne 0.const val refDepth by getLinearDepthReversed(depth, near) val depthA by getLinearDepthReversed(newDepth.load(baseCoord + int2Value(1, 1)).x, near) @@ -183,8 +187,7 @@ class TemporalFilterShader( isEdge set (depthEdge or idEdge) } - oldState set isEdge.toInt1() - filterState.store(baseCoord, float4Value(oldState.toFloat1() / 255f.const, 0f.const, 0f.const, 0f.const)) + filterStateWr.store(baseCoord, float4Value(isEdge.toFloat1(), 0f.const, 0f.const, 0f.const)) val w by filterWeight val isReprojectOutOfScreen by (oldUv.x lt 0f.const) or (oldUv.y lt 0f.const) or (oldUv.x gt 1f.const) or (oldUv.y gt 1f.const) From 541b72195ad8cceed797ed2708ecef62bbc7249a Mon Sep 17 00:00:00 2001 From: Max Thiele Date: Mon, 25 May 2026 12:00:19 +0200 Subject: [PATCH 21/27] Integrate shadow mapping into new deferred pipeline --- .../modules/ksl/ShaderAttributeConfigs.kt | 9 +- .../kool/modules/ksl/blocks/ShadowBlock.kt | 11 +-- .../kool/modules/ksl/blocks/ShadowData.kt | 16 ++- .../kotlin/de/fabmax/kool/util/ShadowMap.kt | 14 ++- .../kotlin/de/fabmax/kool/demo/GltfDemo.kt | 19 ++-- .../de/fabmax/kool/demo/ReflectionDemo.kt | 99 +++++++++++-------- .../kool/demo/deferred2/Deferred2Pipeline.kt | 25 ++++- .../kool/demo/deferred2/LightingPass.kt | 39 +++++++- .../kool/demo/deferred2/TemporalFilterPass.kt | 16 +++ 9 files changed, 176 insertions(+), 72 deletions(-) diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/ShaderAttributeConfigs.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/ShaderAttributeConfigs.kt index 7f5c0662d..c87bd9f14 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/ShaderAttributeConfigs.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/ShaderAttributeConfigs.kt @@ -2,6 +2,7 @@ package de.fabmax.kool.modules.ksl import de.fabmax.kool.math.Vec2f import de.fabmax.kool.modules.ksl.KslLitShader.AmbientLight +import de.fabmax.kool.modules.ksl.ShadowMapConfig.Companion.SHADOW_SAMPLE_PATTERN_4x4 import de.fabmax.kool.modules.ksl.blocks.PropertyBlockConfig import de.fabmax.kool.pipeline.Attribute import de.fabmax.kool.pipeline.Texture2d @@ -134,12 +135,12 @@ data class LightingConfig( return this } - fun addShadowMap(shadowMap: ShadowMap, samplePattern: List = ShadowMapConfig.SHADOW_SAMPLE_PATTERN_4x4): Builder { + fun addShadowMap(shadowMap: ShadowMap, samplePattern: List = SHADOW_SAMPLE_PATTERN_4x4): Builder { shadowMaps += ShadowMapConfig(shadowMap, samplePattern) return this } - fun addShadowMaps(shadowMaps: Collection, samplePattern: List = ShadowMapConfig.SHADOW_SAMPLE_PATTERN_4x4): Builder { + fun addShadowMaps(shadowMaps: Collection, samplePattern: List = SHADOW_SAMPLE_PATTERN_4x4): Builder { this.shadowMaps += shadowMaps.map { ShadowMapConfig(it, samplePattern) } return this } @@ -198,3 +199,7 @@ data class ShadowMapConfig(val shadowMap: ShadowMap, val samplePattern: List.toConfig(samplePattern: List = SHADOW_SAMPLE_PATTERN_4x4): List { + return map { ShadowMapConfig(it, samplePattern) } +} diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/ShadowBlock.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/ShadowBlock.kt index bf056512e..334ee628d 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/ShadowBlock.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/ShadowBlock.kt @@ -124,11 +124,9 @@ class ShadowBlockFragmentStage( if (lightSpacePositions.isEmpty() || lightSpaceNormalZs.isEmpty()) { return@apply } - shadowData.shadowMapInfos.forEach { mapInfo -> val light = requireNotNull(mapInfo.shadowMap.light) { "ShadowMap light must be set before creating a shader with it" } shadowFactors[light.lightIndex] set 1f.const - when (mapInfo.shadowMap) { is SimpleShadowMap -> { sampleSimpleShadowMap(lightSpacePositions, lightSpaceNormalZs, mapInfo) @@ -150,7 +148,7 @@ class ShadowBlockFragmentStage( val subMapIdx = mapInfo.fromIndexIncl val posLightSpace = lightSpacePositions[subMapIdx] - `if` (shadowData.shadowCfg.flipBacksideNormals.const or (lightSpaceNormalZs[subMapIdx] lt 0f.const)) { + `if` (shadowData.flipBacksideNormals.const or (lightSpaceNormalZs[subMapIdx] lt 0f.const)) { // normal points towards light source, compute shadow factor shadowFactors[light.lightIndex] set getShadowMapFactor(shadowData.depthMaps[subMapIdx], posLightSpace, mapInfo.samplePattern) }.`else` { @@ -166,7 +164,7 @@ class ShadowBlockFragmentStage( ) { val lightIdx = mapInfo.shadowMap.light?.lightIndex ?: 0 - `if`(shadowData.shadowCfg.flipBacksideNormals.const or (lightSpaceNormalZs[mapInfo.fromIndexIncl] lt 0f.const)) { + `if`(shadowData.flipBacksideNormals.const or (lightSpaceNormalZs[mapInfo.fromIndexIncl] lt 0f.const)) { // normal points towards light source, compute shadow factor val sampleW = float1Var(0f.const) val sampleSum = float1Var(0f.const) @@ -181,10 +179,9 @@ class ShadowBlockFragmentStage( all(projPos gt Vec3f(0f, 0f, -1f).const) and all(projPos lt Vec3f(1f, 1f, 1f).const)) { - // determine how close proj pos is to shadow map border and use that to blend - // between cascades + // determine how close proj pos is to shadow map border and use that to blend between cascades val p = float2Var(abs((projPos.xy - 0.5f.const) * 2f.const)) - val c = 1f.const - clamp(max(p.x, p.y) - 0.9f.const, 0f.const, 0.05f.const) * 10f.const + val c = 1f.const - (max(p.x, p.y) - 0.9f.const) * 10f.const val w = float1Var(c * (1f.const - sampleW)) // projected position is inside shadow map bounds, sample shadow map diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/ShadowData.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/ShadowData.kt index ed1524be5..c8cf1cfa7 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/ShadowData.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/ShadowData.kt @@ -3,6 +3,7 @@ package de.fabmax.kool.modules.ksl.blocks import de.fabmax.kool.math.Vec2f import de.fabmax.kool.modules.ksl.KslShaderListener import de.fabmax.kool.modules.ksl.LightingConfig +import de.fabmax.kool.modules.ksl.ShadowMapConfig import de.fabmax.kool.modules.ksl.lang.* import de.fabmax.kool.pipeline.DrawCommand import de.fabmax.kool.pipeline.ShaderBase @@ -12,10 +13,19 @@ import de.fabmax.kool.util.SimpleShadowMap context(program: KslProgram) fun shadowData(shadowCfg: LightingConfig): ShadowData { - return (program.dataBlocks.find { it is ShadowData } as? ShadowData) ?: ShadowData(shadowCfg, program) + return (program.dataBlocks.find { it is ShadowData } as? ShadowData) ?: ShadowData(shadowCfg.shadowMaps, shadowCfg.flipBacksideNormals, program) } -class ShadowData(val shadowCfg: LightingConfig, program: KslProgram) : KslDataBlock(NAME, program), KslShaderListener { +context(program: KslProgram) +fun shadowData(shadowMapConfig: List, flipBacksideNormals: Boolean = true): ShadowData { + return (program.dataBlocks.find { it is ShadowData } as? ShadowData) ?: ShadowData(shadowMapConfig, flipBacksideNormals, program) +} + +class ShadowData( + val shadowMaps: List, + val flipBacksideNormals: Boolean, + program: KslProgram, +) : KslDataBlock(NAME, program), KslShaderListener { val shadowMapInfos: List val subMaps: List val numSubMaps: Int get() = subMaps.size @@ -29,7 +39,7 @@ class ShadowData(val shadowCfg: LightingConfig, program: KslProgram) : KslDataBl var i = 0 val mapInfos = mutableListOf() val maps = mutableListOf() - for (shadowMap in shadowCfg.shadowMaps) { + for (shadowMap in shadowMaps) { val info = ShadowMapInfo(shadowMap.shadowMap, i, shadowMap.samplePattern) i = info.toIndexExcl mapInfos += info diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/util/ShadowMap.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/util/ShadowMap.kt index edad15e1f..0e8785e8f 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/util/ShadowMap.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/util/ShadowMap.kt @@ -16,6 +16,8 @@ sealed interface ShadowMap { var light: Light? var isShadowMapEnabled: Boolean val subMaps: List + + fun addToScene(scene: Scene) } class SimpleShadowMap( @@ -39,7 +41,7 @@ class SimpleShadowMap( ShadowMap { constructor(scene: Scene, light: Light?, drawNode: Node = scene, mapSize: Int = 2048): this(scene.camera, drawNode, light, mapSize) { - scene.addOffscreenPass(this) + addToScene(scene) } val lightViewProjMat = MutableMat4f() @@ -91,6 +93,10 @@ class SimpleShadowMap( } } + override fun addToScene(scene: Scene) { + scene.addOffscreenPass(this) + } + override fun setupDrawCommand(cmd: DrawCommand, ctx: KoolContext) { super.setupDrawCommand(cmd, ctx) if (cmd.mesh.shadowGeometry.isNotEmpty()) { @@ -216,7 +222,7 @@ class CascadedShadowMap( mapSizes: List? = null, drawNode: Node = scene ): this(scene.camera, drawNode, light, maxRange, numCascades, nearOffset, mapSizes) { - subMaps.forEach { scene.addOffscreenPass(it) } + addToScene(scene) } override var light: Light? = light @@ -263,6 +269,10 @@ class CascadedShadowMap( } } + override fun addToScene(scene: Scene) { + subMaps.forEach { scene.addOffscreenPass(it) } + } + fun setMapRanges(vararg farRanges: Float) { var near = 0f for (i in 0 until min(farRanges.size, mapRanges.size)) { diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/GltfDemo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/GltfDemo.kt index 7e682376c..78076ae5f 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/GltfDemo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/GltfDemo.kt @@ -3,6 +3,7 @@ package de.fabmax.kool.demo import de.fabmax.kool.Assets import de.fabmax.kool.KoolContext import de.fabmax.kool.demo.deferred2.Deferred2Pipeline +import de.fabmax.kool.demo.deferred2.createShadowMaps import de.fabmax.kool.demo.deferred2.defaultOutputQuad import de.fabmax.kool.demo.deferred2.gbufferShader import de.fabmax.kool.demo.menu.DemoMenu @@ -11,6 +12,7 @@ import de.fabmax.kool.modules.gltf.GltfLoadConfig import de.fabmax.kool.modules.gltf.GltfMaterialConfig import de.fabmax.kool.modules.gltf.loadGltfModel import de.fabmax.kool.modules.ksl.KslPbrShader +import de.fabmax.kool.modules.ksl.toConfig import de.fabmax.kool.modules.ui2.* import de.fabmax.kool.pipeline.ao.AoPipeline import de.fabmax.kool.pipeline.ao.AoRadius @@ -109,21 +111,19 @@ class GltfDemo : DemoScene("glTF Models") { mainScene.setupLighting() // create deferred pipeline -// val defCfg = DeferredPipelineConfig().apply { -// isWithAmbientOcclusion = true -// isWithScreenSpaceReflections = true -// baseReflectionStep = 0.02f -// maxGlobalLights = 2 -// isWithVignette = true -// useImageBasedLighting(envMap) -// } + val camera = PerspectiveCamera() + val sceneContent = Node() + val shadows = mainScene.lighting.createShadowMaps(sceneContent, camera) + shadows.forEach { it.addToScene(mainScene) } deferredPipeline = Deferred2Pipeline( - content = Node(), + content = sceneContent, scene = mainScene, ibl = envMap, isScreenSpaceReflections = true, maxGlobalLights = 2, + camera = camera, lighting = mainScene.lighting, + shadowMapConfig = shadows.toConfig() ) deferredPipeline.renderScale = 1f / UiScale.windowScale.value deferredPipeline.aoPass.radius = AoRadius.absoluteRadius(0.2f) @@ -395,7 +395,6 @@ class GltfDemo : DemoScene("glTF Models") { // shadowMaps = if (isDeferredShading) deferredPipeline.shadowMaps else shadowsForward, // scrSpcAmbientOcclusionMap = if (isDeferredShading) deferredPipeline.aoPipeline?.aoMap else aoPipelineForward?.aoMap, environmentMap = envMap, - //isDeferredShading = isDeferredShading shaderFactory = { pbrConfig -> if (isDeferredShading) { gbufferShader { diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/ReflectionDemo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/ReflectionDemo.kt index 573c2f98f..6783802db 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/ReflectionDemo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/ReflectionDemo.kt @@ -1,6 +1,7 @@ package de.fabmax.kool.demo import de.fabmax.kool.KoolContext +import de.fabmax.kool.demo.deferred2.* import de.fabmax.kool.demo.menu.DemoMenu import de.fabmax.kool.math.Vec3f import de.fabmax.kool.math.deg @@ -8,8 +9,8 @@ import de.fabmax.kool.math.randomF import de.fabmax.kool.math.toRad import de.fabmax.kool.modules.gltf.GltfLoadConfig import de.fabmax.kool.modules.ksl.KslUnlitShader +import de.fabmax.kool.modules.ksl.toConfig import de.fabmax.kool.modules.ui2.* -import de.fabmax.kool.pipeline.deferred.* import de.fabmax.kool.scene.* import de.fabmax.kool.toString import de.fabmax.kool.util.* @@ -17,7 +18,7 @@ import kotlin.math.* class ReflectionDemo : DemoScene("Reflections") { - private lateinit var deferredPipeline: DeferredPipeline + private lateinit var deferredPipeline: Deferred2Pipeline private val hdri by hdriGradient(ColorGradient(Color.DARK_GRAY.mix(Color.BLACK, 0.75f), Color.DARK_GRAY, toLinear = true)) private val floorAlbedo by texture2d("${DemoLoader.materialPath}/woodfloor/WoodFlooringMahoganyAfricanSanded001_COL_2K.jpg") @@ -35,8 +36,8 @@ class ReflectionDemo : DemoScene("Reflections") { private val lightChoices = listOf("1", "2", "3", "4") private val lightGroup = Node("light-group") - private val isSsrEnabled = mutableStateOf(true).onChange { _, new -> deferredPipeline.isSsrEnabled = new } - private val ssrMapSize = mutableStateOf(0.5f).onChange { _, new -> deferredPipeline.reflectionMapSize = new } + private val isSsrEnabled = mutableStateOf(true)//.onChange { _, new -> deferredPipeline.isSsrEnabled = new } + private val ssrMapSize = mutableStateOf(0.5f)//.onChange { _, new -> deferredPipeline.reflectionMapSize = new } private val isShowSsrMap = mutableStateOf(true) private val lightCount = mutableStateOf(4) private val lightPower = mutableStateOf(500f) @@ -51,26 +52,13 @@ class ReflectionDemo : DemoScene("Reflections") { private var bunnyMesh: Mesh<*>? = null private var groundMesh: TextureMesh? = null - private var modelShader: DeferredKslPbrShader? = null + private var modelShader: GbufferShader? = null override fun lateInit(ctx: KoolContext) { updateLighting() } override fun Scene.setupMainScene(ctx: KoolContext) { - orbitCamera { - zoomMethod = OrbitInputTransform.ZoomMethod.ZOOM_CENTER - setZoom(17.0, max = 50.0) - translation.set(0.0, 2.0, 0.0) - setRotation(0f, -5f) - // let the camera slowly rotate around vertical axis - onUpdate += { - if (isAutoRotate.value) { - verticalRotation += Time.deltaT * 3f - } - } - } - addNode(lightGroup) lightGroup.onUpdate { lightGroup.transform.rotate((-3f).deg * Time.deltaT, Vec3f.Y_AXIS) @@ -86,25 +74,50 @@ class ReflectionDemo : DemoScene("Reflections") { } private fun setupDeferred(scene: Scene) { - val defCfg = DeferredPipelineConfig().apply { - isWithAmbientOcclusion = false - isWithScreenSpaceReflections = true - useImageBasedLighting(hdri) - } - deferredPipeline = DeferredPipeline(scene, defCfg) +// val defCfg = DeferredPipelineConfig().apply { +// isWithAmbientOcclusion = false +// isWithScreenSpaceReflections = true +// useImageBasedLighting(hdri) +// } + val content = Node() + val cam = PerspectiveCamera() + val shadowMaps = mainScene.lighting.createShadowMaps(content, cam) + shadowMaps.forEach { it.addToScene(mainScene) } + deferredPipeline = Deferred2Pipeline( + content = content, + scene = scene, + ibl = hdri, + camera = cam, + isScreenSpaceReflections = true, + maxGlobalLights = 4, + lighting = mainScene.lighting, + shadowMapConfig = shadowMaps.toConfig() + ) + //scene.camera = deferredPipeline.camera - scene += deferredPipeline.createDefaultOutputQuad().also { - (it.shader as? DeferredOutputShader)?.setupVignette(0f) - } - scene += Skybox.cube(hdri.reflectionMap, 1f) + scene += deferredPipeline.defaultOutputQuad(null) - modelShader = deferredKslPbrShader { + modelShader = gbufferShader { color { uniformColor(matColors[selectedColorIdx.value].linColor) } roughness { uniformProperty(this@ReflectionDemo.roughness.value) } metallic { uniformProperty(this@ReflectionDemo.metallic.value) } } - deferredPipeline.sceneContent.apply { + deferredPipeline.content.apply { + val cam = orbitCamera(deferredPipeline.camera) { + zoomMethod = OrbitInputTransform.ZoomMethod.ZOOM_CENTER + setZoom(17.0, max = 50.0) + translation.set(0.0, 2.0, 0.0) + setRotation(0f, -5f) + // let the camera slowly rotate around vertical axis + onUpdate += { + if (isAutoRotate.value) { + verticalRotation += Time.deltaT * 3f + } + } + } + addNode(cam) + addTextureMesh(isNormalMapped = true) { generate { rect { @@ -118,7 +131,7 @@ class ReflectionDemo : DemoScene("Reflections") { isCastingShadow = false groundMesh = this - shader = deferredKslPbrShader { + shader = gbufferShader { color { textureColor(floorAlbedo) } normalMapping { useNormalMap(floorNormal) } roughness { textureProperty(floorRoughness) } @@ -133,9 +146,9 @@ class ReflectionDemo : DemoScene("Reflections") { private fun updateLighting() { lights.forEachIndexed { i, light -> - if (i < deferredPipeline.shadowMaps.size) { - deferredPipeline.shadowMaps[i].isShadowMapEnabled = false - } +// if (i < deferredPipeline.shadowMaps.size) { +// deferredPipeline.shadowMaps[i].isShadowMapEnabled = false +// } light.disable(mainScene.lighting) } @@ -145,9 +158,9 @@ class ReflectionDemo : DemoScene("Reflections") { lights[i].setup(pos) lights[i].enable(mainScene.lighting) pos += step - if (i < deferredPipeline.shadowMaps.size) { - deferredPipeline.shadowMaps[i].isShadowMapEnabled = true - } +// if (i < deferredPipeline.shadowMaps.size) { +// deferredPipeline.shadowMaps[i].isShadowMapEnabled = true +// } } lights.forEach { it.updateVisibility() } @@ -223,12 +236,12 @@ class ReflectionDemo : DemoScene("Reflections") { .margin(sizes.gap) .align(AlignmentX.Start, AlignmentY.Bottom) - Image(deferredPipeline.reflections?.reflectionMap) { - modifier - .height(500.dp) - .imageProvider(FlatImageProvider(deferredPipeline.reflections?.reflectionMap, true).mirrorY()) - .backgroundColor(Color.BLACK) - } +// Image(deferredPipeline.reflections?.reflectionMap) { +// modifier +// .height(500.dp) +// .imageProvider(FlatImageProvider(deferredPipeline.reflections?.reflectionMap, true).mirrorY()) +// .backgroundColor(Color.BLACK) +// } } } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt index 54b1327f0..323e3388a 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt @@ -4,6 +4,7 @@ import de.fabmax.kool.math.MutableMat4f import de.fabmax.kool.math.Vec2f import de.fabmax.kool.math.Vec2i import de.fabmax.kool.modules.ksl.KslShader +import de.fabmax.kool.modules.ksl.ShadowMapConfig import de.fabmax.kool.modules.ksl.blocks.ColorSpaceConversion import de.fabmax.kool.modules.ksl.blocks.convertColorSpace import de.fabmax.kool.modules.ksl.lang.* @@ -24,7 +25,9 @@ class Deferred2Pipeline( val scene: Scene, val ibl: EnvironmentMap, val isScreenSpaceReflections: Boolean, + val camera: Camera = PerspectiveCamera(), val lighting: Lighting = Lighting(), + val shadowMapConfig: List = emptyList(), val maxGlobalLights: Int = 1, var renderScale: Float = 1f, var tsaa: List = TSAA_4, @@ -34,7 +37,6 @@ class Deferred2Pipeline( (scene.mainRenderPass.viewport.width * renderScale).toInt().coerceAtLeast(16), (scene.mainRenderPass.viewport.height * renderScale).toInt().coerceAtLeast(16) ) - val camera = PerspectiveCamera() val idAllocator: ObjectIdAllocator = DefaultObjectIdAllocator(maxObjects) private val camDataBuffer = StructBuffer(DeferredCamDataLayout, 1) val camData = camDataBuffer.asStorageBuffer() @@ -201,6 +203,27 @@ class Deferred2Pipeline( } } +fun Lighting.createShadowMaps( + sceneContent: Node, + sceneCam: Camera, + range: Float = 100f, + mapSize: Int = 2048, +): List { + val shadows = mutableListOf() + for (light in lights) { + val shadowMap: ShadowMap? = when (light) { + is Light.Directional -> CascadedShadowMap(sceneCam, sceneContent, light, maxRange = range, mapSizes = List(3) { mapSize }) + is Light.Spot -> SimpleShadowMap(sceneCam, sceneContent, light, mapSize) + is Light.Point -> { + logW { "Point light shadow maps not yet supported" } + null + } + } + shadowMap?.let { shadows += shadowMap } + } + return shadows +} + fun makeDitherPattern(): Texture2d { val buf = Uint8Buffer(16) fun u(i: Int): UByte = (255f * (i-1).toFloat() / (buf.capacity - 1)).toInt().toUByte() diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt index c24ae60fd..5c56607d9 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt @@ -3,8 +3,10 @@ package de.fabmax.kool.demo.deferred2 import de.fabmax.kool.KoolSystem import de.fabmax.kool.math.Mat3f import de.fabmax.kool.math.Vec2i +import de.fabmax.kool.math.Vec4f import de.fabmax.kool.modules.ksl.KslShader import de.fabmax.kool.modules.ksl.NormalLightRange +import de.fabmax.kool.modules.ksl.ShadowMapConfig import de.fabmax.kool.modules.ksl.blocks.* import de.fabmax.kool.modules.ksl.lang.* import de.fabmax.kool.pipeline.* @@ -30,7 +32,7 @@ class LightingPass( ) { val lightingOutput: Texture2d get() = colorTexture!! - private val lightingShader = DeferredLightingShader(pipeline.isScreenSpaceReflections, pipeline.maxGlobalLights) + private val lightingShader = DeferredLightingShader(pipeline.isScreenSpaceReflections, pipeline.maxGlobalLights, pipeline.shadowMapConfig) init { camera = pipeline.camera @@ -75,6 +77,7 @@ class LightingPass( class DeferredLightingShader( isScreenSpaceReflections: Boolean, maxGlobalLights: Int, + shadowMapConfig: List, ) : KslShader("deferred2-lighting") { var depthTex by bindTexture2d("depth") var scaledViewZ by bindTexture2d("scaledViewZ") @@ -97,7 +100,7 @@ class DeferredLightingShader( cullMethod = CullMethod.NO_CULLING, depthTest = DepthCompareOp.ALWAYS ) - program.program(isScreenSpaceReflections, maxGlobalLights) + program.program(isScreenSpaceReflections, maxGlobalLights, shadowMapConfig) bindTexture1d("tgradient", GradientTexture(ColorGradient.ROCKET)) } @@ -105,6 +108,7 @@ class DeferredLightingShader( private fun KslProgram.program( isScreenSpaceReflections: Boolean, maxGlobalLights: Int, + shadowMapConfig: List, ) { val uv = interStageFloat2() fullscreenQuadVertexStage(uv) @@ -119,6 +123,7 @@ class DeferredLightingShader( val brdf = texture2d("brdf") val aoMap = texture2d("aoMap") + val ambientShadowFactor = uniformFloat1("uAmbientShadowFactor") val ambientOri = uniformMat3("uAmbientTextureOri") val camDataLayout = struct(DeferredCamDataLayout) val camData = storage("camData", camDataLayout) @@ -153,9 +158,35 @@ class DeferredLightingShader( val ambient by irradiance.sample(worldNormal).rgb * ssao - val normalLightRange = NormalLightRange.ZeroToOne - val shadowFactors = float1Array(maxGlobalLights, 1f.const) val lightData = sceneLightData(maxGlobalLights) + val shadowData = shadowData(shadowMapConfig) + val shadowFactors = float1Array(maxGlobalLights, 1f.const) + val avgShadow = float1Var(0f.const) + if (shadowData.numSubMaps > 0) { + val lightSpacePositions = List(shadowData.numSubMaps) { float4Var(Vec4f.ZERO.const) } + val lightSpaceNormalZs = List(shadowData.numSubMaps) { float1Var(0f.const) } + + // transform positions to light space + shadowData.shadowMapInfos.forEach { mapInfo -> + mapInfo.subMaps.forEachIndexed { i, subMap -> + val subMapIdx = mapInfo.fromIndexIncl + i + val viewProj = shadowData.shadowMapViewProjMats[subMapIdx] + val normalLightSpace = float3Var(normalize((viewProj * float4Value(worldNormal, 0f.const)).xyz)) + lightSpaceNormalZs[subMapIdx] set normalLightSpace.z + lightSpacePositions[subMapIdx] set viewProj * float4Value(worldPos, 1f.const) + lightSpacePositions[subMapIdx].xyz += normalLightSpace * kotlin.math.abs(subMap.shaderDepthOffset).const + } + } + // adjust light strength values by shadow maps + fragmentShadowBlock(lightSpacePositions, lightSpaceNormalZs, shadowData, shadowFactors) + fori(0.const, lightData.lightCount) { i -> + avgShadow += shadowFactors[i] + } + avgShadow /= max(1f.const, lightData.lightCount.toFloat1()) + } + ambient set ambient * (1f.const - (1f.const - avgShadow) * ambientShadowFactor) + + val normalLightRange = NormalLightRange.ZeroToOne val material = pbrMaterialBlock(maxGlobalLights, listOf(reflection), brdf, normalLightRange) { inCamPos(camPos) inNormal(worldNormal) diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt index b06664cfc..ab501e54d 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt @@ -204,6 +204,22 @@ class TemporalFilterShader( `if`(any(isNan(filtered))) { filtered set curSrgb } + +// `if`((!filterHit and !isEdge)) { +// filtered set Color.CYAN.const.rgb +// }.elseIf(!filterHit) { +// filtered set Color.BLUE.const.rgb +// }.elseIf(wasEdge and !isEdge) { +// filtered set Color.RED.const.rgb +// }.elseIf(isReprojectOutOfScreen) { +// filtered set Color.MAGENTA.const.rgb +// }.elseIf(w eq 0f.const) { +// filtered set Color.GREEN.const.rgb +// } +// `if`(isEdge) { +// filtered set float3Value(8f, 0f, 0f) +// } + newFilter[baseCoord] = float4Value(filtered, 1f) } } From 2ab2ab2d34bd02e9c31183b991fdb2a5cd1621cf Mon Sep 17 00:00:00 2001 From: Max Thiele Date: Mon, 25 May 2026 13:35:57 +0200 Subject: [PATCH 22/27] Configurable ssr --- .../de/fabmax/kool/demo/DeferredDemo.kt | 3 +- .../kotlin/de/fabmax/kool/demo/GltfDemo.kt | 15 ++- .../de/fabmax/kool/demo/ReflectionDemo.kt | 92 +++++++------------ .../kool/demo/deferred2/Deferred2Pipeline.kt | 39 ++++++-- .../kool/demo/deferred2/Deferred2Test.kt | 3 +- .../kool/demo/deferred2/LightingPass.kt | 47 ++++++---- 6 files changed, 99 insertions(+), 100 deletions(-) diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/DeferredDemo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/DeferredDemo.kt index ec14a3a20..3c13a9b56 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/DeferredDemo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/DeferredDemo.kt @@ -82,9 +82,8 @@ class DeferredDemo : DemoScene("Deferred Shading") { val content = Node() content.makeContent() - pipeline = Deferred2Pipeline(content, scene = this, ibl, isScreenSpaceReflections = false, lighting = lighting) + pipeline = Deferred2Pipeline(content, scene = this, ibl, lighting = lighting) pipeline.renderScale = 1f / UiScale.windowScale.value - //pipeline.filterPass.filterWeight = 0f pipeline.aoPass.radius = AoRadius.absoluteRadius(1.5f) bloomPass = pipeline.installBloomPass() diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/GltfDemo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/GltfDemo.kt index 78076ae5f..0148920a0 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/GltfDemo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/GltfDemo.kt @@ -19,12 +19,10 @@ import de.fabmax.kool.pipeline.ao.AoRadius import de.fabmax.kool.scene.* import de.fabmax.kool.scene.geometry.MeshBuilder import de.fabmax.kool.scene.geometry.generateNormals -import de.fabmax.kool.toString import de.fabmax.kool.util.* import kotlinx.coroutines.async import kotlin.math.PI import kotlin.math.cos -import kotlin.math.roundToInt import kotlin.math.sin class GltfDemo : DemoScene("glTF Models") { @@ -97,10 +95,13 @@ class GltfDemo : DemoScene("glTF Models") { setupPipelines(isDeferredShading.value, new) } private val isSsr: MutableStateValue = mutableStateOf(true).onChange { _, new -> - //deferredPipeline.isSsrEnabled = new + if (new) { + deferredPipeline.enableScreenSpaceReflections() + } else { + deferredPipeline.disableScreenSpaceReflections() + } setupPipelines(isDeferredShading.value, isAo.value) } - private val ssrMapSize = mutableStateOf(0.5f)//.onChange { _, new -> deferredPipeline.reflectionMapSize = new } override fun lateInit(ctx: KoolContext) { currentModel.isVisible = true @@ -119,15 +120,14 @@ class GltfDemo : DemoScene("glTF Models") { content = sceneContent, scene = mainScene, ibl = envMap, - isScreenSpaceReflections = true, maxGlobalLights = 2, camera = camera, lighting = mainScene.lighting, shadowMapConfig = shadows.toConfig() ) + deferredPipeline.enableScreenSpaceReflections() deferredPipeline.renderScale = 1f / UiScale.windowScale.value deferredPipeline.aoPass.radius = AoRadius.absoluteRadius(0.2f) - //ssrMapSize.set(deferredPipeline.reflectionMapSize) // create forward pipeline aoPipelineForward = AoPipeline.createForward(mainScene).apply { @@ -322,9 +322,6 @@ class GltfDemo : DemoScene("glTF Models") { LabeledSwitch("Ambient occlusion".l, isAo) if (isDeferredShading.value) { LabeledSwitch("Screen space reflections".l, isSsr) - MenuSlider2("SSR map size".l, ssrMapSize.use(), 0.1f, 1f, { it.toString(1) }) { - ssrMapSize.set((it * 10).roundToInt() / 10f) - } } LabeledSwitch("Auto rotate view".l, isAutoRotate) } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/ReflectionDemo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/ReflectionDemo.kt index 6783802db..c4159bcf1 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/ReflectionDemo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/ReflectionDemo.kt @@ -12,7 +12,6 @@ import de.fabmax.kool.modules.ksl.KslUnlitShader import de.fabmax.kool.modules.ksl.toConfig import de.fabmax.kool.modules.ui2.* import de.fabmax.kool.scene.* -import de.fabmax.kool.toString import de.fabmax.kool.util.* import kotlin.math.* @@ -28,17 +27,23 @@ class ReflectionDemo : DemoScene("Reflections") { private val model by model("${DemoLoader.modelPath}/bunny.gltf.gz", GltfLoadConfig(generateNormals = true, applyMaterials = false)) private val lights = listOf( - LightMesh(MdColor.CYAN), - LightMesh(MdColor.RED), - LightMesh(MdColor.AMBER), - LightMesh(MdColor.GREEN)) + LightMesh(MdColor.CYAN), + LightMesh(MdColor.RED), + LightMesh(MdColor.AMBER), + LightMesh(MdColor.GREEN) + ) + private var shadowMaps: List = emptyList() private val lightChoices = listOf("1", "2", "3", "4") private val lightGroup = Node("light-group") - private val isSsrEnabled = mutableStateOf(true)//.onChange { _, new -> deferredPipeline.isSsrEnabled = new } - private val ssrMapSize = mutableStateOf(0.5f)//.onChange { _, new -> deferredPipeline.reflectionMapSize = new } - private val isShowSsrMap = mutableStateOf(true) + private val isSsrEnabled = mutableStateOf(true).onChange { _, new -> + if (new) { + deferredPipeline.enableScreenSpaceReflections() + } else { + deferredPipeline.disableScreenSpaceReflections() + } + } private val lightCount = mutableStateOf(4) private val lightPower = mutableStateOf(500f) private val lightSaturation = mutableStateOf(0.4f) @@ -59,43 +64,32 @@ class ReflectionDemo : DemoScene("Reflections") { } override fun Scene.setupMainScene(ctx: KoolContext) { - addNode(lightGroup) - lightGroup.onUpdate { - lightGroup.transform.rotate((-3f).deg * Time.deltaT, Vec3f.Y_AXIS) - } - lighting.clear() lights.forEach { lighting.addLight(it.light) lightGroup.addNode(it) } - setupDeferred(this) - } - - private fun setupDeferred(scene: Scene) { -// val defCfg = DeferredPipelineConfig().apply { -// isWithAmbientOcclusion = false -// isWithScreenSpaceReflections = true -// useImageBasedLighting(hdri) -// } val content = Node() - val cam = PerspectiveCamera() - val shadowMaps = mainScene.lighting.createShadowMaps(content, cam) + shadowMaps = mainScene.lighting.createShadowMaps(content, camera) shadowMaps.forEach { it.addToScene(mainScene) } deferredPipeline = Deferred2Pipeline( content = content, - scene = scene, + scene = this, ibl = hdri, - camera = cam, - isScreenSpaceReflections = true, + camera = camera, maxGlobalLights = 4, lighting = mainScene.lighting, shadowMapConfig = shadowMaps.toConfig() ) - //scene.camera = deferredPipeline.camera + deferredPipeline.renderScale = 1f / UiScale.windowScale.value + deferredPipeline.enableScreenSpaceReflections() - scene += deferredPipeline.defaultOutputQuad(null) + addNode(deferredPipeline.defaultOutputQuad(null, writeDepth = true)) + addNode(lightGroup) + lightGroup.onUpdate { + lightGroup.transform.rotate((-3f).deg * Time.deltaT, Vec3f.Y_AXIS) + } modelShader = gbufferShader { color { uniformColor(matColors[selectedColorIdx.value].linColor) } @@ -103,8 +97,8 @@ class ReflectionDemo : DemoScene("Reflections") { metallic { uniformProperty(this@ReflectionDemo.metallic.value) } } - deferredPipeline.content.apply { - val cam = orbitCamera(deferredPipeline.camera) { + content.apply { + val camTransform = orbitCamera(camera) { zoomMethod = OrbitInputTransform.ZoomMethod.ZOOM_CENTER setZoom(17.0, max = 50.0) translation.set(0.0, 2.0, 0.0) @@ -116,7 +110,7 @@ class ReflectionDemo : DemoScene("Reflections") { } } } - addNode(cam) + addNode(camTransform) addTextureMesh(isNormalMapped = true) { generate { @@ -146,9 +140,9 @@ class ReflectionDemo : DemoScene("Reflections") { private fun updateLighting() { lights.forEachIndexed { i, light -> -// if (i < deferredPipeline.shadowMaps.size) { -// deferredPipeline.shadowMaps[i].isShadowMapEnabled = false -// } + if (i < shadowMaps.size) { + shadowMaps[i].isShadowMapEnabled = false + } light.disable(mainScene.lighting) } @@ -158,9 +152,9 @@ class ReflectionDemo : DemoScene("Reflections") { lights[i].setup(pos) lights[i].enable(mainScene.lighting) pos += step -// if (i < deferredPipeline.shadowMaps.size) { -// deferredPipeline.shadowMaps[i].isShadowMapEnabled = true -// } + if (i < shadowMaps.size) { + shadowMaps[i].isShadowMapEnabled = true + } } lights.forEach { it.updateVisibility() } @@ -171,13 +165,6 @@ class ReflectionDemo : DemoScene("Reflections") { val txtSize = UiSizes.baseSize * 0.75f LabeledSwitch("SSR enabled".l, isSsrEnabled) - LabeledSwitch("Show map".l, isShowSsrMap) - MenuRow { - Text("Map size".l) { labelStyle(lblSize) } - MenuSlider(ssrMapSize.use(), 0.1f, 1f, { it.toString(1) }, txtWidth = txtSize) { - ssrMapSize.set((it * 10).roundToInt() / 10f) - } - } Text("Material".l) { sectionTitleStyle() } MenuRow { @@ -230,21 +217,6 @@ class ReflectionDemo : DemoScene("Reflections") { LabeledSwitch("Light indicators".l, isShowLightIndicators) LabeledSwitch("Auto rotate view".l, isAutoRotate) - if (isShowSsrMap.value) { - surface.popup().apply { - modifier - .margin(sizes.gap) - .align(AlignmentX.Start, AlignmentY.Bottom) - -// Image(deferredPipeline.reflections?.reflectionMap) { -// modifier -// .height(500.dp) -// .imageProvider(FlatImageProvider(deferredPipeline.reflections?.reflectionMap, true).mirrorY()) -// .backgroundColor(Color.BLACK) -// } - } - } - updateLighting() } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt index 323e3388a..9481277fc 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt @@ -3,6 +3,7 @@ package de.fabmax.kool.demo.deferred2 import de.fabmax.kool.math.MutableMat4f import de.fabmax.kool.math.Vec2f import de.fabmax.kool.math.Vec2i +import de.fabmax.kool.math.clamp import de.fabmax.kool.modules.ksl.KslShader import de.fabmax.kool.modules.ksl.ShadowMapConfig import de.fabmax.kool.modules.ksl.blocks.ColorSpaceConversion @@ -24,7 +25,6 @@ class Deferred2Pipeline( val content: Node, val scene: Scene, val ibl: EnvironmentMap, - val isScreenSpaceReflections: Boolean, val camera: Camera = PerspectiveCamera(), val lighting: Lighting = Lighting(), val shadowMapConfig: List = emptyList(), @@ -119,6 +119,14 @@ class Deferred2Pipeline( } } + fun enableScreenSpaceReflections(numReflectionRays: Int = 3) { + lightingPass.numReflectionRays = numReflectionRays.clamp(1, 16) + } + + fun disableScreenSpaceReflections() { + lightingPass.numReflectionRays = 0 + } + private fun swapBuffers() { camDataBuffer.set(0) { set(it.proj, camera.proj) @@ -304,8 +312,8 @@ fun Deferred2Pipeline.installBloomPass(): BloomPass { return bloomPass } -fun Deferred2Pipeline.defaultOutputQuad(bloomPass: BloomPass?): Mesh<*> { - val outputShader = defaultOutputShader(bloomPass) +fun Deferred2Pipeline.defaultOutputQuad(bloomPass: BloomPass?, writeDepth: Boolean = false): Mesh<*> { + val outputShader = defaultOutputShader(bloomPass, writeDepth) return TextureMesh().apply { generate { generateFullscreenQuad() @@ -316,12 +324,18 @@ fun Deferred2Pipeline.defaultOutputQuad(bloomPass: BloomPass?): Mesh<*> { fun Deferred2Pipeline.defaultOutputShader( bloomPass: BloomPass?, + writeDepth: Boolean = false, ): KslShader { - val pipelineConfig = PipelineConfig( - blendMode = BlendMode.DISABLED, - depthTest = DepthCompareOp.ALWAYS, - isWriteDepth = false, - ) + val pipelineConfig = if (writeDepth) { + PipelineConfig(blendMode = BlendMode.DISABLED) + } else { + PipelineConfig( + blendMode = BlendMode.DISABLED, + depthTest = DepthCompareOp.ALWAYS, + isWriteDepth = false, + ) + } + val outputShader = KslShader("deferred2-output", pipelineConfig) { val uv = interStageFloat2() fullscreenQuadVertexStage(uv) @@ -329,6 +343,7 @@ fun Deferred2Pipeline.defaultOutputShader( val output = texture2d("deferredOutput") val bloom = texture2d("bloomOutput") val ditherTex = texture2d("ditherPattern") + val depthTex = if (writeDepth) texture2d("depth", isUnfilterable = true) else null main { val uvi = (uv.output * output.size().toFloat2()).toInt2() @@ -337,6 +352,10 @@ fun Deferred2Pipeline.defaultOutputShader( val ditherNoise by ditherTex.load(ditherC).r val srgb by convertColorSpace(color, ColorSpaceConversion.LinearToSrgbHdr()) + (ditherNoise - 0.5f.const) / 255f.const colorOutput(srgb) + + depthTex?.let { + outDepth set it.load(uvi).x + } } } } @@ -347,10 +366,14 @@ fun Deferred2Pipeline.defaultOutputShader( val bloomMap = bloomPass?.bloomMap ?: SingleColorTexture(Color.BLACK) outputShader.bindTexture2d("bloomOutput", bloomMap) var inputTex by outputShader.bindTexture2d("deferredOutput", defaultSampler = SamplerSettings().nearest().clamped()) + var inputDepth by outputShader.bindTexture2d("depth") onSwap { val filterOutput = filterPass.filterOutput.newVal outputShader.swapPipelineData(filterOutput) { inputTex = filterOutput + if (writeDepth) { + inputDepth = gbuffers.newVal.depth + } } } return outputShader diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt index 12be47903..e3261e447 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt @@ -51,7 +51,8 @@ class Deferred2Test : DemoScene("Deferred2 Test") { } //val ibl = EnvironmentMap.fromSingleColor(Color.BLACK) - pipeline = Deferred2Pipeline(content, scene = this, ibl, isScreenSpaceReflections = true, lighting = lighting) + pipeline = Deferred2Pipeline(content, scene = this, ibl, lighting = lighting) + pipeline.enableScreenSpaceReflections() pipeline.renderScale = 0.5f pipeline.aoPass.radius = AoRadius.relativeRadius(1/20f) filterWeight.value = pipeline.filterPass.filterWeight.toInt() diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt index 5c56607d9..b65c49c01 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt @@ -32,7 +32,9 @@ class LightingPass( ) { val lightingOutput: Texture2d get() = colorTexture!! - private val lightingShader = DeferredLightingShader(pipeline.isScreenSpaceReflections, pipeline.maxGlobalLights, pipeline.shadowMapConfig) + private val lightingShader = DeferredLightingShader(pipeline.maxGlobalLights, pipeline.shadowMapConfig) + var numReflectionRays: Int = lightingShader.numReflectionRays + var reflectionRayStepIncrease: Float = lightingShader.reflectionRayStepIncrease init { camera = pipeline.camera @@ -70,12 +72,13 @@ class LightingPass( aoMap = pipeline.aoPass.aoMap camData = pipeline.camData oldColor = pipeline.filterPass.filterOutput.oldVal + numReflectionRays = this@LightingPass.numReflectionRays + reflectionRayStepIncrease = this@LightingPass.reflectionRayStepIncrease } } } class DeferredLightingShader( - isScreenSpaceReflections: Boolean, maxGlobalLights: Int, shadowMapConfig: List, ) : KslShader("deferred2-lighting") { @@ -90,6 +93,9 @@ class DeferredLightingShader( var aoMap by bindTexture2d("aoMap") var camData by bindStorage("camData") + var numReflectionRays by bindUniformInt1("numReflectionRays") + var reflectionRayStepIncrease by bindUniformFloat1("reflectionRayStepIncrease", 1.5f) + var oldColor by bindTexture2d("oldColor") var ambientMapOrientation: Mat3f by bindUniformMat3("uAmbientTextureOri", Mat3f.IDENTITY) @@ -100,13 +106,12 @@ class DeferredLightingShader( cullMethod = CullMethod.NO_CULLING, depthTest = DepthCompareOp.ALWAYS ) - program.program(isScreenSpaceReflections, maxGlobalLights, shadowMapConfig) + program.program(maxGlobalLights, shadowMapConfig) bindTexture1d("tgradient", GradientTexture(ColorGradient.ROCKET)) } private fun KslProgram.program( - isScreenSpaceReflections: Boolean, maxGlobalLights: Int, shadowMapConfig: List, ) { @@ -123,6 +128,8 @@ class DeferredLightingShader( val brdf = texture2d("brdf") val aoMap = texture2d("aoMap") + val numReflectionRays = uniformInt1("numReflectionRays") + val reflectionRayStepIncrease = uniformFloat1("reflectionRayStepIncrease") val ambientShadowFactor = uniformFloat1("uAmbientShadowFactor") val ambientOri = uniformMat3("uAmbientTextureOri") val camDataLayout = struct(DeferredCamDataLayout) @@ -203,15 +210,13 @@ class DeferredLightingShader( setLightData(lightData, shadowFactors, 1f.const) } - if (isScreenSpaceReflections) { + val outColor by material.outColor + albedo * emissiveStrength + `if`(numReflectionRays gt 0.const) { val oldColor = texture2d("oldColor") - val screenReflection by screenReflect(material, viewNormal, scaledViewZ, oldColor, camData) - val finalColor by material.outAmbient + material.outLight + screenReflection + albedo * emissiveStrength - colorOutput(finalColor) - } else { - colorOutput(material.outColor + albedo * emissiveStrength) + val screenReflection by screenReflect(material, viewNormal, scaledViewZ, oldColor, camData, numReflectionRays, reflectionRayStepIncrease) + outColor set material.outAmbient + material.outLight + screenReflection + albedo * emissiveStrength } - + colorOutput(outColor) outDepth set depthSample } } @@ -224,7 +229,9 @@ fun KslScopeBuilder.screenReflect( viewNormal: KslExprFloat3, viewZ: KslUniform, oldColor: KslUniform, - camData: KslStructStorage + camData: KslStructStorage, + numReflectionRays: KslExprInt1, + reflectionRayStepIncrease: KslExprFloat1, ): KslExprFloat3 { val fnProjViewPos = functionFloat2("fnProiViewPos") { val viewPos = paramFloat3("viewPos") @@ -248,6 +255,7 @@ fun KslScopeBuilder.screenReflect( val origin = paramFloat3("origin") val rayDir = paramFloat3("rayDir") val noise = paramFloat3("noise") + val maxIncrease = paramFloat1("maxIncrease") body { val baseDist by -origin.z @@ -257,7 +265,6 @@ fun KslScopeBuilder.screenReflect( val step by baseDist * 0.025f.const + noise.x * 0.01f.const val prevStep by 0f.const val stepScale by 1f.const - val maxIncrease by 1.5f.const val directionFac by abs(dot(rayDir, normalize(origin))) repeat(16.const) { @@ -295,7 +302,7 @@ fun KslScopeBuilder.screenReflect( val viewPos by (camData.view * float4Value(material.inFragmentPos, 1f)).xyz val reflectionWeight by 0f.const - val numRays by 0f.const + val numRays by 0.const val rayDir by reflect(normalize(viewPos), viewNormal) val noise by noise33(viewPos * (camData.frameIdx % 64.const + 1.const).toFloat1()) @@ -303,12 +310,12 @@ fun KslScopeBuilder.screenReflect( val reflectionColorOut by 0f.const3 val minColor by 1000f.const3 val maxColor by 0f.const3 - val initialRays by clamp((roughFactor * length(specFactor) * 20f.const).toInt1(), 1.const, 4.const) + val initialRays by clamp((roughFactor * length(specFactor) * 20f.const).toInt1(), 1.const, numReflectionRays) repeat(initialRays) { - numRays += 1f.const + numRays += 1.const val scatterOffset by (noise - 0.5f.const) * roughFactor * scatteringCoeff val scatteredRayDir by normalize(rayDir + scatterOffset) - val rayResult by fnCastRay(viewPos, scatteredRayDir, noise) + val rayResult by fnCastRay(viewPos, scatteredRayDir, noise, reflectionRayStepIncrease) `if`(rayResult.z gt 0f.const) { val sampleColor by oldColor.sample(rayResult.xy).rgb * rayResult.z * specFactor reflectionColorOut += sampleColor @@ -325,11 +332,11 @@ fun KslScopeBuilder.screenReflect( } val thresh by length(maxColor - minColor) - `while`((thresh gt 0.1f.const) and (numRays lt 6f.const)) { - numRays += 1f.const + `while`((thresh gt 0.1f.const) and (numRays lt numReflectionRays * 2.const)) { + numRays += 1.const val scatterOffset by (noise - 0.5f.const) * roughFactor * scatteringCoeff val scatteredRayDir by normalize(rayDir + scatterOffset) - val rayResult by fnCastRay(viewPos, scatteredRayDir, noise) + val rayResult by fnCastRay(viewPos, scatteredRayDir, noise, reflectionRayStepIncrease) `if`(rayResult.z gt 0f.const) { reflectionColorOut += oldColor.sample(rayResult.xy).rgb * rayResult.z * specFactor reflectionWeight += rayResult.z From f3c3c9daad1ec096195eb53981028d7405955ba2 Mon Sep 17 00:00:00 2001 From: Max Thiele Date: Mon, 25 May 2026 17:26:14 +0200 Subject: [PATCH 23/27] Fix ao and ssr on webgpu backend --- .../kool/pipeline/ao/ComputeAoPipeline.kt | 42 ++++++++++--------- .../pipeline/backend/vk/BindGroupDataVk.kt | 19 ++++++--- .../backend/webgpu/WgpuBindGroupData.kt | 11 ++++- .../kool/demo/deferred2/Deferred2Pipeline.kt | 29 +++++++------ .../kool/demo/deferred2/LightingPass.kt | 9 ++-- .../kool/demo/deferred2/TemporalFilterPass.kt | 5 ++- 6 files changed, 70 insertions(+), 45 deletions(-) diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/ao/ComputeAoPipeline.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/ao/ComputeAoPipeline.kt index df9d7cac8..85ee4b384 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/ao/ComputeAoPipeline.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/ao/ComputeAoPipeline.kt @@ -1,6 +1,7 @@ package de.fabmax.kool.pipeline.ao import de.fabmax.kool.KoolSystem +import de.fabmax.kool.math.Vec2f import de.fabmax.kool.math.Vec2i import de.fabmax.kool.math.Vec3f import de.fabmax.kool.math.Vec3i @@ -96,7 +97,7 @@ class ComputeAoPass( private val halfHeight: Int get() = (height / 2).coerceAtLeast(1) val scaledNormals = StorageTexture2d(halfWidth, halfHeight, TexFormat.R_I32, mipMapping = MipMapping.Limited(SCALE_LEVELS), name = "normalOutput") - val scaledDists = StorageTexture2d(halfWidth, halfHeight, distFormat, mipMapping = MipMapping.Limited(SCALE_LEVELS), name = "distOutput") + val scaledDists = StorageTexture2d(halfWidth, halfHeight, distFormat, mipMapping = MipMapping.Limited(SCALE_LEVELS), name = "scaledDists", samplerSettings = SamplerSettings().nearest()) val aoNoisy = StorageTexture2d(halfWidth, halfHeight, TexFormat.R, name = "aoOutputNoisy") val filteredAo = StorageTexture2d(halfWidth, halfHeight, TexFormat.R, name = "filteredAo") private val noiseTex = generateFilterNoiseTex(NOISE_TEX_SZ) @@ -241,7 +242,7 @@ class ComputeAoPass( downSampleShader.bindTexture2d("normalInput", inputNormals, SamplerSettings().nearest()) downSampleShader.bindTexture2d("distInput", inputDepth, SamplerSettings().nearest()) downSampleShader.bindStorageTexture2d("normalOutput", scaledNormals) - downSampleShader.bindStorageTexture2d("distOutput", scaledDists) + downSampleShader.bindStorageTexture2d("distOutputFirst", scaledDists) downSampleShader.bindUniformFloat1("camNear", camera.clipNear) return downSampleShader } @@ -293,19 +294,17 @@ class ComputeAoPass( private fun makeDownSampleLowerPasses() { val distInput = downSampleLowerShader.bindTexture2d("distInput") - val loadLod = downSampleLowerShader.bindUniformInt1("loadLod") - val distOutput = downSampleLowerShader.bindStorageTexture2d("distOutput") + val distOutput = downSampleLowerShader.bindStorageTexture2d("distOutputLower") for (level in 1 until SCALE_LEVELS) { val groupsX = ((halfWidth shr level) + 7) / 8 val groupsY = ((halfHeight shr level) + 7) / 8 val task = addTask(downSampleLowerShader, Vec3i(groupsX, groupsY, 1)) - + val inputSampler = SamplerSettings().clamped().nearest().limitMipLevels(baseLevel = level - 1, numLevels = 1) val key = "$level" task.onBeforeDispatch { downSampleLowerShader.swapPipelineDataCapturing(key) { - distInput.set(scaledDists) - loadLod.set(level - 1) + distInput.set(scaledDists, inputSampler) distOutput.set(scaledDists, level) } } @@ -338,12 +337,12 @@ class ComputeAoPass( } } - private fun downSamplingShader() = KslComputeShader("down-sample-shader") { + private fun downSamplingShader() = KslComputeShader("ao-down-sample-shader") { computeStage(8, 8) { val normalInput = texture2dInt("normalInput") val distInput = texture2d("distInput", isUnfilterable = true) val normalOutput = storageTexture2d("normalOutput", TexFormat.R_I32) - val distOutput = storageTexture2d("distOutput", distFormat) + val distOutput = storageTexture2d("distOutputFirst", distFormat) val camNear = uniformFloat1("camNear") main { @@ -366,19 +365,20 @@ class ComputeAoPass( } } - private fun downSamplingLowerShader() = KslComputeShader("down-sample-lower-shader") { + private fun downSamplingLowerShader() = KslComputeShader("ao-down-sample-lower-shader") { computeStage(8, 8) { val distInput = texture2d("distInput", isUnfilterable = true) - val distOutput = storageTexture2d("distOutput", distFormat) - val loadLod = uniformInt1("loadLod") + val distOutput = storageTexture2d("distOutputLower", distFormat) main { val baseCoord by inGlobalInvocationId.xy.toInt2() val loadCoord by baseCoord * 2.const val maxDepth by 0f.const - val samplePos = listOf(Vec2i(0, 0), Vec2i(1, 0), Vec2i(0, 1), Vec2i(1, 1)) + val samplePos = listOf(Vec2f(0f, 0f), Vec2f(1f, 0f), Vec2f(0f, 1f), Vec2f(1f, 1f)) + val size by 1f.const / distInput.size().toFloat2() samplePos.forEach { sample -> - val sampleDepth = float1Var(distInput.load(loadCoord + sample.const, loadLod).x) + val c = float2Var((loadCoord.toFloat2() + (sample + Vec2f(0.5f)).const) * size) + val sampleDepth = float1Var(distInput.sample(c, lod = 0f.const).x) `if`(sampleDepth gt maxDepth) { maxDepth set sampleDepth } @@ -391,7 +391,7 @@ class ComputeAoPass( private fun aoShader() = KslComputeShader("ao-shader") { computeStage(8, 8) { val normalInput = texture2dInt("normalInput") - val distInput = texture2d("distInput") + val distInput = texture2d("distInput", isUnfilterable = true) val noiseTex = texture2d("noiseTex") val aoOutput = storageTexture2d("aoOutput", aoNoisy.format) val kernelStruct = struct(KernelStruct) @@ -468,8 +468,8 @@ class ComputeAoPass( private fun denoiseShader() = KslComputeShader("ao-denoise-shader") { val noisyAo = texture2d("noisyAo") - val distInput = texture2d("distInput") - val normalInput = texture2d("normalInput") + val distInput = texture2d("distInput", isUnfilterable = true) + val normalInput = texture2dInt("normalInput") val uRadius = uniformFloat1("uRadius") val filteredAo = storageTexture2d("filteredAo", filteredAo.format) @@ -479,7 +479,8 @@ class ComputeAoPass( val dim = NOISE_TEX_SZ / 2 val baseDist by distInput.load(baseCoord).x - val baseNormal by decodeNormalRgb(normalInput.load(baseCoord).rgb) + val encodedNormal by normalInput.load(baseCoord).r + val baseNormal by decodeNormalInt(encodedNormal) val ao by noisyAo.load(baseCoord).x val sumWeight by 1f.const @@ -493,7 +494,8 @@ class ComputeAoPass( val coord = int2Var(baseCoord + Vec2i(x, y).const) val dist = float1Var(distInput.load(coord).x) val distWeight = float1Var(1f.const - smoothStep(0f.const, sampleR, abs(dist - baseDist))) - val normalWeight = float1Var(saturate(dot(decodeNormalRgb(normalInput.load(coord).rgb), baseNormal))) + val sampleNormal = int1Var(normalInput.load(coord).r) + val normalWeight = float1Var(saturate(dot(decodeNormalInt(sampleNormal), baseNormal))) val weight = float1Var(0.0001f.const + distWeight * normalWeight) ao += noisyAo.load(coord).x * weight sumWeight += weight @@ -509,7 +511,7 @@ class ComputeAoPass( private fun upsampleShader() = KslComputeShader("ao-upsample-shader") { val filteredAo = texture2d("filteredAo") val distInput = texture2d("distInput", isUnfilterable = true) - val scaledDistInput = texture2d("scaledDistInput") + val scaledDistInput = texture2d("scaledDistInput", isUnfilterable = true) val camNear = uniformFloat1("camNear") val finalAo = storageTexture2d("finalAo", aoMap.format) diff --git a/kool-core/src/desktopMain/kotlin/de/fabmax/kool/pipeline/backend/vk/BindGroupDataVk.kt b/kool-core/src/desktopMain/kotlin/de/fabmax/kool/pipeline/backend/vk/BindGroupDataVk.kt index 44660c1ae..3c5ace9de 100644 --- a/kool-core/src/desktopMain/kotlin/de/fabmax/kool/pipeline/backend/vk/BindGroupDataVk.kt +++ b/kool-core/src/desktopMain/kotlin/de/fabmax/kool/pipeline/backend/vk/BindGroupDataVk.kt @@ -294,16 +294,25 @@ class BindGroupDataVk( view?.let { backend.device.destroyImageView(it) } sampler?.let { backend.device.destroySampler(it) } + val isDepthTex = binding.layout.sampleType == TextureSampleType.DEPTH + val isUnfilterable = binding.layout.sampleType == TextureSampleType.UNFILTERABLE_FLOAT + val compare = if (isDepthTex) samplerSettings.compareOp.vk else VK_COMPARE_OP_ALWAYS + val maxAnisotropy = if ( tex.mipMapping.isMipMapped && samplerSettings.minFilter == FilterMethod.LINEAR && - samplerSettings.magFilter == FilterMethod.LINEAR + samplerSettings.magFilter == FilterMethod.LINEAR && + samplerSettings.mipFilter == FilterMethod.LINEAR && + !isUnfilterable && !isDepthTex ) samplerSettings.maxAnisotropy else 1 - val isDepthTex = binding.layout.sampleType == TextureSampleType.DEPTH - val isUnfilterable = binding.layout.sampleType == TextureSampleType.UNFILTERABLE_FLOAT - val compare = if (isDepthTex) samplerSettings.compareOp.vk else VK_COMPARE_OP_ALWAYS - + if (isUnfilterable && ( + samplerSettings.magFilter != FilterMethod.NEAREST || + samplerSettings.minFilter != FilterMethod.NEAREST || + samplerSettings.mipFilter != FilterMethod.NEAREST + )) { + logE { "Texture ${tex.name} is marked unfilterable (in bind group ${data.layout.name}), but sampler settings specify filtering" } + } sampler = backend.device.createSampler { magFilter(if (isUnfilterable) VK_FILTER_NEAREST else samplerSettings.magFilter.vk) minFilter(if (isUnfilterable) VK_FILTER_NEAREST else samplerSettings.minFilter.vk) diff --git a/kool-core/src/webMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/WgpuBindGroupData.kt b/kool-core/src/webMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/WgpuBindGroupData.kt index e3a54c1cd..90e399397 100644 --- a/kool-core/src/webMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/WgpuBindGroupData.kt +++ b/kool-core/src/webMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/WgpuBindGroupData.kt @@ -177,10 +177,18 @@ class WgpuBindGroupData( val samplerSettings = sampler ?: tex.samplerSettings val maxAnisotropy = if (tex.mipMapping.isMipMapped && samplerSettings.minFilter == FilterMethod.LINEAR && - samplerSettings.magFilter == FilterMethod.LINEAR + samplerSettings.magFilter == FilterMethod.LINEAR && + samplerSettings.mipFilter == FilterMethod.LINEAR ) samplerSettings.maxAnisotropy else 1 val compare = if (layout.sampleType == TextureSampleType.DEPTH) samplerSettings.compareOp.wgpu else null + if (layout.sampleType == TextureSampleType.UNFILTERABLE_FLOAT && ( + samplerSettings.magFilter != FilterMethod.NEAREST || + samplerSettings.minFilter != FilterMethod.NEAREST || + samplerSettings.mipFilter != FilterMethod.NEAREST + )) { + logE { "Texture ${tex.name} is marked unfilterable (in bind group ${data.layout.name}), but sampler settings specify filtering" } + } val sampler = device.createSampler( addressModeU = samplerSettings.addressModeU.wgpu, addressModeV = samplerSettings.addressModeV.wgpu, @@ -189,6 +197,7 @@ class WgpuBindGroupData( mipmapFilter = samplerSettings.mipFilter.wgpuMipFilter(tex.mipMapping.isMipMapped), maxAnisotropy = maxAnisotropy, compare = compare, + label = tex.name ) textureBindings += TextureBinding(this, loadedTex) diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt index 9481277fc..c7748de59 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt @@ -94,24 +94,10 @@ class Deferred2Pipeline( } } - var oldSize = size - scene.onRenderScene += { - val newSize = size - if (oldSize != newSize) { - logD { "Resizing to ${newSize.x}x${newSize.y}" } - oldSize = newSize - gbuffers.a.setSize(size.x, size.y) - gbuffers.b.setSize(size.x, size.y) - aoPass.resize(size.x, size.y) - lightingPass.setSize(size.x, size.y) - filterPass.resize(size) - resizeListeners.forEachUpdated { it(size) } - } - } - scene.coroutineScope.launch { withContext(KoolDispatchers.Synced) { while (true) { + resizeIfNeeded() swapBuffers() yield() } @@ -156,6 +142,19 @@ class Deferred2Pipeline( gbuffers.oldVal.isEnabled = true } + private fun resizeIfNeeded() { + val newSize = size + if (lightingPass.width != newSize.x || lightingPass.height != newSize.y) { + logD { "Resizing to ${newSize.x}x${newSize.y}" } + gbuffers.a.setSize(newSize.x, newSize.y) + gbuffers.b.setSize(newSize.x, newSize.y) + aoPass.resize(newSize.x, newSize.y) + lightingPass.setSize(newSize.x, newSize.y) + filterPass.resize(newSize) + resizeListeners.forEachUpdated { it(newSize) } + } + } + fun onResize(block: (Vec2i) -> Unit) { resizeListeners += block } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt index b65c49c01..612acfa8e 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt @@ -2,6 +2,7 @@ package de.fabmax.kool.demo.deferred2 import de.fabmax.kool.KoolSystem import de.fabmax.kool.math.Mat3f +import de.fabmax.kool.math.Vec2f import de.fabmax.kool.math.Vec2i import de.fabmax.kool.math.Vec4f import de.fabmax.kool.modules.ksl.KslShader @@ -281,7 +282,7 @@ fun KslScopeBuilder.screenReflect( } val nextStep by clamp(dError * (0.75f.const + noise.y * 0.5f.const), -prevStepSize * maxIncrease, prevStepSize * maxIncrease) - val foregroundObjThresh by max(prevStepSize, baseDist * 0.05f.const / directionFac) + val foregroundObjThresh by max(prevStepSize * reflectionRayStepIncrease, baseDist * 0.05f.const) * 0.5f.const / directionFac `if`(-nextStep gt foregroundObjThresh) { prevStep set step step += prevStepSize @@ -306,6 +307,8 @@ fun KslScopeBuilder.screenReflect( val rayDir by reflect(normalize(viewPos), viewNormal) val noise by noise33(viewPos * (camData.frameIdx % 64.const + 1.const).toFloat1()) + val ddx by Vec2f.X_AXIS.const + val ddy by Vec2f.Y_AXIS.const val scatteringCoeff by 0.4f.const val reflectionColorOut by 0f.const3 val minColor by 1000f.const3 @@ -317,7 +320,7 @@ fun KslScopeBuilder.screenReflect( val scatteredRayDir by normalize(rayDir + scatterOffset) val rayResult by fnCastRay(viewPos, scatteredRayDir, noise, reflectionRayStepIncrease) `if`(rayResult.z gt 0f.const) { - val sampleColor by oldColor.sample(rayResult.xy).rgb * rayResult.z * specFactor + val sampleColor by oldColor.sample(rayResult.xy, ddx, ddy).rgb * rayResult.z * specFactor reflectionColorOut += sampleColor reflectionWeight += rayResult.z minColor set min(minColor, sampleColor) @@ -338,7 +341,7 @@ fun KslScopeBuilder.screenReflect( val scatteredRayDir by normalize(rayDir + scatterOffset) val rayResult by fnCastRay(viewPos, scatteredRayDir, noise, reflectionRayStepIncrease) `if`(rayResult.z gt 0f.const) { - reflectionColorOut += oldColor.sample(rayResult.xy).rgb * rayResult.z * specFactor + reflectionColorOut += oldColor.sample(rayResult.xy, ddx, ddy).rgb * rayResult.z * specFactor reflectionWeight += rayResult.z thresh -= 0.1f.const }.`else` { diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt index ab501e54d..822a16f66 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt @@ -1,5 +1,6 @@ package de.fabmax.kool.demo.deferred2 +import de.fabmax.kool.math.Vec2f import de.fabmax.kool.math.Vec2i import de.fabmax.kool.math.Vec3i import de.fabmax.kool.modules.ksl.KslComputeShader @@ -120,6 +121,8 @@ class TemporalFilterShader( main { val baseCoord by inGlobalInvocationId.xy.toInt2() val newColor by lightingOutput.load(baseCoord).rgb + val ddx by Vec2f.X_AXIS.const + val ddy by Vec2f.Y_AXIS.const `if` (filterWeight eq 0f.const) { newFilter[baseCoord] = float4Value(newColor, 1f) `return`() @@ -195,7 +198,7 @@ class TemporalFilterShader( w set 0f.const } - val oldColor by oldFilter.sample(oldUv).rgb + val oldColor by oldFilter.sample(oldUv, ddx, ddy).rgb val curSrgb by convertColorSpace(newColor, ColorSpaceConversion.LinearToSrgb()) val oldSrgb by convertColorSpace(oldColor, ColorSpaceConversion.LinearToSrgb()) val weighted by (oldSrgb * w + curSrgb) / (w + 1f.const) From 82a524cdb1540acb2cd3a8aa59ca428f84519d2f Mon Sep 17 00:00:00 2001 From: Max Thiele Date: Mon, 25 May 2026 17:52:15 +0200 Subject: [PATCH 24/27] Moved deferred2 to core --- .../pipeline}/deferred2/Deferred2Pipeline.kt | 2 +- .../pipeline}/deferred2/DeferredLightShader.kt | 2 +- .../kool/pipeline}/deferred2/DeferredLights.kt | 2 +- .../kool/pipeline}/deferred2/GbufferPass.kt | 2 +- .../kool/pipeline}/deferred2/GbufferShader.kt | 2 +- .../kool/pipeline}/deferred2/LightingPass.kt | 5 +++-- .../pipeline}/deferred2/ObjectIdAllocator.kt | 2 +- .../pipeline}/deferred2/ProjectionFunctions.kt | 2 +- .../pipeline}/deferred2/ReprojectComputePass.kt | 2 +- .../pipeline}/deferred2/TemporalFilterPass.kt | 17 +---------------- .../kool/demo/{deferred2 => }/Deferred2Test.kt | 4 ++-- .../kotlin/de/fabmax/kool/demo/DeferredDemo.kt | 2 +- .../kotlin/de/fabmax/kool/demo/Demos.kt | 1 - .../kotlin/de/fabmax/kool/demo/GltfDemo.kt | 8 ++++---- .../de/fabmax/kool/demo/ReflectionDemo.kt | 2 +- 15 files changed, 20 insertions(+), 35 deletions(-) rename {kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo => kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline}/deferred2/Deferred2Pipeline.kt (99%) rename {kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo => kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline}/deferred2/DeferredLightShader.kt (99%) rename {kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo => kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline}/deferred2/DeferredLights.kt (99%) rename {kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo => kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline}/deferred2/GbufferPass.kt (99%) rename {kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo => kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline}/deferred2/GbufferShader.kt (99%) rename {kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo => kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline}/deferred2/LightingPass.kt (99%) rename {kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo => kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline}/deferred2/ObjectIdAllocator.kt (98%) rename {kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo => kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline}/deferred2/ProjectionFunctions.kt (98%) rename {kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo => kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline}/deferred2/ReprojectComputePass.kt (99%) rename {kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo => kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline}/deferred2/TemporalFilterPass.kt (93%) rename kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/{deferred2 => }/Deferred2Test.kt (99%) diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/Deferred2Pipeline.kt similarity index 99% rename from kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt rename to kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/Deferred2Pipeline.kt index c7748de59..8daf68066 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Pipeline.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/Deferred2Pipeline.kt @@ -1,4 +1,4 @@ -package de.fabmax.kool.demo.deferred2 +package de.fabmax.kool.pipeline.deferred2 import de.fabmax.kool.math.MutableMat4f import de.fabmax.kool.math.Vec2f diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/DeferredLightShader.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/DeferredLightShader.kt similarity index 99% rename from kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/DeferredLightShader.kt rename to kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/DeferredLightShader.kt index a1453dca3..193cc2e0a 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/DeferredLightShader.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/DeferredLightShader.kt @@ -1,4 +1,4 @@ -package de.fabmax.kool.demo.deferred2 +package de.fabmax.kool.pipeline.deferred2 import de.fabmax.kool.KoolSystem import de.fabmax.kool.math.Vec4f diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/DeferredLights.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/DeferredLights.kt similarity index 99% rename from kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/DeferredLights.kt rename to kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/DeferredLights.kt index 4b75d5bc3..e7366a8f2 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/DeferredLights.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/DeferredLights.kt @@ -1,4 +1,4 @@ -package de.fabmax.kool.demo.deferred2 +package de.fabmax.kool.pipeline.deferred2 import de.fabmax.kool.math.* import de.fabmax.kool.pipeline.swapPipelineData diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/GbufferPass.kt similarity index 99% rename from kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt rename to kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/GbufferPass.kt index 66a5a2a35..d837b33fd 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferPass.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/GbufferPass.kt @@ -1,4 +1,4 @@ -package de.fabmax.kool.demo.deferred2 +package de.fabmax.kool.pipeline.deferred2 import de.fabmax.kool.math.MutableMat4f import de.fabmax.kool.math.Vec2i diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferShader.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/GbufferShader.kt similarity index 99% rename from kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferShader.kt rename to kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/GbufferShader.kt index f1cb31669..f3d30c2d1 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/GbufferShader.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/GbufferShader.kt @@ -1,4 +1,4 @@ -package de.fabmax.kool.demo.deferred2 +package de.fabmax.kool.pipeline.deferred2 import de.fabmax.kool.modules.ksl.BasicVertexConfig import de.fabmax.kool.modules.ksl.KslShader diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/LightingPass.kt similarity index 99% rename from kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt rename to kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/LightingPass.kt index 612acfa8e..f63764d75 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/LightingPass.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/LightingPass.kt @@ -1,4 +1,4 @@ -package de.fabmax.kool.demo.deferred2 +package de.fabmax.kool.pipeline.deferred2 import de.fabmax.kool.KoolSystem import de.fabmax.kool.math.Mat3f @@ -18,6 +18,7 @@ import de.fabmax.kool.scene.Node import de.fabmax.kool.scene.Skybox import de.fabmax.kool.scene.VertexLayouts import de.fabmax.kool.util.ColorGradient +import kotlin.math.abs class LightingPass( size: Vec2i, @@ -182,7 +183,7 @@ class DeferredLightingShader( val normalLightSpace = float3Var(normalize((viewProj * float4Value(worldNormal, 0f.const)).xyz)) lightSpaceNormalZs[subMapIdx] set normalLightSpace.z lightSpacePositions[subMapIdx] set viewProj * float4Value(worldPos, 1f.const) - lightSpacePositions[subMapIdx].xyz += normalLightSpace * kotlin.math.abs(subMap.shaderDepthOffset).const + lightSpacePositions[subMapIdx].xyz += normalLightSpace * abs(subMap.shaderDepthOffset).const } } // adjust light strength values by shadow maps diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ObjectIdAllocator.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/ObjectIdAllocator.kt similarity index 98% rename from kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ObjectIdAllocator.kt rename to kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/ObjectIdAllocator.kt index 7097c7041..51bf307c8 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ObjectIdAllocator.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/ObjectIdAllocator.kt @@ -1,4 +1,4 @@ -package de.fabmax.kool.demo.deferred2 +package de.fabmax.kool.pipeline.deferred2 import de.fabmax.kool.scene.Mesh import de.fabmax.kool.scene.NodeId diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ProjectionFunctions.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/ProjectionFunctions.kt similarity index 98% rename from kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ProjectionFunctions.kt rename to kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/ProjectionFunctions.kt index dae312b28..1d22804b1 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ProjectionFunctions.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/ProjectionFunctions.kt @@ -1,4 +1,4 @@ -package de.fabmax.kool.demo.deferred2 +package de.fabmax.kool.pipeline.deferred2 import de.fabmax.kool.modules.ksl.blocks.getLinearDepthReversed import de.fabmax.kool.modules.ksl.lang.* diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ReprojectComputePass.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/ReprojectComputePass.kt similarity index 99% rename from kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ReprojectComputePass.kt rename to kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/ReprojectComputePass.kt index b2ca6aee3..b28602642 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/ReprojectComputePass.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/ReprojectComputePass.kt @@ -1,4 +1,4 @@ -package de.fabmax.kool.demo.deferred2 +package de.fabmax.kool.pipeline.deferred2 import de.fabmax.kool.math.MutableMat4f import de.fabmax.kool.math.Vec3i diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/TemporalFilterPass.kt similarity index 93% rename from kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt rename to kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/TemporalFilterPass.kt index 822a16f66..548cc9279 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/TemporalFilterPass.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/TemporalFilterPass.kt @@ -1,4 +1,4 @@ -package de.fabmax.kool.demo.deferred2 +package de.fabmax.kool.pipeline.deferred2 import de.fabmax.kool.math.Vec2f import de.fabmax.kool.math.Vec2i @@ -208,21 +208,6 @@ class TemporalFilterShader( filtered set curSrgb } -// `if`((!filterHit and !isEdge)) { -// filtered set Color.CYAN.const.rgb -// }.elseIf(!filterHit) { -// filtered set Color.BLUE.const.rgb -// }.elseIf(wasEdge and !isEdge) { -// filtered set Color.RED.const.rgb -// }.elseIf(isReprojectOutOfScreen) { -// filtered set Color.MAGENTA.const.rgb -// }.elseIf(w eq 0f.const) { -// filtered set Color.GREEN.const.rgb -// } -// `if`(isEdge) { -// filtered set float3Value(8f, 0f, 0f) -// } - newFilter[baseCoord] = float4Value(filtered, 1f) } } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/Deferred2Test.kt similarity index 99% rename from kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt rename to kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/Deferred2Test.kt index e3261e447..e1a66f807 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/deferred2/Deferred2Test.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/Deferred2Test.kt @@ -1,7 +1,6 @@ -package de.fabmax.kool.demo.deferred2 +package de.fabmax.kool.demo import de.fabmax.kool.KoolContext -import de.fabmax.kool.demo.* import de.fabmax.kool.demo.menu.DemoMenu import de.fabmax.kool.math.* import de.fabmax.kool.modules.gltf.GltfLoadConfig @@ -9,6 +8,7 @@ import de.fabmax.kool.modules.ksl.blocks.ColorBlockConfig import de.fabmax.kool.modules.ui2.* import de.fabmax.kool.pipeline.BloomPass import de.fabmax.kool.pipeline.ao.AoRadius +import de.fabmax.kool.pipeline.deferred2.* import de.fabmax.kool.scene.* import de.fabmax.kool.toString import de.fabmax.kool.util.Color diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/DeferredDemo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/DeferredDemo.kt index 3c13a9b56..33a37561a 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/DeferredDemo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/DeferredDemo.kt @@ -1,7 +1,6 @@ package de.fabmax.kool.demo import de.fabmax.kool.KoolContext -import de.fabmax.kool.demo.deferred2.* import de.fabmax.kool.demo.menu.DemoMenu import de.fabmax.kool.math.* import de.fabmax.kool.modules.ksl.KslUnlitShader @@ -12,6 +11,7 @@ import de.fabmax.kool.modules.ui2.* import de.fabmax.kool.pipeline.BloomPass import de.fabmax.kool.pipeline.DepthCompareOp import de.fabmax.kool.pipeline.ao.AoRadius +import de.fabmax.kool.pipeline.deferred2.* import de.fabmax.kool.scene.* import de.fabmax.kool.scene.geometry.IndexedVertexList import de.fabmax.kool.scene.geometry.MeshBuilder diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/Demos.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/Demos.kt index d3bc19f8a..d5af5b88d 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/Demos.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/Demos.kt @@ -5,7 +5,6 @@ import de.fabmax.kool.KoolSystem import de.fabmax.kool.Platform import de.fabmax.kool.demo.bees.BeeDemo import de.fabmax.kool.demo.creativecoding.CreativeCodingDemo -import de.fabmax.kool.demo.deferred2.Deferred2Test import de.fabmax.kool.demo.helloworld.* import de.fabmax.kool.demo.pathtracing.PathTracingDemo import de.fabmax.kool.demo.pbr.PbrDemo diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/GltfDemo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/GltfDemo.kt index 0148920a0..fca2e7eb7 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/GltfDemo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/GltfDemo.kt @@ -2,10 +2,6 @@ package de.fabmax.kool.demo import de.fabmax.kool.Assets import de.fabmax.kool.KoolContext -import de.fabmax.kool.demo.deferred2.Deferred2Pipeline -import de.fabmax.kool.demo.deferred2.createShadowMaps -import de.fabmax.kool.demo.deferred2.defaultOutputQuad -import de.fabmax.kool.demo.deferred2.gbufferShader import de.fabmax.kool.demo.menu.DemoMenu import de.fabmax.kool.math.* import de.fabmax.kool.modules.gltf.GltfLoadConfig @@ -16,6 +12,10 @@ import de.fabmax.kool.modules.ksl.toConfig import de.fabmax.kool.modules.ui2.* import de.fabmax.kool.pipeline.ao.AoPipeline import de.fabmax.kool.pipeline.ao.AoRadius +import de.fabmax.kool.pipeline.deferred2.Deferred2Pipeline +import de.fabmax.kool.pipeline.deferred2.createShadowMaps +import de.fabmax.kool.pipeline.deferred2.defaultOutputQuad +import de.fabmax.kool.pipeline.deferred2.gbufferShader import de.fabmax.kool.scene.* import de.fabmax.kool.scene.geometry.MeshBuilder import de.fabmax.kool.scene.geometry.generateNormals diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/ReflectionDemo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/ReflectionDemo.kt index c4159bcf1..ab822c3da 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/ReflectionDemo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/ReflectionDemo.kt @@ -1,7 +1,6 @@ package de.fabmax.kool.demo import de.fabmax.kool.KoolContext -import de.fabmax.kool.demo.deferred2.* import de.fabmax.kool.demo.menu.DemoMenu import de.fabmax.kool.math.Vec3f import de.fabmax.kool.math.deg @@ -11,6 +10,7 @@ import de.fabmax.kool.modules.gltf.GltfLoadConfig import de.fabmax.kool.modules.ksl.KslUnlitShader import de.fabmax.kool.modules.ksl.toConfig import de.fabmax.kool.modules.ui2.* +import de.fabmax.kool.pipeline.deferred2.* import de.fabmax.kool.scene.* import de.fabmax.kool.util.* import kotlin.math.* From 60a9e752a4d5a349bcef5f578b1715548879c50f Mon Sep 17 00:00:00 2001 From: Max Thiele Date: Mon, 25 May 2026 19:48:48 +0200 Subject: [PATCH 25/27] Use deferred2 for vehicle demo --- .../de/fabmax/kool/pipeline/BindGroupData.kt | 4 + .../de/fabmax/kool/pipeline/PipelineBase.kt | 21 +++-- .../pipeline/deferred2/Deferred2Pipeline.kt | 6 +- .../kool/pipeline/deferred2/DeferredLights.kt | 2 + .../kool/pipeline/deferred2/GbufferPass.kt | 5 +- .../kool/pipeline/deferred2/GbufferShader.kt | 4 + .../kool/pipeline/deferred2/LightingPass.kt | 9 +- .../pipeline/deferred2/TemporalFilterPass.kt | 5 +- .../de/fabmax/kool/util/Buffer.desktop.kt | 3 + .../kool/demo/physics/vehicle/DemoVehicle.kt | 69 ++++++++------- .../kool/demo/physics/vehicle/GuardRail.kt | 77 ++++++++-------- .../kool/demo/physics/vehicle/Playground.kt | 16 ++-- .../fabmax/kool/demo/physics/vehicle/Track.kt | 6 +- .../kool/demo/physics/vehicle/VehicleDemo.kt | 88 ++++++++++--------- .../kool/demo/physics/vehicle/VehicleWorld.kt | 14 ++- .../kool/demo/physics/vehicle/ui/Timer.kt | 24 +++-- .../kool/physics/vehicle/VehicleUtils.kt | 10 +-- 17 files changed, 206 insertions(+), 157 deletions(-) diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/BindGroupData.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/BindGroupData.kt index 7b50fc58b..2258968f5 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/BindGroupData.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/BindGroupData.kt @@ -43,6 +43,10 @@ class BindGroupData(val layout: BindGroupLayout, val name: String) : BaseReleasa return copy } + fun copyTo(other: BindGroupData) { + bindings.copyTo(other.bindings) + } + override fun captureBuffer() { bindings.copyToIfModded(_bufferedBindingData) captured = true diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/PipelineBase.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/PipelineBase.kt index 294961cdb..c9c5ba789 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/PipelineBase.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/PipelineBase.kt @@ -34,8 +34,13 @@ abstract class PipelineBase(val name: String, val bindGroupLayouts: BindGroupLay pipelineHashBuilder += bindGroupLayouts.meshScope.hash } - fun swapPipelineData(key: Any?) { - pipelineData = pipelineSwapData.getOrPut(key) { pipelineData.copy() } + fun swapPipelineData(key: Any?, copyBindings: Boolean = false) { + val old = pipelineData + val new = pipelineSwapData.getOrPut(key) { pipelineData.copy() } + if (copyBindings) { + old.copyTo(new) + } + pipelineData = new } override fun captureBuffer() { @@ -74,21 +79,21 @@ abstract class PipelineBase(val name: String, val bindGroupLayouts: BindGroupLay } } -inline fun > T.swapPipelineData(key: Any?, block: T.() -> Unit) { +inline fun > T.swapPipelineData(key: Any?, copyBindings: Boolean = false, block: T.() -> Unit) { createdPipeline?.let { - it.swapPipelineData(key) + it.swapPipelineData(key, copyBindings) block() } } -inline fun PipelineBase.swapPipelineDataCapturing(key: Any?, block: () -> Unit) { - swapPipelineData(key) +inline fun PipelineBase.swapPipelineDataCapturing(key: Any?, copyBindings: Boolean = false, block: () -> Unit) { + swapPipelineData(key, copyBindings) block() captureBuffer() } -inline fun > T.swapPipelineDataCapturing(key: Any?, block: T.() -> Unit) { - createdPipeline?.swapPipelineDataCapturing(key) { +inline fun > T.swapPipelineDataCapturing(key: Any?, copyBindings: Boolean = false, block: T.() -> Unit) { + createdPipeline?.swapPipelineDataCapturing(key, copyBindings) { block() } } diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/Deferred2Pipeline.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/Deferred2Pipeline.kt index 8daf68066..85c89887f 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/Deferred2Pipeline.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/Deferred2Pipeline.kt @@ -12,7 +12,6 @@ import de.fabmax.kool.modules.ksl.lang.* import de.fabmax.kool.pipeline.* import de.fabmax.kool.pipeline.FullscreenShaderUtil.fullscreenQuadVertexStage import de.fabmax.kool.pipeline.FullscreenShaderUtil.generateFullscreenQuad -import de.fabmax.kool.pipeline.ao.AoRadius import de.fabmax.kool.pipeline.ao.ComputeAoPass import de.fabmax.kool.pipeline.ibl.EnvironmentMap import de.fabmax.kool.scene.* @@ -61,9 +60,8 @@ class Deferred2Pipeline( private val resizeListeners = BufferedList<(Vec2i) -> Unit>() init { - aoPass.kernelSize = 4 - aoPass.radius = AoRadius.relativeRadius(1 / 20f) - aoPass.temporalKernels = tsaa.size + aoPass.kernelSize = 32 / tsaa.size.coerceAtLeast(2) + aoPass.temporalKernels = tsaa.size.coerceAtLeast(1) scene.addComputePass(reprojectMatrixComputePass) scene.addOffscreenPass(gbuffers.a) diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/DeferredLights.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/DeferredLights.kt index e7366a8f2..b5161523a 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/DeferredLights.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/DeferredLights.kt @@ -28,6 +28,7 @@ class DeferredLights( instances = pointLightInstances, name = "DeferredPointLights" ).apply { + isCastingShadow = false generate { makePointLightMesh() } shader = lightShader } @@ -159,6 +160,7 @@ class DeferredLights( instances = instances, name = "DeferredPointLights" ).apply { + isCastingShadow = false generate { makeSpotLightMesh(angle) } shader = lightShader } diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/GbufferPass.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/GbufferPass.kt index d837b33fd..e39ff525b 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/GbufferPass.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/GbufferPass.kt @@ -6,7 +6,6 @@ import de.fabmax.kool.pipeline.* import de.fabmax.kool.scene.InstanceLayouts import de.fabmax.kool.scene.Mesh import de.fabmax.kool.util.* -import kotlin.math.max class GbufferPass( initialSize: Vec2i, @@ -42,7 +41,7 @@ class GbufferPass( onAfterCollectDrawCommands += { viewData -> val upload = pipeline.reprojectMatrixComputePass.uploadData.newVal upload.viewProjMat.set(viewData.drawQueue.viewProjMatF) - upload.modelMats.limit = 0 + // fixme: upload.modelMats.limit = 0 alphaMeshes.clear() lightMeshes.clear() @@ -62,7 +61,7 @@ class GbufferPass( val idRange = pipeline.idAllocator.getIdRange(cmd.mesh) val bufferPos = idRange.from * 16 shader.objectId = idRange.from - upload.modelMats.limit = max(upload.modelMats.limit, bufferPos + idRange.size * 16) + // fixme: upload.modelMats.limit = max(upload.modelMats.limit, bufferPos + idRange.size * 16) try { val instances = mesh.instances diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/GbufferShader.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/GbufferShader.kt index f3d30c2d1..eceb94e0e 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/GbufferShader.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/GbufferShader.kt @@ -178,10 +178,14 @@ class GbufferShaderConfig(builder: Builder) { metallicCfg.block() } + fun metallic(constValue: Float) = metallic { uniformProperty(constValue) } + inline fun roughness(block: PropertyBlockConfig.Builder.() -> Unit) { roughnessCfg.block() } + fun roughness(constValue: Float) = roughness { uniformProperty(constValue) } + inline fun ao(block: PropertyBlockConfig.Builder.() -> Unit) { aoCfg.block() } diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/LightingPass.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/LightingPass.kt index f63764d75..fc4f1b8d2 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/LightingPass.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/LightingPass.kt @@ -37,6 +37,7 @@ class LightingPass( private val lightingShader = DeferredLightingShader(pipeline.maxGlobalLights, pipeline.shadowMapConfig) var numReflectionRays: Int = lightingShader.numReflectionRays var reflectionRayStepIncrease: Float = lightingShader.reflectionRayStepIncrease + var ambientShadowFactor: Float = lightingShader.ambientShadowFactor init { camera = pipeline.camera @@ -63,7 +64,9 @@ class LightingPass( fun swapBuffers() { val newGbuffer = pipeline.gbuffers.newVal - lightingShader.swapPipelineData(newGbuffer) { + // swap pipeline data for next frame. copy bindings from previous data because it contains the updated + // light space matrices for shadows + lightingShader.swapPipelineData(newGbuffer, copyBindings = true) { depthTex = newGbuffer.depth scaledViewZ = pipeline.aoPass.scaledDists encodedNormals = newGbuffer.normals @@ -76,6 +79,7 @@ class LightingPass( oldColor = pipeline.filterPass.filterOutput.oldVal numReflectionRays = this@LightingPass.numReflectionRays reflectionRayStepIncrease = this@LightingPass.reflectionRayStepIncrease + ambientShadowFactor = this@LightingPass.ambientShadowFactor } } } @@ -97,6 +101,7 @@ class DeferredLightingShader( var numReflectionRays by bindUniformInt1("numReflectionRays") var reflectionRayStepIncrease by bindUniformFloat1("reflectionRayStepIncrease", 1.5f) + var ambientShadowFactor by bindUniformFloat1("ambientShadowFactor", 0f) var oldColor by bindTexture2d("oldColor") @@ -315,6 +320,7 @@ fun KslScopeBuilder.screenReflect( val minColor by 1000f.const3 val maxColor by 0f.const3 val initialRays by clamp((roughFactor * length(specFactor) * 20f.const).toInt1(), 1.const, numReflectionRays) + val hit by false.const repeat(initialRays) { numRays += 1.const val scatterOffset by (noise - 0.5f.const) * roughFactor * scatteringCoeff @@ -326,6 +332,7 @@ fun KslScopeBuilder.screenReflect( reflectionWeight += rayResult.z minColor set min(minColor, sampleColor) maxColor set max(maxColor, sampleColor) + hit set true.const }.`else` { reflectionColorOut += envReflectionColor reflectionWeight += 1f.const diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/TemporalFilterPass.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/TemporalFilterPass.kt index 548cc9279..8d6c94a86 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/TemporalFilterPass.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/TemporalFilterPass.kt @@ -198,6 +198,10 @@ class TemporalFilterShader( w set 0f.const } + // reduce filter weight in screen regions with high motion + val dUv = saturate(length(oldUv - baseCoord.toFloat2() / sizeF) / 0.01f.const) + w *= (1f.const - dUv) + val oldColor by oldFilter.sample(oldUv, ddx, ddy).rgb val curSrgb by convertColorSpace(newColor, ColorSpaceConversion.LinearToSrgb()) val oldSrgb by convertColorSpace(oldColor, ColorSpaceConversion.LinearToSrgb()) @@ -207,7 +211,6 @@ class TemporalFilterShader( `if`(any(isNan(filtered))) { filtered set curSrgb } - newFilter[baseCoord] = float4Value(filtered, 1f) } } diff --git a/kool-core/src/desktopMain/kotlin/de/fabmax/kool/util/Buffer.desktop.kt b/kool-core/src/desktopMain/kotlin/de/fabmax/kool/util/Buffer.desktop.kt index fe4055488..547940cae 100644 --- a/kool-core/src/desktopMain/kotlin/de/fabmax/kool/util/Buffer.desktop.kt +++ b/kool-core/src/desktopMain/kotlin/de/fabmax/kool/util/Buffer.desktop.kt @@ -38,6 +38,7 @@ abstract class GenericBuffer( override var limit: Int get() = if (isAutoLimit) pos else buffer.limit() set(value) { + modCount++ buffer.limit(value) isAutoLimit = false } @@ -45,6 +46,7 @@ abstract class GenericBuffer( override var position: Int get() = pos set(value) { + modCount++ buffer.position(value) pos = value } @@ -52,6 +54,7 @@ abstract class GenericBuffer( protected var pos = 0 override fun clear() { + modCount++ buffer.clear() position = 0 } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/DemoVehicle.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/DemoVehicle.kt index 9f3ca4786..fd29db861 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/DemoVehicle.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/DemoVehicle.kt @@ -10,10 +10,10 @@ import de.fabmax.kool.physics.setPosition import de.fabmax.kool.physics.setRotation import de.fabmax.kool.physics.vehicle.Vehicle import de.fabmax.kool.physics.vehicle.VehicleProperties -import de.fabmax.kool.pipeline.deferred.DeferredKslPbrShader -import de.fabmax.kool.pipeline.deferred.DeferredPointLights -import de.fabmax.kool.pipeline.deferred.DeferredSpotLights -import de.fabmax.kool.pipeline.deferred.deferredKslPbrShader +import de.fabmax.kool.pipeline.deferred2.DynamicPointLight +import de.fabmax.kool.pipeline.deferred2.DynamicSpotLight +import de.fabmax.kool.pipeline.deferred2.GbufferShader +import de.fabmax.kool.pipeline.deferred2.gbufferShader import de.fabmax.kool.scene.Model import de.fabmax.kool.scene.Node import de.fabmax.kool.util.Color @@ -41,12 +41,12 @@ class DemoVehicle(val demo: VehicleDemo, private val vehicleModel: Model, ctx: K private var previousGear = 0 - private val brakeLightShader: DeferredKslPbrShader - private val reverseLightShader: DeferredKslPbrShader - private val rearLightLt: DeferredPointLights.PointLight - private val rearLightRt: DeferredPointLights.PointLight - private val headLightLt: DeferredSpotLights.SpotLight - private val headLightRt: DeferredSpotLights.SpotLight + private val brakeLightShader: GbufferShader + private val reverseLightShader: GbufferShader + private val rearLightLt: DynamicPointLight + private val rearLightRt: DynamicPointLight + private val headLightLt: DynamicSpotLight + private val headLightRt: DynamicSpotLight private val rearLightColorBrake = Color(1f, 0.01f, 0.01f) private val rearLightColorReverse = Color(1f, 1f, 1f) @@ -72,38 +72,36 @@ class DemoVehicle(val demo: VehicleDemo, private val vehicleModel: Model, ctx: K resetVehiclePos(hard = true) - vehicleModel.meshes["mesh_head_lights_0"]?.shader = deferredKslPbrShader { - emission { constColor(Color(25f, 25f, 25f)) } + vehicleModel.meshes["mesh_head_lights_0"]?.shader = gbufferShader { + color { uniformColor(Color.WHITE) } + emission { uniformProperty(25f) } } - brakeLightShader = deferredKslPbrShader { - color { constColor(Color(0.5f, 0.0f, 0.0f)) } - emission { uniformColor(Color.BLACK) } + brakeLightShader = gbufferShader { + color { uniformColor(Color(0.5f, 0.0f, 0.0f)) } + emission { uniformProperty(0f) } } vehicleModel.meshes["mesh_brake_lights_0"]?.shader = brakeLightShader - reverseLightShader = deferredKslPbrShader { - color { constColor(Color(0.6f, 0.6f, 0.6f)) } - emission { uniformColor(Color.BLACK) } + reverseLightShader = gbufferShader { + color { uniformColor(Color(0.6f, 0.6f, 0.6f)) } + emission { uniformProperty(0f) } } vehicleModel.meshes["mesh_reverse_lights_0"]?.shader = reverseLightShader - headLightLt = DeferredSpotLights.SpotLight().apply { + val dynLights = demo.vehicleWorld.deferredLights + headLightLt = dynLights.addSpotLight(maxAngle = 30f.deg) { spotAngle = 30f.deg coreRatio = 0.5f radius = 0f intensity = 1000f } - headLightRt = DeferredSpotLights.SpotLight().apply { + headLightRt = dynLights.addSpotLight(maxAngle = 30f.deg) { spotAngle = 30f.deg coreRatio = 0.5f radius = 0f intensity = 1000f } - val headLights = world.deferredPipeline.createSpotLights(30f.deg) - headLights.addSpotLight(headLightLt) - headLights.addSpotLight(headLightRt) - - rearLightLt = world.deferredPipeline.dynamicPointLights.addPointLight { } - rearLightRt = world.deferredPipeline.dynamicPointLights.addPointLight { } + rearLightLt = dynLights.addPointLight { } + rearLightRt = dynLights.addPointLight { } vehicleModel.onUpdate += { updateVehicle() @@ -149,20 +147,20 @@ class DemoVehicle(val demo: VehicleDemo, private val vehicleModel: Model, ctx: K lightColor = Color.BLACK } } - rearLightLt.radius = lightRadius - rearLightRt.radius = lightRadius + rearLightLt.strengthByRadius(lightRadius) + rearLightRt.strengthByRadius(lightRadius) rearLightLt.color.set(lightColor) rearLightRt.color.set(lightColor) if (vehicle.brakeInput > 0f) { - brakeLightShader.emission = Color(40f, 0.25f, 0.125f) + brakeLightShader.emission = 40f } else { - brakeLightShader.emission = Color.BLACK + brakeLightShader.emission = 0f } if (vehicle.isReverse) { - reverseLightShader.emission = Color(20f, 20f, 20f) + reverseLightShader.emission = 20f } else { - reverseLightShader.emission = Color.BLACK + reverseLightShader.emission = 0f } var maxSlip = 0f @@ -181,9 +179,9 @@ class DemoVehicle(val demo: VehicleDemo, private val vehicleModel: Model, ctx: K } previousGear = gear - rearLightLt.position.set(0.4f, 0.6f, -2.5f) + rearLightLt.position.set(0.4f, 0.6f, -2.7f) vehicle.transform.transform(rearLightLt.position) - rearLightRt.position.set(-0.4f, 0.6f, -2.5f) + rearLightRt.position.set(-0.4f, 0.6f, -2.7f) vehicle.transform.transform(rearLightRt.position) headLightLt.rotation.set(QuatF.IDENTITY).mul(vehicle.transform.rotation).rotate((-85f).deg, Vec3f.Y_AXIS).rotate((-7f).deg, Vec3f.Z_AXIS) @@ -270,6 +268,9 @@ class DemoVehicle(val demo: VehicleDemo, private val vehicleModel: Model, ctx: K val vehicle = Vehicle(vehicleProps, world.physics) world.physics.addActor(vehicle) +// vehicleModel.meshes.forEach { println(it.value.name) } +// vehicleModel.printHierarchy() + vehicleGroup.apply { transform = vehicle.transform diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/GuardRail.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/GuardRail.kt index 3d7bb426a..42e76e587 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/GuardRail.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/GuardRail.kt @@ -6,14 +6,18 @@ import de.fabmax.kool.physics.RigidDynamic import de.fabmax.kool.physics.Shape import de.fabmax.kool.physics.geometry.BoxGeometry import de.fabmax.kool.physics.joints.FixedJoint -import de.fabmax.kool.pipeline.deferred.DeferredKslPbrShader -import de.fabmax.kool.pipeline.deferred.DeferredPointLights +import de.fabmax.kool.pipeline.deferred2.DynamicPointLight +import de.fabmax.kool.pipeline.deferred2.GbufferShader +import de.fabmax.kool.pipeline.deferred2.gbufferShader import de.fabmax.kool.scene.* import de.fabmax.kool.scene.geometry.MeshBuilder import de.fabmax.kool.scene.geometry.generateNormals import de.fabmax.kool.scene.geometry.simpleShape import de.fabmax.kool.util.* -import kotlin.math.* +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.max +import kotlin.math.sin class GuardRail { @@ -119,8 +123,7 @@ class GuardRail { geometry.generateNormals() } - - shader = GuardRailShader.createShader() + shader = guardRailShader() onUpdate += { signInstances.clear() @@ -179,7 +182,7 @@ class GuardRail { val emission = MutableVec2f() val actor: RigidDynamic val joint: FixedJoint - val pointLight: DeferredPointLights.PointLight + val pointLight: DynamicPointLight init { val signBox = BoxGeometry(Vec3f(2f, 2f, 0.3f)) @@ -194,15 +197,14 @@ class GuardRail { joint = FixedJoint(track.trackActor, actor, initPose.getPose(), PoseF()) joint.enableBreakage(2e5f, 2e5f) - pointLight = world.deferredPipeline.dynamicPointLights.addPointLight { + pointLight = world.deferredLights.addPointLight { color.set(MdColor.ORANGE toneLin 300) } } fun updateInstance(buf: StructBuffer) { - pointLight.intensity = max(emission.x, emission.y) * 100f - pointLight.radius = sqrt(pointLight.intensity) - actor.transform.transform(pointLight.position.set(0f, 0.5f, 0.1f)) + pointLight.strengthByIntensity(max(emission.x, emission.y) * 50f) + actor.transform.transform(pointLight.position.set(0f, 0.5f, 0.5f)) buf.put { set(it.modelMat, actor.transform.matrixF) @@ -211,38 +213,31 @@ class GuardRail { } } - private class GuardRailShader(cfg: Config) : DeferredKslPbrShader(cfg) { - companion object { - fun createShader(): GuardRailShader { - val cfg = Config.Builder().apply { - vertices { instancedModelMatrix() } - color { vertexColor() } - emission { - constColor(VehicleDemo.color(500, false).mulRgb(20f)) - } - - modelCustomizer = { - val emissionFactor = interStageFloat1() - - vertexStage { - main { - val emissionDir = vertexAttrib(VertexLayouts.TexCoord.texCoord) - val emissionInst = instanceAttrib(InstanceLayout.emission) - val emissionLt = emissionDir.x * emissionInst.x - val emissionRt = emissionDir.y * emissionInst.y - emissionFactor.input set max(emissionLt, emissionRt) - } - } - fragmentStage { - main { - val emissionPort = getFloat4Port("emissionColor") - val color = float4Var(emissionPort.input.input) - emissionPort.input(color * emissionFactor.output) - } - } - } + private fun guardRailShader(): GbufferShader = gbufferShader { + vertices { instancedModelMatrix() } + color { vertexColor() } + roughness(0.8f) + modelCustomizer = { + val emissionFactor = interStageFloat1() + vertexStage { + main { + val emissionDir = vertexAttrib(VertexLayouts.TexCoord.texCoord) + val emissionInst = instanceAttrib(InstanceLayout.emission) + val emissionLt = emissionDir.x * emissionInst.x + val emissionRt = emissionDir.y * emissionInst.y + emissionFactor.input set max(emissionLt, emissionRt) * 50f.const + } + } + fragmentStage { + main { + val emission = emissionFactor.output + val emissionPort = getFloat1Port("emissionStrength") + emissionPort.input(emission) + + val baseColorPort = getFloat4Port("baseColor") + val color = float4Var(baseColorPort.input.input) + baseColorPort.input(color + VehicleDemo.color(500, true).const * saturate(emission)) } - return GuardRailShader(cfg.build()) } } } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/Playground.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/Playground.kt index a57076282..5e88975b3 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/Playground.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/Playground.kt @@ -7,7 +7,7 @@ import de.fabmax.kool.physics.Shape import de.fabmax.kool.physics.geometry.BoxGeometry import de.fabmax.kool.physics.joints.RevoluteJoint import de.fabmax.kool.physics.setPosition -import de.fabmax.kool.pipeline.deferred.deferredKslPbrShader +import de.fabmax.kool.pipeline.deferred2.gbufferShader import de.fabmax.kool.scene.ColorMesh import de.fabmax.kool.scene.geometry.MeshBuilder import de.fabmax.kool.scene.geometry.generateNormals @@ -18,15 +18,15 @@ object Playground { fun makePlayground(vehicleWorld: VehicleWorld) { makeBoxes(MutableMat4f().translate(-20f, 0f, 30f), vehicleWorld) - //makeRocker(Mat4f().translate(0f, 0f, 30f), vehicleWorld) + //makeRocker(MutableMat4f().translate(0f, 0f, 30f), vehicleWorld) - vehicleWorld.deferredPipeline.sceneContent += ColorMesh().apply { + vehicleWorld.deferredPipeline.content += ColorMesh().apply { generate { makeRamp(MutableMat4f().translate(-20f, 0f, 80f).rotate(180f.deg, Vec3f.Y_AXIS)) makeBumps(MutableMat4f().translate(20f, 0f, 0f)) makeHalfPipe(MutableMat4f().translate(-40f, 0f, 30f).rotate(90f.deg, Vec3f.NEG_Y_AXIS)) } - shader = deferredKslPbrShader { + shader = gbufferShader { color { vertexColor() } } @@ -54,7 +54,7 @@ object Playground { world.physics.addActor(body) val color = if (i % 2 == 0) VehicleDemo.color(400) else VehicleDemo.color(200) - world.deferredPipeline.sceneContent += world.toPrettyMesh(body, color) + world.deferredPipeline.content += world.toPrettyMesh(body, color) } } } @@ -70,12 +70,12 @@ object Playground { simulationFilterData = world.obstacleSimFilterData queryFilterData = world.obstacleQryFilterData attachShape(Shape(BoxGeometry(Vec3f(7.5f, 0.15f, 15f)), world.defaultMaterial)) - setPosition(frame.transform(MutableVec3f(0f, 1.7f, 0f))) + setPosition(frame.transform(MutableVec3f(0f, 0f, 0f))) } world.physics.addActor(anchor) world.physics.addActor(rocker) - world.deferredPipeline.sceneContent += world.toPrettyMesh(anchor, VehicleDemo.color(400)) - world.deferredPipeline.sceneContent += world.toPrettyMesh(rocker, VehicleDemo.color(200)) + world.deferredPipeline.content += world.toPrettyMesh(anchor, VehicleDemo.color(400)) + world.deferredPipeline.content += world.toPrettyMesh(rocker, VehicleDemo.color(200)) RevoluteJoint(anchor, rocker, PoseF(Vec3f(0f, 0.85f, 0f)), PoseF(Vec3f(0f, 0f, 0.2f))) } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/Track.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/Track.kt index b6938cc87..0c895675e 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/Track.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/Track.kt @@ -6,7 +6,7 @@ import de.fabmax.kool.math.spatial.NearestTraverser import de.fabmax.kool.math.spatial.pointKdTree import de.fabmax.kool.physics.RigidStatic import de.fabmax.kool.pipeline.* -import de.fabmax.kool.pipeline.deferred.deferredKslPbrShader +import de.fabmax.kool.pipeline.deferred2.gbufferShader import de.fabmax.kool.scene.Mesh import de.fabmax.kool.scene.Node import de.fabmax.kool.scene.VertexLayouts @@ -348,14 +348,14 @@ class Track(val world: VehicleWorld) : Node() { albedoMap.releaseWith(trackMesh) roughnessMap.releaseWith(trackMesh) - trackMesh.shader = deferredKslPbrShader { + trackMesh.shader = gbufferShader { color { textureColor(albedoMap) } roughness { textureProperty(roughnessMap) } } } private fun makeSupportMeshShader() { - trackSupportMesh.shader = deferredKslPbrShader { + trackSupportMesh.shader = gbufferShader { color { vertexColor() } roughness { vertexProperty(TrackSupportLayout.roughness) } } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/VehicleDemo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/VehicleDemo.kt index 9f27a89da..3f729c6e3 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/VehicleDemo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/VehicleDemo.kt @@ -9,15 +9,13 @@ import de.fabmax.kool.math.* import de.fabmax.kool.modules.gltf.GltfLoadConfig import de.fabmax.kool.modules.gltf.GltfMaterialConfig import de.fabmax.kool.modules.ksl.blocks.ColorBlockConfig -import de.fabmax.kool.modules.ksl.blocks.ColorSpaceConversion +import de.fabmax.kool.modules.ksl.toConfig +import de.fabmax.kool.modules.ui2.UiScale import de.fabmax.kool.modules.ui2.UiSurface import de.fabmax.kool.physics.* import de.fabmax.kool.physics.geometry.PlaneGeometry import de.fabmax.kool.physics.util.ActorTrackingCamRig -import de.fabmax.kool.pipeline.DepthCompareOp -import de.fabmax.kool.pipeline.deferred.DeferredPipeline -import de.fabmax.kool.pipeline.deferred.DeferredPipelineConfig -import de.fabmax.kool.pipeline.deferred.deferredKslPbrShader +import de.fabmax.kool.pipeline.deferred2.* import de.fabmax.kool.scene.* import de.fabmax.kool.util.* @@ -28,7 +26,18 @@ class VehicleDemo : DemoScene("Vehicle Demo") { private val groundNormal by texture2d("${DemoLoader.materialPath}/tile_flat/tiles_flat_fine_normal.png") private val vehicleModel by model( "${DemoLoader.modelPath}/kool-car.glb", - GltfLoadConfig(materialConfig = GltfMaterialConfig(isDeferredShading = true)) + GltfLoadConfig(materialConfig = GltfMaterialConfig( + shaderFactory = { pbrConfig -> + gbufferShader { + vertexCfg.modelMatrixComposition = pbrConfig.vertexCfg.modelMatrixComposition + colorCfg.colorSources.addAll(pbrConfig.colorCfg.colorSources) + normalMapCfg.set(pbrConfig.normalMapCfg) + roughnessCfg.propertySources.addAll(pbrConfig.roughnessCfg.propertySources) + metallicCfg.propertySources.addAll(pbrConfig.metallicCfg.propertySources) + aoCfg.propertySources.addAll(pbrConfig.aoCfg.propertySources) + } + } + )) ) lateinit var vehicleWorld: VehicleWorld @@ -38,10 +47,13 @@ class VehicleDemo : DemoScene("Vehicle Demo") { private var track: Track? = null var timer: TrackTimer? = null - private lateinit var deferredPipeline: DeferredPipeline + internal lateinit var deferredPipeline: Deferred2Pipeline override suspend fun loadResources(ctx: KoolContext) { - val shadows = CascadedShadowMap(mainScene, mainScene.lighting.lights[0], maxRange = 400f, mapSizes = listOf(4096, 2048, 2048)).apply { + val sceneCam = PerspectiveCamera() + val sceneContent = Node() + + val shadows = CascadedShadowMap(sceneCam, sceneContent, mainScene.lighting.lights[0], maxRange = 400f, mapSizes = listOf(4096, 2048, 2048)).apply { mapRanges[0].set(0f, 0.03f) mapRanges[1].set(0.03f, 0.17f) mapRanges[2].set(0.17f, 1f) @@ -50,52 +62,47 @@ class VehicleDemo : DemoScene("Vehicle Demo") { map.shaderDepthOffset = if (i == 0) -0.0004f else -0.002f } } - showLoadText("Loading Physics".l()) + shadows.addToScene(mainScene) showLoadText("Creating Deferred Render Pipeline".l()) - val defCfg = DeferredPipelineConfig().apply { - maxGlobalLights = 1 - isWithAmbientOcclusion = true - isWithScreenSpaceReflections = false - isWithBloom = true - isWithVignette = true - - bloomKernelSize = 10 - bloomAvgDownSampling = false - - useImageBasedLighting(ibl) - useShadowMaps(emptyList()) - useShadowMaps(listOf(shadows)) - - // set output depth compare op to ALWAYS, so that the skybox with maximum depth value is drawn - outputDepthTest = DepthCompareOp.ALWAYS - } - deferredPipeline = DeferredPipeline(mainScene, defCfg).apply { - aoPipeline?.mapSize = 0.75f - lightingPassShader.ambientShadowFactor = 0.3f - lightingPassContent += Skybox.cube(ibl.reflectionMap, 1f, colorSpaceConversion = ColorSpaceConversion.AsIs) - } - mainScene += deferredPipeline.createDefaultOutputQuad() - - shadows.drawNode = deferredPipeline.sceneContent + deferredPipeline = Deferred2Pipeline( + content = sceneContent, + camera = sceneCam, + scene = mainScene, + ibl = ibl, + lighting = mainScene.lighting, + shadowMapConfig = listOf(shadows).toConfig(), + renderScale = 1f / UiScale.windowScale.value, + ) + deferredPipeline.enableScreenSpaceReflections() + deferredPipeline.lightingPass.ambientShadowFactor = 0.3f + val bloom = deferredPipeline.installBloomPass() + mainScene += deferredPipeline.defaultOutputQuad(bloom) + shadows.drawNode = deferredPipeline.content + + val deferredLights = DeferredLights(deferredPipeline) showLoadText("Creating Physics World".l()) val physics = PhysicsWorld(mainScene) - vehicleWorld = VehicleWorld(mainScene, physics, deferredPipeline) + vehicleWorld = VehicleWorld(mainScene, physics, deferredPipeline, deferredLights) vehicle = DemoVehicle(this@VehicleDemo, vehicleModel, ctx) showLoadText("Loading Vehicle Audio".l()) vehicle.vehicleAudio.loadAudio() showLoadText("Creating Physics World".l()) - deferredPipeline.sceneContent.apply { + deferredPipeline.content.apply { addNode(vehicle.vehicleGroup) - makeGround() showLoadText("Creating Playground".l()) Playground.makePlayground(vehicleWorld) showLoadText("Creating Track".l()) makeTrack(vehicleWorld) + addNode(deferredLights) +// onUpdate { +// val w = (8f - vehicle.vehicle.linearVelocity.length()).coerceAtLeast(0f) +// deferredPipeline.filterPass.filterWeight = w +// } } } @@ -105,12 +112,12 @@ class VehicleDemo : DemoScene("Vehicle Demo") { setColor(Color.WHITE, 0.75f) } + val camera = deferredPipeline.camera val camRig = ActorTrackingCamRig(vehicleWorld.physics, vehicle.vehicle).apply { - camera.setClipRange(1f, 1e9f) camera.setupCamera(Vec3f(0f, 2.75f, 6f), lookAt = Vec3f(0f, 1.75f, 0f)) addNode(camera) } - addNode(camRig) + deferredPipeline.content.addNode(camRig) onUpdate += { updateDashboard() @@ -226,7 +233,7 @@ class VehicleDemo : DemoScene("Vehicle Demo") { stepsY = sizeY.toInt() / 100 } } - shader = deferredKslPbrShader { + shader = gbufferShader { color { textureColor(groundAlbedo) constColor(color(100), blendMode = ColorBlockConfig.BlendMode.Multiply) @@ -234,6 +241,7 @@ class VehicleDemo : DemoScene("Vehicle Demo") { normalMapping { useNormalMap(groundNormal) } + roughness(0.35f) } } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/VehicleWorld.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/VehicleWorld.kt index 78a6f686e..9f147a9c8 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/VehicleWorld.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/VehicleWorld.kt @@ -3,15 +3,21 @@ package de.fabmax.kool.demo.physics.vehicle import de.fabmax.kool.physics.* import de.fabmax.kool.physics.geometry.TriangleMeshGeometry import de.fabmax.kool.physics.vehicle.VehicleUtils -import de.fabmax.kool.pipeline.deferred.DeferredPipeline -import de.fabmax.kool.pipeline.deferred.deferredKslPbrShader +import de.fabmax.kool.pipeline.deferred2.Deferred2Pipeline +import de.fabmax.kool.pipeline.deferred2.DeferredLights +import de.fabmax.kool.pipeline.deferred2.gbufferShader import de.fabmax.kool.scene.Node import de.fabmax.kool.scene.Scene import de.fabmax.kool.scene.addColorMesh import de.fabmax.kool.scene.geometry.IndexedVertexList import de.fabmax.kool.util.Color -class VehicleWorld(val scene: Scene, val physics: PhysicsWorld, val deferredPipeline: DeferredPipeline) { +class VehicleWorld( + val scene: Scene, + val physics: PhysicsWorld, + val deferredPipeline: Deferred2Pipeline, + val deferredLights: DeferredLights, +) { val defaultMaterial = Material(0.5f) val obstacleSimFilterData = FilterData(VehicleUtils.COLLISION_FLAG_DRIVABLE_OBSTACLE, VehicleUtils.COLLISION_FLAG_DRIVABLE_OBSTACLE_AGAINST) @@ -28,7 +34,7 @@ class VehicleWorld(val scene: Scene, val physics: PhysicsWorld, val deferredPipe } } } - shader = deferredKslPbrShader { + shader = gbufferShader { color { vertexColor() } roughness(rough) metallic(metal) diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/ui/Timer.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/ui/Timer.kt index 4798ef6cc..501e8ad95 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/ui/Timer.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/ui/Timer.kt @@ -2,6 +2,7 @@ package de.fabmax.kool.demo.physics.vehicle.ui import de.fabmax.kool.demo.UiSizes import de.fabmax.kool.modules.ui2.* +import de.fabmax.kool.pipeline.deferred2.Deferred2Pipeline import de.fabmax.kool.toString import de.fabmax.kool.util.Color import de.fabmax.kool.util.MdColor @@ -13,7 +14,20 @@ class Timer(val vehicleUi: VehicleUi) : Composable { val sec1Time = mutableStateOf(0f) val sec2Time = mutableStateOf(0f) - private val isHeadlights = mutableStateOf(vehicleUi.vehicle.isHeadlightsOn).onChange { _, new -> vehicleUi.vehicle.isHeadlightsOn = new } + private val isReflections = mutableStateOf(true).onChange { _, new -> + val pipeline = vehicleUi.vehicle.demo.deferredPipeline + if (new) { + pipeline.enableScreenSpaceReflections() + pipeline.tsaa = Deferred2Pipeline.TSAA_4 + pipeline.filterPass.filterWeight = 8f + pipeline.aoPass.temporalKernels = 4 + } else { + pipeline.disableScreenSpaceReflections() + pipeline.tsaa = Deferred2Pipeline.TSAA_NONE + pipeline.filterPass.filterWeight = 0f + pipeline.aoPass.temporalKernels = 1 + } + } private val isSound = mutableStateOf(false).onChange { _, new -> vehicleUi.onToggleSound(new) } private class TimerBackground(val bgColor: Color) : UiRenderer { @@ -156,18 +170,18 @@ class Timer(val vehicleUi: VehicleUi) : Composable { modifier .width(Grow.Std) .height(Grow.Std) - Text("Headlights".l) { + Text("Reflections".l) { modifier .width(Grow.Std) .height(Grow.Std) .margin(end = sizes.gap) .baselineMargin(sizes.gap * 1.5f) .textColor(labelColor) - .onClick { isHeadlights.toggle() } + .onClick { isReflections.toggle() } } - Switch(isHeadlights.use()) { + Switch(isReflections.use()) { modifier - .onToggle { isHeadlights.set(it) } + .onToggle { isReflections.set(it) } .margin(end = sizes.smallGap, top = sizes.smallGap * 0.5f) } } diff --git a/kool-physics/src/commonMain/kotlin/de/fabmax/kool/physics/vehicle/VehicleUtils.kt b/kool-physics/src/commonMain/kotlin/de/fabmax/kool/physics/vehicle/VehicleUtils.kt index 2e329a2e2..1846a6646 100644 --- a/kool-physics/src/commonMain/kotlin/de/fabmax/kool/physics/vehicle/VehicleUtils.kt +++ b/kool-physics/src/commonMain/kotlin/de/fabmax/kool/physics/vehicle/VehicleUtils.kt @@ -52,9 +52,9 @@ object VehicleUtils { const val COLLISION_FLAG_OBSTACLE = 1 shl 3 const val COLLISION_FLAG_DRIVABLE_OBSTACLE = 1 shl 4 - const val COLLISION_FLAG_GROUND_AGAINST = COLLISION_FLAG_CHASSIS or COLLISION_FLAG_OBSTACLE or COLLISION_FLAG_DRIVABLE_OBSTACLE - const val COLLISION_FLAG_WHEEL_AGAINST = COLLISION_FLAG_WHEEL or COLLISION_FLAG_CHASSIS or COLLISION_FLAG_OBSTACLE - const val COLLISION_FLAG_CHASSIS_AGAINST = COLLISION_FLAG_GROUND or COLLISION_FLAG_WHEEL or COLLISION_FLAG_CHASSIS or COLLISION_FLAG_OBSTACLE or COLLISION_FLAG_DRIVABLE_OBSTACLE - const val COLLISION_FLAG_OBSTACLE_AGAINST = COLLISION_FLAG_GROUND or COLLISION_FLAG_WHEEL or COLLISION_FLAG_CHASSIS or COLLISION_FLAG_OBSTACLE or COLLISION_FLAG_DRIVABLE_OBSTACLE - const val COLLISION_FLAG_DRIVABLE_OBSTACLE_AGAINST = COLLISION_FLAG_GROUND or COLLISION_FLAG_CHASSIS or COLLISION_FLAG_OBSTACLE or COLLISION_FLAG_DRIVABLE_OBSTACLE + const val COLLISION_FLAG_GROUND_AGAINST = COLLISION_FLAG_CHASSIS or COLLISION_FLAG_OBSTACLE or COLLISION_FLAG_DRIVABLE_OBSTACLE + const val COLLISION_FLAG_WHEEL_AGAINST = COLLISION_FLAG_WHEEL or COLLISION_FLAG_CHASSIS or COLLISION_FLAG_OBSTACLE + const val COLLISION_FLAG_CHASSIS_AGAINST = COLLISION_FLAG_GROUND or COLLISION_FLAG_WHEEL or COLLISION_FLAG_CHASSIS or COLLISION_FLAG_OBSTACLE or COLLISION_FLAG_DRIVABLE_OBSTACLE + const val COLLISION_FLAG_OBSTACLE_AGAINST = COLLISION_FLAG_GROUND or COLLISION_FLAG_WHEEL or COLLISION_FLAG_CHASSIS or COLLISION_FLAG_OBSTACLE or COLLISION_FLAG_DRIVABLE_OBSTACLE + const val COLLISION_FLAG_DRIVABLE_OBSTACLE_AGAINST = COLLISION_FLAG_GROUND or COLLISION_FLAG_CHASSIS or COLLISION_FLAG_OBSTACLE or COLLISION_FLAG_DRIVABLE_OBSTACLE } From 7b66529b97efc87fdeb541a9ac3de7ac15b5542c Mon Sep 17 00:00:00 2001 From: Max Thiele Date: Fri, 29 May 2026 22:26:32 +0200 Subject: [PATCH 26/27] Finalizing deferred 2 --- .../de/fabmax/kool/modules/gltf/GltfConfig.kt | 29 +- .../modules/ksl/ShaderAttributeConfigs.kt | 8 + .../ksl/blocks/ColorSpaceConversion.kt | 2 +- .../pipeline/deferred2/Deferred2Pipeline.kt | 59 +++- .../kool/pipeline/deferred2/GbufferPass.kt | 17 +- .../kool/pipeline/deferred2/LightingPass.kt | 8 +- .../deferred2/ReprojectComputePass.kt | 14 +- .../pipeline/deferred2/TemporalFilterPass.kt | 2 +- .../de/fabmax/kool/scene/ModelTemplate.kt | 2 +- .../de/fabmax/kool/util/Buffer.desktop.kt | 108 ++++-- .../kotlin/de/fabmax/kool/demo/AoDemo.kt | 4 +- .../de/fabmax/kool/demo/DeferredDemo.kt | 308 +++++++++--------- .../kotlin/de/fabmax/kool/demo/GltfDemo.kt | 148 ++------- .../kool/demo/physics/vehicle/DemoVehicle.kt | 3 - .../kool/demo/physics/vehicle/VehicleDemo.kt | 29 +- .../kool/demo/physics/vehicle/ui/Timer.kt | 2 + .../de/fabmax/kool/demo/procedural/Glass.kt | 22 +- .../kool/demo/procedural/ProceduralDemo.kt | 54 ++- .../de/fabmax/kool/demo/procedural/Roses.kt | 10 +- .../de/fabmax/kool/demo/procedural/Table.kt | 4 +- .../de/fabmax/kool/demo/procedural/Vase.kt | 5 +- 21 files changed, 431 insertions(+), 407 deletions(-) diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/gltf/GltfConfig.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/gltf/GltfConfig.kt index 99fc41f69..3284ac832 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/gltf/GltfConfig.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/gltf/GltfConfig.kt @@ -4,9 +4,12 @@ import de.fabmax.kool.AssetLoader import de.fabmax.kool.modules.ksl.KslPbrShader import de.fabmax.kool.modules.ksl.KslShader import de.fabmax.kool.modules.ksl.ModelMatrixComposition +import de.fabmax.kool.modules.ksl.blocks.ColorSpaceConversion import de.fabmax.kool.pipeline.Texture2d import de.fabmax.kool.pipeline.deferred.DeferredKslPbrShader +import de.fabmax.kool.pipeline.deferred2.gbufferShader import de.fabmax.kool.pipeline.ibl.EnvironmentMap +import de.fabmax.kool.scene.Mesh import de.fabmax.kool.util.ShadowMap import de.fabmax.kool.util.Struct @@ -35,5 +38,29 @@ data class GltfMaterialConfig( val maxNumberOfLights: Int = 4, val fixedNumberOfJoints: Int = 0, val modelMatrixComposition: List = emptyList(), - val shaderFactory: ((DeferredKslPbrShader.Config) -> KslShader)? = null, + val shaderFactory: GltfShaderFactory? = null, ) + +fun interface GltfShaderFactory { + fun createShader(mesh: Mesh<*>, pbrConfig: DeferredKslPbrShader.Config.Builder): KslShader +} + +object GltfDeferredShaderFactory : GltfShaderFactory { + override fun createShader(mesh: Mesh<*>, pbrConfig: DeferredKslPbrShader.Config.Builder): KslShader { + return if (mesh.isOpaque) { + val cfg = pbrConfig.build() + gbufferShader { + vertexCfg.set(cfg.vertexCfg) + colorCfg.colorSources.addAll(cfg.colorCfg.colorSources) + normalMapCfg.set(cfg.normalMapCfg) + roughnessCfg.propertySources.addAll(cfg.roughnessCfg.propertySources) + metallicCfg.propertySources.addAll(cfg.metallicCfg.propertySources) + aoCfg.propertySources.addAll(cfg.aoCfg.propertySources) + alphaMode = cfg.alphaMode + } + } else { + pbrConfig.colorSpaceConversion = ColorSpaceConversion.AsIs + KslPbrShader(pbrConfig.build()) + } + } +} diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/ShaderAttributeConfigs.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/ShaderAttributeConfigs.kt index c87bd9f14..c833e57ee 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/ShaderAttributeConfigs.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/ShaderAttributeConfigs.kt @@ -43,6 +43,14 @@ data class BasicVertexConfig( field = value } + fun set(other: BasicVertexConfig) { + isFlipBacksideNormals = other.isFlipBacksideNormals + maxNumberOfBones = other.maxNumberOfBones + morphAttributes.addAll(other.morphAttributes) + displacementCfg.propertySources.addAll(other.displacementCfg.propertySources) + modelMatrixComposition = other.modelMatrixComposition + } + fun enableArmatureFixedNumberOfBones(fixedNumberOfBones: Int): Builder { this.maxNumberOfBones = fixedNumberOfBones return this diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/ColorSpaceConversion.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/ColorSpaceConversion.kt index fed35ca6b..259189c09 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/ColorSpaceConversion.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/ColorSpaceConversion.kt @@ -190,7 +190,7 @@ class ToneMapLinearColorUncharted2(parentScope: KslScopeBuilder) : fun KslScopeBuilder.convertColorSpace(inputColor: KslExprFloat3, conversion: ColorSpaceConversion): KslVectorExpression = when(conversion) { - ColorSpaceConversion.AsIs -> inputColor + ColorSpaceConversion.AsIs -> clamp(inputColor, 0f.const3, 1000f.const3) is ColorSpaceConversion.SrgbToLinear -> pow(inputColor, Vec3f(conversion.gamma).const) is ColorSpaceConversion.LinearToSrgb -> pow(inputColor, Vec3f(conversion.gamma).const) is ColorSpaceConversion.LinearToSrgbHdr -> { diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/Deferred2Pipeline.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/Deferred2Pipeline.kt index 85c89887f..93b22c90e 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/Deferred2Pipeline.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/Deferred2Pipeline.kt @@ -1,9 +1,6 @@ package de.fabmax.kool.pipeline.deferred2 -import de.fabmax.kool.math.MutableMat4f -import de.fabmax.kool.math.Vec2f -import de.fabmax.kool.math.Vec2i -import de.fabmax.kool.math.clamp +import de.fabmax.kool.math.* import de.fabmax.kool.modules.ksl.KslShader import de.fabmax.kool.modules.ksl.ShadowMapConfig import de.fabmax.kool.modules.ksl.blocks.ColorSpaceConversion @@ -309,8 +306,13 @@ fun Deferred2Pipeline.installBloomPass(): BloomPass { return bloomPass } -fun Deferred2Pipeline.defaultOutputQuad(bloomPass: BloomPass?, writeDepth: Boolean = false): Mesh<*> { - val outputShader = defaultOutputShader(bloomPass, writeDepth) +fun Deferred2Pipeline.defaultOutputQuad( + bloomPass: BloomPass?, + writeDepth: Boolean = false, + vignette: Vignette? = null, + chromaticAberration: Vec3f? = null, +): Mesh<*> { + val outputShader = defaultOutputShader(bloomPass, writeDepth, vignette, chromaticAberration) return TextureMesh().apply { generate { generateFullscreenQuad() @@ -322,6 +324,8 @@ fun Deferred2Pipeline.defaultOutputQuad(bloomPass: BloomPass?, writeDepth: Boole fun Deferred2Pipeline.defaultOutputShader( bloomPass: BloomPass?, writeDepth: Boolean = false, + vignette: Vignette? = null, + chromaticAberration: Vec3f? = null, ): KslShader { val pipelineConfig = if (writeDepth) { PipelineConfig(blendMode = BlendMode.DISABLED) @@ -341,13 +345,43 @@ fun Deferred2Pipeline.defaultOutputShader( val bloom = texture2d("bloomOutput") val ditherTex = texture2d("ditherPattern") val depthTex = if (writeDepth) texture2d("depth", isUnfilterable = true) else null + val vignetteR = if (vignette != null) uniformFloat2("vignetteR") else null + val aberrationCfg = if (chromaticAberration != null) uniformFloat3("aberrationCfg") else null + + val fnSampleRgb = functionFloat3("fnSampleRgb") { + val tex = paramColorTex2d() + val uv = paramFloat2() + body { + if (aberrationCfg == null) { + tex.sample(uv).rgb + } else { + val centerUv by uv - 0.5f.const + val s by length(centerUv) / 0.3f.const + val str by aberrationCfg * s * s + val uvR by centerUv * (1f.const + str.r) + val uvG by centerUv * (1f.const + str.g) + val uvB by centerUv * (1f.const + str.b) + val r by tex.sample(uvR + 0.5f.const).r + val g by tex.sample(uvG + 0.5f.const).g + val b by tex.sample(uvB + 0.5f.const).b + float3Value(r, g, b) + } + } + } main { val uvi = (uv.output * output.size().toFloat2()).toInt2() - val color by output.sample(uv.output).rgb + bloom.sample(uv.output).rgb + val color by fnSampleRgb(output, uv.output) + fnSampleRgb(bloom, uv.output) val ditherC by uvi % ditherTex.size() val ditherNoise by ditherTex.load(ditherC).r val srgb by convertColorSpace(color, ColorSpaceConversion.LinearToSrgbHdr()) + (ditherNoise - 0.5f.const) / 255f.const + + vignetteR?.let { + val vignetteColor = uniformFloat4("vignetteColor") + val uvR by length(uv.output - 0.5f.const2) + val vignetteF by smoothStep(vignetteR.x, vignetteR.y, uvR) * vignetteColor.a + srgb set mix(srgb, vignetteColor.rgb, vignetteF) + } colorOutput(srgb) depthTex?.let { @@ -364,6 +398,13 @@ fun Deferred2Pipeline.defaultOutputShader( outputShader.bindTexture2d("bloomOutput", bloomMap) var inputTex by outputShader.bindTexture2d("deferredOutput", defaultSampler = SamplerSettings().nearest().clamped()) var inputDepth by outputShader.bindTexture2d("depth") + vignette?.let { + outputShader.bindUniformFloat2("vignetteR", Vec2f(it.innerRadius, it.outerRadius)) + outputShader.bindUniformColor("vignetteColor", it.vignetteColor) + } + chromaticAberration?.let { + outputShader.bindUniformFloat3("aberrationCfg", it) + } onSwap { val filterOutput = filterPass.filterOutput.newVal outputShader.swapPipelineData(filterOutput) { @@ -375,3 +416,7 @@ fun Deferred2Pipeline.defaultOutputShader( } return outputShader } + +data class Vignette(val innerRadius: Float = 0.4f, val outerRadius: Float = 0.71f, val vignetteColor: Color = Color.BLACK.withAlpha(0.3f)) + +val ChromaticAberrationDefault = Vec3f(-0.003f, 0.0f, 0.003f) diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/GbufferPass.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/GbufferPass.kt index e39ff525b..2aba8da16 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/GbufferPass.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/GbufferPass.kt @@ -6,6 +6,7 @@ import de.fabmax.kool.pipeline.* import de.fabmax.kool.scene.InstanceLayouts import de.fabmax.kool.scene.Mesh import de.fabmax.kool.util.* +import kotlin.math.max class GbufferPass( initialSize: Vec2i, @@ -41,10 +42,17 @@ class GbufferPass( onAfterCollectDrawCommands += { viewData -> val upload = pipeline.reprojectMatrixComputePass.uploadData.newVal upload.viewProjMat.set(viewData.drawQueue.viewProjMatF) - // fixme: upload.modelMats.limit = 0 alphaMeshes.clear() lightMeshes.clear() + // This is a bit of a hack to prevent modifying data while it is uploaded to oldModelMats binding during + // the first frame. Later frames do not upload data to oldModelMats because of buffer swapping. Therefore, + // it is then safe to modify the buffer. + val canUpload = pipeline.reprojectMatrixComputePass.isWarmedUp + if (canUpload) { + upload.modelMats.limit = 0 + } + val drawQueue = viewData.drawQueue val it = drawQueue.iterator() while (it.hasNext()) { @@ -61,9 +69,9 @@ class GbufferPass( val idRange = pipeline.idAllocator.getIdRange(cmd.mesh) val bufferPos = idRange.from * 16 shader.objectId = idRange.from - // fixme: upload.modelMats.limit = max(upload.modelMats.limit, bufferPos + idRange.size * 16) - try { + if (canUpload) { + upload.modelMats.limit = max(upload.modelMats.limit, bufferPos + idRange.size * 16) val instances = mesh.instances if (instances != null) { val matrixExtractor = shader.config.instanceModelMatExtractor ?: DefaultInstanceModelMatrixExtractor @@ -78,9 +86,6 @@ class GbufferPass( upload.modelMats.position = bufferPos cmd.modelMatF.putTo(upload.modelMats) } - } catch (e: Exception) { - logE { "Error updating model matrices" } - e.printStackTrace() } } is DeferredLightShader -> { diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/LightingPass.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/LightingPass.kt index fc4f1b8d2..66686f2f2 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/LightingPass.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/LightingPass.kt @@ -27,7 +27,7 @@ class LightingPass( drawNode = Node(), attachmentConfig = AttachmentConfig { addColor(TexFormat.RG11B10_F, filterMethod = FilterMethod.NEAREST) // metal, roughness, ao - transientDepth() + defaultDepth() }, initialSize = size, name = "deferred2-lighting-pass" @@ -59,6 +59,12 @@ class LightingPass( viewData.drawQueue.addMesh(mesh, pipeline) } } + for (i in gbuffer.alphaMeshes.indices) { + val mesh = gbuffer.alphaMeshes[i] + mesh.getOrCreatePipeline(ctx)?.let { pipeline -> + viewData.drawQueue.addMesh(mesh, pipeline) + } + } } } diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/ReprojectComputePass.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/ReprojectComputePass.kt index b28602642..467a0853c 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/ReprojectComputePass.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/ReprojectComputePass.kt @@ -15,20 +15,18 @@ class ReprojectComputePass( private val pipeline: Deferred2Pipeline ) : ComputePass("deferred2-reproject-compute-pass") { - val uploadData = AlternatingPair { - UploadData(maxObjects) - } - - val modelMats = AlternatingPair { - StorageBuffer(GpuType.Mat4, maxObjects) - } + val uploadData = AlternatingPair { UploadData(maxObjects) } + val modelMats = AlternatingPair { StorageBuffer(GpuType.Mat4, maxObjects) } val reprojectMats = StorageBuffer(GpuType.Mat4, maxObjects) + private var frameCnt = 0 + internal val isWarmedUp: Boolean get() = frameCnt > 2 private val shader = ReprojectComputeShader(reprojectMats) init { val groupsX = (maxObjects + 63) / 64 - addTask(shader, Vec3i(groupsX, 1, 1)) + val task = addTask(shader, Vec3i(groupsX, 1, 1)) + task.onAfterDispatch { frameCnt++ } onRelease { modelMats.a.releaseDelayed(1) modelMats.b.releaseDelayed(1) diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/TemporalFilterPass.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/TemporalFilterPass.kt index 8d6c94a86..f2aa43ecc 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/TemporalFilterPass.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/TemporalFilterPass.kt @@ -57,7 +57,7 @@ class TemporalFilterPass( val newGbuffer = pipeline.gbuffers.newVal val oldGbuffer = pipeline.gbuffers.oldVal - newDepth = newGbuffer.depth + newDepth = pipeline.lightingPass.depthTexture oldDepth = oldGbuffer.depth oldMeta = oldGbuffer.objectIds newMeta = newGbuffer.objectIds diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/ModelTemplate.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/ModelTemplate.kt index 373706e03..65b012b26 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/ModelTemplate.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/ModelTemplate.kt @@ -551,7 +551,7 @@ class ModelTemplate(val scene: GltfScene, val gltfFile: GltfFile) : BaseReleasab val shaderFactory = cfg.materialConfig.shaderFactory val shader = when { - shaderFactory != null -> shaderFactory(pbrConfig.build()) + shaderFactory != null -> shaderFactory.createShader(mesh, pbrConfig) isDeferred -> { pbrConfig.pipelineCfg.blendMode = BlendMode.DISABLED DeferredKslPbrShader(pbrConfig.build()) diff --git a/kool-core/src/desktopMain/kotlin/de/fabmax/kool/util/Buffer.desktop.kt b/kool-core/src/desktopMain/kotlin/de/fabmax/kool/util/Buffer.desktop.kt index 547940cae..e30308f7d 100644 --- a/kool-core/src/desktopMain/kotlin/de/fabmax/kool/util/Buffer.desktop.kt +++ b/kool-core/src/desktopMain/kotlin/de/fabmax/kool/util/Buffer.desktop.kt @@ -19,6 +19,38 @@ inline fun Int32Buffer.useRaw(block: (IntBuffer) -> R): R = (this as Int32Bu inline fun Float32Buffer.useRaw(block: (FloatBuffer) -> R): R = (this as Float32BufferImpl).useRaw(block) inline fun MixedBuffer.useRaw(block: (ByteBuffer) -> R): R = (this as MixedBufferImpl).useRaw(block) + +fun Uint8BufferImpl(capacity: Int, isAutoLimit: Boolean = false) = Uint8BufferImpl( + buffer = ByteBuffer.allocateDirect(capacity).order(ByteOrder.nativeOrder()), + isAutoLimit = isAutoLimit +) + +fun Uint8BufferImpl(data: ByteArray): Uint8BufferImpl { + val buf = Uint8BufferImpl(ByteBuffer.allocateDirect(data.size).order(ByteOrder.nativeOrder()), false) + buf.put(data) + return buf +} + +fun Uint16BufferImpl(capacity: Int, isAutoLimit: Boolean = false) = Uint16BufferImpl( + buffer = ByteBuffer.allocateDirect(capacity * 2).order(ByteOrder.nativeOrder()).asShortBuffer(), + isAutoLimit = isAutoLimit +) + +fun Int32BufferImpl(capacity: Int, isAutoLimit: Boolean = false) = Int32BufferImpl( + buffer = ByteBuffer.allocateDirect(capacity * 4).order(ByteOrder.nativeOrder()).asIntBuffer(), + isAutoLimit = isAutoLimit +) + +fun Float32BufferImpl(capacity: Int, isAutoLimit: Boolean = false) = Float32BufferImpl( + buffer = ByteBuffer.allocateDirect(capacity * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(), + isAutoLimit = isAutoLimit +) + +fun MixedBufferImpl(capacity: Int, isAutoLimit: Boolean = false) = MixedBufferImpl( + buffer = ByteBuffer.allocateDirect(capacity).order(ByteOrder.nativeOrder()), + isAutoLimit = isAutoLimit +) + abstract class GenericBuffer( override val capacity: Int, protected val buffer: B, @@ -80,46 +112,41 @@ abstract class GenericBuffer( finishRawBuffer() val modAfter = modCount if (modBefore != modAfter) { - logE { "Buffer was modified while used raw" } + logE { "Buffer was modified externally while used raw" } } return result } } -class Uint8BufferImpl(buffer: ByteBuffer, isAutoLimit: Boolean = false) : - GenericBuffer(buffer.capacity(), buffer, isAutoLimit), Uint8Buffer -{ - - constructor(capacity: Int, isAutoLimit: Boolean = false) : this( - ByteBuffer.allocateDirect(capacity).order(ByteOrder.nativeOrder()), - isAutoLimit - ) - - constructor(data: ByteArray): this(ByteBuffer.allocateDirect(data.size).order(ByteOrder.nativeOrder()), false) { - put(data) - } - +class Uint8BufferImpl( + buffer: ByteBuffer, + isAutoLimit: Boolean = false +) : GenericBuffer(buffer.capacity(), buffer, isAutoLimit), Uint8Buffer { override fun get(i: Int): UByte { return buffer[i].toUByte() } override fun set(i: Int, value: UByte) { + modCount++ buffer.put(i, value.toByte()) } override fun put(value: UByte): Uint8Buffer { + modCount++ buffer.put(value.toByte()) pos++ return this } override fun put(data: ByteArray, offset: Int, len: Int): Uint8Buffer { + modCount++ buffer.put(data, offset, len) pos += len return this } override fun put(data: Uint8Buffer): Uint8Buffer { + modCount++ data.useRaw { buffer.put(it) pos += data.limit @@ -128,33 +155,35 @@ class Uint8BufferImpl(buffer: ByteBuffer, isAutoLimit: Boolean = false) : } } -class Uint16BufferImpl(buffer: ShortBuffer, isAutoLimit: Boolean = false) : - GenericBuffer(buffer.capacity(), buffer, isAutoLimit), Uint16Buffer -{ - - constructor(capacity: Int, isAutoLimit: Boolean = false) : this(ByteBuffer.allocateDirect(capacity * 2).order(ByteOrder.nativeOrder()).asShortBuffer(), isAutoLimit) - +class Uint16BufferImpl( + buffer: ShortBuffer, + isAutoLimit: Boolean = false +) : GenericBuffer(buffer.capacity(), buffer, isAutoLimit), Uint16Buffer { override fun get(i: Int): UShort { return buffer[i].toUShort() } override fun set(i: Int, value: UShort) { + modCount++ buffer.put(i, value.toShort()) } override fun put(value: UShort): Uint16Buffer { + modCount++ buffer.put(value.toShort()) pos++ return this } override fun put(data: ShortArray, offset: Int, len: Int): Uint16Buffer { + modCount++ buffer.put(data, offset, len) pos += len return this } override fun put(data: Uint16Buffer): Uint16Buffer { + modCount++ data.useRaw { buffer.put(it) pos += data.limit @@ -166,30 +195,31 @@ class Uint16BufferImpl(buffer: ShortBuffer, isAutoLimit: Boolean = false) : class Int32BufferImpl(buffer: IntBuffer, isAutoLimit: Boolean = false) : GenericBuffer(buffer.capacity(), buffer, isAutoLimit), Int32Buffer { - - constructor(capacity: Int, isAutoLimit: Boolean = false) : this(ByteBuffer.allocateDirect(capacity * 4).order(ByteOrder.nativeOrder()).asIntBuffer(), isAutoLimit) - override fun get(i: Int): Int { return buffer[i] } override fun set(i: Int, value: Int) { + modCount++ buffer.put(i, value) } override fun put(value: Int): Int32Buffer { + modCount++ buffer.put(value) pos++ return this } override fun put(data: IntArray, offset: Int, len: Int): Int32Buffer { + modCount++ buffer.put(data, offset, len) pos += len return this } override fun put(data: Int32Buffer): Int32Buffer { + modCount++ data.useRaw { buffer.put(it) pos += data.limit @@ -204,38 +234,35 @@ class Int32BufferImpl(buffer: IntBuffer, isAutoLimit: Boolean = false) : class Float32BufferImpl(buffer: FloatBuffer, isAutoLimit: Boolean = false) : GenericBuffer(buffer.capacity(), buffer, isAutoLimit), Float32Buffer { - - constructor(capacity: Int, isAutoLimit: Boolean = false) : this(ByteBuffer.allocateDirect(capacity * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(), isAutoLimit) - override fun get(i: Int): Float { return buffer[i] } override fun set(i: Int, value: Float) { - buffer.put(i, value) modCount++ + buffer.put(i, value) } override fun put(value: Float): Float32Buffer { + modCount++ buffer.put(value) pos++ - modCount++ return this } override fun put(data: FloatArray, offset: Int, len: Int): Float32Buffer { + modCount++ buffer.put(data, offset, len) pos += len - modCount++ return this } override fun put(data: Float32Buffer): Float32Buffer { + modCount++ data.useRaw { buffer.put(it) pos += data.limit } - modCount++ return this } } @@ -243,28 +270,29 @@ class Float32BufferImpl(buffer: FloatBuffer, isAutoLimit: Boolean = false) : class MixedBufferImpl(buffer: ByteBuffer, isAutoLimit: Boolean = false) : GenericBuffer(buffer.capacity(), buffer, isAutoLimit), MixedBuffer { - - constructor(capacity: Int, isAutoLimit: Boolean = false) : this(ByteBuffer.allocateDirect(capacity).order(ByteOrder.nativeOrder()), isAutoLimit) - override fun put(data: MixedBuffer): MixedBuffer { + modCount++ data.useRaw { buffer.put(it) } pos += data.limit return this } override fun putUint8(value: UByte): MixedBuffer { + modCount++ buffer.put(value.toByte()) pos++ return this } override fun putUint8(data: ByteArray, offset: Int, len: Int): MixedBuffer { + modCount++ buffer.put(data, offset, len) pos += len return this } override fun putUint8(data: Uint8Buffer): MixedBuffer { + modCount++ data.useRaw { buffer.put(it) } pos += data.limit return this @@ -275,17 +303,20 @@ class MixedBufferImpl(buffer: ByteBuffer, isAutoLimit: Boolean = false) : } override fun setUint8(byteIndex: Int, value: UByte): MixedBuffer { + modCount++ buffer.put(byteIndex, value.toByte()) return this } override fun putUint16(value: UShort): MixedBuffer { + modCount++ buffer.putShort(value.toShort()) pos += SIZEOF_SHORT return this } override fun putUint16(data: ShortArray, offset: Int, len: Int): MixedBuffer { + modCount++ if (len <= BUFFER_CONV_THRESH) { for (i in 0 until len) { buffer.putShort(data[offset + i]) @@ -298,6 +329,7 @@ class MixedBufferImpl(buffer: ByteBuffer, isAutoLimit: Boolean = false) : } override fun putUint16(data: Uint16Buffer): MixedBuffer { + modCount++ if (data.limit <= BUFFER_CONV_THRESH) { for (i in 0 until data.limit) { buffer.putShort(data[i].toShort()) @@ -316,17 +348,20 @@ class MixedBufferImpl(buffer: ByteBuffer, isAutoLimit: Boolean = false) : } override fun setUint16(byteIndex: Int, value: UShort): MixedBuffer { + modCount++ buffer.putShort(byteIndex, value.toShort()) return this } override fun putInt32(value: Int): MixedBuffer { + modCount++ buffer.putInt(value) pos += SIZEOF_INT return this } override fun putInt32(data: IntArray, offset: Int, len: Int): MixedBuffer { + modCount++ if (len <= BUFFER_CONV_THRESH) { for (i in 0 until len) { buffer.putInt(data[offset + i]) @@ -339,6 +374,7 @@ class MixedBufferImpl(buffer: ByteBuffer, isAutoLimit: Boolean = false) : } override fun putInt32(data: Int32Buffer): MixedBuffer { + modCount++ if (data.limit <= BUFFER_CONV_THRESH) { for (i in 0 until data.limit) { buffer.putInt(data[i]) @@ -357,17 +393,20 @@ class MixedBufferImpl(buffer: ByteBuffer, isAutoLimit: Boolean = false) : } override fun setInt32(byteIndex: Int, value: Int): MixedBuffer { + modCount++ buffer.putInt(byteIndex, value) return this } override fun putFloat32(value: Float): MixedBuffer { + modCount++ buffer.putFloat(value) pos += SIZEOF_FLOAT return this } override fun putFloat32(data: FloatArray, offset: Int, len: Int): MixedBuffer { + modCount++ if (len <= BUFFER_CONV_THRESH) { for (i in 0 until len) { buffer.putFloat(data[offset + i]) @@ -380,6 +419,7 @@ class MixedBufferImpl(buffer: ByteBuffer, isAutoLimit: Boolean = false) : } override fun putFloat32(data: Float32Buffer): MixedBuffer { + modCount++ if (data.limit <= BUFFER_CONV_THRESH) { for (i in 0 until data.limit) { buffer.putFloat(data[i]) @@ -398,11 +438,13 @@ class MixedBufferImpl(buffer: ByteBuffer, isAutoLimit: Boolean = false) : } override fun setFloat32(byteIndex: Int, value: Float): MixedBuffer { + modCount++ buffer.putFloat(byteIndex, value) return this } override fun putPadding(nBytes: Int): MixedBuffer { + modCount++ pos += nBytes buffer.position(pos) return this diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/AoDemo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/AoDemo.kt index a2aef6119..45e2941a0 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/AoDemo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/AoDemo.kt @@ -187,9 +187,7 @@ class AoDemo : DemoScene("Ambient Occlusion") { color { textureColor(albedoMap) } normalMapping { useNormalMap(normalMap) } roughness { textureProperty(roughnessMap) } - ao { - textureProperty(ambientOcclusionMap) - } + ao { textureProperty(ambientOcclusionMap) } lighting { enableSsao(aoPipeline.aoMap) imageBasedAmbientLight(ibl.irradianceMap) diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/DeferredDemo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/DeferredDemo.kt index 33a37561a..63aed87e2 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/DeferredDemo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/DeferredDemo.kt @@ -4,17 +4,17 @@ import de.fabmax.kool.KoolContext import de.fabmax.kool.demo.menu.DemoMenu import de.fabmax.kool.math.* import de.fabmax.kool.modules.ksl.KslUnlitShader -import de.fabmax.kool.modules.ksl.UnlitShaderConfig import de.fabmax.kool.modules.ksl.blocks.ColorSpaceConversion +import de.fabmax.kool.modules.ksl.blocks.noise13 import de.fabmax.kool.modules.ksl.lang.* import de.fabmax.kool.modules.ui2.* import de.fabmax.kool.pipeline.BloomPass import de.fabmax.kool.pipeline.DepthCompareOp +import de.fabmax.kool.pipeline.Texture2d import de.fabmax.kool.pipeline.ao.AoRadius +import de.fabmax.kool.pipeline.decodeNormalInt import de.fabmax.kool.pipeline.deferred2.* import de.fabmax.kool.scene.* -import de.fabmax.kool.scene.geometry.IndexedVertexList -import de.fabmax.kool.scene.geometry.MeshBuilder import de.fabmax.kool.util.Color import de.fabmax.kool.util.MdColor import de.fabmax.kool.util.Time @@ -27,7 +27,7 @@ class DeferredDemo : DemoScene("Deferred Shading") { private lateinit var pipeline: Deferred2Pipeline private lateinit var bloomPass: BloomPass - private lateinit var objects: ColorMesh + private lateinit var objects: Node private lateinit var objectShader: GbufferShader private lateinit var lightPositionMesh: Mesh @@ -158,47 +158,54 @@ class DeferredDemo : DemoScene("Deferred Shading") { } private fun Node.makeContent() { - objects = addColorMesh { - generate { - val sphereProtos = mutableListOf>() - for (i in 0..10) { - val builder = MeshBuilder(IndexedVertexList(VertexLayouts.PositionNormalColor)) - sphereProtos += builder.geometry - builder.apply { - icoSphere { - steps = 3 - radius = rand.randomF(0.3f, 0.4f) - center.set(0f, 0.1f + radius, 0f) - } + objects = addGroup { + val cubeInstances = MeshInstanceList(InstanceLayouts.ModelMat) + val sphereInstances = MeshInstanceList(InstanceLayouts.ModelMat) + objectShader = gbufferShader { + vertices { instancedModelMatrix() } + color { vertexColor() } + roughness { uniformProperty(roughness.value) } + } + addColorMesh(instances = sphereInstances) { + generate { + color = Color.WHITE + icoSphere { + steps = 3 + center.set(0f, 1.1f, 0f) + } + } + shader = objectShader + } + addColorMesh(instances = cubeInstances) { + generate { + color = Color.WHITE + cube { + origin.set(0f, 0.6f, 0f) } } + shader = objectShader + } - for (x in -19..19) { - for (y in -19..19) { - color = Color.WHITE - withTransform { - translate(x.toFloat(), 0f, y.toFloat()) - if ((x + 100) % 2 == (y + 100) % 2) { - cube { - size.set( - rand.randomF(0.6f, 0.8f), - rand.randomF(0.6f, 0.95f), - rand.randomF(0.6f, 0.8f) - ) - origin.set(0f, 0.1f + size.y * 0.5f, 0f) - } - } else { - geometry(sphereProtos[rand.randomI(sphereProtos.indices)]) - } + val t = MutableMat4f() + for (x in -19..19) { + for (y in -19..19) { + t.setIdentity().translate(x.toFloat(), 0f, y.toFloat()) + if ((x + 100) % 2 == (y + 100) % 2) { + cubeInstances.addInstance { + val sx = rand.randomF(0.6f, 0.8f) + val sy = rand.randomF(0.6f, 0.95f) + val sz = rand.randomF(0.6f, 0.8f) + t.scale(Vec3f(sx, sy, sz)) + set(it.modelMat, t) + } + } else { + sphereInstances.addInstance { + t.scale(rand.randomF(0.3f, 0.4f)) + set(it.modelMat, t) } } } } - objectShader = gbufferShader { - color { vertexColor() } - roughness { uniformProperty(roughness.value) } - } - shader = objectShader } lightPositionMesh = addMesh(VertexLayouts.PositionNormal, instances = lightPosInsts) { @@ -328,20 +335,6 @@ class DeferredDemo : DemoScene("Deferred Shading") { } } -// Text("Bloom".l) { sectionTitleStyle() } -// MenuRow { -// Text("Strength".l) { labelStyle(lblSize) } -// MenuSlider(bloomStrength.use(), 0f, 5f, txtWidth = txtSize) { bloomStrength.set(it) } -// } -// MenuRow { -// Text("Radius".l) { labelStyle(lblSize) } -// MenuSlider(bloomRadius.use(), 0f, 2.5f, txtWidth = txtSize) { bloomRadius.set(it) } -// } -// MenuRow { -// Text("Threshold".l) { labelStyle(lblSize) } -// MenuSlider(bloomThreshold.use(), 0f, 2f, txtWidth = txtSize) { bloomThreshold.set(it) } -// } - Text("Objects".l) { sectionTitleStyle() } LabeledSwitch("Show objects".l, isObjects) MenuRow { @@ -356,73 +349,79 @@ class DeferredDemo : DemoScene("Deferred Shading") { .align(AlignmentX.Start, AlignmentY.Bottom) .layout(ColumnLayout) -// val albedoMetal = deferredPipeline.activePass.materialPass.albedoMetal -// val normalRough = deferredPipeline.activePass.materialPass.normalRoughness -// val positionFlags = deferredPipeline.activePass.materialPass.positionFlags -// val bloom = deferredPipeline.activePass.bloomPass?.bloomMap -// val ao = deferredPipeline.aoPipeline?.aoMap - -// Row { -// modifier.margin(vertical = sizes.gap) -// Image { -// modifier -// .imageSize(ImageSize.FixedScale(0.3f)) -// .imageProvider(FlatImageProvider(albedoMetal, true).mirrorY()) -// .margin(horizontal = sizes.gap) -// .customShader(albedoMapShader.apply { colorMap = albedoMetal }) -// Text("Albedo".l) { imageLabelStyle() } -// } -// Image { -// modifier -// .imageSize(ImageSize.FixedScale(0.3f)) -// .imageProvider(FlatImageProvider(normalRough, true).mirrorY()) -// .margin(horizontal = sizes.gap) -// .customShader(normalMapShader.apply { colorMap = normalRough }) -// Text("Normals".l) { imageLabelStyle() } -// } -// } -// Row { -// modifier.margin(vertical = sizes.gap) -// Image { -// modifier -// .imageSize(ImageSize.FixedScale(0.3f)) -// .imageProvider(FlatImageProvider(positionFlags, true).mirrorY()) -// .margin(horizontal = sizes.gap) -// .customShader(positionMapShader.apply { colorMap = positionFlags }) -// Text("Position".l) { imageLabelStyle() } -// } -// Image(ao) { -// modifier -// .imageSize(ImageSize.FixedScale(0.3f / deferredPipeline.aoMapSize)) -// .imageProvider(FlatImageProvider(ao, true).mirrorY()) -// .margin(horizontal = sizes.gap) -// .customShader(AoDemo.aoMapShader.apply { colorMap = ao }) -// Text("Ambient occlusion".l) { imageLabelStyle() } -// } -// } -// Row { -// modifier.margin(vertical = sizes.gap) -// Image(positionFlags) { -// modifier -// .imageSize(ImageSize.FixedScale(0.3f)) -// .imageProvider(FlatImageProvider(positionFlags, true).mirrorY()) -// .margin(horizontal = sizes.gap) -// .customShader(metalRoughFlagsShader.apply { -// metal = albedoMetal -// rough = normalRough -// flags = deferredPipeline.activePass.materialPass.positionFlags -// }) -// Text("Metal (r), roughness (g), flags (b)".l) { imageLabelStyle() } -// } -// Image(bloom) { -// modifier -// .imageSize(ImageSize.FixedScale(0.6f * ((positionFlags.height) / bloom!!.width))) -// .imageProvider(FlatImageProvider(bloom, true).mirrorY()) -// .margin(horizontal = sizes.gap) -// .customShader(bloomMapShader.apply { colorMap = bloom }) -// Text("Bloom".l) { imageLabelStyle() } -// } -// } + val albedo = pipeline.gbuffers.a.albedoEmission + val normals = pipeline.gbuffers.a.normals + val metalRoughAo = pipeline.gbuffers.a.metalRoughnessAo + val objectIds = pipeline.gbuffers.a.objectIds + val ssao = pipeline.aoPass.aoMap + val bloom = bloomPass.bloomMap + + val normalShader = remember { normalShader(normals) } + val objectIdShader = remember { objectIdShader(objectIds) } + + Row { + modifier.margin(vertical = sizes.gap) + Image { + modifier + .width(600.dp) + .imageSize(ImageSize.ZoomContent) + .imageProvider(FlatImageProvider(albedo, true)) + .margin(horizontal = sizes.gap) + .customShader(albedoMapShader.apply { colorMap = albedo }) + Text("Albedo".l) { imageLabelStyle() } + } + Image { + modifier + .width(600.dp) + .imageSize(ImageSize.ZoomContent) + .imageProvider(FlatImageProvider(normals, true)) + .margin(horizontal = sizes.gap) + .customShader(normalShader) + Text("Normals".l) { imageLabelStyle() } + } + } + Row { + modifier.margin(vertical = sizes.gap) + Image { + modifier + .width(600.dp) + .imageSize(ImageSize.ZoomContent) + .imageProvider(FlatImageProvider(metalRoughAo, true)) + .margin(horizontal = sizes.gap) + .customShader(metalRoughAoShader.apply { colorMap = metalRoughAo }) + Text("Metal, roughness, AO".l) { imageLabelStyle() } + } + Image { + modifier + .width(600.dp) + .imageSize(ImageSize.ZoomContent) + .imageProvider(FlatImageProvider(ssao, true)) + .margin(horizontal = sizes.gap) + .customShader(AoDemo.aoMapShader.apply { colorMap = ssao }) + Text("SSAO".l) { imageLabelStyle() } + } + } + Row { + modifier.margin(vertical = sizes.gap) + Image(objectIds) { + modifier + .width(600.dp) + .imageSize(ImageSize.ZoomContent) + .imageProvider(FlatImageProvider(objectIds, true)) + .margin(horizontal = sizes.gap) + .customShader(objectIdShader) + Text("Object IDs".l) { imageLabelStyle() } + } + Image(bloom) { + modifier + .width(600.dp) + .imageSize(ImageSize.ZoomContent) + .imageProvider(FlatImageProvider(bloom, true)) + .margin(horizontal = sizes.gap) + .customShader(bloomMapShader.apply { colorMap = bloom }) + Text("Bloom".l) { imageLabelStyle() } + } + } } } } @@ -481,10 +480,8 @@ class DeferredDemo : DemoScene("Deferred Shading") { companion object { const val MAX_LIGHTS = 5000 - private val albedoMapShader = gBufferShader(0f, 1f) - private val normalMapShader = gBufferShader(1f, 0.5f) - private val positionMapShader = gBufferShader(10f, 0.05f) - private val metalRoughFlagsShader = MetalRoughFlagsShader() + private val albedoMapShader = gBufferShader() + private val metalRoughAoShader = gBufferShader() private val bloomMapShader = KslUnlitShader { pipeline { depthTest = DepthCompareOp.ALWAYS } color { textureData() } @@ -500,7 +497,7 @@ class DeferredDemo : DemoScene("Deferred Shading") { } } - private fun gBufferShader(offset: Float, scale: Float) = KslUnlitShader { + private fun gBufferShader() = KslUnlitShader { pipeline { depthTest = DepthCompareOp.ALWAYS } color { textureData() } modelCustomizer = { @@ -508,41 +505,54 @@ class DeferredDemo : DemoScene("Deferred Shading") { main { val baseColorPort = getFloat4Port("baseColor") val inColor = float4Var(baseColorPort.input.input) - inColor.rgb set (inColor.rgb + offset.const) * scale.const baseColorPort.input(float4Value(inColor.rgb, 1f.const)) } } } } - } - private class MetalRoughFlagsShader : KslUnlitShader(cfg) { - var flags by bindTexture2d("tFlags") - var rough by bindTexture2d("tRough") - var metal by bindTexture2d("tMetal") - - companion object { - val cfg = UnlitShaderConfig { - pipeline { depthTest = DepthCompareOp.ALWAYS } - colorSpaceConversion = ColorSpaceConversion.AsIs - modelCustomizer = { - val uv = interStageFloat2() - vertexStage { - main { - uv.input set vertexAttrib(VertexLayouts.TexCoord.texCoord) - } + private fun normalShader(normalTex: Texture2d) = KslUnlitShader { + pipeline { depthTest = DepthCompareOp.ALWAYS } + color { constColor(Color.BLACK) } + modelCustomizer = { + val uv = interStageFloat2() + vertexStage { + main { uv.input set vertexAttrib(VertexLayouts.TexCoord.texCoord) } + } + fragmentStage { + main { + val normalTex = texture2dInt("normalTex") + val baseColorPort = getFloat4Port("baseColor") + val normalEnc by normalTex.load((uv.output * normalTex.size().toFloat2()).toInt2()).x + val normalDec by decodeNormalInt(normalEnc) * 0.5f.const + 0.5f.const + baseColorPort.input(float4Value(normalDec, 1f.const)) } - fragmentStage { - main { - val metal = texture2d("tMetal").sample(uv.output).a - val rough = texture2d("tRough").sample(uv.output).a - val flags = texture2d("tFlags").sample(uv.output).a - val color = float4Var(float4Value(metal, rough, flags, 1f.const)) - getFloat4Port("baseColor").input(color) - } + } + } + }.apply { + bindTexture2d("normalTex", normalTex) + } + + private fun objectIdShader(objectIds: Texture2d) = KslUnlitShader { + pipeline { depthTest = DepthCompareOp.ALWAYS } + color { constColor(Color.BLACK) } + modelCustomizer = { + val uv = interStageFloat2() + vertexStage { + main { uv.input set vertexAttrib(VertexLayouts.TexCoord.texCoord) } + } + fragmentStage { + main { + val objectIds = texture2dInt("objectIds") + val baseColorPort = getFloat4Port("baseColor") + val objectId by objectIds.load((uv.output * objectIds.size().toFloat2()).toInt2()).x + val objectColor by noise13(objectId.toUint1()) + baseColorPort.input(float4Value(objectColor, 1f.const)) } } } + }.apply { + bindTexture2d("objectIds", objectIds) } } } \ No newline at end of file diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/GltfDemo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/GltfDemo.kt index fca2e7eb7..a2891485d 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/GltfDemo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/GltfDemo.kt @@ -4,13 +4,12 @@ import de.fabmax.kool.Assets import de.fabmax.kool.KoolContext import de.fabmax.kool.demo.menu.DemoMenu import de.fabmax.kool.math.* +import de.fabmax.kool.modules.gltf.GltfDeferredShaderFactory import de.fabmax.kool.modules.gltf.GltfLoadConfig import de.fabmax.kool.modules.gltf.GltfMaterialConfig import de.fabmax.kool.modules.gltf.loadGltfModel -import de.fabmax.kool.modules.ksl.KslPbrShader import de.fabmax.kool.modules.ksl.toConfig import de.fabmax.kool.modules.ui2.* -import de.fabmax.kool.pipeline.ao.AoPipeline import de.fabmax.kool.pipeline.ao.AoRadius import de.fabmax.kool.pipeline.deferred2.Deferred2Pipeline import de.fabmax.kool.pipeline.deferred2.createShadowMaps @@ -19,7 +18,10 @@ import de.fabmax.kool.pipeline.deferred2.gbufferShader import de.fabmax.kool.scene.* import de.fabmax.kool.scene.geometry.MeshBuilder import de.fabmax.kool.scene.geometry.generateNormals -import de.fabmax.kool.util.* +import de.fabmax.kool.util.Color +import de.fabmax.kool.util.MdColor +import de.fabmax.kool.util.Time +import de.fabmax.kool.util.l import kotlinx.coroutines.async import kotlin.math.PI import kotlin.math.cos @@ -73,34 +75,22 @@ class GltfDemo : DemoScene("glTF Models") { private val envMap by hdriImage("${DemoLoader.hdriPath}/shanghai_bund_1k.rgbe.png") -// private lateinit var orbitTransform: OrbitInputTransform private var camTranslationTarget: Vec3d? = null private var trackModel = false - private val shadowsForward = mutableListOf() - private var aoPipelineForward: AoPipeline? = null - private val contentGroupForward = Node() - private lateinit var deferredPipeline: Deferred2Pipeline - private val contentGroupDeferred = Node() + //private val contentGroup = Node() private var animationDeltaTime = 0f private val animationSpeed = mutableStateOf(0.5f) private val isAutoRotate = mutableStateOf(true) - private val isDeferredShading: MutableStateValue = mutableStateOf(true).onChange { _, new -> - setupPipelines(new, isAo.value) - } - private val isAo: MutableStateValue = mutableStateOf(true).onChange { _, new -> - setupPipelines(isDeferredShading.value, new) - } private val isSsr: MutableStateValue = mutableStateOf(true).onChange { _, new -> if (new) { deferredPipeline.enableScreenSpaceReflections() } else { deferredPipeline.disableScreenSpaceReflections() } - setupPipelines(isDeferredShading.value, isAo.value) } override fun lateInit(ctx: KoolContext) { @@ -129,21 +119,9 @@ class GltfDemo : DemoScene("glTF Models") { deferredPipeline.renderScale = 1f / UiScale.windowScale.value deferredPipeline.aoPass.radius = AoRadius.absoluteRadius(0.2f) - // create forward pipeline - aoPipelineForward = AoPipeline.createForward(mainScene).apply { - radius = AoRadius.absoluteRadius(0.2f) - } - shadowsForward += listOf( - SimpleShadowMap(mainScene, mainScene.lighting.lights[0], contentGroupForward, mapSize = 2048), - SimpleShadowMap(mainScene, mainScene.lighting.lights[1], contentGroupForward, mapSize = 2048) - ) - // load models models.map { - it to mainScene.coroutineScope.async { - it.load(false) - it.load(true) - } + it to mainScene.coroutineScope.async { it.load() } }.forEach { (model, deferred) -> showLoadText("Loading ${model.name}") deferred.await() @@ -153,11 +131,8 @@ class GltfDemo : DemoScene("glTF Models") { override fun Scene.setupMainScene(ctx: KoolContext) { setupCamera() - addNode(Skybox.cube(envMap.reflectionMap, 1.5f)) - - makeDeferredContent() - makeForwardContent() - setupPipelines(isDeferredShading.value, isAo.value) + deferredPipeline.content.setupContentGroup() + addNode(deferredPipeline.defaultOutputQuad(null)) onUpdate { animationDeltaTime = Time.deltaT * animationSpeed.value @@ -165,36 +140,6 @@ class GltfDemo : DemoScene("glTF Models") { } } - private fun setupPipelines(isDeferred: Boolean, isAo: Boolean) { - val fwdState = !isDeferred - - contentGroupForward.isVisible = fwdState - shadowsForward.forEach { it.isShadowMapEnabled = fwdState } - aoPipelineForward?.isEnabled = fwdState && isAo - - contentGroupDeferred.isVisible = isDeferred - //deferredPipeline.isEnabled = isDeferred - //deferredPipeline.isAoEnabled = isAo - } - - private fun Scene.makeForwardContent() { - contentGroupForward.setupContentGroup(false) - addNode(contentGroupForward) - } - - private fun Scene.makeDeferredContent() { - deferredPipeline.content.setupContentGroup(true) - - // main scene only contains a quad used to draw the deferred shading output - contentGroupDeferred.apply { - isFrustumChecked = false - val outputMesh = deferredPipeline.defaultOutputQuad(null) - //(outputMesh.shader as? DeferredOutputShader)?.setupVignette(0f) - addNode(outputMesh) - } - addNode(contentGroupDeferred) - } - private fun setupCamera() { val cam = orbitCamera(deferredPipeline.camera) { setRotation(0f, -30f) @@ -204,9 +149,8 @@ class GltfDemo : DemoScene("glTF Models") { onUpdate += { var translationTarget = camTranslationTarget if (trackModel) { - val model = currentModel.forwardModel - model?.let { - val center = model.globalCenter + currentModel.model?.let { + val center = it.globalCenter translationTarget = Vec3d(center.x.toDouble(), center.y.toDouble(), center.z.toDouble()) } } else if (isAutoRotate.value) { @@ -241,7 +185,7 @@ class GltfDemo : DemoScene("glTF Models") { } } - private fun Node.setupContentGroup(isDeferredShading: Boolean) { + private fun Node.setupContentGroup() { transform.rotate((-60.0).deg, Vec3d.Y_AXIS) onUpdate { if (isAutoRotate.value) { @@ -255,35 +199,16 @@ class GltfDemo : DemoScene("glTF Models") { roundCylinder(4.1f, 0.2f) } - shader = if (isDeferredShading) { - gbufferShader { - color { textureColor(colorMap) } - normalMapping { useNormalMap(normalMap) } - ao { textureProperty(aoMap) } - roughness { textureProperty(roughnessMap) } - } - } else { - KslPbrShader { - color { textureColor(colorMap) } - normalMapping { useNormalMap(normalMap) } - ao { textureProperty(aoMap) } - roughness { textureProperty(roughnessMap) } - lighting { - enableSsao(aoPipelineForward?.aoMap) - addShadowMaps(shadowsForward) - imageBasedAmbientLight(envMap.irradianceMap) - } - reflectionMap = envMap.reflectionMap - } + shader = gbufferShader { + color { textureColor(colorMap) } + normalMapping { useNormalMap(normalMap) } + ao { textureProperty(aoMap) } + roughness { textureProperty(roughnessMap) } } } models.forEach { model -> - if (isDeferredShading) { - model.deferredModel?.let { addNode(it) } - } else { - model.forwardModel?.let { addNode(it) } - } + model.model?.let { addNode(it) } } } @@ -316,13 +241,7 @@ class GltfDemo : DemoScene("glTF Models") { if (currentModel.name == "Fox") { MenuSlider2("Movement speed".l, animationSpeed.use(), 0f, 1f) { animationSpeed.set(it) } } - - Text("Settings".l) { sectionTitleStyle() } - LabeledSwitch("Deferred shading".l, isDeferredShading) - LabeledSwitch("Ambient occlusion".l, isAo) - if (isDeferredShading.value) { - LabeledSwitch("Screen space reflections".l, isSsr) - } + LabeledSwitch("Screen space reflections".l, isSsr) LabeledSwitch("Auto rotate view".l, isAutoRotate) } @@ -376,9 +295,7 @@ class GltfDemo : DemoScene("glTF Models") { val zoom: Double, val normalizeBoneWeights: Boolean = false ) { - - var forwardModel: Model? = null - var deferredModel: Model? = null + var model: Model? = null var isVisible: Boolean = false var animate: Model.(Float) -> Unit = { dt -> @@ -387,25 +304,10 @@ class GltfDemo : DemoScene("glTF Models") { override fun toString() = name - suspend fun load(isDeferredShading: Boolean): Model { + suspend fun load(): Model { val materialCfg = GltfMaterialConfig( -// shadowMaps = if (isDeferredShading) deferredPipeline.shadowMaps else shadowsForward, -// scrSpcAmbientOcclusionMap = if (isDeferredShading) deferredPipeline.aoPipeline?.aoMap else aoPipelineForward?.aoMap, environmentMap = envMap, - shaderFactory = { pbrConfig -> - if (isDeferredShading) { - gbufferShader { - vertexCfg.modelMatrixComposition = pbrConfig.vertexCfg.modelMatrixComposition - colorCfg.colorSources.addAll(pbrConfig.colorCfg.colorSources) - normalMapCfg.set(pbrConfig.normalMapCfg) - roughnessCfg.propertySources.addAll(pbrConfig.roughnessCfg.propertySources) - metallicCfg.propertySources.addAll(pbrConfig.metallicCfg.propertySources) - aoCfg.propertySources.addAll(pbrConfig.aoCfg.propertySources) - } - } else { - KslPbrShader(pbrConfig) - } - } + shaderFactory = GltfDeferredShaderFactory ) val modelCfg = GltfLoadConfig( generateNormals = generateNormals, @@ -434,11 +336,7 @@ class GltfDemo : DemoScene("glTF Models") { animate(animationDeltaTime) } } - if (isDeferredShading) { - deferredModel = model - } else { - forwardModel = model - } + this@GltfModel.model = model return model } } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/DemoVehicle.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/DemoVehicle.kt index fd29db861..773f7199b 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/DemoVehicle.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/DemoVehicle.kt @@ -268,9 +268,6 @@ class DemoVehicle(val demo: VehicleDemo, private val vehicleModel: Model, ctx: K val vehicle = Vehicle(vehicleProps, world.physics) world.physics.addActor(vehicle) -// vehicleModel.meshes.forEach { println(it.value.name) } -// vehicleModel.printHierarchy() - vehicleGroup.apply { transform = vehicle.transform diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/VehicleDemo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/VehicleDemo.kt index 3f729c6e3..f0eec7059 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/VehicleDemo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/VehicleDemo.kt @@ -5,7 +5,10 @@ import de.fabmax.kool.demo.DemoLoader import de.fabmax.kool.demo.DemoScene import de.fabmax.kool.demo.menu.DemoMenu import de.fabmax.kool.demo.physics.vehicle.ui.VehicleUi +import de.fabmax.kool.input.KeyboardInput +import de.fabmax.kool.input.LocalKeyCode import de.fabmax.kool.math.* +import de.fabmax.kool.modules.gltf.GltfDeferredShaderFactory import de.fabmax.kool.modules.gltf.GltfLoadConfig import de.fabmax.kool.modules.gltf.GltfMaterialConfig import de.fabmax.kool.modules.ksl.blocks.ColorBlockConfig @@ -26,18 +29,7 @@ class VehicleDemo : DemoScene("Vehicle Demo") { private val groundNormal by texture2d("${DemoLoader.materialPath}/tile_flat/tiles_flat_fine_normal.png") private val vehicleModel by model( "${DemoLoader.modelPath}/kool-car.glb", - GltfLoadConfig(materialConfig = GltfMaterialConfig( - shaderFactory = { pbrConfig -> - gbufferShader { - vertexCfg.modelMatrixComposition = pbrConfig.vertexCfg.modelMatrixComposition - colorCfg.colorSources.addAll(pbrConfig.colorCfg.colorSources) - normalMapCfg.set(pbrConfig.normalMapCfg) - roughnessCfg.propertySources.addAll(pbrConfig.roughnessCfg.propertySources) - metallicCfg.propertySources.addAll(pbrConfig.metallicCfg.propertySources) - aoCfg.propertySources.addAll(pbrConfig.aoCfg.propertySources) - } - } - )) + GltfLoadConfig(materialConfig = GltfMaterialConfig(shaderFactory = GltfDeferredShaderFactory)) ) lateinit var vehicleWorld: VehicleWorld @@ -77,7 +69,7 @@ class VehicleDemo : DemoScene("Vehicle Demo") { deferredPipeline.enableScreenSpaceReflections() deferredPipeline.lightingPass.ambientShadowFactor = 0.3f val bloom = deferredPipeline.installBloomPass() - mainScene += deferredPipeline.defaultOutputQuad(bloom) + mainScene += deferredPipeline.defaultOutputQuad(bloom, vignette = Vignette()) shadows.drawNode = deferredPipeline.content val deferredLights = DeferredLights(deferredPipeline) @@ -99,10 +91,13 @@ class VehicleDemo : DemoScene("Vehicle Demo") { showLoadText("Creating Track".l()) makeTrack(vehicleWorld) addNode(deferredLights) -// onUpdate { -// val w = (8f - vehicle.vehicle.linearVelocity.length()).coerceAtLeast(0f) -// deferredPipeline.filterPass.filterWeight = w -// } + } + + val lightToggleListener = KeyboardInput.addKeyListener(LocalKeyCode('l'), "Toggle head lights", filter = { it.isPressed }) { + vehicle.isHeadlightsOn = !vehicle.isHeadlightsOn + } + mainScene.onRelease { + KeyboardInput.removeKeyListener(lightToggleListener) } } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/ui/Timer.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/ui/Timer.kt index 501e8ad95..6df6ba91f 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/ui/Timer.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/ui/Timer.kt @@ -21,11 +21,13 @@ class Timer(val vehicleUi: VehicleUi) : Composable { pipeline.tsaa = Deferred2Pipeline.TSAA_4 pipeline.filterPass.filterWeight = 8f pipeline.aoPass.temporalKernels = 4 + pipeline.aoPass.kernelSize = 8 } else { pipeline.disableScreenSpaceReflections() pipeline.tsaa = Deferred2Pipeline.TSAA_NONE pipeline.filterPass.filterWeight = 0f pipeline.aoPass.temporalKernels = 1 + pipeline.aoPass.kernelSize = 16 } } private val isSound = mutableStateOf(false).onChange { _, new -> vehicleUi.onToggleSound(new) } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/procedural/Glass.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/procedural/Glass.kt index 637b71bc6..15f8e3166 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/procedural/Glass.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/procedural/Glass.kt @@ -4,11 +4,11 @@ import de.fabmax.kool.math.Vec3f import de.fabmax.kool.math.Vec4f import de.fabmax.kool.math.deg import de.fabmax.kool.modules.ksl.KslPbrShader +import de.fabmax.kool.modules.ksl.blocks.ColorSpaceConversion import de.fabmax.kool.modules.ksl.blocks.cameraData import de.fabmax.kool.modules.ksl.lang.* -import de.fabmax.kool.pipeline.deferred.DeferredPassSwapListener -import de.fabmax.kool.pipeline.deferred.DeferredPasses -import de.fabmax.kool.pipeline.deferred.deferredKslPbrShader +import de.fabmax.kool.pipeline.deferred2.Deferred2Pipeline +import de.fabmax.kool.pipeline.deferred2.gbufferShader import de.fabmax.kool.pipeline.ibl.EnvironmentMap import de.fabmax.kool.pipeline.shading.AlphaMode import de.fabmax.kool.scene.* @@ -16,7 +16,7 @@ import de.fabmax.kool.scene.geometry.MeshBuilder import de.fabmax.kool.scene.geometry.generateNormals import de.fabmax.kool.util.* -class Glass(val ibl: EnvironmentMap, shadowMap: SimpleShadowMap) : Node(), DeferredPassSwapListener { +class Glass(val ibl: EnvironmentMap, shadowMap: SimpleShadowMap) : Node() { private val glasShader: GlassShader = GlassShader(ibl, shadowMap) @@ -29,8 +29,8 @@ class Glass(val ibl: EnvironmentMap, shadowMap: SimpleShadowMap) : Node(), Defer transform.scale(0.9f) } - override fun onSwap(previousPasses: DeferredPasses, currentPasses: DeferredPasses) { - glasShader.refractionColorMap = currentPasses.lightingPass.colorTexture + fun swapBuffers(pipeline: Deferred2Pipeline) { + glasShader.refractionColorMap = pipeline.filterPass.filterOutput.oldVal } private fun makeBody() = addColorMesh { @@ -56,7 +56,6 @@ class Glass(val ibl: EnvironmentMap, shadowMap: SimpleShadowMap) : Node(), Defer geometry.removeDegeneratedTriangles() geometry.generateNormals() } - isOpaque = false shader = glasShader } @@ -68,13 +67,10 @@ class Glass(val ibl: EnvironmentMap, shadowMap: SimpleShadowMap) : Node(), Defer geometry.generateNormals() } - shader = deferredKslPbrShader { + shader = gbufferShader { roughness(0.0f) color { - constColor(Color(0.3f, 0f, 0.1f).mix(Color.BLACK, 0.2f).toLinear()) - } - emission { - constColor(Color(0.3f, 0f, 0.1f).toLinear().withAlpha(0.8f)) + constColor(Color(0.5f, 0f, 0.1f).mix(Color.BLACK, 0.2f).toLinear()) } } } @@ -207,6 +203,7 @@ class Glass(val ibl: EnvironmentMap, shadowMap: SimpleShadowMap) : Node(), Defer lighting { addShadowMap(shadowMap) } roughness(0f) enableImageBasedLighting(ibl) + colorSpaceConversion = ColorSpaceConversion.AsIs }.build() fun glassShaderModel(cfg: Config) = Model(cfg).apply { @@ -235,6 +232,7 @@ class Glass(val ibl: EnvironmentMap, shadowMap: SimpleShadowMap) : Node(), Defer val refractionPos = float3Var(worldPos + refractionDir * matThickness.output) val clipPos = float4Var(camData.viewProjMat * float4Value(refractionPos, 1f.const)) val samplePos = float2Var(clipPos.xy / clipPos.w * 0.5f.const + 0.5f.const) + samplePos.y set 1f.const - samplePos.y val refractionColor = float4Var(Vec4f.ZERO.const) `if`((samplePos.x gt 0f.const) and (samplePos.x lt 1f.const) and diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/procedural/ProceduralDemo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/procedural/ProceduralDemo.kt index 3a8e0a4eb..3b03ae0fd 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/procedural/ProceduralDemo.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/procedural/ProceduralDemo.kt @@ -6,14 +6,14 @@ import de.fabmax.kool.demo.menu.DemoMenu import de.fabmax.kool.math.Vec3f import de.fabmax.kool.math.randomI import de.fabmax.kool.math.spatial.BoundingBoxF -import de.fabmax.kool.modules.ksl.blocks.ColorSpaceConversion +import de.fabmax.kool.modules.ksl.toConfig import de.fabmax.kool.modules.ui2.* -import de.fabmax.kool.pipeline.DepthCompareOp import de.fabmax.kool.pipeline.ao.AoRadius -import de.fabmax.kool.pipeline.deferred.DeferredPipeline -import de.fabmax.kool.pipeline.deferred.DeferredPipelineConfig +import de.fabmax.kool.pipeline.deferred2.Deferred2Pipeline +import de.fabmax.kool.pipeline.deferred2.defaultOutputQuad +import de.fabmax.kool.pipeline.deferred2.installBloomPass +import de.fabmax.kool.scene.Node import de.fabmax.kool.scene.Scene -import de.fabmax.kool.scene.Skybox import de.fabmax.kool.scene.orbitCamera import de.fabmax.kool.util.* @@ -52,33 +52,29 @@ class ProceduralDemo : DemoScene("Procedural Geometry") { setDefaultDepthOffset(true) shadowBounds = BoundingBoxF(Vec3f(-30f, 0f, -30f), Vec3f(30f, 60f, 30f)) } - val deferredCfg = DeferredPipelineConfig().apply { - isWithScreenSpaceReflections = true - isWithAmbientOcclusion = true - maxGlobalLights = 1 - isWithVignette = true - isWithBloom = true - useImageBasedLighting(ibl) - useShadowMaps(listOf(shadowMap)) - outputDepthTest = DepthCompareOp.ALWAYS - } - val deferredPipeline = DeferredPipeline(this@setupMainScene, deferredCfg).apply { - aoPipeline?.radius = AoRadius.absoluteRadius(0.6f) - - sceneContent.apply { - addNode(Glass(ibl, shadowMap).also { onSwap += it }) - addNode(Vase()) - addNode(Table(this@ProceduralDemo)) - - roses = Roses() - addNode(roses) - } + val content = Node() + val pipeline = Deferred2Pipeline( + content = content, + scene = this, + ibl = ibl, + camera = camera, + lighting = lighting, + shadowMapConfig = listOf(shadowMap).toConfig(), + renderScale = 1f / UiScale.windowScale.value + ) + pipeline.enableScreenSpaceReflections() + pipeline.aoPass.radius = AoRadius.absoluteRadius(0.6f) + val bloom = pipeline.installBloomPass() - lightingPassContent += Skybox.cube(ibl.reflectionMap, 1f, colorSpaceConversion = ColorSpaceConversion.AsIs) + content.apply { + addNode(Glass(ibl, shadowMap).apply { pipeline.onSwap { swapBuffers(pipeline) } }) + addNode(Vase()) + addNode(Table(this@ProceduralDemo)) + roses = Roses() + addNode(roses) } - shadowMap.drawNode = deferredPipeline.sceneContent - addNode(deferredPipeline.createDefaultOutputQuad()) + addNode(pipeline.defaultOutputQuad(bloom)) } override fun createMenu(menu: DemoMenu, ctx: KoolContext) = menuSurface { diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/procedural/Roses.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/procedural/Roses.kt index a96d18231..878396317 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/procedural/Roses.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/procedural/Roses.kt @@ -1,7 +1,7 @@ package de.fabmax.kool.demo.procedural import de.fabmax.kool.math.* -import de.fabmax.kool.pipeline.deferred.deferredKslPbrShader +import de.fabmax.kool.pipeline.deferred2.gbufferShader import de.fabmax.kool.scene.ColorMesh import de.fabmax.kool.scene.Node import de.fabmax.kool.scene.addGroup @@ -62,7 +62,7 @@ class Roses : Node() { geometry.removeDegeneratedTriangles() geometry.generateNormals() } - shader = deferredKslPbrShader { + shader = gbufferShader { color { vertexColor() } roughness(0.3f) } @@ -73,7 +73,7 @@ class Roses : Node() { geometry.removeDegeneratedTriangles() geometry.generateNormals() } - shader = deferredKslPbrShader { + shader = gbufferShader { color { vertexColor() } roughness(0.5f) } @@ -84,7 +84,7 @@ class Roses : Node() { geometry.removeDegeneratedTriangles() geometry.generateNormals() } - shader = deferredKslPbrShader { + shader = gbufferShader { color { vertexColor() } roughness(0.5f) } @@ -95,7 +95,7 @@ class Roses : Node() { geometry.removeDegeneratedTriangles() geometry.generateNormals() } - shader = deferredKslPbrShader { + shader = gbufferShader { color { vertexColor() } roughness(0.8f) } diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/procedural/Table.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/procedural/Table.kt index 48c960ead..7e6445a93 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/procedural/Table.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/procedural/Table.kt @@ -2,7 +2,7 @@ package de.fabmax.kool.demo.procedural import de.fabmax.kool.math.Vec3f import de.fabmax.kool.math.deg -import de.fabmax.kool.pipeline.deferred.deferredKslPbrShader +import de.fabmax.kool.pipeline.deferred2.gbufferShader import de.fabmax.kool.scene.Mesh import de.fabmax.kool.scene.VertexLayouts import de.fabmax.kool.scene.geometry.* @@ -21,7 +21,7 @@ class Table(demo: ProceduralDemo) : Mesh(IndexedVertexList(VertexLay geometry.removeDegeneratedTriangles() geometry.generateNormals() } - shader = deferredKslPbrShader { + shader = gbufferShader { color { vertexColor() } roughness(0.3f) } @@ -156,5 +156,4 @@ class Vase : Mesh(IndexedVertexList(VertexLay } } } - } \ No newline at end of file From f431f89f37b2c9596915444e550178a7dcc64fd9 Mon Sep 17 00:00:00 2001 From: Max Thiele Date: Fri, 29 May 2026 22:52:04 +0200 Subject: [PATCH 27/27] Updated readme and demo entries --- README.md | 9 +++++---- .../src/commonMain/kotlin/de/fabmax/kool/demo/Demos.kt | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5f9d15248..747b875e6 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,11 @@ The code for all demos is available in the [kool-demo](kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo) subproject. You can also run them locally by cloning this repo and running `./gradlew :kool-demo:runDesktop` -- [Island](https://kool-engine.github.io/live/demos/?demo=phys-terrain): Height-map based - island incl. some wind-affected vegetation + a basic controllable character. - [Physics - Vehicle](https://kool-engine.github.io/live/demos/?demo=phys-vehicle): A drivable vehicle (W, A, S, D / - cursor keys, R to reset) based on the Nvidia PhysX vehicles SDK. **WebGPU only** + cursor keys, R to reset) based on the Nvidia PhysX vehicles SDK. Also nice showcase for deferred + rendering with screen-space reflections (incl. artifacts) **WebGPU only** +- [Island](https://kool-engine.github.io/live/demos/?demo=phys-terrain): Height-map based + island incl. some wind-affected vegetation and a basic controllable character. - [Physics - Ragdoll](https://kool-engine.github.io/live/demos/?demo=phys-ragdoll): Ragdoll physics demo. - [Physics - Joints](https://kool-engine.github.io/live/demos/?demo=phys-joints): Physics demo consisting of a chain running over two gears. Uses a lot of multi shapes and revolute joints. @@ -169,7 +170,7 @@ the libs are resolved and added to the IntelliJ module classpath. - Support for physical based rendering (with metallic workflow) and image-based lighting - (Almost) complete support for [glTF 2.0](https://github.com/KhronosGroup/glTF) model format (including animations, morph targets and skins) - Skin / armature mesh animation (vertex shader based) -- Deferred shading +- Deferred shading with screen-space reflections and temporal filtering - Various tone-mapping options: - ACES (default) - Khronos PBR Neutral diff --git a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/Demos.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/Demos.kt index d5af5b88d..87e7f35ec 100644 --- a/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/Demos.kt +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/Demos.kt @@ -65,7 +65,6 @@ object Demos { entry("gltf", "glTF Models") { GltfDemo() } entry("ssr", "Reflections") { ReflectionDemo() } entry("deferred", "Deferred Shading", NeedsComputeShaders) { DeferredDemo() } - entry("deferred2test", "Deferred 2 Test", NeedsComputeShaders) { Deferred2Test() } entry("procedural", "Procedural Roses", NeedsComputeShaders) { ProceduralDemo() } entry("pbr", "PBR Materials") { PbrDemo() } } @@ -101,6 +100,7 @@ object Demos { entry("struct-test", "Hello Structs") { HelloStructs() } entry("launched-effect-test", "Launched Effect Test") { LaunchedEffectTest() } entry("platformer2d", "2D Platformer") { PlatformerDemo() } + entry("deferred2test", "Deferred 2 Test", NeedsComputeShaders) { Deferred2Test() } } val categories = mutableListOf(physicsDemos, graphicsDemos, techDemos, hiddenDemos)