Esfog_UnityShader教程_镜面反射SpecularReflection
作者信息与原文链接
教程引言
本系列教程第四篇,原计划昨日完成,稍作偷懒便推迟至今日。本期将深入探讨镜面反射的基本原理与具体代码。本篇承接上一篇《Esfog_UnityShader教程_漫反射DiffuseReflection》,若您尚未阅读或对漫反射理解不足,建议点击链接查看,这将有助于您更好地理解本篇内容。
镜面反射概述
在上一篇关于漫反射的讲解中,我们提及光照处理中最常见的两个课题:漫反射和镜面反射(高光)。本节将着重介绍镜面反射,并顺带讲解环境光,如此可使渲染效果更为出色。
直观感受
如上图(图片来自网络)所示,漫反射与镜面反射给人的直观感受有所不同。漫反射的效果是,对于物体的某一位置,无论从哪个角度观察,其亮度始终保持一致。而镜面反射则不然,从不同位置观察物体同一点的受光照情况,结果会有差异。这是因为光照反射并非均匀地分布在各个方向,而是与反射光线和视线的偏差大小成反比。当视线与反射光线重合时,看到的亮度最高;若角度大于 90 度,则无法看到高光效果。例如,在阳光下观察表面光滑的汽车,会发现汽车外壳上的高光区域会随观察位置的改变而移动和变化。
计算所需向量
如上图(图片取自《Cg Programming in Unity》),计算镜面反射所需的参数比漫反射更多,一共涉及 4 个向量:光的入射方向 L(反方向,下同)、表面法线 N、观察目标位置到摄像机的向量 V(视线)以及反射光线 R。这里仅考虑平行光的情况,其他类型光源的计算方法有所不同,您可自行了解,本文不再赘述,若后续有相关应用,会具体说明。
计算原理
参考上图,最终决定我们看到的光强的是视线 V 和反射向量 R。这两个向量的夹角越小,意味着光线经物体表面反射后越接近直接进入我们的眼睛(摄像机),其关系类似于上一篇漫反射中入射光线 L 和法线 N 的关系。因此,我们通过公式 V·R = |V||R|cosθ 来计算。从这个公式可以理解,为何在不同位置观察物体同一点的镜面反射结果会不同。虽然仅需两个向量即可确定影响高光的因子,但反射光线 R 需要通过法线 N 和入射光线 L 计算得出。
计算反射光线 R
如图所示(图片取自网友 butwang 博客),我们可先计算一个向量 s,将其乘以 2 得到 2s,再根据向量加法规则 L + 2s = R 计算最终结果。计算向量 s 需利用 L 和 L 在 N 上的投影向量。由于 N·L = |N||L|cosθ,若 N 为单位向量,则 |N| = 1,所以 N·L = |L|cosθ,L 在 N 上的投影距离为 N·L,将结果乘以 N 的单位向量,可得 L 在 N 上的投影向量为 (L·N)N。通过向量减法可计算出 s = (L·N)N - L,进而得出 R = 2s + L = 2((L·N)N - L) + L = 2N(N·L) - L。不过,Cg 已提供 reflect(L, N) 函数来简化这一计算过程,第一个参数为光的真正入射方向(非反方向),第二个参数为法向量。
镜面反射计算公式
《The Cg Tutorial》中给出的镜面反射计算公式如下(经调整后效果相同):
下面对公式进行解释:Ks 为材质的反射颜色,与上一篇漫反射公式中的 Kd 类似。Kd 通常可设置为贴图颜色,而 Ks 一般不能如此设置。个人理解,Ks 用于设置物体表面受高光时呈现的颜色。若有相应的高光贴图,可利用 Ks 读取贴图颜色,以实现不同位置的不同高光效果;若无高光贴图,默认设置为纯白即可。lightColor 为光源颜色。facing 变量用于处理特殊情况,当计算光照强度时,虽 V 和 R 的夹角小于 90 度,但如果 L 和 N 的夹角大于 90 度,物体实际上不应受该光照影响。因此,若 N 和 L 夹角大于 90 度,facing 为 0,否则为 1。max(V·R, 0) 与上一篇漫反射中的 max(N·L, 0) 类似。指数 shininess 用于调整光泽度,其值越大,物体表面越光泽,亮斑越小越集中;反之则越大越分散,一般设为 10 较为合适。
环境光计算
现实生活中,真实物体除直接接受光源光照外,还会受到周围物体反射光的影响。即使物体未直接受光源照射,也会呈现一定亮度。在渲染中,这些受其他物体反射得到的光统称为环境光。在 Unity 中,场景中所有物体使用相同的环境光,可在 Edit->Render Setting->Ambient Light 中进行设置,通常环境光较弱,只是对真实世界效果的大致模拟。《The Cg Tutorial》中给出的环境光计算公式如下:
其中,Ka 为材质关于环境光的系数,可理解为与漫反射中的 Kd 保持一致,globalAmbient 为我们设置的环境光颜色。
最终,将环境光、漫反射和镜面反射的颜色相加,得到最终的渲染颜色:Color = ambient + diffuse + specular。
具体代码实现
Shader "Esfog/SpecularReflection"
{
Properties
{
_MainTex ("Base (RGB)", 2D) = "white" {}
_SpecColor("SpecularColor", Color) = (1,1,1,1)
_Shininess("Shininess", Float) = 10
}
SubShader
{
Pass
{
Tags { "RenderType"="Opaque" "LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#pragma target 5.0
uniform float4 _LightColor0;
uniform sampler2D _MainTex;
uniform float _Shininess;
uniform float4 _SpecColor;
struct VertexOutput
{
float4 pos:SV_POSITION;
float4 posWorld:TEXCOORD0;
float3 normal:TEXCOORD1;
float2 uv:TEXCOORD2;
};
VertexOutput vert(appdata_base input)
{
VertexOutput o;
o.pos = mul(UNITY_MATRIX_MVP, input.vertex);
o.posWorld = mul(_Object2World, input.vertex);
o.normal = normalize(mul(float4(input.normal, 0.0), _World2Object).xyz);
o.uv = input.texcoord.xy;
return o;
}
float4 frag(VertexOutput input):COLOR
{
float3 normalDir = normalize(input.normal);
float3 viewDir = normalize(float3(_WorldSpaceCameraPos - input.posWorld));
float4 Kd = tex2D(_MainTex, input.uv);
float4 Ks = _SpecColor;
float4 Ka = Kd;
float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
float3 ambientLighting = Ka.rgb * UNITY_LIGHTMODEL_AMBIENT.rgb;
float3 diffuseReflection = Kd.rgb * _LightColor0.rgb * max(0.0, dot(normalDir, lightDir));
float facing;
if (dot(normalDir, lightDir) <= 0)
{
facing = 0;
}
else
{
facing = 1;
}
float3 SpecularReflection = facing * _LightColor0.rgb * _SpecColor.rgb * pow(max(0, dot(reflect(-lightDir, normalDir), viewDir)), _Shininess);
return float4(ambientLighting + diffuseReflection + SpecularReflection, 1);
}
ENDCG
}
}
FallBack "Diffuse"
}
代码解释
部分代码与漫反射基本相同,以下仅解释本节新增部分:
- 第 6 - 7 行:在 Properties 中定义了新变量 _Shininess(float 类型)和 _SpecColor(Color 类型)。_Shininess 用于调节物体表面的光泽度,原理前文已解释;_SpecColor 用于调整高光反射颜色,若有高光贴图,可将其改为 2D 类型。
- 第 21 - 22 行:声明后续使用的变量,并与 Properties 中的变量关联。
- 第 26 行:在 VertexOutput 中新增一个变量,用于计算视线向量,因此需知道物体在世界空间中的位置(可在任意空间计算,只要参与计算的两个向量处于同一空间即可)。
- 第 35 行:将模型空间的点左乘 _Object2World 矩阵,转换为世界空间的顶点位置。
- 第 45 - 47 行:分别为漫反射、高光和环境光的反射系数赋值,注意高光与其他两者的区别。
- 第 49 行:利用环境光计算公式计算环境光,其中 UNITY_LIGHTMODEL_AMBIENT 是 Unity 提供的用于获取场景中环境光颜色的变量。
- 第 51 - 59 行:计算镜面反射公式中的 facing,原理前文已说明。
- 第 60 行:利用镜面反射公式计算高光颜色。需注意两点:一是给 reflect 函数传入射光时,应传入真正的入射方向,因此需在之前计算的 lightDir 前加负号;二是在 Cg 中通过 pow 函数计算指数,该函数在常见编程语言中均有提供。
- 第 61 行:将计算得到的漫反射、高光和环境光颜色相加,得到最终的渲染颜色。
效果展示与总结
本系列教程第四篇至此结束。结合前一篇内容,我们对最基本的光照模型有了初步了解。下一篇可能会结合漫反射和镜面反射进行实例演示,也可能继续探讨其他相关内容。后续内容可能会涉及更多数学原理和思考方法,Shader 的难点不在于语法,而在于理解背后的原理。深入探索背后的知识,比单纯掌握使用方法收获更大。
继续使用上一篇中的模型展示效果:
- 上图为使用上一篇漫反射的效果。
- 上图为使用本篇 ambient + diffuse + specular 的效果,质感明显提升。若添加高光贴图,效果将更佳。
感谢大家的支持!希望这些内容能给您带来启发。