playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
818 lines (817 loc) • 25.3 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 { Debug } from "../core/debug.js";
import { hash32Fnv1a } from "../core/hash.js";
import {
LIGHTTYPE_DIRECTIONAL,
SORTMODE_BACK2FRONT,
SORTMODE_CUSTOM,
SORTMODE_FRONT2BACK,
SORTMODE_MATERIALMESH,
SORTMODE_NONE
} from "./constants.js";
import { Material } from "./materials/material.js";
let layerCounter = 0;
const lightKeys = [];
const _tempMaterials = /* @__PURE__ */ new Set();
function sortManual(drawCallA, drawCallB) {
return drawCallA.drawOrder - drawCallB.drawOrder;
}
function sortMaterialMesh(drawCallA, drawCallB) {
const keyA = drawCallA._sortKeyForward;
const keyB = drawCallB._sortKeyForward;
if (keyA === keyB) {
return drawCallB.mesh.id - drawCallA.mesh.id;
}
return keyB - keyA;
}
function sortBackToFront(drawCallA, drawCallB) {
return drawCallB._sortKeyDynamic - drawCallA._sortKeyDynamic;
}
function sortFrontToBack(drawCallA, drawCallB) {
return drawCallA._sortKeyDynamic - drawCallB._sortKeyDynamic;
}
const sortCallbacks = [null, sortManual, sortMaterialMesh, sortBackToFront, sortFrontToBack];
class CulledInstances {
constructor() {
/**
* Visible opaque mesh instances.
*
* @type {MeshInstance[]}
*/
__publicField(this, "opaque", []);
/**
* Visible transparent mesh instances.
*
* @type {MeshInstance[]}
*/
__publicField(this, "transparent", []);
}
}
class Layer {
/**
* Create a new Layer instance.
*
* @param {object} options - Object for passing optional arguments. These arguments are the
* same as properties of the Layer.
*/
constructor(options = {}) {
// --- Identity ---
/**
* A unique ID of the layer. Layer IDs are stored inside {@link ModelComponent#layers},
* {@link RenderComponent#layers}, {@link CameraComponent#layers},
* {@link LightComponent#layers} and {@link ElementComponent#layers} instead of names.
* Can be used in {@link LayerComposition#getLayerById}.
*
* @type {number}
*/
__publicField(this, "id");
/**
* Name of the layer. Can be used in {@link LayerComposition#getLayerByName}.
*
* @type {string}
*/
__publicField(this, "name");
// --- Enabled state & ref counting ---
/**
* @type {boolean}
* @private
*/
__publicField(this, "_enabled", true);
/**
* @type {number}
* @private
*/
__publicField(this, "_refCounter", 1);
// --- Sorting ---
/**
* Defines the method used for sorting opaque (that is, not semi-transparent) mesh
* instances before rendering. Can be:
*
* - {@link SORTMODE_NONE}
* - {@link SORTMODE_MANUAL}
* - {@link SORTMODE_MATERIALMESH}
* - {@link SORTMODE_BACK2FRONT}
* - {@link SORTMODE_FRONT2BACK}
*
* Defaults to {@link SORTMODE_MATERIALMESH}.
*
* @type {number}
*/
__publicField(this, "opaqueSortMode", SORTMODE_MATERIALMESH);
/**
* Defines the method used for sorting semi-transparent mesh instances before rendering. Can be:
*
* - {@link SORTMODE_NONE}
* - {@link SORTMODE_MANUAL}
* - {@link SORTMODE_MATERIALMESH}
* - {@link SORTMODE_BACK2FRONT}
* - {@link SORTMODE_FRONT2BACK}
*
* Defaults to {@link SORTMODE_BACK2FRONT}.
*
* @type {number}
*/
__publicField(this, "transparentSortMode", SORTMODE_BACK2FRONT);
/**
* @type {Function|null}
* @ignore
*/
__publicField(this, "customSortCallback", null);
/**
* @type {Function|null}
* @ignore
*/
__publicField(this, "customCalculateSortValues", null);
// --- Clear flags ---
/** @private */
__publicField(this, "_clearColorBuffer", false);
/** @private */
__publicField(this, "_clearDepthBuffer", false);
/** @private */
__publicField(this, "_clearStencilBuffer", false);
// --- Enable / disable callbacks ---
/**
* Custom function that is called after the layer has been enabled. This happens when:
*
* - The layer is created with {@link enabled} set to true (which is the default value).
* - {@link enabled} was changed from false to true
*
* @type {Function}
*/
__publicField(this, "onEnable");
/**
* Custom function that is called after the layer has been disabled. This happens when:
*
* - {@link enabled} was changed from true to false
* - {@link decrementCounter} was called and set the counter to zero.
*
* @type {Function}
*/
__publicField(this, "onDisable");
// --- Mesh instances & shadow casters ---
/**
* Mesh instances assigned to this layer.
*
* @type {MeshInstance[]}
* @ignore
*/
__publicField(this, "meshInstances", []);
/**
* Mesh instances assigned to this layer, stored in a set.
*
* @type {Set<MeshInstance>}
* @ignore
*/
__publicField(this, "meshInstancesSet", /* @__PURE__ */ new Set());
/**
* Shadow casting instances assigned to this layer.
*
* @type {MeshInstance[]}
* @ignore
*/
__publicField(this, "shadowCasters", []);
/**
* Shadow casting instances assigned to this layer, stored in a set.
*
* @type {Set<MeshInstance>}
* @ignore
*/
__publicField(this, "shadowCastersSet", /* @__PURE__ */ new Set());
/**
* Visible (culled) mesh instances assigned to this layer. Looked up by the Camera.
*
* @type {WeakMap<Camera, CulledInstances>}
* @private
*/
__publicField(this, "_visibleInstances", /* @__PURE__ */ new WeakMap());
// --- Lights ---
/**
* All lights assigned to a layer.
*
* @type {Light[]}
* @private
*/
__publicField(this, "_lights", []);
/**
* All lights assigned to a layer stored in a set.
*
* @type {Set<Light>}
* @private
*/
__publicField(this, "_lightsSet", /* @__PURE__ */ new Set());
/**
* Set of light used by clustered lighting (omni and spot, but no directional).
*
* @type {Set<Light>}
* @private
*/
__publicField(this, "_clusteredLightsSet", /* @__PURE__ */ new Set());
/**
* Lights separated by light type. Lights in the individual arrays are sorted by the key,
* to match their order in _lightIdHash, so that their order matches the order expected by the
* generated shader code.
*
* @type {Light[][]}
* @private
*/
__publicField(this, "_splitLights", [[], [], []]);
/**
* True if _splitLights needs to be updated, which means if lights were added or removed from
* the layer, or their key changed.
*
* @private
*/
__publicField(this, "_splitLightsDirty", true);
/** @private */
__publicField(this, "_lightHash", 0);
/** @private */
__publicField(this, "_lightHashDirty", false);
/** @private */
__publicField(this, "_lightIdHash", 0);
/** @private */
__publicField(this, "_lightIdHashDirty", false);
/**
* True if the objects rendered on the layer require light cube (emitters with lighting do).
*
* @ignore
*/
__publicField(this, "requiresLightCube", false);
// --- Cameras ---
/**
* @type {CameraComponent[]}
* @ignore
*/
__publicField(this, "cameras", []);
/**
* @type {Set<Camera>}
* @ignore
*/
__publicField(this, "camerasSet", /* @__PURE__ */ new Set());
// --- GSplat ---
/**
* @type {GSplatPlacement[]}
* @ignore
*/
__publicField(this, "gsplatPlacements", []);
/**
* @type {Set<GSplatPlacement>}
* @ignore
*/
__publicField(this, "gsplatPlacementsSet", /* @__PURE__ */ new Set());
/**
* @type {GSplatPlacement[]}
* @ignore
*/
__publicField(this, "gsplatShadowCasters", []);
/**
* @type {Set<GSplatPlacement>}
* @ignore
*/
__publicField(this, "gsplatShadowCastersSet", /* @__PURE__ */ new Set());
/**
* True if the gsplatPlacements array was modified.
*
* @ignore
*/
__publicField(this, "gsplatPlacementsDirty", true);
// --- Composition / shader versioning ---
/**
* True if the composition is invalidated.
*
* @ignore
*/
__publicField(this, "_dirtyComposition", false);
/** @private */
__publicField(this, "_shaderVersion", -1);
// --- Profiler ---
__publicField(this, "skipRenderAfter", Number.MAX_VALUE);
__publicField(this, "_skipRenderCounter", 0);
__publicField(this, "_renderTime", 0);
__publicField(this, "_forwardDrawCalls", 0);
// deprecated, not useful on a layer anymore, could be moved to camera
__publicField(this, "_shadowDrawCalls", 0);
if (options.id !== void 0) {
this.id = options.id;
layerCounter = Math.max(this.id + 1, layerCounter);
} else {
this.id = layerCounter++;
}
this.name = options.name;
this._enabled = options.enabled ?? true;
this._refCounter = this._enabled ? 1 : 0;
this.opaqueSortMode = options.opaqueSortMode ?? SORTMODE_MATERIALMESH;
this.transparentSortMode = options.transparentSortMode ?? SORTMODE_BACK2FRONT;
this._clearColorBuffer = !!options.clearColorBuffer;
this._clearDepthBuffer = !!options.clearDepthBuffer;
this._clearStencilBuffer = !!options.clearStencilBuffer;
this.onEnable = options.onEnable;
this.onDisable = options.onDisable;
if (this._enabled && this.onEnable) {
this.onEnable();
}
}
/**
* Sets the enabled state of the layer. Disabled layers are skipped. Defaults to true.
*
* @type {boolean}
*/
set enabled(val) {
if (val !== this._enabled) {
this._dirtyComposition = true;
this.gsplatPlacementsDirty = true;
this._enabled = val;
if (val) {
this.incrementCounter();
if (this.onEnable) this.onEnable();
} else {
this.decrementCounter();
if (this.onDisable) this.onDisable();
}
}
}
/**
* Gets the enabled state of the layer.
*
* @type {boolean}
*/
get enabled() {
return this._enabled;
}
/**
* Sets whether the camera will clear the color buffer when it renders this layer.
*
* @type {boolean}
*/
set clearColorBuffer(val) {
this._clearColorBuffer = val;
this._dirtyComposition = true;
}
/**
* Gets whether the camera will clear the color buffer when it renders this layer.
*
* @type {boolean}
*/
get clearColorBuffer() {
return this._clearColorBuffer;
}
/**
* Sets whether the camera will clear the depth buffer when it renders this layer.
*
* @type {boolean}
*/
set clearDepthBuffer(val) {
this._clearDepthBuffer = val;
this._dirtyComposition = true;
}
/**
* Gets whether the camera will clear the depth buffer when it renders this layer.
*
* @type {boolean}
*/
get clearDepthBuffer() {
return this._clearDepthBuffer;
}
/**
* Sets whether the camera will clear the stencil buffer when it renders this layer.
*
* @type {boolean}
*/
set clearStencilBuffer(val) {
this._clearStencilBuffer = val;
this._dirtyComposition = true;
}
/**
* Gets whether the camera will clear the stencil buffer when it renders this layer.
*
* @type {boolean}
*/
get clearStencilBuffer() {
return this._clearStencilBuffer;
}
/**
* Gets whether the layer contains omni or spot lights.
*
* @type {boolean}
* @ignore
*/
get hasClusteredLights() {
return this._clusteredLightsSet.size > 0;
}
/**
* Gets the lights used by clustered lighting in a set.
*
* @type {Set<Light>}
* @ignore
*/
get clusteredLightsSet() {
return this._clusteredLightsSet;
}
/**
* Increments the usage counter of this layer. By default, layers are created with counter set
* to 1 (if {@link Layer.enabled} is true) or 0 (if it was false). Incrementing the counter
* from 0 to 1 will enable the layer and call {@link Layer.onEnable}. Use this function to
* "subscribe" multiple effects to the same layer. For example, if the layer is used to render
* a reflection texture which is used by 2 mirrors, then each mirror can call this function
* when visible and {@link Layer.decrementCounter} if invisible. In such case the reflection
* texture won't be updated, when there is nothing to use it, saving performance.
*
* @ignore
*/
incrementCounter() {
if (this._refCounter === 0) {
this._enabled = true;
if (this.onEnable) this.onEnable();
}
this._refCounter++;
}
/**
* Decrements the usage counter of this layer. Decrementing the counter from 1 to 0 will
* disable the layer and call {@link Layer.onDisable}.
*
* @ignore
*/
decrementCounter() {
if (this._refCounter === 1) {
this._enabled = false;
if (this.onDisable) this.onDisable();
} else if (this._refCounter === 0) {
Debug.warn("Trying to decrement layer counter below 0");
return;
}
this._refCounter--;
}
/**
* Adds a gsplat placement to this layer.
*
* @param {GSplatPlacement} placement - A placement of a gsplat.
* @ignore
*/
addGSplatPlacement(placement) {
if (!this.gsplatPlacementsSet.has(placement)) {
this.gsplatPlacements.push(placement);
this.gsplatPlacementsSet.add(placement);
this.gsplatPlacementsDirty = true;
}
}
/**
* Removes a gsplat placement from this layer.
*
* @param {GSplatPlacement} placement - A placement of a gsplat.
* @ignore
*/
removeGSplatPlacement(placement) {
const index = this.gsplatPlacements.indexOf(placement);
if (index >= 0) {
this.gsplatPlacements.splice(index, 1);
this.gsplatPlacementsSet.delete(placement);
this.gsplatPlacementsDirty = true;
}
}
/**
* Adds a gsplat placement to this layer as a shadow caster.
*
* @param {GSplatPlacement} placement - A placement of a gsplat.
* @ignore
*/
addGSplatShadowCaster(placement) {
if (!this.gsplatShadowCastersSet.has(placement)) {
this.gsplatShadowCasters.push(placement);
this.gsplatShadowCastersSet.add(placement);
this.gsplatPlacementsDirty = true;
}
}
/**
* Removes a gsplat placement from the shadow casters of this layer.
*
* @param {GSplatPlacement} placement - A placement of a gsplat.
* @ignore
*/
removeGSplatShadowCaster(placement) {
const index = this.gsplatShadowCasters.indexOf(placement);
if (index >= 0) {
this.gsplatShadowCasters.splice(index, 1);
this.gsplatShadowCastersSet.delete(placement);
this.gsplatPlacementsDirty = true;
}
}
/**
* Adds an array of mesh instances to this layer.
*
* @param {MeshInstance[]} meshInstances - Array of {@link MeshInstance}.
* @param {boolean} [skipShadowCasters] - Set it to true if you don't want these mesh instances
* to cast shadows in this layer. Defaults to false.
*/
addMeshInstances(meshInstances, skipShadowCasters) {
const destMeshInstances = this.meshInstances;
const destMeshInstancesSet = this.meshInstancesSet;
for (let i = 0; i < meshInstances.length; i++) {
const mi = meshInstances[i];
if (!destMeshInstancesSet.has(mi)) {
destMeshInstances.push(mi);
destMeshInstancesSet.add(mi);
_tempMaterials.add(mi.material);
}
}
if (!skipShadowCasters) {
this.addShadowCasters(meshInstances);
}
if (_tempMaterials.size > 0) {
const sceneShaderVer = this._shaderVersion;
_tempMaterials.forEach((mat) => {
if (sceneShaderVer >= 0 && mat._shaderVersion !== sceneShaderVer) {
if (mat.getShaderVariant !== Material.prototype.getShaderVariant) {
mat.clearVariants();
}
mat._shaderVersion = sceneShaderVer;
}
});
_tempMaterials.clear();
}
}
/**
* Removes multiple mesh instances from this layer.
*
* @param {MeshInstance[]} meshInstances - Array of {@link MeshInstance}. If they were added to
* this layer, they will be removed.
* @param {boolean} [skipShadowCasters] - Set it to true if you want to still cast shadows from
* removed mesh instances or if they never did cast shadows before. Defaults to false.
*/
removeMeshInstances(meshInstances, skipShadowCasters) {
const destMeshInstances = this.meshInstances;
const destMeshInstancesSet = this.meshInstancesSet;
for (let i = 0; i < meshInstances.length; i++) {
const mi = meshInstances[i];
if (destMeshInstancesSet.has(mi)) {
destMeshInstancesSet.delete(mi);
const j = destMeshInstances.indexOf(mi);
if (j >= 0) {
destMeshInstances.splice(j, 1);
}
}
}
if (!skipShadowCasters) {
this.removeShadowCasters(meshInstances);
}
}
/**
* Adds an array of mesh instances to this layer, but only as shadow casters (they will not be
* rendered anywhere, but only cast shadows on other objects).
*
* @param {MeshInstance[]} meshInstances - Array of {@link MeshInstance}.
*/
addShadowCasters(meshInstances) {
const shadowCasters = this.shadowCasters;
const shadowCastersSet = this.shadowCastersSet;
for (let i = 0; i < meshInstances.length; i++) {
const mi = meshInstances[i];
if (mi.castShadow && !shadowCastersSet.has(mi)) {
shadowCastersSet.add(mi);
shadowCasters.push(mi);
}
}
}
/**
* Removes multiple mesh instances from the shadow casters list of this layer, meaning they
* will stop casting shadows.
*
* @param {MeshInstance[]} meshInstances - Array of {@link MeshInstance}. If they were added to
* this layer, they will be removed.
*/
removeShadowCasters(meshInstances) {
const shadowCasters = this.shadowCasters;
const shadowCastersSet = this.shadowCastersSet;
for (let i = 0; i < meshInstances.length; i++) {
const mi = meshInstances[i];
if (shadowCastersSet.has(mi)) {
shadowCastersSet.delete(mi);
const j = shadowCasters.indexOf(mi);
if (j >= 0) {
shadowCasters.splice(j, 1);
}
}
}
}
/**
* Removes all mesh instances from this layer.
*
* @param {boolean} [skipShadowCasters] - Set it to true if you want to continue the existing mesh
* instances to cast shadows. Defaults to false, which removes shadow casters as well.
*/
clearMeshInstances(skipShadowCasters = false) {
this.meshInstances.length = 0;
this.meshInstancesSet.clear();
if (!skipShadowCasters) {
this.shadowCasters.length = 0;
this.shadowCastersSet.clear();
}
}
markLightsDirty() {
this._lightHashDirty = true;
this._lightIdHashDirty = true;
this._splitLightsDirty = true;
}
hasLight(light) {
return this._lightsSet.has(light);
}
/**
* Adds a light to this layer.
*
* @param {LightComponent} light - A {@link LightComponent}.
*/
addLight(light) {
const l = light.light;
if (!this._lightsSet.has(l)) {
this._lightsSet.add(l);
this._lights.push(l);
this.markLightsDirty();
}
if (l.type !== LIGHTTYPE_DIRECTIONAL) {
this._clusteredLightsSet.add(l);
}
}
/**
* Removes a light from this layer.
*
* @param {LightComponent} light - A {@link LightComponent}.
*/
removeLight(light) {
const l = light.light;
if (this._lightsSet.has(l)) {
this._lightsSet.delete(l);
this._lights.splice(this._lights.indexOf(l), 1);
this.markLightsDirty();
}
if (l.type !== LIGHTTYPE_DIRECTIONAL) {
this._clusteredLightsSet.delete(l);
}
}
/**
* Removes all lights from this layer.
*/
clearLights() {
this._lightsSet.forEach((light) => light.removeLayer(this));
this._lightsSet.clear();
this._clusteredLightsSet.clear();
this._lights.length = 0;
this.markLightsDirty();
}
get splitLights() {
if (this._splitLightsDirty) {
this._splitLightsDirty = false;
const splitLights = this._splitLights;
for (let i = 0; i < splitLights.length; i++) {
splitLights[i].length = 0;
}
const lights = this._lights;
for (let i = 0; i < lights.length; i++) {
const light = lights[i];
if (light.enabled) {
splitLights[light._type].push(light);
}
}
for (let i = 0; i < splitLights.length; i++) {
splitLights[i].sort((a, b) => a.key - b.key);
}
}
return this._splitLights;
}
evaluateLightHash(localLights, directionalLights, useIds) {
let hash = 0;
const lights = this._lights;
for (let i = 0; i < lights.length; i++) {
const isLocalLight = lights[i].type !== LIGHTTYPE_DIRECTIONAL;
if (localLights && isLocalLight || directionalLights && !isLocalLight) {
lightKeys.push(useIds ? lights[i].id : lights[i].key);
}
}
if (lightKeys.length > 0) {
lightKeys.sort();
hash = hash32Fnv1a(lightKeys);
lightKeys.length = 0;
}
return hash;
}
getLightHash(isClustered) {
if (this._lightHashDirty) {
this._lightHashDirty = false;
this._lightHash = this.evaluateLightHash(!isClustered, true, false);
}
return this._lightHash;
}
// This is only used in clustered lighting mode
getLightIdHash() {
if (this._lightIdHashDirty) {
this._lightIdHashDirty = false;
this._lightIdHash = this.evaluateLightHash(true, false, true);
}
return this._lightIdHash;
}
/**
* Adds a camera to this layer.
*
* @param {CameraComponent} camera - A {@link CameraComponent}.
*/
addCamera(camera) {
if (!this.camerasSet.has(camera.camera)) {
this.camerasSet.add(camera.camera);
this.cameras.push(camera);
this._dirtyComposition = true;
}
}
/**
* Removes a camera from this layer.
*
* @param {CameraComponent} camera - A {@link CameraComponent}.
*/
removeCamera(camera) {
if (this.camerasSet.has(camera.camera)) {
this.camerasSet.delete(camera.camera);
const index = this.cameras.indexOf(camera);
this.cameras.splice(index, 1);
this._dirtyComposition = true;
}
}
/**
* Removes all cameras from this layer.
*/
clearCameras() {
this.cameras.length = 0;
this.camerasSet.clear();
this._dirtyComposition = true;
}
/**
* @param {MeshInstance[]} drawCalls - Array of mesh instances.
* @param {Vec3} camPos - Camera position.
* @param {Vec3} camFwd - Camera forward vector.
* @private
*/
_calculateSortDistances(drawCalls, camPos, camFwd) {
const count = drawCalls.length;
const { x: px, y: py, z: pz } = camPos;
const { x: fx, y: fy, z: fz } = camFwd;
for (let i = 0; i < count; i++) {
const drawCall = drawCalls[i];
let zDist;
if (drawCall.calculateSortDistance) {
zDist = drawCall.calculateSortDistance(drawCall, camPos, camFwd);
} else {
const meshPos = drawCall.aabb.center;
zDist = (meshPos.x - px) * fx + (meshPos.y - py) * fy + (meshPos.z - pz) * fz;
}
const bucket = drawCall._drawBucket * 1e9;
drawCall._sortKeyDynamic = bucket + zDist;
}
}
/**
* Get access to culled mesh instances for the provided camera.
*
* @param {Camera} camera - The camera.
* @returns {CulledInstances} The culled mesh instances.
* @ignore
*/
getCulledInstances(camera) {
let instances = this._visibleInstances.get(camera);
if (!instances) {
instances = new CulledInstances();
this._visibleInstances.set(camera, instances);
}
return instances;
}
/**
* @param {Camera} camera - The camera to sort the visible mesh instances for.
* @param {boolean} transparent - True if transparent sorting should be used.
* @ignore
*/
sortVisible(camera, transparent) {
const sortMode = transparent ? this.transparentSortMode : this.opaqueSortMode;
if (sortMode === SORTMODE_NONE) {
return;
}
const culledInstances = this.getCulledInstances(camera);
const instances = transparent ? culledInstances.transparent : culledInstances.opaque;
const cameraNode = camera.node;
if (sortMode === SORTMODE_CUSTOM) {
const sortPos = cameraNode.getPosition();
const sortDir = cameraNode.forward;
if (this.customCalculateSortValues) {
this.customCalculateSortValues(instances, instances.length, sortPos, sortDir);
}
if (this.customSortCallback) {
instances.sort(this.customSortCallback);
}
} else {
if (sortMode === SORTMODE_BACK2FRONT || sortMode === SORTMODE_FRONT2BACK) {
const sortPos = cameraNode.getPosition();
const sortDir = cameraNode.forward;
this._calculateSortDistances(instances, sortPos, sortDir);
}
instances.sort(sortCallbacks[sortMode]);
}
}
}
export {
CulledInstances,
Layer
};