UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

366 lines (365 loc) 15.2 kB
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 { LAYERID_SKYBOX, LAYERID_IMMEDIATE, TONEMAP_NONE, GAMMA_NONE } from "../../scene/constants.js"; import { ADDRESS_CLAMP_TO_EDGE, FILTER_LINEAR, PIXELFORMAT_RGBA8 } from "../../platform/graphics/constants.js"; import { Texture } from "../../platform/graphics/texture.js"; import { FramePass } from "../../platform/graphics/frame-pass.js"; import { FramePassColorGrab } from "../../scene/graphics/frame-pass-color-grab.js"; import { RenderPassForward } from "../../scene/renderer/render-pass-forward.js"; import { RenderTarget } from "../../platform/graphics/render-target.js"; import { FramePassBloom } from "./frame-pass-bloom.js"; import { RenderPassCompose } from "./render-pass-compose.js"; import { RenderPassTAA } from "./render-pass-taa.js"; import { FramePassDof } from "./frame-pass-dof.js"; import { RenderPassPrepass } from "./render-pass-prepass.js"; import { RenderPassSsao } from "./render-pass-ssao.js"; import { SSAOTYPE_COMBINE, SSAOTYPE_LIGHTING, SSAOTYPE_NONE } from "./constants.js"; import { Debug } from "../../core/debug.js"; import { RenderPassDownsample } from "./render-pass-downsample.js"; import { Color } from "../../core/math/color.js"; class CameraFrameOptions { constructor() { __publicField(this, "formats"); __publicField(this, "stencil", false); __publicField(this, "samples", 1); __publicField(this, "sceneColorMap", false); // skybox is the last layer rendered before the grab passes __publicField(this, "lastGrabLayerId", LAYERID_SKYBOX); __publicField(this, "lastGrabLayerIsTransparent", false); // immediate layer is the last layer rendered before the post-processing __publicField(this, "lastSceneLayerId", LAYERID_IMMEDIATE); __publicField(this, "lastSceneLayerIsTransparent", true); // TAA __publicField(this, "taaEnabled", false); // Bloom __publicField(this, "bloomEnabled", false); // SSAO __publicField(this, "ssaoType", SSAOTYPE_NONE); __publicField(this, "ssaoBlurEnabled", true); __publicField(this, "prepassEnabled", false); // DOF __publicField(this, "dofEnabled", false); __publicField(this, "dofNearBlur", false); __publicField(this, "dofHighQuality", true); } } const _defaultOptions = new CameraFrameOptions(); class FramePassCameraFrame extends FramePass { constructor(app, cameraFrame, cameraComponent, options = {}) { Debug.assert(app); super(app.graphicsDevice); __publicField(this, "app"); __publicField(this, "prePass"); __publicField(this, "scenePass"); __publicField(this, "composePass"); __publicField(this, "bloomPass"); __publicField(this, "ssaoPass"); __publicField(this, "taaPass"); __publicField(this, "scenePassHalf"); __publicField(this, "dofPass"); __publicField(this, "_renderTargetScale", 1); /** * True if the render pass needs to be re-created because layers have been added or removed. * * @ignore */ __publicField(this, "layersDirty", false); /** * The camera frame that this render pass belongs to. * * @type {CameraFrame} */ __publicField(this, "cameraFrame"); /** * @type {RenderTarget|null} * @private */ __publicField(this, "rt", null); this.app = app; this.cameraComponent = cameraComponent; this.cameraFrame = cameraFrame; this.options = this.sanitizeOptions(options); this.setupRenderPasses(this.options); } destroy() { this.reset(); } reset() { this.sceneTexture = null; this.sceneTextureHalf = null; if (this.rt) { this.rt.destroyTextureBuffers(); this.rt.destroy(); this.rt = null; } if (this.rtHalf) { this.rtHalf.destroyTextureBuffers(); this.rtHalf.destroy(); this.rtHalf = null; } this.beforePasses.forEach((pass) => pass.destroy()); this.beforePasses.length = 0; this.prePass = null; this.scenePass = null; this.scenePassTransparent = null; this.colorGrabPass = null; this.composePass = null; this.bloomPass = null; this.ssaoPass = null; this.taaPass = null; this.afterPass = null; this.scenePassHalf = null; this.dofPass = null; } sanitizeOptions(options) { options = Object.assign({}, _defaultOptions, options); if (options.taaEnabled || options.ssaoType !== SSAOTYPE_NONE || options.dofEnabled) { options.prepassEnabled = true; } return options; } set renderTargetScale(value) { this._renderTargetScale = value; if (this.scenePass) { this.scenePass.scaleX = value; this.scenePass.scaleY = value; } } get renderTargetScale() { return this._renderTargetScale; } needsReset(options) { const currentOptions = this.options; const arraysNotEqual = (arr1, arr2) => arr1 !== arr2 && (!(Array.isArray(arr1) && Array.isArray(arr2)) || arr1.length !== arr2.length || !arr1.every((value, index) => value === arr2[index])); return options.ssaoType !== currentOptions.ssaoType || options.ssaoBlurEnabled !== currentOptions.ssaoBlurEnabled || options.taaEnabled !== currentOptions.taaEnabled || options.samples !== currentOptions.samples || options.stencil !== currentOptions.stencil || options.bloomEnabled !== currentOptions.bloomEnabled || options.prepassEnabled !== currentOptions.prepassEnabled || options.sceneColorMap !== currentOptions.sceneColorMap || options.dofEnabled !== currentOptions.dofEnabled || options.dofNearBlur !== currentOptions.dofNearBlur || options.dofHighQuality !== currentOptions.dofHighQuality || arraysNotEqual(options.formats, currentOptions.formats); } // manually called, applies changes update(options) { options = this.sanitizeOptions(options); if (this.needsReset(options) || this.layersDirty) { this.layersDirty = false; this.reset(); } this.options = options; if (!this.sceneTexture) { this.setupRenderPasses(this.options); } } createRenderTarget(name, depth, stencil, samples, flipY) { const texture = new Texture(this.device, { name, width: 4, height: 4, format: this.hdrFormat, mipmaps: false, minFilter: FILTER_LINEAR, magFilter: FILTER_LINEAR, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE }); return new RenderTarget({ colorBuffer: texture, depth, stencil, samples, flipY }); } setupRenderPasses(options) { const { device } = this; const cameraComponent = this.cameraComponent; const targetRenderTarget = cameraComponent.renderTarget; this.hdrFormat = device.getRenderableHdrFormat(options.formats, true, options.samples) || PIXELFORMAT_RGBA8; this._bloomEnabled = options.bloomEnabled && this.hdrFormat !== PIXELFORMAT_RGBA8; this._sceneHalfEnabled = this._bloomEnabled || options.dofEnabled; cameraComponent.shaderParams.ssaoEnabled = options.ssaoType === SSAOTYPE_LIGHTING; const flipY = !!targetRenderTarget?.flipY; this.rt = this.createRenderTarget("SceneColor", true, options.stencil, options.samples, flipY); this.sceneTexture = this.rt.colorBuffer; if (this._sceneHalfEnabled) { this.rtHalf = this.createRenderTarget("SceneColorHalf", false, false, 1, flipY); this.sceneTextureHalf = this.rtHalf.colorBuffer; } this.sceneOptions = { resizeSource: targetRenderTarget, scaleX: this.renderTargetScale, scaleY: this.renderTargetScale }; this.createPasses(options); const allPasses = this.collectPasses(); this.beforePasses = allPasses.filter((element) => element !== void 0 && element !== null); this.updateCameraUseFlags(); } /** * Scan all RenderPassForward instances in the pass chain and mark the first / last * render action per camera with firstCameraUse / lastCameraUse. This mirrors what * LayerComposition does for the non-CameraFrame path and ensures that beforePasses * collection and EVENT_PRERENDER / EVENT_POSTRENDER fire exactly once per camera. * * @private */ updateCameraUseFlags() { const firstSeen = /* @__PURE__ */ new Map(); const lastSeen = /* @__PURE__ */ new Map(); for (let i = 0; i < this.beforePasses.length; i++) { const pass = this.beforePasses[i]; if (pass instanceof RenderPassForward) { const actions = pass.renderActions; for (let j = 0; j < actions.length; j++) { const ra = actions[j]; const cam = ra.camera; if (cam) { if (!firstSeen.has(cam)) { firstSeen.set(cam, ra); } lastSeen.set(cam, ra); } } } } firstSeen.forEach((ra) => { ra.firstCameraUse = true; }); lastSeen.forEach((ra) => { ra.lastCameraUse = true; }); } collectPasses() { return [this.prePass, this.ssaoPass, this.scenePass, this.colorGrabPass, this.scenePassTransparent, this.taaPass, this.scenePassHalf, this.bloomPass, this.dofPass, this.composePass, this.afterPass]; } createPasses(options) { this.setupScenePrepass(options); this.setupSsaoPass(options); const scenePassesInfo = this.setupScenePass(options); const sceneTextureWithTaa = this.setupTaaPass(options); this.setupSceneHalfPass(options, sceneTextureWithTaa); this.setupBloomPass(options, this.sceneTextureHalf); this.setupDofPass(options, this.sceneTexture, this.sceneTextureHalf); this.setupComposePass(options); this.setupAfterPass(options, scenePassesInfo); } setupScenePrepass(options) { if (options.prepassEnabled) { const { app, device, cameraComponent } = this; const { scene, renderer } = app; this.prePass = new RenderPassPrepass(device, scene, renderer, cameraComponent, this.sceneOptions); } } setupScenePassSettings(pass) { pass.gammaCorrection = GAMMA_NONE; pass.toneMapping = TONEMAP_NONE; } setupScenePass(options) { const { app, device, cameraComponent } = this; const { scene, renderer } = app; const composition = scene.layers; this.scenePass = new RenderPassForward(device, composition, scene, renderer); this.setupScenePassSettings(this.scenePass); this.scenePass.init(this.rt, this.sceneOptions); const lastLayerId = options.sceneColorMap ? options.lastGrabLayerId : options.lastSceneLayerId; const lastLayerIsTransparent = options.sceneColorMap ? options.lastGrabLayerIsTransparent : options.lastSceneLayerIsTransparent; const ret = { lastAddedIndex: 0, // the last layer index added to the scene pass clearRenderTarget: true // true if the render target should be cleared }; ret.lastAddedIndex = this.scenePass.addLayers(composition, cameraComponent, ret.lastAddedIndex, ret.clearRenderTarget, lastLayerId, lastLayerIsTransparent); ret.clearRenderTarget = false; if (options.sceneColorMap) { this.colorGrabPass = new FramePassColorGrab(device); this.colorGrabPass.source = this.rt; this.scenePassTransparent = new RenderPassForward(device, composition, scene, renderer); this.setupScenePassSettings(this.scenePassTransparent); this.scenePassTransparent.init(this.rt); ret.lastAddedIndex = this.scenePassTransparent.addLayers(composition, cameraComponent, ret.lastAddedIndex, ret.clearRenderTarget, options.lastSceneLayerId, options.lastSceneLayerIsTransparent); if (!this.scenePassTransparent.rendersAnything) { this.scenePassTransparent.destroy(); this.scenePassTransparent = null; } if (this.scenePassTransparent) { if (options.prepassEnabled) { this.scenePassTransparent.depthStencilOps.storeDepth = true; } } } return ret; } setupSsaoPass(options) { const { ssaoBlurEnabled, ssaoType } = options; const { device, cameraComponent } = this; if (ssaoType !== SSAOTYPE_NONE) { this.ssaoPass = new RenderPassSsao(device, this.sceneTexture, cameraComponent, ssaoBlurEnabled); } } setupSceneHalfPass(options, sourceTexture) { if (this._sceneHalfEnabled) { this.scenePassHalf = new RenderPassDownsample(this.device, this.sceneTexture, { boxFilter: true, removeInvalid: true // remove invalid pixels to avoid bloom / dof artifacts }); this.scenePassHalf.name = "RenderPassSceneHalf"; this.scenePassHalf.init(this.rtHalf, { resizeSource: sourceTexture, scaleX: 0.5, scaleY: 0.5 }); this.scenePassHalf.setClearColor(Color.BLACK); } } setupBloomPass(options, inputTexture) { if (this._bloomEnabled) { this.bloomPass = new FramePassBloom(this.device, inputTexture, this.hdrFormat); } } setupDofPass(options, inputTexture, inputTextureHalf) { if (options.dofEnabled) { this.dofPass = new FramePassDof(this.device, this.cameraComponent, inputTexture, inputTextureHalf, options.dofHighQuality, options.dofNearBlur); } } setupTaaPass(options) { let textureWithTaa = this.sceneTexture; if (options.taaEnabled) { this.taaPass = new RenderPassTAA(this.device, this.sceneTexture, this.cameraComponent); textureWithTaa = this.taaPass.historyTexture; } return textureWithTaa; } setupComposePass(options) { this.composePass = new RenderPassCompose(this.device); this.composePass.bloomTexture = this.bloomPass?.bloomTexture; this.composePass.hdrScene = this.hdrFormat !== PIXELFORMAT_RGBA8; this.composePass.taaEnabled = options.taaEnabled; this.composePass.cocTexture = this.dofPass?.cocTexture; this.composePass.blurTexture = this.dofPass?.blurTexture; this.composePass.blurTextureUpscale = !this.dofPass?.highQuality; const cameraComponent = this.cameraComponent; const targetRenderTarget = cameraComponent.renderTarget; this.composePass.init(targetRenderTarget); this.composePass.ssaoTexture = options.ssaoType === SSAOTYPE_COMBINE ? this.ssaoPass.ssaoTexture : null; } setupAfterPass(options, scenePassesInfo) { const { app, cameraComponent } = this; const { scene, renderer } = app; const composition = scene.layers; const targetRenderTarget = cameraComponent.renderTarget; this.afterPass = new RenderPassForward(this.device, composition, scene, renderer); this.afterPass.init(targetRenderTarget); this.afterPass.addLayers(composition, cameraComponent, scenePassesInfo.lastAddedIndex, scenePassesInfo.clearRenderTarget); } frameUpdate() { if (this.layersDirty) { this.cameraFrame.update(); } super.frameUpdate(); const sceneTexture = this.taaPass?.update() ?? this.rt.colorBuffer; this.composePass.sceneTexture = sceneTexture; this.scenePassHalf?.setSourceTexture(sceneTexture); } } export { CameraFrameOptions, FramePassCameraFrame };