[{"content":"UE 虚拟人技术体系：MetaHuman、Live Link 面捕/动捕与动画模块集成 文章开头先放一个链接，PLAVE \u0026lsquo;Star Trace\u0026rsquo; M/V\n前几天刷 B 站，看到 PLAVE 的新 MV《Star Trace》。近几年我偶尔刷Melon榜单的时候会经常听到这个男团的作品，而且排名还非常的靠前，在韩国那个偶像产业极度内卷，近年来对于音乐作品要求逐渐变得更加严格而且趋向欧美化的情况下，这支虚拟男团还能再榜单占据一席之地并且开办自己的演唱会（JYP家团甚至经常进不了前十，Melon和JYP的爱恨情仇啊），这件事还是令我感到非常震惊。\n（前段时间还看过一个虚拟形象是狗的男团，给我整破绷了，很难想象制作人和团员的精神状态。）\n我真不行了\n这件事的分量，不在皮更好看。真正的变化是三条链路被拉直到同一个时间尺度上：表演捕捉、资产与绑定、实时渲染与合成。过去它们各走各的节奏 —— 动捕一天、渲染一夜、后期一周；现在被压进同一秒。虚拟制片也真正的加入到工业化生产流水线。\n把视线拉回国内，Asoul、星瞳，以及抖音上各家做的皮套人和饼干人，其实各自占据了这条光谱的不同位置：有的验证了直播陪伴的商业模式能不能持续变现，有的把大场直播的技术上限往上顶了一格，更多中小团队则在验证低成本、可日更的可行性。单看任何一家都不足以说明问题，三家拼起来才是一条完整的产业曲线 —— 从能播，到能看，到能上台。（甚至抖音自己就有皮套人商城）\n行业认可也已经不是媒体口径的自说自话。PLAVE 成为首个在韩国主流音乐节目拿到一位的虚拟偶像团体，又在体育场级别的场馆里完成演出；Epic 官方把他们作为 UE 集成管线的代表性案例对外讲述；Hololive 的 FLOW GLOW 用全篇 UE 制作完成了一整场免费 3D live。引擎厂商、音乐工业、演出主办方，三个原本互不相干的圈子在同一条产线上碰面。\n再往后看两三年，竞争点会从 \u0026ldquo;做得出来\u0026rdquo; 转向 \u0026ldquo;做得起、改得动、播得稳\u0026rdquo;。成本结构一旦降下来，虚拟制片就会从顶流专属的炫技，变成中型团队的标准配置；而面向动画师的工作界面正在被统一。\n本文将该链路划分为五层：资产（MetaHuman）、传输（Live Link）、解算与重定向（MetaHuman Animator、IK Retargeter）、驱动（AnimBP、Control Rig）、输出（Sequencer、录制与烘焙）。未标注部分为工程经验与取舍，不代表官方规范。\n0. 阅读说明 0.1 适用读者 角色 建议阅读范围 主要内容 美术 / 角色 第 2、4、5 章 MetaHuman 资产组成、面捕与动捕数据形态、常见表现问题的归因 TA / 技术美术 全篇，重点第 3、4、6 章 实时面捕、动捕重定向、分层动画与录制链路的搭建 动画 / 导演 第 1、4、6、9 章 实时与离线的质量边界、表演采集与录制流程 程序 / 主程 第 3、6、7、8 章 Live Link 数据模型、运行时边界、数据消费与排错 制片 / 技术负责人 第 1、7、9 章 虚拟人产线的设备、人员与验收项评估 0.2 核心结论 分层是排错的前提：资产、传输、解算、驱动、输出分别对应不同的故障模式。最终表现可能同为“角色不动”，但修复位置完全不同。 MetaHuman 的资产与运行时对象分离：Character 资产是源资产，经 Assembly 生成角色蓝图与运行时资产；修改 Character 后必须重新装配。 Live Link 的核心是三元组：Source 定义数据来源与传输方式，Subject 定义数据流名称，Role 定义数据契约；三者任一不匹配，消费端都无法获得数据。 静态数据与帧数据分离：StaticData 描述名称表、骨骼层级与属性 schema，只在连接或拓扑变化时推送；FrameData 每帧传输数值。静态数据重推会清空该 Subject 的帧缓冲，不能每帧调用。 面捕分为实时与离线两条路径：实时路径优先保证延迟与交互，离线路径优先保证质量与可重复性；两者的输入要求、处理流程与成本结构不同。 动捕的关键环节是重定向：源骨架与 MetaHuman 骨架不同，需通过 IK Retargeter 与 Retarget Pose 对齐；脚底滑动、腕部扭转、脊柱塌陷通常源于该环节。 动画集成以两套 AnimBP 为基础：身体与面部分别驱动，在角色蓝图、Control Rig 后处理与 Sequencer 层合流；单 AnimBP 方案会增加调试与复用成本。 实时流与最终资产分离：Live Link 适用于制作、预览与实时直播；需要稳定回放或交付时，应录制或烘焙为 AnimSequence、Montage 或 Level Sequence。 排错顺序固定：插件、Subject 名、Role、消费端（组件或 AnimNode）、骨骼与重定向、渲染与 LOD。 0.3 术语最小集 术语 定义 MetaHuman Epic 提供的高保真数字人资产与工具链，包含身份、身体、毛发、材质、绑定与动画 rig。 MetaHuman Creator 创建和编辑 MetaHuman Character 资产的工具；UE 5.6 起以内置资产编辑器形式提供。 Assembly（装配） 将 Character 资产转换为可在关卡中使用的角色蓝图与运行时资产的过程。 Live Link UE 的实时数据总线，以 Subject 为单位将外部设备或软件的数据送入引擎。 Source（源） 数据的来源与传输实现，例如 Live Link Face、动捕插件、视频源或音频源。 Subject（主体） 一条具名数据流；引擎通过 Subject 名定位对应的面部或身体数据。 Role（角色） 数据的类型契约，决定数据被解释为骨骼动画、单个 Transform 还是曲线。 Frame Data Subject 每帧携带的实际数据，包括骨骼 pose、曲线、Transform 与元数据。 Live Link Face Epic 的移动端面捕应用，将 ARKit 兼容的表情曲线与头部位姿实时发送至引擎。 BlendShape / Morph Target 通过目标形状相对基础网格的顶点差值加权叠加来实现形变；Maya 称 Blend Shape，UE 称 Morph Target。 In-Between Maya Blend Shape 的中间目标；开启时目标按顺序串联过渡，关闭时多个目标并行叠加。 MetaHuman Animator 基于视频、深度或音频离线生成面部与身体动画的工具集。 Retargeting（重定向） 将一套骨架的动画映射到另一套骨架；UE5 中主要由 IK Retargeter 完成。 Retarget Pose 重定向的对齐基准姿势；源骨架与目标骨架在该姿势下保持一致。 Control Rig UE 的程序化绑定与动画 rig 系统，用于面部与身体的后处理及手动修正。 AnimBP（动画蓝图） 运行时动画逻辑图；Live Link 数据通常通过它进入角色。 Post Process AnimBP 在骨骼动画求值之后执行的动画蓝图，常用于脚部 IK、骨盆修正等后处理。 Take Recorder 引擎内录制工具，将实时表演记录为可回放的 Sequence 或动画数据。 Timecode 时间码；用于面部、身体、音频与摄像机等多源数据的统一时间基准。 0.4 五层模型 flowchart LR subgraph CAP[\"采集 Capture\"] F[\"面部Live Link Face / 视频 / 音频\"] B[\"身体光学 / 惯性 / 手套\"] A[\"声音麦克风 / 音频文件\"] end subgraph TRANS[\"传输 Transport\"] LL[\"Live LinkSource → Subject → Role\"] end subgraph SOLVE[\"解算 / 重定向 Solve \u0026 Retarget\"] MA[\"MetaHuman Animator离线解算\"] IK[\"IK Retargeter身体映射\"] end subgraph DRIVE[\"驱动 Drive\"] ABP[\"AnimBP身体 / 面部\"] CR[\"Control Rig后处理 / 手动\"] end subgraph OUT[\"输出 Output\"] SEQ[\"Sequencer / Take Recorder\"] BAKE[\"AnimSequence / Montage\"] GAME[\"运行时角色\"] end F --\u003e LL B --\u003e LL A --\u003e LL LL --\u003e MA LL --\u003e IK MA --\u003e ABP IK --\u003e ABP ABP --\u003e CR CR --\u003e SEQ SEQ --\u003e BAKE BAKE --\u003e GAME各层的典型故障模式与首要检查项如下：\n层 典型表现 首要检查项 采集 表情僵硬、抖动、追踪丢失 设备状态、光照、遮挡、应用是否在前台 传输 Subject 列表为空或名称不符 网络、插件、端口、Subject 命名 解算 / 重定向 动作相似但比例异常、脚底滑动、腕部扭转 Retarget Pose、链映射、根运动 驱动 数据已到达但角色无响应 AnimBP 是否接入、Role 是否匹配、混合权重 输出 录制后回放异常或资产不完整 录制范围、资产引用、LOD、烘焙结果 0.5 目录 第 1 章 虚拟人的系统边界与产线划分 第 2 章 MetaHuman：资产、装配与运行时 第 3 章 Live Link：Source、Subject 与 Role 第 4 章 面捕：实时、离线与 MetaHuman Animator 第 5 章 动捕：光学、惯性与重定向 第 6 章 与动画模块集成：AnimBP、Control Rig、Sequencer 第 7 章 工程落地：流水线、性能与验收 第 8 章 常见问题与排查 第 9 章 最小实施路径与参数对照 总结 第 10 章 附录与参考 第 1 章 虚拟人的系统边界与产线划分 1.1 虚拟人与常规角色的差异 虚拟人并非普通角色资产的简单升级。二者在动画来源、面部复杂度、骨架来源、时间基准与交付形态上均存在差异：\n维度 常规游戏角色 虚拟人 动画来源 手工关键帧、状态机、动作库 真人表演驱动或离线解算 面部 少量表情与材质变化 数十至上百条表情曲线、眼球、舌头、牙齿 身体 标准骨架与固定动作库 多品牌动捕源、异构骨架、需保持体积与脚底接触 时间基准 游戏帧 拍摄时间码、音频采样、多机位同步 交付形态 实时运行 实时直播、影视级离线、预渲染 因此，虚拟人项目的主要工程量集中在表演数据的稳定采集、时间对齐与绑定映射，而非模型导入。\n1.2 实时与离线两条产线 维度 实时产线 离线产线 典型场景 直播、虚拟主播、虚拟发布会、预演 影视、CG 短片、高质量过场 面部输入 Live Link Face、摄像头、音频 深度视频（TrueDepth / 立体 HMC）、单目视频、音频 解算位置 设备端或引擎内实时推理 MetaHuman Animator 离线处理 延迟要求 数十至数百毫秒 不敏感，可反复迭代 质量上限 受算力、网络与追踪稳定性限制 较高，可人工清理与重解算 数据留存 需主动录制，否则表演不可复现 天然产出资产，可纳入版本管理 成本结构 设备、算力与现场人力 采集、算力与后期人力 设计原则：实时产线优先保证连续性与可恢复性，离线产线优先保证可重复性与可修正性。两条产线的资产、参数与采集规范应分别管理，避免混用。\n1.3 延迟构成 实时虚拟人的响应延迟由完整链路决定，网络仅是其中一环：\n环节 主要延迟来源 控制手段 采集 摄像头曝光、应用推理、追踪平滑 提高帧率、降低平滑、改善光照 传输 Wi-Fi 抖动、丢包、缓冲 有线或专用 AP、固定 IP、减少中间设备 引擎接收 Live Link 评估线程、插值 调整插值、检查 Subject 时间偏移 解算 视频源推理、离线处理 实时场景使用轻量源，离线场景独立处理 动画求值 AnimBP、Control Rig、后处理 降低更新频率、拆分后处理、启用 LOD 渲染 毛发、皮肤、眼球、光追 使用 Optimized 装配、降低 LOD、关闭非必要效果 若瓶颈位于采集端或无线网络，提升引擎侧算力不会显著改善延迟。\n1.4 案例分析：PLAVE 与实时虚拟偶像管线 PLAVE 是韩国 VLAST 推出的五人虚拟男团，2023 年出道。与完全由 AI 生成的虚拟形象不同，PLAVE 的角色由真人表演者通过动作捕捉与面部捕捉实时驱动；Epic 官方将其列为基于 Unreal Engine 的虚拟偶像案例[19]，路透社的报道则描述了其“动捕棚直连演出场地、表演者可实时看到观众反应”的演出方式[20]。从管线角度看，PLAVE 是 UE 实时虚拟制片能力在音乐演出场景中的落地案例，而非单纯的 3D 动画企划。\nPLAVE 五位成员的赛车主题造型（图片由用户提供）。\n图中展示了五人的赛车主题造型。发色、服装分区与配饰是角色识别的主要手段；在实时管线中，这些设计需要转化为可复用的材质、发片与绑定方案，而不能停留在静态插画层面。\n需要先区分路线：从公开形象看，PLAVE 的角色属于风格化 3D 形象，制作方法和角色资产构成与 MetaHuman 写实资产存在略微差异。二者共享“资产—传输—解算—驱动—输出”的管线结构，但预算分配不同：写实路线把成本投入皮肤、毛发、眼球与微表情；风格化路线把成本投入轮廓、材质分层、表情可读性与舞台表现力。\n层级 PLAVE 的公开做法 技术要点 资产 风格化角色、服装与发型，强调插画感与辨识度 卡通渲染、描边、发片与布料；角色 TD（Character TD）与绑定决定表情和动作上限 采集 真人表演者演唱与表演，动捕与面部捕捉驱动 动作、表情、口型与音频同步；需要校准与表演指导 传输 实时动捕数据进入引擎 低延迟、丢帧处理、时间码；现场网络与设备冗余 解算 / 驱动 实时重定向、表情映射、虚拟舞台 动作重定向、面部曲线映射、摄像机与灯光控制 输出 直播、演唱会、MV 与综艺内容 实时渲染与预渲染混合；现场大屏、直播推流与录制备份 从制作与技术管理视角看，PLAVE 的参考价值主要有三点：\n角色设计与技术预算匹配：风格化形象降低了写实渲染成本，但对轮廓、发型、服装与表情设计提出更高要求。角色越简洁，观众越容易把注意力放在表演与音乐上；这也意味着绑定和表情系统必须足够稳定。 实时链路与现场反馈闭环：VLAST 将动捕棚与演出场地直连，并让表演者看到观众反应[20]。这不是单纯的渲染问题，而是采集、传输、驱动、输出与现场导播的协同；任何一环延迟，都会破坏“人在现场”的感觉。 内容形态决定管线取舍：演唱会强调实时与低延迟，MV 与综艺可以使用预渲染或离线精修；同一套角色资产需要同时支持两种输出。制作上通常采用“实时预览 + 录制/烘焙 + 后期”的混合流程，而不是把所有内容都压在实时链路上。 公开资料有限但是可以从中提取出 PLAVE 的管线结构与技术要点，整体还是基于UE的实时虚拟制片管线。\n第 2 章 MetaHuman：资产、装配与运行时 2.1 MetaHuman 的资产组成 MetaHuman 由一组相互绑定的资产组成，而非单一骨骼网格，可分为外观资产与驱动资产两类：\n类别 资产 / 组件 作用 外观 身体网格、面部网格、牙齿、舌头、眼睛 最终渲染几何体 外观 皮肤、眼睛、毛发材质 决定真实感与着色表现 外观 Groom（毛发） 头发、眉毛、睫毛，独立资产与绑定 驱动 身体骨架与身体 Control Rig 身体姿态、IK、后处理 驱动 面部骨架与面部 Control Rig 表情、眼球、下颌、舌头 驱动 面部与身体 AnimBP 运行时动画逻辑 逻辑 角色蓝图 组装网格、组件、AnimBP 与碰撞 数据 身份、LOD、纹理、材质实例 由 Character 资产与装配流程生成 面部与身体是两套相对独立的绑定，各自拥有骨架、rig 与 AnimBP，最终在角色蓝图层组合为完整角色。\n2.2 从 Character 资产到可运行角色 在 UE 5.6 中，MetaHuman Creator 作为引擎内置的 Character 资产编辑器工作[13]。编辑完成的结果是 MetaHuman Character 资产，尚不能直接作为关卡角色使用；需通过 Assembly（装配） 生成角色蓝图与运行时资产[10]。\nflowchart LR A[\"MetaHuman Character 资产身份 / 外观 / 毛发 / 材质\"] --\u003e B{\"选择装配管线\"} B --\u003e|\"UE Cine\"| C[\"影视级装配最高保真\"] B --\u003e|\"UE Optimized\"| D[\"实时优化装配High / Medium / Low\"] B --\u003e|\"UEFN Export\"| U[\"UEFN 资产\"] B --\u003e|\"DCC Export\"| X[\"外部 DCC 资产\"] C --\u003e E[\"角色蓝图Body + Face + Groom + AnimBP\"] D --\u003e E E --\u003e F[\"放入关卡 / Sequencer / 录制\"]官方提供四种装配目标：UE Cine、UE Optimized、UEFN Export、DCC Export。其中 UE Optimized 与 UEFN Export 均提供 High、Medium、Low 三档优化级别，可按目标平台分配预算。\n管线 适用场景 代价与注意事项 UE Cine 过场、影视、预渲染、高质量直播 面数、毛发、材质与绑定开销较高，帧率压力大 UE Optimized 游戏运行时、长时间直播、VR 与中低端设备 官方定位为与 Cine 相近的保真度，并通过 High/Medium/Low 三档做性能取舍 UEFN Export 需要将角色导入 UEFN 项目 导出时 UEFN 必须关闭，优化档位分为三档 DCC Export 需要进入外部 DCC（如 Maya）的管线 进入外部工具后，材质、绑定与毛发需按目标 DCC 重新适配 注意：装配并非一次性步骤。修改 Character 资产（面部、材质、毛发、LOD）后必须重新装配，否则关卡中仍使用旧版本资产。\n2.3 身体与面部两套绑定 项目 身体 面部 骨架 MetaHuman 身体骨架，设计与 UE5 标准骨架兼容，便于重定向 面部骨架，配合大量控制曲线与形态目标 动画入口 身体 AnimBP、Live Link Pose、状态机 面部 AnimBP，通常由曲线驱动 Control Rig 动捕数据 骨骼 Transform（FK/IK） Blendshape / 曲线与头部位姿 常见问题 脚底滑动、腕部扭转、脊柱塌陷、比例异常 表情过强、眼球方向错误、口型不同步 由于面部与身体使用独立绑定，“面捕与动捕”通常对应两个 Subject 与两套 AnimBP。将二者合并到单一 AnimBP 会增加调试与复用成本，更常见的做法是分别驱动，在角色蓝图或 Sequencer 层合流。\n2.4 运行时对象与开销 对象 主要开销 优化方向 身体骨骼网格 骨骼数、LOD、材质、阴影 使用 Optimized 装配、降低 LOD、控制阴影与光追开销 面部骨骼网格 面部骨骼、形态目标、材质 启用面部 LOD、限制同屏可见数量 Groom 发丝数、阴影、模拟 降低发丝密度、启用 LOD、关闭非必要物理模拟 眼睛、牙齿、舌头 材质复杂度与透明 简化材质、合并渲染 pass 动画求值 AnimBP、Control Rig、后处理 降低更新频率、拆分后处理、启用 LOD 更新 Live Link 网络接收、推理、插值 减少 Subject 数量、固定帧率、分机处理 MetaHuman 的开销具有组合特征：单独评估毛发或皮肤时，开销尚在可接受范围；与实时面捕、光追同时启用时，预算会快速耗尽。应先确定目标平台与帧率，再选择装配管线。\n2.5 实施注意事项 区分 Character 资产与角色蓝图：Character 是源资产，关卡中应放置装配后的角色蓝图。 材质修改需追溯源资产：仅修改材质实例时，重新装配或切换 LOD 后可能被覆盖。 保持 LOD 同步：身体、面部与毛发各有 LOD，切换不同步会导致外观不一致。 确认 Groom 绑定：Groom 为独立资产，装配或迁移时需检查绑定关系。 第 3 章 Live Link：Source、Subject 与 Role 3.1 设计目的 若缺少统一的数据总线，每个设备厂商都需实现独立的数据注入逻辑，角色蓝图将被厂商 SDK 耦合。Live Link 的设计目标是将采集设备与引擎消费端解耦[5]：\n数据流可以概括为：设备或软件 → Source（传输实现） → Subject（命名数据流） → Role（数据契约） → 消费者（AnimBP / 组件 / 工具）。\n该结构带来的直接收益包括：\n面捕应用、光学动捕、惯性动捕与视频源可同时接入，互不干扰。 更换设备时只需更换 Source，角色侧的 Subject 名与 AnimBP 尽量保持不变。 同一份数据可被多个消费者读取，用于预览、录制、调试或多角色驱动。 3.2 三元组：Source、Subject 与 Role 概念 职责 示例 Source 定义数据来源与传输方式 Live Link Face、Vicon、Xsens、MetaHuman Video、Audio Subject 标识一条具名数据流 iPhoneFace、Actor01_Body、Webcam Role 定义数据的类型契约 Animation（骨骼与曲线）、Transform（单个变换）、Basic（曲线与元数据） 三者中，Subject 命名是最常见的错误来源。设备端填写的名称、引擎中选择的名称与 AnimBP 中读取的名称必须完全一致；大小写、空格与后缀均属于名称的一部分。\n3.3 Role 与数据面 Role 决定 Subject 的数据契约，同时约束静态数据与帧数据的具体类型：\nRole 静态数据 帧数据 典型用途 Animation 骨骼名称与父子索引、曲线名称表 骨骼 Transform、曲线值 身体动捕、面部表情驱动 Transform 跟踪标志（位置/旋转/缩放是否有效） 单个对象的 Transform 摄像机、道具、追踪点 Camera 镜头参数与跟踪标志 镜头 Transform 与参数 虚拟制片、镜头同步 Basic 曲线或属性名称表 属性 float 值 自定义参数、调试数据 Light 灯光参数与跟踪标志 灯光 Transform 与参数 虚拟制片灯光联动 面捕与动捕数据绝大多数落在 Animation Role 或 Basic Role。身体动捕通常提供完整骨架的 Transform，面捕通常提供表情曲线；头部位姿可能作为帧数据的一部分提供，也可能拆分为独立 Transform Subject，取决于源类型与配置。\n3.4 静态数据与帧数据：PushStaticData 的设计 Live Link 将 Subject 的数据拆分为两类生命周期不同的部分：\nStaticData（静态数据）：描述数据的结构，例如骨骼名称、骨骼父子关系、曲线名称表、跟踪标志。它不随帧变化。 FrameData（帧数据）：描述某一时刻的取值，例如骨骼 Transform、曲线 float 值、世界时间与到达时间。 维度 StaticData FrameData 变化频率 建立连接或拓扑变化时 每帧 内容 名称表、层级、属性 schema、跟踪标志 pose、曲线值、时间戳、元数据 传输方式 一次推送，必要时重推 持续推送 客户端处理 建立 Subject、分配缓冲、清空旧帧 写入缓冲、插值、评估 消费端依赖 名称映射、重定向缓存、曲线绑定 驱动骨骼 pose 与曲线值 缺失后果 Subject 无效、曲线缺失、T-pose 保持上一帧或停止更新 3.4.1 LiveLinkFaceSource 中的 PushStaticData Live Link Face 属于曲线型面捕源，位于 MetaHuman Live Link 插件的 LiveLinkFaceSource 模块：App 通过 UDP 发送表情曲线值与头部位姿，源侧接收并转发给 Live Link Client。PushStaticData 是源侧的静态数据登记入口，最终调用 ILiveLinkClient::PushSubjectStaticData_AnyThread(SubjectKey, Role, StaticData)[15]。\n对于这类源，静态数据的核心是表情曲线名称表。FLiveLinkBaseStaticData::PropertyNames 的定义即“每帧将发送的曲线值的名称”；帧数据中的 PropertyValues 按相同顺序提供对应的 float 值[16][17]。头部位姿属于每帧数据，是否包含头部旋转与位移由 ULiveLinkFaceSourceDefaults 中的默认配置项 bHeadOrientation 与 bHeadTranslation 控制[18]。具体静态数据类型可能由插件定义为派生结构，以下代码仅表示调用关系。\n// 结构示意：类型名与 Role 以所用插件版本为准 void FLiveLinkFaceSource::PushStaticData() { FLiveLinkStaticDataStruct StaticData(FLiveLinkBaseStaticData::StaticStruct()); FLiveLinkBaseStaticData\u0026amp; FaceStatic = *StaticData.Cast\u0026lt;FLiveLinkBaseStaticData\u0026gt;(); FaceStatic.PropertyNames = BlendShapeNames; // ARKit 52 条曲线名，顺序即帧值顺序 Client-\u0026gt;PushSubjectStaticData_AnyThread( FLiveLinkSubjectKey(SourceGuid, SubjectName), RoleClass, MoveTemp(StaticData)); } void FLiveLinkFaceSource::PushFrameData(const FFacePacket\u0026amp; Packet) { FLiveLinkFrameDataStruct FrameData(FLiveLinkBaseFrameData::StaticStruct()); FLiveLinkBaseFrameData\u0026amp; FaceFrame = *FrameData.Cast\u0026lt;FLiveLinkBaseFrameData\u0026gt;(); // 填充 WorldTime、PropertyValues 等字段 FaceFrame.PropertyValues = Packet.BlendShapeValues; // 与 PropertyNames 同序 Client-\u0026gt;PushSubjectFrameData_AnyThread( FLiveLinkSubjectKey(SourceGuid, SubjectName), MoveTemp(FrameData)); } 需要特别注意：PushSubjectStaticData_AnyThread 会清空该 Subject 已缓冲的所有帧。因此静态数据不能在每帧调用中重复推送，否则会持续丢弃帧缓冲并导致动画停滞或跳变。其合理调用时机包括：\nSubject 首次建立、设备首次连接。 设备断开后重连。 曲线集合或骨骼拓扑发生变化，例如应用切换 blendshape 集、固件升级或更换设备。 源设置改变了数据结构，例如启用或关闭头部位姿数据。 3.4.2 为什么这样设计 将结构数据与帧数据分离，主要基于以下考虑：\n降低带宽与序列化成本：52 条曲线名若每帧重复发送，会显著增加网络负载；名称表只需发送一次，帧数据只传 float 值。 先声明后赋值：Client 与消费端可以在值到达前建立名称映射、分配缓冲并校验 Role；这对后续加入的编辑器会话与重连场景尤为重要。 支持按名称绑定：重定向资产与 AnimBP 通过曲线名或骨骼名建立映射，而不是依赖数组下标；名称表是这套映射的基准。 支持动态拓扑：当设备或应用改变曲线集合时，重新推送静态数据即可重建 Subject，而无需重启引擎。 线程与内存友好：静态数据是不可变快照，可被多个消费端共享；帧数据只需填充与名称表等长的数值数组，减少每帧的名称开销与分配压力。 若只推送帧数据而没有可用的静态数据快照，Client 通常无法将该 Subject 视为有效，帧数据也不会进入正常的评估流程。因此 PushStaticData 是 Subject 生命周期中的必要步骤，而不是可选的优化。源侧与 Client 的接口约定、Source 的实现方式，参见 Live Link Plugin Development 文档[14]。\n3.4.3 数据类型的层次 除 StaticData 与 FrameData 这两个核心类别外，Live Link 还使用若干辅助类型描述 Subject 的身份、容器与时间信息：\n层次 典型类型 说明 标识 FLiveLinkSubjectKey、FLiveLinkSubjectName 由 SourceGuid 与 Subject 名定位一条数据流 Role ULiveLinkRole 及其子类 定义数据契约与消费方式 静态数据 FLiveLinkBaseStaticData、FLiveLinkSkeletonStaticData、FLiveLinkTransformStaticData、FLiveLinkCameraStaticData、FLiveLinkLightStaticData 名称表、骨骼层级、跟踪标志与角色专属 schema 帧数据 FLiveLinkBaseFrameData、FLiveLinkAnimationFrameData、FLiveLinkTransformFrameData、FLiveLinkCameraFrameData、FLiveLinkLightFrameData 每帧的 pose、曲线值、Transform 与参数 容器 FLiveLinkStaticDataStruct、FLiveLinkFrameDataStruct、FLiveLinkSubjectFrameData 类型擦除的传输容器，以及 Client 侧静态数据与帧数据的聚合 时间与元数据 FLiveLinkWorldTime、FLiveLinkMetaData、FLiveLinkFrameIdentifier 世界时间、场景时间码、帧标识与调试信息 源配置 ULiveLinkFaceSourceSettings、ULiveLinkFaceSourceDefaults 地址、端口、Subject 名与头部位姿开关 其中 FLiveLinkSkeletonStaticData 包含 BoneNames 与 BoneParents，FLiveLinkAnimationFrameData 包含与骨骼顺序一致的 Transforms；FLiveLinkBaseStaticData 则只包含 PropertyNames，由 FLiveLinkBaseFrameData::PropertyValues 按同序提供数值。Role 的作用在于把这些类型组合成消费端可解释的数据契约。\n3.5 数据如何到达 AnimBP Live Link 数据进入角色主要有三种方式：\n接入方式 适用对象 特点 Live Link Pose（AnimBP 节点） 骨骼动画 作为 AnimGraph 的 pose 源，可与状态机、分层混合 Live Link Controller（Actor 组件） Actor 组件与简单驱动 将 Subject 数据推送给组件，可选 Transform 或骨骼动画控制器 代码读取（ILiveLinkClient） 工具与自定义逻辑 灵活性最高，但需自行处理线程、时间与生命周期 Live Link Pose 的关键参数包括：\nSubject Name：指定读取的数据流。 Role：指定数据类型，通常为 Animation。 Retarget Asset：源骨架与目标骨架不同时所需的重定向资产。 Evaluate / Interp：控制是否评估与是否插值；插值可降低抖动，但会增加延迟。 从数据包到 AnimBP 的处理链路如下：\n源侧接收：LiveLinkFaceSource 通过 UDP 接收数据包，解析曲线值与头部位姿。 静态数据登记：首次连接或拓扑变化时调用 PushStaticData，向 Client 提交曲线名称表与 Role。 帧数据推送：每个数据包调用 PushSubjectFrameData，提交曲线值、时间戳与元数据。 Client 缓冲与评估：Client 保存静态数据快照与帧缓冲，按 Subject 设置进行插值、时间偏移与丢帧处理，生成评估帧。 消费端读取：AnimBP 的 Live Link Pose 节点或 Live Link Controller 在求值时取得评估帧。 重定向与映射：ULiveLinkRetargetAsset 依据静态数据中的骨骼名与曲线名，将源数据映射到目标骨架与目标曲线。 驱动与录制：AnimBP 输出骨骼 pose 与曲线，Control Rig 进行后处理；Sequencer 与 Take Recorder 消费最终结果。 需要强调：AnimBP 不会直接接收 StaticData 结构，它接收的是经过 Client 评估与 Retarget Asset 转换后的骨骼 pose 与曲线；StaticData 的作用是使这些骨骼名与曲线名在链路中可解析、可映射。\nsequenceDiagram participant APP as 面捕 App participant SRC as 源 Source participant LC as Client participant LN as Pose 节点 participant RA as Retarget participant ABP as AnimBP APP-\u003e\u003eSRC: UDP 数据包（每帧） SRC-\u003e\u003eLC: PushStaticData（连接或拓扑变化） SRC-\u003e\u003eLC: PushFrameData（每帧） Note over SRC,LC: StaticData 仅一次；FrameData 每帧 LC-\u003e\u003eLC: 缓存 + 插值 / 时间偏移 LN-\u003e\u003eLC: 请求评估帧 LC--\u003e\u003eLN: 评估帧 LN-\u003e\u003eRA: BuildPose / BuildCurve RA--\u003e\u003eLN: Pose + Curves Note over LN,RA: StaticData 名称表用于映射 LN--\u003e\u003eABP: FPoseContext ABP-\u003e\u003eABP: 分层混合 / Control Rig在这条链路中，StaticData 不参与每帧数值求值，但主要决定三个方面：Subject 是否有效、帧数据中的数组如何解释、消费端如何按名称建立映射。调试时应先确认静态数据结构与名称表是否符合预期，再检查帧数据是否持续到达。\n各模块的职责如下：\n模块 / 类型 职责 LiveLinkFaceSource 通过 UDP 接收数据，构造并推送静态数据与帧数据 LiveLinkInterface 定义 Role、静态数据、帧数据与 Subject 标识等基础类型 Live Link Client 管理 Source 与 Subject，维护静态数据快照与帧缓冲，生成评估帧 Live Link Pose / Live Link Controller 在 AnimBP 或 Actor 组件中消费评估帧 ULiveLinkRetargetAsset 依据名称表将源骨骼与源曲线映射到目标骨架与目标曲线 AnimBP 与 Control Rig 处理 pose 与曲线，执行面部/身体后处理 Take Recorder / Sequencer 录制、回放、剪辑并烘焙最终动画数据 3.6 时间、插值与同步 实时表演的时间一致性涉及四项因素：\nSubject 时间戳：源端时间与引擎时间不一定一致，Live Link 会进行缓冲与评估。 时间偏移：音画不同步时，应分别确认音频延迟、网络延迟与动画求值延迟，再决定调整哪一侧的 offset。 插值：插值可减少抖动，但会引入额外延迟；直播场景优先保证声画同步。 时间码：面部、身体、音频与摄像机合流时，Timecode 是比手动对齐更可靠的公共基准。 3.7 Live Link Hub Live Link Hub 是独立应用，用于接收、管理和录制实时动画数据，并向同一网络中的多个编辑器会话分发数据[4]。其适用场景包括：\n设备较多的单人场景：面部、身体与视频源集中管理，引擎仅负责消费。 多人或虚拟制片场景：现场多台机器与多个编辑会话共享同一批 Subject。 官方对资源竞争有明确说明：MetaHuman 的实时视频与音频源会大量占用 GPU；若 Live Link Hub 与 UE 运行在同一台机器，两个进程会竞争 GPU 资源，动画生成速度与质量可能下降。因此：\n可放在引擎内 Live Link 面板运行的源，优先放在引擎内，由单一进程统一调度。 必须使用 Live Link Hub 时，优先部署在另一台机器。 同机运行时，应通过 t.maxfps 为引擎与 Hub 设置帧率上限，避免无上限渲染耗尽 GPU 资源。 3.8 源类型与选型 源 数据 适用场景 注意事项 Live Link Face 表情曲线、头部位姿、音频 实时面捕、预演、直播 应用需在前台、同网段、Subject 名一致 MetaHuman Video Source 单目视频推理的表情 无移动设备、使用摄像头 占用 GPU；质量受光照与角度影响 MetaHuman Audio Source 音频驱动的口型与表情 仅有配音、无演员 表现力有限，适合口型精度要求不高的场景 MetaHuman Animator（离线） 深度或单目视频解算 影视、高质量过场 非实时源，产出为资产 光学动捕（Vicon、OptiTrack 等） 高精度骨骼 影视、舞台、多人捕捉 场地、标定、反光点与成本 惯性动捕（Xsens 等） 骨骼 Transform 外景、快速部署 漂移、脚底接触、磁干扰 手套（Manus 等） 手指骨骼 精细手部表演 与身体骨架的合并与重定向 自定义 Source 任意数据 内部工具、特殊硬件 需自行实现传输、时间与错误处理 第 4 章 面捕：实时、离线与 MetaHuman Animator 4.0 概念全景与渲染链路 没有对系统和管线的改造需求，这节可以跳过。这节内容记不住也没关系，我也记不住\n面捕数据最终要改变屏幕上的面部形状，中间涉及两类形变数据：骨骼变换（骨骼通道）与顶点偏移（顶点通道）。二者不是两条并行管线，而是在顶点处理的同一阶段先后生效；BlendShape/Morph Target、Control Rig、蒙皮与遮罩分别处在这条链路的不同位置。如果想要全面的了解相关的知识点，需要先建立这张概念地图，再进入 4.1 的面捕数据与后续的 DCC 处理。\n概念 所在层 作用 主要输出 BlendShape / Morph Target 顶点形变 保存目标形状相对基础网格的顶点偏移，按权重叠加 Morph Target 权重、顶点 delta Control Rig 骨骼控制 将控制值转换为骨骼变换、IK 与校正 骨骼 Transform 蒙皮与遮罩（Skinning / Mask Skin） 顶点绑定 每个顶点记录骨骼索引与权重，遮罩用于限定或覆盖区域权重 蒙皮后的顶点 AnimBP 动画逻辑 状态机、分层、曲线与 Pose 求值 Pose、Curves Live Link 数据传输 将面捕设备的帧数据送入引擎 帧数据、曲线 渲染 光栅化与着色 将顶点数据转换为像素 屏幕图像 “Mask Skin”通常指带遮罩的蒙皮权重层：蒙皮决定骨骼变换如何分配给顶点，Mask 用于按区域限制、排除或覆盖权重；UE 中可用 Skin Weight Profile 切换或覆盖权重。骨骼变化只影响权重非零的顶点，蒙皮本身不会改变骨骼或控制值。\n一个骨骼变化从输入到屏幕，通常经过以下阶段。需要注意的是，顶点形变与蒙皮不是两次独立遍历，而是在同一个顶点着色器内先后执行，因此合并为「顶点处理」一个阶段：\n阶段 发生了什么 关键数据 / 对象 1. 输入 Live Link 曲线或面板控制值进入动画系统 帧数据、曲线、FRigControlValue 2. 动画求值 AnimBP/Retarget 生成 Pose 与 Curves；Control Rig 求值控制值 FPoseContext、骨骼局部 Transform 3. 骨骼层级 局部 Transform 逐级累乘为组件空间变换，再与绑定姿态求逆得到蒙皮矩阵 骨骼 Transform、蒙皮矩阵调色板 4. 顶点处理 先叠加 Morph Target 的位置与切线 delta，再按权重混合蒙皮矩阵，Mask 限定参与区域 MorphTargetWeights、顶点 delta、Skin Weight、Skin Weight Profile 5. 渲染 顶点着色、光栅化、材质、阴影与后处理 屏幕像素 sequenceDiagram participant IN as 输入 participant EV as 动画求值 participant SK as 骨骼链路 participant VP as 顶点处理 participant RD as 渲染 IN-\u003e\u003eEV: 曲线 / 控制值 EV-\u003e\u003eEV: 控制值 → 骨骼局部 Transform EV-\u003e\u003eSK: 骨骼局部 Transform SK-\u003e\u003eSK: 逐级累乘 → 组件空间 SK-\u003e\u003eSK: 乘绑定姿态逆矩阵 → 蒙皮矩阵 SK-\u003e\u003eVP: 蒙皮矩阵调色板 VP-\u003e\u003eVP: 叠加 Morph delta（参考姿态空间） VP-\u003e\u003eVP: 按权重混合蒙皮矩阵 VP-\u003e\u003eRD: 顶点位置 / 法线 / 切线 RD-\u003e\u003eRD: 光栅化 / 材质 / 阴影 / 后处理 RD--\u003e\u003eIN: 屏幕结果关系可以概括为两条不同的驱动路径，它们在顶点处理阶段相遇：\n骨骼路径（Control Rig / 骨骼动画）：Control Rig 实实在在地改变骨骼变换，骨骼链路将其逐级累乘，再乘绑定姿态逆矩阵得到蒙皮矩阵；蒙皮阶段用这些矩阵把顶点搬到新位置，于是姿态、轮廓与表情随骨骼运动而变化。\n顶点路径（Morph Target）：Morph Target 不产生任何骨骼数据，它只改顶点——在参考姿态的网格顶点上叠加 delta。这里要注意 delta 的基底是网格顶点，而非骨骼数据；delta 也不改变蒙皮权重与蒙皮矩阵，它改变的是「即将被蒙皮的顶点」，因此蒙皮结果随之变化，这些顶点仍会被同一套矩阵带着跟随头部运动。\n路径 驱动对象 关键中间数据 最终影响 Sequencer 中的关键帧对象 骨骼路径 骨骼局部 Transform 蒙皮矩阵调色板 顶点随矩阵移动，姿态与轮廓改变 Control Rig 控制值、面部骨骼 顶点路径 参考姿态顶点上的 delta Morph Target 权重、顶点偏移 顶点先在局部空间偏移，再走同一套蒙皮 Morph Target 曲线 两条路径都能表达表情，只是载体不同，也因此可以混用：Sequencer 中既能给 Control Rig 控制值或面部骨骼打帧，也能给 Morph Target 曲线打帧。MetaHuman 的面部以骨骼与 Control Rig 为主，Morph Target 更多承担局部校正与辅助形变（见 4.1.3）。Live Link 与面板都只是输入源，本身不直接产生最终画面。\n4.0.1 蒙皮：骨骼如何带动顶点 蒙皮解决的是「顶点不知道骨骼在动」这个问题。顶点与骨骼之间没有直接引用，二者靠导入时烘焙的一份绑定信息建立对应，运行期只做查表与加权求和。静态侧有三份只读数据：\n数据 内容 何时确定 参考姿态顶点 绑定姿态（reference pose）下的 Position / Normal / Tangent 导入 每顶点骨骼绑定 BoneIndices 与 BoneWeights，默认 4 个影响 导入，可被 Skin Weight Profile 覆盖 每骨骼绑定矩阵 绑定姿态下该骨骼组件空间全局变换的逆 导入时烘焙 动态侧，动画、Control Rig 与 IK 的输出都只是骨骼相对父骨的局部变换，需要逐级累乘后再折算回绑定姿态：\n$$G_b = G_{parent} \\cdot L_b, \\qquad M_b = G_b \\cdot B_b^{-1}$$$M_b$ 即蒙皮矩阵，也是唯一真正作用到顶点上的量。它有一个自校验性质：骨骼回到绑定姿态时 $G_b = B_b$，于是 $M_b = I$，顶点完全不动。所有「骨骼没动就不该变形」的正确性都由这一步保证。\n顶点按索引取到自己的若干蒙皮矩阵后做加权求和，即线性混合蒙皮（LBS）：\n$$\\vec{v}' = \\sum_{j} w_j \\, M_{b_j} \\vec{v}_{ref}, \\qquad \\sum_j w_j = 1$$运动与形变来自同一个式子的两种取值：权重集中在一根骨骼上的顶点整体随该骨骼做刚体运动，表现为位移与旋转；权重跨两根骨骼的顶点被两个矩阵拉向不同方向，混合结果落在中间，表现为关节处的弯曲、挤压与拉伸。关节形变并不是额外施加的形变，而是权重渐变把离散的刚体运动变成了连续场。\n法线与切线需用蒙皮矩阵变换后重新归一化，严格做法是取逆转置；骨骼矩阵为正交时两者等价，存在非均匀缩放时才有差别。漏掉法线变换的典型症状是光照不随肢体弯曲变化，皮肤呈塑料感。\nLBS 的固有缺陷来自「对矩阵做线性插值」，而旋转不在线性空间中：关节大幅扭转时混合后的矩阵不再正交，顶点被向内吸，出现糖纸效应与体积塌陷。常见对策如下。\n方案 做法 代价 双四元数蒙皮（DQS） 以对偶四元数插值代替矩阵插值 需管线支持，扭转表现与 LBS 不同 校正 Morph Target 在极端姿态下雕刻修正形状并驱动 美术工作量 Delta Mush 对蒙皮结果做平滑后再回贴 额外计算 Pose Driver / ML Deformer 按姿态规则或学习模型修正 训练与部署成本 UE 侧的关键实现是：所有 $M_b$ 组成蒙皮矩阵调色板上传 GPU，顶点着色器按 BoneIndices 索引；启用 Skin Cache 后蒙皮结果写入缓存，供深度、阴影、GBuffer 等后续 Pass 复用，避免每 Pass 重算；每顶点影响数默认为 4，可在项目中提高，代价是顶点数据体积与带宽。\nflowchart LR R[\"参考姿态顶点Position / Normal / Tangent\"] --\u003e W[\"按 BoneIndices取对应矩阵\"] P[\"骨骼局部变换动画 / Control Rig / IK\"] --\u003e G[\"逐级累乘得到组件空间变换\"] G --\u003e M[\"乘绑定姿态逆矩阵得到蒙皮矩阵\"] M --\u003e W S[\"BoneWeights默认 4 个影响\"] --\u003e V[\"加权求和混合各骨骼矩阵\"] W --\u003e V V --\u003e O[\"组件空间顶点再转世界空间与渲染\"]4.0.2 顶点处理内部的顺序 在同一顶点着色器内，Morph 与蒙皮的执行顺序是固定的「先 Morph、后蒙皮」：\nPosition = RefPosition + Σ w_i · delta_i // 参考姿态的局部空间 Position = Σ_j w_j · M_bj · Position // 蒙皮到组件空间 Position = ComponentToWorld · Position // 到世界空间 这个顺序有实际意义：Morph delta 是相对参考姿态雕刻出来的局部偏移，先叠加再蒙皮，表情就会跟随头部旋转与身体运动；若反过来先蒙皮再叠加未经变换的 delta，表情偏移不参与骨骼转动，转头时五官会「留在原地」并把面部撕开。\n顺序 表情是否随头转动 表现 Morph → 蒙皮 是 delta 与顶点同处参考空间，一起被骨骼带走，符合预期 蒙皮 → Morph 否 delta 未经过骨骼变换，转头时五官错位、表面撕裂 4.1 面捕的数据形态 面部捕捉的原始输出通常为一组归一化系数或曲线，而非最终表情。以 ARKit 体系为例，Live Link Face 发送约 52 条 BlendShape 曲线、头部位姿与音频，覆盖眉、眼、鼻、颊、嘴、下颌与舌头等区域。\n数据到达 MetaHuman 面部前还需经过解算与映射：\nflowchart LR A[\"表演面部与声音\"] --\u003e B[\"追踪BlendShape / 深度 / 视频\"] B --\u003e C[\"解算表情系数 → rig 控制\"] C --\u003e D[\"重定向源面部 → 目标 MetaHuman\"] D --\u003e E[\"后处理眼睑 / 眼球 / 舌头 / 修正\"] E --\u003e F[\"面部 AnimBP / Control Rig\"] F --\u003e G[\"渲染\"]ARKit 的 52 条曲线与 MetaHuman 的面部控制并非一一对应。MetaHuman 的面部包含更多控制项（眼睑、眼球、舌头、牙齿、面颊细节等），因此中间必然存在映射与补全；这也是同一段面捕数据应用于不同面部资产时表现存在差异的原因。\n4.1.1 BlendShape 的基本概念 BlendShape（混合形状）是一类基于顶点差值的形变技术。它先保存一组目标形状（target shape）相对于基础网格（base mesh）的顶点偏移，再按权重将偏移叠加到基础网格上。其数学形式可概括为 $V(w) = V_{base} + \\sum_{i} w_i \\left( V_i - V_{base} \\right)$，其中 $w_i$ 是第 $i$ 个目标形状的权重。\nflowchart LR B[\"Base 顶点静态参考数据\"] --\u003e A[\"加权累加\"] D1[\"目标 A 的 deltaV_A − V_base\"] --\u003e M1[\"× w_A\"] D2[\"目标 B 的 deltaV_B − V_base\"] --\u003e M2[\"× w_B\"] D3[\"目标 N 的 deltaV_N − V_base\"] --\u003e M3[\"× w_N\"] M1 --\u003e A M2 --\u003e A M3 --\u003e A A --\u003e O[\"输出顶点 V(w)写入新的顶点缓冲\"]为什么是累加\n当只有一个目标且 $w=1$ 时，$V = V_{base} + (V_i - V_{base}) = V_i$，$V_{base}$ 被自己抵消，结果正好落在目标形状上——这正是「把差值直接加到 base 上」的直觉写法。累加是为了覆盖多个目标同时生效的情形：微笑与张嘴往往并发，两者的偏移都必须计入，因此 $\\sum_i$ 遍历的是目标形状列表，而不是顶点。\n把式子展开可以看清 base 的地位：\n$$V(w) = \\left(1 - \\sum_i w_i\\right) V_{base} + \\sum_i w_i V_i$$base 相当于一个权重为 $1 - \\sum_i w_i$ 的「第 0 个形状」：所有 $w_i = 0$ 时余项为 1，完全回到 base；$\\sum w_i = 1$ 时 base 被抵消，结果退化为目标形状的加权平均；$\\sum w_i \u0026gt; 1$ 时余项为负，base 被反向扣除，于是出现过冲。\n两层遍历\n计算中有两层循环，只有一层写进了公式：外层遍历目标形状（显式的 $\\sum_i$），内层遍历所有顶点（隐式，逐元素运算）。单看第 $k$ 个顶点：\n$$v_k(w) = v_k^{base} + \\sum_i w_i \\left( v_{k,i} - v_k^{base} \\right)$$顶点之间互不搬运位移，第 $k$ 个顶点只吸收自己在各目标形状中对应的 delta。因此各顶点的求值完全独立，可任意并行；但也意味着顶点索引一旦变化，delta 就会错位——这就是后文 Check Topology 与重拓扑后需用 Wrap deformer 传递形变的根本原因。\nbase 必须是静态只读\n每次求值都从同一份 base 重新计算，结果写入另一份顶点缓冲，base 本身永不修改。这使该运算成为纯函数：输出只取决于 base、delta 与权重，与之前算过几次、以什么顺序算无关。若改成把偏移累积写回当前顶点，权重从 0.9 突降到 0.1 时旧偏移无法撤销，每帧重复求值还会持续漂移，多系统叠加也会变成顺序敏感。\n性质 累积写回 base + delta 权重突降 已加的偏移撤不回 从 base 重算，天然正确 重复求值 误差与偏移不断叠加 无状态，结果恒等 多系统叠加 顺序敏感 加法可交换，顺序无关 并行求值 有读写依赖，只能串行 只读 base、只写输出，可完全并行 两个使用约束\n权重不做归一化，因此 $w_i$ 可以小于 0（反向）或大于 1（夸张外推），这是表现力的来源，也是穿插与体积膨胀的来源，需要校正形状兜底。另外必须保持基础网格与目标形状的顶点数量、顺序、拓扑完全一致，否则按下标对应的 delta 会整体错乱。\n目标形状除顶点位置外，还可携带法线与切线增量；引擎在合成时据此更新位置与着色数据，并对法线重新归一化，否则光照会随表情变化而变暗或翻转。BlendShape 适合局部、可叠加的形变，例如表情、口型与肌肉隆起；大范围旋转、体积保持与肢体运动通常交给骨骼或 Control Rig。\n软件 / 引擎 常用名称 数据组织 Autodesk Maya Blend Shape blendShape 节点，基础网格与多个 target shape Unreal Engine Morph Target SkeletalMesh 的顶点 delta，配合曲线或权重 Blender Shape Key basis 与多个 shape key 3ds Max Morpher Morpher 修改器与多个 channel 4.1.2 Maya 中的 BlendShape Maya 将 Blend Shape 实现为一个 deformer：基础网格加一个或多个目标形状，由 blendShape 节点的权重控制[29]。典型创建流程是复制基础网格、雕刻目标表情，先选择目标形状、最后选择基础网格，再执行 Create Deformers \u0026gt; Blend Shape。\n属性 作用 面部绑定常用设置 Envelope 整体形变强度 1.0 Origin 相对基础形状的 Local 或 World 变换 Local In-Between 目标串联或并行混合 关闭（并行） Check Topology 校验基础与目标拓扑是否一致 开启 Deformation Order deformer 在历史中的位置 Default In-Between 是 Maya Blend Shape 的关键选项。开启时，目标形状按选择顺序串联，形变会从一个目标过渡到下一个目标，适合下巴张开等分段变化；关闭时，多个目标并行叠加，适合多个独立表情的组合。面部绑定通常关闭 In-Between，以便用多个基础表情组合出更丰富的表情。\nBlend Shape 的权重可以关键帧、Set Driven Key 或表达式驱动，Shape Editor 用于管理目标形状。基础网格与目标形状必须保持相同的顶点数量与顺序；拓扑不一致时，需要先重拓扑或使用 Wrap deformer 传递形变。校正形状（corrective shape）通常在极端姿态下复制基础网格并雕刻修正，再作为额外目标加入，由组合权重或驱动关键帧控制。\n4.1.3 UE 中的 Morph Target UE 将 BlendShape 称为 Morph Target，并将其作为 SkeletalMesh 的顶点形变数据。通过 FBX 导入时启用 Import Morph Targets，Maya 的目标名称会成为 Morph Target 名称，通常为 [BlendShapeNode]_[BlendShape][26]。Morph Target 按 LOD 存储，每个目标记录顶点位置增量与切线 Z 增量，运行时按权重叠加。\nUE 中常见的驱动方式有三类：\n蓝图显式设置：在 Event Graph 中使用 Set Morph Target 节点，目标为 SkeletalMeshComponent，输入名称与 0–1 的权重[27]。 动画曲线驱动：在 Skeleton 资产上建立与 Morph Target 同名的曲线，在 AnimSequence 中为曲线打关键帧；SkeletalMeshComponent 维护 MorphTargetCurves，按名称将曲线值写入对应的 Morph Target 权重[28]。 Live Link 曲线驱动：Retarget Asset 的 BuildCurveData 将面捕曲线写入 PoseContext；若曲线名与 Morph Target 名称一致，最终会驱动对应的顶点形变。MetaHuman 的面部并非纯 BlendShape 方案，而是由控制曲线驱动 Control Rig、骨骼与校正形态；BlendShape 在其中主要承担局部校正与辅助形变。 UE 中的 MetaHuman 面部 Control Rig 面板会在面部显示一组控制点与控制器，并按眉、眼、鼻、口、下颌等区域组织。选中控制点后，可在视口中移动它，直接写入 Control Rig 控制值，再由 Control Rig 驱动对应面部骨骼。部分工具会把这些控制项进一步组织为滑块或属性面板，但核心仍是 Control Rig 控制值，而不是 Morph Target。它主要用于手动摆 pose、检查曲线映射与录制预览，也可在 Sequencer 中对控制值打关键帧。\nUE 编辑器中的 MetaHuman 面部 Control Rig 面板，黄点为面部控制点，可用于手动调整表情与骨骼。\n控制方式 数据去向 最终结果 面部控制面板控件（控制点 / 滑块） Control Rig 控制值 面部骨骼旋转或位移 Set Morph Target Morph Target 权重 顶点位置增量 动画曲线 / Live Link 曲线值 由 Retarget Asset 或 Control Rig 映射到骨骼或 Morph Target 如果某个控制项最终驱动的是 Morph Target 曲线，它才会间接改变顶点形变；MetaHuman 面部板仍以骨骼与 Control Rig 为主。调试面捕时，可以先在面板中移动控制点，确认目标骨骼或控制项是否有响应，再回到 Live Link 曲线检查输入，从而区分“曲线没有进入”和“绑定没有响应”两类问题。\n对于自定义曲线，通常需要在 Skeleton 的 Curve Metadata 中将其标记为 Morph Target，动画与 AnimBP 才会把它识别为形态目标曲线；导入的 Morph Target 若缺少对应元数据，也应在 Skeleton 中补齐。\n性能方面，每个处于激活状态的 Morph Target 都会参与顶点计算，目标数量、网格顶点数与 LOD 共同决定开销。Morph 与蒙皮共享同一次顶点处理（见 4.0.2），启用 GPU Skin Cache 后二者在同一顶点任务内完成，不会额外遍历一遍顶点，但增量仍随激活目标数与顶点数线性增长。应在 Project Settings \u0026gt; Rendering \u0026gt; Optimizations 中按目标平台决定是否启用 Use GPU for computing morph targets[27]，并通过删除无用目标、按 LOD 配置目标、控制校正形状数量来维持预算。\n4.1.4 从 Maya 到 UE 的交付要点 环节 Maya 侧 UE 侧 检查项 拓扑 基础与目标保持同拓扑 Morph Target 按顶点索引匹配 Check Topology、导入日志 命名 目标名称清晰、唯一 [BlendShapeNode]_[BlendShape] Morph Target 列表与曲线名 In-Between 串联目标 导入后验证映射；不符合预期时烘焙为独立目标 权重范围与过渡效果 法线 / 切线 导出法线与切线 切线 Z 增量参与形变 高光与阴影检查 变换 冻结变换、面部使用 Local 不应叠加额外变换 rest pose 对比 数量 控制目标与校正形状数量 LOD、GPU morph、内存 性能预算 flowchart LR A[\"Maya 基础网格 + 目标形状\"] --\u003e B[\"Blend Shape 节点权重 / In-Between\"] B --\u003e C[\"FBX 导出Blend Shape 通道\"] C --\u003e D[\"UE 导入Morph Target（+ 可选曲线）\"] D --\u003e E[\"Set Morph Target / 动画曲线 / Live Link\"] E --\u003e F[\"SkeletalMesh 顶点形变\"]4.1.5 面板控制与 Live Link 的流程差异 1. 拖动控制点或滑块时调用的函数\n在 Control Rig 编辑模式下拖动控制点时，FControlRigEditMode::SetControlShapeTransform 的直接编辑分支会调用 UControlRig::SetControlGlobalTransform，把视口中的世界变换交给 Control Rig；如果存在约束，还会通过 SetControlLocalTransform 修正局部值并更新关键帧数据：\n// ControlRigEditor/Private/EditMode/ControlRigEditMode.cpp（节选） ControlRig-\u0026gt;SetControlGlobalTransform( InShapeActor-\u0026gt;ControlName, InGlobalTransform, /*bNotify=*/true, InContext, /*bSetupUndo=*/true, /*bPrintPython=*/false, /*bFixEulerFlips=*/true); SetControlGlobalTransform 内部先把世界变换换算成控制值，再进入统一的写入路径：\n// ControlRig/Private/ControlRig.cpp（节选） FRigControlValue Value = GetControlValueFromGlobalTransform( InControlName, GlobalTransform, TransformType); SetControlValue( InControlName, Value, bNotify, Context, bSetupUndo, bPrintPythonCommands, bFixEulerFlips); 最终写入的是控制元素当前值。非 Additive 的 Control Rig 会直接走下面的写入；Additive rig 会先把值缓存到 ControlValues，在 backward solve 后再应用：\n// ControlRig/Private/ControlRig.cpp（节选，非 Additive 分支） DynamicHierarchy-\u0026gt;SetControlValue( ControlElement, InValue, ERigControlValueType::Current, bSetupUndo, false, bPrintPythonCommands, bFixEulerFlips); 运行时 UMG 滑块通常通过 UControlRigComponent 进入同一条路径：\n// UFaceControlPanelWidget.cpp（示例） // 成员：UPROPERTY(EditAnywhere, Category=\u0026#34;Face\u0026#34;) // TObjectPtr\u0026lt;UControlRigComponent\u0026gt; FaceRig; void UFaceControlPanelWidget::OnJawOpenChanged(float Value) { if (!FaceRig) { return; } FaceRig-\u0026gt;SetControlFloat( TEXT(\u0026#34;CTRL_JawOpen\u0026#34;), FMath::Clamp(Value, 0.f, 1.f)); } SetControlFloat 内部调用 UControlRig::SetControlValue\u0026lt;float\u0026gt;，随后进入同一个 SetControlValueImpl。如果控制点类型是 Transform，则改用 SetControlTransform(..., EControlRigComponentSpace::RigSpace)。\n2. 修改了什么值\n修改的是 FRigControlElement 的 FRigControlValue，写入位置为 ERigControlValueType::Current；值类型由控制本身决定，可能是 float、bool、int、Vector2D 或 Transform。构造模式下还会同步更新控制偏移的 Initial 与 Current 变换。 拖动控制点时，GetControlValueFromGlobalTransform 会先把世界空间变换换算到控制空间，ApplyLimits 再套用最小/最大值，最后写入 Current。 它修改的不是骨骼 Transform，也不是 Morph Target 权重。骨骼变换是在 Control Rig 求值后由 rig 图写入 hierarchy 的结果；Morph Target 只有在 rig 图或曲线额外驱动时才会变化。 bNotify=true 时 OnControlModified 会广播，编辑器据此触发 auto-key、Sequencer 关键帧与约束更新。 3. 与 Live Link 的差异\n维度 面板路径 Live Link 路径 数据源 UI 事件或鼠标拖动 面捕设备或 App 入口 FControlRigEditMode 或 UControlRigComponent Source → Subject → Live Link Client 数据形态 FRigControlValue（float / Transform） Live Link 帧数据（曲线 / Transform） 中间处理 控制空间换算、限值、OnControlModified StaticData 登记、插值、时间偏移、Retarget 更新频率 UI 拖动或编辑器帧 设备帧率或固定帧率 时间基准 编辑器时间 / Sequencer Timecode 与 Subject 时间偏移 记录方式 Sequencer 控制值关键帧 Take Recorder / Live Link Hub 录制 延迟 本地即时 网络与评估延迟 多角色 单实例操作 一个 Subject 可被多个消费者读取 flowchart TB subgraph PANEL[\"面板路径\"] direction LR P1[\"滑块 / 控制点\"] --\u003e P2[\"SetControlFloat / SetControlGlobalTransform\"] P2 --\u003e P3[\"FRigControlValue::Current\"] P3 --\u003e P4[\"Control Rig Evaluate\"] end subgraph LL[\"Live Link 路径\"] direction LR L1[\"面捕设备\"] --\u003e L2[\"Source / Subject\"] L2 --\u003e L3[\"Client 评估帧\"] L3 --\u003e L4[\"Live Link Pose\"] L4 --\u003e L5[\"Retarget / BuildCurveData\"] L5 --\u003e L6[\"AnimBP / Control Rig\"] end P4 --\u003e B[\"面部骨骼\"] L6 --\u003e B4. 变量关系与 Morph Target 的影响\n路径 直接写入或产生的变量 中间变量 最终影响 面板（Control Rig） FRigControlValue（Current） Control Rig VM → FRigTransformElement 的 CurrentLocal / CurrentGlobal 面部骨骼变换；若 rig 输出曲线，则附带 Morph 权重 面板（直接绑 Morph Target） MorphTargetCurves[Name] MorphTargetWeights 顶点位置与切线增量 Live Link Live Link 帧数据：Transforms / PropertyValues（Animation Role 为 FLiveLinkAnimationFrameData，Basic Role 为 FLiveLinkBaseFrameData） FPoseContext 的 FCompactPose + FBlendedCurve → Control Rig 控制值或 MorphTargetCurves 骨骼变换，以及可选的 Morph 权重 Live Link 的数据是上游输入，必须先经过 Client 评估、Retarget 与 AnimBP 映射，才会成为 Control Rig 控制值或 Morph Target 权重；面板滑块是下游直接写入，绕过这些中间变量。二者最终可以落到同一个 Control Rig 控制或同一个 MorphTargetCurves 条目，但写入的变量层级不同：Live Link 先改帧数据与曲线，面板直接改控制值或权重。\n如果 Live Link 曲线名与 Morph Target 一致，曲线会经 BuildCurveData 写入 FBlendedCurve，再由 USkeletalMeshComponent::ApplyAnimationCurvesToComponent 调用 AppendActiveMorphTargets，最终落到 MorphTargetWeights；如果曲线映射到 Control Rig 控制，则先变成控制值，再由 Control Rig 求值骨骼。\nBlendShape / Morph Target 的权重变化，默认影响范围如下：\n对象 是否受影响 说明 顶点位置与切线 是 Morph Target delta 按权重叠加，直接改变渲染网格 骨骼 Transform 否（默认） 只有 Control Rig 或 AnimBP 额外读取时才产生关联 材质参数 否（默认） 需要材质曲线或参数单独驱动 碰撞与物理 否（默认） 碰撞体与物理资产基于骨骼，不会自动跟随 Morph 形变 渲染与阴影 是 轮廓、表情细节、法线/切线都会变化 性能 是 活跃目标数、顶点数、LOD 与 CPU/GPU Morph 计算共同决定开销 骨骼变换与 Morph Target 权重共同作用：骨骼决定整体姿态，Morph 提供局部顶点偏移，二者在顶点处理阶段合成；Morph 不会反向改写骨骼或控制值。\n流程上的关键变化是：面板路径从 UI 直接进入 Control Rig，跳过 Source、Subject、Client 与 Retarget；Live Link 路径在到达 Control Rig 前，还要经过静态数据登记、评估帧生成与曲线/姿态重定向。面板修改的是控制值，适合手动摆 pose、单实例调试与 Sequencer 关键帧；Live Link 修改的是帧数据，适合表演驱动、多角色分发与录制。\n4.2 三条实时路径 路径 输入 优点 限制 Live Link Face（iOS/Android） 手机前置或后置摄像头、TrueDepth、麦克风 部署快、延迟低、可移动 依赖手机性能与网络；Android 实时追踪有特定芯片要求 MetaHuman Video Source 普通摄像头或视频 无需移动设备，适合桌面环境 占用 GPU；单目存在深度歧义，侧脸与遮挡表现较弱；对视频文件的支持有限，通常需要单独集成解码与送帧流程 MetaHuman Audio Source 麦克风或音频 无演员也可驱动口型 仅含声音信息，表演细节有限 上述实时路径由 MetaHuman Animator 的实时管线承载[1][2]。\nLive Link Face 的工程注意事项[3]：\nAndroid 端目前为 Beta 版本，需确认 Live Link Face 在 Google Play 的区域可用性与目标机型的兼容性。 应用必须在前台运行，否则引擎无法发现设备。 移动设备与 PC 应处于同一网络，并放行防火墙；优先使用有线或专用 AP。 应用中的 Subject 名必须与引擎中选择的名称一致。 必要时在应用中指定目标 IP，避免自动发现失败。 长时间录制需关注设备发热与降频。 4.3 离线路径：MetaHuman Animator MetaHuman Animator 是用于从表演数据生成动画的工具集，支持实时、离线深度与离线单目/音频三类输入[1]。本节讨论后两类，实时路径见 4.2 节：\n实时输入：单目摄像头、移动设备（Live Link Face）、音频源。 离线深度输入：iPhone TrueDepth 或立体头戴相机（HMC）采集的深度数据。 离线单目或音频输入：普通视频或纯音频。 离线深度解算不仅使用二维纹理，还使用几何深度信息，因此能够进行更稳定的身份对齐、头部姿态估计与面部形变求解，质量上限高于实时单目方案[12]。其代价是采集流程更复杂、处理时间更长，并对拍摄规范提出更高要求[11]。\n离线产线与实时产线的核心差异在于可迭代性：单条 take 不满意时可重拍、重解算或清理；实时直播不具备该条件。因此两者在采集规范、设备配置与人员安排上应分别设计。\n4.4 采集规范 面捕质量的上限在很大程度上由采集现场决定：\n项目 建议 原因 光照 均匀、柔和、无频闪 硬阴影与频闪会导致追踪丢失 角度 以正脸为主，允许有限侧脸 单目方案对大角度侧脸与遮挡较敏感，且对入画比例有一定要求 遮挡 头发不遮挡眉毛，眼镜避免反光 眉毛与眼角是表情的关键区域 表情幅度 覆盖目标表演范围，避免极端测试 过度夸张会导致解算与重定向失真 中性表情 先录制中性表情 身份对齐、neutral pose 与后处理均需要基准 帧率 尽量高且稳定 低帧率会丢失快速表情与头部运动 音频 单独录制，并打板或记录时间码 后期口型对齐与剪辑依赖音频 时长 分段拍摄 便于局部重拍与分段处理 4.5 连接角色 实时面捕接入 MetaHuman 的流程可归纳为四步：\n启用插件：启用 Live Link 与 MetaHuman Live Link。 建立 Subject：在 Live Link 窗口添加对应 Source，并确认 Subject 出现在列表中。 连接角色：在关卡中 MetaHuman 角色蓝图的 Details 面板勾选 Use Live Link 并选择 Subject；或在 MetaHuman Creator 的 Preview Settings 中将 Animation Controller 设为 LiveLink 并选择 Subject，进行预览。 确认消费端：检查面部 AnimBP 是否启用、Live Link Pose 是否读取同一 Subject、混合权重是否被其他节点覆盖。 Subject 出现在 Live Link 列表仅表示数据已到达；角色产生动画才表示数据被消费端正确使用。\n4.6 面部后处理 实时面捕的常见问题表现为表情正确但缺乏真实感，而非缺少表情数据：\n现象 可能原因 调整方向 眼球朝向缺乏焦点 眼球驱动缺失或目标点错误 检查眼球控制、Aim 与 Eye 后处理 眼睑形变异常 眼睑曲线映射错误 调整眼睑范围并增加修正 口型幅度过大或塌陷 曲线增益过大、形态目标过冲 降低增益、增加限制与后处理 舌头或牙齿穿模 舌头曲线缺失或下颌范围错误 检查舌头控制与下颌限位 左右脸不对称 追踪偏差或曲线镜像错误 检查镜像、校准与重录 头部运动但面部无表情 仅收到 Transform，缺少表情曲线 检查 Subject 数据、Role 与 AnimBP MetaHuman 的面部通常由 Control Rig 承担中间层：曲线先驱动控制点，再由控制点驱动骨骼与形态目标，后处理再修正眼睑、眼球与舌头。该结构便于修正，但链路较长，问题排查需逐段进行。\n4.7 质量边界 维度 实时单目 实时深度或移动设备 离线深度 表情细节 中 中高 高 头部姿态 中 高 高 侧脸与遮挡 弱 中 中高 可重复性 低（现场） 低（现场） 高（可重解算） 延迟 低 低 无（离线） 成本 低 中 高 选型应同时考虑质量上限与交付稳定性，而非仅比较峰值质量。以离线流程产出的高质量资产可用于直播，但流程必须按实时产线设计；实时面捕可用于影视预演，但不应直接作为最终镜头。\n4.8 面捕传递表情系数，动捕传递骨骼数据 第 4 章的各个小节，实际是同一条链路的不同环节：数据以什么形态产生、怎么传、交给谁消费、会在哪里失真。把它们串起来会发现一条分界——面捕传的是角色无关的表情系数，动捕（第 5 章 动捕：光学、惯性与重定向）传的是角色相关的骨骼数据。\n环节 对应小节 结论 数据形态 4.1 设备输出的是归一化系数或曲线（ARKit 约 52 条），不是最终表情 传输 4.2 三条实时路径的输入都是相机、深度或音频，输出仍是系数与头部位姿 解算与重定向 4.1、4.3 系数经映射与补全落到 Control Rig 控制或 Morph 权重；离线用深度提升求解质量 采集 4.4 光照、角度、遮挡与中性表情决定质量上限，后期只能修补 消费与后处理 4.5、4.6 Subject 出现只代表数据到达；眼睑、眼球、舌头等问题在系数层修正最方便 质量边界 4.7 实时与离线各有上限，选型看交付稳定性而非峰值质量 为什么采集后要先算表情系数，再转换成 Morph Target\nUE使用的追踪算法原生输出的就是系数。消费级摄像头与深度传感器通常只能直接观测到「眉抬起了多少、嘴角拉向哪边」这类相对变化，给不出目标角色某根骨骼绕某个轴的旋转角——这个换算依赖目标 rig 的骨骼结构，只能在接收端完成。系数因此成为天然的中间层：\n它与角色无关，同一份数据可以映射到不同面部资产，这正是 4.1 中「同一段数据在不同资产上表现不同」却又「能通用」的原因； 它可以在接收端走两条路径中的任意一条：直接落到 MorphTargetWeights 驱动顶点，或经 Control Rig 控制值驱动骨骼（见 4.0 的骨骼路径与顶点路径）； 4.6 列出的过冲、左右不对称、舌齿穿模等问题，本质都是曲线层的问题，在这里做增益、限幅、平滑、镜像，比改骨骼或改模型容易得多。 Morph Target 之所以成为常见落点，是因为它逐顶点表达局部形变、开销可控、可由同名曲线直接驱动；但它不是唯一去向——MetaHuman 的面部让曲线先驱动 Control Rig，再由骨骼与校正形态共同完成表情，Morph 更多承担局部修正。代价是映射有损：系数空间与目标角色的表情空间并不重合，眼、舌、齿等缺乏对应系数的部位需要额外交付通道或后处理补足。\n为什么面捕不直接传骨骼\n根本原因是层级不同：骨骼旋转是角色相关的最终姿态，面部骨骼的数量、命名、层级与绑定方式在不同 rig 之间差异极大；表情系数是角色无关的中间表示，可以直接作为传输契约。下面几项都是这一点的推论：\n维度 传表情系数 传骨骼 Transform 设备端依赖 不需要知道目标角色 必须知道目标骨架的骨骼数、命名、层级与绑定参数 数据形态 追踪算法原生输出，0~1 无量纲标量 需在设备端完成从系数到旋转的求解，而求解依赖目标 rig 跨角色复用 一份数据可映射到任意面部资产 与特定骨架绑定，跨角色需重定向 数据量与带宽 每帧数十个标量，可大幅压缩 上百根骨骼的平移与旋转，且随骨骼数线性增长 丢帧与降噪 可插值、平滑，退化表现为表情变淡 缺失即错位，恢复时跳变明显 解耦与演进 设备固件与角色资产各自迭代，契约只有曲线名 任一侧改骨架都可能破坏兼容 对照：动捕为什么传骨骼\n动捕的源与目标同构：光学与惯性系统解算出的本来就是骨骼 Transform，且身体骨架有相对统一的语义（髋、脊柱、四肢、手脚），跨角色时只需处理比例与关节朝向，这正是第 5 章重定向要解决的问题。面部没有这种统一约定——不同角色的面部骨骼从几十根到数百根不等，命名与绑定各异，而且大量细微形变（皮肤滑移、皱纹、嘴唇贴合）本就不适合用骨骼表达。因此面捕把角色无关的系数放在链路上游，把「翻译成哪种 rig」留到接收端；动捕则直接传骨骼，再用重定向适配目标骨架。\n两条链路的差异最终会回落到第 4 章开头的现象上：面捕的表达力受映射与补全的限制，动捕的精度受解算与重定向的限制。认清这一点，选型时才不会把「面捕数据质量」与「角色绑定质量」混为一谈。\n第 5 章 动捕：光学、惯性与重定向 5.1 动捕源类型 类型 代表 优点 缺点 光学 Vicon、OptiTrack 等 精度高、支持多人、捕捉体积可扩展 场地成本高、标定繁琐、易受遮挡与反光影响 惯性 Xsens 等 便携、适合外景、部署快 存在漂移、脚底接触较差、易受磁干扰 视觉或其他 摄像头、VR 设备、手机 AR 成本低、易用 精度与稳定性有限 无论采用哪种源，进入引擎的均为一套源骨架的 Transform。MetaHuman 使用另一套身体骨架，不能直接复制旋转，必须进行重定向[7]。此外，重定向之后通常还需通过关节与足部 IK 抑制漂移、保持脚底接触。\n5.2 重定向链路 flowchart LR A[\"源骨架动捕设备 / 软件\"] --\u003e B[\"Live Link SubjectAnimation Role\"] B --\u003e C[\"IK Retargeter链映射 + Retarget Pose\"] C --\u003e D[\"MetaHuman 身体骨架\"] D --\u003e E[\"身体 AnimBP状态机 / 分层 / IK\"] E --\u003e F[\"Control Rig 后处理脚底 / 骨盆 / 手\"] F --\u003e G[\"最终 pose\"]重定向需解决三类问题：\n比例：源演员与 MetaHuman 的臂长、腿长与脊柱长度不同。 朝向：源骨架的 T-pose 或 A-pose、轴向与旋转顺序可能不同。 约束：脚底需贴地、手部需贴合目标、膝关节不能反折。 5.3 Retarget Pose 与链设置 IK Retargeter 的核心并非一键映射，其流程是先对齐基准姿势，再定义链：\n设置 作用 常见错误 Retarget Pose 使源骨架与目标骨架处于同一基准姿势 一侧为 T-pose、另一侧为 A-pose，导致肩、肘、腕偏移 Chain（链） 定义骨骼对应关系 遗漏 twist 骨、手指链或辅助骨 Rotation Mode 定义旋转映射方式 直接复制导致扭转累积 Translation Mode 定义位移映射方式 骨盆位移被放大或缩小，角色漂浮 Retarget Root 定义根骨与骨盆处理方式 根运动重复叠加，角色瞬移 IK 设置 定义末端目标与关节限制 膝关节反折、手部穿过身体 推荐的调整顺序：\n对齐站姿：在 Retarget Pose 下使源骨架与目标骨架呈现同一姿势。 对齐大关节：确认肩、肘、髋、膝的方向正确。 检查末端：确认脚底贴地、手部姿态自然。 修正细节：处理 twist 骨、手指与脊柱分段。 5.4 根运动与脚底 脚底滑动是动捕虚拟人真实感下降的主要原因之一，通常有三类来源：\n原因 表现 处理方式 根运动未对齐 角色整体漂移或原地踏步 检查根运动开关、Retarget Root 与移动组件 缺少脚底接触 脚在地面滑动但未固定 使用 Control Rig 或 IK 实现脚部锁定与贴地 骨盆高度错误 角色漂浮或陷入地面 修正骨盆位移、地面对齐与重定向位移缩放 实时直播中，脚底问题会被镜头放大。条件允许时，应优先选择包含脚底接触信息的动捕方案，或在 Control Rig 中增加脚部 IK 与锁定。\n5.5 手指、道具与多人捕捉 手指：手套数据通常作为独立 Subject，重定向至 MetaHuman 手部骨骼；若身体动捕不包含手指数据，手部将保持默认 pose。 道具：道具 Transform 可使用 Transform Role，或通过父子约束与 Attach 绑定至手骨；道具与手部的偏移应在校准阶段测量。 多人：每位演员对应一个 Subject；应优先采用统一命名规范（如 Actor01_Body、Actor01_Face），而非在蓝图中硬编码。 5.6 面部、身体与声音合流 三条流合流时，时间基准与角色映射最为关键：\nflowchart LR F[\"Face Subject\"] --\u003e FB[\"面部 AnimBP\"] B[\"Body Subject\"] --\u003e BB[\"身体 AnimBP\"] A[\"Audio\"] --\u003e AU[\"音频组件 / 口型（可选，可旁路）\"] FB --\u003e MH[\"MetaHuman 角色\"] BB --\u003e MH AU --\u003e MH TC[\"Timecode / 同步信号\"] -.-\u003e F TC -.-\u003e B TC -.-\u003e A实施建议：\n面部、身体与音频分别使用独立数据流，避免合并为单条流。 使用 Timecode 或统一时钟对齐；无时间码时，至少通过打板或拍手完成一次人工对齐。 录制时应同时记录三条流，以便后期重新对齐与替换。 演员名、Subject 名与角色蓝图的映射关系应写入文档或数据资产。 在实时直播中，UE 的音频管线可能引入额外延迟，因此常将音频交由直播软件、调音台或采集设备直接输出，作为最终直播声轨，并与画面统一对齐。 是否使用 UE 音频组件，应依据同步方案与延迟预算确定。 5.7 选型参考 需求 优先方案 备注 影视级、多人、可扩展捕捉体积 光学动捕 场地与标定是主要成本 外景、快速部署 惯性动捕 需处理漂移与脚底接触 直播、单人、低成本 惯性或视觉动捕配合手机面捕 稳定性优先于精度 手指细节 数据手套 与身体流分开重定向 预演与走位 轻量视觉动捕 不追求最终质量 第 6 章 与动画模块集成：AnimBP、Control Rig、Sequencer 6.1 Live Link 在动画蓝图中的位置 面部与身体的接入方式不同，但整体结构相似[8]：\n层 身体 面部 数据入口 Live Link Pose（Animation） Live Link Pose（曲线）或曲线读取 逻辑 状态机、分层、Slot、Aim Offset 表情后处理、眼球、舌头 后处理 Control Rig（脚部 IK、骨盆） Control Rig（眼睑、口型修正） 输出 最终身体 pose 最终面部 pose 常见的身体 AnimBP 结构如下：\nflowchart LR LSM[\"Locomotion State Machine\"] --\u003e LBP[\"Layered Blend Per Bone\"] LLP[\"Live Link Pose（Body Subject）\"] --\u003e LBP LBP --\u003e SLOT[\"Slot（Montage）\"] SLOT --\u003e PP[\"Post Process AnimBP（Control Rig：Foot IK / Pelvis）\"] PP --\u003e OUT[\"Output Pose\"]关键环节在于混合规则：\nLayered Blend Per Bone：按骨骼分层，例如下半身使用 locomotion、上半身使用动捕数据；需注意根骨与脊柱的混合点。 Slot：使 Montage 与 Notify 能够覆盖实时数据，用于挥手、招手与剧情动作。 Aim Offset：使头部或眼球看向目标，同时保留动捕的基础姿态。 Inertialization：在状态切换时使用惯性化过渡，避免跳变。 常用节点及其边界：\n节点 用途 常见问题 Live Link Pose 将实时 Subject 引入 AnimGraph Subject 名或 Role 不匹配，未设置 Retarget Asset Layered Blend Per Bone 上下身、面部与身体分层 混合骨骼选择错误，根运动被混合两次 Blend Poses by Bool/Enum 状态切换与动作覆盖 缺少过渡，切换时产生跳变 Aim Offset 头部或眼球看向目标 轴心与范围未设置，转头时形变异常 Two Bone IK / Leg IK 脚底贴地、手部贴合物体 未设置关节限制，膝关节反折 Transform (Modify) Bone 程序化微调 每帧开销较高，叠加过多会导致抖动 Slot / Montage 覆盖式剧情动作 忽略 Slot 层级与权重 Apply Additive 呼吸、表情与抖动叠加 基础姿势不干净，叠加结果异常 Sync Group 走跑切换的步态同步 相位未对齐，脚底滑动 6.1.1 关键函数与线程 3.5 节从数据对象与客户端角度描述了链路；本节从动画更新线程与函数调用角度描述同一链路的执行顺序。要理解 Live Link 与动画系统的联动，需要先区分动画更新（Update）与动画求值（Evaluate）。UAnimInstance::UpdateAnimation 是每帧动画更新的入口，负责触发蓝图与 C++ 的更新逻辑、准备变量和状态；它并不生成最终 pose。Live Link Pose 节点是在 AnimGraph 求值阶段才把数据转换为 pose 与曲线的。\n阶段 典型函数 / 事件 线程 职责 与 Live Link 的关系 更新入口 UAnimInstance::UpdateAnimation 游戏线程 启动本帧动画更新，调度后续求值 不直接读取或写入 Live Link pose 游戏线程更新 NativeUpdateAnimation / Blueprint Update Animation 游戏线程 采集输入、计算状态、设置 AnimBP 变量 可在此查询 Live Link 快照，为后续节点准备参数 线程安全更新 NativeThreadSafeUpdateAnimation / Blueprint Thread Safe Update Animation Worker 线程 并行计算，只读写线程安全数据 消费上一步准备好的数据；不应调用非线程安全的 Blueprint 节点 节点更新 FAnimNode_Base::Update_AnyThread Worker 线程 推进节点时间与内部状态 Live Link Pose 的准备阶段 节点求值 FAnimNode_LiveLinkPose::Evaluate_AnyThread Worker 线程 生成 pose 与曲线 拉取评估帧，调用 Retarget Asset 写入 FPoseContext 后处理 Control Rig / Post Process AnimBP Worker 线程 修正 pose、执行 IK 与程序化动画 消费已生成的 pose 与曲线 输出 SkeletalMeshComponent 游戏线程 应用最终 pose 并渲染 这是 Live Link 数据的最终视觉结果 关键区别：Live Link 不会主动调用 UpdateAnimation，也不会把 pose 写进某个蓝图变量。它是被消费的数据源：Live Link Pose 节点在 AnimGraph 求值时，主动向 Live Link Client 请求该 Subject 的评估帧。\n因此，即使 AnimBP 的 Event Graph 为空，只要 AnimGraph 中接入了有效的 Live Link Pose 节点，角色仍会随 Subject 更新；反过来，只在 UpdateAnimation 中读取曲线，并不会自动把数据接到最终 pose。\nNativeUpdateAnimation 的官方说明也强调：适合在这一步“收集数据”，把主要计算放到 NativeThreadSafeUpdateAnimation[21][22]。这与 Live Link 的接入方式一致：游戏线程负责选择 Subject、准备参数和处理输入，动画线程负责把评估帧转换为 pose。\n6.1.2 Live Link Pose 的执行时序 以启用多线程动画更新为例，单帧内的执行顺序如下：\n初始化与缓存骨骼：节点初始化时创建并初始化 Retarget Asset；缓存骨骼阶段确定目标骨架的骨骼索引。Live Link 的 StaticData（BoneNames、PropertyNames）为骨骼与曲线的名称映射提供依据。 游戏线程更新：UpdateAnimation 触发 NativeUpdateAnimation 与 Blueprint Update Animation。蓝图在这里计算状态、切换参数，必要时通过 Live Link Blueprint 节点读取一份当前快照用于逻辑或调试。 线程安全更新：NativeThreadSafeUpdateAnimation / Blueprint Thread Safe Update Animation 在 Worker 线程执行，计算可以由动画线程安全读取的变量。此阶段不应直接操作 Live Link 的蓝图接口。 节点更新：AnimGraph 中的节点执行 Update_AnyThread，推进时间与内部状态。Live Link Pose 在这一阶段通常只做轻量准备，不生成最终 pose。 节点求值：FAnimNode_LiveLinkPose::Evaluate_AnyThread 向 Live Link Client 请求该 Subject 的评估帧[23]；Client 按 Subject 设置完成插值、时间偏移与丢帧处理后返回 FLiveLinkSubjectFrameData。 重定向与写回：节点调用 Retarget Asset 的 BuildPoseFromAnimationData 与 BuildCurveData，把源数据写入 FCompactPose 与 FBlendedCurve，组成 FPoseContext[24]。 混合与后处理：后续的 Layered Blend Per Bone、Slot、Aim Offset、Control Rig 等节点继续修改 pose 与曲线。 输出与录制：最终 pose 应用到 SkeletalMeshComponent；Take Recorder / Sequencer 可以录制评估后的结果，而不是原始的 Live Link 数据流。 sequenceDiagram participant GT as 游戏线程 UAnimInstance participant WT as Worker AnimGraph participant LL as Live Link Client participant LN as Live Link Pose participant RA as Retarget Asset participant SM as SkeletalMeshComponent GT-\u003e\u003eGT: UpdateAnimation / Blueprint Update Animation GT-\u003e\u003eWT: 提交动画更新任务 WT-\u003e\u003eWT: NativeThreadSafeUpdateAnimation（可选） WT-\u003e\u003eLN: Update_AnyThread WT-\u003e\u003eLN: Evaluate_AnyThread LN-\u003e\u003eLL: 请求评估帧（Subject / Role / 时间偏移） LL--\u003e\u003eLN: FLiveLinkSubjectFrameData LN-\u003e\u003eRA: BuildPoseFromAnimationData / BuildCurveData RA--\u003e\u003eLN: FCompactPose + FBlendedCurve LN--\u003e\u003eWT: FPoseContext WT-\u003e\u003eWT: 分层混合 / Control Rig 后处理 WT--\u003e\u003eGT: 最终 Pose GT-\u003e\u003eSM: 应用 Pose 并渲染如果关闭多线程动画更新，上述 Worker 阶段会回到游戏线程执行，函数职责与数据顺序不变，只是线程位置不同。调试时可以用 stat anim 观察动画求值开销，并结合 Live Link 窗口确认 Subject 是否持续更新。\n6.1.3 蓝图节点与使用边界 节点 / 类型 所在位置 用途 注意事项 Live Link Pose AnimGraph 将 Subject 的评估帧转换为 pose 与曲线 主要消费方式；在动画求值时读取，不经过 Event Graph Live Link Controller Actor 组件 向组件推送 Subject 数据，可选 Transform 或骨骼动画控制器 不是 AnimGraph 节点，适合不经 AnimBP 的驱动场景 Evaluate Live Link Frame Event Graph / 工具蓝图 手动评估 Subject 当前帧或指定时间的帧 会触发一次评估，避免与 Live Link Pose 重复消费 Get Animation Frame Data / Get Curves Event Graph / 工具蓝图 从 Subject Frame 读取帧数据或曲线表 适合逻辑判断、调试与自定义处理，不直接生成 pose Get Live Link Subject Role / State / Subjects Event Graph / 工具蓝图 查询 Subject 是否存在、角色与状态 用于诊断与选择 Subject，不产生动画 GetCurveValue AnimBP 读取当前 AnimInstance 的曲线值 Event Graph 先于 AnimGraph 求值，通常只能读到上一帧结果 上述节点的职责划分，与 FAnimNode_LiveLinkPose 的求值位置及 ULiveLinkRetargetAsset 的 pose/curve 构建接口一致[23][24]；Live Link Blueprint 库则提供手动评估与数据读取入口[25]。\n使用边界可以归纳为三条：\n产生 pose 用 Live Link Pose：它是 AnimGraph 节点，负责把评估帧写入 PoseContext；不要试图在 UpdateAnimation 中手动拼一个 pose。 读取数据用 Live Link Blueprint 库：EvaluateLiveLinkFrame、GetCurves 等节点适合在游戏线程做逻辑判断、UI 或调试；它们返回的是数据快照，不是角色的最终动画。 线程安全更新只处理线程安全数据：Blueprint Thread Safe Update Animation 中不要调用非线程安全的 Live Link Blueprint 节点；需要的数据应在 Blueprint Update Animation 中准备好。 一个典型的分工如下：\nflowchart TB subgraph EG[\"Event Graph：Blueprint Update Animation\"] A[\"读取输入与状态\"] --\u003e B[\"设置 AnimBP 变量\"] B --\u003e C[\"可选：Evaluate Live Link Frame\"] end subgraph AG[\"AnimGraph\"] D[\"Live Link Pose\"] --\u003e E[\"Layered Blend / Slot / Aim Offset\"] E --\u003e F[\"Control Rig 后处理\"] F --\u003e G[\"Output Pose\"] end B -.-\u003e|\"变量与参数\"| D C -.-\u003e|\"逻辑 / 调试数据\"| DSubject 名、Retarget Asset 与骨骼映射属于节点的结构性配置，应在动画图或初始化阶段确定。运行中频繁更换这些配置可能触发重初始化或缓存失效，并带来额外性能开销；如果确实需要切换，应在切换后确认新 Subject 的静态数据已就绪，并预留一帧以上的稳定时间。\n6.2 面部与身体的协同 MetaHuman 的面部与身体通常是两个骨骼网格、两个 AnimBP。协同位置如下：\n位置 职责 角色蓝图 挂载身体、面部与毛发组件，设置各自 AnimBP，暴露 Subject 名或参数 面部 AnimBP 读取面捕曲线，驱动面部 Control Rig 身体 AnimBP 读取身体 Subject，混合 locomotion 与后处理 Sequencer 将两条流录成轨道，进行剪辑、叠加与手动修正 音频 独立轨道或组件，按 Timecode 对齐 面部与身体不宜在 AnimBP 内强耦合。在角色蓝图或 Sequencer 层合流更便于替换、调试与复用。\n6.3 Control Rig Control Rig 在虚拟人管线中通常承担三类任务[6]：\n后处理：脚部 IK、骨盆修正、手部贴合物件、脊柱微调。 程序化动画：呼吸、眨眼、眼球微动与姿态限制。 手动修正：在 Sequencer 中通过 Control Rig 轨道修改关键帧。 MetaHuman 的身体通常包含一个 Post Process AnimBP，其中使用 Control Rig 完成脚底与姿态修正；面部也有独立的 Control Rig 结构。使用时需注意：\n后处理每帧求值，节点越多开销越高；直播场景应控制复杂度。 Control Rig 的修正会覆盖上游结果，调参前需确认骨骼的实际修改来源。 若仅录制最终 pose，后处理逻辑不会被保存；需要可回放性时，应录制动画或保留 rig 参数。 6.4 Sequencer 与 Take Recorder 实时表演需经过录制才能成为可回放、可剪辑、可交付的内容[9]：\n工具 作用 适用场景 Live Link Hub Capture Manager 接收并录制实时 take 现场采集、原始数据留存 Take Recorder 在引擎内录制 Actor 与 Sequencer 数据 预演、镜头、动画资产 Sequencer 剪辑、分层、叠加与输出 影视、过场、直播包装 Movie Render Queue 高质量离线渲染 最终画面输出 录制时的工程规范：\n每条 take 使用统一命名，包含日期、演员与镜头号。 面部、身体与音频同时录制，并保留原始 Subject 数据。 录制前确认 Timecode 与帧率，避免回放时无法对齐。 录制完成后立即抽帧检查脚底、手部、眼部、口型与头发穿插。 6.5 运行时与交付 Live Link 适用于编辑器与制作阶段，不应作为默认运行方案：\n场景 建议 游戏运行时 烘焙为 AnimSequence、Montage 或 Level Sequence，运行时仅播放 实时直播 可保留 Live Link，但需接受网络与设备风险，并准备降级方案 影视过场 录制、清理并烘焙，按镜头交付 预演 直接使用实时流，优先保证效率 录制或烘焙后的资产应在目标运行场景中回放一致；实时直播链路则应保留降级方案与备用素材。\n6.6 主蓝图结构参考 组件 / 层级 挂载内容 职责 Body SkeletalMeshComponent AnimBP_Body Locomotion State Machine、Live Link Pose（Body Subject）、Layered Blend Per Bone、Slot（Montage）、Post Process AnimBP（Control Rig：Foot IK / Pelvis） Face SkeletalMeshComponent AnimBP_Face Live Link Pose（Face Subject）、表情后处理、Control Rig（眼睑 / 眼球 / 口型） Groom Components — 头发、眉毛与睫毛的渲染及绑定 Audio Component（可选） — 直播链路中可能旁路，按同步方案决定是否使用 参数 BodySubjectName、FaceSubjectName、UseLiveLink 角色蓝图暴露的 Subject 与开关配置 该结构并非唯一实现，但将数据入口、逻辑、后处理与输出分离，便于逐层替换与排错。\n第 7 章 工程落地：流水线、性能与验收 7.1 可交付流水线 阶段 产出 验收点 角色定义 身份、风格、目标平台与帧率 角色资产与装配管线确定 绑定与装配 MetaHuman Character 与 Assembly 面部、身体、毛发与材质在目标平台可运行 采集方案 面捕与动捕设备、场地、时间码 试拍通过，延迟可接受 数据采集 take、原始 Subject 与音频 命名规范、备份完整、可重放 解算与重定向 动画与 Retarget 资产 表情、动作与脚底达标 集成 AnimBP、Control Rig、Sequencer 实时预览与录制均正常 清理与交付 烘焙动画与镜头 回放一致、动画资产完整 7.2 资产与命名 Subject 命名：采用 项目_演员_部位 结构，例如 Demo_A01_Face、Demo_A01_Body，避免使用默认名。 Take 命名：包含日期、镜头与条数，例如 20261110_SH010_T03。 重定向资产：一个源骨架对应一个 Retargeter，命名中包含源类型，避免混用。 版本管理：MetaHuman 资产、Groom、动画与 Level Sequence 均使用 LFS；二进制资产不依赖“另存为”管理版本。 7.3 性能预算 项目 关注指标 优化手段 面部 面部骨骼与形态目标、材质、Live Link 推理 面部 LOD、限制同屏数量、降低更新率 身体 骨骼数、AnimBP 节点、Control Rig 简化后处理、LOD 更新、减少分层 毛发 发丝数、阴影、模拟 降低密度、启用 LOD、关闭物理模拟 皮肤与眼睛 材质复杂度、透明、SSS 简化材质、合并 pass 动画数据 Subject 数量、网络包、插值 固定帧率、分机处理、减少无用 Subject 渲染 GPU 帧时间 Optimized 装配、关闭非必要光追与阴影 上述指标因项目而异，不宜直接套用。有效做法是先建立基线：分别测试空场景、单角色、开启与关闭面捕、开启与关闭毛发，再确定优化优先级。\n工程建议：在开发与制作阶段，应尽量减少 AnimBP 中对动捕数据的处理逻辑。更可取的做法是在预处理阶段或 C++ 中完成数据清洗、重定向与修正，AnimBP 仅将处理后的结果连接至 Output Pose，以降低图表复杂度与运行时求值开销。\n第 8 章 常见问题与排查 排查应按“数据源到画面”的顺序进行，避免在错误层级反复调整参数。\n现象 可能原因 检查与处理 Live Link 列表中没有 Subject 不同网段、防火墙拦截、应用不在前台、插件未启用 检查同网段、端口放行、应用前台状态与插件启用状态 存在 Subject，但角色为 T-pose AnimBP 未接入 Live Link Pose、Subject 名不匹配、权重为 0 检查 AnimBP 节点、名称与混合权重 身体无动作，仅头部转动 仅收到 Transform，缺少 Animation 数据或曲线 检查 Role、源数据与消费端类型 面部有动画而身体无动作 身体 Subject 未连接或身体 AnimBP 未启用 分别检查两套 AnimBP 表情幅度不足 曲线增益过低、后处理压制、追踪质量不足 调整增益、检查后处理、改善光照 表情幅度过大或穿模 曲线增益过高、形态目标过冲 降低增益、增加限位、修正后处理 口型与音频不同步 音频延迟、时间偏移、插值过大 检查音频延迟、Subject offset、降低插值 画面抖动或姿态跳变 Wi-Fi 丢包、帧率不稳、追踪丢失 使用有线或专用 AP、固定帧率、改善光照 角色漂移或瞬移 根运动重复、Retarget Root 错误、Actor 位移叠加 关闭重复根运动、检查重定向根设置 脚底滑动 缺少脚部 IK、骨盆高度错误、地面不平 增加脚部 IK 与锁定、修正骨盆、对齐地面 腕部扭转或膝关节反折 Retarget Pose 不一致、链映射错误、缺少 IK 限制 对齐基准姿势、补全链、增加关节限制 头发不跟随或穿模 Groom 绑定丢失、LOD 不同步、物理设置异常 检查绑定、LOD 与毛发碰撞 录制后回放异常 录制范围不完整、资产引用缺失、烘焙结果异常 检查录制范围、资产引用与烘焙结果 长时间直播后性能下降 内存或句柄泄漏、设备发热降频、网络重连失败 执行长跑测试、增加散热、完善重连策略 Live Link Hub 下动画生成变慢 与引擎竞争 GPU 资源 分机运行，并使用 t.maxfps 限制帧率 第 9 章 最小实施路径与参数对照 9.1 实时面捕最小路径 在 UE 5.6 项目中启用 Live Link 与 MetaHuman Live Link 插件并重启编辑器。 准备一个已装配的 MetaHuman 角色，并将其放入关卡。 在移动设备安装 Live Link Face，与 PC 连接同一网络，并在应用中填写 Subject 名。 在引擎中打开 Window → Virtual Production → Live Link，添加 Live Link Face Source，确认 Subject 出现。 选中角色蓝图，在 Details 的 Live Link 区域勾选 Use Live Link 并选择 Subject。 面对镜头做出表情，确认面部与头部均产生动画；若无动画，按第 8 章顺序排查。 需要留存时，使用 Take Recorder 或 Live Link Hub 录制 take，并回放检查。 9.2 离线面捕最小路径 按采集规范录制一条包含深度信息的视频（iPhone TrueDepth 或 HMC）。 在引擎中打开 MetaHuman Animator，导入素材并建立或选择身份。 解算得到表演资产与动画，检查中性帧、极端表情与头部姿态。 将动画应用于目标 MetaHuman，必要时执行重定向与后处理修正。 进入 Sequencer 完成剪辑、渲染或烘焙。 9.3 身体动捕最小路径 由动捕软件输出 Live Link，或通过 Live Link Hub 中转，确认 Subject 出现。 创建或选择 IK Retargeter，源骨架选择动捕骨架，目标选择 MetaHuman 身体骨架。 调整 Retarget Pose，使源与目标站姿一致，并补全链映射。 在身体 AnimBP 中使用 Live Link Pose 读取 Subject，接入分层与后处理。 检查脚底、根运动与手部姿态，必要时通过 Control Rig 修正。 录制并烘焙。 9.4 参数生效位置 目标 首要调整位置 次要调整位置 表情幅度 面部曲线增益与后处理 源应用追踪设置 口型同步 音频延迟与 Subject 时间偏移 插值与帧率 身体比例 Retarget Pose 与重定向缩放 角色资产比例（需评估） 脚底接触 Control Rig 脚部 IK 骨盆位移与地面对齐 抖动 网络、帧率与插值 追踪光照与平滑参数 性能 装配管线、LOD 与毛发 AnimBP 与后处理复杂度、Subject 数量 延迟 采集与网络 插值、求值频率与渲染 总结 虚拟人管线并非单一资产或单一插件的产物，而是覆盖资产、传输、解算、驱动与交付的系统工程。其构成包括：以 MetaHuman 为资产与绑定基础，以 Live Link 为实时数据总线，以 MetaHuman Animator 与 IK Retargeter 完成解算与重定向，以 AnimBP 与 Control Rig 完成驱动，以 Sequencer 与烘焙完成交付。其核心价值在于将面部、身体、声音与镜头纳入统一的数据结构、命名规范与时间基准，使跨设备、跨角色的集成具备可复用性与可排错性。\n实际落地时，应先确定目标平台与实时/离线产线，再选择装配管线、采集方案与数据留存方式；静态数据结构、Subject 命名、Retarget Pose 与 Timecode 应作为前置工程约束，而非事后修补项。随着实时渲染、移动端面捕与离线解算能力的提升，该管线已应用于虚拟偶像、虚拟主播、影视预演与互动内容等场景，但其质量与稳定性仍取决于采集规范、数据治理与性能预算的协同设计。\n第 10 章 附录与参考 10.1 模块与插件 名称 作用 Live Link 实时数据总线，提供 Source、Subject 与 Role 模型 MetaHuman Live Link MetaHuman 的实时面捕源与连接能力 Live Link Face 移动端面捕应用（iOS/Android） MetaHuman Animator 基于视频、深度或音频离线生成动画 Live Link Hub 独立的数据接收、管理与录制应用 IK Rig / IK Retargeter 骨架重定向 Control Rig 程序化绑定与后处理 Take Recorder / Sequencer 录制、剪辑与输出 Movie Render Queue 高质量离线渲染 10.2 关键 API 与概念 API / 概念 说明 ILiveLinkClient 引擎侧 Live Link 客户端，负责 Subject 管理与帧数据分发 ILiveLinkSource 自定义源需实现的数据接入接口 FLiveLinkSubjectFrameData 单个 Subject 一帧的数据容器 FLiveLinkAnimationFrameData Animation Role 的帧数据，包含骨骼 pose 与曲线 FLiveLinkBaseStaticData Subject 的基础静态描述，提供每帧曲线值的名称表（PropertyNames） FLiveLinkSkeletonStaticData Animation Role 的骨架静态描述，提供 BoneNames 与 BoneParents FLiveLinkBaseFrameData 帧数据基类，包含 WorldTime、PropertyValues 与 MetaData ULiveLinkRole Role 基类，定义数据契约 Live Link Pose AnimBP 中消费 Animation 数据的节点 Live Link Controller Actor 组件侧的数据消费方式 10.4 参考 正文中的引擎行为以 Epic 官方文档为准；项目取舍部分来自实际管线经验，不代表官方规范。\n[1] Epic Games, MetaHuman Animator in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/metahuman/metahuman-animator-in-unreal-engine\n[2] Epic Games, Real-Time Animation for MetaHumans in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/metahuman/realtime-animation-for-metahumans-in-unreal-engine\n[3] Epic Games, Live Link Face Application for MetaHuman Animator in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/metahuman/live-link-face-application-for-metahuman-animator-in-unreal-engine\n[4] Epic Games, Live Link Hub in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/live-link-hub-in-unreal-engine\n[5] Epic Games, Live Link in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/live-link-in-unreal-engine\n[6] Epic Games, Control Rig in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/control-rig-in-unreal-engine\n[7] Epic Games, IK Rig in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/ik-rig-in-unreal-engine\n[8] Epic Games, Animation Blueprints in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/animation-blueprints-in-unreal-engine\n[9] Epic Games, Take Recorder in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/take-recorder-in-unreal-engine\n[10] Epic Games, MetaHuman Assembly, Epic Games Documentation. https://dev.epicgames.com/documentation/metahuman/assembly\n[11] Epic Games, MetaHuman Animator Video Capture Guidelines, Epic Games Documentation. https://dev.epicgames.com/documentation/metahuman/metahuman-animator-video-capture-guidelines-in-unreal-engine\n[12] Epic Games, Animation from Depth Data, Epic Games Documentation. https://dev.epicgames.com/documentation/metahuman/animation-from-depth-data\n[13] Epic Games, MetaHuman Creator in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/metahuman/metahuman-creator-in-unreal-engine\n[14] Epic Games, Live Link Plugin Development in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/live-link-plugin-development-in-unreal-engine\n[15] Epic Games, FLiveLinkClient::PushSubjectStaticData_AnyThread, Unreal Engine 5.6 API Documentation. https://dev.epicgames.com/documentation/unreal-engine/API/Plugins/LiveLink/FLiveLinkClient/PushSubjectStaticData_AnyThread\n[16] Epic Games, FLiveLinkBaseStaticData, Unreal Engine 5.6 API Documentation. https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/LiveLinkInterface/FLiveLinkBaseStaticData\n[17] Epic Games, FLiveLinkBaseFrameData, Unreal Engine 5.6 API Documentation. https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/LiveLinkInterface/FLiveLinkBaseFrameData\n[18] Epic Games, ULiveLinkFaceSourceDefaults, Unreal Engine 5.6 API Documentation. https://dev.epicgames.com/documentation/unreal-engine/API/Plugins/LiveLinkFaceSource/ULiveLinkFaceSourceDefaults\n[19] Epic Games, Meet PLAVE: the K-Pop band powered by Unreal Engine, Unreal Engine Spotlight. https://www.unrealengine.com/en-US/spotlights/introducing-plave-the-unreal-engine-powered-animated-korean-boy-band\n[20] Reuters, Virtual band PLAVE mixes K-pop and technology to charm fans, China Daily Asia, May 1, 2025. https://www.chinadailyasia.com/hk/article/610818\n[21] Epic Games, UAnimInstance::NativeUpdateAnimation, Unreal Engine 5.6 API Documentation. https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/Engine/UAnimInstance/NativeUpdateAnimation\n[22] Epic Games, UAnimInstance::NativeThreadSafeUpdateAnimation, Unreal Engine 5.6 API Documentation. https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/Engine/UAnimInstance/NativeThreadSafeUpdateAnimation\n[23] Epic Games, FAnimNode_LiveLinkPose, Unreal Engine 5.6 API Documentation. https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/LiveLinkAnimationCore/FAnimNode_LiveLinkPose\n[24] Epic Games, ULiveLinkRetargetAsset, Unreal Engine 5.6 API Documentation. https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/LiveLinkAnimationCore/ULiveLinkRetargetAsset\n[25] Epic Games, ULiveLinkBlueprintLibrary, Unreal Engine 5.6 API Documentation. https://dev.epicgames.com/documentation/unreal-engine/API/Plugins/LiveLink/ULiveLinkBlueprintLibrary\n[26] Epic Games, FBX Morph Target Pipeline in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/fbx-morph-target-pipeline-in-unreal-engine\n[27] Epic Games, Morph Target Previewer, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/morph-target-previewer\n[28] Epic Games, Animation Curves in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/animation-curves-in-unreal-engine\n[29] Autodesk, Create Deformers \u0026gt; Blend Shape, Maya User Guide. https://download.autodesk.com/us/Maya/2009help/files/Create_Deformers__Blend_Shape.htm\n说明：文中界面名称与工作流以 UE 5.6 为准；不同小版本、不同装配管线（UE Cine、UE Optimized、UEFN Export、DCC Export）可能在菜单与参数上存在差异。示例结构与命名属于工程建议，并非唯一实现。\n","date":"2026-11-10T00:00:00Z","permalink":"/docs/unreal/ue-virtual-human/","title":"UE 虚拟人技术体系：MetaHuman、Live Link 面捕/动捕与动画模块集成"},{"content":"游戏引擎中的空间数据结构与算法：碰撞、导航与可见性 本文采用“结构概览 + 应用分析”的组织方式：前半部分讨论各类数据结构与算法的定义、适用范围、关键实现逻辑、使用方法与示例流程；后半部分讨论它们在碰撞检测、导航寻路和大世界可见性系统中的组合方式。 说明：记忆并不可靠。笔者记忆力有限，因此将许多内容整理为文档，以便反复复习。游戏引擎是复杂的大型软件系统，涉及多个领域；本文内容基于不同阶段遇到的实际问题和个人理解整理。\n阅读顺序可按需调整：需要补充基础时从第 1 章开始；需要解决具体问题时参阅第 2~4 章；需要快速回顾时参见第 7.4 节。\n核心结论可概括为三点：\n碰撞检测：首先通过空间索引筛选潜在碰撞对，随后由几何算法计算接触信息；高速物体需额外启用连续碰撞检测（CCD）。 导航：将可行走表面烘焙为导航网格，在多边形图上执行 A* 生成走廊，再通过漏斗算法拉直路径，最后由局部避障处理动态障碍。 可见性：依次执行距离/空间粗筛、视锥剔除和遮挡剔除；大世界还需叠加分块流送、HLOD 与坐标精度处理。 第 0 章 空间查询的统一框架 游戏引擎中表面差异较大的若干系统，在底层可归约为同一类问题：给定一个空间区域或一条空间路径，快速检索与之相关的对象。\n查询类型 输入 输出 典型场景 点查询 一个点 命中/所在区域 鼠标拾取、触发器、地形高度 范围查询 球、盒、圆柱 区域内的对象集合 爆炸、交互物、AOI、人群查询 射线查询 起点、方向、距离 第一个/所有命中 射击、视线、鼠标 picking 视锥查询 相机视锥 可见对象集合 渲染剔除 最近邻查询 点、k 最近的 k 个对象 最近敌人、掩体、交互目标 路径查询 起点、终点 可达路径 导航、寻路 连续查询 运动中的形状 首次接触时间/扫掠结果 CCD、角色移动、子弹 所有这些查询都可以拆成同一个三段式：\n\\[ T_{query} \\approx T_{traverse} + N_{candidate} \\cdot T_{test} + N_{result} \\cdot T_{output} \\]空间索引用于降低 \\(N_{candidate}\\) 的规模，精确测试负责从候选中得到最终结果。索引只解决候选集问题，不保证几何正确性；这是理解后续内容的首要原则。\nflowchart LR Q[\"查询：点 / 球 / 盒 / 射线 / 视锥 / 路径\"] --\u003e B[\"空间索引粗筛\"] B --\u003e C[\"候选对象集合\"] C --\u003e F[\"过滤：层、标签、距离、朝向、状态\"] F --\u003e E[\"精确测试：几何 / 遮挡 / 代价\"] E --\u003e R[\"结果：命中 / 最近 / 可见集 / 路径\"] U[\"对象移动、增删、流送\"] -.-\u003e B空间索引本身也有维护成本，其总成本可表示为：\n\\[ T_{total} = T_{build} + N_{query} \\cdot T_{query} + N_{update} \\cdot T_{update} \\]部分结构查询开销较低但更新开销较高；另一些结构更新开销较低但查询候选较多。结构选型本质上取决于更新模式，而非仅取决于查询复杂度。\n0.1 结构/算法速查表 表 0-1 汇总各类结构/算法的定位。第 1 章逐项展开，第 2~4 章讨论其在具体管线中的应用。\n结构 / 算法 定位 查询强项 更新成本 典型用途 均匀网格 / 空间哈希 将坐标量化为网格单元，以桶存储对象 范围、邻域 很低 子弹、掉落物、AOI、人群 四叉树 / 八叉树 递归四分/八分空间 范围、层次剔除 中等 地形、静态场景、可见性 松散八叉树 放大节点包围盒以减少重插 范围、动态对象 较低 大世界静态场景 + 动态物体 BVH 以包围盒层次组织对象 射线、视锥、范围 动态树中等 物理 broad phase、射线、网格 Sweep and Prune 沿单轴排序端点并扫描 重叠对 很低（时间相干） 刚体、布娃娃、动态道具 BSP / Portal / PVS 预计算空间与可见关系 室内可见性 基本静态 房间、走廊、室内关卡 导航网格 / 多边形图 将可行走面烘焙为带邻接的凸多边形 最近点、路径 烘焙开销高、运行开销低 角色寻路 A* / Dijkstra 在图上按代价搜索最短路 路径 每帧查询 导航、AI、网格寻路 漏斗算法 将多边形走廊收紧为折线 路径拉直 很低 导航路径后处理 HZB / 软件遮挡 / 硬件查询 以深度或预计算判断遮挡 可见性 中到高 渲染剔除 ORCA / RVO 在速度空间中规避邻居 局部避障 每帧 人群、群体移动 此外，R-tree、kd-tree、球树等结构也可用于空间查询。R-tree 更接近面向磁盘或静态 GIS 的 BVH，kd-tree 更适合点定位和离线加速；实时游戏中最常用的仍是网格、BVH、八叉树和 SAP。\n第 1 章 数据结构与算法概览 本章为结构/算法介绍模块，不绑定具体引擎模块。每项按统一框架说明：\n定义：结构或算法解决的问题； 特点：适用范围与限制； 特殊逻辑：影响正确性的关键实现细节； 使用方法：插入、查询与更新方式； 示例：具体数值或场景示例； 执行流程：执行顺序图示； 代价与限制：复杂度之外的工程成本。 1.1 均匀网格与空间哈希 定义：将世界划分为固定大小的网格单元，每个对象按其包围盒覆盖的单元登记；查询时仅访问查询区域覆盖的单元。\n特点\n查询通过坐标计算直接定位网格单元，无需树遍历，常数开销较低。 适用于尺寸接近、分布相对均匀且频繁移动的对象，例如子弹、碎片、掉落物、人群和 MMO 的 AOI。 对象尺寸差异较大时性能会退化：单个大型对象可能覆盖大量网格单元。 世界边界固定时可使用稠密数组；世界范围无限或对象稀疏时应使用哈希表。 struct CellCoord { int x, y, z; }; CellCoord ToCell(const Vec3\u0026amp; p, float cellSize) { return { (int)std::floor(p.x / cellSize), (int)std::floor(p.y / cellSize), (int)std::floor(p.z / cellSize) }; } // 简化版：用哈希表存稀疏网格 std::unordered_map\u0026lt;uint64_t, std::vector\u0026lt;Entity\u0026gt;\u0026gt; buckets; void Insert(Entity e, const AABB\u0026amp; box) { CellCoord lo = ToCell(box.min, cellSize); CellCoord hi = ToCell(box.max, cellSize); for (int z = lo.z; z \u0026lt;= hi.z; ++z) for (int y = lo.y; y \u0026lt;= hi.y; ++y) for (int x = lo.x; x \u0026lt;= hi.x; ++x) buckets[Hash(x, y, z)].push_back(e); } 特殊逻辑\n网格结构有三类典型问题：\n单元尺寸决定候选数量。经验上可取典型对象直径的 12 倍。角色高度为 1.8m 时，单元边长可取 24m；过小会导致大型对象覆盖大量单元，过大则会使单元内候选对象过多。 大型对象跨越多个单元。倒伏树木、大型载具或建筑可能覆盖数百个单元。可采用分层网格、将大型对象单独存入“超大对象列表”，或使用 BVH/八叉树处理异构尺寸。 候选重复。两个对象的 AABB 在多个单元中重叠时，同一对象对会被多次发现。查询阶段需通过 pair key 去重，或仅按对象中心所在单元登记，并额外检查边界。 工程实现中还需注意：\n负坐标必须使用 floor，不能直接进行整数截断，否则 -0.1 与 0.1 会落入同一单元。 热路径应避免频繁使用 std::unordered_map；可使用扁平 std::vector 配合开放寻址哈希，或采用覆盖固定世界范围的稠密数组。 大世界通常仅对 XZ 平面分格，Y 轴按楼层或层级处理；这样可将单元数量从立方级降至平方级。 使用方法\n插入：计算 AABB 覆盖的单元范围，将对象 ID 写入对应单元的桶。 查询：计算查询形状覆盖的单元范围，收集桶内对象，再以精确形状过滤。 更新：对象移动并跨越单元时，从旧单元删除并插入新单元；未跨越单元则不更新索引。 删除：按对象记录的单元列表逐个移除，避免全表扫描。 示例\n一个 100m × 100m 的平面，单元边长 5m，共 20 × 20 = 400 个单元。场景中存在 1000 发子弹，查询角色周围 15m 范围：\n查询覆盖约 3 × 3 = 9 个单元； 平均每个单元 2.5 发子弹，候选数量约为 22； 与遍历 1000 发子弹相比，候选数量约降至 1/45。 若将单元边长改为 1m：\n单元数量增至 10000； 一个 10m 长的飞行器将覆盖约 100 个单元； 插入与删除开销显著增加。 执行流程\nflowchart LR A[\"对象 AABB\"] --\u003e B[\"计算覆盖的格子范围\"] B --\u003e C[\"逐格插入对象 ID\"] C --\u003e D[\"查询形状覆盖格子\"] D --\u003e E[\"收集候选并去重\"] E --\u003e F[\"精确形状测试\"]1.2 四叉树、八叉树与松散八叉树 定义：四叉树为二维形式，八叉树为三维形式；当节点内对象数量超过阈值时分裂为四个或八个子节点，对象数量减少后再合并。\n特点\n该结构适用于静态或半静态场景，例如地形、建筑、植被、关卡几何和可见性层次。查询从根节点开始，若节点区域与查询区域不相交，则对整棵子树剪枝。\n特殊逻辑\n分裂阈值：节点内对象数超过 N 时分裂，低于 M 时合并。N 过小会导致树深增加，N 过大则退化为线性列表。 最大深度：必须设置上限，否则对象重合时可能无限分裂。 对象跨越节点边界是该结构的主要问题，常见处理方式有三种：\n对象仅挂载到包含其中心的节点，查询时同时测试相邻节点； 将对象复制到所有相交的叶子节点，查询时去重； 使用松散八叉树（loose octree）：每个节点的实际包围盒按 looseness 系数（常见 1.5~2.0）扩张，对象按中心分配；只要对象未越出松弛边界，就无需重新插入。 松散八叉树的作用是为动态对象预留移动余量，避免对象在节点边界附近频繁删除与插入。该结构适用于大世界静态场景与少量动态物体并存的场合。\n示例\n一个 1024m × 1024m 的场景，四叉树最大深度为 5：\n第 0 层为 1024m，第 1 层为 512m，……第 5 层为 32m； 叶子单元尺寸为 32m × 32m； 场景中分布 200 棵静态树，查询角色周围 100m 范围时，仅需访问覆盖该圆形的若干叶子节点，无需遍历全部 200 棵。 执行流程\nflowchart TD R[\"根节点 1024m\"] --\u003e A[\"西北 512m\"] R --\u003e B[\"东北 512m\"] R --\u003e C[\"西南 512m\"] R --\u003e D[\"东南 512m\"] A --\u003e A1[\"继续分裂到 32m 叶子\"] B --\u003e B1[\"对象少，保持粗粒度\"] C --\u003e C1[\"继续分裂到 32m 叶子\"] D --\u003e D1[\"对象少，保持粗粒度\"]代价与限制\n对象跨越边界会导致重复或漏查；松散八叉树可缓解该问题，但无法完全消除。 每帧重建整棵树通常不可接受；静态部分应只构建一次，动态对象使用独立层级。 指针式树结构的缓存局部性较差；热路径可将其扁平化为数组。 分裂/合并阈值若仅凭经验设定，可能导致树过深或过浅。 1.3 BVH：现代引擎最通用的空间索引 定义：BVH（Bounding Volume Hierarchy）是一种二叉树结构，叶节点存放一个或少数几个对象，内部节点的 AABB 为子节点 AABB 的并集。\nstruct BVHNode { AABB bounds; int left; // 内部节点：左孩子索引 int right; // 内部节点：右孩子索引 int object; // 叶子：对象索引 bool isLeaf; }; 构建过程的核心是节点划分策略，最常用的方法是 SAH（Surface Area Heuristic）：\n\\[ C = C_{trav} + \\frac{A_L}{A} C_L + \\frac{A_R}{A} C_R \\]其含义是：若左子节点的包围盒面积较大，则射线或视锥进入该区域的概率更高，应承担更高的期望遍历成本。SAH 枚举候选切分并选择期望成本最小的方案；工程上通常采用“分桶 SAH”，将候选切分数量从 \\(O(n)\\) 降至常数级。\n特点\n主要优点：\n对尺寸差异较大的对象具有较好的稳定性； 射线、视锥与范围查询可复用同一棵树； 动态 BVH 支持插入、删除和旋转平衡，Box2D 的 b2DynamicTree 为典型实现； 可扁平化为数组，具备良好的缓存局部性。 主要代价：\n构建与重构开销较高； 动态更新会降低树质量，需要定期重构； 遍历存在分支预测与指针跳转开销，极端情况下性能不如网格。 特殊逻辑\n常用的工程优化是 fat AABB：将叶节点包围盒按固定 margin 扩张。对象在小范围内移动时仅更新叶子 bounds，不改变树结构；只有越出 fat bounds 时才删除并重新插入。\n使用方法\n构建：递归选择切分轴和切分点，生成孩子，叶子里存对象。 查询：维护一个栈；节点不相交就跳过；叶子做精确测试；需要最近命中时按距离优先遍历近孩子。 更新：删除旧叶子、插入新叶子、refit 祖先；必要时旋转或重建子树。 示例\n四个对象 A、B、C、D：\n首先将 A+B 合并为一个节点，将 C+D 合并为一个节点； 然后将这两个节点合并为根节点； 射线从左上射入，先测试根节点，再测试较近的子节点；若 A+B 节点不相交，则跳过整棵子树，仅测试 C、D。 执行流程\nflowchart TD R[\"根 AABB：A+B+C+D\"] --\u003e L[\"左节点：A+B\"] R --\u003e R2[\"右节点：C+D\"] L --\u003e A[\"叶子 A\"] L --\u003e B[\"叶子 B\"] R2 --\u003e C[\"叶子 C\"] R2 --\u003e D[\"叶子 D\"]代价与限制\n每帧重建 BVH 开销较高；动态对象应使用动态树或分层索引。 仅执行 refit 而不重建会导致包围盒逐渐膨胀，查询性能下降。 射线遍历若不对子节点排序，会优先测试较远节点，产生额外开销。 大型对象会增大祖先节点 AABB，降低剪枝效率；可将其放入单独列表或使用分层结构。 1.4 Sweep and Prune：时间相干性与增量排序 定义：SAP 沿单一坐标轴对所有对象的 AABB 端点排序；扫描过程中维护活动列表，仅对活动列表中的对象检测另外两个轴。\n特点\n其性能优势主要来自时间相干性，而非渐进复杂度：物理对象相邻两帧的位置变化通常较小，端点数组接近有序，使用插入排序可使更新开销接近 \\(O(n)\\)。\n适用场景：ragdoll、大量尺寸相近的动态刚体、连续运动的角色。\n不适用场景：大型静态物体与大量小物体混合，或对象尺寸差异极端。\n特殊逻辑\n端点排序：每个 AABB 在所选轴上具有两个端点；左端点进入活动列表，右端点离开。 活动列表：扫描至左端点时，将当前对象与活动列表中的每个对象检测另外两个轴；扫描至右端点时将其移除。 增量排序：帧间仅执行插入排序，避免每帧调用完整的 std::sort。 轴选择：选择方差最大的轴，或每帧动态选择轴；单轴分布退化时效果会下降。 大型对象：横跨整个场景的对象会长期保留在活动列表中，导致候选数量急剧增加。 使用方法\n初始化：按 x 端点排序，建立活动列表。 每帧：更新移动对象的端点，插入排序；扫描并生成重叠对。 过滤：层、通道、睡眠状态在生成 pair 后立即过滤。 示例\n三个盒子在 x 轴：\nA: [0, 2] B: [1, 4] C: [3, 5] 端点排序：A左0、B左1、A右2、C左3、B右4、C右5。\n扫描：\n0 进入 A，活动列表 {A}； 1 进入 B，和 A 测 y/z，得到 A-B； 2 A 离开； 3 进入 C，和 B 测 y/z，得到 B-C； 4 B 离开； 5 C 离开。 A-C 在 x 上不重叠，不需要测。\n执行流程\nflowchart LR E0[\"0: A 左\"] --\u003e E1[\"1: B 左\"] E1 --\u003e E2[\"2: A 右\"] E2 --\u003e E3[\"3: C 左\"] E3 --\u003e E4[\"4: B 右\"] E4 --\u003e E5[\"5: C 右\"] E1 -. \"活动列表 {A,B} → 测 A-B\" .-\u003e P1[\"候选对 A-B\"] E3 -. \"活动列表 {B,C} → 测 B-C\" .-\u003e P2[\"候选对 B-C\"]代价与限制\n对象尺寸差异较大时，活动列表会显著增长。 端点相等及浮点误差需要一致的排序规则。 单轴排序仅完成第一维筛选，另外两个轴仍需检测；排序完成并不代表检测结束。 大世界坐标下端点数值较大，需要注意浮点精度与比较方式。 1.5 其他结构：BSP、Portal、PVS、R-tree 定义：将室内空间划分为房间或凸单元，通过 portal 连接，并预计算每个单元可见的单元集合；运行时仅查询预计算结果。\n特点\n运行时开销很低，适用于室内、走廊、洞穴和固定关卡。 预处理开销较高，对动态门、可破坏墙体和移动房间支持较差。 PVS 采用保守策略：允许过度绘制，但不可漏绘。 与视锥剔除为互补关系，不能相互替代。 特殊逻辑\n空间单元：BSP 用平面递归切分，或手工划分房间；每个单元通常是凸的。 Portal：两个单元之间的连接面（门、窗、走廊口）。可见性通过 portal 传播。 PVS 位掩码：每个单元存一个 bitset，记录它能看到的单元集合；运行时取当前相机所在单元的 PVS，再和视锥求交。 Anti-portal：用于阻断不应传播的可见性，减少过度保守。 动态物体：PVS 仅处理静态空间；动态物体通常单独执行视锥或遮挡测试。 使用方法\n离线：切分空间、标记 portal、从每个单元做可见性传播、烘焙 PVS。 运行时：相机所在单元 → 取 PVS → 和视锥求交 → 只提交可见单元里的静态物体。 动态门：门打开/关闭时切换 portal 状态，或退化为运行时遮挡。 示例\n三个房间 A、B、C：\nA 与 B 之间设有门 P1； B 与 C 之间设有门 P2； A 无法看到 C，因为视线需要经过两次转折。 PVS：\nPVS[A] = {A, B} PVS[B] = {A, B, C} PVS[C] = {B, C} 相机位于 A 时，仅提交 A、B；C 虽可能位于视锥内，但被墙体遮挡。\n执行流程\nflowchart LR A[\"房间 A\"] ---|\"P1 门\"| B[\"房间 B\"] B ---|\"P2 门\"| C[\"房间 C\"] A -. \"PVS[A] = A,B\" .-\u003e B C -. \"PVS[C] = B,C\" .-\u003e B代价与限制\nPVS 内存占用可能较高，最坏情况下与单元数量呈平方关系。 动态门/破坏墙需要重新计算或运行时补遮挡。 过度保守会导致过度绘制，过度激进则会造成漏绘；漏绘属于可见错误。 大世界开放场景不适合纯 PVS，通常改用 HZB 或软件遮挡。 R-tree 更接近面向磁盘或静态 GIS 的 BVH，kd-tree 更适合点定位和离线加速；它们并非实时游戏的主要索引结构，但其设计思想具有参考价值。\n1.6 导航网格 / 多边形图 定义：将可行走表面烘焙为一组带邻接关系的凸多边形，角色在多边形图上执行寻路，而非直接在三角形网格上搜索。\n特点\n贴合可行走表面，路径自然，查询精度较高。 烘焙开销较高，动态修改较复杂。 多边形为凸，内部任意两点的连线不会穿越墙体，便于路径生成。 运行时数据通常按 tile 划分，支持流送和局部重建。 特殊逻辑\n凸多边形：每个多边形内部可走，边是边界或邻接。 邻接关系：每条边记录相邻多边形；A* 的“邻居”即这些邻接多边形。 Off-Mesh Link：跳跃、攀爬、传送、门，单独建链接，带代价和方向。 Detail Mesh：多边形用于路径生成，细节网格用于高度查询；角色贴地时通过 detail mesh 获取高度。 Tile：大世界按块构建，tile 边界必须正确衔接；仅重建受影响的 tile。 使用方法\n烘焙：Recast 将三角形场景体素化、分区，并生成 PolyMesh/DetailMesh。 查询：最近多边形、随机点、射线、路径。 更新：动态障碍切割局部 tile，或运行时用 Off-Mesh Link/避障绕开。 示例\n三个多边形 A、B、C：\nA 和 B 共享 portal P1； B 和 C 共享 portal P2； 从 A 到 C，A* 在图上得到 A → B → C； 漏斗算法随后根据 P1、P2 的左右边界，将路径转换为折线。 执行流程\nflowchart LR A[\"多边形 A\"] ---|\"P1\"| B[\"多边形 B\"] B ---|\"P2\"| C[\"多边形 C\"] A -. \"A* 邻居\" .-\u003e B B -. \"A* 邻居\" .-\u003e C代价与限制\n导航网格与碰撞几何不一致时，会出现“导航网格显示可通行，但实际碰撞体无法通过”的问题。 agent 半径或高度烘焙不正确，会导致贴墙或过门失败。 跨 tile 接缝丢失邻接关系，角色会在边界处受阻。 动态障碍变化时每帧重建整张导航网格，会导致性能显著下降。 1.7 A* / Dijkstra：图上的最短路径搜索 定义：A* 以“已付出代价与到终点估计代价之和”对待探索节点排序；Dijkstra 是启发式函数恒为 0 的 A*。\n特点\n图可为网格、导航多边形、路点或分层 cluster。 A* 的搜索效率通常高于 Dijkstra，因为启发式函数将搜索导向终点。 启发式函数不得高估真实代价（admissible），否则无法保证最优性。 内存管理与开放列表实现往往比算法本身对性能影响更大。 特殊逻辑\n\\[ f(n) = g(n) + h(n) \\] \\(g(n)\\)：从起点到 \\(n\\) 的真实代价； \\(h(n)\\)：从 \\(n\\) 到终点的估计； 每次从开放列表取 \\(f\\) 最小的节点； 如果发现更小的 \\(g\\)，更新父节点并将节点重新放回开放列表（reopen）； 网格常用欧氏距离或八方向 octile 距离；导航网格常用欧氏距离。 使用方法\n节点：多边形/格子/路点。 边：邻接关系 + 代价（距离、坡度、区域类型、风险）。 开放列表：二叉堆。 关闭列表：visited stamp 或布尔数组，避免每帧分配哈希表。 结束：到达目标，或开放列表空（不可达）。 示例\n3×3 网格（只允许上下左右移动），S 在左上，G 在右下，中间有障碍：\nS . . . # . . . G 从 S 出发，A* 会优先探索 \\(f\\) 小的格子；绕过中间的 #，最终得到一条 4 步路径。Dijkstra 同样可找到该路径，但会向四周均匀扩展，探索更多节点。\n执行流程\nflowchart TD A[\"起点加入开放列表\"] --\u003e B[\"取 f 最小的节点\"] B --\u003e C{\"是终点？\"} C --\u003e|是| D[\"回溯父节点，输出路径\"] C --\u003e|否| E[\"遍历邻居，计算 g\"] E --\u003e F{\"g 更小？\"} F --\u003e|是| G[\"更新父节点，加入开放列表\"] F --\u003e|否| H[\"跳过\"] G --\u003e B H --\u003e B代价与限制\n启发式不可采纳，路径不是最优。 不 reopen，遇到更优路径也不更新。 每次查询都分配大量节点或哈希表，导致 GC 或分配器压力较大。 仅按距离计算代价，忽略坡度、门、跳跃和危险区域。 目标不可达时直接失败，缺少“最接近可行点”的降级策略。 1.8 漏斗算法：多边形走廊收紧 定义：A* 生成由若干多边形构成的走廊；漏斗算法沿 portal 的左右边界逐步收紧，输出一条尽可能短的折线路径。\n特点\n仅处理几何收紧，不重新执行寻路。 计算开销较低，通常作为路径后处理的最后一步。 依赖正确的 portal 顺序与左右边界定义。 相比“取多边形中心再平滑”的方法更可靠，因为它严格尊重可行走区域边界。 特殊逻辑\nPortal：相邻多边形的共享边，有左端点和右端点。 漏斗：从起点开始，左边界和右边界张开。 收紧：每经过一个 portal，检查新左右边界是否穿过旧边界。 输出角点：如果新边界穿过旧边界，前一个角点必须保留，输出它并重置漏斗。 SSF：Mikko Mononen 的 Simple Stupid Funnel Algorithm 是最常见实现。 使用方法\n从 A* 路径生成 portal 列表； 初始化左右边界； 依次处理 portal，收紧边界； 输出起点、角点、终点； 可再做一次碰撞/高度修正。 示例\n路径经过两个 portal：\nP1 左 L1、右 R1； P2 左 L2、右 R2； 漏斗从起点张开，经过 P1 后左边界被 L1 限制； 经过 P2 时，如果 R2 穿过右边界，说明 P1 的右端点是一个必须保留的角点； 输出“起点 → P1 角点 → 终点”的折线。 执行流程\nflowchart LR S[\"起点\"] --\u003e P1[\"Portal P1：L1 / R1\"] P1 --\u003e P2[\"Portal P2：L2 / R2\"] P2 --\u003e G[\"终点\"] P1 -. \"收紧左边界\" .-\u003e F[\"漏斗\"] P2 -. \"穿过旧边界则输出角点\" .-\u003e F F --\u003e O[\"输出折线路径\"]代价与限制\nportal 顺序错误会导致路径穿墙。 左右端点定义颠倒会使漏斗向错误方向收紧。 共线或退化 portal 需要特殊处理。 仅拉直路径而不检测碰撞时，动态障碍可能位于路径折线上。 1.9 遮挡剔除方案：HZB、软件遮挡与硬件查询 定义：视锥剔除仅判断物体是否位于画面范围内；遮挡剔除进一步判断位于画面内的物体是否被其他几何体遮挡。\n特点\nPVS/Portal 属于预计算方案；HZB 与软件遮挡属于运行时方案。 遮挡剔除必须保持保守性，不得剔除实际可见的物体。 应先处理大型遮挡物，再测试小型物体；若使用尚未写入深度缓冲的遮挡物执行测试，结果不可靠。 通常与 LOD、HLOD 和 GPU culling 配合使用。 特殊逻辑\nHZB（Hierarchical Z-Buffer）：将深度缓冲构建为 mip 金字塔，每个上层像素存储下层区域的最大深度；测试时将物体 AABB 投影到屏幕，选择合适的 mip 层级，并比较物体最近深度与遮挡深度。 软件遮挡：由 CPU 将大型遮挡物以低分辨率光栅化到深度缓冲，再测试对象 AABB；开销可控，但结果偏保守。 硬件遮挡查询：由 GPU 查询对象是否被遮挡；结果准确，但存在延迟，易造成 GPU 停顿。 延迟一帧：许多方案使用上一帧的深度结果执行本帧剔除，允许少量误差以避免同步等待。 使用方法\n先执行距离与视锥剔除，减少候选对象； 优先提交大型遮挡物，建立深度缓冲； 对剩余对象执行 HZB 或软件遮挡测试； 通过测试的对象提交渲染；未通过测试的对象在下一帧重新测试或降级处理。 示例\n1024×1024 的深度缓冲：\nmip0 1024²，mip1 512²，mip2 256²，mip3 128²； 一个物体的屏幕包围盒约 100×40 像素； 选 128² 或 256² 的 mip 做一次保守比较； 如果物体最近深度大于该区域最大遮挡深度，说明被挡住，剔除。 执行流程\nflowchart TD D0[\"深度缓冲 1024²\"] --\u003e D1[\"Mip1 512²：取 2×2 最大深度\"] D1 --\u003e D2[\"Mip2 256²\"] D2 --\u003e D3[\"Mip3 128²\"] D3 --\u003e T[\"物体 AABB 投影 → 选 mip → 比较深度\"] T --\u003e V{\"被挡住？\"} V --\u003e|是| C[\"剔除\"] V --\u003e|否| R[\"提交渲染\"]代价与限制\n保守性不足会导致漏绘，使可见物体突然消失。 同步回读 GPU 查询结果会造成 GPU 停顿。 未按遮挡物优先级排序，在大型遮挡物写入深度前执行测试，结果不可靠。 透明物体、特效和粒子通常不参与遮挡深度写入。 1.10 ORCA / RVO：局部避障 定义：每个 agent 在速度空间中为邻居构造速度障碍，并选择最接近期望速度且不会导致碰撞的新速度。\n特点\n用于解决全局路径正确、但运动过程中发生碰撞的问题。 计算范围是局部的，不需要重新执行寻路。 适用于人群、队伍和大量 NPC。 不保证全局最优，也不处理复杂地形；需与导航路径配合使用。 特殊逻辑\n速度障碍：如果两个 agent 保持当前相对速度，未来会碰撞，这个相对速度就落在速度障碍里。 半平面：ORCA 将速度障碍近似为半平面约束；每个邻居对应一个约束。 线性规划：在所有半平面约束下，找离期望速度最近的速度。 互惠：两个 agent 各自承担部分避让责任，避免单个 agent 承担全部避让。 时间视野：仅考虑未来一段时间；窗口过短会导致抖动，过长会过度保守。 使用方法\n每个 agent 有期望速度（来自路径）； 收集邻居（空间哈希/BVH）； 为每个邻居构造 ORCA 半平面； 解线性规划得到新速度； 用新速度移动，下一帧重复。 示例\n两个 agent 相向而行：\nA 的期望速度向右，B 的期望速度向左； 若不执行避让，相对速度将指向碰撞方向； ORCA 使 A 略微向右下、B 略微向左下调整； 两者以较小横向偏移相互通过，路径仅产生小幅偏移。 执行流程\nflowchart LR A[\"期望速度\"] --\u003e B[\"收集邻居\"] B --\u003e C[\"构造速度障碍 / 半平面\"] C --\u003e D[\"线性规划求新速度\"] D --\u003e E[\"移动并更新位置\"] E --\u003e B代价与限制\n邻居数量过多时，线性规划开销较高；应先用空间索引筛选邻居。 约束过强会导致抖动，过弱会导致 agent 拥挤。 仅处理 agent 之间的避让，不处理静态障碍。 与导航路径不一致时，agent 可能被推离可行走面。 第 2 章 碰撞检测：broad phase 与空间索引 第 1 章中的网格、BVH 与 SAP 主要服务于 broad phase；本章讨论它们如何与 narrow phase、CCD 衔接。\n碰撞检测的完整链路是：\nflowchart LR A[\"物理步 / 角色移动\"] --\u003e B[\"Broad phase：空间索引\"] B --\u003e C[\"候选对象对\"] C --\u003e D[\"过滤：层、通道、睡眠、触发器\"] D --\u003e E[\"Narrow phase：精确几何\"] E --\u003e F[\"接触流形 / 穿透深度\"] F --\u003e G[\"约束求解 / 位置修正\"] E -. \"高速物体\" .-\u003e H[\"CCD / TOI：高速物体补测\"] H -. \"TOI 接触\" .-\u003e G2.1 Broad phase：候选集筛选 Broad phase 的任务并非计算接触点，而是尽快排除不可能相交的对象对。该阶段允许少量误报，但不可漏报。\n常见选型：\n空间哈希/网格：适用于大量子弹、碎片、掉落物、角色和触发器；对象尺寸接近时综合开销最低。 BVH/动态树：角色、道具、载具、可破坏物混合；尺寸差异大；还要支持射线和视锥查询。 SAP：大量尺寸相近的刚体、布娃娃、物理道具。 八叉树/松散八叉树：静态场景 + 少量动态对象；大世界分块。 一个典型的 broad phase 循环：\n// 伪代码：先收集候选对，再去重 for (Entity a : movingEntities) { AABB queryBox = Expand(a.bounds, margin); for (Entity b : index.Query(queryBox)) { if (a.id \u0026gt;= b.id) continue; // 每对只处理一次 if (!LayersCanCollide(a, b)) continue; if (!Overlap(a.fatBounds, b.fatBounds)) continue; pairs.push_back({a, b}); } } 关键工程点：\n膨胀 AABB（fat AABB）：扩大包围盒，减少对象微动导致的树更新；越出膨胀边界后再重新插入。 Pair cache：保留上一帧的接触对，减少重复计算；用 generation/handle 防止悬空引用。 更新传播：对象移动后，更新叶子 bounds，再向上 refit 祖先；如果树结构失衡到阈值，重建子树。 睡眠/岛：静止对象不参与 broad phase 更新；一整片静止刚体作为一个 island 休眠。 过滤器：层、碰撞通道、触发器与查询标志应在 broad phase 之后、narrow phase 之前尽可能剔除，避免精确测试处理无效候选。 2.2 Narrow phase：几何算法与空间索引的分工 Broad phase 生成候选对之后，narrow phase 才判断“是否接触、接触位置以及穿透深度”。\n常见组合：\n形状 算法 球-球、球-胶囊、胶囊-胶囊 解析距离，最快 OBB-OBB SAT（分离轴定理），15 根轴 凸包-凸包 GJK 求距离，EPA 求穿透深度；或 MPR 网格-网格 三角形 BVH + 三角形对测试 高度场 按格子局部取三角形，避免全量遍历 复合形状 子形状 broad phase + 局部 narrow phase 需要特别注意的几点：\n碰撞结果并非简单的布尔值，而是接触流形：包含位置、法线、穿透深度和多个接触点。接触点不足会导致刚体抖动。 持续接触需启用 warm starting：沿用上一帧的冲量，否则堆叠稳定性会下降。 网格碰撞需处理单面三角形、内部边和退化三角形；角色贴墙移动时的“幽灵碰撞”通常来自内部边。 Broad phase 的误报可以接受，漏报会造成严重错误，因此索引的保守性比精确性更重要。 2.3 CCD：高速物体的穿透问题 离散碰撞只在离散时间点检测。如果子弹一帧移动 5 米，墙厚 10 厘米，它可能直接从墙的一侧跳到另一侧，中间没有采样点，即 tunneling。\n解决方法：\nSwept AABB / 扫掠形状：将运动轨迹构造为扫掠体，先与静态几何求交。 Conservative Advancement：使用当前距离和相对速度推进时间，直到距离小于阈值或到达 TOI。 Speculative Contacts：在 broad phase 阶段将“下一帧可能接触”的接触点纳入，使求解器提前处理。 Substepping：将一帧划分为多个小步；实现简单但开销较高。 Conservative Advancement 的基本推导：\n\\[ t_{next} = t + \\frac{d(t)}{|v_{rel}| + \\epsilon} \\]其中 \\(d(t)\\) 是当前最近距离。只要距离还大于 0，就以“不会穿透”的速度前进；重复直到接触或超过时间上限。\n角色控制器通常不启用完整刚体 CCD，而采用胶囊体 sweep + 滑动 + 台阶检测 + 地面吸附：\n先沿移动方向对胶囊体执行 sweep； 与墙面接触时，将速度投影到墙面并继续滑动； 遇到台阶时尝试抬升； 最后向下吸附地面，避免下坡时悬空。 2.4 碰撞系统的常见问题 现象 常见原因 角色在墙角抖动 接触点不足、法线跳变、warm starting 丢失 高速物体穿墙 未启用 CCD，或 CCD 仅覆盖动态对象而未覆盖静态网格 物体在网格内部边处受阻 单面三角形或内部边处理不当，或角色半径过小 物理步开销逐渐增大 broad phase 每帧重建、pair cache 失效、睡眠机制未生效 触发器重复触发 缺少进入/离开状态机，或重叠查询每帧均发送事件 大世界坐标漂移 float 精度不足；碰撞形状原点距世界原点过远 第 3 章 导航寻路：从可行走面到可执行路径 第 1 章已分别讨论导航网格、A*、漏斗算法与 ORCA；本章讨论它们在引擎中如何组合为可执行路径。\n导航系统可划分为四个层次：\n表示：世界中哪些区域可以行走（导航网格、格子、路点）。 查询：查询点位于哪个多边形，以及最近的可走点位置。 寻路：从起点到终点经过哪些多边形（A*、Dijkstra、分层寻路）。 执行：如何生成自然的移动路径（漏斗拉直、局部避障、人群）。 3.1 四种常见表示 表示 优点 缺点 适用 格子 实现简单、动态修改成本低 内存占用高、路径呈锯齿状、精度受格距限制 策略、塔防、体素、原型 路点 美术可控、数据量小 不覆盖自由空间，路径易受路点分布影响 巡逻、固定路线、旧式 FPS 导航网格 贴合可行走面、路径自然、查询精确 烘焙成本高、动态修改复杂 大多数 3D 游戏 体素导航 支持多层、破坏、三维移动 内存和查询成本高 体素世界、飞行、复杂立体场景 3.2 Recast：导航网格的构建流程 UE 与 Unity 的导航底层均主要基于 Recast/Detour。Recast 负责离线构建，Detour 负责运行时查询。\nflowchart LR A[\"三角形场景\"] --\u003e B[\"体素化 / 光栅化\"] B --\u003e C[\"高度场 Heightfield\"] C --\u003e D[\"紧凑高度场 CompactHeightfield\"] D --\u003e E[\"可行走区域标记\"] E --\u003e F[\"分水岭分区 Watershed\"] F --\u003e G[\"轮廓 Contours\"] G --\u003e H[\"多边形网格 PolyMesh\"] H --\u003e I[\"细节网格 DetailMesh\"] I --\u003e J[\"Detour NavMesh 运行时数据\"]各阶段说明：\n体素化：将三角形光栅化到规则体素网格，标记实心区域、可行走区域、坡度与高度。 高度场：每个 XZ 单元记录若干层 span；多层结构可用于表达桥、楼梯和洞穴。 紧凑高度场：移除多余 span，仅保留可行走表面与邻居关系。 分水岭分区：根据到障碍物的距离场将可行走区域划分为若干区域，再合并过小区域；该步骤决定导航多边形的形状。 轮廓：沿区域边界提取轮廓并简化折线。 PolyMesh：将轮廓转换为凸多边形，并建立多边形邻接关系。 DetailMesh：记录每个多边形内部的高度细节，使角色能够贴合斜坡和台阶。 Detour：将多边形、邻接关系、Off-Mesh Link 与区域代价组织为运行时查询结构。 3.3 运行时数据结构 导航网格的核心并非三角形，而是带邻接关系的多边形图：\n每个多边形：顶点、中心、法线、区域 ID、代价标志； 每条边：相邻多边形索引，或者“墙/边界”； Off-Mesh Link：跳跃、攀爬、传送、门； 查询结构：按 tile 分块，按 AABB 建 BVH/网格，支持最近多边形查询。 常见查询：\nFindNearestPoly：将任意点吸附到最近的可走多边形； FindPath：在多边形图上执行 A*； Raycast：沿导航面投射射线，判断两点间是否可直达； FindRandomPoint：在可行走面上随机取点； GetPolyHeight：获取多边形内部的高度。 UE 中对应 RecastNavMesh、FNavLocation、UNavigationSystemV1 这一层；Unity 中对应 NavMeshSurface、NavMeshAgent、NavMeshLink。名称不同，但底层概念一致。\n3.4 A* 在导航系统中的应用 第 1 章已讨论 A* 的基本机制；本节说明其在导航图中的具体应用方式。\nA* 的核心公式：\n\\[ f(n) = g(n) + h(n) \\] \\(g(n)\\)：从起点到 \\(n\\) 的真实代价； \\(h(n)\\)：从 \\(n\\) 到终点的启发式估计； 只要 \\(h\\) 不高估真实代价（admissible），A* 就能找到最优路径。 伪代码：\nstd::priority_queue\u0026lt;Node, std::vector\u0026lt;Node\u0026gt;, CompareByF\u0026gt; open; open.push(start); g[start] = 0; while (!open.empty()) { Node cur = open.top(); open.pop(); if (cur == goal) return Reconstruct(cur); for (Edge e : graph.Neighbors(cur)) { float ng = g[cur] + e.cost; if (ng \u0026lt; g[e.to]) { g[e.to] = ng; parent[e.to] = cur; open.push({e.to, ng + Heuristic(e.to, goal)}); } } } 工程实现中，以下因素通常比算法本身更影响性能：\n启发式函数：网格常用欧氏距离或八方向 octile 距离，导航网格常用欧氏距离。权重略大于 1 的 weighted A* 可换取更高搜索速度，但不再保证最优性。 数据结构：二叉堆通常足够；节点池与 visited stamp 可避免每帧分配；应避免在每个节点上使用哈希表查询 g。 代价模型：不应仅按距离计算代价。草地、沼泽、楼梯、门和跳跃链接都应设置区域代价；A* 的边权应表示“时间/风险”，而非单纯距离。 部分路径：目标不可达时返回“最接近目标的可行点”，优于直接失败。 分层寻路：大世界先用 HPA*/分层图寻找跨区块走廊，再在局部 tile 上细化。 3.5 漏斗算法：走廊收紧与折线生成 第 1 章已讨论漏斗算法的几何过程；本节说明其在导航管线中的位置。\nA* 在多边形图上得到的是依次经过的多边形序列，路径会贴近多边形中心或边界，呈现折线形态。漏斗算法（Funnel Algorithm）将这条走廊收紧为一条尽可能短的折线。\n做法：\n将路径视为一系列 portal（相邻多边形共享的边）； 维护一个漏斗，左边界和右边界从起点张开； 每经过一个 portal，检查左右边界是否仍然有效； 如果新边界穿过旧边界，说明前一个角点必须保留，输出它并重置漏斗； 最后输出终点。 Mikko Mononen 的 Simple Stupid Funnel Algorithm（SSF） 是最常见的实现。相比“先取多边形中心再平滑”的方法，它直接处理可行走区域边界，因此更可靠。\n3.6 动态障碍、人群与流场 导航数据并非一次烘焙即可长期有效。动态障碍和大量 agent 会引入新的问题：\n局部避障：ORCA/RVO 根据邻居的速度和半径计算避让速度；它不改变全局路径，仅负责避免与邻近 agent 碰撞。 导航网格切割/雕刻：动态障碍将多边形标记为不可走，或在运行时挖洞；需要重建局部 tile。 Tile Cache：Detour 的 dtTileCache 使用可重建 tile 处理动态障碍，避免重新构建整张导航网格。 导航 Invoker：大世界仅在高优先级区域（如玩家附近）动态生成导航，远处不生成。 流场（Flow Field）：对大规模单位，先用 Dijkstra 在格子上计算“每个格子指向下一步”的向量场，由所有单位共享；适用于 RTS、塔防和大规模单位寻路。其缺点是动态障碍需要重新计算，且路径质量受格子分辨率限制。 分层 A*：将地图划分为 cluster，先计算 cluster 级路径，再计算局部路径；适用于超大世界。 3.7 大世界导航的工程点 分块（tile）：每块导航网格独立构建、独立流送；只重建脏 tile。 Tile 边界：相邻 tile 的边必须正确衔接；否则 agent 会在接缝处受阻。 多层：桥、楼梯和洞穴通过多层 navmesh 或 Off-Mesh Link 表达。 异步：烘焙与查询不应阻塞主线程；查询可使用快照，避免读取未完成数据。 版本：导航数据需携带版本号；热更新或流送时旧路径需可失效。 3.8 导航常见问题 现象 常见原因 角色贴墙移动或在转角受阻 agent 半径/高度烘焙不正确，或漏斗算法边界处理错误 路径可达但无法通过 导航网格与碰撞几何不一致；门或可破坏物未同步 跨 tile 时受阻 tile 接缝未对齐，或邻接关系丢失 上下楼路径异常 多层 navmesh 未区分，或 Off-Mesh Link 代价设置错误 人群抖动 局部避障参数过强、更新频率不一致、缺少优先级 大世界烘焙开销过高 未使用分块或 Invoker，整张图一次性构建 第 4 章 大世界可见性：附近对象与不可见剔除 第 1 章已讨论 PVS、HZB 与遮挡剔除方案；本章将其放回“附近对象筛选 + 渲染剔除 + 大世界流送”的完整链路。\n“角色附近的可见物”涉及两个不同问题，需要分别处理：\n问题 关注点 常用手段 游戏逻辑的“附近” 可交互、可攻击、可触发、网络同步 范围查询、空间哈希、AOI 网格、优先级评分 渲染的“可见” 相机是否可见、是否被遮挡 视锥剔除、遮挡剔除、LOD、HLOD 若将两者混用，就会出现“背后的交互物因不在视锥内而无法高亮”或“视野外的敌人仍在执行完整 AI 更新”等问题。\n4.1 附近交互物的查询流程 典型查询是“以角色为圆心、半径 R 范围内的所有可交互对象”。最直接的做法是球体 overlap，但可通过以下步骤降低开销并提高稳定性：\n通过空间哈希或网格获取角色周围 3×3 或 5×5 的单元； 对单元内对象执行距离平方比较，避免开方运算； 按层、标签和状态过滤（可交互、存活、未冷却）； 若玩法需要，使用视线射线确认目标未被墙体遮挡； 按距离、朝向和优先级排序，取前 N 个； 引入迟滞：进入半径与离开半径不同，避免边界抖动导致 UI 频繁闪烁。 // 伪代码：环形扩展 + 提前退出 for (int ring = 0; ring \u0026lt;= maxRing; ++ring) { for (Cell cell : CellsInRing(playerCell, ring)) { for (Entity e : grid[cell]) { if (!IsInteractable(e)) continue; float d2 = DistanceSquared(player.pos, e.pos); if (d2 \u0026gt; radius * radius) continue; candidates.push_back({e, d2}); } } // 如果当前环的最小可能距离已经大于已找到的第 N 名，停止 if (ring * cellSize \u0026gt; NthBestDistance(candidates, N)) break; } 当对象数量较多时，可使用最大堆维护前 N 个最近对象；若还需考虑朝向，应将“距离 + 角度 + 优先级”合成为统一评分，而非仅按距离排序。\nMMO 的 AOI（Area of Interest）采用类似思路，但服务于网络同步：网格 cell、九宫格、距离带、更新频率分级以及进入/离开事件。AOI 网格的 cell 通常大于渲染网格单元，因为网络同步对空间精度要求较低。\n4.2 渲染剔除：从粗到细的四级筛选 flowchart LR A[\"场景对象\"] --\u003e B[\"距离 / LOD 粗筛\"] B --\u003e C[\"视锥剔除\"] C --\u003e D[\"遮挡剔除\"] D --\u003e E[\"提交渲染 / GPU Culling\"] E --\u003e F[\"阴影、反射等额外 pass\"]第一道：距离与 LOD 远处的小物体可直接剔除，或替换为 imposter/HLOD。距离剔除应最先执行，因为其开销最低。\n第二道：视锥剔除 用相机的六个平面测试包围体。球体测试最简单：\n\\[ d = n \\cdot c + d_0 \\]若 \\(d \u003c -r\\)，球体完全位于平面外侧，应予以剔除。AABB 可使用正顶点/负顶点测试；层次结构（BVH/八叉树）可在父节点整体位于视锥外时对整棵子树剪枝。\n第三道：遮挡剔除 视锥内的物体不一定看得见，因为它可能被墙、地形、建筑挡住。常见方案：\n方案 原理 优点 缺点 PVS / Portal 预计算房间到房间的可见集 运行时开销低、适合室内 预处理开销高，对动态门和可破坏物支持较差 软件遮挡 将大型遮挡物以低分辨率光栅化到深度缓冲，再测试对象 AABB 通用、可控 CPU 成本、保守性与漏剔除 HZB 用深度金字塔做层次遮挡测试 GPU 友好、适合大规模 实现复杂、需要保守处理 硬件遮挡查询 GPU 查询对象是否被遮挡 准确 存在延迟，易造成 GPU 停顿 Umbra 等商业方案 预计算/运行时混合 成熟 许可、数据管线依赖 遮挡剔除的基本原则是：先提交大型遮挡物以建立深度缓冲，再测试小型物体；若使用尚未写入深度缓冲的遮挡物执行测试，结果不可靠。\n第四道：GPU Culling 现代引擎通常将可见性测试迁移至 GPU：由 compute shader 执行视锥或遮挡测试，将可见实例写入 indirect draw 参数，避免 CPU 回读。Nanite 等虚拟几何方案进一步使用 cluster 层次与 HZB 进行细粒度剔除。其核心思路仍是“层次结构 + 保守测试 + 批量提交”。\n4.3 大世界流送：避免一次性加载整张地图 大世界通常按网格分块：\nWorld Partition / Cell：将世界划分为 cell，按玩家位置流送；每个 cell 具有独立的 actor 集合。 Data Layer：同一空间里按玩法层加载（主线、支线、遗迹）。 HLOD：远处不加载细粒度 actor，改用合并后的代理网格。 Streaming Source：不仅考虑玩家位置，还需考虑相机、载具和传送门；多个 source 的并集决定加载范围。 Additive Level / Scene：Unity 用 additive scene + Addressables；UE 用 World Partition + Level Streaming。 Cell 大小的权衡：\n过小：流送频繁、actor 管理开销大，并可能产生边界 popping； 过大：内存峰值高、加载卡顿、HLOD 粒度较粗。 经验上应使“玩家正常移动跨越一个 cell 的时间”大于“异步加载与激活”的时间，并在边界处预留预加载环。\n4.4 坐标精度：大世界坐标系统的典型问题 32 位 float 在原点附近精度较高，但离原点越远，最小可表示间隔越大。以 1 单位 = 1 厘米为例：\n10 km 处，float 间隔约 0.1 厘米； 100 km 处，约 1 厘米； 1000 km 处，约 8 厘米。 这会导致：\n角色/物体抖动； 碰撞形状错位； 阴影、贴花、粒子闪烁； 物理求解不稳定。 常见解决：\nFloating Origin / World Origin Rebasing：玩家远离原点后，将世界整体平移回原点，所有物体坐标同步偏移； Camera-relative rendering：渲染时以相机为原点计算； Large World Coordinates：用 double 或分块局部坐标保存大世界位置； 分块局部空间：每个 cell 用自己的局部坐标，跨 cell 时转换。 UE5 的 World Partition 与 Large World Coordinates、Unity 的 floating origin 方案，均用于解决同一类坐标精度问题。\n4.5 视线查询：可见性、交互与感知的区别 “这个物体是否可见”在玩法上通常不是视锥问题，而是**视线（Line of Sight）**问题：\n从角色眼睛/相机向目标发一条射线； 命中墙就不可见； 只对特定层（世界静态、角色、可交互物）做查询； 对高频查询执行缓存或分帧，避免每个交互物每帧执行一次射线检测。 若玩法需要“隔墙高亮”，则不应将视线查询作为唯一判据；应将“感知可见”“UI 可见”“渲染可见”拆分为三个独立概念。\n第 5 章 查询加速：索引之外的优化手段 空间索引仅构成查询加速的一部分。实际优化手段可归纳为以下五类：\n5.1 减少候选 合理的格子尺寸/树的分裂阈值； 多级索引：粗层筛选大区域，细层筛选对象； 层/标签/通道过滤前置； 用包围球做第一轮，AABB 做第二轮，精确形状最后做。 5.2 减少每次测试的成本 距离比较用平方距离，避免 sqrt； AABB 先测，OBB/三角形后测； SIMD 批量测 4/8 个 AABB； 提前返回：首个命中即返回，或达到 N 个结果后停止。 5.3 减少内存跳转 BVH/八叉树用扁平数组，不用节点指针； SoA（Structure of Arrays）代替 AoS，方便 SIMD 和缓存； 空间哈希用开放寻址，不用链式 unordered_map； 对象句柄用 generational index，避免悬空指针。 5.4 利用时间相干性 fat AABB：小移动不更新树； pair cache：复用上一帧的候选对； 缓存最近所在 cell/多边形； 增量更新，而不是每帧重建； 对静态对象只构建一次，动态对象单独一层。 5.5 并行与预算 查询按空间分块并行； 每个线程本地网格，最后合并； 导航与 AI 查询分帧执行，避免单帧完成全部计算； 远距离对象降频更新； 为查询设置预算（时间/数量），超出预算时执行降级策略。 5.6 Morton 码与空间局部性 将三维坐标的二进制位交错，可得到一维 Morton 码（Z-order curve）：\n\\[ morton = interleave(x, y, z) \\]它能把空间相邻的格子映射到相近的一维索引，适合：\n线性八叉树； 空间哈希的 key； 排序后批量处理； GPU 上的空间分块。 其代价是 Morton 序并非完全连续，查询时仍需处理边界；不应仅因结构复杂而放弃更简单的网格方案。\n5.7 性能指标 指标 说明 候选数/查询 索引是否真的把候选降下来了 查询耗时 P50/P99 平均值可能掩盖长尾延迟，卡顿应关注 P99 更新耗时 动态对象较多时，更新开销可能高于查询 内存占用 BVH 节点、网格桶、导航 tile 最坏情况 所有对象集中于同一单元/节点时是否出现性能退化 构建/流送时间 大世界加载和 tile 重建是否可接受 第 6 章 其他模块中的空间查询 空间索引不仅服务于碰撞、导航和渲染。许多模块仅改变了查询形状与更新频率。\n模块 核心查询 常用结构 关键点 网络同步 / AOI 玩家周围范围、优先级 网格、九宫格、距离带 相关性 ≠ 可见性；更新频率分级；进入/离开事件 AI 感知 视锥、视线、听觉半径 空间哈希、BVH、分层网格 分帧更新；感知记忆；避免每帧对全部 agent 执行射线检测 音频 听者周围声源、遮挡 网格、BVH 距离衰减、 occlusion、reverb zone；声音优先级 触发器 / 玩法 重叠、进入/离开 空间哈希、物理 broad phase 事件去重；层过滤；避免事件风暴 渲染 视锥、遮挡、光源影响 BVH、八叉树、PVS/HZB 大遮挡物优先；GPU culling；HLOD 植被/实例化 相机周围 instances 网格、四叉树、GPU cluster 每帧重建代价高；用间接绘制 程序化放置 附近已放置对象、最小间距 网格、Poisson disk 网格加速邻域检查；流式生成 寻路 / 人群 最近可走点、邻居 NavMesh、网格、BVH 全局路径与局部避障分离 物理查询 overlap/sweep/raycast BVH、SAP、网格 查询过滤；CCD；确定性 以下模块常被忽略，但其空间查询模式具有代表性：\n6.1 网络 AOI：同步相关性与可见性的区别 MMO 的 AOI 通常按网格或九宫格管理玩家周围的对象，其关注点是“客户端需要同步哪些实体”，而非“相机是否可见”。具体而言：\ncell 可以比渲染格子大； 不同实体有不同更新频率（玩家、NPC、掉落物）； 进入/离开需发送事件，不应仅发送全量快照； 边界需引入迟滞，避免玩家在单元边缘往返移动导致实体反复创建与销毁； 大世界还需考虑跨服与分线，空间索引仅是第一层。 6.2 AI 感知：视锥 + 视线 + 记忆 AI 的“看到玩家”判定通常分为三步：\n距离/视锥粗筛：仅检查前方一定角度和距离内的目标； 视线射线：确认目标未被墙体遮挡； 感知记忆：目标离开视野后不立即清除，而是保留一段时间的位置记忆。 大量 AI 不应每帧执行全量扫描。可按帧分桶、按距离降低更新频率、通过空间哈希查询邻居，再将结果写入黑板或感知组件。\n6.3 音频：空间查询的简化实现 音频系统通常不需要每帧执行精确几何查询：\n使用网格或 BVH 查找听者附近的声源； 对重要声源执行遮挡射线检测； 通过 reverb zone/portal 处理室内外过渡； 对远处声源降低更新频率，甚至停止更新。 音频预算通常比渲染和物理更有限，因此近似计算与优先级策略比精确性更重要。\n6.4 渲染：空间索引的最终使用方 渲染的剔除链路通常是：\n场景空间索引（BVH/八叉树/World Partition cell）； 视锥剔除； 遮挡剔除（HZB/PVS/软件遮挡）； LOD/HLOD 选择； GPU culling + indirect draw。 对超大世界而言，层次结构 + 批量提交比“每个对象单独一次 draw call”更为重要。Nanite 的 cluster 层次、植被 instancing 与 HLOD 合并网格，都是同一思想的不同实现。\n第 7 章 系统设计：选型清单 7.1 选型时需确认的八个问题 设计空间查询模块时，应依次确认以下问题：\n查询形状：点、球体、盒体、射线、视锥、路径，还是最近 k 个对象？ 查询频率：每帧一次、每个角色一次，还是每帧数千次？ 对象动态性：静态对象与动态对象的比例如何？两者的索引策略通常不同。 对象尺寸分布：尺寸接近时可选用网格或 SAP；差异较大时宜选用 BVH 或八叉树。 世界规模：是否需要分块、流送与浮点原点重置？ 最坏情况：所有对象集中于同一区域、单个对象横跨全图，还是大量对象同时移动？ 更新成本：每帧重建 BVH 通常不可接受；每帧重建网格也可能开销过高。 确定性要求：物理或网络回放是否对遍历顺序和浮点行为敏感？ 7.2 简化选型树 根据上述问题，可得到如下简化选型路径：\nflowchart TD A[\"对象数量是否较大？\"] --\u003e|否| B[\"暴力遍历 + 提前返回\"] A --\u003e|是| C[\"对象尺寸是否接近？\"] C --\u003e|是| D[\"网格 / 空间哈希 / SAP\"] C --\u003e|否| E[\"是否以静态对象为主？\"] E --\u003e|是| F[\"八叉树 / BVH / BSP\"] E --\u003e|否| G[\"动态 BVH / 分层网格\"] D --\u003e H[\"是否需要射线/视锥查询？\"] F --\u003e H G --\u003e H H --\u003e|是| I[\"BVH 或层次网格\"] H --\u003e|否| J[\"保持简单方案，先测量瓶颈\"] 最后一项判断尤为关键：当对象数量仅为数百时，暴力遍历往往比维护索引更高效。索引的价值来自规模，而非结构复杂度。\n7.3 常见实现问题 常见问题 后果 建议 每帧重建完整 BVH/八叉树 构建开销过高 静态部分仅构建一次，动态对象使用增量更新 未根据对象尺寸分布确定网格单元大小 单元内候选过多，或大型对象跨单元过多 以典型对象直径为基准，必要时采用分层网格 未设置膨胀 AABB 对象微小移动即触发索引重插 为动态对象设置合理 margin 候选对象对未去重 narrow phase 重复计算 使用 pair key 去重，或在查询阶段统一去重 对象销毁后未从索引移除 残留失效句柄，产生错误查询结果 对象生命周期结束时同步移除索引项 以渲染可见性替代玩法可见性 角色背后的交互物无法触发或同步 区分渲染可见、玩法可见与网络相关 导航网格与碰撞几何不一致 路径可达但角色无法通过 使用一致的源数据，并同步更新 动态障碍变化时重建整张导航网格 运行时开销过高 使用 tile cache、局部切割或分层更新 同步回读遮挡查询结果 造成 GPU 停顿 使用上一帧结果或异步查询 大世界使用绝对 float 坐标 精度下降，出现抖动与碰撞错位 使用原点重置、局部坐标或 double 未设置查询预算 单帧查询量过大 分帧、降频并限制结果数量 仅统计平均耗时 忽略长尾延迟与最坏情况 统计 P50/P95/P99 与最坏情况 7.4 核心结论摘要 碰撞检测：broad phase 生成候选对象对，narrow phase 计算接触流形；高速物体需启用 CCD。动态树与膨胀 AABB 是常用组合。 导航：Recast 负责构建导航网格，Detour 负责运行时查询；A* 生成多边形走廊，漏斗算法拉直路径，ORCA/RVO 处理局部避障；大世界采用分块导航网格与 Navigation Invoker。 可见性：依次执行距离/LOD 粗筛、视锥剔除、遮挡剔除与 GPU culling；PVS 适用于室内场景，HZB 与软件遮挡适用于大范围场景。 附近交互物：通过空间哈希检索邻接单元，使用平方距离与状态过滤，再执行视线检测、优先级评分与迟滞处理。 大世界：采用分块流送、HLOD 与浮点原点重置/局部坐标；单元大小需权衡流送频率与内存占用。 查询加速：减少候选数量、降低单次测试成本、改善内存局部性、利用时间相干性，并设置并行与预算策略。 选型：优先考虑更新模式，再考虑查询形状；对象数量较少时，暴力遍历可能优于维护索引。 当需要重新梳理问题时，可按以下顺序判断：查询形状 → 候选集 → 精确测试 → 更新成本 → 最坏情况。\n","date":"2026-11-06T00:00:00Z","permalink":"/docs/game-engine/spatial-data-structures/","title":"游戏引擎中的空间数据结构与算法：碰撞、导航与可见性"},{"content":"交互物大概是关卡里最容易被低估的一类东西。玩家看到一扇门，走过去，按一下，门开了——这条链路在 demo 里十分钟能搭完，但要做到“手感对、状态不错、多人不炸、性能不崩”，往往会拖掉一个迭代的大部分时间。\n这篇不是某个插件的使用教程，而是一份设计口径：先把交互物拆成可复用的四层，再分别讨论六种交互形态的差异、状态怎么流转、射线怎么打、碰撞体怎么选、声音和动效怎么接。引擎侧以 UE5 为准，API 名称在 5.4–5.8 之间基本稳定；文中的引擎行为尽量给出官方文档出处，设计取舍部分来自项目经验，不冒充标准答案。\n文中带 [n] 的句子对应文末的参考链接；没有标注的部分是设计经验与项目取舍，不冒充官方结论。\n一句话：交互物不是“一个 Actor 加一个按钮”，而是“感知 → 判定 → 执行 → 反馈”四层，每一层都有自己的失败方式。\n0. 阅读说明 0.1 这篇文章适合谁 角色 建议读法 能拿走什么 策划 / 关卡 第 1、2、3 章，第 9.6 节清单 知道一类交互物该有哪些状态、反馈和失败提示 程序 全篇，重点第 1、3、4、5、7 章 一套接口、状态机、检测与生成物的落地方式 TA / 音频 第 2.6、2.7、6、8 章 材质音效、动效实体、命中包的组合方法 主程 / 技术负责人 第 3.7、5.7、9 章 网络权限、碰撞分离、性能预算与验收口径 0.2 TL;DR 分层：感知（我看见谁）→ 判定（我能不能用）→ 执行（世界怎么变）→ 反馈（玩家怎么感知）。四层混在一起，后面一定返工。 分类看驱动源：移动、NPC、静态、动态、动效，外加表现层的“动效实体”。同一种外观可能是不同类，别按美术资产分类。 UObject 做交互物是为了省 Actor 同步：只同步交互字段，客户端按需组装 Mesh/音频/动效；代价是自己做序列化、路由、晚加入和版本兼容。 状态机至少七个状态：只用 bIsOpen 这类布尔，会在多人、打断、冷却、损坏上崩。 射线只负责选中，不负责许可：命中之后还要过 CanInteract（距离、角度、标签、占用、资源）。 碰撞体是交互代理，不是美术外形：交互体积和物理碰撞要分开；不规则形状用复合简单碰撞或凸包，不要无脑复杂碰撞。 材质决定音效，数据表决定映射：物理材质给表面类型，数据资产把表面类型映射到声音，别在蓝图里写 switch。 弹孔是“贴花 + 命中包”：朝向、尺寸、寿命、上限、附着，一个都不能少。 动效实体要池化、限额、按重要度降级：它是交互的标点符号，不是无成本的装饰。 0.3 术语最小集 术语 一句话 交互物（Interactable） 能被玩家或 AI 主动触发、并改变自身或世界状态的对象。 交互者（Interactor） 发起交互的一方，通常是玩家角色、控制器或 AI Agent。 交互代理（Interaction Proxy） 专门用于“被选中/被检测”的碰撞体或组件，可以和物理碰撞分离。 交互体积（Interaction Volume） 判定“是否在可交互范围内”的体积，常用盒体、球体或胶囊体。 焦点（Focus） 当前被准星/射线选中的交互物，是客户端表现，不一定等于服务端许可。 保留（Reservation / Claim） 多人或 AI 场景下，某个交互位被某交互者临时占用，避免重复触发。 命中包（Impact Package） 一次命中要播放的一组表现：贴花、粒子、声音、灯光、震屏。 动效实体（Effect Entity） 有生命周期、只负责表现、会被生成和销毁的对象，不参与交互判定。 表面类型（Surface Type） 物理材质上的枚举，用来区分金属、石头、木头、血肉等，驱动音效与粒子。 贴花（Decal） 投影到网格表面的材质，常用于弹孔、血迹、涂鸦。 0.4 四层模型 flowchart LR S[\"感知 Sense射线 / 重叠 / 输入\"] --\u003e R[\"判定 ResolveCanInteract / 标签 / 距离\"] R --\u003e E[\"执行 Execute状态 / 动画 / 物理 / 任务\"] E --\u003e P[\"反馈 Present音效 / 动效 / UI / 镜头\"] P -.-\u003e|\"下一次感知\"| S四层各自的典型故障：\n层 故障表现 常见原因 感知 打不中、穿墙能点、手柄够不到 射线起点错、通道错、碰撞体缺失 判定 看起来能用，按了没反应 CanInteract 条件太隐晦，或客户端/服务端不一致 执行 门开了但碰撞没动、NPC 瞬移 只改了表现没改状态，或没对齐根运动 反馈 状态对了但“感觉不对” 反馈晚于状态、声音材质错、动效被裁剪 设计原则：每一层只做一件事。对象回答“这个请求是否合法”，交互者负责“何时发起”，服务端负责“是否批准”。\n0.5 目录 第 1 章 一套统一的交互契约 第 2 章 交互物的六种形态 第 3 章 交互状态流转 第 4 章 射线瞄准交互 第 5 章 碰撞：碰撞箱、胶囊体与不规则形状 第 6 章 音频触发与材质音效 第 7 章 射弹与弹孔 第 8 章 动效实体 第 9 章 工程落地与验收 第 10 章 参考 第 1 章 一套统一的交互契约 一句话：交互物只回答“此刻这个请求是否合法”，不替玩家做输入决策，也不自己决定网络权限。\n1.1 为什么需要契约 如果没有统一契约，项目里很快会出现这些写法：\n门蓝图里写 Cast\u0026lt;APlayerCharacter\u0026gt;，然后直接读玩家的输入； 宝箱里写 if (bIsOpen) return;，但没有“被占用”和“冷却”； NPC 里写 if (Distance \u0026lt; 200)，但距离是按 NPC 原点算的，不是按交互点； 玩家控制器里写 if (HitActor-\u0026gt;IsA(ADoor))，每加一类交互物就改一次控制器。 这些写法单独看都能跑，合在一起就会变成“每类交互物一套逻辑、每个新职业改一次玩家代码”。契约的作用是把变化点收敛到对象自己身上。\nBlueprint Interface 的官方定义是“一组只有名字、没有实现的函数”，任何实现它的蓝图都保证拥有这些函数；接口不能加变量、编辑图表或加组件。[4] 这正好适合交互物：不同对象共享“能被交互”这一件事，但各自实现细节。C++ 侧则常用 TScriptInterface 持有接口，避免硬引用具体类。[20]\n1.2 交互接口 // 交互契约：对象只回答“此刻这个请求是否合法”，不替玩家做输入决策 UINTERFACE(MinimalAPI, Blueprintable) class UInteractable : public UInterface { GENERATED_BODY() }; class IInteractable { GENERATED_BODY() public: UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = \u0026#34;Interaction\u0026#34;) bool CanInteract(const FInteractionContext\u0026amp; Context) const; UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = \u0026#34;Interaction\u0026#34;) FInteractionPrompt GetPrompt(const FInteractionContext\u0026amp; Context) const; UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = \u0026#34;Interaction\u0026#34;) void OnFocusBegin(const FInteractionContext\u0026amp; Context); UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = \u0026#34;Interaction\u0026#34;) void OnFocusEnd(const FInteractionContext\u0026amp; Context); UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = \u0026#34;Interaction\u0026#34;) void BeginInteract(const FInteractionContext\u0026amp; Context); UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = \u0026#34;Interaction\u0026#34;) void EndInteract(const FInteractionContext\u0026amp; Context, EInteractionResult Result); }; 几个约定：\nCanInteract 只做判定，不改状态，不播声音。它会被 UI、射线、AI 反复调用，必须是幂等的。 GetPrompt 返回“按 E 打开”“需要钥匙”“已被占用”这类信息。失败也要有提示，否则玩家只会觉得“游戏坏了”。 OnFocusBegin/End 只做高亮、描边、提示 UI，不做 gameplay 状态变更。 BeginInteract 和 EndInteract 成对出现；EndInteract 必须能处理取消、失败、被打断。 蓝图调用接口用 Execute_CanInteract(Object, Context)，不要直接 Cast 到具体类。 1.3 上下文：把“谁、用什么、打在哪”带进去 USTRUCT(BlueprintType) struct FInteractionContext { GENERATED_BODY() UPROPERTY(BlueprintReadOnly) TObjectPtr\u0026lt;AActor\u0026gt; Instigator = nullptr; UPROPERTY(BlueprintReadOnly) TObjectPtr\u0026lt;AController\u0026gt; Controller = nullptr; UPROPERTY(BlueprintReadOnly) FHitResult Hit; UPROPERTY(BlueprintReadOnly) FGameplayTag InputTag; UPROPERTY(BlueprintReadOnly) FVector InteractionPoint = FVector::ZeroVector; }; Hit 里已经带了 HitComponent、BoneName、ImpactPoint、ImpactNormal 和 PhysMaterial。其中 BoneName 在骨骼网格上很有用：同样是“打中敌人”，打头、打甲、打弱点应该是不同反馈；PhysMaterial 则直接决定音效和粒子。[5]\n1.4 交互描述符：把可调参数从代码里拿出来 UENUM(BlueprintType) enum class EInteractionHold : uint8 { Tap, // 按一下 Hold, // 按住 Repeat // 按住连续触发 }; USTRUCT(BlueprintType) struct FInteractionDescriptor { GENERATED_BODY() UPROPERTY(EditAnywhere) FGameplayTag InteractionTag; // Interaction.Door.Open UPROPERTY(EditAnywhere) FText Verb; // “打开” UPROPERTY(EditAnywhere) float MaxDistance = 220.f; UPROPERTY(EditAnywhere) float MaxAngleDegrees = 60.f; UPROPERTY(EditAnywhere) EInteractionHold HoldType = EInteractionHold::Tap; UPROPERTY(EditAnywhere) float HoldDuration = 0.f; UPROPERTY(EditAnywhere) FGameplayTagQuery RequiredTags; UPROPERTY(EditAnywhere) FGameplayTagQuery BlockedTags; UPROPERTY(EditAnywhere) TSoftObjectPtr\u0026lt;UAnimMontage\u0026gt; Montage; }; 交互描述符放在数据资产里，同一套代码就能配置出“木门”“铁门”“需要钥匙的门”“只能从一侧打开的门”。这也是后面第 6 章材质音效和第 9 章数据驱动的基础。\n1.5 一次交互的完整生命周期 输入按下 -\u0026gt; 交互者确认焦点存在 -\u0026gt; 本地预测：CanInteract？ -\u0026gt; 否：显示失败提示，结束 -\u0026gt; 是：进入 Pending（UI 先给“正在执行”的弱反馈） -\u0026gt; 服务器 RPC：ServerBeginInteract(目标, 上下文) -\u0026gt; 服务器复核距离/角度/标签/占用 -\u0026gt; 批准：状态机进入 Executing，广播表现 -\u0026gt; 拒绝：客户端回滚 Pending，提示原因 -\u0026gt; 执行完成/取消：EndInteract(Result) 这里的关键是“本地预测”和“服务器复核”分开：本地预测让手感跟手，服务器复核保证不作弊、不重复。第 3.7 节会展开。\n第 2 章 交互物的六种形态 一句话：分类看“谁在驱动状态变化”，不看外观。门和电梯都是移动交互物，拉杆和宝箱都是静态交互物，草丛和吊灯都是动效交互物。\n严格说，前五类是真正的交互物，第六类“动效实体”是表现层对象，不参与交互判定。但它们在工程上共用生成、池化、预算和调试链路，所以放在同一章讲，避免把“表现”当成“玩法”或反过来。\n2.1 分类矩阵 类型 驱动源 状态复杂度 是否依赖物理 典型例子 主要风险 移动交互物 位移/旋转/动画 中 可选 门、电梯、阀门、抽屉 移动时穿模、把玩家卡住 NPC 交互 对话/任务/行为树 高 否 对话、交易、任务、搜刮 对齐、占用、打断 静态交互 一次性/冷却 低 否 拉杆、按钮、终端、宝箱 反馈与状态不同步 动态交互 物理/抓取/投掷 中高 是 木箱、桶、可破坏物 网络同步、抖动、性能 动效交互 表现参数 低 否 草、帘子、风铃、灯 误导玩家以为能用 动效实体 生成/销毁 无 否 命中粒子、弹孔、音效体 数量失控、池化缺失 这张表不是互斥的。一扇“可以被炸开的铁门”同时是移动交互物和动态交互物；一个“拉下后播放动画”的拉杆是静态交互物 + 动效交互。实现时选一个“主类型”负责状态，其他类型只作为表现或条件。\n2.2 移动交互物 移动交互物的核心不是“让它动”，而是“它动的时候，世界仍然自洽”：碰撞跟着动、玩家站上去会被带着走、AI 导航知道路没了、网络上的位置一致。\n三种常见驱动方式：\n驱动 做法 优点 缺点 适用 动画驱动 用 UAnimSequence / UAnimMontage 驱动门板或机械臂 表现力强、和角色动画同源 碰撞要跟骨骼/插槽同步，网络成本高 门、闸机、机械装置 插值驱动 在 Tick 或 Timeline 里插值 RelativeLocation/Rotation 可控、便宜、易网络同步 运动曲线要自己调，容易机械感 电梯、抽屉、旋转门 物理驱动 约束 + 马达/力 真实、可被撞开 不稳定、难预测、网络贵 可破坏门、重物、布娃娃 插值驱动的门，最小实现是“目标角度 + 插值速度 + 状态”：\n// 示意：门只需要“目标角度”，不需要每帧重新算逻辑 void ADoor::Tick(float DeltaSeconds) { Super::Tick(DeltaSeconds); const float Target = (State == EDoorState::Opening) ? OpenAngle : ClosedAngle; const float Current = DoorMesh-\u0026gt;GetRelativeRotation().Yaw; const float Next = FMath::FInterpConstantTo(Current, Target, DeltaSeconds, TurnRate); DoorMesh-\u0026gt;SetRelativeRotation(FRotator(0.f, Next, 0.f)); if (FMath::IsNearlyEqual(Next, Target, 0.1f)) { State = (Target == OpenAngle) ? EDoorState::Open : EDoorState::Closed; } } 门这类移动交互物，设计时要提前回答几个问题：\n开合方向：单开、双开、推拉、滑动？玩家从哪一侧推？ 玩家站位：门板扫过玩家时，是停下、反向、推开玩家，还是直接穿过？最常见的是“检测到玩家就暂停并反向”，但这会让玩家反复卡门；更好的做法是门板碰撞设为“可推移玩家”，或者干脆让门板不阻挡玩家、只阻挡子弹。 碰撞与导航：门打开后，门板的碰撞要跟着走；如果门会改变 AI 路径，需要 UNavModifierComponent 或动态导航。 网络：服务器决定开关，客户端只做表现；门的 bReplicateMovement 打开，或者用 RepNotify 同步状态和目标角度。 声音：开门声、门轴声、锁舌声、关闭撞击声，最好分层；只放一个“门开”音效会显得很薄。 电梯/移动平台还有一个额外问题：站在上面的玩家要跟着走。UE 的角色移动组件依赖“Base（底座）”来继承移动；平台移动太快或只改了视觉不改碰撞，玩家就会滑下去。平台至少要保证碰撞组件真实移动，并在网络端同步。\n2.3 NPC 交互 NPC 交互的难点不在“对话”，而在“对齐”和“占用”。\n对齐：NPC 的根运动、转身、坐下、伸手，和玩家站的位置往往对不上。Motion Warping 可以在动画蒙太奇里用 Anim Notify State 划出“可扭曲窗口”，运行时把根运动对齐到命名目标点。[16] 例如对话时把 NPC 的朝向对齐到玩家，或者让玩家自动走到 NPC 面前的交互位。 占用：两个玩家同时点同一个 NPC，不能都触发对话。Smart Object 提供了“全局数据库 + 空间分区 + 预约”的机制：交互物只提供信息（槽位、标签、行为定义），真正的执行逻辑由交互者实现。[15] 这比在 NPC 蓝图里塞一个 bInUse 更稳，因为预约是系统级的，查询可以按区域和 Gameplay Tag 过滤。 状态：NPC 至少有 Idle、Patrol、Talk、Trade、Quest、Combat、Dead 这些状态；交互许可要跟着状态走。战斗中还能交易的 NPC，要么是设计刻意，要么是 bug。 注视：对话时 NPC 看玩家、玩家看 NPC。可以用 Aim Offset、Control Rig 或简单的头部 LookAt；不要直接旋转整个 Actor，否则脚会滑。 对话相机：过肩镜头、焦点切换、景深。相机切换要避免“瞬移感”，用 PlayerCameraManager 的混合，或者把相机挂在对话场景的固定机位上。 NPC 交互的状态机通常比门复杂得多，适合用 StateTree 这类层级状态机：它把行为树的 Selector 和状态机的 State/Transition 结合起来，每个状态有 Enter Condition 和 Task。[18] 但不要因为“它高级”就给每个宝箱都套 StateTree——简单对象用简单状态机，维护成本更低。\n2.4 静态交互 静态交互物的特点是：位置不变、状态不多、反馈要重。拉杆、按钮、终端、宝箱、可阅读的告示牌，都属于这一类。\n设计上最重要的三条：\n一次性还是可重复：宝箱通常一次性；拉杆可能可重复；按钮可能带冷却。这个差异必须写进状态机，不能靠蓝图里的 DoOnce 节点糊弄。 生效时机：交互的“实际效果”不要绑在输入按下那一帧，而要绑在动画通知（Anim Notify）或时间轴的关键帧上。手还没碰到拉杆，门就开了，玩家会觉得轻飘；手穿模了门还没开，玩家会觉得延迟。 失败反馈：上锁的宝箱、没权限的终端、已经拉过的拉杆，都要有明确的“为什么不能用”。最差的做法是按键没反应。 静态交互物通常不需要复杂碰撞：一个盒体交互体积 + 一个简单碰撞即可。真正需要花时间的是动画、声音、UI 提示和状态同步。\n2.5 动态交互 动态交互物是“物理参与状态”的一类：木箱、桶、可投掷物、可破坏物、可抓取的尸体。\n它和静态交互最大的区别是：物理状态不能直接塞进交互状态机。否则你会得到 Idle / Grabbed / Thrown / Resting / Sleeping / Broken 这种既像交互又像物理的混合状态，最后没人能说清“到底谁在控制它”。\n更稳的做法是：\n交互状态机只关心 Idle / Focused / Ready / Held / Cooldown； 物理状态（是否模拟、是否睡眠、速度多大）作为 CanInteract 的输入条件； 抓取用“附着 + 关闭物理”或“物理约束 + 目标点”两种实现，按游戏需求选。 抓取方式 做法 优点 缺点 附着 把物体 Attach 到角色手部插槽，关闭物理 稳定、便宜、好同步 会穿模，不能和世界碰撞 物理约束 用约束/物理手柄把物体拉向目标点 真实、能碰撞 抖动、网络贵、容易卡住 投掷则要注意质量与冲量：同样大小的木箱和铁箱，如果给同样的冲量，轻的会飞、重的几乎不动；如果给同样的速度，重的会像炮弹。通常按“质量归一化的冲量”或“设计好的初速度”来调。\n网络方面，动态交互物是最容易超预算的：大量物体的 bReplicateMovement、频繁的睡眠/唤醒、客户端预测后的回滚，都会造成带宽和抖动。常见做法是“只有被交互者持有的物体才高频同步，其余走休眠”。\n2.6 动效交互 动效交互是“只改变表现、不改变 gameplay 状态”的交互。草被踩弯、帘子被拨开、风铃响、灯光晃、水面起涟漪，都是这一类。\n它的价值是让世界“有反应”，成本低、观感收益高。但它有一个很大的风险：误导玩家的交互预期。如果草能被动、门能被动、箱子能被动，玩家会自然认为“所有会动的东西都能按 E”。所以需要一套稳定的视觉语法：\n语法 含义 例子 有高亮/描边 + 提示 可交互 门、宝箱、NPC 只有物理/风/角色触发 纯反应，不可交互 草、帘子、水 有声音但无提示 环境反馈 远处风声、脚步回声 有粒子但无状态 纯表现 灰尘、火星 动效交互的节奏也很重要：主反应要快（0–100 ms），次级运动可以慢（200–600 ms）。例如踩到草地，草立刻弯，然后慢慢回弹；如果回弹和弯曲一样快，就像塑料。\n2.7 动效实体 动效实体是表现层的“临时对象”：命中粒子、弹孔、火花、烟雾、音效体、镜头震动。它有三个特征：\n有生命周期：生成、播放、消失； 不参与交互判定：玩家不能“按 E 使用”一团火花； 可池化：同一类实体大量重复出现，必须复用。 它和动效交互的区别是：动效交互是“交互物的一种属性”，动效实体是“一次表现的单位”。一扇门被打开时，门本身是移动交互物，开门时扬起的灰尘是动效实体。\n2.8 三种承载方式：Actor、UObject、Niagara 一句话：Actor 是“世界里的实体”，UObject 是“逻辑与数据”，Niagara 是“看得见的表现”；三者经常一起用，但谁来承担碰撞、权威状态和网络，必须选清楚。\n前面的六种形态回答“这是什么交互”，这一节回答“把交互挂在什么东西上”。同一个宝箱，可以做成 Actor、可以把数据拆成 UObject 只同步必要字段再由宿主按需组装、也可以做成一团 Niagara 光效加一个隐藏代理——三种做法的成本和边界完全不同。\n先说明：Actor 本身也是 UObject 的子类；下面说的“普通 UObject”特指不继承 Actor、也不继承 SceneComponent 的数据/逻辑对象。它要参与世界交互时，需要一个宿主来组装。\n2.8.1 Actor 作为交互物 特点\nActor 是 UE 里可以被放置或生成到关卡中的对象，可以包含 ActorComponent，并负责网络属性/函数复制。[22] 交互链路完整：射线命中碰撞组件 → Actor 实现接口或挂交互组件 → 状态、动画、声音、网络都挂在同一个对象上。 适合有“实体感”的交互：门、宝箱、NPC、机关、物理道具、载具。 能被关卡、Sequencer、Smart Object、AI 感知、流送和网络相关性管理。 局限\n“重”：每个 Actor 都有注册、Transform、组件、可选的 Tick 和网络相关性；几十个没问题，成千上万个“小草/小物件”都用 Actor，关卡和网络会先崩。 默认以 Actor 为交互身份：一个 Actor 上如果有多个可交互子物件（货架上的商品），要么拆成多个 Actor，要么做组件级二级交互，Actor 本身不够细。 网络和生命周期复杂：复制、Owner、NetCullDistance、销毁/流送都要处理，不是“放个 Actor 就完事”。每个参与复制的 Actor 都有通道和复制配置，哪怕这个交互物只有一个状态，也要带整套 Actor 配置；数量多时带宽和 CPU 都会涨。 纯数据/UI 交互用 Actor 是浪费：背包物品、对话选项不需要 Transform 和碰撞。 适合：需要被射线/碰撞选中、需要世界坐标、需要复制或持久存在于关卡的交互。\n2.8.2 UObject 作为交互物：数据同步 + 按需组装 这里的“普通 Object”指非 Actor、非 SceneComponent 的 UObject。用它做交互物，核心动机不是“UObject 更轻”，而是把网络同步从“每个 Actor 全量同步”换成“一份紧凑数据 + 按需组装”：Actor 一旦参与复制，就有 Actor 通道、Transform、组件列表和复制配置；UObject 只带交互需要的数据，客户端拿到数据后只组装这次真正需要的部分。[23] [24]\n为什么能省\n成本 Actor 做法 UObject + 按需组装 网络通道 每个参与复制的 Actor 都要有通道和复制配置 宿主/Manager 批量承载，多个对象共用一条通道 同步内容 Transform、组件、属性、RPC、生命周期 只同步交互字段：ID、状态、参数、资源引用 客户端对象 完整 Actor + 全部组件 只创建需要的 Mesh/音频/动效/碰撞 变更粒度 属性/组件级，带固定开销 字段级 delta，按需发 数量级 受通道数和带宽限制 更适合大量同类对象 代价 原生、简单 自己做序列化、路由、版本、晚加入 注意：UObject 本身不会自动复制。它需要由一个参与复制的宿主 Actor/Component 或 Manager 承载，通过 RPC、FFastArraySerializer、自定义 NetSerialize 或数据通道批量发送。下面说的“UObject 同步”都是这个意思。\n同步模型\n服务器持有权威数据（UObject/Manager）：交互状态、冷却、归属、解锁条件都在服务器。 同步层只发必要字段：InteractionId、状态、参数、资源引用/版本；不是整个对象图。 客户端收到后创建/更新本地 UObject；Assembler 按数据决定装什么。 客户端输入走宿主 RPC 回服务器；服务器校验后改数据，再同步结果。 晚加入/重连：先发全量快照，再发 delta；销毁/卸载要显式发 removal。 数据带版本号，资源引用用 soft path/ID；客户端缺资产时降级，不崩。 数据对象长什么样\n// 示意：只放同步所需的数据，不放 BeginPlay/Tick/碰撞 UCLASS() class UInteractableData : public UObject { GENERATED_BODY() public: UPROPERTY() FName InteractionId; UPROPERTY() uint8 State = 0; UPROPERTY() uint16 DataVersion = 1; UPROPERTY() uint8 AssemblyFlags = 0; // Mesh / Audio / VFX / Collision UPROPERTY() FTransform LocalTransform; UPROPERTY() TSoftObjectPtr\u0026lt;UStaticMesh\u0026gt; Mesh; UPROPERTY() TSoftObjectPtr\u0026lt;UMaterialInterface\u0026gt; Material; UPROPERTY() TSoftObjectPtr\u0026lt;USoundBase\u0026gt; Audio; UPROPERTY() TSoftObjectPtr\u0026lt;UNiagaraSystem\u0026gt; VFX; }; 客户端怎么按需组装\n收到数据/快照后，按 InteractionId 找宿主/插槽；找不到就占位或跳过。 按 AssemblyFlags 只装需要的部分：只装 Mesh、只挂音频、只挂 Niagara，或者只保留数据不装表现。 需要碰撞时：Attach 到宿主复用碰撞，或新建 UStaticMeshComponent 并设置 QueryOnly/响应。 绑定命中路由：HitComponent、BoneName 或插槽名 → UObject。 绑定时装/表现：音频、Niagara、动画；状态变化只更新参数。 对象移出兴趣范围/卸载：解绑、回收组件；数据保留或释放。 和 Actor 做法最大的区别\nActor 是“先有完整对象，再同步它的复制配置”；UObject 是“先有数据，再按需长出表现”。所以：\n大量静态/半静态、表现可裁剪的交互物，UObject 方案更容易把带宽和对象数压下来。 需要独立物理、移动、复杂 RPC、完整生命周期的对象，Actor 仍然更直接。 代价与边界\n问题 说明 / 建议 没有原生复制 UObject 不会自己进通道；必须由宿主/Manager 批量承载 序列化与版本 自己定义字段、delta、版本兼容；字段变更要能向后兼容 身份与晚加入 稳定 InteractionId；晚加入先全量快照再 delta；删除要显式 权威 服务器改数据；客户端只组装和预测表现；交互结果以服务器为准 组装生命周期 与宿主/流送/兴趣范围绑定；卸载要解绑、回收，避免悬空 命中路由 多个对象共享宿主时，用组件/插槽/骨骼区分；否则点一个触发全部 资源加载 soft ref + 异步；未加载时禁用交互/占位 调试 没有 Actor 的 Inspector/通道视图；需要自建调试面板/日志 物理/移动 需要独立物理、移动复制、复杂 RPC 的对象，仍用 Actor 适合：大量同类交互物、静态/半静态场景物件、表现可裁剪、数据驱动关卡、网络带宽敏感的项目。\n不适合：重物理/移动平台/NPC、需要独立 Actor 通道和 RPC、需要复杂组件生命周期、少量独特对象。\n一句话：UObject 做交互物，省的是“每个 Actor 都要同步一整包东西”的钱；花的是“自己实现数据同步、组装和路由”的复杂度。它适合大量、可裁剪的交互物，不适合替代所有 Actor。\n前提：如果每个 UObject 还是配一个 Actor 代理，那就没有省。这套设计成立的关键是“共享宿主/Manager + 批量同步 + 按需组装”，而不是把 Actor 换成 UObject 这个名字。\n2.8.3 Niagara 效果作为交互物 先澄清一个常见误会：Niagara 是效果系统，UNiagaraComponent 是 Niagara System 的 primitive component，粒子本身不是 gameplay 对象。[25] 所以“用 Niagara 做交互物”通常有两种含义：\nNiagara 只是外观，交互靠代理：隐藏的 Actor/Box/Sphere 负责碰撞和选中，Niagara 负责“看起来像一团火/光/虫群”。这是最常用、最稳的做法。 真的想让粒子参与交互：需要 CPU 粒子、碰撞事件、数据通道或自定义查询，成本和边界都多，不适合作为通用交互方案。 特点\n表现力强：非固体、体积感、动态形状、发光、流体、群体动画，适合“没有实体但看得见”的交互物（能量核心、幽灵、光门、虫群）。 可参数化：用 User Parameter 或 Data Channel 把交互状态传给 Niagara；Niagara Data Channels 用于 game code 与 Niagara 之间、或 Niagara 系统之间的数据通信。[26] 生成和池化方便：一次性效果用 SpawnSystemAtLocation / SpawnSystemAttached，配合池化和 scalability。 不参与 gameplay：默认不作为可查询碰撞体，也没有游戏状态意义上的权威与存档；粒子状态通常不做网络复制，粒子被裁剪/LOD/降级时，“交互物”可能直接消失。 局限\n没有碰撞代理：射线打不到粒子；必须加 Actor/Component 代理。 没有权威和持久化：粒子状态不是游戏状态，不能拿来存档、回滚或复制；服务器不应依赖粒子位置做判定。 性能和可见性：GPU 粒子、碰撞、事件读取、GPU readback 都可能很贵；Scalability 可能把效果降没。 交互粒度粗：“打中某一颗粒子”很难，通常只能做范围/近似交互；要精确命中就用代理或 CPU 粒子。 调试割裂：视觉和逻辑分离，容易出现“看得到但点不到”或“点得到但看不到”。 适合：视觉主体是效果、交互是范围/近似、状态由代理维护的场景（能量场、光门、虫群、可调查的幻象）。\n结论：第三种做法严格说不是“用粒子做交互”，而是“用 Niagara 当外观，用 Actor/Component 做交互”。真正要避免的是让 Niagara 承担权威状态，或让 UObject 承担碰撞。\n2.8.4 对比 维度 Actor UObject + 组装 Niagara 世界坐标 / Transform 有 无（宿主提供） 组件有，粒子无 射线 / 碰撞命中 原生 借用宿主 / 组装组件 默认无，需代理 关卡摆放 / 流送 支持 不支持（数据 + 宿主） 随宿主 Actor 网络同步 每个 Actor 通道 + 完整复制配置 宿主/Manager 批量，只发字段 表现层，通常不复制 同步粒度 Actor / 组件 / 属性 字段级 delta 无 game state 客户端对象 完整 Actor + 组件 按需组装，可能只有数据 组件 + 粒子 持久化 / 存档 需自己序列化 数据天然可存，需稳定 ID 不适合 Tick / 更新 可选 无（宿主驱动） 组件 Tick / 模拟 数量级成本 高（通道 / 带宽） 低到中（数据小，组装有成本） 中到高（GPU/粒子） 表现力 中（靠组件） 取决于组装 高 典型用途 门、宝箱、NPC、机关 大量数据化交互物、背包/对话 能量场、光门、虫群、命中特效 主要坑 Actor 数量 / 带宽 序列化 / 路由 / 晚加入 无碰撞 / 无权威 / 被裁剪 2.8.5 怎么选 先问四个问题：\n需要独立物理、移动、复杂 RPC 或完整生命周期吗？ 需要 → Actor；只是交互数据和表现 → UObject + 组装。 需要被射线/碰撞选中吗？ 需要 → Actor，或 UObject + 宿主碰撞；纯 UI/数据 → UObject 直接。 数量级大、同步预算紧、表现可裁剪吗？ 是 → UObject 数据 + 按需组装；否 → Actor。 视觉主体是实体还是效果？ 实体 → Actor + Mesh；效果 → Actor 代理 + Niagara。 一句话选型：\n需求 推荐 门、宝箱、NPC、机关 Actor + 交互组件 需要独立物理/移动的物件 Actor（物理/移动组件） 背包物品、对话选项、任务条目 UObject 大量静态/半静态、同步预算紧的交互物 UObject 数据 + 按需组装 能量场、光门、虫群 Actor 代理 + Niagara 命中火花、弹孔 Niagara/Decal 动效实体，不是交互物 混合是常态：\n宿主 Actor / SceneComponent（权威：碰撞、生命周期、网络） ├─ UInteractableComponent（契约与状态机） ├─ UObject（交互数据：ID、状态、参数、资源引用） ├─ Assembler（按数据组装 StaticMesh / 绑定表现） └─ UNiagaraComponent（表现，读状态参数） Actor 负责“世界身份与完整生命周期”，UObject 负责“可裁剪的数据同步”，Assembler 负责“按需长出表现”，Niagara 负责“看起来怎样”。省同步的关键不是 UObject 这个类型，而是“共享宿主/Manager + 批量同步 + 只组装需要的东西”；如果每个 UObject 还配一个 Actor 代理，那就没有省。\n第 3 章 交互状态流转 一句话：状态机的价值不是“多几个状态”，而是让“谁能改状态、什么时候改、改不回去怎么办”有唯一答案。\n3.1 状态集合 状态 含义 谁维护 玩家可见表现 Unavailable 未解锁、未通电、被封印 对象/服务器 无提示，或“需要 XX” Idle 可被感知，但未被选中 对象 无 Focused 被准星/射线选中 客户端 高亮、描边、提示 Ready 当前请求合法，等待输入 对象 “按 E 打开” Reserved 已被某交互者占用 服务器 “有人正在使用” Executing 正在执行 对象/服务器 动画、进度条、声音 Cooldown 执行完，暂时不能再触发 对象/服务器 灰色、倒计时 Broken 损坏、销毁、禁用 对象/服务器 破坏表现、不可用 注意 Focused 是客户端状态，Reserved/Executing 是权威状态。把两者混在一个枚举里，网络同步就会很痛苦：客户端一移动，焦点变了，难道要发 RPC 告诉服务器“我不看它了”？\n3.2 转换表 从 到 条件 副作用 Unavailable Idle 解锁条件满足 播放“通电/解封”表现 Idle Focused 射线命中且距离/角度合法 高亮、显示提示 Focused Idle 射线离开或超时 取消高亮 Focused Ready CanInteract 为真 提示从“靠近”变为“按 E” Ready Executing 输入按下且服务器批准 播放蒙太奇、锁定输入、播放声音 Ready Reserved 其他交互者已占用 提示“有人正在使用” Executing Cooldown 执行完成 应用结果、播放完成反馈 Executing Idle 取消/打断/离开范围 回滚、播放取消反馈 Cooldown Idle 冷却结束 恢复提示 任意 Broken 被破坏/禁用 停止所有表现、断开交互 3.3 状态图 stateDiagram-v2 [*] --\u003e Unavailable Unavailable --\u003e Idle: 解锁/通电 Idle --\u003e Focused: 进入准星 Focused --\u003e Idle: 离开准星 Focused --\u003e Ready: CanInteract Ready --\u003e Reserved: 被他人占用 Reserved --\u003e Ready: 释放 Ready --\u003e Executing: 输入 + 服务器批准 Executing --\u003e Cooldown: 完成 Executing --\u003e Idle: 取消/打断 Cooldown --\u003e Idle: 冷却结束 Idle --\u003e Broken: 破坏/禁用 Executing --\u003e Broken: 执行中被破坏 Broken --\u003e [*]3.4 三个边界：权限、并发、中断 权限：谁能改状态？\n服务器：Reserved、Executing、Cooldown、Broken 的权威； 客户端：Focused、本地提示、本地预测的 Pending； 对象自己：Unavailable → Idle 这类条件转换，可以在服务器或本地判定，但结果要同步。 并发：两个玩家同时按同一个拉杆怎么办？\n单机：一个交互者，简单； 多人：服务器先到先得，后到者收到 Reserved，提示“有人正在使用”； AI：Smart Object 预约，避免 NPC 抢同一个座位； 同一玩家：输入去抖 + 状态锁，避免一帧内触发两次。 中断：执行到一半怎么办？\n常见中断源：受伤、死亡、移动超出范围、切换武器、进入过场、另一个交互抢占了手部、网络断开。每种中断都要回答：\n结果是否已经应用？如果已经应用，不能回滚（例如宝箱已经打开，不能因为玩家死亡而关上）； 表现是否要收尾？例如门关到一半被打断，是停在中间，还是继续关； 冷却是否要退？通常不退，防止刷； 占用是否要释放？必须释放，否则其他人永远用不了。 3.5 用 Gameplay Tags 表达条件 Gameplay Tags 是层级化的标签系统，可以用来描述对象属性、正在做什么、事件触发。[17] 对交互物很合适：\nState.Interactable.Ready State.Interactable.Busy State.Interactable.Broken Interaction.Door.Open Interaction.RequiresKey Interaction.Blocked.InCombat 用 FGameplayTagQuery 做条件，比在蓝图里连一堆布尔更可读，也方便数据驱动：\nRequiredTags：必须全部满足（例如 Interaction.RequiresKey + Item.Key.Bronze）； BlockedTags：任一命中就拒绝（例如 State.Dead、Interaction.Blocked.InCombat）； InteractionTag：标识“这次交互是什么”，用于动画、声音、UI 的映射。 3.6 StateTree 适合 NPC，不一定适合门 StateTree 是通用层级状态机，把行为树的 Selector 和状态机的 State/Transition 结合起来；状态有 Enter Condition，任务（Task）可以并行执行，Transition 可以从叶子往根评估。[18] 对 NPC 这种“多状态、多条件、需要行为选择”的对象很合适。\n但门、宝箱、按钮这类对象只有几个状态，用 StateTree 是杀鸡用牛刀：调试链路长、启动成本高、设计师也不一定愿意进 StateTree 编辑器。选型建议：\n对象 推荐 门、抽屉、按钮、宝箱 轻量枚举状态机 + 组件 NPC、Boss、复杂机关 StateTree / 行为树 / GAS 大量同类小交互物 数据驱动 + 共享基类，不要每个都写蓝图 3.7 网络：服务器权威，客户端预测 交互的网络模型可以总结成三句话：\n状态变更走服务器：开门、交易、拾取、造成伤害，都由服务器批准。 焦点和提示走客户端：射线检测、UI 高亮、按钮提示是本地表现，不需要同步。 表现走多播/不可靠：粒子、声音、贴花是 cosmetic，丢了就丢了，不要为了它们重传。 客户端预测的常见做法：\n按下时立刻进入 Pending，播一个“弱反馈”（按钮按下、UI 变化），但不要播“结果”（门已经开了）； 服务器批准后，广播“结果”，客户端从 Pending 进入 Executing； 服务器拒绝时，客户端回滚 Pending，提示原因； 不要用“客户端先开门，服务器拒绝再关回去”这种强预测，玩家会看到门抽搐。 3.8 常见状态 bug 现象 根因 修法 交互物永远停在 Busy 中断时没释放占用 在 EndInteract 和 OnUnregister 都释放 一次输入触发两次 输入没去抖/状态没锁 输入层去抖 + Executing 锁 准星已经移开，提示还在 焦点没清 每帧/定时重算焦点，或离开时显式清理 冷却只在客户端 服务器没记冷却 冷却时间戳放服务器，客户端用剩余时间显示 门开了又关上 客户端预测 + 服务器拒绝 改成 Pending 弱反馈 NPC 对话中还能被攻击交互 状态没阻挡 BlockedTags 加 State.InCombat 第 4 章 射线瞄准交互 一句话：射线只负责“我看见谁”，不负责“我能不能用”；把选中和许可分开，才能同时支持键鼠、手柄、触屏和 VR。\n4.1 从屏幕中心到世界 最常见的第三人称/第一人称瞄准交互：\n取玩家视点（GetPlayerViewPoint）或相机位置； 从视点沿相机前向发射一条射线，长度等于交互距离； 命中后取 HitActor，检查是否实现 IInteractable； 再调用 CanInteract 做距离、角度、标签、占用判定； 通过后设为焦点，显示提示。 为什么不直接从角色胸口发一条射线？因为玩家是用“屏幕准星”瞄准的，从相机发更符合直觉。但相机射线会穿过角色身后的墙，所以还要用“角色到命中点的距离”或“角色到交互物的距离”做二次校验。\n4.2 单线、多线、扫描 方式 API 特点 适用 单线 LineTraceSingleByChannel 最便宜，只取最近命中 键鼠、准星精确瞄准 多线 LineTraceMultiByChannel 返回到第一个阻挡命中为止的所有命中 需要穿透/多个候选 球形扫描 SphereTraceSingleByChannel 有体积，容错高 手柄、触屏、VR 手部 盒体扫描 BoxTraceSingleByChannel 可调形状，适合宽交互 大物件、载具 重叠 OverlapMultiByChannel 不依赖朝向 范围交互、近身拾取 LineTraceMulti 的官方说明是：沿给定直线做碰撞追踪，返回遇到的所有命中，直到并包括第一个阻挡命中。[5] 这个语义很重要：它不会返回阻挡命中之后的物体，所以“隔墙选中”不会发生，但“多个重叠的交互物”可以一次拿到。\n4.3 命中结果读什么 FHitResult 里最常用的字段：\n字段 用途 GetActor() 找到交互物 Actor GetComponent() 找到具体组件（碰撞箱/骨骼/交互体积） ImpactPoint 交互点、贴花位置、粒子位置 ImpactNormal 贴花朝向、粒子朝向、反弹方向 BoneName 骨骼命中部位（爆头、弱点、护甲） PhysMaterial 表面类型，驱动音效和粒子 Distance 距离判定、瞄准辅助评分 bBlockingHit 是否被阻挡 一个常见错误是只读 HitActor，然后用 Actor 原点算距离。对于一扇大门，Actor 原点可能在门框中心，玩家站在门边时距离仍然很远；应该用 ImpactPoint 或交互物上的“交互点”组件。\n4.4 通道设计 不要把交互和子弹混在一个通道里。推荐至少分出：\n通道 用途 谁响应 Visibility 通用视线、相机遮挡 世界静态、角色 Camera 相机碰撞 世界静态 Interaction 交互射线 交互物体积 Projectile 子弹/射弹 角色、可破坏物、世界 InteractionProbe 角色近身检测 交互物体积 在 Project Settings → Collision 里加自定义 Object Channel 和 Trace Channel；交互物用 Interaction 通道响应，角色胶囊体不响应，避免“射线打到自己”。[2]\n4.5 瞄准辅助 手柄和触屏需要辅助，否则很难选中小物件。常见手段：\n锥形评分：在准星周围一定角度内找候选，按“夹角 + 距离 + 优先级”排序； 球形扫描：用 SphereTrace 代替线，给小物件一个“虚拟体积”； 磁性吸附：当候选在阈值内，把准星/提示吸附过去；离开时要有滞回（进入 8°，退出 12°），否则会抖； 焦点保持：已经选中的目标，允许在更大范围内保持焦点，避免手抖丢失； 优先级：任务目标 \u0026gt; 可交互物 \u0026gt; 动效交互 \u0026gt; 背景。 评分可以写成：\nfloat Score = FMath::Pow(1.f - Angle / MaxAngle, 2.f) * (1.f - Distance / MaxDistance); 这只是示意，实际还要乘上“交互物优先级”“是否被遮挡”“是否在屏幕内”等权重。关键是不要让辅助抢走玩家的意图：玩家明明瞄着远处敌人，准星却被近处箱子吸走，就是失败。\n4.6 频率与稳定性 射线不需要每帧打。常见做法：\n相机/角色移动时，10–20 Hz 重算； 静止时降到 5 Hz，或有输入时再算； 焦点变化时立即更新 UI； 用定时器而不是 Tick，方便统一降频和暂停。 稳定性的关键是滞回和去抖：进入焦点需要持续 0.05–0.1 秒，离开焦点需要离开 0.1–0.2 秒。否则玩家轻微移动，提示就会闪。\n4.7 一段可用的检测代码 bool UInteractionSenseComponent::FindFocus(FHitResult\u0026amp; OutHit) const { APlayerController* PC = GetOwningPlayerController(); if (!PC) { return false; } FVector ViewLocation; FRotator ViewRotation; PC-\u0026gt;GetPlayerViewPoint(ViewLocation, ViewRotation); const FVector End = ViewLocation + ViewRotation.Vector() * TraceDistance; FCollisionQueryParams Params(SCENE_QUERY_STAT(InteractionFocus), false, GetOwner()); Params.AddIgnoredActor(GetOwner()); Params.bReturnPhysicalMaterial = true; return GetWorld()-\u0026gt;LineTraceSingleByChannel( OutHit, ViewLocation, End, ECC_GameTraceChannel1, Params); } 这里的 ECC_GameTraceChannel1 是自定义通道的默认枚举名；实际项目里建议在 DefaultEngine.ini 里给它起名 Interaction，代码里再用 UEngineTypes::ConvertToTraceType 或项目自己的常量封装，避免“通道 1 到底是什么”的考古。\n如果想吃透最小闭环，社区的 Line Trace + 接口交互教程可以对照着看：它把“打射线 → 判断接口 → 触发交互”压缩成了一个很短的例子。[21]\n4.8 常见坑 现象 原因 修法 射线穿过墙选中后面的物体 用了 Visibility 且墙不响应 用专用 Interaction 通道，墙设为阻挡 射线打到自己 没忽略自身 Params.AddIgnoredActor(GetOwner()) 小物件很难选中 只有细线，没有体积 球形扫描或给交互体积 复杂碰撞导致掉帧 bTraceComplex = true 交互射线用简单碰撞 子组件命中但 Actor 不对 组件属于别的 Actor 用 GetActor() 而不是组件 owner 猜 焦点闪烁 没有滞回/去抖 进入/退出阈值 + 时间窗 第 5 章 碰撞：碰撞箱、胶囊体与不规则形状 一句话：碰撞体是交互的“代理”，不是美术的“外形”。代理要服务查询、移动和手感，而不是追求和模型完全一致。\n5.1 简单碰撞与复杂碰撞 UE 的碰撞体分两类：简单碰撞是立方体、球体、胶囊体、凸包等基本形状；复杂碰撞是物体的三角网格（trimesh）。默认情况下引擎会同时生成简单和复杂形状，再根据查询类型选用。[1]\nStatic Mesh 的 Collision Complexity 有四个选项：\n选项 含义 注意 Project Default 用项目物理设置，简单查询用简单碰撞，复杂查询用复杂碰撞 默认行为 Simple And Complex 同时生成两种，按查询类型选用 内存占用较高 Use Simple Collision As Complex 复杂查询也用简单碰撞 省内存，适合简单几何 Use Complex Collision As Simple 简单查询也用复杂碰撞 不能模拟该物体，但可以和其他简单碰撞物体碰撞 “Use Complex Collision As Simple”是最容易被误用的选项：它让复杂网格参与简单查询，适合静态场景的精确碰撞，但如果你把它用在需要模拟的物体上，物体会无法正常模拟。官方文档明确说，使用它时不能模拟该物体，但可以用来和其他模拟中的简单物体碰撞。[1]\n5.2 碰撞启用与响应 每个物理体有 Object Type 和一组 Response。碰撞响应有三种：Block、Overlap、Ignore；追踪响应同理，只是追踪本身也可以被设为某种响应类型。[2]\n几条容易记错的规则：\n两个物体都设为 Block，才会真正阻挡； 一个 Overlap、一个 Block，结果是 Overlap，不是 Block； 一个 Ignore、一个 Overlap，不会产生 Overlap 事件； Overlap 事件需要开启 Generate Overlap Events； 要触发 Event Hit，需要开启 Simulation Generates Hit Events； 不建议同一个物体同时处理 Collision 和 Overlap 事件，手动管理成本很高。 Collision Enabled 有四种：No Collision、Query Only、Physics Only、Collision Enabled。[3] 交互体积通常用 Query Only：它能被射线/重叠查询到，但不参与物理模拟，也不会挡住玩家。\n5.3 三件套：物理体、交互体积、命中代理 一个成熟的交互物，碰撞通常不止一个：\n组件 作用 典型设置 物理体（Mesh/Box/Capsule） 真实阻挡、模拟、承载 Collision Enabled，按需 Block 交互体积（Box/Sphere/Capsule） 判定“是否可被选中/在范围内” Query Only，响应 Interaction 命中代理（可选） 让射线打到“可交互区域”，而不是整面墙 简单碰撞，挂在插槽/骨骼上 三者分开的好处：\n美术可以把模型做得很大，但交互体积只覆盖“把手”附近，避免玩家隔着老远就能开门； 物理碰撞可以很粗糙，交互体积可以很精确，互不牵制； 交互体积可以比物理体大一圈，提升手柄/触屏的手感； 复杂形状可以用多个简单碰撞拼，不需要复杂碰撞。 5.4 碰撞箱 UBoxComponent 是最常用的交互体积：便宜、可旋转、边界清晰。\n适合：\n门、宝箱、终端、按钮； 需要“正面交互”的物体（盒体的朝向就是交互面）； 需要精确控制交互范围的机关。 注意事项：\n盒体的 Box Extent 是半尺寸，不是全尺寸； 盒体默认以中心为原点，挂在插槽上时要调 RelativeLocation； 盒体可以设 Query Only，不要让它挡住玩家； 如果盒体要跟着门板动，挂在门板组件下，而不是挂在 Actor 根节点。 5.5 胶囊体 胶囊体常用于两类东西：\n角色：Character 类默认带 CapsuleComponent，用于移动碰撞；官方文档明确说，为了计算复杂几何，Character 的碰撞组件假设是一个竖直朝向的胶囊。[6] 交互体积：站在地上的物件、NPC 的“可交互范围”、近身拾取，都可以用胶囊体。 胶囊体的好处是“没有尖角”，角色贴着走不会卡；坏处是“边界不直观”，尤其是竖直/水平朝向和半径/半高。对于交互体积，胶囊体适合“人形/圆柱形”的对象，不适合方形柜子。\n5.6 不规则形状 不规则形状的碰撞，按成本从低到高：\n方案 做法 成本 适用 单个简单形状 用一个盒/球/胶囊包住 最低 远距离、低精度 复合简单碰撞 多个盒/球/凸包拼起来 低 家具、机械、门框 凸包分解 用 V-HACD 等工具把网格拆成多个凸包 中 不规则石块、雕塑 复杂碰撞（trimesh） 直接用三角网格 高 静态世界、精确射线 物理资产（骨骼） 每根骨骼一个胶囊/盒 中 角色、生物、可破坏肢体 选择原则：\n会被大量查询的（交互射线、子弹）：用简单碰撞； 只做静态遮挡的（墙、地面）：可以用复杂碰撞； 需要模拟的：绝对不能用 UseComplexAsSimple； 角色/生物：用物理资产，命中时读 BoneName 做部位反馈。 一个常见误区是“碰撞越贴合模型越好”。对于交互，贴合反而可能降低手感：门把手的碰撞如果只有几厘米，手柄玩家很难选中；加一个 20–30 cm 的交互盒，体验会好很多。对于移动，贴合会让角色卡在装饰物上；适当的“碰撞简化”是必要的。\n5.7 交互体积与物理碰撞分离 这是整章最重要的一条：不要让“能走进去”和“能按 E”共用同一个碰撞体。\n反例：\n用门板的复杂碰撞做交互检测：射线打到门框边缘，判定失败； 用角色的胶囊体做交互范围：玩家站在箱子后面，仍然能隔箱交互； 用整面墙的碰撞做“按钮”的交互体积：玩家点墙的任何地方都能触发。 正例：\n门板：物理碰撞（阻挡）+ 门把手交互盒（Query Only）； 宝箱：箱体物理碰撞 + 正面上方的交互盒； NPC：角色胶囊（移动）+ 头/胸的交互盒（对话/搜刮）； 电梯按钮：墙面物理碰撞 + 按钮面板的交互盒。 5.8 查询参数 FCollisionQueryParams 里几个常用开关：\n参数 作用 AddIgnoredActor 忽略自己、队友、载具 bTraceComplex 是否用复杂碰撞；交互射线通常 false bReturnPhysicalMaterial 返回物理材质，用于音效/粒子 bFindInitialOverlaps 重叠查询是否包含初始重叠 bIgnoreTouches 忽略 Touch 事件 bSkipNarrowPhase 跳过窄相，只看包围盒（慎用） 5.9 常见坑 现象 原因 修法 交互物“没有碰撞” 组件 Collision Enabled = No Collision 改成 Query Only 射线打不到 通道不响应 / 组件没开查询 检查 Object Type 和 Response 玩家被交互体积挡住 交互体积设成了 Collision Enabled 改成 Query Only 门开了碰撞还在原地 碰撞没挂在动的组件下 把碰撞挂到门板 复杂碰撞导致卡顿 每帧对复杂网格做射线 用简单碰撞/交互体积 角色卡在门槛 简单碰撞太贴合/有缝隙 加一个斜坡碰撞或简化 重叠事件不触发 没开 Generate Overlap Events 打开并检查双方响应 第 6 章 音频触发与材质音效 一句话：声音是交互的“确认键”。玩家很多时候不是看到交互成功，而是听到交互成功。\n6.1 三层声音 一次交互的声音，通常可以拆成三层：\n层 作用 例子 动作层 告诉玩家“输入被接收了” 按钮咔哒、拉杆吱呀、手碰到门 材质层 告诉玩家“这是什么材质” 金属、木头、石头、血肉 结果层 告诉玩家“发生了什么” 门锁打开、宝箱开启、机关启动 三层不一定要同时存在，但缺少任何一层都会让反馈变薄。只有动作层，像在按空气；只有结果层，像游戏在替你做事。\n6.2 触发点 触发点 适合 注意 输入按下 UI 点击、按钮反馈 只适合“确认输入”，不适合“世界结果” 动画通知（Anim Notify） 脚步、手接触、武器挥砍 和动画帧精确对齐 状态进入 门开始移动、机关启动 适合持续音/循环音 物理事件 碰撞、破碎、掉落 需要过滤和限频 射线命中 子弹命中、近战命中 用 FHitResult 的物理材质 最常用的是动画通知：手碰到门把手的瞬间播放“咔哒”，脚落地的瞬间播放脚步声。把声音绑在输入上，容易出现“手还没碰到，声音先响”的延迟感。\n6.3 物理材质 → 表面类型 → 音效 UE 的物理材质（Physical Material）用来定义物体在物理交互中的响应，也可以携带表面类型（Surface Type）。官方文档把物理材质描述为“应用于物理模拟图元、直接或通过材质应用，用来配置和控制物理属性的资产”。[7]\n官方的一个完整示例是“给角色添加脚步声”：用物理材质表示表面类型（金属、石头等），用 Sound Cue 做不同表面的声音，再用一个自定义 Data Asset 保存“表面 → 声音”的映射，最后在蓝图函数库里按检测到的表面返回对应声音。[8] 这个结构可以直接搬到交互音效上：\nMesh / Material -\u0026gt; Physical Material -\u0026gt; Surface Type (Metal / Stone / Wood / Flesh) -\u0026gt; SurfaceAudioDataAsset -\u0026gt; Sound Cue / MetaSound -\u0026gt; Audio Component (3D, attenuation, concurrency) 6.4 一个数据资产示例 USTRUCT(BlueprintType) struct FSurfaceAudioEntry { GENERATED_BODY() UPROPERTY(EditAnywhere) TEnumAsByte\u0026lt;EPhysicalSurface\u0026gt; Surface = SurfaceType_Default; UPROPERTY(EditAnywhere) TSoftObjectPtr\u0026lt;USoundBase\u0026gt; Sound; UPROPERTY(EditAnywhere) float Volume = 1.f; UPROPERTY(EditAnywhere) float PitchJitter = 0.08f; }; UCLASS(BlueprintType) class USurfaceAudioDataAsset : public UDataAsset { GENERATED_BODY() public: UPROPERTY(EditAnywhere) TArray\u0026lt;FSurfaceAudioEntry\u0026gt; Entries; USoundBase* Find(EPhysicalSurface Surface) const { for (const FSurfaceAudioEntry\u0026amp; Entry : Entries) { if (Entry.Surface == Surface) { return Entry.Sound.LoadSynchronous(); } } return nullptr; } }; 实际项目里不要把 LoadSynchronous 放在高频路径上；可以在加载时预加载，或者用 TSoftObjectPtr + 异步加载。这里只是为了展示结构。\n6.5 同一材质的不同音效 “不同材质不同音效”只是第一层。同一个材质，还需要变化：\n随机变体：每个表面准备 3–5 个样本，随机选，避免重复； 音高抖动：±5–10% 的随机音高，让重复命中不机械； 力度分层：轻踩/重踩、轻击/重击，用不同的样本或音量； 复合声音：主材质声 + 细节声（碎石、金属余响）+ 角色 effort； 距离/空间：近处是 3D 声音，远处可能降级为 2D 或直接不播。 一个简单的随机化：\nvoid UInteractionAudioComponent::PlaySurfaceSound(EPhysicalSurface Surface, const FVector\u0026amp; Location) { USoundBase* Sound = SurfaceAudio-\u0026gt;Find(Surface); if (!Sound) { return; } const float Pitch = 1.f + FMath::FRandRange(-PitchJitter, PitchJitter); UGameplayStatics::PlaySoundAtLocation(this, Sound, Location, FRotator::ZeroRotator, Volume, Pitch); } 6.6 空间化与衰减 3D 声音需要衰减（Attenuation）。USoundAttenuation 的官方定义就是“定义声音随听者距离如何变化”。[10] 交互音效通常分为：\n类型 空间化 衰减 例子 近身交互 3D 短距离 开门、拉杆、拾取 环境反馈 3D 中长距离 机关轰鸣、远处爆炸 UI/确认 2D 无 按钮点击、任务完成 持续机械 3D + 循环 中距离 电梯运行、传送带 常见错误是把 UI 音效做成 3D，玩家转个视角声音就没了；或者把环境音效做成 2D，走到哪里都一样响。\n6.7 MetaSounds：让声音跟随参数 MetaSounds 是 UE5 的下一代音源系统：它本质上是一个 DSP 渲染图，支持采样级精度、运行时参数、预设复用，并且可以从 gameplay 事件触发。[9] 对交互物很有用的场景：\n门的开合速度驱动金属摩擦声的音高/滤波； 机关的能量等级驱动嗡鸣的层数； 材质的硬度驱动撞击声的瞬态； 玩家的距离驱动混响/低通。 如果项目还在用 Sound Cue，也完全可以先把“材质 → 声音”的数据表跑通，再逐步把复杂交互换成 MetaSound。\n6.8 音频预算与并发 项目 建议 说明 同时播放的交互音效 24–32 用 USoundConcurrency 限流 同一声音的实例 2–3 超过就截断最旧 随机变体 3–5/表面 避免“机关枪重复” 预加载 交互物周边 不要第一次开门时卡一下 遮挡 重要交互 墙后的声音要低通/衰减 6.9 常见坑 现象 原因 修法 声音比动画慢 绑在动画结束/蓝图延迟 绑在 Anim Notify 的接触帧 所有材质一个声 没读 PhysMaterial 用 Hit.PhysMaterial 或组件覆盖 声音重复到烦 只有一个样本 多变体 + 音高抖动 + 限频 转身声音消失 2D/3D 设置错 UI 用 2D，世界用 3D 远处声音太响 衰减没调 用距离衰减 + 低通 多人声音不同步 客户端各自播 由服务器多播，或接受 cosmetic 差异 第 7 章 射弹与弹孔 一句话：弹孔不是“往墙上贴一张图”，而是“命中点 + 法线 + 材质 + 生命周期 + 预算”的组合。\n7.1 即时命中与射弹 方式 做法 优点 缺点 适用 即时命中（Hitscan） 按下瞬间做射线，命中点直接结算 便宜、确定、好同步 没有飞行时间，不能拐弯 步枪、手枪、狙击 射弹（Projectile） 生成一个带速度的 Actor，飞行中检测碰撞 有飞行时间、可追踪、可反弹 网络复杂、性能更高 火箭、榴弹、箭、投掷物 很多游戏会混合：服务器用即时命中做权威结算，客户端播一个“假射弹”做视觉；或者反过来，服务器生成射弹，客户端做预测。关键是权威结算只有一套，否则会出现“我看到打中了，服务器说没中”。\n7.2 射弹运动组件 UE 的 UProjectileMovementComponent 负责更新另一个组件的位置，支持反弹、追踪等行为。官方文档特别说明：如果被更新的组件正在模拟物理，只有初始发射参数（初始速度非零时）会影响射弹，之后由物理模拟接管。[13]\nAProjectile::AProjectile() { ProjectileMovement = CreateDefaultSubobject\u0026lt;UProjectileMovementComponent\u0026gt;(TEXT(\u0026#34;ProjectileMovement\u0026#34;)); ProjectileMovement-\u0026gt;InitialSpeed = 12000.f; ProjectileMovement-\u0026gt;MaxSpeed = 12000.f; ProjectileMovement-\u0026gt;ProjectileGravityScale = 0.2f; ProjectileMovement-\u0026gt;bRotationFollowsVelocity = true; ProjectileMovement-\u0026gt;bShouldBounce = false; } 常用参数：\n参数 作用 InitialSpeed / MaxSpeed 初速度与最大速度 ProjectileGravityScale 重力缩放，0 为直线飞行 bShouldBounce 是否反弹 Bounciness / Friction 反弹系数与摩擦 bRotationFollowsVelocity 弹体朝向跟随速度 HomingTargetComponent 追踪目标 OnProjectileStop 停止时的回调，适合生成命中表现 7.3 命中包 一次命中要播放的东西，最好打包成数据资产，而不是在蓝图里连十个节点：\nUSTRUCT(BlueprintType) struct FImpactPackage { GENERATED_BODY() UPROPERTY(EditAnywhere) TObjectPtr\u0026lt;UMaterialInterface\u0026gt; DecalMaterial; UPROPERTY(EditAnywhere) FVector DecalSize = FVector(10.f, 6.f, 6.f); UPROPERTY(EditAnywhere) TObjectPtr\u0026lt;UNiagaraSystem\u0026gt; ImpactVFX; UPROPERTY(EditAnywhere) TObjectPtr\u0026lt;USoundBase\u0026gt; ImpactSound; UPROPERTY(EditAnywhere) float DecalLifeSpan = 30.f; }; 然后按“表面类型 + 命中部位”选包：\nMetal -\u0026gt; 火花 + 金属声 + 小弹孔 Wood -\u0026gt; 木屑 + 闷响 + 深弹孔 Stone -\u0026gt; 粉尘 + 脆响 + 浅弹孔 Flesh -\u0026gt; 血雾 + 湿响 + 无弹孔（或血迹贴花） 7.4 弹孔贴花 UE 的贴花（Decal）是投影到网格表面的材质。[11] 官方 API 把它描述为“渲染到网格表面的材质，像模型上的贴纸”，贴花组件有 DecalSize、FadeIn/Out、SortOrder 等参数，适合做弹孔、血迹、涂鸦。[12]\n生成弹孔的常用 API 是 UGameplayStatics::SpawnDecalAtLocation：\nconst FRotator DecalRotation = FRotationMatrix::MakeFromX(Hit.ImpactNormal).Rotator(); UGameplayStatics::SpawnDecalAtLocation( this, ImpactPackage-\u0026gt;DecalMaterial, ImpactPackage-\u0026gt;DecalSize, Hit.ImpactPoint + Hit.ImpactNormal * 1.5f, // 沿法线抬一点，避免 z-fighting DecalRotation, ImpactPackage-\u0026gt;DecalLifeSpan); 几个细节：\n朝向：让贴花的投影轴与命中法线对齐。不同项目对“贴花朝里/朝外”的约定可能不同，如果发现贴花反了，把法线取反即可；先在编辑器里确认一次。 偏移：沿法线抬 1–2 cm，避免和表面重叠产生 z-fighting；抬太多会“浮”在表面。 尺寸：DecalSize 的 X 是投影深度，Y/Z 是平面尺寸；弹孔通常 5–15 cm，太大像贴纸，太小看不清。 随机：随机 roll（绕法线旋转）和 0.8–1.2 的缩放，避免所有弹孔一模一样。 附着：打在可移动物体上时，用 SpawnDecalAttached 挂到组件/骨骼，否则物体会从弹孔下面走掉。 寿命：LifeSpan 或 SetFadeOut，不要让弹孔永久堆积。 7.5 弹孔预算与池化 弹孔是最容易“看起来便宜、实际很贵”的东西：\n问题 后果 做法 无限生成 Draw call/材质实例爆炸 每个区域上限 128–256，超出淡出最旧 每个弹孔一个 Dynamic Material Instance 内存和排序成本 共享材质，参数用实例 永久存在 关卡越玩越卡 LifeSpan 30–120 秒，或按区域清理 高精度贴花 移动端 overdraw 低配用简单面片/材质混合 打在骨骼上 弹孔随动画游走 挂骨骼/插槽，或只生成在静态表面 7.6 穿透与多命中 穿透需要多次查询：\n从起点打到第一个命中； 记录入口点和法线； 沿射线继续，找到出口； 在入口和出口各生成一个弹孔/粒子； 按表面厚度和材质决定是否继续。 LineTraceMulti 可以一次拿到多个命中，但它会在第一个阻挡命中处停止；如果要穿过多个物体，需要多次单线查询，或者用 Sweep + 自定义过滤。穿透的预算要单独控制：一发子弹最多穿 2–3 层，否则性能和表现都会失控。\n7.7 网络 弹孔、火花、声音是 cosmetic：用 NetMulticast, Unreliable 广播，丢了不影响玩法； 伤害/破坏是权威：服务器结算，客户端表现； 射弹的移动：服务器权威 + 客户端预测，或者客户端本地生成“假射弹”，服务器用即时命中复核； 弹孔数量：每个客户端自己限流，不要同步“弹孔列表”。 7.8 一个可参考的开源例子 GitHub 上的 UE5-Weapon-System 把射弹、枪口闪光、弹孔贴花、命中粒子放在一个示例里，适合对照本文的“命中包”结构看。[19] 它不是标准答案，但能帮你快速看到“一次射击要生成哪些东西”。\n第 8 章 动效实体 一句话：动效实体是交互的标点符号。用得好，交互有重量；用不好，满屏都是噪音。\n8.1 Niagara 是主力 UE5 的 Niagara 是主要的视觉特效系统，官方文档把它描述为“在 UE5 中做视觉特效（VFX）的主要工具”。[14] 一次交互的动效实体，通常由 Niagara 系统 + 贴花 + 声音 + 灯光组成。\n8.2 一个交互的“表现包” 交互 Niagara 贴花 声音 其他 子弹命中 火花/碎屑/烟 弹孔 材质命中声 灯光闪烁、震屏 开门 灰尘 无 门轴/锁舌 灯光变化 宝箱开启 光柱/尘埃 无 锁扣/木头 镜头轻推 机关启动 能量环/电弧 无 嗡鸣/充能 灯光、手柄震动 NPC 对话 无/氛围粒子 无 语音/环境 相机、景深 “表现包”最好也数据驱动：一个 UInteractionEffectSet 资产，按交互标签索引，运行时按标签取用。这样美术/TA 可以独立调整，不需要程序改蓝图。\n8.3 生命周期 一个动效实体的生命周期：\nSpawn -\u0026gt; Activate -\u0026gt; (Loop / Once) -\u0026gt; Deactivate -\u0026gt; Return to Pool / Destroy 关键点：\nSpawn：用 UNiagaraFunctionLibrary::SpawnSystemAtLocation 或 SpawnSystemAttached； Attach：需要跟随物体/骨骼时用 Attached，注意插槽和缩放； AutoDestroy：一次性特效播完自动销毁；循环特效要手动停； Pooling：Niagara 系统可以开启池化，避免频繁创建/销毁； OnSystemFinished：需要回调时绑定，注意对象生命周期。 8.4 池化与预算 项目 建议 说明 每帧生成上限 16–32 超出合并/丢弃 同时存活命中特效 64–128 按距离/重要度降级 弹孔贴花 128–256/区域 超出淡出最旧 同时播放音效 24–32 用 concurrency 粒子系统复杂度 按平台分级 移动端降发射数/材质 生命周期 0.5–3 秒 不要留永久粒子 8.5 降级 动效实体是最适合做“重要度降级”的：\n距离远的命中特效，减少粒子数/关闭光源； 屏幕边缘的特效，降低更新频率； 低配平台，关闭次要发射器； 多人场景，限制同屏特效数量； 过场/UI 打开时，暂停或降低环境特效。 Niagara 本身有 scalability 和 effect type 的机制，可以把“低/中/高”配置做成资产，而不是在代码里写 if (Platform == Mobile)。\n8.6 调试 用 stat niagara 看粒子开销； 用 Niagara Debugger 看发射器/粒子数； 用 showflag.Particles 等开关隔离问题； 用 stat game 看整体帧时间； 用 stat audio 看音频线程； 用 stat physics 看物理开销。 第 9 章 工程落地与验收 一句话：把“每类交互物写一套”变成“一套组件 + 数据配置 + 少量特例”，项目才能规模化。\n9.1 组件划分 组件 职责 不该做什么 UInteractableComponent 实现接口、状态机、描述符、许可判定 不做射线检测、不播声音 UInteractionSenseComponent 射线/重叠、焦点、瞄准辅助 不改交互物状态 UInteractionMoveComponent 门/电梯/抽屉的位移与旋转 不决定“能不能开” UInteractionAudioComponent 表面音效、随机化、衰减 不做 gameplay 判定 UInteractionEffectComponent 生成命中包/动效实体 不管理交互状态 UInteractionPromptWidget 提示 UI、进度条、失败原因 不直接调用交互 这样拆分后，一个“门”= 交互组件 + 移动组件 + 音频组件 + 碰撞；一个“宝箱”= 交互组件 + 音频组件 + 动效组件 + 动画。新交互物主要是拼组件和配数据，而不是复制蓝图。\n9.2 数据驱动 建议至少有三张表/资产：\n交互描述表：交互标签 → 动词、距离、角度、按住时长、所需标签、动画； 表面音效表：表面类型 → 声音、音量、音高抖动、变体； 命中包表：表面类型/部位 → 贴花、粒子、声音、灯光。 再加上 Gameplay Tags 做条件，基本可以做到“新加一种材质/一种门，不改代码”。\n9.3 网络清单 交互状态由服务器权威； 客户端只预测“输入被接收”，不预测“结果已发生”； 占用/预约在服务器释放，断线/死亡/销毁都要释放； 冷却时间戳在服务器，客户端只做显示； 表现走多播/不可靠，不参与结算； 射弹/投掷物的权威结算只有一套； 弹孔/粒子/声音不做可靠同步。 9.4 性能预算 项目 建议上限 备注 交互射线 10–20 Hz 不用 Tick 交互物数量/区域 按场景，50–200 用空间分区/距离剔除 复杂碰撞查询 仅静态世界 动态物体用简单碰撞 每帧动效实体 16–32 超出合并 同屏命中特效 64–128 按距离降级 弹孔 128–256/区域 淡出最旧 同时交互音效 24–32 concurrency 交互 UI 更新 焦点变化时 不要每帧刷 9.5 调试入口 工具 用途 show Collision / showflag.Collision 1 看碰撞体 DrawDebugLine / DrawDebugSphere 看射线和检测范围 Gameplay Debugger 看 AI/感知/标签 stat game 看整体帧时间 stat physics 看物理开销 stat niagara 看粒子开销 stat audio 看音频线程 Audio Insights 看音频并发/衰减 Niagara Debugger 看发射器/粒子 9.6 验收清单 一个交互物上线前，至少过一遍：\n玩家能在 3 秒内判断“能不能交互”； 按下到第一反馈 \u0026lt; 100 ms； 失败有明确原因（太远/被占用/未解锁/方向不对）； 执行中可被打断，打断后状态能恢复； 多人下不会重复触发/永久占用； 键鼠、手柄、触屏都能选中； 碰撞不会把玩家卡住/穿模； 材质音效正确，重复触发不刺耳； 弹孔/动效有上限，低配可降级； 网络延迟下有合理表现； 有调试开关/日志，能定位“为什么没触发”。 9.7 反模式 反模式 为什么不好 替代 在 Tick 里做射线 浪费、难降频 定时器/事件 用 Cast 判断每类交互物 每加一类改一次 接口 + 标签 只用 bIsOpen 多人/冷却/损坏会崩 显式状态机 交互体积=物理碰撞 手感差、穿模/挡路 分离代理 所有查询用复杂碰撞 性能差 简单碰撞/交互体积 音效写 switch 难扩展 数据表 + 表面类型 弹孔无限生成 越玩越卡 上限 + 淡出 客户端直接改状态 作弊/不同步 服务器权威 反馈绑在输入上 手还没碰到就响 Anim Notify 每个交互物一个蓝图 维护爆炸 组件 + 数据 9.8 交互常用接口速查 下面只列搭一个最小闭环最常用的一组接口，按“契约、检测、碰撞、反馈、NPC 与条件”分组，一句话说明用途；具体机制看前面对应章节，不在这里展开。\n契约\n接口 / 类 一句话说明 IInteractable（自定义 UInterface） 统一 CanInteract、GetPrompt、BeginInteract、EndInteract；对象只回答“能不能”，不抢输入决策。 Blueprint Interface 蓝图版契约；门、宝箱、NPC 都实现同一组函数，调用方不用 Cast 到具体类。 检测\n接口 / 类 一句话说明 LineTraceSingleByChannel 从准星发一条线，取最近命中，最常用的交互检测。 LineTraceMultiByChannel 取到第一个阻挡命中为止的多个结果，做穿透或多候选。 SphereTraceSingleByChannel 带体积的射线，手柄/触屏更容易选中小物件。 OverlapMultiByChannel 范围重叠检测，适合近身拾取、交互区。 FHitResult 命中结果：Actor、组件、命中点、法线、骨骼名、物理材质。 FCollisionQueryParams 查询参数：忽略自身、是否复杂碰撞、是否返回物理材质。 碰撞\n接口 / 类 一句话说明 UPrimitiveComponent 碰撞三件套 SetCollisionEnabled / SetCollisionObjectType / SetCollisionResponseToChannel，控制“能不能被查到、算什么类型、和谁响应”。 UBoxComponent / UCapsuleComponent / USphereComponent 交互体积，通常设 Query Only，只被查询、不挡路。 反馈\n接口 / 类 一句话说明 UPhysicalMaterial + EPhysicalSurface 给表面分类，驱动材质音效和命中粒子。 UGameplayStatics::PlaySoundAtLocation 在世界位置播放 3D 音效，配合 Attenuation 使用。 UGameplayStatics::SpawnDecalAtLocation 生成弹孔/血迹贴花，注意朝向、法线偏移和寿命。 UNiagaraFunctionLibrary::SpawnSystemAtLocation 生成命中、开门等一次性 Niagara 动效。 UAnimNotify 在动画的接触帧触发声音、特效或结算，避免“手还没碰到就响”。 NPC 与条件\n接口 / 类 一句话说明 USmartObjectComponent / USmartObjectSubsystem NPC 交互位的注册、查询与预约，避免两个玩家/ NPC 抢同一个位置。 UMotionWarpingComponent 把根运动对齐到交互位或目标点，解决“站歪了”的对齐问题。 FGameplayTag / FGameplayTagQuery 表达“需要钥匙”“战斗中禁用”“已被占用”等条件。 第 10 章 参考 正文中以 [n] 形式标注的引用编号，对应下列条目。引擎行为以 Epic 官方文档为准；设计取舍部分为项目经验总结，不代替官方规范。\n[1] Epic Games, Simple versus Complex Collision in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/simple-versus-complex-collision-in-unreal-engine\n[2] Epic Games, Collision in Unreal Engine - Overview, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/collision-in-unreal-engine---overview\n[3] Epic Games, Collision Response Reference in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/collision-response-reference-in-unreal-engine\n[4] Epic Games, Blueprint Interface in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/blueprint-interface-in-unreal-engine\n[5] Epic Games, UKismetSystemLibrary::LineTraceMulti, Epic Games API Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Engine/Kismet/UKismetSystemLibrary/LineTraceMulti\n[6] Epic Games, Characters in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/characters-in-unreal-engine\n[7] Epic Games, Physical Materials in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/physical-materials-in-unreal-engine\n[8] Epic Games, Artist 11: Add Footstep Sounds to a Character, Epic Games Documentation. https://dev.epicgames.com/documentation/unreal-engine/artist-11-add-footstep-sounds-to-a-character\n[9] Epic Games, MetaSounds: The Next Generation Sound Sources in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/metasounds-the-next-generation-sound-sources-in-unreal-engine\n[10] Epic Games, USoundAttenuation, Epic Games API Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Engine/Sound/USoundAttenuation\n[11] Epic Games, Decals in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/decals-in-unreal-engine\n[12] Epic Games, UDecalComponent, Epic Games API Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Engine/Components/UDecalComponent\n[13] Epic Games, unreal.ProjectileMovementComponent, Epic Games Python API Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/python-api/class/ProjectileMovementComponent\n[14] Epic Games, Creating Visual Effects in Niagara for Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/creating-visual-effects-in-niagara-for-unreal-engine\n[15] Epic Games, Smart Objects in Unreal Engine - Overview, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/smart-objects-in-unreal-engine---overview\n[16] Epic Games, Motion Warping in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/motion-warping-in-unreal-engine\n[17] Epic Games, Using Gameplay Tags in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/using-gameplay-tags-in-unreal-engine\n[18] Epic Games, Overview of State Tree in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/overview-of-state-tree-in-unreal-engine\n[19] ArtemChig, UE5-Weapon-System (GitHub repository: projectile, muzzle flash, bullet hole decals, impact particles). https://github.com/ArtemChig/UE5-Weapon-System\n[20] Epic Developer Community, Designing an Interaction System in C++: Using Interfaces and TScriptInterface. https://dev.epicgames.com/community/learning/tutorials/58Okd/unreal-engine-designing-an-interaction-system-in-c-using-interfaces-and-tscriptinterface\n[21] Epic Developer Community, Unreal Engine 5: Line Trace + Interface Interaction Tutorial (Japanese). https://dev.epicgames.com/community/learning/tutorials/9mL75/unreal-engine-5-line-trace\n[22] Epic Games, AActor, Epic Games API Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Engine/GameFramework/AActor\n[23] Epic Games, UObject, Epic Games API Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/CoreUObject/UObject/UObject\n[24] Epic Games, Game Objects in Unreal Engine, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/game-objects-in-unreal-engine\n[25] Epic Games, UNiagaraComponent, Epic Games API Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Plugins/Niagara/UNiagaraComponent\n[26] Epic Games, Niagara Data Channels Overview, Epic Games Documentation. https://dev.epicgames.com/documentation/en-us/unreal-engine/niagara-data-channels-overview\n说明：部分社区教程页面需要登录或依赖动态渲染，正文只引用其标题与主题，不依赖其具体代码；引擎行为以官方文档为准。示例代码为说明结构而写，落地时需要按项目模块、命名规范和网络模型调整。\n交互物设计的终点不是“能按 E”，而是“玩家不用思考就知道这里能做什么、做完之后世界变成了什么样”。把感知、判定、执行、反馈拆开，把碰撞、声音、动效当成一等公民，交互才会从“功能”变成“手感”。\n","date":"2026-10-19T00:00:00Z","permalink":"/docs/unreal/ue-interaction-design/","title":"UE 交互物设计：从射线、碰撞到状态流转与反馈"},{"content":"最近用 UE5.6 做项目，踩了一堆光追相关的坑——地编、美术、程序那边都遇到过不少两眼一抹黑的时刻。把一些概念性的东西和引擎模块设计整理一下，下次碰到光追相关的问题至少有个基础知识抓手。\n光线追踪到底是什么、UE 的渲染模块如何容纳\u0026quot;光线\u0026quot;、以及 UE 的光追系统由哪些部分组成。概念口径参考《计算机图形学基础》（Fundamentals of Computer Graphics, 5th ed.），引擎实现参考 UE 5.6 本地源码。\n0. 阅读说明 0.1 内容与基准 章节 内容 面向 依据 第 1 章 光线追踪是什么 概念 《计算机图形学基础》第 3-4 章 第 2 章 UE Render 模块如何容纳光追 工程 UE 5.6 源码 第 3 章 Lumen：UE 处理间接光的第一层架构 工程 UE 5.6 源码 第 4 章 UE 通用光追系统设计 工程 UE 5.6 源码 第 5 章 工程视图：要动的开关、常见翻车与排查入口 美术 / TA / 程序 UE 5.6 源码 第 6 章 附录：源码索引 / 控制台速查 / 参考 工具 UE 5.6 源码 0.2 三条阅读路径 正文每节开头（以及若干关键段）有一行 \u0026gt; **一句话**：...，它是该节结论的白话版本。按下面三条路径读，可以只看自己需要的那部分：\n角色 建议路径 读完能得到什么 美术 / 地编 第 1 章 -\u0026gt; 2.0 节的总览图 -\u0026gt; 5.1 节（要动的开关）-\u0026gt; 5.2 节（常见翻车） 知道光追在哪里介入画面、动哪个开关会影响什么、哪些设置容易被误解 TA / 技术美术 上面 + 3.2 节（Lumen 组成）-\u0026gt; 3.4 节（每帧流程）-\u0026gt; 4.8 节（降噪）-\u0026gt; 5.3 节（排错谱系） 能做质量与性能取舍，能定位常见异常 程序 全篇；第 5 章可跳到 5.3 节 拿到\u0026quot;概念 -\u0026gt; 代码实体\u0026quot;的映射、调用链入口与版本边界 0.3 术语最小集 首次出现的缩写先在这里给一句话口径，正文不再重复解释：\n# 术语 一句话 1 光栅化（Rasterization） 把三角形投影到屏幕、按对象顺序逐像素写入；UE 的主流程 2 G-Buffer 延迟渲染的中间缓冲集合（法线、粗糙度、深度等），后续光照与降噪都从这里取信息 3 Pass 管线里的一段独立工作（一次绘制或一次 compute dispatch），有明确的输入输出资源 4 RDG（Render Dependency Graph） UE 的 Pass 编排系统：Pass 只声明输入输出，排序与资源分配由它负责 5 BVH（Bounding Volume Hierarchy） 按包围盒分层组织的加速树，用于把\u0026quot;找最近交点\u0026quot;从线性扫描降到近似对数级 6 BLAS（Bottom-Level Acceleration Structure） 单个几何体内部的 BVH，只描述形状，不含世界位姿 7 TLAS（Top-Level Acceleration Structure） 场景实例的 BVH，每个实例引用一个 BLAS 并携带变换与掩码 8 SBT（Shader Binding Table） 把\u0026quot;几何 + 实例 + 材质\u0026quot;映射到具体命中着色器的索引表，DXR 规范要求 9 Hit Shader 光线命中后执行的着色程序：Closest Hit（取最近交点着色）、Any Hit（常用来做遮挡判定） 10 permutation（变体） 同一份 shader 源码按编译期开关（如是否启用 Nanite 光追）生成的多份实例 11 全管线光追 / Inline 光追 前者用独立的 RayGen 管线派发；后者在普通 compute shader 内嵌 TraceRayInline 调用 12 SER（Shader Execution Reordering） 硬件按材质/命中给光线重新排序、改善执行相干性的能力；硬件不支持时开关无效果 13 时空降噪 用当前帧邻域（空间）与历史帧（时间）把\u0026quot;每像素少量光线\u0026quot;的结果补成干净画面 0.4 约定 引擎源码路径均相对于 UE 根目录（如 Engine/Source/Runtime/...）。 概念口径统一参照《计算机图形学基础》（Fundamentals of Computer Graphics, 5th ed.）对应章节。 文中 \u0026gt; **一句话**：... 是该节结论的白话版本，便于先建立直觉；正文其余部分保留术语精确，机制、公式、时序与约束按原样陈述。 1. 光线追踪是什么 1.1 渲染的两条路线：逐物体 vs 逐像素 渲染的目标是将\u0026quot;三维场景\u0026quot;转换为\u0026quot;二维像素图\u0026quot;。像素图的本质是什么？光栅图像是一个二维数组，每个元素（像素）存储图像在该位置附近颜色的局部平均值（点样本）。[1] 换言之，无论采用何种渲染方法，最终产物几乎总是一张光栅图像。\n实现这一转换存在两种相反的组织方式：\n对象顺序（Object-Order） 图像顺序（Image-Order） for every object: for every pixel: 计算它影响了哪些像素 计算它被哪个物体遮挡、 -\u0026gt; 光栅化 / 扫描线 以及该物体的着色结果 -\u0026gt; 光线追踪 光栅化采用对象顺序：将三角形投影到屏幕空间，逐像素覆盖写入帧缓冲。其优势在于速度，但\u0026quot;某一像素究竟能看到什么\u0026quot;并非原生问题——可见性需要 Z 缓冲、阴影需要 Shadow Map 等额外机制来近似。 光线追踪采用图像顺序：直接从\u0026quot;像素应看到什么\u0026quot;这一问题出发，天然能够精确求解可见性、阴影、反射、折射等效果。这些在对象顺序框架中需要大量近似手段的问题，在光线追踪中是算法流程的自然产物。[2] 需要澄清的常见误区：\u0026ldquo;光追输出光栅图像\u0026quot;并不矛盾。光追只是通过图像顺序的方式确定每个像素的颜色，输出仍然是光栅位图——图像在数学上可抽象为定义在矩形区域上的连续函数，光栅像素只是该函数的离散采样。[1]\n1.2 基本做法：生成光线、求交、着色 基本光线追踪算法可归纳为三步[3]：\n光线生成（Ray Generation）：对每个像素，从视点 e 发射一条穿过该像素的视线（view ray）。视线的数学表达是三维参数直线： p(t) = e + t * (s - e) 其中 e 为视点位置，s 为像素在图像平面上的采样点，参数 t 表示沿视线方向的距离。t \u0026lt; 0 对应相机后方的点，t = 0 即为视点本身。[4]\n求交（Intersection）：寻找视线与场景中最近的有效交点。这一步有两个关键语义：\n\u0026ldquo;最近\u0026quot;的含义：离视点最近的物体会遮挡其后所有物体，因此只需保留最小的合法 t 值。 与一组物体求交的朴素做法：把一组物体视为\u0026quot;一个复合表面\u0026rdquo;，逐个求交并持续收缩搜索区间 [t0, t1]——每找到一个更近的命中点，就把区间上界 t1 收紧到该点的 t，后续物体只在更窄的区间内参与竞争。[5] // 一组物体求交：最近交点 + 区间收缩（示意代码） HitRecord Hit(Group g, Ray r, float t0, float t1) { HitRecord closest = HitRecord(); // t = INFINITY，表示未命中 for (auto surface : g.surfaces) { HitRecord rec = surface.Hit(r, t0, t1); if (rec.t \u0026lt; t1) { // 仅接受比当前更近的命中 closest = rec; t1 = rec.t; // 收缩搜索区间 } } return closest; } 需要注意的是：逐物体线性扫描的时间复杂度为 O(N)，在大规模场景中不可用。工程实践中会引入加速结构（如 BVH）将\u0026quot;查找最近物体\u0026quot;降为近 O(log N)——这正是第 4 章 UE 光追系统中 BLAS/TLAS 所要解决的问题。在概念上，加速结构同样被视为\u0026quot;一个可求交的对象\u0026rdquo;，与普通表面在组合层级上是平级的。[5]\n着色（Shading）：在获得交点位置、表面法线、材质属性与光源信息后，计算该像素的最终颜色。着色模型本身与光栅化完全通用——区别仅在于几何信息（交点、法线）与可见性信息是求交阶段直接给出的精确值。[6] 阴影是这套流程的自然延伸：判断点 x 是否被某光源照亮，只需从 x 出发向光源方向发射一条阴影光线（shadow ray）；若中途被其他物体遮挡，则 x 处于该光源的阴影中。为避免 x 所在表面因浮点误差被自身误判为遮挡，阴影光线的起始参数需设置一个极小的正偏移 t = epsilon，而非从 t = 0 开始。\n1.3 为什么是从摄像机\u0026quot;反向\u0026quot;发射光线 这是初学者最常遇到的困惑：现实中光从光源射向场景，为何算法要反过来从相机发射？\n答案可以从物理与计算两个层面分别理解：\n物理层面：光路具有可逆性。一条从光源出发、经多次弹射最终进入相机的光路，其反向路径（从相机到光源）在物理上是等价的。渲染所关心的只是\u0026quot;最终到达相机的光\u0026quot;，无论正向还是反向计算，结果一致。\n计算层面：正向追踪效率极低。若从光源向所有方向均匀发射\u0026quot;光子\u0026quot;，能进入某一像素视锥的比例微乎其微——绝大多数计算资源都会浪费在永远不会被看见的路径上。反向追踪则不同：每条光线都指向一个确定的像素，发射的全部视线都对最终图像有贡献。[2]\n因此\u0026quot;反向\u0026quot;并非对物理事实的违背，而是一种采样策略：图像顺序的光线追踪天然只采样\u0026quot;对最终图像有贡献的路径\u0026quot;，将计算量集中在有效区域。\n正向（物理直觉）: light -\u0026gt; 场景散射 .................... 绝大多数光子永远无法抵达相机 反向（算法实现）: camera -\u0026gt; 像素对应方向 -\u0026gt; 命中物体 -\u0026gt; (按需)继续发射阴影/反射光线 ^ 每条光线都对对应像素有确定性贡献 1.4 光线追踪与 Shader 的关系 一句话：光追只回答\u0026quot;光线碰到谁\u0026quot;，碰到之后长什么样由着色程序决定——这条分界线在硬件光追里就是\u0026quot;几何层\u0026quot;与\u0026quot;Hit Shader\u0026quot;的分工。\n\u0026ldquo;求交确定最近物体\u0026quot;只回答了\u0026quot;光线击中了谁\u0026rdquo;，还未回答\u0026quot;该物体呈现什么样\u0026quot;。答案取决于命中点执行哪段材质/光照程序。光线追踪与着色器（Shader）的关系可从以下角度理解：\n光线追踪框架先完成\u0026quot;几何判定\u0026quot;，再向着色阶段提供标准输入参数：求交直接给出交点位置 p = e + t*d 与表面法线 n；视线方向 v 取入射光线方向的反向（即 v = -d_hat）；此外还包括材质属性（漫反射/镜面系数、粗糙度等）与纹理坐标——以上构成着色所需的完整输入集。 阴影 / 光源可见性属于附加输入：判断交点是否被某光源照亮，需沿光源方向 l 额外发射一条阴影光线，将遮挡结果（二元或软阴影系数）并入着色计算[7]、[8]。 着色模型本身（漫反射 + BRDF、反射、折射）在光线追踪与光栅化之间完全通用——同一套着色公式可无缝用于两条渲染路线。[6] 进入 GPU 硬件光追（DXR / Vulkan RT）时代后，上述流程被拆分为固定管线遍历 + 多阶段可编程 Shader：遍历由硬件完成，可编程阶段负责生成光线、判定命中与计算颜色。这四个阶段（Ray Generation / Intersection 与 Any Hit / Closest Hit / Miss）的职责清单放在 4.3 节，与 UE 的 SBT 和材质 Hit Shader 一起讲——它们是同一件事的规范侧与实现侧。\n核心结论可以用一句话概括：光追框架决定\u0026quot;光线去向、命中对象\u0026quot;，Shader 决定\u0026quot;命中后计算的颜色值\u0026quot;。 在 UE 中，这两个角色被清晰地划分为两个系统：前者对应第 4 章的\u0026quot;光追场景 + 着色器绑定表\u0026quot;，后者对应\u0026quot;材质 Hit Shader\u0026quot;（Closest Hit / Any Hit 与 UE 材质系统的绑定）。这条分界线是阅读后续 UE 实现代码的关键钥匙。\n本章小结：光追以图像顺序从像素出发，经生成 -\u0026gt; 求交 -\u0026gt; 着色三步；反向发射是光路可逆下的高效采样策略；硬件光追拆为 RayGen / Intersection / ClosestHit / Miss 四阶段，光追框架与 Hit Shader 职责分离。\n2. UE 渲染模块：光栅为主，光线为辅 2.0 一帧总览 一句话：UE 每帧先按光栅化画出画面，只有当某个效果\u0026quot;光栅化做不好\u0026quot;时，才临时把场景整理成光追可用的形式、额外发射一些光线去补，补完再降噪合成。\nflowchart TD A[\"帧开始：场景数据就位（实例、变换、材质）\"] --\u003e B[\"按常规光栅化：得到每个像素的几何与深度信息\"] B --\u003e C[\"常规光照与阴影：直接光、阴影贴图、屏幕空间近似\"] C --\u003e D{\"本帧有没有'光栅化做不好'的效果？间接光 / 反射 / 精确阴影 / 半透明折射\"} D --\u003e|没有| E[\"按常规流程合成输出\"] D --\u003e|有| F[\"把场景整理成光追可用的形式（加速结构）\"] F --\u003e G[\"发射光线：屏幕探针 / 阴影光线 / 反射光线\"] G --\u003e H[\"命中后取光照结果：查缓存或实时着色\"] H --\u003e I[\"时空降噪：把少量光线的结果补成干净画面\"] I --\u003e E这段骨架的三层含义分别对应后文三章：整理场景与发射光线是第 4 章（通用光追系统）；\u0026ldquo;间接光\u0026quot;这条路主要由第 3 章（Lumen）负责；\u0026ldquo;哪些效果需要额外光线、用什么开关控制\u0026quot;在第 5 章。\n两个容易误解的点：一是光追不替代光栅化，它是插在主循环里的若干片段，关掉后画面仍然完整，只是少了那几类效果；二是**\u0026ldquo;有光追效果\u0026quot;不等于\u0026quot;每帧重建全部加速结构\u0026rdquo;**——几何的加速结构按静态 / Nanite / 动态三类分别维护，重建是增量的（4.2 节）。\n第 1 章讲概念；本章进入 UE 工程实现。首先需要建立的整体认知是：UE 渲染器并未\u0026ldquo;以光追取代光栅化\u0026rdquo;，而是以光栅为主流程，将光追作为若干嵌入 Pass 执行。理解这一\u0026quot;嵌入\u0026quot;机制的三个关键维度是：代码组织位置（Renderer 模块）、Pass 编排方式（RDG）、以及光追 Pass 在主循环中的挂载点。\n2.1 Renderer 模块与 RDG UE 渲染器位于 Engine/Source/Runtime/Renderer/。核心实现代码集中在 Runtime/Renderer/Private/ 下，按功能拆分子目录。与光线追踪主题相关的目录与文件包括：\nEngine/Source/Runtime/Renderer/Private/ DeferredShadingRenderer.{h,cpp} \u0026lt;- 延迟渲染主循环（所有光追 Pass 的最终编排入口） RayTracing/ \u0026lt;- 通用硬件光追设施与独立 RT Pass Lumen/ \u0026lt;- Lumen（全局光照 / 反射）实现 Nanite/ \u0026lt;- Nanite（含 Nanite 专用光追 BLAS 管理） PathTracing.{h,cpp} \u0026lt;- 参考级 Path Tracing（收敛级完整光追） MegaLights/ \u0026lt;- 5.6 新增灯光虚拟化系统（可选硬件光追） ScreenSpaceDenoise.{h,cpp} \u0026lt;- 屏幕空间时空降噪器（光追效果降噪的统一入口） 自 UE 5.0 起，主渲染器废弃了\u0026quot;在普通函数内串行执行立即模式 RHI 调用\u0026quot;的旧模式，全面切换为RDG（Render Dependency Graph）声明式 Pass 编排：每个 Pass 在注册时显式声明输入与输出的 RDG 资源，RDG 内部构建依赖拓扑图，自动完成资源生命周期管理、依赖排序、资源屏障优化与 GPU Render-Pass 合并。RDG 的核心入口是 FRDGBuilder，头文件位于 Engine/Source/Runtime/RenderCore/Public/RenderGraphBuilder.h。渲染器的每一个步骤都注册为 RDG Pass，所有 RHI 调用被约束在 Pass 的执行回调内执行。光线追踪相关逻辑完全复用这套 RDG 体系，不存在独立的调度路径——唯一区别仅在于 Pass 内部执行的是光线追踪 GPU 阶段（RayGen / TraceRay / DispatchRays），而非常规的光栅化或计算着色器调度。\n2.2 RHI 层的光追抽象：DXR 的封装 RHI（Render Hardware Interface）是 UE 对所有图形 API 的统一抽象层，位于 Engine/Source/Runtime/RHI/。硬件光追所需的资源对象——加速结构、光追管线状态、Shader 绑定表——在 RHI 层均有对应的抽象接口：FRHIRayTracingGeometry（BLAS）、FRHIRayTracingScene（TLAS）、FRHIRayTracingPipelineState 等，统一声明于 Engine/Source/Runtime/RHI/Public/RHIResources.h。\n具体实现落在各平台的 RHI 后端。以桌面 D3D12 为例，Engine/Source/Runtime/D3D12RHI/Private/D3D12RayTracing.{h,cpp} 将 UE 的 RHI 抽象翻译为 DXR 原生调用，包括加速结构构建、DispatchRays 派发等。更高阶的执行优化（如 Shader Execution Reordering，SER）由渲染层通过控制台开关暴露（Lumen 侧对应 r.Lumen.HardwareRayTracing.ShaderExecutionReordering），实际生效与否取决于平台与驱动的能力支持，RHI 层在运行时以能力探测结果为最终依据。\nUE 5.x 中硬件光追的主力后端为 D3D12 + DXR。光追功能的总开关是 r.RayTracing，它驱动渲染器内部 FFamilyPipelineState::bRayTracing 状态位；各类具体效果（阴影、AO、半透明等）在项目设置与 RaytracingOptions.h 中进一步细分独立开关。引擎代码中大量使用 IsRayTracingEnabled() / RHISupportsRayTracing*() 作为能力门禁，在光追不可用时零成本短路相关路径。\n2.3 场景数据流与主循环挂载点 光线追踪需要遍历\u0026quot;完整场景\u0026rdquo;，因此对几何可见性的要求高于光栅化。UE 中场景侧的数据流向为：\nFPrimitiveSceneInfo（CPU 端渲染代理） | v FGPUScene（GPU 场景，存储实例数据 / 变换矩阵，挂载于 SceneRenderer::Scene-\u0026gt;GPUScene） | v FRayTracingScene（每帧重建的光追专用场景） 设计要点：UE 5 引入 GPUScene 后，光追场景的构建（第 4.1 节的 FRayTracingScene::Create）直接以 GPU 场景为输入，而非逐 Primitive 从 CPU 搬运数据。这一设计从根本上降低了场景构建的 CPU 开销——每帧只需将实例信息整理进加速结构，几何数据本身可跨帧缓存复用。\n在延迟渲染器 FDeferredShadingSceneRenderer（DeferredShadingRenderer.cpp）的主循环中，有两个编排函数是光追链路的核心入口：\nDispatchRayTracingWorldUpdates：收集本帧所有需要更新的光追几何与实例，提交 BLAS 构建与场景构建请求（Create + Build）。 SetupRayTracingRenderingData：为每个 View 组装 Lumen / Nanite / 光源网格等光追所需的统一缓冲，并判断本帧是否存在 Inline 光追 Pass。 实际的各光追 Pass 派发则嵌入在光栅化主循环的对应阶段内，例如天空光采样阶段、半透明渲染阶段。这些 Pass 的执行条件通过一组 ShouldRender* 判断汇总为\u0026quot;本帧是否需要构建光追场景\u0026quot;的综合标志，用于在不需要光追时提前短路整条场景构建链路。这正是\u0026quot;光追是嵌入光栅主循环的 image-order 计算片段\u0026quot;这一论断在代码层面的直接体现。\n本章小结：UE 以光栅为主流程，光追作为嵌入 Pass 执行；Renderer 模块通过 RDG（FRDGBuilder）统一调度 Pass；RHI 层抽象 DXR 调用；场景数据沿 FPrimitiveSceneInfo -\u0026gt; FGPUScene -\u0026gt; FRayTracingScene 流转，主循环由 DispatchRayTracingWorldUpdates 和 SetupRayTracingRenderingData 两个编排函数驱动。\n3. Lumen：UE 处理\u0026quot;间接光\u0026quot;的第一层架构 一句话：间接光与反射是最\u0026quot;吃光追能力\u0026quot;的两件事，UE 把它们交给 Lumen；Lumen 是第 4 章那套通用光追基础设施的最大使用者，而不是基础设施本身。\n本章与第 4 章的读法（先给结论，避免读反顺序）：第 4 章的通用光追系统是基础设施层，负责加速结构与命中分派，服务所有消费者；Lumen 是它的首要租户。Lumen 既可以完全不发射硬件光线（软件模式：屏幕空间追踪 + 距离场追踪），也可以借用基础设施发射硬件光线（硬件模式）。因此 3.2 与 3.4 节里的\u0026quot;光线来源\u0026quot;应理解为一个可替换的部件；若更想先看底座，可先读 4.1、4.2 节再回本章。\n从用户视角提问\u0026quot;UE 5 如何处理光线\u0026rdquo;，第一个答案往往不是某个\u0026quot;光追开关\u0026quot;，而是 Lumen——它是 UE 当前全局光照（GI）与反射的默认架构。在深入通用光追系统之前，先建立对 Lumen 的完整认知，原因有三：\nLumen 是 GI / 反射（多次弹射、间接光）这类\u0026quot;最依赖光追能力\u0026quot;问题的当前标准答案； Lumen 可以选择性启用硬件光追作为部分光线的来源，是通用 RT 系统的首要租户； 第 4 章的\u0026quot;通用光追系统\u0026quot;负责构建底层加速结构，供 Lumen（可选）与其他独立效果（阴影、AO、半透明等）共同复用。 3.1 为什么需要 Lumen \u0026ldquo;间接光照\u0026rdquo;（Global Illumination, GI）是实时渲染中最具挑战的问题：一个像素的最终颜色不仅来自直接光照，还包含周围物体反弹而来的间接光，且反弹次数在理论上是无限的。在 Lumen 出现之前，实时引擎对这一问题的解决方案是一系列静态或近似手段（详见 3.3 节历史回顾）：光照贴图烘焙（场景变更后需重新烘焙）、屏幕空间 GI（仅覆盖屏幕可见区域）、反射捕获体与 SSR（反射保真度有限）等。\nLumen 的定位是系统性补齐上述短板：提供全动态、可扩展的实时间接光照与反射方案——太阳方位可调、场景几何体可移动/销毁、物体位姿可动态变化，间接光照结果能够实时跟上，视觉目标逼近离线烘焙与离线光追的质量。Lumen 是 UE 5 的默认光照架构（延迟渲染器中 GI 与反射的\u0026quot;方法枚举\u0026quot;默认指向 Lumen，见 3.3 节）。\n3.2 Lumen 的组成 Lumen 并非单一 Pass，而是一套场景表示 + 多级缓存 + 光线来源策略的组合体系。基于 5.6.1 源码（Engine/Source/Runtime/Renderer/Private/Lumen/），其核心可拆分为四个组件：\n组成模块 核心作用 5.6.1 代表文件（短名） 场景表示 将场景转换为\u0026quot;可供光线跟踪\u0026quot;的近似表示，避免对所有三角形做精确求交 LumenScene.cpp、LumenMeshCards.cpp、LumenMeshSDFCulling.cpp Screen Probes（屏幕探针） 在屏幕空间稀疏布置探针，向四周发射光线，单帧内产出可用的间接光照结果 LumenScreenProbeGather.cpp、LumenScreenProbeFiltering.cpp Radiance Cache（辐照度缓存） 将屏幕探针的结果缓存到世界空间并跨帧累积，使光照信息向屏幕不可见区域传播 LumenRadianceCache.cpp Surface Cache（表面缓存） 在 Mesh 表面以卡片（Card）为单位存储辐照度与材质结果，供光线命中时直接查询 LumenSurfaceCache.cpp、LumenSurfaceCacheFeedback.cpp 三个缓存组件的协作关系可以直观地理解为：\nScreen Probes -\u0026gt; 负责\u0026#34;当前可见像素\u0026#34;的快速出结果（屏幕空间，单帧内完成） Radiance Cache -\u0026gt; 负责\u0026#34;不可见 / 远处 / 间接\u0026#34;信息的时空传播（世界空间，跨帧累积） Surface Cache -\u0026gt; 光线命中 Mesh 表面时，直接查询\u0026#34;该表面当时的颜色\u0026#34;而无需重新着色 在场景表示层面，Lumen 采用了两项极具 UE 特色的近似技巧：\n全局距离场（Global Distance Field / Mesh SDF）：将场景网格近似为有符号距离场，光线通过\u0026quot;跟踪 SDF\u0026quot;能够以极低开销穿越大片空旷区域并快速逼近表面（LumenMeshSDFCulling.cpp、LumenMeshCards.cpp）。 Mesh Cards（网格卡片）：将每个 Mesh 的\u0026quot;宏观光照状态\u0026quot;展开为若干张正交投影的卡片纹理，光线命中卡片即可采样——这正是 Surface Cache 的物理载体。 在光线来源策略上，Lumen 提供两种运行模式：\n软件模式（默认）：光线不经过硬件光追单元，而是采用屏幕空间追踪 + SDF 追踪的混合方案。Screen Probe 先对屏幕空间可见的几何发射光线（LumenScreenProbeTracing.cpp），不可见区域则通过 SDF 在世界空间追踪补足。 硬件光追模式（可选）：当平台支持且项目设置开启时，同一套 Lumen 管线将\u0026quot;发射光线\u0026quot;替换为真正的硬件光追调用（硬件光追相关实现见 LumenScreenProbeHardwareRayTracing.cpp、LumenRadianceCacheHardwareRayTracing.cpp、LumenReflectionHardwareRayTracing.cpp 等）。硬件 RT 提供的并非\u0026quot;另一套 GI 求解器\u0026quot;，而是更精确、更稳定的光线来源——这正是 UE 官方对\u0026quot;Lumen + RT\u0026quot;关系的定位：RT 是 Lumen 的可选加速器，而非独立的完整 GI 方案。 flowchart TD subgraph RAY[\"光线来源层 (二选一只决定光线怎么发出)\"] S[\"软件模式（默认）Screen 空间追踪 + SDF 世界空间追踪\"] H[\"硬件光追模式（可选）DXR RayGen，分别服务：Screen Probe / Radiance Cache / Reflection\"] end subgraph RESULT[\"采样/结果层 (两模式都走同一套流程)\"] SP[\"屏幕探针结果 Screen Probes负责当前可见区域，一帧内出结果\"] end subgraph CACHE[\"缓存层 (两模式共用，与光线来源无关)\"] RC[\"Radiance Cache世界空间跨帧累积，传播到不可见区域\"] SC[\"Surface Cache / Mesh Cards光线命中表面时的查询缓存\"] end subgraph FINAL_OUT[\"输出层\"] FILTER[\"Screen Probe 滤波 / 插值\"] LIGHT[\"合成进主光照\"] end S --\u003e SP H --\u003e SP SP --\u003e RC SP --\u003e SC RC --\u003e FILTER SC --\u003e FILTER FILTER --\u003e LIGHT结构说明：软件模式与硬件模式的差异仅限于\u0026quot;光线来源层\u0026quot;——前者通过屏幕空间追踪 + SDF 追踪实现，后者通过 DXR 的 RayGen 实现（屏幕探针、辐照度缓存、反射三个采样环节各有独立的硬件 RayGen Shader）；从\u0026quot;屏幕探针结果\u0026quot;往下的 Radiance Cache 与 Surface Cache 是两种模式共用的统一缓存结构，不随光线来源策略切换。\n5.6.1 的硬件 RT 模式还有两项值得注意的工程细节：一是支持标准光追管线与 Inline 光追两种派发方式（控制台开关 r.Lumen.HardwareRayTracing.Inline）；二是暴露了大量命中光照相关的细粒度开关（r.Lumen.HardwareRayTracing.HitLighting.* 族，包括命中处直接光、阴影模式、天空光、反射捕获参与度等），这些开关集中定义于 Lumen/LumenHardwareRayTracingCommon.cpp 顶部——若需要深入排查 Lumen 硬件 RT 的各类表现差异，该文件是首选的切入点。\n3.3 Lumen 与\u0026quot;上代\u0026quot;渲染方案的差异 本节为架构演进回顾，聚焦历史方案与当前架构的对比，不涉及 5.6.1 代码的具体行为细节。\n\u0026ldquo;上代\u0026quot;概念需要从两个维度分别考察：\n（a）UE4.x / 早期 UE5 的实时间接光照方案谱系\n历史方案 核心思路 主要限制 光照贴图烘焙（Lightmap） 离线将间接光烘焙进 UV 贴图 完全静态；光照/几何变化均需重新烘焙 反射捕获体（Reflection Capture）+ 探针 将环境光烘焙为低分辨率 Cubemap 仅能表现低频间接光，动态场景适应性差 屏幕空间 GI（SSGI） 利用屏幕空间信息估算间接光 仅\u0026quot;看见\u0026quot;屏幕内像素；遮挡与屏幕外区域失效 距离场软阴影 / AO 基于 Distance Field 近似计算阴影与 AO 仅为 AO/阴影级别近似，不涉及 GI 求解 独立硬件光追 GI/反射 Pass（约 5.0-5.1） 直接用 RT 渲染间接光与反射 蒙特卡洛噪声严重，每像素成本高，缺乏可扩展缓存机制 Lumen 相对于上述方案的根本突破在于多级缓存架构：它不在每个像素上执行完整的路径追踪，而是将\u0026quot;廉价的稀疏光线发射\u0026quot;与\u0026quot;跨帧、跨屏幕的缓存复用\u0026quot;相结合，使计算成本可控且场景保持完全动态。这正是 Lumen 能够将\u0026quot;接近离线质量的 GI\u0026quot;带入实时渲染的核心原因。\n（b）独立 RT Pass 架构与 Lumen 的组织差异\n早期 UE5 中，硬件光追的 GI 与反射以独立 Pass 的形式存在（如单独的 Ray Tracing GI、Ray Tracing Reflections 效果）。演进至 5.6.1，RayTracing/ 目录中已不存在独立的 GI / 反射 Pass 文件——两者在组织上完全并入 Lumen（反射对应 LumenReflections，GI 对应 Screen Probe / Radiance Cache 体系）。今日 RayTracing/ 目录保留的是\u0026quot;通用光追基础设施 + 非 GI 类效果\u0026rdquo;（阴影、AO、半透明、天空光等，详见第 4.4 节）。\n渲染器内部的 GI / 反射 / AO \u0026ldquo;方法枚举\u0026quot;仍然保留了回退至上代方案的选项：\n// DeferredShadingRenderer.h（5.6.1，节选） enum class EDiffuseIndirectMethod { Disabled, SSGI, Lumen, Plugin }; enum class EAmbientOcclusionMethod { Disabled, SSAO, SSGI, RTAO }; enum class EReflectionsMethod { Disabled, SSR, Lumen }; 具体含义为：GI 可在 关闭 / SSGI（上代屏幕空间方案）/ Lumen / 自定义插件 之间选择；AO 可在 SSAO / SSGI 附带 AO / 硬件光追 AO（RTAO）之间选择；反射可在 SSR / Lumen 之间选择。默认组合为完整的 Lumen 体系（Lumen GI + Lumen Reflections），项目可根据目标平台按需回退。这张枚举表同时是理解\u0026quot;Lumen 与通用光追职责边界\u0026quot;的坐标系：RTAO 属于通用光追效果，而 GI 与反射默认由 Lumen 体系负责。\n3.4 Lumen 每帧工作流概览 进入通用光追系统之前，先用极简流程将 Lumen 的时间线固定在整体框架中（各环节的具体细节在第 4 章相关章节展开）：\nflowchart TD A[帧起始: 光栅化生成 G-Buffer / 深度] --\u003e B[Screen Probes: 屏幕空间布置稀疏探针] B --\u003e C{光线来源策略} C -- 软件 --\u003e D[Screen 空间追踪 + 世界空间 SDF 追踪] C -- 硬件 --\u003e E[硬件 RayGen: ScreenProbe / RadianceCache / Reflection] D --\u003e F[Radiance Cache: 跨帧累积间接光信息] E --\u003e F F --\u003e G[Surface Cache: 命中 Mesh 卡片时查询表面光照] G --\u003e H[合成前: Screen Probe 结果滤波/插值 -\u003e 生成间接光照缓冲] H --\u003e I[主光照合成: 直接光 + 间接光]Lumen 的核心哲学可以概括为：\u0026ldquo;每帧只发射少量光线，将信息存入缓存跨帧复用。\u0026rdquo; 这一设计理念是理解 Lumen 体系的核心抓手——无论是性能优化还是问题排查，都可以从\u0026quot;光线发射的稀疏性策略\u0026quot;与\u0026quot;缓存的复用效率\u0026quot;这两个维度系统切入。\n本章小结：Lumen 是 UE 默认的 GI/反射架构，通过场景表示（SDF + Mesh Cards）、屏幕探针、辐照度缓存、表面缓存四块组合实现全动态间接光；采用 Screen Probes -\u0026gt; Radiance Cache -\u0026gt; Surface Cache 的多级缓存流水线；软件（Screen+SDF）与硬件（DXR RayGen）双模式仅差异在光线来源层，缓存结构共用。\n4. UE 通用光追系统设计 一句话：这一章是\u0026quot;底座\u0026rdquo;——它只负责两件事：把场景整理成能高效求交的加速结构，以及把\u0026quot;命中后该跑哪个着色器\u0026quot;组织成一张索引表；具体是谁在用（Lumen、阴影、AO、半透明）属于上层。\n本章进入 UE 光追系统的核心：通用硬件光追基础设施。它是\u0026quot;光线与谁相交\u0026quot;这一问题的工程实现者——向下对接 D3D12RHI / DXR，向上服务两类消费者：Lumen（作为可选的硬件光线来源）与独立 RT 效果（阴影、AO、天空光、半透明等）。代码主要分布于 RayTracing/ 目录与 DeferredShadingRenderer.cpp 的编排函数中。\n4.1 光追场景：FRayTracingScene 的每帧生命周期 UE 中的光追场景是场景级对象（挂载于 FScene，即 Scene-\u0026gt;RayTracingScene），而非 View 级对象。在展开生命周期之前，先明确 DXR 加速结构的两个核心概念 BLAS 与 TLAS：\nBLAS（Bottom-Level，底层加速结构）：将单个几何体的三角形组织为 BVH 树，仅描述\u0026quot;形状\u0026quot;（几何内部的层次关系），不包含世界位姿信息。 TLAS（Top-Level，顶层加速结构）：将\u0026quot;场景中的实例\u0026quot;组织为 BVH 树，每个实例引用一个 BLAS 并携带变换矩阵、实例掩码、命中分组索引等元数据，描述\u0026quot;哪个物体以何种位姿出现在场景中\u0026quot;。 两者涉及两种关键顺序，需严格区分：构建顺序为先 BLAS 后 TLAS（TLAS 的实例必须引用已就绪的 BLAS 地址）；光线遍历顺序则恰好相反，先 TLAS 后 BLAS（先确定命中哪个实例，再通过实例变换矩阵将光线变换至该 BLAS 的局部空间，最终查找具体三角形）。这正是下方 Build 方法注释中\u0026quot;BLAS 已就绪时，这里主要做 TLAS 构建\u0026quot;的语义来源。核心接口声明见 RayTracing/RayTracingScene.h：\nclass FRayTracingScene { // Create: 帧起始阶段，基于 FGPUScene 收集/计算需要入场景的实例与几何 void Create(FRDGBuilder\u0026amp; GraphBuilder, const FViewInfo\u0026amp; View, const FGPUScene* GPUScene, ERDGPassFlags ComputePassFlags); // Build: 真正提交加速结构构建（BLAS 就绪后，此处以 TLAS 构建为主） void Build(FRDGBuilder\u0026amp; GraphBuilder, ERDGPassFlags ComputePassFlags, FRDGBufferRef DynamicGeometryScratchBuffer); // PostRender: 帧末收尾（统计复位 / 临时资源释放） void PostRender(FRDGBuilder\u0026amp; GraphBuilder, ERDGPassFlags ComputePassFlags); }; 每帧生命周期可归纳为\u0026quot;复位 -\u0026gt; 几何/实例更新 -\u0026gt; 场景构建 -\u0026gt; 数据就绪 -\u0026gt; 消费使用 -\u0026gt; 帧末收尾\u0026quot;五个阶段，各阶段对应函数如下（FRayTracingScene 方法见 RayTracingScene.h，编排函数见 DeferredShadingRenderer.cpp）：\n阶段 对应函数 核心职责 复位 FRayTracingScene::Reset 清空内部数组，为新一轮实例收集做准备 几何/实例更新 DispatchRayTracingWorldUpdates 等待场景初始化任务；提交\u0026quot;待构建 BLAS\u0026quot;列表；处理 Nanite 与动态几何的更新请求 场景构建 FRayTracingScene::Create + FRayTracingScene::Build 基于 Scene-\u0026gt;GPUScene 重建 TLAS 顶层场景 数据就绪 SetupRayTracingRenderingData 为各 View 组装 Lumen / Nanite / 光源网格等统一缓冲 消费使用 各光追 Pass（见 4.4 节） 场景消费：阴影 / 天空光 / AO / 半透明 / Lumen 硬件光追 帧末收尾 FRayTracingScene::PostRender 释放临时资源、复位统计信息 整条链路的总开关由 FamilyPipelineState[\u0026amp;FFamilyPipelineState::bRayTracing]（由 r.RayTracing 控制台变量驱动）控制。当整帧不存在任何需要执行的光追 Pass 时，整个场景构建链路会被完全短路——不会白白支付加速结构的构建与维护成本。\n具体调用链（下表是 FDeferredShadingSceneRenderer::DispatchRayTracingWorldUpdates 内部的执行顺序，也是排查\u0026quot;加速结构没准备好\u0026quot;这类问题的第一现场）：\n顺序 调用 作用 1 Nanite::GRayTracingManager.ProcessBuildRequests(GraphBuilder) 处理 Nanite 侧已排队的 BLAS 构建请求 2 GRayTracingGeometryManager-\u0026gt;ForceBuildIfPending(GraphBuilder.RHICmdList, RayTracingScene.GeometriesToBuild) 对本帧必须就绪的几何强制完成挂起的 BLAS 构建，输入来自 RequestBuildAccelerationStructure 排队的请求 3 Nanite::GRayTracingManager.ProcessBuildRequests(GraphBuilder)（第二次） 返回\u0026quot;本帧是否有 BLAS 被重建\u0026quot;；为真时对相关视图调用 PathTracingInvalidate()，因为 Path Tracing 的跨帧样本累积要求几何在时间上一致（4.6 节） 4 DynamicGeometryUpdateManager-\u0026gt;AddDynamicGeometryUpdatePass(...) 注册动态几何（蒙皮 / 布料 / 顶点动画）的顶点更新 Pass，并输出本帧的 scratch buffer，供随后的 FRayTracingScene::Build 使用 5 ((FRayTracingGeometryManager*)GRayTracingGeometryManager)-\u0026gt;ResetVisibleGeometries() 复位本帧的可见几何集合，供下一帧重新收集 除这条主链外还有两处调用点：一是 FDeferredShadingSceneRenderer::Render 里的 GRayTracingGeometryManager-\u0026gt;PreRender() 与其后的 ProcessBuildRequests(GraphBuilder.RHICmdList)（源码此处留有 // TODO: should only process build requests once per frame，即当前实现每帧会处理两次请求）；二是主循环中多次出现的 SetupRayTracingRenderingData(GraphBuilder)，它是否需要执行由本函数的返回值 bNeedToSetupRayTracingRenderingData 与 Lumen 的两个谓词（Lumen::UseHardwareRayTracedSceneLighting / Lumen::UseAsyncCompute）共同决定，见 4.5 节。\n4.2 几何与实例进入加速结构的机制 几何侧（BLAS） 按数据来源分为三类独立管理，实例侧（TLAS） 负责实例收集、剔除与掩码通道。UE 的具体实现如下：\n几何侧（BLAS）：三类来源的独立管理策略\n几何来源 核心管理机制 常规静态网格 FRayTracingGeometryManager（声明于 Engine 模块的 Rendering/RayTracingGeometryManager.h）：请求侧 RequestBuildAccelerationStructure(Geometry, Priority, BuildMode) 入队，BoostPriority 调整排队顺序，RemoveBuildRequest 撤销；提交侧 ProcessBuildRequests(CmdList, bBuildAll) 批量执行；ForceBuildIfPending 用于在本帧内强制完成指定几何的挂起构建 Nanite 网格 Nanite::GRayTracingManager（Nanite/NaniteRayTracing.{h,cpp}）：Add / Remove / AddVisiblePrimitive 维护参与光追的 Primitive，RequestUpdates 收集更新请求，Update + ProcessUpdateRequests 计算裁剪后可见的集群并生成 BLAS，ProcessBuildRequests 提交构建，EndFrame 收尾；ScheduledBuilds 是\u0026quot;本帧要构建的几何\u0026quot;，PendingBuilds 的注释把未构建的几何归因于\u0026quot;每帧 BLAS 构建预算节流\u0026quot;（注释里用的是旧名 r.RayTracing.Nanite.MaxBlasBuildsPerFrame，实际 CVar 为 r.RayTracing.Nanite.MaxBuiltPrimitivesPerFrame） 动态/蒙皮几何 FRayTracingDynamicGeometryUpdateManager（Renderer/Public/RayTracingDynamicGeometryUpdateManager.h）：AddDynamicGeometryToUpdate 登记需要逐帧更新顶点的几何，AddDynamicGeometryUpdatePass 申请 scratch buffer 并注册更新 Pass；旧的 DispatchUpdates / EndUpdate / Update 已在 5.5、5.6 相继标记弃用，新代码只需调用 AddDynamicGeometryUpdatePass Nanite 链路：UE 5.6 中 Nanite 网格参与硬件光追时，并不把原始高模塞进 BLAS，而是由 Nanite::GRayTracingManager 依据 Nanite 自身的裁剪结果，把\u0026quot;当前可见的集群集合\u0026quot;流式构建为 BLAS——光追场景下的几何裁剪收益因此与光栅化路径保持一致。\n实例侧（TLAS）：场景成员与可见性通道\nTLAS 的输入是\u0026quot;实例列表\u0026quot;，UE 通过一组专用工具对其进行管理：\nEngine/Source/Runtime/Renderer/Private/RayTracing/ RayTracingInstance.{h,cpp} FRayTracingInstance：单个实例 = 几何 + 变换 + 掩码 + 标志位 RayTracingInstanceMask.{h,cpp} 实例掩码：通道可见性控制（主光追 / 阴影 / 天空光分层） RayTracingInstanceCulling.{h,cpp} 实例剔除：每帧仅将\u0026#34;可能被需要的\u0026#34;实例放入 TLAS RayTracingInstanceBufferUtil.cpp 实例数据 -\u0026gt; GPU Buffer 的组装工具 \u0026ldquo;实例掩码\u0026quot;是\u0026quot;阴影光线仅判断遮挡、无需材质计算\u0026quot;这一原则的工程化落地：通过掩码通道，同一份场景能够分别服务于\u0026quot;材质命中求解\u0026rdquo;（Closest Hit）与\u0026quot;遮挡测试\u0026quot;（Any Hit / 阴影光线），避免为阴影路径支付昂贵的材质着色开销。[7]\n4.3 命中与材质：从 HitRecord 到 Shader 绑定表 一句话：软件模型里的\u0026quot;命中记录 + 多态分派\u0026quot;，在 UE 里被拆成三件具体东西：draw 命令集、Hit Shader，以及一张\u0026quot;命中索引 -\u0026gt; 该跑哪个 Shader\u0026quot;的绑定表。\n硬件光追的四个可编程阶段（1.4 节提到的规范侧清单，此处给出职责划分）：\n可编程阶段 在基本光追流程中的角色 Ray Generation（射线生成） 第 1 步：生成视线 / 阴影 / 反射光线，对应 1.2 节的\u0026quot;光线生成\u0026quot; Intersection / Any Hit 求交期间的命中决策（加速结构遍历本身是固定管线），对应 1.2 节的\u0026quot;求交\u0026quot; Closest Hit 找到最近交点后执行，对应第 3 步\u0026quot;着色\u0026quot;的入口 Miss 光线未击中任何物体时执行（返回背景色或采样天空光） 四个阶段里，只有 Closest Hit 与 Any Hit 会随\u0026quot;命中到什么材质\u0026quot;而不同——这正是 UE 需要材质 Hit Shader 与 SBT 的原因。\n第 1.2 节的软件光追模型中，HitRecord（记录命中表面、参数 t、法线）与 Surface::hit 的多态分派是核心抽象[5]。UE 的工程化版本将这一概念拆分为三个独立组件：\n将\u0026quot;表面集合\u0026quot;物化为 GPU 端 Mesh Draw 命令集：RayTracingMeshDrawCommands.cpp 将场景网格转换为光追可用的 draw 命令集合（对应软件模型中\u0026quot;将一组物体视为复合 Surface\u0026quot;的 Group 组织方式[5]）。 将\u0026quot;材质着色程序\u0026quot;物化为 Hit Shader：RayTracingMaterialHitShaders.{h,cpp} 为每个 mesh draw 生成对应的 Closest Hit / Any Hit Shader——其内部通过 UE 统一材质系统完成\u0026quot;命中后颜色计算\u0026quot;。材质管线完全复用 FDeferredShadingSceneRenderer 统一管理的着色流程。[6] 将\u0026quot;命中后分派哪个 Shader\u0026quot;物化为 SBT（Shader Binding Table）：RayTracingShaderBindingTable.{h,cpp} 将\u0026quot;几何 + 实例 + 材质\u0026quot;的笛卡尔积组装为 DXR 规范要求的着色器绑定表，使 GPU 命中时能够通过索引快速定位到正确的 Hit Shader。 软件光追基础模型 UE 工程化实现 Scene 表面集合 FGPUScene / FRayTracingScene（4.1、4.2 节） Surface.hit(ray) BLAS 遍历（固定管线）+ 可编程命中阶段 HitRecord(s, t, n) 命中数据 + RayTracingBarycentrics（重心坐标等） 材质（决定颜色） Closest Hit / Any Hit（RayTracingMaterialHitShaders） 光源 / 光照方向 RayTracingLighting（光照采样与光源网格） 多态分派 Shader Binding Table（命中索引 -\u0026gt; 正确 Shader） 这张表把第 1 章的概念模型与第 4 章的代码实体一一对应。1.4 节给出的职责分界线在这里可以直接落地：几何层（BLAS / TLAS / SBT 索引）决定\u0026quot;光线碰到谁\u0026quot;，SBT 与 Hit Shader 决定\u0026quot;碰到后计算多少光照\u0026quot;。\n4.4 应用 Pass 全景 一句话：这些效果没有一个是\u0026quot;整屏的光追开关\u0026quot;，它们各自挂在主循环里最合适的阶段（光照循环、环境光、AO、半透明），关掉某一个只是少了那一类效果。\nUE 中直接消费通用光追系统的效果以 RayTracing/ 目录下的实现文件为准。每个效果都有两道门：通用门（平台是否支持光追、是否允许该视图用光追）与效果门（ShouldRender* 系列函数，见 RaytracingOptions.h）。逐项梳理如下：\n效果名称 挂载点 / 调用链 效果门与开关 实现说明 Ray Traced Shadows（光追阴影） LightRendering.cpp 在光照循环内逐光源调用 RenderRayTracingShadows（RayTracingShadows.cpp） ShouldRenderRayTracingShadows / ShouldRenderRayTracingShadowsForLight；r.RayTracing.Shadows（默认 0，即仍用传统阴影贴图），另有 r.RayTracing.Shadows.Lights.* 按光源类型细分 非独立全屏 Pass，而是作为阴影渲染的可选模式嵌入逐光源渲染流程；降噪也发生在光照循环内（DenoiseShadowVisibilityMasks） Ray Traced Ambient Occlusion RayTracingAmbientOcclusion.cpp；渲染器侧通过 EAmbientOcclusionMethod::RTAO 枚举接入 ShouldRenderRayTracingAmbientOcclusion；r.RayTracing.AmbientOcclusion（默认 -1，由后处理体积驱动；0 关 / 1 开） 输出与 ScreenSpaceDenoise 结合执行时空降噪 Ray Traced Sky Light（天空光采样） 渲染器在天空光采样阶段调用 RenderRayTracingSkyLight（RaytracingSkylight.cpp） ShouldRenderRayTracingSkyLight；r.RayTracing.SkyLight（默认 0） 以 RT 精确求交取代\u0026quot;反射捕获体 + 低分辨率环境贴图\u0026quot;的近似，额外输出 SkyLightHitDistanceTexture 等缓冲；它不能按视图临时开关（原因见 4.9 节） Ray Traced Translucency（半透明光追） 渲染器在半透明阶段调用 RenderRayTracingTranslucency（RayTracingTranslucency.cpp） ShouldRenderRayTracingTranslucency；r.RayTracing.Translucency（默认 -1，由后处理体积驱动） 由 RenderRayTracingPrimaryRaysView（RayTracingPrimaryRays.cpp）提供\u0026quot;主光线\u0026quot;求交与着色，处理半透明折射等光栅化难以精确表达的效果 配套实现（非独立效果） RayTracingDecals.{h,cpp}、RayTracingBarycentrics.cpp、RayTracingDebug.cpp、RayTracingTraversalStatistics.{h,cpp} 无独立 ShouldRender* 门禁 贴花在光追下的求交支持、重心坐标验证、可视化调试与遍历性能统计 效果门之上还有一层公共判定：ShouldRenderRayTracingEffect(bEffectEnabled, CompatibilityFlags, View) 会先检查 IsRayTracingEnabled(ShaderPlatform) 与 View.IsRayTracingAllowedForView()，再由 CompatibilityFlags 判断该效果能否在当前可用的派发方式上运行——效果必须声明支持\u0026quot;全管线\u0026quot;或\u0026quot;Inline\u0026quot;至少之一，否则直接不执行（详见 4.10 节）。\nflowchart TD MAIN[\"渲染主循环（光栅化为主流程）\"] MAIN --\u003e P1[\"1. 光照循环：RT 阴影（逐光源）LightRendering.cpp\"] MAIN --\u003e P2[\"2. 环境光阶段：RT 天空光（可选）DeferredShadingRenderer.cpp\"] MAIN --\u003e P3[\"3. AO 阶段：RTAORayTracingAmbientOcclusion.cpp\"] MAIN --\u003e P4[\"4. 半透明阶段：RT TranslucencyRayTracingTranslucency.cpp\"] MAIN --\u003e P5[\"5. 合成前：Lumen 硬件 Screen Probe / Radiance Cache / Reflection（见 3.2 节）\"]编号表示这些挂载点在一次渲染中的先后位置（同一阶段内部可能并行）。\n4.5 光追系统与 Lumen 及其他系统的租户关系 一句话：底座只有一套，用它的有好几家；每家在\u0026quot;整理场景\u0026quot;这一步各自提需求，中间不需要谁认识谁。\n本节是理解 UE 光追架构的核心：通用光追场景（第 4.1 节）是\u0026quot;基础设施层\u0026quot;，Lumen 与各类独立效果仅是它的不同\u0026quot;租户\u0026quot;。\nLumen 硬件模式：当 r.Lumen.HardwareRayTracing 开启且平台支持时，Lumen 通过 Lumen::UseHardwareRayTracedSceneLighting（声明于 Lumen/Lumen.h，实现于 Lumen/LumenSceneLighting.cpp）参与调度编排：FDeferredShadingSceneRenderer::Render 中 SetupRayTracingRenderingData 的多处调用点都带 Lumen::UseHardwareRayTracedSceneLighting(ViewFamily) 或 Lumen::UseAsyncCompute(ViewFamily) 条件，即**\u0026ldquo;底座什么时候需要被组装好\u0026quot;这件事是由租户决定的**。Lumen 还向场景构建器传递定制化需求：场景选项的构造函数 FSceneOptions::FSceneOptions(...)（RayTracing/RayTracing.cpp）依次调用 LumenHardwareRayTracing::SetRayTracingSceneOptions(View, DiffuseIndirectMethod, ReflectionsMethod, SceneOptions)（Lumen/LumenHardwareRayTracingCommon.h）、RayTracingShadows::SetRayTracingSceneOptions(View.bHasRayTracingShadows, SceneOptions)，再按半透明与 Path Tracing 的需求补齐——同一份场景选项由多个消费者分别写入，谁需要哪类几何就打开哪个开关，避免为单一用途构建整场景。 Lumen 硬件 Shader 的结构印证：FLumenHardwareRayTracingShaderBase（同文件中定义）是所有 Lumen 硬件 RayGen 的公共基类，其参数列表中同时出现 FNaniteRayTracingUniformParameters（Nanite 场景数据）与 FRayTracingLightGrid（通用光源网格）——这一设计再次印证\u0026quot;Lumen 硬件光追 = 通用光追场景 + 通用光源数据\u0026quot;的复用架构。 Inline 光追的调度：5.6 中的光追并不总是采用\u0026quot;全管线派发\u0026quot;模式。Lumen 可选 Inline 光追（在 Compute Shader 中内嵌 TraceRayInline 调用，开关 r.Lumen.HardwareRayTracing.Inline，默认 1），判定谓词是 Lumen::UseHardwareInlineRayTracing(ViewFamily)；SetupRayTracingRenderingData 在编排时会检测本帧是否存在任何 Inline 光追 Pass。它的代价与收益见 4.10 节。 5.6 新增的 RT 消费者：MegaLights（灯光虚拟化系统，MegaLights/MegaLightsRayTracing.cpp）可选择性使用 Inline 光追评估大规模光源的直接光照；HairStrands（毛发系统）与 HeterogeneousVolumes（异质体积渲染）也通过 IsRayTracingEnabled() 门禁接入通用光追体系（详见各系统实现中的入口判断）。 核心结论：Lumen 决定\u0026quot;需要多少间接光照\u0026rdquo;，通用光追系统决定\u0026quot;光线如何发射、命中如何计算\u0026quot;；5.6 中越来越多的子系统（Lumen、MegaLights、毛发、体积）选择复用这套基础设施，而非各自独立造轮。\n4.6 Path Tracing（参考级完整光追） 第 1.3 节提到，实时光追通过各类近似手段替代了\u0026quot;完整的多弹射积分\u0026quot;。UE 同时保留了\u0026quot;老老实实做积分\u0026quot;的完整参考实现——Path Tracing（Engine/Source/Runtime/Renderer/Private/PathTracing.cpp）。\n它作为可选渲染模式存在（非实时路径默认启用），逐像素逐样本执行完整的多弹射路径追踪直至收敛，输出近乎离线渲染质量的参考图像。 它的核心价值之一是作为正确性验证的参照系：开发与调试光追 / Lumen 效果时，可对照 Path Tracing 的输出判断偏色、泄漏、噪声来源等问题。 它内置独立的时空降噪器（PathTracingSpatialTemporalDenoising.cpp），通过跨帧样本累积压低蒙特卡洛噪声。 它在架构上甚至绕开了实时那套\u0026quot;光栅为主\u0026quot;的主循环（代码注释明确指出 Path Tracing 使用自己的光源管理器，不需要常规的实时光源网格）。值得注意的联动点：当 Nanite BLAS 重建时会触发 ViewState-\u0026gt;PathTracingInvalidate()，因为 Path Tracing 的逐帧样本累积要求几何数据在时间上保持一致。 4.7 每帧数据流时序 将第 4.1-4.5 节的各环节拼接为完整的时序图（以\u0026quot;本帧存在 RT / Lumen 硬件 Pass\u0026quot;为前置条件）：\nsequenceDiagram participant R as FDeferredShadingSceneRenderer::Render participant G as GPU (D3D12/DXR) participant S as FRayTracingScene participant P as RT / Lumen Passes R-\u003e\u003eR: 确定 bRayTracing 与各 ShouldRender* 条件 R-\u003e\u003eR: DispatchRayTracingWorldUpdates Note over R: 收集: 可见实例 / Nanite 可见性 / 动态几何更新请求 R-\u003e\u003eG: 静态 BLAS 延迟构建: ForceBuildIfPending R-\u003e\u003eG: Nanite GRayTracingManager ProcessBuildRequests R-\u003e\u003eG: 动态几何更新 Pass: AddDynamicGeometryUpdatePass R-\u003e\u003eS: Reset(清理上帧残留数据) R-\u003e\u003eS: Create(输入 GPUScene) / Build(构建 TLAS) R-\u003e\u003eR: SetupRayTracingRenderingData(组装各 View 的 Lumen/Nanite/光源缓冲) loop 渲染主循环各阶段 P-\u003e\u003eS: 阴影 / 天空光 / AO / 半透明 RayGen P-\u003e\u003eG: TraceRay / DispatchRays (SBT 索引命中 Shader) P-\u003e\u003eP: 时空降噪 -\u003e 合成到 SceneColor P-\u003e\u003eS: Lumen 硬件 ScreenProbe / RadianceCache / Reflection (若启用) end R-\u003e\u003eS: PostRender(帧末收尾与资源释放)4.8 降噪体系 一句话：实时每像素只发很少的光线，噪声必然存在；UE 备了三套降噪通道，各自匹配不同形态的结果。\n实时光追每像素通常仅发射 1 根（或极少量）光线，蒙特卡洛噪声是固有属性。UE 5.6.1 的降噪体系分为三层：\n通用屏幕空间时空降噪器：ScreenSpaceDenoise.{h,cpp} 定义的 IScreenSpaceDenoiser 是 RT 阴影 / AO / 天空光 / 半透明的统一降噪通道。它按\u0026quot;逐像素信号 + 命中距离 / 掩码\u0026quot;的接口组织输入输出（FShadowVisibilityInputs、FAmbientOcclusionInputs、FReflectionsInputs、FDiffuseIndirectInputs 等），并提供三个运行模式（EMode）：Disabled / DefaultDenoiser（用 IScreenSpaceDenoiser::GetDefaultDenoiser()）/ ThirdPartyDenoiser（用全局 GScreenSpaceDenoiser，可被插件替换）。调用站在各效果自己那边：例如光追阴影的降噪发生在光照循环内（LightRendering.cpp 调用 DenoiseShadowVisibilityMasks），AO 与天空光分别在 RayTracingAmbientOcclusion.cpp、RaytracingSkylight.cpp 内调用对应入口；r.Shadow.Denoiser（默认 2）与 r.AmbientOcclusion.Denoiser（默认 2）这类开关选择的正是上面三种模式（注意后者的名字里没有 RayTracing 前缀）。 Lumen 专用滤波管线：Lumen Screen Probe 的结果重建与缓存复用强耦合，因此不走上面的通用通道，而是自己的滤波管线（LumenScreenProbeFiltering.cpp 等）；它只复用 ScreenSpaceDenoise.h 里的信号纹理结构（FSSDSignalTextures）作为数据容器。 Path Tracing 的独立时空降噪：详见 4.6 节。 噪声问题从反面塑造了架构决策：独立 RT GI/反射（UE5 早期方案）最终被 Lumen 取代的核心原因之一，正是\u0026quot;每像素发射大量光线 + 强力降噪\u0026quot;的成本结构远不如\u0026quot;稀疏光线发射 + 缓存复用\u0026quot;的方案健康（呼应 3.3 节的架构演进分析）。\n4.9 边界条件与工程限制 一句话：光追的可用性由平台能力决定，成本由\u0026quot;进场景的实例规模\u0026quot;决定，而它几乎从不全量开启。\n平台能力面：硬件光追需要 D3D12 + DXR 兼容的 GPU（RHI 层对应 D3D12RayTracing.{h,cpp} 的实现）。Inline 光追、Shader Execution Reordering（SER）等属于更细粒度的平台相关能力，引擎通过 RHISupportsRayTracing* 系列函数在运行时逐能力探测；渲染层的控制台开关（如 r.Lumen.HardwareRayTracing.Inline / ...ShaderExecutionReordering）仅在与硬件能力匹配时才真正生效。 成本构成面：TLAS 每帧重建 + BLAS 增量更新的总代价与可见实例数强相关。RayTracingInstanceCulling（实例剔除）、掩码通道拆分、r.RayTracing.Culling（取值 0-3，分别对应不剔除 / 只剔除相机后方的远近与立体角组合策略）共同目标是把\u0026quot;进入光追场景\u0026quot;的实例规模压到最小。凡是本帧用不到的物体，都应通过剔除机制挡在光追场景之外。可参考的量级：r.Lumen.HardwareRayTracing 的帮助文本明确提示，场景实例超过 10 万时硬件光追的场景更新开销显著。 与 Nanite 的联动面：5.6 中 Nanite 通过 Nanite::GRayTracingManager 参与硬件光追（BLAS 由可见集群流式构建，详见 4.2 节）；Lumen 硬件 Shader 为此提供了 NANITE_RAY_TRACING 的专用 permutation。 一个不能按视图开关的效果：光追天空光要求 FScene 的 base pass 着色器按\u0026quot;是否启用光追天空光\u0026quot;配置，而该配置变化会触发静态网格元素重建（bScenesPrimitivesNeedStaticMeshElementUpdate），无法逐帧或逐视图切换——因此它被单独实现为 ShouldRenderRayTracingSkyLightEffect()，属全局开关，常见于影视级捕获而非游戏运行时（DeferredShadingRenderer.cpp 中的注释说明了这一限制）。 帮助文本与默认值可能不一致：r.RayTracing.Culling 的帮助文本写\u0026quot;0: Culling disabled (default)\u0026quot;，而实际默认参数是 3；r.RayTracing.Shadows.SamplesPerPixel 的默认参数是 -1（表示交给默认值 1）。这类不一致在排查时容易误判，判断应以声明处的默认参数为准。 4.10 派发模式与执行相干性 一句话：同一套光线既可以用独立管线派发，也可以内嵌在普通 compute shader 里；选哪种取决于硬件能力与相干性优化。\n全管线 vs Inline。效果本身要声明自己支持哪种派发方式：ERayTracingPipelineCompatibilityFlags（RaytracingOptions.h）只有两个取值 FullPipeline 与 Inline。门禁函数 ShouldRenderRayTracingEffectInternal（DeferredShadingRenderer.cpp）在此基础上再叠加平台与开关：\n判定 条件 允许全管线 GRHISupportsRayTracingShaders 且 r.RayTracing.AllowPipeline 且效果声明了 FullPipeline 允许 Inline GRHISupportsInlineRayTracing 且 r.RayTracing.AllowInline 且效果声明了 Inline 两者都不允许 该效果直接不执行 也就是说 Inline 光追的一个实际作用，是让效果在\u0026quot;支持 Inline 但不支持完整光追管线\u0026quot;（或反之）的设备上仍能存活。此外 r.RayTracing.ForceAllRayTracingEffects 可以强制开关全部效果，用于验证问题是否出在能力探测上。\nInline 模式有两条具体约束与收益（均来自源码注释）：\n约束：当前的 Inline 实现要求线程组与 wave 一一对应（LumenHardwareRayTracingCommon.cpp 的 GetThreadGroupSizeInternal 注释），因此线程组尺寸不能自由选择。 收益：Lumen 的异步计算路径\u0026quot;只能用于 Inline 光追，或 RHI 支持异步光追派发\u0026quot;（Lumen.cpp）——即 Inline 让硬件光追结果可以更自然地并入 Async Compute 队列。 SER（Shader Execution Reordering）。开关 r.Lumen.HardwareRayTracing.ShaderExecutionReordering 默认 true，帮助文本的口径是：用它改善材质求值的相干性，对使用大量材质的场景可能有性能收益，硬件不支持时该开关无效果——所以\u0026quot;开了没变化\u0026quot;通常不是 bug，而是能力探测未通过（可用 5.3 节的排查路径确认）。\n本章小结：通用光追系统向下管理 FRayTracingScene 每帧生命周期（Reset -\u0026gt; Build -\u0026gt; Pass -\u0026gt; PostRender）、BLAS 三类管理（静态 / Nanite 流式 / 动态蒙皮）与 TLAS 剔除掩码，向上通过 SBT 命中分派服务 RT Pass 清单（阴影 / AO / 天空光 / 半透明等）与多租户（Lumen / MegaLights / 毛发 / 体积），配合三层降噪、两种派发模式与平台能力约束，构成完整的硬件光追基础设施。\n5. 工程视图：要动的开关、常见翻车与排查入口 一句话：这一章不引入新机制，只把前面四章的结论按\u0026quot;谁在什么情况下会用到\u0026quot;重排成三类表——该动哪些开关、哪些现象最常见、出了问题先看哪里。\n光追不是单个开关，而是链条上的若干道门；开关拨了没反应，通常是上游某道门没开。本章默认值均取自 5.6 源码的变量声明处；标 (只读) 的变量只能在启动时设置（ECVF_ReadOnly），运行时用控制台改无效。\n5.1 要动的开关（按角色） （a）美术 / 地编：影响画面表现\n想改变什么 关键项 默认值 说明 是否整体允许光追 r.RayTracing 0，(只读) 总开关只决定\u0026quot;允许不允许\u0026quot;，不代表每类效果都开；需在启动时设置 间接光 / 反射方案 EDiffuseIndirectMethod / EReflectionsMethod Lumen 可回退 SSGI / SSR（3.3 节） 光追阴影 r.RayTracing.Shadows 0 默认仍用阴影贴图；打开后阴影精确，但成本与噪声上升 光追天空光 r.RayTracing.SkyLight 0 不能逐视图开关（4.9 节），面向影视级捕获 光追 AO r.RayTracing.AmbientOcclusion -1（后处理体积驱动） 效果门还会要求 AO 强度大于 0 光追半透明 r.RayTracing.Translucency -1（后处理体积驱动） 默认走光栅化半透明 （b）TA / 技术美术：质量与性能取舍\n想改变什么 关键项 默认值 说明 Lumen 的光线来源 r.Lumen.HardwareRayTracing 1 平台不支持时自动回退软件模式；实例超过 10 万的场景，场景更新开销显著 命中处是否实时着色 r.Lumen.HardwareRayTracing.LightingMode 0 0 走 Surface Cache；1 / 2 在命中点实时算光照，质量升、成本显著升 派发方式 r.Lumen.HardwareRayTracing.Inline 1 Inline 更易并入 Async Compute（4.10 节） 执行相干性 r.Lumen.HardwareRayTracing.ShaderExecutionReordering true 硬件不支持时该开关无效果 进入光追场景的实例规模 r.RayTracing.Culling 3（取值 0-3） 帮助文本把 0 写成 default，与声明处不一致（4.9 节） 阴影 / AO 降噪算法 r.Shadow.Denoiser、r.AmbientOcclusion.Denoiser 均 2 后者名字里没有 RayTracing 前缀，容易搜错（4.8 节） （c）程序：能力门与调试入口\n想做什么 关键项 / 函数 默认值 说明 判断是不是能力探测挡掉了效果 r.RayTracing.ForceAllRayTracingEffects -1 设 0 / 1 强制关 / 开全部光追效果，绕过各效果门（4.10 节） 允许或禁止某种派发方式 r.RayTracing.AllowPipeline、r.RayTracing.AllowInline 均 1 效果必须声明对应的 ERayTracingPipelineCompatibilityFlags 才会执行 观察遍历开销与命中分派 RayTracingTraversalStatistics.{h,cpp}、RayTracingDebug.cpp 无独立门 遍历统计与可视化调试 5.2 常见翻车 现象 最可能的成因 先动哪里 打开光追后画面完全没变化 总开关 r.RayTracing 默认 0 且只读，未在启动时设置；或平台不支持光追 r.RayTracing（启动参数 / 项目设置）-\u0026gt; 4.10 节的能力门 某一类效果没出现 该效果自己的门没开：阴影 / 天空光默认 0，AO 与半透明默认 -1 由后处理体积驱动 5.1 节 (a) 逐项对照默认值 Lumen 看起来不受光追影响 Lumen 默认走软件模式；硬件模式要求平台支持且 r.Lumen.HardwareRayTracing 开启 4.5 节、5.1 节 (b) 开了 SER 性能没有变化 硬件不支持 SER，开关不产生效果 4.10 节 天空光无法按视图或按帧临时切换 该效果是全局开关，切换会触发 base pass 重配置与静态网格元素重建 4.9 节 实例规模变大后帧率骤降 硬件光追的场景更新成本与实例数强相关，超过 10 万实例时显著 4.9 节；检查剔除设置 Nanite 物体在光追里闪烁或缺失 加速结构尚未就绪：Nanite BLAS 受帧预算节流，PendingBuilds 跨帧累积 4.2 节、5.3 节 Path Tracing 画面跳变或重影 BLAS 重建触发 PathTracingInvalidate()，跨帧样本累积被清空 4.6 节 噪声残留 降噪通道或模式选择不合适 4.8 节 5.3 排错谱系 按\u0026quot;现象 -\u0026gt; 第一现场（开关 / 谓词）-\u0026gt; 第二现场（函数）-\u0026gt; 落地文件\u0026quot;的顺序定位：\n现象 第一现场 第二现场 落地文件 光追完全没生效 r.RayTracing、IsRayTracingEnabled()、GRHISupportsRayTracingShaders ShouldRenderRayTracingEffectInternal DeferredShadingRenderer.cpp 某个效果不执行 该效果的 ShouldRender* 与其 CompatibilityFlags ShouldRenderRayTracingEffect RaytracingOptions.h 加速结构没准备好、几何缺失 RayTracingScene.GeometriesToBuild DispatchRayTracingWorldUpdates（五步链，4.1 节） DeferredShadingRenderer.cpp、RayTracingScene.h Nanite 光追几何跨帧闪烁 ScheduledBuilds / PendingBuilds Nanite::GRayTracingManager::ProcessBuildRequests NaniteRayTracing.h 动态物体（蒙皮 / 布料）光追几何不更新 AddDynamicGeometryToUpdate AddDynamicGeometryUpdatePass RayTracingDynamicGeometryUpdateManager.h Lumen 硬件光追没生效 r.Lumen.HardwareRayTracing、Lumen::UseHardwareInlineRayTracing SetupRayTracingRenderingData 的调用条件 LumenSceneLighting.cpp、DeferredShadingRenderer.cpp 命中处光照表现异常 r.Lumen.HardwareRayTracing.LightingMode、...HitLighting.* 族 命中光照开关集中定义处 LumenHardwareRayTracingCommon.cpp 光追天空光泄漏或关不掉 ShouldRenderRayTracingSkyLightEffect() base pass 重配置 bScenesPrimitivesNeedStaticMeshElementUpdate DeferredShadingRenderer.cpp 实例过多导致卡顿 r.RayTracing.Culling RayTracingInstanceCulling RayTracingInstanceCulling.cpp 本章小结：开关分三层（总门 / 效果门 / 能力门），且多处默认值是\u0026quot;关闭\u0026quot;或\u0026quot;交给后处理体积\u0026quot;，因此\u0026quot;没效果\u0026quot;最常见的原因是门没开，而非功能异常；排错沿\u0026quot;开关 -\u0026gt; 谓词 -\u0026gt; 函数 -\u0026gt; 文件\u0026quot;逐级下沉，可覆盖绝大多数\u0026quot;光追不生效\u0026quot;类问题。\n6. 附录 6.1 源码文件索引 正文采用\u0026quot;文件短名\u0026quot;引用，本节提供短名到引擎相对路径的完整映射。\n短名 相对引擎根路径 DeferredShadingRenderer.cpp Engine/Source/Runtime/Renderer/Private/DeferredShadingRenderer.cpp DeferredShadingRenderer.h Engine/Source/Runtime/Renderer/Private/DeferredShadingRenderer.h RayTracingScene.{h,cpp} Engine/Source/Runtime/Renderer/Private/RayTracing/RayTracingScene.{h,cpp} RayTracingInstance*.{h,cpp} Engine/Source/Runtime/Renderer/Private/RayTracing/RayTracingInstance*.{h,cpp} RayTracingInstanceCulling.{h,cpp} Engine/Source/Runtime/Renderer/Private/RayTracing/RayTracingInstanceCulling.{h,cpp} RayTracingInstanceMask.{h,cpp} Engine/Source/Runtime/Renderer/Private/RayTracing/RayTracingInstanceMask.{h,cpp} RayTracingMaterialHitShaders.{h,cpp} Engine/Source/Runtime/Renderer/Private/RayTracing/RayTracingMaterialHitShaders.{h,cpp} RayTracingShaderBindingTable.{h,cpp} Engine/Source/Runtime/Renderer/Private/RayTracing/RayTracingShaderBindingTable.{h,cpp} RayTracingMeshDrawCommands.cpp Engine/Source/Runtime/Renderer/Private/RayTracing/RayTracingMeshDrawCommands.cpp RayTracingShadows.cpp Engine/Source/Runtime/Renderer/Private/RayTracing/RayTracingShadows.cpp RayTracingAmbientOcclusion.cpp Engine/Source/Runtime/Renderer/Private/RayTracing/RayTracingAmbientOcclusion.cpp RayTracingPrimaryRays.cpp Engine/Source/Runtime/Renderer/Private/RayTracing/RayTracingPrimaryRays.cpp RayTracingTranslucency.cpp Engine/Source/Runtime/Renderer/Private/RayTracing/RayTracingTranslucency.cpp RaytracingSkylight.cpp Engine/Source/Runtime/Renderer/Private/RayTracing/RaytracingSkylight.cpp RayTracingDecals.{h,cpp} Engine/Source/Runtime/Renderer/Private/RayTracing/RayTracingDecals.{h,cpp} RayTracingBarycentrics.cpp Engine/Source/Runtime/Renderer/Private/RayTracing/RayTracingBarycentrics.cpp RayTracingDebug.cpp Engine/Source/Runtime/Renderer/Private/RayTracing/RayTracingDebug.cpp RaytracingOptions.h Engine/Source/Runtime/Renderer/Private/RayTracing/RaytracingOptions.h RayTracingDynamicGeometryUpdateManager.cpp Engine/Source/Runtime/Renderer/Private/RayTracing/RayTracingDynamicGeometryUpdateManager.cpp NaniteRayTracing.{h,cpp} Engine/Source/Runtime/Renderer/Private/Nanite/NaniteRayTracing.{h,cpp} LightRendering.cpp Engine/Source/Runtime/Renderer/Private/LightRendering.cpp ScreenSpaceDenoise.{h,cpp} Engine/Source/Runtime/Renderer/Private/ScreenSpaceDenoise.{h,cpp} LumenScreenProbeHardwareRayTracing.cpp Engine/Source/Runtime/Renderer/Private/Lumen/LumenScreenProbeHardwareRayTracing.cpp LumenRadianceCacheHardwareRayTracing.cpp Engine/Source/Runtime/Renderer/Private/Lumen/LumenRadianceCacheHardwareRayTracing.cpp LumenReflectionHardwareRayTracing.cpp Engine/Source/Runtime/Renderer/Private/Lumen/LumenReflectionHardwareRayTracing.cpp LumenHardwareRayTracingCommon.{h,cpp} Engine/Source/Runtime/Renderer/Private/Lumen/LumenHardwareRayTracingCommon.{h,cpp} LumenScreenProbeGather.cpp 等 Engine/Source/Runtime/Renderer/Private/Lumen/（Screen Probe 系列文件） LumenRadianceCache.cpp Engine/Source/Runtime/Renderer/Private/Lumen/LumenRadianceCache.cpp LumenSurfaceCache.cpp Engine/Source/Runtime/Renderer/Private/Lumen/LumenSurfaceCache.cpp LumenMeshCards.cpp Engine/Source/Runtime/Renderer/Private/Lumen/LumenMeshCards.cpp PathTracing.cpp Engine/Source/Runtime/Renderer/Private/PathTracing.cpp PathTracingSpatialTemporalDenoising.cpp Engine/Source/Runtime/Renderer/Private/PathTracingSpatialTemporalDenoising.cpp MegaLightsRayTracing.cpp Engine/Source/Runtime/Renderer/Private/MegaLights/MegaLightsRayTracing.cpp D3D12RayTracing.{h,cpp} Engine/Source/Runtime/D3D12RHI/Private/D3D12RayTracing.{h,cpp} RHIResources.h Engine/Source/Runtime/RHI/Public/RHIResources.h Lumen.h、LumenSceneLighting.cpp Engine/Source/Runtime/Renderer/Private/Lumen/Lumen.h、Engine/Source/Runtime/Renderer/Private/Lumen/LumenSceneLighting.cpp RayTracingGeometryManager.h Engine/Source/Runtime/Engine/Public/Rendering/RayTracingGeometryManager.h RayTracingDynamicGeometryUpdateManager.h Engine/Source/Runtime/Renderer/Public/RayTracingDynamicGeometryUpdateManager.h RayTracingTraversalStatistics.{h,cpp} Engine/Source/Runtime/Renderer/Private/RayTracing/RayTracingTraversalStatistics.{h,cpp} 反向索引（按问题找文件）\n需求 / 问题 首选文件 光追总开关与派发门禁 DeferredShadingRenderer.cpp、RaytracingOptions.h 加速结构生命周期与实例剔除 RayTracingScene.{h,cpp}、RayTracingInstanceCulling.{h,cpp} 材质命中与 SBT 组织 RayTracingMaterialHitShaders.{h,cpp}、RayTracingShaderBindingTable.{h,cpp} Lumen 光线来源与命中光照 LumenHardwareRayTracingCommon.{h,cpp} 降噪统一入口 ScreenSpaceDenoise.{h,cpp} Nanite 光追 BLAS 管理 NaniteRayTracing.{h,cpp} 动态几何顶点更新 RayTracingDynamicGeometryUpdateManager.h 平台能力探测与 DXR 翻译 D3D12RayTracing.{h,cpp}、RHIResources.h 6.2 控制台变量 / 设置速查 变量名 默认值 作用说明 定义出处 r.RayTracing 0（只读） 光追总开关，驱动 bRayTracing 管线状态位 DeferredShadingRenderer.cpp r.RayTracing.AllowPipeline 1 允许全管线光追派发 DeferredShadingRenderer.cpp r.RayTracing.AllowInline 1 允许 Inline 光追派发 DeferredShadingRenderer.cpp r.RayTracing.ForceAllRayTracingEffects -1 强制关 / 开全部光追效果（-1 表示不强制） DeferredShadingRenderer.cpp r.RayTracing.Shadows 0 用光追阴影取代传统阴影贴图 LightRendering.cpp r.RayTracing.AmbientOcclusion -1 光追 AO（-1 由后处理体积驱动） RayTracingAmbientOcclusion.cpp r.RayTracing.AmbientOcclusion.SamplesPerPixel -1 每像素采样数（-1 表示取默认值 1） RayTracingAmbientOcclusion.cpp r.RayTracing.SkyLight 0 光追天空光（全局开关，不可逐视图） RaytracingSkylight.cpp r.RayTracing.Translucency -1 光追半透明（-1 由后处理体积驱动） RayTracingTranslucency.cpp r.RayTracing.Culling 3（取值 0-3） 光追实例剔除策略 RayTracingInstanceCulling.cpp r.RayTracing.Nanite.MaxBuiltPrimitivesPerFrame 8388608（约 8M 三角形） Nanite BLAS 每帧构建预算（节流来源）；同族还有 MaxStagingBufferSizeMB(1024)、BLASScratchSizeMultipleMB(64) NaniteRayTracing.cpp r.RayTracing.Nanite.MaxBlasBuildsPerFrame 不存在 旧名，仅残留于 NaniteRayTracing.h 的注释；实际控制项见上一行（待高版本源码复核） — r.Lumen.HardwareRayTracing 1 Lumen 启用硬件光追（需平台能力支持） LumenHardwareRayTracingCommon.cpp r.Lumen.HardwareRayTracing.Inline 1 Lumen 硬件光追切换为 Inline 派发模式 同上 r.Lumen.HardwareRayTracing.LightingMode 0 命中处光照计算模式（0 走 Surface Cache） 同上 r.Lumen.HardwareRayTracing.HitLighting.DirectLighting 1 命中处直接光是否实时计算 同上 r.Lumen.HardwareRayTracing.HitLighting.ShadowMode 2 命中处阴影模式（0 关 / 1 硬 / 2 软） 同上 r.Lumen.HardwareRayTracing.HitLighting.Skylight 2 命中处天空光（2 表示仅独立 Lumen 反射启用） 同上 r.Lumen.HardwareRayTracing.ShaderExecutionReordering true 启用 SER（需硬件能力支持） 同上 r.Shadow.Denoiser 2 阴影降噪算法模式（0 关 / 1 渲染器默认 / 2 可被插件替换） LightRendering.cpp r.AmbientOcclusion.Denoiser 2 AO 降噪算法模式（名字中无 RayTracing 前缀） RayTracingAmbientOcclusion.cpp 使用提示（易误判项）：一是 r.RayTracing.Culling 的帮助文本写 \u0026ldquo;0: Culling disabled (default)\u0026quot;，而声明处的默认参数是 3；二是 r.RayTracing.AmbientOcclusion.SamplesPerPixel 的默认参数 -1 表示取默认值 1，而非\u0026quot;关闭\u0026rdquo;；三是 r.Shadow.Denoiser 与 r.AmbientOcclusion.Denoiser 的帮助文本都把第 0 项写成 \u0026ldquo;(default)\u0026quot;，而两者的声明默认值都是 2；四是 r.RayTracing.Nanite.MaxBlasBuildsPerFrame 只在 NaniteRayTracing.h 的注释里出现，渲染器源码中没有该 CVar 声明——它已是旧名，实际控制项为 r.RayTracing.Nanite.MaxBuiltPrimitivesPerFrame（默认 8M 三角形），不要把旧名当作可调参数依赖（待高版本源码复核）。控制台变量的语义与默认值一律以声明处为准，帮助文本只作线索。\n6.3 参考 正文中以 [1] 形式标注的引用编号，对应下列条目；全部内容出自《计算机图形学基础（第 5 版）》（Fundamentals of Computer Graphics, 5th ed.）。\n[1]: 第 3 章\u0026quot;光栅图像\u0026rdquo; —— 像素定义、光栅概念与图像的函数抽象。 [2]: 第 4 章\u0026quot;光线追踪\u0026quot;开篇 —— 对象顺序与图像顺序两种渲染组织方式的对比。 [3]: 第 4.1 节\u0026quot;基本光追算法\u0026quot; —— 光线生成、求交、着色三步法的完整流程。 [4]: 第 4.3 节\u0026quot;计算视图光线\u0026quot; —— 视线参数方程 p(t) = e + t(s - e) 与参数 t 的几何语义。 [5]: 第 4.4 节\u0026quot;光线与物体的交点\u0026quot; —— 与一组物体求交的 Group 抽象、t 区间收缩算法与加速结构概念。 [6]: 第 4.5 节\u0026quot;着色 Shading\u0026quot; —— 着色输入参数的完整清单与着色模型在两条渲染路线间的通用性。 [7]: 第 4.5.3 节\u0026quot;阴影\u0026quot; —— 阴影光线的构造方法与 epsilon 偏移的数值处理。 [8]: 第 4.5.1 节\u0026quot;光源\u0026quot; —— 点光源 / 方向光源 / 环境光的数学表达与着色所需向量。\n补充说明：以上 8 条引用均借用《计算机图形学基础》第 5 版的概念口径，作为本文术语定义的统一基准。本文范围之外的相邻主题（渲染方程与蒙特卡洛路径追踪的数学基础、DXR 加速结构构建的底层算法、降噪器的具体滤波核设计）不在本篇展开。\nUE 5.6 的光追以图像顺序算法为基础，经 RDG 与 FGPUScene 接入渲染主循环，上层由 Lumen 负责 GI/反射的多级缓存软/硬件双模式求解，底层由通用光追系统管理 BLAS/TLAS/SBT 与多租户 RT Pass，硬件光线最终沿 TLAS -\u0026gt; BLAS -\u0026gt; SBT -\u0026gt; Hit Shader 完成闭环。\n","date":"2026-09-28T00:00:00Z","permalink":"/docs/unreal/ue-raytracing/","title":"UE 光线追踪：概念、Render 模块与系统组成"},{"content":" 这篇讲 UE 5.6 里的毛发系统（Groom）。它分成两半：前半 回答\u0026quot;头发这东西到底怎么变成一个能算的东西\u0026quot;，尽量不假设你懂图形学或物理；后半 回答\u0026quot;UE 具体把它写成了哪些代码、调哪些参数\u0026quot;，这部分会大量出现类名和函数名，看不懂可以先跳过，不影响读懂前半。\n前言 游戏里的头发，常见的做法是贴图造假：在头皮上贴几片带头发图案的透明卡片，看起来像有头发，但它不是一根一根的。UE 的 Groom 是另一条路——它把每一根头发当成一根真的细线来算，所以它能做到真的甩动、互相碰撞、透光。\n代价就是：贵。贵到需要一整套专门的流程，而这篇就是把这套流程从\u0026quot;它是什么\u0026quot;讲到\u0026quot;它怎么算出来的\u0026quot;。\n太长不看（TL;DR） 给没时间通读的你，三句话先记住：\n是什么：Groom 把每一根头发当成一根会动的细线逐根算（真发丝），而不是在头皮上贴带头发图案的卡片（发片）；换来的是真的甩动、碰撞、透光，代价是贵、需要专门管线。 上手只盯三个层级：资产定基线 → 组件覆盖（总开关不开就不生效）→ 求解器组件 / CVar 最外层钳制。想调某个效果，先查\u0026quot;改哪里才算数\u0026quot;的参数地图。 最容易\u0026quot;没反应\u0026quot;：三条驱动路（物理求解 / Deformer / 蒙皮绑定）或插值数据不满足时会静默失效——不报错、停在默认姿态。看到\u0026quot;毛发不动\u0026quot;，先查这三条路的条件，而不是到处找 bug（见5.2 节）。 阅读说明（版本 / 读者 / 结构）：本文以 UE 5.6 的 HairStrands 插件与渲染器源码为基准。默认读者是完全没接触过毛发系统的开发者或者美术创作者：前半（第 04 章）只讲物理原理，不依赖任何引擎知识；下篇（第 511 章）讲 UE 具体实现与上手，其中标注\u0026quot;源码细节，可跳过\u0026ldquo;的部分，看不懂类名不影响理解。想直接上手，跳到第 9 章；想先看全文骨架，看下面的目录。\n名词解释 文中会出现一批固定叫法。先在这里各给一句定义，读到生词可以回来查：\n术语 英文 解释 节点（珠子） Node / point 头发上被采样的一个点，记位置与朝向，是求解时最小的更新单位。 边（绳子） Edge / segment 相邻两个节点之间的一段，记长度、方向与相对旋转；三类形变约束都作用在它上面。 控制线 Guides 承载形变与模拟的稀疏曲线，是模拟和绑定实际作用的对象，数量少。 可见发丝 Strands 由控制线插值展开、最终画出来的发丝，数量远多于控制线。 发组 Hair Group 资产里配置的最小单元；同组的各项设置按组索引一一对齐。 模拟空间 Simulation space 求解采用的参考空间（通常是某根骨骼的局部空间），头发在其中被计算。 朝向 Orientation / frame 附在节点上的一组参考方向（或用四元数表示），用来区分\u0026quot;弯曲\u0026quot;与\u0026quot;整体旋转\u0026rdquo;，并让扭转可定义。 约束 Constraint 一条\u0026quot;满足时为零\u0026quot;的几何规则（如距离不变、夹角不变）；求解器按梯度把违反量逐次拉回。 柔度 Compliance 约束的软硬程度；值越大越软，每次修正推得越少。 拉伸 / 弯曲 / 扭转 Stretch / Bend / Twist 一根发丝仅有的三种形变，刚度数量级相差很大，必须分别建模。 子步 Substep 把一帧时间切成几段分别求解；影响大位移与高速段是否跟手。 迭代 Iteration 每段内把约束反复求解的次数；影响头发\u0026quot;软还是硬\u0026quot;。 体素化 Voxelization 把毛发密度写进三维网格，用于近似自碰撞，或供间接光与阴影使用。 深阴影 Deep Shadow（DOM） 沿光线方向分层记录透射量的阴影表示，用来表现\u0026quot;一大把头发会透光\u0026quot;。 曲线页 Curve page 流送与显存分配的最小粒度，按固定曲线数对齐。 发卡 Card 用一片带头发图案的纹理近似一簇发丝的画法。 平行移动框架 Parallel transport frame 用相邻切线之间的最小旋转逐节点传播朝向，避免沿头发累积出虚假扭转。 Darboux 向量 Darboux vector 描述相邻两朝向之间相对旋转的向量，同时携带弯曲与扭转。 双覆盖 Double cover 四元数 q 与 -q 表示同一旋转，取解时需取一致的一个，否则会在符号翻转处产生假弯曲。 位置动力学 PBD / XPBD 用\u0026quot;约束投影\u0026quot;而非弹簧力求解的范式，UE 的求解器基于它。 Cosserat 杆 Cosserat rod 用朝向（而非只有位置）描述细杆的力学模型，能同时表达弯曲与扭转。 目录（TOC） 第 0 章 入门：头发难在哪 第 1 章 把一根头发变成能算的东西 第 2 章 让头发动起来：不\u0026quot;用力\u0026quot;，而是\u0026quot;拉回去\u0026quot; 第 3 章 头发怎么和世界打交道 第 4 章 每一帧到底发生什么 下篇 · UE 是怎么把它实现的（第 5~9 章） 第 5 章 系统组成：UE 把它拆成了几块 第 6 章 数据从哪来：Groom 资产 第 7 章 怎么算：求解实现 第 8 章 怎么画：这么细的东西怎么显示 第 9 章 上手：改哪里、会出什么问题、多贵 第 10 章 和别的方案比：Groom 与 gFur 第 11 章 附录 第 0 章 入门：头发难在哪 一句话：头发难做，是因为它\u0026quot;又多、又细、又软\u0026quot;；UE 的选择是老老实实一根一根算，再用一堆工程手段把成本压下来。\n0.1 头发为什么难做 把头发和普通物体（比如一个箱子）比一下，难点有三个：\n多。一头长发动辄十几万根。箱子只有一个，头发有十几万个\u0026quot;独立的东西\u0026quot;都是软的。 细。一根头发比屏幕上的一个像素还细。也就是说，用普通的\u0026quot;画三角形\u0026quot;的方式，很多头发根本挤不进一个像素里，会互相盖住、闪烁。 软。它不是刚体，每一根会甩、会相互纠缠、会被风带着动、会透光（不然一大把头发会像一堵黑墙）。 这三点决定了：头发既不能用\u0026quot;画普通模型\u0026quot;的办法算，也不能用\u0026quot;算一个刚体\u0026quot;的办法算，需要一套专门的东西。\n0.2 常见的两种做法 做法 思路 优点 缺点 发片 / 发卡 在头皮上贴几片带头发图案的透明卡片 便宜、能大量用 不是真头发，动起来假，近看穿帮 真发丝（UE Groom 走的这条） 每根头发当成一根会动的细线，逐根算 真实、能甩能碰撞 贵，需要专门管线 UE 的 Groom 属于后者。它并不排斥前者：同一条头发也可以按远近切换成\u0026quot;发卡\u0026quot;，甚至用一片纹理来画（后面第 8 章讲）。\n0.3 一根头发在引擎里长什么样 要\u0026quot;算\u0026quot;头发，第一步是把它变成能存、能改的东西。做法是：把一根连续的长头发，切成有限的一小段一小段，每一段的接头处记两件事——位置和朝向。\nflowchart LR A[\"真实的一根头发连续、无数个点\"] --\u003e|切成有限段| B[\"一串'珠子'每颗珠子 = 位置 + 朝向\"] B --\u003e C(\"珠子0 发根钉在头皮上\") C --- D(\"珠子1\") D --- E(\"珠子2\") E -.-\u003e F(\"...\") F --- G(\"珠子N 发梢自由\")可以把一根头发想象成一根串珠手链：\n珠子 = 头发上的采样点，它记住自己\u0026quot;在哪\u0026quot;（位置）。 珠子之间的绳子 = 一段头发，它记住相邻两颗珠子的\u0026quot;距离、方向、相对角度\u0026quot;。 每颗珠子上还别着一个小箭头（朝向），用来记录\u0026quot;这段头发是扭着的还是没扭\u0026quot;。 发根那颗珠子被钉在头皮上（跟着头动），发梢那颗珠子自由（可以乱飞）。头发怎么动，就变成了\u0026quot;这串珠子怎么动\u0026quot;。\n0.4 整条流水线：三步 从\u0026quot;美术做好的头发\u0026quot;到\u0026quot;屏幕上会动的头发\u0026quot;，一共就三步：\nflowchart LR subgraph S1[\"第一步：准备数据\"] A[\"美术软件里做的头发导成 Groom 资产\"] end subgraph S2[\"第二步：每帧算它怎么动\"] B[\"把每根头发算一遍受力 -\u003e 变形 -\u003e 碰撞\"] end subgraph S3[\"第三步：画出来\"] C[\"把算完的头发光照、上阴影、合成到画面\"] end S1 --\u003e S2 --\u003e S3 第一步产出的是\u0026quot;静止姿态\u0026quot;的头发（还没受力时的样子），它决定了后面所有计算\u0026quot;往哪儿弹回去\u0026quot;。 第二步是这篇前半（第 1~4 章）的主题：一根头发怎么算。 第三步在第 8 章详述：这么细的东西怎么画。 本章小结：头发难在\u0026quot;多、细、软\u0026quot;，所以不能当普通模型画，也不能当刚体算。UE 的做法是把每根头发切成有限段，每段接头处记\u0026quot;位置 + 朝向\u0026quot;，于是\u0026quot;头发怎么动\u0026quot;被转化为\u0026quot;这串珠子怎么动\u0026quot;。整条流水线只有三步：准备数据、每帧计算、画出来。\n第 1 章 把一根头发变成能算的东西 一句话：一根头发只会三种变形（拉伸、弯曲、扭转）；要算它，就得把它切成有限段、给每颗珠子补上一个\u0026quot;朝向\u0026quot;，再配上\u0026quot;重量\u0026quot;。\n1.1 头发只会三种变形 不管你多用力扯头发，它只会发生三件事：\n变形 白话 有多难发生 对观感的影响 拉伸（沿长度方向拉长 / 压短） 把头发当橡皮筋拉 几乎拉不动（头发几乎不可压缩） 拉长了就露馅，所以规则要定得\u0026quot;很硬\u0026quot; 弯曲（横着折） 把头发弯成一个弧形 很容易，随便一甩就弯 决定头发是蓬松还是贴头 扭转（绕着自身的轴拧） 拧毛巾 很难看出来，但影响很大 决定头发弯完之后\u0026quot;往哪边弯\u0026quot;，从而改变高光走向 关键点：这三件事的\u0026quot;软硬程度\u0026quot;差了非常多个数量级。头发几乎拉不动（拉伸很硬），却很容易弯（弯曲很软）。所以\u0026quot;用一根弹簧来代表头发\u0026quot;是不行的——那根弹簧要么被拉长（违反\u0026quot;拉不动\u0026quot;），要么硬得像钢丝（弯不起来）。\n正确做法是分开建模：拉伸、弯曲、扭转各算各的。[7]\n1.2 把头发切成有限的段 真实的头发是一条连续曲线，上面有无数个点，计算机没法一个个处理。必须把它切成有限段。[1]\nflowchart LR subgraph C[\"连续曲线（无穷多个点）\"] X[\"从头到尾连续\"] end subgraph D[\"离散成 N+1 颗珠子\"] P0((\"珠子0发根\")) --- P1((\"珠子1\")) P1 --- P2((\"珠子2\")) P2 -.-\u003e PN((\"珠子N发梢\")) end C --\u003e|每隔一小段取一个点| D切完之后，一根头发由三样东西描述：\n珠子（节点）：记位置和朝向，是\u0026quot;可以被改\u0026quot;的最小单位。 绳子（边）：记相邻两颗珠子之间的距离、方向、相对角度，三种变形规则都作用在它上面。 静止姿态（rest pose）：还没受力时的样子，是所有规则\u0026quot;弹回去\u0026quot;的目标——就像橡皮筋松手后要回到原长。 代价：段数决定精度。段切得少，头发就表达不出细腻的层次（看起来一根根是直的）；段切得多，效果好但更贵。这个\u0026quot;每根头发切几段\u0026quot;是后面最常调的旋钮之一。\n1.3 为什么每颗珠子还必须记\u0026quot;朝向\u0026quot; 只记位置不够。举个反例：这两种情况仅靠位置无法区分：\n只看两端位置，这两件事长得一模一样： 弯了： o o o （中间那颗偏离了直线） 转了： o o o （整根被扳到了另一个方向） 两者都表现为\u0026#34;端点位置变了\u0026#34;。 如果只有位置，\u0026ldquo;这根头发弯了\u0026quot;和\u0026quot;整根头发被转了个角度\u0026quot;分不出来。 一旦每颗珠子带上\u0026quot;朝向\u0026rdquo;（那个小箭头），两件事就能区分了：弯是\u0026quot;箭头和绳子不一致\u0026quot;，转是\u0026quot;箭头自己也一起转了、和绳子仍然一致\u0026quot;。 正因为有了朝向，弯曲和扭转才有可定义的对象。[7]\n1.4 朝向怎么沿着头发一路传下去 这里有个陷阱。假设你从头到尾给每颗珠子都\u0026quot;凭切线随便定一个朝向\u0026quot;——朝向里包含一个\u0026quot;绕头发自身转了多少\u0026quot;的分量，这个分量是自由的、没有唯一答案。结果就是：朝向会沿着头发一路越拧越麻花，最后算出来的头发是\u0026quot;扭成麻花\u0026quot;的。\n解决办法叫平行移动框架：每次只按\u0026quot;相邻两段之间最小的那个旋转\u0026quot;去更新朝向，绝不额外引入绕自身的转动，于是不会累积出虚假的扭转。[5]\n它有两种用法：\n一开始初始化时：必须从头（发根）到尾（发梢）一根一根顺着传，保证整根头发上的朝向是连贯的。 每帧计算时：只按相邻珠子局部修正，这样才可以并行（每颗珠子各算各的，不用等别人）。 1.5 每颗珠子有多重 最后一件事：给每颗珠子配上\u0026quot;重量\u0026quot;和\u0026quot;转动惯量\u0026quot;（可以理解成\u0026quot;转起来有多费劲\u0026quot;）。\n为什么需要？因为规则被违反时，要决定\u0026quot;谁动多少\u0026quot;。直觉是：轻的动得多，重的动得少。发根那颗珠子通常\u0026quot;很重\u0026quot;（几乎不动），发梢那颗\u0026quot;很轻\u0026quot;（到处乱飞）。引擎用的正是这个直觉——按\u0026quot;重量/转动惯量的倒数\u0026quot;来分配修正量。\n注意有两套，别混：\n管位置的规则，看重量（质量）。 管朝向的规则，看转动惯量。 在 UE 里，这两套量由数据接口的 ComputeNodeMass 与 ComputeNodeInertia 提供（第 7 章细讲）。\n本章小结：一根头发只会拉伸、弯曲、扭转三种变形，而且三者软硬差距极大，必须分开算。要能算，就把它切成有限段：珠子记位置和朝向，绳子记两颗珠子之间的关系，静止姿态是弹回的目标。朝向是必须的，否则\u0026quot;弯了\u0026quot;和\u0026quot;整根转了\u0026quot;无法区分；朝向的传播要用平行移动框架，避免沿头发累积出虚假的扭转（初始化时串行传、每帧并行局部修正）。最后给珠子配上重量和转动惯量，让规则被违反时按\u0026quot;谁轻谁多动\u0026quot;来分配修正。\n第 2 章 让头发动起来：不\u0026quot;用力\u0026quot;，而是\u0026quot;拉回去\u0026quot; 一句话：与其给珠子加力（容易算炸），不如先规定\u0026quot;头发本来应该是什么样\u0026quot;，每帧把偏离的地方按比例拉回去。这就是 UE 求解器全部规则的共同做法。\n2.1 为什么不直接\u0026quot;加力\u0026quot; 最直觉的想法是：把每段头发当成一根弹簧，受力就伸长/弯曲，算出力再让珠子动。\n但弹簧有个麻烦：\u0026ldquo;有多硬\u0026quot;和\u0026quot;每帧算多长\u0026quot;是绑死的。弹簧越硬，就越需要把每帧切得非常非常小来算，否则会算飞（数值发散）。而头发恰恰需要\u0026quot;拉伸极硬、弯曲极软\u0026rdquo;——硬的那部分会把整根头发逼到用极小步长，成本爆炸。[3]\n2.2 换个思路：写成\u0026quot;规则\u0026quot;，再把违反的地方拉回去 换个问法：不问\u0026quot;力有多大\u0026quot;，而问\u0026quot;本来应该是什么样，现在差了多少，把差的那部分补回去\u0026quot;。\n这就是约束的思路。一条约束就是一句\u0026quot;应该满足的话\u0026quot;，比如：\n\u0026#34;这两颗珠子之间的距离，应该等于静止时的距离。\u0026#34; 如果现在被拉长了 3 厘米，那就把这两颗珠子往回推一点，让距离缩短一些——不是一次推到位，而是每次推掉一部分。一帧里把这条规则重复推很多遍（叫\u0026quot;迭代\u0026quot;），结果就越来越接近满足。[3]\n这种\u0026quot;先把位置预测出来、再按规则反复拉回去\u0026quot;的做法，英文叫 PBD（位置动力学）；UE 用的是它的改进版 XPBD。\n2.3 一条规则上要写清楚的三件事 要写清楚的 白话 影响 规则是什么 \u0026ldquo;距离应该等于静止长度\u0026rdquo; 决定头发长什么样 有多软 叫柔度（compliance）：软的就少推点，硬的就多推点 决定头发是硬挺还是绵软 影响谁 由参与珠子的\u0026quot;重量/转动惯量的倒数\u0026quot;决定 轻的动得多，重的动得少 其中\u0026quot;有多软\u0026quot;这一项，UE 会先把它除以\u0026quot;每步时长的平方\u0026quot;再用。为什么要这么绕？为了让同一套参数在不同帧率下含义一致——不想出现\u0026quot;帧率越高头发越硬\u0026quot;这种事。[4]\n但要澄清一个常见误解：这样做并不代表\u0026quot;结果和帧率无关\u0026quot;。因为每帧迭代次数是固定的，步长变大时，同样的迭代次数收敛得更不充分，头发还是会变软、变滞后（第 4 章展开）。\n2.4 拉伸、弯曲、扭转其实是同一件事 回到第 1 章那三种变形。实现上它们是同一个公式，区别只在\u0026quot;这条规则管几颗珠子\u0026quot;：\n规则 管几颗珠子 规则在说 拉伸 相邻两颗 你俩的距离别变 弯曲 隔一颗，管三颗 你这个弯的幅度别超 扭转 隔两颗，管四颗 你俩的\u0026quot;箭头\u0026quot;别相对拧太多 珠子越\u0026quot;轻\u0026quot;、规则越\u0026quot;软\u0026quot;，这条规则被违反时就越容易拉回来。UE 里这是同一个函数用不同参数（NodeOffset）实现的，第 7 章细讲。[7]\n2.5 \u0026ldquo;箭头\u0026quot;的两种记法 因为有了朝向（箭头），\u0026ldquo;弯曲\u0026quot;就有两种写法：\n写法 A（看方向）：把绳子方向拉回\u0026quot;父珠子的箭头转过静止角度后的方向\u0026rdquo;。简单、稳，弹簧一族用这种。 写法 B（看相对旋转）：直接规定\u0026quot;相邻两颗珠子的箭头之间应该转多少\u0026rdquo;。它一次就把弯曲和扭转都管住了，是\u0026quot;杆\u0026quot;一族的用法；麻烦在于：一个旋转用两个相反的四个数字（四元数）都能表示，取解时要挑对，否则会在某个角度突然翻车（俗称\u0026quot;双覆盖\u0026quot;问题）。[6][7] UE 两套求解路线正好对应这两种（弹簧族 vs 杆族），第 7 章细讲。\n2.6 阻尼：让它别抖 阻尼（damping）像\u0026quot;泡在水里\u0026quot;的阻力。它不改变头发最终该停在哪，只影响\u0026quot;多快停下来\u0026quot;。\n阻尼太小：头发来回弹、抖。 阻尼太大：头发变得迟钝、像泡在糖浆里。 对毛发来说，合适的阻尼是\u0026quot;看着不抖、也不像泡酱里\u0026quot;——这取决于迭代次数和子步数（第 4 章）。\n2.7 发根硬、发梢软 真实头发是渐变的：发根粗硬、发梢细软。实现里不是用一个常数，而是沿头发逐点采样成一张表——从头到尾，刚度/摩擦/粗细都按位置给不同值。\n这张表的密度 = 每根头发切了几段。段数不够时，渐变会变成一格一格的阶梯——这就是\u0026quot;怎么调都不够自然\u0026quot;时最该看的旋钮。\n本章小结：直接给头发加弹簧力不可行，因为\u0026quot;刚度\u0026quot;和\u0026quot;步长\u0026quot;绑在一起，硬的部分会把步长逼到极小。UE 改用\u0026quot;约束\u0026quot;：先规定头发应该满足的规则，每帧把违反的部分按比例拉回去，迭代多次逼近。一条规则要写清楚\u0026quot;规则是什么、有多软（柔度）、影响谁（重量/惯量）\u0026quot;。拉伸、弯曲、扭转其实是同一个公式，区别只在管几颗珠子；朝向有两种记法（看方向的、看相对旋转的），后者同时携带弯曲与扭转但要处理\u0026quot;四元数二义\u0026quot;。阻尼不改变终点，只影响多快稳住；刚度参数沿头发渐变，由\u0026quot;切几段\u0026quot;决定其分辨率。\n第 3 章 头发怎么和世界打交道 一句话：头发会被角色\u0026quot;甩\u0026quot;起来，是因为它是在跟着角色一起动的坐标系里被算的；而碰撞和风都用了更省事、但有代价的近似。\n3.1 撞到身体 头发撞到头皮/身体时，要处理两件事：\n别穿进去：一旦珠子跑到身体里面，就把它推出来。 擦过去的摩擦：贴着身体滑动时，按摩擦决定\u0026quot;黏住不动\u0026quot;还是\u0026quot;滑过去\u0026quot;。 实现上有两种：硬约束（直接推出去，简单但可能抖）和软约束（允许暂时穿一点点，换来更顺滑，代价是需要多迭代几下）。UE 两种都有（第 7 章）。[6]\n3.2 头发和头发之间 十万根头发两两判断\u0026quot;撞没撞\u0026quot;是不可能的（组合数太大）。省事的做法是体素化：把所有头发的密度灌进一个三维网格（就像把头发塞进一堆小格子里），格子之间互相\u0026quot;排斥\u0026quot;，近似出头发互相推开的效果。\n代价要说清楚：这种做法不产生具体的接触点和接触法线，所以它没有\u0026quot;逐点的摩擦\u0026quot;这回事；能分辨的最小间距由网格粗细决定——太粗会粘成一坨，太细又贵。UE 用一个速度网格 + 一个压力网格来做（第 7 章）。\n3.3 风 把每一小段头发当成一根很细的圆柱，风对它的阻力是两项相加：\n阻力 = 与速度成正比的部分（慢速时主导） + 与速度平方成正比的部分（快速时主导） 迎风面的大小，取决于\u0026quot;风的方向和头发方向的夹角\u0026quot;——正对着风时最粗，顺着风时最细。这也是为什么\u0026quot;甩得越快，阻力增长得越不成比例\u0026quot;：快速时是平方项在起作用。\n3.4 角色动，头发为什么会被甩 头发是在一个跟着角色一起动的坐标系里算的。于是角色本身的速度、转动、加速，就变成了作用在头发上的一股\u0026quot;假力\u0026quot;（就像公交车急刹车时你会往前倾）。\n好处：头发默认跟着角色走，不会因为角色一停就整体\u0026quot;留在原地\u0026quot;。 前提：角色每帧的加速必须被正确地喂进去，否则头发会莫名其妙往某个方向飘。 要注意：喂进去的是每帧算出来的速度（用相邻两帧位置相减得到），不是动画师设计的速度曲线——所以它和帧率有关（第 4 章）。UE 里由组件的\u0026quot;线/角速度比例\u0026quot;和\u0026quot;瞬移阈值\u0026quot;控制（第 7 章）。\n3.5 为什么一模拟头发就往下掉 美术摆好的头发姿态，通常不是\u0026quot;刚好被重力托住\u0026quot;的姿态。你一开模拟，重力立刻把它往下拽，头发就塌下来了，和美术想要的不一样。\n常见解法：开模拟前，先让重力和头发自己达成一次平衡，得到一个\u0026quot;自然下垂\u0026quot;的姿态；然后把它和美术原始姿态按一个系数混合，当作新的\u0026quot;静止姿态\u0026quot;。说白了，就是把\u0026quot;往下掉\u0026quot;这件事本身定义成新的静止样子。UE 里对应\u0026quot;重力预载\u0026quot;（第 7 章）。\n本章小结：头发是在跟随角色的坐标系里被算的，所以角色的运动变成一股\u0026quot;惯性假力\u0026quot;，头发才会被甩起来；喂进去的是逐帧差分的速度，因此与帧率相关。碰撞分两条：和身体碰撞有\u0026quot;推出 + 摩擦\u0026quot;，头发之间只用体素化近似排斥——不产生接触点和法线，也就没有逐点摩擦，能分辨的间距由网格粗细决定。风把每小段头发当细圆柱，阻力是\u0026quot;线性项 + 平方项\u0026quot;，迎风面积随风与头发的夹角变化。静止姿态与重力天然矛盾，解法是先解一次重力平衡，再和原始姿态按系数混合，把\u0026quot;下垂\u0026quot;定义成新的静止姿态。\n第 4 章 每一帧到底发生什么 一句话：一帧 = 先按力把珠子推一下 + 反复把规则拉回去 + 收尾；\u0026ldquo;换个帧率，头发手感就变了\u0026quot;的根子在\u0026quot;每步多长\u0026rdquo;，不在迭代本身。\n4.1 一帧的三段 flowchart TD A[\"这一帧结束了多少时间（delta）\"] --\u003e B[\"第一段：让珠子按力自由动一下（先算速度，再算位置，叫'预测'）\"] B --\u003e C[\"第二段：把违反的规则一条条拉回去重复很多遍（迭代）\"] C --\u003e D[\"第三段：收尾只修正当前珠子，把最后一点误差抹平\"] D --\u003e E[\"把珠子的位置/朝向交给渲染，画出来\"]三段各司其职：第一段负责\u0026quot;顺着力动\u0026quot;，第二段负责\u0026quot;别违反规则\u0026quot;，第三段负责\u0026quot;收干净\u0026quot;。第 7 章会看到 UE 里这三段分别对应哪些函数。[3]\n4.2 两个不同的旋钮：子步 与 迭代 它们常常被混为一谈，其实管的是两件不同的事：\n旋钮 白话 主要影响 成本 子步数 把这一帧切成几段分别算 甩得厉害、动得快时准不准（跟不跟手） 与段数成正比 迭代数 每段里把规则反复解几遍 头发\u0026quot;软还是硬\u0026quot;（规则拉得牢不牢） 与遍数成正比 一句话：子步管时间切多细，迭代管规则解多透。少子步会在快速甩动时\u0026quot;跟不上、穿透\u0026quot;；少迭代会让头发整体发软、拉不住。\n4.3 为什么换个帧率，手感就变了 这里要分清三个时间量：\n一帧多长（delta）：机器跑得快就短，卡一下就长。 每帧算一次：求解器每帧被调用一次，拿到的就是这一帧的 delta。 每一步多长：delta 会被一个上限拦一下（太长就截断），再按子步数平均分。 由此得到三个结论：\n超长帧会被截断：比如切关卡卡了半秒，求解不会让头发\u0026quot;瞬移半秒\u0026quot;，而是让这一帧的模拟变慢。表现是\u0026quot;头发跟不上角色\u0026quot;——这是为了稳定付出的代价。 改帧率 = 改刚度：因为每步长度直接来自 delta，帧率高时步长小、收敛充分（靠近设定的硬度）；帧率低时步长大、收敛不足（更软更滞后）。这就是\u0026quot;同一份头发在不同机器上手感不一样\u0026quot;的来源。 按\u0026quot;帧数\u0026quot;计数的东西会随帧率变：比如重置后有一段\u0026quot;启动缓冲\u0026quot;，它是按帧数算的，60 帧下这段真实时长就是 30 帧下的一半。 成熟求解器的通常做法是两层补偿：位移大的帧自动多切几段（动态子步）+ 迭代数跟着步长放大，让等效手感不随帧率漂。UE 提供了 r.HairStrands.EnableAdaptiveSubsteps 等开关，但插件里没有做固定步长（第 7 章）。[4]\n4.4 现象对参数 这张表是全篇\u0026quot;调参直觉\u0026quot;的来源：\n你看到的 大概是哪里的事 先动哪个 整体发软、拉不住 迭代不够，或规则太软 迭代数、刚度/缩放曲线 抖、高频颤 子步不够、阻尼太小、步子太大 子步数、阻尼 穿模（穿过身体/自己） 碰撞没开，或自碰撞网格太粗 碰撞开关、网格粗细 像塑料、不蓬松 弯曲太硬，或沿头发曲线太平 弯曲刚度、弯曲缩放曲线 高速甩动跟不上 子步不够，或 delta 被截断 子步数、帧率 4.5 为什么这类计算适合显卡并行 规则之间会互相牵扯（同一颗珠子被好几条规则用到），所以不能无脑并行。做法叫分组着色：把\u0026quot;不抢同一颗珠子\u0026quot;的规则放进同一组，组里可以同时算，组与组之间排队。\n一条规则管几颗珠子，就能分成几组——比如管相邻两颗的（拉伸）按 x % 2 交替，管三颗的（弯曲）按 (x / 2) % 2 交替，管四颗的（扭转）按 (x / 3) % 2 交替。这三个式子的取值都只有 0/1，所以三种约束实际都是两色（红/黑）交替，差别只在取模步长；经典红黑着色就是前者的特例。UE 的实现与此一致（第 7 章）。\n还要记住一点：分组的顺序会影响结果——这就是\u0026quot;同样的参数，不同求解器结果略有差异\u0026quot;的原因之一。\n本章小结：一帧分三段——按力预测、反复拉回规则、收尾修正。子步与迭代是两个不同的旋钮：子步把一帧切成几段（管大位移准不准），迭代把规则反复解几遍（管软硬）。手感随帧率变化的根子在\u0026quot;每步多长\u0026quot;：delta 先被上限截断、再按子步均分，所以低帧率会让模拟变慢变软，\u0026ldquo;改帧率等于改刚度\u0026rdquo;；按帧计数的机制（如重置后的启动窗口）也会随帧率改变真实时长。规则之间互相牵扯，需要按\u0026quot;管几颗珠子\u0026quot;做分组着色，组内并行、组间排队，而且分组顺序本身影响结果。\n下篇 · UE 是怎么把它实现的（第 5~11 章） 下篇把前 4 章的每一条道理，对应到 UE 的具体类名 / 函数名 / 资产字段。看不懂类名没关系：每章开头都有一句\u0026quot;白话导读\u0026quot;和一个\u0026quot;一句话\u0026quot;结论；标注\u0026quot;源码细节，可跳过\u0026ldquo;的部分跳过去也不影响理解。真正要动手的人重点看第 9 章（上手地图）和第 11 章（控制台速查）。\n第 5 章 系统组成：UE 把它拆成了几块 一句话：UE 把这件事分成\u0026quot;准备数据、计算、画出来\u0026quot;三块，谁管哪份数据分得很清楚；其中\u0026quot;计算\u0026quot;这块的流程是写在 Niagara 资产里的，不在 C++ 里。\n白话导读：前 4 章讲的是\u0026quot;物理上应该怎么算\u0026rdquo;，不依赖任何引擎。从这章起进入 UE 的具体实现，会出现大量类名（如 UGroomAsset）。看不懂类名就先看每节的白话部分，它们讲的是同样的道理。\n5.1 三块与一条数据流 毛发插件叫 Groom，位于 Engine/Plugins/Runtime/HairStrands，内部又拆成 7 个模块，各管一摊：\n模块 管什么 HairStrandsCore 资产、组件、绑定、每组实例、渲染代理、Niagara 数据接口 HairStrandsSolver 求解组件（负责划分曲线、出设置） HairStrandsDeformer 用 DeformerGraph / Optimus 驱动形变 HairStrandsDataflow 编辑期的 Dataflow 节点 HairStrandsEditor 资产菜单、导入器、细节面板 HairStrandsRuntime 高速缓存流送管理器的注册 HairCardGeneratorFramework 从网格生成发卡的框架 而\u0026quot;画出来\u0026quot;这部分不在插件里，在引擎渲染器的 Engine/Source/Runtime/Renderer/Private/HairStrands。\nflowchart LR subgraph ASSET[\"准备数据（HairStrandsCore）\"] GA[\"UGroomAsset头发资产\"] BA[\"UGroomBindingAsset发根钉到网格上\"] end subgraph RUNTIME[\"运行时（HairStrandsCore）\"] GC[\"UGroomComponent挂在角色身上的组件\"] GI[\"FHairGroupInstance每组的实际数据\"] end subgraph SOLVE[\"计算（HairStrandsSolver + Niagara）\"] SC[\"UGroomSolverComponent划分曲线 + 出设置\"] NB[\"Niagara 系统真正在 GPU 上算\"] end subgraph RENDER[\"画出来（Renderer）\"] SP[\"FHairStrandsSceneProxy可见性 / 光照 / 合成\"] end GA --\u003e GC BA --\u003e GC GC --\u003e GI GI --\u003e|几何与参数| SC SC --\u003e|入队| NB NB --\u003e|变形后的位置| GI GI --\u003e|交给渲染| SP各层各自拥有什么（源码细节，可跳过）：\n层 类（文件） 拥有什么 资产 UGroomAsset（Source/HairStrandsCore/Public/GroomAsset.h） 控制线/可见发丝/发卡/网格、插值、物理、渲染、LOD、材质 绑定 UGroomBindingAsset（.../GroomBindingAsset.h） 发根到网格三角形的预烘焙投影与权重 组件 UGroomComponent（.../GroomComponent.h） 资产/绑定/物理资产引用、Deformer、模拟设置、每组实例 每组实例 FHairGroupInstance（.../GroomInstance.h） 每组的 Rest / Deformed / Interpolation 等 GPU 资源 求解器 UGroomSolverComponent（Source/HairStrandsSolver/Public/GroomSolverComponent.h） Deformer 实例、动态曲线划分、关联的 Groom 组件 渲染 FHairStrandsSceneProxy（GroomComponent.cpp 内）与 Engine/Source/Runtime/Renderer/Public/HairStrandsInterface.h 跨模块交接的 FHairStrandsInstance / FHairGroupPublicData 5.2 头发\u0026quot;怎么动\u0026quot;的三条路 一个发组怎么动，UE 提供了三条路，优先级从高到低、不是叠加：\nflowchart TD A[\"每个发组 / 每个 LOD\"] --\u003e B{\"开了物理求解吗？IsSimulationEnable\"} B --\u003e|是| S[\"走物理求解（Niagara / Solver Deformer）\"] B --\u003e|否| C{\"开了 Deformer 吗？IsDeformationEnable\"} C --\u003e|是| D[\"走 DeformerGraph 骨骼驱动\"] C --\u003e|否| E{\"绑定类型 BindingType\"} E --\u003e|Skinning 且有绑定资产| F[\"蒙皮绑定（默认）\"] E --\u003e|Rigid 且有已注册网格| G[\"刚性跟随网格\"] E --\u003e|资源不全| H[\"降级为'没绑定'（静默）\"] 物理求解：真正会自己算头发怎么动的那条。由资产开关和组件覆盖共同决定（IsSimulationEnable）。 Deformer：不是物理解算，而是用一张\u0026quot;骨骼驱动图\u0026quot;来摆头发；资产开关是 FHairSolverSettings::bEnableDeformation。 蒙皮绑定：默认路径。头发被\u0026quot;焊\u0026quot;在身体网格上，靠网格变形跟着动。绑定类型由 FHairLODSettings::BindingType 决定（NoneBinding / Rigid / Skinning，默认 Skinning）。 最容易踩的坑是：这三条路判定不满足时不会报错，而是\u0026quot;毛发不动 / 停在默认姿态\u0026quot;。看到\u0026quot;毛发没反应\u0026quot;，先查这三条路的条件，而不是到处找 bug。\n5.3 为什么把求解放在 Niagara 上 好处很直接：用 Niagara 就能在 GPU 上算、能通过\u0026quot;数据接口\u0026quot;把资产里的参数和网格喂进去、还能白拿它现成的调度和子步框架。\n代价也很直接：求解的\u0026quot;流程编排\u0026quot;（先算什么、循环几次、怎么分支）写在了 Niagara 系统资产里，不在 C++ 代码里。于是：\n调试时在 C++ 里看不到\u0026quot;它到底迭代了几次\u0026quot;，只能看资产配置； 流程随二进制资产走，跨版本升级和版本管理比纯代码更麻烦。 这也是\u0026quot;同一个求解器组件，在不同项目里表现不一样\u0026quot;的根源。\n5.4 三层时序 sequenceDiagram participant GT as 游戏线程 participant NI as Niagara 实例 participant GPU as 显卡 GT-\u003e\u003eGT: 组件 Tick：更新设置、判断要不要重置、更新跟随的骨骼 GT-\u003e\u003eGT: 求解器组件 Tick：按距离划分曲线 GT-\u003e\u003eNI: 到点触发求解 NI-\u003e\u003eGPU: 展开流程：预测 -\u003e 迭代 -\u003e 收尾 GPU--\u003e\u003eGT: 写回变形后的位置与朝向，供渲染读取顺序由 tick group 推导，保证\u0026quot;在骨骼更新之后、渲染之前\u0026quot;发生。所谓\u0026quot;同一帧内\u0026quot;，指的是渲染读到的是本帧算完的结果。\n5.5 前半的量，在后半的哪里（对照表） 前 4 章的量 后面对应的东西 详见 珠子 / 绳子 / 朝向 数据接口里的节点位置、边方向、节点朝向 1.2 节 / 7.2 节 重量 / 转动惯量 求解 shader 里的等效质量项 1.5 节 / 7.2 节 柔度（有多软） 资产里的弯曲/拉伸刚度与沿头发缩放曲线 2.3 节 / 7.4 节 拉伸 / 弯曲 / 扭转 同一族函数，按\u0026quot;管几颗珠子\u0026quot;区分 2.4 节 / 7.2 节 \u0026ldquo;箭头\u0026quot;的两种记法 弹簧族 / 杆族两条求解路线 2.5 节 / 7.2 节 发根硬发梢软 逐点参数表（32 个采样点） 2.7 节 / 7.4 节 自碰撞体素近似 速度网格 + 压力网格（同帧 ping-pong） 3.2 节 / 7.3 节 风（线性 + 平方 + 迎风） 外力计算入口 3.3 节 / 7.2 节 惯性假力 模拟空间 + 线/角速度比例 + 瞬移阈值 3.4 节 / 7.4 节 重力预载 按系数在当前/平衡姿态间插值 3.5 节 / 7.4 节 子步 / 迭代 资产与组件上的子步数、迭代数 4.2 节 / 7.7 节 每步多长 delta 截断后按子步均分 4.3 节 / 7.7 节 本章小结：UE 把毛发分成\u0026quot;准备数据、计算、画出来\u0026quot;三块，按\u0026quot;谁拥有哪份数据\u0026quot;划边界。毛发怎么动有物理求解、Deformer、蒙皮绑定三条路，它们不是叠加关系，条件不满足时是静默失效而非报错。求解之所以放在 Niagara 上，是为了 GPU 计算、数据接口和现成的调度框架，代价是\u0026quot;流程编排\u0026quot;落在资产里而不在代码里。执行跨游戏线程、Niagara、GPU 三层，顺序由 tick group 决定。\n第 6 章 数据从哪来：Groom 资产 一句话：屏幕上十万根头发，真正存下来、真正参与计算的只有少量\u0026quot;骨架\u0026rdquo;；其余是照着骨架\u0026quot;长\u0026quot;出来的，配置则以\u0026quot;发组\u0026quot;为单位。\n白话导读：这章回答\u0026quot;这些头发是从哪来的、怎么存的\u0026quot;。如果你只想搞懂原理，记住一句就够：存的是少量骨架，画的是大量填充。\n6.1 两级数据：控制线与可见发丝 控制线（Guides）：真正参与计算的少量\u0026quot;骨架头发\u0026quot;。 可见发丝（Strands）：屏幕上实际画出来的那一大把，每一根都记着\u0026quot;我最像哪根骨架、像多少\u0026quot;，由骨架插值\u0026quot;长\u0026quot;出来。 两者的数量比就是\u0026quot;一根骨架撑起几根真发\u0026quot;。要注意的坑：插值数据一旦缺失，不会报错，而是让形变相关功能整体失效（头发停在默认姿态）——这是最容易被误判的故障。\n6.2 发组：配置的最小单位 头发在资产里按\u0026quot;发组\u0026quot;组织（比如\u0026quot;刘海\u0026quot;、\u0026ldquo;后脑\u0026quot;各是一组）。每种配置都是一张数组，按组的编号一一对齐（第 0 张对应第 0 组，以此类推）（源码细节，可跳过）：\n数组 装什么 HairGroupsInfo 每组有多少曲线、多少控制线、最长多长 HairGroupsRendering 材质槽名 + 几何/阴影/高级渲染设置 HairGroupsPhysics 求解、外力、约束、发丝参数 HairGroupsInterpolation 抽稀与插值设置 HairGroupsLOD 每组的 LOD 设置 HairGroupsCards / HairGroupsMeshes 发卡 / 网格源描述 HairGroupsMaterials 材质槽（材质 + 名称） HairGroupsPlatformData 真正存几何的那份 Bulk Data 改组数会让这些对齐关系和绑定一起失效，这是修改资产时最容易出事的地方。\n6.3 绑定：把发根钉到网格上 头发要跟着身体动，就得知道\u0026quot;每根发根落在身体网格的哪个位置\u0026rdquo;。UGroomBindingAsset 就是把这件事提前算好存下来：每根发根落在哪个三角形上、用哪几个顶点按什么权重插值（源码细节，可跳过：RootToUniqueTriangleIndexBuffer / RootBarycentricBuffer / MeshInterpolationWeightsBuffer 等，封装在 FHairStrandsRootBulkData）。\n两件事必须知道：\n不提供绑定资产时，投影会在运行时现算，启动时有一笔较大的 GPU 开销——表现就是\u0026quot;第一次看到角色卡一下\u0026quot;。 绑定资产里存的是源网格的三角形编号，所以跨骨架复用会出问题：绑定与网格的 LOD 数量不一致时会直接判定不兼容（回退成未绑定、毛发不动），而 LOD 数量一致时不会报错、却会给出错误的形变。 6.4 LOD 与\u0026quot;第一次进游戏卡一下\u0026quot; LOD（远近不同精度）有三种模式：\n模式 行为 Default 跟随项目设置 Manual 用你手工设的每组 LOD Auto 按屏幕占比自动增减，忽略你手设的 LOD 另外，资产里的设置会被**编译成一份\u0026quot;派生数据\u0026quot;缓存在磁盘上。改动设置会改变它的\u0026quot;钥匙\u0026quot;，从而触发重新生成——这就是\u0026ldquo;改完设置第一次进游戏卡一下\u0026rdquo;**的原因：它在现生成 PostLoad / PostEditChangeProperty 时触发重建。\n本章小结：控制线是参与计算的少量骨架，可见发丝是由它插值长出来的一大把；插值数据缺失不会报错，而是让形变功能整体失效。配置以\u0026quot;发组\u0026quot;为单位、按编号对齐，改组数会连带失效。绑定资产把\u0026quot;发根落在网格哪个三角形、怎么插值\u0026quot;提前烘焙好，省掉运行时投影，代价是跨骨架复用会出错。LOD 有 Default / Manual / Auto 三种，Auto 会忽略手工设置；派生数据的重建就是\u0026quot;改完设置第一次进游戏卡顿\u0026quot;的来源。\n第 7 章 怎么算：求解实现 一句话：求解器组件自己不算，它只负责\u0026quot;划分要算哪些头发、把设置准备好\u0026quot;，真正的计算发生在 Niagara 系统图和 GPU 上。\n白话导读：这章把第 2~4 章的每一条道理，对应到 UE 的具体类与函数。看不懂函数名就只看每节的结论句。\n7.1 谁出设置、谁真正执行 这是最容易搞错的一点：UGroomSolverComponent 不是执行者。\n做的事 谁来做 准备设置（参数、覆盖） UGroomComponent::SimulationSettings + 资产里的 HairGroupsPhysics 划分\u0026quot;这帧要算哪些头发\u0026quot; UGroomSolverComponent::SelectDynamicCurves 把任务入队 UGroomSolverComponent::SendRenderDynamicData_Concurrent 真正计算 Niagara 系统（StableSpringsSystem / StableRodsSystem / 自定义） 它的 TickComponent 里只有\u0026quot;标脏 + 划分曲线\u0026quot;，没有任何解算代码。走哪套求解系统由枚举 EGroomNiagaraSolvers 决定。\n7.2 前 4 章的公式落在哪（源码细节，可跳过） 前 4 章的量 对应函数（Niagara 数据接口 UNiagaraDataInterfaceHairStrands） 珠子位置 / 朝向 ComputeNodePosition / ComputeNodeOrientation 重量 / 转动惯量 ComputeNodeMass / ComputeNodeInertia 绳子（长度/方向/旋转） ComputeEdgeLength / ComputeEdgeDirection / ComputeEdgeRotation 静止姿态 ComputeRestPosition / ComputeRestOrientation 预测（按力动一下） AdvectNodePosition / AdvectNodeOrientation 拉伸 / 弯曲 / 扭转 Setup/Solve/Project{Stretch,Bend,Twist}SpringMaterial 杆族（相对旋转） Setup/Solve/Project{Stretch,Bend}RodMaterial 硬 / 软碰撞 Solve/ProjectHardCollisionConstraint、Setup/Solve/ProjectSoftCollisionConstraint 风 ComputeAirDragForce 重力与预载 GetGravityVector / GetGravityPreloading 规则本身写在 Engine/Plugins/Runtime/HairStrands/Shaders/Private/ 下，统一用 Setup -\u0026gt; Solve -\u0026gt; Project（准备 -\u0026gt; 解 -\u0026gt; 收尾）三段命名：\n文件 提供什么 NiagaraHookeSpringMaterial.ush 弹簧族（方向路线） NiagaraCosseratRodMaterial.ush 杆族（相对旋转路线） NiagaraAngularSpringMaterial.ush 角度弹簧族 NiagaraElasticRodMaterial.ush 弹性杆（Darboux）族 NiagaraStaticCollisionConstraint.ush 碰撞 NiagaraStrandsExternalForce.ush 风 NiagaraDataInterfaceHairStrandsTemplate.ush 把上面这些包装成数据接口入口 第 2 章说的\u0026quot;同一条规则管几颗珠子\u0026quot;，在这里就是参数 NodeOffset：\nNodeOffset = 0 -\u0026gt; 拉伸（相邻两颗） NodeOffset = 1 -\u0026gt; 弯曲（管三颗） NodeOffset = 2 -\u0026gt; 扭转（管四颗） 与 4.5 节的\u0026quot;分组着色\u0026quot;一致：拉伸按 x % 2、弯曲按 (x / 2) % 2、扭转按 (x / 3) % 2——三个式子的取值都只有 0/1，因此三者都是两色（红/黑）交替，区别只在取模步长。\n\u0026ldquo;有多软\u0026rdquo;（柔度）不是资产上的一个字段，而是在准备阶段算出来的：\n柔度 = 4.0 / (杨氏模量 * pi * 杆半径 * 静止长度 * 杆半径 * dt^2) 式中\u0026quot;杆半径\u0026quot;在源码里对应的参数名是 RodThickness（它被填进截面积公式中 R 这一项，即 面积 = pi * R^2 里的 R）。\n那个 dt^2 就是 2.3 节说的\u0026quot;按步长平方归一化\u0026quot;。\n7.3 自碰撞网格长什么样 自碰撞用两个数据接口：速度网格（UNiagaraDataInterfaceVelocityGrid）和压力网格（UNiagaraDataInterfacePressureGrid）。\n它们都用同帧 ping-pong：一边读一边写会读到\u0026quot;写坏一半\u0026quot;的数据，所以准备了两块缓冲（CurrentGridBuffer / DestinationGridBuffer），写完之后要 Swap() 一下才能读。分辨率由 GridDimension 控制，通道数是固定的（速度网格 6 个、压力网格 18 个）。分辨率越高，能分开的头发越细，显存也按分辨率的三次方增长。\n7.4 参数的三个来源与覆盖边界 参数可以来自三个地方，\u0026ldquo;改哪里才算数\u0026quot;要先分清楚：\n资产（HairGroupsPhysics）：基线值。 组件覆盖（UGroomComponent::SimulationSettings）：只有当\u0026quot;覆盖总开关\u0026quot;打开时才参与。 求解器组件（FGroomSolverSettings）与 CVar：更外层的控制。 资产里能调的参数（源码细节，可跳过）：\n结构 字段 FHairStretchConstraint StretchStiffness / StretchScale / SolveStretch / ProjectStretch / StretchDamping FHairBendConstraint BendStiffness / BendScale / SolveBend / ProjectBend / BendDamping FHairCollisionConstraint StaticFriction / KineticFriction / GridDimension / CollisionRadius / RadiusScale 等 FHairStrandsParameters StrandsSize / StrandsDensity / StrandsThickness / ThicknessScale 等 FHairExternalForces GravityVector / AirDrag / AirVelocity \u0026ldquo;发根硬发梢软\u0026quot;这张沿头发分布的表，对应数据接口里的 ParamsScale：\nParamsScale：32 * 4 个数 四条曲线：拉伸缩放 / 弯曲缩放 / 半径 / 粗细 以 VertexCoord = i / (StrandsSize - 1) 逐点采样 每条曲线最多 32 个采样点 关键上限：每条曲线最多 32 个采样点；而\u0026quot;每根头发切几段\u0026rdquo;（StrandsSize）的可选值本身只有 2/4/8/16/32，上限正好也是 32。也就是说采样点数与段数一一对应、上限同为 32，精度因此被封死在 32 点——这是 2.7 节说的\u0026quot;怎么调都不够自然\u0026quot;的原因之一。\n另外，同名参数可能分属不同路径：资产上的 SubSteps / IterationCount、数据接口里的 SubSteps（默认 5）/ IterationCount（默认 20）、组件上的 NumSubSteps（默认 2）/ NumSolverIterations（默认 5）是三套不同的东西，不能互相推导。\n7.5 按距离决定\u0026quot;这帧算多少\u0026rdquo; SelectDynamicCurves 按\u0026quot;离镜头多远\u0026quot;决定这帧要算多少根控制线：\n距离比例 = clamp((距离 - 最近距离) / (最远距离 - 最近距离)) 要算的控制线数 = (1 - 距离比例) * 总控制线数 一个反直觉点：划分的结果和帧率无关，但划分本身的消耗和帧率成正比——每帧都要重算一遍，距离没变结果一样，CPU 时间照花。\n还要区分：\u0026ldquo;算多少\u0026rdquo;（模拟 LOD）和\u0026quot;画多少\u0026quot;（渲染 LOD）不是一回事。\n7.6 生命周期与重置 阶段 入口 创建 / 注册 UGroomSolverComponent::OnRegister、CreateRenderState_Concurrent 每帧推进 TickComponent -\u0026gt; SendRenderDynamicData_Concurrent 关联 Groom AddGroomComponent / RemoveGroomComponent 释放 OnUnregister / DestroyRenderState_Concurrent 重置会在三种情况发生：初始化、瞬移（位移超过 TeleportDistance）、不可见/隐藏；数据接口还会在\u0026quot;网格资产被换掉\u0026quot;时触发重置。\n重置后有一段按帧计数的\u0026quot;启动缓冲\u0026quot;（上限由 r.HairStrands.SimulationMaxDelay 控制，默认 4 帧）。因为按帧计数，这段的真实时长会随帧率变化（4.3 节）。\n7.7 一轮求解的完整时序 sequenceDiagram participant GT as 游戏线程 participant SC as 求解器组件 participant NB as Niagara participant GPU as 显卡 GT-\u003e\u003eSC: TickComponent：标脏 SC-\u003e\u003eSC: SelectDynamicCurves：按距离划分 SC-\u003e\u003eNB: 入队 NB-\u003e\u003eNB: 取 delta（先被上限截断）-\u003e 按子步数均分 loop 每个子步 NB-\u003e\u003eGPU: 预测（AdvectNodePosition / AdvectNodeOrientation） loop 每次迭代 GPU-\u003e\u003eGPU: 准备 -\u003e 解 -\u003e 收尾（拉伸 / 弯曲 / 扭转 / 碰撞） end end GPU--\u003e\u003eGT: 写回位置与朝向 -\u003e 渲染读取哪些机制会随帧率变化：\n机制 随帧率变吗 说明 delta 截断 是 超长帧被截断，模拟变慢 每步时长 dt 是 等于截断后的 delta 除以子步数 迭代顺序 / 分组 否 由曲线结构决定 划分结果 否 但它的开销随帧率增加 重置启动缓冲 是 按帧计数，真实时长随帧率变 固定步长 没有实现 插件里没找到固定步长；设置项提示的 \u0026ldquo;24 fps\u0026rdquo; 在源码里没有对应实现，以源码为准 本章小结：求解器组件只负责\u0026quot;出设置 + 划分曲线\u0026quot;，真正的计算在 Niagara 与 GPU 上。前 4 章的每一条道理都能落到具体函数：三类规则在同一族 shader 里靠 NodeOffset 区分；柔度由杨氏模量、杆半径、静止长度和步长派生（dt 平方在分母）；\u0026ldquo;发根硬发梢软\u0026quot;由最多 32 个采样点的逐点表承载，所以精度上限就是\u0026quot;每根切几段\u0026rdquo;。自碰撞用读写型数据接口做同帧 ping-pong，写完必须交换缓冲才能读。参数有资产、组件覆盖、求解器组件三层，\u0026ldquo;总开关没开\u0026quot;和\u0026quot;这个字段本来就不给覆盖\u0026quot;是两种不同的没生效。按距离裁剪的是\u0026quot;算多少\u0026rdquo;，且开销随帧率增长；重置后的启动缓冲按帧计数，时长随帧率变。\n第 8 章 怎么画：这么细的东西怎么显示 一句话：头发比像素还细，直接按普通方式画会互相盖住、闪烁；所以 UE 换了个顺序——先搞清楚哪些像素上有头发，再只给这些像素上色。\n白话导读：前面都在讲\u0026quot;算\u0026quot;。这章讲\u0026quot;画\u0026quot;。只要记住\u0026quot;先找哪些像素有头发、再只算这些像素\u0026quot;这一句，剩下都是细节。\n8.1 三种画法（可切换） 画法 怎么画 真实度 成本 开关 曲线 真的一根根画 最高 最高 r.HairStrands.Strands（默认 1） 发卡 用贴了头发图案的片 中 中 r.HairStrands.Cards（默认 1，需启动时开） 网格 直接当普通模型画 低 低 r.HairStrands.Meshes（默认 1，需启动时开） 坑：强制某一种画法时，如果资产里没有那类数据，会什么都不显示（不是报错）。r.HairStrands.UseCardsInsteadOfStrands 的提示就明确说了这点。\n8.1.1 Groom（发丝）与 Cards（发卡）之间怎么用 LOD 切换 三种画法不只\u0026quot;能手动强制切\u0026quot;，更常规的用法是把它们做成一套 LOD 降级链：近处/主角用真发丝（Strands，最真最贵），拉远后自动换成发卡（Cards，中等），更远再换网格（Meshes，最省）。\u0026ldquo;怎么切\u0026quot;由 Groom 的 LOD 系统驱动：\n每个 LOD 等级可指定一种画法：LOD 0 用 Strands、LOD 1 用 Cards、LOD 2 用 Meshes——画法本身就是 LOD 的一部分（9.5.2 节 的 Visible Geometry Type 就是在选当前 LOD 用哪种几何）。 靠\u0026quot;屏幕占比\u0026quot;触发：相机拉远，头发在屏幕上占的面积变小，一旦跌破该等级的 Screen Size 阈值，就自动降到下一级（Strands→Cards→Meshes）。LOD Mode（Default / Manual / Auto）+ Auto LOD Bias 决定这套判定是自动还是手写；Minimum LOD 钳制\u0026quot;最低允许降到哪一级\u0026rdquo;（9.5.2 节）。 资产必须有那级的数据：想切到 Cards，资产里必须先 Add Card asset；想切到 Meshes，必须先 Add Mesh asset。缺数据的等级一旦被选中，会什么都不显示——这是\u0026quot;远处头发突然没了\u0026quot;最常见的根因（9.5.4 节 / 9.5.5 节）。 渲染 LOD 与模拟 LOD 分开：换画法（渲染 LOD）降的是\u0026quot;怎么画\u0026quot;；而\u0026quot;算多少根\u0026quot;由独立的模拟 LOD 控制（SelectDynamicCurves，7.5 节）。Cards/Meshes 通常不参与逐根求解，所以远处头发连物理也一起省了。 一句话：Groom↔Card 的切换 = \u0026ldquo;LOD 等级里把画法降级 + 屏幕占比触发 + 资产得有那级数据\u0026rdquo;。要启用，先确认资产里建了 Cards/Meshes 数据，再在 LOD 面板把对应等级的 Visible Geometry Type 设好、调 Screen Size 阈值；别直接强制 r.HairStrands.UseCardsInsteadOfStrands——那是不看距离的硬切换，没数据照样黑屏。\n8.2 一条专用的流水线 毛发走的是自己的一套流程，入口在 Engine/Source/Runtime/Renderer/Private/HairStrands：\nflowchart TD subgraph P1[\"第一步（RenderHairPrePass）\"] A[\"按区域分组 AABB\"] --\u003e B[\"体素化（把密度写进 3D 网格）\"] B --\u003e C[\"深阴影（沿光线分层记录遮挡）\"] end subgraph P2[\"第二步（RenderHairBasePass）\"] D[\"可见性：算出哪些像素有头发、有多少层\"] --\u003e E[\"按屏幕小块（tile）聚合成清单\"] E --\u003e F[\"只给清单里的样本算光照\"] end subgraph P3[\"第三步（RenderHairComposition）\"] G[\"合成回画面（按需要插在半透明前或后）\"] end P1 --\u003e P2 --\u003e P3两个关键设计：\n先可见性、再着色：先把\u0026quot;哪个像素有头发、有几层\u0026quot;整理成一份很紧凑的清单，然后只给清单里的东西算光照。这样光的计算量正比于\u0026quot;真正看得见的头发\u0026quot;，而不是\u0026quot;画了多少段\u0026quot;。 按小块聚合（tile）：把屏幕切成 8x8 的小块，只挑\u0026quot;里面有头发的块\u0026quot;去处理，没有头发的块直接跳过。这是毛发在大屏幕上还能跑得动的关键。 8.3 光照怎么打 头发和普通物体不一样，它会透光——一大把头发不是一堵黑墙。所以光照分三路：\n光 怎么处理 环境光 / 间接光 用上面那份 3D 体素（密度网格）去\u0026quot;穿一遍\u0026quot;，估算被头发挡住了多少 直射光的阴影 用\u0026quot;深阴影\u0026quot;：不是简单记\u0026quot;挡住/没挡住\u0026quot;，而是沿光线分层记每层透过去多少 头发内部多次反弹 提前算好一张表（LUT），运行时查表，省掉实时计算 头发的散射模型（光打到一根头发上往哪散）在源码里以 R / TT / TRT 等分量 + \u0026ldquo;双重散射\u0026quot;命名，本质是经典头发散射模型加近似，对应参考 8。\n8.4 远近与\u0026quot;头发突然冒出来\u0026rdquo; 远处的头发会按\u0026quot;页\u0026quot;流送（一页固定装多少根），加载/卸载的粒度正是\u0026quot;页\u0026quot;。所以不看细节时，一旦发生流送，表现是头发突然出现或突然消失，不是渐显。 CPU 侧还有两个开关决定\u0026quot;数据用完就扔\u0026quot;和\u0026quot;异步加载\u0026quot;（ReleaseAfterUse / AsyncLoading），它们是在内存和\u0026quot;第一次进场景卡顿\u0026quot;之间做取舍。 8.5 光追与抗锯齿 光追：毛发可以参与光追（默认开，且需要资产主动勾选）。两个调优项是\u0026quot;每段拆成几个小包围盒\u0026quot;（拆得越多光追越快、构建越慢）和\u0026quot;光追时把头发半径缩放多少\u0026quot;。 抗锯齿：毛发太细，会闪。可见性那一步会额外产出一张\u0026quot;遮罩\u0026quot;和\u0026quot;速度\u0026quot;，交给 TAA/TSR 这套时序抗锯齿去处理，避免边缘闪烁和拖影。 8.6 几个\u0026quot;看起来像 bug\u0026quot;的开关 开关 默认 看起来像什么 bug r.HairStrands.BoundMode 0 设为 1 时用网格包围盒判断可见性；头发大幅甩动时会被整块裁掉（像\u0026quot;毛发突然没了\u0026quot;） r.HairStrands.ComposeAfterTranslucency 1 控制毛发和半透明/景深的叠加顺序；设错会出现覆盖顺序错乱 r.HairStrands.UseCardsInsteadOfStrands 0 强制发卡；资产没发卡数据时什么都不显示 r.HairStrands.WriteGBufferData 1 毛发写不写 GBuffer；影响依赖 GBuffer 的后续效果 另有一个\u0026quot;说明和实现不一致\u0026quot;的例子：r.HairStrands.SwapType 的提示写 Default:1，实际默认是 2。遇到这种一律以源码为准。\n本章小结：毛发比像素细，所以不走普通画法：先算可见性、按 8x8 小块聚合、只给\u0026quot;有头发的块\u0026quot;着色、最后合成。这个顺序就是它的性能设计——tile 聚合是关键。画法有曲线、发卡、网格三种，缺数据时会什么都不显示。光照分三路（体素间接光、深阴影直射光、LUT 多次散射），因为头发会透光。流送以\u0026quot;页\u0026quot;为粒度，表现是突然出现/消失。光追有两个调优项，细几何的抗锯齿靠可见性产出的遮罩交给 TAA/TSR。\n第 9 章 上手：改哪里、会出什么问题、多贵 一句话：同一个效果往往在三个地方都能改，先弄清\u0026quot;改哪里才生效\u0026quot;；再记住成本大致由哪些量相乘决定。\n白话导读：这章是给真正动手的人看的\u0026quot;地图\u0026quot;，遇到问题可以直接查表。\n9.0 第一次上手：最小路径与最易踩的坑 如果你是第一次接 Groom，按下面 5 步走完，就能看到一个能动的、不出错的毛发（每步对应正文出处）：\n导入资产并烘焙绑定：把头发导成 UGroomAsset，并为角色网格烘焙一份 UGroomBindingAsset。不烘焙，首次显示会现算投影、卡一下（6.3 节）。 挂组件：给角色加 UGroomComponent，把资产和绑定填进去（5.1 节）。 确认\u0026quot;三条路\u0026quot;之一成立：开物理求解（IsSimulationEnable），或开 Deformer，或走蒙皮绑定。三条都不满足时毛发会静默停在默认姿态，不报错——这是新手\u0026quot;毛发没反应\u0026quot;的头号原因（5.2 节）。 开模拟后先做重力预载：否则头发会被重力直接拉塌，和美术摆的姿态不一致（3.5 节）。 看效果再按表调：软 / 抖 / 穿模 / 像塑料，分别对应哪个旋钮，见4.4 节的现象对参数表；\u0026ldquo;改哪里才算数\u0026quot;见下文的参数地图。 最易踩的 5 个坑（都不会报错，靠查条件定位）：\n现象 根因 定位 毛发完全不动 / 停在默认姿态 三条驱动路都不满足，或插值数据缺失 5.2 节 / 6.1 节 毛发不动或形变错乱 绑定资产跨骨架复用 / 与网格不匹配 6.3 节 强制发卡/网格后什么都不显示 资产里没有那类数据 8.1 节 改了组件参数却没变化 覆盖总开关没开 7.4 节 / 9.1 节 改完设置第一次进游戏卡一下 派生数据在重新生成，是正常现象 6.4 节 9.1 改哪里才算数（参数地图） 你想调的 资产 组件覆盖 求解器组件 全局开关 拉伸/弯曲刚度、沿头发曲线 HairGroupsPhysics 需\u0026quot;覆盖总开关\u0026quot;打开 — — 摩擦、自碰撞网格粗细 HairGroupsPhysics 同上 — — 子步数 / 迭代数 资产里的求解设置 — NumSubSteps(2) / NumSolverIterations(5) r.HairStrands.EnableAdaptiveSubsteps 算多少根（按距离） — — MinLODDistance / MaxLODDistance r.HairStrands.MaxSimulatedLOD 总开关 组的求解开关 bOverrideSettings + bEnableSimulation — r.HairStrands.Simulation(1) 判据：资产是基线；只有\u0026quot;覆盖总开关\u0026quot;打开，组件上的值才会参与；CVar 在最外层统一钳制。\n9.2 常见故障：约束 + 触发条件 + 引擎行为 写成\u0026quot;在什么前提下，出现什么条件，引擎会怎样\u0026rdquo;，方便直接对照：\n类别 前提 触发条件 结果 设置无效 要覆盖生效 覆盖总开关没开 组件值被忽略，用资产的 设置无效 字段可覆盖 这个字段本来就不给覆盖 开了总开关也没用 设置无效 要能形变 插值数据缺失 形变功能整体失效，不报错 没反应 绑定要匹配 绑定资产与网格不匹配/跨骨架复用 毛发不动或形变错乱 没反应 三条路要满足 求解/Deformer 判定都不满足 停在默认姿态 观感不对 迭代要够 迭代不足 整体发软、拉不住 观感不对 网格要够细 自碰撞网格太粗 头发互相穿插/粘连 观感不对 包围盒要够大 BoundMode=1 且甩动大 被错误裁掉 随时间变 步长来自帧 帧率低 变软、变滞后 随时间变 按帧计数 帧率变化 重置缓冲的真实时长改变 9.3 成本大概怎么算 求解侧 约等于 头发根数 x 每根切几段 x 迭代次数 x 子步数 x 每秒帧数 渲染侧 约等于 头发段数 x 屏幕占用（涉及的 tile 数） 两侧的瓶颈通常不同：求解吃\u0026quot;头发规模 + 迭代\u0026quot;，渲染吃\u0026quot;屏幕覆盖率\u0026quot;。想加效果时，先判断瓶颈在哪一侧，再决定加曲线/段数（更细）还是加迭代/子步（更稳）。\n9.4 出问题先看哪 入口 看什么 r.HairStrands.Dump / .GroomAsset / .GroomComponent 打印资产/组件里到底有什么 r.HairStrands.ViewMode 切换各种调试视图（LOD 着色、控制线、朝向、AO、深阴影 等） r.HairStrands.BindingValidation 检查绑定是否匹配 r.HairStrands.DeepShadow.DebugMode 看深阴影 r.HairStrands.PlotBsdf 看头发散射函数 注意\u0026quot;字段在、但提示与行为不一致 / 存疑\u0026quot;的情况（如 SimulationFrameRate 存在但未被使用、SubSteps 的 \u0026ldquo;24 fps\u0026rdquo; 提示无固定步长实现、r.HairStrands.SwapType 提示与默认疑似不一致；以下均基于 UE 5.6 源码观察，需在高版本复核）：这类清单统一汇总在11.2 节，遇到一律以你所用版本的源码和资产为准，别被提示误导。\n9.5 界面上的选项逐一说明（附全部面板截图） GroomAsset 编辑器各面板都藏着性能选项。决定\u0026quot;这套毛发整体贵不贵\u0026quot;的规模类选项（发量、每根段数、控制线数、碰撞网格、求解密度）在 Strands / Interpolation / LOD / Physics(StrandsParameters) ；决定\u0026quot;每帧怎么算得更准更省\u0026quot;的在 Physics（迭代 / 子步 / 约束开关）。 下文按面板逐一说明每个字段，⚠＝影响运行性能（影响不分大小都写入，并给出相对量级：高 / 中 / 低——量级是按 9.3 节 成本模型推导的相对排序，非官方数字）。截图取自 UE 5.6 的 MetaHuman Groom 示例资产。\n9.5.1 Strands（发丝渲染）——发量规模类 字段（英文 · 译文） 作用 性能影响 ⚠ Pt/Curve（每根节点数） 每根头发的曲线节点/细分数量 高：决定发丝几何细分，直接进\u0026quot;每根段数\u0026quot;成本项 ⚠ Hair Shadow Density（发丝阴影密度） 阴影采样密度 中：越高阴影越贵 ⚠ Use Hair Raytracing Geometry（使用光追几何） 是否用真实发丝做光追 中：光追开关 ⚠ Voxelize（体素化） 自遮挡/体积化预计算 中：构建期成本（8.5 节） ⚠ Use Stable Rasterization（稳定光栅化） 光栅化稳定性 中：渲染路径成本 ⚠ Scatter Scene Lighting（散射场景光照） 发丝间散射光照 中：光照成本 ⚠ Raytracing Radius Scale（光追半径缩放） 光追射线半径 低-中：光追相关 Hair Root Scale / Hair Tip Scale（发根/发梢宽度比例） 根部/梢部粗细 ⚠（间接）低：越粗光栅面积越大 Max. Width（最大宽度） 发丝宽度上限 ⚠（间接）低：同上 9.5.2 LOD（细节等级）——算多少 / 画多少 字段（英文 · 译文） 作用 性能影响 ⚠ Minimum LOD / Hair LOD（最小 LOD） 最低允许的 LOD 等级，设定\u0026quot;再怎么降也保底多少\u0026quot; 高：决定最低渲染/模拟规模 ⚠ Curve Decimation / Vertex Decimation（曲线/顶点抽稀） LOD 时的抽稀比例 中-高：降采样后规模 ⚠ LOD Mode（Global / Auto）+ Auto LOD Bias（自动偏移） 是否启用自动 LOD 系统 中：开关 ⚠ Visible Geometry Type（可见几何类型：Strands） 用哪种画法显示 中：Strands 最贵，Cards/Meshes 更省（8.1 节） ⚠ Simulation（RBF Interpolation） 模拟是否随 LOD 降精度 中 ⚠ Angular Threshold（角度阈值）/ Screen Size（屏幕尺寸） LOD 切换判定条件 低 Binding Type（绑定类型：Skinning） 绑定方式 低-中 Thickness Scale（厚度缩放） 观感 低 9.5.3 Interpolation（控制线 / 插值）——求解骨架规模 字段（英文 · 译文） 作用 性能影响 ⚠ Imported Guides（导入控制线数，Min / Max） 参与求解的\u0026quot;骨架\u0026quot;曲线数量 高：直接决定物理求解规模（6.1 节 / 9.3 节） ⚠ Curve Decimation / Vertex Decimation（抽稀） 控制线降采样 中-高 ⚠ Interpolation Quality（High / Parametric / Smooth / Transform） 从控制线插值出全头发的质量 中：High 最贵 ⚠ Generated guide density（生成控制线密度） 自动生成控制线的密度 中 ⚠ Rigged guide num curves/points（绑定控制线数/点数） 绑定驱动用的控制线规模 中 ⚠ Enable Guide-Cache Support（启用控制线缓存） 控制线结果缓存 中：内存/带宽 ⚠ Hair Interpolation / RBF Interpolation 插值算法选择 低-中 Interpolation Distance（插值距离） 插值影响半径 低 Randomize Guide / Use Unique Guide 控制线随机化/独立化 低（观感/一致性） Guide Type / Hair Interpolation Type / Rigged Skeletal Mesh 控制线来源与类型 低 9.5.4 Cards（发卡） 字段（英文 · 译文） 作用 性能影响 ⚠ Add Card asset（添加发卡资产） 是否提供发卡数据；有则运行时可用发卡画法 中：发卡通常比 Strands 曲线便宜（8.1 节） 9.5.5 Meshes（网格） 字段（英文 · 译文） 作用 性能影响 ⚠ Add Mesh asset（添加网格资产） 是否提供网格数据；有则可用网格画法 中：网格通常是最便宜的画法（8.1 节） 9.5.6 Dataflow（数据流） 字段（英文 · 译文） 作用 性能影响 Dataflow Asset / Dataflow Terminal（数据流资产 / 终端） 资产生成管线（GroomAssetTerminal） 构建期成本，运行时无直接开销 9.5.7 Material（材质） 字段（英文 · 译文） 作用 性能影响 Material Slots（材质槽） 绑定渲染材质 ⚠（间接）低：着色复杂度由材质决定 9.5.8 Binding（绑定） 字段（英文 · 译文） 作用 性能影响 ⚠ Bindings（绑定资产，如 GB_Hair_MyGroom） 烘焙的绑定/投影资产 中（启动/首次显示）：不烘焙则每次现算投影会卡（6.3 节） 9.5.9 Physics（物理）· 上：求解器与外力 字段（英文 · 译文） 分组 作用 性能影响 ⚠ Sub Steps（子步数） SolverSettings 每帧切成几段分别算，成本 ≈ 段数 × 每段计算 高 ⚠ Iteration Count（迭代数） SolverSettings 每段内把规则反复解几遍，成本 ≈ 遍数 × 每遍计算 高 ⚠ Enable Niagara Simulation（启用 Niagara 模拟） SolverSettings 是否物理求解 高（开关） ⚠ Enable Deformer Simulation（启用 Deformer 模拟） SolverSettings 是否走 Deformer 驱动路 中（开关） ⚠ Niagara Solver（Niagara 求解器：StableSpringsSystem） SolverSettings 选求解系统 中（Springs 轻 / Rods 重） Gravity Preloading（重力预载） SolverSettings 一次性预计算平衡姿态 低 Gravity Vector（重力向量）/ Air Drag（空气阻力）/ Air Velocity（风速） ExternalForces 重力与风，外力项 低（观感为主） Force Visible（强制可见） SolverSettings 强制显示 无性能意义 9.5.10 Physics（物理）· 下：约束与求解参数 字段（英文 · 译文） 分组 作用 性能影响 ⚠ Solve Collision / Project Collision（求解/投影碰撞） CollisionConstraint 碰撞约束阶段开关；碰撞是最贵的一类约束 中-高 ⚠ Solve Bend / Project Bend（求解/投影弯曲） BendConstraint 弯曲约束阶段开关 中 ⚠ Solve Stretch / Project Stretch（求解/投影拉伸） StretchConstraint 拉伸约束阶段开关 中 ⚠ Grid Dimension（碰撞网格分辨率） CollisionConstraint 自碰撞网格；显存与计算 ≈ 按分辨率三次方增长 高（7.3 节 / 3.2 节） ⚠ Strands Size（求解每根点数） StrandsParameters 物理求解用每根头发节点数 高：求解规模核心 ⚠ Strands Density（求解密度） StrandsParameters 物理求解用头发密度/根数 高：求解规模核心 ⚠ Collision Radius（碰撞半径）/ Radius Scale（半径缩放） CollisionConstraint 碰撞半径；越大参与碰撞的对越多 中 ⚠ Strands Viscosity（粘度） CollisionConstraint 求解阻尼项 低-中 Bend/Stretch Stiffness + Stiffness Scale（刚度 + 缩放） Bend/StretchConstraint 软硬/蓬松；刚度太高会收敛不足、需加迭代 ⚠（间接）低 Bend/Stretch Damping（阻尼） Bend/StretchConstraint 防抖 低 Static Friction（静摩擦）/ Kinetic Friction（动摩擦） CollisionConstraint 摩擦行为 低 Strands Smoothing / Strands Thickness / Thickness Scale StrandsParameters 平滑/粗细观感 低 9.5.11 Preview settings（编辑器预览）——非运行时 字段（英文 · 译文） 作用 性能影响 Lighting / Post Processing / Rendering Features（Path Tracing 等） 仅编辑器预览窗口的照明与后期 只影响预览帧率，不进入运行时资产 调参直觉（更新）：想大幅降本，先在规模类动手——降 Strands Pt/Curve、Interpolation Imported Guides、Physics Strands Size/Density、Collision Grid Dimension，或切 LOD / Cards / Meshes 画法；想每帧更省，动 Physics 的 Sub Steps / Iteration Count / Solve·Project 开关。观感旋钮（刚度、阻尼、摩擦、重力、风、粗细）几乎不省钱。标注\u0026quot;（推断）\u0026ldquo;的是按计算模型推导、非官方明示的结论；标注\u0026rdquo;（间接）\u0026ldquo;的表示通过影响别的东西（如收敛、光栅面积）间接产生成本。截图均取自示例资产，字段默认值随资产不同。\n本章小结：同一效果可在资产、组件覆盖、求解器组件、CVar 多处调，\u0026ldquo;改哪里生效\u0026quot;靠参数地图：资产定基线，覆盖总开关打开后组件值才生效。故障可统一写成\u0026quot;前提 + 触发条件 + 结果\u0026rdquo;：设置无效来自总开关/字段范围/插值缺失，没反应来自绑定不匹配或三条路都不满足，观感与时间异常来自迭代不足、网格过粗、包围盒过小或帧率变化。成本大致是\u0026quot;求解侧相乘\u0026quot;与\u0026quot;渲染侧相乘\u0026rdquo;，加效果前先定位瓶颈在哪一侧。\n第 10 章 和别的方案比：Groom 与 gFur 一句话：UE Groom 是\u0026quot;真发丝 + 逐根物理求解\u0026quot;的重路线，gFur 是\u0026quot;发壳 + 骨骼级近似\u0026quot;的轻路线；一个追求\u0026quot;动得对\u0026quot;，一个追求\u0026quot;看起来像、还很省\u0026quot;。\n白话导读：这章把两条路线并排比。结论先给：它们不是替代关系，而是互补——远景/群体用 gFur，近景/主角用 Groom。下面分设计、性能、工程三块看。\n对照对象是市场插件 gFur（GiM，自述 \u0026ldquo;Shell based fur plugin\u0026rdquo;，官方支持 UE 5.0~5.4、另有 UE 4.27 版；本地源码在 Source/GFur、Source/GFurEditor、Shaders/Private）10。\n10.1 设计对比 维度 UE Groom gFur 方案类型 真曲线毛发（strand-based） 发壳毛发（shell-based） 几何数据 控制线 + 可见发丝，逐根独立曲线 GrowMesh 顶点沿法线复制成 N 层壳（层数 1~128），顶点多带一个 FurOffset 与几组 UV 引导 控制线（Guides）承载形变与模拟 可选 FurSplines：可导入 ABC、程序生成，或用引导网格 梳理 导入 groom，再在编辑器里梳理/调物理 编辑器笔刷直接改 splines（长度/弯曲/簇/扭转/噪声/放松/增删） 驱动路径 三条路：物理求解 / Deformer / 蒙皮 骨骼运动 -\u0026gt; 顶点偏移（自定义 vertex factory），另支持 Morph Target 动态（\u0026ldquo;物理\u0026rdquo;） 逐根约束求解（XPBD / Cosserat 杆），带朝向，能表达弯曲与扭转 骨骼级近似：每帧算骨骼的线/角速度与偏移，沿发丝按分布曲线施加 自碰撞 有（体素速度/压力网格近似） 无 扭转 显式表达 无法表达 LOD 三种几何表示（曲线/发卡/网格）+ 曲线 LOD + 按页流送 每级壳层数 + 切 GrowMesh 的 LOD + 每级可单独关物理 光照 / 阴影 / 光追 专用 pass：体素间接光 + 深阴影 + 多次散射表 + 原生光追 走常规材质渲染，靠材质近似；无专用透光、深阴影、光追 参数面 大：资产 / 组件 / 求解器组件 / CVar 四级 小：组件 + 材质为主 10.2 性能对比 维度 UE Groom gFur 动态成本量级 曲线数 x 每根段数 x 迭代数 x 子步数 x 帧率 骨骼数 x 顶点数（约等于壳层数 x GrowMesh 顶点数） 渲染成本量级 曲线段数 x 屏幕占用（tile 数） 壳层数 x 顶点数 x 覆盖像素 主要瓶颈 求解吃曲线规模与迭代；渲染吃屏幕覆盖率 几何量主导（层数 x 顶点数），层数是最直接的旋钮 时间性 步长来自帧 delta（截断后按子步均分），对帧率敏感 每帧一次、由骨骼运动驱动；稳定，但没有\u0026quot;求解收敛\u0026quot;这回事 缩放手段 降曲线/段数、降迭代/子步、按距离裁剪、切发卡 降层数、切 LOD、关物理、用最小屏幕尺寸让它消失 平台 跨平台（Windows / macOS / Linux / 主机等） 开发平台仅 Windows（Win32/Win64）；目标平台 Win64（DX11）、PS4、Xbox One（可经 whitelist 扩展 Switch） 读法：Groom 的成本是\u0026quot;多项相乘\u0026quot;（曲线 x 段数 x 迭代 x 子步 x 帧率），gFur 的成本主要压在\u0026quot;几何量\u0026quot;（层数 x 顶点数）。让 gFur 变便宜，第一件事就是减层数；让 Groom 变便宜，要先判断瓶颈在求解侧还是渲染侧（见第 9 章的成本模型）。\n10.3 工程与集成对比 维度 UE Groom gFur 集成步骤 导入 groom、烘焙绑定、配置 Niagara 系统 加组件 + 指定 GrowMesh + 套材质 依赖 Niagara / ComputeFramework / Chaos / Dataflow / Deformer 仅 Core / Engine / RHI 等基础模块 与对方的关系 可接收 gFur 导出的 groom 可把 splines 导出给 UE Groom（示例内容自带 UE_Hair 资产） 10.4 怎么选 你的场景 建议 主角近景，要真实甩动、透光、光追 用 Groom 大量个体 / 远景 / 毛发团块，要省 用 gFur 既要近景真实又要群体便宜 混用：远景发壳、近景 Groom 设计侧重一句话：gFur 侧重\u0026quot;看起来像\u0026quot;——把钱花在分层观感（壳偏置、噪声、材质光照）上，动态用骨骼级近似换，参数故意做得很少、谁都能接；Groom 侧重\u0026quot;动得对\u0026quot;——把钱花在逐根约束、时间基准、自碰撞和光照/阴影/光追的正确性上，代价是集成复杂、参数面庞大。\n本章小结：两条路线的分野在\u0026quot;几何表示\u0026quot;与\u0026quot;动态粒度\u0026quot;：gFur 用发壳 + 骨骼级近似，换来极低的集成与性能成本，代价是无法表达扭转与自碰撞、不参与透光/深阴影/光追、且仅限 Windows 开发（目标平台覆盖 Win64 与主机）；Groom 用真曲线 + 逐根求解，换来物理正确与光照可扩展，代价是成本高、参数面大、集成重。二者互补：gFur 能把 groom 导出给 UE Groom，而 UE 自身也有发卡/网格这类廉价表示（8.1 节），所以\u0026quot;远景发壳、近景曲线\u0026quot;可以并存。\n第 11 章 附录 11.1 源码文件索引 层次 位置（相对引擎根） 说明 资产与形变 Engine/Plugins/Runtime/HairStrands/Source/HairStrandsCore/Public/（GroomAsset.h、GroomComponent.h、GroomInstance.h、GroomAssetPhysics.h、GroomAssetInterpolation.h） 资产、组件、每组实例、物理与插值设置 绑定 .../HairStrandsCore/Public/GroomBindingAsset.h、HairStrandsDatas.h、GroomBindingBuilder.h 发根投影、权重、构建器 求解 .../HairStrandsSolver/Public/GroomSolverComponent.h 求解器组件、动态曲线划分 求解数据接口 .../HairStrandsCore/Public/Niagara/（NiagaraDataInterfaceHairStrands.h、...VelocityGrid.h、...PressureGrid.h） 参数与网格资源交给 Niagara 求解 shader Engine/Plugins/Runtime/HairStrands/Shaders/Private/ 约束族、碰撞、外力 形变（Deformer） .../HairStrandsDeformer/Public/（DeformerGroom*） Optimus / DeformerGraph 读写通道 渲染 Engine/Source/Runtime/Renderer/Private/HairStrands/、.../Public/HairStrandsInterface.h 可见性、tile、体素化、深阴影、合成 渲染 shader Engine/Shaders/Private/HairStrands/ 光栅、散射、深透射率 11.2 控制台速查 分组 代表变量 默认 作用 几何表示 r.HairStrands.Strands / .Cards / .Meshes 1/1/1 三种画法开关（后两个需启动时开） 模拟 r.HairStrands.Simulation / .EnableAdaptiveSubsteps / .SimulationMaxDelay / .MaxSimulatedLOD 1/0/4/-1 总开关、自适应子步、重置缓冲、模拟 LOD 上限 阴影与照明 r.HairStrands.DeepShadow.Resolution / .KernelType / .DensityScale、r.HairStrands.SkyLighting 2048/2/2/1 深阴影分辨率与核、密度补偿、天光 可见性与光栅 r.HairStrands.Visibility.ComputeRaster / .MSAA.SamplePerPixel / .PPLL、r.HairStrands.RasterizationScale 0/8/0/0.5 光栅路径与采样、吸附光栅缩放 LOD 与流送 r.HairStrands.MinLOD / .AutoLOD.Bias / .Streaming.CurvePage / .StreamOutThreshold 0/0.1/2048/2 LOD 钳制与偏置、按页流送 内存 r.HairStrands.Strands.BulkData.ReleaseAfterUse / .AsyncLoading / .Validation 0/-1/1 用完释放、异步加载、加载校验 光追 r.HairStrands.Raytracing / .RaytracingProceduralSplits / .RaytracingRadiusScale 1/4/0 光追开关与两个调优项 体素化 r.HairStrands.Voxelization / .DensityScale / .Virtual.VoxelPageResolution 1/2.0/32 体素化总开关、密度、页分辨率 包围盒 r.HairStrands.BoundMode 0 运行时包围盒模式（0/1/2） 构建期 r.HairStrands.ClusterBuilder.MaxVoxelResolution / .HairGroupBuilder.MaxVoxelResolution 256/64 聚类与组索引构建分辨率 调试 r.HairStrands.ViewMode / .Dump / .BindingValidation —/—/0 视图模式、数据打印、绑定校验 基于 UE 5.6 源码观察、待高版本复核的\u0026quot;字段/提示与行为不一致\u0026quot;情况（以下结论只对当前所见 5.6 成立，不保证高版本仍然如此，作者会在更高版本复核）：FGroomSolverSettings::SimulationFrameRate——字段存在，但全项目搜索仅命中声明与 UHT 反射生成、无业务引用，蓝图亦搜不到，判为\u0026quot;未被使用\u0026quot;；FHairSolverSettings::SubSteps——提示写 \u0026ldquo;solver calls done at 24 fps\u0026rdquo;，但源码里没有固定 24fps 步长的实现（求解仍按帧 delta），字段本身有效；r.HairStrands.SwapType——提示写 Default:1，实际默认值疑似为 2，待源码复核。\n11.3 参考 [1]《计算机图形学基础》(Fundamentals of Computer Graphics, 5th ed.)：曲线表示， 1.2 节。 [2]《计算机图形学基础》：计算机动画， 4.1 节。 [3] Position Based Dynamics（2007）： 2.1 节 / 4.1 节。 [4] XPBD: Position-Based Simulation of Compliant Constrained Dynamics（2016）： 2.2 节 / 2.3 节 / 4.3 节。 [5] There Is More Than One Way to Frame a Curve（1975）： 1.4 节。 [6] Strands: Interactive Simulation of Thin Solids using Cosserat Models（2002）： 2.5 节 / 3.1 节。 [7] Discrete Elastic Rods（2008）： 1.2 节 / 2.4 节。 [8] Light Scattering from Human Hair Fibers（2003）： 8.3 节。 [9] UE 5.6 引擎源码（HairStrands 插件、渲染器毛发 pass 集、着色器目录）：以文件名 / 类名 / 函数名定位。 [10] gFur（GiM，Shell based fur plugin，官方支持 UE 5.0~5.4、另有 UE 4.27 版）。 ","date":"2026-09-28T00:00:00Z","permalink":"/docs/unreal/ue-groom/","title":"UE 毛发系统 Groom：从物理模型到引擎实现"},{"content":"本文是对《CUDA Programming Guide》的阅读整理。\n1. 阅读路径 CUDA 的知识点彼此关联：不懂 SIMT 就理解不了分支分歧的代价，不懂内存层次就理解不了合并访问，不懂内存模型就写不对跨块同步。因此建议按下面的依赖顺序阅读，而不是按 API 字母序查。\nflowchart TD A[\"执行模型块/网格/warp/SIMT\"] --\u003e B[\"索引与边界threadIdx/blockIdx\"] B --\u003e C[\"内存体系共享/全局/寄存器\"] C --\u003e D[\"数据放置统一内存 vs 显式管理\"] A --\u003e E[\"异步执行流/事件/Graphs\"] C --\u003e F[\"同步与内存模型屏障/原子/作用域\"] E --\u003e F F --\u003e G[\"特性专题TMA/PDL/协同组/图\"] C --\u003e H[\"性能准则利用率/访存/指令\"] E --\u003e H2. 异构执行模型 2.1 为什么是 CPU + GPU CPU 与 GPU 的设计目标不同，芯片资源分配因此分道扬镳 [1][2]：\n维度 CPU GPU 设计目标 让单条线程尽量快 让数千条线程同时在跑（吞吐优先） 并行规模 数十条线程 数千条线程 晶体管去向 大缓存与复杂流控 数据处理单元（浮点等） 隐藏延迟的手段 缓存 + 流控 用海量并行掩盖访存延迟 GPU 用更多晶体管\u0026quot;算\u0026quot;，而不是\u0026quot;等\u0026quot;。但真实应用总是并行段与串行段的混合，所以系统形态是 CPU + GPU 各司其职：CPU 负责控制流与串行逻辑，GPU 负责可并行的密集计算。\n2.2 硬件层次：SM 与 GPC GPU 由若干流多处理器（SM）组成，SM 再组成图形处理簇（GPC）。 每个 SM 内含寄存器堆、统一数据缓存、若干功能单元；统一数据缓存同时为共享内存和 L1 缓存提供物理资源，两者的划分比例可以运行期配置。 各架构的 SM 规格（内存大小、功能单元数量）不同，但不影响编程模型的正确性——模型与硬件解耦。 2.3 网格、线程块与线程 一次 kernel 启动会启动成千上万条线程，它们的组织关系是三层结构 [2]：\nflowchart TD G[\"Grid（网格，1/2/3 维）\"] --\u003e B0[\"Block 0（块内线程同驻一个 SM）\"] G --\u003e B1[\"Block 1\"] G --\u003e B2[\"Block N-1\"] B0 --\u003e T0[\"Thread ...\"] B0 --\u003e T1[\"Thread ...\"]由此导出两条编程铁律：\n块内可协作：块内线程同驻一个 SM，可通过片上共享内存高效通信，并用 __syncthreads() 同步。 块间无顺序保证：块的调度顺序（并发还是串行）不受保证，因此块与块之间不得有数据依赖，任何块顺序下结果都必须正确。 启动时通过执行配置指定网格与块的维度（\u0026lt;\u0026lt;\u0026lt;...\u0026gt;\u0026gt;\u0026gt;），可选参数还有动态共享内存字节数与流；簇大小、SM 配置等更复杂的项需用 cudaLaunchKernelEx 传递启动属性 [3][9]。\n2.4 warp 与 SIMT 块内线程以 32 条为一组构成 warp，warp 是硬件调度单位，以 SIMT（单指令多线程）方式执行。 warp 内每条线程有自己的指令计数器与寄存器状态，因此可以走不同分支；但硬件每次只能发射一条公共指令，于是不同路径被串行执行，其余线程被掩蔽——这就是分支分歧（divergence）。 推论：块内线程数最好是 32 的倍数，否则最后一个 warp 存在空闲 lane，浪费功能单元与访存带宽。 SIMT 与 SIMD 的区别：SIMD 只有单一控制流且数据宽度固定；SIMT 允许每线程独立路径，无固定数据宽度 [2]。 2.5 线程块簇（可选分组） Compute Capability 9.0（Hopper）及以上支持把若干相邻块组成簇 [2][15]：\n指定簇不改变网格维度与块索引；簇内所有块同驻一个 GPC，并被一起调度。 簇内块可以互相访问彼此的共享内存，形成分布式共享内存（DSMEM），并用 Cooperative Groups 提供的接口做跨块同步。 簇的可移植最大尺寸为 8 个块，实际上限由硬件决定。 2.6 计算能力（CC） CC 是设备能力版本号，决定了块内最大线程数、共享内存上限、寄存器数量以及是否支持簇、TMA 等特性 [26]。编译期用虚拟架构与真实架构两个维度控制产物形态，运行期可用 cudaGetDeviceProperties 查询。\n架构 CC 关键新增 Volta 7.0 独立线程调度、Tensor Core Turing 7.5 光线追踪核心 Ampere 8.0 cp.async 异步拷贝、异步屏障 Hopper 9.0 TMA、线程块簇、DPX Blackwell 10.x / 12.x 新一代 Tensor Core 与新数据类型 3. Kernel、索引与边界 3.1 定义与启动 用 __global__ 声明 kernel，返回类型必须为 void；它由许多线程并行执行 [3]。启动时给出执行配置，前两个参数是网格维度与块维度，一维场景可以直接用整数：\n__global__ void vecAdd(float* A, float* B, float* C) { } // 1 个块、256 条线程 vecAdd\u0026lt;\u0026lt;\u0026lt;1, 256\u0026gt;\u0026gt;\u0026gt;(A, B, C); 多维场景用 dim3；完整形式还包含动态共享内存字节数与流：\nkernel\u0026lt;\u0026lt;\u0026lt;gridDim, blockDim, sharedMemBytes, stream\u0026gt;\u0026gt;\u0026gt;(args...); 两点常被忽略：\n每块线程数上限为 1024（资源允许时一个 SM 可同时驻留多个块）。 kernel 启动相对 host 是异步的：host 不会等它开始或完成就继续执行，需要结果时必须显式同步 [3][6]。 3.2 索引计算 kernel 内通过内置变量确定自己的位置 [3][4]：\n变量 含义 值域 threadIdx 块内线程索引（.x/.y/.z） 0 .. blockDim-1 blockDim 块维度 启动时指定 blockIdx 块在网格中的索引 0 .. gridDim-1 gridDim 网格维度 启动时指定 一维并行最常见的线性化方式：\nint workIndex = threadIdx.x + blockDim.x * blockIdx.x; C[workIndex] = A[workIndex] + B[workIndex]; 多维块与网格只是方便表达，本身不影响性能；线程线性化规则是 x 变化最快、y 步长为 blockDim.x、z 步长为 blockDim.x * blockDim.y，这条规则决定了线程如何被划分进 warp [4]。\n3.3 边界检查与块数计算 当数据长度不是块大小的整数倍时，必须做边界检查，并把块数上取整：\n__global__ void vecAddChecked(float* A, float* B, float* C, int n) { int i = threadIdx.x + blockDim.x * blockIdx.x; if (i \u0026lt; n) { C[i] = A[i] + B[i]; } } int threads = 256; int blocks = (n + threads - 1) / threads; // 上取整 vecAddChecked\u0026lt;\u0026lt;\u0026lt;blocks, threads\u0026gt;\u0026gt;\u0026gt;(devA, devB, devC, n); CCCL 提供了 cuda::ceil_div（头文件 \u0026lt;cuda/cmath\u0026gt;）替代手写上取整 [32]。多启动几个\u0026quot;不做事的线程\u0026quot;代价很小，但应避免启动整块都无工作的块；256 线程/块是一个稳妥的起点。\n3.4 一个完整可运行的示例 把前几节拼起来，就是下面这个能编译、能自校验的最小程序。语法上它就是 C++，但每一处与标准 C++ 不同的地方，都在注释里标了出来——差异集中在三点：执行空间、执行配置 \u0026lt;\u0026lt;\u0026lt;...\u0026gt;\u0026gt;\u0026gt;、以及指针属于哪一侧的内存。\n// CUDA C++ 的\u0026#34;非标准\u0026#34;之处：这段代码有三处是标准 C++ 里不存在的写法—— // 执行空间说明符（__global__）、执行配置（\u0026lt;\u0026lt;\u0026lt;...\u0026gt;\u0026gt;\u0026gt;）、设备内存 API（cuda*）。 #include \u0026lt;cstdio\u0026gt; #include \u0026lt;cstdlib\u0026gt; #include \u0026lt;cuda_runtime.h\u0026gt; // CUDA 的 API 是 C 风格接口：返回错误码，不像标准 C++ 那样抛异常， // 所以每个调用都要自己检查返回值，否则错误会一路静默传递。 #define CUDA_CHECK(call) \\ do { \\ cudaError_t err = (call); \\ if (err != cudaSuccess) { \\ std::fprintf(stderr, \u0026#34;%s:%d %s\\n\u0026#34;, __FILE__, __LINE__, \\ cudaGetErrorString(err)); \\ std::exit(EXIT_FAILURE); \\ } \\ } while (0) // __global__ 是执行空间说明符：标准 C++ 没有这个概念。 // 它表示\u0026#34;编译为 GPU 代码、由 host 启动、在 device 上执行\u0026#34;，且必须返回 void。 __global__ void vecAdd(const float* A, const float* B, float* C, int n) { // threadIdx / blockDim / blockIdx 不是标准库变量，而是编译器内置变量： // 语法与普通变量一样，但每条线程各有一份，用它算出自己的唯一编号。 int i = threadIdx.x + blockDim.x * blockIdx.x; // 块大小通常不能整除数据量，最后一块里的部分线程会越界，必须挡住。 if (i \u0026lt; n) { C[i] = A[i] + B[i]; // 这里的指针指向显存，解引用由 GPU 完成 } } int main() { // 从这里往下是 host 代码，由 CPU 执行，写法与普通 C++ 一样。 // 但 host 与 device 的内存是分开的：malloc / new 只对 host 侧有效。 const int n = 1 \u0026lt;\u0026lt; 20; // 1M 个元素 const size_t bytes = n * sizeof(float); float* hA = static_cast\u0026lt;float*\u0026gt;(std::malloc(bytes)); float* hB = static_cast\u0026lt;float*\u0026gt;(std::malloc(bytes)); float* hC = static_cast\u0026lt;float*\u0026gt;(std::malloc(bytes)); for (int i = 0; i \u0026lt; n; ++i) { hA[i] = 1.0f; hB[i] = 2.0f; } // 为什么非要手写这些拷贝？因为 GPU 有自己的显存，与 host 的系统内存是 // 两块物理内存：kernel 跑在 device 侧，只能访问设备内存。 // 把 host 指针当普通 C++ 指针用（在 device 上解引用）不是\u0026#34;慢一点\u0026#34;， // 而是非法访问。所以流程必然是：分配设备内存 -\u0026gt; 拷进去 -\u0026gt; 启动 -\u0026gt; 拷回来。 float *dA = nullptr, *dB = nullptr, *dC = nullptr; CUDA_CHECK(cudaMalloc(\u0026amp;dA, bytes)); CUDA_CHECK(cudaMalloc(\u0026amp;dB, bytes)); CUDA_CHECK(cudaMalloc(\u0026amp;dC, bytes)); // cudaMemcpy 的参数是 C 风格 void*，看不出方向，所以要显式指定 // cudaMemcpyHostToDevice；若系统支持统一虚拟地址空间，也可传 // cudaMemcpyDefault 让驱动按指针推断（见第 5 节）。 CUDA_CHECK(cudaMemcpy(dA, hA, bytes, cudaMemcpyHostToDevice)); CUDA_CHECK(cudaMemcpy(dB, hB, bytes, cudaMemcpyHostToDevice)); // \u0026lt;\u0026lt;\u0026lt;blocks, threads\u0026gt;\u0026gt;\u0026gt; 不是 C++ 语法，而是 nvcc 的执行配置扩展： // 标准 C++ 里没有\u0026#34;一次调用启动 N 个线程\u0026#34;的表达方式。 const int threads = 256; const int blocks = (n + threads - 1) / threads; vecAdd\u0026lt;\u0026lt;\u0026lt;blocks, threads\u0026gt;\u0026gt;\u0026gt;(dA, dB, dC, n); // 关键差异：启动是异步的。这一行返回时 kernel 可能还没开始执行， // 与标准 C++\u0026#34;函数返回即执行完毕\u0026#34;的直觉相反，因此错误分为两处查。 CUDA_CHECK(cudaGetLastError()); // 捕获启动期错误 CUDA_CHECK(cudaDeviceSynchronize()); // 等待执行完成 CUDA_CHECK(cudaMemcpy(hC, dC, bytes, cudaMemcpyDeviceToHost)); bool ok = true; for (int i = 0; i \u0026lt; n \u0026amp;\u0026amp; ok; ++i) { ok = (hC[i] == 3.0f); } std::printf(\u0026#34;result: %s\\n\u0026#34;, ok ? \u0026#34;PASS\u0026#34; : \u0026#34;FAIL\u0026#34;); // 设备内存必须显式释放；host 内存照旧走标准 C++ 的 free。 CUDA_CHECK(cudaFree(dA)); CUDA_CHECK(cudaFree(dB)); CUDA_CHECK(cudaFree(dC)); std::free(hA); std::free(hB); std::free(hC); return 0; } nvcc -O2 -arch=native vector_add.cu -o vector_add ./vector_add 为什么这段代码必须手写两次 cudaMemcpy：CUDA 不只是\u0026quot;给 C++ 加几个关键字\u0026quot;，它把一台机器的内存劈成了两半，而两半之间不会自动搬运数据。\nhost 侧（CPU + 系统内存） device 侧（GPU + 显存） hA / hB / hC dA / dB / dC 由 malloc 分配 由 cudaMalloc 分配 | ^ +---- cudaMemcpy(..., HostToDevice) -------+ +---- cudaMemcpy(..., DeviceToHost) -------+ kernel 在 device 上执行，只能访问设备内存。把 host 指针传进 kernel 再解引用，不是\u0026quot;慢一点\u0026quot;，而是非法访问（报 invalid memory access）。 所以\u0026quot;数据先在 GPU 能访问的内存里\u0026quot;是硬约束，不是风格选择。标准 C++ 里的引用、指针、std::copy 全部只在同一地址空间内成立，跨不过这条边界。 cudaMemcpy 的第 4 个参数正是为这件事服务的：接口是 C 风格的 void*，看不出方向，所以要显式写 cudaMemcpyHostToDevice 或 cudaMemcpyDeviceToHost。系统支持统一虚拟地址空间时，也可以传 cudaMemcpyDefault 让驱动按指针自动推断（见第 5 节）。 想彻底省掉这份手写拷贝，可以改用托管内存（cudaMallocManaged），由驱动按需迁移；代价是数据放置不可控、性能更依赖系统能力。两种路线的取舍在第 5 节有对照表。 四点值得留意：\ncudaGetLastError() 要紧跟启动调用，它把\u0026quot;启动失败\u0026quot;（比如块尺寸越界）与\u0026quot;执行中出错\u0026quot;区分开；漏掉它，错误会漂移到后面某次同步调用上，排查时会指向错误的位置。 校验循环只是示意。真实工程应比较浮点误差，而不是要求逐位相等。 -arch=native 按本机 GPU 生成 Cubin；若要分发给别的机器，应改为生成 PTX 或显式列出多个目标（见第 9 节）。 中文 Windows 上还有一类经典陷阱：源文件是 UTF-8 而系统代码页是 GBK 时，MSVC 会把中文注释末尾的某些字节当成续行符，于是注释吞掉下一行代码，报出 identifier \u0026quot;bytes\u0026quot; is undefined 这种看起来无缘无故的错误。加 -Xcompiler /utf-8 即可（也可以把源文件保存为带 BOM 的 UTF-8）： nvcc -O2 -arch=native -Xcompiler /utf-8 vector_add.cu -o vector_add 4. 内存体系 4.1 层次、作用域与生命周期 内存类型 作用域 生命周期 物理位置 寄存器 线程 线程 SM 本地内存 线程 线程 显存（逻辑私有，物理在全局） 共享内存 块 kernel SM 片上（与 L1 共享资源） 全局内存 网格 + host 应用 显存 常量内存 网格（只读） 应用 显存（带缓存） 访问代价按\u0026quot;距离\u0026quot;递增：寄存器最快，共享内存低延迟，全局内存容量大但延迟高 [2][4]。\nflowchart LR T[\"线程\"] --\u003e|\"私有\"| R[\"寄存器\"] T --\u003e|\"私有, 溢出时\"| L[\"本地内存 (全局)\"] T --\u003e|\"块内共享\"| S[\"共享内存\"] T --\u003e|\"网格可见\"| GM[\"全局内存\"] T --\u003e|\"只读广播\"| CM[\"常量内存\"]落到硬件上，一个 SM 的组织大致是这样：\nSM +-- 寄存器堆 每线程私有，编译器分配 +-- 统一数据缓存 物理资源可配置切分 | +-- 共享内存 块内共享，用户显式管理 | +-- L1 缓存 由硬件管理 +-- 功能单元 FP32 / INT / Tensor Core / 访存单元 ... +-- warp 调度器 每周期挑一个就绪 warp 发射指令 共享内存与 L1 抢的是同一块物理资源，这既是\u0026quot;多给共享内存能加速\u0026quot;的原因，也是\u0026quot;给过头反而变慢\u0026quot;的原因：L1 被挤小，命中率下降会把压力转移回全局内存。\n4.2 共享内存：静态与动态 __shared__ float staticArray[1024]; // 静态：编译期定长 kernel\u0026lt;\u0026lt;\u0026lt;grid, block, sharedBytes\u0026gt;\u0026gt;\u0026gt;(); // 启动时给出字节数 extern __shared__ float dynamicArray[]; // 动态：一个 kernel 只能有一个 动态共享内存每个 kernel 只能有一个；需要多块视图时，申请一个大数组自行切分，并注意对齐：\nextern __shared__ float array[]; short* a0 = (short*)array; // 2 字节对齐 float* a1 = (float*)\u0026amp;a0[128]; // 128*2 = 256 字节，满足 4 字节对齐 int* a2 = (int*)\u0026amp;a1[64]; 若切分位置未按目标类型对齐（例如把 float* 落在 254 字节偏移处），访问行为不正确 [4]。\n4.3 两种访存效率陷阱 未合并的全局访问：同一 warp 的 32 条线程若访问连续地址，硬件会把它们合并成尽可能少的内存事务；地址离散则事务数暴涨、吞吐骤降。设计 kernel 时应让 threadIdx.x 相邻的线程访问相邻地址 [4]：\nfloat v = data[threadIdx.x + blockIdx.x * blockDim.x]; // 合并访问 结构体数组（AoS）是这件事上最常见的坑：相邻线程访问 p[i].x 时地址间隔是 sizeof(Particle)，一个 128 字节的事务里只用得上一小部分；把字段拆成各自连续的数组（SoA）后，同一个 warp 的访问重新变得连续：\nstruct Particle { float x, y, z; }; Particle* p; // AoS：相邻线程的地址间隔 sizeof(Particle) float x = p[i].x; // 事务利用率低 float *px, *py, *pz; // SoA：每个字段各自连续 float x2 = px[i]; // 合并访问 共享内存 bank 冲突：共享内存按 bank 组织（当前架构 32 个 bank，相邻 4 字节映射到相邻 bank）。warp 内多条线程访问同一 bank 的不同地址时访问被串行化；所有线程访问不同 bank、或访问同一地址（广播）都不会冲突。数据重排（如先读入共享内存再转置）常用于把不可合并的访问转成可合并访问 [4]。\n4.4 寄存器、本地内存与占用率 寄存器由编译器分配，可用 -maxrregcount 或 __launch_bounds__ 限制每线程上限；限制过紧会导致寄存器溢出（spilling），值被写到全局内存再读回。 本地内存是逻辑私有、物理位于全局内存的存储，常用于索引不确定的数组、放不进寄存器的结构体以及溢出变量，访问代价与全局内存同级 [4]。 占用率 = SM 上活跃 warp 数 / SM 支持的最大 warp 数，受每线程寄存器数、每块共享内存量、块与线程数上限共同约束；资源不足以驻留至少一个块时 kernel 直接启动失败。它是\u0026quot;够用即好\u0026quot;的指标，不是越高越好——寄存器换并行度、共享内存换 L1 容量都需要权衡 [4]。 5. 数据放置：统一内存与显式管理 数据位于访问它的处理器本地内存时性能最佳。CUDA 给了两条路：让驱动自动搬迁（统一内存），或自己显式管理（cudaMalloc + cudaMemcpy）[7]。\n5.1 统一虚拟地址空间（UVA） 同一 OS 进程内，host 内存与所有 GPU 显存处在同一个虚拟地址空间。因此：\ncudaPointerGetAttributes() 可以从指针值反查内存位置（CPU 还是哪个 GPU）； cudaMemcpy* 的 cudaMemcpyKind 可传 cudaMemcpyDefault，由指针自动推断拷贝方向 [7]。 5.2 统一内存的四种范式 统一内存的行为取决于 OS、内核版本、GPU 与互联方式。用三个设备属性即可判定当前系统属于哪种范式 [7]：\n属性 含义 cudaDevAttrConcurrentManagedAccess 1 = 完整统一内存支持；0 = 受限支持 cudaDevAttrPageableMemoryAccess 1 = 全部系统内存都统一；0 = 仅显式托管分配统一 cudaDevAttrPageableMemoryAccessUsesHostPageTables 1 = 硬件一致性；0 = 软件一致性 int concurrentManagedAccess = 0, pageableMemoryAccess = 0, usesHostPageTables = 0; cudaDeviceGetAttribute(\u0026amp;concurrentManagedAccess, cudaDevAttrConcurrentManagedAccess, dev); cudaDeviceGetAttribute(\u0026amp;pageableMemoryAccess, cudaDevAttrPageableMemoryAccess, dev); cudaDeviceGetAttribute(\u0026amp;usesHostPageTables, cudaDevAttrPageableMemoryAccessUsesHostPageTables, dev); flowchart TD A{\"ConcurrentManagedAccess = 1 ?\"} --\u003e|否| R[\"受限支持Windows / WSL / Tegra\"] A --\u003e|是| B{\"PageableMemoryAccess = 1 ?\"} B --\u003e|否| E[\"完整支持仅显式托管分配\"] B --\u003e|是| C{\"UsesHostPageTables = 1 ?\"} C --\u003e|否| S[\"全分配 + 软件一致性页故障迁移 (HMM)\"] C --\u003e|是| H[\"全分配 + 硬件一致性ATS / NVLink C2C\"] 受限支持：托管内存先落在 CPU 内存，以较粗粒度迁移；GPU 活跃期间 CPU 不得访问；不允许超额订阅。 完整支持：托管内存通常分配在首次触碰它的处理器内存中，被其他处理器使用时按页（软件一致性）或缓存行（硬件一致性）迁移，允许超额订阅显存。 硬件一致性（Grace 系列 + NVLink C2C）：驻留在 GPU 的托管分配可被 CPU 直接访问而无需迁移，还支持 host 侧原生原子；但硬件一致性不会让 host 访问纯 GPU 分配（如 cudaMalloc 的结果）[7]。 托管内存的分配方式有 cudaMallocManaged、cudaMallocFromPoolAsync（池类型为托管）以及 __managed__ 全局变量。性能调优的两个抓手是 cudaMemAdvise（声明只读、偏好位置、将被谁访问）与 cudaMemPrefetchAsync（在 kernel 前异步预取，与计算重叠）[7]。\n5.3 取舍 维度 显式管理 统一内存 代码复杂度 高（每处拷贝都要写） 低（自动迁移） 性能可预测性 高（放置可控） 取决于系统能力与提示质量 适用场景 性能关键、数据流明确 快速开发、复杂数据结构、需要超额订阅 显式管理的典型流程是分配、上行拷贝、执行、下行拷贝、释放；异步拷贝指向 host 的缓冲区必须是页锁定内存，否则会退化为同步行为。页锁定内存用 cudaMallocHost / cudaHostAlloc 分配，用 cudaFreeHost 释放，也可用 cudaHostRegister 把已有内存注册为页锁定 [7]。\n6. 异步执行 6.1 可以并发的六件事 host 计算、device 计算、H2D 拷贝、D2H 拷贝、设备内拷贝、设备间拷贝，这些都可以彼此重叠 [6]。异步接口的语义是调用立即返回（可能在工作完成、甚至开始之前），因此需要结果时必须同步：\n同步风格 行为 代表接口 阻塞 调用阻塞直到完成 cudaDeviceSynchronize 非阻塞 立即返回并给出状态 cudaStreamQuery 回调 完成时执行注册的函数 cudaLaunchHostFunc 6.2 流与事件 流是一个命令队列：往里追加拷贝与 kernel，按入队顺序执行；不同流之间可以并发。事件是插入流的标记，用来表达依赖与计时 [6]。\ncudaStream_t stream; cudaStreamCreate(\u0026amp;stream); kernel\u0026lt;\u0026lt;\u0026lt;grid, block, 0, stream\u0026gt;\u0026gt;\u0026gt;(...); // 流内启动 cudaMemcpyAsync(dst, src, bytes, cudaMemcpyHostToDevice, stream); // 流内异步拷贝 cudaEvent_t start, stop; cudaEventCreate(\u0026amp;start); cudaEventCreate(\u0026amp;stop); cudaEventRecord(start, stream); cudaEventRecord(stop, stream); cudaStreamSynchronize(stream); float elapsedMs = 0.0f; cudaEventElapsedTime(\u0026amp;elapsedMs, start, stop); cudaEventDestroy(start); cudaEventDestroy(stop); cudaStreamDestroy(stream); 两个易错点：\n异步拷贝涉及 CPU 内存时要求页锁定（pinned），否则无法与其他工作重叠 [6]。 默认流与其他流之间存在隐式同步语义；用 cudaStreamNonBlocking 创建的流不与默认流隐式同步 [6]。 6.3 CUDA Graphs 流中每次 kernel 启动都要付出 host 侧设置开销；对短 kernel，这笔开销可能占据端到端时间的显著比例。图把工作流定义一次、实例化一次，之后重复启动的开销极低，并把整体结构交给 CUDA 系统去做跨操作优化 [14]：\n结构：节点是操作，边是依赖；节点依赖满足即可被调度。 构建：Graph API 逐节点构图，或用 Stream Capture 捕获一段流操作自动构图。 执行：cudaGraphInstantiate 实例化后 cudaGraphLaunch 反复启动；图优化的主要成本在实例化期。 更新：cudaGraphExecUpdate 可整图或逐节点更新参数与依赖，避免重新实例化。 6.4 程序化依赖启动（PDL） 普通流模型下，后继 kernel 要等前驱完成、host 才知道可以启动，存在 CPU 往返延迟。PDL 让前驱 kernel 的设备代码自己触发后继 kernel 启动，把 kernel 间依赖交给设备侧控制，从而省掉 CPU 往返 [9][16]。\n7. 同步与内存模型 7.1 块内屏障 __syncthreads() 阻塞块内所有线程直到全部到达，并保证此前所有内存访问在屏障后对块内线程可见 [4]：\n__global__ void blockReduce(const int* in, int* out) { __shared__ int shared[128]; shared[threadIdx.x] = in[blockDim.x * blockIdx.x + threadIdx.x]; __syncthreads(); // 到此为止所有写入对块内可见 if (threadIdx.x == 0) { int sum = 0; for (int i = 0; i \u0026lt; blockDim.x; ++i) sum += shared[i]; out[blockIdx.x] = sum; } } 若块内部分线程因分支未执行 __syncthreads()，行为未定义（可能死锁）。更细粒度的选择是 __syncwarp()（warp 内）、cuda::barrier（可被异步操作到达）与 Cooperative Groups 的组同步 [10][15][17]。\n7.2 原子操作 对全局或共享内存的原子读-改-写避免数据竞争，常用 atomicAdd/atomicSub、atomicExch、atomicMin/atomicMax、atomicInc/atomicDec、atomicCAS、位运算系列 [4]。注意：同一 warp 内对同一地址的原子操作会被串行化，且执行顺序不保证（每次读-改-写都会发生）。\n7.3 线程作用域 内存操作的可见范围与同步范围由线程作用域定义 [10][28]：\n作用域 可见/可同步范围 cuda::thread_scope_thread 仅本线程 cuda::thread_scope_block 同块线程 cuda::thread_scope_device 同一设备（同一内存同步域内） cuda::thread_scope_system 系统内所有线程（含 CPU 与其他 GPU） 它把\u0026quot;同步距离\u0026quot;的成本显式化：块内同步便宜、系统级同步昂贵，因此标准 C++ 那种\u0026quot;同步成本均匀\u0026quot;的假设在 CUDA 中不成立 [28]。\n7.4 一致性保证的层次 同步需求 所需手段 同块内线程 __syncthreads() 或 cuda::barrier 建立 happens-before 跨块/网格内 设备级作用域原子或栅栏（thread_scope_device），或 cooperative launch 跨设备/与 CPU 系统级作用域原子或栅栏（thread_scope_system）；跨内存同步域时还需额外的 fence [21] 内存序方面，cuda::atomic 支持 relaxed/consume/acquire/release/acq_rel/seq_cst，默认为 seq_cst [28]。\n7.5 出错时的典型症状 同步写错的麻烦在于它通常不报错，只表现为\u0026quot;结果偶尔不对\u0026quot;。按症状定位：\n症状 常见原因 正确写法 结果偶发错误，换个块尺寸又好了 读共享内存前漏了块内屏障 写入与读取之间加 __syncthreads() 程序偶发挂死 屏障落在分支里，部分线程到不了 把 __syncthreads() 移出条件分支，或改用 Cooperative Groups 的组同步 单块跑得对，多块结果错 依赖了块间执行顺序 用设备级原子或栅栏；需要网格级同步时用 cooperative launch CC 7.0 前后行为不一致 依赖了隐式 warp 锁步 用 __syncwarp() 显式同步 CPU 读到旧值 缺系统级栅栏就跨设备读写 用 thread_scope_system 的原子或栅栏 共同点：这几种写法在某一代硬件上可能\u0026quot;刚好能跑\u0026quot;，但它们依赖的是实现细节，不是内存模型给出的保证。\n8. 特性专题速览 下表是整个特性面的索引，按\u0026quot;需要知道完整细节时查官方页\u0026quot;的方式使用；每一项在官方指南都有独立章节 [1][13]。\n特性 一句话动机 入口 API / 关键字 Tile 编程模型 以块为单位写代码，把并行度交给编译器，更容易用上 TMA 与 Tensor Core __tile_global__ / __tile__ / ct::tile [5] Cooperative Groups 提供安全、面向未来的组同步与 scan/reduce 原语，替代手写脆弱的 warp 内同步 this_thread_block() / tiled_partition / cg::reduce [15] Programmatic Dependent Launch 前一 kernel 设备侧触发后一 kernel，消除 CPU 往返 cudaTriggerProgrammaticLaunchCompletion [16] 异步屏障 屏障可被异步操作\u0026quot;到达\u0026quot;，支撑生产者-消费者流水线 cuda::barrier [17] 流水线 多阶段异步搬运，stage count 支持预取多批数据 cuda::pipeline + cuda::memcpy_async [18] 异步数据拷贝 全局到共享的搬运不再经寄存器；TMA 支持多维张量、im2col、Swizzle cp.async（CC 8.0+）、TMA（CC 9.0+）[19] CUDA Graphs 定义一次、重复低开销启动，并可做跨操作优化 cudaGraphCreate / Stream Capture [14] 流序内存分配器 分配与释放按流排序，规避 CPU 侧分配锁与 use-after-free 竞态 cudaMallocAsync / cudaFreeAsync [1] L2 缓存控制 把 L2 的一部分留给频繁复读的数据 cudaLimitPersistingL2CacheSize + accessPolicyWindow [20] 内存同步域 跨域原子/栅栏需要额外 fence 才能保证全局排序 cudaDevAttrMemorySyncDomainCount [21] 虚拟内存管理 把物理分配与虚拟映射、访问权限分离，支持稀疏与自定义分配器 cuMemCreate/cuMemMap（仅 Driver API）[22] 进程间通信 跨进程共享显存与事件 cudaIpcGetMemHandle / cudaIpcOpenMemHandle [24] CUDA 动态并行 设备代码自行启动子网格，适合递归与不规则并行 设备侧启动（CDP2，CUDA 12.0 起默认）[23] 图形互操作 与 OpenGL/D3D/Vulkan 共享资源，渲染与计算互喂 cudaGraphicsGLRegisterBuffer 等 [25] 绿色上下文 对 GPU 功耗与频率做上下文级控制 cudaGreenCtxCreate [1] 惰性加载 把 cubin 加载推迟到首次使用，加快启动 CUDA_MODULE_LOADING=LAZY [1] Driver 入口点访问 在 runtime 程序里直接取 driver 函数指针 cudaGetDriverEntryPoint [1] 其中两条细节值得单独记：异步代理（async proxy）——TMA 这类批量搬运走 async proxy，其与普通访问之间不保证排序，需要 proxy fence；CUDA 13 起，__global__ 函数与设备变量默认内部链接，跨编译单元引用需要显式 extern [10][8]。\n9. 编译与工具链 nvcc 是编译器驱动：对外提供统一命令行，对内按阶段调用宿主 C++ 编译器、PTX 汇编器与 ptxas [8][31]。\nflowchart TD S[\"example.cu\"] --\u003e N[\"nvcc 分离 host / device 代码\"] N --\u003e H[\"host 代码 -\u003e 宿主 C++ 编译器 -\u003e 目标文件\"] N --\u003e P[\"device 代码 -\u003e PTX（每个虚拟架构一份）\"] P --\u003e C[\"ptxas -\u003e Cubin（每个真实架构一份）\"] H --\u003e L[\"链接：多份 PTX/Cubin 可打包进 Fatbin\"] C --\u003e L 扩展名 内容 .c / .cpp / .cc / .cxx 仅 host 代码 .h / .hpp / .hh / .hxx host / device / 混合 .cu CUDA 源文件（device + host） .cuh CUDA 头文件 架构控制（默认生成最低架构以最大化兼容性）[8]：\n选项 效果 -arch=compute_XY 只生成该虚拟架构的 PTX（可 JIT 前向兼容） -arch=sm_XY 生成 PTX + Cubin -arch=native 按本机 GPU 生成 Cubin（无 PTX） -arch=all 全部支持架构的 Cubin + 最新 PTX -gencode=arch=compute_XY,code=sm_XY 精确控制多目标 分离编译：默认是整体编译，使用 GPU 代码的编译单元必须自带全部设备代码。加 -rdc=true（别名 -dc）后设备代码可跨编译单元链接，组织更灵活、编译更快、二进制更小，代价是链接期优化机会减少——可用 LTO（-dlto）找回大部分性能 [8]：\nnvcc -dc -dlto -arch=sm_100 definition.cu -o definition.o nvcc -dc -dlto -arch=sm_100 example.cu -o example.o nvcc -dlto definition.o example.o -o program 调试与优化常用组合：-g（host 调试信息）、-G（设备调试信息，会抑制优化）、-lineinfo（行号信息，供 compute-sanitizer 与 Nsight 关联）、-res-usage（资源占用报告）、-Xptxas=-warn-spills（寄存器溢出告警）。性能分析交给 Nsight Compute（kernel 级）与 Nsight Systems（系统级时间线）[33][34]。\n10. 性能准则 官方优化策略可以归纳成三句话 [27]：\n1. 最大化利用率 (Maximize Utilization) 应用级: 用流/图重叠拷贝与计算 设备级: 数据常驻显存, 避免反复 H2D/D2H SM 级 : 用占用率找到块/线程平衡点 2. 最大化访存吞吐 (Maximize Memory Throughput) 减少 host-device 传输(最贵) 全局访问合并(coalesced) 复用数据放共享内存, 避免重复读全局 3. 最大化指令吞吐 (Maximize Instruction Throughput) 避免 warp 内分支分歧 使用内建快速数学函数 循环展开, 但权衡寄存器压力 设计动机值得点明：GPU 靠\u0026quot;海量并行 + 合并访存 + 指令级流水\u0026quot;掩盖延迟，所以优化目标不是让单线程更快，而是让整个 SM 的并行度、访存效率与指令效率同时最大化。反过来，任何把线程串起来、把访存打散、把分支岔开的重构，都会直接吃掉这三项收益。\n先量再调，否则改的大概率不是瓶颈。三个够用的入口：\n手段 看什么 怎么判断 nvcc -res-usage（或 --ptxas-options=-v） 每线程寄存器数、共享内存用量、是否溢出 出现 spill 说明寄存器不够，先放宽 __launch_bounds__ cudaOccupancyMaxActiveBlocksPerMultiprocessor 实际可驻留的块数 与理论上限差得多，说明受寄存器或共享内存限制 Nsight Compute [33] 计算与访存吞吐占比、warp stall 原因 访存吞吐接近峰值而计算占比低，先改访存；反之先改指令 用 Nsight Compute 时别一上来就看全部指标：先判断 kernel 处于\u0026quot;访存受限\u0026quot;还是\u0026quot;计算受限\u0026quot;，再顺着对应的那组指标往下看。\n11. 常见误解 误解 实际情况 占用率越高越快 占用率只是\u0026quot;够用即好\u0026quot;。提高它通常靠降寄存器或降共享内存，代价可能是溢出与重复访存，净效果可能更差 共享内存给得越多越快 共享内存与 L1 共用物理资源，给多了会挤小 L1；bank 冲突没解决时，多给也不会变快 cudaMemcpy 一定慢 慢的是不可重叠的同步拷贝。配合页锁定内存与多流，拷贝能与计算重叠，此时它不再是瓶颈 块数越多越好 块数不足会让 SM 空转，过多则调度与尾部开销上升。覆盖全部数据且能整除，基本就是合适的量 __syncthreads() 能同步整个网格 它只作用于当前块。跨块要用设备级原子或栅栏，网格级同步得用 cooperative launch 12. 结语 CUDA 的每条约束都能追溯到硬件：块内线程同驻一个 SM，所以共享内存可用、块内屏障便宜；块与块之间没有调度顺序，所以跨块同步只能靠全局内存或协同启动；同步成本随线程距离上升，所以线程作用域把这笔代价显式写进了代码。那些\u0026quot;在某一代硬件上刚好能跑\u0026quot;的写法——依赖 warp 锁步、反复重读全局内存、让同一 warp 走不同分支——都会在架构换代时暴露问题。\n优化的方向也只有一个：让并行度、访存效率、指令效率同时最大化。剩下的功课是把它落到工具链上：选对编译目标与分离编译策略，用流与图组织异步，需要新特性时按第 8 节的特性索引回查官方文档。\n12. 术语表 术语 含义 SM 流多处理器，块执行的物理单元 GPC GPU 处理簇，SM 的集合 warp 32 条线程组成的硬件调度单位 SIMT 单指令多线程执行模型 lane warp 中线程的位置（0-31） divergence warp 内分支路径不同导致的串行执行 coalesced access 同一 warp 访问连续地址，合并为少数内存事务 occupancy SM 上活跃 warp 数与理论上限之比 execution configuration 启动配置 \u0026lt;\u0026lt;\u0026lt;grid, block, smem, stream\u0026gt;\u0026gt;\u0026gt; PTX 虚拟指令集，与具体 GPU 无关的中间表示 Cubin / Fatbin 原生机器码 / 多架构打包容器 CC Compute Capability，设备能力版本号 UVA 统一虚拟地址空间 managed memory 统一内存托管分配，可自动迁移 pinned memory 页锁定主机内存，异步拷贝的前提 stream GPU 命令队列 event 流内标记与时间戳 CDP CUDA 动态并行 TMA Tensor Memory Accelerator DSMEM 分布式共享内存（簇内跨块访问） thread scope 内存操作的可见性与同步范围 async proxy 异步操作的内存访问代理 14. 参考 行内引用编号 [n] 对应下列条目。主文档为《CUDA Programming Guide》分章节版，另有经典版《CUDA C++ Programming Guide》保留传统章节编号，两者内容互补。\n链接与章节编号依据 2026 年 9 月的官方文档核对。文中带版本前提的结论（例如 CUDA 13 起设备符号默认内部链接、CDP2 的适用条件）以该版本为准，跨版本使用时请对照官方的变更说明。\n[1]: CUDA Programming Guide（总目录）— https://docs.nvidia.com/cuda/cuda-programming-guide/index.html [2]: CUDA Programming Guide, 1.2 Programming Model（语言无关的编程模型）— https://docs.nvidia.com/cuda/cuda-programming-guide/01-introduction/programming-model.html [3]: CUDA Programming Guide, 2.1 Intro to CUDA C++（kernel、启动、索引、内存、同步）— https://docs.nvidia.com/cuda/cuda-programming-guide/02-basics/intro-to-cuda-cpp.html [4]: CUDA Programming Guide, 2.3 Writing SIMT Kernels — https://docs.nvidia.com/cuda/cuda-programming-guide/02-basics/writing-cuda-kernels.html [5]: CUDA Programming Guide, 2.4 Writing Tile Kernels — https://docs.nvidia.com/cuda/cuda-programming-guide/02-basics/writing-tile-kernels.html [6]: CUDA Programming Guide, 2.5 Asynchronous Execution（流、事件、回调）— https://docs.nvidia.com/cuda/cuda-programming-guide/02-basics/asynchronous-execution.html [7]: CUDA Programming Guide, 2.6 Unified and System Memory — https://docs.nvidia.com/cuda/cuda-programming-guide/02-basics/understanding-memory.html [8]: CUDA Programming Guide, 2.7 NVCC: The NVIDIA CUDA Compiler — https://docs.nvidia.com/cuda/cuda-programming-guide/02-basics/nvcc.html [9]: CUDA Programming Guide, 3.1 Advanced CUDA APIs and Features — https://docs.nvidia.com/cuda/cuda-programming-guide/03-advanced/advanced-host-programming.html [10]: CUDA Programming Guide, 3.2 Advanced Kernel Programming（PTX、硬件实现、线程作用域）— https://docs.nvidia.com/cuda/cuda-programming-guide/03-advanced/advanced-kernel-programming.html [11]: CUDA Programming Guide, 3.3 The CUDA Driver API — https://docs.nvidia.com/cuda/cuda-programming-guide/03-advanced/driver-api.html [12]: CUDA Programming Guide, 3.4 Programming Systems with Multiple GPUs — https://docs.nvidia.com/cuda/cuda-programming-guide/03-advanced/multi-gpu-systems.html [13]: CUDA Programming Guide, 3.5 A Tour of CUDA Features（特性总览）— https://docs.nvidia.com/cuda/cuda-programming-guide/03-advanced/feature-survey.html [14]: CUDA Programming Guide, 4.2 CUDA Graphs — https://docs.nvidia.com/cuda/cuda-programming-guide/04-special-topics/cuda-graphs.html [15]: CUDA Programming Guide, 4.4 Cooperative Groups — https://docs.nvidia.com/cuda/cuda-programming-guide/04-special-topics/cooperative-groups.html [16]: CUDA Programming Guide, 4.5 Programmatic Dependent Launch and Synchronization — https://docs.nvidia.com/cuda/cuda-programming-guide/04-special-topics/programmatic-dependent-launch.html [17]: CUDA Programming Guide, 4.9 Asynchronous Barriers — https://docs.nvidia.com/cuda/cuda-programming-guide/04-special-topics/async-barriers.html [18]: CUDA Programming Guide, 4.10 Pipelines — https://docs.nvidia.com/cuda/cuda-programming-guide/04-special-topics/pipelines.html [19]: CUDA Programming Guide, 4.11 Asynchronous Data Copies（cp.async 与 TMA）— https://docs.nvidia.com/cuda/cuda-programming-guide/04-special-topics/async-copies.html [20]: CUDA Programming Guide, 4.13 L2 Cache Control — https://docs.nvidia.com/cuda/cuda-programming-guide/04-special-topics/l2-cache-control.html [21]: CUDA Programming Guide, 4.14 Memory Synchronization Domains — https://docs.nvidia.com/cuda/cuda-programming-guide/04-special-topics/memory-sync-domains.html [22]: CUDA Programming Guide, 4.16 Virtual Memory Management — https://docs.nvidia.com/cuda/cuda-programming-guide/04-special-topics/virtual-memory-management.html [23]: CUDA Programming Guide, 4.18 CUDA Dynamic Parallelism — https://docs.nvidia.com/cuda/cuda-programming-guide/04-special-topics/dynamic-parallelism.html [24]: CUDA Programming Guide, 4.15 Interprocess Communication — https://docs.nvidia.com/cuda/cuda-programming-guide/04-special-topics/inter-process-communication.html [25]: CUDA Programming Guide, 4.19 CUDA Interoperability with APIs（图形 API 互操作）— https://docs.nvidia.com/cuda/cuda-programming-guide/04-special-topics/graphics-interop.html [26]: CUDA Programming Guide, 5.1 Compute Capabilities — https://docs.nvidia.com/cuda/cuda-programming-guide/05-appendices/compute-capabilities.html [27]: CUDA C++ Programming Guide（经典版，含 Performance Guidelines 与传统章节编号）— https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html [28]: CUDA Programming Guide, 5.7 CUDA C++ Memory Model — https://docs.nvidia.com/cuda/cuda-programming-guide/05-appendices/cuda-cpp-memory-model.html [29]: CUDA Programming Guide, 5.4 C/C++ Language Extensions（说明符与内建函数）— https://docs.nvidia.com/cuda/cuda-programming-guide/05-appendices/cpp-language-extensions.html [30]: CUDA Programming Guide, 5.8 CUDA C++ Execution Model — https://docs.nvidia.com/cuda/cuda-programming-guide/05-appendices/cuda-cpp-execution-model.html [31]: nvcc 用户手册 — https://docs.nvidia.com/cuda/cuda-compiler-driver-nvcc/index.html [32]: CCCL / libcu++（cuda:: 命名空间扩展）— https://nvidia.github.io/cccl/ [33]: Nsight Compute（kernel 级性能分析）— https://developer.nvidia.com/nsight-compute [34]: Nsight Systems（系统级时间线分析）— https://developer.nvidia.com/nsight-systems [35]: PTX ISA 文档 — https://docs.nvidia.com/cuda/parallel-thread-execution/index.html ","date":"2026-09-14T00:00:00Z","permalink":"/docs/cuda/cuda-cpp-programming-guide/","title":"CUDA C++ 编程精要"},{"content":"站点骨架已就绪。本文是一篇示例文章，用于验证 Stack 主题的三项渲染能力；确认无误后可以直接删除本文件，然后在 content/post/ 下按同样结构新建文章。\n新建文章 在项目根目录执行：\nhugo new content post/my-post/index.md 新建的文件默认带有 draft: true，撰写完成后删除该行即可发布。\n代码高亮 圈定语言的代码块会自动高亮。\nstruct FRenderPass { FRDGTextureRef SceneColor; FRDGTextureRef DepthStencil; void AddPass(FRDGBuilder\u0026amp; GraphBuilder) const { // 中文注释可以正常显示 GraphBuilder.AddPass( RDG_EVENT_NAME(\u0026#34;BasePass\u0026#34;), Parameters, ERDGPassFlags::Raster); } }; Mermaid 图 使用 mermaid 作为代码块语言即可生成图，主题会在明暗两种配色下自动切换图例样式。\nflowchart LR A[\"Markdown 源码\"] --\u003e B[\"Hugo 构建\"] B --\u003e C[\"public/ 静态文件\"] C --\u003e D[\"Cloudflare Pages\"]数学公式 行内公式用 $...$ 包裹，例如 $E = mc^2$；块级公式用 $$...$$ 包裹：\n$$ \\int_{0}^{1} x^2 \\,\\mathrm{d}x = \\frac{1}{3} $$","date":"2026-09-14T00:00:00Z","permalink":"/p/welcome/","title":"开始写作"},{"content":"本文件位于 content/docs/rendering/sample.md，用来确认约定的三件事都生效：\n放在 content/docs/\u0026lt;分类\u0026gt;/ 下的 .md 文件会被自动转换成页面，无需任何额外配置； 子目录名即分类，/docs/ 索引页会把分类作为入口列出，点进去能看到该分类下的全部文档； front matter 里的 categories 与目录名保持一致，首页卡片上就会显示对应的分类标签。 写作时可直接使用的能力 代码高亮：\nvoid FRenderModule::StartupModule() { // 中文注释正常显示 } Mermaid 图：\nflowchart LR A[\"content/docs/分类/xxx.md\"] --\u003e B[\"Hugo 渲染\"] B --\u003e C[\"/docs/分类/xxx/\"]公式（math: true 时生效）：\n$$ O = \\sum_{i=1}^{n} L_i \\cdot f_r \\cdot G $$视频与图表：主题自带 bilibili、youtube、video 短代码，可直接在正文调用。\n","date":"2026-09-14T00:00:00Z","permalink":"/docs/rendering/sample/","title":"示例文档"}]