4.3 自定义装配与渲染纹理
前两节都在讲「光」。这一节换一个对象:渲染结果本身。RenderTexture 允许你把一次渲染画进一张纹理,而不是直接上屏。有了它,离屏渲染、镜像、小地图、以及最实用的截图,都是同一个机制的变体。
这一节从类型声明和引擎源码两头把它的能力边界钉死,再给三段可编译的代码。
4.3.1 从 cc.d.ts 认识 RenderTexture
在锁定版本 @cocos/creator-types@3.8.8 的 cc.d.ts 里,RenderTexture 的声明只有一屏:
grep -n "export class RenderTexture" node_modules/@cocos/creator-types/engine/cc.d.ts
输出形如 29757: export class RenderTexture extends ...TextureBase {。
它继承自 TextureBase,也就是说它首先是一张纹理,能被 SpriteFrame、Material 当普通贴图用。在此之上,它多了几个只有渲染目标才需要的成员:
| 成员 | 作用 |
|---|---|
reset(info) / initialize(info) | 按 { width, height } 初始化或重设尺寸 |
resize(width, height) | 改尺寸,声明里注明范围 1–2048 |
readPixels(...) | 把像素读回 Uint8Array(截图用) |
getGFXTexture() | 拿到底层 gfx 纹理 |
get window() | 渲染管线用的窗口,只读 |
引擎实现对应 cocos/asset/assets/render-texture.ts(tag 3.8.8)。注意 initialize / reset 的参数是 IRenderTextureCreateInfo,width 与 height 是必填,passInfo 可选——这意味着绝大多数场景里 rt.reset({ width, height }) 就够了,不必手写渲染通道。
4.3.2 离屏渲染:Camera.targetTexture
把一张 RenderTexture 挂到相机的 targetTexture 上,这个相机的输出就不再上屏。在 cc.d.ts 里它是一对存取器:
grep -n "get targetTexture" node_modules/@cocos/creator-types/engine/cc.d.ts
输出形如 27191: get targetTexture(): RenderTexture | null;。
下面的组件把「第二个相机渲染出的画面」搬进一张纹理,再挂到一个 Sprite 上显示:
import { _decorator, Camera, Component, RenderTexture, Sprite, SpriteFrame } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('OffscreenView')
export class OffscreenView extends Component {
@property(Camera)
sourceCamera: Camera | null = null;
@property(Sprite)
display: Sprite | null = null;
@property
rtSize = 512;
private _rt: RenderTexture | null = null;
start(): void {
if (!this.sourceCamera || !this.display) return;
const rt = new RenderTexture();
rt.reset({ width: this.rtSize, height: this.rtSize });
this._rt = rt;
this.sourceCamera.targetTexture = rt;
// 把渲染纹理当普通贴图用:套一个 SpriteFrame
const frame = new SpriteFrame();
frame.texture = rt;
this.display.spriteFrame = frame;
}
onDestroy(): void {
if (this.sourceCamera) this.sourceCamera.targetTexture = null;
this._rt?.destroy();
this._rt = null;
}
}
这里能成立,正是因为 RenderTexture 继承自 TextureBase——SpriteFrame.texture 要的就是一个 TextureBase。这是它和普通纹理唯一的区别:内容不是从磁盘来的,而是某个相机画上去的。
相机用哪个渲染输出,由两个属性决定:
priority:数值小的先渲染。离屏相机应当排在主相机之前,这样主相机采到的是它这一帧刚画好的结果。visibility:位掩码,决定这个相机看见哪些层。做小地图时,让离屏相机只看见「小地图可见」层,就不会把 UI 也画进去。
4.3.3 镜像:自定义装配与 UV 翻转
「镜像」听起来要重建几何,其实只需要翻转 UV。Cocos 的 2D 渲染分两层:装配器(assembler)负责把组件数据组装成顶点,材质负责怎么着色。内置装配器在 cocos/2d/assembler/sprite/ 下,有 simple.ts / sliced.ts / tiled.ts / bar-filled.ts / radial-filled.ts(tag 3.8.8)——分别对应 Sprite 的普通、九宫格、平铺、进度条、径向填充五种类型。
镜像不动装配器,只动材质。下面这个 .effect 通过一个 flip uniform 翻转 UV。需 Cocos Creator 3.8 编辑器打开,本节不可命令行复现(.effect 由引擎 Effect 编译器编译):
CCEffect %{
techniques:
- passes:
- vert: sprite-vs:vert
frag: sprite-fs:frag
properties:
flip: { value: [1.0, 1.0] }
%}
CCProgram sprite-fs %{
precision highp float;
in vec2 v_uv;
in vec4 v_color;
uniform FlipBlock {
vec2 flip; // (1,1) 正常, (-1,1) 水平镜像, (1,-1) 垂直镜像
};
#pragma builtin(local)
layout(set = 2, binding = 12) uniform sampler2D cc_spriteTexture;
vec4 frag() {
// flip = 1 时取 v_uv,flip = -1 时取 1 - v_uv
vec2 uv = mix(v_uv, vec2(1.0) - v_uv, (1.0 - flip) * 0.5);
return v_color * texture(cc_spriteTexture, uv);
}
}%
mix 的第三个参数是插值因子:flip.x = 1 时因子为 0,UV 不变;flip.x = -1 时因子为 1,u 变成 1 - u,画面水平翻转。切换镜像只要改一个 uniform:
import { _decorator, Component, Material, Sprite, Vec2 } from 'cc';
const { ccclass } = _decorator;
@ccclass('MirrorFlip')
export class MirrorFlip extends Component {
setFlipX(on: boolean): void {
const sprite = this.getComponent(Sprite);
const mat = sprite?.customMaterial;
if (!mat) return;
mat.setProperty('flip', new Vec2(on ? -1 : 1, 1));
}
}
更省事的做法是直接把节点
scale.x设成-1,但那会同时翻转子节点与碰撞体,做 UI 镜像时容易连带出错。用材质翻 UV,几何与逻辑坐标系都不变,只有画面翻转。
4.3.4 截图:readPixels
readPixels 把渲染纹理的像素读回 CPU。它是截图的唯一入口:
import { _decorator, Camera, Component, ImageAsset, RenderTexture, Texture2D } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('Snapshotter')
export class Snapshotter extends Component {
@property(Camera)
captureCamera: Camera | null = null;
private _rt: RenderTexture | null = null;
private ensureTarget(): RenderTexture {
if (!this._rt) {
const rt = new RenderTexture();
rt.reset({ width: 512, height: 512 });
this._rt = rt;
if (this.captureCamera) this.captureCamera.targetTexture = rt;
}
return this._rt;
}
/** 抓一帧,返回一张可直接当贴图用的 Texture2D */
capture(): Texture2D | null {
const rt = this.ensureTarget();
const pixels = rt.readPixels();
if (!pixels) return null;
const img = new ImageAsset({
_data: pixels,
_compressed: false,
width: rt.width,
height: rt.height,
format: Texture2D.PixelFormat.RGBA8888,
});
const tex = new Texture2D();
tex.image = img;
return tex;
}
}
三个必须知道的细节:
- 坐标系:
readPixels读出来的行序是从上到下、每像素 RGBA 四字节,与浏览器 Canvas 的ImageData一致,所以拼进 Canvas 时不用额外翻行。 - 时机:读像素必须发生在这一帧渲染之后。放在
update里读,拿到的往往是上一帧甚至空白。 - 代价:
readPixels会强制 GPU 同步回读,是一次阻塞操作。只能用于用户主动触发的截图,绝不能每帧调。
要在浏览器里导出成 PNG,再把 ImageData 丢给 canvas.toBlob() 即可——那一步是 Web 平台能力,不属于引擎 API。
4.3.5 性能与陷阱
RenderTexture 是显存大户,用错地方会直接爆内存:
| 陷阱 | 后果 | 对策 |
|---|---|---|
每帧 new RenderTexture | 显存碎片 + GC 抖动 | 创建一次,复用 |
| 尺寸开到 4096 | 被静默夹到 2048 | 声明上限就是 2048 |
忘了 destroy() | 纹理泄漏,切场景后仍在 | onDestroy 里释放 |
离屏相机 priority 排错 | 采到上一帧内容 | 让离屏相机先渲染 |
相机 visibility 没收窄 | 把 UI 也画进小地图 | 用独立层过滤 |
第二行值得展开:resize 的声明明确写了 1–2048(cc.d.ts 中 RenderTexture.resize 的注释,tag 3.8.8)。如果你按屏幕物理分辨率去开 RT,在某些高分屏上会得到一个被夹过的、比例失真的结果——要么按 2048 封顶设计,要么用多张 RT 拼接。
4.3.6 工程决策
把这套机制落到项目里,判断标准是「这张纹理多久更新一次」:
- 从不更新(静态小地图、预渲染背景):初始化时画一次,之后一直复用。成本几乎为零。
- 低频更新(每秒几次的小地图、镜子):用节流,不要每帧重画。
- 每帧更新(全屏后处理、实时反射):必须复用同一张 RT,且尺寸尽量小。
反面清单同样重要:不要用 RenderTexture 做「性能优化」的万能药。它把一次渲染变成「先画到纹理、再采样纹理」两步,本身是增加成本的;只有在「渲染结果要被复用多次」或「必须离屏」时,它才划算。卷二 13.3 的帧预算(60 FPS → 16.6 ms/帧)在这里是硬约束:一个每帧更新的全屏 RT,光是回读和重采样就可能吃掉几毫秒。
第 4 章到此结束。三节下来,我们把 2D 的高级渲染拆成了三件事:光照(着色器算)、阴影(几何或预烘焙)、渲染纹理(把结果搬来搬去)。下一章进入物理引擎内部,看 Box2D 后端是怎么接进 Cocos 的。
4.3.7 本节自检清单
- 知道
RenderTexture继承自TextureBase,可直接当贴图用 -
reset/resize尺寸在 1–2048 内 -
RenderTexture创建一次复用,不在循环里new -
onDestroy/onDisable里destroy()并置回targetTexture = null - 离屏相机
priority排在主相机之前 - 镜像用材质翻 UV,而不是改节点
scale -
readPixels只在用户触发的截图里调用,不每帧调 - 相机
visibility用独立层过滤,避免把小地图之外的内容画进去
阅读导航:上一节:4.2 阴影与光照贴图 · 下一节:5.1 内置物理与 Box2D 后端 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。