简易能量守恒光照模型

铁名_IronName Lv4

《Shader 入门精要》中相关部分:读书笔记17《Unity Shader入门精要》LearnOpenGL中关于PBR的教程(中文版)

模型解释

漫反射部分

1
2
3
4
5
float3 F0 = lerp(float3(0.04, 0.04, 0.04), albedo, _Metalness); // 基础反射率
float3 F = fresnelSchlick(VdotH, F0); // 菲涅尔项
float3 kD = (1.0 - F) * (1.0 - _Metalness); // 漫反射比例

float3 diffuse = kD * albedo / 3.14159;

Lambertian 是最简单的理想漫反射模型:表面均匀地向各个方向反射光线,反射强度只与入射光方向和表面法线的夹角余弦成正比。除以 π\pi 之后,在半球面上的积分为 1。

fdiffuse=albedo/πf_{diffuse} = albedo / \pi

F 是 菲涅尔项,因为菲涅尔效应:当视线与法线夹角增大(掠射角)时,镜面反射率增加,漫反射比例相应减少。F 用于计算漫反射比例 kD.

F0(基础反射率)

是“垂直入射时的基础反射率”。它是一个材质属性。对于非金属,这个值很低(约0.02-0.05),比如塑料、木材;对于金属,这个值就是它的漫反射颜色(Albedo),并且是彩色的(例如金子的F0偏黄)。

为什么有 F 和 kD 两个比例?

入射总能量 = 1 (标准化)
├─ 镜面反射部分 = F (直接被反射)
└─ 进入表面部分 = 1 - F
,,├─ 被吸收部分 = (1 - F) × 吸收系数
,,└─ 漫反射部分 = (1 - F) × (1 - 吸收系数)

镜面反射部分

GGX + Smith + Schlick.

1
2
3
4
float alpha = _Roughness * _Roughness;
float D = GGX_Distribution(NdotH, alpha); // 法线分布
float G = SmithGeometry(NdotV, NdotL, alpha); // 几何遮蔽
float3 specular = F * D * G / (4.0 * NdotV * NdotL + 0.0001);

镜面反射部分使用 微平面理论(Torrance-Sparrow 微面元模型):表面由无数微小镜面组成,每个微平面遵循菲涅尔效应。分母可以加个微小数值,避免分母为零的情况。

fspecular=FDG4(NV)(NL)f_{specular}=\frac{F * D * G}{4*(N\cdot V)*(N\cdot L)}

法线分布函数 D 描述微平面 法线朝向半角向量 H(反射光线方向=视线) 的比例。此处使用 GGX 函数:

D=α2π((NH)2(α21)+1)2D = \frac{\alpha^2}{\pi*((N\cdot H)^2*(\alpha^2-1)+1)^2}
  • α=roughness2\alpha=roughness^2,粗糙度越大,α\alpha越大,高光越模糊。
  • 物理意义:粗糙表面微平面朝向更分散,高光区域扩大但峰值亮度降低。

阴影遮蔽函数 G 描述微平面之间相互遮挡的概率。此处使用由 GGX 衍生出的 Smith-Schlick 模型,其中 k=roughness22=α/2k=\frac{{roughness}^2}{2}=\alpha /2

G(I,v,h)=(nI)(nv)((nI)(1k)+k)((nv)(1k)+k)G(\textbf{I},\textbf{v},\textbf{h})=\frac{(\textbf{n}\cdot\textbf{I})(\textbf{n}\cdot\textbf{v})}{((\textbf{n}\cdot\textbf{I})(1-k)+k)((\textbf{n}\cdot\textbf{v})(1-k)+k)}
  • 当表面粗糙是,微平面起伏大,遮挡效应显著,G 值较小,高光变暗。

菲涅尔项 F 描述不同入射角下镜面反射的比例。此处使用Schlick近似:

F(I,h)=F0+(1F0)(1Ih)5F(\textbf{I},\textbf{h})=F_0+(1-F_0)(1-\textbf{I}\cdot\textbf{h})^5
  • F0F_0 是垂直入射时的反射率。非金属约为 0.04, 金属等于 albedo(彩色)
  • 掠射角时 F 趋近于 1(H 和 V 垂直),所有能量都被镜面反射。

