UNPKG

@babylonjs/core

Version:

Getting started? Play directly with the Babylon.js API using our [playground](https://playground.babylonjs.com/). It also contains a lot of samples to learn how to use it.

502 lines (501 loc) 22 kB
import { __decorate } from "../../tslib.es6.js"; import { StorageBuffer } from "../../Buffers/storageBuffer.js"; import { ShaderMaterial } from "../../Materials/shaderMaterial.js"; import { RawTexture } from "../../Materials/Textures/rawTexture.js"; import { RenderTargetTexture } from "../../Materials/Textures/renderTargetTexture.js"; import { UniformBuffer } from "../../Materials/uniformBuffer.js"; import { TmpColors } from "../../Maths/math.color.js"; import { TmpVectors, Vector3 } from "../../Maths/math.vector.js"; import { CreatePlane } from "../../Meshes/Builders/planeBuilder.js"; import { serialize } from "../../Misc/decorators.js"; import { _WarnImport } from "../../Misc/devTools.js"; import { Logger } from "../../Misc/logger.js"; import { RegisterClass } from "../../Misc/typeStore.js"; import { Node } from "../../node.js"; import { Light } from "../light.js"; import { LightConstants } from "../lightConstants.js"; import "../../Meshes/thinInstanceMesh.js"; Node.AddNodeConstructor("Light_Type_5", (name, scene) => { return () => new ClusteredLightContainer(name, [], scene); }); const DefaultDepthSlices = 16; /** * A special light that renders all its associated spot or point lights using a clustered or forward+ system. */ export class ClusteredLightContainer extends Light { static _GetEngineBatchSize(engine) { 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 */ static IsLightSupported(light) { if (ClusteredLightContainer._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; } } /** * True if clustered lighting is supported. */ get isSupported() { return this._batchSize > 0; } /** * Gets the current list of lights added to this clustering system. */ get lights() { return this._lights; } /** * 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. */ get horizontalTiles() { return this._horizontalTiles; } set horizontalTiles(horizontal) { if (this._horizontalTiles === horizontal) { return; } this._horizontalTiles = horizontal; // Force the batch data to be recreated this._tileMaskBatches = -1; } /** * 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. */ get verticalTiles() { return this._verticalTiles; } set verticalTiles(vertical) { if (this._verticalTiles === vertical) { return; } this._verticalTiles = vertical; // Force the batch data to be recreated this._tileMaskBatches = -1; } /** * The number of slices to split the depth range by and cluster lights into. */ get depthSlices() { return this._depthSlices; } set depthSlices(slices) { if (this._depthSlices === slices) { return; } this._depthSlices = slices; this._sliceRanges = new Float32Array(slices * 2); // UBO size depends on the number of depth slices this._uniformBuffer.dispose(); this._uniformBuffer = new UniformBuffer(this.getEngine(), undefined, undefined, this.name); this._buildUniformLayout(); } /** * This limits the range of all the added lights, so even lights with extreme ranges will still have bounds for clustering. */ get maxRange() { return this._maxRange; } set maxRange(range) { 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 clustered light container * @param lights The initial set of lights to add * @param scene The scene the clustered light container belongs to */ constructor(name, lights = [], scene) { super(name, scene); this._lights = []; this._camera = null; // The lights sorted by depth this._sortedLights = []; this._lightDataRenderId = -1; this._tileMaskBatches = -1; this._horizontalTiles = 64; this._verticalTiles = 64; this._sliceScale = 0; this._sliceBias = 0; this._depthSlices = DefaultDepthSlices; this._maxRange = 16383; this._minInverseSquaredRange = 1 / (this._maxRange * this._maxRange); const engine = this.getEngine(); this._batchSize = ClusteredLightContainer._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 ? 1 /* ShaderLanguage.WGSL */ : 0 /* ShaderLanguage.GLSL */, extraInitializationsAsync: async () => { if (engine.isWebGPU) { await Promise.all([import("../../ShadersWGSL/lightProxy.vertex.js"), import("../../ShadersWGSL/lightProxy.fragment.js")]); } else { await Promise.all([import("../../Shaders/lightProxy.vertex.js"), import("../../Shaders/lightProxy.fragment.js")]); } }, }); // Additive blending is for merging masks on WebGL this._proxyMaterial.transparencyMode = ShaderMaterial.MATERIAL_ALPHABLEND; this._proxyMaterial.alphaMode = 1; 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(); this._sliceRanges = new Float32Array(this._depthSlices * 2); if (this._batchSize > 0) { ClusteredLightContainer._SceneComponentInitialization(this._scene); for (const light of lights) { this.addLight(light); } } } getClassName() { return "ClusteredLightContainer"; } // eslint-disable-next-line @typescript-eslint/naming-convention getTypeID() { return LightConstants.LIGHTTYPEID_CLUSTERED_CONTAINER; } /** @internal */ _updateBatches(camera = null) { this._camera = camera; this._proxyMesh.isVisible = this._sortedLights.length > 0; // Ensure space for atleast 1 batch const batches = Math.max(Math.ceil(this._sortedLights.length / this._batchSize), 1); if (this._tileMaskBatches >= batches) { this._proxyMesh.thinInstanceCount = this._sortedLights.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, 5, this._scene, false, false, 1, 1); this._lightDataTexture.name = "LightDataTexture_clustered_" + this.name; 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 ? 0 : 1, format: 6, generateDepthBuffer: false, }); this._tileMaskTexture.renderParticles = false; this._tileMaskTexture.renderSprites = false; this._tileMaskTexture.noPrePassRenderer = true; this._tileMaskTexture.renderList = [this._proxyMesh]; let currentRenderTarget = null; this._tileMaskTexture.onBeforeBindObservable.add(() => { currentRenderTarget = engine._currentRenderTarget; this._updateLightData(); }); this._tileMaskTexture.onAfterUnbindObservable.add(() => { if (engine._currentRenderTarget !== currentRenderTarget) { if (!currentRenderTarget) { engine.restoreDefaultFramebuffer(); } else { engine.bindFramebuffer(currentRenderTarget); } } }); 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._sortedLights.length; this._tileMaskBatches = batches; return this._tileMaskTexture; } _getSliceIndex(camera, depth) { if (depth < camera.minZ) { // Prevent calling log on small or negative values return -1; } return Math.floor(Math.log(depth) * this._sliceScale + this._sliceBias); } _updateLightData() { const camera = this._camera || this._scene.activeCamera; const renderId = this._scene.getRenderId(); if (!camera || this._lightDataRenderId === renderId) { return; } this._lightDataRenderId = renderId; // Resort lights based on distance from camera const view = camera.getViewMatrix(); for (const light of this._sortedLights) { const position = light.computeTransformedInformation() ? light.transformedPosition : light.position; const viewPosition = Vector3.TransformCoordinatesToRef(position, view, TmpVectors.Vector3[0]); light._currentViewDepth = viewPosition.z; } this._sortedLights.sort((a, b) => a._currentViewDepth - b._currentViewDepth); // DOOM 2016 subdivision scheme, copied from: https://www.aortiz.me/2018/12/21/CG.html const logFarNear = Math.log(camera.maxZ / camera.minZ); this._sliceScale = this._depthSlices / logFarNear; this._sliceBias = -(this._depthSlices * Math.log(camera.minZ)) / logFarNear; this._sliceRanges.fill(0); // Last slice which had had its min index updated let minSlice = -1; const buf = this._lightDataBuffer; const offset = this._scene.floatingOriginOffset; for (let i = 0; i < this._sortedLights.length; i += 1) { const light = this._sortedLights[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 - offset.x; buf[off + 1] = position.y - offset.y; buf[off + 2] = position.z - offset.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; } // Update the depth slices that include this light const firstSlice = this._getSliceIndex(camera, light._currentViewDepth - range); const lastSlice = this._getSliceIndex(camera, light._currentViewDepth + range); for (let j = firstSlice; j <= lastSlice; j += 1) { if (j < 0 || j >= this._depthSlices) { continue; } else if (j > minSlice) { // Update min index this._sliceRanges[j * 2] = i; minSlice = j; } // Update max index this._sliceRanges[j * 2 + 1] = i; } } const engine = this.getEngine(); if (engine.isWebGPU) { // Whenever the light data changes we have to flush pending WebGPU command buffers so that // previous render passes use the old data and later render passes use the new data. engine.flushFramebuffer(); } this._lightDataTexture.update(this._lightDataBuffer); } dispose(doNotRecurse, disposeMaterialAndTextures) { 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 */ addLight(light) { if (!ClusteredLightContainer.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._sortedLights.push(light); this._proxyMesh.isVisible = true; this._proxyMesh.thinInstanceCount = this._sortedLights.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 */ removeLight(light) { // Convert to `Light` array without cast so `indexOf` has correct typing const sortedLights = this._sortedLights; const sortedIndex = sortedLights.indexOf(light); if (sortedIndex !== -1) { sortedLights.splice(sortedIndex, 1); this._proxyMesh.thinInstanceCount = sortedLights.length; if (sortedLights.length === 0) { this._proxyMesh.isVisible = false; } } const index = this._lights.indexOf(light); if (index !== -1) { this._lights.splice(index, 1); // We treat the unsorted array as the "real" one so only add back to the scene if it was found in that this._scene.addLight(light); } return index; } _buildUniformLayout() { this._uniformBuffer.addUniform("vLightData", 4); this._uniformBuffer.addUniform("vLightDiffuse", 4); this._uniformBuffer.addUniform("vLightSpecular", 4); this._uniformBuffer.addUniform("vSliceData", 2); // _depthSlices might not be initialized yet this._uniformBuffer.addUniform("vSliceRanges", 2, this._depthSlices ?? DefaultDepthSlices); this._uniformBuffer.addUniform("shadowsInfo", 3); this._uniformBuffer.addUniform("depthValues", 2); this._uniformBuffer.create(); } transferToEffect(effect, lightIndex) { 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.updateFloat2("vSliceData", this._sliceScale, this._sliceBias, lightIndex); this._uniformBuffer.updateFloatArray("vSliceRanges", this._sliceRanges, lightIndex); return this; } transferTexturesToEffect(effect, lightIndex) { 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; } transferToNodeMaterialEffect(_effect) { return this; } prepareLightSpecificDefines(defines, lightIndex) { defines["CLUSTLIGHT" + lightIndex] = true; defines["CLUSTLIGHT_BATCH"] = this._batchSize; defines["CLUSTLIGHT_SLICES"] = this._depthSlices; } _isReady() { this._updateBatches(); return this._proxyMesh.isReady(true, true); } } /** @internal */ ClusteredLightContainer._SceneComponentInitialization = () => { throw _WarnImport("ClusteredLightingSceneComponent"); }; __decorate([ serialize() ], ClusteredLightContainer.prototype, "horizontalTiles", null); __decorate([ serialize() ], ClusteredLightContainer.prototype, "verticalTiles", null); __decorate([ serialize() ], ClusteredLightContainer.prototype, "maxRange", null); // Register Class Name RegisterClass("BABYLON.ClusteredLightContainer", ClusteredLightContainer); //# sourceMappingURL=clusteredLightContainer.js.map