# Unity URP BRDF shader
本篇主要内容是在 Unity URP 中实现 BRDF shader
BRDF 理论部分在这篇文章中讲解。
Unity 版本:Unity 6.0
渲染路径:Forward / Forward+
# 代码
# BRDF
- 经典的 BRDF 模型只描述单次表面反射,这会导致渲染结果偏暗。为了解决这个问题,需要做多次散射补偿。
- 为了防止结果偏暗,本篇使用简单的经验参数近似。
- 符合物理的多次散射可以使用 Kulla-Conty 近似,其理论部分在这篇文章中讲解。
- 环境光照使用拟合预计算的 BRDF LUT
// GGX normal distribution function | |
// GGX 法线分布函数 | |
// GGX 用于描述微表面模型中微观几何表面法线的概率分布 | |
// Unity 中已经定义了 D_GGX,为了防止冲突这里换一个名字 | |
float D_GGX_1(float NdotH, float roughness) | |
{ | |
float a = roughness * roughness; | |
float a2 = a * a; | |
float denom = (NdotH * NdotH * (a2 - 1) + 1); | |
return a2 / (PI * denom * denom); | |
} | |
// Smith geometric occlusion function | |
// Smith 几何遮蔽函数 | |
// 几何遮蔽 (G) 在 BRDF 中用于模拟微表面间的自阴影和遮蔽效应 | |
float V_Smith(float NdotV, float NdotL, float roughness) | |
{ | |
// Smith-GGX 高度相关几何遮蔽,性能优化版 | |
// 这里返回的是 V = G / (4 * NdotL * NdotV) | |
float a = roughness * roughness; | |
float a2 = a * a; | |
float v = NdotL * sqrt(NdotV * NdotV * (1 - a2) + a2); | |
float l = NdotV * sqrt(NdotL * NdotL * (1 - a2) + a2); | |
return 0.5 / max(v + l, 0.001); | |
// G_Smith | |
// 原始 GGX 高度相关几何遮蔽 的实现,后续还要 除以 4 * NdotV * NdotL | |
//float r = pow(roughness, 4); | |
//float k = 2 * NdotL * NdotV; | |
//float v = NdotL * sqrt(r + (1 - r) * pow(NdotV, 2)); | |
//float l = NdotV * sqrt(r + (1 - r) * pow(NdotL, 2)); | |
//return k / max(v + l, 0.001); | |
// UE4 的实现 | |
//float a = roughness * roughness; | |
//float v = NdotL * (NdotV * (1 - a) + a); | |
//float l = NdotV * (NdotL * (1 - a) + a); | |
//return 0.5 / max(v + l, 0.001); | |
// Unity HDRP 的实现 | |
//float a = roughness; | |
//float v = NdotL * (NdotV * (1 - a) + a); | |
//float l = NdotV * sqrt(NdotL * NdotL * (1 - a * a) + a * a); | |
//return 0.5 / max(v + l, 0.001); | |
} | |
// Schlick Fresnel | |
// Schlick 菲涅尔近似 | |
// 菲涅尔是指光照基于观察者的角度来形成不同强度反射的现象 | |
half3 F_Schlick(float HdotV, half3 F0) | |
{ | |
return F0 + (1 - F0) * pow(1 - HdotV, 5); | |
} | |
// 拟合预计算的 BRDF LUT (Scale 和 Bias) | |
// Brian Karis 在 "Real Shading in Unreal Engine 4" 中提出的一个经验公式,用于近似环境光照中高光部分的 BRDF 效果 | |
float3 EnvBRDFApprox(half3 f0, float roughness, float NdotV) | |
{ | |
// 这里的常数是针对 GGX 分布的拟合结果 | |
const float4 c0 = { -1, -0.0275, -0.572, 0.022 }; | |
const float4 c1 = { 1, 0.0425, 1.04, -0.04 }; | |
// 计算受粗糙度影响的中间系数 r | |
// 描述了不同粗糙度下反射能量的分布规律 | |
float4 r = roughness * c0 + c1; | |
// 模拟菲涅尔项随角度变化的响应 (Grazing Angle 补偿) | |
//exp2 (-9.28 * NdotV) 是一个非常经典的经验拟合公式,用于模拟物体边缘(掠射角)处,反射率由于菲涅尔效应急剧增加的曲线 | |
float angularResponse = min(r.x * r.x, exp2(-9.28 * NdotV)) * r.x + r.y; | |
// 得到 A (Scale) 和 B (Bias) | |
// 这里的常数 float2 (-1.04, 1.04) 是为了将前面的 angularResponse 映射回正确的积分能量区间 | |
float2 AB = float2(-1.04, 1.04) * angularResponse + r.zw; | |
// 最终结合 F0 | |
// 这个结果直接对应了单次散射下,物体在 IBL 环境中的高光反射率 | |
return f0 * AB.x + AB.y; | |
} | |
half3 DirectBRDF(float3 mainlightDirWS, InputData inputData, SurfaceData surfaceData) | |
{ | |
// 半角向量 | |
float3 halfDir = normalize(mainlightDirWS + inputData.viewDirectionWS); | |
// 各种角度余弦值 | |
float NdotL = saturate(dot(inputData.normalWS, mainlightDirWS)); | |
float NdotV = saturate(dot(inputData.normalWS, inputData.viewDirectionWS)); | |
float NdotH = saturate(dot(inputData.normalWS, halfDir)); | |
float VdotH = saturate(dot(inputData.viewDirectionWS, halfDir)); | |
// 粗糙度 | |
float roughness = max(1 - surfaceData.smoothness, 0.001); | |
// 金属度影响菲涅尔反射率 | |
half3 F0 = lerp((half3)0.04.xxx, surfaceData.albedo, surfaceData.metallic); | |
// 单次反射 | |
float D = D_GGX_1(NdotH, roughness); | |
float V = V_Smith(NdotV, NdotL, roughness); | |
half3 F = F_Schlick(VdotH, F0); | |
half3 specular = D * V * F; | |
// 多次散射 | |
// 为了防止结果偏暗,这里使用简单的经验参数近似 | |
specular *= 3; | |
half3 kd = (1 - F) * (1 - surfaceData.metallic); | |
// 在物理公式中,Lambert 漫反射需要除以 PI 来保证能量守恒: kd * albedo / PI | |
// 但在部分游戏引擎中,光源的强度已经隐含了 PI 的补偿。此时除以 PI 会导致物体的亮度下降 | |
half3 diffuse = kd * surfaceData.albedo; | |
return (diffuse + specular) * NdotL; | |
} | |
// 间接光照的 BRDF 计算 | |
half3 IndirectBRDF(InputData inputData, SurfaceData surfaceData) | |
{ | |
float NdotV = saturate(dot(inputData.normalWS, inputData.viewDirectionWS)); | |
float roughness = max(1 - surfaceData.smoothness, 0.001); | |
float a = roughness * roughness; | |
half3 F0 = lerp((half3)0.04.xxx, surfaceData.albedo, surfaceData.metallic); | |
// 间接漫反射 (使用 SH 环境球谐) | |
half3 indirectDiffuse = SampleSH(inputData.normalWS) * surfaceData.albedo * (1 - surfaceData.metallic); | |
// 间接高光 (使用反射探针) | |
float3 reflectVector = reflect(-inputData.viewDirectionWS, inputData.normalWS); | |
half3 indirectSpecular = GlossyEnvironmentReflection(reflectVector, inputData.positionWS, a, surfaceData.occlusion, inputData.normalizedScreenSpaceUV); | |
// 使用近似 LUT 补偿能量,这一步会根据 NdotV 和 Roughness 计算出高光反射的比例 | |
indirectSpecular *= EnvBRDFApprox(F0, a, NdotV); | |
return indirectDiffuse + indirectSpecular; | |
} |
# Shader
- 为了兼容 SRP Batcher 需要单独写一个 CBUFFER
TEXTURE2D(_BaseMap); | |
SAMPLER(sampler_BaseMap); | |
TEXTURE2D(_BumpMap); | |
SAMPLER(sampler_BumpMap); | |
CBUFFER_START(UnityPerMaterial) | |
float4 _BaseMap_ST; | |
float4 _BumpMap_ST; | |
float _BumpScale; | |
float _Metallic; | |
float _Smoothness; | |
half4 _BaseColor; | |
CBUFFER_END |
- Shader 部分包含 UniversalForward、ShadowCaster、DepthOnly、DepthNormals
Shader "Unlit/MyBRDF" | |
{ | |
Properties | |
{ | |
_BaseMap("Base Map", 2D) = "white" {} | |
_BaseColor("Base Color", Color) = (1, 1, 1, 1) | |
_BumpMap("Normal Map", 2D) = "bump" {} | |
_BumpScale("Bump Scale", float) = 1 | |
_Metallic("Metallic", Range(0, 1)) = 0 | |
_Smoothness("Smoothness", Range(0, 1)) = 0.5 | |
} | |
SubShader | |
{ | |
Tags | |
{ | |
"RenderType" = "Opaque" | |
"RenderPipeline" = "UniversalPipeline" | |
} | |
LOD 100 | |
Pass | |
{ | |
Name "MyForward" | |
Tags | |
{ | |
"LightMode" = "UniversalForward" | |
} | |
HLSLPROGRAM | |
#pragma target 3.0 | |
// 顶点 / 片元入口 | |
#pragma vertex vert | |
#pragma fragment frag | |
// 雾效 | |
#pragma multi_compile_fog | |
// Forward+ | |
#pragma multi_compile _ _FORWARD_PLUS | |
// 主光源阴影 | |
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS | |
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE | |
// 额外光源(逐顶点 / 逐像素) | |
#pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS | |
// 额外光源阴影 | |
#pragma multi_compile _ _ADDITIONAL_LIGHT_SHADOWS | |
// 软阴影 | |
#pragma multi_compile _ _SHADOWS_SOFT | |
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" | |
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" | |
#include "./MyInput.hlsl" | |
#include "./MyBRDF.hlsl" | |
struct Attributes | |
{ | |
float4 positionOS : POSITION; | |
float3 normalOS : NORMAL; | |
float4 tangentOS : TANGENT; | |
float4 texcoord : TEXCOORD0; | |
}; | |
struct Varyings | |
{ | |
float4 positionCS : SV_POSITION; | |
float4 uv : TEXCOORD0; | |
float3 positionWS : TEXCOORD1; | |
float3 normalWS : TEXCOORD2; | |
float4 tangentWS : TEXCOORD3; | |
#ifdef _ADDITIONAL_LIGHTS_VERTEX | |
// x: fogFactor, yzw: vertex light | |
half4 fogFactorAndVertexLight : TEXCOORD4; | |
#else | |
half fogFactor : TEXCOORD4; | |
#endif | |
}; | |
void InitializeSurfaceData(float4 uv, out SurfaceData outSurfaceData) | |
{ | |
outSurfaceData = (SurfaceData)0; | |
// 纹理 | |
half4 albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv.xy); | |
outSurfaceData.alpha = albedo.a; | |
outSurfaceData.albedo = albedo.rgb * _BaseColor.rgb; | |
outSurfaceData.albedo = AlphaModulate(outSurfaceData.albedo, outSurfaceData.alpha); | |
// 法线 | |
half4 packedNormal = SAMPLE_TEXTURE2D(_BumpMap, sampler_BumpMap, uv.zw); | |
outSurfaceData.normalTS = UnpackNormalScale(packedNormal, _BumpScale); | |
outSurfaceData.metallic = _Metallic; | |
outSurfaceData.smoothness = _Smoothness; | |
outSurfaceData.occlusion = 1; | |
} | |
void InitializeInputData(Varyings input, float3 normalTS, out InputData inputData) | |
{ | |
inputData = (InputData)0; | |
inputData.positionWS = input.positionWS; | |
inputData.viewDirectionWS = GetWorldSpaceNormalizeViewDir(input.positionWS); | |
float sign = input.tangentWS.w; | |
float3 bitangentWS = sign * cross(input.normalWS.xyz, input.tangentWS.xyz); | |
inputData.tangentToWorld = half3x3(input.tangentWS.xyz, bitangentWS.xyz, input.normalWS.xyz); | |
// mul(normalTS, tangentToWorld) | |
inputData.normalWS = TransformTangentToWorld(normalTS, inputData.tangentToWorld, true); | |
// 归一化的屏幕 UV | |
inputData.normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(input.positionCS); | |
// 计算阴影坐标 | |
inputData.shadowCoord = TransformWorldToShadowCoord(inputData.positionWS); | |
// 计算雾效坐标 | |
#ifdef _ADDITIONAL_LIGHTS_VERTEX | |
inputData.fogCoord = InitializeInputDataFog(float4(input.positionWS, 1.0), input.fogFactorAndVertexLight.x); | |
inputData.vertexLighting = input.fogFactorAndVertexLight.yzw; | |
#else | |
inputData.fogCoord = InitializeInputDataFog(float4(input.positionWS, 1.0), input.fogFactor); | |
#endif | |
} | |
Varyings vert (Attributes i) | |
{ | |
Varyings o; | |
o.positionWS = TransformObjectToWorld(i.positionOS.xyz); | |
o.positionCS = TransformWorldToHClip(o.positionWS); | |
o.normalWS = TransformObjectToWorldNormal(i.normalOS); | |
o.tangentWS.xyz = TransformObjectToWorldDir(i.tangentOS.xyz); | |
o.tangentWS.w = i.tangentOS.w; | |
o.uv.xy = TRANSFORM_TEX(i.texcoord, _BaseMap); | |
o.uv.zw = TRANSFORM_TEX(i.texcoord, _BumpMap); | |
half3 vertexLight = VertexLighting(o.positionWS, o.normalWS); | |
half fogFactor = 0; | |
#if !defined(_FOG_FRAGMENT) | |
fogFactor = ComputeFogFactor(o.positionCS.z); | |
#endif | |
#ifdef _ADDITIONAL_LIGHTS_VERTEX | |
o.fogFactorAndVertexLight = half4(fogFactor, vertexLight); | |
#else | |
o.fogFactor = fogFactor; | |
#endif | |
return o; | |
} | |
half4 frag (Varyings i) : SV_Target | |
{ | |
// SurfaceData | |
SurfaceData surfaceData; | |
InitializeSurfaceData(i.uv, surfaceData); | |
// InputData | |
InputData inputData; | |
InitializeInputData(i, surfaceData.normalTS, inputData); | |
// 主光源 | |
Light mainLight = GetMainLight(inputData.shadowCoord); | |
half3 mainlightDirWS = normalize(mainLight.direction); | |
// 直接光 | |
half3 directBRDF = DirectBRDF(mainlightDirWS, inputData, surfaceData); | |
half3 diffuse = directBRDF * mainLight.color * mainLight.shadowAttenuation; | |
// 额外光源 | |
#ifdef _ADDITIONAL_LIGHTS | |
uint lightCount = GetAdditionalLightsCount(); | |
LIGHT_LOOP_BEGIN(lightCount) | |
Light light = GetAdditionalLight(lightIndex, inputData.positionWS, half4(1, 1, 1, 1)); | |
float3 addBRDF = DirectBRDF(normalize(light.direction), inputData, surfaceData); | |
diffuse += addBRDF * light.color * light.distanceAttenuation * light.shadowAttenuation; | |
LIGHT_LOOP_END | |
#endif | |
// 环境光 | |
half3 indirectBRDF = IndirectBRDF(inputData, surfaceData); | |
// 最终颜色 | |
half4 finalColor = half4(diffuse + indirectBRDF, surfaceData.alpha); | |
finalColor.rgb = MixFog(finalColor.rgb, inputData.fogCoord); | |
return finalColor; | |
} | |
ENDHLSL | |
} | |
Pass | |
{ | |
Name "MyShadowCaster" | |
Tags | |
{ | |
"LightMode" = "ShadowCaster" | |
} | |
ZTest LEqual | |
ColorMask 0 | |
HLSLPROGRAM | |
#pragma target 3.0 | |
#pragma vertex ShadowPassVertex | |
#pragma fragment ShadowPassFragment | |
// 在生成阴影贴图时用于区分方向光阴影和点光源阴影,因为它们使用不同的公式来应用法线偏移。 | |
#pragma multi_compile_vertex _ _CASTING_PUNCTUAL_LIGHT_SHADOW | |
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" | |
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl" | |
struct Attributes | |
{ | |
float4 positionOS : POSITION; | |
float3 normalOS : NORMAL; | |
}; | |
struct Varyings | |
{ | |
float4 positionCS : SV_POSITION; | |
}; | |
CBUFFER_START(UnityPerFrame) | |
float3 _LightDirection; | |
float3 _LightPosition; | |
CBUFFER_END | |
float4 GetShadowPositionHClip(Attributes input) | |
{ | |
float3 positionWS = TransformObjectToWorld(input.positionOS.xyz); | |
float3 normalWS = TransformObjectToWorldNormal(input.normalOS); | |
#if _CASTING_PUNCTUAL_LIGHT_SHADOW | |
float3 lightDirectionWS = normalize(_LightPosition - positionWS); | |
#else | |
float3 lightDirectionWS = _LightDirection; | |
#endif | |
float4 positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, normalWS, lightDirectionWS)); | |
positionCS = ApplyShadowClamping(positionCS); | |
return positionCS; | |
} | |
Varyings ShadowPassVertex(Attributes input) | |
{ | |
Varyings output; | |
output.positionCS = GetShadowPositionHClip(input); | |
return output; | |
} | |
half4 ShadowPassFragment(Varyings input) : SV_TARGET | |
{ | |
return 0; | |
} | |
ENDHLSL | |
} | |
Pass | |
{ | |
Name "MyDepthOnly" | |
Tags | |
{ | |
"LightMode" = "DepthOnly" | |
} | |
ZWrite On | |
ColorMask R | |
HLSLPROGRAM | |
#pragma target 3.0 | |
#pragma vertex DepthOnlyVertex | |
#pragma fragment DepthOnlyFragment | |
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" | |
struct Attributes | |
{ | |
float4 positionOS : POSITION; | |
}; | |
struct Varyings | |
{ | |
float4 positionCS : SV_POSITION; | |
}; | |
Varyings DepthOnlyVertex(Attributes input) | |
{ | |
Varyings output; | |
output.positionCS = TransformObjectToHClip(input.positionOS.xyz); | |
return output; | |
} | |
half DepthOnlyFragment(Varyings input) : SV_TARGET | |
{ | |
return input.positionCS.z; | |
} | |
ENDHLSL | |
} | |
Pass | |
{ | |
Name "MyDepthNormals" | |
Tags | |
{ | |
"LightMode" = "DepthNormals" | |
} | |
ZWrite On | |
HLSLPROGRAM | |
#pragma target 3.0 | |
#pragma vertex DepthNormalsVertex | |
#pragma fragment DepthNormalsFragment | |
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" | |
#include "./MyInput.hlsl" | |
struct Attributes | |
{ | |
float4 positionOS : POSITION; | |
float4 tangentOS : TANGENT; | |
float3 normal : NORMAL; | |
float2 texcoord : TEXCOORD0; | |
}; | |
struct Varyings | |
{ | |
float4 positionCS : SV_POSITION; | |
float2 uv : TEXCOORD0; | |
float3 normalWS : TEXCOORD1; | |
float4 tangentWS : TEXCOORD2; | |
}; | |
Varyings DepthNormalsVertex(Attributes input) | |
{ | |
Varyings output; | |
VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz); | |
VertexNormalInputs normalInput = GetVertexNormalInputs(input.normal, input.tangentOS); | |
output.positionCS = vertexInput.positionCS; | |
output.normalWS = normalInput.normalWS; | |
output.tangentWS.xyz = normalInput.tangentWS.xyz; | |
output.tangentWS.w = input.tangentOS.w; | |
output.uv = TRANSFORM_TEX(input.texcoord, _BumpMap); | |
return output; | |
} | |
half4 DepthNormalsFragment(Varyings input) : SV_Target | |
{ | |
float3 bitangent = input.tangentWS.w * cross(input.normalWS.xyz, input.tangentWS.xyz); | |
half4 packedNormal = SAMPLE_TEXTURE2D(_BumpMap, sampler_BumpMap, input.uv); | |
float3 normalTS = UnpackNormalScale(packedNormal, _BumpScale); | |
float3 normalWS = TransformTangentToWorld(normalTS, half3x3(input.tangentWS.xyz, bitangent.xyz, input.normalWS.xyz)); | |
half4 outNormalWS = half4(NormalizeNormalPerPixel(normalWS), 0); | |
return outNormalWS; | |
} | |
ENDHLSL | |
} | |
} | |
} |
# 效果
Metallic = 0
Smoothness = 0.5
![img]()
![img]()
Metallic = 1
Smoothness = 0.5
![img]()