直接光照部分

1
2
float3 radiance = _LightColor0 * NdotL; // 入射光能量
float3 directLight = (diffuse + specular) * radiance;
  • diffusespecular 已经是 BRDF 值
  • 乘以 NdotL 是因为光线倾斜入射时,单位面积接受的能量减少。
  • 乘以 lightColor 得到彩色光照。
BRDF
  • BRDF:全称“双向反射分布函数”。它的物理定义是:从某个方向入射的光,有多少比例被反射到另一个方向。它是一个比率
  • 单位“1/sr”sr是立体角的单位“球面度”。1/sr的意思是“每球面度”。因为BRDF描述的是“出射辐亮度 / 入射辐照度”,其量纲就是反立体角(立体角的倒数)。相关概念看课程笔记03 计算机图形学基础
  • 在代码中:我们计算出的 diffusespecular 这两个值就是BRDF函数f(l, v)的值。所以后续计算直接光照时,只需执行 颜色 = (diffuse + specular) * 光源颜色 * NdotL,这正是渲染方程 Lo = f(l, v) * Li * NdotL的直接实现。

环境光照

就用了个简易的常量。

1
float3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * 0.3

是非物理的近似,真正的环境光照应该来自 IBL(基于图像的光照),对环境贴图进行积分。但在直接光照PBR中,常使用一个很小的常量来模拟间接漫反射,防止背光面全黑。

直接光照PBR

只显式计算来自明确光源(如平行光、点光源)的贡献的模型。

笔记

1.Roughness 不能为 0

Roughness = 0 时,GGX 分布函数 D 的分母为 0,导致 NaN。
解决方法:加个 clamp 就好。

2.更快速的菲涅尔项计算

由虚幻给出,原因是exp2这个内建函数比pow快。

F(v,h)=F0+(1F0)(5.55473(vh)6.98316)(vh)F(\textbf{v},\textbf{h})=F_{0}+(1-F_0)^{(-5.55473(\textbf{v}\cdot\textbf{h})-6.98316)(\textbf{v}\cdot\textbf{h})}

完整代码

主要看Base部分,Add部分可能忘了改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
Shader "Test/SimplePBR" {
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
//_MainTex ("Main Tex", 2D) = "white" {}
_Specular ("Specular Color", Color) = (1, 1, 1, 1)
_Metalness ("Metalness", Range(0.0, 1.0)) = 0.5
_Roughness ("RoughNess", Range(0.05, 1.0)) = 0.5
}
SubShader {
Tags { "RenderType"="Opaque" "Queue"="Geometry"}

CGINCLUDE
// 菲涅尔近似(Schlick)
float3 fresnelSchlick(float cosTheta, float3 F0) {
return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}

// GGX 法线分布函数
float GGX_Distribution(float NdotH, float alpha) {
float a2 = alpha * alpha;
float denom = NdotH * NdotH * (a2 - 1.0) + 1.0;
return a2 / (3.14159 * denom * denom);
}

// Smith 几何遮蔽函数(GGX 联合形式)
float SmithGeometry(float NdotV, float NdotL, float alpha) {
float k = alpha / 2.0; // 直接光照下的 k
float GGXV = NdotV / (NdotV * (1.0 - k) + k);
float GGXL = NdotL / (NdotL * (1.0 - k) + k);
return GGXV * GGXL;
}
ENDCG


Pass {
Tags { "LightMode"="ForwardBase" }

CGPROGRAM
#pragma multi_compile_fwdbase

#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"

fixed4 _Color;
//sampler2D _MainTex;
//float4 _MainTex_ST;
fixed4 _Specular;
float _Metalness;
float _Roughness;

struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};

struct v2f {
float4 pos : SV_POSITION;
float3 worldPos : TEXCOORD1;
float3 worldNormal : TEXCOORD2;
float4 uv : TEXCOORD0;
};

v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
//o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;

return o;
}

fixed4 frag(v2f i) : SV_Target {

float3 worldPos = i.worldPos;
float3 worldNormal = i.worldNormal;

fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));

fixed3 halfDir = normalize(lightDir + viewDir);

fixed3 albedo = _Specular.rgb;

