别再死记硬背公式了!用Unity手把手教你写一个能用的PBR着色器(附完整HLSL代码)
2026/5/6 17:44:31 网站建设 项目流程

从零实现Unity PBR着色器:抛弃理论公式的实战指南

很多开发者学习PBR渲染时都会陷入一个怪圈:啃完十几篇理论文章后,面对Unity编辑器依然无从下手。这篇文章将彻底打破这个循环——我们直接从代码入手,用可运行的HLSL实现一个完整的PBR着色器。当你看到金属表面随着粗糙度变化呈现真实的光泽过渡时,那些复杂的BRDF公式会突然变得直观起来。

1. 工程准备:搭建PBR基础框架

在Assets目录下创建两个文件:PBRLib.cgincPBRShader.shader。前者存放工具函数,后者是主着色器。这种分离设计让代码更易维护,也是Unity标准管线的常见做法。

关键文件结构

Assets/ ├── Shaders/ │ ├── PBRLib.cginc │ └── PBRShader.shader

1.1 基础光照模型配置

在PBRShader.shader中设置必要的编译指令和包含文件:

#pragma vertex vert #pragma fragment frag #pragma multi_compile_fwdbase nolightmap #include "UnityCG.cginc" #include "Lighting.cginc" #include "AutoLight.cginc" #include "PBRLib.cginc"

提示:multi_compile_fwdbase确保着色器接收主方向光数据,nolightmap禁用光照贴图以简化实现

1.2 材质属性定义

使用Properties块声明所有可调节参数:

_AlbedoMap("Albedo", 2D) = "white" {} _NormalMap("Normal", 2D) = "bump" {} _MetallicMap("Metallic", 2D) = "black" {} _RoughnessMap("Roughness", 2D) = "white" {} _AlbedoColor("Albedo Color", Color) = (1,1,1,1) _Metallic("Metallic", Range(0,1)) = 0.0 _Roughness("Roughness", Range(0,1)) = 0.5

参数对照表

参数名类型默认值作用
_AlbedoMap2D纹理白色基础颜色贴图
_NormalMap2D纹理法线表面微观细节
_Metallic0-1滑块0金属度混合值
_Roughness0-1滑块0.5表面粗糙程度

2. 核心算法实现:拆解PBR三大方程

2.1 法线分布函数(NDF)实现

在PBRLib.cginc中添加GGX分布函数:

float DistributionGGX(float NdotH, float roughness) { float alpha = roughness * roughness; float alpha2 = alpha * alpha; float denom = (NdotH * NdotH) * (alpha2 - 1.0) + 1.0; return alpha2 / (PI * denom * denom); }

这个函数控制镜面高光的形状:

  • 低粗糙度时产生锐利的高光
  • 高粗糙度时产生模糊的漫反射效果

2.2 几何遮挡函数优化

使用Schlick-GGX近似实现几何项:

float GeometrySchlickGGX(float NdotV, float roughness, bool isDirect) { float k = isDirect ? (roughness + 1.0) * (roughness + 1.0) / 8.0 : roughness * roughness / 2.0; return NdotV / (NdotV * (1.0 - k) + k); }

注意:直接光照和间接光照使用不同的k值计算,这是迪士尼PBR方案的重要优化

2.3 菲涅尔效应实现

Schlick近似法模拟金属反射特性:

float3 FresnelSchlick(float3 F0, float cosTheta) { return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); }

金属与非金属的F0值差异

  • 非金属:0.04(常见电介质)
  • 金属:使用albedo颜色值

3. 着色器组装:从理论到像素

3.1 顶点着色器配置

构建完整的顶点到片段数据结构:

struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float3 worldPos : TEXCOORD1; float3 worldNormal : TEXCOORD2; float3 worldTangent : TEXCOORD3; float3 worldBitangent : TEXCOORD4; LIGHTING_COORDS(5,6) }; v2f vert(appdata_full v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.texcoord, _AlbedoMap); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldTangent = UnityObjectToWorldDir(v.tangent.xyz); o.worldBitangent = cross(o.worldNormal, o.worldTangent) * v.tangent.w; TRANSFER_VERTEX_TO_FRAGMENT(o); return o; }

3.2 片段着色器核心逻辑

完整的PBR光照计算流程:

// 材质参数采样 float3 albedo = sRGBToLinear(tex2D(_AlbedoMap, i.uv).rgb * _AlbedoColor.rgb); float metallic = tex2D(_MetallicMap, i.uv).r * _Metallic; float roughness = max(tex2D(_RoughnessMap, i.uv).r, _Roughness); // 基础向量计算 float3 viewDir = normalize(_WorldSpaceCameraPos - i.worldPos); float3 lightDir = normalize(_WorldSpaceLightPos0.xyz); float3 halfDir = normalize(lightDir + viewDir); // 法线贴图处理 float3 tangentNormal = UnpackNormal(tex2D(_NormalMap, i.uv)); float3x3 TBN = float3x3(i.worldTangent, i.worldBitangent, i.worldNormal); float3 normal = normalize(mul(tangentNormal, TBN)); // BRDF计算 float3 F0 = lerp(0.04, albedo, metallic); float3 F = FresnelSchlick(F0, max(dot(halfDir, viewDir), 0.0)); float3 kS = F; float3 kD = (1.0 - metallic) * (1.0 - F); float3 diffuse = kD * albedo / PI; float3 specular = (DistributionGGX(NdotH, roughness) * GeometrySmith(NdotL, NdotV, roughness, true) * F) / (4.0 * NdotL * NdotV + EPS); // 最终合成 float3 color = (diffuse + specular) * _LightColor0.rgb * NdotL * shadow; return float4(LinearToGamma(color), 1.0);

4. 实战调试与性能优化

4.1 常见问题排查指南

问题现象:材质全黑

  • 检查法线贴图是否正确导入为Normal map格式
  • 确认场景中有有效方向光
  • 验证Albedo贴图的alpha通道是否异常

问题现象:高光区域闪烁

  • 增加EPS极小值防止除零错误
  • 检查roughness值是否被限制在0.01-1.0范围
  • 确认所有点积计算都有max(dot(), 0.0)保护

4.2 分支优化技巧

将条件判断移出关键循环:

// 优化前(性能较差) if(metallic > 0.5) { kD = 0; } else { kD = 1 - F; } // 优化后(使用lerp避免分支) kD = (1.0 - metallic) * (1.0 - F);

4.3 实时调试参数

添加这些调试模式到片段着色器:

// 在return前添加调试开关 #ifdef DEBUG_SPECULAR return float4(specular, 1.0); #endif #ifdef DEBUG_NORMAL return float4(normal * 0.5 + 0.5, 1.0); #endif

在Unity中通过Shader Variants快速切换不同调试视图,这是理解PBR各分量贡献度的最佳方式。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询