主题
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:
- 左侧使用
CEMapSSAO.xml,它会清理自己的颜色背景; - 右侧使用
GameOverlay.xml,它应只画出局部对象; - 两者使用相同的场景内容和相机参数;可共用 Scene/Camera,也可像官方示例一样使用两个结构相同的独立 SceneGraph;
- 将透明 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 蒙皮状态共享问题处理:
关闭 PIP,只保留主 View;再关闭主 View,只保留 PIP,确认单 View 是否正常。
对需要在两个 View 出现的每个
AnimatedModelComponent显式设置:csharpmodel.SimpleSkinning = true; model.ComputeShaderSkinning = false;换装角色要检查所有模型 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 合成,优先选择 |
Rgba16F | HDR 且需要保留 alpha |
Rgba32F | 高精度计算或特殊后处理,显存和带宽成本更高 |
Rg11B10 | HDR 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 可能与独立客户端不同。最终验收必须使用非内嵌运行:
- 先用普通 PIP 对照确认 Viewport 链路;
- 再让透明 PIP 的矩形覆盖主场景中颜色明显的对象;
- 使用纯客户端或
-no_debug_game_in_editor,让画面经过独立客户端的实际渲染链路; - 通过 Runtime MCP
debug.capture_screenshot抓真实渲染截图; - 检查 overlay 对象之外的区域是否仍能看到下面的主场景;
- 对普通 PIP、小地图或 RTT 预览检查雾效是否符合显式选定的 RenderPath,尤其确认远距离镜头仍能看到地面;
- 检查日志中没有
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 支持。