渲染目标
渲染目标是可以替代屏幕接收渲染的矩形表面。它封装一张或多张可渲染的颜色纹理,以及可选的深度和模板缓冲区。相机渲染完成后,颜色纹理保存结果,可用于普通纹理适用的任何位置,最常见的是应用到材质以在场景中显示,或传给后续处理。
它是世界内屏幕、监控显示器、镜子、传送门、反射与折射,以及自定义多通道管线的基础。目标还可渲染到立方体贴图的单个面、纹理数组的单个层,参见立方体贴图面和纹理数组层;也可渲染到体纹理的单个深度切片,参见体纹理切片。
创建渲染目标
先创建要渲染到的颜色纹理,纹理类型和选项参见纹理。必须使用可渲染的非压缩格式,见下文选择格式:
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能否渲染取决于设备支持:- WebGPU 始终支持向浮点和半浮点格式渲染;
GraphicsDevice.textureRG11B10Renderable为 true 时,还支持小浮点格式。 - WebGL2 在
GraphicsDevice.textureFloatRenderable为 true 时支持全部三种格式。否则,如果GraphicsDevice.textureHalfFloatRenderable为 true,仅支持半浮点格式,许多移动 iOS 设备属于这种情况。
建议查询
GraphicsDevice.getRenderableHdrFormat,取得首个受支持选项,而非直接选定一种格式。场景 HDR 渲染参见 HDR 渲染。 - WebGPU 始终支持向浮点和半浮点格式渲染;
-
整数格式(例如
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。同一模式也控制场景深度贴图和深度复制使用的深度解析。
两个示例演示这些技术:自定义带色调映射的颜色解析与硬件解析并排比较,以及逐样本深度雾与使用解析后深度计算的雾进行比较:
清理
渲染目标不拥有纹理,因此使用结束后应分别销毁。先销毁颜色纹理和显式创建的深度纹理,再销毁目标:
renderTarget.colorBuffer.destroy();
renderTarget.destroy();
多个立方体贴图面、数组层或体切片的目标共享同一纹理时,每个目标都需销毁,共享纹理只销毁一次。
示例
下面的示例通过第二台相机将场景渲染到纹理,并显示在世界中的平面上。它使用上述三图层配置,将显示平面排除在渲染目标之外,并每隔几秒在透视与正交投影之间切换纹理相机。