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/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 560a222949d..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, @@ -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 d8ad192d04b..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, @@ -533,6 +534,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")), @@ -734,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 14bf08a80d1..3c104a4e646 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, @@ -869,6 +870,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, @@ -3901,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 new file mode 100644 index 00000000000..bc1deb19cd0 --- /dev/null +++ b/packages/dev/core/src/Lights/Clustered/clusteredLight.ts @@ -0,0 +1,354 @@ +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 { 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 type { Mesh } from "core/Meshes/mesh"; +import { _WarnImport } from "core/Misc/devTools"; +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"; +import { SpotLight } from "../spotLight"; + +import "core/Meshes/thinInstanceMesh"; + +export class ClusteredLight extends Light { + private static _GetEngineMaxLights(engine: AbstractEngine): number { + const caps = engine._caps; + 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.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; + } + } + + public static IsLightSupported(light: Light): boolean { + if (ClusteredLight._GetEngineMaxLights(light.getEngine()) === 0) { + 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; + } + } + + /** @internal */ + public static _SceneComponentInitialization: (scene: Scene) => void = () => { + throw _WarnImport("ClusteredLightSceneComponent"); + }; + + public readonly maxLights: number; + + public get isSupported(): boolean { + return this.maxLights > 0; + } + + private readonly _lights: (PointLight | SpotLight)[] = []; + public get lights(): readonly Light[] { + return this._lights; + } + + private _tileMaskTexture: Nullable; + private _tileMaskBuffer: Nullable; + + private _horizontalTiles = 64; + public get horizontalTiles(): number { + return this._horizontalTiles; + } + + public set horizontalTiles(horizontal: number) { + if (this._horizontalTiles === horizontal) { + return; + } + this._horizontalTiles = horizontal; + this._disposeTileMask(); + } + + private _verticalTiles = 64; + public get verticalTiles(): number { + return this._verticalTiles; + } + + public set verticalTiles(vertical: number) { + if (this._verticalTiles === vertical) { + return; + } + this._verticalTiles = vertical; + this._disposeTileMask(); + } + + private _proxyMesh: Mesh; + private readonly _proxyMatrixBuffer: Float32Array; + private _proxyRenderId = -1; + + 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(); + } + + 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); + this.maxLights = ClusteredLight._GetEngineMaxLights(this.getEngine()); + + this._proxyMatrixBuffer = new Float32Array(this.maxLights * 16); + this._createProxyMesh(); + + if (this.maxLights > 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; + } + + 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(); + + // 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 + matrix.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; + } + + 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); + } + + this._disposeTileMask(); + + this._proxyMesh.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"); + 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); + // Cause the matrix buffer to update + this._proxyRenderId = -1; + } + + 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 + "direction", 4); + this._uniformBuffer.addUniform(struct + "diffuse", 4); + this._uniformBuffer.addUniform(struct + "specular", 4); + this._uniformBuffer.addUniform(struct + "falloff", 4); + } + 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(); + + 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(); + 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); + + 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; + } + + public override transferTexturesToEffect(effect: Effect, lightIndex: string): Light { + const engine = this.getEngine(); + if (engine.isWebGPU) { + (this.getEngine()).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_MAX"] = this.maxLights; + } + + 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 new file mode 100644 index 00000000000..a421e5be612 --- /dev/null +++ b/packages/dev/core/src/Lights/Clustered/clusteredLightSceneComponent.ts @@ -0,0 +1,40 @@ +import type { Scene } from "core/scene"; +import { type RenderTargetsStageAction, SceneComponentConstants, type ISceneComponent } from "core/sceneComponent"; + +import { ClusteredLight } from "./clusteredLight"; + +class ClusteredLightSceneComponent implements ISceneComponent { + public name = SceneComponentConstants.NAME_CLUSTEREDLIGHT; + + public scene: Scene; + + constructor(scene: Scene) { + this.scene = scene; + } + + public dispose(): void {} + + public rebuild(): void {} + + 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 instanceof ClusteredLight && light.isSupported) { + renderTargets.push(light._createTileMask()); + } + } + }; +} + +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..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/Clustered/lightProxyMaterial.ts b/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts new file mode 100644 index 00000000000..d01230b036e --- /dev/null +++ b/packages/dev/core/src/Lights/Clustered/lightProxyMaterial.ts @@ -0,0 +1,49 @@ +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 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_WRITE", `CLUSTLIGHT_MAX ${clusteredLight.maxLights}`, `SEGMENT_ANGLE ${segmentAngle}`], + 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.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 { + 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/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..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/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/materialHelper.functions.ts b/packages/dev/core/src/Materials/materialHelper.functions.ts index ff19a590ac4..f69d50d5f2f 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; @@ -1073,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 tileMaskTexture defines if a tile mask texture must be used */ export function PrepareUniformsAndSamplersForLight( lightIndex: number, @@ -1081,7 +1083,8 @@ export function PrepareUniformsAndSamplersForLight( projectedLightTexture?: any, uniformBuffersList: Nullable = null, updateOnlyBuffersList = false, - iesLightTexture = false + iesLightTexture = false, + tileMaskTexture = false ) { if (uniformBuffersList) { uniformBuffersList.push("Light" + lightIndex); @@ -1124,6 +1127,9 @@ export function PrepareUniformsAndSamplersForLight( if (iesLightTexture) { samplersList.push("iesLightTexture" + lightIndex); } + if (tileMaskTexture) { + samplersList.push("tileMaskTexture" + lightIndex); + } } /** @@ -1162,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/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/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 15873c242f0..3dcf60faf8f 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/lightFragment.fx @@ -205,6 +205,8 @@ vReflectionInfos.y #endif ); + #elif defined(CLUSTLIGHT{X}) && CLUSTLIGHT_MAX > 0 + info = computeClusteredLighting{X}(viewDirectionW, normalW, diffuse{X}.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 61f182059e0..f41effacd83 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}) + SpotLight vLights[CLUSTLIGHT_MAX]; #endif #if defined(AREALIGHT{X}) vec4 vLightWidth; @@ -27,6 +29,10 @@ uniform mat4 textureProjectionMatrix{X}; uniform sampler2D projectionLightTexture{X}; #endif +#ifdef CLUSTLIGHT{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..c22d8176e13 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}) + 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 ff9a32bf424..edc48284867 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 == 0, the result in reality should always be 1 + return 1.0; + } return max(0., pow(cosAngle, exponent)); } @@ -178,4 +182,4 @@ lightingInfo computeAreaLighting(sampler2D ltc1, sampler2D ltc2, vec3 viewDirect } // End Area Light -#endif \ No newline at end of file +#endif diff --git a/packages/dev/core/src/Shaders/ShadersInclude/spotLightDeclaration.fx b/packages/dev/core/src/Shaders/ShadersInclude/spotLightDeclaration.fx new file mode 100644 index 00000000000..2ee2ceb90e9 --- /dev/null +++ b/packages/dev/core/src/Shaders/ShadersInclude/spotLightDeclaration.fx @@ -0,0 +1,8 @@ +// Used in clustered lights +struct SpotLight { + vec4 position; + vec4 direction; + vec4 diffuse; + vec4 specular; + vec4 falloff; +}; diff --git a/packages/dev/core/src/Shaders/default.fragment.fx b/packages/dev/core/src/Shaders/default.fragment.fx index 38077eadcb9..c53abc6c072 100644 --- a/packages/dev/core/src/Shaders/default.fragment.fx +++ b/packages/dev/core/src/Shaders/default.fragment.fx @@ -32,10 +32,14 @@ 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 86eec837b30..340c508fd9c 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 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..5f4d9930371 --- /dev/null +++ b/packages/dev/core/src/Shaders/lightProxy.fragment.fx @@ -0,0 +1,5 @@ +flat varying highp uint vMask; + +void main(void) { + 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..ed087909a03 --- /dev/null +++ b/packages/dev/core/src/Shaders/lightProxy.vertex.fx @@ -0,0 +1,33 @@ +attribute vec3 position; +flat varying highp uint vMask; + +// Declarations +#include +#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 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; + vMask = 1u << gl_InstanceID; +} 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..31fc65e1c9c 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,15 @@ 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 + 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..3116735465e 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..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)); } @@ -178,4 +182,36 @@ fn computeAreaLighting(ltc1: texture_2d, ltc1Sampler:sampler, ltc2:texture_ } // End Area Light -#endif \ No newline at end of file +#endif + +fn computeClusteredLighting( + tileMask: ptr>, + viewDirectionW: vec3f, + vNormal: vec3f, + lightData: vec4f, + lights: ptr>, + diffuseScale: vec3f, + specularScale: vec3f, + 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 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 + } + 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 dfe591e65f9..77729743560 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..753e5da859d 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..3e75b35f66a --- /dev/null +++ b/packages/dev/core/src/ShadersWGSL/lightProxy.fragment.fx @@ -0,0 +1,12 @@ +flat varying vMask: u32; + +// Declarations +#include +#include[0..1] + +@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); +} 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..265f75f8dad --- /dev/null +++ b/packages/dev/core/src/ShadersWGSL/lightProxy.vertex.fx @@ -0,0 +1,37 @@ +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]; + 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); + if angle < maxAngle { + positionUpdated = vertexInputs.position; + } + +#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; +} 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;