playcanvas
Version:
PlayCanvas WebGL game engine
606 lines (603 loc) • 27.2 kB
JavaScript
import { TRACEID_RENDER_ACTION } from '../../core/constants.js';
import { Debug } from '../../core/debug.js';
import { Tracing } from '../../core/tracing.js';
import { EventHandler } from '../../core/event-handler.js';
import { sortPriority } from '../../core/sort.js';
import { LAYERID_DEPTH } from '../constants.js';
import { RenderAction } from './render-action.js';
/**
* @import { CameraComponent } from '../../framework/components/camera/component.js'
* @import { Layer } from '../layer.js'
*/ /**
* Layer Composition is a collection of {@link Layer} that is fed to {@link Scene#layers} to define
* rendering order.
*
* @category Graphics
*/ class LayerComposition extends EventHandler {
destroy() {
this.destroyRenderActions();
}
destroyRenderActions() {
this._renderActions.forEach((ra)=>ra.destroy());
this._renderActions.length = 0;
}
_update() {
var len = this.layerList.length;
// if composition dirty flag is not set, test if layers are marked dirty
if (!this._dirty) {
for(var i = 0; i < len; i++){
if (this.layerList[i]._dirtyComposition) {
this._dirty = true;
break;
}
}
}
if (this._dirty) {
this._dirty = false;
// walk the layers and build an array of unique cameras from all layers
this.cameras.length = 0;
for(var i1 = 0; i1 < len; i1++){
var layer = this.layerList[i1];
layer._dirtyComposition = false;
// for all cameras in the layer
for(var j = 0; j < layer.cameras.length; j++){
var camera = layer.cameras[j];
var index = this.cameras.indexOf(camera);
if (index < 0) {
this.cameras.push(camera);
}
}
}
// sort cameras by priority
if (this.cameras.length > 1) {
sortPriority(this.cameras);
}
// collect a list of layers this camera renders
var cameraLayers = [];
// render in order of cameras sorted by priority
var renderActionCount = 0;
this.destroyRenderActions();
for(var i2 = 0; i2 < this.cameras.length; i2++){
var camera1 = this.cameras[i2];
cameraLayers.length = 0;
// if the camera uses custom render passes, only add a dummy render action to mark
// the place where to add them during building of the frame graph
if (camera1.camera.renderPasses.length > 0) {
this.addDummyRenderAction(renderActionCount, camera1);
renderActionCount++;
continue;
}
// first render action for this camera
var cameraFirstRenderAction = true;
var cameraFirstRenderActionIndex = renderActionCount;
// last render action for the camera
var lastRenderAction = null;
// true if post processing stop layer was found for the camera
var postProcessMarked = false;
// walk all global sorted list of layers (sublayers) to check if camera renders it
// this adds both opaque and transparent sublayers if camera renders the layer
for(var j1 = 0; j1 < len; j1++){
var layer1 = this.layerList[j1];
var isLayerEnabled = layer1.enabled && this.subLayerEnabled[j1];
if (isLayerEnabled) {
// if layer needs to be rendered
if (layer1.cameras.length > 0) {
// if the camera renders this layer
if (camera1.layers.indexOf(layer1.id) >= 0) {
cameraLayers.push(layer1);
// if this layer is the stop layer for postprocessing
if (!postProcessMarked && layer1.id === camera1.disablePostEffectsLayer) {
postProcessMarked = true;
// the previously added render action is the last post-processed layer
if (lastRenderAction) {
// mark it to trigger postprocessing callback
lastRenderAction.triggerPostprocess = true;
}
}
// add render action to describe rendering step
var isTransparent = this.subLayerList[j1];
lastRenderAction = this.addRenderAction(renderActionCount, layer1, isTransparent, camera1, cameraFirstRenderAction, postProcessMarked);
renderActionCount++;
cameraFirstRenderAction = false;
}
}
}
}
// if the camera renders any layers.
if (cameraFirstRenderActionIndex < renderActionCount) {
// mark the last render action as last one using the camera
lastRenderAction.lastCameraUse = true;
}
// if no render action for this camera was marked for end of postprocessing, mark last one
if (!postProcessMarked && lastRenderAction) {
lastRenderAction.triggerPostprocess = true;
}
// handle camera stacking if this render action has postprocessing enabled
if (camera1.renderTarget && camera1.postEffectsEnabled) {
// process previous render actions starting with previous camera
this.propagateRenderTarget(cameraFirstRenderActionIndex - 1, camera1);
}
}
this._logRenderActions();
}
}
getNextRenderAction(renderActionIndex) {
Debug.assert(this._renderActions.length === renderActionIndex);
var renderAction = new RenderAction();
this._renderActions.push(renderAction);
return renderAction;
}
addDummyRenderAction(renderActionIndex, camera) {
var renderAction = this.getNextRenderAction(renderActionIndex);
renderAction.camera = camera;
renderAction.useCameraPasses = true;
}
// function adds new render action to a list, while trying to limit allocation and reuse already allocated objects
addRenderAction(renderActionIndex, layer, isTransparent, camera, cameraFirstRenderAction, postProcessMarked) {
// camera's render target, ignoring depth layer
var rt = layer.id !== LAYERID_DEPTH ? camera.renderTarget : null;
// was camera and render target combo used already
var used = false;
var renderActions = this._renderActions;
for(var i = renderActionIndex - 1; i >= 0; i--){
if (renderActions[i].camera === camera && renderActions[i].renderTarget === rt) {
used = true;
break;
}
}
// for cameras with post processing enabled, on layers after post processing has been applied already (so UI and similar),
// don't render them to render target anymore
if (postProcessMarked && camera.postEffectsEnabled) {
rt = null;
}
// store the properties
var renderAction = this.getNextRenderAction(renderActionIndex);
renderAction.triggerPostprocess = false;
renderAction.layer = layer;
renderAction.transparent = isTransparent;
renderAction.camera = camera;
renderAction.renderTarget = rt;
renderAction.firstCameraUse = cameraFirstRenderAction;
renderAction.lastCameraUse = false;
// clear flags - use camera clear flags in the first render action for each camera,
// or when render target (from layer) was not yet cleared by this camera
var needsCameraClear = cameraFirstRenderAction || !used;
var needsLayerClear = layer.clearColorBuffer || layer.clearDepthBuffer || layer.clearStencilBuffer;
if (needsCameraClear || needsLayerClear) {
renderAction.setupClears(needsCameraClear ? camera : undefined, layer);
}
return renderAction;
}
// executes when post-processing camera's render actions were created to propagate rendering to
// render targets to previous camera as needed
propagateRenderTarget(startIndex, fromCamera) {
for(var a = startIndex; a >= 0; a--){
var ra = this._renderActions[a];
var layer = ra.layer;
// if we hit render action with a render target (other than depth layer), that marks the end of camera stack
// TODO: refactor this as part of depth layer refactoring
if (ra.renderTarget && layer.id !== LAYERID_DEPTH) {
break;
}
// skip over depth layer
if (layer.id === LAYERID_DEPTH) {
continue;
}
// end of stacking if camera with custom render passes
if (ra.useCameraPasses) {
break;
}
// camera stack ends when viewport or scissor of the camera changes
var thisCamera = ra == null ? undefined : ra.camera.camera;
if (thisCamera) {
if (!fromCamera.camera.rect.equals(thisCamera.rect) || !fromCamera.camera.scissorRect.equals(thisCamera.scissorRect)) {
break;
}
}
// render it to render target
ra.renderTarget = fromCamera.renderTarget;
}
}
// logs render action and their properties
_logRenderActions() {
if (Tracing.get(TRACEID_RENDER_ACTION)) {
Debug.trace(TRACEID_RENDER_ACTION, "Render Actions for composition: " + this.name);
for(var i = 0; i < this._renderActions.length; i++){
var ra = this._renderActions[i];
var camera = ra.camera;
if (ra.useCameraPasses) {
Debug.trace(TRACEID_RENDER_ACTION, i + "CustomPasses Cam: " + (camera ? camera.entity.name : '-'));
} else {
var layer = ra.layer;
var enabled = layer.enabled && this.isEnabled(layer, ra.transparent);
var clear = (ra.clearColor ? 'Color ' : '..... ') + (ra.clearDepth ? 'Depth ' : '..... ') + (ra.clearStencil ? 'Stencil' : '.......');
Debug.trace(TRACEID_RENDER_ACTION, i + (" Cam: " + (camera ? camera.entity.name : '-')).padEnd(22, ' ') + (" Lay: " + layer.name).padEnd(22, ' ') + (ra.transparent ? ' TRANSP' : ' OPAQUE') + (enabled ? ' ENABLED ' : ' DISABLED') + (" RT: " + (ra.renderTarget ? ra.renderTarget.name : '-')).padEnd(30, ' ') + " Clear: " + clear + (ra.firstCameraUse ? ' CAM-FIRST' : '') + (ra.lastCameraUse ? ' CAM-LAST' : '') + (ra.triggerPostprocess ? ' POSTPROCESS' : ''));
}
}
}
}
_isLayerAdded(layer) {
var found = this.layerIdMap.get(layer.id) === layer;
Debug.assert(!found, "Layer is already added: " + layer.name);
return found;
}
_isSublayerAdded(layer, transparent) {
var map = transparent ? this.layerTransparentIndexMap : this.layerOpaqueIndexMap;
if (map.get(layer) !== undefined) {
Debug.error("Sublayer " + layer.name + ", transparent: " + transparent + " is already added.");
return true;
}
return false;
}
// Whole layer API
/**
* Adds a layer (both opaque and semi-transparent parts) to the end of the {@link LayerComposition#layerList}.
*
* @param {Layer} layer - A {@link Layer} to add.
*/ push(layer) {
// add both opaque and transparent to the end of the array
if (this._isLayerAdded(layer)) return;
this.layerList.push(layer);
this.layerList.push(layer);
this._opaqueOrder[layer.id] = this.subLayerList.push(false) - 1;
this._transparentOrder[layer.id] = this.subLayerList.push(true) - 1;
this.subLayerEnabled.push(true);
this.subLayerEnabled.push(true);
this._updateLayerMaps();
this._dirty = true;
this.fire('add', layer);
}
/**
* Inserts a layer (both opaque and semi-transparent parts) at the chosen index in the
* {@link LayerComposition#layerList}.
*
* @param {Layer} layer - A {@link Layer} to add.
* @param {number} index - Insertion position.
*/ insert(layer, index) {
// insert both opaque and transparent at the index
if (this._isLayerAdded(layer)) return;
this.layerList.splice(index, 0, layer, layer);
this.subLayerList.splice(index, 0, false, true);
var count = this.layerList.length;
this._updateOpaqueOrder(index, count - 1);
this._updateTransparentOrder(index, count - 1);
this.subLayerEnabled.splice(index, 0, true, true);
this._updateLayerMaps();
this._dirty = true;
this.fire('add', layer);
}
/**
* Removes a layer (both opaque and semi-transparent parts) from {@link LayerComposition#layerList}.
*
* @param {Layer} layer - A {@link Layer} to remove.
*/ remove(layer) {
// remove all occurrences of a layer
var id = this.layerList.indexOf(layer);
delete this._opaqueOrder[id];
delete this._transparentOrder[id];
while(id >= 0){
this.layerList.splice(id, 1);
this.subLayerList.splice(id, 1);
this.subLayerEnabled.splice(id, 1);
id = this.layerList.indexOf(layer);
this._dirty = true;
this.fire('remove', layer);
}
// update both orders
var count = this.layerList.length;
this._updateOpaqueOrder(0, count - 1);
this._updateTransparentOrder(0, count - 1);
this._updateLayerMaps();
}
// Sublayer API
/**
* Adds part of the layer with opaque (non semi-transparent) objects to the end of the
* {@link LayerComposition#layerList}.
*
* @param {Layer} layer - A {@link Layer} to add.
*/ pushOpaque(layer) {
// add opaque to the end of the array
if (this._isSublayerAdded(layer, false)) return;
this.layerList.push(layer);
this._opaqueOrder[layer.id] = this.subLayerList.push(false) - 1;
this.subLayerEnabled.push(true);
this._updateLayerMaps();
this._dirty = true;
this.fire('add', layer);
}
/**
* Inserts an opaque part of the layer (non semi-transparent mesh instances) at the chosen
* index in the {@link LayerComposition#layerList}.
*
* @param {Layer} layer - A {@link Layer} to add.
* @param {number} index - Insertion position.
*/ insertOpaque(layer, index) {
// insert opaque at index
if (this._isSublayerAdded(layer, false)) return;
this.layerList.splice(index, 0, layer);
this.subLayerList.splice(index, 0, false);
var count = this.subLayerList.length;
this._updateOpaqueOrder(index, count - 1);
this.subLayerEnabled.splice(index, 0, true);
this._updateLayerMaps();
this._dirty = true;
this.fire('add', layer);
}
/**
* Removes an opaque part of the layer (non semi-transparent mesh instances) from
* {@link LayerComposition#layerList}.
*
* @param {Layer} layer - A {@link Layer} to remove.
*/ removeOpaque(layer) {
// remove opaque occurrences of a layer
for(var i = 0, len = this.layerList.length; i < len; i++){
if (this.layerList[i] === layer && !this.subLayerList[i]) {
this.layerList.splice(i, 1);
this.subLayerList.splice(i, 1);
len--;
this._updateOpaqueOrder(i, len - 1);
this.subLayerEnabled.splice(i, 1);
this._dirty = true;
if (this.layerList.indexOf(layer) < 0) {
this.fire('remove', layer); // no sublayers left
}
break;
}
}
this._updateLayerMaps();
}
/**
* Adds part of the layer with semi-transparent objects to the end of the {@link LayerComposition#layerList}.
*
* @param {Layer} layer - A {@link Layer} to add.
*/ pushTransparent(layer) {
// add transparent to the end of the array
if (this._isSublayerAdded(layer, true)) return;
this.layerList.push(layer);
this._transparentOrder[layer.id] = this.subLayerList.push(true) - 1;
this.subLayerEnabled.push(true);
this._updateLayerMaps();
this._dirty = true;
this.fire('add', layer);
}
/**
* Inserts a semi-transparent part of the layer at the chosen index in the {@link LayerComposition#layerList}.
*
* @param {Layer} layer - A {@link Layer} to add.
* @param {number} index - Insertion position.
*/ insertTransparent(layer, index) {
// insert transparent at index
if (this._isSublayerAdded(layer, true)) return;
this.layerList.splice(index, 0, layer);
this.subLayerList.splice(index, 0, true);
var count = this.subLayerList.length;
this._updateTransparentOrder(index, count - 1);
this.subLayerEnabled.splice(index, 0, true);
this._updateLayerMaps();
this._dirty = true;
this.fire('add', layer);
}
/**
* Removes a transparent part of the layer from {@link LayerComposition#layerList}.
*
* @param {Layer} layer - A {@link Layer} to remove.
*/ removeTransparent(layer) {
// remove transparent occurrences of a layer
for(var i = 0, len = this.layerList.length; i < len; i++){
if (this.layerList[i] === layer && this.subLayerList[i]) {
this.layerList.splice(i, 1);
this.subLayerList.splice(i, 1);
len--;
this._updateTransparentOrder(i, len - 1);
this.subLayerEnabled.splice(i, 1);
this._dirty = true;
if (this.layerList.indexOf(layer) < 0) {
this.fire('remove', layer); // no sublayers left
}
break;
}
}
this._updateLayerMaps();
}
/**
* Gets index of the opaque part of the supplied layer in the {@link LayerComposition#layerList}.
*
* @param {Layer} layer - A {@link Layer} to find index of.
* @returns {number} The index of the opaque part of the specified layer, or -1 if it is not
* part of the composition.
*/ getOpaqueIndex(layer) {
var _this_layerOpaqueIndexMap_get;
return (_this_layerOpaqueIndexMap_get = this.layerOpaqueIndexMap.get(layer)) != null ? _this_layerOpaqueIndexMap_get : -1;
}
/**
* Gets index of the semi-transparent part of the supplied layer in the {@link LayerComposition#layerList}.
*
* @param {Layer} layer - A {@link Layer} to find index of.
* @returns {number} The index of the semi-transparent part of the specified layer, or -1 if it
* is not part of the composition.
*/ getTransparentIndex(layer) {
var _this_layerTransparentIndexMap_get;
return (_this_layerTransparentIndexMap_get = this.layerTransparentIndexMap.get(layer)) != null ? _this_layerTransparentIndexMap_get : -1;
}
isEnabled(layer, transparent) {
if (layer.enabled) {
var index = transparent ? this.getTransparentIndex(layer) : this.getOpaqueIndex(layer);
if (index >= 0) {
return this.subLayerEnabled[index];
}
}
return false;
}
/**
* Update maps of layer IDs and names to match the layer list.
*
* @private
*/ _updateLayerMaps() {
this.layerIdMap.clear();
this.layerNameMap.clear();
this.layerOpaqueIndexMap.clear();
this.layerTransparentIndexMap.clear();
for(var i = 0; i < this.layerList.length; i++){
var layer = this.layerList[i];
this.layerIdMap.set(layer.id, layer);
this.layerNameMap.set(layer.name, layer);
var subLayerIndexMap = this.subLayerList[i] ? this.layerTransparentIndexMap : this.layerOpaqueIndexMap;
subLayerIndexMap.set(layer, i);
}
}
/**
* Finds a layer inside this composition by its ID. Null is returned, if nothing is found.
*
* @param {number} id - An ID of the layer to find.
* @returns {Layer|null} The layer corresponding to the specified ID. Returns null if layer is
* not found.
*/ getLayerById(id) {
var _this_layerIdMap_get;
return (_this_layerIdMap_get = this.layerIdMap.get(id)) != null ? _this_layerIdMap_get : null;
}
/**
* Finds a layer inside this composition by its name. Null is returned, if nothing is found.
*
* @param {string} name - The name of the layer to find.
* @returns {Layer|null} The layer corresponding to the specified name. Returns null if layer
* is not found.
*/ getLayerByName(name) {
var _this_layerNameMap_get;
return (_this_layerNameMap_get = this.layerNameMap.get(name)) != null ? _this_layerNameMap_get : null;
}
_updateOpaqueOrder(startIndex, endIndex) {
for(var i = startIndex; i <= endIndex; i++){
if (this.subLayerList[i] === false) {
this._opaqueOrder[this.layerList[i].id] = i;
}
}
}
_updateTransparentOrder(startIndex, endIndex) {
for(var i = startIndex; i <= endIndex; i++){
if (this.subLayerList[i] === true) {
this._transparentOrder[this.layerList[i].id] = i;
}
}
}
// Used to determine which array of layers has any sublayer that is
// on top of all the sublayers in the other array. The order is a dictionary
// of <layerId, index>.
_sortLayersDescending(layersA, layersB, order) {
var topLayerA = -1;
var topLayerB = -1;
// search for which layer is on top in layersA
for(var i = 0, len = layersA.length; i < len; i++){
var id = layersA[i];
if (order.hasOwnProperty(id)) {
topLayerA = Math.max(topLayerA, order[id]);
}
}
// search for which layer is on top in layersB
for(var i1 = 0, len1 = layersB.length; i1 < len1; i1++){
var id1 = layersB[i1];
if (order.hasOwnProperty(id1)) {
topLayerB = Math.max(topLayerB, order[id1]);
}
}
// if the layers of layersA or layersB do not exist at all
// in the composition then return early with the other.
if (topLayerA === -1 && topLayerB !== -1) {
return 1;
} else if (topLayerB === -1 && topLayerA !== -1) {
return -1;
}
// sort in descending order since we want
// the higher order to be first
return topLayerB - topLayerA;
}
/**
* Used to determine which array of layers has any transparent sublayer that is on top of all
* the transparent sublayers in the other array.
*
* @param {number[]} layersA - IDs of layers.
* @param {number[]} layersB - IDs of layers.
* @returns {number} Returns a negative number if any of the transparent sublayers in layersA
* is on top of all the transparent sublayers in layersB, or a positive number if any of the
* transparent sublayers in layersB is on top of all the transparent sublayers in layersA, or 0
* otherwise.
* @private
*/ sortTransparentLayers(layersA, layersB) {
return this._sortLayersDescending(layersA, layersB, this._transparentOrder);
}
/**
* Used to determine which array of layers has any opaque sublayer that is on top of all the
* opaque sublayers in the other array.
*
* @param {number[]} layersA - IDs of layers.
* @param {number[]} layersB - IDs of layers.
* @returns {number} Returns a negative number if any of the opaque sublayers in layersA is on
* top of all the opaque sublayers in layersB, or a positive number if any of the opaque
* sublayers in layersB is on top of all the opaque sublayers in layersA, or 0 otherwise.
* @private
*/ sortOpaqueLayers(layersA, layersB) {
return this._sortLayersDescending(layersA, layersB, this._opaqueOrder);
}
/**
* Create a new layer composition.
*
* @param {string} [name] - Optional non-unique name of the layer composition. Defaults to
* "Untitled" if not specified.
*/ constructor(name = 'Untitled'){
super(), // Composition can hold only 2 sublayers of each layer
/**
* A read-only array of {@link Layer} sorted in the order they will be rendered.
*
* @type {Layer[]}
*/ this.layerList = [], /**
* A mapping of {@link Layer#id} to {@link Layer}.
*
* @type {Map<number, Layer>}
* @ignore
*/ this.layerIdMap = new Map(), /**
* A mapping of {@link Layer#name} to {@link Layer}.
*
* @type {Map<string, Layer>}
* @ignore
*/ this.layerNameMap = new Map(), /**
* A mapping of {@link Layer} to its opaque index in {@link LayerComposition#layerList}.
*
* @type {Map<Layer, number>}
* @ignore
*/ this.layerOpaqueIndexMap = new Map(), /**
* A mapping of {@link Layer} to its transparent index in {@link LayerComposition#layerList}.
*
* @type {Map<Layer, number>}
* @ignore
*/ this.layerTransparentIndexMap = new Map(), /**
* A read-only array of boolean values, matching {@link LayerComposition#layerList}. True means only
* semi-transparent objects are rendered, and false means opaque.
*
* @type {boolean[]}
* @ignore
*/ this.subLayerList = [], /**
* A read-only array of boolean values, matching {@link LayerComposition#layerList}. True means the
* layer is rendered, false means it's skipped.
*
* @type {boolean[]}
*/ this.subLayerEnabled = [] // more granular control on top of layer.enabled (ANDed)
, /**
* An array of {@link CameraComponent}s.
*
* @type {CameraComponent[]}
* @ignore
*/ this.cameras = [], /**
* The actual rendering sequence, generated based on layers and cameras
*
* @type {RenderAction[]}
* @ignore
*/ this._renderActions = [], /**
* True if the composition needs to be updated before rendering.
*
* @ignore
*/ this._dirty = false;
this.name = name;
this._opaqueOrder = {};
this._transparentOrder = {};
}
}
export { LayerComposition };