Unity Shader 实战:从零掌握 PBR 基于物理的渲染
一、什么是 PBR?
PBR(Physically Based Rendering,基于物理的渲染)是现代游戏、影视行业的主流渲染方案。
与传统的 Blinn-Phong 光照相比,PBR 的核心区别在于:
| 对比项 | 传统光照(Blinn-Phong) | PBR |
|---|---|---|
| 材质参数 | 高光颜色、高光强度(艺术调整) | 金属度、粗糙度(物理可测量) |
| 能量守恒 | ❌ 无保证 | ✅ 严格遵守 |
| 跨光照一致性 | ❌ 不同环境需重调 | ✅ 任何光照下表现正确 |
| 效果上限 | 中等 | 照片级真实感 |
PBR 广泛应用于:《赛博朋克2077》《艾尔登法环》《战神》等 AAA 大作,以及 Unity、Unreal、Blender 等主流引擎的默认材质系统。
📌 一句话理解:PBR 是用物理规律约束美术行为——只要参数符合现实,材质在任何光照环境下都自然正确。

二、PBR 的三大核心条件
一个渲染系统要称得上 PBR,必须同时满足以下三条:
① 微表面理论(Microfacet Theory)
宏观上看起来光滑的表面,微观上由无数朝向各异的**微小镜面(微平面)**组成。
粗糙表面(高 Roughness): 光滑表面(低 Roughness):
↗↘↗↙↗↘↗↙ → → → → → →
微平面朝向分散 微平面近乎平行
→ 高光模糊、分散 → 高光锐利、集中
**粗糙度(Roughness)**参数控制微平面的分散程度:
Roughness = 0:完美镜面,高光极小极亮Roughness = 1:完全漫反射,无明显高光
② 能量守恒(Energy Conservation)
反射出去的光能 ≤ 接收到的光能,不能无中生有。
入射光 = 漫反射 + 镜面反射 + 吸收
1.0 = kD + kS + 损耗
实现方式:
hlsl
复制
float3 F = FresnelSchlick(HdotV, F0); // 镜面反射比例
float3 kS = F;
float3 kD = (1.0 - kS) * (1.0 - metallic); // 漫反射 = 1 - 镜面
// 金属材质无漫反射(metallic=1 → kD=0)
③ 基于物理的 BRDF
使用符合物理规律的双向反射分布函数描述光照交互,详见下一章。
三、核心数学:Cook-Torrance BRDF
BRDF(Bidirectional Reflectance Distribution Function,双向反射分布函数)定义了入射光在某观察方向上被反射的比例。
PBR 中最常用的是 Cook-Torrance BRDF:
D(h) · F(v,h) · G(l,v,h)
fr(l, v) = kD·------ + ─────────────────────
π 4·(n·l)·(n·v)
漫反射项(Lambert) 镜面反射项(Cook-Torrance)
其中 D、F、G 三项各司其职:

3.1 D —— 法线分布函数(GGX)
描述有多少微平面的法线朝向了半角向量 h,决定高光形状。
GGX(Trowbridge-Reitz) 是当前工业标准:
hlsl
复制
float DistributionGGX(float NdotH, float roughness)
{
float a = roughness * roughness;
float a2 = a * a;
float NdotH2 = NdotH * NdotH;
float denom = (NdotH2 * (a2 - 1.0) + 1.0);
return a2 / (PI * denom * denom);
}
为什么用 roughness²? 直接用 roughness 线性值时高光变化不自然,平方后符合人眼感知的线性变化。
GGX 相比老牌的 Beckmann 分布,优势在于**"长尾效应"**:高光边缘有更自然的渐隐,非常接近真实金属质感。
3.2 F —— 菲涅耳方程(Fresnel)
描述不同入射角下镜面反射率的变化——观察角越斜,反射越强。
生活中随处可见菲涅耳效应:正视水面看到水底,侧视水面看到倒影。
垂直观察(grazing = 0): 掠射观察(grazing = 90°):
反射率 ≈ F0(基础值) 反射率 → 1.0(几乎全反射)

