Axmol 3.x 渲染系统重构:RenderTexture、相机投影与离屏渲染语义统一

这次 PR 是 Axmol v3 渲染重构中的一个重要步骤。它不只是移动了几个文件,也不只是给 RenderTexture 改了接口,而是在整理 Axmol 渲染系统里长期存在的几个隐式假设:

  • RenderTexture 的尺寸到底是 canvas 坐标,还是纹理像素?
  • 2D 渲染到底应该依赖 Director 的全局矩阵栈,还是当前 visiting camera?
  • 离屏渲染应该由 RenderTexture 这个对象自己控制,还是由明确的 render pass 控制?
  • 高 DPI、RenderTexture、Grid、ScrollView、VR 这些路径是否可以使用同一套坐标和投影逻辑?

PR 3208 的目标是让这些语义变得明确。

RenderTexture 不再隐式乘 content scale factor

以前 RenderTexture::create(width, height, ...) 的参数看起来像是普通尺寸,但内部会自动乘以 AX_CONTENT_SCALE_FACTOR()

这在一些老代码里很方便,但也带来一个长期问题:调用者并不清楚自己传入的是逻辑尺寸还是物理纹理尺寸。

这次修改后:

cpp 复制代码
RenderTexture::create(textureSize, ...)

里的 textureSize 就是实际纹理尺寸。内部不再偷偷乘 content scale factor。

如果调用者传入的是 canvas 坐标尺寸,并且希望保留高 DPI 下的物理分辨率,需要显式写出来:

cpp 复制代码
auto textureSize = Director::getInstance()->canvasToPixels(canvasSize);

auto rt = RenderTexture::create(textureSize,
                                PixelFormat::RGBA8,
                                PixelFormat::D24S8);

这让 API 语义更清楚:create() 创建的是指定物理纹理尺寸的 render target。

本 PR 同时提供了一个便利 API:

cpp 复制代码
RenderTexture::createForCanvas(size, ...)

它的语义是:传入 canvas 坐标尺寸,内部通过 Director::canvasToPixels() 转成物理纹理尺寸。也就是说,create(...) 面向物理纹理尺寸,createForCanvas(...) 面向 canvas 坐标尺寸。

RenderTexture 从 2D Node 行为中解耦

老的 RenderTexture 同时承担了很多职责:

  • 是一个可以加入场景树的 node;
  • 内部持有一个 sprite;
  • 可以 begin/end;
  • 可以控制矩阵和 viewport;
  • 可以保存图片;
  • 也代表一张 GPU texture / render target。

这使得它很难和现代 renderer、显式 render pass、VR、多后端渲染保持一致。

这次重构的方向是:RenderTexture 更接近一个纯粹的 texture-backed render target resource,而真正的渲染过程由 RenderTexturePass 控制。

新的逻辑更清晰:

cpp 复制代码
auto rt = RenderTexture::create(textureSize,
                                PixelFormat::RGBA8,
                                PixelFormat::D24S8);

auto pass = RenderTexturePass::obtain(rt);

pass->setViewport(viewport);
pass->begin();
pass->clear(ClearFlag::COLOR | ClearFlag::DEPTH, clearColor);

node->visit(renderer, transform, flags);

pass->end();
renderer->render();

pass->release();

RenderTexturePass 负责 begin/end、clear、viewport、render target restore、camera override 等渲染过程控制。RenderTexture 则保留为纹理资源和读回/保存等能力的载体。

2D 渲染改用 visiting camera view-projection

过去很多 2D draw path 直接读取:

cpp 复制代码
Director::getInstance()->getMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_PROJECTION)

这意味着 2D 渲染依赖 Director 的全局矩阵栈。

这次修改后,相关路径改为:

cpp 复制代码
Camera::getVisitingViewProjectionMatrix()

这影响了 SpriteLabelDrawNodeParticleSystemQuadAtlasNodeFastTMXLayerGrid 等多个渲染组件。

这样做的好处是:2D 和 3D 都可以依赖当前 visiting camera,而不是依赖一个全局 projection matrix。对于离屏渲染、Grid、VR、多 camera 渲染,这个模型更稳定。

