跳到主要内容

渲染目标

渲染目标是可以替代屏幕接收渲染的矩形表面。它封装一张或多张可渲染的颜色纹理,以及可选的深度和模板缓冲区。相机渲染完成后,颜色纹理保存结果,可用于普通纹理适用的任何位置,最常见的是应用到材质以在场景中显示,或传给后续处理。

它是世界内屏幕、监控显示器、镜子、传送门、反射与折射,以及自定义多通道管线的基础。目标还可渲染到立方体贴图的单个面、纹理数组的单个层,参见立方体贴图面和纹理数组层;也可渲染到体纹理的单个深度切片,参见体纹理切片。

创建渲染目标​

先创建要渲染到的颜色纹理,纹理类型和选项参见纹理。必须使用可渲染的非压缩格式,见下文选择格式:

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
});

然后将纹理封装到渲染目标。若渲染的场景需要深度测试,应请求深度缓冲区;通过 samples 设置硬件抗锯齿,参见抗锯齿:

const renderTarget = new RenderTarget({
name: 'RT',
colorBuffer: texture,
depth: true,
origin: RENDERTARGET_ORIGIN_TOP
});

origin 选项将在下文解释。

将场景渲染到目标​

将目标赋给相机的 renderTarget 属性,该相机就会渲染到纹理而非屏幕。设置负的 priority,使其每帧在主相机之前渲染,确保主相机使用纹理时内容已更新:

const textureCamera = new Entity('TextureCamera');
textureCamera.addComponent('camera', {
// rendered before the main camera (default priority 0)
priority: -1,
renderTarget
});
app.root.addChild(textureCamera);

也可用全屏着色器通道或计算着色器等方式填充目标,但最常见的情况是使用相机渲染场景。

使用图层排除显示表面​

如果目标纹理显示在同一场景的对象上,该对象不能被渲染到此目标自身,否则表面会尝试渲染正在生成的纹理,形成自反馈。

使用图层可以清晰地组织这种关系。相机只渲染 layers 数组列出的图层,将显示对象放入纹理相机未列出的图层即可排除。下方渲染到纹理示例使用三个图层和两台相机:

  • World:场景内容。两台相机均列出它,因此同时渲染到纹理和屏幕。
  • Excluded:显示纹理的对象,以及其他只应出现在屏幕上的内容。仅主相机列出它。
  • Skybox:两台相机均列出它。
// 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];

使用结果​

目标颜色纹理可通过 renderTarget.colorBuffer 访问,它就是之前创建的同一纹理。像普通纹理一样将其应用到材质,例如用作显示平面的自发光贴图:

const material = new StandardMaterial();
material.emissiveMap = renderTarget.colorBuffer;
material.emissive = Color.WHITE;
material.update();

立方体贴图面和纹理数组层​

目标不仅可以渲染整张纹理,还能渲染到立方体贴图的单个面,或二维纹理数组的单个层(使用 arrayLength 创建的纹理)。用 face 选项选择立方体贴图面,用 layer 选择数组层。

要渲染多个面或层,为各自创建一个渲染目标,并共享同一纹理。渲染目标是轻量对象;如果面或层只渲染一次,例如烘焙一组视图,之后可销毁目标,纹理会保留结果:

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
}));
}

使用 depth: true 时,每个目标分配独立深度缓冲区。通过 depthBuffer 提供的缓冲区也可为纹理数组或立方体贴图,此时渲染到相同的层或面;也可提供由所有层目标共享的二维深度纹理。

StandardMaterial 不采样纹理数组。要显示某层,需用自定义着色器声明纹理数组并选择采样层,参见在着色器中采样。

渲染到纹理时同时采样​

同一渲染通道不能一边向纹理渲染、一边采样该纹理,即使渲染的是另一层也不行,因为着色器可访问整张纹理:

  • WebGL2:不支持。无法将采样器限制到纹理数组的部分层。
  • WebGPU:可以通过 Texture#getView 创建 TextureView,排除正在渲染的层,并绑定该视图。着色器的层索引相对于视图首层:
// 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));

在 WebGL 上,TextureView 仍绑定整张纹理。要在所有平台读写同一数据,应交替使用两张纹理:渲染到一张,同时采样另一张。

下面的示例从 64 个方向将雕像渲染到纹理数组各层,每层使用一个目标,再绘制数千个替身:这些面向相机的四边形,显示最接近当前观察方向的视图:

纹理数组替身

体纹理切片​

