Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
262494b
Initial test pipeline with somewhat working temporal filtering
fabmax May 13, 2026
d07b990
Use rgba texture for normals
fabmax May 14, 2026
5921aad
Pretty good temporal filter
fabmax May 14, 2026
ac04aca
Integrate bloom into deferred2 pipeline
fabmax May 16, 2026
48af0fd
Object ID + depth based temporal filter
fabmax May 17, 2026
c12e5bc
Moved deferred2 to a demo
fabmax May 17, 2026
04f20fb
Settings and stuff
fabmax May 17, 2026
29bdb92
Integrate ao into deferred 2
fabmax May 17, 2026
376dcfa
Back to int encoded normals for higher precision
fabmax May 17, 2026
1c53b4c
Screen space reflections
fabmax May 20, 2026
1486b55
Emissive materials
fabmax May 22, 2026
513864f
Use demo loader resource paths
fabmax May 22, 2026
c8af4aa
More ssr optimizations
fabmax May 22, 2026
295cc28
Cleanup stuff
fabmax May 23, 2026
38b7460
Do reprojection matrix computations in a compute pass
fabmax May 24, 2026
afb6628
Auto assign object IDs incl instanced mesh support
fabmax May 24, 2026
b858e42
Deferred dynamic lighting
fabmax May 24, 2026
2e1e418
Deferred pipeline helpers
fabmax May 24, 2026
dfae51a
Use new deferred pipeline in demos
fabmax May 24, 2026
beb2c23
Slightly improved temporal filtering
fabmax May 25, 2026
541b721
Integrate shadow mapping into new deferred pipeline
fabmax May 25, 2026
2ab2ab2
Configurable ssr
fabmax May 25, 2026
f3c3c9d
Fix ao and ssr on webgpu backend
fabmax May 25, 2026
82a524c
Moved deferred2 to core
fabmax May 25, 2026
60a9e75
Use deferred2 for vehicle demo
fabmax May 25, 2026
7b66529
Finalizing deferred 2
fabmax May 29, 2026
f431f89
Updated readme and demo entries
fabmax May 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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(
Expand All @@ -32,5 +37,30 @@ data class GltfMaterialConfig(
val isDeferredShading: Boolean = false,
val maxNumberOfLights: Int = 4,
val fixedNumberOfJoints: Int = 0,
val modelMatrixComposition: List<ModelMatrixComposition> = emptyList()
val modelMatrixComposition: List<ModelMatrixComposition> = 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())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -134,12 +143,12 @@ data class LightingConfig(
return this
}

fun addShadowMap(shadowMap: ShadowMap, samplePattern: List<Vec2f> = ShadowMapConfig.SHADOW_SAMPLE_PATTERN_4x4): Builder {
fun addShadowMap(shadowMap: ShadowMap, samplePattern: List<Vec2f> = SHADOW_SAMPLE_PATTERN_4x4): Builder {
shadowMaps += ShadowMapConfig(shadowMap, samplePattern)
return this
}

fun addShadowMaps(shadowMaps: Collection<ShadowMap>, samplePattern: List<Vec2f> = ShadowMapConfig.SHADOW_SAMPLE_PATTERN_4x4): Builder {
fun addShadowMaps(shadowMaps: Collection<ShadowMap>, samplePattern: List<Vec2f> = SHADOW_SAMPLE_PATTERN_4x4): Builder {
this.shadowMaps += shadowMaps.map { ShadowMapConfig(it, samplePattern) }
return this
}
Expand Down Expand Up @@ -198,3 +207,7 @@ data class ShadowMapConfig(val shadowMap: ShadowMap, val samplePattern: List<Vec
}
}
}

fun List<ShadowMap>.toConfig(samplePattern: List<Vec2f> = SHADOW_SAMPLE_PATTERN_4x4): List<ShadowMapConfig> {
return map { ShadowMapConfig(it, samplePattern) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<CameraData>().firstOrNull() ?: CameraData(program)
}

fun KslScopeBuilder.depthToViewSpacePos(linearDepth: KslExprFloat1, clipSpaceXy: KslExprFloat2, camData: CameraData): KslExprFloat3 {
Expand All @@ -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]
Expand Down Expand Up @@ -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)
Expand All @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ class ToneMapLinearColorUncharted2(parentScope: KslScopeBuilder) :

fun KslScopeBuilder.convertColorSpace(inputColor: KslExprFloat3, conversion: ColorSpaceConversion): KslVectorExpression<KslFloat3, KslFloat1> =
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 -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import kotlin.math.PI
context(builder: KslScopeBuilder)
fun pbrMaterialBlock(
maxNumberOfLights: Int,
reflectionMaps: List<KslExpression<KslColorSamplerCube>>?,
reflectionMaps: List<KslExpression<KslColorSamplerCube>>,
brdfLut: KslExpression<KslColorSampler2d>,
normalLightRange: NormalLightRange,
block: PbrMaterialBlock.() -> Unit
Expand All @@ -29,7 +29,7 @@ fun pbrMaterialBlock(
class PbrMaterialBlock(
maxNumberOfLights: Int,
name: String,
reflectionMaps: List<KslExpression<KslColorSamplerCube>>?,
reflectionMaps: List<KslExpression<KslColorSamplerCube>>,
brdfLut: KslExpression<KslColorSampler2d>,
normalLightRange: NormalLightRange,
parentScope: KslScopeBuilder,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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` {
Expand All @@ -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)
Expand All @@ -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
Expand Down
Loading
Loading