UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

606 lines (603 loc) 27.2 kB
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 };