自由学习记录(113)

λ(lambda)不是一个"新物理量",而是 Smith 遮蔽模型里的一个中间统计函数

它的意义可以一句话概括:

λ(v) 表示:在给定观察/光照方向 v 下,一个微表面法线被其它微表面遮挡的"期望遮挡程度"。

λ 的物理含义(直观层)

在微表面模型里,你假设表面是由大量微小镜面(microfacets)组成的。

当你从方向 v 看过去,并不是所有"朝向合适"的微表面都能被看到:

  • 有些被旁边更高的微表面挡住(masking)

  • 有些在入射方向上被挡住(shadowing)

λ(v) 表示:沿方向 v,平均有多少"潜在可见微表面"被挡掉了。

  • λ = 0:几乎无遮挡(法线接近正对)

  • λ 大:遮挡严重(grazing 视角)

它不是概率本身,而是一个用于构造概率的统计量

"从方向 v 看过去,微表面坡度在该方向上的平均陡峭程度是多少。"

λ 是连接"法线统计 (D)"与"可见性 (G)"的桥梁。

各向异性粗糙度在切线平面上本质是一个椭圆尺度张量,而它的两个主轴参数自然就记为 αx、αy

为什么是 x/y?

因为在推导里,切线空间通常写成局部坐标:

  • x 轴:tangent(T)

  • y 轴:bitangent(B)

  • z 轴:normal(N)

所以各向异性参数写成:

  • αx:沿 x(切线)方向

  • αy:沿 y(副切线)方向

这是局部坐标系约定,不是世界坐标。

历史来源:Walter / Heitz / Disney 统一命名

从各向异性 GGX 的经典文献开始,符号就是:

  • αx, αy:椭圆分布尺度

  • Λ(v):Smith 遮蔽统计

  • D(h):NDF

例如:

  • Walter et al. 2007(GGX/Trowbridge-Reitz)

  • Heitz 2014(Smith Joint / VNDF)

  • Disney BRDF 2012(anisotropic roughness)

这些论文都采用 αx、αy 作为主轴参数。

所以引擎代码沿用:

  • Unreal:AlphaX / AlphaY

  • Unity HDRP:roughnessT / roughnessB → alphaT / alphaB

  • Filament:at, ab

ax/ay 是最短的工程写法。

椭圆度量(anisotropic metric)

切线平面变成椭圆缩放:

所以 denom 就是:把 h 在切线平面按 αx/αy 拉伸后,再计算它的平方长度

物理意义:它控制"这种法线出现的概率"

NDF 的作用是:

给定一个半程向量 h,这种微表面法线出现的概率密度是多少?

  • 如果 h 落在椭圆主轴方向上,并且对应粗糙度大 → denom 小 → D 大

  • 如果 h 落在细轴方向上 → denom 大 → D 小

因此:

  • denom 越小,高光越集中(概率密度越高)

  • denom 越大,高光越弱(概率密度越低)

平方让分布衰减更慢,比 Beckmann 更"长尾",这就是 GGX 更真实的原因(尤其 grazing)。

为什么还要加 NoH²

很多人第一次看会疑惑:

为什么切线项除以 α²,而法线项不除?

denom 是"半程向量在各向异性粗糙度椭圆下的距离平方",决定 D(h) 的衰减。

denom 和 Lambda 里的 tan2

同一个椭圆度量在 h-space 与 v-space 的两个投影版本--->GGX D 和 Smith G 能闭合

  1. "h-space 投影"(半程向量 H 在 T/B/N 上的投影)

    这是给 D(h) 用的,也就是你 D_GGX_Aniso() 的这段:

  • ToH = dot(T,H), BoH = dot(B,H), NoH = dot(N,H)

  • denom = (ToH²)/αx² + (BoH²)/αy² + NoH²

  • D = 1 / (π αx αy denom²)

直观含义:把 H 看成一个方向,问"这个 H 在椭圆分布下出现的概率密度是多少"。所以它天然是 "h-space"。

  1. "v-space 投影"(视线 V / 光线 L 在 T/B/N 上的投影)

    这是给 Smith 的遮蔽/阴影(Λ(v)、Λ(l))用的,你 Lambda_GGX_Aniso() 里是:

  • ToV = dot(T,V), BoV = dot(B,V), NoV = dot(N,V)

  • tan²θ_v(aniso) = (ToV²)/αx² + (BoV²)/αy² 然后再除以 NoV²

  • Λ(v) = ( -1 + sqrt(1 + tan²θ_v) ) / 2