float NdotL = max(0, dot(worldNormal, lightDir));
float NdotH = max(0, dot(worldNormal, halfDir));
float VdotH = max(0, dot(viewDir, halfDir));
float NdotV = max(0, dot(worldNormal, viewDir));

// 漫反射部分(Lambertian, 能量守恒)
float3 F0 = lerp(float3(0.04, 0.04, 0.04), albedo, _Metalness); // 基础反射率
float3 F = fresnelSchlick(VdotH, F0); // 菲涅尔项
float3 kD = (1.0 - F) * (1.0 - _Metalness); // 漫反射比例

float3 diffuse = kD * albedo / 3.14159;

// 镜面反射部分(GGX + Smith + Schlick)
float alpha = _Roughness * _Roughness;
float D = GGX_Distribution(NdotH, alpha); // 法线分布
float G = SmithGeometry(NdotV, NdotL, alpha); // 几何遮蔽
float3 specular = F * D * G / (4.0 * NdotV * NdotL + 0.0001);

// 直接光照
float3 radiance = _LightColor0 * NdotL;
float3 directLight = (diffuse + specular) * radiance;

// 环境光照(简易)
float3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * 0.3;

float3 finalColor = directLight + ambient;

return fixed4(finalColor, 1.0);
}
ENDCG
}

Pass {
Tags { "LightMode"="ForwardAdd" }

Blend One One

CGPROGRAM

#pragma multi_compile_fwdadd _LIGHTINGMODEL_LAMBERT _LIGHTINGMODEL_PHONG _LIGHTINGMODEL_BLINNPHONG

// #pragma multi_compile_fwdadd_fullshadows

#pragma vertex vert
#pragma fragment frag

#include "Lighting.cginc"
#include "AutoLight.cginc"

fixed4 _Color;
fixed4 _Specular;
float _Metalness;
float _Roughness;

struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
};

struct v2f {
float4 pos : SV_POSITION;
float3 worldPos : TEXCOORD1;
float3 worldNormal : TEXCOORD2;
float4 uv : TEXCOORD0;
SHADOW_COORDS(3)
};

v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.worldNormal = normalize(mul(v.normal, (float3x3)unity_ObjectToWorld));
//o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;

TRANSFER_SHADOW(o);
return o;
}

fixed4 frag(v2f i) : SV_Target {

float3 worldPos = i.worldPos;
float3 worldNormal = i.worldNormal;
UNITY_LIGHT_ATTENUATION(atten, i, worldPos);

fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));

fixed3 halfDir = normalize(lightDir + viewDir);

fixed3 albedo = _Specular.rgb;

float NdotL = max(0, dot(worldNormal, lightDir));
float NdotH = max(0, dot(worldNormal, halfDir));
float VdotH = max(0, dot(viewDir, halfDir));
float NdotV = max(0, dot(worldNormal, viewDir));

// 漫反射部分(Lambertian, 能量守恒)
float3 F0 = lerp(0.04, albedo, _Metalness); // 基础反射率
float3 F = fresnelSchlick(VdotH, F0); // 菲涅尔项
float3 kD = (1.0 - F) * (1.0 - _Metalness); // 漫反射比例

float3 diffuse = kD * albedo / 3.14159;

// 镜面反射部分(GGX + Smith + Schlick)
float alpha = _Roughness * _Roughness;
float D = GGX_Distribution(NdotH, alpha); // 法线分布
float G = SmithGeometry(NdotV, NdotL, alpha); // 几何遮蔽
float3 specular = F * D * G / (4.0 * NdotV * NdotL + 0.0001);

// 直接光照
float3 radiance = _LightColor0 * NdotL;
float3 directLight = (diffuse + specular) * radiance;

// 环境光照(简易)
float3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * 0.03;

return fixed4(directLight +ambient, 1.0);
}

ENDCG
}
}
FallBack "Specular"
}

  • 标题: 简易能量守恒光照模型
  • 作者: 铁名_IronName
  • 创建于 : 2026-04-08 14:11:37
  • 更新于 : 2026-04-13 17:13:13
  • 链接: https://blog.ironname.top/2026/简易能量守恒光照模型/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论