《Cocos 游戏开发高级编程》4.3 自定义装配与渲染纹理

RenderTexture 是把渲染结果搬进一张纹理的载体。本节从 cc.d.ts 与引擎源码实证它的能力边界,讲清用 Camera.targetTexture 做离屏渲染、用自定义 Effect 做镜像、用 readPixels 做截图,并给出尺寸上限、坐标系与复用等实战陷阱。

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 后端 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「cocos」更多文章

  1. 《Cocos 游戏联机实战》9.3 延迟补偿与体验权衡
  2. 《Cocos 游戏联机实战》9.2 网络状态 UI 与提示
  3. 《Cocos 游戏联机实战》9.1 同步表现与插值动画