渲染目标可渲染到体纹理(以 volume: true 创建)的单个深度切片,用 slice 选项选择。渲染多个切片时,为每个切片创建目标,共享同一纹理:

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
}));
}

向体纹理渲染与向立方体贴图面或纹理数组层渲染存在以下区别:

  • 不支持多重采样:多重采样结果无法解析到深度切片,因此忽略 samples 选项。
  • 二维深度缓冲区:体纹理不能作为深度缓冲区。depth: true 为每个目标分配独立二维深度缓冲区,也可以让所有切片目标共享一张二维深度纹理。
  • 不能边渲染边采样:无论 WebGL2 还是 WebGPU,向某个切片渲染的通道都不能采样整个体纹理,TextureView 也不能排除切片。读写同一数据时,应交替使用两张体纹理。

在自定义着色器中使用三维坐标采样体纹理,参见在着色器中采样。

切片随时间变化时,每帧轮流只渲染其中一部分,可以分摊通道开销。下面的示例向体纹理填入彩色雾数据,可由 CPU 生成,或由 GPU 每帧更新少量切片;然后在场景上进行射线步进,也可选择使用 mipmap:

体纹理

Mipmap​

颜色纹理具有 mipmap 时,每个渲染通道结束后都会重新生成,以便用 mipmap 滤波采样结果。

默认使用盒式滤波生成 mipmap,对上一层每个 2x2 纹素块求平均。采样较小层级来模糊结果时,例如粗糙表面的反射,内容移动会暴露块状纹理并产生闪烁。通过纹理的 mipmapFilter 选项可改用平滑高斯滤波,但生成开销更高:

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
});

高斯滤波适用于支持滤波格式的二维纹理。立方体贴图、纹理数组和体纹理仍使用盒式滤波,上传数据后生成的 mipmap 也使用盒式滤波。

要渲染到特定 mip 层级,使用 mipLevel 选项。只要显式指定,即使为 0,也会禁用自动 mipmap 生成。不能同时使用深度缓冲区并渲染到非 0 层级。

目标渲染到立方体贴图面或数组层时,WebGPU 只重新生成该面或层的 mipmap;WebGL2 会重新生成纹理全部面或层的 mipmap,渲染许多层时开销很大。要只生成一次,应在除最后渲染的目标外,禁用其他目标的 mipmap 生成:

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
}));
}

对于体纹理,WebGL2 和 WebGPU 都会在渲染一个切片后重新生成整个体积的 mipmap,因为每个层级会对多个深度切片滤波。因此,渲染许多切片时,两种后端都应向除最后一个外的所有目标传入 mipLevel: 0,使 mipmap 只生成一次。

方向​

WebGL2 和 WebGPU 原生存储渲染图像的垂直行顺序相反。如果不指定方向,再使用网格 UV 将目标作为普通纹理采样,两个 API 的结果会垂直镜像。origin 选项固定存储方向,让目标在所有平台外观一致。可选值为:

  • RENDERTARGET_ORIGIN_TOP:在所有图形 API 中,第 0 行都是图像顶部,与图像纹理的存储方式相同。任何按普通纹理采样的目标(材质贴图或立方体贴图面)都应使用此值。适合大多数内容,采样代码按加载图像的方式编写即可。
  • RENDERTARGET_ORIGIN_BOTTOM:在所有 API 中,第 0 行都是图像底部,复现 WebGL2 原生布局。适合保留基于 WebGL 约定的现有代码,例如从投影(NDC)坐标推导 UV 的着色器,或通过视口矩形寻址单元的纹理图集。
  • RENDERTARGET_ORIGIN_NATIVE:使用图形 API 原生方向存储,因此 WebGL2 与 WebGPU 的行顺序不同。这是默认值,仅适用于不依赖方向的使用方式,例如通过片元位置推导坐标的屏幕空间采样。

在场景表面上显示渲染目标时,请使用 RENDERTARGET_ORIGIN_TOP。

选择格式​

