从无光照到 PBR:我的环境光照学习记录

一、无光照渲染(C6-01)

做了什么

最基础的一条渲染链路:顶点着色器只做 MVP 变换,片元着色器直接采样纹理输出,没有任何光照计算。

核心是颜色乘法链:最终颜色 = 纹理 × 顶点色 × baseColor(材质的颜色因子)。这是 PBR 里 albedo/baseColor 的雏形------先有"材质颜色从哪来",后面才能谈"光怎么作用在它上面"。

顶点格式也因此加了一个 vec4 color 字段(之前我的工程里顶点只有 pos/normal/uv)。

知识点

  • 无光照渲染是 PBR 的起点:先把"颜色"和"光"分开想。
  • 材质颜色可以来自三层乘法:纹理贴图 × 顶点色 × 材质因子,哪层没有就用 1.0 代替。
  • 顺手做了半透明实验:管线颜色附件开混合(SrcAlpha / OneMinusSrcAlpha),baseColor 的 alpha 设 0.5,半透明物体不写深度。

二、BRDF 积分查找表(C6-02)

做了什么

用计算着色器离线生成一张 256×256 的 BRDF 查找表(LUT),存成 RGBA16F 的 KTX 纹理。这是 Epic split-sum 近似的右半边:把反射方程里"只依赖视线角度和粗糙度"的那部分积分预先算好。

知识点

  • 预计算思想:把每帧要算的昂贵积分(每像素 1024 次采样)提前算成查表,运行时一次纹理采样代替。这是 IBL 的基石。
  • 像素坐标 = 参数坐标:LUT 的横轴是 NoV(法线·视线夹角的余弦),纵轴是 roughness。生成时遍历每个格子,用格子坐标解码出参数,算出积分结果存回格子。
  • 1.0 - uv.y 的翻转:纹理 y 轴向下,想让 roughness=0(镜面)在底部就翻转。关键不是方向,而是生成和采样必须用同一套规则,否则查表错位。
  • vec3 V = vec3(sqrt(1.0 - NoV*NoV), 0.0, NoV):从标量夹角余弦重建单位视线向量------法线朝 Z 轴时 NoV 就是 V.z,再套单位圆约束(勾股定理)求出 V.x。90% 的图形学数学就是勾股定理加单位圆。
  • V 是视线方向(出射),L 是入射光方向,L = 2*dot(V,H)*H - V 是反射定律:L 是 V 关于微平面法线 H 的镜像。整个积分就是在遍历"哪些方向的入射光能反射进眼睛"。
  • Hammersley 低差异序列 + GGX 重要性采样:让 1024 个样本集中在贡献大的方向,而不是均匀乱撒。

这章一开始完全看不懂,后来搞明白"表在解决什么、表里存什么、运行怎么用它"三件事之后,公式细节就都不需要背了。

三、环境贴图预过滤(C6-03)

做了什么

split-sum 的左半边:把 HDR 全景图(等距圆柱投影)转成立方体贴图,然后按粗糙度预过滤成三张 KTX 存盘。每张 HDR 输出三个文件:*_prefilter.ktx(GGX 镜面)、*_charlie.ktx(布料光泽)、*_irradiance.ktx(漫反射辐照度)。

知识点

  • mip 就是粗糙度分级:每一级 mip 对应一个 roughness = mip/(总层数-1),mip0 是原始环境图,越往高层越糊。粗糙表面反射的是"模糊的环境光",按 roughness 选 mip 采样就行。
  • 预过滤在片元着色器里做:对当前 mip 的环境图做重要性采样积分(1024 次),结果写进目标纹理的对应 mip×face。
  • GPU 离屏渲染当图像处理流水线:把 cube 纹理当渲染目标,逐 mip 逐面渲染(cmdBeginRendering 里指定 layer/level),渲完 download 回 CPU 再存 KTX。这个"采样输入 → shader 里做重活 → 写输出 → 下载存盘"的骨架,后面所有预计算都是它。
  • 三种分布各管一摊:Lambertian=漫反射底色、GGX=金属镜面、Charlie=布料光泽,运行时按材质类型查对应表。

这章是离线工具代码,我最后决定不需要学会手写------开发中要么引擎内置(Unity/UE 烘焙环境探针),要么直接抄标准实现。要带走的是概念:它产出什么、和 C6-02 怎么合体。

四、PBR 合体:MetallicRoughness(C6-04)

做了什么

前三章的铺垫在这里合体:完整 PBR 光照渲染一个金属头盔(DamagedHelmet)。光照组成:

text 复制代码
最终颜色 = 环境漫反射 + 环境镜面 + 直接光 + 自发光
             (查 irradiance)  (查 prefilter×LUT)  (F·G·D 实时算)  (emissive 加法)
最后 × AO(环境遮蔽)→ gamma 校正输出

材质有 5 张贴图:baseColor、metalRoughness(g=粗糙度,b=金属度)、normal、AO、emissive。

知识点

  • 两个 mix 就是金属度开关:
glsl 复制代码
diffuseColor  = mix(baseColor, vec3(0), metallic)   // 金属度 ↑ → 漫反射 ↓
specularColor = mix(vec3(0.04), baseColor, metallic) // 金属度 ↑ → 反射率 = 颜色

金属不吸收光,光全部镜面反射回去,所以金属看起来"一直很亮"而且没有漫反射底色;非金属只反射 4%(0.04),光大部分被吸收散射成漫反射,看着更柔和。

  • PBRInfo 结构体:把所有公式需要的数据打包(几何方向 n/v/各夹角 + 材质参数 roughness/metalness/颜色),传给公式函数。打包本身没有技巧,就是结构体赋值。
  • 直接光 vs 环境光:环境光没有方向、没有影子,靠查表(离线预计算好的);直接光有位置方向有影子,必须实时用 F·G·D 公式算(菲涅尔 × 几何遮蔽 × 微平面分布)。
  • 后处理三件事 :× AO(角落缝隙变暗)、+ emissive(自发光不受光照影响)、pow(color, 1/2.2) gamma 校正。
  • gamma 2.2 的来历:显示器的亮度响应是非线性的(输出亮度 ≈ D^2.2),所以发送前要预补偿 ^(1/2.2),两头抵消,人眼看到的才是正确亮度。光照计算必须在线性空间做,纹理采样时解码(sRGB→线性),输出前编码(线性→sRGB)。

动手实践

  • 打开 main.frag 末尾的调试输出,逐个看 5 张贴图的数据(法线、baseColor、AO、emissive、粗糙度灰度图)。
  • 把 metallicFactor 强制改成 1.0 和 0.0,对比金属/非金属的高光和底色差异,直观感受两个 mix 的行为。
  • 给 main() 的 6 步写满自己的注释:采样 → 法线 → 组装参数 → 环境光查表 → 直接光 → 后处理。能不看代码讲出这 6 步,骨架就算掌握了。

五、SpecularGlossiness(C6-05)

做了什么

同一套光照管线,换了一组材质参数。shader 用一个 isSpecularGlossiness 标志同时支持两套体系。画面是一个水瓶,MR 和 SG 两种材质对比渲染。

知识点

两套体系的核心差异(看懂这四行就懂这一章):

MetallicRoughness(C6-04) SpecularGlossiness(C6-05)
参数 baseColor + metallic + roughness diffuse + specular + glossiness
反射率 F0 推导(金属=颜色,非金属=0.04) 直接给(specular 贴图就是 F0)
粗糙度 直接是 roughness roughness = 1 - glossiness
漫反射 mix(baseColor, 0, metallic) baseColor × (1 - 反射率)

历史背景:glTF 1.0 时代主流是 SpecGloss,glTF 2.0 标准化时选了 MetallicRoughness 当默认,SpecGloss 作为扩展保留。现在引擎(UE/Unity)都用 MR;SG 主要用在需要独立控制高光颜色的材质(布料、皮肤、塑料的彩色高光),因为 MR 里高光色被锁死为 baseColor × metallic。

回头看的几点

  1. IBL 是一条完整的预计算流水线:贵的积分 → 离线算成表(LUT、预过滤环境图)→ 运行时查表。C6-02 和 C6-03 是 split-sum 的两半,C6-04 把它们合成完整 PBR。
  2. PBR 光照组成的框架没变:环境漫反射 + 环境镜面 + 直接光 + 自发光,只是每部分的实现方式不同(查表 vs 实时公式)。
  3. 离屏工具代码不用手写,但要读懂:知道它产出什么、怎么被消费,比会写更重要。真正要手写的是运行时的 shader 和组装逻辑。
  4. 动手比看代码有用:调试开关看贴图、改 metallic 看金属/非金属差异、给骨架写注释------三件事做完,比读十遍代码记得牢。

五章走下来,从"无光照渲个模型"到"完整的 PBR 材质光照",Vulkan 渲染里数据是怎么从贴图、buffer 流到 shader 并最终变成屏幕像素的,心里有数了。

相关推荐
小李不想当小白1 小时前
USART串口数据包(STM32标准库学习笔记)
经验分享·笔记·stm32·单片机·嵌入式硬件·学习
xier_ran2 小时前
【infra之路】AI 编译器学习笔记(八):向量化——把小块变成 SIMD 指令
人工智能·笔记·学习
kmomo..2 小时前
Linux 驱动开发学习笔记:I2C 子系统从入门到实践
linux·驱动开发·学习
zhangdaliangmvp2 小时前
四元数的学习理解
学习
遇印记12 小时前
传输层,网络层
运维·服务器·网络·windows·学习
clorinda13 小时前
PyQt5学习:从界面搭建到与机器学习实战
学习
欣欣之王来了15 小时前
Vue3项目脚手架:Vite vs Vue CLI怎么选?
前端·学习·架构·项目·vue3教程
kaixin_啊啊15 小时前
【零基础学AI】第 7 章——让 AI 辅助学习和办公
人工智能·学习
信奥卷王15 小时前
[GESP202506 六级] 学习小组(含视频)
学习