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-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..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 @@ -2,9 +2,14 @@ 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.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 @@ -22,7 +27,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 +37,30 @@ 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: 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/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/ShaderAttributeConfigs.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/ShaderAttributeConfigs.kt index 7f5c0662d..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 @@ -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 @@ -42,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 @@ -134,12 +143,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 +207,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/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/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/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/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/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/modules/ksl/blocks/PbrMaterialBlock.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/blocks/PbrMaterialBlock.kt index f3563da0c..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 @@ -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) + outAmbient set kDAmbient * diffuse * inAoFactor + outLight set lo + outColor set outAmbient + outLight + outSpecular * outSpecularFactor * inAoFactor / inBaseColor.a } } } 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/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/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/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-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/BloomPass.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/BloomPass.kt index a392ac347..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 @@ -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 @@ -25,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, @@ -34,7 +50,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 +58,7 @@ class BloomPass( init { logD { "Using $levels bloom levels, in place: $inPlace" } - makeDownSamplePasses() + makeDownSampleLowerPasses() makeUpSamplePasses() bloomMap.releaseWith(this) if (!inPlace) { @@ -53,40 +69,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-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/NormalDepthMapPass.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/NormalDepthMapPass.kt index f9c4b51b7..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,14 +20,15 @@ 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), 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 @@ -133,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)) } } } @@ -160,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/PipelineBase.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/PipelineBase.kt index 9e36683a3..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,14 +79,21 @@ abstract class PipelineBase(val name: String, val bindGroupLayouts: BindGroupLay } } -inline fun PipelineBase.swapPipelineDataCapturing(key: Any?, block: () -> Unit) { - swapPipelineData(key) +inline fun > T.swapPipelineData(key: Any?, copyBindings: Boolean = false, block: T.() -> Unit) { + createdPipeline?.let { + it.swapPipelineData(key, copyBindings) + block() + } +} + +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/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..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 @@ -9,10 +10,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 +18,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 +65,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 +83,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.R_I32, mipMapping = MipMapping.Limited(SCALE_LEVELS), name = "normalOutput") + 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) + 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 +117,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 + } + + private var uKernelSize by aoShader.bindUniformInt1("uKernelSize", kernelSize) + private var uKernelTemporalSize by aoShader.bindUniformInt1("uKernelTemporalSize", temporalKernels) - var radius: Float = 1f + 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 +152,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 +177,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,12 +225,24 @@ 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()) downSampleShader.bindStorageTexture2d("normalOutput", scaledNormals) - downSampleShader.bindStorageTexture2d("distOutput", scaledDists) + downSampleShader.bindStorageTexture2d("distOutputFirst", scaledDists) downSampleShader.bindUniformFloat1("camNear", camera.clipNear) return downSampleShader } @@ -231,6 +257,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,37 +276,35 @@ 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() { 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 = ((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 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) } } @@ -287,20 +312,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)) } @@ -312,47 +337,48 @@ class ComputeAoPass( } } - private fun downSamplingShader() = KslComputeShader("down-sample-shader") { + private fun downSamplingShader() = KslComputeShader("ao-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 distOutput = storageTexture2d("distOutput", distFormat) + val normalOutput = storageTexture2d("normalOutput", TexFormat.R_I32) + val distOutput = storageTexture2d("distOutputFirst", 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)) } } } - 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 } @@ -364,16 +390,19 @@ class ComputeAoPass( private fun aoShader() = KslComputeShader("ao-shader") { computeStage(8, 8) { - val normalInput = texture2d("normalInput") - val distInput = texture2d("distInput") + val normalInput = texture2dInt("normalInput") + val distInput = texture2d("distInput", isUnfilterable = true) 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") @@ -381,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) { @@ -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 } @@ -438,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) @@ -449,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 @@ -463,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 @@ -479,9 +511,9 @@ 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", finalAo.format) + val finalAo = storageTexture2d("finalAo", aoMap.format) computeStage(8, 8) { main { @@ -510,7 +542,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 +550,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/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/pipeline/deferred2/Deferred2Pipeline.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/Deferred2Pipeline.kt new file mode 100644 index 000000000..93b22c90e --- /dev/null +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/Deferred2Pipeline.kt @@ -0,0 +1,422 @@ +package de.fabmax.kool.pipeline.deferred2 + +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 +import de.fabmax.kool.modules.ksl.blocks.convertColorSpace +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.ComputeAoPass +import de.fabmax.kool.pipeline.ibl.EnvironmentMap +import de.fabmax.kool.scene.* +import de.fabmax.kool.util.* +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.coroutines.yield + +class Deferred2Pipeline( + val content: Node, + val scene: Scene, + val ibl: EnvironmentMap, + 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, + 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 idAllocator: ObjectIdAllocator = DefaultObjectIdAllocator(maxObjects) + private val camDataBuffer = StructBuffer(DeferredCamDataLayout, 1) + val camData = camDataBuffer.asStorageBuffer() + + val reprojectMatrixComputePass = ReprojectComputePass(maxObjects, this) + + val gbuffers = AlternatingPair { + val suff = if (it) "A" else "B" + GbufferPass(size, "deferred2-gbuffer-pass-$suff", this) + } + val aoPass: ComputeAoPass = ComputeAoPass( + camera = camera, + inputDepth = gbuffers.a.depth, + inputNormals = gbuffers.a.normals, + 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>() + + init { + aoPass.kernelSize = 32 / tsaa.size.coerceAtLeast(2) + aoPass.temporalKernels = tsaa.size.coerceAtLeast(1) + + 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.releaseDelayed(1) } + + 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 + } + } + + scene.coroutineScope.launch { + withContext(KoolDispatchers.Synced) { + while (true) { + resizeIfNeeded() + swapBuffers() + yield() + } + } + } + } + + 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) + 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) + set(it.camPosition, camera.globalPos) + set(it.camNear, camera.clipNear) + set(it.frameIdx, Time.frameCount) + } + camData.uploadData(camDataBuffer) + + 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() } + + // 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 + } + + 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 + } + + fun onSwap(block: () -> Unit) { + swapListeners += block + } + + companion object { + private val s = 1f/8f + 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(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), + ) + val TSAA_16 = listOf( + Vec2f(1 * s, 1 * 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(6 * s, 7 * 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(0 * s, -7 * s), + Vec2f(-4 * s, -6 * s), + Vec2f(5 * s, 3 * s), + ) + } +} + +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() + + buf[0] = u(1) + 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) +} + +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 +} + +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") + 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.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] +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] + +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?, + writeDepth: Boolean = false, + vignette: Vignette? = null, + chromaticAberration: Vec3f? = null, +): Mesh<*> { + val outputShader = defaultOutputShader(bloomPass, writeDepth, vignette, chromaticAberration) + return TextureMesh().apply { + generate { + generateFullscreenQuad() + } + shader = outputShader + } +} + +fun Deferred2Pipeline.defaultOutputShader( + bloomPass: BloomPass?, + writeDepth: Boolean = false, + vignette: Vignette? = null, + chromaticAberration: Vec3f? = null, +): KslShader { + 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) + fragmentStage { + val output = texture2d("deferredOutput") + 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 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 { + outDepth set it.load(uvi).x + } + } + } + } + + 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()) + 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) { + inputTex = filterOutput + if (writeDepth) { + inputDepth = gbuffers.newVal.depth + } + } + } + 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/DeferredLightShader.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/DeferredLightShader.kt new file mode 100644 index 000000000..193cc2e0a --- /dev/null +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/DeferredLightShader.kt @@ -0,0 +1,119 @@ +package de.fabmax.kool.pipeline.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-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/DeferredLights.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/DeferredLights.kt new file mode 100644 index 000000000..b5161523a --- /dev/null +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/DeferredLights.kt @@ -0,0 +1,288 @@ +package de.fabmax.kool.pipeline.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( + pipeline: Deferred2Pipeline, + 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() + + val spotLightMeshes = mutableMapOf() + val pointLightMesh = Mesh( + layout = VertexLayouts.Position, + instances = pointLightInstances, + name = "DeferredPointLights" + ).apply { + isCastingShadow = false + generate { makePointLightMesh() } + shader = lightShader + } + + init { + pipeline.onSwap { swapPipelineData(pipeline) } + addNode(pointLightMesh) + if (isDynamic) { + onUpdate { updateLightInstanceData() } + } + } + + 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) { + 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) + + 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) { + // fixme this is very slow for many lights + 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) { + // fixme this is very slow for many lights + spotLights[light.maxSpotAngle]?.remove(light) + spotLightMeshes[light.maxSpotAngle]?.lights?.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 + } + } + + inner class SpotLightMesh(angle: AngleF) { + val lights = mutableListOf() + val instances = MeshInstanceList(DeferredLightInstanceLayout) + val mesh = Mesh( + layout = VertexLayouts.Position, + instances = instances, + name = "DeferredPointLights" + ).apply { + isCastingShadow = false + 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-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/GbufferPass.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/GbufferPass.kt new file mode 100644 index 000000000..2aba8da16 --- /dev/null +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/GbufferPass.kt @@ -0,0 +1,118 @@ +package de.fabmax.kool.pipeline.deferred2 + +import de.fabmax.kool.math.MutableMat4f +import de.fabmax.kool.math.Vec2i +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, + name: String, + val pipeline: Deferred2Pipeline, +) : OffscreenPass2d( + drawNode = pipeline.content, + attachmentConfig = AttachmentConfig { + // albedo, a * 64 = emission strength + addColor(TexFormat.RGBA, filterMethod = FilterMethod.NEAREST) + // metal, roughness, ao, [empty: a, flags maybe?] + addColor(TexFormat.RGBA, filterMethod = FilterMethod.NEAREST) + // 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() + }, + initialSize = initialSize, + name = name +) { + val albedoEmission get() = colorTextures[0] + val metalRoughnessAo get() = colorTextures[1] + val normals get() = colorTextures[2] + 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) + 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()) { + val cmd = it.next() + val mesh = cmd.mesh + 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 + + 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 + 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) + } + } + } + is DeferredLightShader -> { + lightMeshes += cmd.mesh + it.remove() + drawQueue.recycleDrawCommand(cmd) + } + } + } + } + } + } +} + +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) + } + } +} 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 new file mode 100644 index 000000000..eceb94e0e --- /dev/null +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/GbufferShader.kt @@ -0,0 +1,228 @@ +package de.fabmax.kool.pipeline.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 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 { + 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() + } + + 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() + } + + 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-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/LightingPass.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/LightingPass.kt new file mode 100644 index 000000000..66686f2f2 --- /dev/null +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/LightingPass.kt @@ -0,0 +1,370 @@ +package de.fabmax.kool.pipeline.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 +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.* +import de.fabmax.kool.pipeline.FullscreenShaderUtil.fullscreenQuadVertexStage +import de.fabmax.kool.pipeline.FullscreenShaderUtil.generateFullscreenQuad +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 +import kotlin.math.abs + +class LightingPass( + size: Vec2i, + private val pipeline: Deferred2Pipeline, +) : OffscreenPass2d( + drawNode = Node(), + attachmentConfig = AttachmentConfig { + addColor(TexFormat.RG11B10_F, filterMethod = FilterMethod.NEAREST) // metal, roughness, ao + defaultDepth() + }, + initialSize = size, + name = "deferred2-lighting-pass" +) { + val lightingOutput: Texture2d get() = colorTexture!! + + 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 + lighting = pipeline.lighting + + val outputMesh = Mesh(VertexLayouts.PositionTexCoord).apply { + generateFullscreenQuad() + shader = lightingShader + } + 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) + } + } + for (i in gbuffer.alphaMeshes.indices) { + val mesh = gbuffer.alphaMeshes[i] + mesh.getOrCreatePipeline(ctx)?.let { pipeline -> + viewData.drawQueue.addMesh(mesh, pipeline) + } + } + } + } + + fun swapBuffers() { + val newGbuffer = pipeline.gbuffers.newVal + // 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 + albedoEmissionTex = newGbuffer.albedoEmission + metalRoughnessAoTex = newGbuffer.metalRoughnessAo + irradianceMap = pipeline.ibl.irradianceMap + reflectionMap = pipeline.ibl.reflectionMap + aoMap = pipeline.aoPass.aoMap + camData = pipeline.camData + oldColor = pipeline.filterPass.filterOutput.oldVal + numReflectionRays = this@LightingPass.numReflectionRays + reflectionRayStepIncrease = this@LightingPass.reflectionRayStepIncrease + ambientShadowFactor = this@LightingPass.ambientShadowFactor + } + } +} + +class DeferredLightingShader( + maxGlobalLights: Int, + shadowMapConfig: List, +) : KslShader("deferred2-lighting") { + var depthTex by bindTexture2d("depth") + var scaledViewZ by bindTexture2d("scaledViewZ") + var encodedNormals by bindTexture2d("encodedNormals") + 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") + var camData by bindStorage("camData") + + var numReflectionRays by bindUniformInt1("numReflectionRays") + var reflectionRayStepIncrease by bindUniformFloat1("reflectionRayStepIncrease", 1.5f) + var ambientShadowFactor by bindUniformFloat1("ambientShadowFactor", 0f) + + var oldColor by bindTexture2d("oldColor") + + var ambientMapOrientation: Mat3f by bindUniformMat3("uAmbientTextureOri", Mat3f.IDENTITY) + + init { + pipelineConfig = PipelineConfig( + blendMode = BlendMode.DISABLED, + cullMethod = CullMethod.NO_CULLING, + depthTest = DepthCompareOp.ALWAYS + ) + program.program(maxGlobalLights, shadowMapConfig) + + bindTexture1d("tgradient", GradientTexture(ColorGradient.ROCKET)) + } + + private fun KslProgram.program( + maxGlobalLights: Int, + shadowMapConfig: List, + ) { + val uv = interStageFloat2() + fullscreenQuadVertexStage(uv) + fragmentStage { + val depth = texture2d("depth", isUnfilterable = true) + val scaledViewZ = texture2d("scaledViewZ", isUnfilterable = true) + 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 numReflectionRays = uniformInt1("numReflectionRays") + val reflectionRayStepIncrease = uniformFloat1("reflectionRayStepIncrease") + val ambientShadowFactor = uniformFloat1("uAmbientShadowFactor") + val ambientOri = uniformMat3("uAmbientTextureOri") + val camDataLayout = struct(DeferredCamDataLayout) + val camData = storage("camData", camDataLayout) + + main { + 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() + } + + val camNear by camData.camNear + val camPos by camData.camPosition + val invView by camData.invView + val invViewProj by camData.invViewProj + val worldPos by unprojectBaseCoord(depthSample, baseCoord, size, camNear, invViewProj).xyz + val ssao by aoMap.load(baseCoord, lod = 0.const).x + + 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 by float4Var(albedoEmission.load(baseCoord, lod = 0.const)) + val albedo by albedoEmission.xyz + val emissiveStrength by albedoEmission.w * 64f.const + + val metalRoughnessAo by metalRoughnessAo.load(baseCoord, lod = 0.const).xyz + val metallic by metalRoughnessAo.x + val roughness by metalRoughnessAo.y + val ao by metalRoughnessAo.z + + val ambient by irradiance.sample(worldNormal).rgb * ssao + + 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 * 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) + inFragmentPos(worldPos) + inBaseColor(float4Value(albedo, 1f.const)) + + inRoughness(roughness) + inMetallic(metallic) + + inIrradiance(ambient) + inAoFactor(ao) + inAmbientOrientation(ambientOri) + + setLightData(lightData, shadowFactors, 1f.const) + } + + 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, numReflectionRays, reflectionRayStepIncrease) + outColor set material.outAmbient + material.outLight + screenReflection + albedo * emissiveStrength + } + colorOutput(outColor) + outDepth set depthSample + } + } + } +} + +context(_: KslProgram, _: KslShaderStage) +fun KslScopeBuilder.screenReflect( + material: PbrMaterialBlock, + viewNormal: KslExprFloat3, + viewZ: KslUniform, + oldColor: KslUniform, + camData: KslStructStorage, + numReflectionRays: KslExprInt1, + reflectionRayStepIncrease: KslExprFloat1, +): KslExprFloat3 { + val fnProjViewPos = functionFloat2("fnProiViewPos") { + val viewPos = paramFloat3("viewPos") + body { + val p by camData.proj * 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 viewZ.size().toFloat2() + val uvi by (uv * texSz).toInt2() + viewZ.load(uvi, lod = 0.const).x - refDepth + } + } + + val fnCastRay = functionFloat3("fnCastRay") { + val origin = paramFloat3("origin") + val rayDir = paramFloat3("rayDir") + val noise = paramFloat3("noise") + val maxIncrease = paramFloat1("maxIncrease") + + 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 directionFac by abs(dot(rayDir, normalize(origin))) + + repeat(16.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 * reflectionRayStepIncrease, baseDist * 0.05f.const) * 0.5f.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 + } + } + float3Value(stepUv, isHit.toFloat1()) + } + } + + val specFactor by material.outSpecularFactor + val roughFactor by material.inRoughness + val envReflectionColor by material.outSpecular * specFactor * material.inAoFactor + + val viewPos by (camData.view * float4Value(material.inFragmentPos, 1f)).xyz + val reflectionWeight 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()) + + 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 + 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 + 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, ddx, ddy).rgb * rayResult.z * specFactor + reflectionColorOut += sampleColor + reflectionWeight += rayResult.z + minColor set min(minColor, sampleColor) + maxColor set max(maxColor, sampleColor) + hit set true.const + }.`else` { + reflectionColorOut += envReflectionColor + reflectionWeight += 1f.const + minColor set min(minColor, envReflectionColor) + maxColor set max(maxColor, envReflectionColor) + } + noise set noise13(noise.x) + } + + val thresh by length(maxColor - minColor) + `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, reflectionRayStepIncrease) + `if`(rayResult.z gt 0f.const) { + reflectionColorOut += oldColor.sample(rayResult.xy, ddx, ddy).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 +} diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/ObjectIdAllocator.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/ObjectIdAllocator.kt new file mode 100644 index 000000000..51bf307c8 --- /dev/null +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/ObjectIdAllocator.kt @@ -0,0 +1,80 @@ +package de.fabmax.kool.pipeline.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 = 1 + 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(0, 1) + } + 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-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/ProjectionFunctions.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/ProjectionFunctions.kt new file mode 100644 index 000000000..1d22804b1 --- /dev/null +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/ProjectionFunctions.kt @@ -0,0 +1,66 @@ +package de.fabmax.kool.pipeline.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( + 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, 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 + worldPos + } + } +} + +fun KslScopeBuilder.unprojectBaseCoord( + depth: KslExprFloat1, + baseCoord: KslExprInt2, + size: KslExprInt2, + camNear: KslExprFloat1, + invViewProj: KslExprMat4, +): KslExprFloat4 { + val func = parentStage.getOrCreateFunction("fnUnprojectBaseCoord") { UnprojectBaseCoord(this) } + return func(depth, baseCoord, size, camNear, invViewProj) +} + + +class UnprojectUv( + 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 + } + } +} 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 new file mode 100644 index 000000000..467a0853c --- /dev/null +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/ReprojectComputePass.kt @@ -0,0 +1,153 @@ +package de.fabmax.kool.pipeline.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 +import de.fabmax.kool.util.releaseDelayed + +class ReprojectComputePass( + val maxObjects: Int = 65536, + private val pipeline: Deferred2Pipeline +) : ComputePass("deferred2-reproject-compute-pass") { + + 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 + val task = addTask(shader, Vec3i(groupsX, 1, 1)) + task.onAfterDispatch { frameCnt++ } + onRelease { + modelMats.a.releaseDelayed(1) + modelMats.b.releaseDelayed(1) + reprojectMats.releaseDelayed(1) + } + } + + fun swapBuffers() { + val newUpload = uploadData.newVal + modelMats.newVal.uploadData(newUpload.modelMats) + shader.swapPipelineData(newUpload) { + inputOldViewProj = uploadData.oldVal.viewProjMat + oldModelMats = modelMats.oldVal + newModelMats = modelMats.newVal + numMatrices = pipeline.idAllocator.size + } + } +} + +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 numMatrices by bindUniformInt1("numMatrices") + 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() { + computeStage(workGroupSizeX = 64) { + val numMatrices = uniformInt1("numMatrices") + 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() + `if`(idx ge numMatrices) { + `return`() + } + + 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-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/TemporalFilterPass.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/TemporalFilterPass.kt new file mode 100644 index 000000000..f2aa43ecc --- /dev/null +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/deferred2/TemporalFilterPass.kt @@ -0,0 +1,218 @@ +package de.fabmax.kool.pipeline.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 +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.* + +class TemporalFilterPass( + val pipeline: Deferred2Pipeline, + 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()) + } + val filterState = AlternatingPair { + StorageTexture2d(size.x, size.y, TexFormat.R, samplerSettings = SamplerSettings().clamped().nearest()) + } + + private val temporalShader = TemporalFilterShader(filterStorageFmt, pipeline.lightingPass.lightingOutput) + + var filterWeight = 8f + + init { + setupPasses() + onRelease { + filterOutput.a.release() + filterOutput.b.release() + filterState.a.release() + filterState.b.release() + } + } + + fun resize(size: Vec2i) { + this.size = size + filterOutput.a.resize(size.x, size.y) + filterOutput.b.resize(size.x, size.y) + filterState.a.resize(size.x, size.y) + filterState.b.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.swapPipelineData(filterOutput.newVal) { + val newGbuffer = pipeline.gbuffers.newVal + val oldGbuffer = pipeline.gbuffers.oldVal + + newDepth = pipeline.lightingPass.depthTexture + oldDepth = oldGbuffer.depth + oldMeta = oldGbuffer.objectIds + newMeta = newGbuffer.objectIds + oldFilter = filterOutput.oldVal + newFilter = filterOutput.newVal + filterW = filterWeight + filterStateRd = filterState.oldVal + filterStateWr = filterState.newVal + + reprojectMats = pipeline.reprojectMatrixComputePass.reprojectMats + camData = pipeline.camData + } + } +} + +class TemporalFilterShader( + val filterStorageFmt: TexFormat, + lightingOutput: Texture2d, +) : KslComputeShader("deferred2-temporal-filter") { + var lightingOutput by bindTexture2d("lightingOutput", lightingOutput) + var oldMeta by bindTexture2d("oldMeta") + var newMeta by bindTexture2d("newMeta") + var newDepth by bindTexture2d("newDepth") + var oldDepth by bindTexture2d("oldDepth") + var oldFilter by bindTexture2d("oldFilter", defaultSampler = SamplerSettings().clamped().linear()) + var newFilter by bindStorageTexture2d("newFilter") + var filterStateRd by bindTexture2d("filterStateRd") + var filterStateWr by bindStorageTexture2d("filterStateWr") + + var reprojectMats by bindStorage("reprojectMats") + var camData by bindStorage("camData") + var filterW by bindUniformFloat1("uFilterWeight") + + init { + program.program() + } + + private fun KslProgram.program() { + computeStage(workGroupSizeX = 8, workGroupSizeY = 8) { + val lightingOutput = texture2d("lightingOutput") + val oldMeta = texture2dInt("oldMeta") + val newMeta = texture2dInt("newMeta") + val newDepth = texture2d("newDepth", isUnfilterable = true) + val oldDepth = texture2d("oldDepth", isUnfilterable = true) + val oldFilter = texture2d("oldFilter") + val newFilter = if (filterStorageFmt.channels == 3) { + storageTexture2d("newFilter", filterStorageFmt) + } else { + storageTexture2d("newFilter", filterStorageFmt) + } + val filterStateRd = texture2d("filterStateRd") + val filterStateWr = storageTexture2d("filterStateWr", TexFormat.R) + + val matStruct = struct(StorageMatLayout) + val reprojectMats = storage("reprojectMats", matStruct) + val camDataLayout = struct(DeferredCamDataLayout) + val camData = storage("camData", camDataLayout) + + val filterWeight = uniformFloat1("uFilterWeight") + + 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`() + } + + val curMeta by newMeta.load(baseCoord).r + val id by curMeta and 0xffffff.const + val size by newDepth.size() + val sizeF by size.toFloat2() + + val near by camData.camNear + val invViewProj by camData.invViewProj + val depth by newDepth.load(baseCoord).x + val worldPos by unprojectBaseCoord(depth, baseCoord, size, near, invViewProj) + + val oldProj by 0f.const4 + `if`(id ne 0.const) { + val reprojectMat = mat4Var(reprojectMats[id][StorageMatLayout.mat]) + oldProj set reprojectMat * worldPos + }.`else` { + 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 + (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) + 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 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) + } + + 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) + `if`((!filterHit and !isEdge) or (wasEdge and !isEdge) or isReprojectOutOfScreen) { + 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()) + val weighted by (oldSrgb * w + curSrgb) / (w + 1f.const) + val filtered by convertColorSpace(weighted, ColorSpaceConversion.SrgbToLinear()) + + `if`(any(isNan(filtered))) { + filtered set curSrgb + } + newFilter[baseCoord] = float4Value(filtered, 1f) + } + } + } +} 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-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-core/src/commonMain/kotlin/de/fabmax/kool/scene/ModelTemplate.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/ModelTemplate.kt index fc961c306..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 @@ -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.createShader(mesh, pbrConfig) + 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/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-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/desktopMain/kotlin/de/fabmax/kool/util/Buffer.desktop.kt b/kool-core/src/desktopMain/kotlin/de/fabmax/kool/util/Buffer.desktop.kt index ddb575aec..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,11 +19,45 @@ 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, isAutoLimit: Boolean ) : Buffer { + @PublishedApi + internal var modCount = 0 override var isAutoLimit: Boolean = isAutoLimit set(value) { @@ -36,6 +70,7 @@ abstract class GenericBuffer( override var limit: Int get() = if (isAutoLimit) pos else buffer.limit() set(value) { + modCount++ buffer.limit(value) isAutoLimit = false } @@ -43,6 +78,7 @@ abstract class GenericBuffer( override var position: Int get() = pos set(value) { + modCount++ buffer.position(value) pos = value } @@ -50,6 +86,7 @@ abstract class GenericBuffer( protected var pos = 0 override fun clear() { + modCount++ buffer.clear() position = 0 } @@ -70,46 +107,46 @@ 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 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 @@ -118,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 @@ -156,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 @@ -194,30 +234,31 @@ 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) { + modCount++ buffer.put(i, value) } override fun put(value: Float): Float32Buffer { + modCount++ buffer.put(value) pos++ return this } override fun put(data: FloatArray, offset: Int, len: Int): Float32Buffer { + modCount++ buffer.put(data, offset, len) pos += len return this } override fun put(data: Float32Buffer): Float32Buffer { + modCount++ data.useRaw { buffer.put(it) pos += data.limit @@ -229,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 @@ -261,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]) @@ -284,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()) @@ -302,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]) @@ -325,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]) @@ -343,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]) @@ -366,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]) @@ -384,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-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/AoDemo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/AoDemo.kt index 71bddb9be..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 @@ -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) @@ -190,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/Deferred2Test.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/Deferred2Test.kt new file mode 100644 index 000000000..e1a66f807 --- /dev/null +++ b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/Deferred2Test.kt @@ -0,0 +1,331 @@ +package de.fabmax.kool.demo + +import de.fabmax.kool.KoolContext +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.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 +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 Deferred2Test : DemoScene("Deferred2 Test") { + + val ibl by hdriImage("${DemoLoader.hdriPath}/newport_loft.rgbe.png") + + val teapot by model("${DemoLoader.modelPath}/teapot.gltf.gz", GltfLoadConfig(applyMaterials = false)) + + 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 + private val filterWeight = mutableStateOf(16) + private val groundRoughness = mutableStateOf(0.5f) + private val bloom = mutableStateOf(true) + + private val gpuTimes = mutableStateOf(GpuTimes()) + private var gpuTimesAccu = GpuTimes() + + override fun Scene.setupMainScene(ctx: KoolContext) { + val content = deferredContent() + val lighting = Lighting().apply { + clear() + } + + //val ibl = EnvironmentMap.fromSingleColor(Color.BLACK) + 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() + filterWeight.onChange { _, value -> pipeline.filterPass.filterWeight = value.toFloat() } + + bloomPass = pipeline.installBloomPass() + bloomPass.isProfileGpu = true + addNode(pipeline.defaultOutputQuad(bloomPass)) + + val deferredLights = DeferredLights(pipeline) + content.apply { + val orbitCam = orbitCamera(pipeline.camera) { + setRotation(100f, -7f) + } + addNode(orbitCam) + addNode(deferredLights) + } + + 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)) + } + } + + val nAccu = 50 + onUpdate { + gpuTimesAccu = GpuTimes( + 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) + gpuTimesAccu = GpuTimes(0.0, 0.0, 0.0, 0.0, 0.0) + } + } + } + + private fun deferredContent() = Node("deferred content").apply { + addGroup { + 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 { + color { + vertexColor() + uniformColor(uniformName = "uBaseCol", blendMode = ColorBlockConfig.BlendMode.Multiply) + } + roughness { constProperty(0.15f) } + emission { uniformProperty(10f) } + }.apply { + 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 + color = Color(b, b, b) + emission = e + } + } + } + addColorMesh { + generate { + color = MdColor.AMBER.toLinear() + icoSphere { + center.set(-2.5f, 0f, 2.5f) + steps = 4 + radius = 0.5f + } + } + shader = gbufferShader { + color { vertexColor() } + metallic { constProperty(1f) } + roughness { constProperty(0f) } + } + } + addTextureMesh(isNormalMapped = true) { + generate { + cube { } + } + onUpdate { + transform + .setIdentity() + .rotate(90f.deg * Time.gameTime.toFloat(), Vec3f.Y_AXIS) + .translate(2.5f, 0f, 0f) + } + shader = gbufferShader { + color { textureColor(albedoMap) } + normalMapping { useNormalMap(normalMap) } + metallic { textureProperty(metallicMap) } + roughness { textureProperty(roughnessMap) } + ao { textureProperty(aoMap) } + } + } + + val colorCubeInstances = MeshInstanceList(InstanceLayouts.ModelMat) + addColorMesh(instances = colorCubeInstances) { + generate { + cube { colored() } + } + 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 { + translate(0f, -0.5f, 0f) + grid { + sizeX = 50f + sizeY = 50f + texCoordScale.set(10f, 10f) + } + } + shader = gbufferShader { + color { + textureColor(uvChecker) + } + roughness { uniformProperty(groundRoughness.value) } + + }.apply { + groundRoughness.onChange { _, newValue -> roughness = newValue } + } + } + + val modelMesh = teapot.meshes.values.first().apply { + transform.translate(0f, -0.5f, 5f).scale(0.5f).rotate(20f.deg, Vec3f.Y_AXIS) + shader = gbufferShader { + color { + constColor(MdColor.LIME toneLin 500) + } + roughness { constProperty(0.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 { + 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 + .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) + } + Text("Timings".l) { sectionTitleStyle() } + MenuRow { + Column(width = Grow.Std) { + Text("Reproject Matrices:") { } + Text("G-Buffer:") { } + Text("Ambient Occlusion:") { } + Text("Lighting + Reflections:") { } + Text("Filter:") { } + Text("Bloom:") { } + } + 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") { } + Text("${t.filter.toString(2)} ms") { } + Text("${t.bloom.toString(2)} ms") { } + } + } + } +} + +private data class TsaaItem(val label: String, val tsaa: List, val numSamples: Int) { + companion object { + val items = listOf( + 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) + ) + } +} + +private data class ScaleItem(val label: String, val scale: Float) { + companion object { + val items = listOf( + ScaleItem("10 %", 0.1f), + ScaleItem("25 %", 0.25f), + ScaleItem("50 %", 0.5f), + ScaleItem("75 %", 0.75f), + ScaleItem("100 %", 1f), + ) + } +} + +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/DeferredDemo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/DeferredDemo.kt index a9efb6f83..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,16 +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.ColorBlockConfig 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.deferred.* +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 @@ -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 objects: Node + 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,25 @@ 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, lighting = lighting) + pipeline.renderScale = 1f / UiScale.windowScale.value + 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 +106,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 +117,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 +134,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) @@ -164,53 +158,57 @@ 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 = deferredKslPbrShader { - color { vertexColor() } - roughness(0.15f) - } - shader = objectShader } lightPositionMesh = addMesh(VertexLayouts.PositionNormal, instances = lightPosInsts) { - isFrustumChecked = false - isVisible = true - isCastingShadow = false generate { icoSphere { steps = 1 @@ -218,12 +216,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 +233,7 @@ class DeferredDemo : DemoScene("Deferred Shading") { } } - shader = deferredKslPbrShader { + shader = gbufferShader { color { textureColor(groundColor) } normalMapping { useNormalMap(groundNormals) } roughness { textureProperty(groundRoughness) } @@ -263,18 +259,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 +286,7 @@ class DeferredDemo : DemoScene("Deferred Shading") { } updateLightColors() - deferredPipeline.dynamicPointLights.lightInstances.forEach { + deferredLights.pointLights.forEach { it.radius = lightRadius.value it.intensity = lightPower.value } @@ -334,21 +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 { @@ -363,28 +349,34 @@ 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 + 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 - .imageSize(ImageSize.FixedScale(0.3f)) - .imageProvider(FlatImageProvider(albedoMetal, true).mirrorY()) + .width(600.dp) + .imageSize(ImageSize.ZoomContent) + .imageProvider(FlatImageProvider(albedo, true)) .margin(horizontal = sizes.gap) - .customShader(albedoMapShader.apply { colorMap = albedoMetal }) + .customShader(albedoMapShader.apply { colorMap = albedo }) Text("Albedo".l) { imageLabelStyle() } } Image { modifier - .imageSize(ImageSize.FixedScale(0.3f)) - .imageProvider(FlatImageProvider(normalRough, true).mirrorY()) + .width(600.dp) + .imageSize(ImageSize.ZoomContent) + .imageProvider(FlatImageProvider(normals, true)) .margin(horizontal = sizes.gap) - .customShader(normalMapShader.apply { colorMap = normalRough }) + .customShader(normalShader) Text("Normals".l) { imageLabelStyle() } } } @@ -392,39 +384,39 @@ class DeferredDemo : DemoScene("Deferred Shading") { modifier.margin(vertical = sizes.gap) Image { modifier - .imageSize(ImageSize.FixedScale(0.3f)) - .imageProvider(FlatImageProvider(positionFlags, true).mirrorY()) + .width(600.dp) + .imageSize(ImageSize.ZoomContent) + .imageProvider(FlatImageProvider(metalRoughAo, true)) .margin(horizontal = sizes.gap) - .customShader(positionMapShader.apply { colorMap = positionFlags }) - Text("Position".l) { imageLabelStyle() } + .customShader(metalRoughAoShader.apply { colorMap = metalRoughAo }) + Text("Metal, roughness, AO".l) { imageLabelStyle() } } - Image(ao) { + Image { modifier - .imageSize(ImageSize.FixedScale(0.3f / deferredPipeline.aoMapSize)) - .imageProvider(FlatImageProvider(ao, true).mirrorY()) + .width(600.dp) + .imageSize(ImageSize.ZoomContent) + .imageProvider(FlatImageProvider(ssao, true)) .margin(horizontal = sizes.gap) - .customShader(AoDemo.aoMapShader.apply { colorMap = ao }) - Text("Ambient occlusion".l) { imageLabelStyle() } + .customShader(AoDemo.aoMapShader.apply { colorMap = ssao }) + Text("SSAO".l) { imageLabelStyle() } } } Row { modifier.margin(vertical = sizes.gap) - Image(positionFlags) { + Image(objectIds) { modifier - .imageSize(ImageSize.FixedScale(0.3f)) - .imageProvider(FlatImageProvider(positionFlags, true).mirrorY()) + .width(600.dp) + .imageSize(ImageSize.ZoomContent) + .imageProvider(FlatImageProvider(objectIds, true)) .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() } + .customShader(objectIdShader) + Text("Object IDs".l) { imageLabelStyle() } } Image(bloom) { modifier - .imageSize(ImageSize.FixedScale(0.6f * ((positionFlags.height) / bloom!!.width))) - .imageProvider(FlatImageProvider(bloom, true).mirrorY()) + .width(600.dp) + .imageSize(ImageSize.ZoomContent) + .imageProvider(FlatImageProvider(bloom, true)) .margin(horizontal = sizes.gap) .customShader(bloomMapShader.apply { colorMap = bloom }) Text("Bloom".l) { imageLabelStyle() } @@ -452,7 +444,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 @@ -488,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() } @@ -507,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 = { @@ -515,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/Demos.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/Demos.kt index 65c68dd9c..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 @@ -100,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) 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..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,25 +4,27 @@ 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.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.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 -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 kotlinx.coroutines.async import kotlin.math.PI import kotlin.math.cos -import kotlin.math.roundToInt import kotlin.math.sin class GltfDemo : DemoScene("glTF Models") { @@ -73,32 +75,23 @@ 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: DeferredPipeline - private val contentGroupDeferred = Node() + private lateinit var deferredPipeline: Deferred2Pipeline + //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 -> - deferredPipeline.isSsrEnabled = new - setupPipelines(isDeferredShading.value, isAo.value) + if (new) { + deferredPipeline.enableScreenSpaceReflections() + } else { + deferredPipeline.disableScreenSpaceReflections() + } } - private val ssrMapSize = mutableStateOf(0.5f).onChange { _, new -> deferredPipeline.reflectionMapSize = new } override fun lateInit(ctx: KoolContext) { currentModel.isVisible = true @@ -109,35 +102,26 @@ 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 = 0.2f - } - ssrMapSize.set(deferredPipeline.reflectionMapSize) - - // create forward pipeline - aoPipelineForward = AoPipeline.createForward(mainScene).apply { - radius = 0.2f - } - shadowsForward += listOf( - SimpleShadowMap(mainScene, mainScene.lighting.lights[0], contentGroupForward, mapSize = 2048), - SimpleShadowMap(mainScene, mainScene.lighting.lights[1], contentGroupForward, mapSize = 2048) + val camera = PerspectiveCamera() + val sceneContent = Node() + val shadows = mainScene.lighting.createShadowMaps(sceneContent, camera) + shadows.forEach { it.addToScene(mainScene) } + deferredPipeline = Deferred2Pipeline( + content = sceneContent, + scene = mainScene, + ibl = envMap, + 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) // 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() @@ -147,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 @@ -159,38 +140,8 @@ 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.sceneContent.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) - addNode(outputMesh) - } - addNode(contentGroupDeferred) - } - - private fun Scene.setupCamera() { - orbitTransform = orbitCamera { + private fun setupCamera() { + val cam = orbitCamera(deferredPipeline.camera) { setRotation(0f, -30f) zoom = currentModel.zoom translation.set(currentModel.lookAt) @@ -198,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) { @@ -216,6 +166,7 @@ class GltfDemo : DemoScene("glTF Models") { } } } + deferredPipeline.content.addNode(cam) } private fun Scene.setupLighting() { @@ -234,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) { @@ -248,36 +199,16 @@ class GltfDemo : DemoScene("glTF Models") { roundCylinder(4.1f, 0.2f) } - fun KslPbrShader.Config.Builder.materialConfig() { + shader = gbufferShader { color { textureColor(colorMap) } normalMapping { useNormalMap(normalMap) } ao { textureProperty(aoMap) } roughness { textureProperty(roughnessMap) } } - - shader = if (isDeferredShading) { - deferredKslPbrShader { - materialConfig() - } - } else { - KslPbrShader { - materialConfig() - lighting { - enableSsao(aoPipelineForward?.aoMap) - addShadowMaps(shadowsForward) - imageBasedAmbientLight(envMap.irradianceMap) - } - reflectionMap = envMap.reflectionMap - } - } } models.forEach { model -> - if (isDeferredShading) { - model.deferredModel?.let { addNode(it) } - } else { - model.forwardModel?.let { addNode(it) } - } + model.model?.let { addNode(it) } } } @@ -286,7 +217,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 } @@ -310,16 +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) - MenuSlider2("SSR map size".l, ssrMapSize.use(), 0.1f, 1f, { it.toString(1) }) { - ssrMapSize.set((it * 10).roundToInt() / 10f) - } - } + LabeledSwitch("Screen space reflections".l, isSsr) LabeledSwitch("Auto rotate view".l, isAutoRotate) } @@ -373,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 -> @@ -384,12 +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, - isDeferredShading = isDeferredShading + shaderFactory = GltfDeferredShaderFactory ) val modelCfg = GltfLoadConfig( generateNormals = generateNormals, @@ -418,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/ReflectionDemo.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/ReflectionDemo.kt index 573c2f98f..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 @@ -8,16 +8,16 @@ 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.pipeline.deferred2.* import de.fabmax.kool.scene.* -import de.fabmax.kool.toString import de.fabmax.kool.util.* 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") @@ -27,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) @@ -51,60 +57,61 @@ 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) - } - 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) - } - deferredPipeline = DeferredPipeline(scene, defCfg) + val content = Node() + shadowMaps = mainScene.lighting.createShadowMaps(content, camera) + shadowMaps.forEach { it.addToScene(mainScene) } + deferredPipeline = Deferred2Pipeline( + content = content, + scene = this, + ibl = hdri, + camera = camera, + maxGlobalLights = 4, + lighting = mainScene.lighting, + shadowMapConfig = shadowMaps.toConfig() + ) + deferredPipeline.renderScale = 1f / UiScale.windowScale.value + deferredPipeline.enableScreenSpaceReflections() - scene += deferredPipeline.createDefaultOutputQuad().also { - (it.shader as? DeferredOutputShader)?.setupVignette(0f) + addNode(deferredPipeline.defaultOutputQuad(null, writeDepth = true)) + addNode(lightGroup) + lightGroup.onUpdate { + lightGroup.transform.rotate((-3f).deg * Time.deltaT, Vec3f.Y_AXIS) } - scene += Skybox.cube(hdri.reflectionMap, 1f) - 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 { + 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) + setRotation(0f, -5f) + // let the camera slowly rotate around vertical axis + onUpdate += { + if (isAutoRotate.value) { + verticalRotation += Time.deltaT * 3f + } + } + } + addNode(camTransform) + addTextureMesh(isNormalMapped = true) { generate { rect { @@ -118,7 +125,7 @@ class ReflectionDemo : DemoScene("Reflections") { isCastingShadow = false groundMesh = this - shader = deferredKslPbrShader { + shader = gbufferShader { color { textureColor(floorAlbedo) } normalMapping { useNormalMap(floorNormal) } roughness { textureProperty(floorRoughness) } @@ -133,8 +140,8 @@ 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) } @@ -145,8 +152,8 @@ 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 } } @@ -158,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 { @@ -217,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/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/physics/vehicle/DemoVehicle.kt b/kool-demo/src/commonMain/kotlin/de/fabmax/kool/demo/physics/vehicle/DemoVehicle.kt index 9f3ca4786..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 @@ -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) 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..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,19 +5,20 @@ 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 -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 +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(isDeferredShading = true)) + GltfLoadConfig(materialConfig = GltfMaterialConfig(shaderFactory = GltfDeferredShaderFactory)) ) lateinit var vehicleWorld: VehicleWorld @@ -38,10 +39,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 +54,50 @@ 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, vignette = Vignette()) + 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) + } + + val lightToggleListener = KeyboardInput.addKeyListener(LocalKeyCode('l'), "Toggle head lights", filter = { it.isPressed }) { + vehicle.isHeadlightsOn = !vehicle.isHeadlightsOn + } + mainScene.onRelease { + KeyboardInput.removeKeyListener(lightToggleListener) } } @@ -105,12 +107,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 +228,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 +236,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..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 @@ -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,22 @@ 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 + 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) } private class TimerBackground(val bgColor: Color) : UiRenderer { @@ -156,18 +172,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-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 6d07587b6..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,13 +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.deferred.DeferredPipeline -import de.fabmax.kool.pipeline.deferred.DeferredPipelineConfig +import de.fabmax.kool.pipeline.ao.AoRadius +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.* @@ -51,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 = 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 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 } 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 }