Skip to content

GameGraph 透明背景画中画 ​

本文说明如何把第二个 GameGraph 场景作为透明背景 Viewport 叠加到主画面,以及何时应改用 RenderToTexture。

两种画中画路线 ​

路线适用场景关键点
直接附加 Viewport第二个 3D 场景直接覆盖到主画面;透明区域应继续显示主场景使用 EngineRes/RenderPaths/GameOverlay.xml
RenderToTexture(RTT)结果要进入 GameUI、Canvas、材质贴图,或需要独立保存/处理使用带 alpha 的 RenderTargetFormat.Rgba8 / Rgba16F / Rgba32F

如果只是需要“主画面上再画一个角色或局部场景”,优先使用直接附加 Viewport。它不需要中间纹理,也不会额外经过 UI 图片系统。

不要依赖宿主默认 RenderPath ​

每个附加 Viewport 和 RTT Viewport 都必须显式调用 SetRenderPath(...)。未设置时,Viewport 可能回退到当前宿主的默认 RenderPath,而编辑器内嵌调试与独立客户端的默认路径并不等价:

  • 编辑器内嵌调试通常使用 EditorMapSSAOBloomHDR.xml,当前默认不呈现场景雾效;
  • 独立客户端和对战平台客户端通常使用 CEMapSSAO.xml,会呈现场景雾效;
  • 因此同一个普通 PIP、小地图或 RTT 预览可能在编辑器里清晰可见,到客户端却被雾遮住。远距离俯视小地图甚至可能整幅画面都看不到地面。

普通游戏镜头需要匹配客户端环境时显式使用 CEMapSSAO.xml;技术视图或小地图若设计上必须忽略雾,可显式使用 EditorMapSSAOBloomHDR.xml;透明附加画面使用 GameOverlay.xml。不要把编辑器里“刚好没有雾”的结果当作最终行为。

直接附加透明 Viewport ​

透明画中画建议使用独立 SceneGraph。这样 overlay 只包含需要叠加的对象,不会把主场景的地面、天空和环境再次绘制一遍。

csharp
#if CLIENT
using GameGraph.NodeSystem;
using GameGraph.NodeSystem.Component.Graphics;
using System.Numerics;

var overlayScene = new SceneGraph(createOctree: true);

var cameraNode = overlayScene.CreateChild("OverlayCamera")!;
cameraNode.Position = new Vector3(0f, -360f, 160f);
_ = cameraNode.LookAt(new Vector3(0f, 0f, 45f), Vector3.UnitZ);

var camera = cameraNode.CreateComponent<CameraComponent>()!;
camera.Fov = 48f;
camera.NearClip = 1f;
camera.FarClip = 2500f;
camera.AutoAspectRatio = true;

// 在 overlayScene 中创建需要叠加的灯光、模型和特效。

var overlayViewport = Renderer.CreateViewport(overlayScene, camera);
overlayViewport.SetRect(left: 464, top: 24, right: 880, bottom: 304);

if (!overlayViewport.SetRenderPath(
        "EngineRes/RenderPaths/GameOverlay.xml"u8))
{
    throw new InvalidOperationException("GameOverlay RenderPath not found.");
}

var overlayIndex = Renderer.GetNextFreeViewportIndex();
Renderer.SetViewport(overlayIndex, overlayViewport);
#endif

销毁场景时解除 Viewport:

csharp
Renderer.SetViewport(overlayIndex, null);
overlayScene.Dispose();

GameOverlay.xml 为什么透明 ​

核心不是 alpha blend,而是继续使用上一个 Viewport 已经写好的颜色目标:

xml
<renderpath
    finalCopy="false"
    useLastRenderTarget="true"
    substitute="true"
    zPrepass="true">
    <command type="clear" depth="1.0" />
    <!-- scene passes -->
</renderpath>
  • useLastRenderTarget="true":保留前一个 Viewport 的颜色目标;
  • clear 只指定 depth:清理 overlay 自己需要的深度,不清理已有颜色;
  • 不执行 Bloom 等全屏后处理:避免小窗再次处理整块主画面。

完整参考副本位于:

resources/rendering-reference/EngineRes/RenderPaths/GameOverlay.xml

不要删除 useLastRenderTarget。缺少该属性时,Viewport 和 Scene/Camera 都可能设置成功,日志也没有异常,但 overlay 最终不一定显示到主画面。

