用Shader glsl实现一个简单的PBR光照模型
PBR模型定义了各种光照属性,如基础颜色、金属度、粗糙度等,就像给物体设定各种 “性格特点”。顶点着色器负责把顶点从模型空间转换到裁剪空间,同时计算一些用于光照计算的参数,就像给顶点 “搬家” 并准备好 “行李”。而片段着色器是整个 PBR 实现的核心,计算每个像素的颜色。它通过采样纹理获取各种属性值,然后根据 PBR 光照模型计算漫反射和镜面反射项,最后结合环境光得到最终颜色,就像给每个像素 “化妆”,让它们看起来更真实。
关键算法原理
1 菲涅尔反射(Fresnel Reflection)
菲涅尔反射描述了光线在不同介质表面反射和折射的比例随视角变化的现象。简单来说,当我们垂直观察物体表面时,反射光较少;而从倾斜角度观察时,反射光会增多。在 PBR 中,菲涅尔反射用于模拟物体表面的光泽度变化。
half3 F0 = lerp(half3(0.04, 0.04, 0.04), baseColor, metallic);
half3 F = FresnelSchlick(max(dot(normalWS, viewDir), 0.0), F0);
F0:表示表面在垂直入射时的反射率。对于非金属材质,F0 通常取一个固定值 (0.04, 0.04, 0.04);对于金属材质,F0 等于基础颜色 baseColor。这里使用 lerp 函数根据 metallic 值在两者之间进行插值。
FresnelSchlick:是一个常用的菲涅尔反射近似公式,其定义如下:
half3 FresnelSchlick(half cosTheta, half3 F0) {
return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}
其中 cosTheta 是法线和视线方向的点积,表示视角与表面法线的夹角余弦值。
2 法线分布函数(Normal Distribution Function,NDF)
法线分布函数用于描述微表面的法线方向分布情况,它决定了表面的粗糙度对反射光的影响。在这个示例中,使用的是 GGX(Trowbridge-Reitz GGX)法线分布函数。
half NDF = DistributionGGX(normalWS, viewDir, roughness);
half DistributionGGX(half3 N, half3 H, half roughness) {
half a = roughness * roughness;
half a2 = a * a;
half NdotH = max(dot(N, H), 0.0);
half NdotH2 = NdotH * NdotH;
half nom = a2;
half denom = (NdotH2 * (a2 - 1.0) + 1.0);
denom = PI * denom * denom;
return nom / denom;
}
其中 N 是表面法线,H 是半程向量(光照方向和视线方向的中间向量),roughness 是表面的粗糙度。
3 几何遮挡函数(Geometry Function)
几何遮挡函数用于模拟微表面之间的相互遮挡和阴影效果,它考虑了光线在微表面间传播时的衰减。这里使用的是 Smith 几何遮挡函数。
half G = GeometrySmith(normalWS, viewDir, lightDir, roughness);
half GeometrySchlickGGX(half NdotV, half k) {
half nom = NdotV;
half denom = NdotV * (1.0 - k) + k;
return nom / denom;
}
half GeometrySmith(half3 N, half3 V, half3 L, half roughness) {
half k = (roughness + 1.0) * (roughness + 1.0) / 8.0;
half G1 = GeometrySchlickGGX(max(dot(N, V), 0.0), k);
half G2 = GeometrySchlickGGX(max(dot(N, L), 0.0), k);
return G1 * G2;
}
其中 N 是表面法线,V 是视线方向,L 是光照方向,roughness 是表面的粗糙度。
漫反射和镜面反射计算
在 PBR 中,漫反射和镜面反射是光照计算的核心部分。
// 计算漫反射项
half3 kD = (1.0 - F) * (1.0 - metallic);
half3 diffuse = kD * baseColor / PI;
// 计算镜面反射项
half3 numerator = NDF * G * F;
half denominator = 4.0 * max(dot(normalWS, viewDir), 0.0) * max(dot(normalWS, lightDir), 0.0) + 0.001;
half3 specular = numerator / denominator;
- 漫反射项:kD 表示漫反射比例,它是通过 (1.0 - F) * (1.0 - metallic) 计算得到的。F
是菲涅尔反射系数,metallic 是金属度。diffuse 是最终的漫反射颜色,通过 kD 乘以基础颜色 baseColor 并除以
PI 得到。 镜面反射项:镜面反射项的计算使用了前面计算得到的 NDF、G 和
F。分子是它们的乘积,分母是一个与法线、视线和光照方向点积相关的值,加上一个小的常量 0.001 是为了避免除零错误。
最终光照计算
最后,将漫反射、镜面反射和环境光相加得到最终的光照颜色
// 计算光照强度
half NdotL = max(dot(normalWS, lightDir), 0.0);
half3 Lo = (diffuse + specular) * mainLight.color * NdotL;
// 环境光
half3 ambient = unity_AmbientSky.rgb * baseColor;
// 最终颜色
half3 finalColor = ambient + Lo;
Lo:表示直接光照的贡献,它是漫反射和镜面反射之和乘以主光源颜色 mainLight.color 再乘以 NdotL(法线和光照方向的点积)。
ambient:表示环境光的贡献,通过环境光颜色 unity_AmbientSky.rgb 乘以基础颜色 baseColor 得到。
finalColor:最终的颜色是环境光和直接光照的总和。
这些关键算法共同构成了 PBR 光照模型,使得物体表面的光照效果更加真实和自然。
完整代码及注释
使用 Unity Shader 实现基于物理渲染(PBR)的示例代码,代码中带有详细注释
Shader "Custom/PBRShader" {
Properties {
// 基础颜色纹理,就像给物体穿上一件彩色的外套
_BaseMap ("Base Map", 2D) = "white" {}
// 基础颜色,相当于外套的底色,默认是白色
_BaseColor ("Base Color", Color) = (1,1,1,1)
// 金属度纹理,用来决定物体像不像金属,0 是塑料,1 是纯金属
_MetallicMap ("Metallic Map", 2D) = "white" {}
// 金属度数值,和纹理一起控制金属特性
_Metallic ("Metallic", Range(0,1)) = 0
// 粗糙度纹理,粗糙度越大,物体表面越粗糙,就像砂纸一样
_RoughnessMap ("Roughness Map", 2D) = "white" {}
// 粗糙度数值,进一步微调表面粗糙程度
_Roughness ("Roughness", Range(0,1)) = 0.5
// 法线纹理,让物体表面看起来有凹凸感,就像给它加了一层“皱纹”
_NormalMap ("Normal Map", 2D) = "bump" {}
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 100
Pass {
HLSLPROGRAM
// 开启多编译,支持前向渲染的各种光照类型
#pragma multi_compile_fwdbase
// 定义顶点着色器和片段着色器的函数名
#pragma vertex vert
#pragma fragment frag
// 引入 Unity 的核心 HLSL 库,里面有很多好用的工具函数
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
// 引入光照相关的 HLSL 库,处理光照计算就靠它啦
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
// 定义属性对应的变量
struct appdata {
// 顶点位置,就像物体在 3D 世界中的“坐标”
float4 vertex : POSITION;
// 顶点法线,告诉我们物体表面的朝向
float3 normal : NORMAL;
// 纹理坐标,用于定位纹理在物体上的位置
float2 uv : TEXCOORD0;
// 切线,辅助计算法线纹理
float4 tangent : TANGENT;
};
struct v2f {
// 裁剪空间下的顶点位置,是顶点在屏幕上的“投影”
float4 vertex : SV_POSITION;
// 纹理坐标,传递给片段着色器
float2 uv : TEXCOORD0;
// 世界空间下的顶点位置,用于光照计算
float3 worldPos : TEXCOORD1;
// 世界空间下的法线方向
half3 worldNormal : TEXCOORD2;
// 世界空间下的切线方向
half3 worldTangent : TEXCOORD3;
// 世界空间下的副切线方向
half3 worldBitangent : TEXCOORD4;
};
// 定义属性变量
sampler2D _BaseMap;
float4 _BaseMap_ST;
half4 _BaseColor;
sampler2D _MetallicMap;
half _Metallic;
sampler2D _RoughnessMap;
half _Roughness;
sampler2D _NormalMap;
// 顶点着色器,把顶点从模型空间转换到裁剪空间
v2f vert (appdata v) {
v2f o;
// 把顶点位置从模型空间转换到裁剪空间
o.vertex = TransformObjectToHClip(v.vertex.xyz);
// 传递纹理坐标
o.uv = TRANSFORM_TEX(v.uv, _BaseMap);
// 把顶点位置从模型空间转换到世界空间
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
// 把法线从模型空间转换到世界空间
o.worldNormal = TransformObjectToWorldNormal(v.normal);
// 把切线从模型空间转换到世界空间
o.worldTangent = TransformObjectToWorldDir(v.tangent.xyz);
// 计算副切线,通过叉积得到
o.worldBitangent = cross(o.worldNormal, o.worldTangent) * v.tangent.w;
return o;
}
// 片段着色器,计算每个像素的颜色
half4 frag (v2f i) : SV_Target {
// 采样基础颜色纹理
half4 baseMap = tex2D(_BaseMap, i.uv);
// 应用基础颜色
half3 baseColor = baseMap.rgb * _BaseColor.rgb;
// 采样金属度纹理
half metallicMap = tex2D(_MetallicMap, i.uv).r;
// 结合金属度数值
half metallic = metallicMap * _Metallic;
// 采样粗糙度纹理
half roughnessMap = tex2D(_RoughnessMap, i.uv).r;
// 结合粗糙度数值
half roughness = roughnessMap * _Roughness;
// 采样法线纹理
half4 normalMap = tex2D(_NormalMap, i.uv);
// 把法线从切线空间转换到世界空间
half3 normalTS = UnpackNormal(normalMap);
half3x3 tangentToWorld = half3x3(i.worldTangent, i.worldBitangent, i.worldNormal);
half3 normalWS = mul(normalTS, tangentToWorld);
// 获取主光源信息
Light mainLight = GetMainLight();
// 计算视线方向
half3 viewDir = SafeNormalize(_WorldSpaceCameraPos.xyz - i.worldPos);
// 计算光照方向
half3 lightDir = mainLight.direction;
// 计算 PBR 光照模型所需的参数
// 计算菲涅尔反射
half3 F0 = lerp(half3(0.04, 0.04, 0.04), baseColor, metallic);
half3 F = FresnelSchlick(max(dot(normalWS, viewDir), 0.0), F0);
// 计算法线分布函数
half NDF = DistributionGGX(normalWS, viewDir, roughness);
// 计算几何遮挡函数
half G = GeometrySmith(normalWS, viewDir, lightDir, roughness);
// 计算漫反射项
half3 kD = (1.0 - F) * (1.0 - metallic);
half3 diffuse = kD * baseColor / PI;
// 计算镜面反射项
half3 numerator = NDF * G * F;
half denominator = 4.0 * max(dot(normalWS, viewDir), 0.0) * max(dot(normalWS, lightDir), 0.0) + 0.001;
half3 specular = numerator / denominator;
// 计算光照强度
half NdotL = max(dot(normalWS, lightDir), 0.0);
half3 Lo = (diffuse + specular) * mainLight.color * NdotL;
// 环境光
half3 ambient = unity_AmbientSky.rgb * baseColor;
// 最终颜色
half3 finalColor = ambient + Lo;
return half4(finalColor, 1.0);
}
ENDHLSL
}
}
FallBack "Diffuse"
}