Render Targets
A render target is a rectangular rendering surface you can render into instead of the screen. It wraps one or more renderable color textures, along with an optional depth (and stencil) buffer. Once a camera has rendered into it, the color texture holds the result and can be used anywhere a normal texture can - most commonly applied to a material to display it in the scene, or fed into further processing.
This underpins effects such as in-world screens, security monitors, mirrors and portals, reflection and refraction, and custom multi-pass pipelines. A render target can also render into a single face of a cubemap, a single layer of a texture array - see Cubemap faces and texture array layers - or a single depth slice of a volume texture - see Volume texture slices.
Creating a render target
First create the color texture to render into - see Textures for the texture types and their options. It must use a renderable, uncompressed format (see Choosing a format below):
const texture = new Texture(app.graphicsDevice, {
name: 'RT-color',
width: 512,
height: 256,
format: PIXELFORMAT_SRGBA8,
mipmaps: true,
minFilter: FILTER_LINEAR,
magFilter: FILTER_LINEAR,
addressU: ADDRESS_CLAMP_TO_EDGE,
addressV: ADDRESS_CLAMP_TO_EDGE
});
Then wrap it in a render target. Request a depth buffer if the scene you render needs depth testing, and set samples for hardware anti-aliasing (see Anti-aliasing):
const renderTarget = new RenderTarget({
name: 'RT',
colorBuffer: texture,
depth: true,
origin: RENDERTARGET_ORIGIN_TOP
});
The origin option is explained below.
Rendering the scene into it
Assign the render target to a camera's renderTarget property. That camera then renders into the texture instead of the screen. Give it a negative priority so it renders before the main camera each frame, ensuring the texture is up to date when the main camera uses it:
const textureCamera = new Entity('TextureCamera');
textureCamera.addComponent('camera', {
// rendered before the main camera (default priority 0)
priority: -1,
renderTarget
});
app.root.addChild(textureCamera);
A render target can also be filled by means other than a camera - for example a fullscreen shader pass or a compute shader - but rendering a scene with a camera is the most common case.
Excluding the display surface with layers
When the render target's texture is displayed on an object within the same scene, that object must not be rendered into the render target itself - otherwise the surface would try to render the texture it is currently producing, feeding back on itself.
The clean way to arrange this is with layers. A camera only renders the layers listed in its layers array, so placing the display object in a layer the texture camera does not list excludes it. The render-to-texture example below uses three layers and two cameras:
- World - the scene content. Listed by both cameras, so it renders into the texture and to the screen.
- Excluded - the object that displays the texture (and anything else that should appear on screen only). Listed by the main camera only.
- Skybox - listed by both cameras.
// a layer for objects that must not render into the texture
const excludedLayer = new Layer({ name: 'Excluded' });
app.scene.layers.insert(excludedLayer, 1);
const worldLayer = app.scene.layers.getLayerByName('World');
const skyboxLayer = app.scene.layers.getLayerByName('Skybox');
// texture camera renders the scene, but NOT the Excluded layer
textureCamera.camera.layers = [worldLayer.id, skyboxLayer.id];
// main camera renders everything, including the display surface in the Excluded layer
mainCamera.camera.layers = [worldLayer.id, excludedLayer.id, skyboxLayer.id];
Using the result
The render target's color texture is available as renderTarget.colorBuffer (it is the same texture you created). Apply it to a material like any other texture - for instance as the emissive map of the plane that acts as the display surface:
const material = new StandardMaterial();
material.emissiveMap = renderTarget.colorBuffer;
material.emissive = Color.WHITE;
material.update();
Cubemap faces and texture array layers
Instead of a whole texture, a render target can render into a single face of a cubemap, or a single layer of a 2D texture array (a texture created with arrayLength). Select the cubemap face with the face option, or the array layer with the layer option.
To render to several faces or layers, create a render target for each of them, all sharing the same texture. Render targets are lightweight - when the faces or layers are only rendered once, for example to bake a set of views, the render targets can be destroyed afterwards, and the texture keeps the result:
const layerCount = 16;
const textureArray = new Texture(app.graphicsDevice, {
name: 'RT-array',
width: 256,
height: 256,
arrayLength: layerCount,
format: PIXELFORMAT_SRGBA8,
mipmaps: false
});
// a render target for each layer, all sharing the texture array
const renderTargets = [];
for (let layer = 0; layer < layerCount; layer++) {
renderTargets.push(new RenderTarget({
colorBuffer: textureArray,
layer,
depth: true,
origin: RENDERTARGET_ORIGIN_TOP
}));
}
With depth: true, each render target allocates its own depth buffer. A depth buffer you provide with the depthBuffer option can be a texture array (or a cubemap) as well, in which case the same layer of it is rendered to, or a 2D depth texture shared by the render targets of all layers.
StandardMaterial does not sample texture arrays. To display a layer, use a custom shader that declares the texture array and selects the layer to sample - see Sampling in shaders.
Sampling a texture while rendering into it
A texture cannot be sampled in the same render pass that renders into it - even into one of its other layers - as the shader has access to the whole texture:
- WebGL2 - not possible. A sampler cannot be restricted to some of the layers of a texture array.
- WebGPU - possible, by binding a
TextureViewthat excludes the layer being rendered to, created withTexture#getView. The shader indexes the layers of the view relative to its first layer:
// sample only layer 1, while a render target renders into layer 0 - the shader samples it as its layer 0
material.setParameter('uLayers', textureArray.getView(0, 1, 1, 1));
On WebGL, a TextureView binds the whole texture instead. To read and write the same data on all platforms, alternate between two textures - render into one while sampling the other.
The following example renders a statue from 64 directions into the layers of a texture array, using a render target for each layer, and draws thousands of impostors - camera facing quads showing the view closest to the direction they are seen from:
Volume texture slices
A render target can render into a single depth slice of a volume texture (a texture created with volume: true), selected with the slice option. To render to several slices, create a render target for each of them, all sharing the same texture:
const sliceCount = 32;
const volume = new Texture(app.graphicsDevice, {
name: 'RT-volume',
width: 128,
height: 128,
depth: sliceCount,
volume: true,
format: PIXELFORMAT_RGBA8,
mipmaps: false
});
// a render target for each depth slice, all sharing the volume texture
const sliceTargets = [];
for (let slice = 0; slice < sliceCount; slice++) {
sliceTargets.push(new RenderTarget({
colorBuffer: volume,
slice,
depth: false
}));
}
Rendering to a volume texture has these differences from rendering to the faces of a cubemap or the layers of a texture array:
- No multisampling - a multisampled rendering cannot be resolved into a depth slice, so the
samplesoption is ignored. - 2D depth buffer - a volume texture cannot be a depth buffer. With
depth: true, each render target allocates its own 2D depth buffer, or a 2D depth texture can be shared by the render targets of all slices. - No sampling while rendering - the volume texture cannot be sampled in a render pass that renders into one of its slices, on both WebGL2 and WebGPU, and a
TextureViewcannot exclude a slice. To read and write the same data, alternate between two volume textures.
A volume texture is sampled with 3D coordinates in a custom shader, see Sampling in shaders.
When the slices change over time, rendering only some of them each frame, in turn, spreads the cost of the render passes. The following example fills a volume texture with colorful fog data - either on the CPU, or rendered on the GPU into its slices, updating a few slices each frame - and raymarches it over the scene, optionally using its mipmaps:
Mipmaps
When the color texture has mipmaps, the render target regenerates them after each render pass, so the result can be sampled with mipmap filtering.
The mipmaps are generated with a box filter, which averages each block of 2x2 texels of the previous level. When the small mip levels are sampled to blur the result, such as for the reflection of a rough surface, their blocks show and flicker as the content moves. The mipmapFilter option of the texture selects a smooth Gaussian filter instead, at a higher cost of generating the mipmaps:
const texture = new Texture(app.graphicsDevice, {
name: 'RT-color',
width: 512,
height: 256,
format: PIXELFORMAT_SRGBA8,
mipmaps: true,
mipmapFilter: MIPMAPFILTER_GAUSSIAN,
minFilter: FILTER_LINEAR_MIPMAP_LINEAR,
magFilter: FILTER_LINEAR
});
The Gaussian filter applies to 2D textures of formats which can be filtered. Cubemaps, texture arrays and volume textures use the box filter, as do the mipmaps generated for data uploaded to a texture.
To render into a specific mip level instead, use the mipLevel option. Specifying it - even as 0 - also disables the automatic mipmap generation. Rendering to a mip level other than 0 is not supported together with a depth buffer.
When a render target renders into a cubemap face or an array layer, WebGPU regenerates the mipmaps of the rendered face or layer only. WebGL2 regenerates the mipmaps of all faces or layers of the texture, which is costly when rendering into many layers. To generate them only once, disable the mipmap generation on all render targets except the one rendered last:
for (let layer = 0; layer < layerCount; layer++) {
const lastLayer = layer === layerCount - 1;
renderTargets.push(new RenderTarget({
colorBuffer: textureArray,
layer,
depth: true,
// on WebGL2, generate the mipmaps of the whole texture array once, after the last layer
// is rendered (render the layers in order, for example using the camera priority)
mipLevel: app.graphicsDevice.isWebGL2 && !lastLayer ? 0 : undefined
}));
}
For a volume texture, the mipmaps of the whole volume are regenerated after rendering into a slice, on both WebGL2 and WebGPU, as each mip level is filtered from several depth slices. So when rendering into many slices, generate them once on both, by passing mipLevel: 0 to all render targets except the one rendered last.
Orientation
WebGL2 and WebGPU natively store a rendered image with the opposite vertical row order. If you leave the orientation unspecified and then sample the render target as a regular texture (with mesh UVs), the result appears vertically mirrored between the two APIs. The origin option pins the stored orientation so the render target looks identical everywhere. It can be:
RENDERTARGET_ORIGIN_TOP- row 0 is the top of the rendered image, on all graphics APIs, matching how image textures are stored. Use this for any render target you sample as a regular texture (a material map, or a cube map face). Recommended for most content - write the sampling code as if the texture were a loaded image.RENDERTARGET_ORIGIN_BOTTOM- row 0 is the bottom of the rendered image, on all graphics APIs, replicating WebGL2's native layout. Use this to keep consuming code written against WebGL conventions working unchanged - shaders deriving UVs from projected (NDC) coordinates, or texture atlases addressing cells by viewport rectangles.RENDERTARGET_ORIGIN_NATIVE- the image is stored in the graphics API's native orientation, so the row order differs between WebGL2 and WebGPU. This is the default. It is only appropriate for orientation-agnostic consumers, such as screen-space sampling using coordinates derived from the fragment position.
In short: if you display a render target on a surface in your scene, use RENDERTARGET_ORIGIN_TOP.
Choosing a format
The color texture must use a renderable, uncompressed format - see Pixel formats for the groups of formats:
-
PIXELFORMAT_RGBA8is the standard choice, renderable everywhere. -
sRGB formats such as
PIXELFORMAT_SRGBA8are renderable as well. The rendered linear colors are converted to sRGB when written, and blending happens in linear space. See sRGB texture handling. -
PIXELFORMAT_RGB10A2offers 10 bits per RGB channel with 2-bit alpha - higher precision thanRGBA8at the same memory cost, renderable on both WebGL2 and WebGPU.PIXELFORMAT_RGB10A2Uis its unsigned integer variant. -
HDR formats - float
PIXELFORMAT_RGBA32F, half-floatPIXELFORMAT_RGBA16Fand small-floatPIXELFORMAT_RG11B10F- are renderable subject to device support:- on WebGPU, rendering to float and half-float formats is always supported, and to the small-float format when
GraphicsDevice.textureRG11B10Renderableis true. - on WebGL2, rendering to all three is supported when
GraphicsDevice.textureFloatRenderableis true. Otherwise, whenGraphicsDevice.textureHalfFloatRenderableis true, rendering to half-float formats only is supported - the case of many mobile iOS devices.
Rather than picking one directly, query
GraphicsDevice.getRenderableHdrFormat, which returns the first supported option. See HDR Rendering for rendering the scene in HDR. - on WebGPU, rendering to float and half-float formats is always supported, and to the small-float format when
-
Integer formats, such as
PIXELFORMAT_R8U, are renderable too. They store the values a shader outputs as they are, with no filtering or blending, and a multisampled integer render target cannot be hardware-resolved. -
PIXELFORMAT_RGB9E5is a compact HDR format that can be sampled but cannot be used as a render target color buffer, and neither can compressed formats.
See the Texture API reference for the full list of formats and the detailed support rules.
For depth testing during rendering, request a depth buffer with depth: true when creating the render target (as shown above). Use stencil: true as well if you need a stencil buffer.
Resizing
To change a render target's resolution - for example to keep it matched to the output size when the window resizes - call renderTarget.resize(width, height). This resizes the underlying color and depth buffers; their previous contents are not preserved.
Anti-aliasing
Set samples greater than 1 to render the target with hardware multi-sample anti-aliasing (MSAA). The multi-sampled result is automatically resolved into the single-sampled color texture you created, which is the one you sample from:
const renderTarget = new RenderTarget({
colorBuffer: texture,
depth: true,
origin: RENDERTARGET_ORIGIN_TOP,
samples: 4
});
For control over how the samples are resolved - or to read them individually in a shader - see Explicit multisampled render targets below.
Explicit multisampled render targets and custom resolves
The features in this section require Engine 2.22 or later, and are WebGPU only.
The automatic resolve above averages the samples with a fixed hardware "box" filter. Techniques that need a different resolve - a tonemapped color resolve, a min/max depth resolve, or reading the individual samples in a shader - create the multisampled textures explicitly (a Texture with samples greater than 1) and use them directly as the render target's buffers:
// multisampled textures - the render target renders directly into their samples
const msColor = new Texture(app.graphicsDevice, { width, height, format: PIXELFORMAT_RGBA16F, samples: 4 });
const msDepth = new Texture(app.graphicsDevice, { width, height, format: PIXELFORMAT_DEPTH, samples: 4 });
// optional single-sampled resolve targets
const resolvedColor = new Texture(app.graphicsDevice, { width, height, format: PIXELFORMAT_RGBA16F, mipmaps: false });
const resolvedDepth = new Texture(app.graphicsDevice, { width, height, format: PIXELFORMAT_R32F, mipmaps: false });
const renderTarget = new RenderTarget({
colorBuffer: msColor,
resolveBuffer: resolvedColor, // hardware resolve at the end of a render pass
depthBuffer: msDepth,
depthResolveBuffer: resolvedDepth // shader-based resolve, controlled by depthResolveMode
});
- Color: when a
resolveBufferis provided, the color samples are hardware-resolved into it at the end of a render pass, like the automatic path. When it is omitted, the samples are stored instead, and a later pass reads them individually withtextureLoadon atexture_multisampled_2d- a custom resolve. This is also the only way to use MSAA with formats the hardware cannot resolve, such as integer formats. With multiple render targets, each attachment has its own optional resolve texture (resolveBuffers). - Depth: depth has no hardware resolve on WebGPU. A multisampled
depthBuffercan be read per sample as atexture_depth_multisampled_2d, or resolved into thedepthResolveBufferby an engine-provided shader whose operation is selected withdepthResolveMode-DEPTHRESOLVE_MIN(the default, selecting the nearest surface),DEPTHRESOLVE_MAXorDEPTHRESOLVE_SAMPLE0. The same mode also controls the depth resolve used by the scene depth map and by depth copies.
Two examples demonstrate these techniques - a custom tonemapped color resolve compared side by side against the hardware resolve, and per-sample depth fog compared against fog computed from a resolved depth:
Cleaning up
A render target does not own its textures, so destroy them separately when you are done. Destroy the color texture (and depth buffer texture, if you created one explicitly), then the render target:
renderTarget.colorBuffer.destroy();
renderTarget.destroy();
When the render targets of several cubemap faces, array layers or volume slices share a texture, destroy each render target, and the shared texture once.
Example
The following example renders a scene into a texture from a second camera and displays it on a plane in the world. It uses the three-layer setup described above to keep the display plane out of the render target, and switches the texture camera between perspective and orthographic projection every few seconds.
Related pages
- Textures - the texture types, pixel formats, uploading data, sampling, and reading back and copying textures.
- Multiple Render Targets - render to several color buffers at once from a single pass.
- Multiple Cameras - composing views and assigning render targets to cameras.
- Layers - controlling which objects each camera renders.
- Post Effects - built-in and custom post-processing built on render targets.
- Device Loss and Recovery - restoring GPU-generated contents after context or device loss.
- TextureRenderer - inspect render-target color and individual channels on screen.