diff --git a/packages/dev/core/src/Buffers/storageBuffer.ts b/packages/dev/core/src/Buffers/storageBuffer.ts index 2bfa08fab23..95b08c30fd0 100644 --- a/packages/dev/core/src/Buffers/storageBuffer.ts +++ b/packages/dev/core/src/Buffers/storageBuffer.ts @@ -46,6 +46,15 @@ export class StorageBuffer { return this._buffer; } + /** + * Clears the storage buffer to zeros + * @param byteOffset the byte offset to start clearing (optional) + * @param byteLength the byte length to clear (optional) + */ + 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/engineCapabilities.ts b/packages/dev/core/src/Engines/engineCapabilities.ts index 9c512fdc7d2..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 */ @@ -80,6 +82,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 c602e6c9d97..2a010aeb1f7 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, @@ -297,6 +298,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..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, @@ -144,6 +145,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 078ea5432a0..77c4a110218 100644 --- a/packages/dev/core/src/Engines/thinEngine.ts +++ b/packages/dev/core/src/Engines/thinEngine.ts @@ -504,6 +504,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, @@ -531,6 +532,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")), @@ -732,6 +734,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 307ec62ab6d..90f0df394bc 100644 --- a/packages/dev/core/src/Engines/webgpuEngine.ts +++ b/packages/dev/core/src/Engines/webgpuEngine.ts @@ -865,6 +865,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, @@ -877,6 +878,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, @@ -3936,6 +3938,16 @@ export class WebGPUEngine extends ThinWebGPUEngine { return this._createBuffer(data, creationFlags | Constants.BUFFER_CREATIONFLAG_STORAGE, label); } + /** + * Clears a storage buffer to zeroes + * @param storageBuffer the storage buffer to clear + * @param byteOffset the byte offset to start clearing (optional) + * @param byteLength the byte length to clear (optional) + */ + 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 new file mode 100644 index 00000000000..e2b5fc7f9a2 --- /dev/null +++ b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts @@ -0,0 +1,461 @@ +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"; +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 { TmpVectors, Vector3 } from "core/Maths/math.vector"; +import { CreatePlane } from "core/Meshes/Builders/planeBuilder"; +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"; + +import { Light } from "../light"; +import { LightConstants } from "../lightConstants"; +import type { PointLight } from "../pointLight"; +import type { 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. + */ +export class ClusteredLight extends Light { + private static _GetEngineBatchSize(engine: AbstractEngine): number { + const caps = engine._caps; + if (!caps.texelFetch) { + return 0; + } else if (engine.isWebGPU) { + // On WebGPU we use atomic writes to storage textures + return 32; + } 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 + return caps.shaderFloatPrecision; + } else { + // WebGL 1 is not supported due to lack of dynamic for loops + return 0; + } + } + + /** + * 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; + } 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.getTypeID() === LightConstants.LIGHTTYPEID_POINTLIGHT) { + return true; + } else if (light.getTypeID() === LightConstants.LIGHTTYPEID_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; + } + } + + /** @internal */ + public static _SceneComponentInitialization: (scene: Scene) => void = () => { + throw _WarnImport("ClusteredLightSceneComponent"); + }; + + 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; + } + + private _lightDataBuffer: Float32Array; + private _lightDataTexture: RawTexture; + + private _tileMaskBatches = -1; + private _tileMaskTexture: RenderTargetTexture; + 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. + */ + @serialize() + public get horizontalTiles(): number { + return this._horizontalTiles; + } + + public set horizontalTiles(horizontal: number) { + if (this._horizontalTiles === horizontal) { + return; + } + this._horizontalTiles = horizontal; + // Force the batch data to be recreated + this._tileMaskBatches = -1; + } + + 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. + */ + @serialize() + public get verticalTiles(): number { + return this._verticalTiles; + } + + public set verticalTiles(vertical: number) { + if (this._verticalTiles === vertical) { + return; + } + this._verticalTiles = vertical; + // Force the batch data to be recreated + this._tileMaskBatches = -1; + } + + private readonly _proxyMaterial: ShaderMaterial; + private readonly _proxyMesh: Mesh; + + 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. + */ + @serialize() + public get maxRange(): number { + return this._maxRange; + } + + public set maxRange(range: number) { + if (this._maxRange === range) { + return; + } + this._maxRange = range; + 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(); + this._batchSize = ClusteredLight._GetEngineBatchSize(engine); + + const proxyShader = { vertex: "lightProxy", fragment: "lightProxy" }; + this._proxyMaterial = new ShaderMaterial("ProxyMaterial", this._scene, proxyShader, { + attributes: ["position"], + uniforms: ["view", "projection", "tileMaskResolution"], + samplers: ["lightDataTexture"], + uniformBuffers: ["Scene"], + storageBuffers: ["tileMaskBuffer"], + 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._proxyMesh = CreatePlane("ProxyMesh", { size: 2 }); + // Make sure it doesn't render for the default scene + this._scene.removeMesh(this._proxyMesh); + this._proxyMesh.material = this._proxyMaterial; + + this._updateBatches(); + + if (this._batchSize > 0) { + ClusteredLight._SceneComponentInitialization(this._scene); + for (const light of lights) { + this.addLight(light); + } + } + } + + public override getClassName(): string { + return "ClusteredLight"; + } + + // eslint-disable-next-line @typescript-eslint/naming-convention + public override getTypeID(): number { + return LightConstants.LIGHTTYPEID_CLUSTERED; + } + + /** @internal */ + public _updateBatches(): RenderTargetTexture { + 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._proxyMesh.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, + 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._updateLightData(); + }); + + this._tileMaskTexture.onClearObservable.add(() => { + if (engine.isWebGPU) { + // Clear the storage buffer for WebGPU + this._tileMaskBuffer?.clear(); + } else { + // Only clear the texture on WebGL + } + engine.clear({ r: 0, g: 0, b: 0, a: 1 }, true, false); + }); + + if (engine.isWebGPU) { + // WebGPU also needs a storage buffer to write to + 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._proxyMesh.thinInstanceSetBuffer("matrix", new Float32Array(maxLights * 16)); + this._proxyMesh.thinInstanceCount = this._lights.length; + this._tileMaskBatches = batches; + return this._tileMaskTexture; + } + + private _updateLightData(): void { + const buf = this._lightDataBuffer; + for (let i = 0; i < this._lights.length; i += 1) { + const light = this._lights[i]; + const off = i * 20; + const computed = light.computeTransformedInformation(); + const scaledIntensity = light.getScaledIntensity(); + + 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); + + // vLightData + buf[off + 0] = position.x; + buf[off + 1] = position.y; + buf[off + 2] = position.z; + buf[off + 3] = 0; + // vLightDiffuse + buf[off + 4] = diffuse.r; + buf[off + 5] = diffuse.g; + buf[off + 6] = diffuse.b; + buf[off + 7] = range; + // vLightSpecular + buf[off + 8] = specular.r; + buf[off + 9] = specular.g; + buf[off + 10] = specular.b; + buf[off + 11] = light.radius; + // vLightDirection + buf[off + 12] = 0; + buf[off + 13] = 0; + buf[off + 14] = 0; + buf[off + 15] = -1; + // vLightFalloff + buf[off + 16] = range; + buf[off + 17] = inverseSquaredRange; + buf[off + 18] = 0; + buf[off + 19] = 0; + + if (light.getTypeID() === LightConstants.LIGHTTYPEID_SPOTLIGHT) { + const spotLight = light; + const direction = Vector3.NormalizeToRef(computed ? spotLight.transformedDirection : spotLight.direction, TmpVectors.Vector3[0]); + + // vLightData.a + buf[off + 3] = spotLight.exponent; + // vLightDirection + buf[off + 12] = direction.x; + buf[off + 13] = direction.y; + buf[off + 14] = direction.z; + buf[off + 15] = spotLight._cosHalfAngle; + // vLightFalloff.zw + buf[off + 18] = spotLight._lightAngleScale; + buf[off + 19] = spotLight._lightAngleOffset; + } + } + this._lightDataTexture.update(this._lightDataBuffer); + } + + public override dispose(doNotRecurse?: boolean, disposeMaterialAndTextures?: boolean): void { + for (const light of this._lights) { + light.dispose(doNotRecurse, disposeMaterialAndTextures); + } + this._lightDataTexture.dispose(); + this._tileMaskTexture.dispose(); + this._tileMaskBuffer?.dispose(); + this._proxyMesh.dispose(doNotRecurse, disposeMaterialAndTextures); + 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"); + return; + } + this._scene.removeLight(light); + this._lights.push(light); + + this._proxyMesh.isVisible = true; + this._proxyMesh.thinInstanceCount = this._lights.length; + } + + /** + * Removes a light from the clustering system. + * @param light The light to remove + * @returns the index where the light was in the light list + */ + public removeLight(light: Light): number { + const index = this.lights.indexOf(light); + if (index === -1) { + return index; + } + this._lights.splice(index, 1); + this._scene.addLight(light); + + this._proxyMesh.thinInstanceCount = this._lights.length; + if (this._lights.length === 0) { + this._proxyMesh.isVisible = false; + } + return index; + } + + 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(); + } + + public override transferToEffect(effect: Effect, lightIndex: string): 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._tileMaskBatches, lightIndex); + this._uniformBuffer.updateFloat("vNumLights", this._lights.length, lightIndex); + return this; + } + + public override transferTexturesToEffect(effect: Effect, lightIndex: string): Light { + const engine = this.getEngine(); + effect.setTexture("lightDataTexture" + lightIndex, this._lightDataTexture); + if (engine.isWebGPU) { + (engine).setStorageBuffer("tileMaskBuffer" + lightIndex, this._tileMaskBuffer); + } else { + effect.setTexture("tileMaskTexture" + lightIndex, this._tileMaskTexture); + } + return this; + } + + public override transferToNodeMaterialEffect(): Light { + // TODO: ???? + return this; + } + + public override prepareLightSpecificDefines(defines: any, lightIndex: number): void { + defines["CLUSTLIGHT" + lightIndex] = true; + defines["CLUSTLIGHT_BATCH"] = this._batchSize; + } + + public override _isReady(): boolean { + this._updateBatches(); + return this._proxyMesh.isReady(true, true); + } +} + +// Register Class Name +RegisterClass("BABYLON.ClusteredLight", ClusteredLight); 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..02a67d303b6 --- /dev/null +++ b/packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts @@ -0,0 +1,65 @@ +import type { Scene } from "core/scene"; +import type { RenderTargetsStageAction, ISceneComponent } from "core/sceneComponent"; +import { SceneComponentConstants } from "core/sceneComponent"; + +import { ClusteredLight } from "./clusteredLight"; +import { LightConstants } from "../lightConstants"; + +/** + * 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, + this, + this._gatherActiveCameraRenderTargets + ); + } + + private _gatherActiveCameraRenderTargets: RenderTargetsStageAction = (renderTargets) => { + for (const light of this.scene.lights) { + if (light.getTypeID() === LightConstants.LIGHTTYPEID_CLUSTERED && (light).isSupported) { + renderTargets.push((light)._updateBatches()); + } + } + }; +} + +ClusteredLight._SceneComponentInitialization = (scene) => { + if (!scene._getComponent(SceneComponentConstants.NAME_CLUSTEREDLIGHT)) { + scene._addComponent(new ClusteredLightSceneComponent(scene)); + } +}; 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..241382b557c --- /dev/null +++ b/packages/dev/core/src/Lights/Clustered/index.ts @@ -0,0 +1,7 @@ +export * from "./clusteredLight"; +export * from "./clusteredLightSceneComponent"; + +import "../../Shaders/lightProxy.fragment"; +import "../../Shaders/lightProxy.vertex"; +import "../../ShadersWGSL/lightProxy.fragment"; +import "../../ShadersWGSL/lightProxy.vertex"; diff --git a/packages/dev/core/src/Lights/index.ts b/packages/dev/core/src/Lights/index.ts index 75712031eea..b47c3ae5fee 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 "./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 ae45332de9c..64671c4961f 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. @@ -483,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..1d7954a9824 100644 --- a/packages/dev/core/src/Lights/lightConstants.ts +++ b/packages/dev/core/src/Lights/lightConstants.ts @@ -91,6 +91,11 @@ export class LightConstants { */ public static readonly LIGHTTYPEID_RECT_AREALIGHT = 4; + /** + * Light type const id of the clustered light. + */ + 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/Lights/spotLight.ts b/packages/dev/core/src/Lights/spotLight.ts index 71d263626fb..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; diff --git a/packages/dev/core/src/Materials/PBR/pbrBaseMaterial.ts b/packages/dev/core/src/Materials/PBR/pbrBaseMaterial.ts index 63fcb5abbaf..23a945ef6bf 100644 --- a/packages/dev/core/src/Materials/PBR/pbrBaseMaterial.ts +++ b/packages/dev/core/src/Materials/PBR/pbrBaseMaterial.ts @@ -1261,7 +1261,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/Materials/materialHelper.functions.ts b/packages/dev/core/src/Materials/materialHelper.functions.ts index 976429e345f..9f945d9b7d7 100644 --- a/packages/dev/core/src/Materials/materialHelper.functions.ts +++ b/packages/dev/core/src/Materials/materialHelper.functions.ts @@ -577,6 +577,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; @@ -1083,6 +1084,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 clusteredLightTextures defines if the clustered light textures must be used */ export function PrepareUniformsAndSamplersForLight( lightIndex: number, @@ -1091,7 +1093,8 @@ export function PrepareUniformsAndSamplersForLight( projectedLightTexture?: any, uniformBuffersList: Nullable = null, updateOnlyBuffersList = false, - iesLightTexture = false + iesLightTexture = false, + clusteredLightTextures = false ) { if (uniformBuffersList) { uniformBuffersList.push("Light" + lightIndex); @@ -1110,6 +1113,7 @@ export function PrepareUniformsAndSamplersForLight( "vLightHeight" + lightIndex, "vLightFalloff" + lightIndex, "vLightGround" + lightIndex, + "vNumLights" + lightIndex, "lightMatrix" + lightIndex, "shadowsInfo" + lightIndex, "depthValues" + lightIndex @@ -1134,6 +1138,10 @@ export function PrepareUniformsAndSamplersForLight( if (iesLightTexture) { samplersList.push("iesLightTexture" + lightIndex); } + if (clusteredLightTextures) { + samplersList.push("lightDataTexture" + lightIndex); + samplersList.push("tileMaskTexture" + lightIndex); + } } /** @@ -1172,7 +1180,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/Materials/standardMaterial.ts b/packages/dev/core/src/Materials/standardMaterial.ts index 157a334484b..b31a11f5104 100644 --- a/packages/dev/core/src/Materials/standardMaterial.ts +++ b/packages/dev/core/src/Materials/standardMaterial.ts @@ -1230,8 +1230,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_BATCH"]) { for (let index = 0; index < mesh.lightSources.length; index++) { if (!mesh.lightSources[index]._isReady()) { return false; diff --git a/packages/dev/core/src/Materials/uniformBuffer.ts b/packages/dev/core/src/Materials/uniformBuffer.ts index 3e028eb2617..bb3ee7566e9 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/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/lightFragment.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx index 15873c242f0..afe609e26fc 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx @@ -6,7 +6,38 @@ vec4 diffuse{X} = 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}) && CLUSTLIGHT_BATCH > 0 + info = computeClusteredLighting( + lightDataTexture{X}, + tileMaskTexture{X}, + light{X}.vLightData, + int(light{X}.vNumLights), + viewDirectionW, + normalW, + vPositionW, + surfaceAlbedo, + reflectivityOut + #ifdef IRIDESCENCE + , iridescenceIntensity + #endif + #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, vPositionW); @@ -205,6 +236,8 @@ vReflectionInfos.y #endif ); + #elif defined(CLUSTLIGHT{X}) && CLUSTLIGHT_BATCH > 0 + 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/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/ShadersInclude/lightUboDeclaration.fx b/packages/dev/core/src/Shaders/ShadersInclude/lightUboDeclaration.fx index 61f182059e0..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; @@ -27,6 +29,11 @@ uniform mat4 textureProjectionMatrix{X}; 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 #ifdef SHADOW{X} #ifdef SHADOWCSM{X} uniform mat4 lightMatrix{X}[SHADOWCSMNUM_CASCADES{X}]; 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 ff9a32bf424..5abb5861183 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightsFragmentFunctions.fx @@ -178,4 +178,53 @@ lightingInfo computeAreaLighting(sampler2D ltc1, sampler2D ltc2, vec3 viewDirect } // End Area Light -#endif \ No newline at end of file +#endif + +#if defined(CLUSTLIGHT_BATCH) && CLUSTLIGHT_BATCH > 0 +#include + +lightingInfo computeClusteredLighting( + sampler2D lightDataTexture, + sampler2D tileMaskTexture, + vec3 viewDirectionW, + vec3 vNormal, + vec4 lightData, + int numLights, + float glossiness +) { + lightingInfo result; + 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; + int batchOffset = 0; + + for (int i = 0; i < numBatches; i += 1) { + uint mask = uint(texelFetch(tileMaskTexture, tilePosition, 0).r); + tilePosition.y += maskHeight; + + 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; + if (light.vLightDirection.w < 0.0) { + // Assume an angle greater than 180º is a point light + info = computeLighting(viewDirectionW, vNormal, light.vLightData, light.vLightDiffuse.rgb, light.vLightSpecular.rgb, light.vLightDiffuse.a, glossiness); + } else { + 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 + } + batchOffset += CLUSTLIGHT_BATCH; + } + return result; +} +#endif diff --git a/packages/dev/core/src/Shaders/ShadersInclude/pbrClusteredLightingFunctions.fx b/packages/dev/core/src/Shaders/ShadersInclude/pbrClusteredLightingFunctions.fx new file mode 100644 index 00000000000..a5bb7a73333 --- /dev/null +++ b/packages/dev/core/src/Shaders/ShadersInclude/pbrClusteredLightingFunctions.fx @@ -0,0 +1,169 @@ +#if defined(CLUSTLIGHT_BATCH) && CLUSTLIGHT_BATCH > 0 +#include + + lightingInfo computeClusteredLighting( + sampler2D lightDataTexture, + sampler2D tileMaskTexture, + vec4 lightData, + int numLights, + vec3 V, + vec3 N, + vec3 posW, + vec3 surfaceAlbedo, + reflectivityOutParams reflectivityOut + #ifdef IRIDESCENCE + , float iridescenceIntensity + #endif + #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; + 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; + int batchOffset = 0; + + for (int i = 0; i < numBatches; i += 1) { + uint mask = uint(texelFetch(tileMaskTexture, tilePosition, 0).r); + tilePosition.y += maskHeight; + + 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, light.vLightFalloff.x, light.vLightFalloff.y); + // Assume an angle greater than 180º is a point light + if (light.vLightDirection.w >= 0.0) { + 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, light.vLightSpecular.a, preInfo.lightDistance); + preInfo.diffuseRoughness = reflectivityOut.diffuseRoughness; + preInfo.surfaceAlbedo = surfaceAlbedo; + + #ifdef IRIDESCENCE + preInfo.iridescenceIntensity = iridescenceIntensity; + #endif + lightingInfo info; + + // Diffuse contribution + #ifdef SS_TRANSLUCENCY + #ifdef SS_TRANSLUCENCY_LEGACY + info.diffuse = computeDiffuseTransmittedLighting(preInfo, light.vLightDiffuse.rgb, subSurfaceOut.transmittance); + info.diffuseTransmission = vec3(0); + #else + 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, light.vLightDiffuse.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, light.vLightDiffuse.rgb); + #else + info.specular = computeSpecularLighting(preInfo, N, specularEnvironmentR0, coloredFresnel, AARoughnessFactor, light.vLightDiffuse.rgb); + #endif + #endif + + // Sheen contribution + #ifdef SHEEN + #ifdef SHEEN_LINKWITHALBEDO + preInfo.roughness = sheenOut.sheenIntensity; + #else + preInfo.roughness = adjustRoughnessFromLightProperties(sheenOut.sheenRoughness, light.vLightSpecular.a, preInfo.lightDistance); + #endif + info.sheen = computeSheenLighting(preInfo, normalW, sheenOut.sheenColor, specularEnvironmentR90, AARoughnessFactor, light.vLightDiffuse.rgb); + #endif + + // Clear Coat contribution + #ifdef CLEARCOAT + 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 + 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 + } + batchOffset += CLUSTLIGHT_BATCH; + } + return result; + } +#endif 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..cd548a7bdcb --- /dev/null +++ b/packages/dev/core/src/Shaders/lightProxy.fragment.fx @@ -0,0 +1,11 @@ +flat varying vec2 vLimits; +flat varying highp uint vMask; + +void main(void) { + // Ensure the pixel is within the limits for the batch + if (gl_FragCoord.y < vLimits.x || gl_FragCoord.y > vLimits.y) { + 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 new file mode 100644 index 00000000000..34178723dbf --- /dev/null +++ b/packages/dev/core/src/Shaders/lightProxy.vertex.fx @@ -0,0 +1,52 @@ +attribute vec3 position; +flat varying vec2 vLimits; +flat varying highp uint vMask; + +// Uniforms +#include<__decl__sceneVertex> + +uniform sampler2D lightDataTexture; +uniform vec3 tileMaskResolution; + +#include + +void main(void) { + SpotLight light = getClusteredSpotLight(lightDataTexture, gl_InstanceID); + float range = light.vLightFalloff.x; + + vec4 viewPosition = view * vec4(light.vLightData.xyz, 1); + vec4 viewPositionSq = viewPosition * viewPosition; + + // Squared distance for both XZ and YZ + vec2 distSq = viewPositionSq.xy + viewPositionSq.z; + // Compute the horizontal and vertical angles to rotate by to get the sphere horizon positions + vec2 sinSq = (range * range) / distSq; + // Rotation is multiplied by cos (cos^2 and sin*cos) to scale down the vector after rotation + vec2 cosSq = max(1.0 - sinSq, 0.01); + // Flip the sin values (reversing rotation) if the position is negative + vec2 sinCos = position.xy * sqrt(sinSq * cosSq); + + // Apply rotation + vec2 rotatedX = mat2(cosSq.x, -sinCos.x, sinCos.x, cosSq.x) * viewPosition.xz; + vec2 rotatedY = mat2(cosSq.y, -sinCos.y, sinCos.y, cosSq.y) * viewPosition.yz; + // Apply projection + vec4 projX = projection * vec4(rotatedX.x, 0, rotatedX.y, 1); + vec4 projY = projection * vec4(0, rotatedY.x, rotatedY.y, 1); + vec2 projPosition = vec2(projX.x / max(projX.w, 0.01), projY.y / max(projY.w, 0.01)); + // Override with screen extents if rotation invalid (occurs when inside the sphere) + projPosition = mix(projPosition, position.xy, equal(cosSq, vec2(0.01))); + + // Convert to NDC 0->1 space and scale to the tile resolution + vec2 halfTileRes = tileMaskResolution.xy / 2.0; + vec2 tilePosition = (projPosition.xy + 1.0) * 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))); + // Reposition vertically based on current batch + 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 / halfTileRes - 1.0, 0, 1); + vLimits = vec2(offset, offset + tileMaskResolution.y); + vMask = 1u << (gl_InstanceID % CLUSTLIGHT_BATCH); +} diff --git a/packages/dev/core/src/Shaders/pbr.fragment.fx b/packages/dev/core/src/Shaders/pbr.fragment.fx index ac621aa07b2..03cfb5182c4 100644 --- a/packages/dev/core/src/Shaders/pbr.fragment.fx +++ b/packages/dev/core/src/Shaders/pbr.fragment.fx @@ -71,6 +71,8 @@ precision highp float; #include #include +#include + // _____________________________ MAIN FUNCTION ____________________________ void main(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..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/lightFragment.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightFragment.fx index 4d4b65b76f4..5c6d4e54c16 100644 --- a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightFragment.fx +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightFragment.fx @@ -6,7 +6,38 @@ 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( + lightDataTexture{X}, + &tileMaskBuffer{X}, + light{X}.vLightData, + i32(light{X}.vNumLights), + viewDirectionW, + normalW, + fragmentInputs.vPositionW, + surfaceAlbedo, + reflectivityOut, + #ifdef IRIDESCENCE + iridescenceIntensity, + #endif + #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); @@ -206,6 +237,8 @@ uniforms.vReflectionInfos.y #endif ); + #elif defined(CLUSTLIGHT{X}) + 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 d1e0ca42670..0d96c38d06a 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}) + vNumLights: f32, #endif #if defined(AREALIGHT{X}) vLightWidth: vec4f, @@ -32,6 +34,12 @@ var light{X} : Light{X}; var projectionLightTexture{X}Sampler: sampler; var projectionLightTexture{X}: texture_2d; #endif + +#ifdef CLUSTLIGHT{X} + var lightDataTexture{X}: texture_2d; + var tileMaskBuffer{X}: array; +#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..2eb1a9ca274 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}) + 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 da3912d23a5..c2f481d83ed 100644 --- a/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/lightsFragmentFunctions.fx @@ -19,7 +19,7 @@ fn computeLighting(viewDirectionW: vec3f, vNormal: vec3f, lightData: vec4f, diff { var direction: vec3f = lightData.xyz - fragmentInputs.vPositionW; - var attenuation: f32 = max(0., 1.0 - length(direction) / range); + attenuation = max(0., 1.0 - length(direction) / range); lightVectorW = normalize(direction); } else @@ -178,4 +178,51 @@ fn computeAreaLighting(ltc1: texture_2d, ltc1Sampler:sampler, ltc2:texture_ } // End Area Light -#endif \ No newline at end of file +#endif + +#ifdef CLUSTLIGHT_BATCH +#include + +fn computeClusteredLighting( + lightDataTexture: texture_2d, + tileMaskBuffer: ptr>, + viewDirectionW: vec3f, + vNormal: vec3f, + lightData: vec4f, + numLights: i32, + glossiness: f32 +) -> lightingInfo { + var result: lightingInfo; + 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 += 1; + + while mask != 0 { + let trailing = firstTrailingBit(mask); + mask ^= 1u << trailing; + let light = getClusteredSpotLight(lightDataTexture, batchOffset + trailing); + + var info: lightingInfo; + if light.vLightDirection.w < 0.0 { + // Assume an angle greater than 180º is a point light + info = computeLighting(viewDirectionW, vNormal, light.vLightData, light.vLightDiffuse.rgb, light.vLightSpecular.rgb, light.vLightDiffuse.a, glossiness); + } else { + 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 + } + batchOffset += CLUSTLIGHT_BATCH; + } + return result; +} +#endif diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/pbrDirectLightingFunctions.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/pbrDirectLightingFunctions.fx index e0073baf1d6..fcfc687ad34 100644 --- a/packages/dev/core/src/ShadersWGSL/ShadersInclude/pbrDirectLightingFunctions.fx +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/pbrDirectLightingFunctions.fx @@ -206,3 +206,171 @@ fn computeProjectionTextureDiffuseLighting(projectionLightTexture: texture_2d + + fn computeClusteredLighting( + lightDataTexture: texture_2d, + tileMaskBuffer: ptr>, + lightData: vec4f, + numLights: i32, + V: vec3f, + N: vec3f, + posW: vec3f, + surfaceAlbedo: vec3f, + reflectivityOut: reflectivityOutParams, + #ifdef IRIDESCENCE + iridescenceIntensity: f32, + #endif + #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 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 += 1; + + while mask != 0 { + let trailing = firstTrailingBit(mask); + mask ^= 1u << trailing; + let light = getClusteredSpotLight(lightDataTexture, batchOffset + trailing); + + var preInfo = computePointAndSpotPreLightingInfo(light.vLightData, V, N, posW); + preInfo.NdotV = NdotV; + + // Compute Attenuation infos + preInfo.attenuation = computeDistanceLightFalloff(preInfo.lightOffset, preInfo.lightDistanceSquared, light.vLightFalloff.x, light.vLightFalloff.y); + // Assume an angle greater than 180º is a point light + if light.vLightDirection.w >= 0.0 { + 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, light.vLightSpecular.a, preInfo.lightDistance); + preInfo.diffuseRoughness = reflectivityOut.diffuseRoughness; + preInfo.surfaceAlbedo = surfaceAlbedo; + + #ifdef IRIDESCENCE + preInfo.iridescenceIntensity = iridescenceIntensity; + #endif + var info: lightingInfo; + + // Diffuse contribution + #ifdef SS_TRANSLUCENCY + #ifdef SS_TRANSLUCENCY_LEGACY + info.diffuse = computeDiffuseTransmittedLighting(preInfo, light.vLightDiffuse.rgb, subSurfaceOut.transmittance); + info.diffuseTransmission = vec3(0); + #else + 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, light.vLightDiffuse.rgb); + #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, light.vLightDiffuse.rgb); + #else + info.specular = computeSpecularLighting(preInfo, N, specularEnvironmentR0, coloredFresnel, AARoughnessFactor, light.vLightDiffuse.rgb); + #endif + #endif + + // Sheen contribution + #ifdef SHEEN + #ifdef SHEEN_LINKWITHALBEDO + preInfo.roughness = sheenOut.sheenIntensity; + #else + preInfo.roughness = adjustRoughnessFromLightProperties(sheenOut.sheenRoughness, light.vLightSpecular.a, preInfo.lightDistance); + #endif + info.sheen = computeSheenLighting(preInfo, normalW, sheenOut.sheenColor, specularEnvironmentR90, AARoughnessFactor, light.vLightDiffuse.rgb); + #endif + + // Clear Coat contribution + #ifdef CLEARCOAT + 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 + 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 + } + batchOffset += CLUSTLIGHT_BATCH; + } + return result; + } +#endif 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..2f04981f46f --- /dev/null +++ b/packages/dev/core/src/ShadersWGSL/lightProxy.fragment.fx @@ -0,0 +1,16 @@ +flat varying vOffset: u32; +flat varying vMask: u32; + +// Uniforms +uniform tileMaskResolution: vec3f; +var tileMaskBuffer: array>; + +@fragment +fn main(input: FragmentInputs) -> FragmentOutputs { + 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 new file mode 100644 index 00000000000..3a20f6b6fd3 --- /dev/null +++ b/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx @@ -0,0 +1,31 @@ +attribute position: vec3f; +flat varying vOffset: u32; +flat varying vMask: u32; + +// Uniforms +#include + +var lightDataTexture: texture_2d; +uniform tileMaskResolution: vec3f; +uniform halfTileRes: vec2f; + +#include + +@vertex +fn main(input: VertexInputs) -> FragmentInputs { + 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(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 + 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 so set Z to 0 + vertexOutputs.position = vec4f(tilePosition / uniforms.tileMaskResolution.xy * 2.0 - 1.0, 0, 1); + vertexOutputs.vOffset = vertexInputs.instanceIndex / CLUSTLIGHT_BATCH; + vertexOutputs.vMask = 1u << (vertexInputs.instanceIndex % CLUSTLIGHT_BATCH); +} diff --git a/packages/dev/core/src/sceneComponent.ts b/packages/dev/core/src/sceneComponent.ts index cdac46f3dc0..c56e795df34 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; @@ -96,6 +97,7 @@ export class SceneComponentConstants { 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; diff --git a/packages/tools/tests/test/visualization/ReferenceImages/sponza-clustered-lighting.png b/packages/tools/tests/test/visualization/ReferenceImages/sponza-clustered-lighting.png new file mode 100644 index 00000000000..7f355993f4c Binary files /dev/null and b/packages/tools/tests/test/visualization/ReferenceImages/sponza-clustered-lighting.png differ diff --git a/packages/tools/tests/test/visualization/config.json b/packages/tools/tests/test/visualization/config.json index c17bd43ea3a..8ed78f532c3 100644 --- a/packages/tools/tests/test/visualization/config.json +++ b/packages/tools/tests/test/visualization/config.json @@ -2736,6 +2736,11 @@ { "title": "Test code inlining", "playgroundId": "#YG3BBF#51" + }, + { + "title": "Sponza Clustered Lighting", + "playgroundId": "#CSCJO2#12", + "referenceImage": "sponza-clustered-lighting.png" } ] }