颜色纹理必须使用可渲染的非压缩格式,格式分类参见像素格式:

  • PIXELFORMAT_RGBA8 是标准选择,在所有平台均可渲染。

  • sRGB 格式(例如 PIXELFORMAT_SRGBA8)同样可渲染。渲染的线性颜色在写入时转换为 sRGB,混合在线性空间中进行。参见 sRGB 纹理处理。

  • PIXELFORMAT_RGB10A2 提供每个 RGB 通道 10 位、alpha 2 位的精度,与 RGBA8 内存开销相同但精度更高,WebGL2 和 WebGPU 均可渲染。PIXELFORMAT_RGB10A2U 是无符号整数变体。

  • HDR 格式:浮点 PIXELFORMAT_RGBA32F、半浮点 PIXELFORMAT_RGBA16F 和小浮点 PIXELFORMAT_RG11B10F 能否渲染取决于设备支持:

    建议查询 GraphicsDevice.getRenderableHdrFormat,取得首个受支持选项,而非直接选定一种格式。场景 HDR 渲染参见 HDR 渲染。

  • 整数格式(例如 PIXELFORMAT_R8U)也可渲染。它们原样存储着色器输出,不执行滤波或混合;多重采样整数目标无法通过硬件解析。

  • PIXELFORMAT_RGB9E5 是紧凑的 HDR 格式,可以采样,但不能用作目标颜色缓冲区,压缩格式同样不行。

完整格式列表和支持规则参见 Texture API 参考。

要在渲染时进行深度测试,在创建目标时指定 depth: true,如上所示。需要模板缓冲区时,再设置 stencil: true。

调整大小​

调用 renderTarget.resize(width, height) 可更改目标分辨率,例如窗口变化时让其匹配输出尺寸。这会调整底层颜色和深度缓冲区大小,不保留之前的内容。

抗锯齿​

将 samples 设为大于 1,即可使用硬件多重采样抗锯齿(MSAA)。结果会自动解析到你创建的单采样颜色纹理,也是最终采样的纹理:

const renderTarget = new RenderTarget({
colorBuffer: texture,
depth: true,
origin: RENDERTARGET_ORIGIN_TOP,
samples: 4
});

要控制采样的解析方式,或在着色器中单独读取每个样本,参见下文显式多重采样渲染目标与自定义解析。

显式多重采样渲染目标与自定义解析​

备注

本节功能要求引擎 2.22 或以上,且仅受 WebGPU 支持。

上述自动解析使用固定硬件“盒式”滤波对样本求平均。需要不同解析方式时,例如带色调映射的颜色解析、最小/最大深度解析,或在着色器中读取独立样本,应显式创建多重采样纹理(samples 大于 1 的 Texture),并直接作为目标缓冲区:

// 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
});
  • 颜色:提供 resolveBuffer 时,通道结束后由硬件将颜色样本解析到其中,与自动路径类似。省略时则存储样本,后续通道可对 texture_multisampled_2d 调用 textureLoad 逐样本读取,实现自定义解析。这也是对整数等硬件无法解析的格式使用 MSAA 的唯一方式。使用 MRT 时,每个附件都有独立可选的解析纹理(resolveBuffers)。
  • 深度:WebGPU 没有硬件深度解析。可将多重采样 depthBuffer 作为 texture_depth_multisampled_2d 逐样本读取,或通过引擎提供的着色器解析到 depthResolveBuffer。操作由 depthResolveMode 选择:DEPTHRESOLVE_MIN(默认,选择最近表面)、DEPTHRESOLVE_MAX 或 DEPTHRESOLVE_SAMPLE0。同一模式也控制场景深度贴图和深度复制使用的深度解析。

两个示例演示这些技术:自定义带色调映射的颜色解析与硬件解析并排比较,以及逐样本深度雾与使用解析后深度计算的雾进行比较:

自定义 MSAA 解析
MSAA 深度雾

清理​

渲染目标不拥有纹理,因此使用结束后应分别销毁。先销毁颜色纹理和显式创建的深度纹理,再销毁目标:

renderTarget.colorBuffer.destroy();
renderTarget.destroy();

多个立方体贴图面、数组层或体切片的目标共享同一纹理时,每个目标都需销毁,共享纹理只销毁一次。

示例​

下面的示例通过第二台相机将场景渲染到纹理,并显示在世界中的平面上。它使用上述三图层配置,将显示平面排除在渲染目标之外,并每隔几秒在透视与正交投影之间切换纹理相机。

渲染到纹理
  • 纹理:纹理类型、像素格式、上传数据、采样、回读和复制。
  • 多渲染目标:单个通道同时渲染到多个颜色缓冲区。
  • 多相机:组合视图,为相机分配目标。
  • 图层:控制每台相机渲染的对象。
  • 后期效果:基于渲染目标的内置和自定义后处理。
  • 设备丢失与恢复:上下文或设备丢失后恢复 GPU 生成的内容。
  • TextureRenderer:在屏幕上检查目标颜色和独立通道。