直观含义:Smith G 描述的是"沿着某个出射方向 v,微表面自遮蔽的统计",所以它必须以 v 为自变量(v-space)。同理对 l 也做一次,然后 joint 组合。

同一个数学对象(各向异性 GGX)在代码层面有两种等价写法/投影写法

各向异性--[ggx,ggx是一种数学对象

d项和光照和视角无关

H 是由 L 和 V 共同决定的 ,因此从"复合依赖"角度讲,D 会随光照和视角变化------不是因为它"看 L/V",而是因为 H 变了

1.0 / (PI * ax * ay * denom * denom)为什么可以精准的表示一种离散样本的统计情况?

D在描述"无限多微表面法线样本的概率密度极限"。

(1) "统计模型/分布"指的是我们用一个连续概率密度去描述微表面法线的随机性;

(2) "蒙特卡洛采样"指的是用有限样本去数值估计积分。

D(h)=1/(π ax ay denom²) 属于第 (1) 类,不是"积分后的结果",也不依赖"样本无限"这个前提;它本身就是连续分布的解析表达(概率密度/面积密度)。

真正"需要积分"的地方在哪里

需要积分的是最终的镜面反射能量(渲染方程):

积分等于 1,而不是 D(m) 的取值在 0,1

概率密度函数(PDF)一样:PDF 在某些点的值完全可以大于 1,只要整体积分为 1 就行。

因此 D(h) 可以远大于 1,尤其在粗糙度很小(α 很小)且 h 接近法线时,会出现尖峰(这也是你要 clamp roughness/α 的原因之一)。

你可以把它直观地理解成:

"同一个全局 H,在切线平面里投影到 T/B 方向后,分别按 αx、αy 缩放,最后形成一个'椭圆度量'下的长度平方;GGX 用这个椭圆度量的长度来决定该方向出现的概率密度。"

ax,ay并不是离开了ggx 就完全没有意义,离开 microfacet 分布族与其配套的 G/采样/IBL 处理,才算的是失去可比性

一个椭圆度量下的"长度平方"。这类结构不仅 GGX 用,Beckmann/Gaussian、Phong 的某些各向异性形式也有类似"两个轴尺度"的参数。换句话说:

  • ax/ay 的可解释性来自"这是一个各向异性 NDF 的主轴尺度参数"

改用 Beckmann,各向异性的 D 仍会用 αx、αy(但公式不同);它们仍然是"切线平面两个主轴的 roughness 参数",只是对应的尾部行为不同。

"GGX"并不是一个单独的 BRDF,而是一个微表面统计族里的一种 NDF(D 项)选择。

你完全可以在 Cook--Torrance 框架里替换 D 项,只要你同时保证 G(Smith)与之匹配,或者明确接受近似。

Cook--Torrance 的结构允许替换 D

"用 GGX"严格来说是:

  • D = GGX (Trowbridge--Reitz)

  • G = Smith-GGX(与 GGX 分布一致)

  • 采样与 LUT 也通常围绕 GGX

但你可以换掉 D。

新艺术时期

维梅尔 Johannes Vermeer (楊·維梅爾)

黑键提供"天然的手指弧度"。

例子:

Db 大调:大量黑键,手型自然贴合

F# 大调:拇指更容易落白键支撑

C 大调:全白键,反而容易手塌

所以 Chopin 才会说:

"最自然的音阶是 B 大调。"

这不是玄学,是人体工学。

Smith Joint 的几何项 G(以及 Vis)是"方向相关的遮蔽函数",它定义在入射方向 L 和视线方向 V 上,而不是定义在半角向量 H 上。

几何项的物理意义是:

  • 从方向 V 看进去,微表面有多少被遮挡(masking)

  • 从方向 L 打进来,微表面有多少投影被阴影遮住(shadowing)

这两件事只与 入射方向和出射方向有关,不与 H 有直接定义关系。

如果你用 H 代替 V 或 L,会导致:

  • grazing 区域遮蔽不正确

  • 能量不守恒

  • 高光边缘会异常亮或异常暗

Λ 的意义可以理解成:从某个方向 w 看过去,由微表面统计导致的"额外遮挡程度"的量化。角度越 grazing(NoW 越小),Λ 越大,G 越小。

为什么是这个开方形式

这是 GGX(Trowbridge-Reitz)分布下,Smith masking-shadowing 函数的一个常用闭式近似/闭式形式。对于 GGX,几何衰减随角度的变化不像 Beckmann 那样指数衰减,而是具有更重尾(heavy tail),于是会出现这种 的结构。

"避免显式算 tan"

在各向异性 GGX 中,切线平面不是欧式长度,而是椭圆度量:

实际 profiling 怎么验证?

你如果想要"具体高多少"在你的 pipeline 上成立,唯一可靠方法是:

microbenchmark shader

  • baseline:纯 FMA loop

  • variant:插入 tan()

  • 用 Nsight / Radeon GPU Profiler / RenderDoc capture

观察:

  • SFU utilization

  • instruction latency

  • warp occupancy stall

这才是精确答案。

  • GPU 上 tan 消耗高的原因是:它不是基本指令,需要 range reduction + sin/cos + division,并且在 π/2 附近数值病态导致 slow path。

  • 定量上:tan 通常比 mul/add 贵 20--60×,比 sin/cos 贵 2--4×,移动端可能达到 100×。

  • 是否"真的贵"取决于调用频率:每像素、每光源循环里使用会非常明显。

Range Reduction 是什么?

三角函数是周期函数:sin(x) 周期 2π

tan(x) 周期 π

tan(x)=tan(x+kπ)

这意味着:

对任意输入角度 xxx,我们都可以先把它"折叠"回一个小区间,再去做近似计算。

这个"折叠回主区间"的过程就是 range reduction

实现会拆成:2/π​=C1​+C2​+C3​

用多次 FMA 减去,保证余数 rrr 精度够。

这叫 Payne--Hanek reduction 或 Cody--Waite reduction。

这一步不是简单 mod,而是高精度浮点算法。

GPU Shader 中的实际情况

HLSL/GLSL 的 tan()

一般会调用硬件 SFU 或 microcode:

  • 内部必定包含 reduction

  • 输入越大 reduction 越慢

  • 某些架构在大输入时走 slow path

这就是为什么 shader 编程里常见经验:

Avoid transcendental functions unless you know input range.

正入射附近(NoV≈1, NoL≈1)

此时 ToV、BoV 很小,tan2/cos2 很小:

λV ≈ 0,λL ≈ 0

=> G ≈ 1/(1+0+0)=1

=> Vis ≈ 1/(4 NoV NoL) ≈ 1/4

"Vis ≤ 1/4"只在一个很特殊的点成立:NoV=NoL=1 且 λ≈0(也就是 V、L 都正对法线、遮蔽近乎没有):

G≈1,Vis≈1/(4*/*1)=1/4。

roughness → α → D 与 G 的形状

AO 与 DV 的关系:理论无关,工程上通常只作用于 IBL

AO(ambient occlusion)不是 BRDF 参数,它不属于 Cook--Torrance。

DV 是微表面统计模型,AO 是宏观遮蔽近似。

因此:

  • AO 不应直接乘在 direct specular 上

  • AO 常用于环境光项(IBL)

常见做法:

复制代码

indirectDiffuse *= ao; indirectSpecular *= SpecularAO(ao, roughness, NoV);

其中 SpecularAO 通常是启发式:

复制代码

float specAO = saturate(pow(NoV + ao, roughness));

目的:

  • 避免凹槽里反射环境光过亮

  • 但 direct lighting 不受影响

  • DV_SmithJointGGX_Aniso 定义的是 specular BRDF 的微表面统计结构(D+G/Vis)

  • roughness 决定 DV 的输入 αx/αy(高光宽度与遮蔽)

  • metallic 不改变 DV,只决定 Fresnel F0 与 diffuse/specular 能量分配

  • AO 与 DV 理论无关,工程上只用于环境光遮蔽,不应直接影响 direct specular

c_diff = (1 - metallic) * albedo

这对应标准 metallic workflow:

metallic 控制 diffuse/specular 的能量分裂

metallic diffuse specular F0
0 有漫反射 F0≈0.04
1 无漫反射 F0=albedo

正确应是:

复制代码

float3 kd = (1.0 - F) * (1.0 - metallic);

float3 diffuse = kd * albedo / PI;

否则会发生什么?

  • 正视角:F≈0.04,diffuse 基本没问题

  • grazing:F→1,但 diffuse 仍然保持全强

  • 结果就是 grazing 会过亮,不守恒

所以 diffuse 这一行目前是"不完整 PBR"。

cpp 复制代码
                float3 f0 = lerp(0.04, albedo.rgb, saturate(_Metallic));
                float3 F = F_Schlick_local(f0, VoH);
                float3 specular = D * Vis * F;
                float3 kd = (1.0 - F) * (1.0 - _Metallic);

                float3 diffuse = kd * albedo.rgb * (1.0 / PI);
                if (_TEST < 0.5)
                    diffuse = (1.0 - _Metallic) * albedo.rgb * (1.0 / PI);

其实差别不大,一个是为了能量守恒把漫反射的能量额外消减了,看上去可能会保守一点,更加的"温顺",如果没有额外的要求的情况下,我还是更喜欢狂野一点的数值暴力。

所以我选择还是遵守Diffuse能量的消减

cpp 复制代码
float3 radiance = mainLight.color * mainLight.distanceAttenuation * mainLight.shadowAttenuation;
float3 color = (diffuse + specular) * radiance * NoL;

* NoL,本质上不是"材质能量守恒项",而是渲染方程里的几何投影项(cosine law / Lambert 余弦定律)。它描述的是:同样的入射辐亮度 Li 打到一个倾斜表面时,单位表面面积接收到的辐照度(irradiance)会按 cos⁡θ 变小。这跟 BRDF 自身是否能量守恒是两件不同层级的事,但它们在同一条公式里同时出现,所以很容易混在一起。

  • radiance ≈ Li​(包含距离衰减、阴影、光色等)

  • (diffuse + specular) ≈ fr

  • NoL ≈ (n⋅l)

NoL 是"入射几何项",把 radiance 转成 irradiance(接受能量随倾角减少)

渲染方程(反射):

这里的(n⋅l) 就是你代码里的 NoL

"Tashika datta kke" (確かだったっけ) in Japanese means "If I remember correctly, was it X?" or "Was it true/certain?".

Breakdown of the Phrase

  • Tashika (確か): Certain, sure, reliable, or if I remember correctly.
  • Datta (だった): Was (past tense of da / desu).
  • Kke (っけ): A sentence-ending particle used for retrieving or recollecting previously shared information.

Based on the Japanese phrasing, "Ikiteta desho" (生きてたでしょ) most commonly means "They/he/she were/was alive, right?" or "It was still alive, wasn't it?".

Here is a breakdown of the phrase:

  • Ikiteta (生きてた): A colloquial, casual past tense form of ikiteiru (to be living/alive). It means "was alive" or "was living".
  • Desho (でしょ): A conversational, shortened form of deshou. It acts as a tag question, meaning "isn't it?", "right?", or "don't you agree?".

"Haka made itte" could roughly be interpreted in Japanese as "Going to the grave" or "Saying something until the grave" (often implying keeping a secret forever or doing something until death).

  • Haka made (墓まで): Means "Until the grave" or "To the grave".
  • Itte (行って/言って): Means "Going" (from iku ) or "Saying" (from iu).

"Kaeshite hoshii" (返してほしい / かえしてほしい) in Japanese means " I want you to return it " or "I want it back".

The Shader decides "how to interpret the material", while the texture decides "what the material actually is".

最常见的最小集合(游戏量产默认)

  • BaseColor (sRGB)

  • Normal (切线空间,通常BC5/ATI2,线性)

  • ORM 或分离的:Occlusion、Roughness、Metallic(线性;经常打包到一张纹理 RGB)

你大概率能拿到这些,但细节不一定规范(例如 roughness 是否是 perceptual,normal 是否是 DirectX/GL 约定)。

经常"拿不到但很想要"的

  • Height/Displacement(很多项目不做)

  • Bent Normal、Thickness、Curvature(烘焙成本高、工具链不统一)

  • Anisotropy/Aniso direction(更少,尤其在非高端管线)

典型从高模到引擎的链条:

  1. 高模/低模 + UV

  2. Bake:Normal/AO/Curvature/Thickness 等(MikkTSpace 一致性是重点)

  3. 在 Substance Painter / Designer 里用 PBR 模板绘制

  4. 导出预设(Unreal/Unity/自定义)→ 贴图命名与通道打包

  5. 引擎导入:色域、压缩、通道语义、法线翻转、金属度约定

你应重点盯住三件事:

  • 切线空间一致性(MikkTSpace + 正确的 tangent sign)

  • 色域/线性空间(baseColor sRGB,其他一律线性)

  • 粗糙度/光滑度约定(roughness vs smoothness;是否 perceptual;是否要平方到 alpha)

输入契约(你向美术/外包明确写清楚的规范)

至少要在文档里明确:

  • BaseColor:sRGB,禁止含光照/阴影/高光;++金属 baseColor 允许带色(导体反射色)++

  • Metallic:++0/1 为主++ ,允许++少量过渡++;线性

  • Roughness:线性(perceptual roughness),范围0,1,禁止反相;引擎内部 alpha = roughness²

  • Normal:MikkTSpace;DirectX 还是 OpenGL 绿通道约定(必须定死)

  • ORM 打包顺序:例如 R=AO, G=Roughness, B=Metallic(或你项目的顺序)

  • 强制贴图命名/后缀(便于导入器识别

MikkTSpace 解决的是"切线基(TBN)怎么定义"

MikkTSpace规定了如何从 UV/几何计算 tangent、bitangent(以及 tangent.w 的 handedness)。这保证你在 bake(Substance/xNormal/Marmoset 等)和引擎运行时使用同一个 tangent 生成规则时,normal map 的细节方向一致,接缝和镜像 UV 处理一致。

  • 只要 bake 端和引擎端都用 MikkTSpace(或兼容实现),就不会出现"同一张 normal 在接缝处左右不一致"的大问题。

  • Unity(尤其 URP/HDRP)常见工作流默认就是 MikkTSpace(依赖模型导入 tangent),但你仍要确保 DCC 导出的 tangents 与引擎计算方式一致(或干脆让引擎重算,且 bake 端按同样方式)。

  1. DirectX vs OpenGL 绿通道约定解决的是"normal map 编码的坐标系方向"

    切线空间 normal 通常存的是 (x,y,z) = (T方向,B方向,N方向) 的分量,贴图里一般:

  • R = x

  • G = y

  • B = z(或由 x,y 重建)

差异只在 y(绿通道)的符号:

  • "DirectX normal":y 向上(常见理解:+Y 指向 bitangent 的某一方向)

  • "OpenGL normal":y 取反(相当于把 B 轴反过来)

很多工具链把它描述成:OpenGL 相对 DirectX 需要 flip green(G = 1 - G 或 y = -y)。本质就是切线空间的 handedness/轴向约定不同导致的编码差异。

相关推荐
通信小呆呆13 小时前
当算法有了“五感”:多模态数据融合如何向人体感官协同学习?
人工智能·学习·算法·机器学习·机器人
H__Rick13 小时前
自动对焦学习-3
人工智能·学习·计算机视觉
Daisy Lee13 小时前
量化学习-第1章-什么是量化金融
学习·金融·datawhale
Alsn8614 小时前
等待学习-学习目录:Docker 容器安全攻防
学习·安全·docker
YM52e14 小时前
买菜计算器小应用 - HarmonyOS ArkUI 开发实战-PC版本
学习·华为·harmonyos·鸿蒙·鸿蒙系统
小雨下雨的雨14 小时前
HarmonyOS ArkUI训练营入门-组件掌握系列-Animation 动画效果实现-PC版本
学习·华为·harmonyos·鸿蒙
cqbzcsq15 小时前
CellFlow虚拟细胞论文阅读
论文阅读·人工智能·笔记·学习·生物信息
YangYang9YangYan16 小时前
2026初入职场学习数据分析的价值
学习·数据挖掘·数据分析
guslegend17 小时前
理论学习:什么是 Coding Agent?
学习
自传.17 小时前
尚硅谷 Vibe Coding|第三章(1) Claude Code深度使用与进阶技巧 学习笔记
笔记·学习·尚硅谷·vibecoding