【光照】Unity[经验模型]和[物理模型]
在游戏开发中,光照是营造沉浸感的关键。Unity 作为主流引擎,提供了从简单到复杂的多种光照模型。本文将带你从基础概念出发,逐步深入理解经验模型(如 Lambert、Blinn-Phong)和物理模型(如 PBR),并通过完整代码示例掌握它们的实现。## 一、光照基础概念:为什么需要模型?现实世界中,光照与物体表面的交互极其复杂。为了在计算机中模拟这一过程,我们使用光照模型——它是一组数学公式,用于计算表面某点接收到的光能如何转化为人眼看到的颜色。-经验模型:基于视觉直觉,忽略物理正确性,追求计算效率。-物理模型:基于真实物理原理(能量守恒、微表面理论),追求真实感。Unity 的 Shader 是编写光照模型的主要工具。本文所有代码基于 Unity 内置渲染管线(Built-in RP)的 Surface Shader 和顶点/片元 Shader。—## 二、经验模型:Lambert 与 Blinn-Phong### 2.1 Lambert 漫反射模型原理:表面亮度与光线入射角余弦成正比(即朗伯余弦定律)。它假设表面粗糙,光线向所有方向均匀反射。公式:颜色 = 材质颜色 × 光源颜色 × max(0, dot(法线, 光源方向))完整 Shader 代码(Unity Surface Shader 风格):csharpShader "Example/LambertDiffuse" { Properties { _MainTex ("Texture", 2D) = "white" {} _Color ("Color", Color) = (1,1,1,1) } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Lambert sampler2D _MainTex; fixed4 _Color; struct Input { float2 uv_MainTex; }; void surf (Input IN, inout SurfaceOutput o) { fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color; o.Albedo = c.rgb; // 漫反射颜色 o.Alpha = c.a; } ENDCG } FallBack "Diffuse"}注释:-#pragma surface surf Lambert告诉 Unity 使用内置 Lambert 光照函数。-SurfaceOutput结构体中的Albedo就是材质颜色。### 2.2 Blinn-Phong 高光模型原理:在 Lambert 基础上增加高光,使用半向量(视线方向与光源方向的一半)代替反射向量,计算更高效。公式:高光 = 光源颜色 × 高光强度 × pow(max(0, dot(法线, 半向量)), 光泽度)完整 Shader 代码(顶点/片元 Shader 风格):csharpShader "Example/BlinnPhong" { Properties { _MainTex ("Texture", 2D) = "white" {} _SpecColor ("Specular Color", Color) = (1,1,1,1) _Shininess ("Shininess", Range(0.01, 10)) = 1 } SubShader { Tags { "RenderType"="Opaque" } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float3 worldNormal : TEXCOORD1; float3 worldPos : TEXCOORD2; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; float4 _SpecColor; float _Shininess; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; return o; } fixed4 frag (v2f i) : SV_Target { // 采样纹理 fixed4 texColor = tex2D(_MainTex, i.uv); // 法线归一化 float3 N = normalize(i.worldNormal); // 光源方向(假设只有一个主平行光) float3 L = normalize(_WorldSpaceLightPos0.xyz); // 视线方向 float3 V = normalize(_WorldSpaceCameraPos - i.worldPos); // 半向量 float3 H = normalize(L + V); // Lambert 漫反射 float NdotL = max(0, dot(N, L)); float3 diffuse = texColor.rgb * _LightColor0.rgb * NdotL; // Blinn-Phong 高光 float NdotH = max(0, dot(N, H)); float spec = pow(NdotH, _Shininess * 128); // 乘以128是为了匹配常见范围 float3 specular = _SpecColor.rgb * _LightColor0.rgb * spec; // 环境光 float3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * texColor.rgb; // 最终颜色 return fixed4(ambient + diffuse + specular, 1); } ENDCG } } FallBack "Diffuse"}注释:- 手动计算了法线、光源、视线和半向量。-_LightColor0是 Unity 内置的第一个平行光颜色。- 高光项乘以_Shininess * 128是为了让参数范围更直观。—## 三、物理模型:PBR(基于物理的渲染)### 3.1 PBR 核心原理PBR 遵循两个物理原则:1.能量守恒:反射光 + 折射光(次表面散射)≤ 入射光。2.微表面理论:表面由无数微小镜面组成,粗糙度决定高光扩散程度。关键参数:-Albedo:基础颜色,不含光照信息。-Metallic:金属度(1=金属,0=非金属)。-Smoothness:光滑度(1=镜面,0=粗糙)。-Normal Map:法线贴图增加细节。### 3.2 Unity 标准着色器(Standard Shader)Unity 内置了 PBR 实现,只需在 Surface Shader 中指定#pragma surface surf Standard:csharpShader "Example/PBR_Standard" { Properties { _MainTex ("Albedo (RGB)", 2D) = "white" {} _MetallicTex ("Metallic (R)", 2D) = "white" {} _Smoothness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 } SubShader { Tags { "RenderType"="Opaque" } CGPROGRAM #pragma surface surf Standard fullforwardshadows #pragma target 3.0 sampler2D _MainTex; sampler2D _MetallicTex; half _Smoothness; half _Metallic; struct Input { float2 uv_MainTex; }; void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = tex2D (_MainTex, IN.uv_MainTex); o.Albedo = c.rgb; // 从金属贴图读取金属度,否则使用全局值 half metallic = tex2D(_MetallicTex, IN.uv_MainTex).r * _Metallic; o.Metallic = metallic; o.Smoothness = _Smoothness; o.Alpha = c.a; } ENDCG } FallBack "Standard"}注释:-SurfaceOutputStandard是 PBR 专用的输出结构。-Metallic和Smoothness直接驱动能量守恒计算。- 设置fullforwardshadows支持阴影。### 3.3 手动实现简化 PBR(高级用法)为了深入理解,我们实现一个简化版 Cook-Torrance BRDF,包含漫反射(Lambert)和微表面高光(GGX)。csharpShader "Example/SimplePBR" { Properties { _MainTex ("Albedo", 2D) = "white" {} _Metallic ("Metallic", Range(0,1)) = 0 _Smoothness ("Smoothness", Range(0,1)) = 0.5 _NormalMap ("Normal Map", 2D) = "bump" {} } SubShader { Tags { "RenderType"="Opaque" } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float4 tangent : TANGENT; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float3 worldPos : TEXCOORD1; float3 worldNormal : TEXCOORD2; float3 worldTangent : TEXCOORD3; float3 worldBinormal : TEXCOORD4; float4 vertex : SV_POSITION; }; sampler2D _MainTex; sampler2D _NormalMap; float _Metallic; float _Smoothness; // GGX 法线分布函数 float DistributionGGX(float3 N, float3 H, float roughness) { float a = roughness * roughness; float a2 = a * a; float NdotH = max(dot(N, H), 0.0); float NdotH2 = NdotH * NdotH; float denom = (NdotH2 * (a2 - 1.0) + 1.0); return a2 / (3.14159 * denom * denom); } v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldTangent = UnityObjectToWorldDir(v.tangent.xyz); o.worldBinormal = cross(o.worldNormal, o.worldTangent) * v.tangent.w; return o; } fixed4 frag (v2f i) : SV_Target { // 法线贴图采样 float3 normalTS = UnpackNormal(tex2D(_NormalMap, i.uv)); float3x3 TBN = float3x3(i.worldTangent, i.worldBinormal, i.worldNormal); float3 N = normalize(mul(normalTS, TBN)); float3 V = normalize(_WorldSpaceCameraPos - i.worldPos); float3 L = normalize(_WorldSpaceLightPos0.xyz); float3 H = normalize(L + V); float roughness = 1.0 - _Smoothness; // 粗糙度 // 漫反射(能量守恒:金属无漫反射) float3 albedo = tex2D(_MainTex, i.uv).rgb; float3 F0 = lerp(0.04, albedo, _Metallic); // 菲涅尔基础反射率 float3 kS = F0 + (1.0 - F0) * pow(1.0 - max(dot(V, H), 0.0), 5.0); // Schlick菲涅尔 float3 kD = (1.0 - kS) * (1.0 - _Metallic); // 漫反射比例 // 高光项 float D = DistributionGGX(N, H, roughness); float NdotL = max(dot(N, L), 0.0); float NdotV = max(dot(N, V), 0.0); float3 specular = (D * kS) / (4.0 * NdotL * NdotV + 0.0001); float3 diffuse = kD * albedo / 3.14159; float3 final = (diffuse + specular) * _LightColor0.rgb * NdotL; final += UNITY_LIGHTMODEL_AMBIENT.rgb * albedo; return fixed4(final, 1); } ENDCG } } FallBack "Standard"}注释:- 手动实现 TBN 矩阵将法线贴图转换到世界空间。-DistributionGGX函数模拟微表面法线分布。- 菲涅尔效应使用 Schlick 近似,金属反射率取自albedo,非金属固定 0.04。—## 四、经验模型 vs 物理模型:如何选择?| 维度 | 经验模型 | 物理模型 ||------|----------|----------||计算开销| 低(适合移动端) | 高(需 PS4/PC/高端手机) ||真实感| 卡通风格、非写实 | 照片级写实 ||参数直观性| 直接调颜色和强度 | 需理解金属度、粗糙度 ||环境一致性| 依赖美术调参 | 自动适配环境光照 |实践建议:- 卡通游戏 → 使用 Blinn-Phong 或自定义经验模型。- 写实游戏 → 必须使用 PBR(Standard Shader 或自研 BRDF)。- 混合项目 → 低配设备用经验模型,高配设备用 PBR。—## 总结从 Lambert 漫反射到 Blinn-Phong 高光,再到 PBR 的 GGX 模型,我们走过了一条从视觉近似到物理模拟的进化之路。经验模型以其高效和易控性适合风格化渲染,而物理模型通过能量守恒和微表面理论带来了前所未有的真实感。在 Unity 中,你可以通过 Surface Shader 快速使用标准 PBR,也可以编写顶点/片元 Shader 实现自定义 BRDF。理解这两类模型的原理,将让你在不同项目需求面前做出最优选择。