playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
277 lines (276 loc) • 11 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
import { Vec2 } from "../../core/math/vec2.js";
import { Vec3 } from "../../core/math/vec3.js";
import { BoundingBox } from "../../core/shape/bounding-box.js";
import { PIXELFORMAT_R8U } from "../../platform/graphics/constants.js";
import { TextureUtils } from "../../platform/graphics/texture-utils.js";
import { LIGHTTYPE_DIRECTIONAL, LIGHTTYPE_SPOT, MASK_AFFECT_DYNAMIC, MASK_AFFECT_LIGHTMAPPED } from "../constants.js";
import { LightsBuffer } from "./lights-buffer.js";
import { Debug } from "../../core/debug.js";
const tmpSize = new Vec2();
const tempVec3 = new Vec3();
const tempMin3 = new Vec3();
const tempMax3 = new Vec3();
const tempBox = new BoundingBox();
const maxTextureSize = 4096;
class ClusterLight {
constructor() {
this.light = null;
this.min = new Vec3();
this.max = new Vec3();
}
}
class WorldClusters {
constructor(device) {
/** @type {Texture} */
__publicField(this, "clusterTexture");
this.device = device;
this.name = "Untitled";
this.reportCount = 0;
this.boundsMin = new Vec3();
this.boundsMax = new Vec3();
this.boundsDelta = new Vec3();
this._cells = new Vec3(1, 1, 1);
this._cellsLimit = new Vec3();
this.cells = this._cells;
this.maxCellLightCount = 4;
this._usedLights = [];
this._usedLights.push(new ClusterLight());
this.lightsBuffer = new LightsBuffer(device);
this.registerUniforms(device);
}
set maxCellLightCount(count) {
if (count !== this._maxCellLightCount) {
this._maxCellLightCount = count;
this._cellsDirty = true;
}
}
get maxCellLightCount() {
return this._maxCellLightCount;
}
set cells(value) {
tempVec3.copy(value).floor();
if (!this._cells.equals(tempVec3)) {
this._cells.copy(tempVec3);
this._cellsLimit.copy(tempVec3).sub(Vec3.ONE);
this._cellsDirty = true;
}
}
get cells() {
return this._cells;
}
destroy() {
this.lightsBuffer.destroy();
this.releaseClusterTexture();
}
releaseClusterTexture() {
if (this.clusterTexture) {
this.clusterTexture.destroy();
this.clusterTexture = null;
}
}
registerUniforms(device) {
this._numClusteredLightsId = device.scope.resolve("numClusteredLights");
this._clusterMaxCellsId = device.scope.resolve("clusterMaxCells");
this._clusterWorldTextureId = device.scope.resolve("clusterWorldTexture");
this._clusterBoundsMinId = device.scope.resolve("clusterBoundsMin");
this._clusterBoundsMinData = new Float32Array(3);
this._clusterBoundsDeltaId = device.scope.resolve("clusterBoundsDelta");
this._clusterBoundsDeltaData = new Float32Array(3);
this._clusterCellsCountByBoundsSizeId = device.scope.resolve("clusterCellsCountByBoundsSize");
this._clusterCellsCountByBoundsSizeData = new Float32Array(3);
this._clusterCellsDotId = device.scope.resolve("clusterCellsDot");
this._clusterCellsDotData = new Int32Array(3);
this._clusterCellsMaxId = device.scope.resolve("clusterCellsMax");
this._clusterCellsMaxData = new Int32Array(3);
this._clusterTextureWidthId = device.scope.resolve("clusterTextureWidth");
}
// updates itself based on parameters stored in the scene
updateParams(lightingParams) {
if (lightingParams) {
this.cells = lightingParams.cells;
this.maxCellLightCount = lightingParams.maxLightsPerCell;
this.lightsBuffer.cookiesEnabled = lightingParams.cookiesEnabled;
this.lightsBuffer.shadowsEnabled = lightingParams.shadowsEnabled;
this.lightsBuffer.areaLightsEnabled = lightingParams.areaLightsEnabled;
}
}
updateCells() {
if (this._cellsDirty) {
this._cellsDirty = false;
const cx = this._cells.x;
const cy = this._cells.y;
const cz = this._cells.z;
const numCells = cx * cy * cz;
const totalPixels = this.maxCellLightCount * numCells;
const { x: width, y: height } = TextureUtils.calcTextureSize(totalPixels, tmpSize, this.maxCellLightCount);
Debug.assert(
width <= maxTextureSize && height <= maxTextureSize,
"Clustered lights parameters cause the texture size to be over the limit, please adjust them."
);
this._clusterCellsMaxData[0] = cx;
this._clusterCellsMaxData[1] = cy;
this._clusterCellsMaxData[2] = cz;
this._clusterCellsDotData[0] = this.maxCellLightCount;
this._clusterCellsDotData[1] = cx * cz * this.maxCellLightCount;
this._clusterCellsDotData[2] = cx * this.maxCellLightCount;
this.clusters = new Uint8ClampedArray(totalPixels);
this.counts = new Int32Array(numCells);
this.releaseClusterTexture();
this.clusterTexture = this.lightsBuffer.createTexture(this.device, width, height, PIXELFORMAT_R8U, "ClusterTexture");
}
}
uploadTextures() {
this.clusterTexture.lock().set(this.clusters);
this.clusterTexture.unlock();
this.lightsBuffer.uploadTextures();
}
updateUniforms() {
this._numClusteredLightsId.setValue(this._usedLights.length);
this.lightsBuffer.updateUniforms();
this._clusterWorldTextureId.setValue(this.clusterTexture);
this._clusterMaxCellsId.setValue(this.maxCellLightCount);
const boundsDelta = this.boundsDelta;
this._clusterCellsCountByBoundsSizeData[0] = this._cells.x / boundsDelta.x;
this._clusterCellsCountByBoundsSizeData[1] = this._cells.y / boundsDelta.y;
this._clusterCellsCountByBoundsSizeData[2] = this._cells.z / boundsDelta.z;
this._clusterCellsCountByBoundsSizeId.setValue(this._clusterCellsCountByBoundsSizeData);
this._clusterBoundsMinData[0] = this.boundsMin.x;
this._clusterBoundsMinData[1] = this.boundsMin.y;
this._clusterBoundsMinData[2] = this.boundsMin.z;
this._clusterBoundsDeltaData[0] = boundsDelta.x;
this._clusterBoundsDeltaData[1] = boundsDelta.y;
this._clusterBoundsDeltaData[2] = boundsDelta.z;
this._clusterBoundsMinId.setValue(this._clusterBoundsMinData);
this._clusterBoundsDeltaId.setValue(this._clusterBoundsDeltaData);
this._clusterCellsDotId.setValue(this._clusterCellsDotData);
this._clusterCellsMaxId.setValue(this._clusterCellsMaxData);
this._clusterTextureWidthId.setValue(this.clusterTexture.width);
}
// evaluates min and max coordinates of AABB of the light in the cell space
evalLightCellMinMax(clusteredLight, min, max) {
min.copy(clusteredLight.min);
min.sub(this.boundsMin);
min.div(this.boundsDelta);
min.mul2(min, this.cells);
min.floor();
max.copy(clusteredLight.max);
max.sub(this.boundsMin);
max.div(this.boundsDelta);
max.mul2(max, this.cells);
max.ceil();
min.max(Vec3.ZERO);
max.min(this._cellsLimit);
}
collectLights(lights) {
const maxLights = this.lightsBuffer.maxLights;
const usedLights = this._usedLights;
let lightIndex = 1;
lights.forEach((light) => {
const runtimeLight = !!(light.mask & (MASK_AFFECT_DYNAMIC | MASK_AFFECT_LIGHTMAPPED));
const zeroAngleSpotlight = light.type === LIGHTTYPE_SPOT && light._outerConeAngle === 0;
if (light.enabled && light.type !== LIGHTTYPE_DIRECTIONAL && light.visibleThisFrame && light.intensity > 0 && runtimeLight && !zeroAngleSpotlight) {
if (lightIndex < maxLights) {
let clusteredLight;
if (lightIndex < usedLights.length) {
clusteredLight = usedLights[lightIndex];
} else {
clusteredLight = new ClusterLight();
usedLights.push(clusteredLight);
}
clusteredLight.light = light;
light.getBoundingBox(tempBox);
clusteredLight.min.copy(tempBox.getMin());
clusteredLight.max.copy(tempBox.getMax());
lightIndex++;
} else {
Debug.warnOnce(`Clustered lighting: more than ${maxLights - 1} lights in the frame, ignoring some.`);
}
}
});
usedLights.length = lightIndex;
}
// evaluate the area all lights cover
evaluateBounds() {
const usedLights = this._usedLights;
const min = this.boundsMin;
const max = this.boundsMax;
if (usedLights.length > 1) {
min.copy(usedLights[1].min);
max.copy(usedLights[1].max);
for (let i = 2; i < usedLights.length; i++) {
min.min(usedLights[i].min);
max.max(usedLights[i].max);
}
} else {
min.set(0, 0, 0);
max.set(1, 1, 1);
}
this.boundsDelta.sub2(max, min);
this.lightsBuffer.setBounds(min, this.boundsDelta);
}
updateClusters(lightingParams) {
this.counts.fill(0);
this.clusters.fill(0);
this.lightsBuffer.areaLightsEnabled = lightingParams ? lightingParams.areaLightsEnabled : false;
const divX = this._cells.x;
const divZ = this._cells.z;
const counts = this.counts;
const limit = this._maxCellLightCount;
const clusters = this.clusters;
const pixelsPerCellCount = this.maxCellLightCount;
let tooManyLights = false;
const usedLights = this._usedLights;
for (let i = 1; i < usedLights.length; i++) {
const clusteredLight = usedLights[i];
const light = clusteredLight.light;
this.lightsBuffer.addLightData(light, i);
this.evalLightCellMinMax(clusteredLight, tempMin3, tempMax3);
const xStart = tempMin3.x;
const xEnd = tempMax3.x;
const yStart = tempMin3.y;
const yEnd = tempMax3.y;
const zStart = tempMin3.z;
const zEnd = tempMax3.z;
for (let x = xStart; x <= xEnd; x++) {
for (let z = zStart; z <= zEnd; z++) {
for (let y = yStart; y <= yEnd; y++) {
const clusterIndex = x + divX * (z + y * divZ);
const count = counts[clusterIndex];
if (count < limit) {
clusters[pixelsPerCellCount * clusterIndex + count] = i;
counts[clusterIndex] = count + 1;
} else {
tooManyLights = true;
}
}
}
}
}
if (tooManyLights) {
const reportLimit = 5;
if (this.reportCount < reportLimit) {
console.warn(`Too many lights in light cluster ${this.name}, please adjust parameters.${this.reportCount === reportLimit - 1 ? " Giving up on reporting it." : ""}`);
this.reportCount++;
}
}
}
// internal update of the cluster data, executes once per frame
update(lights, lightingParams = null) {
this.updateParams(lightingParams);
this.updateCells();
this.collectLights(lights);
this.evaluateBounds();
this.updateClusters(lightingParams);
this.uploadTextures();
}
// called on already updated clusters, activates for rendering by setting up uniforms / textures on the device
activate() {
this.updateUniforms();
}
}
export {
WorldClusters
};