使用 Schlick 近似 高效计算:
hlsl
复制
float3 FresnelSchlick(float cosTheta, float3 F0)
{
return F0 + (1.0 - F0) * pow(saturate(1.0 - cosTheta), 5.0);
}
F0(基础反射率) 由材质决定:
- 非金属(塑料、皮肤、木头):F0 ≈
float3(0.04, 0.04, 0.04)(约 4%) - 金属:F0 = Albedo 颜色(金的 F0 是金黄色,铁的 F0 是灰色)
hlsl
复制
// 金属工作流中 F0 的计算
float3 F0 = lerp(float3(0.04, 0.04, 0.04), albedo, metallic);
3.3 G —— 几何遮蔽函数(Smith)
描述微平面之间的自遮挡和自阴影,粗糙表面的微平面会互相遮挡,导致能量损失。
↓ 光线被相邻微平面遮挡
↗↘↗↘↗↘↗↘
←→←→←→←→ ← 部分出射光被阻挡(几何遮蔽)
使用 Smith + Schlick-GGX 组合:
hlsl
复制
float GeometrySchlickGGX(float NdotX, float roughness)
{
float r = roughness + 1.0;
float k = (r * r) / 8.0; // 直接光照用此 k 值
return NdotX / (NdotX * (1.0 - k) + k);
}
float GeometrySmith(float NdotV, float NdotL, float roughness)
{
// 分别计算视线方向和光线方向的遮蔽,相乘
return GeometrySchlickGGX(NdotV, roughness)
* GeometrySchlickGGX(NdotL, roughness);
}
四、金属度/粗糙度工作流
现代 PBR 流程统一使用 Metallic-Roughness 工作流,一套贴图打天下:
| 贴图 | 通道 | 含义 |
|---|---|---|
| Albedo(BaseColor) | RGB | 固有色(金属存储 F0,非金属存储漫反射色) |
| Metallic | R(灰度) | 金属度:0 = 非金属,1 = 金属 |
| Roughness | R(灰度) | 粗糙度:0 = 完全光滑,1 = 完全粗糙 |
| Normal | RGB | 法线贴图(增加表面细节) |
| AO | R(灰度) | 环境遮蔽(凹陷处变暗) |

