- 图形学
- 3D渲染
【免费下载链接】spritejs
A cross platform high-performance graphics system.
导读:在 SpriteJS 3D 扩展中,RenderTarget是一块可绘制的 FrameBuffer,用于把 3D 场景先渲染到离屏缓冲,再以纹理的形式贴回屏幕,从而支撑「渲染结果作为输入再次渲染」这类多次渲染需求。本文以 docs/zh-cn/ext3d/render-target.md 为核心,结合仓库内可运行的 demo 与 API 文档,系统讲解 RenderTarget 的构造参数、layer.renderTarget调用方式、多 Target 叠加、双缓冲复用以及autoClear控制缓冲区保留等完整实战方案。
为什么需要渲染对象(RenderTarget)
在普通的 3D 绘制流程里,场景内容直接被渲染到屏幕上。但有些时候我们需要多次渲染,例如:
- 先把一些内容渲染到 FrameBuffer 中,然后作为纹理输出到屏幕;
- 将前一步的渲染结果当作下一步的输入,形成链式/迭代处理(如后期处理、GPGPU 流体模拟)。
在 SpriteJS 的 3D 扩展(sprite-extend-3d)中,这个需求由RenderTarget对象解决——它定义了一个用于将元素绘制到 FrameBuffer 的对象(见 docs/zh-cn/api/ext3d/render-target.md)。RenderTarget 继承自Group3d,因此它本身就是一个 3D 分组节点,可以像普通 Group 一样append子元素、拥有自己的相机。
使用前请确保已按 docs/zh-cn/ext3d/index.md 的说明加载
sprite-extend-3d(在spritejs之后引入即可),随后通过spritejs.ext3d.RenderTarget访问该 API。
快速上手:把场景渲染到 FrameBuffer 再贴回屏幕
下面这段完整示例(与 demos/3d/render_target/index.js 一致)展示了 RenderTarget 最核心的用法:一个立方体mesh被渲染到target的 FrameBuffer 上,然后target.texture作为另一个立方体mesh2的 Program 纹理,最终mesh2输出到屏幕。
const vertex = /* glsl */ ` precision highp float; precision highp int; attribute vec3 position; attribute vec3 normal; attribute vec2 uv; uniform mat4 modelViewMatrix; uniform mat4 projectionMatrix; uniform mat3 normalMatrix; varying vec2 vUv; varying vec3 vNormal; void main() { vUv = uv; vNormal = normalize(normalMatrix * normal); gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `; const fragment = /* glsl */ ` precision highp float; precision highp int; uniform sampler2D tMap; varying vec2 vUv; varying vec3 vNormal; void main() { vec3 normal = normalize(vNormal); float lighting = 0.2 * dot(normal, normalize(vec3(-0.3, 0.8, 0.6))); vec3 tex = texture2D(tMap, vUv).rgb; gl_FragColor.rgb = tex + lighting + vec3(vUv - 0.5, 0.0) * 0.1; gl_FragColor.a = 1.0; } `; const {Scene} = spritejs; const {Cube, RenderTarget} = spritejs.ext3d; const container = document.getElementById('container'); const scene = new Scene({ container, displayRatio: 2, }); const layer = scene.layer3d('fglayer', { camera: { fov: 35, }, }); layer.camera.attributes.pos = [0, 1, 5]; layer.camera.lookAt([0, 0, 0]); const texture = layer.createTexture({ image: new Uint8Array([ 191, 25, 54, 255, 96, 18, 54, 255, 96, 18, 54, 255, 37, 13, 53, 255, ]), width: 2, height: 2, magFilter: layer.gl.NEAREST, }); const program = layer.createProgram({ vertex, fragment, texture, }); const target = new RenderTarget(layer.gl, { width: 512, height: 512, camera: { fov: 35, }, }); target.camera.attributes.pos = [0, 1, 5]; target.camera.lookAt([0, 0, 0]); const mesh = new Cube(program); target.append(mesh); const targetProgram = layer.createProgram({ vertex, fragment, texture: target.texture, }); target.addEventListener('beforerender', (e) => { layer.gl.clearColor(0.15, 0.05, 0.2, 1); }); target.addEventListener('afterrender', (e) => { layer.gl.clearColor(1, 1, 1, 0); }); const mesh2 = new Cube(targetProgram); layer.append(mesh2); layer.tick(() => { layer.renderTarget(target); mesh.attributes.rotateY -= 1.5; mesh2.attributes.rotateY -= 0.9; mesh2.attributes.rotateX -= 1; });拆解这段代码的完整流程:
- 定义着色器:顶点着色器做常规的
modelViewMatrix/projectionMatrix/normalMatrix变换,片元着色器通过uniform sampler2D tMap采样纹理,并叠加一个简单的方向光计算。 - 创建 3D 层与相机:
scene.layer3d('fglayer', {camera: {fov: 35}})创建一个带透视相机的 3D 层;layer.camera与target.camera分别设置位置[0, 1, 5]并lookAt([0, 0, 0])——RenderTarget 拥有独立的相机,可以看作一个迷你 3D 场景。 - 创建 RenderTarget:
new RenderTarget(layer.gl, {width: 512, height: 512, camera: {fov: 35}})在离屏开辟一块 512×512 的 FrameBuffer。 - 把元素挂到 target 上:
target.append(mesh),这与把元素append到 layer 上的方式完全一致。 - 回读纹理:通过
target.texture(只读属性,返回渲染后的纹理数据)创建第二个 ProgramtargetProgram,用它再生成一个立方体mesh2并layer.append(mesh2)。 - 渲染到离屏缓冲:在
layer.tick中调用layer.renderTarget(target),先执行 target 上元素的渲染;beforerender/afterrender事件用来在渲染前后修改clearColor(如渲染时清为深紫色(0.15, 0.05, 0.2),渲染结束后恢复为白色透明(1, 1, 1, 0))。 - 驱动动画:每帧分别旋转两个立方体,即可看到
mesh2表面显示的是mesh渲染到 FrameBuffer 中的画面。
RenderTarget 构造参数详解
根据 docs/zh-cn/api/ext3d/render-target.md,new RenderTarget(gl, attrs)支持的配置项及默认值如下:
| 配置项 | 默认值 | 说明 |
|---|---|---|
width | gl.canvas.width | 离屏缓冲宽度(像素) |
height | gl.canvas.height | 离屏缓冲高度(像素) |
target | gl.FRAMEBUFFER | 绑定的帧缓冲类型 |
color | 1 | 颜色附件的数量 |
depth | true | 是否启用深度缓冲 |
stencil | false | 是否启用模板缓冲 |
depthTexture | false | 是否使用深度纹理(注意:开启后与 stencil 不兼容,即「stencil breaks」) |
wrapS | gl.CLAMP_TO_EDGE | 水平方向纹理环绕方式 |
wrapT | gl.CLAMP_TO_EDGE | 垂直方向纹理环绕方式 |
minFilter | gl.LINEAR | 纹理缩小过滤方式 |
magFilter | minFilter | 纹理放大过滤方式(默认跟随 minFilter) |
type | gl.UNSIGNED_BYTE | 纹理数据类型 |
format | gl.RGBA | 纹理格式 |
internalFormat | format | 内部存储格式(默认跟随 format) |
unpackAlignment | — | 像素数据行对齐方式 |
premultiplyAlpha | — | 是否预乘 Alpha |
camera | cameraOptions | 该 RenderTarget 使用的相机配置 |
buffer | false | 是否启用双缓冲(true时可用swap()交换缓冲) |
实用建议:
- 需要精确纹理采样时,可像示例一样指定
magFilter: layer.gl.NEAREST,避免放大时的线性插值模糊; - 涉及深度测试的 3D 内容要保持
depth: true;只做 2D 后处理或 GPGPU 计算时可设depth: false以节省开销; - 高精度浮点模拟(如流体)可将
type设为半浮点(WebGL2 下为gl.HALF_FLOAT)并显式指定format/internalFormat,参见下文「源码佐证」一节的demos/3d/post/index.js。
作为Group3d的子类,RenderTarget 同样继承Node3d的位置/旋转/缩放属性(pos、rotate、scale等)与 Group 的子元素管理能力,也拥有readonly texture属性。核心方法包括:
renderBy(layer, {root = this, ...options} = {}):让 RenderTarget 实例被 layer 渲染;swap():当构造时设置了buffer后,交换当前缓冲与目标缓冲。
layer.renderTarget:把内容绘制到目标缓冲区
layer.renderTarget(target, options)是Layer3d提供的方法,用于渲染指定的 RenderTarget 对象(参见 docs/zh-cn/api/ext3d/layer3d.md)。与bindTarget的「持续绑定」不同,renderTarget通常配合tick在每帧显式调用,用于一次性绘制某个目标。
它支持传入渲染选项,在 demos/3d/post/index.js 的流体模拟中可以看到实际用法:
layer.renderTarget(velocity, { root: splatProgram, sort: false, update: false, });其中:
root:本次渲染的根节点,缺省时渲染 RenderTarget 自身挂载的子元素。传入外部节点即可实现「不把 root 添加到 renderTarget 上,针对不同 renderTarget 复用同一个 root」;sort:是否对网格排序,计算类渲染(如后处理)可设为false跳过排序;update:是否更新网格数据,固定几何(如全屏三角形)可设为false跳过更新以提升性能。
底层上,这些选项与Layer3d.setRenderOptions(opts)的配置(update = true、sort = true、frustumCull = true、clear)对应,均来自 docs/zh-cn/api/ext3d/layer3d.md。
多次渲染的三种实现方案
「渲染结果作为纹理继续参与渲染」是 RenderTarget 最常见的进阶场景。官方文档给出了三种逐步递进的方案。
方案一:创建多个 RenderTarget 叠加
当需要多道处理时,最直接的做法是创建多个 RenderTarget,把同一个 root 复用到不同目标上,再读取各自的texture:
const target1 = new RenderTarget(layer.gl, { width: 512, height: 512, camera: { fov: 35, }, }); const target2 = new RenderTarget(layer.gl, { width: 512, height: 512, camera: { fov: 35, }, }); const target3 = new RenderTarget(layer.gl, { width: 512, height: 512, camera: { fov: 35, }, }); ... const root = new Mesh3d(...); layer.tick(() => { // 我们可以不将root添加到renderTarget上,这样就可以针对不同的renderTarget复用root layer.renderTarget(target1, {root}); root.program.uniforms.tMap.value = target1.texture; layer.renderTarget(target2, {root}); root.program.uniforms.tMap.value = target2.texture; layer.renderTarget(target3, {root}); root.program.uniforms.tMap.value = target2.texture; ... });注意两个关键点:
- root 不挂载到 RenderTarget 上,而是通过
layer.renderTarget(target, {root})临时指定,这样同一个root可以在不同 target 之间复用; - 纹理通过 uniform 切换:每轮渲染前把
root.program.uniforms.tMap.value指向上一个 target 的texture,实现结果链式传递。
这种方案的缺点是:处理 N 次就要创建 N 个 RenderTarget,FrameBuffer 与纹理都占用显存,比较消耗内存。
方案二:两个 target 依次交换
如果处理是反复迭代的,其实只需要两个target,依次交换使用即可:
const root = new Mesh3d(...); layer.tick(() => { // 我们可以不将root添加到renderTarget上,这样就可以针对不同的renderTarget复用root layer.renderTarget(target1, {root}); root.program.uniforms.tMap.value = target1.texture; layer.renderTarget(target2, {root}); root.program.uniforms.tMap.value = target2.texture; layer.renderTarget(target1, {root}); ... });target1 → target2 → target1 → ...交替渲染,每次读取的是上一轮写出的结果,从而把内存占用从 N 个 target 降到 2 个。
方案三:双缓冲 buffer: true + swap()
更简洁的做法是:创建 RenderTarget 时传入配置项buffer: true,启用双缓冲,这样连第二个 target 都不需要,代码简化为:
const root = new Mesh3d(...); layer.tick(() => { // 我们可以不将root添加到renderTarget上,这样就可以针对不同的renderTarget复用root layer.renderTarget(target, {root}); root.program.uniforms.tMap.value = target.texture; target.swap(); layer.renderTarget(target, {root}); root.program.uniforms.tMap.value = target.texture; target.swap(); layer.renderTarget(target, {root}); ... });target.swap()交换缓冲与目标:渲染时写入「背面缓冲」,swap后再读到的target.texture就是刚渲染完的那一帧,天然规避了「一边读一边写同一块缓冲」的冲突。
保留缓冲区内容:layer.autoClear
默认情况下,每次执行layer.renderTarget都会清空之前缓冲区中的内容(clearColor默认由beforerender事件控制)。如果不希望清空,可以把layer.autoClear设为false,这样每次渲染会保留缓冲区中已有的内容,新内容直接叠加其上:
layer.autoClear = false; // 执行多次 renderTarget,缓冲区内容不丢失 layer.renderTarget(target, {root}); ... layer.autoClear = true; // 记得恢复Layer3d的autoClear属性(默认true)含义即「绘制前是否自动清除 layer 或 FrameBuffer」,见 docs/zh-cn/api/ext3d/layer3d.md。在迭代式渲染中关闭它还能省去大量gl.clear调用,提升性能。
源码佐证:流体模拟中的 RenderTarget 链式用法
仓库内的 demos/3d/post/index.js(对应文档 docs/zh-cn/ext3d/post.md)是一个把 RenderTarget 用到极致的 GPGPU 流体模拟示例,与本文主题高度相关,可以作为进阶参考:
- 多个 RenderTarget 分工:分别创建
density(染料密度)、velocity(速度场)、pressure(压强)、divergence(散度)、curl(旋度)等模拟场,尺寸和格式各不相同(如RGBA16F、RG16F、R16F),并通过supportRenderTextureFormat检测设备对渲染纹理格式的支持情况; - 半浮点纹理:在 WebGL2 下使用
gl.HALF_FLOAT与浮点内部格式,模拟场因此拥有比 8bit 更高的数值精度; - buffer + swap 迭代:
velocity、density、pressure均配置buffer: true,每完成一步模拟就swap()一次,将上一步结果作为下一步输入(splat、advection、divergence、pressure、gradient subtract 等多道 Pass 依次执行); - 关闭自动清屏:
layer.autoClear = false让模拟过程不再清除缓冲区,每帧结束时再恢复为true; - 全屏三角形复用:所有模拟 Pass 共用一个由
Geometry构造的全屏三角形网格,配合sort: false, update: false提升渲染效率。
这套写法正是「多个 RenderTarget + 双缓冲 swap + autoClear 控制」三种方案在生产级场景中的综合应用,也印证了本文上述各节所述机制。
小结与延伸阅读
RenderTarget 让 SpriteJS 3D 扩展具备了完整的离屏渲染能力:
- 离屏绘制:
new RenderTarget(gl, attrs)创建 FrameBuffer,元素照常append,用layer.renderTarget(target)触发渲染; - 结果回读:
target.texture作为 Program 的 texture 参与下一次渲染; - 多次处理:优先考虑
buffer: true+swap()的双缓冲方案,其次是两个 target 交换,最后才用多 target 叠加; - 内容保留:
layer.autoClear = false控制渲染前是否清空缓冲区。
如需进一步深入,可在仓库中继续查阅:
- RenderTarget 完整 API 参考(构造参数、属性、方法全表)
- Layer3d API 参考(
renderTarget、bindTarget、autoClear、setRenderOptions等) - 3D 扩展总览与安装方式
- 后期处理通道指南(
layer.post与 RenderTarget 的关系,官方指出后期处理本质上就是通过多个 RenderTarget 实现) - 可运行示例:demos/3d/render_target/index.js 与 demos/3d/post/index.js
- 图形学
- 3D渲染
【免费下载链接】spritejs
A cross platform high-performance graphics system.
相关推荐
Node Editor Framework核心组件解析:Node、Canvas、ConnectionPort深度剖析
Node Editor Framework核心组件解析:Node、Canvas、ConnectionPort深度剖析 Node Editor Framework
如何快速上手 CrossPoint Reader:从刷机到阅读的完整教程
如何快速上手 CrossPoint Reader:从刷机到阅读的完整教程 想要为你的 Xteink X3/X4 电纸书刷入开源固件 CrossPoint Rea
如何使用WaybackProxy在5分钟内访问历史网页:简单教程与配置指南
如何使用WaybackProxy在5分钟内访问历史网页:简单教程与配置指南 WaybackProxy是一款强大的HTTP代理工具,能够通过互联网档案馆Wayba
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考