自由学习记录(129)

SphericalHarmonicsL2 中的 L2 代表 Level 2 (或 2nd Order ),即 二阶球谐函数

球谐函数(SH)通过一组系数来模拟环境光(通常是来自环境探针或光照探针的间接光)。

  • L0 (1个系数):仅表示环境光的平均亮度(无方向感,像个发光的球)。
  • L1 (4个系数):增加了基础的方向性(类似一个模糊的方向光,能分出明暗面)。
  • L2 (9个系数):增加了更细腻的明暗过渡和方向细节(Unity 标准的探针质量)。

Unity 中,SphericalHarmonicsL2 结构体用于存储这 9个系数 (通常是 RGB 三个通道,总共 27 个浮点数)。

当你调用 SampleSH() 时,它本质上是利用这 L2 阶的 9 个数据点,根据物体表面的法线方向,还原出该方向上应该受到的间接光颜色。

  • 主流标准:L2 是实时渲染中的"黄金标准"。它足以模拟出自然的漫反射光照(Diffuse GI),且计算开销极低。
  • 为什么不用 L3 或更高?:更高阶(如 L3, L4)虽然更精确,但计算量呈平方级增长,且对于平滑的漫反射材质来说,肉眼几乎看不出区别。

Spherical Harmonics L2 使用的 9个数据点(系数)

代表了球面空间中不同频率的特征球面

第 0 阶 (1个系数) :常数项。代表球面的平均亮度(Ambient 基调)。

1 阶 (3个系数) :线性项。代表 X、Y、Z 三个轴向上的光照差异(基础方向感)。

2 阶 (5个系数)二次方项。代表更复杂的对比度和方向细节(如四个斜角方向的明暗变化)。

虽然说是 9 个系数,但在 Unity 的 SphericalHarmonicsL2 结构体中,你会看到它存储了 27 个值。
这是因为光照是有颜色的,所以 R、G、B 每个颜色通道 都需要独立的 9 个 SH 系数。

在 Unity 的底层(尤其是针对移动端优化时),这 9 个系数并不是简单排列的。为了计算效率,Unity 通常将它们分为三部分传给 GPU:

  • SHAr: (R通道的前4个系数,包含 L0 和 L1)
  • SHAg: (G通道的前4个系数)
  • SHAb: (B通道的前4个系数)
  • SHBr, SHBg, SHBb: (三原色对应的 L2 阶剩下的 5 个系数)
  • SHC: (第9个残余系数,通常用于最后的微调)

直观理解
你可以把这 9 个数据点想象成一个 "可变形的球体"

  1. 开始是一个正圆球(L0)。
  2. 根据 L1 的 3 个系数,球体被拉长或推向某个方向。
  3. 根据 L2 的 5 个系数,球体表面被压出凹凸和棱角。
    最终这个"变形球"的形状,就代表了来自四面八方的光照强度。

  • SH :一种数据表示形式 ,不是单独的光照系统。主要用于表示低频环境漫反射 。Unity 里 ambient probe、light probe 本质上都大量用 SH。Unity Documentation+1

  • Baked GI :总称,表示把 GI 预先烘焙出来。它的结果可以落到 LightmapLight Probe 、Ambient Probe 等载体里。Unity Documentation+1

  • Light Probe :存的是空间中的间接漫反射光 ,主要给动态物体 或不能吃 lightmap 的物体。数据格式通常是 SH。Unity Documentation+1

  • Lightmap :存的是表面上的 baked lighting ,主要给静态物体 。本质是贴图,不是 SH。Unity Documentation+1

  • Reflection Probe :存的是环境反射信息 ,本质更接近 cubemap / prefiltered env map,主要给镜面反射,不是给 diffuse 漫反射。

SH 理解成"编码格式",而不是"某一种 probe"。

Baked GI 是什么层级

Baked GI 是上位概念。

它表示:场景的全局光照,尤其是间接光、bounce light、环境贡献,不在运行时现算,而是离线或预计算。

Unity 官方文档里,ambient probe、light probes、lightmaps 都属于这套烘焙体系的结果承载形式。Unity Documentation+1

所以关系是:

overflow-visible! 复制代码

