playcanvas
Version:
PlayCanvas WebGL game engine
390 lines (387 loc) • 11.9 kB
JavaScript
import { hash32Fnv1a } from '../core/hash.js';
import { SORTMODE_MATERIALMESH, SORTMODE_BACK2FRONT, LIGHTTYPE_DIRECTIONAL, SORTMODE_NONE, SORTMODE_CUSTOM, SORTMODE_FRONT2BACK } from './constants.js';
import { Material } from './materials/material.js';
let layerCounter = 0;
const lightKeys = [];
const _tempMaterials = 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(){
this.opaque = [];
this.transparent = [];
}
}
class Layer {
constructor(options = {}){
this.meshInstances = [];
this.meshInstancesSet = new Set();
this.shadowCasters = [];
this.shadowCastersSet = new Set();
this._visibleInstances = new WeakMap();
this._lights = [];
this._lightsSet = new Set();
this._clusteredLightsSet = new Set();
this._splitLights = [
[],
[],
[]
];
this._splitLightsDirty = true;
this.requiresLightCube = false;
this.cameras = [];
this.camerasSet = new Set();
this._dirtyComposition = false;
if (options.id !== undefined) {
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;
if (options.renderTarget) {
this.renderTarget = options.renderTarget;
}
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();
}
this.customSortCallback = null;
this.customCalculateSortValues = null;
this._lightHash = 0;
this._lightHashDirty = false;
this._lightIdHash = 0;
this._lightIdHashDirty = false;
this.skipRenderAfter = Number.MAX_VALUE;
this._skipRenderCounter = 0;
this._renderTime = 0;
this._forwardDrawCalls = 0;
this._shadowDrawCalls = 0;
this._shaderVersion = -1;
}
set enabled(val) {
if (val !== this._enabled) {
this._dirtyComposition = true;
this._enabled = val;
if (val) {
this.incrementCounter();
if (this.onEnable) this.onEnable();
} else {
this.decrementCounter();
if (this.onDisable) this.onDisable();
}
}
}
get enabled() {
return this._enabled;
}
set clearColorBuffer(val) {
this._clearColorBuffer = val;
this._dirtyComposition = true;
}
get clearColorBuffer() {
return this._clearColorBuffer;
}
set clearDepthBuffer(val) {
this._clearDepthBuffer = val;
this._dirtyComposition = true;
}
get clearDepthBuffer() {
return this._clearDepthBuffer;
}
set clearStencilBuffer(val) {
this._clearStencilBuffer = val;
this._dirtyComposition = true;
}
get clearStencilBuffer() {
return this._clearStencilBuffer;
}
get hasClusteredLights() {
return this._clusteredLightsSet.size > 0;
}
get clusteredLightsSet() {
return this._clusteredLightsSet;
}
incrementCounter() {
if (this._refCounter === 0) {
this._enabled = true;
if (this.onEnable) this.onEnable();
}
this._refCounter++;
}
decrementCounter() {
if (this._refCounter === 1) {
this._enabled = false;
if (this.onDisable) this.onDisable();
} else if (this._refCounter === 0) {
return;
}
this._refCounter--;
}
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();
}
}
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);
}
}
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);
}
}
}
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);
}
}
}
}
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);
}
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);
}
}
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);
}
}
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;
}
getLightIdHash() {
if (this._lightIdHashDirty) {
this._lightIdHashDirty = false;
this._lightIdHash = this.evaluateLightHash(true, false, true);
}
return this._lightIdHash;
}
addCamera(camera) {
if (!this.camerasSet.has(camera.camera)) {
this.camerasSet.add(camera.camera);
this.cameras.push(camera);
this._dirtyComposition = true;
}
}
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;
}
}
clearCameras() {
this.cameras.length = 0;
this.camerasSet.clear();
this._dirtyComposition = true;
}
_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;
}
}
getCulledInstances(camera) {
let instances = this._visibleInstances.get(camera);
if (!instances) {
instances = new CulledInstances();
this._visibleInstances.set(camera, instances);
}
return instances;
}
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 };