From bd026864a62d94bfd74eae229c08e5d9266eadeb Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Tue, 8 Jul 2025 11:05:11 +1000 Subject: [PATCH 01/30] Clustered lights initial implementation --- packages/dev/core/src/Engines/thinEngine.ts | 2 +- .../dev/core/src/Lights/clusteredLight.ts | 106 ++++++++++++++++++ packages/dev/core/src/Lights/index.ts | 1 + packages/dev/core/src/Lights/light.ts | 35 +++--- packages/dev/core/src/Lights/spotLight.ts | 11 +- .../src/Materials/materialHelper.functions.ts | 1 + .../dev/core/src/Materials/uniformBuffer.ts | 15 ++- .../lightClusteredDeclaration.fx | 7 ++ .../Shaders/ShadersInclude/lightFragment.fx | 8 +- .../ShadersInclude/lightUboDeclaration.fx | 4 + .../ShadersInclude/lightVxUboDeclaration.fx | 4 + .../ShadersInclude/lightsFragmentFunctions.fx | 16 ++- .../dev/core/src/Shaders/default.fragment.fx | 1 + .../dev/core/src/Shaders/default.vertex.fx | 1 + 14 files changed, 187 insertions(+), 25 deletions(-) create mode 100644 packages/dev/core/src/Lights/clusteredLight.ts create mode 100644 packages/dev/core/src/Shaders/ShadersInclude/lightClusteredDeclaration.fx diff --git a/packages/dev/core/src/Engines/thinEngine.ts b/packages/dev/core/src/Engines/thinEngine.ts index d8ad192d04b..b45444ba888 100644 --- a/packages/dev/core/src/Engines/thinEngine.ts +++ b/packages/dev/core/src/Engines/thinEngine.ts @@ -122,7 +122,7 @@ export class ThinEngine extends AbstractEngine { { key: "Chrome/74.+?Mobile", capture: null, captureConstraint: null, targets: ["vao"] }, { key: "Mac OS.+Chrome/71", capture: null, captureConstraint: null, targets: ["vao"] }, { key: "Mac OS.+Chrome/72", capture: null, captureConstraint: null, targets: ["vao"] }, - { key: "Mac OS.+Chrome", capture: null, captureConstraint: null, targets: ["uniformBuffer"] }, + // { key: "Mac OS.+Chrome", capture: null, captureConstraint: null, targets: ["uniformBuffer"] }, { key: "Chrome/12\\d\\..+?Mobile", capture: null, captureConstraint: null, targets: ["uniformBuffer"] }, // desktop osx safari 15.4 { key: ".*AppleWebKit.*(15.4).*Safari", capture: null, captureConstraint: null, targets: ["antialias", "maxMSAASamples"] }, diff --git a/packages/dev/core/src/Lights/clusteredLight.ts b/packages/dev/core/src/Lights/clusteredLight.ts new file mode 100644 index 00000000000..5fe3475bbc3 --- /dev/null +++ b/packages/dev/core/src/Lights/clusteredLight.ts @@ -0,0 +1,106 @@ +import type { Effect } from "core/Materials/effect"; +import type { Scene } from "core/scene"; +import type { Nullable } from "core/types"; + +import { Light } from "./light"; +import { SpotLight } from "./spotLight"; + +const MAX_CLUSTERED_LIGHTS = 32; + +export class ClusteredLight extends Light { + private static _GetUnsupportedReason(light: Light): Nullable { + // light.getEngine()._alphaMode + if (!(light instanceof SpotLight)) { + return "light is not a spot light"; + } + // TODO: don't allow lights with shadows + return null; + } + + public static IsLightSupported(light: Light): boolean { + return this._GetUnsupportedReason(light) === null; + } + + private _lights: Light[] = []; + public get lights(): readonly Light[] { + return this._lights; + } + + private get _clusteredLights(): number { + return Math.min(this._lights.length, MAX_CLUSTERED_LIGHTS); + } + + constructor(name: string, lights: Light[] = [], scene?: Scene) { + super(name, scene); + for (const light of lights) { + this.addLight(light); + } + } + + public override getClassName(): string { + return "ClusteredLight"; + } + + public addLight(light: Light): void { + const reason = ClusteredLight._GetUnsupportedReason(light); + if (reason !== null) { + throw new Error(`Cannot cluster light: ${reason}`); + } + this._scene.removeLight(light); + this._lights.push(light); + this._markMeshesAsLightDirty(); + } + + protected _buildUniformLayout(): void { + for (let i = 0; i < MAX_CLUSTERED_LIGHTS; i += 1) { + const iAsString = i.toString(); + this._uniformBuffer.addUniform("vLightData" + iAsString, 4); + this._uniformBuffer.addUniform("vLightDiffuse" + iAsString, 4); + this._uniformBuffer.addUniform("vLightSpecular" + iAsString, 4); + this._uniformBuffer.addUniform("vLightDirection" + iAsString, 4); + this._uniformBuffer.addUniform("vLightFalloff" + iAsString, 4); + } + this._uniformBuffer.create(true); + } + + /** + * Binds the lights information from the scene to the effect for the given mesh. + * @param lightIndex Light index + * @param scene The scene where the light belongs to + * @param effect The effect we are binding the data to + * @param useSpecular Defines if specular is supported + */ + public override _bindLight(lightIndex: number, scene: Scene, effect: Effect, useSpecular: boolean): void { + let needUpdate = false; + + this._uniformBuffer.bindToEffect(effect, "Light" + lightIndex.toString()); + + for (let i = 0; i < this._clusteredLights; i += 1) { + if (this._lights[i]._bindLightToUniform(i, scene, effect, this._uniformBuffer, useSpecular, false)) { + needUpdate = true; + } + } + + if (needUpdate) { + this._uniformBuffer.update(); + } else { + this._uniformBuffer.bindUniformBuffer(); + } + } + + public transferToEffect(): Light { + return this; + } + + public transferToNodeMaterialEffect(): Light { + // TODO: ???? + return this; + } + + public prepareLightSpecificDefines(defines: any, lightIndex: number): void { + defines["CLUSTLIGHT" + lightIndex] = true; + // We're just using a define for now until we add proper light clustering + defines["CLUSTLIGHT_COUNT" + lightIndex] = this._clusteredLights; + defines["CLUSTLIGHTSUPPORTED"] = true; + } +} diff --git a/packages/dev/core/src/Lights/index.ts b/packages/dev/core/src/Lights/index.ts index 75712031eea..be0a3dbee22 100644 --- a/packages/dev/core/src/Lights/index.ts +++ b/packages/dev/core/src/Lights/index.ts @@ -8,4 +8,5 @@ export * from "./pointLight"; export * from "./spotLight"; export * from "./areaLight"; export * from "./rectAreaLight"; +export * from "./clusteredLight"; export * from "./IES/iesLoader"; diff --git a/packages/dev/core/src/Lights/light.ts b/packages/dev/core/src/Lights/light.ts index ae45332de9c..ffafa22a426 100644 --- a/packages/dev/core/src/Lights/light.ts +++ b/packages/dev/core/src/Lights/light.ts @@ -393,7 +393,7 @@ export abstract class Light extends Node implements ISortableLight { * @param lightIndex The index of the light in the effect to update * @returns The light */ - public abstract transferToEffect(effect: Effect, lightIndex: string): Light; + public abstract transferToEffect(effect: Effect, lightIndex: string, uniformBuffer?: UniformBuffer): Light; /** * Sets the passed Effect "effect" with the Light textures. @@ -417,30 +417,41 @@ export abstract class Light extends Node implements ISortableLight { */ public _bindLight(lightIndex: number, scene: Scene, effect: Effect, useSpecular: boolean, receiveShadows = true): void { const iAsString = lightIndex.toString(); - let needUpdate = false; this._uniformBuffer.bindToEffect(effect, "Light" + iAsString); + const needUpdate = this._bindLightToUniform(lightIndex, scene, effect, this._uniformBuffer, useSpecular, receiveShadows); + + // Textures might still need to be rebound. + this.transferTexturesToEffect(effect, iAsString); + + if (needUpdate) { + this._uniformBuffer.update(); + } else { + this._uniformBuffer.bindUniformBuffer(); + } + } - if (this._renderId !== scene.getRenderId() || this._lastUseSpecular !== useSpecular || !this._uniformBuffer.useUbo) { + public _bindLightToUniform(lightIndex: number, scene: Scene, effect: Effect, uniformBuffer: UniformBuffer, useSpecular: boolean, receiveShadows = true): boolean { + const iAsString = lightIndex.toString(); + let needUpdate = false; + + if (this._renderId !== scene.getRenderId() || this._lastUseSpecular !== useSpecular || !uniformBuffer.useUbo) { this._renderId = scene.getRenderId(); this._lastUseSpecular = useSpecular; const scaledIntensity = this.getScaledIntensity(); - this.transferToEffect(effect, iAsString); + this.transferToEffect(effect, iAsString, uniformBuffer); this.diffuse.scaleToRef(scaledIntensity, TmpColors.Color3[0]); - this._uniformBuffer.updateColor4("vLightDiffuse", TmpColors.Color3[0], this.range, iAsString); + uniformBuffer.updateColor4("vLightDiffuse", TmpColors.Color3[0], this.range, iAsString); if (useSpecular) { this.specular.scaleToRef(scaledIntensity, TmpColors.Color3[1]); - this._uniformBuffer.updateColor4("vLightSpecular", TmpColors.Color3[1], this.radius, iAsString); + uniformBuffer.updateColor4("vLightSpecular", TmpColors.Color3[1], this.radius, iAsString); } needUpdate = true; } - // Textures might still need to be rebound. - this.transferTexturesToEffect(effect, iAsString); - // Shadows if (scene.shadowsEnabled && this.shadowEnabled && receiveShadows) { const shadowGenerator = this.getShadowGenerator(scene.activeCamera) ?? this.getShadowGenerator(); @@ -450,11 +461,7 @@ export abstract class Light extends Node implements ISortableLight { } } - if (needUpdate) { - this._uniformBuffer.update(); - } else { - this._uniformBuffer.bindUniformBuffer(); - } + return needUpdate; } /** diff --git a/packages/dev/core/src/Lights/spotLight.ts b/packages/dev/core/src/Lights/spotLight.ts index 71d263626fb..26d2573e8a5 100644 --- a/packages/dev/core/src/Lights/spotLight.ts +++ b/packages/dev/core/src/Lights/spotLight.ts @@ -422,24 +422,25 @@ export class SpotLight extends ShadowLight { * Sets the passed Effect object with the SpotLight transformed position (or position if not parented) and normalized direction. * @param effect The effect to update * @param lightIndex The index of the light in the effect to update + * @param uniformBuffer The uniform buffer to update * @returns The spot light */ - public transferToEffect(effect: Effect, lightIndex: string): SpotLight { + public transferToEffect(effect: Effect, lightIndex: string, uniformBuffer = this._uniformBuffer): SpotLight { let normalizeDirection; if (this.computeTransformedInformation()) { - this._uniformBuffer.updateFloat4("vLightData", this.transformedPosition.x, this.transformedPosition.y, this.transformedPosition.z, this.exponent, lightIndex); + uniformBuffer.updateFloat4("vLightData", this.transformedPosition.x, this.transformedPosition.y, this.transformedPosition.z, this.exponent, lightIndex); normalizeDirection = Vector3.Normalize(this.transformedDirection); } else { - this._uniformBuffer.updateFloat4("vLightData", this.position.x, this.position.y, this.position.z, this.exponent, lightIndex); + uniformBuffer.updateFloat4("vLightData", this.position.x, this.position.y, this.position.z, this.exponent, lightIndex); normalizeDirection = Vector3.Normalize(this.direction); } - this._uniformBuffer.updateFloat4("vLightDirection", normalizeDirection.x, normalizeDirection.y, normalizeDirection.z, this._cosHalfAngle, lightIndex); + uniformBuffer.updateFloat4("vLightDirection", normalizeDirection.x, normalizeDirection.y, normalizeDirection.z, this._cosHalfAngle, lightIndex); - this._uniformBuffer.updateFloat4("vLightFalloff", this.range, this._inverseSquaredRange, this._lightAngleScale, this._lightAngleOffset, lightIndex); + uniformBuffer.updateFloat4("vLightFalloff", this.range, this._inverseSquaredRange, this._lightAngleScale, this._lightAngleOffset, lightIndex); return this; } diff --git a/packages/dev/core/src/Materials/materialHelper.functions.ts b/packages/dev/core/src/Materials/materialHelper.functions.ts index ff19a590ac4..19d96e4b0ac 100644 --- a/packages/dev/core/src/Materials/materialHelper.functions.ts +++ b/packages/dev/core/src/Materials/materialHelper.functions.ts @@ -567,6 +567,7 @@ export function PrepareDefinesForLights(scene: Scene, mesh: AbstractMesh, define defines["DIRLIGHT" + index] = false; defines["SPOTLIGHT" + index] = false; defines["AREALIGHT" + index] = false; + defines["CLUSTLIGHT" + index] = false; defines["SHADOW" + index] = false; defines["SHADOWCSM" + index] = false; defines["SHADOWCSMDEBUG" + index] = false; diff --git a/packages/dev/core/src/Materials/uniformBuffer.ts b/packages/dev/core/src/Materials/uniformBuffer.ts index c4bb00aa5d7..5cf00974e9c 100644 --- a/packages/dev/core/src/Materials/uniformBuffer.ts +++ b/packages/dev/core/src/Materials/uniformBuffer.ts @@ -40,6 +40,7 @@ export class UniformBuffer { private _currentEffectName: string; private _name: string; private _currentFrameId: number; + private _suffixUbo = false; // Pool for avoiding memory leaks private static _MAX_UNIFORM_SIZE = 256; @@ -538,8 +539,9 @@ export class UniformBuffer { /** * Effectively creates the WebGL Uniform Buffer, once layout is completed with `addUniform`. + * @param suffixUbo Set to true to apply the suffixes to UBOs as well */ - public create(): void { + public create(suffixUbo = false): void { if (this._noUBO) { return; } @@ -554,6 +556,7 @@ export class UniformBuffer { this._rebuild(); this._needSync = true; + this._suffixUbo = suffixUbo; } // The result of this method is used for debugging purpose, as part of the buffer name @@ -890,7 +893,10 @@ export class UniformBuffer { this._currentEffect.setFloat4(name + suffix, x, y, z, w); } - private _updateFloat4ForUniform(name: string, x: number, y: number, z: number, w: number) { + private _updateFloat4ForUniform(name: string, x: number, y: number, z: number, w: number, suffix = "") { + if (this._suffixUbo) { + name += suffix; + } UniformBuffer._TempBuffer[0] = x; UniformBuffer._TempBuffer[1] = y; UniformBuffer._TempBuffer[2] = z; @@ -992,7 +998,10 @@ export class UniformBuffer { this._currentEffect.setDirectColor4(name + suffix, color); } - private _updateColor4ForUniform(name: string, color: IColor3Like, alpha: number) { + private _updateColor4ForUniform(name: string, color: IColor3Like, alpha: number, suffix = "") { + if (this._suffixUbo) { + name += suffix; + } UniformBuffer._TempBuffer[0] = color.r; UniformBuffer._TempBuffer[1] = color.g; UniformBuffer._TempBuffer[2] = color.b; diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightClusteredDeclaration.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightClusteredDeclaration.fx new file mode 100644 index 00000000000..254af9a5885 --- /dev/null +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightClusteredDeclaration.fx @@ -0,0 +1,7 @@ +struct ClusteredLight { + vec4 vLightData; // Position + vec4 vLightDiffuse; + vec4 vLightSpecular; + vec4 vLightDirection; + vec4 vLightFalloff; +}; diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx index 15873c242f0..dc73c7abb61 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx @@ -3,7 +3,11 @@ //No light calculation #else - vec4 diffuse{X} = light{X}.vLightDiffuse; + #ifdef CLUSTLIGHT{X} + vec4 diffuse{X} = vec4(1); + #else + vec4 diffuse{X} = light{X}.vLightDiffuse; + #endif #define CUSTOM_LIGHT{X}_COLOR // Use to modify light color. Currently only supports diffuse. #ifdef PBR @@ -205,6 +209,8 @@ vReflectionInfos.y #endif ); + #elif defined(CLUSTLIGHT{X}) + info = computeClusteredLighting(light{X}.vLights, CLUSTLIGHT_COUNT{X}, viewDirectionW, normalW, diffuse{X}, glossiness); #endif #endif diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx index 61f182059e0..36467b93a4e 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx @@ -1,6 +1,9 @@ #ifdef LIGHT{X} uniform Light{X} { + #ifdef CLUSTLIGHT{X} + ClusteredLight vLights[32]; + #else vec4 vLightData; vec4 vLightDiffuse; @@ -19,6 +22,7 @@ #endif vec4 shadowsInfo; vec2 depthValues; + #endif } light{X}; #ifdef IESLIGHTTEXTURE{X} uniform sampler2D iesLightTexture{X}; diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightVxUboDeclaration.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightVxUboDeclaration.fx index 1c1a2005e17..38c96de83e8 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightVxUboDeclaration.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightVxUboDeclaration.fx @@ -1,6 +1,9 @@ #ifdef LIGHT{X} uniform Light{X} { + #ifdef CLUSTLIGHT{X} + ClusteredLight vLights[32]; + #else vec4 vLightData; vec4 vLightDiffuse; @@ -19,6 +22,7 @@ #endif vec4 shadowsInfo; vec2 depthValues; + #endif } light{X}; #ifdef SHADOW{X} #ifdef SHADOWCSM{X} diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx index ff9a32bf424..f3ffc7650af 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx @@ -178,4 +178,18 @@ lightingInfo computeAreaLighting(sampler2D ltc1, sampler2D ltc2, vec3 viewDirect } // End Area Light -#endif \ No newline at end of file +#endif + +lightingInfo computeClusteredLighting(ClusteredLight lights[32], int lightCount, vec3 viewDirectionW, vec3 vNormal, vec4 diffuseColor, float glossiness) { + lightingInfo aggInfo; + // TODO: only do this on WebGL 2 + for (int i = 0; i < lightCount; i += 1) { + vec4 lightDiffuse = lights[i].vLightDiffuse * diffuseColor; + lightingInfo info = computeSpotLighting(viewDirectionW, vNormal, lights[i].vLightData, lights[i].vLightDirection, lightDiffuse.rgb, lights[i].vLightSpecular.rgb, lightDiffuse.a, glossiness); + aggInfo.diffuse += info.diffuse; + #ifdef SPECULARTERM + aggInfo.specular += info.specular; + #endif + } + return aggInfo; +} diff --git a/packages/dev/core/src/Shaders/default.fragment.fx b/packages/dev/core/src/Shaders/default.fragment.fx index 38077eadcb9..dd26571a823 100644 --- a/packages/dev/core/src/Shaders/default.fragment.fx +++ b/packages/dev/core/src/Shaders/default.fragment.fx @@ -32,6 +32,7 @@ varying vec4 vColor; #include // Lights +#include #include<__decl__lightFragment>[0..maxSimultaneousLights] #include diff --git a/packages/dev/core/src/Shaders/default.vertex.fx b/packages/dev/core/src/Shaders/default.vertex.fx index 86eec837b30..2ccb1e02480 100644 --- a/packages/dev/core/src/Shaders/default.vertex.fx +++ b/packages/dev/core/src/Shaders/default.vertex.fx @@ -58,6 +58,7 @@ varying vec4 vColor; #include #include +#include #include<__decl__lightVxFragment>[0..maxSimultaneousLights] #include From 88f1e7929f6e80fc9cccd861744ade89931cd646 Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Tue, 8 Jul 2025 13:27:53 +1000 Subject: [PATCH 02/30] Make changes a bit less invasive to other code --- .../core/src/Engines/engineCapabilities.ts | 2 + packages/dev/core/src/Engines/nativeEngine.ts | 1 + packages/dev/core/src/Engines/nullEngine.ts | 1 + packages/dev/core/src/Engines/thinEngine.ts | 3 +- packages/dev/core/src/Engines/webgpuEngine.ts | 1 + .../dev/core/src/Lights/clusteredLight.ts | 133 +++++++++++------- packages/dev/core/src/Lights/light.ts | 38 +++-- packages/dev/core/src/Lights/spotLight.ts | 20 +-- .../dev/core/src/Materials/uniformBuffer.ts | 15 +- .../lightClusteredDeclaration.fx | 10 +- .../Shaders/ShadersInclude/lightFragment.fx | 10 +- .../ShadersInclude/lightUboDeclaration.fx | 6 +- .../ShadersInclude/lightVxUboDeclaration.fx | 6 +- .../ShadersInclude/lightsFragmentFunctions.fx | 23 +-- 14 files changed, 145 insertions(+), 124 deletions(-) diff --git a/packages/dev/core/src/Engines/engineCapabilities.ts b/packages/dev/core/src/Engines/engineCapabilities.ts index 9c512fdc7d2..65004b72fa3 100644 --- a/packages/dev/core/src/Engines/engineCapabilities.ts +++ b/packages/dev/core/src/Engines/engineCapabilities.ts @@ -80,6 +80,8 @@ export interface EngineCapabilities { depthTextureExtension: boolean; /** Defines if float color buffer are supported */ colorBufferFloat: boolean; + /** Defines if float color blending is supported */ + blendFloat: boolean; /** Defines if half float color buffer are supported */ colorBufferHalfFloat?: boolean; /** Gets disjoint timer query extension (null if not supported) */ diff --git a/packages/dev/core/src/Engines/nativeEngine.ts b/packages/dev/core/src/Engines/nativeEngine.ts index 560a222949d..7e8e86e074a 100644 --- a/packages/dev/core/src/Engines/nativeEngine.ts +++ b/packages/dev/core/src/Engines/nativeEngine.ts @@ -297,6 +297,7 @@ export class NativeEngine extends Engine { fragmentDepthSupported: false, highPrecisionShaderSupported: true, colorBufferFloat: false, + blendFloat: false, supportFloatTexturesResolve: false, rg11b10ufColorRenderable: false, textureFloat: true, diff --git a/packages/dev/core/src/Engines/nullEngine.ts b/packages/dev/core/src/Engines/nullEngine.ts index 9bbe1e4e470..992a5647ed1 100644 --- a/packages/dev/core/src/Engines/nullEngine.ts +++ b/packages/dev/core/src/Engines/nullEngine.ts @@ -144,6 +144,7 @@ export class NullEngine extends Engine { fragmentDepthSupported: false, highPrecisionShaderSupported: true, colorBufferFloat: false, + blendFloat: false, supportFloatTexturesResolve: false, rg11b10ufColorRenderable: false, textureFloat: false, diff --git a/packages/dev/core/src/Engines/thinEngine.ts b/packages/dev/core/src/Engines/thinEngine.ts index b45444ba888..f3e2c4ed63a 100644 --- a/packages/dev/core/src/Engines/thinEngine.ts +++ b/packages/dev/core/src/Engines/thinEngine.ts @@ -122,7 +122,7 @@ export class ThinEngine extends AbstractEngine { { key: "Chrome/74.+?Mobile", capture: null, captureConstraint: null, targets: ["vao"] }, { key: "Mac OS.+Chrome/71", capture: null, captureConstraint: null, targets: ["vao"] }, { key: "Mac OS.+Chrome/72", capture: null, captureConstraint: null, targets: ["vao"] }, - // { key: "Mac OS.+Chrome", capture: null, captureConstraint: null, targets: ["uniformBuffer"] }, + { key: "Mac OS.+Chrome", capture: null, captureConstraint: null, targets: ["uniformBuffer"] }, { key: "Chrome/12\\d\\..+?Mobile", capture: null, captureConstraint: null, targets: ["uniformBuffer"] }, // desktop osx safari 15.4 { key: ".*AppleWebKit.*(15.4).*Safari", capture: null, captureConstraint: null, targets: ["antialias", "maxMSAASamples"] }, @@ -533,6 +533,7 @@ export class ThinEngine extends AbstractEngine { drawBuffersExtension: false, maxMSAASamples: 1, colorBufferFloat: !!(this._webGLVersion > 1 && this._gl.getExtension("EXT_color_buffer_float")), + blendFloat: this._gl.getExtension("EXT_float_blend") !== null, supportFloatTexturesResolve: false, rg11b10ufColorRenderable: false, colorBufferHalfFloat: !!(this._webGLVersion > 1 && this._gl.getExtension("EXT_color_buffer_half_float")), diff --git a/packages/dev/core/src/Engines/webgpuEngine.ts b/packages/dev/core/src/Engines/webgpuEngine.ts index 14bf08a80d1..16bc7d8d4c5 100644 --- a/packages/dev/core/src/Engines/webgpuEngine.ts +++ b/packages/dev/core/src/Engines/webgpuEngine.ts @@ -869,6 +869,7 @@ export class WebGPUEngine extends ThinWebGPUEngine { fragmentDepthSupported: true, highPrecisionShaderSupported: true, colorBufferFloat: true, + blendFloat: this._deviceEnabledExtensions.indexOf(WebGPUConstants.FeatureName.Float32Blendable) >= 0, supportFloatTexturesResolve: false, // See https://github.com/gpuweb/gpuweb/issues/3844 rg11b10ufColorRenderable: this._deviceEnabledExtensions.indexOf(WebGPUConstants.FeatureName.RG11B10UFloatRenderable) >= 0, textureFloat: true, diff --git a/packages/dev/core/src/Lights/clusteredLight.ts b/packages/dev/core/src/Lights/clusteredLight.ts index 5fe3475bbc3..5dccff92e42 100644 --- a/packages/dev/core/src/Lights/clusteredLight.ts +++ b/packages/dev/core/src/Lights/clusteredLight.ts @@ -1,33 +1,53 @@ +import type { AbstractEngine } from "core/Engines/abstractEngine"; +import { ThinEngine } from "core/Engines/thinEngine"; import type { Effect } from "core/Materials/effect"; +import { Vector3 } from "core/Maths/math.vector"; +import { TmpColors } from "core/Maths/math.color"; import type { Scene } from "core/scene"; -import type { Nullable } from "core/types"; import { Light } from "./light"; +import { PointLight } from "./pointLight"; import { SpotLight } from "./spotLight"; const MAX_CLUSTERED_LIGHTS = 32; export class ClusteredLight extends Light { - private static _GetUnsupportedReason(light: Light): Nullable { - // light.getEngine()._alphaMode - if (!(light instanceof SpotLight)) { - return "light is not a spot light"; + private static _IsEngineSupported(engine: AbstractEngine): boolean { + if (engine.isWebGPU) { + return true; + } else if (engine instanceof ThinEngine && engine.version > 1) { + // On WebGL 2 we use additive float blending as the light mask + return engine._caps.colorBufferFloat && engine._caps.blendFloat; + } else { + // WebGL 1 is not supported due to lack of dynamic for loops + return false; } - // TODO: don't allow lights with shadows - return null; } public static IsLightSupported(light: Light): boolean { - return this._GetUnsupportedReason(light) === null; + if (!ClusteredLight._IsEngineSupported(light.getEngine())) { + return false; + } else if (light.shadowEnabled && light._scene.shadowsEnabled && light.getShadowGenerators()) { + // Shadows are not supported + return false; + } else if (light instanceof PointLight) { + return true; + } else if (light instanceof SpotLight) { + // Extra texture bindings per light are not supported + return !(light.projectionTexture || light.iesProfileTexture); + } else { + // Currently only point and spot lights are supported + return false; + } } - private _lights: Light[] = []; + private _lights: (PointLight | SpotLight)[] = []; public get lights(): readonly Light[] { return this._lights; } - private get _clusteredLights(): number { - return Math.min(this._lights.length, MAX_CLUSTERED_LIGHTS); + public get isSupported(): boolean { + return ClusteredLight._IsEngineSupported(this.getEngine()); } constructor(name: string, lights: Light[] = [], scene?: Scene) { @@ -42,53 +62,66 @@ export class ClusteredLight extends Light { } public addLight(light: Light): void { - const reason = ClusteredLight._GetUnsupportedReason(light); - if (reason !== null) { - throw new Error(`Cannot cluster light: ${reason}`); + if (ClusteredLight.IsLightSupported(light)) { + this._scene.removeLight(light); + this._lights.push(light); } - this._scene.removeLight(light); - this._lights.push(light); - this._markMeshesAsLightDirty(); } protected _buildUniformLayout(): void { + this._uniformBuffer.addUniform("vLightData", 4); + this._uniformBuffer.addUniform("vLightDiffuse", 4); + this._uniformBuffer.addUniform("vLightSpecular", 4); for (let i = 0; i < MAX_CLUSTERED_LIGHTS; i += 1) { - const iAsString = i.toString(); - this._uniformBuffer.addUniform("vLightData" + iAsString, 4); - this._uniformBuffer.addUniform("vLightDiffuse" + iAsString, 4); - this._uniformBuffer.addUniform("vLightSpecular" + iAsString, 4); - this._uniformBuffer.addUniform("vLightDirection" + iAsString, 4); - this._uniformBuffer.addUniform("vLightFalloff" + iAsString, 4); + // These technically don't have to match the field name but also why not + const struct = `vLights[${i}].`; + this._uniformBuffer.addUniform(struct + "position", 4); + this._uniformBuffer.addUniform(struct + "direction", 4); + this._uniformBuffer.addUniform(struct + "diffuse", 4); + this._uniformBuffer.addUniform(struct + "specular", 4); + this._uniformBuffer.addUniform(struct + "falloff", 4); } - this._uniformBuffer.create(true); + this._uniformBuffer.addUniform("shadowsInfo", 3); + this._uniformBuffer.addUniform("depthValues", 2); + this._uniformBuffer.create(); } - /** - * Binds the lights information from the scene to the effect for the given mesh. - * @param lightIndex Light index - * @param scene The scene where the light belongs to - * @param effect The effect we are binding the data to - * @param useSpecular Defines if specular is supported - */ - public override _bindLight(lightIndex: number, scene: Scene, effect: Effect, useSpecular: boolean): void { - let needUpdate = false; - - this._uniformBuffer.bindToEffect(effect, "Light" + lightIndex.toString()); - - for (let i = 0; i < this._clusteredLights; i += 1) { - if (this._lights[i]._bindLightToUniform(i, scene, effect, this._uniformBuffer, useSpecular, false)) { - needUpdate = true; + public transferToEffect(effect: Effect, lightIndex: string): Light { + const len = Math.min(this._lights.length, MAX_CLUSTERED_LIGHTS); + this._uniformBuffer.updateFloat4("vLightData", len, 0, 0, 0, lightIndex); + + for (let i = 0; i < len; i += 1) { + const light = this._lights[i]; + const spotLight = light instanceof SpotLight ? light : null; + const struct = `vLights[${i}].`; + + let position: Vector3; + let direction: Vector3; + if (light.computeTransformedInformation()) { + position = light.transformedPosition; + direction = Vector3.Normalize(light.transformedDirection); + } else { + position = light.position; + direction = Vector3.Normalize(light.direction); } + this._uniformBuffer.updateFloat4(struct + "position", position.x, position.y, position.z, spotLight?.exponent ?? 0, lightIndex); + this._uniformBuffer.updateFloat4(struct + "direction", direction.x, direction.y, direction.z, spotLight?._cosHalfAngle ?? 0, lightIndex); + + const scaledIntensity = light.getScaledIntensity(); + light.diffuse.scaleToRef(scaledIntensity, TmpColors.Color3[0]); + this._uniformBuffer.updateColor4(struct + "diffuse", TmpColors.Color3[0], light.range, lightIndex); + light.specular.scaleToRef(scaledIntensity, TmpColors.Color3[1]); + this._uniformBuffer.updateColor4(struct + "specular", TmpColors.Color3[1], light.radius, lightIndex); + + this._uniformBuffer.updateFloat4( + struct + "falloff", + light.range, + light._inverseSquaredRange, + spotLight?._lightAngleScale ?? 0, + spotLight?._lightAngleOffset ?? 0, + lightIndex + ); } - - if (needUpdate) { - this._uniformBuffer.update(); - } else { - this._uniformBuffer.bindUniformBuffer(); - } - } - - public transferToEffect(): Light { return this; } @@ -99,8 +132,6 @@ export class ClusteredLight extends Light { public prepareLightSpecificDefines(defines: any, lightIndex: number): void { defines["CLUSTLIGHT" + lightIndex] = true; - // We're just using a define for now until we add proper light clustering - defines["CLUSTLIGHT_COUNT" + lightIndex] = this._clusteredLights; - defines["CLUSTLIGHTSUPPORTED"] = true; + defines["CLUSTLIGHTSUPPORTED"] = this.isSupported; } } diff --git a/packages/dev/core/src/Lights/light.ts b/packages/dev/core/src/Lights/light.ts index ffafa22a426..c1016025513 100644 --- a/packages/dev/core/src/Lights/light.ts +++ b/packages/dev/core/src/Lights/light.ts @@ -144,7 +144,8 @@ export abstract class Light extends Node implements ISortableLight { public intensity = 1.0; private _range = Number.MAX_VALUE; - protected _inverseSquaredRange = 0; + /** @internal */ + public _inverseSquaredRange = 0; /** * Defines how far from the source the light is impacting in scene units. @@ -393,7 +394,7 @@ export abstract class Light extends Node implements ISortableLight { * @param lightIndex The index of the light in the effect to update * @returns The light */ - public abstract transferToEffect(effect: Effect, lightIndex: string, uniformBuffer?: UniformBuffer): Light; + public abstract transferToEffect(effect: Effect, lightIndex: string): Light; /** * Sets the passed Effect "effect" with the Light textures. @@ -417,41 +418,30 @@ export abstract class Light extends Node implements ISortableLight { */ public _bindLight(lightIndex: number, scene: Scene, effect: Effect, useSpecular: boolean, receiveShadows = true): void { const iAsString = lightIndex.toString(); + let needUpdate = false; this._uniformBuffer.bindToEffect(effect, "Light" + iAsString); - const needUpdate = this._bindLightToUniform(lightIndex, scene, effect, this._uniformBuffer, useSpecular, receiveShadows); - - // Textures might still need to be rebound. - this.transferTexturesToEffect(effect, iAsString); - - if (needUpdate) { - this._uniformBuffer.update(); - } else { - this._uniformBuffer.bindUniformBuffer(); - } - } - public _bindLightToUniform(lightIndex: number, scene: Scene, effect: Effect, uniformBuffer: UniformBuffer, useSpecular: boolean, receiveShadows = true): boolean { - const iAsString = lightIndex.toString(); - let needUpdate = false; - - if (this._renderId !== scene.getRenderId() || this._lastUseSpecular !== useSpecular || !uniformBuffer.useUbo) { + if (this._renderId !== scene.getRenderId() || this._lastUseSpecular !== useSpecular || !this._uniformBuffer.useUbo) { this._renderId = scene.getRenderId(); this._lastUseSpecular = useSpecular; const scaledIntensity = this.getScaledIntensity(); - this.transferToEffect(effect, iAsString, uniformBuffer); + this.transferToEffect(effect, iAsString); this.diffuse.scaleToRef(scaledIntensity, TmpColors.Color3[0]); - uniformBuffer.updateColor4("vLightDiffuse", TmpColors.Color3[0], this.range, iAsString); + this._uniformBuffer.updateColor4("vLightDiffuse", TmpColors.Color3[0], this.range, iAsString); if (useSpecular) { this.specular.scaleToRef(scaledIntensity, TmpColors.Color3[1]); - uniformBuffer.updateColor4("vLightSpecular", TmpColors.Color3[1], this.radius, iAsString); + this._uniformBuffer.updateColor4("vLightSpecular", TmpColors.Color3[1], this.radius, iAsString); } needUpdate = true; } + // Textures might still need to be rebound. + this.transferTexturesToEffect(effect, iAsString); + // Shadows if (scene.shadowsEnabled && this.shadowEnabled && receiveShadows) { const shadowGenerator = this.getShadowGenerator(scene.activeCamera) ?? this.getShadowGenerator(); @@ -461,7 +451,11 @@ export abstract class Light extends Node implements ISortableLight { } } - return needUpdate; + if (needUpdate) { + this._uniformBuffer.update(); + } else { + this._uniformBuffer.bindUniformBuffer(); + } } /** diff --git a/packages/dev/core/src/Lights/spotLight.ts b/packages/dev/core/src/Lights/spotLight.ts index 26d2573e8a5..2d51edcbee2 100644 --- a/packages/dev/core/src/Lights/spotLight.ts +++ b/packages/dev/core/src/Lights/spotLight.ts @@ -41,10 +41,13 @@ export class SpotLight extends ShadowLight { private _angle: number; private _innerAngle: number = 0; - private _cosHalfAngle: number; + /** @internal */ + public _cosHalfAngle: number; - private _lightAngleScale: number; - private _lightAngleOffset: number; + /** @internal */ + public _lightAngleScale: number; + /** @internal */ + public _lightAngleOffset: number; private _iesProfileTexture: Nullable = null; @@ -422,25 +425,24 @@ export class SpotLight extends ShadowLight { * Sets the passed Effect object with the SpotLight transformed position (or position if not parented) and normalized direction. * @param effect The effect to update * @param lightIndex The index of the light in the effect to update - * @param uniformBuffer The uniform buffer to update * @returns The spot light */ - public transferToEffect(effect: Effect, lightIndex: string, uniformBuffer = this._uniformBuffer): SpotLight { + public transferToEffect(effect: Effect, lightIndex: string): SpotLight { let normalizeDirection; if (this.computeTransformedInformation()) { - uniformBuffer.updateFloat4("vLightData", this.transformedPosition.x, this.transformedPosition.y, this.transformedPosition.z, this.exponent, lightIndex); + this._uniformBuffer.updateFloat4("vLightData", this.transformedPosition.x, this.transformedPosition.y, this.transformedPosition.z, this.exponent, lightIndex); normalizeDirection = Vector3.Normalize(this.transformedDirection); } else { - uniformBuffer.updateFloat4("vLightData", this.position.x, this.position.y, this.position.z, this.exponent, lightIndex); + this._uniformBuffer.updateFloat4("vLightData", this.position.x, this.position.y, this.position.z, this.exponent, lightIndex); normalizeDirection = Vector3.Normalize(this.direction); } - uniformBuffer.updateFloat4("vLightDirection", normalizeDirection.x, normalizeDirection.y, normalizeDirection.z, this._cosHalfAngle, lightIndex); + this._uniformBuffer.updateFloat4("vLightDirection", normalizeDirection.x, normalizeDirection.y, normalizeDirection.z, this._cosHalfAngle, lightIndex); - uniformBuffer.updateFloat4("vLightFalloff", this.range, this._inverseSquaredRange, this._lightAngleScale, this._lightAngleOffset, lightIndex); + this._uniformBuffer.updateFloat4("vLightFalloff", this.range, this._inverseSquaredRange, this._lightAngleScale, this._lightAngleOffset, lightIndex); return this; } diff --git a/packages/dev/core/src/Materials/uniformBuffer.ts b/packages/dev/core/src/Materials/uniformBuffer.ts index 5cf00974e9c..c4bb00aa5d7 100644 --- a/packages/dev/core/src/Materials/uniformBuffer.ts +++ b/packages/dev/core/src/Materials/uniformBuffer.ts @@ -40,7 +40,6 @@ export class UniformBuffer { private _currentEffectName: string; private _name: string; private _currentFrameId: number; - private _suffixUbo = false; // Pool for avoiding memory leaks private static _MAX_UNIFORM_SIZE = 256; @@ -539,9 +538,8 @@ export class UniformBuffer { /** * Effectively creates the WebGL Uniform Buffer, once layout is completed with `addUniform`. - * @param suffixUbo Set to true to apply the suffixes to UBOs as well */ - public create(suffixUbo = false): void { + public create(): void { if (this._noUBO) { return; } @@ -556,7 +554,6 @@ export class UniformBuffer { this._rebuild(); this._needSync = true; - this._suffixUbo = suffixUbo; } // The result of this method is used for debugging purpose, as part of the buffer name @@ -893,10 +890,7 @@ export class UniformBuffer { this._currentEffect.setFloat4(name + suffix, x, y, z, w); } - private _updateFloat4ForUniform(name: string, x: number, y: number, z: number, w: number, suffix = "") { - if (this._suffixUbo) { - name += suffix; - } + private _updateFloat4ForUniform(name: string, x: number, y: number, z: number, w: number) { UniformBuffer._TempBuffer[0] = x; UniformBuffer._TempBuffer[1] = y; UniformBuffer._TempBuffer[2] = z; @@ -998,10 +992,7 @@ export class UniformBuffer { this._currentEffect.setDirectColor4(name + suffix, color); } - private _updateColor4ForUniform(name: string, color: IColor3Like, alpha: number, suffix = "") { - if (this._suffixUbo) { - name += suffix; - } + private _updateColor4ForUniform(name: string, color: IColor3Like, alpha: number) { UniformBuffer._TempBuffer[0] = color.r; UniformBuffer._TempBuffer[1] = color.g; UniformBuffer._TempBuffer[2] = color.b; diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightClusteredDeclaration.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightClusteredDeclaration.fx index 254af9a5885..a940cc5c490 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightClusteredDeclaration.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightClusteredDeclaration.fx @@ -1,7 +1,7 @@ struct ClusteredLight { - vec4 vLightData; // Position - vec4 vLightDiffuse; - vec4 vLightSpecular; - vec4 vLightDirection; - vec4 vLightFalloff; + vec4 position; + vec4 direction; + vec4 diffuse; + vec4 specular; + vec4 falloff; }; diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx index dc73c7abb61..259cae8dc0e 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx @@ -3,11 +3,7 @@ //No light calculation #else - #ifdef CLUSTLIGHT{X} - vec4 diffuse{X} = vec4(1); - #else - vec4 diffuse{X} = light{X}.vLightDiffuse; - #endif + vec4 diffuse{X} = light{X}.vLightDiffuse; #define CUSTOM_LIGHT{X}_COLOR // Use to modify light color. Currently only supports diffuse. #ifdef PBR @@ -209,8 +205,8 @@ vReflectionInfos.y #endif ); - #elif defined(CLUSTLIGHT{X}) - info = computeClusteredLighting(light{X}.vLights, CLUSTLIGHT_COUNT{X}, viewDirectionW, normalW, diffuse{X}, glossiness); + #elif defined(CLUSTLIGHT{X}) && defined(CLUSTLIGHTSUPPORTED) + info = computeClusteredLighting(viewDirectionW, normalW, light{X}.vLightData, light{X}.vLights, diffuse{X}, light{X}.vLightSpecular.rgb, glossiness); #endif #endif diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx index 36467b93a4e..e3cb8daf0e7 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx @@ -1,9 +1,6 @@ #ifdef LIGHT{X} uniform Light{X} { - #ifdef CLUSTLIGHT{X} - ClusteredLight vLights[32]; - #else vec4 vLightData; vec4 vLightDiffuse; @@ -15,6 +12,8 @@ vec4 vLightFalloff; #elif defined(HEMILIGHT{X}) vec3 vLightGround; + #elif defined(CLUSTLIGHT{X}) + ClusteredLight vLights[32]; #endif #if defined(AREALIGHT{X}) vec4 vLightWidth; @@ -22,7 +21,6 @@ #endif vec4 shadowsInfo; vec2 depthValues; - #endif } light{X}; #ifdef IESLIGHTTEXTURE{X} uniform sampler2D iesLightTexture{X}; diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightVxUboDeclaration.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightVxUboDeclaration.fx index 38c96de83e8..6da47c15e2b 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightVxUboDeclaration.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightVxUboDeclaration.fx @@ -1,9 +1,6 @@ #ifdef LIGHT{X} uniform Light{X} { - #ifdef CLUSTLIGHT{X} - ClusteredLight vLights[32]; - #else vec4 vLightData; vec4 vLightDiffuse; @@ -15,6 +12,8 @@ vec4 vLightFalloff; #elif defined(HEMILIGHT{X}) vec3 vLightGround; + #elif defined(CLUSTLIGHT{X}) + ClusteredLight vLights[32]; #endif #if defined(AREALIGHT{X}) vec4 vLightWidth; @@ -22,7 +21,6 @@ #endif vec4 shadowsInfo; vec2 depthValues; - #endif } light{X}; #ifdef SHADOW{X} #ifdef SHADOWCSM{X} diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx index f3ffc7650af..9cf79d20c25 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx @@ -180,16 +180,21 @@ lightingInfo computeAreaLighting(sampler2D ltc1, sampler2D ltc2, vec3 viewDirect // End Area Light #endif -lightingInfo computeClusteredLighting(ClusteredLight lights[32], int lightCount, vec3 viewDirectionW, vec3 vNormal, vec4 diffuseColor, float glossiness) { - lightingInfo aggInfo; - // TODO: only do this on WebGL 2 - for (int i = 0; i < lightCount; i += 1) { - vec4 lightDiffuse = lights[i].vLightDiffuse * diffuseColor; - lightingInfo info = computeSpotLighting(viewDirectionW, vNormal, lights[i].vLightData, lights[i].vLightDirection, lightDiffuse.rgb, lights[i].vLightSpecular.rgb, lightDiffuse.a, glossiness); - aggInfo.diffuse += info.diffuse; +#ifdef CLUSTLIGHTSUPPORTED +lightingInfo computeClusteredLighting(vec3 viewDirectionW, vec3 vNormal, vec4 lightData, ClusteredLight lights[32], vec4 diffuseScale, vec3 specularScale, float glossiness) { + lightingInfo result; + int len = int(lightData.x); + + // TODO: Dynamic for loops aren't supported on WebGL 1 + for (int i = 0; i < len; i += 1) { + vec4 diffuse = lights[i].diffuse * diffuseScale; + vec3 specular = lights[i].specular.rgb * specularScale; + lightingInfo info = computeSpotLighting(viewDirectionW, vNormal, lights[i].position, lights[i].direction, diffuse.rgb, specular, diffuse.a, glossiness); + result.diffuse += info.diffuse; #ifdef SPECULARTERM - aggInfo.specular += info.specular; + result.specular += info.specular; #endif } - return aggInfo; + return result; } +#endif From 40eec1a69ac74e606dd5b735f7df9417fd62512c Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Wed, 9 Jul 2025 06:33:44 +1000 Subject: [PATCH 03/30] Detect how many supported lights there are --- .../dev/core/src/Lights/clusteredLight.ts | 48 ++++++++++++------- .../Shaders/ShadersInclude/lightFragment.fx | 2 +- .../ShadersInclude/lightUboDeclaration.fx | 2 +- .../ShadersInclude/lightVxUboDeclaration.fx | 2 +- .../ShadersInclude/lightsFragmentFunctions.fx | 5 +- 5 files changed, 37 insertions(+), 22 deletions(-) diff --git a/packages/dev/core/src/Lights/clusteredLight.ts b/packages/dev/core/src/Lights/clusteredLight.ts index 5dccff92e42..8a56b2c18bd 100644 --- a/packages/dev/core/src/Lights/clusteredLight.ts +++ b/packages/dev/core/src/Lights/clusteredLight.ts @@ -9,23 +9,31 @@ import { Light } from "./light"; import { PointLight } from "./pointLight"; import { SpotLight } from "./spotLight"; -const MAX_CLUSTERED_LIGHTS = 32; - export class ClusteredLight extends Light { - private static _IsEngineSupported(engine: AbstractEngine): boolean { - if (engine.isWebGPU) { - return true; - } else if (engine instanceof ThinEngine && engine.version > 1) { + private static _GetEngineMaxLights(engine: AbstractEngine): number { + const caps = engine._caps; + if (!engine.supportsUniformBuffers) { + return 0; + } else if (engine.isWebGPU) { + // On WebGPU we use atomic writes to storage textures + return 32; + } else if (engine instanceof ThinEngine && engine._webGLVersion > 1) { // On WebGL 2 we use additive float blending as the light mask - return engine._caps.colorBufferFloat && engine._caps.blendFloat; + if (!caps.colorBufferFloat || !caps.blendFloat) { + return 0; + } + // Due to the use of floats we want to limit lights to the precision of floats + const gl = engine._gl; + const format = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, caps.highPrecisionShaderSupported ? gl.HIGH_FLOAT : gl.MEDIUM_FLOAT); + return format?.precision ?? 0; } else { // WebGL 1 is not supported due to lack of dynamic for loops - return false; + return 0; } } public static IsLightSupported(light: Light): boolean { - if (!ClusteredLight._IsEngineSupported(light.getEngine())) { + if (ClusteredLight._GetEngineMaxLights(light.getEngine()) === 0) { return false; } else if (light.shadowEnabled && light._scene.shadowsEnabled && light.getShadowGenerators()) { // Shadows are not supported @@ -46,14 +54,19 @@ export class ClusteredLight extends Light { return this._lights; } + public readonly maxLights: number; + public get isSupported(): boolean { - return ClusteredLight._IsEngineSupported(this.getEngine()); + return this.maxLights > 0; } constructor(name: string, lights: Light[] = [], scene?: Scene) { super(name, scene); - for (const light of lights) { - this.addLight(light); + this.maxLights = ClusteredLight._GetEngineMaxLights(this.getEngine()); + if (this.maxLights > 0) { + for (const light of lights) { + this.addLight(light); + } } } @@ -69,10 +82,13 @@ export class ClusteredLight extends Light { } protected _buildUniformLayout(): void { + // We can't use `this.maxLights` since this will get called during construction + const maxLights = ClusteredLight._GetEngineMaxLights(this.getEngine()); + this._uniformBuffer.addUniform("vLightData", 4); this._uniformBuffer.addUniform("vLightDiffuse", 4); this._uniformBuffer.addUniform("vLightSpecular", 4); - for (let i = 0; i < MAX_CLUSTERED_LIGHTS; i += 1) { + for (let i = 0; i < maxLights; i += 1) { // These technically don't have to match the field name but also why not const struct = `vLights[${i}].`; this._uniformBuffer.addUniform(struct + "position", 4); @@ -87,7 +103,7 @@ export class ClusteredLight extends Light { } public transferToEffect(effect: Effect, lightIndex: string): Light { - const len = Math.min(this._lights.length, MAX_CLUSTERED_LIGHTS); + const len = Math.min(this._lights.length, this.maxLights); this._uniformBuffer.updateFloat4("vLightData", len, 0, 0, 0, lightIndex); for (let i = 0; i < len; i += 1) { @@ -105,7 +121,7 @@ export class ClusteredLight extends Light { direction = Vector3.Normalize(light.direction); } this._uniformBuffer.updateFloat4(struct + "position", position.x, position.y, position.z, spotLight?.exponent ?? 0, lightIndex); - this._uniformBuffer.updateFloat4(struct + "direction", direction.x, direction.y, direction.z, spotLight?._cosHalfAngle ?? 0, lightIndex); + this._uniformBuffer.updateFloat4(struct + "direction", direction.x, direction.y, direction.z, spotLight?._cosHalfAngle ?? -1, lightIndex); const scaledIntensity = light.getScaledIntensity(); light.diffuse.scaleToRef(scaledIntensity, TmpColors.Color3[0]); @@ -132,6 +148,6 @@ export class ClusteredLight extends Light { public prepareLightSpecificDefines(defines: any, lightIndex: number): void { defines["CLUSTLIGHT" + lightIndex] = true; - defines["CLUSTLIGHTSUPPORTED"] = this.isSupported; + defines["CLUSTLIGHT_MAX"] = this.maxLights; } } diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx index 259cae8dc0e..e2a7a4025c1 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx @@ -205,7 +205,7 @@ vReflectionInfos.y #endif ); - #elif defined(CLUSTLIGHT{X}) && defined(CLUSTLIGHTSUPPORTED) + #elif defined(CLUSTLIGHT{X}) && CLUSTLIGHT_MAX > 0 info = computeClusteredLighting(viewDirectionW, normalW, light{X}.vLightData, light{X}.vLights, diffuse{X}, light{X}.vLightSpecular.rgb, glossiness); #endif #endif diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx index e3cb8daf0e7..0428b1d4704 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx @@ -13,7 +13,7 @@ #elif defined(HEMILIGHT{X}) vec3 vLightGround; #elif defined(CLUSTLIGHT{X}) - ClusteredLight vLights[32]; + ClusteredLight vLights[CLUSTLIGHT_MAX]; #endif #if defined(AREALIGHT{X}) vec4 vLightWidth; diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightVxUboDeclaration.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightVxUboDeclaration.fx index 6da47c15e2b..f02595aee0f 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightVxUboDeclaration.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightVxUboDeclaration.fx @@ -13,7 +13,7 @@ #elif defined(HEMILIGHT{X}) vec3 vLightGround; #elif defined(CLUSTLIGHT{X}) - ClusteredLight vLights[32]; + ClusteredLight vLights[CLUSTLIGHT_MAX]; #endif #if defined(AREALIGHT{X}) vec4 vLightWidth; diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx index 9cf79d20c25..adb48f1c651 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx @@ -180,12 +180,11 @@ lightingInfo computeAreaLighting(sampler2D ltc1, sampler2D ltc2, vec3 viewDirect // End Area Light #endif -#ifdef CLUSTLIGHTSUPPORTED -lightingInfo computeClusteredLighting(vec3 viewDirectionW, vec3 vNormal, vec4 lightData, ClusteredLight lights[32], vec4 diffuseScale, vec3 specularScale, float glossiness) { +#if defined(CLUSTLIGHT_MAX) && CLUSTLIGHT_MAX > 0 +lightingInfo computeClusteredLighting(vec3 viewDirectionW, vec3 vNormal, vec4 lightData, ClusteredLight lights[CLUSTLIGHT_MAX], vec4 diffuseScale, vec3 specularScale, float glossiness) { lightingInfo result; int len = int(lightData.x); - // TODO: Dynamic for loops aren't supported on WebGL 1 for (int i = 0; i < len; i += 1) { vec4 diffuse = lights[i].diffuse * diffuseScale; vec3 specular = lights[i].specular.rgb * specularScale; From b6347be8bb12e92a77c7edc3db5b6f8eb85df8ef Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Wed, 9 Jul 2025 11:08:10 +1000 Subject: [PATCH 04/30] Render lights to render-target --- .../Lights/{ => Clustered}/clusteredLight.ts | 125 ++++++++++++++++-- .../Clustered/clusteredLightSceneComponent.ts | 52 ++++++++ .../dev/core/src/Lights/Clustered/index.ts | 2 + packages/dev/core/src/Lights/index.ts | 2 +- packages/dev/core/src/Lights/light.ts | 2 +- .../dev/core/src/Lights/lightConstants.ts | 2 + packages/dev/core/src/sceneComponent.ts | 2 + 7 files changed, 173 insertions(+), 14 deletions(-) rename packages/dev/core/src/Lights/{ => Clustered}/clusteredLight.ts (53%) create mode 100644 packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts create mode 100644 packages/dev/core/src/Lights/Clustered/index.ts diff --git a/packages/dev/core/src/Lights/clusteredLight.ts b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts similarity index 53% rename from packages/dev/core/src/Lights/clusteredLight.ts rename to packages/dev/core/src/Lights/Clustered/clusteredLight.ts index 8a56b2c18bd..dc227007ada 100644 --- a/packages/dev/core/src/Lights/clusteredLight.ts +++ b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts @@ -1,29 +1,37 @@ +import { Constants } from "core/Engines/constants"; import type { AbstractEngine } from "core/Engines/abstractEngine"; -import { ThinEngine } from "core/Engines/thinEngine"; +import type { ThinEngine } from "core/Engines/thinEngine"; import type { Effect } from "core/Materials/effect"; +import { RenderTargetTexture } from "core/Materials/Textures/renderTargetTexture"; import { Vector3 } from "core/Maths/math.vector"; import { TmpColors } from "core/Maths/math.color"; +import type { Mesh } from "core/Meshes/mesh"; +import { Logger } from "core/Misc/logger"; +import { _WarnImport } from "core/Misc/devTools"; import type { Scene } from "core/scene"; -import { Light } from "./light"; -import { PointLight } from "./pointLight"; -import { SpotLight } from "./spotLight"; +import { Light } from "../light"; +import { LightConstants } from "../lightConstants"; +import { PointLight } from "../pointLight"; +import { SpotLight } from "../spotLight"; + +import { StandardMaterial } from "core/Materials/standardMaterial"; export class ClusteredLight extends Light { private static _GetEngineMaxLights(engine: AbstractEngine): number { const caps = engine._caps; - if (!engine.supportsUniformBuffers) { + if (!engine.supportsUniformBuffers || !caps.texelFetch) { return 0; } else if (engine.isWebGPU) { // On WebGPU we use atomic writes to storage textures return 32; - } else if (engine instanceof ThinEngine && engine._webGLVersion > 1) { + } else if (engine.version > 1) { // On WebGL 2 we use additive float blending as the light mask if (!caps.colorBufferFloat || !caps.blendFloat) { return 0; } // Due to the use of floats we want to limit lights to the precision of floats - const gl = engine._gl; + const gl = (engine)._gl; const format = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, caps.highPrecisionShaderSupported ? gl.HIGH_FLOAT : gl.MEDIUM_FLOAT); return format?.precision ?? 0; } else { @@ -42,14 +50,19 @@ export class ClusteredLight extends Light { return true; } else if (light instanceof SpotLight) { // Extra texture bindings per light are not supported - return !(light.projectionTexture || light.iesProfileTexture); + return !light.projectionTexture && !light.iesProfileTexture; } else { // Currently only point and spot lights are supported return false; } } - private _lights: (PointLight | SpotLight)[] = []; + /** @internal */ + public static _SceneComponentInitialization: (scene: Scene) => Mesh = () => { + throw _WarnImport("ClusteredLightSceneComponent"); + }; + + private readonly _lights: (PointLight | SpotLight)[] = []; public get lights(): readonly Light[] { return this._lights; } @@ -60,13 +73,37 @@ export class ClusteredLight extends Light { return this.maxLights > 0; } + /** @internal */ + public readonly _lightMask: RenderTargetTexture; + private readonly _sphere: Mesh; + private readonly _lightSpheres: Mesh[] = []; + constructor(name: string, lights: Light[] = [], scene?: Scene) { super(name, scene); - this.maxLights = ClusteredLight._GetEngineMaxLights(this.getEngine()); + this._sphere = ClusteredLight._SceneComponentInitialization(this._scene); + + const engine = this.getEngine(); + this.maxLights = ClusteredLight._GetEngineMaxLights(engine); + + // TODO: make size configurable + this._lightMask = new RenderTargetTexture("LightMask", 128, this._scene, { + type: engine.isWebGPU ? Constants.TEXTURETYPE_UNSIGNED_INTEGER : Constants.TEXTURETYPE_FLOAT, + format: Constants.TEXTUREFORMAT_RED, + generateDepthBuffer: true, + generateStencilBuffer: true, + }); + if (this.maxLights > 0) { for (const light of lights) { this.addLight(light); } + + this._lightMask.renderParticles = false; + this._lightMask.renderSprites = false; + this._lightMask.noPrePassRenderer = true; + // Use the default render list + this._lightMask.renderList = null; + this._lightMask.customRenderFunction = this._renderLightMask; } } @@ -74,13 +111,51 @@ export class ClusteredLight extends Light { return "ClusteredLight"; } + // eslint-disable-next-line @typescript-eslint/naming-convention + public override getTypeID(): number { + return LightConstants.LIGHTTYPEID_CLUSTERED; + } + + public override dispose(doNotRecurse?: boolean, disposeMaterialAndTextures?: boolean): void { + super.dispose(doNotRecurse, disposeMaterialAndTextures); + } + public addLight(light: Light): void { - if (ClusteredLight.IsLightSupported(light)) { + if (!ClusteredLight.IsLightSupported(light)) { + Logger.Warn("Attempting to add a light to cluster that does not support clustering"); + } + if (this._lights.length === this.maxLights) { + // Only log this once (hence equals) but add the light anyway + Logger.Warn(`Attempting to add more lights to cluster than what is supported (${this.maxLights})`); + } + if (light instanceof PointLight || light instanceof SpotLight) { + // We already warned the user above if we are unable to add this, + // still try to add supported light types even if their properties aren't supported this._scene.removeLight(light); - this._lights.push(light); + this._lights.push(light); + + const material = new StandardMaterial("LightMaterial"); + material.disableLighting = true; + material.emissiveColor = light.diffuse; + + const sphere = this._sphere.clone(); + sphere.material = material; + sphere.position = light.position; + sphere.scaling = new Vector3(0.1, 0.1, 0.1); + this._scene.removeMesh(sphere); + this._lightSpheres.push(sphere); } } + public override _isReady(): boolean { + for (const sphere of this._lightSpheres) { + if (!sphere.isReady()) { + return false; + } + } + return super._isReady(); + } + protected _buildUniformLayout(): void { // We can't use `this.maxLights` since this will get called during construction const maxLights = ClusteredLight._GetEngineMaxLights(this.getEngine()); @@ -150,4 +225,30 @@ export class ClusteredLight extends Light { defines["CLUSTLIGHT" + lightIndex] = true; defines["CLUSTLIGHT_MAX"] = this.maxLights; } + + private _renderLightMask: RenderTargetTexture["customRenderFunction"] = (opaqueSubMeshes, alphaTestSubMeshes, transparentSubMeshes, depthOnlySubMeshes, beforeTransparents) => { + const engine = this.getEngine(); + // Draw everything as depth only + engine.setColorWrite(false); + for (let i = 0; i < depthOnlySubMeshes.length; i += 1) { + depthOnlySubMeshes.data[i].render(false); + } + for (let i = 0; i < opaqueSubMeshes.length; i += 1) { + opaqueSubMeshes.data[i].render(false); + } + for (let i = 0; i < alphaTestSubMeshes.length; i += 1) { + alphaTestSubMeshes.data[i].render(false); + } + engine.setColorWrite(true); + + // We don't render any transparent meshes for the light mask + // TODO: we should stencil out transparent materials and always enable lights that are over transparent materials + beforeTransparents?.(); + + const len = Math.min(this._lights.length, this.maxLights); + // TODO: instancing + for (let i = 0; i < len; i += 1) { + this._lightSpheres[i].subMeshes[0].render(false); + } + }; } diff --git a/packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts b/packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts new file mode 100644 index 00000000000..2ed3d239b04 --- /dev/null +++ b/packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts @@ -0,0 +1,52 @@ +import { CreateSphere } from "core/Meshes/Builders/sphereBuilder"; +import type { Mesh } from "core/Meshes/mesh"; +import type { Scene } from "core/scene"; +import { type RenderTargetsStageAction, SceneComponentConstants, type ISceneComponent } from "core/sceneComponent"; + +import { ClusteredLight } from "./clusteredLight"; + +import { StandardMaterial } from "core/Materials/standardMaterial"; + +class ClusteredLightSceneComponent implements ISceneComponent { + public name = SceneComponentConstants.NAME_CLUSTEREDLIGHT; + + public scene: Scene; + public readonly sphere: Mesh; + + constructor(scene: Scene) { + this.scene = scene; + this.sphere = CreateSphere("LightSphere", { segments: 8, diameter: 2 }, scene); + scene.removeMesh(this.sphere); + + const material = new StandardMaterial("SphereMaterial", scene); + material.disableLighting = true; + this.sphere.material = material; + } + + public dispose(): void { + this.sphere.dispose(); + } + + public rebuild(): void {} + + public register(): void { + this.scene._gatherRenderTargetsStage.registerStep(SceneComponentConstants.STEP_GATHERRENDERTARGETS_CLUSTEREDLIGHT, this, this._gatherRenderTargets); + } + + private _gatherRenderTargets: RenderTargetsStageAction = (renderTargets) => { + for (const light of this.scene.lights) { + if (light instanceof ClusteredLight && light.isSupported) { + renderTargets.push(light._lightMask); + } + } + }; +} + +ClusteredLight._SceneComponentInitialization = (scene) => { + let component = scene._getComponent(SceneComponentConstants.NAME_CLUSTEREDLIGHT); + if (!component) { + component = new ClusteredLightSceneComponent(scene); + scene._addComponent(component); + } + return component.sphere; +}; diff --git a/packages/dev/core/src/Lights/Clustered/index.ts b/packages/dev/core/src/Lights/Clustered/index.ts new file mode 100644 index 00000000000..58db779168a --- /dev/null +++ b/packages/dev/core/src/Lights/Clustered/index.ts @@ -0,0 +1,2 @@ +export * from "./clusteredLight"; +export * from "./clusteredLightSceneComponent"; diff --git a/packages/dev/core/src/Lights/index.ts b/packages/dev/core/src/Lights/index.ts index be0a3dbee22..b47c3ae5fee 100644 --- a/packages/dev/core/src/Lights/index.ts +++ b/packages/dev/core/src/Lights/index.ts @@ -8,5 +8,5 @@ export * from "./pointLight"; export * from "./spotLight"; export * from "./areaLight"; export * from "./rectAreaLight"; -export * from "./clusteredLight"; +export * from "./Clustered/index"; export * from "./IES/iesLoader"; diff --git a/packages/dev/core/src/Lights/light.ts b/packages/dev/core/src/Lights/light.ts index c1016025513..64671c4961f 100644 --- a/packages/dev/core/src/Lights/light.ts +++ b/packages/dev/core/src/Lights/light.ts @@ -484,7 +484,7 @@ export abstract class Light extends Node implements ISortableLight { */ public override toString(fullDetails?: boolean): string { let ret = "Name: " + this.name; - ret += ", type: " + ["Point", "Directional", "Spot", "Hemispheric"][this.getTypeID()]; + ret += ", type: " + ["Point", "Directional", "Spot", "Hemispheric", "Clustered"][this.getTypeID()]; if (this.animations) { for (let i = 0; i < this.animations.length; i++) { ret += ", animation[0]: " + this.animations[i].toString(fullDetails); diff --git a/packages/dev/core/src/Lights/lightConstants.ts b/packages/dev/core/src/Lights/lightConstants.ts index f4932a5d818..a021613bc9b 100644 --- a/packages/dev/core/src/Lights/lightConstants.ts +++ b/packages/dev/core/src/Lights/lightConstants.ts @@ -91,6 +91,8 @@ export class LightConstants { */ public static readonly LIGHTTYPEID_RECT_AREALIGHT = 4; + public static readonly LIGHTTYPEID_CLUSTERED = 5; + /** * Sort function to order lights for rendering. * @param a First Light object to compare to second. diff --git a/packages/dev/core/src/sceneComponent.ts b/packages/dev/core/src/sceneComponent.ts index cdac46f3dc0..7666a32bde2 100644 --- a/packages/dev/core/src/sceneComponent.ts +++ b/packages/dev/core/src/sceneComponent.ts @@ -39,6 +39,7 @@ export class SceneComponentConstants { public static readonly NAME_AUDIO = "Audio"; public static readonly NAME_FLUIDRENDERER = "FluidRenderer"; public static readonly NAME_IBLCDFGENERATOR = "iblCDFGenerator"; + public static readonly NAME_CLUSTEREDLIGHT = "ClusteredLight"; public static readonly STEP_ISREADYFORMESH_EFFECTLAYER = 0; @@ -92,6 +93,7 @@ export class SceneComponentConstants { public static readonly STEP_GATHERRENDERTARGETS_DEPTHRENDERER = 0; public static readonly STEP_GATHERRENDERTARGETS_GEOMETRYBUFFERRENDERER = 1; public static readonly STEP_GATHERRENDERTARGETS_SHADOWGENERATOR = 2; + public static readonly STEP_GATHERRENDERTARGETS_CLUSTEREDLIGHT = 3; public static readonly STEP_GATHERRENDERTARGETS_POSTPROCESSRENDERPIPELINEMANAGER = 3; public static readonly STEP_GATHERACTIVECAMERARENDERTARGETS_DEPTHRENDERER = 0; From ff310afb57a078d822acca97d453c406c7488aa5 Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Thu, 10 Jul 2025 09:09:11 +1000 Subject: [PATCH 05/30] Tiled clustering --- .../src/Lights/Clustered/clusteredLight.ts | 61 +++++++++++-------- .../src/Lights/Clustered/lightMaskMaterial.ts | 23 +++++++ .../src/Materials/materialHelper.functions.ts | 10 ++- .../Shaders/ShadersInclude/lightFragment.fx | 2 +- .../ShadersInclude/lightUboDeclaration.fx | 3 + .../ShadersInclude/lightsFragmentFunctions.fx | 18 ++++-- .../core/src/Shaders/lightMask.fragment.fx | 9 +++ .../dev/core/src/Shaders/lightMask.vertex.fx | 27 ++++++++ 8 files changed, 117 insertions(+), 36 deletions(-) create mode 100644 packages/dev/core/src/Lights/Clustered/lightMaskMaterial.ts create mode 100644 packages/dev/core/src/Shaders/lightMask.fragment.fx create mode 100644 packages/dev/core/src/Shaders/lightMask.vertex.fx diff --git a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts index dc227007ada..d3b71b26606 100644 --- a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts +++ b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts @@ -4,19 +4,18 @@ import type { ThinEngine } from "core/Engines/thinEngine"; import type { Effect } from "core/Materials/effect"; import { RenderTargetTexture } from "core/Materials/Textures/renderTargetTexture"; import { Vector3 } from "core/Maths/math.vector"; -import { TmpColors } from "core/Maths/math.color"; +import { Color4, TmpColors } from "core/Maths/math.color"; import type { Mesh } from "core/Meshes/mesh"; import { Logger } from "core/Misc/logger"; import { _WarnImport } from "core/Misc/devTools"; import type { Scene } from "core/scene"; +import { LightMaskMaterial } from "./lightMaskMaterial"; import { Light } from "../light"; import { LightConstants } from "../lightConstants"; import { PointLight } from "../pointLight"; import { SpotLight } from "../spotLight"; -import { StandardMaterial } from "core/Materials/standardMaterial"; - export class ClusteredLight extends Light { private static _GetEngineMaxLights(engine: AbstractEngine): number { const caps = engine._caps; @@ -89,8 +88,7 @@ export class ClusteredLight extends Light { this._lightMask = new RenderTargetTexture("LightMask", 128, this._scene, { type: engine.isWebGPU ? Constants.TEXTURETYPE_UNSIGNED_INTEGER : Constants.TEXTURETYPE_FLOAT, format: Constants.TEXTUREFORMAT_RED, - generateDepthBuffer: true, - generateStencilBuffer: true, + samplingMode: Constants.TEXTURE_NEAREST_SAMPLINGMODE, }); if (this.maxLights > 0) { @@ -103,6 +101,7 @@ export class ClusteredLight extends Light { this._lightMask.noPrePassRenderer = true; // Use the default render list this._lightMask.renderList = null; + this._lightMask.clearColor = new Color4(); this._lightMask.customRenderFunction = this._renderLightMask; } } @@ -117,34 +116,28 @@ export class ClusteredLight extends Light { } public override dispose(doNotRecurse?: boolean, disposeMaterialAndTextures?: boolean): void { + for (const light of this._lights) { + light.dispose(doNotRecurse, disposeMaterialAndTextures); + } super.dispose(doNotRecurse, disposeMaterialAndTextures); } public addLight(light: Light): void { if (!ClusteredLight.IsLightSupported(light)) { Logger.Warn("Attempting to add a light to cluster that does not support clustering"); - } - if (this._lights.length === this.maxLights) { + return; + } else if (this._lights.length === this.maxLights) { // Only log this once (hence equals) but add the light anyway Logger.Warn(`Attempting to add more lights to cluster than what is supported (${this.maxLights})`); } - if (light instanceof PointLight || light instanceof SpotLight) { - // We already warned the user above if we are unable to add this, - // still try to add supported light types even if their properties aren't supported - this._scene.removeLight(light); - this._lights.push(light); - - const material = new StandardMaterial("LightMaterial"); - material.disableLighting = true; - material.emissiveColor = light.diffuse; - - const sphere = this._sphere.clone(); - sphere.material = material; - sphere.position = light.position; - sphere.scaling = new Vector3(0.1, 0.1, 0.1); - this._scene.removeMesh(sphere); - this._lightSpheres.push(sphere); - } + this._scene.removeLight(light); + this._lights.push(light); + + const sphere = this._sphere.clone(this._cache); + sphere.material = new LightMaskMaterial(light.name, this, this._lights.length - 1, this._scene); + sphere.parent = light; + this._scene.removeMesh(sphere); + this._lightSpheres.push(sphere); } public override _isReady(): boolean { @@ -178,8 +171,10 @@ export class ClusteredLight extends Light { } public transferToEffect(effect: Effect, lightIndex: string): Light { + const maskSize = this._lightMask.getSize(); + const engine = this.getEngine(); const len = Math.min(this._lights.length, this.maxLights); - this._uniformBuffer.updateFloat4("vLightData", len, 0, 0, 0, lightIndex); + this._uniformBuffer.updateFloat4("vLightData", maskSize.width / engine.getRenderWidth(), maskSize.height / engine.getRenderHeight(), len, 0, lightIndex); for (let i = 0; i < len; i += 1) { const light = this._lights[i]; @@ -216,6 +211,11 @@ export class ClusteredLight extends Light { return this; } + public override transferTexturesToEffect(effect: Effect, lightIndex: string): Light { + effect.setTexture("lightMaskTexture" + lightIndex, this._lightMask); + return this; + } + public transferToNodeMaterialEffect(): Light { // TODO: ???? return this; @@ -242,13 +242,20 @@ export class ClusteredLight extends Light { engine.setColorWrite(true); // We don't render any transparent meshes for the light mask - // TODO: we should stencil out transparent materials and always enable lights that are over transparent materials beforeTransparents?.(); const len = Math.min(this._lights.length, this.maxLights); + const depthWrite = engine.getDepthWrite(); + engine.setDepthWrite(false); + + // Draw the light meshes // TODO: instancing for (let i = 0; i < len; i += 1) { - this._lightSpheres[i].subMeshes[0].render(false); + const sphere = this._lightSpheres[i]; + sphere.subMeshes[0].render(true); } + + engine.setAlphaMode(Constants.ALPHA_DISABLE); + engine.setDepthWrite(depthWrite); }; } diff --git a/packages/dev/core/src/Lights/Clustered/lightMaskMaterial.ts b/packages/dev/core/src/Lights/Clustered/lightMaskMaterial.ts new file mode 100644 index 00000000000..d8a06da2420 --- /dev/null +++ b/packages/dev/core/src/Lights/Clustered/lightMaskMaterial.ts @@ -0,0 +1,23 @@ +import { Constants } from "core/Engines/constants"; +import type { Scene } from "core/scene"; +import { ShaderMaterial } from "core/Materials/shaderMaterial"; + +import type { ClusteredLight } from "./clusteredLight"; + +export class LightMaskMaterial extends ShaderMaterial { + constructor(name: string, light: ClusteredLight, index: number, scene: Scene) { + super(name, scene, "lightMask", { + attributes: ["position"], + uniforms: ["index", "world", "viewProjection"], + uniformBuffers: ["Scene", "Mesh", "Light0"], + defines: ["LIGHT0", "CLUSTLIGHT0", `CLUSTLIGHT_MAX ${light.maxLights}`], + extraInitializationsAsync: async () => { + await Promise.all([import("../../Shaders/lightMask.vertex"), import("../../Shaders/lightMask.fragment")]); + }, + }); + + this.setUniformBuffer("Light0", light._uniformBuffer); + this.setUInt("index", index); + this.alphaMode = Constants.ALPHA_ADD; + } +} diff --git a/packages/dev/core/src/Materials/materialHelper.functions.ts b/packages/dev/core/src/Materials/materialHelper.functions.ts index 19d96e4b0ac..c62c511211b 100644 --- a/packages/dev/core/src/Materials/materialHelper.functions.ts +++ b/packages/dev/core/src/Materials/materialHelper.functions.ts @@ -1074,6 +1074,7 @@ export function PrepareDefinesForCamera(scene: Scene, defines: any): boolean { * @param uniformBuffersList defines an optional list of uniform buffers * @param updateOnlyBuffersList True to only update the uniformBuffersList array * @param iesLightTexture defines if IES texture must be used + * @param lightMaskTexture defines if light mask texture must be used */ export function PrepareUniformsAndSamplersForLight( lightIndex: number, @@ -1082,7 +1083,8 @@ export function PrepareUniformsAndSamplersForLight( projectedLightTexture?: any, uniformBuffersList: Nullable = null, updateOnlyBuffersList = false, - iesLightTexture = false + iesLightTexture = false, + lightMaskTexture = false ) { if (uniformBuffersList) { uniformBuffersList.push("Light" + lightIndex); @@ -1125,6 +1127,9 @@ export function PrepareUniformsAndSamplersForLight( if (iesLightTexture) { samplersList.push("iesLightTexture" + lightIndex); } + if (lightMaskTexture) { + samplersList.push("lightMaskTexture" + lightIndex); + } } /** @@ -1163,7 +1168,8 @@ export function PrepareUniformsAndSamplersList(uniformsListOrOptions: string[] | defines["PROJECTEDLIGHTTEXTURE" + lightIndex], uniformBuffersList, false, - defines["IESLIGHTTEXTURE" + lightIndex] + defines["IESLIGHTTEXTURE" + lightIndex], + defines["CLUSTLIGHT" + lightIndex] ); } diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx index e2a7a4025c1..56814363f95 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx @@ -206,7 +206,7 @@ #endif ); #elif defined(CLUSTLIGHT{X}) && CLUSTLIGHT_MAX > 0 - info = computeClusteredLighting(viewDirectionW, normalW, light{X}.vLightData, light{X}.vLights, diffuse{X}, light{X}.vLightSpecular.rgb, glossiness); + info = computeClusteredLighting(lightMaskTexture{X}, viewDirectionW, normalW, light{X}.vLightData, light{X}.vLights, diffuse{X}.rgb, light{X}.vLightSpecular.rgb, glossiness); #endif #endif diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx index 0428b1d4704..0a548d1e38c 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx @@ -29,6 +29,9 @@ uniform mat4 textureProjectionMatrix{X}; uniform sampler2D projectionLightTexture{X}; #endif +#ifdef CLUSTLIGHT{X} + uniform sampler2D lightMaskTexture{X}; +#endif #ifdef SHADOW{X} #ifdef SHADOWCSM{X} uniform mat4 lightMatrix{X}[SHADOWCSMNUM_CASCADES{X}]; diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx index adb48f1c651..2cdf867d3e1 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx @@ -181,14 +181,20 @@ lightingInfo computeAreaLighting(sampler2D ltc1, sampler2D ltc2, vec3 viewDirect #endif #if defined(CLUSTLIGHT_MAX) && CLUSTLIGHT_MAX > 0 -lightingInfo computeClusteredLighting(vec3 viewDirectionW, vec3 vNormal, vec4 lightData, ClusteredLight lights[CLUSTLIGHT_MAX], vec4 diffuseScale, vec3 specularScale, float glossiness) { +#define inline +lightingInfo computeClusteredLighting(sampler2D lightMask, vec3 viewDirectionW, vec3 vNormal, vec4 lightData, ClusteredLight lights[CLUSTLIGHT_MAX], vec3 diffuseScale, vec3 specularScale, float glossiness) { lightingInfo result; - int len = int(lightData.x); - - for (int i = 0; i < len; i += 1) { - vec4 diffuse = lights[i].diffuse * diffuseScale; + vec4 maskTexel = texelFetch(lightMask, ivec2(gl_FragCoord.xy * lightData.xy), 0); + uint mask = uint(maskTexel.r); + uint len = uint(lightData.z); + + for (uint i = 0u; i < len; i += 1u) { + if ((mask & (1u << i)) == 0u) { + continue; + } + vec3 diffuse = lights[i].diffuse.rgb * diffuseScale; vec3 specular = lights[i].specular.rgb * specularScale; - lightingInfo info = computeSpotLighting(viewDirectionW, vNormal, lights[i].position, lights[i].direction, diffuse.rgb, specular, diffuse.a, glossiness); + lightingInfo info = computeSpotLighting(viewDirectionW, vNormal, lights[i].position, lights[i].direction, diffuse.rgb, specular, lights[i].diffuse.a, glossiness); result.diffuse += info.diffuse; #ifdef SPECULARTERM result.specular += info.specular; diff --git a/packages/dev/core/src/Shaders/lightMask.fragment.fx b/packages/dev/core/src/Shaders/lightMask.fragment.fx new file mode 100644 index 00000000000..427beb5a23e --- /dev/null +++ b/packages/dev/core/src/Shaders/lightMask.fragment.fx @@ -0,0 +1,9 @@ +// Uniforms +uniform highp uint index; + +#include +#include[0..1] + +void main(void) { + gl_FragColor = vec4(1 << index, 0, 0, 1); +} diff --git a/packages/dev/core/src/Shaders/lightMask.vertex.fx b/packages/dev/core/src/Shaders/lightMask.vertex.fx new file mode 100644 index 00000000000..bd57838e6d5 --- /dev/null +++ b/packages/dev/core/src/Shaders/lightMask.vertex.fx @@ -0,0 +1,27 @@ +attribute vec3 position; + +// Uniforms +uniform highp uint index; + +#include +#include + +#include +#include[0..1] + +// TODO: switch default direction to up?? +const vec3 down = vec3(0, -1, 0); + +float acosClamped(float v) { + return acos(clamp(v, 0.0, 1.0)); +} + +void main(void) { + float lightAngle = acosClamped(light0.vLights[index].direction.a); + float posAngle = acosClamped(dot(down, position)); + // We allow some wiggle room equal to the rotation of one slice of the sphere + vec3 vPosition = posAngle - lightAngle < 0.32 ? position : vec3(0); + + vPosition *= light0.vLights[index].diffuse.a; + gl_Position = viewProjection * world * vec4(vPosition, 1); +} From 9664efc018ed3f4a59746dadb24735b8978b1f0b Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Thu, 10 Jul 2025 12:02:57 +1000 Subject: [PATCH 06/30] Expose shader precision via engine --- .../dev/core/src/Engines/engineCapabilities.ts | 2 ++ packages/dev/core/src/Engines/nativeEngine.ts | 1 + packages/dev/core/src/Engines/nullEngine.ts | 1 + packages/dev/core/src/Engines/thinEngine.ts | 14 ++++++++++++++ packages/dev/core/src/Engines/webgpuEngine.ts | 1 + .../core/src/Lights/Clustered/clusteredLight.ts | 5 +---- .../Shaders/ShadersInclude/lightUboDeclaration.fx | 3 ++- 7 files changed, 22 insertions(+), 5 deletions(-) diff --git a/packages/dev/core/src/Engines/engineCapabilities.ts b/packages/dev/core/src/Engines/engineCapabilities.ts index 65004b72fa3..328f4d1fa8b 100644 --- a/packages/dev/core/src/Engines/engineCapabilities.ts +++ b/packages/dev/core/src/Engines/engineCapabilities.ts @@ -27,6 +27,8 @@ export interface EngineCapabilities { maxVertexUniformVectors: number; /** Maximum number of uniforms per fragment shader */ maxFragmentUniformVectors: number; + /** The number of bits that can be accurately represented in shader floats */ + shaderFloatPrecision: number; /** Defines if standard derivatives (dx/dy) are supported */ standardDerivatives: boolean; /** Defines if s3tc texture compression is supported */ diff --git a/packages/dev/core/src/Engines/nativeEngine.ts b/packages/dev/core/src/Engines/nativeEngine.ts index 7e8e86e074a..e2655e655c1 100644 --- a/packages/dev/core/src/Engines/nativeEngine.ts +++ b/packages/dev/core/src/Engines/nativeEngine.ts @@ -286,6 +286,7 @@ export class NativeEngine extends Engine { maxDrawBuffers: 8, maxFragmentUniformVectors: 16, maxVertexUniformVectors: 16, + shaderFloatPrecision: 23, // TODO: is this correct? standardDerivatives: true, astc: null, pvrtc: null, diff --git a/packages/dev/core/src/Engines/nullEngine.ts b/packages/dev/core/src/Engines/nullEngine.ts index 992a5647ed1..4f1bd115a1c 100644 --- a/packages/dev/core/src/Engines/nullEngine.ts +++ b/packages/dev/core/src/Engines/nullEngine.ts @@ -133,6 +133,7 @@ export class NullEngine extends Engine { maxVaryingVectors: 16, maxFragmentUniformVectors: 16, maxVertexUniformVectors: 16, + shaderFloatPrecision: 10, // Minimum precision for mediump floats WebGL 1 standardDerivatives: false, astc: null, pvrtc: null, diff --git a/packages/dev/core/src/Engines/thinEngine.ts b/packages/dev/core/src/Engines/thinEngine.ts index f3e2c4ed63a..a808b2ef1dc 100644 --- a/packages/dev/core/src/Engines/thinEngine.ts +++ b/packages/dev/core/src/Engines/thinEngine.ts @@ -506,6 +506,7 @@ export class ThinEngine extends AbstractEngine { maxVaryingVectors: this._gl.getParameter(this._gl.MAX_VARYING_VECTORS), maxFragmentUniformVectors: this._gl.getParameter(this._gl.MAX_FRAGMENT_UNIFORM_VECTORS), maxVertexUniformVectors: this._gl.getParameter(this._gl.MAX_VERTEX_UNIFORM_VECTORS), + shaderFloatPrecision: 0, parallelShaderCompile: this._gl.getExtension("KHR_parallel_shader_compile") || undefined, standardDerivatives: this._webGLVersion > 1 || this._gl.getExtension("OES_standard_derivatives") !== null, maxAnisotropy: 1, @@ -735,6 +736,19 @@ export class ThinEngine extends AbstractEngine { if (vertexhighp && fragmenthighp) { this._caps.highPrecisionShaderSupported = vertexhighp.precision !== 0 && fragmenthighp.precision !== 0; + this._caps.shaderFloatPrecision = Math.min(vertexhighp.precision, fragmenthighp.precision); + } + // This will check both the capability and the `useHighPrecisionFloats` option + if (!this._shouldUseHighPrecisionShader) { + const vertexmedp = this._gl.getShaderPrecisionFormat(this._gl.VERTEX_SHADER, this._gl.MEDIUM_FLOAT); + const fragmentmedp = this._gl.getShaderPrecisionFormat(this._gl.FRAGMENT_SHADER, this._gl.MEDIUM_FLOAT); + if (vertexmedp && fragmentmedp) { + this._caps.shaderFloatPrecision = Math.min(vertexmedp.precision, fragmentmedp.precision); + } + } + if (this._caps.shaderFloatPrecision < 10) { + // WebGL spec requires mediump precision to atleast be 10 + this._caps.shaderFloatPrecision = 10; } } diff --git a/packages/dev/core/src/Engines/webgpuEngine.ts b/packages/dev/core/src/Engines/webgpuEngine.ts index 16bc7d8d4c5..a6e6da67dc3 100644 --- a/packages/dev/core/src/Engines/webgpuEngine.ts +++ b/packages/dev/core/src/Engines/webgpuEngine.ts @@ -857,6 +857,7 @@ export class WebGPUEngine extends ThinWebGPUEngine { maxVaryingVectors: this._deviceLimits.maxInterStageShaderVariables, maxFragmentUniformVectors: Math.floor(this._deviceLimits.maxUniformBufferBindingSize / 4), maxVertexUniformVectors: Math.floor(this._deviceLimits.maxUniformBufferBindingSize / 4), + shaderFloatPrecision: 23, // WGSL always uses IEEE-754 binary32 floats (which have 23 bits of significand) standardDerivatives: true, astc: (this._deviceEnabledExtensions.indexOf(WebGPUConstants.FeatureName.TextureCompressionASTC) >= 0 ? true : undefined) as any, s3tc: (this._deviceEnabledExtensions.indexOf(WebGPUConstants.FeatureName.TextureCompressionBC) >= 0 ? true : undefined) as any, diff --git a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts index d3b71b26606..91648278ad5 100644 --- a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts +++ b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts @@ -1,6 +1,5 @@ import { Constants } from "core/Engines/constants"; import type { AbstractEngine } from "core/Engines/abstractEngine"; -import type { ThinEngine } from "core/Engines/thinEngine"; import type { Effect } from "core/Materials/effect"; import { RenderTargetTexture } from "core/Materials/Textures/renderTargetTexture"; import { Vector3 } from "core/Maths/math.vector"; @@ -30,9 +29,7 @@ export class ClusteredLight extends Light { return 0; } // Due to the use of floats we want to limit lights to the precision of floats - const gl = (engine)._gl; - const format = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, caps.highPrecisionShaderSupported ? gl.HIGH_FLOAT : gl.MEDIUM_FLOAT); - return format?.precision ?? 0; + return caps.shaderFloatPrecision; } else { // WebGL 1 is not supported due to lack of dynamic for loops return 0; diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx index 0a548d1e38c..11a61ffcc11 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx @@ -30,7 +30,8 @@ uniform sampler2D projectionLightTexture{X}; #endif #ifdef CLUSTLIGHT{X} - uniform sampler2D lightMaskTexture{X}; + // Ensure the mask is sampled with high precision + uniform highp sampler2D lightMaskTexture{X}; #endif #ifdef SHADOW{X} #ifdef SHADOWCSM{X} From 0a4b98f4fcf0b6b7475d14aa7d5620550ef01f2e Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Fri, 11 Jul 2025 11:21:15 +1000 Subject: [PATCH 07/30] Thin instances --- .../src/Lights/Clustered/clusteredLight.ts | 81 ++++++++++++------- .../Clustered/clusteredLightSceneComponent.ts | 22 +---- .../src/Lights/Clustered/lightMaskMaterial.ts | 23 ------ .../Lights/Clustered/lightProxyMaterial.ts | 37 +++++++++ .../core/src/Materials/standardMaterial.ts | 4 +- .../core/src/Shaders/lightMask.fragment.fx | 9 --- .../core/src/Shaders/lightProxy.fragment.fx | 6 ++ ...ghtMask.vertex.fx => lightProxy.vertex.fx} | 25 +++--- 8 files changed, 117 insertions(+), 90 deletions(-) delete mode 100644 packages/dev/core/src/Lights/Clustered/lightMaskMaterial.ts create mode 100644 packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts delete mode 100644 packages/dev/core/src/Shaders/lightMask.fragment.fx create mode 100644 packages/dev/core/src/Shaders/lightProxy.fragment.fx rename packages/dev/core/src/Shaders/{lightMask.vertex.fx => lightProxy.vertex.fx} (50%) diff --git a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts index 91648278ad5..ccefea304df 100644 --- a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts +++ b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts @@ -4,17 +4,21 @@ import type { Effect } from "core/Materials/effect"; import { RenderTargetTexture } from "core/Materials/Textures/renderTargetTexture"; import { Vector3 } from "core/Maths/math.vector"; import { Color4, TmpColors } from "core/Maths/math.color"; +import { CreateSphere } from "core/Meshes/Builders/sphereBuilder"; import type { Mesh } from "core/Meshes/mesh"; import { Logger } from "core/Misc/logger"; import { _WarnImport } from "core/Misc/devTools"; +import { RenderingManager } from "core/Rendering/renderingManager"; import type { Scene } from "core/scene"; -import { LightMaskMaterial } from "./lightMaskMaterial"; +import { LightProxyMaterial } from "./lightProxyMaterial"; import { Light } from "../light"; import { LightConstants } from "../lightConstants"; import { PointLight } from "../pointLight"; import { SpotLight } from "../spotLight"; +import "core/Meshes/thinInstanceMesh"; + export class ClusteredLight extends Light { private static _GetEngineMaxLights(engine: AbstractEngine): number { const caps = engine._caps; @@ -54,7 +58,7 @@ export class ClusteredLight extends Light { } /** @internal */ - public static _SceneComponentInitialization: (scene: Scene) => Mesh = () => { + public static _SceneComponentInitialization: (scene: Scene) => void = () => { throw _WarnImport("ClusteredLightSceneComponent"); }; @@ -71,12 +75,11 @@ export class ClusteredLight extends Light { /** @internal */ public readonly _lightMask: RenderTargetTexture; - private readonly _sphere: Mesh; - private readonly _lightSpheres: Mesh[] = []; + private readonly _lightProxy: Mesh; + private readonly _matrixBuffer: Float32Array; constructor(name: string, lights: Light[] = [], scene?: Scene) { super(name, scene); - this._sphere = ClusteredLight._SceneComponentInitialization(this._scene); const engine = this.getEngine(); this.maxLights = ClusteredLight._GetEngineMaxLights(engine); @@ -87,11 +90,12 @@ export class ClusteredLight extends Light { format: Constants.TEXTUREFORMAT_RED, samplingMode: Constants.TEXTURE_NEAREST_SAMPLINGMODE, }); + this._lightProxy = CreateSphere("LightProxy", { diameter: 2, segments: 8 }, scene); + this._lightProxy.isVisible = false; + this._matrixBuffer = new Float32Array(this.maxLights * 16); if (this.maxLights > 0) { - for (const light of lights) { - this.addLight(light); - } + ClusteredLight._SceneComponentInitialization(this._scene); this._lightMask.renderParticles = false; this._lightMask.renderSprites = false; @@ -99,7 +103,20 @@ export class ClusteredLight extends Light { // Use the default render list this._lightMask.renderList = null; this._lightMask.clearColor = new Color4(); - this._lightMask.customRenderFunction = this._renderLightMask; + this._lightMask.customRenderFunction = this._renderLightMaskGroup; + this._lightMask.onAfterRenderObservable.add(this._afterRenderLightMask); + + // Prevent clearing between render groups + for (let i = RenderingManager.MIN_RENDERINGGROUPS; i < RenderingManager.MAX_RENDERINGGROUPS; i += 1) { + this._lightMask.setRenderingAutoClearDepthStencil(i, false); + } + + this._lightProxy.material = new LightProxyMaterial("LightMaterial", this); + this._lightProxy.thinInstanceSetBuffer("matrix", this._matrixBuffer, 16, false); + + for (const light of lights) { + this.addLight(light); + } } } @@ -116,6 +133,7 @@ export class ClusteredLight extends Light { for (const light of this._lights) { light.dispose(doNotRecurse, disposeMaterialAndTextures); } + this._lightProxy.dispose(doNotRecurse, disposeMaterialAndTextures); super.dispose(doNotRecurse, disposeMaterialAndTextures); } @@ -129,21 +147,10 @@ export class ClusteredLight extends Light { } this._scene.removeLight(light); this._lights.push(light); - - const sphere = this._sphere.clone(this._cache); - sphere.material = new LightMaskMaterial(light.name, this, this._lights.length - 1, this._scene); - sphere.parent = light; - this._scene.removeMesh(sphere); - this._lightSpheres.push(sphere); } public override _isReady(): boolean { - for (const sphere of this._lightSpheres) { - if (!sphere.isReady()) { - return false; - } - } - return super._isReady(); + return this._lightProxy.isReady(true, true); } protected _buildUniformLayout(): void { @@ -223,13 +230,20 @@ export class ClusteredLight extends Light { defines["CLUSTLIGHT_MAX"] = this.maxLights; } - private _renderLightMask: RenderTargetTexture["customRenderFunction"] = (opaqueSubMeshes, alphaTestSubMeshes, transparentSubMeshes, depthOnlySubMeshes, beforeTransparents) => { + private _renderLightMaskGroup: RenderTargetTexture["customRenderFunction"] = ( + opaqueSubMeshes, + alphaTestSubMeshes, + transparentSubMeshes, + depthOnlySubMeshes, + beforeTransparents + ) => { const engine = this.getEngine(); // Draw everything as depth only engine.setColorWrite(false); for (let i = 0; i < depthOnlySubMeshes.length; i += 1) { depthOnlySubMeshes.data[i].render(false); } + // TODO: skip meshes that were already drawn during `depthOnly` for (let i = 0; i < opaqueSubMeshes.length; i += 1) { opaqueSubMeshes.data[i].render(false); } @@ -240,18 +254,29 @@ export class ClusteredLight extends Light { // We don't render any transparent meshes for the light mask beforeTransparents?.(); + }; + private _afterRenderLightMask = () => { const len = Math.min(this._lights.length, this.maxLights); - const depthWrite = engine.getDepthWrite(); - engine.setDepthWrite(false); + if (len === 0) { + // Theres no lights to render + return; + } - // Draw the light meshes - // TODO: instancing + this._lightProxy.thinInstanceCount = len; for (let i = 0; i < len; i += 1) { - const sphere = this._lightSpheres[i]; - sphere.subMeshes[0].render(true); + const light = this._lights[i]; + // TODO: cache matrices, somehow detect unchanged? + // TODO: scale by range of light + // TODO: rotate spotlights to face direction + light.getWorldMatrix().copyToArray(this._matrixBuffer, i * 16); } + this._lightProxy.thinInstanceBufferUpdated("matrix"); + const engine = this.getEngine(); + const depthWrite = engine.getDepthWrite(); + engine.setDepthWrite(false); + this._lightProxy.render(this._lightProxy.subMeshes[0], true); engine.setAlphaMode(Constants.ALPHA_DISABLE); engine.setDepthWrite(depthWrite); }; diff --git a/packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts b/packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts index 2ed3d239b04..f5b84e63c85 100644 --- a/packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts +++ b/packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts @@ -1,31 +1,18 @@ -import { CreateSphere } from "core/Meshes/Builders/sphereBuilder"; -import type { Mesh } from "core/Meshes/mesh"; import type { Scene } from "core/scene"; import { type RenderTargetsStageAction, SceneComponentConstants, type ISceneComponent } from "core/sceneComponent"; import { ClusteredLight } from "./clusteredLight"; -import { StandardMaterial } from "core/Materials/standardMaterial"; - class ClusteredLightSceneComponent implements ISceneComponent { public name = SceneComponentConstants.NAME_CLUSTEREDLIGHT; public scene: Scene; - public readonly sphere: Mesh; constructor(scene: Scene) { this.scene = scene; - this.sphere = CreateSphere("LightSphere", { segments: 8, diameter: 2 }, scene); - scene.removeMesh(this.sphere); - - const material = new StandardMaterial("SphereMaterial", scene); - material.disableLighting = true; - this.sphere.material = material; } - public dispose(): void { - this.sphere.dispose(); - } + public dispose(): void {} public rebuild(): void {} @@ -43,10 +30,7 @@ class ClusteredLightSceneComponent implements ISceneComponent { } ClusteredLight._SceneComponentInitialization = (scene) => { - let component = scene._getComponent(SceneComponentConstants.NAME_CLUSTEREDLIGHT); - if (!component) { - component = new ClusteredLightSceneComponent(scene); - scene._addComponent(component); + if (!scene._getComponent(SceneComponentConstants.NAME_CLUSTEREDLIGHT)) { + scene._addComponent(new ClusteredLightSceneComponent(scene)); } - return component.sphere; }; diff --git a/packages/dev/core/src/Lights/Clustered/lightMaskMaterial.ts b/packages/dev/core/src/Lights/Clustered/lightMaskMaterial.ts deleted file mode 100644 index d8a06da2420..00000000000 --- a/packages/dev/core/src/Lights/Clustered/lightMaskMaterial.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Constants } from "core/Engines/constants"; -import type { Scene } from "core/scene"; -import { ShaderMaterial } from "core/Materials/shaderMaterial"; - -import type { ClusteredLight } from "./clusteredLight"; - -export class LightMaskMaterial extends ShaderMaterial { - constructor(name: string, light: ClusteredLight, index: number, scene: Scene) { - super(name, scene, "lightMask", { - attributes: ["position"], - uniforms: ["index", "world", "viewProjection"], - uniformBuffers: ["Scene", "Mesh", "Light0"], - defines: ["LIGHT0", "CLUSTLIGHT0", `CLUSTLIGHT_MAX ${light.maxLights}`], - extraInitializationsAsync: async () => { - await Promise.all([import("../../Shaders/lightMask.vertex"), import("../../Shaders/lightMask.fragment")]); - }, - }); - - this.setUniformBuffer("Light0", light._uniformBuffer); - this.setUInt("index", index); - this.alphaMode = Constants.ALPHA_ADD; - } -} diff --git a/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts b/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts new file mode 100644 index 00000000000..824d5268fe5 --- /dev/null +++ b/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts @@ -0,0 +1,37 @@ +import { Constants } from "core/Engines/constants"; +import type { Effect } from "core/Materials/effect"; +import { ShaderMaterial } from "core/Materials/shaderMaterial"; +import type { Matrix } from "core/Maths/math.vector"; +import type { Mesh } from "core/Meshes/mesh"; +import type { SubMesh } from "core/Meshes/subMesh"; +import type { Nullable } from "core/types"; + +import type { ClusteredLight } from "./clusteredLight"; + +async function InitializeLightProxy(): Promise { + await Promise.all([import("../../Shaders/lightProxy.vertex"), import("../../Shaders/lightProxy.fragment")]); +} + +export class LightProxyMaterial extends ShaderMaterial { + private readonly _clusteredLight: ClusteredLight; + + constructor(name: string, clusteredLight: ClusteredLight) { + super(name, clusteredLight._scene, "lightProxy", { + attributes: ["position"], + uniforms: ["world"], + uniformBuffers: ["Scene", "Mesh", "Light0"], + defines: ["LIGHT0", "CLUSTLIGHT0", `CLUSTLIGHT_MAX ${clusteredLight.maxLights}`], + extraInitializationsAsync: InitializeLightProxy, + }); + + this._clusteredLight = clusteredLight; + this.alphaMode = Constants.ALPHA_ADD; + } + + public override bindForSubMesh(world: Matrix, mesh: Mesh, subMesh: SubMesh): void { + if (subMesh.effect) { + this._clusteredLight._bindLight(0, this.getScene(), subMesh.effect, false, false); + } + super.bindForSubMesh(world, mesh, subMesh); + } +} diff --git a/packages/dev/core/src/Materials/standardMaterial.ts b/packages/dev/core/src/Materials/standardMaterial.ts index ccc8f1d952d..93f1f6a74ab 100644 --- a/packages/dev/core/src/Materials/standardMaterial.ts +++ b/packages/dev/core/src/Materials/standardMaterial.ts @@ -1229,8 +1229,8 @@ export class StandardMaterial extends PushMaterial { } } - // Check if Area Lights have LTC texture. - if (defines["AREALIGHTUSED"]) { + // Check if lights are ready + if (defines["AREALIGHTUSED"] || defines["CLUSTLIGHT_MAX"]) { for (let index = 0; index < mesh.lightSources.length; index++) { if (!mesh.lightSources[index]._isReady()) { return false; diff --git a/packages/dev/core/src/Shaders/lightMask.fragment.fx b/packages/dev/core/src/Shaders/lightMask.fragment.fx deleted file mode 100644 index 427beb5a23e..00000000000 --- a/packages/dev/core/src/Shaders/lightMask.fragment.fx +++ /dev/null @@ -1,9 +0,0 @@ -// Uniforms -uniform highp uint index; - -#include -#include[0..1] - -void main(void) { - gl_FragColor = vec4(1 << index, 0, 0, 1); -} diff --git a/packages/dev/core/src/Shaders/lightProxy.fragment.fx b/packages/dev/core/src/Shaders/lightProxy.fragment.fx new file mode 100644 index 00000000000..94a14fcc3e4 --- /dev/null +++ b/packages/dev/core/src/Shaders/lightProxy.fragment.fx @@ -0,0 +1,6 @@ +// Input +flat varying highp uint vMask; + +void main(void) { + gl_FragColor = vec4(vMask, 0, 0, 1); +} diff --git a/packages/dev/core/src/Shaders/lightMask.vertex.fx b/packages/dev/core/src/Shaders/lightProxy.vertex.fx similarity index 50% rename from packages/dev/core/src/Shaders/lightMask.vertex.fx rename to packages/dev/core/src/Shaders/lightProxy.vertex.fx index bd57838e6d5..241ec3c3f9c 100644 --- a/packages/dev/core/src/Shaders/lightMask.vertex.fx +++ b/packages/dev/core/src/Shaders/lightProxy.vertex.fx @@ -1,14 +1,17 @@ -attribute vec3 position; - -// Uniforms -uniform highp uint index; - +// Uniform Buffers #include #include #include #include[0..1] +// Attributes +attribute vec3 position; +#include + +// Output +flat varying highp uint vMask; + // TODO: switch default direction to up?? const vec3 down = vec3(0, -1, 0); @@ -17,11 +20,15 @@ float acosClamped(float v) { } void main(void) { - float lightAngle = acosClamped(light0.vLights[index].direction.a); + float lightAngle = acosClamped(light0.vLights[gl_InstanceID].direction.a); float posAngle = acosClamped(dot(down, position)); + // We allow some wiggle room equal to the rotation of one slice of the sphere - vec3 vPosition = posAngle - lightAngle < 0.32 ? position : vec3(0); + vec3 positionUpdated = posAngle - lightAngle < 0.32 ? position : vec3(0); + positionUpdated *= light0.vLights[gl_InstanceID].diffuse.a; + +#include - vPosition *= light0.vLights[index].diffuse.a; - gl_Position = viewProjection * world * vec4(vPosition, 1); + gl_Position = viewProjection * finalWorld * vec4(positionUpdated, 1); + vMask = 1u << gl_InstanceID; } From 3ab951b69c5ee9327991501a3227eae9190f2da7 Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Tue, 15 Jul 2025 06:53:46 +1000 Subject: [PATCH 08/30] Make light mask configurable --- .../src/Lights/Clustered/clusteredLight.ts | 74 ++++++++++++------- .../Clustered/clusteredLightSceneComponent.ts | 2 +- .../ShadersInclude/lightsFragmentFunctions.fx | 4 +- 3 files changed, 49 insertions(+), 31 deletions(-) diff --git a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts index ccefea304df..c27668a3174 100644 --- a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts +++ b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts @@ -10,6 +10,7 @@ import { Logger } from "core/Misc/logger"; import { _WarnImport } from "core/Misc/devTools"; import { RenderingManager } from "core/Rendering/renderingManager"; import type { Scene } from "core/scene"; +import type { Nullable } from "core/types"; import { LightProxyMaterial } from "./lightProxyMaterial"; import { Light } from "../light"; @@ -62,19 +63,21 @@ export class ClusteredLight extends Light { throw _WarnImport("ClusteredLightSceneComponent"); }; - private readonly _lights: (PointLight | SpotLight)[] = []; - public get lights(): readonly Light[] { - return this._lights; - } - public readonly maxLights: number; public get isSupported(): boolean { return this.maxLights > 0; } - /** @internal */ - public readonly _lightMask: RenderTargetTexture; + private readonly _lights: (PointLight | SpotLight)[] = []; + public get lights(): readonly Light[] { + return this._lights; + } + + public tileWidth = 128; + public tileHeight = 128; + + private _lightMask: Nullable; private readonly _lightProxy: Mesh; private readonly _matrixBuffer: Float32Array; @@ -84,12 +87,6 @@ export class ClusteredLight extends Light { const engine = this.getEngine(); this.maxLights = ClusteredLight._GetEngineMaxLights(engine); - // TODO: make size configurable - this._lightMask = new RenderTargetTexture("LightMask", 128, this._scene, { - type: engine.isWebGPU ? Constants.TEXTURETYPE_UNSIGNED_INTEGER : Constants.TEXTURETYPE_FLOAT, - format: Constants.TEXTUREFORMAT_RED, - samplingMode: Constants.TEXTURE_NEAREST_SAMPLINGMODE, - }); this._lightProxy = CreateSphere("LightProxy", { diameter: 2, segments: 8 }, scene); this._lightProxy.isVisible = false; this._matrixBuffer = new Float32Array(this.maxLights * 16); @@ -97,20 +94,6 @@ export class ClusteredLight extends Light { if (this.maxLights > 0) { ClusteredLight._SceneComponentInitialization(this._scene); - this._lightMask.renderParticles = false; - this._lightMask.renderSprites = false; - this._lightMask.noPrePassRenderer = true; - // Use the default render list - this._lightMask.renderList = null; - this._lightMask.clearColor = new Color4(); - this._lightMask.customRenderFunction = this._renderLightMaskGroup; - this._lightMask.onAfterRenderObservable.add(this._afterRenderLightMask); - - // Prevent clearing between render groups - for (let i = RenderingManager.MIN_RENDERINGGROUPS; i < RenderingManager.MAX_RENDERINGGROUPS; i += 1) { - this._lightMask.setRenderingAutoClearDepthStencil(i, false); - } - this._lightProxy.material = new LightProxyMaterial("LightMaterial", this); this._lightProxy.thinInstanceSetBuffer("matrix", this._matrixBuffer, 16, false); @@ -133,6 +116,7 @@ export class ClusteredLight extends Light { for (const light of this._lights) { light.dispose(doNotRecurse, disposeMaterialAndTextures); } + this._lightMask?.dispose(); this._lightProxy.dispose(doNotRecurse, disposeMaterialAndTextures); super.dispose(doNotRecurse, disposeMaterialAndTextures); } @@ -149,6 +133,40 @@ export class ClusteredLight extends Light { this._lights.push(light); } + /** @internal */ + public _getLightMask(): RenderTargetTexture { + const engine = this.getEngine(); + // const width = Math.ceil(engine.getRenderWidth(true) / this.tileWidth); + // const height = Math.ceil(engine.getRenderHeight(true) / this.tileHeight); + const width = 16; + const height = 16; + if (this._lightMask && this._lightMask.getRenderWidth() === width && this._lightMask.getRenderHeight() === height) { + return this._lightMask; + } + + this._lightMask?.dispose(); + this._lightMask = new RenderTargetTexture("LightMask", { width, height }, this._scene, { + type: engine.isWebGPU ? Constants.TEXTURETYPE_UNSIGNED_INTEGER : Constants.TEXTURETYPE_FLOAT, + format: Constants.TEXTUREFORMAT_RED, + samplingMode: Constants.TEXTURE_NEAREST_SAMPLINGMODE, + }); + this._lightMask.renderParticles = false; + this._lightMask.renderSprites = false; + this._lightMask.noPrePassRenderer = true; + // Use the default render list + this._lightMask.renderList = null; + this._lightMask.clearColor = new Color4(); + + this._lightMask.customRenderFunction = this._renderLightMaskGroup; + this._lightMask.onAfterRenderObservable.add(this._afterRenderLightMask); + + // Prevent clearing between render groups + for (let i = RenderingManager.MIN_RENDERINGGROUPS; i < RenderingManager.MAX_RENDERINGGROUPS; i += 1) { + this._lightMask.setRenderingAutoClearDepthStencil(i, false); + } + return this._lightMask; + } + public override _isReady(): boolean { return this._lightProxy.isReady(true, true); } @@ -175,7 +193,7 @@ export class ClusteredLight extends Light { } public transferToEffect(effect: Effect, lightIndex: string): Light { - const maskSize = this._lightMask.getSize(); + const maskSize = this._lightMask?.getSize() ?? { width: 0, height: 0 }; const engine = this.getEngine(); const len = Math.min(this._lights.length, this.maxLights); this._uniformBuffer.updateFloat4("vLightData", maskSize.width / engine.getRenderWidth(), maskSize.height / engine.getRenderHeight(), len, 0, lightIndex); diff --git a/packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts b/packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts index f5b84e63c85..a23eb683768 100644 --- a/packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts +++ b/packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts @@ -23,7 +23,7 @@ class ClusteredLightSceneComponent implements ISceneComponent { private _gatherRenderTargets: RenderTargetsStageAction = (renderTargets) => { for (const light of this.scene.lights) { if (light instanceof ClusteredLight && light.isSupported) { - renderTargets.push(light._lightMask); + renderTargets.push(light._getLightMask()); } } }; diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx index 2cdf867d3e1..353b40203c2 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx @@ -186,9 +186,9 @@ lightingInfo computeClusteredLighting(sampler2D lightMask, vec3 viewDirectionW, lightingInfo result; vec4 maskTexel = texelFetch(lightMask, ivec2(gl_FragCoord.xy * lightData.xy), 0); uint mask = uint(maskTexel.r); - uint len = uint(lightData.z); + int len = int(lightData.z); - for (uint i = 0u; i < len; i += 1u) { + for (int i = 0; i < len; i += 1) { if ((mask & (1u << i)) == 0u) { continue; } From 458b5a14d7cbf97102dbc952879fadecd14c1f85 Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Wed, 16 Jul 2025 11:12:48 +1000 Subject: [PATCH 09/30] WebGPU support --- .../dev/core/src/Buffers/storageBuffer.ts | 4 + .../WebGPU/webgpuShaderProcessorsWGSL.ts | 3 +- packages/dev/core/src/Engines/webgpuEngine.ts | 4 + .../src/Lights/Clustered/clusteredLight.ts | 195 ++++++++++-------- .../Clustered/clusteredLightSceneComponent.ts | 10 +- .../Lights/Clustered/lightProxyMaterial.ts | 17 +- .../ShadersInclude/clusteredLightFunctions.fx | 13 ++ .../ShadersInclude/lightFragment.fx | 2 + .../ShadersInclude/lightUboDeclaration.fx | 12 ++ .../ShadersInclude/lightVxUboDeclaration.fx | 2 + .../ShadersInclude/lightsFragmentFunctions.fx | 35 +++- .../core/src/ShadersWGSL/default.fragment.fx | 1 + .../core/src/ShadersWGSL/default.vertex.fx | 1 + .../src/ShadersWGSL/lightProxy.fragment.fx | 20 ++ .../core/src/ShadersWGSL/lightProxy.vertex.fx | 35 ++++ packages/dev/core/src/sceneComponent.ts | 2 +- 16 files changed, 259 insertions(+), 97 deletions(-) create mode 100644 packages/dev/core/src/ShadersWGSL/ShadersInclude/clusteredLightFunctions.fx create mode 100644 packages/dev/core/src/ShadersWGSL/lightProxy.fragment.fx create mode 100644 packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx diff --git a/packages/dev/core/src/Buffers/storageBuffer.ts b/packages/dev/core/src/Buffers/storageBuffer.ts index 2bfa08fab23..727eb576bdb 100644 --- a/packages/dev/core/src/Buffers/storageBuffer.ts +++ b/packages/dev/core/src/Buffers/storageBuffer.ts @@ -46,6 +46,10 @@ export class StorageBuffer { return this._buffer; } + public clear(byteOffset?: number, byteLength?: number): void { + this._engine.clearStorageBuffer(this._buffer, byteOffset, byteLength); + } + /** * Updates the storage buffer * @param data the data used to update the storage buffer diff --git a/packages/dev/core/src/Engines/WebGPU/webgpuShaderProcessorsWGSL.ts b/packages/dev/core/src/Engines/WebGPU/webgpuShaderProcessorsWGSL.ts index ebe472beec1..7c62b93c1bb 100644 --- a/packages/dev/core/src/Engines/WebGPU/webgpuShaderProcessorsWGSL.ts +++ b/packages/dev/core/src/Engines/WebGPU/webgpuShaderProcessorsWGSL.ts @@ -335,7 +335,8 @@ export class WebGPUShaderProcessorWGSL extends WebGPUShaderProcessor { } public finalizeShaders(vertexCode: string, fragmentCode: string): { vertexCode: string; fragmentCode: string } { - const enabledExtensions: string[] = []; + // TODO: conditionally enable if needed AND supported + const enabledExtensions: string[] = ["subgroups"]; const fragCoordCode = fragmentCode.indexOf("fragmentInputs.position") >= 0 && !this.pureMode diff --git a/packages/dev/core/src/Engines/webgpuEngine.ts b/packages/dev/core/src/Engines/webgpuEngine.ts index a6e6da67dc3..3c104a4e646 100644 --- a/packages/dev/core/src/Engines/webgpuEngine.ts +++ b/packages/dev/core/src/Engines/webgpuEngine.ts @@ -3903,6 +3903,10 @@ export class WebGPUEngine extends ThinWebGPUEngine { return this._createBuffer(data, creationFlags | Constants.BUFFER_CREATIONFLAG_STORAGE, label); } + public clearStorageBuffer(storageBuffer: DataBuffer, byteOffset?: number, byteLength?: number): void { + this._renderEncoder.clearBuffer(storageBuffer.underlyingResource, byteOffset, byteLength); + } + /** * Updates a storage buffer * @param buffer the storage buffer to update diff --git a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts index c27668a3174..483b3e91754 100644 --- a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts +++ b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts @@ -1,14 +1,16 @@ -import { Constants } from "core/Engines/constants"; +import { StorageBuffer } from "core/Buffers/storageBuffer"; import type { AbstractEngine } from "core/Engines/abstractEngine"; +import type { WebGPUEngine } from "core/Engines/webgpuEngine"; import type { Effect } from "core/Materials/effect"; import { RenderTargetTexture } from "core/Materials/Textures/renderTargetTexture"; +import { TmpColors } from "core/Maths/math.color"; import { Vector3 } from "core/Maths/math.vector"; -import { Color4, TmpColors } from "core/Maths/math.color"; import { CreateSphere } from "core/Meshes/Builders/sphereBuilder"; import type { Mesh } from "core/Meshes/mesh"; -import { Logger } from "core/Misc/logger"; import { _WarnImport } from "core/Misc/devTools"; -import { RenderingManager } from "core/Rendering/renderingManager"; +import { Logger } from "core/Misc/logger"; +import type { Observer } from "core/Misc/observable"; +import { CeilingPOT } from "core/Misc/tools.functions"; import type { Scene } from "core/scene"; import type { Nullable } from "core/types"; @@ -74,29 +76,94 @@ export class ClusteredLight extends Light { return this._lights; } - public tileWidth = 128; - public tileHeight = 128; + /** @internal */ + public readonly _tileMaskTarget: RenderTargetTexture; + private _tileMaskBuffer: Nullable; + private _tileMaskStride = 0; + private readonly _resizeObserver: Observer; + + private _tileWidth = 128; + public get tileWidth(): number { + return this._tileWidth; + } + + public set tileWidth(width: number) { + if (this._tileWidth === width) { + return; + } + this._tileWidth = width; + this._tileMaskBuffer?.dispose(); + this._tileMaskBuffer = null; + } + + private _tileHeight = 128; + public get tileHeight(): number { + return this._tileHeight; + } + + public set tileHeight(height: number) { + if (this._tileHeight === height) { + return; + } + this._tileHeight = height; + this._tileMaskBuffer?.dispose(); + this._tileMaskBuffer = null; + } - private _lightMask: Nullable; - private readonly _lightProxy: Mesh; - private readonly _matrixBuffer: Float32Array; + private readonly _proxyMesh: Mesh; + private readonly _proxyMatrixBuffer: Float32Array; constructor(name: string, lights: Light[] = [], scene?: Scene) { super(name, scene); const engine = this.getEngine(); this.maxLights = ClusteredLight._GetEngineMaxLights(engine); + const getRenderSize = () => ({ + width: engine.getRenderWidth(true), + height: engine.getRenderHeight(true), + }); - this._lightProxy = CreateSphere("LightProxy", { diameter: 2, segments: 8 }, scene); - this._lightProxy.isVisible = false; - this._matrixBuffer = new Float32Array(this.maxLights * 16); + this._tileMaskTarget = new RenderTargetTexture("TileMask", getRenderSize(), this._scene, { + generateDepthBuffer: true, + generateStencilBuffer: true, + noColorAttachment: true, + }); + this._tileMaskTarget.renderList = null; + this._tileMaskTarget.renderParticles = false; + this._tileMaskTarget.renderSprites = false; + this._tileMaskTarget.noPrePassRenderer = true; + // Use the default render list + this._tileMaskTarget.renderList = null; + this._tileMaskTarget.customRenderFunction = this._renderTileMask; + + this._tileMaskTarget.onClearObservable.add(() => { + // If its already created it should be the correct size + const buffer = this._tileMaskBuffer ?? this._createTileMaskBuffer(); + buffer.clear(); + engine.clear(null, false, true, true); + }); - if (this.maxLights > 0) { - ClusteredLight._SceneComponentInitialization(this._scene); + // This forces materials to run as a depth prepass + this._tileMaskTarget.onBeforeRenderObservable.add(() => engine.setColorWrite(false)); + this._tileMaskTarget.onAfterRenderObservable.add(() => engine.setColorWrite(true)); - this._lightProxy.material = new LightProxyMaterial("LightMaterial", this); - this._lightProxy.thinInstanceSetBuffer("matrix", this._matrixBuffer, 16, false); + this._resizeObserver = engine.onResizeObservable.add(() => { + this._tileMaskTarget.resize(getRenderSize()); + if (this._tileMaskBuffer) { + // Update the buffer size + this._createTileMaskBuffer(); + } + }); + + this._proxyMesh = CreateSphere("LightProxy", { diameter: 2, segments: 8 }, this._scene); + this._proxyMesh.isVisible = false; + this._proxyMesh.material = new LightProxyMaterial("ProxyMaterial", this); + this._proxyMatrixBuffer = new Float32Array(this.maxLights * 16); + this._proxyMesh.thinInstanceSetBuffer("matrix", this._proxyMatrixBuffer, 16, false); + + if (this.maxLights > 0) { + ClusteredLight._SceneComponentInitialization(this._scene); for (const light of lights) { this.addLight(light); } @@ -116,8 +183,12 @@ export class ClusteredLight extends Light { for (const light of this._lights) { light.dispose(doNotRecurse, disposeMaterialAndTextures); } - this._lightMask?.dispose(); - this._lightProxy.dispose(doNotRecurse, disposeMaterialAndTextures); + + this._tileMaskTarget.dispose(); + this._tileMaskBuffer?.dispose(); + this._resizeObserver.remove(); + + this._proxyMesh.dispose(doNotRecurse, disposeMaterialAndTextures); super.dispose(doNotRecurse, disposeMaterialAndTextures); } @@ -133,42 +204,20 @@ export class ClusteredLight extends Light { this._lights.push(light); } - /** @internal */ - public _getLightMask(): RenderTargetTexture { + private _createTileMaskBuffer(): StorageBuffer { const engine = this.getEngine(); - // const width = Math.ceil(engine.getRenderWidth(true) / this.tileWidth); - // const height = Math.ceil(engine.getRenderHeight(true) / this.tileHeight); - const width = 16; - const height = 16; - if (this._lightMask && this._lightMask.getRenderWidth() === width && this._lightMask.getRenderHeight() === height) { - return this._lightMask; + this._tileMaskStride = Math.ceil(engine.getRenderWidth(true) / this._tileWidth); + const tilesY = Math.ceil(engine.getRenderHeight(true) / this._tileHeight); + const size = this._tileMaskStride * tilesY * 4; + if (!this._tileMaskBuffer || this._tileMaskBuffer.getBuffer().capacity < size) { + this._tileMaskBuffer?.dispose(); + this._tileMaskBuffer = new StorageBuffer(engine, CeilingPOT(size)); } - - this._lightMask?.dispose(); - this._lightMask = new RenderTargetTexture("LightMask", { width, height }, this._scene, { - type: engine.isWebGPU ? Constants.TEXTURETYPE_UNSIGNED_INTEGER : Constants.TEXTURETYPE_FLOAT, - format: Constants.TEXTUREFORMAT_RED, - samplingMode: Constants.TEXTURE_NEAREST_SAMPLINGMODE, - }); - this._lightMask.renderParticles = false; - this._lightMask.renderSprites = false; - this._lightMask.noPrePassRenderer = true; - // Use the default render list - this._lightMask.renderList = null; - this._lightMask.clearColor = new Color4(); - - this._lightMask.customRenderFunction = this._renderLightMaskGroup; - this._lightMask.onAfterRenderObservable.add(this._afterRenderLightMask); - - // Prevent clearing between render groups - for (let i = RenderingManager.MIN_RENDERINGGROUPS; i < RenderingManager.MAX_RENDERINGGROUPS; i += 1) { - this._lightMask.setRenderingAutoClearDepthStencil(i, false); - } - return this._lightMask; + return this._tileMaskBuffer; } public override _isReady(): boolean { - return this._lightProxy.isReady(true, true); + return this._proxyMesh.isReady(true, true); } protected _buildUniformLayout(): void { @@ -193,10 +242,8 @@ export class ClusteredLight extends Light { } public transferToEffect(effect: Effect, lightIndex: string): Light { - const maskSize = this._lightMask?.getSize() ?? { width: 0, height: 0 }; - const engine = this.getEngine(); const len = Math.min(this._lights.length, this.maxLights); - this._uniformBuffer.updateFloat4("vLightData", maskSize.width / engine.getRenderWidth(), maskSize.height / engine.getRenderHeight(), len, 0, lightIndex); + this._uniformBuffer.updateFloat4("vLightData", this._tileWidth, this._tileHeight, this._tileMaskStride, len, lightIndex); for (let i = 0; i < len; i += 1) { const light = this._lights[i]; @@ -234,7 +281,7 @@ export class ClusteredLight extends Light { } public override transferTexturesToEffect(effect: Effect, lightIndex: string): Light { - effect.setTexture("lightMaskTexture" + lightIndex, this._lightMask); + (this.getEngine()).setStorageBuffer("tileMaskBuffer" + lightIndex, this._tileMaskBuffer); return this; } @@ -248,16 +295,13 @@ export class ClusteredLight extends Light { defines["CLUSTLIGHT_MAX"] = this.maxLights; } - private _renderLightMaskGroup: RenderTargetTexture["customRenderFunction"] = ( - opaqueSubMeshes, - alphaTestSubMeshes, - transparentSubMeshes, - depthOnlySubMeshes, - beforeTransparents - ) => { - const engine = this.getEngine(); - // Draw everything as depth only - engine.setColorWrite(false); + private _renderTileMask: RenderTargetTexture["customRenderFunction"] = (opaqueSubMeshes, alphaTestSubMeshes, transparentSubMeshes, depthOnlySubMeshes, beforeTransparents) => { + const len = Math.min(this._lights.length, this.maxLights); + if (len === 0) { + // Theres no lights to render + return; + } + for (let i = 0; i < depthOnlySubMeshes.length; i += 1) { depthOnlySubMeshes.data[i].render(false); } @@ -268,34 +312,19 @@ export class ClusteredLight extends Light { for (let i = 0; i < alphaTestSubMeshes.length; i += 1) { alphaTestSubMeshes.data[i].render(false); } - engine.setColorWrite(true); - // We don't render any transparent meshes for the light mask beforeTransparents?.(); - }; + // TODO: draw transparents - private _afterRenderLightMask = () => { - const len = Math.min(this._lights.length, this.maxLights); - if (len === 0) { - // Theres no lights to render - return; - } - - this._lightProxy.thinInstanceCount = len; + this._proxyMesh.thinInstanceCount = len; for (let i = 0; i < len; i += 1) { const light = this._lights[i]; // TODO: cache matrices, somehow detect unchanged? // TODO: scale by range of light // TODO: rotate spotlights to face direction - light.getWorldMatrix().copyToArray(this._matrixBuffer, i * 16); + light.getWorldMatrix().copyToArray(this._proxyMatrixBuffer, i * 16); } - this._lightProxy.thinInstanceBufferUpdated("matrix"); - - const engine = this.getEngine(); - const depthWrite = engine.getDepthWrite(); - engine.setDepthWrite(false); - this._lightProxy.render(this._lightProxy.subMeshes[0], true); - engine.setAlphaMode(Constants.ALPHA_DISABLE); - engine.setDepthWrite(depthWrite); + this._proxyMesh.thinInstanceBufferUpdated("matrix"); + this._proxyMesh.render(this._proxyMesh.subMeshes[0], false); }; } diff --git a/packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts b/packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts index a23eb683768..aea615a4d86 100644 --- a/packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts +++ b/packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts @@ -17,13 +17,17 @@ class ClusteredLightSceneComponent implements ISceneComponent { public rebuild(): void {} public register(): void { - this.scene._gatherRenderTargetsStage.registerStep(SceneComponentConstants.STEP_GATHERRENDERTARGETS_CLUSTEREDLIGHT, this, this._gatherRenderTargets); + this.scene._gatherActiveCameraRenderTargetsStage.registerStep( + SceneComponentConstants.STEP_GATHERACTIVECAMERARENDERTARGETS_CLUSTEREDLIGHT, + this, + this._gatherActiveCameraRenderTargets + ); } - private _gatherRenderTargets: RenderTargetsStageAction = (renderTargets) => { + private _gatherActiveCameraRenderTargets: RenderTargetsStageAction = (renderTargets) => { for (const light of this.scene.lights) { if (light instanceof ClusteredLight && light.isSupported) { - renderTargets.push(light._getLightMask()); + renderTargets.pushNoDuplicate(light._tileMaskTarget); } } }; diff --git a/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts b/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts index 824d5268fe5..9fe055d2ad7 100644 --- a/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts +++ b/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts @@ -1,31 +1,32 @@ -import { Constants } from "core/Engines/constants"; -import type { Effect } from "core/Materials/effect"; +import { ShaderLanguage } from "core/Materials/shaderLanguage"; import { ShaderMaterial } from "core/Materials/shaderMaterial"; import type { Matrix } from "core/Maths/math.vector"; import type { Mesh } from "core/Meshes/mesh"; import type { SubMesh } from "core/Meshes/subMesh"; -import type { Nullable } from "core/types"; import type { ClusteredLight } from "./clusteredLight"; async function InitializeLightProxy(): Promise { - await Promise.all([import("../../Shaders/lightProxy.vertex"), import("../../Shaders/lightProxy.fragment")]); + await Promise.all([import("../../ShadersWGSL/lightProxy.vertex"), import("../../ShadersWGSL/lightProxy.fragment")]); } export class LightProxyMaterial extends ShaderMaterial { private readonly _clusteredLight: ClusteredLight; constructor(name: string, clusteredLight: ClusteredLight) { - super(name, clusteredLight._scene, "lightProxy", { + const shader = { vertex: "lightProxy", fragment: "lightProxy" }; + super(name, clusteredLight._scene, shader, { attributes: ["position"], - uniforms: ["world"], uniformBuffers: ["Scene", "Mesh", "Light0"], - defines: ["LIGHT0", "CLUSTLIGHT0", `CLUSTLIGHT_MAX ${clusteredLight.maxLights}`], + storageBuffers: ["tileMaskBuffer0"], + defines: ["LIGHT0", "CLUSTLIGHT0", `CLUSTLIGHT_WRITE`], + shaderLanguage: ShaderLanguage.WGSL, extraInitializationsAsync: InitializeLightProxy, }); this._clusteredLight = clusteredLight; - this.alphaMode = Constants.ALPHA_ADD; + this.backFaceCulling = false; + this.disableDepthWrite = true; } public override bindForSubMesh(world: Matrix, mesh: Mesh, subMesh: SubMesh): void { diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/clusteredLightFunctions.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/clusteredLightFunctions.fx new file mode 100644 index 00000000000..4151d2d88d9 --- /dev/null +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/clusteredLightFunctions.fx @@ -0,0 +1,13 @@ +struct ClusteredLight { + position: vec4f, + direction: vec4f, + diffuse: vec4f, + specular: vec4f, + falloff: vec4f, +} + +fn tileMaskIndex(lightData: vec4f, fragPos: vec4f) -> u32 { + let uData = vec3u(lightData.xyz); + let tilePos = vec2u(fragPos.xy) / uData.xy; + return tilePos.y * uData.z + tilePos.x; +} diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightFragment.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightFragment.fx index 4d4b65b76f4..489525893f9 100644 --- a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightFragment.fx +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightFragment.fx @@ -206,6 +206,8 @@ uniforms.vReflectionInfos.y #endif ); + #elif defined(CLUSTLIGHT{X}) + info = computeClusteredLighting(&tileMaskBuffer{X}, viewDirectionW, normalW, light{X}.vLightData, &light{X}.vLights, diffuse{X}.rgb, light{X}.vLightSpecular.rgb, glossiness); #endif #endif diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightUboDeclaration.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightUboDeclaration.fx index d1e0ca42670..0fe4876a5b6 100644 --- a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightUboDeclaration.fx +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightUboDeclaration.fx @@ -11,6 +11,8 @@ vLightFalloff: vec4f, #elif defined(HEMILIGHT{X}) vLightGround: vec3f, + #elif defined(CLUSTLIGHT{X}) + vLights: array, #endif #if defined(AREALIGHT{X}) vLightWidth: vec4f, @@ -32,6 +34,16 @@ var light{X} : Light{X}; var projectionLightTexture{X}Sampler: sampler; var projectionLightTexture{X}: texture_2d; #endif + +#ifdef CLUSTLIGHT{X} +#ifdef CLUSTLIGHT_WRITE + var tileMaskBuffer{X}: array>; +#else + // TODO: try to make read-only + var tileMaskBuffer{X}: array; +#endif +#endif + #ifdef SHADOW{X} #ifdef SHADOWCSM{X} uniform lightMatrix{X}: array; diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightVxUboDeclaration.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightVxUboDeclaration.fx index c4e2d7c2290..5f987ce3641 100644 --- a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightVxUboDeclaration.fx +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightVxUboDeclaration.fx @@ -11,6 +11,8 @@ vLightFalloff: vec4f, #elif defined(HEMILIGHT{X}) vLightGround: vec3f, + #elif defined(CLUSTLIGHT{X}) + vLights: array, #endif #if defined(AREALIGHT{X}) vLightWidth: vec4f, diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx index da3912d23a5..b036b0561ab 100644 --- a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx @@ -178,4 +178,37 @@ fn computeAreaLighting(ltc1: texture_2d, ltc1Sampler:sampler, ltc2:texture_ } // End Area Light -#endif \ No newline at end of file +#endif + +fn computeClusteredLighting( + tileMask: ptr, read_write>, + viewDirectionW: vec3f, + vNormal: vec3f, + lightData: vec4f, + lights: ptr>, + diffuseScale: vec3f, + specularScale: vec3f, + glossiness: f32 +) -> lightingInfo { + var result: lightingInfo; + let index = tileMaskIndex(lightData, fragmentInputs.position); + // TODO: merge subgroups + let mask = subgroupOr(tileMask[index]); + let len = u32(lightData.w); + + for (var i = 0u; i < len; i += 1u) { + if (mask & (1u << i)) == 0 { + continue; + } + let diffuse = lights[i].diffuse.rgb * diffuseScale; + let specular = lights[i].specular.rgb * specularScale; + let info = computeSpotLighting(viewDirectionW, vNormal, lights[i].position, lights[i].direction, diffuse.rgb, specular, lights[i].diffuse.a, glossiness); + result.diffuse += info.diffuse; + #ifdef SPECULARTERM + result.specular += info.specular; + #endif + } + + // result.diffuse = vec3f(f32(mask >> 9) / f32(0x7fffff), 0, 0); + return result; +} diff --git a/packages/dev/core/src/ShadersWGSL/default.fragment.fx b/packages/dev/core/src/ShadersWGSL/default.fragment.fx index dfe591e65f9..b4984831d4f 100644 --- a/packages/dev/core/src/ShadersWGSL/default.fragment.fx +++ b/packages/dev/core/src/ShadersWGSL/default.fragment.fx @@ -22,6 +22,7 @@ varying vColor: vec4f; #include // Lights +#include #include[0..maxSimultaneousLights] #include diff --git a/packages/dev/core/src/ShadersWGSL/default.vertex.fx b/packages/dev/core/src/ShadersWGSL/default.vertex.fx index 628ecc3bc98..debc3a97c3e 100644 --- a/packages/dev/core/src/ShadersWGSL/default.vertex.fx +++ b/packages/dev/core/src/ShadersWGSL/default.vertex.fx @@ -56,6 +56,7 @@ varying vColor: vec4f; #include #include +#include #include<__decl__lightVxFragment>[0..maxSimultaneousLights] #include diff --git a/packages/dev/core/src/ShadersWGSL/lightProxy.fragment.fx b/packages/dev/core/src/ShadersWGSL/lightProxy.fragment.fx new file mode 100644 index 00000000000..c304f7043a9 --- /dev/null +++ b/packages/dev/core/src/ShadersWGSL/lightProxy.fragment.fx @@ -0,0 +1,20 @@ +flat varying vMask: u32; + +// Declarations +#include +#include[0..1] + +@fragment +fn main(input: FragmentInputs) -> FragmentOutputs { + let index = tileMaskIndex(light0.vLightData, fragmentInputs.position); + + // To reduce atomic contention we elect one wavefront to set the index + // However, as a fallback we also check for wavefronts that might have different indices + let elected = subgroupElect(); + let electedIndex = subgroupBroadcastFirst(index); + if elected || index != electedIndex { + atomicOr(&tileMaskBuffer0[index], fragmentInputs.vMask); + } + + // atomicOr(&tileMaskBuffer0[index], fragmentInputs.vMask); +} diff --git a/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx b/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx new file mode 100644 index 00000000000..1959d4d1d7b --- /dev/null +++ b/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx @@ -0,0 +1,35 @@ +attribute position: vec3f; +flat varying vMask: u32; + +// Declarations +#include +#include +#include + +#include +#include[0..1] + +const DOWN = vec3f(0, -1, 0); + +fn acosClamped(v: f32) -> f32 { + return acos(clamp(v, 0, 1)); +} + +@vertex +fn main(input: VertexInputs) -> FragmentInputs { + let light = &light0.vLights[vertexInputs.instanceIndex]; + let maxAngle = acosClamped(light.direction.w); + let angle = acosClamped(dot(DOWN, vertexInputs.position)); + + var positionUpdated = vec3f(0); + // We allow some wiggle room equal to the rotation of one slice of the sphere + if angle - maxAngle < 0.32 { + positionUpdated = vertexInputs.position; + } + positionUpdated *= light.diffuse.a; + +#include + + vertexOutputs.position = scene.viewProjection * finalWorld * vec4f(positionUpdated, 1); + vertexOutputs.vMask = 1u << vertexInputs.instanceIndex; +} diff --git a/packages/dev/core/src/sceneComponent.ts b/packages/dev/core/src/sceneComponent.ts index 7666a32bde2..c56e795df34 100644 --- a/packages/dev/core/src/sceneComponent.ts +++ b/packages/dev/core/src/sceneComponent.ts @@ -93,11 +93,11 @@ export class SceneComponentConstants { public static readonly STEP_GATHERRENDERTARGETS_DEPTHRENDERER = 0; public static readonly STEP_GATHERRENDERTARGETS_GEOMETRYBUFFERRENDERER = 1; public static readonly STEP_GATHERRENDERTARGETS_SHADOWGENERATOR = 2; - public static readonly STEP_GATHERRENDERTARGETS_CLUSTEREDLIGHT = 3; public static readonly STEP_GATHERRENDERTARGETS_POSTPROCESSRENDERPIPELINEMANAGER = 3; public static readonly STEP_GATHERACTIVECAMERARENDERTARGETS_DEPTHRENDERER = 0; public static readonly STEP_GATHERACTIVECAMERARENDERTARGETS_FLUIDRENDERER = 1; + public static readonly STEP_GATHERACTIVECAMERARENDERTARGETS_CLUSTEREDLIGHT = 2; public static readonly STEP_POINTERMOVE_SPRITE = 0; public static readonly STEP_POINTERDOWN_SPRITE = 0; From e965dc101d6999f09664cc15672ee173a7f3638a Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Thu, 17 Jul 2025 13:29:08 +1000 Subject: [PATCH 10/30] Simplify, remove depth prepass, add back webgl support --- .../src/Lights/Clustered/clusteredLight.ts | 221 ++++++++---------- .../Clustered/clusteredLightSceneComponent.ts | 2 +- .../Lights/Clustered/lightProxyMaterial.ts | 25 +- .../src/Materials/materialHelper.functions.ts | 8 +- .../Shaders/ShadersInclude/lightFragment.fx | 2 +- .../ShadersInclude/lightUboDeclaration.fx | 4 +- .../ShadersInclude/lightVxUboDeclaration.fx | 2 +- .../ShadersInclude/lightsFragmentFunctions.fx | 6 +- ...Declaration.fx => spotLightDeclaration.fx} | 3 +- .../dev/core/src/Shaders/default.fragment.fx | 2 +- .../dev/core/src/Shaders/default.vertex.fx | 2 +- .../core/src/Shaders/lightProxy.fragment.fx | 1 - .../dev/core/src/Shaders/lightProxy.vertex.fx | 26 +-- .../ShadersInclude/clusteredLightFunctions.fx | 13 -- .../ShadersInclude/lightUboDeclaration.fx | 5 +- .../ShadersInclude/lightVxUboDeclaration.fx | 2 +- .../ShadersInclude/lightsFragmentFunctions.fx | 15 +- .../ShadersInclude/spotLightDeclaration.fx | 8 + .../core/src/ShadersWGSL/default.fragment.fx | 2 +- .../core/src/ShadersWGSL/default.vertex.fx | 2 +- .../src/ShadersWGSL/lightProxy.fragment.fx | 16 +- .../core/src/ShadersWGSL/lightProxy.vertex.fx | 2 +- 22 files changed, 171 insertions(+), 198 deletions(-) rename packages/dev/core/src/Shaders/ShadersInclude/{lightClusteredDeclaration.fx => spotLightDeclaration.fx} (63%) delete mode 100644 packages/dev/core/src/ShadersWGSL/ShadersInclude/clusteredLightFunctions.fx create mode 100644 packages/dev/core/src/ShadersWGSL/ShadersInclude/spotLightDeclaration.fx diff --git a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts index 483b3e91754..14a6146865c 100644 --- a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts +++ b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts @@ -1,4 +1,5 @@ import { StorageBuffer } from "core/Buffers/storageBuffer"; +import { Constants } from "core/Engines/constants"; import type { AbstractEngine } from "core/Engines/abstractEngine"; import type { WebGPUEngine } from "core/Engines/webgpuEngine"; import type { Effect } from "core/Materials/effect"; @@ -9,8 +10,6 @@ import { CreateSphere } from "core/Meshes/Builders/sphereBuilder"; import type { Mesh } from "core/Meshes/mesh"; import { _WarnImport } from "core/Misc/devTools"; import { Logger } from "core/Misc/logger"; -import type { Observer } from "core/Misc/observable"; -import { CeilingPOT } from "core/Misc/tools.functions"; import type { Scene } from "core/scene"; import type { Nullable } from "core/types"; @@ -76,38 +75,33 @@ export class ClusteredLight extends Light { return this._lights; } - /** @internal */ - public readonly _tileMaskTarget: RenderTargetTexture; + private _tileMaskTexture: Nullable; private _tileMaskBuffer: Nullable; - private _tileMaskStride = 0; - private readonly _resizeObserver: Observer; - private _tileWidth = 128; - public get tileWidth(): number { - return this._tileWidth; + private _horizontalTiles = 128; // TODO: 64 + public get horizontalTiles(): number { + return this._horizontalTiles; } - public set tileWidth(width: number) { - if (this._tileWidth === width) { + public set horizontalTiles(horizontal: number) { + if (this._horizontalTiles === horizontal) { return; } - this._tileWidth = width; - this._tileMaskBuffer?.dispose(); - this._tileMaskBuffer = null; + this._horizontalTiles = horizontal; + this._disposeTileMask(); } - private _tileHeight = 128; - public get tileHeight(): number { - return this._tileHeight; + private _verticalTiles = 128; + public get verticalTiles(): number { + return this._verticalTiles; } - public set tileHeight(height: number) { - if (this._tileHeight === height) { + public set verticalTiles(vertical: number) { + if (this._verticalTiles === vertical) { return; } - this._tileHeight = height; - this._tileMaskBuffer?.dispose(); - this._tileMaskBuffer = null; + this._verticalTiles = vertical; + this._disposeTileMask(); } private readonly _proxyMesh: Mesh; @@ -115,49 +109,12 @@ export class ClusteredLight extends Light { constructor(name: string, lights: Light[] = [], scene?: Scene) { super(name, scene); - const engine = this.getEngine(); this.maxLights = ClusteredLight._GetEngineMaxLights(engine); - const getRenderSize = () => ({ - width: engine.getRenderWidth(true), - height: engine.getRenderHeight(true), - }); - - this._tileMaskTarget = new RenderTargetTexture("TileMask", getRenderSize(), this._scene, { - generateDepthBuffer: true, - generateStencilBuffer: true, - noColorAttachment: true, - }); - this._tileMaskTarget.renderList = null; - this._tileMaskTarget.renderParticles = false; - this._tileMaskTarget.renderSprites = false; - this._tileMaskTarget.noPrePassRenderer = true; - // Use the default render list - this._tileMaskTarget.renderList = null; - this._tileMaskTarget.customRenderFunction = this._renderTileMask; - - this._tileMaskTarget.onClearObservable.add(() => { - // If its already created it should be the correct size - const buffer = this._tileMaskBuffer ?? this._createTileMaskBuffer(); - buffer.clear(); - engine.clear(null, false, true, true); - }); - - // This forces materials to run as a depth prepass - this._tileMaskTarget.onBeforeRenderObservable.add(() => engine.setColorWrite(false)); - this._tileMaskTarget.onAfterRenderObservable.add(() => engine.setColorWrite(true)); - this._resizeObserver = engine.onResizeObservable.add(() => { - this._tileMaskTarget.resize(getRenderSize()); - if (this._tileMaskBuffer) { - // Update the buffer size - this._createTileMaskBuffer(); - } - }); - - this._proxyMesh = CreateSphere("LightProxy", { diameter: 2, segments: 8 }, this._scene); - this._proxyMesh.isVisible = false; - this._proxyMesh.material = new LightProxyMaterial("ProxyMaterial", this); + this._proxyMesh = CreateSphere("ProxyMesh", { diameter: 2, segments: 8 }, this._scene); + this._proxyMesh.material = new LightProxyMaterial("ProxyMeshMaterial", this); + this._scene.removeMesh(this._proxyMesh); this._proxyMatrixBuffer = new Float32Array(this.maxLights * 16); this._proxyMesh.thinInstanceSetBuffer("matrix", this._proxyMatrixBuffer, 16, false); @@ -179,14 +136,76 @@ export class ClusteredLight extends Light { return LightConstants.LIGHTTYPEID_CLUSTERED; } + private _updateMatrixBuffer(): void { + const len = Math.min(this._lights.length, this.maxLights); + if (len === 0) { + // Nothing to render + this._proxyMesh.isVisible = false; + return; + } + + this._proxyMesh.isVisible = true; + this._proxyMesh.thinInstanceCount = len; + for (let i = 0; i < len; i += 1) { + const light = this._lights[i]; + // TODO: cache matrices, somehow detect unchanged? + // TODO: scale by range of light + // TODO: rotate spotlights to face direction + light.getWorldMatrix().copyToArray(this._proxyMatrixBuffer, i * 16); + } + this._proxyMesh.thinInstanceBufferUpdated("matrix"); + } + + /** @internal */ + public _createTileMask(): RenderTargetTexture { + if (this._tileMaskTexture) { + return this._tileMaskTexture; + } + + const engine = this.getEngine(); + const textureSize = { width: this._horizontalTiles, height: this._verticalTiles }; + this._tileMaskTexture = new RenderTargetTexture("TileMaskTexture", textureSize, this._scene, { + // We don't write anything on WebGPU so make it as small as possible + type: engine.isWebGPU ? Constants.TEXTURETYPE_UNSIGNED_BYTE : Constants.TEXTURETYPE_FLOAT, + format: Constants.TEXTUREFORMAT_RED, + generateDepthBuffer: false, + }); + + this._tileMaskTexture.renderParticles = false; + this._tileMaskTexture.renderSprites = false; + this._tileMaskTexture.noPrePassRenderer = true; + this._tileMaskTexture.renderList = [this._proxyMesh]; + + this._tileMaskTexture.onBeforeBindObservable.add(() => this._updateMatrixBuffer()); + + this._tileMaskTexture.onClearObservable.add(() => { + // Clear the storage buffer if it exists + this._tileMaskBuffer?.clear(); + engine.clear({ r: 0, g: 0, b: 0, a: 1 }, true, false); + }); + + if (engine.isWebGPU) { + // WebGPU also needs a storage buffer to write to + const bufferSize = this._horizontalTiles * this._verticalTiles * 4; + this._tileMaskBuffer = new StorageBuffer(engine, bufferSize); + } + + return this._tileMaskTexture; + } + + private _disposeTileMask(): void { + this._tileMaskTexture?.dispose(); + this._tileMaskTexture = null; + this._tileMaskBuffer?.dispose(); + this._tileMaskBuffer = null; + } + public override dispose(doNotRecurse?: boolean, disposeMaterialAndTextures?: boolean): void { for (const light of this._lights) { light.dispose(doNotRecurse, disposeMaterialAndTextures); } - this._tileMaskTarget.dispose(); - this._tileMaskBuffer?.dispose(); - this._resizeObserver.remove(); + this._disposeTileMask(); this._proxyMesh.dispose(doNotRecurse, disposeMaterialAndTextures); super.dispose(doNotRecurse, disposeMaterialAndTextures); @@ -204,23 +223,7 @@ export class ClusteredLight extends Light { this._lights.push(light); } - private _createTileMaskBuffer(): StorageBuffer { - const engine = this.getEngine(); - this._tileMaskStride = Math.ceil(engine.getRenderWidth(true) / this._tileWidth); - const tilesY = Math.ceil(engine.getRenderHeight(true) / this._tileHeight); - const size = this._tileMaskStride * tilesY * 4; - if (!this._tileMaskBuffer || this._tileMaskBuffer.getBuffer().capacity < size) { - this._tileMaskBuffer?.dispose(); - this._tileMaskBuffer = new StorageBuffer(engine, CeilingPOT(size)); - } - return this._tileMaskBuffer; - } - - public override _isReady(): boolean { - return this._proxyMesh.isReady(true, true); - } - - protected _buildUniformLayout(): void { + protected override _buildUniformLayout(): void { // We can't use `this.maxLights` since this will get called during construction const maxLights = ClusteredLight._GetEngineMaxLights(this.getEngine()); @@ -241,9 +244,13 @@ export class ClusteredLight extends Light { this._uniformBuffer.create(); } - public transferToEffect(effect: Effect, lightIndex: string): Light { + public override transferToEffect(effect: Effect, lightIndex: string): Light { + const engine = this.getEngine(); + const hscale = this._horizontalTiles / engine.getRenderWidth(); + const vscale = this._verticalTiles / engine.getRenderHeight(); + const len = Math.min(this._lights.length, this.maxLights); - this._uniformBuffer.updateFloat4("vLightData", this._tileWidth, this._tileHeight, this._tileMaskStride, len, lightIndex); + this._uniformBuffer.updateFloat4("vLightData", hscale, vscale, this._horizontalTiles, len, lightIndex); for (let i = 0; i < len; i += 1) { const light = this._lights[i]; @@ -281,50 +288,26 @@ export class ClusteredLight extends Light { } public override transferTexturesToEffect(effect: Effect, lightIndex: string): Light { - (this.getEngine()).setStorageBuffer("tileMaskBuffer" + lightIndex, this._tileMaskBuffer); + const engine = this.getEngine(); + if (engine.isWebGPU) { + (this.getEngine()).setStorageBuffer("tileMaskBuffer" + lightIndex, this._tileMaskBuffer); + } else { + effect.setTexture("tileMaskTexture" + lightIndex, this._tileMaskTexture); + } return this; } - public transferToNodeMaterialEffect(): Light { + public override transferToNodeMaterialEffect(): Light { // TODO: ???? return this; } - public prepareLightSpecificDefines(defines: any, lightIndex: number): void { + public override prepareLightSpecificDefines(defines: any, lightIndex: number): void { defines["CLUSTLIGHT" + lightIndex] = true; defines["CLUSTLIGHT_MAX"] = this.maxLights; } - private _renderTileMask: RenderTargetTexture["customRenderFunction"] = (opaqueSubMeshes, alphaTestSubMeshes, transparentSubMeshes, depthOnlySubMeshes, beforeTransparents) => { - const len = Math.min(this._lights.length, this.maxLights); - if (len === 0) { - // Theres no lights to render - return; - } - - for (let i = 0; i < depthOnlySubMeshes.length; i += 1) { - depthOnlySubMeshes.data[i].render(false); - } - // TODO: skip meshes that were already drawn during `depthOnly` - for (let i = 0; i < opaqueSubMeshes.length; i += 1) { - opaqueSubMeshes.data[i].render(false); - } - for (let i = 0; i < alphaTestSubMeshes.length; i += 1) { - alphaTestSubMeshes.data[i].render(false); - } - - beforeTransparents?.(); - // TODO: draw transparents - - this._proxyMesh.thinInstanceCount = len; - for (let i = 0; i < len; i += 1) { - const light = this._lights[i]; - // TODO: cache matrices, somehow detect unchanged? - // TODO: scale by range of light - // TODO: rotate spotlights to face direction - light.getWorldMatrix().copyToArray(this._proxyMatrixBuffer, i * 16); - } - this._proxyMesh.thinInstanceBufferUpdated("matrix"); - this._proxyMesh.render(this._proxyMesh.subMeshes[0], false); - }; + public override _isReady(): boolean { + return this._proxyMesh.isReady(true, true); + } } diff --git a/packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts b/packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts index aea615a4d86..a421e5be612 100644 --- a/packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts +++ b/packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts @@ -27,7 +27,7 @@ class ClusteredLightSceneComponent implements ISceneComponent { private _gatherActiveCameraRenderTargets: RenderTargetsStageAction = (renderTargets) => { for (const light of this.scene.lights) { if (light instanceof ClusteredLight && light.isSupported) { - renderTargets.pushNoDuplicate(light._tileMaskTarget); + renderTargets.push(light._createTileMask()); } } }; diff --git a/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts b/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts index 9fe055d2ad7..615488927f9 100644 --- a/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts +++ b/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts @@ -1,3 +1,4 @@ +import { Constants } from "core/Engines/constants"; import { ShaderLanguage } from "core/Materials/shaderLanguage"; import { ShaderMaterial } from "core/Materials/shaderMaterial"; import type { Matrix } from "core/Maths/math.vector"; @@ -6,27 +7,33 @@ import type { SubMesh } from "core/Meshes/subMesh"; import type { ClusteredLight } from "./clusteredLight"; -async function InitializeLightProxy(): Promise { - await Promise.all([import("../../ShadersWGSL/lightProxy.vertex"), import("../../ShadersWGSL/lightProxy.fragment")]); -} - export class LightProxyMaterial extends ShaderMaterial { private readonly _clusteredLight: ClusteredLight; constructor(name: string, clusteredLight: ClusteredLight) { + const engine = clusteredLight.getEngine(); const shader = { vertex: "lightProxy", fragment: "lightProxy" }; + super(name, clusteredLight._scene, shader, { attributes: ["position"], + uniforms: ["world"], uniformBuffers: ["Scene", "Mesh", "Light0"], storageBuffers: ["tileMaskBuffer0"], - defines: ["LIGHT0", "CLUSTLIGHT0", `CLUSTLIGHT_WRITE`], - shaderLanguage: ShaderLanguage.WGSL, - extraInitializationsAsync: InitializeLightProxy, + defines: ["LIGHT0", "CLUSTLIGHT0", `CLUSTLIGHT_MAX ${clusteredLight.maxLights}`, "CLUSTLIGHT_WRITE"], + shaderLanguage: engine.isWebGPU ? ShaderLanguage.WGSL : ShaderLanguage.GLSL, + extraInitializationsAsync: async () => { + if (engine.isWebGPU) { + await Promise.all([import("../../ShadersWGSL/lightProxy.vertex"), import("../../ShadersWGSL/lightProxy.fragment")]); + } else { + await Promise.all([import("../../Shaders/lightProxy.vertex"), import("../../Shaders/lightProxy.fragment")]); + } + }, }); this._clusteredLight = clusteredLight; - this.backFaceCulling = false; - this.disableDepthWrite = true; + this.cullBackFaces = false; + this.transparencyMode = ShaderMaterial.MATERIAL_ALPHABLEND; + this.alphaMode = Constants.ALPHA_ADD; } public override bindForSubMesh(world: Matrix, mesh: Mesh, subMesh: SubMesh): void { diff --git a/packages/dev/core/src/Materials/materialHelper.functions.ts b/packages/dev/core/src/Materials/materialHelper.functions.ts index c62c511211b..f69d50d5f2f 100644 --- a/packages/dev/core/src/Materials/materialHelper.functions.ts +++ b/packages/dev/core/src/Materials/materialHelper.functions.ts @@ -1074,7 +1074,7 @@ export function PrepareDefinesForCamera(scene: Scene, defines: any): boolean { * @param uniformBuffersList defines an optional list of uniform buffers * @param updateOnlyBuffersList True to only update the uniformBuffersList array * @param iesLightTexture defines if IES texture must be used - * @param lightMaskTexture defines if light mask texture must be used + * @param tileMaskTexture defines if a tile mask texture must be used */ export function PrepareUniformsAndSamplersForLight( lightIndex: number, @@ -1084,7 +1084,7 @@ export function PrepareUniformsAndSamplersForLight( uniformBuffersList: Nullable = null, updateOnlyBuffersList = false, iesLightTexture = false, - lightMaskTexture = false + tileMaskTexture = false ) { if (uniformBuffersList) { uniformBuffersList.push("Light" + lightIndex); @@ -1127,8 +1127,8 @@ export function PrepareUniformsAndSamplersForLight( if (iesLightTexture) { samplersList.push("iesLightTexture" + lightIndex); } - if (lightMaskTexture) { - samplersList.push("lightMaskTexture" + lightIndex); + if (tileMaskTexture) { + samplersList.push("tileMaskTexture" + lightIndex); } } diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx index 56814363f95..d7f63dad391 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx @@ -206,7 +206,7 @@ #endif ); #elif defined(CLUSTLIGHT{X}) && CLUSTLIGHT_MAX > 0 - info = computeClusteredLighting(lightMaskTexture{X}, viewDirectionW, normalW, light{X}.vLightData, light{X}.vLights, diffuse{X}.rgb, light{X}.vLightSpecular.rgb, glossiness); + info = computeClusteredLighting(tileMaskTexture{X}, viewDirectionW, normalW, light{X}.vLightData, light{X}.vLights, diffuse{X}.rgb, light{X}.vLightSpecular.rgb, glossiness); #endif #endif diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx index 11a61ffcc11..f41effacd83 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx @@ -13,7 +13,7 @@ #elif defined(HEMILIGHT{X}) vec3 vLightGround; #elif defined(CLUSTLIGHT{X}) - ClusteredLight vLights[CLUSTLIGHT_MAX]; + SpotLight vLights[CLUSTLIGHT_MAX]; #endif #if defined(AREALIGHT{X}) vec4 vLightWidth; @@ -31,7 +31,7 @@ #endif #ifdef CLUSTLIGHT{X} // Ensure the mask is sampled with high precision - uniform highp sampler2D lightMaskTexture{X}; + uniform highp sampler2D tileMaskTexture{X}; #endif #ifdef SHADOW{X} #ifdef SHADOWCSM{X} diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightVxUboDeclaration.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightVxUboDeclaration.fx index f02595aee0f..c22d8176e13 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightVxUboDeclaration.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightVxUboDeclaration.fx @@ -13,7 +13,7 @@ #elif defined(HEMILIGHT{X}) vec3 vLightGround; #elif defined(CLUSTLIGHT{X}) - ClusteredLight vLights[CLUSTLIGHT_MAX]; + SpotLight vLights[CLUSTLIGHT_MAX]; #endif #if defined(AREALIGHT{X}) vec4 vLightWidth; diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx index 353b40203c2..4eeda6b1a8a 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx @@ -182,12 +182,12 @@ lightingInfo computeAreaLighting(sampler2D ltc1, sampler2D ltc2, vec3 viewDirect #if defined(CLUSTLIGHT_MAX) && CLUSTLIGHT_MAX > 0 #define inline -lightingInfo computeClusteredLighting(sampler2D lightMask, vec3 viewDirectionW, vec3 vNormal, vec4 lightData, ClusteredLight lights[CLUSTLIGHT_MAX], vec3 diffuseScale, vec3 specularScale, float glossiness) { +lightingInfo computeClusteredLighting(sampler2D tileMask, vec3 viewDirectionW, vec3 vNormal, vec4 lightData, SpotLight lights[CLUSTLIGHT_MAX], vec3 diffuseScale, vec3 specularScale, float glossiness) { lightingInfo result; - vec4 maskTexel = texelFetch(lightMask, ivec2(gl_FragCoord.xy * lightData.xy), 0); + vec4 maskTexel = texelFetch(tileMask, ivec2(gl_FragCoord.xy * lightData.xy), 0); uint mask = uint(maskTexel.r); - int len = int(lightData.z); + int len = int(lightData.w); for (int i = 0; i < len; i += 1) { if ((mask & (1u << i)) == 0u) { continue; diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightClusteredDeclaration.fx b/packages/dev/core/src/Shaders/ShadersInclude/spotLightDeclaration.fx similarity index 63% rename from packages/dev/core/src/Shaders/ShadersInclude/lightClusteredDeclaration.fx rename to packages/dev/core/src/Shaders/ShadersInclude/spotLightDeclaration.fx index a940cc5c490..2ee2ceb90e9 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightClusteredDeclaration.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/spotLightDeclaration.fx @@ -1,4 +1,5 @@ -struct ClusteredLight { +// Used in clustered lights +struct SpotLight { vec4 position; vec4 direction; vec4 diffuse; diff --git a/packages/dev/core/src/Shaders/default.fragment.fx b/packages/dev/core/src/Shaders/default.fragment.fx index dd26571a823..2c26ce0e05d 100644 --- a/packages/dev/core/src/Shaders/default.fragment.fx +++ b/packages/dev/core/src/Shaders/default.fragment.fx @@ -32,7 +32,7 @@ varying vec4 vColor; #include // Lights -#include +#include #include<__decl__lightFragment>[0..maxSimultaneousLights] #include diff --git a/packages/dev/core/src/Shaders/default.vertex.fx b/packages/dev/core/src/Shaders/default.vertex.fx index 2ccb1e02480..340c508fd9c 100644 --- a/packages/dev/core/src/Shaders/default.vertex.fx +++ b/packages/dev/core/src/Shaders/default.vertex.fx @@ -58,7 +58,7 @@ varying vec4 vColor; #include #include -#include +#include #include<__decl__lightVxFragment>[0..maxSimultaneousLights] #include diff --git a/packages/dev/core/src/Shaders/lightProxy.fragment.fx b/packages/dev/core/src/Shaders/lightProxy.fragment.fx index 94a14fcc3e4..5f4d9930371 100644 --- a/packages/dev/core/src/Shaders/lightProxy.fragment.fx +++ b/packages/dev/core/src/Shaders/lightProxy.fragment.fx @@ -1,4 +1,3 @@ -// Input flat varying highp uint vMask; void main(void) { diff --git a/packages/dev/core/src/Shaders/lightProxy.vertex.fx b/packages/dev/core/src/Shaders/lightProxy.vertex.fx index 241ec3c3f9c..8207a6f0a67 100644 --- a/packages/dev/core/src/Shaders/lightProxy.vertex.fx +++ b/packages/dev/core/src/Shaders/lightProxy.vertex.fx @@ -1,30 +1,28 @@ -// Uniform Buffers +attribute vec3 position; +flat varying highp uint vMask; + +// Declarations #include #include - -#include -#include[0..1] - -// Attributes -attribute vec3 position; #include -// Output -flat varying highp uint vMask; +#include +#include[0..1] // TODO: switch default direction to up?? -const vec3 down = vec3(0, -1, 0); +const vec3 DOWN = vec3(0, -1, 0); float acosClamped(float v) { return acos(clamp(v, 0.0, 1.0)); } void main(void) { - float lightAngle = acosClamped(light0.vLights[gl_InstanceID].direction.a); - float posAngle = acosClamped(dot(down, position)); - + float maxAngle = acosClamped(light0.vLights[gl_InstanceID].direction.a); // We allow some wiggle room equal to the rotation of one slice of the sphere - vec3 positionUpdated = posAngle - lightAngle < 0.32 ? position : vec3(0); + maxAngle += 0.32; + + float angle = acosClamped(dot(DOWN, position)); + vec3 positionUpdated = angle < maxAngle ? position : vec3(0); positionUpdated *= light0.vLights[gl_InstanceID].diffuse.a; #include diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/clusteredLightFunctions.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/clusteredLightFunctions.fx deleted file mode 100644 index 4151d2d88d9..00000000000 --- a/packages/dev/core/src/ShadersWGSL/ShadersInclude/clusteredLightFunctions.fx +++ /dev/null @@ -1,13 +0,0 @@ -struct ClusteredLight { - position: vec4f, - direction: vec4f, - diffuse: vec4f, - specular: vec4f, - falloff: vec4f, -} - -fn tileMaskIndex(lightData: vec4f, fragPos: vec4f) -> u32 { - let uData = vec3u(lightData.xyz); - let tilePos = vec2u(fragPos.xy) / uData.xy; - return tilePos.y * uData.z + tilePos.x; -} diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightUboDeclaration.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightUboDeclaration.fx index 0fe4876a5b6..31fc65e1c9c 100644 --- a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightUboDeclaration.fx +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightUboDeclaration.fx @@ -12,7 +12,7 @@ #elif defined(HEMILIGHT{X}) vLightGround: vec3f, #elif defined(CLUSTLIGHT{X}) - vLights: array, + vLights: array, #endif #if defined(AREALIGHT{X}) vLightWidth: vec4f, @@ -39,8 +39,7 @@ var light{X} : Light{X}; #ifdef CLUSTLIGHT_WRITE var tileMaskBuffer{X}: array>; #else - // TODO: try to make read-only - var tileMaskBuffer{X}: array; + var tileMaskBuffer{X}: array; #endif #endif diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightVxUboDeclaration.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightVxUboDeclaration.fx index 5f987ce3641..3116735465e 100644 --- a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightVxUboDeclaration.fx +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightVxUboDeclaration.fx @@ -12,7 +12,7 @@ #elif defined(HEMILIGHT{X}) vLightGround: vec3f, #elif defined(CLUSTLIGHT{X}) - vLights: array, + vLights: array, #endif #if defined(AREALIGHT{X}) vLightWidth: vec4f, diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx index b036b0561ab..1786f7313fc 100644 --- a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx @@ -181,22 +181,23 @@ fn computeAreaLighting(ltc1: texture_2d, ltc1Sampler:sampler, ltc2:texture_ #endif fn computeClusteredLighting( - tileMask: ptr, read_write>, + tileMask: ptr>, viewDirectionW: vec3f, vNormal: vec3f, lightData: vec4f, - lights: ptr>, + lights: ptr>, diffuseScale: vec3f, specularScale: vec3f, glossiness: f32 ) -> lightingInfo { var result: lightingInfo; - let index = tileMaskIndex(lightData, fragmentInputs.position); + let tilePos = vec2u(fragmentInputs.position.xy * lightData.xy); + let strideLen = vec2u(lightData.zw); + let mask = tileMask[tilePos.y * strideLen.x + tilePos.x]; + // TODO: merge subgroups - let mask = subgroupOr(tileMask[index]); - let len = u32(lightData.w); - for (var i = 0u; i < len; i += 1u) { + for (var i = 0u; i < strideLen.y; i += 1u) { if (mask & (1u << i)) == 0 { continue; } @@ -208,7 +209,5 @@ fn computeClusteredLighting( result.specular += info.specular; #endif } - - // result.diffuse = vec3f(f32(mask >> 9) / f32(0x7fffff), 0, 0); return result; } diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/spotLightDeclaration.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/spotLightDeclaration.fx new file mode 100644 index 00000000000..a10fa8a1048 --- /dev/null +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/spotLightDeclaration.fx @@ -0,0 +1,8 @@ +// Used in clustered lights +struct SpotLight { + position: vec4f, + direction: vec4f, + diffuse: vec4f, + specular: vec4f, + falloff: vec4f, +} diff --git a/packages/dev/core/src/ShadersWGSL/default.fragment.fx b/packages/dev/core/src/ShadersWGSL/default.fragment.fx index b4984831d4f..77729743560 100644 --- a/packages/dev/core/src/ShadersWGSL/default.fragment.fx +++ b/packages/dev/core/src/ShadersWGSL/default.fragment.fx @@ -22,7 +22,7 @@ varying vColor: vec4f; #include // Lights -#include +#include #include[0..maxSimultaneousLights] #include diff --git a/packages/dev/core/src/ShadersWGSL/default.vertex.fx b/packages/dev/core/src/ShadersWGSL/default.vertex.fx index debc3a97c3e..753e5da859d 100644 --- a/packages/dev/core/src/ShadersWGSL/default.vertex.fx +++ b/packages/dev/core/src/ShadersWGSL/default.vertex.fx @@ -56,7 +56,7 @@ varying vColor: vec4f; #include #include -#include +#include #include<__decl__lightVxFragment>[0..maxSimultaneousLights] #include diff --git a/packages/dev/core/src/ShadersWGSL/lightProxy.fragment.fx b/packages/dev/core/src/ShadersWGSL/lightProxy.fragment.fx index c304f7043a9..3e75b35f66a 100644 --- a/packages/dev/core/src/ShadersWGSL/lightProxy.fragment.fx +++ b/packages/dev/core/src/ShadersWGSL/lightProxy.fragment.fx @@ -1,20 +1,12 @@ flat varying vMask: u32; // Declarations -#include +#include #include[0..1] @fragment fn main(input: FragmentInputs) -> FragmentOutputs { - let index = tileMaskIndex(light0.vLightData, fragmentInputs.position); - - // To reduce atomic contention we elect one wavefront to set the index - // However, as a fallback we also check for wavefronts that might have different indices - let elected = subgroupElect(); - let electedIndex = subgroupBroadcastFirst(index); - if elected || index != electedIndex { - atomicOr(&tileMaskBuffer0[index], fragmentInputs.vMask); - } - - // atomicOr(&tileMaskBuffer0[index], fragmentInputs.vMask); + let tilePos = vec2u(fragmentInputs.position.xy); + let stride = u32(light0.vLightData.z); + atomicOr(&tileMaskBuffer0[tilePos.y * stride + tilePos.x], fragmentInputs.vMask); } diff --git a/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx b/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx index 1959d4d1d7b..65b956ad653 100644 --- a/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx +++ b/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx @@ -6,7 +6,7 @@ flat varying vMask: u32; #include #include -#include +#include #include[0..1] const DOWN = vec3f(0, -1, 0); From 52f9108a28a876ecf87ea1446496265901684326 Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Thu, 17 Jul 2025 14:27:52 +1000 Subject: [PATCH 11/30] Add more options, prevent depth clipping --- .../src/Lights/Clustered/clusteredLight.ts | 44 ++++++++++++++----- .../Lights/Clustered/lightProxyMaterial.ts | 6 ++- .../dev/core/src/Shaders/lightProxy.vertex.fx | 6 ++- .../core/src/ShadersWGSL/lightProxy.vertex.fx | 11 +++-- 4 files changed, 49 insertions(+), 18 deletions(-) diff --git a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts index 14a6146865c..9b4ca885012 100644 --- a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts +++ b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts @@ -78,7 +78,7 @@ export class ClusteredLight extends Light { private _tileMaskTexture: Nullable; private _tileMaskBuffer: Nullable; - private _horizontalTiles = 128; // TODO: 64 + private _horizontalTiles = 64; public get horizontalTiles(): number { return this._horizontalTiles; } @@ -91,7 +91,7 @@ export class ClusteredLight extends Light { this._disposeTileMask(); } - private _verticalTiles = 128; + private _verticalTiles = 64; public get verticalTiles(): number { return this._verticalTiles; } @@ -104,20 +104,30 @@ export class ClusteredLight extends Light { this._disposeTileMask(); } - private readonly _proxyMesh: Mesh; + private _proxyMesh: Mesh; private readonly _proxyMatrixBuffer: Float32Array; + private _proxySegments = 4; + public get proxySegments(): number { + return this._proxySegments; + } + + public set proxySegments(segments: number) { + if (this._proxySegments === segments) { + return; + } + this._proxyMesh.dispose(false, true); + this._createProxyMesh(); + } + + public maxRange = 16383; + constructor(name: string, lights: Light[] = [], scene?: Scene) { super(name, scene); - const engine = this.getEngine(); - this.maxLights = ClusteredLight._GetEngineMaxLights(engine); - - this._proxyMesh = CreateSphere("ProxyMesh", { diameter: 2, segments: 8 }, this._scene); - this._proxyMesh.material = new LightProxyMaterial("ProxyMeshMaterial", this); - this._scene.removeMesh(this._proxyMesh); + this.maxLights = ClusteredLight._GetEngineMaxLights(this.getEngine()); this._proxyMatrixBuffer = new Float32Array(this.maxLights * 16); - this._proxyMesh.thinInstanceSetBuffer("matrix", this._proxyMatrixBuffer, 16, false); + this._createProxyMesh(); if (this.maxLights > 0) { ClusteredLight._SceneComponentInitialization(this._scene); @@ -200,6 +210,18 @@ export class ClusteredLight extends Light { this._tileMaskBuffer = null; } + private _createProxyMesh(): void { + this._proxyMesh = CreateSphere("ProxyMesh", { diameter: 2, segments: this._proxySegments }, this._scene); + // Make sure it doesn't render for the default scene + this._scene.removeMesh(this._proxyMesh); + if (this._tileMaskTexture) { + this._tileMaskTexture.renderList = [this._proxyMesh]; + } + + this._proxyMesh.material = new LightProxyMaterial("ProxyMeshMaterial", this); + this._proxyMesh.thinInstanceSetBuffer("matrix", this._proxyMatrixBuffer, 16, false); + } + public override dispose(doNotRecurse?: boolean, disposeMaterialAndTextures?: boolean): void { for (const light of this._lights) { light.dispose(doNotRecurse, disposeMaterialAndTextures); @@ -271,7 +293,7 @@ export class ClusteredLight extends Light { const scaledIntensity = light.getScaledIntensity(); light.diffuse.scaleToRef(scaledIntensity, TmpColors.Color3[0]); - this._uniformBuffer.updateColor4(struct + "diffuse", TmpColors.Color3[0], light.range, lightIndex); + this._uniformBuffer.updateColor4(struct + "diffuse", TmpColors.Color3[0], Math.min(light.range, this.maxRange), lightIndex); light.specular.scaleToRef(scaledIntensity, TmpColors.Color3[1]); this._uniformBuffer.updateColor4(struct + "specular", TmpColors.Color3[1], light.radius, lightIndex); diff --git a/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts b/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts index 615488927f9..d01230b036e 100644 --- a/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts +++ b/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts @@ -13,13 +13,15 @@ export class LightProxyMaterial extends ShaderMaterial { constructor(name: string, clusteredLight: ClusteredLight) { const engine = clusteredLight.getEngine(); const shader = { vertex: "lightProxy", fragment: "lightProxy" }; + // The angle between two vertical segments on the sphere + const segmentAngle = Math.PI / (clusteredLight.proxySegments + 2); super(name, clusteredLight._scene, shader, { attributes: ["position"], uniforms: ["world"], uniformBuffers: ["Scene", "Mesh", "Light0"], storageBuffers: ["tileMaskBuffer0"], - defines: ["LIGHT0", "CLUSTLIGHT0", `CLUSTLIGHT_MAX ${clusteredLight.maxLights}`, "CLUSTLIGHT_WRITE"], + defines: ["LIGHT0", "CLUSTLIGHT0", "CLUSTLIGHT_WRITE", `CLUSTLIGHT_MAX ${clusteredLight.maxLights}`, `SEGMENT_ANGLE ${segmentAngle}`], shaderLanguage: engine.isWebGPU ? ShaderLanguage.WGSL : ShaderLanguage.GLSL, extraInitializationsAsync: async () => { if (engine.isWebGPU) { @@ -34,6 +36,8 @@ export class LightProxyMaterial extends ShaderMaterial { this.cullBackFaces = false; this.transparencyMode = ShaderMaterial.MATERIAL_ALPHABLEND; this.alphaMode = Constants.ALPHA_ADD; + + // this.fillMode = Constants.MATERIAL_WireFrameFillMode; } public override bindForSubMesh(world: Matrix, mesh: Mesh, subMesh: SubMesh): void { diff --git a/packages/dev/core/src/Shaders/lightProxy.vertex.fx b/packages/dev/core/src/Shaders/lightProxy.vertex.fx index 8207a6f0a67..e43cea4a978 100644 --- a/packages/dev/core/src/Shaders/lightProxy.vertex.fx +++ b/packages/dev/core/src/Shaders/lightProxy.vertex.fx @@ -18,8 +18,8 @@ float acosClamped(float v) { void main(void) { float maxAngle = acosClamped(light0.vLights[gl_InstanceID].direction.a); - // We allow some wiggle room equal to the rotation of one slice of the sphere - maxAngle += 0.32; + // We allow some wiggle room equal to the difference between two vertical sphere segments + maxAngle += SEGMENT_ANGLE; float angle = acosClamped(dot(DOWN, position)); vec3 positionUpdated = angle < maxAngle ? position : vec3(0); @@ -28,5 +28,7 @@ void main(void) { #include gl_Position = viewProjection * finalWorld * vec4(positionUpdated, 1); + // Since we don't write to depth just set this to 0 to prevent clipping + gl_Position.z = 0.0; vMask = 1u << gl_InstanceID; } diff --git a/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx b/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx index 65b956ad653..f0ca1fb6ac4 100644 --- a/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx +++ b/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx @@ -18,12 +18,13 @@ fn acosClamped(v: f32) -> f32 { @vertex fn main(input: VertexInputs) -> FragmentInputs { let light = &light0.vLights[vertexInputs.instanceIndex]; - let maxAngle = acosClamped(light.direction.w); - let angle = acosClamped(dot(DOWN, vertexInputs.position)); + var maxAngle = acosClamped(light.direction.w); + // We allow some wiggle room equal to the difference between two vertical sphere segments + maxAngle += SEGMENT_ANGLE; + let angle = acosClamped(dot(DOWN, vertexInputs.position)); var positionUpdated = vec3f(0); - // We allow some wiggle room equal to the rotation of one slice of the sphere - if angle - maxAngle < 0.32 { + if angle < maxAngle { positionUpdated = vertexInputs.position; } positionUpdated *= light.diffuse.a; @@ -31,5 +32,7 @@ fn main(input: VertexInputs) -> FragmentInputs { #include vertexOutputs.position = scene.viewProjection * finalWorld * vec4f(positionUpdated, 1); + // Since we don't write to depth just set this to 0 to prevent clipping + vertexOutputs.position.z = 0.0; vertexOutputs.vMask = 1u << vertexInputs.instanceIndex; } From d8e954a63b15d75ab7e37d8f1a7e24e0a4922aae Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Fri, 18 Jul 2025 08:29:08 +1000 Subject: [PATCH 12/30] Scale matrices --- .../src/Lights/Clustered/clusteredLight.ts | 39 ++++++++++++++++--- .../dev/core/src/Shaders/lightProxy.vertex.fx | 1 - .../core/src/ShadersWGSL/lightProxy.vertex.fx | 1 - 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts index 9b4ca885012..80d8ddba969 100644 --- a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts +++ b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts @@ -5,7 +5,7 @@ import type { WebGPUEngine } from "core/Engines/webgpuEngine"; import type { Effect } from "core/Materials/effect"; import { RenderTargetTexture } from "core/Materials/Textures/renderTargetTexture"; import { TmpColors } from "core/Maths/math.color"; -import { Vector3 } from "core/Maths/math.vector"; +import { Matrix, TmpVectors, Vector3 } from "core/Maths/math.vector"; import { CreateSphere } from "core/Meshes/Builders/sphereBuilder"; import type { Mesh } from "core/Meshes/mesh"; import { _WarnImport } from "core/Misc/devTools"; @@ -106,6 +106,7 @@ export class ClusteredLight extends Light { private _proxyMesh: Mesh; private readonly _proxyMatrixBuffer: Float32Array; + private _proxyRenderId = -1; private _proxySegments = 4; public get proxySegments(): number { @@ -120,7 +121,19 @@ export class ClusteredLight extends Light { this._createProxyMesh(); } - public maxRange = 16383; + private _maxRange = 16383; + public get maxRange(): number { + return this._maxRange; + } + + public set maxRange(range: number) { + if (this._maxRange === range) { + return; + } + this._maxRange = range; + // Cause the matrix buffer to update + this._proxyRenderId = -1; + } constructor(name: string, lights: Light[] = [], scene?: Scene) { super(name, scene); @@ -147,6 +160,13 @@ export class ClusteredLight extends Light { } private _updateMatrixBuffer(): void { + const renderId = this._scene.getRenderId(); + if (this._proxyRenderId === renderId) { + // Prevent updates in the same render + return; + } + this._proxyRenderId = renderId; + const len = Math.min(this._lights.length, this.maxLights); if (len === 0) { // Nothing to render @@ -158,10 +178,15 @@ export class ClusteredLight extends Light { this._proxyMesh.thinInstanceCount = len; for (let i = 0; i < len; i += 1) { const light = this._lights[i]; - // TODO: cache matrices, somehow detect unchanged? - // TODO: scale by range of light + let matrix = light.getWorldMatrix(); + + // Scale by the range of the light + const range = Math.min(light.range, this.maxRange); + const scaling = Matrix.ScalingToRef(range, range, range, TmpVectors.Matrix[0]); + matrix = scaling.multiplyToRef(matrix, TmpVectors.Matrix[1]); + // TODO: rotate spotlights to face direction - light.getWorldMatrix().copyToArray(this._proxyMatrixBuffer, i * 16); + matrix.copyToArray(this._proxyMatrixBuffer, i * 16); } this._proxyMesh.thinInstanceBufferUpdated("matrix"); } @@ -243,6 +268,8 @@ export class ClusteredLight extends Light { } this._scene.removeLight(light); this._lights.push(light); + // Cause the matrix buffer to update + this._proxyRenderId = -1; } protected override _buildUniformLayout(): void { @@ -293,7 +320,7 @@ export class ClusteredLight extends Light { const scaledIntensity = light.getScaledIntensity(); light.diffuse.scaleToRef(scaledIntensity, TmpColors.Color3[0]); - this._uniformBuffer.updateColor4(struct + "diffuse", TmpColors.Color3[0], Math.min(light.range, this.maxRange), lightIndex); + this._uniformBuffer.updateColor4(struct + "diffuse", TmpColors.Color3[0], light.range, lightIndex); light.specular.scaleToRef(scaledIntensity, TmpColors.Color3[1]); this._uniformBuffer.updateColor4(struct + "specular", TmpColors.Color3[1], light.radius, lightIndex); diff --git a/packages/dev/core/src/Shaders/lightProxy.vertex.fx b/packages/dev/core/src/Shaders/lightProxy.vertex.fx index e43cea4a978..ed087909a03 100644 --- a/packages/dev/core/src/Shaders/lightProxy.vertex.fx +++ b/packages/dev/core/src/Shaders/lightProxy.vertex.fx @@ -23,7 +23,6 @@ void main(void) { float angle = acosClamped(dot(DOWN, position)); vec3 positionUpdated = angle < maxAngle ? position : vec3(0); - positionUpdated *= light0.vLights[gl_InstanceID].diffuse.a; #include diff --git a/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx b/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx index f0ca1fb6ac4..265f75f8dad 100644 --- a/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx +++ b/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx @@ -27,7 +27,6 @@ fn main(input: VertexInputs) -> FragmentInputs { if angle < maxAngle { positionUpdated = vertexInputs.position; } - positionUpdated *= light.diffuse.a; #include From fb795968895363e2573b1c598a387bbf16cca06b Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Fri, 18 Jul 2025 09:19:21 +1000 Subject: [PATCH 13/30] PointLight support --- .../src/Lights/Clustered/clusteredLight.ts | 32 +++++++------------ .../ShadersInclude/lightsFragmentFunctions.fx | 4 +++ 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts index 80d8ddba969..bc1deb19cd0 100644 --- a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts +++ b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts @@ -303,20 +303,12 @@ export class ClusteredLight extends Light { for (let i = 0; i < len; i += 1) { const light = this._lights[i]; - const spotLight = light instanceof SpotLight ? light : null; const struct = `vLights[${i}].`; - let position: Vector3; - let direction: Vector3; - if (light.computeTransformedInformation()) { - position = light.transformedPosition; - direction = Vector3.Normalize(light.transformedDirection); - } else { - position = light.position; - direction = Vector3.Normalize(light.direction); - } - this._uniformBuffer.updateFloat4(struct + "position", position.x, position.y, position.z, spotLight?.exponent ?? 0, lightIndex); - this._uniformBuffer.updateFloat4(struct + "direction", direction.x, direction.y, direction.z, spotLight?._cosHalfAngle ?? -1, lightIndex); + const computed = light.computeTransformedInformation(); + const position = computed ? light.transformedPosition : light.position; + const exponent = light instanceof SpotLight ? light.exponent : 0; + this._uniformBuffer.updateFloat4(struct + "position", position.x, position.y, position.z, exponent, lightIndex); const scaledIntensity = light.getScaledIntensity(); light.diffuse.scaleToRef(scaledIntensity, TmpColors.Color3[0]); @@ -324,14 +316,14 @@ export class ClusteredLight extends Light { light.specular.scaleToRef(scaledIntensity, TmpColors.Color3[1]); this._uniformBuffer.updateColor4(struct + "specular", TmpColors.Color3[1], light.radius, lightIndex); - this._uniformBuffer.updateFloat4( - struct + "falloff", - light.range, - light._inverseSquaredRange, - spotLight?._lightAngleScale ?? 0, - spotLight?._lightAngleOffset ?? 0, - lightIndex - ); + if (light instanceof SpotLight) { + const direction = Vector3.Normalize(computed ? light.transformedDirection : light.direction); + this._uniformBuffer.updateFloat4(struct + "direction", direction.x, direction.y, direction.z, light._cosHalfAngle, lightIndex); + this._uniformBuffer.updateFloat4(struct + "falloff", light.range, light._inverseSquaredRange, light._lightAngleScale, light._lightAngleOffset, lightIndex); + } else { + this._uniformBuffer.updateFloat4(struct + "direction", 0, 1, 0, -1, lightIndex); + this._uniformBuffer.updateFloat4(struct + "falloff", light.range, light._inverseSquaredRange, 0.5, 0.5, lightIndex); + } } return this; } diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx index 4eeda6b1a8a..2c32c2d383f 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx @@ -45,6 +45,10 @@ lightingInfo computeLighting(vec3 viewDirectionW, vec3 vNormal, vec4 lightData, } float getAttenuation(float cosAngle, float exponent) { + if (exponent == 0.0) { + // Undefined behaviour can occur if exponent is == 0, the result in reality should always be 1 + return 1.0; + } return max(0., pow(cosAngle, exponent)); } From 536eec5e1f3aaceef566999fb6236554b7d4b729 Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Fri, 18 Jul 2025 09:19:40 +1000 Subject: [PATCH 14/30] Comment change --- .../core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx index 2c32c2d383f..5ad4527ad37 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx @@ -46,7 +46,7 @@ lightingInfo computeLighting(vec3 viewDirectionW, vec3 vNormal, vec4 lightData, float getAttenuation(float cosAngle, float exponent) { if (exponent == 0.0) { - // Undefined behaviour can occur if exponent is == 0, the result in reality should always be 1 + // Undefined behaviour can occur if exponent == 0, the result in reality should always be 1 return 1.0; } return max(0., pow(cosAngle, exponent)); From ba10f9d3938c19e11408e6344210b48d7a99d672 Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Fri, 18 Jul 2025 11:52:10 +1000 Subject: [PATCH 15/30] Fix WebGL performance --- .../ShadersInclude/clusteredLightFunctions.fx | 22 +++++++++++++++++ .../Shaders/ShadersInclude/lightFragment.fx | 2 +- .../ShadersInclude/lightsFragmentFunctions.fx | 24 ------------------- .../dev/core/src/Shaders/default.fragment.fx | 3 +++ .../ShadersInclude/lightsFragmentFunctions.fx | 6 ++++- 5 files changed, 31 insertions(+), 26 deletions(-) create mode 100644 packages/dev/core/src/Shaders/ShadersInclude/clusteredLightFunctions.fx diff --git a/packages/dev/core/src/Shaders/ShadersInclude/clusteredLightFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/clusteredLightFunctions.fx new file mode 100644 index 00000000000..ec873cbada0 --- /dev/null +++ b/packages/dev/core/src/Shaders/ShadersInclude/clusteredLightFunctions.fx @@ -0,0 +1,22 @@ +#if defined(LIGHT{X}) && defined(CLUSTLIGHT{X}) && CLUSTLIGHT_MAX > 0 +lightingInfo computeClusteredLighting{X}(vec3 viewDirectionW, vec3 vNormal, vec3 diffuseScale, float glossiness) { + lightingInfo result; + vec4 maskTexel = texelFetch(tileMaskTexture{X}, ivec2(gl_FragCoord.xy * light{X}.vLightData.xy), 0); + uint mask = uint(maskTexel.r); + + int len = int(light{X}.vLightData.w); + for (int i = 0; i < len; i += 1) { + if ((mask & (1u << i)) == 0u) { + continue; + } + vec3 diffuse = light{X}.vLights[i].diffuse.rgb * diffuseScale; + vec3 specular = light{X}.vLights[i].specular.rgb * light{X}.vLightSpecular.rgb; + lightingInfo info = computeSpotLighting(viewDirectionW, vNormal, light{X}.vLights[i].position, light{X}.vLights[i].direction, diffuse, specular, light{X}.vLights[i].diffuse.a, glossiness); + result.diffuse += info.diffuse; + #ifdef SPECULARTERM + result.specular += info.specular; + #endif + } + return result; +} +#endif diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx index d7f63dad391..3dcf60faf8f 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx @@ -206,7 +206,7 @@ #endif ); #elif defined(CLUSTLIGHT{X}) && CLUSTLIGHT_MAX > 0 - info = computeClusteredLighting(tileMaskTexture{X}, viewDirectionW, normalW, light{X}.vLightData, light{X}.vLights, diffuse{X}.rgb, light{X}.vLightSpecular.rgb, glossiness); + info = computeClusteredLighting{X}(viewDirectionW, normalW, diffuse{X}.rgb, glossiness); #endif #endif diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx index 5ad4527ad37..edc48284867 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx @@ -183,27 +183,3 @@ lightingInfo computeAreaLighting(sampler2D ltc1, sampler2D ltc2, vec3 viewDirect // End Area Light #endif - -#if defined(CLUSTLIGHT_MAX) && CLUSTLIGHT_MAX > 0 -#define inline -lightingInfo computeClusteredLighting(sampler2D tileMask, vec3 viewDirectionW, vec3 vNormal, vec4 lightData, SpotLight lights[CLUSTLIGHT_MAX], vec3 diffuseScale, vec3 specularScale, float glossiness) { - lightingInfo result; - vec4 maskTexel = texelFetch(tileMask, ivec2(gl_FragCoord.xy * lightData.xy), 0); - uint mask = uint(maskTexel.r); - - int len = int(lightData.w); - for (int i = 0; i < len; i += 1) { - if ((mask & (1u << i)) == 0u) { - continue; - } - vec3 diffuse = lights[i].diffuse.rgb * diffuseScale; - vec3 specular = lights[i].specular.rgb * specularScale; - lightingInfo info = computeSpotLighting(viewDirectionW, vNormal, lights[i].position, lights[i].direction, diffuse.rgb, specular, lights[i].diffuse.a, glossiness); - result.diffuse += info.diffuse; - #ifdef SPECULARTERM - result.specular += info.specular; - #endif - } - return result; -} -#endif diff --git a/packages/dev/core/src/Shaders/default.fragment.fx b/packages/dev/core/src/Shaders/default.fragment.fx index 2c26ce0e05d..c53abc6c072 100644 --- a/packages/dev/core/src/Shaders/default.fragment.fx +++ b/packages/dev/core/src/Shaders/default.fragment.fx @@ -37,6 +37,9 @@ varying vec4 vColor; #include #include +// WebGL does not support passing uniform arrays by reference and the copy heavily impacts performance. +// Bit of a hacky solution but we can just create a function per clustered light that references the uniforms directly. +#include[0..maxSimultaneousLights] // Samplers #include(_DEFINENAME_,DIFFUSE,_VARYINGNAME_,Diffuse,_SAMPLERNAME_,diffuse) diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx index 1786f7313fc..0e563f8ef02 100644 --- a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx @@ -45,6 +45,10 @@ fn computeLighting(viewDirectionW: vec3f, vNormal: vec3f, lightData: vec4f, diff } fn getAttenuation(cosAngle: f32, exponent: f32) -> f32 { + if (exponent == 0.0) { + // Undefined behaviour can occur if exponent == 0, the result in reality should always be 1 + return 1.0; + } return max(0., pow(cosAngle, exponent)); } @@ -203,7 +207,7 @@ fn computeClusteredLighting( } let diffuse = lights[i].diffuse.rgb * diffuseScale; let specular = lights[i].specular.rgb * specularScale; - let info = computeSpotLighting(viewDirectionW, vNormal, lights[i].position, lights[i].direction, diffuse.rgb, specular, lights[i].diffuse.a, glossiness); + let info = computeSpotLighting(viewDirectionW, vNormal, lights[i].position, lights[i].direction, diffuse, specular, lights[i].diffuse.a, glossiness); result.diffuse += info.diffuse; #ifdef SPECULARTERM result.specular += info.specular; From 2b05344810360d63440803135547da0057e1a8cf Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Tue, 22 Jul 2025 09:20:47 +1000 Subject: [PATCH 16/30] Expand proxy mesh, fix lack of conservative --- .../src/Lights/Clustered/clusteredLight.ts | 22 +++++++++-- .../Lights/Clustered/lightProxyMaterial.ts | 28 +++++++++----- .../dev/core/src/Shaders/lightProxy.vertex.fx | 37 ++++++++++++------- .../core/src/ShadersWGSL/lightProxy.vertex.fx | 33 +++++++++++------ 4 files changed, 81 insertions(+), 39 deletions(-) diff --git a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts index bc1deb19cd0..ed4853d13a9 100644 --- a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts +++ b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts @@ -104,8 +104,9 @@ export class ClusteredLight extends Light { this._disposeTileMask(); } - private _proxyMesh: Mesh; + private readonly _proxyMaterial: LightProxyMaterial; private readonly _proxyMatrixBuffer: Float32Array; + private _proxyMesh: Mesh; private _proxyRenderId = -1; private _proxySegments = 4; @@ -117,8 +118,10 @@ export class ClusteredLight extends Light { if (this._proxySegments === segments) { return; } - this._proxyMesh.dispose(false, true); + this._proxySegments = segments; + this._proxyMesh.dispose(); this._createProxyMesh(); + this._proxyMaterial._updateUniforms(); } private _maxRange = 16383; @@ -139,6 +142,7 @@ export class ClusteredLight extends Light { super(name, scene); this.maxLights = ClusteredLight._GetEngineMaxLights(this.getEngine()); + this._proxyMaterial = new LightProxyMaterial("ProxyMaterial", this); this._proxyMatrixBuffer = new Float32Array(this.maxLights * 16); this._createProxyMesh(); @@ -225,6 +229,8 @@ export class ClusteredLight extends Light { this._tileMaskBuffer = new StorageBuffer(engine, bufferSize); } + // If the tile mask was disposed it means the tile counts have changed + this._proxyMaterial._updateUniforms(); return this._tileMaskTexture; } @@ -236,14 +242,22 @@ export class ClusteredLight extends Light { } private _createProxyMesh(): void { - this._proxyMesh = CreateSphere("ProxyMesh", { diameter: 2, segments: this._proxySegments }, this._scene); + // Compute the lowest point in the sphere and expand the diameter accordingly + const rotationZ = Matrix.RotationZ(-Math.PI / (2 + this._proxySegments)); + const rotationY = Matrix.RotationY(Math.PI / this._proxySegments); + const start = Vector3.Up(); + const afterRotZ = Vector3.TransformCoordinates(start, rotationZ); + const end = Vector3.TransformCoordinates(afterRotZ, rotationY); + const midPoint = Vector3.Lerp(start, end, 0.5); + this._proxyMesh = CreateSphere("ProxyMesh", { diameter: 2 / midPoint.length(), segments: this._proxySegments }, this._scene); + // Make sure it doesn't render for the default scene this._scene.removeMesh(this._proxyMesh); if (this._tileMaskTexture) { this._tileMaskTexture.renderList = [this._proxyMesh]; } - this._proxyMesh.material = new LightProxyMaterial("ProxyMeshMaterial", this); + this._proxyMesh.material = this._proxyMaterial; this._proxyMesh.thinInstanceSetBuffer("matrix", this._proxyMatrixBuffer, 16, false); } diff --git a/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts b/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts index d01230b036e..009646cabc9 100644 --- a/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts +++ b/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts @@ -1,7 +1,7 @@ import { Constants } from "core/Engines/constants"; import { ShaderLanguage } from "core/Materials/shaderLanguage"; import { ShaderMaterial } from "core/Materials/shaderMaterial"; -import type { Matrix } from "core/Maths/math.vector"; +import { Vector2, type Matrix } from "core/Maths/math.vector"; import type { Mesh } from "core/Meshes/mesh"; import type { SubMesh } from "core/Meshes/subMesh"; @@ -11,20 +11,17 @@ export class LightProxyMaterial extends ShaderMaterial { private readonly _clusteredLight: ClusteredLight; constructor(name: string, clusteredLight: ClusteredLight) { - const engine = clusteredLight.getEngine(); const shader = { vertex: "lightProxy", fragment: "lightProxy" }; - // The angle between two vertical segments on the sphere - const segmentAngle = Math.PI / (clusteredLight.proxySegments + 2); - + const webgpu = clusteredLight.getEngine().isWebGPU; super(name, clusteredLight._scene, shader, { attributes: ["position"], - uniforms: ["world"], + uniforms: ["world", "angleBias", "positionBias"], uniformBuffers: ["Scene", "Mesh", "Light0"], storageBuffers: ["tileMaskBuffer0"], - defines: ["LIGHT0", "CLUSTLIGHT0", "CLUSTLIGHT_WRITE", `CLUSTLIGHT_MAX ${clusteredLight.maxLights}`, `SEGMENT_ANGLE ${segmentAngle}`], - shaderLanguage: engine.isWebGPU ? ShaderLanguage.WGSL : ShaderLanguage.GLSL, + defines: ["LIGHT0", "CLUSTLIGHT0", "CLUSTLIGHT_WRITE", `CLUSTLIGHT_MAX ${clusteredLight.maxLights}`], + shaderLanguage: webgpu ? ShaderLanguage.WGSL : ShaderLanguage.GLSL, extraInitializationsAsync: async () => { - if (engine.isWebGPU) { + if (webgpu) { await Promise.all([import("../../ShadersWGSL/lightProxy.vertex"), import("../../ShadersWGSL/lightProxy.fragment")]); } else { await Promise.all([import("../../Shaders/lightProxy.vertex"), import("../../Shaders/lightProxy.fragment")]); @@ -37,7 +34,18 @@ export class LightProxyMaterial extends ShaderMaterial { this.transparencyMode = ShaderMaterial.MATERIAL_ALPHABLEND; this.alphaMode = Constants.ALPHA_ADD; - // this.fillMode = Constants.MATERIAL_WireFrameFillMode; + this._updateUniforms(); + } + + /** @internal */ + public _updateUniforms(): void { + // Bias the angle by one sphere segment so the spotlight is slightly too large instead of slightly too small + const angleBias = -Math.PI / (this._clusteredLight.proxySegments + 2); + this.setFloat("angleBias", angleBias); + // Bias the NDC position by one tile so all tiles it overlaps with gets filled (in lieu of conservative rendering) + // We also add a little extra offset just to counteract any inaccuracies + const positionBias = new Vector2(2 / this._clusteredLight.horizontalTiles + 0.001, 2 / this._clusteredLight.verticalTiles + 0.001); + this.setVector2("positionBias", positionBias); } public override bindForSubMesh(world: Matrix, mesh: Mesh, subMesh: SubMesh): void { diff --git a/packages/dev/core/src/Shaders/lightProxy.vertex.fx b/packages/dev/core/src/Shaders/lightProxy.vertex.fx index ed087909a03..86068f7068d 100644 --- a/packages/dev/core/src/Shaders/lightProxy.vertex.fx +++ b/packages/dev/core/src/Shaders/lightProxy.vertex.fx @@ -1,6 +1,9 @@ attribute vec3 position; flat varying highp uint vMask; +uniform float angleBias; +uniform vec2 positionBias; + // Declarations #include #include @@ -12,22 +15,28 @@ flat varying highp uint vMask; // TODO: switch default direction to up?? const vec3 DOWN = vec3(0, -1, 0); -float acosClamped(float v) { - return acos(clamp(v, 0.0, 1.0)); -} - void main(void) { - float maxAngle = acosClamped(light0.vLights[gl_InstanceID].direction.a); - // We allow some wiggle room equal to the difference between two vertical sphere segments - maxAngle += SEGMENT_ANGLE; - - float angle = acosClamped(dot(DOWN, position)); - vec3 positionUpdated = angle < maxAngle ? position : vec3(0); - #include - gl_Position = viewProjection * finalWorld * vec4(positionUpdated, 1); - // Since we don't write to depth just set this to 0 to prevent clipping - gl_Position.z = 0.0; + // Get the center (last column) and transformed offset (everything but the last column) of the projected position + vec4 projPosition = viewProjection * finalWorld[3]; + vec4 offset = viewProjection * (mat3x4(finalWorld) * position); + + // For spot lights we keep it at the center if its larger than the spotlight angle + float maxAngle = acos(light0.vLights[gl_InstanceID].direction.w); + // We use the original position for this angle, it will get rotated to face the spotlight direction + float angle = acos(dot(DOWN, normalize(position))) + angleBias; + if (angle < maxAngle) { + // Pointlights or positions within the angle of a spotlight + projPosition += offset; + } + + gl_Position = vec4( + // Offset the position in NDC space away from the center + projPosition.xy + sign(offset.xy) * positionBias * projPosition.w, + // Since we don't write to depth just set this to 0 to prevent clipping + 0, + projPosition.w + ); vMask = 1u << gl_InstanceID; } diff --git a/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx b/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx index 265f75f8dad..339dba75cf8 100644 --- a/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx +++ b/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx @@ -1,6 +1,9 @@ attribute position: vec3f; flat varying vMask: u32; +uniform angleBias: f32; +uniform positionBias: vec2f; + // Declarations #include #include @@ -17,21 +20,29 @@ fn acosClamped(v: f32) -> f32 { @vertex fn main(input: VertexInputs) -> FragmentInputs { +#include let light = &light0.vLights[vertexInputs.instanceIndex]; - var maxAngle = acosClamped(light.direction.w); - // We allow some wiggle room equal to the difference between two vertical sphere segments - maxAngle += SEGMENT_ANGLE; - let angle = acosClamped(dot(DOWN, vertexInputs.position)); - var positionUpdated = vec3f(0); + // Get the center (last column) and transformed offset (everything but the last column) of the projected position + var projPosition = scene.viewProjection * finalWorld[3]; + let finalWorld3 = mat3x4(finalWorld[0], finalWorld[1], finalWorld[2]); + let offset = scene.viewProjection * (finalWorld3 * vertexInputs.position); + + // For spot lights we keep it at the center if its larger than the spotlight angle + let maxAngle = acos(light.direction.w); + // We use the original position for this angle, it will get rotated to face the spotlight direction + let angle = acos(dot(DOWN, normalize(vertexInputs.position))) + uniforms.angleBias; if angle < maxAngle { - positionUpdated = vertexInputs.position; + // Pointlights or positions within the angle of a spotlight + projPosition += offset; } -#include - - vertexOutputs.position = scene.viewProjection * finalWorld * vec4f(positionUpdated, 1); - // Since we don't write to depth just set this to 0 to prevent clipping - vertexOutputs.position.z = 0.0; + vertexOutputs.position = vec4f( + // Offset the position in NDC space away from the center + projPosition.xy + sign(offset.xy) * uniforms.positionBias * projPosition.w, + // Since we don't write to depth just set this to 0 to prevent clipping + 0, + projPosition.w + ); vertexOutputs.vMask = 1u << vertexInputs.instanceIndex; } From 48fecf895ed11b923a52e74f4b79b1f8c1bb2a1f Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Tue, 22 Jul 2025 13:51:11 +1000 Subject: [PATCH 17/30] PBR materials (no webgpu yet) --- packages/dev/core/src/Engines/thinEngine.ts | 2 +- .../ShadersInclude/clusteredLightFunctions.fx | 34 ++-- .../Shaders/ShadersInclude/lightFragment.fx | 30 +++- .../pbrClusteredLightFunctions.fx | 146 ++++++++++++++++++ .../pbrDirectLightingFalloffFunctions.fx | 7 +- packages/dev/core/src/Shaders/pbr.fragment.fx | 5 + packages/dev/core/src/Shaders/pbr.vertex.fx | 1 + 7 files changed, 205 insertions(+), 20 deletions(-) create mode 100644 packages/dev/core/src/Shaders/ShadersInclude/pbrClusteredLightFunctions.fx diff --git a/packages/dev/core/src/Engines/thinEngine.ts b/packages/dev/core/src/Engines/thinEngine.ts index a808b2ef1dc..c10fe907897 100644 --- a/packages/dev/core/src/Engines/thinEngine.ts +++ b/packages/dev/core/src/Engines/thinEngine.ts @@ -122,7 +122,7 @@ export class ThinEngine extends AbstractEngine { { key: "Chrome/74.+?Mobile", capture: null, captureConstraint: null, targets: ["vao"] }, { key: "Mac OS.+Chrome/71", capture: null, captureConstraint: null, targets: ["vao"] }, { key: "Mac OS.+Chrome/72", capture: null, captureConstraint: null, targets: ["vao"] }, - { key: "Mac OS.+Chrome", capture: null, captureConstraint: null, targets: ["uniformBuffer"] }, + // { key: "Mac OS.+Chrome", capture: null, captureConstraint: null, targets: ["uniformBuffer"] }, { key: "Chrome/12\\d\\..+?Mobile", capture: null, captureConstraint: null, targets: ["uniformBuffer"] }, // desktop osx safari 15.4 { key: ".*AppleWebKit.*(15.4).*Safari", capture: null, captureConstraint: null, targets: ["antialias", "maxMSAASamples"] }, diff --git a/packages/dev/core/src/Shaders/ShadersInclude/clusteredLightFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/clusteredLightFunctions.fx index ec873cbada0..06326310a24 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/clusteredLightFunctions.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/clusteredLightFunctions.fx @@ -1,22 +1,22 @@ #if defined(LIGHT{X}) && defined(CLUSTLIGHT{X}) && CLUSTLIGHT_MAX > 0 lightingInfo computeClusteredLighting{X}(vec3 viewDirectionW, vec3 vNormal, vec3 diffuseScale, float glossiness) { - lightingInfo result; - vec4 maskTexel = texelFetch(tileMaskTexture{X}, ivec2(gl_FragCoord.xy * light{X}.vLightData.xy), 0); - uint mask = uint(maskTexel.r); + lightingInfo result; + vec4 maskTexel = texelFetch(tileMaskTexture{X}, ivec2(gl_FragCoord.xy * light{X}.vLightData.xy), 0); + uint mask = uint(maskTexel.r); - int len = int(light{X}.vLightData.w); - for (int i = 0; i < len; i += 1) { - if ((mask & (1u << i)) == 0u) { - continue; - } - vec3 diffuse = light{X}.vLights[i].diffuse.rgb * diffuseScale; - vec3 specular = light{X}.vLights[i].specular.rgb * light{X}.vLightSpecular.rgb; - lightingInfo info = computeSpotLighting(viewDirectionW, vNormal, light{X}.vLights[i].position, light{X}.vLights[i].direction, diffuse, specular, light{X}.vLights[i].diffuse.a, glossiness); - result.diffuse += info.diffuse; - #ifdef SPECULARTERM - result.specular += info.specular; - #endif - } - return result; + int len = int(light{X}.vLightData.w); + for (int i = 0; i < len; i += 1) { + if ((mask & (1u << i)) == 0u) { + continue; + } + vec3 diffuse = light{X}.vLights[i].diffuse.rgb * diffuseScale; + vec3 specular = light{X}.vLights[i].specular.rgb * light{X}.vLightSpecular.rgb; + lightingInfo info = computeSpotLighting(viewDirectionW, vNormal, light{X}.vLights[i].position, light{X}.vLights[i].direction, diffuse, specular, light{X}.vLights[i].diffuse.a, glossiness); + result.diffuse += info.diffuse; + #ifdef SPECULARTERM + result.specular += info.specular; + #endif + } + return result; } #endif diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx index 3dcf60faf8f..c3e4187c288 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx @@ -7,6 +7,33 @@ #define CUSTOM_LIGHT{X}_COLOR // Use to modify light color. Currently only supports diffuse. #ifdef PBR + #if defined(CLUSTLIGHT{X}) && CLUSTLIGHT_MAX > 0 + info = computeClusteredLighting{X}( + viewDirectionW, + normalW, + vPositionW, + surfaceAlbedo, + reflectivityOut, + diffuse{X}.rgb + #ifdef SS_TRANSLUCENCY + , subSurfaceOut + #endif + #ifdef SPECULARTERM + , light{X}.vLightSpecular.rgb + , AARoughnessFactors.x + #endif + #ifdef ANISOTROPIC + , anisotropicOut + #endif + #ifdef SHEEN + , sheenOut + #endif + #ifdef CLEARCOAT + , clearcoatOut + #endif + ); + #else + // Compute Pre Lighting infos #ifdef SPOTLIGHT{X} preInfo = computePointAndSpotPreLightingInfo(light{X}.vLightData, viewDirectionW, normalW, vPositionW); @@ -186,7 +213,8 @@ #endif #endif #endif - #else + #endif // CLUSTLIGHT{X} + #else // PBR #ifdef SPOTLIGHT{X} #ifdef IESLIGHTTEXTURE{X} info = computeIESSpotLighting(viewDirectionW, normalW, light{X}.vLightData, light{X}.vLightDirection, diffuse{X}.rgb, light{X}.vLightSpecular.rgb, diffuse{X}.a, glossiness, iesLightTexture{X}); diff --git a/packages/dev/core/src/Shaders/ShadersInclude/pbrClusteredLightFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/pbrClusteredLightFunctions.fx new file mode 100644 index 00000000000..c58a64a8311 --- /dev/null +++ b/packages/dev/core/src/Shaders/ShadersInclude/pbrClusteredLightFunctions.fx @@ -0,0 +1,146 @@ +#if defined(LIGHT{X}) && defined(CLUSTLIGHT{X}) && CLUSTLIGHT_MAX > 0 +lightingInfo computeClusteredLighting{X}( + vec3 V, + vec3 N, + vec3 posW, + vec3 surfaceAlbedo, + reflectivityOutParams reflectivityOut, + vec3 diffuseScale + #ifdef SS_TRANSLUCENCY + , subSurfaceOutParams subSurfaceOut + #endif + #ifdef SPECULARTERM + , vec3 specularScale + , float AARoughnessFactor + #endif + #ifdef ANISOTROPIC + , anisotropicOutParams anisotropicOut + #endif + #ifdef SHEEN + , sheenOutParams sheenOut + #endif + #ifdef CLEARCOAT + , clearcoatOutParams clearcoatOut + #endif +) { + float NdotV = absEps(dot(N, V)); +#include + #ifdef CLEARCOAT + specularEnvironmentR0 = clearcoatOut.specularEnvironmentR0; + #endif + + lightingInfo result; + vec4 maskTexel = texelFetch(tileMaskTexture{X}, ivec2(gl_FragCoord.xy * light{X}.vLightData.xy), 0); + uint mask = uint(maskTexel.r); + + int len = int(light{X}.vLightData.w); + for (int i = 0; i < len; i += 1) { + if ((mask & (1u << i)) == 0u) { + continue; + } + SpotLight light = light{X}.vLights[i]; + preLightingInfo preInfo = computePointAndSpotPreLightingInfo(light.position, V, N, posW); + + // Compute Attenuation infos + preInfo.NdotV = NdotV; + preInfo.attenuation = computeDistanceLightFalloff(preInfo.lightOffset, preInfo.lightDistanceSquared, light.falloff.x, light.falloff.y); + preInfo.attenuation *= computeDirectionalLightFalloff(light.direction.xyz, preInfo.L, light.direction.w, light.position.w, light.falloff.z, light.falloff.w); + + float radius = light.specular.a; + preInfo.roughness = adjustRoughnessFromLightProperties(reflectivityOut.roughness, radius, preInfo.lightDistance); + preInfo.diffuseRoughness = reflectivityOut.diffuseRoughness; + preInfo.surfaceAlbedo = surfaceAlbedo; + lightingInfo info; + + // Diffuse contribution + vec3 diffuse = light.diffuse.rgb * diffuseScale; + #ifdef SS_TRANSLUCENCY + #ifdef SS_TRANSLUCENCY_LEGACY + info.diffuse = computeDiffuseTransmittedLighting(preInfo, diffuse, subSurfaceOut.transmittance); + info.diffuseTransmission = vec3(0); + #else + info.diffuse = computeDiffuseLighting(preInfo, diffuse) * (1.0 - subSurfaceOut.translucencyIntensity); + info.diffuseTransmission = computeDiffuseTransmittedLighting(preInfo, diffuse, subSurfaceOut.transmittance); + #endif + #else + info.diffuse = computeDiffuseLighting(preInfo, diffuse); + #endif + + // Specular contribution + #ifdef SPECULARTERM + vec3 specular = light.specular.rgb * specularScale; + #if CONDUCTOR_SPECULAR_MODEL == CONDUCTOR_SPECULAR_MODEL_OPENPBR + vec3 metalFresnel = reflectivityOut.specularWeight * getF82Specular(preInfo.VdotH, specularEnvironmentR0, reflectivityOut.colorReflectanceF90, reflectivityOut.roughness); + vec3 dielectricFresnel = fresnelSchlickGGX(preInfo.VdotH, reflectivityOut.dielectricColorF0, reflectivityOut.colorReflectanceF90); + vec3 coloredFresnel = mix(dielectricFresnel, metalFresnel, reflectivityOut.metallic); + #else + vec3 coloredFresnel = fresnelSchlickGGX(preInfo.VdotH, specularEnvironmentR0, reflectivityOut.colorReflectanceF90); + #endif + #ifndef LEGACY_SPECULAR_ENERGY_CONSERVATION + float NdotH = dot(N, preInfo.H); + vec3 fresnel = fresnelSchlickGGX(NdotH, vec3(reflectanceF0), specularEnvironmentR90); + info.diffuse *= (vec3(1.0) - fresnel); + #endif + #ifdef ANISOTROPIC + info.specular = computeAnisotropicSpecularLighting(preInfo, V, N, anisotropicOut.anisotropicTangent, anisotropicOut.anisotropicBitangent, anisotropicOut.anisotropy, clearcoatOut.specularEnvironmentR0, specularEnvironmentR90, AARoughnessFactor, diffuse); + #else + info.specular = computeSpecularLighting(preInfo, N, specularEnvironmentR0, coloredFresnel, AARoughnessFactor, diffuse); + #endif + #endif + + // Sheen contribution + #ifdef SHEEN + #ifdef SHEEN_LINKWITHALBEDO + preInfo.roughness = sheenOut.sheenIntensity; + #endif + preInfo.roughness = adjustRoughnessFromLightProperties(sheenOut.sheenRoughness, radius, preInfo.lightDistance); + info.sheen = computeSheenLighting(preInfo, normalW, sheenOut.sheenColor, specularEnvironmentR90, AARoughnessFactor, diffuse); + #endif + + // Clear Coat contribution + #ifdef CLEARCOAT + preInfo.roughness = adjustRoughnessFromLightProperties(clearcoatOut.clearCoatRoughness, radius, preInfo.lightDistance); + info.clearCoat = computeClearCoatLighting(preInfo, clearcoatOut.clearCoatNormalW, clearcoatOut.clearCoatAARoughnessFactors.x, clearcoatOut.clearCoatIntensity, diffuse); + + #ifdef CLEARCOAT_TINT + // Absorption + float absorption = computeClearCoatLightingAbsorption(clearcoatOut.clearCoatNdotVRefract, preInfo.L, clearcoatOut.clearCoatNormalW, clearcoatOut.clearCoatColor, clearcoatOut.clearCoatThickness, clearcoatOut.clearCoatIntensity); + info.diffuse *= absorption; + #ifdef SS_TRANSLUCENCY + info.diffuseTransmission *= absorption; + #endif + #ifdef SPECULARTERM + info.specular *= absorption; + #endif + #endif + + info.diffuse *= info.clearCoat.w; + #ifdef SS_TRANSLUCENCY + info.diffuseTransmission *= info.clearCoat.w; + #endif + #ifdef SPECULARTERM + info.specular *= info.clearCoat.w; + #endif + #ifdef SHEEN + info.sheen *= info.clearCoat.w; + #endif + #endif + + // Apply contributions to result + result.diffuse += info.diffuse; + #ifdef SS_TRANSLUCENCY + result.diffuseTransmission += info.diffuseTransmission; + #endif + #ifdef SPECULARTERM + result.specular += info.specular; + #endif + #ifdef CLEARCOAT + result.clearCoat += info.clearCoat; + #endif + #ifdef SHEEN + result.sheen += info.sheen; + #endif + } + return result; +} +#endif diff --git a/packages/dev/core/src/Shaders/ShadersInclude/pbrDirectLightingFalloffFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/pbrDirectLightingFalloffFunctions.fx index 0d89954cfb4..61b22763833 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/pbrDirectLightingFalloffFunctions.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/pbrDirectLightingFalloffFunctions.fx @@ -40,7 +40,12 @@ float computeDirectionalLightFalloff_Standard(vec3 lightDirection, vec3 directio float cosAngle = maxEps(dot(-lightDirection, directionToLightCenterW)); if (cosAngle >= cosHalfAngle) { - falloff = max(0., pow(cosAngle, exponent)); + if (exponent == 0.0) { + // Undefined behaviour can occur if exponent == 0, the result in reality should always be 1 + falloff = 1.0; + } else { + falloff = max(0., pow(cosAngle, exponent)); + } } return falloff; diff --git a/packages/dev/core/src/Shaders/pbr.fragment.fx b/packages/dev/core/src/Shaders/pbr.fragment.fx index ac621aa07b2..24e06cb52bd 100644 --- a/packages/dev/core/src/Shaders/pbr.fragment.fx +++ b/packages/dev/core/src/Shaders/pbr.fragment.fx @@ -30,6 +30,7 @@ precision highp float; #include<__decl__pbrFragment> #include +#include #include<__decl__lightFragment>[0..maxSimultaneousLights] #include #include @@ -71,6 +72,10 @@ precision highp float; #include #include +// WebGL does not support passing uniform arrays by reference and the copy heavily impacts performance. +// Bit of a hacky solution but we can just create a function per clustered light that references the uniforms directly. +#include[0..maxSimultaneousLights] + // _____________________________ MAIN FUNCTION ____________________________ void main(void) { diff --git a/packages/dev/core/src/Shaders/pbr.vertex.fx b/packages/dev/core/src/Shaders/pbr.vertex.fx index 778fa33de08..73b2254ad98 100644 --- a/packages/dev/core/src/Shaders/pbr.vertex.fx +++ b/packages/dev/core/src/Shaders/pbr.vertex.fx @@ -98,6 +98,7 @@ varying vec4 vColor; #include #include #include +#include #include<__decl__lightVxFragment>[0..maxSimultaneousLights] #include From 84ee75c6f5f01a1053457d589db20445c6744530 Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Wed, 23 Jul 2025 10:20:38 +1000 Subject: [PATCH 18/30] Small tweaks --- .../core/src/Lights/Clustered/clusteredLight.ts | 16 +++++++++++++--- .../src/Lights/Clustered/lightProxyMaterial.ts | 2 ++ .../ShadersInclude/spotLightDeclaration.fx | 2 +- .../ShadersInclude/spotLightDeclaration.fx | 2 +- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts index ed4853d13a9..b0a5a0b587d 100644 --- a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts +++ b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts @@ -48,6 +48,9 @@ export class ClusteredLight extends Light { } else if (light.shadowEnabled && light._scene.shadowsEnabled && light.getShadowGenerators()) { // Shadows are not supported return false; + } else if (light.falloffType !== Light.FALLOFF_DEFAULT) { + // Only the default falloff is supported + return false; } else if (light instanceof PointLight) { return true; } else if (light instanceof SpotLight) { @@ -184,12 +187,19 @@ export class ClusteredLight extends Light { const light = this._lights[i]; let matrix = light.getWorldMatrix(); + // TODO + // if (light instanceof SpotLight) { + // // Rotate spotlights to face direction + // const quat = Quaternion.FromUnitVectorsToRef(Vector3.UpReadOnly, light.direction, TmpVectors.Quaternion[0]); + // const rotation = Matrix.FromQuaternionToRef(quat, TmpVectors.Matrix[0]); + // matrix = rotation.multiplyToRef(matrix, TmpVectors.Matrix[1]); + // } + // Scale by the range of the light const range = Math.min(light.range, this.maxRange); const scaling = Matrix.ScalingToRef(range, range, range, TmpVectors.Matrix[0]); - matrix = scaling.multiplyToRef(matrix, TmpVectors.Matrix[1]); + matrix = scaling.multiplyToRef(matrix, TmpVectors.Matrix[2]); - // TODO: rotate spotlights to face direction matrix.copyToArray(this._proxyMatrixBuffer, i * 16); } this._proxyMesh.thinInstanceBufferUpdated("matrix"); @@ -297,9 +307,9 @@ export class ClusteredLight extends Light { // These technically don't have to match the field name but also why not const struct = `vLights[${i}].`; this._uniformBuffer.addUniform(struct + "position", 4); - this._uniformBuffer.addUniform(struct + "direction", 4); this._uniformBuffer.addUniform(struct + "diffuse", 4); this._uniformBuffer.addUniform(struct + "specular", 4); + this._uniformBuffer.addUniform(struct + "direction", 4); this._uniformBuffer.addUniform(struct + "falloff", 4); } this._uniformBuffer.addUniform("shadowsInfo", 3); diff --git a/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts b/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts index 009646cabc9..d8edbdda7ed 100644 --- a/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts +++ b/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts @@ -30,7 +30,9 @@ export class LightProxyMaterial extends ShaderMaterial { }); this._clusteredLight = clusteredLight; + // Cull front faces so it still shows when intersecting with the camera this.cullBackFaces = false; + // Additive blending is for merging masks on WebGL this.transparencyMode = ShaderMaterial.MATERIAL_ALPHABLEND; this.alphaMode = Constants.ALPHA_ADD; diff --git a/packages/dev/core/src/Shaders/ShadersInclude/spotLightDeclaration.fx b/packages/dev/core/src/Shaders/ShadersInclude/spotLightDeclaration.fx index 2ee2ceb90e9..b620d530cd6 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/spotLightDeclaration.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/spotLightDeclaration.fx @@ -1,8 +1,8 @@ // Used in clustered lights struct SpotLight { vec4 position; - vec4 direction; vec4 diffuse; vec4 specular; + vec4 direction; vec4 falloff; }; diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/spotLightDeclaration.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/spotLightDeclaration.fx index a10fa8a1048..80ef25338c3 100644 --- a/packages/dev/core/src/ShadersWGSL/ShadersInclude/spotLightDeclaration.fx +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/spotLightDeclaration.fx @@ -1,8 +1,8 @@ // Used in clustered lights struct SpotLight { position: vec4f, - direction: vec4f, diffuse: vec4f, specular: vec4f, + direction: vec4f, falloff: vec4f, } From 9d9c272c3d1aa079f8632f1c66a210ee65944a04 Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Wed, 23 Jul 2025 14:49:28 +1000 Subject: [PATCH 19/30] Switch to using disc mesh --- .../src/Lights/Clustered/clusteredLight.ts | 120 ++++++------------ .../Lights/Clustered/lightProxyMaterial.ts | 21 +-- .../dev/core/src/Shaders/lightProxy.vertex.fx | 40 ++---- .../core/src/ShadersWGSL/lightProxy.vertex.fx | 43 ++----- 4 files changed, 66 insertions(+), 158 deletions(-) diff --git a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts index b0a5a0b587d..273497c0298 100644 --- a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts +++ b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts @@ -5,8 +5,8 @@ import type { WebGPUEngine } from "core/Engines/webgpuEngine"; import type { Effect } from "core/Materials/effect"; import { RenderTargetTexture } from "core/Materials/Textures/renderTargetTexture"; import { TmpColors } from "core/Maths/math.color"; -import { Matrix, TmpVectors, Vector3 } from "core/Maths/math.vector"; -import { CreateSphere } from "core/Meshes/Builders/sphereBuilder"; +import { Vector3 } from "core/Maths/math.vector"; +import { CreateDisc } from "core/Meshes/Builders/discBuilder"; import type { Mesh } from "core/Meshes/mesh"; import { _WarnImport } from "core/Misc/devTools"; import { Logger } from "core/Misc/logger"; @@ -107,27 +107,24 @@ export class ClusteredLight extends Light { this._disposeTileMask(); } - private readonly _proxyMaterial: LightProxyMaterial; - private readonly _proxyMatrixBuffer: Float32Array; - private _proxyMesh: Mesh; - private _proxyRenderId = -1; + private _lightProxy: Mesh; - private _proxySegments = 4; - public get proxySegments(): number { - return this._proxySegments; + private _proxyTesselation = 8; + public get proxyTesselation(): number { + return this._proxyTesselation; } - public set proxySegments(segments: number) { - if (this._proxySegments === segments) { + public set proxyTesselation(tesselation: number) { + if (this._proxyTesselation === tesselation) { return; } - this._proxySegments = segments; - this._proxyMesh.dispose(); + this._proxyTesselation = tesselation; + this._lightProxy.dispose(false, true); this._createProxyMesh(); - this._proxyMaterial._updateUniforms(); } private _maxRange = 16383; + private _minInverseSquaredRange = 1 / (this._maxRange * this._maxRange); public get maxRange(): number { return this._maxRange; } @@ -137,16 +134,13 @@ export class ClusteredLight extends Light { return; } this._maxRange = range; - // Cause the matrix buffer to update - this._proxyRenderId = -1; + this._minInverseSquaredRange = 1 / (range * range); } constructor(name: string, lights: Light[] = [], scene?: Scene) { super(name, scene); this.maxLights = ClusteredLight._GetEngineMaxLights(this.getEngine()); - this._proxyMaterial = new LightProxyMaterial("ProxyMaterial", this); - this._proxyMatrixBuffer = new Float32Array(this.maxLights * 16); this._createProxyMesh(); if (this.maxLights > 0) { @@ -166,45 +160,6 @@ export class ClusteredLight extends Light { return LightConstants.LIGHTTYPEID_CLUSTERED; } - private _updateMatrixBuffer(): void { - const renderId = this._scene.getRenderId(); - if (this._proxyRenderId === renderId) { - // Prevent updates in the same render - return; - } - this._proxyRenderId = renderId; - - const len = Math.min(this._lights.length, this.maxLights); - if (len === 0) { - // Nothing to render - this._proxyMesh.isVisible = false; - return; - } - - this._proxyMesh.isVisible = true; - this._proxyMesh.thinInstanceCount = len; - for (let i = 0; i < len; i += 1) { - const light = this._lights[i]; - let matrix = light.getWorldMatrix(); - - // TODO - // if (light instanceof SpotLight) { - // // Rotate spotlights to face direction - // const quat = Quaternion.FromUnitVectorsToRef(Vector3.UpReadOnly, light.direction, TmpVectors.Quaternion[0]); - // const rotation = Matrix.FromQuaternionToRef(quat, TmpVectors.Matrix[0]); - // matrix = rotation.multiplyToRef(matrix, TmpVectors.Matrix[1]); - // } - - // Scale by the range of the light - const range = Math.min(light.range, this.maxRange); - const scaling = Matrix.ScalingToRef(range, range, range, TmpVectors.Matrix[0]); - matrix = scaling.multiplyToRef(matrix, TmpVectors.Matrix[2]); - - matrix.copyToArray(this._proxyMatrixBuffer, i * 16); - } - this._proxyMesh.thinInstanceBufferUpdated("matrix"); - } - /** @internal */ public _createTileMask(): RenderTargetTexture { if (this._tileMaskTexture) { @@ -223,9 +178,7 @@ export class ClusteredLight extends Light { this._tileMaskTexture.renderParticles = false; this._tileMaskTexture.renderSprites = false; this._tileMaskTexture.noPrePassRenderer = true; - this._tileMaskTexture.renderList = [this._proxyMesh]; - - this._tileMaskTexture.onBeforeBindObservable.add(() => this._updateMatrixBuffer()); + this._tileMaskTexture.renderList = [this._lightProxy]; this._tileMaskTexture.onClearObservable.add(() => { // Clear the storage buffer if it exists @@ -239,8 +192,6 @@ export class ClusteredLight extends Light { this._tileMaskBuffer = new StorageBuffer(engine, bufferSize); } - // If the tile mask was disposed it means the tile counts have changed - this._proxyMaterial._updateUniforms(); return this._tileMaskTexture; } @@ -252,23 +203,29 @@ export class ClusteredLight extends Light { } private _createProxyMesh(): void { - // Compute the lowest point in the sphere and expand the diameter accordingly - const rotationZ = Matrix.RotationZ(-Math.PI / (2 + this._proxySegments)); - const rotationY = Matrix.RotationY(Math.PI / this._proxySegments); - const start = Vector3.Up(); - const afterRotZ = Vector3.TransformCoordinates(start, rotationZ); - const end = Vector3.TransformCoordinates(afterRotZ, rotationY); - const midPoint = Vector3.Lerp(start, end, 0.5); - this._proxyMesh = CreateSphere("ProxyMesh", { diameter: 2 / midPoint.length(), segments: this._proxySegments }, this._scene); + // The disc is made of `tesselation` isoceles triangles, and the lowest radius is the height of one of those triangles + // We can get the height from half the angle of that triangle (assuming a side length of 1) + const lowRadius = Math.cos(Math.PI / this._proxyTesselation); + // We scale up the disc so the lowest radius still wraps the light + this._lightProxy = CreateDisc("LightProxy", { radius: 1 / lowRadius, tessellation: this._proxyTesselation }); // Make sure it doesn't render for the default scene - this._scene.removeMesh(this._proxyMesh); + this._scene.removeMesh(this._lightProxy); if (this._tileMaskTexture) { - this._tileMaskTexture.renderList = [this._proxyMesh]; + this._tileMaskTexture.renderList = [this._lightProxy]; } - this._proxyMesh.material = this._proxyMaterial; - this._proxyMesh.thinInstanceSetBuffer("matrix", this._proxyMatrixBuffer, 16, false); + this._lightProxy.material = new LightProxyMaterial("LightProxyMaterial", this); + // We don't actually use the matrix data but we need enough capacity for the lights + this._lightProxy.thinInstanceSetBuffer("matrix", new Float32Array(this.maxLights * 16)); + this._updateProxyMesh(); + } + + private _updateProxyMesh(): void { + const len = Math.min(this._lights.length, this.maxLights); + this._lightProxy.thinInstanceCount = len; + // Hide the mesh if theres no instances + this._lightProxy.isVisible = len > 0; } public override dispose(doNotRecurse?: boolean, disposeMaterialAndTextures?: boolean): void { @@ -278,7 +235,7 @@ export class ClusteredLight extends Light { this._disposeTileMask(); - this._proxyMesh.dispose(doNotRecurse, disposeMaterialAndTextures); + this._lightProxy.dispose(doNotRecurse, disposeMaterialAndTextures); super.dispose(doNotRecurse, disposeMaterialAndTextures); } @@ -292,8 +249,7 @@ export class ClusteredLight extends Light { } this._scene.removeLight(light); this._lights.push(light); - // Cause the matrix buffer to update - this._proxyRenderId = -1; + this._updateProxyMesh(); } protected override _buildUniformLayout(): void { @@ -335,18 +291,20 @@ export class ClusteredLight extends Light { this._uniformBuffer.updateFloat4(struct + "position", position.x, position.y, position.z, exponent, lightIndex); const scaledIntensity = light.getScaledIntensity(); + const range = Math.min(light.range, this.maxRange); light.diffuse.scaleToRef(scaledIntensity, TmpColors.Color3[0]); - this._uniformBuffer.updateColor4(struct + "diffuse", TmpColors.Color3[0], light.range, lightIndex); + this._uniformBuffer.updateColor4(struct + "diffuse", TmpColors.Color3[0], range, lightIndex); light.specular.scaleToRef(scaledIntensity, TmpColors.Color3[1]); this._uniformBuffer.updateColor4(struct + "specular", TmpColors.Color3[1], light.radius, lightIndex); + const inverseSquaredRange = Math.max(light._inverseSquaredRange, this._minInverseSquaredRange); if (light instanceof SpotLight) { const direction = Vector3.Normalize(computed ? light.transformedDirection : light.direction); this._uniformBuffer.updateFloat4(struct + "direction", direction.x, direction.y, direction.z, light._cosHalfAngle, lightIndex); - this._uniformBuffer.updateFloat4(struct + "falloff", light.range, light._inverseSquaredRange, light._lightAngleScale, light._lightAngleOffset, lightIndex); + this._uniformBuffer.updateFloat4(struct + "falloff", range, inverseSquaredRange, light._lightAngleScale, light._lightAngleOffset, lightIndex); } else { this._uniformBuffer.updateFloat4(struct + "direction", 0, 1, 0, -1, lightIndex); - this._uniformBuffer.updateFloat4(struct + "falloff", light.range, light._inverseSquaredRange, 0.5, 0.5, lightIndex); + this._uniformBuffer.updateFloat4(struct + "falloff", range, inverseSquaredRange, 0.5, 0.5, lightIndex); } } return this; @@ -373,6 +331,6 @@ export class ClusteredLight extends Light { } public override _isReady(): boolean { - return this._proxyMesh.isReady(true, true); + return this._lightProxy.isReady(true, true); } } diff --git a/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts b/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts index d8edbdda7ed..94255a9060d 100644 --- a/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts +++ b/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts @@ -15,8 +15,8 @@ export class LightProxyMaterial extends ShaderMaterial { const webgpu = clusteredLight.getEngine().isWebGPU; super(name, clusteredLight._scene, shader, { attributes: ["position"], - uniforms: ["world", "angleBias", "positionBias"], - uniformBuffers: ["Scene", "Mesh", "Light0"], + uniforms: ["halfTileRes"], + uniformBuffers: ["Scene", "Light0"], storageBuffers: ["tileMaskBuffer0"], defines: ["LIGHT0", "CLUSTLIGHT0", "CLUSTLIGHT_WRITE", `CLUSTLIGHT_MAX ${clusteredLight.maxLights}`], shaderLanguage: webgpu ? ShaderLanguage.WGSL : ShaderLanguage.GLSL, @@ -30,29 +30,16 @@ export class LightProxyMaterial extends ShaderMaterial { }); this._clusteredLight = clusteredLight; - // Cull front faces so it still shows when intersecting with the camera - this.cullBackFaces = false; // Additive blending is for merging masks on WebGL this.transparencyMode = ShaderMaterial.MATERIAL_ALPHABLEND; this.alphaMode = Constants.ALPHA_ADD; - - this._updateUniforms(); - } - - /** @internal */ - public _updateUniforms(): void { - // Bias the angle by one sphere segment so the spotlight is slightly too large instead of slightly too small - const angleBias = -Math.PI / (this._clusteredLight.proxySegments + 2); - this.setFloat("angleBias", angleBias); - // Bias the NDC position by one tile so all tiles it overlaps with gets filled (in lieu of conservative rendering) - // We also add a little extra offset just to counteract any inaccuracies - const positionBias = new Vector2(2 / this._clusteredLight.horizontalTiles + 0.001, 2 / this._clusteredLight.verticalTiles + 0.001); - this.setVector2("positionBias", positionBias); } public override bindForSubMesh(world: Matrix, mesh: Mesh, subMesh: SubMesh): void { if (subMesh.effect) { this._clusteredLight._bindLight(0, this.getScene(), subMesh.effect, false, false); + + subMesh.effect.setFloat2("halfTileRes", this._clusteredLight.horizontalTiles / 2, this._clusteredLight.verticalTiles / 2); } super.bindForSubMesh(world, mesh, subMesh); } diff --git a/packages/dev/core/src/Shaders/lightProxy.vertex.fx b/packages/dev/core/src/Shaders/lightProxy.vertex.fx index 86068f7068d..43e526f5ca6 100644 --- a/packages/dev/core/src/Shaders/lightProxy.vertex.fx +++ b/packages/dev/core/src/Shaders/lightProxy.vertex.fx @@ -1,42 +1,26 @@ attribute vec3 position; flat varying highp uint vMask; -uniform float angleBias; -uniform vec2 positionBias; - -// Declarations +// Uniforms #include -#include -#include - #include #include[0..1] -// TODO: switch default direction to up?? -const vec3 DOWN = vec3(0, -1, 0); +uniform vec2 halfTileRes; void main(void) { -#include + SpotLight light = light0.vLights[gl_InstanceID]; - // Get the center (last column) and transformed offset (everything but the last column) of the projected position - vec4 projPosition = viewProjection * finalWorld[3]; - vec4 offset = viewProjection * (mat3x4(finalWorld) * position); + // We don't apply the view matrix to the disc since we want it always facing the camera + vec4 viewPosition = view * vec4(light.position.xyz, 1) + vec4(position * light.falloff.x, 0); + vec4 projPosition = projection * viewPosition; - // For spot lights we keep it at the center if its larger than the spotlight angle - float maxAngle = acos(light0.vLights[gl_InstanceID].direction.w); - // We use the original position for this angle, it will get rotated to face the spotlight direction - float angle = acos(dot(DOWN, normalize(position))) + angleBias; - if (angle < maxAngle) { - // Pointlights or positions within the angle of a spotlight - projPosition += offset; - } + // Convert to NDC space and scale to the tile resolution + vec2 tilePosition = projPosition.xy / projPosition.w * halfTileRes; + // Round to a whole tile boundary with a bit of wiggle room + tilePosition = mix(floor(tilePosition) - 0.01, ceil(tilePosition) + 0.01, greaterThan(position.xy, vec2(0))); - gl_Position = vec4( - // Offset the position in NDC space away from the center - projPosition.xy + sign(offset.xy) * positionBias * projPosition.w, - // Since we don't write to depth just set this to 0 to prevent clipping - 0, - projPosition.w - ); + // We don't care about depth and don't want it to be clipped + gl_Position = vec4(tilePosition.xy / halfTileRes, 0, 1); vMask = 1u << gl_InstanceID; } diff --git a/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx b/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx index 339dba75cf8..cf5543b6337 100644 --- a/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx +++ b/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx @@ -1,48 +1,27 @@ attribute position: vec3f; flat varying vMask: u32; -uniform angleBias: f32; -uniform positionBias: vec2f; - -// Declarations +// Uniforms #include -#include -#include - #include #include[0..1] -const DOWN = vec3f(0, -1, 0); - -fn acosClamped(v: f32) -> f32 { - return acos(clamp(v, 0, 1)); -} +uniform halfTileRes: vec2f; @vertex fn main(input: VertexInputs) -> FragmentInputs { -#include let light = &light0.vLights[vertexInputs.instanceIndex]; - // Get the center (last column) and transformed offset (everything but the last column) of the projected position - var projPosition = scene.viewProjection * finalWorld[3]; - let finalWorld3 = mat3x4(finalWorld[0], finalWorld[1], finalWorld[2]); - let offset = scene.viewProjection * (finalWorld3 * vertexInputs.position); + // We don't apply the view matrix to the disc since we want it always facing the camera + let viewPosition = scene.view * vec4f(light.position.xyz, 1) + vec4f(vertexInputs.position * light.falloff.x, 0); + let projPosition = scene.projection * viewPosition; - // For spot lights we keep it at the center if its larger than the spotlight angle - let maxAngle = acos(light.direction.w); - // We use the original position for this angle, it will get rotated to face the spotlight direction - let angle = acos(dot(DOWN, normalize(vertexInputs.position))) + uniforms.angleBias; - if angle < maxAngle { - // Pointlights or positions within the angle of a spotlight - projPosition += offset; - } + // Convert to NDC space and scale to the tile resolution + var tilePosition = projPosition.xy / projPosition.w * uniforms.halfTileRes; + // Round to a whole tile boundary with a bit of wiggle room + tilePosition = select(floor(tilePosition) - 0.01, ceil(tilePosition) + 0.01, vertexInputs.position.xy > vec2f(0)); - vertexOutputs.position = vec4f( - // Offset the position in NDC space away from the center - projPosition.xy + sign(offset.xy) * uniforms.positionBias * projPosition.w, - // Since we don't write to depth just set this to 0 to prevent clipping - 0, - projPosition.w - ); + // We don't care about depth and don't want it to be clipped + vertexOutputs.position = vec4f(tilePosition.xy / uniforms.halfTileRes, 0, 1); vertexOutputs.vMask = 1u << vertexInputs.instanceIndex; } From 6987f2f9a9bb02de0d2dc0982e973c3faf5136f1 Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Thu, 24 Jul 2025 11:38:27 +1000 Subject: [PATCH 20/30] PBR WebGPU --- .../Lights/Clustered/lightProxyMaterial.ts | 2 +- .../Shaders/ShadersInclude/lightFragment.fx | 9 +- .../pbrClusteredLightFunctions.fx | 5 +- .../ShadersInclude/lightFragment.fx | 30 +++- .../pbrDirectLightingFunctions.fx | 150 ++++++++++++++++++ .../dev/core/src/ShadersWGSL/pbr.fragment.fx | 1 + .../dev/core/src/ShadersWGSL/pbr.vertex.fx | 1 + 7 files changed, 187 insertions(+), 11 deletions(-) diff --git a/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts b/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts index 94255a9060d..653ab3eebe6 100644 --- a/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts +++ b/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts @@ -1,7 +1,7 @@ import { Constants } from "core/Engines/constants"; import { ShaderLanguage } from "core/Materials/shaderLanguage"; import { ShaderMaterial } from "core/Materials/shaderMaterial"; -import { Vector2, type Matrix } from "core/Maths/math.vector"; +import type { Matrix } from "core/Maths/math.vector"; import type { Mesh } from "core/Meshes/mesh"; import type { SubMesh } from "core/Meshes/subMesh"; diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx index c3e4187c288..4b804c1b5aa 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx @@ -6,8 +6,7 @@ vec4 diffuse{X} = light{X}.vLightDiffuse; #define CUSTOM_LIGHT{X}_COLOR // Use to modify light color. Currently only supports diffuse. - #ifdef PBR - #if defined(CLUSTLIGHT{X}) && CLUSTLIGHT_MAX > 0 + #if defined(PBR) && defined(CLUSTLIGHT{X}) && CLUSTLIGHT_MAX > 0 info = computeClusteredLighting{X}( viewDirectionW, normalW, @@ -19,7 +18,6 @@ , subSurfaceOut #endif #ifdef SPECULARTERM - , light{X}.vLightSpecular.rgb , AARoughnessFactors.x #endif #ifdef ANISOTROPIC @@ -32,7 +30,7 @@ , clearcoatOut #endif ); - #else + #elif defined(PBR) // Compute Pre Lighting infos #ifdef SPOTLIGHT{X} @@ -213,8 +211,7 @@ #endif #endif #endif - #endif // CLUSTLIGHT{X} - #else // PBR + #else #ifdef SPOTLIGHT{X} #ifdef IESLIGHTTEXTURE{X} info = computeIESSpotLighting(viewDirectionW, normalW, light{X}.vLightData, light{X}.vLightDirection, diffuse{X}.rgb, light{X}.vLightSpecular.rgb, diffuse{X}.a, glossiness, iesLightTexture{X}); diff --git a/packages/dev/core/src/Shaders/ShadersInclude/pbrClusteredLightFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/pbrClusteredLightFunctions.fx index c58a64a8311..e5efab093d7 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/pbrClusteredLightFunctions.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/pbrClusteredLightFunctions.fx @@ -10,7 +10,6 @@ lightingInfo computeClusteredLighting{X}( , subSurfaceOutParams subSurfaceOut #endif #ifdef SPECULARTERM - , vec3 specularScale , float AARoughnessFactor #endif #ifdef ANISOTROPIC @@ -68,7 +67,6 @@ lightingInfo computeClusteredLighting{X}( // Specular contribution #ifdef SPECULARTERM - vec3 specular = light.specular.rgb * specularScale; #if CONDUCTOR_SPECULAR_MODEL == CONDUCTOR_SPECULAR_MODEL_OPENPBR vec3 metalFresnel = reflectivityOut.specularWeight * getF82Specular(preInfo.VdotH, specularEnvironmentR0, reflectivityOut.colorReflectanceF90, reflectivityOut.roughness); vec3 dielectricFresnel = fresnelSchlickGGX(preInfo.VdotH, reflectivityOut.dielectricColorF0, reflectivityOut.colorReflectanceF90); @@ -92,8 +90,9 @@ lightingInfo computeClusteredLighting{X}( #ifdef SHEEN #ifdef SHEEN_LINKWITHALBEDO preInfo.roughness = sheenOut.sheenIntensity; + #else + preInfo.roughness = adjustRoughnessFromLightProperties(sheenOut.sheenRoughness, radius, preInfo.lightDistance); #endif - preInfo.roughness = adjustRoughnessFromLightProperties(sheenOut.sheenRoughness, radius, preInfo.lightDistance); info.sheen = computeSheenLighting(preInfo, normalW, sheenOut.sheenColor, specularEnvironmentR90, AARoughnessFactor, diffuse); #endif diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightFragment.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightFragment.fx index 489525893f9..334c8f36839 100644 --- a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightFragment.fx +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightFragment.fx @@ -6,7 +6,35 @@ var diffuse{X}: vec4f = light{X}.vLightDiffuse; #define CUSTOM_LIGHT{X}_COLOR // Use to modify light color. Currently only supports diffuse. - #ifdef PBR + #if defined(PBR) && defined(CLUSTLIGHT{X}) + info = computeClusteredLighting( + &tileMaskBuffer{X}, + light{X}.vLightData, + &light{X}.vLights, + viewDirectionW, + normalW, + fragmentInputs.vPositionW, + surfaceAlbedo, + reflectivityOut, + diffuse{X}.rgb, + #ifdef SS_TRANSLUCENCY + subSurfaceOut, + #endif + #ifdef SPECULARTERM + AARoughnessFactors.x, + #endif + #ifdef ANISOTROPIC + anisotropicOut, + #endif + #ifdef SHEEN + sheenOut, + #endif + #ifdef CLEARCOAT + clearcoatOut, + #endif + ); + #elif defined(PBR) + // Compute Pre Lighting infos #ifdef SPOTLIGHT{X} preInfo = computePointAndSpotPreLightingInfo(light{X}.vLightData, viewDirectionW, normalW, fragmentInputs.vPositionW); diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/pbrDirectLightingFunctions.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/pbrDirectLightingFunctions.fx index e0073baf1d6..af7ab5975bb 100644 --- a/packages/dev/core/src/ShadersWGSL/ShadersInclude/pbrDirectLightingFunctions.fx +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/pbrDirectLightingFunctions.fx @@ -206,3 +206,153 @@ fn computeProjectionTextureDiffuseLighting(projectionLightTexture: texture_2d>, + lightData: vec4f, + lights: ptr>, + V: vec3f, + N: vec3f, + posW: vec3f, + surfaceAlbedo: vec3f, + reflectivityOut: reflectivityOutParams, + diffuseScale: vec3f, + #ifdef SS_TRANSLUCENCY + subSurfaceOut: subSurfaceOutParams, + #endif + #ifdef SPECULARTERM + AARoughnessFactor: f32, + #endif + #ifdef ANISOTROPIC + anisotropicOut: anisotropicOutParams, + #endif + #ifdef SHEEN + sheenOut: sheenOutParams, + #endif + #ifdef CLEARCOAT + clearcoatOut: clearcoatOutParams, + #endif + ) -> lightingInfo { + let NdotV = absEps(dot(N, V)); +#include + #ifdef CLEARCOAT + specularEnvironmentR0 = clearcoatOut.specularEnvironmentR0; + #endif + + var result: lightingInfo; + let tilePos = vec2u(fragmentInputs.position.xy * lightData.xy); + let strideLen = vec2u(lightData.zw); + let mask = tileMask[tilePos.y * strideLen.x + tilePos.x]; + + for (var i = 0u; i < strideLen.y; i += 1u) { + if (mask & (1u << i)) == 0 { + continue; + } + let light = &lights[i]; + var preInfo = computePointAndSpotPreLightingInfo(light.position, V, N, posW); + + // Compute Attenuation infos + preInfo.NdotV = NdotV; + preInfo.attenuation = computeDistanceLightFalloff(preInfo.lightOffset, preInfo.lightDistanceSquared, light.falloff.x, light.falloff.y); + preInfo.attenuation *= computeDirectionalLightFalloff(light.direction.xyz, preInfo.L, light.direction.w, light.position.w, light.falloff.z, light.falloff.w); + + let radius = light.specular.a; + preInfo.roughness = adjustRoughnessFromLightProperties(reflectivityOut.roughness, radius, preInfo.lightDistance); + preInfo.diffuseRoughness = reflectivityOut.diffuseRoughness; + preInfo.surfaceAlbedo = surfaceAlbedo; + var info: lightingInfo; + + // Diffuse contribution + let diffuse = light.diffuse.rgb * diffuseScale; + #ifdef SS_TRANSLUCENCY + #ifdef SS_TRANSLUCENCY_LEGACY + info.diffuse = computeDiffuseTransmittedLighting(preInfo, diffuse, subSurfaceOut.transmittance); + info.diffuseTransmission = vec3(0); + #else + info.diffuse = computeDiffuseLighting(preInfo, diffuse) * (1.0 - subSurfaceOut.translucencyIntensity); + info.diffuseTransmission = computeDiffuseTransmittedLighting(preInfo, diffuse, subSurfaceOut.transmittance); + #endif + #else + info.diffuse = computeDiffuseLighting(preInfo, diffuse); + #endif + + // Specular contribution + #ifdef SPECULARTERM + #if CONDUCTOR_SPECULAR_MODEL == CONDUCTOR_SPECULAR_MODEL_OPENPBR + let metalFresnel = reflectivityOut.specularWeight * getF82Specular(preInfo.VdotH, specularEnvironmentR0, reflectivityOut.colorReflectanceF90, reflectivityOut.roughness); + let dielectricFresnel = fresnelSchlickGGXVec3(preInfo.VdotH, reflectivityOut.dielectricColorF0, reflectivityOut.colorReflectanceF90); + let coloredFresnel = mix(dielectricFresnel, metalFresnel, reflectivityOut.metallic); + #else + let coloredFresnel = fresnelSchlickGGXVec3(preInfo.VdotH, specularEnvironmentR0, reflectivityOut.colorReflectanceF90); + #endif + #ifndef LEGACY_SPECULAR_ENERGY_CONSERVATION + let NdotH = dot(N, preInfo.H); + let fresnel = fresnelSchlickGGXVec3(NdotH, vec3(reflectanceF0), specularEnvironmentR90); + info.diffuse *= (vec3(1.0) - fresnel); + #endif + #ifdef ANISOTROPIC + info.specular = computeAnisotropicSpecularLighting(preInfo, V, N, anisotropicOut.anisotropicTangent, anisotropicOut.anisotropicBitangent, anisotropicOut.anisotropy, clearcoatOut.specularEnvironmentR0, specularEnvironmentR90, AARoughnessFactor, diffuse); + #else + info.specular = computeSpecularLighting(preInfo, N, specularEnvironmentR0, coloredFresnel, AARoughnessFactor, diffuse); + #endif + #endif + + // Sheen contribution + #ifdef SHEEN + #ifdef SHEEN_LINKWITHALBEDO + preInfo.roughness = sheenOut.sheenIntensity; + #else + preInfo.roughness = adjustRoughnessFromLightProperties(sheenOut.sheenRoughness, radius, preInfo.lightDistance); + #endif + preInfo.roughness = adjustRoughnessFromLightProperties(sheenOut.sheenRoughness, radius, preInfo.lightDistance); + info.sheen = computeSheenLighting(preInfo, normalW, sheenOut.sheenColor, specularEnvironmentR90, AARoughnessFactor, diffuse); + #endif + + // Clear Coat contribution + #ifdef CLEARCOAT + preInfo.roughness = adjustRoughnessFromLightProperties(clearcoatOut.clearCoatRoughness, radius, preInfo.lightDistance); + info.clearCoat = computeClearCoatLighting(preInfo, clearcoatOut.clearCoatNormalW, clearcoatOut.clearCoatAARoughnessFactors.x, clearcoatOut.clearCoatIntensity, diffuse); + + #ifdef CLEARCOAT_TINT + // Absorption + let absorption = computeClearCoatLightingAbsorption(clearcoatOut.clearCoatNdotVRefract, preInfo.L, clearcoatOut.clearCoatNormalW, clearcoatOut.clearCoatColor, clearcoatOut.clearCoatThickness, clearcoatOut.clearCoatIntensity); + info.diffuse *= absorption; + #ifdef SS_TRANSLUCENCY + info.diffuseTransmission *= absorption; + #endif + #ifdef SPECULARTERM + info.specular *= absorption; + #endif + #endif + + info.diffuse *= info.clearCoat.w; + #ifdef SS_TRANSLUCENCY + info.diffuseTransmission *= info.clearCoat.w; + #endif + #ifdef SPECULARTERM + info.specular *= info.clearCoat.w; + #endif + #ifdef SHEEN + info.sheen *= info.clearCoat.w; + #endif + #endif + + // Apply contributions to result + result.diffuse += info.diffuse; + #ifdef SS_TRANSLUCENCY + result.diffuseTransmission += info.diffuseTransmission; + #endif + #ifdef SPECULARTERM + result.specular += info.specular; + #endif + #ifdef CLEARCOAT + result.clearCoat += info.clearCoat; + #endif + #ifdef SHEEN + result.sheen += info.sheen; + #endif + } + return result; + } +#endif diff --git a/packages/dev/core/src/ShadersWGSL/pbr.fragment.fx b/packages/dev/core/src/ShadersWGSL/pbr.fragment.fx index 91bae999b44..6333eeed574 100644 --- a/packages/dev/core/src/ShadersWGSL/pbr.fragment.fx +++ b/packages/dev/core/src/ShadersWGSL/pbr.fragment.fx @@ -14,6 +14,7 @@ #include #include +#include #include[0..maxSimultaneousLights] #include #include diff --git a/packages/dev/core/src/ShadersWGSL/pbr.vertex.fx b/packages/dev/core/src/ShadersWGSL/pbr.vertex.fx index dd7d851f241..2cffb06bf29 100644 --- a/packages/dev/core/src/ShadersWGSL/pbr.vertex.fx +++ b/packages/dev/core/src/ShadersWGSL/pbr.vertex.fx @@ -93,6 +93,7 @@ varying vColor: vec4f; #include #include #include +#include #include[0..maxSimultaneousLights] #include From 57b34e0a8935d65ca28e3b04c78e2864474215f2 Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Thu, 24 Jul 2025 15:41:02 +1000 Subject: [PATCH 21/30] Initial uncapped limit (hacky, webgl only, not amazing performance, no pbr) --- .../src/Lights/Clustered/clusteredLight.ts | 262 +++++++++++------- .../Clustered/clusteredLightSceneComponent.ts | 2 +- .../Lights/Clustered/lightProxyMaterial.ts | 2 +- .../src/Materials/materialHelper.functions.ts | 7 +- .../ShadersInclude/clusteredLightFunctions.fx | 11 +- .../Shaders/ShadersInclude/lightFragment.fx | 4 +- .../ShadersInclude/lightUboDeclaration.fx | 3 +- .../ShadersInclude/lightVxUboDeclaration.fx | 2 - .../ShadersInclude/lightsFragmentFunctions.fx | 42 +++ .../dev/core/src/Shaders/default.fragment.fx | 4 - .../dev/core/src/Shaders/default.vertex.fx | 1 - .../core/src/Shaders/lightProxy.fragment.fx | 9 + .../dev/core/src/Shaders/lightProxy.vertex.fx | 24 +- 13 files changed, 249 insertions(+), 124 deletions(-) diff --git a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts index 273497c0298..742894736a3 100644 --- a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts +++ b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts @@ -3,9 +3,12 @@ import { Constants } from "core/Engines/constants"; import type { AbstractEngine } from "core/Engines/abstractEngine"; import type { WebGPUEngine } from "core/Engines/webgpuEngine"; import type { Effect } from "core/Materials/effect"; +import { ShaderLanguage } from "core/Materials/shaderLanguage"; +import { ShaderMaterial } from "core/Materials/shaderMaterial"; +import { RawTexture } from "core/Materials/Textures/rawTexture"; import { RenderTargetTexture } from "core/Materials/Textures/renderTargetTexture"; import { TmpColors } from "core/Maths/math.color"; -import { Vector3 } from "core/Maths/math.vector"; +import { Vector2, Vector3 } from "core/Maths/math.vector"; import { CreateDisc } from "core/Meshes/Builders/discBuilder"; import type { Mesh } from "core/Meshes/mesh"; import { _WarnImport } from "core/Misc/devTools"; @@ -13,7 +16,6 @@ import { Logger } from "core/Misc/logger"; import type { Scene } from "core/scene"; import type { Nullable } from "core/types"; -import { LightProxyMaterial } from "./lightProxyMaterial"; import { Light } from "../light"; import { LightConstants } from "../lightConstants"; import { PointLight } from "../pointLight"; @@ -22,7 +24,7 @@ import { SpotLight } from "../spotLight"; import "core/Meshes/thinInstanceMesh"; export class ClusteredLight extends Light { - private static _GetEngineMaxLights(engine: AbstractEngine): number { + private static _GetEngineBatchSize(engine: AbstractEngine): number { const caps = engine._caps; if (!engine.supportsUniformBuffers || !caps.texelFetch) { return 0; @@ -43,7 +45,7 @@ export class ClusteredLight extends Light { } public static IsLightSupported(light: Light): boolean { - if (ClusteredLight._GetEngineMaxLights(light.getEngine()) === 0) { + if (ClusteredLight._GetEngineBatchSize(light.getEngine()) === 0) { return false; } else if (light.shadowEnabled && light._scene.shadowsEnabled && light.getShadowGenerators()) { // Shadows are not supported @@ -67,10 +69,10 @@ export class ClusteredLight extends Light { throw _WarnImport("ClusteredLightSceneComponent"); }; - public readonly maxLights: number; + private readonly _batchSize: number; public get isSupported(): boolean { - return this.maxLights > 0; + return this._batchSize > 0; } private readonly _lights: (PointLight | SpotLight)[] = []; @@ -78,7 +80,11 @@ export class ClusteredLight extends Light { return this._lights; } - private _tileMaskTexture: Nullable; + private _lightDataBuffer: Float32Array; + private _lightDataTexture: RawTexture; + + private _tileMaskBatches = -1; + private _tileMaskTexture: RenderTargetTexture; private _tileMaskBuffer: Nullable; private _horizontalTiles = 64; @@ -91,7 +97,8 @@ export class ClusteredLight extends Light { return; } this._horizontalTiles = horizontal; - this._disposeTileMask(); + // Force the batch data to be recreated + this._tileMaskBatches = -1; } private _verticalTiles = 64; @@ -104,9 +111,12 @@ export class ClusteredLight extends Light { return; } this._verticalTiles = vertical; - this._disposeTileMask(); + // Force the batch data to be recreated + this._tileMaskBatches = -1; } + private readonly _proxyMaterial: ShaderMaterial; + // TODO: rename to proxyMesh private _lightProxy: Mesh; private _proxyTesselation = 8; @@ -119,7 +129,7 @@ export class ClusteredLight extends Light { return; } this._proxyTesselation = tesselation; - this._lightProxy.dispose(false, true); + this._lightProxy.dispose(); this._createProxyMesh(); } @@ -139,11 +149,35 @@ export class ClusteredLight extends Light { constructor(name: string, lights: Light[] = [], scene?: Scene) { super(name, scene); - this.maxLights = ClusteredLight._GetEngineMaxLights(this.getEngine()); + const engine = this.getEngine(); + this._batchSize = ClusteredLight._GetEngineBatchSize(engine); + + const proxyShader = { vertex: "lightProxy", fragment: "lightProxy" }; + this._proxyMaterial = new ShaderMaterial("ProxyMaterial", this._scene, proxyShader, { + attributes: ["position"], + uniforms: ["tileMaskResolution"], + uniformBuffers: ["Scene"], + storageBuffers: ["tileMaskBuffer"], + samplers: ["lightDataTexture"], + defines: [`CLUSTLIGHT_BATCH ${this._batchSize}`], + shaderLanguage: engine.isWebGPU ? ShaderLanguage.WGSL : ShaderLanguage.GLSL, + extraInitializationsAsync: async () => { + if (engine.isWebGPU) { + await Promise.all([import("../../ShadersWGSL/lightProxy.vertex"), import("../../ShadersWGSL/lightProxy.fragment")]); + } else { + await Promise.all([import("../../Shaders/lightProxy.vertex"), import("../../Shaders/lightProxy.fragment")]); + } + }, + }); + + // Additive blending is for merging masks on WebGL + this._proxyMaterial.transparencyMode = ShaderMaterial.MATERIAL_ALPHABLEND; + this._proxyMaterial.alphaMode = Constants.ALPHA_ADD; this._createProxyMesh(); + this._updateBatches(); - if (this.maxLights > 0) { + if (this._batchSize > 0) { ClusteredLight._SceneComponentInitialization(this._scene); for (const light of lights) { this.addLight(light); @@ -160,14 +194,61 @@ export class ClusteredLight extends Light { return LightConstants.LIGHTTYPEID_CLUSTERED; } + private _createProxyMesh(): void { + // The disc is made of `tesselation` isoceles triangles, and the lowest radius is the height of one of those triangles + // We can get the height from half the angle of that triangle (assuming a side length of 1) + const lowRadius = Math.cos(Math.PI / this._proxyTesselation); + // We scale up the disc so the lowest radius still wraps the light + this._lightProxy = CreateDisc("LightProxy", { radius: 1 / lowRadius, tessellation: this._proxyTesselation }); + // Make sure it doesn't render for the default scene + this._scene.removeMesh(this._lightProxy); + this._lightProxy.material = this._proxyMaterial; + + if (this._tileMaskBatches > 0) { + this._tileMaskTexture.renderList = [this._lightProxy]; + + // We don't actually use the matrix data but we need enough capacity for the lights + this._lightProxy.thinInstanceSetBuffer("matrix", new Float32Array(this._tileMaskBatches * this._batchSize * 16)); + this._lightProxy.thinInstanceCount = this._lights.length; + this._lightProxy.isVisible = this._lights.length > 0; + } + } + /** @internal */ - public _createTileMask(): RenderTargetTexture { - if (this._tileMaskTexture) { + public _updateBatches(): RenderTargetTexture { + this._lightProxy.isVisible = this._lights.length > 0; + + // Ensure space for atleast 1 batch + const batches = Math.max(Math.ceil(this._lights.length / this._batchSize), 1); + if (this._tileMaskBatches >= batches) { + this._lightProxy.thinInstanceCount = this._lights.length; return this._tileMaskTexture; } - const engine = this.getEngine(); + // Round up to a batch size so we don't have to reallocate as often + const maxLights = batches * this._batchSize; + + this._lightDataBuffer = new Float32Array(20 * maxLights); + this._lightDataTexture?.dispose(); + this._lightDataTexture = new RawTexture( + this._lightDataBuffer, + 5, + maxLights, + Constants.TEXTUREFORMAT_RGBA, + this._scene, + false, + false, + Constants.TEXTURE_NEAREST_SAMPLINGMODE, + Constants.TEXTURETYPE_FLOAT + ); + this._proxyMaterial.setTexture("lightDataTexture", this._lightDataTexture); + + this._tileMaskTexture?.dispose(); const textureSize = { width: this._horizontalTiles, height: this._verticalTiles }; + if (!engine.isWebGPU) { + // In WebGL we shift the light proxy by the batch number + textureSize.height *= batches; + } this._tileMaskTexture = new RenderTargetTexture("TileMaskTexture", textureSize, this._scene, { // We don't write anything on WebGPU so make it as small as possible type: engine.isWebGPU ? Constants.TEXTURETYPE_UNSIGNED_BYTE : Constants.TEXTURETYPE_FLOAT, @@ -180,6 +261,10 @@ export class ClusteredLight extends Light { this._tileMaskTexture.noPrePassRenderer = true; this._tileMaskTexture.renderList = [this._lightProxy]; + this._tileMaskTexture.onBeforeBindObservable.add(() => { + this._updateLightData(); + }); + this._tileMaskTexture.onClearObservable.add(() => { // Clear the storage buffer if it exists this._tileMaskBuffer?.clear(); @@ -188,53 +273,80 @@ export class ClusteredLight extends Light { if (engine.isWebGPU) { // WebGPU also needs a storage buffer to write to - const bufferSize = this._horizontalTiles * this._verticalTiles * 4; + this._tileMaskBuffer?.dispose(); + const bufferSize = this._horizontalTiles * this._verticalTiles * batches * 4; this._tileMaskBuffer = new StorageBuffer(engine, bufferSize); } + this._proxyMaterial.setVector3("tileMaskResolution", new Vector3(this._horizontalTiles, this.verticalTiles, batches)); + + // We don't actually use the matrix data but we need enough capacity for the lights + this._lightProxy.thinInstanceSetBuffer("matrix", new Float32Array(maxLights * 16)); + this._lightProxy.thinInstanceCount = this._lights.length; + this._tileMaskBatches = batches; return this._tileMaskTexture; } - private _disposeTileMask(): void { - this._tileMaskTexture?.dispose(); - this._tileMaskTexture = null; - this._tileMaskBuffer?.dispose(); - this._tileMaskBuffer = null; - } + private _updateLightData(): void { + for (let i = 0; i < this._lights.length; i += 1) { + const light = this._lights[i]; + const offset = i * 20; + const computed = light.computeTransformedInformation(); + const scaledIntensity = light.getScaledIntensity(); - private _createProxyMesh(): void { - // The disc is made of `tesselation` isoceles triangles, and the lowest radius is the height of one of those triangles - // We can get the height from half the angle of that triangle (assuming a side length of 1) - const lowRadius = Math.cos(Math.PI / this._proxyTesselation); - // We scale up the disc so the lowest radius still wraps the light - this._lightProxy = CreateDisc("LightProxy", { radius: 1 / lowRadius, tessellation: this._proxyTesselation }); + const position = computed ? light.transformedPosition : light.position; + const diffuse = light.diffuse.scaleToRef(scaledIntensity, TmpColors.Color3[0]); + const specular = light.specular.scaleToRef(scaledIntensity, TmpColors.Color3[1]); + const range = Math.min(light.range, this.maxRange); + const inverseSquaredRange = Math.max(light._inverseSquaredRange, this._minInverseSquaredRange); + this._lightDataBuffer.set( + [ + // vLightData + position.x, + position.y, + position.z, + 0, + // vLightDiffuse + diffuse.r, + diffuse.g, + diffuse.b, + range, + // vLightSpecular + specular.r, + specular.g, + specular.b, + light.radius, + // vLightDirection + 0, + 1, + 0, + -1, + // vLightFalloff + range, + inverseSquaredRange, + 0.5, + 0.5, + ], + offset + ); - // Make sure it doesn't render for the default scene - this._scene.removeMesh(this._lightProxy); - if (this._tileMaskTexture) { - this._tileMaskTexture.renderList = [this._lightProxy]; + if (light instanceof SpotLight) { + const direction = Vector3.Normalize(computed ? light.transformedDirection : light.direction); + this._lightDataBuffer[offset + 3] = light.exponent; // vLightData.a + this._lightDataBuffer.set([direction.x, direction.y, direction.z, light._cosHalfAngle], offset + 12); // vLightDirection + this._lightDataBuffer.set([light._lightAngleScale, light._lightAngleOffset], offset + 18); // vLightFalloff.zw + } } - - this._lightProxy.material = new LightProxyMaterial("LightProxyMaterial", this); - // We don't actually use the matrix data but we need enough capacity for the lights - this._lightProxy.thinInstanceSetBuffer("matrix", new Float32Array(this.maxLights * 16)); - this._updateProxyMesh(); - } - - private _updateProxyMesh(): void { - const len = Math.min(this._lights.length, this.maxLights); - this._lightProxy.thinInstanceCount = len; - // Hide the mesh if theres no instances - this._lightProxy.isVisible = len > 0; + this._lightDataTexture.update(this._lightDataBuffer); } public override dispose(doNotRecurse?: boolean, disposeMaterialAndTextures?: boolean): void { for (const light of this._lights) { light.dispose(doNotRecurse, disposeMaterialAndTextures); } - - this._disposeTileMask(); - + this._lightDataTexture.dispose(); + this._tileMaskTexture.dispose(); + this._tileMaskBuffer?.dispose(); this._lightProxy.dispose(doNotRecurse, disposeMaterialAndTextures); super.dispose(doNotRecurse, disposeMaterialAndTextures); } @@ -243,31 +355,18 @@ export class ClusteredLight extends Light { if (!ClusteredLight.IsLightSupported(light)) { Logger.Warn("Attempting to add a light to cluster that does not support clustering"); return; - } else if (this._lights.length === this.maxLights) { - // Only log this once (hence equals) but add the light anyway - Logger.Warn(`Attempting to add more lights to cluster than what is supported (${this.maxLights})`); } this._scene.removeLight(light); this._lights.push(light); - this._updateProxyMesh(); + + this._lightProxy.isVisible = true; + this._lightProxy.thinInstanceCount = this._lights.length; } protected override _buildUniformLayout(): void { - // We can't use `this.maxLights` since this will get called during construction - const maxLights = ClusteredLight._GetEngineMaxLights(this.getEngine()); - this._uniformBuffer.addUniform("vLightData", 4); this._uniformBuffer.addUniform("vLightDiffuse", 4); this._uniformBuffer.addUniform("vLightSpecular", 4); - for (let i = 0; i < maxLights; i += 1) { - // These technically don't have to match the field name but also why not - const struct = `vLights[${i}].`; - this._uniformBuffer.addUniform(struct + "position", 4); - this._uniformBuffer.addUniform(struct + "diffuse", 4); - this._uniformBuffer.addUniform(struct + "specular", 4); - this._uniformBuffer.addUniform(struct + "direction", 4); - this._uniformBuffer.addUniform(struct + "falloff", 4); - } this._uniformBuffer.addUniform("shadowsInfo", 3); this._uniformBuffer.addUniform("depthValues", 2); this._uniformBuffer.create(); @@ -277,43 +376,15 @@ export class ClusteredLight extends Light { const engine = this.getEngine(); const hscale = this._horizontalTiles / engine.getRenderWidth(); const vscale = this._verticalTiles / engine.getRenderHeight(); - - const len = Math.min(this._lights.length, this.maxLights); - this._uniformBuffer.updateFloat4("vLightData", hscale, vscale, this._horizontalTiles, len, lightIndex); - - for (let i = 0; i < len; i += 1) { - const light = this._lights[i]; - const struct = `vLights[${i}].`; - - const computed = light.computeTransformedInformation(); - const position = computed ? light.transformedPosition : light.position; - const exponent = light instanceof SpotLight ? light.exponent : 0; - this._uniformBuffer.updateFloat4(struct + "position", position.x, position.y, position.z, exponent, lightIndex); - - const scaledIntensity = light.getScaledIntensity(); - const range = Math.min(light.range, this.maxRange); - light.diffuse.scaleToRef(scaledIntensity, TmpColors.Color3[0]); - this._uniformBuffer.updateColor4(struct + "diffuse", TmpColors.Color3[0], range, lightIndex); - light.specular.scaleToRef(scaledIntensity, TmpColors.Color3[1]); - this._uniformBuffer.updateColor4(struct + "specular", TmpColors.Color3[1], light.radius, lightIndex); - - const inverseSquaredRange = Math.max(light._inverseSquaredRange, this._minInverseSquaredRange); - if (light instanceof SpotLight) { - const direction = Vector3.Normalize(computed ? light.transformedDirection : light.direction); - this._uniformBuffer.updateFloat4(struct + "direction", direction.x, direction.y, direction.z, light._cosHalfAngle, lightIndex); - this._uniformBuffer.updateFloat4(struct + "falloff", range, inverseSquaredRange, light._lightAngleScale, light._lightAngleOffset, lightIndex); - } else { - this._uniformBuffer.updateFloat4(struct + "direction", 0, 1, 0, -1, lightIndex); - this._uniformBuffer.updateFloat4(struct + "falloff", range, inverseSquaredRange, 0.5, 0.5, lightIndex); - } - } + this._uniformBuffer.updateFloat4("vLightData", hscale, vscale, this._verticalTiles, this._lights.length, lightIndex); return this; } public override transferTexturesToEffect(effect: Effect, lightIndex: string): Light { const engine = this.getEngine(); + effect.setTexture("lightsTexture" + lightIndex, this._lightDataTexture); if (engine.isWebGPU) { - (this.getEngine()).setStorageBuffer("tileMaskBuffer" + lightIndex, this._tileMaskBuffer); + (engine).setStorageBuffer("tileMaskBuffer" + lightIndex, this._tileMaskBuffer); } else { effect.setTexture("tileMaskTexture" + lightIndex, this._tileMaskTexture); } @@ -327,10 +398,11 @@ export class ClusteredLight extends Light { public override prepareLightSpecificDefines(defines: any, lightIndex: number): void { defines["CLUSTLIGHT" + lightIndex] = true; - defines["CLUSTLIGHT_MAX"] = this.maxLights; + defines["CLUSTLIGHT_BATCH"] = this._batchSize; } public override _isReady(): boolean { + this._updateBatches(); return this._lightProxy.isReady(true, true); } } diff --git a/packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts b/packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts index a421e5be612..9c9bdd0fc62 100644 --- a/packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts +++ b/packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts @@ -27,7 +27,7 @@ class ClusteredLightSceneComponent implements ISceneComponent { private _gatherActiveCameraRenderTargets: RenderTargetsStageAction = (renderTargets) => { for (const light of this.scene.lights) { if (light instanceof ClusteredLight && light.isSupported) { - renderTargets.push(light._createTileMask()); + renderTargets.push(light._updateBatches()); } } }; diff --git a/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts b/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts index 653ab3eebe6..4e200c2c6d6 100644 --- a/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts +++ b/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts @@ -18,7 +18,7 @@ export class LightProxyMaterial extends ShaderMaterial { uniforms: ["halfTileRes"], uniformBuffers: ["Scene", "Light0"], storageBuffers: ["tileMaskBuffer0"], - defines: ["LIGHT0", "CLUSTLIGHT0", "CLUSTLIGHT_WRITE", `CLUSTLIGHT_MAX ${clusteredLight.maxLights}`], + defines: ["LIGHT0", "CLUSTLIGHT0", "CLUSTLIGHT_WRITE"], shaderLanguage: webgpu ? ShaderLanguage.WGSL : ShaderLanguage.GLSL, extraInitializationsAsync: async () => { if (webgpu) { diff --git a/packages/dev/core/src/Materials/materialHelper.functions.ts b/packages/dev/core/src/Materials/materialHelper.functions.ts index f69d50d5f2f..0d40904d2af 100644 --- a/packages/dev/core/src/Materials/materialHelper.functions.ts +++ b/packages/dev/core/src/Materials/materialHelper.functions.ts @@ -1074,7 +1074,7 @@ export function PrepareDefinesForCamera(scene: Scene, defines: any): boolean { * @param uniformBuffersList defines an optional list of uniform buffers * @param updateOnlyBuffersList True to only update the uniformBuffersList array * @param iesLightTexture defines if IES texture must be used - * @param tileMaskTexture defines if a tile mask texture must be used + * @param clusteredLightsTexture defines if a tile mask texture must be used */ export function PrepareUniformsAndSamplersForLight( lightIndex: number, @@ -1084,7 +1084,7 @@ export function PrepareUniformsAndSamplersForLight( uniformBuffersList: Nullable = null, updateOnlyBuffersList = false, iesLightTexture = false, - tileMaskTexture = false + clusteredLightsTexture = false ) { if (uniformBuffersList) { uniformBuffersList.push("Light" + lightIndex); @@ -1127,7 +1127,8 @@ export function PrepareUniformsAndSamplersForLight( if (iesLightTexture) { samplersList.push("iesLightTexture" + lightIndex); } - if (tileMaskTexture) { + if (clusteredLightsTexture) { + samplersList.push("lightDataTexture" + lightIndex); samplersList.push("tileMaskTexture" + lightIndex); } } diff --git a/packages/dev/core/src/Shaders/ShadersInclude/clusteredLightFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/clusteredLightFunctions.fx index 06326310a24..da907fda11d 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/clusteredLightFunctions.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/clusteredLightFunctions.fx @@ -3,15 +3,20 @@ lightingInfo computeClusteredLighting{X}(vec3 viewDirectionW, vec3 vNormal, vec3 lightingInfo result; vec4 maskTexel = texelFetch(tileMaskTexture{X}, ivec2(gl_FragCoord.xy * light{X}.vLightData.xy), 0); uint mask = uint(maskTexel.r); + vec3 specularScale = light{X}.vLightSpecular.rgb; int len = int(light{X}.vLightData.w); for (int i = 0; i < len; i += 1) { if ((mask & (1u << i)) == 0u) { continue; } - vec3 diffuse = light{X}.vLights[i].diffuse.rgb * diffuseScale; - vec3 specular = light{X}.vLights[i].specular.rgb * light{X}.vLightSpecular.rgb; - lightingInfo info = computeSpotLighting(viewDirectionW, vNormal, light{X}.vLights[i].position, light{X}.vLights[i].direction, diffuse, specular, light{X}.vLights[i].diffuse.a, glossiness); + vec4 position = texelFetch(lightsDataTexture{X}, ivec2(0, i), 0); + vec4 diffuse = texelFetch(lightsDataTexture{X}, ivec2(1, i), 0); + vec4 specular = texelFetch(lightsDataTexture{X}, ivec2(2, i), 0); + vec4 direction = texelFetch(lightsDataTexture{X}, ivec2(3, i), 0); + vec4 falloff = texelFetch(lightsDataTexture{X}, ivec2(4, i), 0); + + lightingInfo info = computeSpotLighting(viewDirectionW, vNormal, position, direction, diffuse.rgb * diffuseScale, specular.rgb * specularScale, diffuse.a, glossiness); result.diffuse += info.diffuse; #ifdef SPECULARTERM result.specular += info.specular; diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx index 4b804c1b5aa..1b5738277b5 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx @@ -230,8 +230,8 @@ vReflectionInfos.y #endif ); - #elif defined(CLUSTLIGHT{X}) && CLUSTLIGHT_MAX > 0 - info = computeClusteredLighting{X}(viewDirectionW, normalW, diffuse{X}.rgb, glossiness); + #elif defined(CLUSTLIGHT{X}) && CLUSTLIGHT_BATCH > 0 + info = computeClusteredLighting(lightDataTexture{X}, tileMaskTexture{X}, viewDirectionW, normalW, light{X}.vLightData, glossiness); #endif #endif diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx index f41effacd83..cb911a7100a 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx @@ -12,8 +12,6 @@ vec4 vLightFalloff; #elif defined(HEMILIGHT{X}) vec3 vLightGround; - #elif defined(CLUSTLIGHT{X}) - SpotLight vLights[CLUSTLIGHT_MAX]; #endif #if defined(AREALIGHT{X}) vec4 vLightWidth; @@ -30,6 +28,7 @@ uniform sampler2D projectionLightTexture{X}; #endif #ifdef CLUSTLIGHT{X} + uniform sampler2D lightDataTexture{X}; // Ensure the mask is sampled with high precision uniform highp sampler2D tileMaskTexture{X}; #endif diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightVxUboDeclaration.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightVxUboDeclaration.fx index c22d8176e13..1c1a2005e17 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightVxUboDeclaration.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightVxUboDeclaration.fx @@ -12,8 +12,6 @@ vec4 vLightFalloff; #elif defined(HEMILIGHT{X}) vec3 vLightGround; - #elif defined(CLUSTLIGHT{X}) - SpotLight vLights[CLUSTLIGHT_MAX]; #endif #if defined(AREALIGHT{X}) vec4 vLightWidth; diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx index edc48284867..284f8ff2fa6 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx @@ -183,3 +183,45 @@ lightingInfo computeAreaLighting(sampler2D ltc1, sampler2D ltc2, vec3 viewDirect // End Area Light #endif + +#if defines(CLUSTLIGHT_BATCH) && CLUSTLIGHT_BATCH > 0 +lightingInfo computeClusteredLighting( + sampler2D lightDataTexture, + sampler2D tileMaskTexture, + vec3 viewDirectionW, + vec3 vNormal, + vec4 clusteredData, + float glossiness +) { + lightingInfo result; + ivec2 maskCoord = ivec2(gl_FragCoord.xy * clusteredData.xy); + int height = int(clusteredData.z); + int len = int(clusteredData.w); + + for (int i = 0; i < len;) { + vec4 maskTexel = texelFetch(tileMaskTexture, maskCoord, 0); + uint mask = uint(maskTexel.r); + maskCoord.y += height; + + int batchEnd = min(i + CLUSTLIGHT_BATCH, len); + uint bit = 1u; + for(; i < batchEnd; i += 1, bit <<= 1) { + if ((mask & bit) == 0u) { + continue; + } + vec4 lightData = texelFetch(lightDataTexture, ivec2(0, i), 0); + vec4 diffuse = texelFetch(lightDataTexture, ivec2(1, i), 0); + vec4 specular = texelFetch(lightDataTexture, ivec2(2, i), 0); + vec4 direction = texelFetch(lightDataTexture, ivec2(3, i), 0); + vec4 falloff = texelFetch(lightDataTexture, ivec2(4, i), 0); + + lightingInfo info = computeSpotLighting(viewDirectionW, vNormal, lightData, direction, diffuse.rgb, specular.rgb, diffuse.a, glossiness); + result.diffuse += info.diffuse; + #ifdef SPECULARTERM + result.specular += info.specular; + #endif + } + } + return result; +} +#endif diff --git a/packages/dev/core/src/Shaders/default.fragment.fx b/packages/dev/core/src/Shaders/default.fragment.fx index c53abc6c072..38077eadcb9 100644 --- a/packages/dev/core/src/Shaders/default.fragment.fx +++ b/packages/dev/core/src/Shaders/default.fragment.fx @@ -32,14 +32,10 @@ varying vec4 vColor; #include // Lights -#include #include<__decl__lightFragment>[0..maxSimultaneousLights] #include #include -// WebGL does not support passing uniform arrays by reference and the copy heavily impacts performance. -// Bit of a hacky solution but we can just create a function per clustered light that references the uniforms directly. -#include[0..maxSimultaneousLights] // Samplers #include(_DEFINENAME_,DIFFUSE,_VARYINGNAME_,Diffuse,_SAMPLERNAME_,diffuse) diff --git a/packages/dev/core/src/Shaders/default.vertex.fx b/packages/dev/core/src/Shaders/default.vertex.fx index 340c508fd9c..86eec837b30 100644 --- a/packages/dev/core/src/Shaders/default.vertex.fx +++ b/packages/dev/core/src/Shaders/default.vertex.fx @@ -58,7 +58,6 @@ varying vec4 vColor; #include #include -#include #include<__decl__lightVxFragment>[0..maxSimultaneousLights] #include diff --git a/packages/dev/core/src/Shaders/lightProxy.fragment.fx b/packages/dev/core/src/Shaders/lightProxy.fragment.fx index 5f4d9930371..dfe06b3e6b1 100644 --- a/packages/dev/core/src/Shaders/lightProxy.fragment.fx +++ b/packages/dev/core/src/Shaders/lightProxy.fragment.fx @@ -1,5 +1,14 @@ +flat varying highp int vBatch; flat varying highp uint vMask; +uniform vec3 tileMaskResolution; + void main(void) { + // Ensure the pixel we're writing to is of the correct batch + int coordBatch = int(gl_FragCoord.y / tileMaskResolution.y); + if (coordBatch != vBatch) { + discard; + } + gl_FragColor = vec4(vMask, 0, 0, 1); } diff --git a/packages/dev/core/src/Shaders/lightProxy.vertex.fx b/packages/dev/core/src/Shaders/lightProxy.vertex.fx index 43e526f5ca6..a7859a64a52 100644 --- a/packages/dev/core/src/Shaders/lightProxy.vertex.fx +++ b/packages/dev/core/src/Shaders/lightProxy.vertex.fx @@ -1,26 +1,30 @@ attribute vec3 position; +flat varying highp int vBatch; flat varying highp uint vMask; // Uniforms #include -#include -#include[0..1] -uniform vec2 halfTileRes; +uniform vec3 tileMaskResolution; +uniform sampler2D lightDataTexture; void main(void) { - SpotLight light = light0.vLights[gl_InstanceID]; + vec4 lightData = texelFetch(lightDataTexture, ivec2(0, gl_InstanceID), 0); + vec4 falloff = texelFetch(lightDataTexture, ivec2(4, gl_InstanceID), 0); + vBatch = gl_InstanceID / CLUSTLIGHT_BATCH; + vMask = 1u << (gl_InstanceID % CLUSTLIGHT_BATCH); // We don't apply the view matrix to the disc since we want it always facing the camera - vec4 viewPosition = view * vec4(light.position.xyz, 1) + vec4(position * light.falloff.x, 0); + vec4 viewPosition = view * vec4(lightData.xyz, 1) + vec4(position * falloff.x, 0); vec4 projPosition = projection * viewPosition; - // Convert to NDC space and scale to the tile resolution - vec2 tilePosition = projPosition.xy / projPosition.w * halfTileRes; + // Convert to NDC 0->1 space and scale to the tile resolution + vec2 tilePosition = (projPosition.xy / projPosition.w + 1.0) / 2.0 * tileMaskResolution.xy; // Round to a whole tile boundary with a bit of wiggle room tilePosition = mix(floor(tilePosition) - 0.01, ceil(tilePosition) + 0.01, greaterThan(position.xy, vec2(0))); + // Reposition vertically based on current batch + tilePosition.y = (tilePosition.y + float(vBatch) * tileMaskResolution.y) / tileMaskResolution.z; - // We don't care about depth and don't want it to be clipped - gl_Position = vec4(tilePosition.xy / halfTileRes, 0, 1); - vMask = 1u << gl_InstanceID; + // We don't care about depth and don't want it to be clipped so set Z to 0 + gl_Position = vec4(tilePosition.xy / tileMaskResolution.xy * 2.0 - 1.0, 0, 1); } From 6b44f843772104ad1ca19d7b5c1f5dc7ae85b9c5 Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Fri, 25 Jul 2025 09:28:13 +1000 Subject: [PATCH 22/30] WebGPU support and performance improvement --- .../src/Lights/Clustered/clusteredLight.ts | 48 +++++++++-------- .../Lights/Clustered/lightProxyMaterial.ts | 46 ---------------- .../src/Materials/materialHelper.functions.ts | 6 +-- .../core/src/Materials/standardMaterial.ts | 2 +- .../ShadersInclude/clusteredLightFunctions.fx | 27 ---------- .../Shaders/ShadersInclude/lightFragment.fx | 2 +- .../ShadersInclude/lightUboDeclaration.fx | 2 + .../ShadersInclude/lightVxUboDeclaration.fx | 2 + .../ShadersInclude/lightsFragmentFunctions.fx | 26 ++++----- .../ShadersInclude/spotLightDeclaration.fx | 8 --- .../core/src/Shaders/lightProxy.fragment.fx | 9 ++-- .../dev/core/src/Shaders/lightProxy.vertex.fx | 13 ++--- .../ShadersInclude/lightFragment.fx | 2 +- .../ShadersInclude/lightUboDeclaration.fx | 7 +-- .../ShadersInclude/lightVxUboDeclaration.fx | 2 +- .../ShadersInclude/lightsFragmentFunctions.fx | 53 +++++++++++-------- .../ShadersInclude/spotLightDeclaration.fx | 8 --- .../src/ShadersWGSL/lightProxy.fragment.fx | 13 ++--- .../core/src/ShadersWGSL/lightProxy.vertex.fx | 20 ++++--- 19 files changed, 112 insertions(+), 184 deletions(-) delete mode 100644 packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts delete mode 100644 packages/dev/core/src/Shaders/ShadersInclude/clusteredLightFunctions.fx delete mode 100644 packages/dev/core/src/Shaders/ShadersInclude/spotLightDeclaration.fx delete mode 100644 packages/dev/core/src/ShadersWGSL/ShadersInclude/spotLightDeclaration.fx diff --git a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts index 742894736a3..fcf72ca13a3 100644 --- a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts +++ b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts @@ -8,7 +8,7 @@ import { ShaderMaterial } from "core/Materials/shaderMaterial"; import { RawTexture } from "core/Materials/Textures/rawTexture"; import { RenderTargetTexture } from "core/Materials/Textures/renderTargetTexture"; import { TmpColors } from "core/Maths/math.color"; -import { Vector2, Vector3 } from "core/Maths/math.vector"; +import { Vector3 } from "core/Maths/math.vector"; import { CreateDisc } from "core/Meshes/Builders/discBuilder"; import type { Mesh } from "core/Meshes/mesh"; import { _WarnImport } from "core/Misc/devTools"; @@ -116,8 +116,7 @@ export class ClusteredLight extends Light { } private readonly _proxyMaterial: ShaderMaterial; - // TODO: rename to proxyMesh - private _lightProxy: Mesh; + private _proxyMesh: Mesh; private _proxyTesselation = 8; public get proxyTesselation(): number { @@ -129,7 +128,7 @@ export class ClusteredLight extends Light { return; } this._proxyTesselation = tesselation; - this._lightProxy.dispose(); + this._proxyMesh.dispose(); this._createProxyMesh(); } @@ -156,9 +155,9 @@ export class ClusteredLight extends Light { this._proxyMaterial = new ShaderMaterial("ProxyMaterial", this._scene, proxyShader, { attributes: ["position"], uniforms: ["tileMaskResolution"], + samplers: ["lightDataTexture"], uniformBuffers: ["Scene"], storageBuffers: ["tileMaskBuffer"], - samplers: ["lightDataTexture"], defines: [`CLUSTLIGHT_BATCH ${this._batchSize}`], shaderLanguage: engine.isWebGPU ? ShaderLanguage.WGSL : ShaderLanguage.GLSL, extraInitializationsAsync: async () => { @@ -199,29 +198,29 @@ export class ClusteredLight extends Light { // We can get the height from half the angle of that triangle (assuming a side length of 1) const lowRadius = Math.cos(Math.PI / this._proxyTesselation); // We scale up the disc so the lowest radius still wraps the light - this._lightProxy = CreateDisc("LightProxy", { radius: 1 / lowRadius, tessellation: this._proxyTesselation }); + this._proxyMesh = CreateDisc("ProxyMesh", { radius: 1 / lowRadius, tessellation: this._proxyTesselation }); // Make sure it doesn't render for the default scene - this._scene.removeMesh(this._lightProxy); - this._lightProxy.material = this._proxyMaterial; + this._scene.removeMesh(this._proxyMesh); + this._proxyMesh.material = this._proxyMaterial; if (this._tileMaskBatches > 0) { - this._tileMaskTexture.renderList = [this._lightProxy]; + this._tileMaskTexture.renderList = [this._proxyMesh]; // We don't actually use the matrix data but we need enough capacity for the lights - this._lightProxy.thinInstanceSetBuffer("matrix", new Float32Array(this._tileMaskBatches * this._batchSize * 16)); - this._lightProxy.thinInstanceCount = this._lights.length; - this._lightProxy.isVisible = this._lights.length > 0; + this._proxyMesh.thinInstanceSetBuffer("matrix", new Float32Array(this._tileMaskBatches * this._batchSize * 16)); + this._proxyMesh.thinInstanceCount = this._lights.length; + this._proxyMesh.isVisible = this._lights.length > 0; } } /** @internal */ public _updateBatches(): RenderTargetTexture { - this._lightProxy.isVisible = this._lights.length > 0; + this._proxyMesh.isVisible = this._lights.length > 0; // Ensure space for atleast 1 batch const batches = Math.max(Math.ceil(this._lights.length / this._batchSize), 1); if (this._tileMaskBatches >= batches) { - this._lightProxy.thinInstanceCount = this._lights.length; + this._proxyMesh.thinInstanceCount = this._lights.length; return this._tileMaskTexture; } const engine = this.getEngine(); @@ -259,7 +258,7 @@ export class ClusteredLight extends Light { this._tileMaskTexture.renderParticles = false; this._tileMaskTexture.renderSprites = false; this._tileMaskTexture.noPrePassRenderer = true; - this._tileMaskTexture.renderList = [this._lightProxy]; + this._tileMaskTexture.renderList = [this._proxyMesh]; this._tileMaskTexture.onBeforeBindObservable.add(() => { this._updateLightData(); @@ -276,13 +275,14 @@ export class ClusteredLight extends Light { this._tileMaskBuffer?.dispose(); const bufferSize = this._horizontalTiles * this._verticalTiles * batches * 4; this._tileMaskBuffer = new StorageBuffer(engine, bufferSize); + this._proxyMaterial.setStorageBuffer("tileMaskBuffer", this._tileMaskBuffer); } this._proxyMaterial.setVector3("tileMaskResolution", new Vector3(this._horizontalTiles, this.verticalTiles, batches)); // We don't actually use the matrix data but we need enough capacity for the lights - this._lightProxy.thinInstanceSetBuffer("matrix", new Float32Array(maxLights * 16)); - this._lightProxy.thinInstanceCount = this._lights.length; + this._proxyMesh.thinInstanceSetBuffer("matrix", new Float32Array(maxLights * 16)); + this._proxyMesh.thinInstanceCount = this._lights.length; this._tileMaskBatches = batches; return this._tileMaskTexture; } @@ -347,7 +347,7 @@ export class ClusteredLight extends Light { this._lightDataTexture.dispose(); this._tileMaskTexture.dispose(); this._tileMaskBuffer?.dispose(); - this._lightProxy.dispose(doNotRecurse, disposeMaterialAndTextures); + this._proxyMesh.dispose(doNotRecurse, disposeMaterialAndTextures); super.dispose(doNotRecurse, disposeMaterialAndTextures); } @@ -359,14 +359,15 @@ export class ClusteredLight extends Light { this._scene.removeLight(light); this._lights.push(light); - this._lightProxy.isVisible = true; - this._lightProxy.thinInstanceCount = this._lights.length; + this._proxyMesh.isVisible = true; + this._proxyMesh.thinInstanceCount = this._lights.length; } protected override _buildUniformLayout(): void { this._uniformBuffer.addUniform("vLightData", 4); this._uniformBuffer.addUniform("vLightDiffuse", 4); this._uniformBuffer.addUniform("vLightSpecular", 4); + this._uniformBuffer.addUniform("vNumLights", 1); this._uniformBuffer.addUniform("shadowsInfo", 3); this._uniformBuffer.addUniform("depthValues", 2); this._uniformBuffer.create(); @@ -376,13 +377,14 @@ export class ClusteredLight extends Light { const engine = this.getEngine(); const hscale = this._horizontalTiles / engine.getRenderWidth(); const vscale = this._verticalTiles / engine.getRenderHeight(); - this._uniformBuffer.updateFloat4("vLightData", hscale, vscale, this._verticalTiles, this._lights.length, lightIndex); + this._uniformBuffer.updateFloat4("vLightData", this._horizontalTiles, this._verticalTiles, hscale, vscale, lightIndex); + this._uniformBuffer.updateFloat("vNumLights", this._lights.length); return this; } public override transferTexturesToEffect(effect: Effect, lightIndex: string): Light { const engine = this.getEngine(); - effect.setTexture("lightsTexture" + lightIndex, this._lightDataTexture); + effect.setTexture("lightDataTexture" + lightIndex, this._lightDataTexture); if (engine.isWebGPU) { (engine).setStorageBuffer("tileMaskBuffer" + lightIndex, this._tileMaskBuffer); } else { @@ -403,6 +405,6 @@ export class ClusteredLight extends Light { public override _isReady(): boolean { this._updateBatches(); - return this._lightProxy.isReady(true, true); + return this._proxyMesh.isReady(true, true); } } diff --git a/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts b/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts deleted file mode 100644 index 4e200c2c6d6..00000000000 --- a/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Constants } from "core/Engines/constants"; -import { ShaderLanguage } from "core/Materials/shaderLanguage"; -import { ShaderMaterial } from "core/Materials/shaderMaterial"; -import type { Matrix } from "core/Maths/math.vector"; -import type { Mesh } from "core/Meshes/mesh"; -import type { SubMesh } from "core/Meshes/subMesh"; - -import type { ClusteredLight } from "./clusteredLight"; - -export class LightProxyMaterial extends ShaderMaterial { - private readonly _clusteredLight: ClusteredLight; - - constructor(name: string, clusteredLight: ClusteredLight) { - const shader = { vertex: "lightProxy", fragment: "lightProxy" }; - const webgpu = clusteredLight.getEngine().isWebGPU; - super(name, clusteredLight._scene, shader, { - attributes: ["position"], - uniforms: ["halfTileRes"], - uniformBuffers: ["Scene", "Light0"], - storageBuffers: ["tileMaskBuffer0"], - defines: ["LIGHT0", "CLUSTLIGHT0", "CLUSTLIGHT_WRITE"], - shaderLanguage: webgpu ? ShaderLanguage.WGSL : ShaderLanguage.GLSL, - extraInitializationsAsync: async () => { - if (webgpu) { - await Promise.all([import("../../ShadersWGSL/lightProxy.vertex"), import("../../ShadersWGSL/lightProxy.fragment")]); - } else { - await Promise.all([import("../../Shaders/lightProxy.vertex"), import("../../Shaders/lightProxy.fragment")]); - } - }, - }); - - this._clusteredLight = clusteredLight; - // Additive blending is for merging masks on WebGL - this.transparencyMode = ShaderMaterial.MATERIAL_ALPHABLEND; - this.alphaMode = Constants.ALPHA_ADD; - } - - public override bindForSubMesh(world: Matrix, mesh: Mesh, subMesh: SubMesh): void { - if (subMesh.effect) { - this._clusteredLight._bindLight(0, this.getScene(), subMesh.effect, false, false); - - subMesh.effect.setFloat2("halfTileRes", this._clusteredLight.horizontalTiles / 2, this._clusteredLight.verticalTiles / 2); - } - super.bindForSubMesh(world, mesh, subMesh); - } -} diff --git a/packages/dev/core/src/Materials/materialHelper.functions.ts b/packages/dev/core/src/Materials/materialHelper.functions.ts index 0d40904d2af..8d404f3c33d 100644 --- a/packages/dev/core/src/Materials/materialHelper.functions.ts +++ b/packages/dev/core/src/Materials/materialHelper.functions.ts @@ -1074,7 +1074,7 @@ export function PrepareDefinesForCamera(scene: Scene, defines: any): boolean { * @param uniformBuffersList defines an optional list of uniform buffers * @param updateOnlyBuffersList True to only update the uniformBuffersList array * @param iesLightTexture defines if IES texture must be used - * @param clusteredLightsTexture defines if a tile mask texture must be used + * @param clusteredLightTextures defines if the clustered light textures must be used */ export function PrepareUniformsAndSamplersForLight( lightIndex: number, @@ -1084,7 +1084,7 @@ export function PrepareUniformsAndSamplersForLight( uniformBuffersList: Nullable = null, updateOnlyBuffersList = false, iesLightTexture = false, - clusteredLightsTexture = false + clusteredLightTextures = false ) { if (uniformBuffersList) { uniformBuffersList.push("Light" + lightIndex); @@ -1127,7 +1127,7 @@ export function PrepareUniformsAndSamplersForLight( if (iesLightTexture) { samplersList.push("iesLightTexture" + lightIndex); } - if (clusteredLightsTexture) { + if (clusteredLightTextures) { samplersList.push("lightDataTexture" + lightIndex); samplersList.push("tileMaskTexture" + lightIndex); } diff --git a/packages/dev/core/src/Materials/standardMaterial.ts b/packages/dev/core/src/Materials/standardMaterial.ts index 93f1f6a74ab..0c718cbfa0c 100644 --- a/packages/dev/core/src/Materials/standardMaterial.ts +++ b/packages/dev/core/src/Materials/standardMaterial.ts @@ -1230,7 +1230,7 @@ export class StandardMaterial extends PushMaterial { } // Check if lights are ready - if (defines["AREALIGHTUSED"] || defines["CLUSTLIGHT_MAX"]) { + if (defines["AREALIGHTUSED"] || defines["CLUSTLIGHT_BATCH"]) { for (let index = 0; index < mesh.lightSources.length; index++) { if (!mesh.lightSources[index]._isReady()) { return false; diff --git a/packages/dev/core/src/Shaders/ShadersInclude/clusteredLightFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/clusteredLightFunctions.fx deleted file mode 100644 index da907fda11d..00000000000 --- a/packages/dev/core/src/Shaders/ShadersInclude/clusteredLightFunctions.fx +++ /dev/null @@ -1,27 +0,0 @@ -#if defined(LIGHT{X}) && defined(CLUSTLIGHT{X}) && CLUSTLIGHT_MAX > 0 -lightingInfo computeClusteredLighting{X}(vec3 viewDirectionW, vec3 vNormal, vec3 diffuseScale, float glossiness) { - lightingInfo result; - vec4 maskTexel = texelFetch(tileMaskTexture{X}, ivec2(gl_FragCoord.xy * light{X}.vLightData.xy), 0); - uint mask = uint(maskTexel.r); - vec3 specularScale = light{X}.vLightSpecular.rgb; - - int len = int(light{X}.vLightData.w); - for (int i = 0; i < len; i += 1) { - if ((mask & (1u << i)) == 0u) { - continue; - } - vec4 position = texelFetch(lightsDataTexture{X}, ivec2(0, i), 0); - vec4 diffuse = texelFetch(lightsDataTexture{X}, ivec2(1, i), 0); - vec4 specular = texelFetch(lightsDataTexture{X}, ivec2(2, i), 0); - vec4 direction = texelFetch(lightsDataTexture{X}, ivec2(3, i), 0); - vec4 falloff = texelFetch(lightsDataTexture{X}, ivec2(4, i), 0); - - lightingInfo info = computeSpotLighting(viewDirectionW, vNormal, position, direction, diffuse.rgb * diffuseScale, specular.rgb * specularScale, diffuse.a, glossiness); - result.diffuse += info.diffuse; - #ifdef SPECULARTERM - result.specular += info.specular; - #endif - } - return result; -} -#endif diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx index 1b5738277b5..e0c309ecb96 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx @@ -231,7 +231,7 @@ #endif ); #elif defined(CLUSTLIGHT{X}) && CLUSTLIGHT_BATCH > 0 - info = computeClusteredLighting(lightDataTexture{X}, tileMaskTexture{X}, viewDirectionW, normalW, light{X}.vLightData, glossiness); + info = computeClusteredLighting(lightDataTexture{X}, tileMaskTexture{X}, viewDirectionW, normalW, light{X}.vLightData, int(light{X}.vNumLights), glossiness); #endif #endif diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx index cb911a7100a..9bff28ecf69 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx @@ -12,6 +12,8 @@ vec4 vLightFalloff; #elif defined(HEMILIGHT{X}) vec3 vLightGround; + #elif defined(CLUSTLIGHT{X}) + float vNumLights; // TODO: remove once depth clustering is added #endif #if defined(AREALIGHT{X}) vec4 vLightWidth; diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightVxUboDeclaration.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightVxUboDeclaration.fx index 1c1a2005e17..af15b78efa7 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightVxUboDeclaration.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightVxUboDeclaration.fx @@ -12,6 +12,8 @@ vec4 vLightFalloff; #elif defined(HEMILIGHT{X}) vec3 vLightGround; + #elif defined(CLUSTLIGHT{X}) + float vNumLights; // TODO: remove once depth clustering is added #endif #if defined(AREALIGHT{X}) vec4 vLightWidth; diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx index 284f8ff2fa6..047590ee10a 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx @@ -191,22 +191,21 @@ lightingInfo computeClusteredLighting( vec3 viewDirectionW, vec3 vNormal, vec4 clusteredData, + int numLights, float glossiness ) { lightingInfo result; - ivec2 maskCoord = ivec2(gl_FragCoord.xy * clusteredData.xy); - int height = int(clusteredData.z); - int len = int(clusteredData.w); - - for (int i = 0; i < len;) { - vec4 maskTexel = texelFetch(tileMaskTexture, maskCoord, 0); - uint mask = uint(maskTexel.r); - maskCoord.y += height; - - int batchEnd = min(i + CLUSTLIGHT_BATCH, len); - uint bit = 1u; - for(; i < batchEnd; i += 1, bit <<= 1) { - if ((mask & bit) == 0u) { + int maskHeight = int(clusteredData.y); + ivec2 tilePosition = ivec2(gl_FragCoord.xy * clusteredData.zw); + tilePosition.x = min(tilePosition.x, maskHeight - 1); + + for (int i = 0; i < numLights;) { + uint mask = uint(texelFetch(tileMaskTexture, tilePosition, 0).r); + tilePosition.y += maskHeight; + + int batchEnd = min(i + CLUSTLIGHT_BATCH, numLights); + for(; i < batchEnd && mask != 0u; i += 1, mask >>= 1) { + if ((mask & 1u) == 0u) { continue; } vec4 lightData = texelFetch(lightDataTexture, ivec2(0, i), 0); @@ -221,6 +220,7 @@ lightingInfo computeClusteredLighting( result.specular += info.specular; #endif } + i = batchEnd; } return result; } diff --git a/packages/dev/core/src/Shaders/ShadersInclude/spotLightDeclaration.fx b/packages/dev/core/src/Shaders/ShadersInclude/spotLightDeclaration.fx deleted file mode 100644 index b620d530cd6..00000000000 --- a/packages/dev/core/src/Shaders/ShadersInclude/spotLightDeclaration.fx +++ /dev/null @@ -1,8 +0,0 @@ -// Used in clustered lights -struct SpotLight { - vec4 position; - vec4 diffuse; - vec4 specular; - vec4 direction; - vec4 falloff; -}; diff --git a/packages/dev/core/src/Shaders/lightProxy.fragment.fx b/packages/dev/core/src/Shaders/lightProxy.fragment.fx index dfe06b3e6b1..cd548a7bdcb 100644 --- a/packages/dev/core/src/Shaders/lightProxy.fragment.fx +++ b/packages/dev/core/src/Shaders/lightProxy.fragment.fx @@ -1,12 +1,9 @@ -flat varying highp int vBatch; +flat varying vec2 vLimits; flat varying highp uint vMask; -uniform vec3 tileMaskResolution; - void main(void) { - // Ensure the pixel we're writing to is of the correct batch - int coordBatch = int(gl_FragCoord.y / tileMaskResolution.y); - if (coordBatch != vBatch) { + // Ensure the pixel is within the limits for the batch + if (gl_FragCoord.y < vLimits.x || gl_FragCoord.y > vLimits.y) { discard; } diff --git a/packages/dev/core/src/Shaders/lightProxy.vertex.fx b/packages/dev/core/src/Shaders/lightProxy.vertex.fx index a7859a64a52..73952a4321a 100644 --- a/packages/dev/core/src/Shaders/lightProxy.vertex.fx +++ b/packages/dev/core/src/Shaders/lightProxy.vertex.fx @@ -1,18 +1,16 @@ attribute vec3 position; -flat varying highp int vBatch; +flat varying vec2 vLimits; flat varying highp uint vMask; // Uniforms #include -uniform vec3 tileMaskResolution; uniform sampler2D lightDataTexture; +uniform vec3 tileMaskResolution; void main(void) { vec4 lightData = texelFetch(lightDataTexture, ivec2(0, gl_InstanceID), 0); vec4 falloff = texelFetch(lightDataTexture, ivec2(4, gl_InstanceID), 0); - vBatch = gl_InstanceID / CLUSTLIGHT_BATCH; - vMask = 1u << (gl_InstanceID % CLUSTLIGHT_BATCH); // We don't apply the view matrix to the disc since we want it always facing the camera vec4 viewPosition = view * vec4(lightData.xyz, 1) + vec4(position * falloff.x, 0); @@ -23,8 +21,11 @@ void main(void) { // Round to a whole tile boundary with a bit of wiggle room tilePosition = mix(floor(tilePosition) - 0.01, ceil(tilePosition) + 0.01, greaterThan(position.xy, vec2(0))); // Reposition vertically based on current batch - tilePosition.y = (tilePosition.y + float(vBatch) * tileMaskResolution.y) / tileMaskResolution.z; + float offset = float(gl_InstanceID / CLUSTLIGHT_BATCH) * tileMaskResolution.y; + tilePosition.y = (tilePosition.y + offset) / tileMaskResolution.z; // We don't care about depth and don't want it to be clipped so set Z to 0 - gl_Position = vec4(tilePosition.xy / tileMaskResolution.xy * 2.0 - 1.0, 0, 1); + gl_Position = vec4(tilePosition / tileMaskResolution.xy * 2.0 - 1.0, 0, 1); + vLimits = vec2(offset, offset + tileMaskResolution.y); + vMask = 1u << (gl_InstanceID % CLUSTLIGHT_BATCH); } diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightFragment.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightFragment.fx index 334c8f36839..131a792e7b0 100644 --- a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightFragment.fx +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightFragment.fx @@ -235,7 +235,7 @@ #endif ); #elif defined(CLUSTLIGHT{X}) - info = computeClusteredLighting(&tileMaskBuffer{X}, viewDirectionW, normalW, light{X}.vLightData, &light{X}.vLights, diffuse{X}.rgb, light{X}.vLightSpecular.rgb, glossiness); + info = computeClusteredLighting(lightDataTexture{X}, &tileMaskBuffer{X}, viewDirectionW, normalW, light{X}.vLightData, i32(light{X}.vNumLights), glossiness); #endif #endif diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightUboDeclaration.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightUboDeclaration.fx index 31fc65e1c9c..0d96c38d06a 100644 --- a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightUboDeclaration.fx +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightUboDeclaration.fx @@ -12,7 +12,7 @@ #elif defined(HEMILIGHT{X}) vLightGround: vec3f, #elif defined(CLUSTLIGHT{X}) - vLights: array, + vNumLights: f32, #endif #if defined(AREALIGHT{X}) vLightWidth: vec4f, @@ -36,12 +36,9 @@ var light{X} : Light{X}; #endif #ifdef CLUSTLIGHT{X} -#ifdef CLUSTLIGHT_WRITE - var tileMaskBuffer{X}: array>; -#else + var lightDataTexture{X}: texture_2d; var tileMaskBuffer{X}: array; #endif -#endif #ifdef SHADOW{X} #ifdef SHADOWCSM{X} diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightVxUboDeclaration.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightVxUboDeclaration.fx index 3116735465e..2eb1a9ca274 100644 --- a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightVxUboDeclaration.fx +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightVxUboDeclaration.fx @@ -12,7 +12,7 @@ #elif defined(HEMILIGHT{X}) vLightGround: vec3f, #elif defined(CLUSTLIGHT{X}) - vLights: array, + vNumLights: f32, #endif #if defined(AREALIGHT{X}) vLightWidth: vec4f, diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx index 0e563f8ef02..3ca5ef86639 100644 --- a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx @@ -185,33 +185,44 @@ fn computeAreaLighting(ltc1: texture_2d, ltc1Sampler:sampler, ltc2:texture_ #endif fn computeClusteredLighting( - tileMask: ptr>, + lightDataTexture: texture_2d, + tileMaskBuffer: ptr>, viewDirectionW: vec3f, vNormal: vec3f, - lightData: vec4f, - lights: ptr>, - diffuseScale: vec3f, - specularScale: vec3f, + clusteredData: vec4f, + numLights: i32, glossiness: f32 ) -> lightingInfo { var result: lightingInfo; - let tilePos = vec2u(fragmentInputs.position.xy * lightData.xy); - let strideLen = vec2u(lightData.zw); - let mask = tileMask[tilePos.y * strideLen.x + tilePos.x]; - - // TODO: merge subgroups - - for (var i = 0u; i < strideLen.y; i += 1u) { - if (mask & (1u << i)) == 0 { - continue; + let maskResolution = vec2i(clusteredData.xy); + let maskStride = maskResolution.x * maskResolution.y; + let tilePosition = vec2i(fragmentInputs.position.xy * clusteredData.zw); + var tileIndex = min(tilePosition.y * maskResolution.x + tilePosition.x, maskStride - 1); + + for (var i = 0; i < numLights;) { + var mask = tileMaskBuffer[tileIndex]; + tileIndex += maskStride; + let batchEnd = min(i + CLUSTLIGHT_BATCH, numLights); + + for (; i < batchEnd && mask != 0; i += 1) { + // Skip as much as we can + let trailing = firstTrailingBit(mask); + mask >>= trailing + 1; + i += i32(trailing); + + let lightData = textureLoad(lightDataTexture, vec2i(0, i), 0); + let diffuse = textureLoad(lightDataTexture, vec2i(1, i), 0); + let specular = textureLoad(lightDataTexture, vec2i(2, i), 0); + let direction = textureLoad(lightDataTexture, vec2i(3, i), 0); + let falloff = textureLoad(lightDataTexture, vec2i(4, i), 0); + + let info = computeSpotLighting(viewDirectionW, vNormal, lightData, direction, diffuse.rgb, specular.rgb, diffuse.a, glossiness); + result.diffuse += info.diffuse; + #ifdef SPECULARTERM + result.specular += info.specular; + #endif } - let diffuse = lights[i].diffuse.rgb * diffuseScale; - let specular = lights[i].specular.rgb * specularScale; - let info = computeSpotLighting(viewDirectionW, vNormal, lights[i].position, lights[i].direction, diffuse, specular, lights[i].diffuse.a, glossiness); - result.diffuse += info.diffuse; - #ifdef SPECULARTERM - result.specular += info.specular; - #endif + i = batchEnd; } return result; } diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/spotLightDeclaration.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/spotLightDeclaration.fx deleted file mode 100644 index 80ef25338c3..00000000000 --- a/packages/dev/core/src/ShadersWGSL/ShadersInclude/spotLightDeclaration.fx +++ /dev/null @@ -1,8 +0,0 @@ -// Used in clustered lights -struct SpotLight { - position: vec4f, - diffuse: vec4f, - specular: vec4f, - direction: vec4f, - falloff: vec4f, -} diff --git a/packages/dev/core/src/ShadersWGSL/lightProxy.fragment.fx b/packages/dev/core/src/ShadersWGSL/lightProxy.fragment.fx index 3e75b35f66a..b92742a534f 100644 --- a/packages/dev/core/src/ShadersWGSL/lightProxy.fragment.fx +++ b/packages/dev/core/src/ShadersWGSL/lightProxy.fragment.fx @@ -1,12 +1,13 @@ +flat varying vOffset: u32; flat varying vMask: u32; -// Declarations -#include -#include[0..1] +// Uniforms +uniform tileMaskResolution: vec3f; +var tileMaskBuffer: array>; @fragment fn main(input: FragmentInputs) -> FragmentOutputs { - let tilePos = vec2u(fragmentInputs.position.xy); - let stride = u32(light0.vLightData.z); - atomicOr(&tileMaskBuffer0[tilePos.y * stride + tilePos.x], fragmentInputs.vMask); + let uPosition = vec2u(fragmentInputs.position.xy); + let tileIndex = fragmentInputs.vOffset + uPosition.y * u32(uniforms.tileMaskResolution.x) + uPosition.x; + atomicOr(&tileMaskBuffer[tileIndex], fragmentInputs.vMask); } diff --git a/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx b/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx index cf5543b6337..6d1d6217490 100644 --- a/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx +++ b/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx @@ -1,27 +1,31 @@ attribute position: vec3f; +flat varying vOffset: u32; flat varying vMask: u32; // Uniforms #include -#include -#include[0..1] +var lightDataTexture: texture_2d; +uniform tileMaskResolution: vec3f; uniform halfTileRes: vec2f; @vertex fn main(input: VertexInputs) -> FragmentInputs { - let light = &light0.vLights[vertexInputs.instanceIndex]; + let lightData = textureLoad(lightDataTexture, vec2u(0, vertexInputs.instanceIndex), 0); + let falloff = textureLoad(lightDataTexture, vec2u(4, vertexInputs.instanceIndex), 0); // We don't apply the view matrix to the disc since we want it always facing the camera - let viewPosition = scene.view * vec4f(light.position.xyz, 1) + vec4f(vertexInputs.position * light.falloff.x, 0); + let viewPosition = scene.view * vec4f(lightData.xyz, 1) + vec4f(vertexInputs.position * falloff.x, 0); let projPosition = scene.projection * viewPosition; - // Convert to NDC space and scale to the tile resolution - var tilePosition = projPosition.xy / projPosition.w * uniforms.halfTileRes; + // Convert to NDC 0->1 space and scale to the tile resolution + var tilePosition = (projPosition.xy / projPosition.w + 1.0) / 2.0 * uniforms.tileMaskResolution.xy; // Round to a whole tile boundary with a bit of wiggle room tilePosition = select(floor(tilePosition) - 0.01, ceil(tilePosition) + 0.01, vertexInputs.position.xy > vec2f(0)); - // We don't care about depth and don't want it to be clipped - vertexOutputs.position = vec4f(tilePosition.xy / uniforms.halfTileRes, 0, 1); + // We don't care about depth and don't want it to be clipped so set Z to 0 + vertexOutputs.position = vec4f(tilePosition / uniforms.tileMaskResolution.xy * 2.0 - 1.0, 0, 1); + let uResolution = vec2u(uniforms.tileMaskResolution.xy); + vertexOutputs.vOffset = vertexInputs.instanceIndex / CLUSTLIGHT_BATCH * uResolution.x * uResolution.y; vertexOutputs.vMask = 1u << vertexInputs.instanceIndex; } From b9c71d31957a5912962138a8e71adbb13b1eceec Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Fri, 25 Jul 2025 09:42:26 +1000 Subject: [PATCH 23/30] Add support for non-UBO devices --- packages/dev/core/src/Engines/thinEngine.ts | 2 +- packages/dev/core/src/Lights/Clustered/clusteredLight.ts | 6 +++--- packages/dev/core/src/Materials/materialHelper.functions.ts | 1 + packages/dev/core/src/Materials/uniformBuffer.ts | 6 +++--- .../src/Shaders/ShadersInclude/lightFragmentDeclaration.fx | 6 ++++++ packages/dev/core/src/Shaders/lightProxy.vertex.fx | 2 +- 6 files changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/dev/core/src/Engines/thinEngine.ts b/packages/dev/core/src/Engines/thinEngine.ts index c10fe907897..a808b2ef1dc 100644 --- a/packages/dev/core/src/Engines/thinEngine.ts +++ b/packages/dev/core/src/Engines/thinEngine.ts @@ -122,7 +122,7 @@ export class ThinEngine extends AbstractEngine { { key: "Chrome/74.+?Mobile", capture: null, captureConstraint: null, targets: ["vao"] }, { key: "Mac OS.+Chrome/71", capture: null, captureConstraint: null, targets: ["vao"] }, { key: "Mac OS.+Chrome/72", capture: null, captureConstraint: null, targets: ["vao"] }, - // { key: "Mac OS.+Chrome", capture: null, captureConstraint: null, targets: ["uniformBuffer"] }, + { key: "Mac OS.+Chrome", capture: null, captureConstraint: null, targets: ["uniformBuffer"] }, { key: "Chrome/12\\d\\..+?Mobile", capture: null, captureConstraint: null, targets: ["uniformBuffer"] }, // desktop osx safari 15.4 { key: ".*AppleWebKit.*(15.4).*Safari", capture: null, captureConstraint: null, targets: ["antialias", "maxMSAASamples"] }, diff --git a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts index fcf72ca13a3..bbb915a1982 100644 --- a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts +++ b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts @@ -26,7 +26,7 @@ import "core/Meshes/thinInstanceMesh"; export class ClusteredLight extends Light { private static _GetEngineBatchSize(engine: AbstractEngine): number { const caps = engine._caps; - if (!engine.supportsUniformBuffers || !caps.texelFetch) { + if (!caps.texelFetch) { return 0; } else if (engine.isWebGPU) { // On WebGPU we use atomic writes to storage textures @@ -154,7 +154,7 @@ export class ClusteredLight extends Light { const proxyShader = { vertex: "lightProxy", fragment: "lightProxy" }; this._proxyMaterial = new ShaderMaterial("ProxyMaterial", this._scene, proxyShader, { attributes: ["position"], - uniforms: ["tileMaskResolution"], + uniforms: ["view", "projection", "tileMaskResolution"], samplers: ["lightDataTexture"], uniformBuffers: ["Scene"], storageBuffers: ["tileMaskBuffer"], @@ -378,7 +378,7 @@ export class ClusteredLight extends Light { const hscale = this._horizontalTiles / engine.getRenderWidth(); const vscale = this._verticalTiles / engine.getRenderHeight(); this._uniformBuffer.updateFloat4("vLightData", this._horizontalTiles, this._verticalTiles, hscale, vscale, lightIndex); - this._uniformBuffer.updateFloat("vNumLights", this._lights.length); + this._uniformBuffer.updateFloat("vNumLights", this._lights.length, lightIndex); return this; } diff --git a/packages/dev/core/src/Materials/materialHelper.functions.ts b/packages/dev/core/src/Materials/materialHelper.functions.ts index 8d404f3c33d..cb874b25e44 100644 --- a/packages/dev/core/src/Materials/materialHelper.functions.ts +++ b/packages/dev/core/src/Materials/materialHelper.functions.ts @@ -1103,6 +1103,7 @@ export function PrepareUniformsAndSamplersForLight( "vLightHeight" + lightIndex, "vLightFalloff" + lightIndex, "vLightGround" + lightIndex, + "vNumLights" + lightIndex, "lightMatrix" + lightIndex, "shadowsInfo" + lightIndex, "depthValues" + lightIndex diff --git a/packages/dev/core/src/Materials/uniformBuffer.ts b/packages/dev/core/src/Materials/uniformBuffer.ts index c4bb00aa5d7..989526210d7 100644 --- a/packages/dev/core/src/Materials/uniformBuffer.ts +++ b/packages/dev/core/src/Materials/uniformBuffer.ts @@ -66,7 +66,7 @@ export class UniformBuffer { * This is dynamic to allow compat with webgl 1 and 2. * You will need to pass the name of the uniform as well as the value. */ - public updateFloat: (name: string, x: number) => void; + public updateFloat: (name: string, x: number, suffix?: string) => void; /** * Lambda to Update a vec2 of float in a uniform buffer. @@ -856,8 +856,8 @@ export class UniformBuffer { this.updateUniform(name, UniformBuffer._TempBuffer, 8); } - private _updateFloatForEffect(name: string, x: number) { - this._currentEffect.setFloat(name, x); + private _updateFloatForEffect(name: string, x: number, suffix = "") { + this._currentEffect.setFloat(name + suffix, x); } private _updateFloatForUniform(name: string, x: number) { diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightFragmentDeclaration.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightFragmentDeclaration.fx index 7fdf9f71984..41250be1219 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightFragmentDeclaration.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightFragmentDeclaration.fx @@ -90,4 +90,10 @@ uniform mat4 textureProjectionMatrix{X}; uniform sampler2D projectionLightTexture{X}; #endif + #ifdef CLUSTLIGHT{X} + uniform float vNumLights{X}; + uniform sampler2D lightDataTexture{X}; + // Ensure the mask is sampled with high precision + uniform highp sampler2D tileMaskTexture{X}; + #endif #endif \ No newline at end of file diff --git a/packages/dev/core/src/Shaders/lightProxy.vertex.fx b/packages/dev/core/src/Shaders/lightProxy.vertex.fx index 73952a4321a..00e19541cb7 100644 --- a/packages/dev/core/src/Shaders/lightProxy.vertex.fx +++ b/packages/dev/core/src/Shaders/lightProxy.vertex.fx @@ -3,7 +3,7 @@ flat varying vec2 vLimits; flat varying highp uint vMask; // Uniforms -#include +#include<__decl__sceneVertex> uniform sampler2D lightDataTexture; uniform vec3 tileMaskResolution; From 19e3b561f3227c51d0bc34ef3c59d7850d589764 Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Fri, 25 Jul 2025 10:38:48 +1000 Subject: [PATCH 24/30] PBR support --- .../Shaders/ShadersInclude/lightFragment.fx | 11 +- .../ShadersInclude/lightsFragmentFunctions.fx | 4 +- .../pbrClusteredLightFunctions.fx | 145 ------------ .../pbrDirectLightingFunctions.fx | 159 +++++++++++++ packages/dev/core/src/Shaders/pbr.fragment.fx | 6 +- packages/dev/core/src/Shaders/pbr.vertex.fx | 1 - .../ShadersInclude/lightFragment.fx | 4 +- .../ShadersInclude/lightsFragmentFunctions.fx | 1 - .../pbrDirectLightingFunctions.fx | 210 +++++++++--------- .../dev/core/src/ShadersWGSL/pbr.fragment.fx | 1 - 10 files changed, 282 insertions(+), 260 deletions(-) delete mode 100644 packages/dev/core/src/Shaders/ShadersInclude/pbrClusteredLightFunctions.fx diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx index e0c309ecb96..e894ce271c1 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx @@ -6,14 +6,17 @@ vec4 diffuse{X} = light{X}.vLightDiffuse; #define CUSTOM_LIGHT{X}_COLOR // Use to modify light color. Currently only supports diffuse. - #if defined(PBR) && defined(CLUSTLIGHT{X}) && CLUSTLIGHT_MAX > 0 - info = computeClusteredLighting{X}( + #if defined(PBR) && defined(CLUSTLIGHT{X}) && CLUSTLIGHT_BATCH > 0 + info = computeClusteredLighting( + lightDataTexture{X}, + tileMaskTexture{X}, + light{X}.vLightData, + int(light{X}.vNumLights), viewDirectionW, normalW, vPositionW, surfaceAlbedo, - reflectivityOut, - diffuse{X}.rgb + reflectivityOut #ifdef SS_TRANSLUCENCY , subSurfaceOut #endif diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx index 047590ee10a..1151066ecb7 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx @@ -184,7 +184,7 @@ lightingInfo computeAreaLighting(sampler2D ltc1, sampler2D ltc2, vec3 viewDirect // End Area Light #endif -#if defines(CLUSTLIGHT_BATCH) && CLUSTLIGHT_BATCH > 0 +#if defined(CLUSTLIGHT_BATCH) && CLUSTLIGHT_BATCH > 0 lightingInfo computeClusteredLighting( sampler2D lightDataTexture, sampler2D tileMaskTexture, @@ -202,12 +202,12 @@ lightingInfo computeClusteredLighting( for (int i = 0; i < numLights;) { uint mask = uint(texelFetch(tileMaskTexture, tilePosition, 0).r); tilePosition.y += maskHeight; - int batchEnd = min(i + CLUSTLIGHT_BATCH, numLights); for(; i < batchEnd && mask != 0u; i += 1, mask >>= 1) { if ((mask & 1u) == 0u) { continue; } + vec4 lightData = texelFetch(lightDataTexture, ivec2(0, i), 0); vec4 diffuse = texelFetch(lightDataTexture, ivec2(1, i), 0); vec4 specular = texelFetch(lightDataTexture, ivec2(2, i), 0); diff --git a/packages/dev/core/src/Shaders/ShadersInclude/pbrClusteredLightFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/pbrClusteredLightFunctions.fx deleted file mode 100644 index e5efab093d7..00000000000 --- a/packages/dev/core/src/Shaders/ShadersInclude/pbrClusteredLightFunctions.fx +++ /dev/null @@ -1,145 +0,0 @@ -#if defined(LIGHT{X}) && defined(CLUSTLIGHT{X}) && CLUSTLIGHT_MAX > 0 -lightingInfo computeClusteredLighting{X}( - vec3 V, - vec3 N, - vec3 posW, - vec3 surfaceAlbedo, - reflectivityOutParams reflectivityOut, - vec3 diffuseScale - #ifdef SS_TRANSLUCENCY - , subSurfaceOutParams subSurfaceOut - #endif - #ifdef SPECULARTERM - , float AARoughnessFactor - #endif - #ifdef ANISOTROPIC - , anisotropicOutParams anisotropicOut - #endif - #ifdef SHEEN - , sheenOutParams sheenOut - #endif - #ifdef CLEARCOAT - , clearcoatOutParams clearcoatOut - #endif -) { - float NdotV = absEps(dot(N, V)); -#include - #ifdef CLEARCOAT - specularEnvironmentR0 = clearcoatOut.specularEnvironmentR0; - #endif - - lightingInfo result; - vec4 maskTexel = texelFetch(tileMaskTexture{X}, ivec2(gl_FragCoord.xy * light{X}.vLightData.xy), 0); - uint mask = uint(maskTexel.r); - - int len = int(light{X}.vLightData.w); - for (int i = 0; i < len; i += 1) { - if ((mask & (1u << i)) == 0u) { - continue; - } - SpotLight light = light{X}.vLights[i]; - preLightingInfo preInfo = computePointAndSpotPreLightingInfo(light.position, V, N, posW); - - // Compute Attenuation infos - preInfo.NdotV = NdotV; - preInfo.attenuation = computeDistanceLightFalloff(preInfo.lightOffset, preInfo.lightDistanceSquared, light.falloff.x, light.falloff.y); - preInfo.attenuation *= computeDirectionalLightFalloff(light.direction.xyz, preInfo.L, light.direction.w, light.position.w, light.falloff.z, light.falloff.w); - - float radius = light.specular.a; - preInfo.roughness = adjustRoughnessFromLightProperties(reflectivityOut.roughness, radius, preInfo.lightDistance); - preInfo.diffuseRoughness = reflectivityOut.diffuseRoughness; - preInfo.surfaceAlbedo = surfaceAlbedo; - lightingInfo info; - - // Diffuse contribution - vec3 diffuse = light.diffuse.rgb * diffuseScale; - #ifdef SS_TRANSLUCENCY - #ifdef SS_TRANSLUCENCY_LEGACY - info.diffuse = computeDiffuseTransmittedLighting(preInfo, diffuse, subSurfaceOut.transmittance); - info.diffuseTransmission = vec3(0); - #else - info.diffuse = computeDiffuseLighting(preInfo, diffuse) * (1.0 - subSurfaceOut.translucencyIntensity); - info.diffuseTransmission = computeDiffuseTransmittedLighting(preInfo, diffuse, subSurfaceOut.transmittance); - #endif - #else - info.diffuse = computeDiffuseLighting(preInfo, diffuse); - #endif - - // Specular contribution - #ifdef SPECULARTERM - #if CONDUCTOR_SPECULAR_MODEL == CONDUCTOR_SPECULAR_MODEL_OPENPBR - vec3 metalFresnel = reflectivityOut.specularWeight * getF82Specular(preInfo.VdotH, specularEnvironmentR0, reflectivityOut.colorReflectanceF90, reflectivityOut.roughness); - vec3 dielectricFresnel = fresnelSchlickGGX(preInfo.VdotH, reflectivityOut.dielectricColorF0, reflectivityOut.colorReflectanceF90); - vec3 coloredFresnel = mix(dielectricFresnel, metalFresnel, reflectivityOut.metallic); - #else - vec3 coloredFresnel = fresnelSchlickGGX(preInfo.VdotH, specularEnvironmentR0, reflectivityOut.colorReflectanceF90); - #endif - #ifndef LEGACY_SPECULAR_ENERGY_CONSERVATION - float NdotH = dot(N, preInfo.H); - vec3 fresnel = fresnelSchlickGGX(NdotH, vec3(reflectanceF0), specularEnvironmentR90); - info.diffuse *= (vec3(1.0) - fresnel); - #endif - #ifdef ANISOTROPIC - info.specular = computeAnisotropicSpecularLighting(preInfo, V, N, anisotropicOut.anisotropicTangent, anisotropicOut.anisotropicBitangent, anisotropicOut.anisotropy, clearcoatOut.specularEnvironmentR0, specularEnvironmentR90, AARoughnessFactor, diffuse); - #else - info.specular = computeSpecularLighting(preInfo, N, specularEnvironmentR0, coloredFresnel, AARoughnessFactor, diffuse); - #endif - #endif - - // Sheen contribution - #ifdef SHEEN - #ifdef SHEEN_LINKWITHALBEDO - preInfo.roughness = sheenOut.sheenIntensity; - #else - preInfo.roughness = adjustRoughnessFromLightProperties(sheenOut.sheenRoughness, radius, preInfo.lightDistance); - #endif - info.sheen = computeSheenLighting(preInfo, normalW, sheenOut.sheenColor, specularEnvironmentR90, AARoughnessFactor, diffuse); - #endif - - // Clear Coat contribution - #ifdef CLEARCOAT - preInfo.roughness = adjustRoughnessFromLightProperties(clearcoatOut.clearCoatRoughness, radius, preInfo.lightDistance); - info.clearCoat = computeClearCoatLighting(preInfo, clearcoatOut.clearCoatNormalW, clearcoatOut.clearCoatAARoughnessFactors.x, clearcoatOut.clearCoatIntensity, diffuse); - - #ifdef CLEARCOAT_TINT - // Absorption - float absorption = computeClearCoatLightingAbsorption(clearcoatOut.clearCoatNdotVRefract, preInfo.L, clearcoatOut.clearCoatNormalW, clearcoatOut.clearCoatColor, clearcoatOut.clearCoatThickness, clearcoatOut.clearCoatIntensity); - info.diffuse *= absorption; - #ifdef SS_TRANSLUCENCY - info.diffuseTransmission *= absorption; - #endif - #ifdef SPECULARTERM - info.specular *= absorption; - #endif - #endif - - info.diffuse *= info.clearCoat.w; - #ifdef SS_TRANSLUCENCY - info.diffuseTransmission *= info.clearCoat.w; - #endif - #ifdef SPECULARTERM - info.specular *= info.clearCoat.w; - #endif - #ifdef SHEEN - info.sheen *= info.clearCoat.w; - #endif - #endif - - // Apply contributions to result - result.diffuse += info.diffuse; - #ifdef SS_TRANSLUCENCY - result.diffuseTransmission += info.diffuseTransmission; - #endif - #ifdef SPECULARTERM - result.specular += info.specular; - #endif - #ifdef CLEARCOAT - result.clearCoat += info.clearCoat; - #endif - #ifdef SHEEN - result.sheen += info.sheen; - #endif - } - return result; -} -#endif diff --git a/packages/dev/core/src/Shaders/ShadersInclude/pbrDirectLightingFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/pbrDirectLightingFunctions.fx index fd7c3f32c2d..7469ce87b4a 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/pbrDirectLightingFunctions.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/pbrDirectLightingFunctions.fx @@ -204,3 +204,162 @@ vec3 computeProjectionTextureDiffuseLighting(sampler2D projectionLightSampler, m return sheenTerm * info.attenuation * info.NdotL * lightColor; } #endif + +#if defined(CLUSTLIGHT_BATCH) && CLUSTLIGHT_BATCH > 0 + lightingInfo computeClusteredLighting( + sampler2D lightDataTexture, + sampler2D tileMaskTexture, + vec4 clusteredData, + int numLights, + vec3 V, + vec3 N, + vec3 posW, + vec3 surfaceAlbedo, + reflectivityOutParams reflectivityOut + #ifdef SS_TRANSLUCENCY + , subSurfaceOutParams subSurfaceOut + #endif + #ifdef SPECULARTERM + , float AARoughnessFactor + #endif + #ifdef ANISOTROPIC + , anisotropicOutParams anisotropicOut + #endif + #ifdef SHEEN + , sheenOutParams sheenOut + #endif + #ifdef CLEARCOAT + , clearcoatOutParams clearcoatOut + #endif + ) { + float NdotV = absEps(dot(N, V)); +#include + #ifdef CLEARCOAT + specularEnvironmentR0 = clearcoatOut.specularEnvironmentR0; + #endif + + lightingInfo result; + int maskHeight = int(clusteredData.y); + ivec2 tilePosition = ivec2(gl_FragCoord.xy * clusteredData.zw); + tilePosition.x = min(tilePosition.x, maskHeight - 1); + + for (int i = 0; i < numLights;) { + uint mask = uint(texelFetch(tileMaskTexture, tilePosition, 0).r); + tilePosition.y += maskHeight; + int batchEnd = min(i + CLUSTLIGHT_BATCH, numLights); + for (; i < batchEnd && mask != 0u; i += 1, mask >>= 1) { + if ((mask & 1u) == 0u) { + continue; + } + + vec4 lightData = texelFetch(lightDataTexture, ivec2(0, i), 0); + vec4 diffuse = texelFetch(lightDataTexture, ivec2(1, i), 0); + vec4 specular = texelFetch(lightDataTexture, ivec2(2, i), 0); + vec4 direction = texelFetch(lightDataTexture, ivec2(3, i), 0); + vec4 falloff = texelFetch(lightDataTexture, ivec2(4, i), 0); + + preLightingInfo preInfo = computePointAndSpotPreLightingInfo(lightData, V, N, posW); + preInfo.NdotV = NdotV; + + // Compute Attenuation infos + preInfo.attenuation = computeDistanceLightFalloff(preInfo.lightOffset, preInfo.lightDistanceSquared, falloff.x, falloff.y); + preInfo.attenuation *= computeDirectionalLightFalloff(direction.xyz, preInfo.L, direction.w, lightData.w, falloff.z, falloff.w); + + preInfo.roughness = adjustRoughnessFromLightProperties(reflectivityOut.roughness, specular.a, preInfo.lightDistance); + preInfo.diffuseRoughness = reflectivityOut.diffuseRoughness; + preInfo.surfaceAlbedo = surfaceAlbedo; + lightingInfo info; + + // Diffuse contribution + #ifdef SS_TRANSLUCENCY + #ifdef SS_TRANSLUCENCY_LEGACY + info.diffuse = computeDiffuseTransmittedLighting(preInfo, diffuse.rgb, subSurfaceOut.transmittance); + info.diffuseTransmission = vec3(0); + #else + info.diffuse = computeDiffuseLighting(preInfo, diffuse.rgb) * (1.0 - subSurfaceOut.translucencyIntensity); + info.diffuseTransmission = computeDiffuseTransmittedLighting(preInfo, diffuse.rgb, subSurfaceOut.transmittance); + #endif + #else + info.diffuse = computeDiffuseLighting(preInfo, diffuse.rgb); + #endif + + // Specular contribution + #ifdef SPECULARTERM + #if CONDUCTOR_SPECULAR_MODEL == CONDUCTOR_SPECULAR_MODEL_OPENPBR + vec3 metalFresnel = reflectivityOut.specularWeight * getF82Specular(preInfo.VdotH, specularEnvironmentR0, reflectivityOut.colorReflectanceF90, reflectivityOut.roughness); + vec3 dielectricFresnel = fresnelSchlickGGX(preInfo.VdotH, reflectivityOut.dielectricColorF0, reflectivityOut.colorReflectanceF90); + vec3 coloredFresnel = mix(dielectricFresnel, metalFresnel, reflectivityOut.metallic); + #else + vec3 coloredFresnel = fresnelSchlickGGX(preInfo.VdotH, specularEnvironmentR0, reflectivityOut.colorReflectanceF90); + #endif + #ifndef LEGACY_SPECULAR_ENERGY_CONSERVATION + float NdotH = dot(N, preInfo.H); + vec3 fresnel = fresnelSchlickGGX(NdotH, vec3(reflectanceF0), specularEnvironmentR90); + info.diffuse *= (vec3(1.0) - fresnel); + #endif + #ifdef ANISOTROPIC + info.specular = computeAnisotropicSpecularLighting(preInfo, V, N, anisotropicOut.anisotropicTangent, anisotropicOut.anisotropicBitangent, anisotropicOut.anisotropy, clearcoatOut.specularEnvironmentR0, specularEnvironmentR90, AARoughnessFactor, diffuse.rgb); + #else + info.specular = computeSpecularLighting(preInfo, N, specularEnvironmentR0, coloredFresnel, AARoughnessFactor, diffuse.rgb); + #endif + #endif + + // Sheen contribution + #ifdef SHEEN + #ifdef SHEEN_LINKWITHALBEDO + preInfo.roughness = sheenOut.sheenIntensity; + #else + preInfo.roughness = adjustRoughnessFromLightProperties(sheenOut.sheenRoughness, specular.a, preInfo.lightDistance); + #endif + info.sheen = computeSheenLighting(preInfo, normalW, sheenOut.sheenColor, specularEnvironmentR90, AARoughnessFactor, diffuse.rgb); + #endif + + // Clear Coat contribution + #ifdef CLEARCOAT + preInfo.roughness = adjustRoughnessFromLightProperties(clearcoatOut.clearCoatRoughness, specular.a, preInfo.lightDistance); + info.clearCoat = computeClearCoatLighting(preInfo, clearcoatOut.clearCoatNormalW, clearcoatOut.clearCoatAARoughnessFactors.x, clearcoatOut.clearCoatIntensity, diffuse.rgb); + + #ifdef CLEARCOAT_TINT + // Absorption + float absorption = computeClearCoatLightingAbsorption(clearcoatOut.clearCoatNdotVRefract, preInfo.L, clearcoatOut.clearCoatNormalW, clearcoatOut.clearCoatColor, clearcoatOut.clearCoatThickness, clearcoatOut.clearCoatIntensity); + info.diffuse *= absorption; + #ifdef SS_TRANSLUCENCY + info.diffuseTransmission *= absorption; + #endif + #ifdef SPECULARTERM + info.specular *= absorption; + #endif + #endif + + info.diffuse *= info.clearCoat.w; + #ifdef SS_TRANSLUCENCY + info.diffuseTransmission *= info.clearCoat.w; + #endif + #ifdef SPECULARTERM + info.specular *= info.clearCoat.w; + #endif + #ifdef SHEEN + info.sheen *= info.clearCoat.w; + #endif + #endif + + // Apply contributions to result + result.diffuse += info.diffuse; + #ifdef SS_TRANSLUCENCY + result.diffuseTransmission += info.diffuseTransmission; + #endif + #ifdef SPECULARTERM + result.specular += info.specular; + #endif + #ifdef CLEARCOAT + result.clearCoat += info.clearCoat; + #endif + #ifdef SHEEN + result.sheen += info.sheen; + #endif + } + i = batchEnd; + } + return result; + } +#endif diff --git a/packages/dev/core/src/Shaders/pbr.fragment.fx b/packages/dev/core/src/Shaders/pbr.fragment.fx index 24e06cb52bd..e6f34be3b5b 100644 --- a/packages/dev/core/src/Shaders/pbr.fragment.fx +++ b/packages/dev/core/src/Shaders/pbr.fragment.fx @@ -30,7 +30,6 @@ precision highp float; #include<__decl__pbrFragment> #include -#include #include<__decl__lightFragment>[0..maxSimultaneousLights] #include #include @@ -50,7 +49,6 @@ precision highp float; #include #include #include -#include #include #include #include @@ -72,9 +70,7 @@ precision highp float; #include #include -// WebGL does not support passing uniform arrays by reference and the copy heavily impacts performance. -// Bit of a hacky solution but we can just create a function per clustered light that references the uniforms directly. -#include[0..maxSimultaneousLights] +#include // _____________________________ MAIN FUNCTION ____________________________ void main(void) { diff --git a/packages/dev/core/src/Shaders/pbr.vertex.fx b/packages/dev/core/src/Shaders/pbr.vertex.fx index 73b2254ad98..778fa33de08 100644 --- a/packages/dev/core/src/Shaders/pbr.vertex.fx +++ b/packages/dev/core/src/Shaders/pbr.vertex.fx @@ -98,7 +98,6 @@ varying vec4 vColor; #include #include #include -#include #include<__decl__lightVxFragment>[0..maxSimultaneousLights] #include diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightFragment.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightFragment.fx index 131a792e7b0..d3137faed44 100644 --- a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightFragment.fx +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightFragment.fx @@ -8,15 +8,15 @@ #if defined(PBR) && defined(CLUSTLIGHT{X}) info = computeClusteredLighting( + lightDataTexture{X}, &tileMaskBuffer{X}, light{X}.vLightData, - &light{X}.vLights, + i32(light{X}.vNumLights), viewDirectionW, normalW, fragmentInputs.vPositionW, surfaceAlbedo, reflectivityOut, - diffuse{X}.rgb, #ifdef SS_TRANSLUCENCY subSurfaceOut, #endif diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx index 3ca5ef86639..c14f48ed00d 100644 --- a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx @@ -203,7 +203,6 @@ fn computeClusteredLighting( var mask = tileMaskBuffer[tileIndex]; tileIndex += maskStride; let batchEnd = min(i + CLUSTLIGHT_BATCH, numLights); - for (; i < batchEnd && mask != 0; i += 1) { // Skip as much as we can let trailing = firstTrailingBit(mask); diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/pbrDirectLightingFunctions.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/pbrDirectLightingFunctions.fx index af7ab5975bb..b7774d34964 100644 --- a/packages/dev/core/src/ShadersWGSL/ShadersInclude/pbrDirectLightingFunctions.fx +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/pbrDirectLightingFunctions.fx @@ -207,17 +207,17 @@ fn computeProjectionTextureDiffuseLighting(projectionLightTexture: texture_2d>, - lightData: vec4f, - lights: ptr>, + lightDataTexture: texture_2d, + tileMaskBuffer: ptr>, + clusteredData: vec4f, + numLights: i32, V: vec3f, N: vec3f, posW: vec3f, surfaceAlbedo: vec3f, reflectivityOut: reflectivityOutParams, - diffuseScale: vec3f, #ifdef SS_TRANSLUCENCY subSurfaceOut: subSurfaceOutParams, #endif @@ -241,117 +241,129 @@ fn computeProjectionTextureDiffuseLighting(projectionLightTexture: texture_2d>= trailing + 1; + i += i32(trailing); + + let lightData = textureLoad(lightDataTexture, vec2i(0, i), 0); + let diffuse = textureLoad(lightDataTexture, vec2i(1, i), 0); + let specular = textureLoad(lightDataTexture, vec2i(2, i), 0); + let direction = textureLoad(lightDataTexture, vec2i(3, i), 0); + let falloff = textureLoad(lightDataTexture, vec2i(4, i), 0); + + var preInfo = computePointAndSpotPreLightingInfo(lightData, V, N, posW); + preInfo.NdotV = NdotV; + + // Compute Attenuation infos + preInfo.attenuation = computeDistanceLightFalloff(preInfo.lightOffset, preInfo.lightDistanceSquared, falloff.x, falloff.y); + preInfo.attenuation *= computeDirectionalLightFalloff(direction.xyz, preInfo.L, direction.w, lightData.w, falloff.z, falloff.w); + + preInfo.roughness = adjustRoughnessFromLightProperties(reflectivityOut.roughness, specular.a, preInfo.lightDistance); + preInfo.diffuseRoughness = reflectivityOut.diffuseRoughness; + preInfo.surfaceAlbedo = surfaceAlbedo; + var info: lightingInfo; + + // Diffuse contribution + #ifdef SS_TRANSLUCENCY + #ifdef SS_TRANSLUCENCY_LEGACY + info.diffuse = computeDiffuseTransmittedLighting(preInfo, diffuse.rgb, subSurfaceOut.transmittance); + info.diffuseTransmission = vec3(0); + #else + info.diffuse = computeDiffuseLighting(preInfo, diffuse.rgb) * (1.0 - subSurfaceOut.translucencyIntensity); + info.diffuseTransmission = computeDiffuseTransmittedLighting(preInfo, diffuse.rgb, subSurfaceOut.transmittance); + #endif #else - let coloredFresnel = fresnelSchlickGGXVec3(preInfo.VdotH, specularEnvironmentR0, reflectivityOut.colorReflectanceF90); + info.diffuse = computeDiffuseLighting(preInfo, diffuse.rgb); #endif - #ifndef LEGACY_SPECULAR_ENERGY_CONSERVATION - let NdotH = dot(N, preInfo.H); - let fresnel = fresnelSchlickGGXVec3(NdotH, vec3(reflectanceF0), specularEnvironmentR90); - info.diffuse *= (vec3(1.0) - fresnel); - #endif - #ifdef ANISOTROPIC - info.specular = computeAnisotropicSpecularLighting(preInfo, V, N, anisotropicOut.anisotropicTangent, anisotropicOut.anisotropicBitangent, anisotropicOut.anisotropy, clearcoatOut.specularEnvironmentR0, specularEnvironmentR90, AARoughnessFactor, diffuse); - #else - info.specular = computeSpecularLighting(preInfo, N, specularEnvironmentR0, coloredFresnel, AARoughnessFactor, diffuse); + + // Specular contribution + #ifdef SPECULARTERM + #if CONDUCTOR_SPECULAR_MODEL == CONDUCTOR_SPECULAR_MODEL_OPENPBR + let metalFresnel = reflectivityOut.specularWeight * getF82Specular(preInfo.VdotH, specularEnvironmentR0, reflectivityOut.colorReflectanceF90, reflectivityOut.roughness); + let dielectricFresnel = fresnelSchlickGGXVec3(preInfo.VdotH, reflectivityOut.dielectricColorF0, reflectivityOut.colorReflectanceF90); + let coloredFresnel = mix(dielectricFresnel, metalFresnel, reflectivityOut.metallic); + #else + let coloredFresnel = fresnelSchlickGGXVec3(preInfo.VdotH, specularEnvironmentR0, reflectivityOut.colorReflectanceF90); + #endif + #ifndef LEGACY_SPECULAR_ENERGY_CONSERVATION + let NdotH = dot(N, preInfo.H); + let fresnel = fresnelSchlickGGXVec3(NdotH, vec3(reflectanceF0), specularEnvironmentR90); + info.diffuse *= (vec3(1.0) - fresnel); + #endif + #ifdef ANISOTROPIC + info.specular = computeAnisotropicSpecularLighting(preInfo, V, N, anisotropicOut.anisotropicTangent, anisotropicOut.anisotropicBitangent, anisotropicOut.anisotropy, clearcoatOut.specularEnvironmentR0, specularEnvironmentR90, AARoughnessFactor, diffuse.rgb); + #else + info.specular = computeSpecularLighting(preInfo, N, specularEnvironmentR0, coloredFresnel, AARoughnessFactor, diffuse.rgb); + #endif #endif - #endif - // Sheen contribution - #ifdef SHEEN - #ifdef SHEEN_LINKWITHALBEDO - preInfo.roughness = sheenOut.sheenIntensity; - #else - preInfo.roughness = adjustRoughnessFromLightProperties(sheenOut.sheenRoughness, radius, preInfo.lightDistance); + // Sheen contribution + #ifdef SHEEN + #ifdef SHEEN_LINKWITHALBEDO + preInfo.roughness = sheenOut.sheenIntensity; + #else + preInfo.roughness = adjustRoughnessFromLightProperties(sheenOut.sheenRoughness, specular.a, preInfo.lightDistance); + #endif + preInfo.roughness = adjustRoughnessFromLightProperties(sheenOut.sheenRoughness, specular.a, preInfo.lightDistance); + info.sheen = computeSheenLighting(preInfo, normalW, sheenOut.sheenColor, specularEnvironmentR90, AARoughnessFactor, diffuse.rgb); #endif - preInfo.roughness = adjustRoughnessFromLightProperties(sheenOut.sheenRoughness, radius, preInfo.lightDistance); - info.sheen = computeSheenLighting(preInfo, normalW, sheenOut.sheenColor, specularEnvironmentR90, AARoughnessFactor, diffuse); - #endif - - // Clear Coat contribution - #ifdef CLEARCOAT - preInfo.roughness = adjustRoughnessFromLightProperties(clearcoatOut.clearCoatRoughness, radius, preInfo.lightDistance); - info.clearCoat = computeClearCoatLighting(preInfo, clearcoatOut.clearCoatNormalW, clearcoatOut.clearCoatAARoughnessFactors.x, clearcoatOut.clearCoatIntensity, diffuse); - - #ifdef CLEARCOAT_TINT - // Absorption - let absorption = computeClearCoatLightingAbsorption(clearcoatOut.clearCoatNdotVRefract, preInfo.L, clearcoatOut.clearCoatNormalW, clearcoatOut.clearCoatColor, clearcoatOut.clearCoatThickness, clearcoatOut.clearCoatIntensity); - info.diffuse *= absorption; + + // Clear Coat contribution + #ifdef CLEARCOAT + preInfo.roughness = adjustRoughnessFromLightProperties(clearcoatOut.clearCoatRoughness, specular.a, preInfo.lightDistance); + info.clearCoat = computeClearCoatLighting(preInfo, clearcoatOut.clearCoatNormalW, clearcoatOut.clearCoatAARoughnessFactors.x, clearcoatOut.clearCoatIntensity, diffuse.rgb); + + #ifdef CLEARCOAT_TINT + // Absorption + let absorption = computeClearCoatLightingAbsorption(clearcoatOut.clearCoatNdotVRefract, preInfo.L, clearcoatOut.clearCoatNormalW, clearcoatOut.clearCoatColor, clearcoatOut.clearCoatThickness, clearcoatOut.clearCoatIntensity); + info.diffuse *= absorption; + #ifdef SS_TRANSLUCENCY + info.diffuseTransmission *= absorption; + #endif + #ifdef SPECULARTERM + info.specular *= absorption; + #endif + #endif + + info.diffuse *= info.clearCoat.w; #ifdef SS_TRANSLUCENCY - info.diffuseTransmission *= absorption; + info.diffuseTransmission *= info.clearCoat.w; #endif #ifdef SPECULARTERM - info.specular *= absorption; + info.specular *= info.clearCoat.w; + #endif + #ifdef SHEEN + info.sheen *= info.clearCoat.w; #endif #endif - info.diffuse *= info.clearCoat.w; + // Apply contributions to result + result.diffuse += info.diffuse; #ifdef SS_TRANSLUCENCY - info.diffuseTransmission *= info.clearCoat.w; + result.diffuseTransmission += info.diffuseTransmission; #endif #ifdef SPECULARTERM - info.specular *= info.clearCoat.w; + result.specular += info.specular; + #endif + #ifdef CLEARCOAT + result.clearCoat += info.clearCoat; #endif #ifdef SHEEN - info.sheen *= info.clearCoat.w; + result.sheen += info.sheen; #endif - #endif - - // Apply contributions to result - result.diffuse += info.diffuse; - #ifdef SS_TRANSLUCENCY - result.diffuseTransmission += info.diffuseTransmission; - #endif - #ifdef SPECULARTERM - result.specular += info.specular; - #endif - #ifdef CLEARCOAT - result.clearCoat += info.clearCoat; - #endif - #ifdef SHEEN - result.sheen += info.sheen; - #endif + } + i = batchEnd; } return result; } diff --git a/packages/dev/core/src/ShadersWGSL/pbr.fragment.fx b/packages/dev/core/src/ShadersWGSL/pbr.fragment.fx index 6333eeed574..91bae999b44 100644 --- a/packages/dev/core/src/ShadersWGSL/pbr.fragment.fx +++ b/packages/dev/core/src/ShadersWGSL/pbr.fragment.fx @@ -14,7 +14,6 @@ #include #include -#include #include[0..maxSimultaneousLights] #include #include From aa0c2c49fb194d97e132f73b7b029e09695dbe1f Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Fri, 25 Jul 2025 12:02:31 +1000 Subject: [PATCH 25/30] Fix tile size issue --- .../core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx | 2 +- .../src/Shaders/ShadersInclude/pbrDirectLightingFunctions.fx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx index 1151066ecb7..844a1c85239 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx @@ -197,7 +197,7 @@ lightingInfo computeClusteredLighting( lightingInfo result; int maskHeight = int(clusteredData.y); ivec2 tilePosition = ivec2(gl_FragCoord.xy * clusteredData.zw); - tilePosition.x = min(tilePosition.x, maskHeight - 1); + tilePosition.y = min(tilePosition.y, maskHeight - 1); for (int i = 0; i < numLights;) { uint mask = uint(texelFetch(tileMaskTexture, tilePosition, 0).r); diff --git a/packages/dev/core/src/Shaders/ShadersInclude/pbrDirectLightingFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/pbrDirectLightingFunctions.fx index 7469ce87b4a..0ac892cefa1 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/pbrDirectLightingFunctions.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/pbrDirectLightingFunctions.fx @@ -241,7 +241,7 @@ vec3 computeProjectionTextureDiffuseLighting(sampler2D projectionLightSampler, m lightingInfo result; int maskHeight = int(clusteredData.y); ivec2 tilePosition = ivec2(gl_FragCoord.xy * clusteredData.zw); - tilePosition.x = min(tilePosition.x, maskHeight - 1); + tilePosition.y = min(tilePosition.y, maskHeight - 1); for (int i = 0; i < numLights;) { uint mask = uint(texelFetch(tileMaskTexture, tilePosition, 0).r); From b4b8b8a730fa5bcad833afe22be77bcc5cda3841 Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Tue, 29 Jul 2025 15:26:04 +1000 Subject: [PATCH 26/30] Bring over improvements from depth clustering PR --- .../ShadersInclude/clusteredLightFunctions.fx | 17 +++++ .../Shaders/ShadersInclude/helperFunctions.fx | 10 ++- .../ShadersInclude/lightsFragmentFunctions.fx | 37 +++++------ .../pbrDirectLightingFunctions.fx | 63 ++++++++++--------- .../dev/core/src/Shaders/lightProxy.vertex.fx | 7 ++- .../ShadersInclude/clusteredLightFunctions.fx | 17 +++++ .../ShadersInclude/lightsFragmentFunctions.fx | 28 ++++----- .../core/src/ShadersWGSL/default.fragment.fx | 1 - .../core/src/ShadersWGSL/lightProxy.vertex.fx | 7 ++- 9 files changed, 116 insertions(+), 71 deletions(-) create mode 100644 packages/dev/core/src/Shaders/ShadersInclude/clusteredLightFunctions.fx create mode 100644 packages/dev/core/src/ShadersWGSL/ShadersInclude/clusteredLightFunctions.fx diff --git a/packages/dev/core/src/Shaders/ShadersInclude/clusteredLightFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/clusteredLightFunctions.fx new file mode 100644 index 00000000000..cca83311d87 --- /dev/null +++ b/packages/dev/core/src/Shaders/ShadersInclude/clusteredLightFunctions.fx @@ -0,0 +1,17 @@ +struct SpotLight { + vec4 vLightData; + vec4 vLightDiffuse; + vec4 vLightSpecular; + vec4 vLightDirection; + vec4 vLightFalloff; +}; + +SpotLight getClusteredSpotLight(sampler2D lightDataTexture, int index) { + return SpotLight( + texelFetch(lightDataTexture, ivec2(0, index), 0), + texelFetch(lightDataTexture, ivec2(1, index), 0), + texelFetch(lightDataTexture, ivec2(2, index), 0), + texelFetch(lightDataTexture, ivec2(3, index), 0), + texelFetch(lightDataTexture, ivec2(4, index), 0) + ); +} diff --git a/packages/dev/core/src/Shaders/ShadersInclude/helperFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/helperFunctions.fx index 2dce3059c3f..f6bf13fde37 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/helperFunctions.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/helperFunctions.fx @@ -234,4 +234,12 @@ float sqrtClamped(float value) { float avg(vec3 value) { return dot(value, vec3(0.333333333)); -} \ No newline at end of file +} + +#ifdef WEBGL2 +// Returns the position of the only set bit in the value, only works if theres exactly 1 bit set +int onlyBitPosition(uint value) { + // https://graphics.stanford.edu/~seander/bithacks.html#ZerosOnRightFloatCast + return (floatBitsToInt(float(value)) >> 23) - 0x7f; +} +#endif diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx index 844a1c85239..d9688a0979c 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx @@ -185,42 +185,43 @@ lightingInfo computeAreaLighting(sampler2D ltc1, sampler2D ltc2, vec3 viewDirect #endif #if defined(CLUSTLIGHT_BATCH) && CLUSTLIGHT_BATCH > 0 +#include + lightingInfo computeClusteredLighting( sampler2D lightDataTexture, sampler2D tileMaskTexture, vec3 viewDirectionW, vec3 vNormal, - vec4 clusteredData, + vec4 lightData, int numLights, float glossiness ) { lightingInfo result; - int maskHeight = int(clusteredData.y); - ivec2 tilePosition = ivec2(gl_FragCoord.xy * clusteredData.zw); + int maskHeight = int(lightData.y); + ivec2 tilePosition = ivec2(gl_FragCoord.xy * lightData.zw); tilePosition.y = min(tilePosition.y, maskHeight - 1); - for (int i = 0; i < numLights;) { + int numBatches = (numLights + CLUSTLIGHT_BATCH - 1) / CLUSTLIGHT_BATCH; + int batchOffset = 0; + + for (int i = 0; i < numBatches; i += 1) { uint mask = uint(texelFetch(tileMaskTexture, tilePosition, 0).r); tilePosition.y += maskHeight; - int batchEnd = min(i + CLUSTLIGHT_BATCH, numLights); - for(; i < batchEnd && mask != 0u; i += 1, mask >>= 1) { - if ((mask & 1u) == 0u) { - continue; - } - - vec4 lightData = texelFetch(lightDataTexture, ivec2(0, i), 0); - vec4 diffuse = texelFetch(lightDataTexture, ivec2(1, i), 0); - vec4 specular = texelFetch(lightDataTexture, ivec2(2, i), 0); - vec4 direction = texelFetch(lightDataTexture, ivec2(3, i), 0); - vec4 falloff = texelFetch(lightDataTexture, ivec2(4, i), 0); - - lightingInfo info = computeSpotLighting(viewDirectionW, vNormal, lightData, direction, diffuse.rgb, specular.rgb, diffuse.a, glossiness); + + while (mask != 0u) { + // This gets the lowest set bit + uint bit = mask & -mask; + mask ^= bit; + int position = onlyBitPosition(bit); + SpotLight light = getClusteredSpotLight(lightDataTexture, batchOffset + position); + + lightingInfo info = computeSpotLighting(viewDirectionW, vNormal, light.vLightData, light.vLightDirection, light.vLightDiffuse.rgb, light.vLightSpecular.rgb, light.vLightDiffuse.a, glossiness); result.diffuse += info.diffuse; #ifdef SPECULARTERM result.specular += info.specular; #endif } - i = batchEnd; + batchOffset += CLUSTLIGHT_BATCH; } return result; } diff --git a/packages/dev/core/src/Shaders/ShadersInclude/pbrDirectLightingFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/pbrDirectLightingFunctions.fx index 0ac892cefa1..967f2b02f92 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/pbrDirectLightingFunctions.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/pbrDirectLightingFunctions.fx @@ -206,10 +206,12 @@ vec3 computeProjectionTextureDiffuseLighting(sampler2D projectionLightSampler, m #endif #if defined(CLUSTLIGHT_BATCH) && CLUSTLIGHT_BATCH > 0 +#include + lightingInfo computeClusteredLighting( sampler2D lightDataTexture, sampler2D tileMaskTexture, - vec4 clusteredData, + vec4 lightData, int numLights, vec3 V, vec3 N, @@ -239,33 +241,32 @@ vec3 computeProjectionTextureDiffuseLighting(sampler2D projectionLightSampler, m #endif lightingInfo result; - int maskHeight = int(clusteredData.y); - ivec2 tilePosition = ivec2(gl_FragCoord.xy * clusteredData.zw); + int maskHeight = int(lightData.y); + ivec2 tilePosition = ivec2(gl_FragCoord.xy * lightData.zw); tilePosition.y = min(tilePosition.y, maskHeight - 1); - for (int i = 0; i < numLights;) { + int numBatches = (numLights + CLUSTLIGHT_BATCH - 1) / CLUSTLIGHT_BATCH; + int batchOffset = 0; + + for (int i = 0; i < numBatches; i += 1) { uint mask = uint(texelFetch(tileMaskTexture, tilePosition, 0).r); tilePosition.y += maskHeight; - int batchEnd = min(i + CLUSTLIGHT_BATCH, numLights); - for (; i < batchEnd && mask != 0u; i += 1, mask >>= 1) { - if ((mask & 1u) == 0u) { - continue; - } - - vec4 lightData = texelFetch(lightDataTexture, ivec2(0, i), 0); - vec4 diffuse = texelFetch(lightDataTexture, ivec2(1, i), 0); - vec4 specular = texelFetch(lightDataTexture, ivec2(2, i), 0); - vec4 direction = texelFetch(lightDataTexture, ivec2(3, i), 0); - vec4 falloff = texelFetch(lightDataTexture, ivec2(4, i), 0); - - preLightingInfo preInfo = computePointAndSpotPreLightingInfo(lightData, V, N, posW); + + while (mask != 0u) { + // This gets the lowest set bit + uint bit = mask & -mask; + mask ^= bit; + int position = onlyBitPosition(bit); + SpotLight light = getClusteredSpotLight(lightDataTexture, batchOffset + position); + + preLightingInfo preInfo = computePointAndSpotPreLightingInfo(light.vLightData, V, N, posW); preInfo.NdotV = NdotV; // Compute Attenuation infos - preInfo.attenuation = computeDistanceLightFalloff(preInfo.lightOffset, preInfo.lightDistanceSquared, falloff.x, falloff.y); - preInfo.attenuation *= computeDirectionalLightFalloff(direction.xyz, preInfo.L, direction.w, lightData.w, falloff.z, falloff.w); + preInfo.attenuation = computeDistanceLightFalloff(preInfo.lightOffset, preInfo.lightDistanceSquared, light.vLightFalloff.x, light.vLightFalloff.y); + preInfo.attenuation *= computeDirectionalLightFalloff(light.vLightDirection.xyz, preInfo.L, light.vLightDirection.w, light.vLightData.w, light.vLightFalloff.z, light.vLightFalloff.w); - preInfo.roughness = adjustRoughnessFromLightProperties(reflectivityOut.roughness, specular.a, preInfo.lightDistance); + preInfo.roughness = adjustRoughnessFromLightProperties(reflectivityOut.roughness, light.vLightSpecular.a, preInfo.lightDistance); preInfo.diffuseRoughness = reflectivityOut.diffuseRoughness; preInfo.surfaceAlbedo = surfaceAlbedo; lightingInfo info; @@ -273,14 +274,14 @@ vec3 computeProjectionTextureDiffuseLighting(sampler2D projectionLightSampler, m // Diffuse contribution #ifdef SS_TRANSLUCENCY #ifdef SS_TRANSLUCENCY_LEGACY - info.diffuse = computeDiffuseTransmittedLighting(preInfo, diffuse.rgb, subSurfaceOut.transmittance); + info.diffuse = computeDiffuseTransmittedLighting(preInfo, light.vLightDiffuse.rgb, subSurfaceOut.transmittance); info.diffuseTransmission = vec3(0); #else - info.diffuse = computeDiffuseLighting(preInfo, diffuse.rgb) * (1.0 - subSurfaceOut.translucencyIntensity); - info.diffuseTransmission = computeDiffuseTransmittedLighting(preInfo, diffuse.rgb, subSurfaceOut.transmittance); + info.diffuse = computeDiffuseLighting(preInfo, light.vLightDiffuse.rgb) * (1.0 - subSurfaceOut.translucencyIntensity); + info.diffuseTransmission = computeDiffuseTransmittedLighting(preInfo, light.vLightDiffuse.rgb, subSurfaceOut.transmittance); #endif #else - info.diffuse = computeDiffuseLighting(preInfo, diffuse.rgb); + info.diffuse = computeDiffuseLighting(preInfo, light.vLightDiffuse.rgb); #endif // Specular contribution @@ -298,9 +299,9 @@ vec3 computeProjectionTextureDiffuseLighting(sampler2D projectionLightSampler, m info.diffuse *= (vec3(1.0) - fresnel); #endif #ifdef ANISOTROPIC - info.specular = computeAnisotropicSpecularLighting(preInfo, V, N, anisotropicOut.anisotropicTangent, anisotropicOut.anisotropicBitangent, anisotropicOut.anisotropy, clearcoatOut.specularEnvironmentR0, specularEnvironmentR90, AARoughnessFactor, diffuse.rgb); + info.specular = computeAnisotropicSpecularLighting(preInfo, V, N, anisotropicOut.anisotropicTangent, anisotropicOut.anisotropicBitangent, anisotropicOut.anisotropy, clearcoatOut.specularEnvironmentR0, specularEnvironmentR90, AARoughnessFactor, light.vLightDiffuse.rgb); #else - info.specular = computeSpecularLighting(preInfo, N, specularEnvironmentR0, coloredFresnel, AARoughnessFactor, diffuse.rgb); + info.specular = computeSpecularLighting(preInfo, N, specularEnvironmentR0, coloredFresnel, AARoughnessFactor, light.vLightDiffuse.rgb); #endif #endif @@ -309,15 +310,15 @@ vec3 computeProjectionTextureDiffuseLighting(sampler2D projectionLightSampler, m #ifdef SHEEN_LINKWITHALBEDO preInfo.roughness = sheenOut.sheenIntensity; #else - preInfo.roughness = adjustRoughnessFromLightProperties(sheenOut.sheenRoughness, specular.a, preInfo.lightDistance); + preInfo.roughness = adjustRoughnessFromLightProperties(sheenOut.sheenRoughness, light.vLightSpecular.a, preInfo.lightDistance); #endif - info.sheen = computeSheenLighting(preInfo, normalW, sheenOut.sheenColor, specularEnvironmentR90, AARoughnessFactor, diffuse.rgb); + info.sheen = computeSheenLighting(preInfo, normalW, sheenOut.sheenColor, specularEnvironmentR90, AARoughnessFactor, light.vLightDiffuse.rgb); #endif // Clear Coat contribution #ifdef CLEARCOAT - preInfo.roughness = adjustRoughnessFromLightProperties(clearcoatOut.clearCoatRoughness, specular.a, preInfo.lightDistance); - info.clearCoat = computeClearCoatLighting(preInfo, clearcoatOut.clearCoatNormalW, clearcoatOut.clearCoatAARoughnessFactors.x, clearcoatOut.clearCoatIntensity, diffuse.rgb); + preInfo.roughness = adjustRoughnessFromLightProperties(clearcoatOut.clearCoatRoughness, light.vLightSpecular.a, preInfo.lightDistance); + info.clearCoat = computeClearCoatLighting(preInfo, clearcoatOut.clearCoatNormalW, clearcoatOut.clearCoatAARoughnessFactors.x, clearcoatOut.clearCoatIntensity, light.vLightDiffuse.rgb); #ifdef CLEARCOAT_TINT // Absorption @@ -358,7 +359,7 @@ vec3 computeProjectionTextureDiffuseLighting(sampler2D projectionLightSampler, m result.sheen += info.sheen; #endif } - i = batchEnd; + batchOffset += CLUSTLIGHT_BATCH; } return result; } diff --git a/packages/dev/core/src/Shaders/lightProxy.vertex.fx b/packages/dev/core/src/Shaders/lightProxy.vertex.fx index 00e19541cb7..4ba6cfb2f0f 100644 --- a/packages/dev/core/src/Shaders/lightProxy.vertex.fx +++ b/packages/dev/core/src/Shaders/lightProxy.vertex.fx @@ -8,12 +8,13 @@ flat varying highp uint vMask; uniform sampler2D lightDataTexture; uniform vec3 tileMaskResolution; +#include + void main(void) { - vec4 lightData = texelFetch(lightDataTexture, ivec2(0, gl_InstanceID), 0); - vec4 falloff = texelFetch(lightDataTexture, ivec2(4, gl_InstanceID), 0); + SpotLight light = getClusteredSpotLight(lightDataTexture, gl_InstanceID); // We don't apply the view matrix to the disc since we want it always facing the camera - vec4 viewPosition = view * vec4(lightData.xyz, 1) + vec4(position * falloff.x, 0); + vec4 viewPosition = view * vec4(light.vLightData.xyz, 1) + vec4(position * light.vLightFalloff.x, 0); vec4 projPosition = projection * viewPosition; // Convert to NDC 0->1 space and scale to the tile resolution diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/clusteredLightFunctions.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/clusteredLightFunctions.fx new file mode 100644 index 00000000000..4cc55a0bf9b --- /dev/null +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/clusteredLightFunctions.fx @@ -0,0 +1,17 @@ +struct SpotLight { + vLightData: vec4f, + vLightDiffuse: vec4f, + vLightSpecular: vec4f, + vLightDirection: vec4f, + vLightFalloff: vec4f, +} + +fn getClusteredSpotLight(lightDataTexture: texture_2d, index: u32) -> SpotLight { + return SpotLight( + textureLoad(lightDataTexture, vec2u(0, index), 0), + textureLoad(lightDataTexture, vec2u(1, index), 0), + textureLoad(lightDataTexture, vec2u(2, index), 0), + textureLoad(lightDataTexture, vec2u(3, index), 0), + textureLoad(lightDataTexture, vec2u(4, index), 0) + ); +} diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx index c14f48ed00d..9c356550ae8 100644 --- a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx @@ -184,6 +184,9 @@ fn computeAreaLighting(ltc1: texture_2d, ltc1Sampler:sampler, ltc2:texture_ // End Area Light #endif +#ifdef CLUSTLIGHT_BATCH +#include + fn computeClusteredLighting( lightDataTexture: texture_2d, tileMaskBuffer: ptr>, @@ -199,29 +202,26 @@ fn computeClusteredLighting( let tilePosition = vec2i(fragmentInputs.position.xy * clusteredData.zw); var tileIndex = min(tilePosition.y * maskResolution.x + tilePosition.x, maskStride - 1); - for (var i = 0; i < numLights;) { + let numBatches = (numLights + CLUSTLIGHT_BATCH - 1) / CLUSTLIGHT_BATCH; + var batchOffset = 0u; + + for (var i = 0; i < numBatches; i += 1) { var mask = tileMaskBuffer[tileIndex]; tileIndex += maskStride; - let batchEnd = min(i + CLUSTLIGHT_BATCH, numLights); - for (; i < batchEnd && mask != 0; i += 1) { - // Skip as much as we can - let trailing = firstTrailingBit(mask); - mask >>= trailing + 1; - i += i32(trailing); - let lightData = textureLoad(lightDataTexture, vec2i(0, i), 0); - let diffuse = textureLoad(lightDataTexture, vec2i(1, i), 0); - let specular = textureLoad(lightDataTexture, vec2i(2, i), 0); - let direction = textureLoad(lightDataTexture, vec2i(3, i), 0); - let falloff = textureLoad(lightDataTexture, vec2i(4, i), 0); + while mask != 0 { + let trailing = firstTrailingBit(mask); + mask ^= 1u << trailing; + let light = getClusteredSpotLight(lightDataTexture, batchOffset + trailing); - let info = computeSpotLighting(viewDirectionW, vNormal, lightData, direction, diffuse.rgb, specular.rgb, diffuse.a, glossiness); + let info = computeSpotLighting(viewDirectionW, vNormal, light.vLightData, light.vLightDirection, light.vLightDiffuse.rgb, light.vLightSpecular.rgb, light.vLightDiffuse.a, glossiness); result.diffuse += info.diffuse; #ifdef SPECULARTERM result.specular += info.specular; #endif } - i = batchEnd; + batchOffset += CLUSTLIGHT_BATCH; } return result; } +#endif diff --git a/packages/dev/core/src/ShadersWGSL/default.fragment.fx b/packages/dev/core/src/ShadersWGSL/default.fragment.fx index 77729743560..dfe591e65f9 100644 --- a/packages/dev/core/src/ShadersWGSL/default.fragment.fx +++ b/packages/dev/core/src/ShadersWGSL/default.fragment.fx @@ -22,7 +22,6 @@ varying vColor: vec4f; #include // Lights -#include #include[0..maxSimultaneousLights] #include diff --git a/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx b/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx index 6d1d6217490..70428faf2ff 100644 --- a/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx +++ b/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx @@ -9,13 +9,14 @@ var lightDataTexture: texture_2d; uniform tileMaskResolution: vec3f; uniform halfTileRes: vec2f; +#include + @vertex fn main(input: VertexInputs) -> FragmentInputs { - let lightData = textureLoad(lightDataTexture, vec2u(0, vertexInputs.instanceIndex), 0); - let falloff = textureLoad(lightDataTexture, vec2u(4, vertexInputs.instanceIndex), 0); + let light = getClusteredSpotLight(lightDataTexture, vertexInputs.instanceIndex); // We don't apply the view matrix to the disc since we want it always facing the camera - let viewPosition = scene.view * vec4f(lightData.xyz, 1) + vec4f(vertexInputs.position * falloff.x, 0); + let viewPosition = scene.view * vec4f(light.vLightData.xyz, 1) + vec4f(vertexInputs.position * light.vLightFalloff.x, 0); let projPosition = scene.projection * viewPosition; // Convert to NDC 0->1 space and scale to the tile resolution From 5ac5e8a9f6d1c95432f24fddd8d783bb8eed0935 Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Wed, 30 Jul 2025 08:45:12 +1000 Subject: [PATCH 27/30] Cleanup and fixes --- .../WebGPU/webgpuShaderProcessorsWGSL.ts | 3 +- .../src/Lights/Clustered/clusteredLight.ts | 6 +- .../core/src/Materials/PBR/pbrBaseMaterial.ts | 2 +- .../ShadersInclude/lightsFragmentFunctions.fx | 4 +- .../pbrDirectLightingFunctions.fx | 4 +- packages/dev/core/src/Shaders/pbr.fragment.fx | 1 - .../ShadersInclude/lightsFragmentFunctions.fx | 11 ++-- .../pbrDirectLightingFalloffFunctions.fx | 7 +- .../pbrDirectLightingFunctions.fx | 64 +++++++++---------- .../core/src/ShadersWGSL/default.vertex.fx | 1 - .../src/ShadersWGSL/lightProxy.fragment.fx | 7 +- .../core/src/ShadersWGSL/lightProxy.vertex.fx | 5 +- .../dev/core/src/ShadersWGSL/pbr.vertex.fx | 1 - 13 files changed, 57 insertions(+), 59 deletions(-) diff --git a/packages/dev/core/src/Engines/WebGPU/webgpuShaderProcessorsWGSL.ts b/packages/dev/core/src/Engines/WebGPU/webgpuShaderProcessorsWGSL.ts index 7c62b93c1bb..ebe472beec1 100644 --- a/packages/dev/core/src/Engines/WebGPU/webgpuShaderProcessorsWGSL.ts +++ b/packages/dev/core/src/Engines/WebGPU/webgpuShaderProcessorsWGSL.ts @@ -335,8 +335,7 @@ export class WebGPUShaderProcessorWGSL extends WebGPUShaderProcessor { } public finalizeShaders(vertexCode: string, fragmentCode: string): { vertexCode: string; fragmentCode: string } { - // TODO: conditionally enable if needed AND supported - const enabledExtensions: string[] = ["subgroups"]; + const enabledExtensions: string[] = []; const fragCoordCode = fragmentCode.indexOf("fragmentInputs.position") >= 0 && !this.pureMode diff --git a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts index bbb915a1982..2cfc144128d 100644 --- a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts +++ b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts @@ -324,8 +324,8 @@ export class ClusteredLight extends Light { // vLightFalloff range, inverseSquaredRange, - 0.5, - 0.5, + 1_000_000, + 1_000_000, ], offset ); @@ -377,7 +377,7 @@ export class ClusteredLight extends Light { const engine = this.getEngine(); const hscale = this._horizontalTiles / engine.getRenderWidth(); const vscale = this._verticalTiles / engine.getRenderHeight(); - this._uniformBuffer.updateFloat4("vLightData", this._horizontalTiles, this._verticalTiles, hscale, vscale, lightIndex); + this._uniformBuffer.updateFloat4("vLightData", hscale, vscale, this._verticalTiles, this._tileMaskBatches, lightIndex); this._uniformBuffer.updateFloat("vNumLights", this._lights.length, lightIndex); return this; } diff --git a/packages/dev/core/src/Materials/PBR/pbrBaseMaterial.ts b/packages/dev/core/src/Materials/PBR/pbrBaseMaterial.ts index 841eab9fb71..3234b8ddd0e 100644 --- a/packages/dev/core/src/Materials/PBR/pbrBaseMaterial.ts +++ b/packages/dev/core/src/Materials/PBR/pbrBaseMaterial.ts @@ -1260,7 +1260,7 @@ export abstract class PBRBaseMaterial extends PushMaterial { } // Check if Area Lights have LTC texture. - if (defines["AREALIGHTUSED"]) { + if (defines["AREALIGHTUSED"] || defines["CLUSTLIGHT_BATCH"]) { for (let index = 0; index < mesh.lightSources.length; index++) { if (!mesh.lightSources[index]._isReady()) { return false; diff --git a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx index d9688a0979c..890463aa051 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx @@ -197,8 +197,8 @@ lightingInfo computeClusteredLighting( float glossiness ) { lightingInfo result; - int maskHeight = int(lightData.y); - ivec2 tilePosition = ivec2(gl_FragCoord.xy * lightData.zw); + ivec2 tilePosition = ivec2(gl_FragCoord.xy * lightData.xy); + int maskHeight = int(lightData.z); tilePosition.y = min(tilePosition.y, maskHeight - 1); int numBatches = (numLights + CLUSTLIGHT_BATCH - 1) / CLUSTLIGHT_BATCH; diff --git a/packages/dev/core/src/Shaders/ShadersInclude/pbrDirectLightingFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/pbrDirectLightingFunctions.fx index 967f2b02f92..05a65b0e04b 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/pbrDirectLightingFunctions.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/pbrDirectLightingFunctions.fx @@ -241,8 +241,8 @@ vec3 computeProjectionTextureDiffuseLighting(sampler2D projectionLightSampler, m #endif lightingInfo result; - int maskHeight = int(lightData.y); - ivec2 tilePosition = ivec2(gl_FragCoord.xy * lightData.zw); + ivec2 tilePosition = ivec2(gl_FragCoord.xy * lightData.xy); + int maskHeight = int(lightData.z); tilePosition.y = min(tilePosition.y, maskHeight - 1); int numBatches = (numLights + CLUSTLIGHT_BATCH - 1) / CLUSTLIGHT_BATCH; diff --git a/packages/dev/core/src/Shaders/pbr.fragment.fx b/packages/dev/core/src/Shaders/pbr.fragment.fx index e6f34be3b5b..d953058489a 100644 --- a/packages/dev/core/src/Shaders/pbr.fragment.fx +++ b/packages/dev/core/src/Shaders/pbr.fragment.fx @@ -69,7 +69,6 @@ precision highp float; #include #include #include - #include // _____________________________ MAIN FUNCTION ____________________________ diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx index 9c356550ae8..b007e0f1973 100644 --- a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx @@ -192,22 +192,21 @@ fn computeClusteredLighting( tileMaskBuffer: ptr>, viewDirectionW: vec3f, vNormal: vec3f, - clusteredData: vec4f, + lightData: vec4f, numLights: i32, glossiness: f32 ) -> lightingInfo { var result: lightingInfo; - let maskResolution = vec2i(clusteredData.xy); - let maskStride = maskResolution.x * maskResolution.y; - let tilePosition = vec2i(fragmentInputs.position.xy * clusteredData.zw); - var tileIndex = min(tilePosition.y * maskResolution.x + tilePosition.x, maskStride - 1); + let tilePosition = vec2i(fragmentInputs.position.xy * lightData.xy); + let maskResolution = vec2i(lightData.zw); + var tileIndex = (tilePosition.x * maskResolution.x + tilePosition.y) * maskResolution.y; let numBatches = (numLights + CLUSTLIGHT_BATCH - 1) / CLUSTLIGHT_BATCH; var batchOffset = 0u; for (var i = 0; i < numBatches; i += 1) { var mask = tileMaskBuffer[tileIndex]; - tileIndex += maskStride; + tileIndex += 1; while mask != 0 { let trailing = firstTrailingBit(mask); diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/pbrDirectLightingFalloffFunctions.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/pbrDirectLightingFalloffFunctions.fx index d15383cd1d3..e658fc24b07 100644 --- a/packages/dev/core/src/ShadersWGSL/ShadersInclude/pbrDirectLightingFalloffFunctions.fx +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/pbrDirectLightingFalloffFunctions.fx @@ -47,7 +47,12 @@ fn computeDirectionalLightFalloff_Standard(lightDirection: vec3f, directionToLig var cosAngle: f32 = maxEps(dot(-lightDirection, directionToLightCenterW)); if (cosAngle >= cosHalfAngle) { - falloff = max(0., pow(cosAngle, exponent)); + if exponent == 0.0 { + // Undefined behaviour can occur if exponent == 0, the result in reality should always be 1 + falloff = 1.0; + } else { + falloff = max(0., pow(cosAngle, exponent)); + } } return falloff; diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/pbrDirectLightingFunctions.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/pbrDirectLightingFunctions.fx index b7774d34964..db34de9ee0b 100644 --- a/packages/dev/core/src/ShadersWGSL/ShadersInclude/pbrDirectLightingFunctions.fx +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/pbrDirectLightingFunctions.fx @@ -208,10 +208,12 @@ fn computeProjectionTextureDiffuseLighting(projectionLightTexture: texture_2d + fn computeClusteredLighting( lightDataTexture: texture_2d, tileMaskBuffer: ptr>, - clusteredData: vec4f, + lightData: vec4f, numLights: i32, V: vec3f, N: vec3f, @@ -241,35 +243,30 @@ fn computeProjectionTextureDiffuseLighting(projectionLightTexture: texture_2d>= trailing + 1; - i += i32(trailing); + tileIndex += 1; - let lightData = textureLoad(lightDataTexture, vec2i(0, i), 0); - let diffuse = textureLoad(lightDataTexture, vec2i(1, i), 0); - let specular = textureLoad(lightDataTexture, vec2i(2, i), 0); - let direction = textureLoad(lightDataTexture, vec2i(3, i), 0); - let falloff = textureLoad(lightDataTexture, vec2i(4, i), 0); + while mask != 0 { + let trailing = firstTrailingBit(mask); + mask ^= 1u << trailing; + let light = getClusteredSpotLight(lightDataTexture, batchOffset + trailing); - var preInfo = computePointAndSpotPreLightingInfo(lightData, V, N, posW); + var preInfo = computePointAndSpotPreLightingInfo(light.vLightData, V, N, posW); preInfo.NdotV = NdotV; // Compute Attenuation infos - preInfo.attenuation = computeDistanceLightFalloff(preInfo.lightOffset, preInfo.lightDistanceSquared, falloff.x, falloff.y); - preInfo.attenuation *= computeDirectionalLightFalloff(direction.xyz, preInfo.L, direction.w, lightData.w, falloff.z, falloff.w); + preInfo.attenuation = computeDistanceLightFalloff(preInfo.lightOffset, preInfo.lightDistanceSquared, light.vLightFalloff.x, light.vLightFalloff.y); + preInfo.attenuation *= computeDirectionalLightFalloff(light.vLightDirection.xyz, preInfo.L, light.vLightDirection.w, light.vLightData.w, light.vLightFalloff.z, light.vLightFalloff.w); - preInfo.roughness = adjustRoughnessFromLightProperties(reflectivityOut.roughness, specular.a, preInfo.lightDistance); + preInfo.roughness = adjustRoughnessFromLightProperties(reflectivityOut.roughness, light.vLightSpecular.a, preInfo.lightDistance); preInfo.diffuseRoughness = reflectivityOut.diffuseRoughness; preInfo.surfaceAlbedo = surfaceAlbedo; var info: lightingInfo; @@ -277,14 +274,14 @@ fn computeProjectionTextureDiffuseLighting(projectionLightTexture: texture_2d #include -#include #include<__decl__lightVxFragment>[0..maxSimultaneousLights] #include diff --git a/packages/dev/core/src/ShadersWGSL/lightProxy.fragment.fx b/packages/dev/core/src/ShadersWGSL/lightProxy.fragment.fx index b92742a534f..2f04981f46f 100644 --- a/packages/dev/core/src/ShadersWGSL/lightProxy.fragment.fx +++ b/packages/dev/core/src/ShadersWGSL/lightProxy.fragment.fx @@ -7,7 +7,10 @@ var tileMaskBuffer: array>; @fragment fn main(input: FragmentInputs) -> FragmentOutputs { - let uPosition = vec2u(fragmentInputs.position.xy); - let tileIndex = fragmentInputs.vOffset + uPosition.y * u32(uniforms.tileMaskResolution.x) + uPosition.x; + let maskResolution = vec2u(uniforms.tileMaskResolution.yz); + let tilePosition = vec2u(fragmentInputs.position.xy); + // We store the tiles in column-major so we don't need to know the width of the tilemask, allowing for one less uniform needed for clustered lights. + // Height is already needed for the WebGL implementation since it stores clusters vertically to reduce texture size in an assumed horizontal desktop resolution. + let tileIndex = (tilePosition.x * maskResolution.x + tilePosition.y) * maskResolution.y + fragmentInputs.vOffset; atomicOr(&tileMaskBuffer[tileIndex], fragmentInputs.vMask); } diff --git a/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx b/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx index 70428faf2ff..3a20f6b6fd3 100644 --- a/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx +++ b/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx @@ -26,7 +26,6 @@ fn main(input: VertexInputs) -> FragmentInputs { // We don't care about depth and don't want it to be clipped so set Z to 0 vertexOutputs.position = vec4f(tilePosition / uniforms.tileMaskResolution.xy * 2.0 - 1.0, 0, 1); - let uResolution = vec2u(uniforms.tileMaskResolution.xy); - vertexOutputs.vOffset = vertexInputs.instanceIndex / CLUSTLIGHT_BATCH * uResolution.x * uResolution.y; - vertexOutputs.vMask = 1u << vertexInputs.instanceIndex; + vertexOutputs.vOffset = vertexInputs.instanceIndex / CLUSTLIGHT_BATCH; + vertexOutputs.vMask = 1u << (vertexInputs.instanceIndex % CLUSTLIGHT_BATCH); } diff --git a/packages/dev/core/src/ShadersWGSL/pbr.vertex.fx b/packages/dev/core/src/ShadersWGSL/pbr.vertex.fx index 2cffb06bf29..dd7d851f241 100644 --- a/packages/dev/core/src/ShadersWGSL/pbr.vertex.fx +++ b/packages/dev/core/src/ShadersWGSL/pbr.vertex.fx @@ -93,7 +93,6 @@ varying vColor: vec4f; #include #include #include -#include #include[0..maxSimultaneousLights] #include From a908bf0d0e85c480b4cf29a1590ad7fc491e2738 Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Wed, 30 Jul 2025 11:08:53 +1000 Subject: [PATCH 28/30] Add doc comments --- .../src/Lights/Clustered/clusteredLight.ts | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts index 2cfc144128d..e2f87684d56 100644 --- a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts +++ b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts @@ -23,6 +23,9 @@ import { SpotLight } from "../spotLight"; import "core/Meshes/thinInstanceMesh"; +/** + * A special light that renders all its associated spot or point lights using a clustered or forward+ system. + */ export class ClusteredLight extends Light { private static _GetEngineBatchSize(engine: AbstractEngine): number { const caps = engine._caps; @@ -44,6 +47,13 @@ export class ClusteredLight extends Light { } } + /** + * Checks if the clustered lighting system supports the given light with its current parameters. + * This will also check if the light's associated engine supports clustered lighting. + * + * @param light The light to test + * @returns true if the light and its engine is supported + */ public static IsLightSupported(light: Light): boolean { if (ClusteredLight._GetEngineBatchSize(light.getEngine()) === 0) { return false; @@ -71,11 +81,17 @@ export class ClusteredLight extends Light { private readonly _batchSize: number; + /** + * True if clustered lighting is supported. + */ public get isSupported(): boolean { return this._batchSize > 0; } private readonly _lights: (PointLight | SpotLight)[] = []; + /** + * Gets the current list of lights added to this clustering system. + */ public get lights(): readonly Light[] { return this._lights; } @@ -88,6 +104,10 @@ export class ClusteredLight extends Light { private _tileMaskBuffer: Nullable; private _horizontalTiles = 64; + /** + * The number of tiles in the horizontal direction to cluster lights into. + * A lower value will reduce memory and make the clustering step faster, while a higher value increases memory and makes the rendering step faster. + */ public get horizontalTiles(): number { return this._horizontalTiles; } @@ -102,6 +122,10 @@ export class ClusteredLight extends Light { } private _verticalTiles = 64; + /** + * The number of tiles in the vertical direction to cluster lights into. + * A lower value will reduce memory and make the clustering step faster, while a higher value increases memory and makes the rendering step faster. + */ public get verticalTiles(): number { return this._verticalTiles; } @@ -119,6 +143,10 @@ export class ClusteredLight extends Light { private _proxyMesh: Mesh; private _proxyTesselation = 8; + /** + * The amount of tesselation the light proxy (or light mesh) should have. + * A higher value increases memory and makes the clustering step slower, but reduces the amount of "false-positives" (lights marked as being in a tile when its not) which could slow down rendering. + */ public get proxyTesselation(): number { return this._proxyTesselation; } @@ -134,6 +162,9 @@ export class ClusteredLight extends Light { private _maxRange = 16383; private _minInverseSquaredRange = 1 / (this._maxRange * this._maxRange); + /** + * This limits the range of all the added lights, so even lights with extreme ranges will still have bounds for clustering. + */ public get maxRange(): number { return this._maxRange; } @@ -146,6 +177,13 @@ export class ClusteredLight extends Light { this._minInverseSquaredRange = 1 / (range * range); } + /** + * Creates a new clustered light system with an initial set of lights. + * + * @param name The name of the ClusteredLight + * @param lights The initial set of lights to add + * @param scene The scene the ClusteredLight belongs to + */ constructor(name: string, lights: Light[] = [], scene?: Scene) { super(name, scene); const engine = this.getEngine(); @@ -184,10 +222,18 @@ export class ClusteredLight extends Light { } } + /** + * Returns the string "ClusteredLight". + * @returns the class name + */ public override getClassName(): string { return "ClusteredLight"; } + /** + * Returns the light type ID (integer). + * @returns The light Type id as a constant defines in Light.LIGHTTYPEID_x + */ // eslint-disable-next-line @typescript-eslint/naming-convention public override getTypeID(): number { return LightConstants.LIGHTTYPEID_CLUSTERED; @@ -340,6 +386,11 @@ export class ClusteredLight extends Light { this._lightDataTexture.update(this._lightDataBuffer); } + /** + * Releases resources associated with this node. + * @param doNotRecurse Set to true to not recurse into each children (recurse into each children by default) + * @param disposeMaterialAndTextures Set to true to also dispose referenced materials and textures (false by default) + */ public override dispose(doNotRecurse?: boolean, disposeMaterialAndTextures?: boolean): void { for (const light of this._lights) { light.dispose(doNotRecurse, disposeMaterialAndTextures); @@ -351,6 +402,10 @@ export class ClusteredLight extends Light { super.dispose(doNotRecurse, disposeMaterialAndTextures); } + /** + * Adds a light to the clustering system. + * @param light The light to add + */ public addLight(light: Light): void { if (!ClusteredLight.IsLightSupported(light)) { Logger.Warn("Attempting to add a light to cluster that does not support clustering"); @@ -373,6 +428,12 @@ export class ClusteredLight extends Light { this._uniformBuffer.create(); } + /** + * Sets the passed Effect "effect" with the Light information. + * @param effect The effect to update + * @param lightIndex The index of the light in the effect to update + * @returns The light + */ public override transferToEffect(effect: Effect, lightIndex: string): Light { const engine = this.getEngine(); const hscale = this._horizontalTiles / engine.getRenderWidth(); @@ -382,6 +443,12 @@ export class ClusteredLight extends Light { return this; } + /** + * Sets the passed Effect "effect" with the Light textures. + * @param effect The effect to update + * @param lightIndex The index of the light in the effect to update + * @returns The light + */ public override transferTexturesToEffect(effect: Effect, lightIndex: string): Light { const engine = this.getEngine(); effect.setTexture("lightDataTexture" + lightIndex, this._lightDataTexture); @@ -393,16 +460,26 @@ export class ClusteredLight extends Light { return this; } + /** + * Sets the passed Effect "effect" with the Light information. + * @returns The light + */ public override transferToNodeMaterialEffect(): Light { // TODO: ???? return this; } + /** + * Prepares the list of defines specific to the light type. + * @param defines the list of defines + * @param lightIndex defines the index of the light for the effect + */ public override prepareLightSpecificDefines(defines: any, lightIndex: number): void { defines["CLUSTLIGHT" + lightIndex] = true; defines["CLUSTLIGHT_BATCH"] = this._batchSize; } + /** @internal */ public override _isReady(): boolean { this._updateBatches(); return this._proxyMesh.isReady(true, true); From a421718603f075908c33d30c72219b5140752d55 Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Wed, 30 Jul 2025 11:25:37 +1000 Subject: [PATCH 29/30] Build fixup --- .../Clustered/clusteredLightSceneComponent.ts | 25 ++++++++++++++++++- .../dev/core/src/Lights/Clustered/index.ts | 5 ++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts b/packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts index 9c9bdd0fc62..1cedab45247 100644 --- a/packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts +++ b/packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts @@ -3,19 +3,42 @@ import { type RenderTargetsStageAction, SceneComponentConstants, type ISceneComp import { ClusteredLight } from "./clusteredLight"; -class ClusteredLightSceneComponent implements ISceneComponent { +/** + * A scene component required for running the clustering step in clustered lights + */ +export class ClusteredLightSceneComponent implements ISceneComponent { + /** + * The name of the component. Each component must have a unique name. + */ public name = SceneComponentConstants.NAME_CLUSTEREDLIGHT; + /** + * The scene the component belongs to. + */ public scene: Scene; + /** + * Creates a new scene component. + * @param scene The scene the component belongs to + */ constructor(scene: Scene) { this.scene = scene; } + /** + * Disposes the component and the associated resources. + */ public dispose(): void {} + /** + * Rebuilds the elements related to this component in case of + * context lost for instance. + */ public rebuild(): void {} + /** + * Register the component to one instance of a scene. + */ public register(): void { this.scene._gatherActiveCameraRenderTargetsStage.registerStep( SceneComponentConstants.STEP_GATHERACTIVECAMERARENDERTARGETS_CLUSTEREDLIGHT, diff --git a/packages/dev/core/src/Lights/Clustered/index.ts b/packages/dev/core/src/Lights/Clustered/index.ts index 58db779168a..241382b557c 100644 --- a/packages/dev/core/src/Lights/Clustered/index.ts +++ b/packages/dev/core/src/Lights/Clustered/index.ts @@ -1,2 +1,7 @@ export * from "./clusteredLight"; export * from "./clusteredLightSceneComponent"; + +import "../../Shaders/lightProxy.fragment"; +import "../../Shaders/lightProxy.vertex"; +import "../../ShadersWGSL/lightProxy.fragment"; +import "../../ShadersWGSL/lightProxy.vertex"; From c49bffad75fec582f1041a6c462a87739374426c Mon Sep 17 00:00:00 2001 From: Jasmine Minter Date: Wed, 30 Jul 2025 11:31:38 +1000 Subject: [PATCH 30/30] Serialize and register --- .../core/src/Lights/Clustered/clusteredLight.ts | 14 ++++++++++++++ packages/dev/core/src/Lights/lightConstants.ts | 3 +++ 2 files changed, 17 insertions(+) diff --git a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts index e2f87684d56..9b99ffd2d00 100644 --- a/packages/dev/core/src/Lights/Clustered/clusteredLight.ts +++ b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts @@ -12,7 +12,10 @@ import { Vector3 } from "core/Maths/math.vector"; import { CreateDisc } from "core/Meshes/Builders/discBuilder"; import type { Mesh } from "core/Meshes/mesh"; import { _WarnImport } from "core/Misc/devTools"; +import { serialize } from "core/Misc/decorators"; import { Logger } from "core/Misc/logger"; +import { RegisterClass } from "core/Misc/typeStore"; +import { Node } from "core/node"; import type { Scene } from "core/scene"; import type { Nullable } from "core/types"; @@ -23,6 +26,10 @@ import { SpotLight } from "../spotLight"; import "core/Meshes/thinInstanceMesh"; +Node.AddNodeConstructor("Light_Type_5", (name, scene) => { + return () => new ClusteredLight(name, [], scene); +}); + /** * A special light that renders all its associated spot or point lights using a clustered or forward+ system. */ @@ -108,6 +115,7 @@ export class ClusteredLight extends Light { * The number of tiles in the horizontal direction to cluster lights into. * A lower value will reduce memory and make the clustering step faster, while a higher value increases memory and makes the rendering step faster. */ + @serialize() public get horizontalTiles(): number { return this._horizontalTiles; } @@ -126,6 +134,7 @@ export class ClusteredLight extends Light { * The number of tiles in the vertical direction to cluster lights into. * A lower value will reduce memory and make the clustering step faster, while a higher value increases memory and makes the rendering step faster. */ + @serialize() public get verticalTiles(): number { return this._verticalTiles; } @@ -147,6 +156,7 @@ export class ClusteredLight extends Light { * The amount of tesselation the light proxy (or light mesh) should have. * A higher value increases memory and makes the clustering step slower, but reduces the amount of "false-positives" (lights marked as being in a tile when its not) which could slow down rendering. */ + @serialize() public get proxyTesselation(): number { return this._proxyTesselation; } @@ -165,6 +175,7 @@ export class ClusteredLight extends Light { /** * This limits the range of all the added lights, so even lights with extreme ranges will still have bounds for clustering. */ + @serialize() public get maxRange(): number { return this._maxRange; } @@ -485,3 +496,6 @@ export class ClusteredLight extends Light { return this._proxyMesh.isReady(true, true); } } + +// Register Class Name +RegisterClass("BABYLON.ClusteredLight", ClusteredLight); diff --git a/packages/dev/core/src/Lights/lightConstants.ts b/packages/dev/core/src/Lights/lightConstants.ts index a021613bc9b..1d7954a9824 100644 --- a/packages/dev/core/src/Lights/lightConstants.ts +++ b/packages/dev/core/src/Lights/lightConstants.ts @@ -91,6 +91,9 @@ export class LightConstants { */ public static readonly LIGHTTYPEID_RECT_AREALIGHT = 4; + /** + * Light type const id of the clustered light. + */ public static readonly LIGHTTYPEID_CLUSTERED = 5; /**