从零实现Unity PBR着色器抛弃理论公式的实战指南很多开发者学习PBR渲染时都会陷入一个怪圈啃完十几篇理论文章后面对Unity编辑器依然无从下手。这篇文章将彻底打破这个循环——我们直接从代码入手用可运行的HLSL实现一个完整的PBR着色器。当你看到金属表面随着粗糙度变化呈现真实的光泽过渡时那些复杂的BRDF公式会突然变得直观起来。1. 工程准备搭建PBR基础框架在Assets目录下创建两个文件PBRLib.cginc和PBRShader.shader。前者存放工具函数后者是主着色器。这种分离设计让代码更易维护也是Unity标准管线的常见做法。关键文件结构Assets/ ├── Shaders/ │ ├── PBRLib.cginc │ └── PBRShader.shader1.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各分量贡献度的最佳方式。