Plain text
Baked GI
├─ Lightmap (表面上的 baked 结果)
├─ Light Probe (空间中的 baked 结果)
└─ Ambient Probe (全局默认环境 probe,SH)

而 Reflection Probe 通常和 GI 系统相关,但它主要服务于反射,不是 diffuse GI 的主载体。Unity Documentation+1

Lightmap:存"表面上的光"

Lightmap 存的是:

某块静态表面最终接收到的 baked lighting

包括常见的:

  • 间接光

  • baked 直射光(取决于模式)

  • AO/阴影信息的一部分

它的特征是:

  • 依附在几何表面 UV 上

  • 只适合静态物体

  • 本质是 texture,不是 SH

  • 精度高,细节高,可以有阴影边界和高频变化

Unity 官方文档明确区分了:
lightmaps 存的是"光打到场景表面"
light probes 存的是"光在场景空间中传播"Unity Documentation+1

所以你可以记成:

  • Lightmap = surface lighting cache

  • Light Probe = volumetric sample of indirect diffuse

Reflection Probe 经常和 Light Probe 混着记,但两者根本不是一类东西。

如果你从 shader 视角看,会更清楚。

7.1 SH / Light Probe / Ambient Probe

输出的是 diffuse 环境项:

overflow-visible! 复制代码

hlsl
float3 ambient = SampleSH(normalWS);

这个 ambient 背后来源可能是:

  • ambient probe

  • interpolated light probes

  • 某些 GI 系统提供给 shader 的 SH 数据

但形式上都可视为"SH diffuse ambient"。


7.2 Lightmap

通常是:

overflow-visible! 复制代码

hlsl
float3 bakedGI = SAMPLE_GI(lightmapUV, vertexSH, normalWS);

或对应管线包装函数。

如果对象有 lightmap,很多管线会优先用 lightmap 数据,而不是单纯从 probe 来。因为 lightmap 的表面分辨率更高。


7.3 Reflection Probe

通常是:

overflow-visible! 复制代码

hlsl
float3 refl = SAMPLE_TEXTURECUBE_LOD(_ReflectionCubemap, dir, mip);

或者通过管线封装函数拿反射向量采环境图。

这部分是 specular / IBL 线,不是 SampleSH 那条线。


8. 一个非常实用的对照表

你可以直接这么记:

名词 本质 主要存什么 主要给谁用 数据形式
SH 表示方法 低频方向光照函数 ambient/light probe 等 球谐系数
Baked GI 上位系统 预计算全局光照结果 静态和动态对象 多种载体
Lightmap 表面缓存 表面上的 baked lighting 静态物体 纹理
Light Probe 空间采样 空间中的间接漫反射 动态物体/LOD SH
Ambient Probe 全局默认 probe 场景级环境漫反射 默认环境光 fallback SH
Reflection Probe 反射环境缓存 环境镜面反射 有 specular 的材质 cubemap

这个表基本就把关系捋顺了。


9. 放到你关心的 stylized face / brow 上怎么理解

如果是眉毛、妆容、睫毛这种风格化元素:

  • 不想吃主光 N·L

  • 不想吃复杂阴影

  • 只想有一点环境存在感

那通常只会用:

overflow-visible! 复制代码

hlsl
float3 ambient = SampleSH(faceNormalWS);
color = baseColor * lerp(1.0, ambient, k);

这里吃的是 SH / probe 的 diffuse ambient

它不会用到:

  • Reflection Probe,因为眉毛一般不做镜面环境反射

  • Lightmap,因为这类通常是角色动态面部,不走静态 lightmap

  • 完整 Baked GI surface result,因为目标是"保设计色",不是物理正确

"很多 instance → 合并 draw call → FPS 提升"

那确实很像 Unity GPU Instancing / Indirect Draw

需要把问题从:

"如何减少 draw call"

升级为:

"如何把几何处理从 object-level pipeline 变成 cluster-level GPU pipeline"。

geometry 被拆成很多 cluster。

例如:

一棵树

15k triangles

被拆成:

≈ 200 clusters

每个 cluster:

≈ 64 triangles

然后 GPU 做:

frustum culling

cone culling

LOD

如果最后:

只有 40 clusters visible

那么 GPU 实际 raster:

40 × 64 ≈ 2560 triangles

