playcanvas
Version:
PlayCanvas WebGL game engine
234 lines (231 loc) • 7.89 kB
JavaScript
import { ADDRESS_CLAMP_TO_EDGE, FILTER_NEAREST, PIXELFORMAT_RGBA16F, PIXELFORMAT_RGBA32F, PIXELFORMAT_SRGBA8, PIXELFORMAT_RGBA8 } from '../../../platform/graphics/constants.js';
import { RenderTarget } from '../../../platform/graphics/render-target.js';
import { Texture } from '../../../platform/graphics/texture.js';
import { LAYERID_DEPTH } from '../../../scene/constants.js';
class PostEffectEntry {
constructor(effect, inputTarget){
this.effect = effect;
this.inputTarget = inputTarget;
this.outputTarget = null;
this.name = effect.constructor.name;
}
}
class PostEffectQueue {
constructor(app, camera){
this.app = app;
this.camera = camera;
this.destinationRenderTarget = null;
this.effects = [];
this.enabled = false;
this.depthTarget = null;
camera.on('set:rect', this.onCameraRectChanged, this);
}
_allocateColorBuffer(format, name) {
const rect = this.camera.rect;
const renderTarget = this.destinationRenderTarget;
const device = this.app.graphicsDevice;
const width = Math.floor(rect.z * (renderTarget?.width ?? device.width));
const height = Math.floor(rect.w * (renderTarget?.height ?? device.height));
const colorBuffer = new Texture(device, {
name: name,
format: format,
width: width,
height: height,
mipmaps: false,
minFilter: FILTER_NEAREST,
magFilter: FILTER_NEAREST,
addressU: ADDRESS_CLAMP_TO_EDGE,
addressV: ADDRESS_CLAMP_TO_EDGE
});
return colorBuffer;
}
_createOffscreenTarget(useDepth, hdr) {
const device = this.app.graphicsDevice;
const outputRt = this.destinationRenderTarget ?? device.backBuffer;
const srgb = outputRt.isColorBufferSrgb(0);
const format = (hdr && device.getRenderableHdrFormat([
PIXELFORMAT_RGBA16F,
PIXELFORMAT_RGBA32F
], true)) ?? (srgb ? PIXELFORMAT_SRGBA8 : PIXELFORMAT_RGBA8);
const name = `${this.camera.entity.name}-posteffect-${this.effects.length}`;
const colorBuffer = this._allocateColorBuffer(format, name);
return new RenderTarget({
colorBuffer: colorBuffer,
depth: useDepth,
stencil: useDepth && this.app.graphicsDevice.supportsStencil,
samples: useDepth ? device.samples : 1
});
}
_resizeOffscreenTarget(rt) {
const format = rt.colorBuffer.format;
const name = rt.colorBuffer.name;
rt.destroyFrameBuffers();
rt.destroyTextureBuffers();
rt._colorBuffer = this._allocateColorBuffer(format, name);
rt._colorBuffers = [
rt._colorBuffer
];
}
_destroyOffscreenTarget(rt) {
rt.destroyTextureBuffers();
rt.destroy();
}
addEffect(effect) {
const effects = this.effects;
const isFirstEffect = effects.length === 0;
const inputTarget = this._createOffscreenTarget(isFirstEffect, effect.hdr);
const newEntry = new PostEffectEntry(effect, inputTarget);
effects.push(newEntry);
this._sourceTarget = newEntry.inputTarget;
if (effects.length > 1) {
effects[effects.length - 2].outputTarget = newEntry.inputTarget;
}
this._newPostEffect = effect;
if (effect.needsDepthBuffer) {
this._requestDepthMap();
}
this.enable();
this._newPostEffect = undefined;
}
removeEffect(effect) {
let index = -1;
for(let i = 0, len = this.effects.length; i < len; i++){
if (this.effects[i].effect === effect) {
index = i;
break;
}
}
if (index >= 0) {
if (index > 0) {
this.effects[index - 1].outputTarget = index + 1 < this.effects.length ? this.effects[index + 1].inputTarget : null;
} else {
if (this.effects.length > 1) {
if (!this.effects[1].inputTarget._depth) {
this._destroyOffscreenTarget(this.effects[1].inputTarget);
this.effects[1].inputTarget = this._createOffscreenTarget(true, this.effects[1].hdr);
this._sourceTarget = this.effects[1].inputTarget;
}
this.camera.renderTarget = this.effects[1].inputTarget;
}
}
this._destroyOffscreenTarget(this.effects[index].inputTarget);
this.effects.splice(index, 1);
}
if (this.enabled) {
if (effect.needsDepthBuffer) {
this._releaseDepthMap();
}
}
if (this.effects.length === 0) {
this.disable();
}
}
_requestDepthMaps() {
for(let i = 0, len = this.effects.length; i < len; i++){
const effect = this.effects[i].effect;
if (this._newPostEffect === effect) {
continue;
}
if (effect.needsDepthBuffer) {
this._requestDepthMap();
}
}
}
_releaseDepthMaps() {
for(let i = 0, len = this.effects.length; i < len; i++){
const effect = this.effects[i].effect;
if (effect.needsDepthBuffer) {
this._releaseDepthMap();
}
}
}
_requestDepthMap() {
const depthLayer = this.app.scene.layers.getLayerById(LAYERID_DEPTH);
if (depthLayer) {
depthLayer.incrementCounter();
this.camera.requestSceneDepthMap(true);
}
}
_releaseDepthMap() {
const depthLayer = this.app.scene.layers.getLayerById(LAYERID_DEPTH);
if (depthLayer) {
depthLayer.decrementCounter();
this.camera.requestSceneDepthMap(false);
}
}
destroy() {
for(let i = 0, len = this.effects.length; i < len; i++){
this.effects[i].inputTarget.destroy();
}
this.effects.length = 0;
this.disable();
}
enable() {
if (!this.enabled && this.effects.length) {
this.enabled = true;
this._requestDepthMaps();
this.app.graphicsDevice.on('resizecanvas', this._onCanvasResized, this);
this.destinationRenderTarget = this.camera.renderTarget;
this.camera.renderTarget = this.effects[0].inputTarget;
this.camera.onPostprocessing = ()=>{
if (this.enabled) {
let rect = null;
const len = this.effects.length;
if (len) {
for(let i = 0; i < len; i++){
const fx = this.effects[i];
let destTarget = fx.outputTarget;
if (i === len - 1) {
rect = this.camera.rect;
if (this.destinationRenderTarget) {
destTarget = this.destinationRenderTarget;
}
}
fx.effect.render(fx.inputTarget, destTarget, rect);
}
}
}
};
}
}
disable() {
if (this.enabled) {
this.enabled = false;
this.app.graphicsDevice.off('resizecanvas', this._onCanvasResized, this);
this._releaseDepthMaps();
this._destroyOffscreenTarget(this._sourceTarget);
this.camera.renderTarget = this.destinationRenderTarget;
this.camera.onPostprocessing = null;
}
}
_onCanvasResized(width, height) {
const rect = this.camera.rect;
const renderTarget = this.destinationRenderTarget;
width = renderTarget?.width ?? width;
height = renderTarget?.height ?? height;
this.camera.camera.aspectRatio = width * rect.z / (height * rect.w);
this.resizeRenderTargets();
}
resizeRenderTargets() {
const device = this.app.graphicsDevice;
const renderTarget = this.destinationRenderTarget;
const width = renderTarget?.width ?? device.width;
const height = renderTarget?.height ?? device.height;
const rect = this.camera.rect;
const desiredWidth = Math.floor(rect.z * width);
const desiredHeight = Math.floor(rect.w * height);
const effects = this.effects;
for(let i = 0, len = effects.length; i < len; i++){
const fx = effects[i];
if (fx.inputTarget.width !== desiredWidth || fx.inputTarget.height !== desiredHeight) {
this._resizeOffscreenTarget(fx.inputTarget);
}
}
}
onCameraRectChanged(name, oldValue, newValue) {
if (this.enabled) {
this.resizeRenderTargets();
}
}
}
export { PostEffectQueue };