移除 visit 中的 legacy matrix stack 依赖

一些 2D 节点过去会在 visit() 中 push/pop MATRIX_STACK_MODELVIEW。这属于旧渲染模型的兼容层。

这次重构移除了多处这种依赖,让节点渲染更多依赖自身的 _modelViewTransform 和当前 visiting camera。

这使得渲染状态更局部、更明确,也减少了渲染路径之间互相影响的可能。

Grid 渲染改为相机模型

Grid 效果过去会临时修改 Director 的 projection,再在结束时恢复。这在现代渲染管线下并不理想,尤其是当 RenderTexture、camera、VR、resize 等路径交织时。

现在 NodeGrid 使用一个专门的 canvas orthographic camera 来完成 Grid 捕获/绘制,Grid blit 也使用 visiting camera view-projection。

这避免了对全局 projection 的修改,也让 Grid 行为更符合新的 camera-based 渲染模型。

纹理尺寸 API 更明确

这次也替换了不少旧的纹理尺寸 API:

cpp 复制代码
getPixelsWide()  -> getWidth()
getPixelsHigh()  -> getHeight()
getContentSizeInPixels() -> getPixelSize()

这样可以减少"content size"和"pixel size"的混淆。

在新的语义下:

  • getWidth() / getHeight() 表示纹理像素尺寸;
  • getPixelSize() 表示纹理像素 size;
  • canvas 坐标和 node content size 不再和 texture pixel size 混用。

VR 渲染也受益于这次重构

Generic VR renderer 现在可以更明确地区分:

  • outputSize:最终 distortion 输出 viewport 尺寸;
  • textureSize:离屏 RenderTexture 的实际物理尺寸;
  • renderScale:VR 离屏渲染的超采样倍率;
  • eye viewport:每只眼在离屏纹理中的区域;
  • scissor transform:把普通 UI 裁剪区域映射到每只眼 viewport 中。

例如:

cpp 复制代码
vrRenderer->setRenderScale(2.0f);

这表示最终输出尺寸不变,但离屏渲染使用更高分辨率,以改善 distortion 后的采样质量。

这比把 VR 分辨率隐式绑定到 content scale factor 更清楚。

为什么这次改动重要

这次 PR 的核心不是"改几个 API 名字",而是把 Axmol 渲染系统中的几个空间概念拆清楚:

text 复制代码
canvas coordinates
texture pixels
render view viewport
render surface size
camera view-projection
offscreen render target

这些概念过去经常通过隐式转换和全局状态连接在一起。短期看方便,长期会让 RenderTexture、Grid、VR、resize、高 DPI、多后端渲染变得很难维护。

PR 3208 把这些隐式关系显式化,为后续 Axmol v3 的渲染架构打基础。

迁移成本

主要迁移点是 RenderTexture::create()

如果旧代码传的是 canvas 尺寸,并依赖内部自动 content scale,优先改成:

cpp 复制代码
auto rt = RenderTexture::createForCanvas(size,
                                         PixelFormat::RGBA8,
                                         PixelFormat::D24S8);

如果你想显式控制转换,也可以写成:

cpp 复制代码
auto rt = RenderTexture::create(Director::getInstance()->canvasToPixels(size),
                                PixelFormat::RGBA8,
                                PixelFormat::D24S8);

如果旧代码传的本来就是物理纹理尺寸,则继续使用 create(...),无需额外转换。

自定义渲染代码也应该从 Director projection matrix 迁移到 Camera::getVisitingViewProjectionMatrix()

总结

这次重构让 Axmol 的渲染语义更明确:

  • RenderTexture::create() 创建指定物理纹理尺寸的 render target;
  • 高 DPI 转换由调用者显式完成;
  • 2D 渲染使用 visiting camera;
  • Grid 不再修改全局 projection;
  • RenderTexturePass 负责离屏渲染过程;
  • VR 可以用明确的 render scale 控制离屏渲染质量。

这为后续更清晰的 RenderTexture、camera、VR 和 renderer 架构提供了基础。