而不是:

15000 triangles

这里的核心价值不是 draw call,而是:

triangle reduction。

一个真正合理的问题应该是:

传统 instance pipeline 的可见性粒度太粗。

object-level culling

→ geometry waste

而 cluster pipeline:

cluster-level visibility

这就是 Nanite / mesh shader pipeline 的核心。

一个更准确的问题描述可以是:

传统 instance-based rendering 中,可见性判断通常在 object-level 进行,导致大量不可见三角形仍然被 GPU 处理。如何通过 cluster-based geometry pipeline 实现细粒度 GPU culling,从而减少无效几何处理?

不是:

"减少 draw call"

而是:

"减少无效 triangle processing"。

这才是 Mesh Shader 的意义。

传统 pipeline:

object-level LOD

一棵树:

LOD0

LOD1

LOD2

整个模型切换。

而 cluster renderer 可以:

cluster-level LOD

近处 cluster:

LOD0

远处 cluster:

LOD2

这会进一步减少 triangles。

How can we reduce wasted triangle processing in large geometry scenes by performing cluster-level visibility on GPU?

而不是:

How to reduce draw calls.

所以如果一棵树:

15000 triangles

100k instances

即使使用 HISM:

GPU 仍然会 transform

overflow-visible! 复制代码

1.5B triangles

只是 draw call 从 100k 变成几百。

这就是为什么 instancing 并不能解决 几何规模问题

Nanite 解决的是:

geometry scalability

它的 pipeline大致是:

mesh

→ clusters (~128 triangles)

GPU:

cluster visibility

→ software raster occlusion

→ material pass

也就是说 Nanite 的核心是:

overflow-visible! 复制代码

cluster-level visibility

而不是 instancing。

所以 Nanite 的优势来自:

triangle reduction

例如:

原始 mesh:

10M triangles

屏幕上只需要:

200k triangles

Nanite 会只 raster 那 200k。

cluster renderer,而不是 shader,是正确的理解。

这确实不是:

Unity instancing

或 compute instancing

它是一个 renderer architecture change

demo 场景必须"人为制造瓶颈"。

因为如果你直接用 UE:

  • HISM

  • Nanite

  • LOD

  • culling

UE 已经做了大量优化。

结果就是:

你根本看不到 renderer 改进。

所以 demo 场景需要:

刻意关闭 UE 的一些系统。

例如:

  • 不使用 Nanite

  • 不使用 HISM

  • 不使用 LOD

然后让传统 pipeline 真的变重。

核心结论是HISM通过树状数据结构实现高效剔除,但仅适合植被等海量实例场景,室内或少量实例场景并不适用。与普通InstanceStaticMeshComponent的区别(支持不同LOD消失距离)树状结构:类似kdtree但分支因子默认16,通过FClusterNode管理实例序号和子节点编号,避免重组instance buffer;剔除时通过节点包围盒快速计算,UE4 4.22新增的动态合并实例功能。通过层级树结构减少每帧剔除计算量

通过层级树结构减少每帧剔除计算量是CPU端的核心工作​。HISM(Hierarchical Instanced Static Mesh Component)的层级树结构本质是一种空间加速算法,通过将海量实例(如植被)组织为类似kdtree的树状结构(默认分支因子为16),让CPU能快速定位并剔除不可见实例,从而降低每帧需要提交给GPU的渲染数据量。

具体来说,CPU通过以下步骤实现高效剔除:

  1. 层级包围盒管理:树中每个节点(FClusterNode)存储子节点编号和实例序号范围,并维护一个包围盒。当相机视角变化时,CPU从根节点开始遍历,通过判断节点包围盒是否与视锥体相交,快速排除完全不可见的子树,避免对每个实例单独检测。
  2. 批量遮挡查询:结合遮挡查询(Occlusion Query)技术,CPU会对节点包围盒进行批量可见性判断。例如,若一个节点的包围盒被其他物体完全遮挡,其下所有实例可直接剔除,无需逐个处理。
  3. 平衡计算负载:通过调整树的分支因子(如InternalBranchingFactor)和最小实例数量(MinInstancePerOcclusionQuery),CPU可在剔除精度与计算开销间找到平衡。例如,当节点实例数量较少时,直接渲染而非执行复杂查询。

