[{"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":"示例文档"}]