playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
337 lines (336 loc) • 12.7 kB
JavaScript
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
};