这种设计的优势在于将O(n)的线性遍历优化为O(log n)的树状检索​,尤其在十万级以上实例场景中能显著降低CPU的每帧剔除耗时。但需注意,层级树构建和维护本身会占用额外内存,且仅适用于静态实例;对于动态物体或少量实例,其开销可能超过收益。

为何不直接在GPU端处理?因为GPU擅长并行计算密集型任务(如像素渲染),而遮挡剔除需要复杂的空间逻辑判断和动态决策,更适合CPU主导。不过,现代引擎也开始探索CPU与GPU协同剔除(如GPU生成Hi-Z深度图辅助CPU判断),但核心的层级树遍历和实例筛选仍由CPU完成。

用 foliage paint,UE 确实会自动使用 HISMHISM 仍然是 object-level pipeline。只减少 draw calls

ReSharper 是 JetBrains 做的一个 IDE 代码分析与重构插件 ,最早是给 Visual Studio 用的。它本质上不是编译器,也不是调试器,而是一套 静态分析 + 代码理解 + 重构工具层 。后来 JetBrains 把它的核心做成了独立 IDE ------ Rider

Visual Studio 本身有一套 IntelliSense 导航系统,例如:

  • Go To Definition F12

  • Go To Implementation Ctrl+F12

  • Find All References Shift+F12

  • Go To Symbol Ctrl+T

  • Peek Definition Alt+F12

这些和 ReSharper 的功能对应。

Visual Studio 的插件体系叫:

复制代码

VSIX extension

问题是:

  • API 非常复杂

  • UI 扩展困难

  • 编辑器 API 比较旧

  • 线程模型复杂

  • 调试难度大

很多开发者吐槽:

写 VS 插件比写普通应用还难。

而 VS Code 插件:

  • TypeScript

  • 几百行代码

  • 就能做一个完整插件

这也是为什么 几乎所有 AI coding 工具都先支持 VS Code

例如:

  • Copilot

  • Cursor

  • Codeium

  • Tabnine

  • Codex

全部先做 VS Code。

VS Code 用户规模更大(对 AI 工具更重要)

现在开发者市场:

复制代码

VS Code > JetBrains IDE > Visual Studio

VS Code 用户量是最大的。

AI 公司做 IDE 插件时会优先考虑:

复制代码

开发成本低
用户多
插件生态成熟

VS Code 完全符合。

S Code 是跨平台

VS Code:

复制代码

Windows
Linux
Mac

Visual Studio:

复制代码

主要是 Windows

AI 工具更希望:

复制代码

一套插件跑三平台

VS Code 更合适。

Rider安装没有难度,直接下一步到底就完事,不过你不得不安装vs2022,因为本质上Rider只是调用的msbuild来构建项目,后台编译器是.NET那一套,windows上就是这么的情况,免不了的(如果想用g++,mingw什么那路就绕太远了,不如直接用linux)。

Unity 是脚本热重载(managed runtime reload)
UE Live Coding 是 C++ 二进制补丁(native hot patch)

因此它们的能力范围不同。

先说 Unity 的行为。

Unity 的脚本是 C# + Mono / IL2CPP runtime。当你修改脚本时:

  1. 编译 C#

  2. 重新加载程序集

  3. 重新初始化脚本实例

这个过程叫 domain reload(可关闭)。所以 Unity 可以做到:

  • 修改函数实现

  • 修改字段

  • 修改逻辑

然后场景里的对象自动更新。因为对象本身是 托管对象,Unity 可以重新绑定。

所以你会看到:

写代码 → 保存 → Unity 自动编译 → 场景继续运行。

UE 不是这样。

UE 的代码是 C++ 原生代码。运行时已经加载的是:

复制代码

GameModule.dll
Engine.dll

这些二进制代码已经被操作系统加载到进程内存中。

C++ 没有类似 C# 的 runtime 可以重新加载函数。

Live Coding 的做法是:

  1. 编译修改过的 C++ 文件

  2. 生成新的 object code

  3. 生成 patch dll

  4. 把新的函数地址 覆盖旧函数入口

也就是:

复制代码

old function → jump → new function

因此 Live Coding 本质是 runtime function patching

UE 编辑器里按:

复制代码

Ctrl + Alt + F11

会触发:

复制代码

Live Coding compile

然后新的代码会被 patch 到运行中的 editor。

但这种机制有很多限制。

第一类限制:结构变化不安全

如果你修改了:

  • class layout

  • UPROPERTY

  • virtual table

  • struct size

Live Coding 很可能失败,或者产生未定义行为。

例如:

复制代码

C++
class AMyActor : public AActor
{
int A;
};

改成:

复制代码

C++
class AMyActor : public AActor
{
int A;
int B;
};

这种情况通常必须 重新编译并重启 editor

Unity 不会遇到这个问题,因为 C# 对象由 runtime 管理。

当你在 IDE 改了 C++代码,UE 不会自动编译。

Unity 是:

复制代码

保存脚本
→ Unity 自动编译

UE 是:

复制代码

保存C++
→ 什么也不会发生

你必须主动触发编译:

  • Ctrl + Alt + F11 → Live Coding

  • 或 UE 编辑器点 Compile

  • 或 VS Build

所以 Live Coding 只是触发编译的一种方式

Live Coding只适合修改函数逻辑

例如:

复制代码

C++
float GetSpeed ()
{
return 600.f ;
}

改成:

复制代码

C++
return 900.f ;

流程:

复制代码

Ctrl+Alt+F11
→ 编译 patch
→ 函数替换
→ 立刻生效

不需要重启 Editor。

改"类结构"是另一件事

例如:

原来:

复制代码

C++
class AMyActor : public AActor
{
int Health;
};

改成:

复制代码

C++
class AMyActor : public AActor
{
int Health;
int Armor;
};

这里改变了:

复制代码

sizeof(class)
member offset
memory layout

运行中的 Actor 已经在内存里:

复制代码

Actor instance

内存结构固定

Live Coding 不会重新创建这些对象

因此:

  • 新成员不会出现在已有实例

  • Blueprint 不会刷新

  • 可能甚至崩

通常需要:

复制代码

关闭 UE Editor
Build
重新打开

关卡/渲染数据的构建

快速打开和符号跳转
Ctrl + P:按文件名快速打开文件。
Ctrl + Shift + O:跳转到当前文件里的函数或符号。
Ctrl + T:在整个工程里搜索类、函数、变量。

这三个组合起来,可以完全替代传统 IDE 的项目浏览。

vscode多光标编辑

Alt + Click:在多个位置放光标

Ctrl + D:选择下一个相同单词

Ctrl + Shift + L:选择所有相同单词

这在改变量名、批量插入代码时非常高效。

代码折叠和结构导航

Ctrl + Shift + [ / Ctrl + Shift + ] 可以折叠或展开代码块。

配合侧边栏的 Outline 视图,可以快速查看当前文件的结构。对于大文件或 shader 特别有用。

VS Code 可以定义构建任务,比如编译 C++、运行脚本、打包项目。通过 tasks.json 可以配置,例如一个简单任务可以调用编译脚本或构建工具。之后只需要 Ctrl + Shift + B 就能执行。调试系统完全通过配置文件管理。插件提供功能

插件把功能注册成 command

命令面板负责调用这些 command命令面板本身只是一个 命令调度器,而大部分命令其实来自插件。

插件没有 UI 也能工作

很多插件其实 只有命令,没有界面

例如:

一个代码生成插件可能只提供:

复制代码

Generate Class
Generate Getter
Generate Constructor

用户通过命令面板触发。

跳转历史

有时候跳转多次后需要回退:

  • Alt + ← 返回上一个位置

  • Alt + → 前进

类似浏览器的前进后退。

查找引用(Find References)

快捷键 Shift + F12

可以查看某个函数或变量被哪些地方调用。

例如:

复制代码

C++
ApplyDamage ()

会列出所有调用位置。

查看定义(Peek Definition)

快捷键 Alt + F12跳转到定义(Go to Definition)

快捷键 F12

当光标在函数或变量上时,可以跳到它的定义位置。

时候跳转多次后需要回退:

  • Alt + ← 返回上一个位置

  • Alt + → 前进

类似浏览器的前进后退。

Ctrl + P 后输入:

复制代码

Plain text
:120

可以直接跳到第 120 行。

