@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
JavaScript
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