使用普通 PIP 做对照 ​

排查时同时放两个 Viewport:

  1. 左侧使用 CEMapSSAO.xml,它会清理自己的颜色背景;
  2. 右侧使用 GameOverlay.xml,它应只画出局部对象;
  3. 两者使用相同的场景内容和相机参数;可共用 Scene/Camera,也可像官方示例一样使用两个结构相同的独立 SceneGraph;
  4. 将透明 PIP 放到主画面颜色明显的区域上,避免黑色或空白背景让结果难以判断。
csharp
var legacy = Renderer.CreateViewport(pipScene, pipCamera);
legacy.SetRect(24, 24, 440, 304);
_ = legacy.SetRenderPath("EngineRes/RenderPaths/CEMapSSAO.xml"u8);
Renderer.SetViewport(Renderer.GetNextFreeViewportIndex(), legacy);

var transparent = Renderer.CreateViewport(pipScene, pipCamera);
transparent.SetRect(464, 24, 880, 304);
_ = transparent.SetRenderPath("EngineRes/RenderPaths/GameOverlay.xml"u8);
Renderer.SetViewport(Renderer.GetNextFreeViewportIndex(), transparent);

判断方式:

  • 两个都不显示:优先检查 Viewport 索引、Scene/Camera、相机裁剪和对象材质;
  • 普通 PIP 显示,透明 PIP 不显示:优先检查 GameOverlay.xml 是否为已修复版本、资源实际加载来源和 SetRenderPath 返回值;
  • 两个都显示,但透明 PIP 有底色:检查是否使用了普通 RenderPath,或 overlay Scene 是否包含天空、地面等覆盖背景的对象。

官方 code_sample 的 RendererViewportSample 提供这一双 PIP 对照。

疑难解答:同一骨骼模型在两个 View 中撕裂 ​

如果主镜头和 PIP / 小地图都能显示,但同一个骨骼角色只有在两个 View 同时渲染时出现肢体拉伸、骨骼撕裂或姿态错乱,先按多 View 蒙皮状态共享问题处理:

  1. 关闭 PIP,只保留主 View;再关闭主 View,只保留 PIP,确认单 View 是否正常。

  2. 对需要在两个 View 出现的每个 AnimatedModelComponent 显式设置:

    csharp
    model.SimpleSkinning = true;
    model.ComputeShaderSkinning = false;
  3. 换装角色要检查所有模型 part;如果普通蒙皮受设备骨骼 / uniform 上限限制,让 PIP 过滤角色是更稳妥的临时方案。

这只是兼容性规避。SimpleSkinning 是模型级开关,不是每个 View 的独立覆盖;当前纹理蒙皮 atlas 和 Compute 输出 Geometry 仍可能共享渲染状态。长期的 View-local 蒙皮隔离由引擎侧另行跟踪,完整的三路路径说明见 GameGraph 3D 骨骼动画。

RenderToTexture 保留 alpha ​

RTT 必须显式选择带 alpha 的格式。旧重载的 highDynamicRange: false 使用 RGB 格式,不提供可依赖的 alpha。

csharp
var texture = Texture2D.CreateRenderTarget(
    width: 256,
    height: 192,
    format: RenderTargetFormat.Rgba8);

var surface = texture.RenderSurface
    ?? throw new InvalidOperationException("RenderSurface not created.");

var rttViewport = Renderer.CreateViewport(rttScene, rttCamera);
rttViewport.SetRect(0, 0, 0, 0);
_ = rttViewport.SetRenderPath("RenderPaths/MyTransparentRtt.xml"u8);

surface.NumViewports = 1;
surface.SetViewport(0, rttViewport);
surface.UpdateMode = RenderSurfaceUpdateMode.UpdateAlways;
surface.QueueUpdate();

用于透明 RTT 的 RenderPath 应清为透明色:

xml
<command type="clear" color="0 0 0 0" depth="1.0" stencil="0" />

选择格式:

格式用途
Rgba8普通透明 UI 合成,优先选择
Rgba16FHDR 且需要保留 alpha
Rgba32F高精度计算或特殊后处理,显存和带宽成本更高
Rg11B10HDR RGB,无 alpha,不适合透明画中画

更新时机与像素回读 ​

RenderSurface.UpdateMode 控制 RTT 何时渲染:

