playcanvas
Version:
PlayCanvas WebGL game engine
430 lines (427 loc) • 19.4 kB
JavaScript
import { Debug } from '../../core/debug.js';
import { TRACEID_RENDER_TARGET_ALLOC } from '../../core/constants.js';
import { isSrgbPixelFormat, PIXELFORMAT_DEPTH, PIXELFORMAT_DEPTH16, PIXELFORMAT_DEPTHSTENCIL, PIXELFORMAT_R32F } from './constants.js';
import { DebugGraphics } from './debug-graphics.js';
import { GraphicsDevice } from './graphics-device.js';
import { TextureUtils } from './texture-utils.js';
/**
* @import { Texture } from './texture.js'
*/ var id = 0;
/**
* A render target is a rectangular rendering surface.
*
* @category Graphics
*/ class RenderTarget {
/**
* Frees resources associated with this render target.
*/ destroy() {
Debug.trace(TRACEID_RENDER_TARGET_ALLOC, "DeAlloc: Id " + this.id + " " + this.name);
var device = this._device;
if (device) {
device.targets.delete(this);
if (device.renderTarget === this) {
device.setRenderTarget(null);
}
this.destroyFrameBuffers();
}
}
/**
* Free device resources associated with this render target.
*
* @ignore
*/ destroyFrameBuffers() {
var device = this._device;
if (device) {
this.impl.destroy(device);
}
}
/**
* Free textures associated with this render target.
*
* @ignore
*/ destroyTextureBuffers() {
var _this__depthBuffer, _this__colorBuffers;
(_this__depthBuffer = this._depthBuffer) == null ? void 0 : _this__depthBuffer.destroy();
this._depthBuffer = null;
(_this__colorBuffers = this._colorBuffers) == null ? void 0 : _this__colorBuffers.forEach((colorBuffer)=>{
colorBuffer.destroy();
});
this._colorBuffers = null;
this._colorBuffer = null;
}
/**
* Resizes the render target to the specified width and height. Internally this resizes all the
* assigned texture color and depth buffers.
*
* @param {number} width - The width of the render target in pixels.
* @param {number} height - The height of the render target in pixels.
*/ resize(width, height) {
if (this.width !== width || this.height !== height) {
var // resize textures
_this__depthBuffer, _this__colorBuffers;
if (this.mipLevel > 0) {
Debug.warn('Only a render target rendering to mipLevel 0 can be resized, ignoring.', this);
return;
}
// release existing
var device = this._device;
this.destroyFrameBuffers();
if (device.renderTarget === this) {
device.setRenderTarget(null);
}
(_this__depthBuffer = this._depthBuffer) == null ? void 0 : _this__depthBuffer.resize(width, height);
(_this__colorBuffers = this._colorBuffers) == null ? void 0 : _this__colorBuffers.forEach((colorBuffer)=>{
colorBuffer.resize(width, height);
});
// initialize again
this.validateMrt();
this.impl = device.createRenderTargetImpl(this);
}
}
validateMrt() {
Debug.call(()=>{
if (this._colorBuffers) {
var { width, height, cubemap, volume } = this._colorBuffers[0];
for(var i = 1; i < this._colorBuffers.length; i++){
var colorBuffer = this._colorBuffers[i];
Debug.assert(colorBuffer.width === width, 'All render target color buffers must have the same width', this);
Debug.assert(colorBuffer.height === height, 'All render target color buffers must have the same height', this);
Debug.assert(colorBuffer.cubemap === cubemap, 'All render target color buffers must have the same cubemap setting', this);
Debug.assert(colorBuffer.volume === volume, 'All render target color buffers must have the same volume setting', this);
}
}
});
}
/**
* Initializes the resources associated with this render target.
*
* @ignore
*/ init() {
this.impl.init(this._device, this);
}
/** @ignore */ get initialized() {
return this.impl.initialized;
}
/** @ignore */ get device() {
return this._device;
}
/**
* Called when the device context was lost. It releases all context related resources.
*
* @ignore
*/ loseContext() {
this.impl.loseContext();
}
/**
* If samples > 1, resolves the anti-aliased render target (WebGL2 only). When you're rendering
* to an anti-aliased render target, pixels aren't written directly to the readable texture.
* Instead, they're first written to a MSAA buffer, where each sample for each pixel is stored
* independently. In order to read the results, you first need to 'resolve' the buffer - to
* average all samples and create a simple texture with one color per pixel. This function
* performs this averaging and updates the colorBuffer and the depthBuffer. If autoResolve is
* set to true, the resolve will happen after every rendering to this render target, otherwise
* you can do it manually, during the app update or similar.
*
* @param {boolean} [color] - Resolve color buffer. Defaults to true.
* @param {boolean} [depth] - Resolve depth buffer. Defaults to true if the render target has a
* depth buffer.
*/ resolve(color, depth) {
if (color === void 0) color = true;
if (depth === void 0) depth = !!this._depthBuffer;
// TODO: consider adding support for MRT to this function.
if (this._device && this._samples > 1) {
DebugGraphics.pushGpuMarker(this._device, "RESOLVE-RT:" + this.name + ":" + (color ? '[color]' : '') + ":" + (depth ? '[depth]' : ''));
this.impl.resolve(this._device, this, color, depth);
DebugGraphics.popGpuMarker(this._device);
}
}
/**
* Copies color and/or depth contents of source render target to this one. Formats, sizes and
* anti-aliasing samples must match. Depth buffer can only be copied on WebGL 2.0.
*
* @param {RenderTarget} source - Source render target to copy from.
* @param {boolean} [color] - If true, will copy the color buffer. Defaults to false.
* @param {boolean} [depth] - If true, will copy the depth buffer. Defaults to false.
* @returns {boolean} True if the copy was successful, false otherwise.
*/ copy(source, color, depth) {
// TODO: consider adding support for MRT to this function.
if (!this._device) {
if (source._device) {
this._device = source._device;
} else {
Debug.error('Render targets are not initialized');
return false;
}
}
DebugGraphics.pushGpuMarker(this._device, "COPY-RT:" + source.name + "->" + this.name);
var success = this._device.copyRenderTarget(source, this, color, depth);
DebugGraphics.popGpuMarker(this._device);
return success;
}
/**
* Number of antialiasing samples the render target uses.
*
* @type {number}
*/ get samples() {
return this._samples;
}
/**
* True if the render target contains the depth attachment.
*
* @type {boolean}
*/ get depth() {
return this._depth;
}
/**
* True if the render target contains the stencil attachment.
*
* @type {boolean}
*/ get stencil() {
return this._stencil;
}
/**
* Color buffer set up on the render target.
*
* @type {Texture}
*/ get colorBuffer() {
return this._colorBuffer;
}
/**
* Accessor for multiple render target color buffers.
*
* @param {*} index - Index of the color buffer to get.
* @returns {Texture} - Color buffer at the specified index.
*/ getColorBuffer(index) {
var _this__colorBuffers;
return (_this__colorBuffers = this._colorBuffers) == null ? void 0 : _this__colorBuffers[index];
}
/**
* Depth buffer set up on the render target. Only available, if depthBuffer was set in
* constructor. Not available if depth property was used instead.
*
* @type {Texture}
*/ get depthBuffer() {
return this._depthBuffer;
}
/**
* If the render target is bound to a cubemap, this property specifies which face of the
* cubemap is rendered to. Can be:
*
* - {@link CUBEFACE_POSX}
* - {@link CUBEFACE_NEGX}
* - {@link CUBEFACE_POSY}
* - {@link CUBEFACE_NEGY}
* - {@link CUBEFACE_POSZ}
* - {@link CUBEFACE_NEGZ}
*
* @type {number}
*/ get face() {
return this._face;
}
/**
* Mip level of the render target.
*
* @type {number}
*/ get mipLevel() {
return this._mipLevel;
}
/**
* True if the mipmaps are automatically generated for the color buffer(s) if it contains
* a mip chain.
*
* @type {boolean}
*/ get mipmaps() {
return this._mipmaps;
}
/**
* Width of the render target in pixels.
*
* @type {number}
*/ get width() {
var _this__colorBuffer, _this__depthBuffer;
var width = ((_this__colorBuffer = this._colorBuffer) == null ? void 0 : _this__colorBuffer.width) || ((_this__depthBuffer = this._depthBuffer) == null ? void 0 : _this__depthBuffer.width) || this._device.width;
if (this._mipLevel > 0) {
width = TextureUtils.calcLevelDimension(width, this._mipLevel);
}
return width;
}
/**
* Height of the render target in pixels.
*
* @type {number}
*/ get height() {
var _this__colorBuffer, _this__depthBuffer;
var height = ((_this__colorBuffer = this._colorBuffer) == null ? void 0 : _this__colorBuffer.height) || ((_this__depthBuffer = this._depthBuffer) == null ? void 0 : _this__depthBuffer.height) || this._device.height;
if (this._mipLevel > 0) {
height = TextureUtils.calcLevelDimension(height, this._mipLevel);
}
return height;
}
/**
* Gets whether the format of the specified color buffer is sRGB.
*
* @param {number} index - The index of the color buffer.
* @returns {boolean} True if the color buffer is sRGB, false otherwise.
* @ignore
*/ isColorBufferSrgb(index) {
if (index === void 0) index = 0;
if (this.device.backBuffer === this) {
return isSrgbPixelFormat(this.device.backBufferFormat);
}
var colorBuffer = this.getColorBuffer(index);
return colorBuffer ? isSrgbPixelFormat(colorBuffer.format) : false;
}
/**
* Creates a new RenderTarget instance. A color buffer or a depth buffer must be set.
*
* @param {object} [options] - Object for passing optional arguments.
* @param {boolean} [options.autoResolve] - If samples > 1, enables or disables automatic MSAA
* resolve after rendering to this RT (see {@link RenderTarget#resolve}). Defaults to true.
* @param {Texture} [options.colorBuffer] - The texture that this render target will treat as a
* rendering surface.
* @param {Texture[]} [options.colorBuffers] - The textures that this render target will treat
* as a rendering surfaces. If this option is set, the colorBuffer option is ignored.
* @param {boolean} [options.depth] - If set to true, depth buffer will be created. Defaults to
* true. Ignored if depthBuffer is defined.
* @param {Texture} [options.depthBuffer] - The texture that this render target will treat as a
* depth/stencil surface (WebGL2 only). If set, the 'depth' and 'stencil' properties are
* ignored. Texture must have {@link PIXELFORMAT_DEPTH} or {@link PIXELFORMAT_DEPTHSTENCIL}
* format.
* @param {number} [options.mipLevel] - If set to a number greater than 0, the render target
* will render to the specified mip level of the color buffer. Defaults to 0.
* @param {number} [options.face] - If the colorBuffer parameter is a cubemap, use this option
* to specify the face of the cubemap to render to. Can be:
*
* - {@link CUBEFACE_POSX}
* - {@link CUBEFACE_NEGX}
* - {@link CUBEFACE_POSY}
* - {@link CUBEFACE_NEGY}
* - {@link CUBEFACE_POSZ}
* - {@link CUBEFACE_NEGZ}
*
* Defaults to {@link CUBEFACE_POSX}.
* @param {boolean} [options.flipY] - When set to true the image will be flipped in Y. Default
* is false.
* @param {string} [options.name] - The name of the render target.
* @param {number} [options.samples] - Number of hardware anti-aliasing samples. Default is 1.
* @param {boolean} [options.stencil] - If set to true, depth buffer will include stencil.
* Defaults to false. Ignored if depthBuffer is defined or depth is false.
* @example
* // Create a 512x512x24-bit render target with a depth buffer
* const colorBuffer = new pc.Texture(graphicsDevice, {
* width: 512,
* height: 512,
* format: pc.PIXELFORMAT_RGB8
* });
* const renderTarget = new pc.RenderTarget({
* colorBuffer: colorBuffer,
* depth: true
* });
*
* // Set the render target on a camera component
* camera.renderTarget = renderTarget;
*
* // Destroy render target at a later stage. Note that the color buffer needs
* // to be destroyed separately.
* renderTarget.colorBuffer.destroy();
* renderTarget.destroy();
* camera.renderTarget = null;
*/ constructor(options = {}){
var _options_colorBuffer, _options_colorBuffers, _options_depthBuffer, _this__colorBuffers;
Debug.assert(!(options instanceof GraphicsDevice), 'pc.RenderTarget constructor no longer accepts GraphicsDevice parameter.');
this.id = id++;
var _options_colorBuffer_device, _ref, _ref1;
// device, from one of the buffers
var device = (_ref1 = (_ref = (_options_colorBuffer_device = (_options_colorBuffer = options.colorBuffer) == null ? void 0 : _options_colorBuffer.device) != null ? _options_colorBuffer_device : (_options_colorBuffers = options.colorBuffers) == null ? void 0 : _options_colorBuffers[0].device) != null ? _ref : (_options_depthBuffer = options.depthBuffer) == null ? void 0 : _options_depthBuffer.device) != null ? _ref1 : options.graphicsDevice;
Debug.assert(device, 'Failed to obtain the device, colorBuffer nor depthBuffer store it.');
this._device = device;
// samples
var { maxSamples } = this._device;
var _options_samples;
this._samples = Math.min((_options_samples = options.samples) != null ? _options_samples : 1, maxSamples);
if (device.isWebGPU) {
// WebGPU only supports values of 1 or 4 for samples
this._samples = this._samples > 1 ? maxSamples : 1;
}
// Use the single colorBuffer in the colorBuffers array. This allows us to always just use the array internally.
this._colorBuffer = options.colorBuffer;
if (options.colorBuffer) {
this._colorBuffers = [
options.colorBuffer
];
}
// Process optional arguments
this._depthBuffer = options.depthBuffer;
var _options_face;
this._face = (_options_face = options.face) != null ? _options_face : 0;
if (this._depthBuffer) {
var format = this._depthBuffer._format;
if (format === PIXELFORMAT_DEPTH || format === PIXELFORMAT_DEPTH16) {
this._depth = true;
this._stencil = false;
} else if (format === PIXELFORMAT_DEPTHSTENCIL) {
this._depth = true;
this._stencil = true;
} else if (format === PIXELFORMAT_R32F && this._depthBuffer.device.isWebGPU && this._samples > 1) {
// on WebGPU, when multisampling is enabled, we use R32F format for the specified buffer,
// which we can resolve depth to using a shader
this._depth = true;
this._stencil = false;
} else {
Debug.warn('Incorrect depthBuffer format. Must be pc.PIXELFORMAT_DEPTH or pc.PIXELFORMAT_DEPTHSTENCIL');
this._depth = false;
this._stencil = false;
}
} else {
var _options_depth;
this._depth = (_options_depth = options.depth) != null ? _options_depth : true;
var _options_stencil;
this._stencil = (_options_stencil = options.stencil) != null ? _options_stencil : false;
}
// MRT
if (options.colorBuffers) {
Debug.assert(!this._colorBuffers, 'When constructing RenderTarget and options.colorBuffers is used, options.colorBuffer must not be used.');
if (!this._colorBuffers) {
this._colorBuffers = [
...options.colorBuffers
];
// set the main color buffer to point to 0 index
this._colorBuffer = options.colorBuffers[0];
}
}
var _options_autoResolve;
this.autoResolve = (_options_autoResolve = options.autoResolve) != null ? _options_autoResolve : true;
// use specified name, otherwise get one from color or depth buffer
this.name = options.name;
if (!this.name) {
var _this__colorBuffer;
this.name = (_this__colorBuffer = this._colorBuffer) == null ? void 0 : _this__colorBuffer.name;
}
if (!this.name) {
var _this__depthBuffer;
this.name = (_this__depthBuffer = this._depthBuffer) == null ? void 0 : _this__depthBuffer.name;
}
if (!this.name) {
this.name = 'Untitled';
}
var _options_flipY;
// render image flipped in Y
this.flipY = (_options_flipY = options.flipY) != null ? _options_flipY : false;
var _options_mipLevel;
this._mipLevel = (_options_mipLevel = options.mipLevel) != null ? _options_mipLevel : 0;
if (this._mipLevel > 0 && this._depth) {
Debug.error("Rendering to a mipLevel is not supported when render target uses a depth buffer. Ignoring mipLevel " + this._mipLevel + " for render target " + this.name, {
renderTarget: this,
options
});
this._mipLevel = 0;
}
// if we render to a specific mipmap (even 0), do not generate mipmaps
this._mipmaps = options.mipLevel === undefined;
this.validateMrt();
// device specific implementation
this.impl = device.createRenderTargetImpl(this);
Debug.trace(TRACEID_RENDER_TARGET_ALLOC, "Alloc: Id " + this.id + " " + this.name + ": " + this.width + "x" + this.height + " " + ("[samples: " + this.samples + "]") + ("" + (((_this__colorBuffers = this._colorBuffers) == null ? void 0 : _this__colorBuffers.length) ? "[MRT: " + this._colorBuffers.length + "]" : '')) + ("" + (this.colorBuffer ? '[Color]' : '')) + ("" + (this.depth ? '[Depth]' : '')) + ("" + (this.stencil ? '[Stencil]' : '')) + ("[Face:" + this.face + "]"));
}
}
export { RenderTarget };