或者:

复制代码

Plain text
@functionName

可以跳到当前文件中的函数。

例如:

  • C++:clangd 或 C/C++ extension

  • C#:C# Dev Kit / OmniSharp

  • Python:Python extension

如果没有语言服务器,VS Code 只能做文本搜索,而无法理解代码结构。

VS Code 的搜索和跳转主要依赖三种机制:

  • 文件索引(Ctrl+P)

  • 语言服务器符号索引(F12、Ctrl+T)

  • 文本搜索(Ctrl+Shift+F)

alt+←

回退跳转,,,这样的东西我居然没有早就知道,,,aaaa

查找引用(Find References)

快捷键 Shift + F12

可以查看某个函数或变量被哪些地方调用。

例如:

复制代码

C++
ApplyDamage ()

会列出所有调用位置。

改成"像文件快速打开那种模糊匹配"

这种更像 Ctrl+P 的 Quick Open,不是普通全文搜索。

VS Code 的 Search 本身主要是文本搜索,不是完全意义上的"任意模糊检索器"。它更偏:

  • 关键字搜索
  • 正则
  • 大小写
  • 全词匹配
  • 文件包含/排除

在 VS Code 里,Ctrl+P 对应的命令本质上是:

  • Quick Open
  • 用来按文件名做模糊查找并打开文件

所以你把它理解成:

  • P = path
  • 或者 P = project file lookup

会比较好记,但这更多是记忆法,不是官方定义。

  • Go to Symbol in Editor...

    这是你最常用的那个。

    作用:在当前打开文件里跳转到符号,比如函数、变量、类、方法、shader 里的结构项。

    可以理解成"当前文件内大纲搜索"。

  • Go To Symbol in Accessible View

    这是无障碍视图里的版本。

    作用:给屏幕阅读器/辅助访问模式用,在 Accessible View 这个特殊界面里跳转符号。

    普通使用基本碰不到,可以几乎忽略。

  • Terminal: Open Detected Link...

    这是终端里的命令。

    作用:当焦点在 VS Code 内置终端时,打开终端里识别到的链接,比如文件路径、URL、报错跳转位置。

为什么它们能共用 Ctrl+Shift+O:

因为 when 条件不同。

  • 在编辑器聚焦时,触发 Go to Symbol in Editor
  • 在无障碍视图聚焦时,触发 Accessible View 那条
  • 在终端聚焦时,触发 Terminal: Open Detected Link

所以它们不是硬冲突,而是"同键不同场景"。

你可以把它粗暴记成:

  • 编辑器里:Ctrl+Shift+O = 当前文件符号搜索
  • 终端里:Ctrl+Shift+O = 打开终端识别出来的链接
  • 无障碍视图里:给辅助功能用

对你这个 shader 场景还要补一句:

  • .shader / .hlsl 的"定义、引用、符号"能力,取决于 VS Code 对这类语言的支持插件
  • 所以 Ctrl+Shift+O 往往只能给你"部分结构化符号"
  • Shift+F12 在 shader 文件里不一定像 C# 那么准
  • 很多时候反而 Ctrl+F / Ctrl+Shift+F 更直接
相关推荐
Shining05962 小时前
Triton & 九齿系列《Triton 练气术》
开发语言·人工智能·python·学习·其他·infinitensor
今天你TLE了吗3 小时前
JVM学习笔记:第七章——对象实例化、内存布局&访问定位
java·jvm·笔记·学习
悠哉悠哉愿意3 小时前
【物联网学习笔记】串口发送
笔记·物联网·学习
载数而行5203 小时前
QT前置2 可视化文件,QRC文件两种处理
c++·qt·学习
云边散步3 小时前
godot2D游戏教程系列二(9)
笔记·学习·游戏·游戏开发
观书喜夜长3 小时前
每日一练:攻防世界「easyupload文件上传漏洞」详细解析与防御
学习·web安全·网络安全
廋到被风吹走3 小时前
持续学习方向 低代码/平台工程
学习·低代码
·中年程序渣·3 小时前
Spring AI Alibaba入门学习(一)
人工智能·学习·spring
Engineer邓祥浩3 小时前
JVM学习笔记(1) 总述
jvm·笔记·学习