模式行为
ManualUpdate只有调用 QueueUpdate() 才排队;适合逐帧控制或回读
UpdateVisible纹理被可见视口或材质实际使用时更新(默认)
UpdateAlways每帧都更新,即使当前不可见

QueueUpdate() 只是请求后续更新,不会立即渲染;IsUpdateQueued 会在请求交给渲染器后清除,不能作为渲染完成信号。需要读回时,先配置有效的 viewport 并 QueueUpdate(),让目标 RTT 实际经过一次渲染,再在后续更新中调用 GetImage()。在正常持续渲染时可安排到后续帧读取,但单凭 UrhoCore.Time.FrameNumber 前进不能证明指定 RTT 已画完;窗口暂停渲染或 viewport 无效时尤其如此。同一次更新中立即读取仍可能得到上一帧内容。

Texture2D.GetImage(gamma) 返回当前 GPU 纹理内容转换后的 8 位 Image,不是 HDR 原始值;调用会等待这次回读完成,但不会触发新的场景渲染。

GetImage 在纹理不可读、multiSample > 1 且 autoResolve = false,或格式转换失败时返回 null。

  • gamma == 1 时按纹理实际格式转换为 RGB/RGBA8;不做色调映射,也不会自动把 sRGB 解码为线性。
  • gamma != 1 时先把颜色限制到 0..1,再按 pow(value, 1 / gamma) 转到 8 位;alpha 不参与 gamma。
  • gamma 应为正数;0 或负值没有有效语义。
  • gamma 使用幂函数调整,不等同于标准 sRGB 的分段传递函数;已经编码的输出不要再无条件做一次 gamma 转换。
  • 超过 1 的 HDR 值在转成 8 位时会截断;即使 RTT 使用 Rgba16F / Rgba32F,也不要指望 GetImage 返回 HDR。
  • 结果是线性还是 sRGB 编码,取决于 RTT 的实际格式和 SRGB 状态以及 RenderPath 的后处理;例如带 GammaCorrection 的路径输出编码后的 LDR 字节,而线性 RTT 没有 Gamma 步骤时返回线性字节,PIE 线性输出还要考虑编辑器 UI 的最终编码。不能一概说成“色调映射后的 sRGB”。

用于黄金图对比时,固定 RTT 分辨率、格式、RenderPath、画质和采样设置,并给 8 位量化留出容差。

RTT 可通过 RuntimeTexture.Register / .Image(texture, name) 进入普通 GameUI,也可用 Canvas.DrawTexture 绘制。材质显示 RTT 时,不要为了绑定纹理直接调用依赖默认贴图的构造路径;先确认项目运行环境能解析相应默认材质资源。

验证要求 ​

多 Viewport 在编辑器内嵌调试中的 RenderTarget、坐标、编号和默认 RenderPath 可能与独立客户端不同。最终验收必须使用非内嵌运行:

  1. 先用普通 PIP 对照确认 Viewport 链路;
  2. 再让透明 PIP 的矩形覆盖主场景中颜色明显的对象;
  3. 使用纯客户端或 -no_debug_game_in_editor,让画面经过独立客户端的实际渲染链路;
  4. 通过 Runtime MCP debug.capture_screenshot 抓真实渲染截图;
  5. 检查 overlay 对象之外的区域是否仍能看到下面的主场景;
  6. 对普通 PIP、小地图或 RTT 预览检查雾效是否符合显式选定的 RenderPath,尤其确认远距离镜头仍能看到地面;
  7. 检查日志中没有 MissingMethodException、RenderPath 加载失败或 WASM 异常。

运行时资源边界要明确:编辑器安装目录和源码工作区是独立目录。修改源码仓库中的 EngineRes XML 后,不能仅凭 -use_local_res 推断纯客户端已经读取该文件。测试尚未发布的 XML 时,把它放进测试地图的 res/ 并使用唯一项目路径,或从日志/资源加载链取得明确证据;修改后重新启动客户端。

可读 XML 参考 ​

resources/rendering-reference/ 还提供:

  • CEMapSSAO.xml、GameSnapshot.xml;
  • KnownTechniquePath 对应的 14 个 Technique XML;
  • 默认 Material;
  • Day、Dusk、Night 光照预设。

这些文件用于理解原始配置,不会自动参与项目运行。Technique 引用的 shader/defines 仍需要已发布 shadercache 支持。

相关文档 ​