UNPKG

playcanvas

Version:

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

337 lines (336 loc) 12.7 kB
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 { RenderPassDownsample } from "./render-pass-downsample.js"; import { Color } from "../../core/math/color.js"; class CameraFrameOptions { formats; stencil = false; samples = 1; sceneColorMap = false; // skybox is the last layer rendered before the grab passes lastGrabLayerId = LAYERID_SKYBOX; lastGrabLayerIsTransparent = false; // immediate layer is the last layer rendered before the post-processing lastSceneLayerId = LAYERID_IMMEDIATE; lastSceneLayerIsTransparent = true; // TAA taaEnabled = false; // Bloom bloomEnabled = false; // SSAO ssaoType = SSAOTYPE_NONE; ssaoBlurEnabled = true; prepassEnabled = false; // DOF dofEnabled = false; dofNearBlur = false; dofHighQuality = true; } const _defaultOptions = new CameraFrameOptions(); class FramePassCameraFrame extends FramePass { app; prePass; scenePass; composePass; bloomPass; ssaoPass; taaPass; scenePassHalf; dofPass; _renderTargetScale = 1; layersDirty = false; cameraFrame; rt = null; constructor(app, cameraFrame, cameraComponent, options = {}) { super(app.graphicsDevice); 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(); } 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 };