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.

401 lines (400 loc) 17 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 { TmpColors } from "../../Maths/math.color.js"; import { TmpVectors, Vector3 } from "../../Maths/math.vector.js"; import { CreatePlane } from "../../Meshes/Builders/planeBuilder.js"; import { _WarnImport } from "../../Misc/devTools.js"; import { serialize } from "../../Misc/decorators.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); }); /** * 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; } /** * 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._tileMaskBatches = -1; this._horizontalTiles = 64; this._verticalTiles = 64; 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(); 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() { 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, 5, this._scene, false, false, 1, 1); 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]; 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; } _updateLightData() { 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); } 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._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 */ removeLight(light) { 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; } _buildUniformLayout() { 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(); } 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.updateFloat("vNumLights", this._lights.length, 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() { // TODO: ???? return this; } prepareLightSpecificDefines(defines, lightIndex) { defines["CLUSTLIGHT" + lightIndex] = true; defines["CLUSTLIGHT_BATCH"] = this._batchSize; } _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