典型材质参数参考
| 材质 | Metallic | Roughness | F0 |
|---|---|---|---|
| 黄金 | 1.0 | 0.1 | (1.0, 0.77, 0.34) |
| 铁 | 1.0 | 0.6 | (0.56, 0.57, 0.58) |
| 塑料(光滑) | 0.0 | 0.1 | (0.04, 0.04, 0.04) |
| 皮肤 | 0.0 | 0.7 | (0.03, 0.03, 0.03) |
| 木头 | 0.0 | 0.8 | (0.04, 0.04, 0.04) |
五、完整 Shader 实现(URP)
5.1 Shader 属性声明
hlsl
复制
Shader "Custom/PBR"
{
Properties
{
_AlbedoMap ("Albedo", 2D) = "white" {}
_AlbedoColor ("Albedo Color", Color) = (1,1,1,1)
_NormalMap ("Normal Map", 2D) = "bump" {}
_MetallicMap ("Metallic", 2D) = "black" {}
_Metallic ("Metallic", Range(0,1)) = 0
_RoughnessMap ("Roughness", 2D) = "white" {}
_Roughness ("Roughness", Range(0,1)) = 0.5
_AOMap ("AO Map", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" }
Pass
{
Name "ForwardLit"
Tags { "LightMode"="UniversalForward" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#define PI 3.14159265358979
5.2 D、F、G 三大函数
hlsl
复制
// ── D:GGX 法线分布函数 ──────────────────────────
float D_GGX(float NdotH, float roughness)
{
float a = roughness * roughness;
float a2 = a * a;
float d = (NdotH * NdotH * (a2 - 1.0) + 1.0);
return a2 / (PI * d * d);
}
// ── F:菲涅耳(Schlick 近似)────────────────────
float3 F_Schlick(float cosTheta, float3 F0)
{
return F0 + (1.0 - F0) * pow(saturate(1.0 - cosTheta), 5.0);
}
// ── G:几何遮蔽(Smith + Schlick-GGX)───────────
float G_SchlickGGX(float NdotX, float roughness)
{
float r = roughness + 1.0;
float k = (r * r) / 8.0;
return NdotX / (NdotX * (1.0 - k) + k);
}
float G_Smith(float NdotV, float NdotL, float roughness)
{
return G_SchlickGGX(NdotV, roughness) * G_SchlickGGX(NdotL, roughness);
}
5.3 顶点着色器
hlsl
复制
struct Attributes
{
float4 positionOS : POSITION;
float3 normalOS : NORMAL;
float4 tangentOS : TANGENT;
float2 uv : TEXCOORD0;
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float3 positionWS : TEXCOORD0;
float3 normalWS : TEXCOORD1;
float3 tangentWS : TEXCOORD2;
float3 bitangentWS: TEXCOORD3;
float2 uv : TEXCOORD4;
};
Varyings vert(Attributes IN)
{
Varyings OUT;
VertexPositionInputs posInputs = GetVertexPositionInputs(IN.positionOS.xyz);
VertexNormalInputs normInputs = GetVertexNormalInputs(IN.normalOS, IN.tangentOS);
OUT.positionCS = posInputs.positionCS;
OUT.positionWS = posInputs.positionWS;
OUT.normalWS = normInputs.normalWS;
OUT.tangentWS = normInputs.tangentWS;
OUT.bitangentWS = normInputs.bitangentWS;
OUT.uv = TRANSFORM_TEX(IN.uv, _AlbedoMap);
return OUT;
}
5.4 完整片元着色器
hlsl
复制
六、法线贴图详解
法线贴图是 PBR 材质中非常重要的一环,它在不增加面数的情况下为模型表面添加细节。
无法线贴图: 有法线贴图:
平滑光照,无细节 凹凸感、划痕、纹理细节
┌─────────────┐ ┌~~~~~─────~~~~~┐
│ │ │ 表面有起伏感 │
└─────────────┘ └~~~~~─────~~~~~┘
法线贴图的 RGB 通道存储的是切线空间中的法线方向:
- R → X(左右偏移)
- G → Y(上下偏移,注意 OpenGL/DX 方向差异)
- B → Z(深度,通常偏蓝)
hlsl
复制
// URP 中解码法线贴图并变换到世界空间
float3 normalTS = UnpackNormal(SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, uv));
float3x3 TBN = float3x3(tangentWS, bitangentWS, normalWS);
float3 N = normalize(mul(normalTS, TBN));
七、色调映射与 Gamma 校正
PBR 计算在线性空间中进行,但显示器是 Gamma 空间,必须做两步处理:
步骤一:色调映射(Tone Mapping)
HDR(高动态范围)光照值映射到 LDR(0~1)显示范围:
hlsl
复制
// Reinhard 色调映射(简单实用)
color = color / (color + 1.0);
// ACES 色调映射(更接近电影调色,对比更强)
float3 ACESToneMapping(float3 x) {
float a = 2.51, b = 0.03, c = 2.43, d = 0.59, e = 0.14;
return saturate((x*(a*x+b))/(x*(c*x+d)+e));
}
步骤二:Gamma 校正
hlsl
复制
// 线性空间 → Gamma 2.2 空间(用于显示器输出)
color = pow(max(color, 0), 1.0 / 2.2);
注意:如果你使用的是 URP 并开启了 HDR + Post Processing,色调映射和 Gamma 校正通常由后处理管线自动完成,Shader 中无需手动处理。
八、效果对比:不同参数表现
Roughness 变化效果
Roughness: 0.0 0.2 0.5 0.8 1.0
● ● ● ● ●
(镜面) (抛光) (半哑) (磨砂) (粉笔)
小亮斑 略大亮斑 中等高光 模糊高光 几乎无高光
Metallic 变化效果
Metallic: 0.0 1.0
● → ●
(塑料) (金属)
高光白色 高光带颜色(随 Albedo)
有漫反射 无漫反射(全镜面)
九、与 NPR 的组合:半写实风格
PBR 和 NPR 并非对立,现代游戏常常混合使用:
| 游戏 | 策略 |
|---|---|
| 《原神》 | PBR 材质工作流 + NPR 卡通光照 + SDF 面部阴影 |
| 《崩坏:星穹铁道》 | PBR 底层 + 卡通描边 + Ramp 阴影 |
| 《蔚蓝档案》 | 强 NPR 卡通 + 少量 PBR 金属质感 |
混合方案核心思路:
- 用 PBR 工作流制作贴图(保证材质物理正确)
- 替换光照计算部分为 NPR 卡通化处理
- 保留 F0/菲涅耳让金属感真实
十、推荐学习资源
| 资源 | 链接 | 说明 |
|---|---|---|
| LearnOpenGL PBR | learnopengl.com | 最权威的 PBR 入门教程 |
| Unity 中文课堂 | learn.u3d.cn | 三节课掌握 PBR(中文) |
| 毛星云 PBR 白皮书 | 知乎专栏 | 深度 PBR 理论完全解析 |
| Substance 材质规范 | AdobeHelp | 工业级 PBR 材质制作指南 |
小结
本文系统讲解了 PBR 的完整知识体系:
- ✅ 三大核心条件:微表面理论、能量守恒、物理 BRDF
- ✅ DFG 三项:GGX 法线分布、Schlick 菲涅耳、Smith 几何遮蔽
- ✅ 金属/粗糙度工作流:F0 插值、贴图规范
- ✅ 完整 URP Shader:从顶点到片元的完整实现
- ✅ 法线贴图:TBN 矩阵变换
- ✅ 色调映射 + Gamma 校正:Reinhard / ACES
PBR 是现代渲染的基础,理解它不仅能写出更好的 Shader,也能与美术同学更高效地沟通材质制作规范。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)