Unity3D 的物理渲染和光照模型
为什么地球在两极严寒,而在赤道火热?这个问题,仿佛与着色器毫不相干,但却是理解光照模型怎样工作的基础。正如这个教程前面部分所解释的,表面着色器使用数学模型来预测光照在三角形上怎样反射。总的来说,Unity 引擎支持两种着色技术,一个是哑光着色器,一个是镜面材料着色器。前一种对于不透明表面的支持很完美,而后一种则用来模拟反射对象。这些光照模型背后的数学可能非常复杂,但是如果你想创造属于你自己的光照效果,你就得理解它们是如何工作的。直到 Unity 版本 4.x,默认的漫射光照模型都基于朗伯反射(Lambertian reflectance)的。
漫反射面:郎伯模型
回到最开始的问题,两极冷的原因就在于它受照射的阳光比赤道区域少。这是由于它们受到太阳的斜射。下图显示了八角形两极区域受到的光线明显少于正面区域。
蓝线代表了正交法向量单位长度。橙线表示了光线的方向。光通量的衰减取决于光线方向与法向向量的夹角。在郎伯模型( Lambertian model )中,它的值等于垂直的入射光线。
其可以表述为:
\[I= \left \| L \right \| \, cos \alpha = cos \alpha\]
式中, \left \| L \right \| 为 L(这个量是之前定义过的)的长度 ,然后 \alpha 为 N 和 L 的夹角。这个算子在向量代数中被叫做点积 ,在前边的文章中也已经简单介绍了。正式的写法应该是这个样子的:
\[A \cdot B = \left \| A \right \| \, \left \| B \right \| \, cos \alpha\]
在 Cg/HLSL 中都可以使用点操作符。其将返回一个从 -1 到 1 的数,当两向量正交时将返回 0,并且当它们平行时 \pm 为 1。我们将使用其作为一个乘法系数,代表了从一个光源接收到多少三角光(三角函数光,即正弦余弦的意思,意会!)。
朗伯着色器(Lambertian shader)
我们现在已经有必要来理解一个朗伯模型在着色器中是如何实现的。Cg/HLSL 允许用一个自定义的函数替换标准的朗伯模型。在第8行,在指令 #pragma surface 中使用 SimpleLambert,强制着色器搜索叫做 LightingSimpleLambert 的函数:
Shader "Example/SimpleLambert" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderType" = "Opaque" } CGPROGRAM #pragma surface surf SimpleLambert struct Input { float2 uv_MainTex; }; sampler2D _MainTex; void surf (Input IN, inout SurfaceOutput o) { o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb; } half4 LightingSimpleLambert (SurfaceOutput s, half3 lightDir, half atten) { half NdotL = dot (s.Normal, lightDir); half4 c; c.rgb = s.Albedo * _LightColor0.rgb * (NdotL * atten * 2); c.a = s.Alpha; return c; } ENDCG } Fallback "Diffuse" } Shader "Example/SimpleLambert" {Properties {_MainTex ("Texture", 2D) = "white" {}}SubShader {Tags { "RenderType" = "Opaque" }CGPROGRAM#pragma surface surf SimpleLambertstruct Input {float2 uv_MainTex;};sampler2D _MainTex;void surf (Input IN, inout SurfaceOutput o) {o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;}half4 LightingSimpleLambert (SurfaceOutput s, half3 lightDir, half atten) {half NdotL = dot (s.Normal, lightDir);half4 c;c.rgb = s.Albedo * _LightColor0.rgb * (NdotL * atten * 2);c.a = s.Alpha;return c;}ENDCG}Fallback "Diffuse"}
从19到25行 展示了朗伯模型如何简单地在一个表面着色器中再实现。NdotL 为光颜色的强度系数(相乘)。参数衰减器用于调制光强。其需要被两个量乘是最初 Unity3D 用于仿真某些特效的技巧。这在 Aras Pranckevičius 中有解释,在Unity4中留存了下来为了向后兼容。最终在 Unity5 进行了修复,所以如果你在 Unity5 上再实现一个朗伯模型的时候,仅仅只要乘上一个量就行啦。
理解标准发光模型的原理是改变其不可或缺的步骤。许多可选的着色技术事实上仍然使用朗伯模型作为其第一步。
Toon shading
最近在游戏中常用的风格之一即是Toon shading(又称 cel shading).这是一种非逼真渲染风格,通过改变了光在一个模型上反射实际情况来给人以手绘的感觉。为了达到这样的效果,我们需要用一个自定义模型来替换至今使用的标准光照模型。最常见用于达到这种效果的方法就是使用加性纹理,在下面的着色器中叫做_RampTex。
Shader "Example/Toon Shading" { Properties { _MainTex ("Texture", 2D) = "white" {} _RampTex ("Ramp", 2D) = "white" {} } SubShader { Tags { "RenderType" = "Opaque" } CGPROGRAM #pragma surface surf Toon struct Input { float2 uv_MainTex; }; sampler2D _MainTex; void surf (Input IN, inout SurfaceOutput o) { o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb; } sampler2D _RampTex; fixed4 LightingToon (SurfaceOutput s, fixed3 lightDir, fixed atten) { half NdotL = dot(s.Normal, lightDir); NdotL = tex2D(_RampTex, fixed2(NdotL, 0.5)); fixed4 c; c.rgb = s.Albedo * _LightColor0.rgb * NdotL * atten * 2; c.a = s.Alpha; return c; } ENDCG } Fallback "Diffuse" } Shader "Example/Toon Shading" {Properties {_MainTex ("Texture", 2D) = "white" {}_RampTex ("Ramp", 2D) = "white" {}}SubShader {Tags { "RenderType" = "Opaque" }CGPROGRAM#pragma surface surf Toonstruct Input {float2 uv_MainTex;};sampler2D _MainTex;void surf (Input IN, inout SurfaceOutput o) {o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;}sampler2D _RampTex;fixed4 LightingToon (SurfaceOutput s, fixed3 lightDir, fixed atten){half NdotL = dot(s.Normal, lightDir);NdotL = tex2D(_RampTex, fixed2(NdotL, 0.5));fixed4 c;c.rgb = s.Albedo * _LightColor0.rgb * NdotL * atten * 2;c.a = s.Alpha;return c;}ENDCG}Fallback "Diffuse"}
LightingToon模型计算了光强的朗伯系数NdotL并使用ramp纹理以将其离散化。该例子中仅映射了四级光强阶。不同的ramp纹理将细化地获得不同的toon shading变形。
镜面: Blinn-Phong模型
朗伯模型并不能仿真镜面反射材料。这种情况下就需要另一种技术; Unity4.x 采用了 Blinn-Phong 模型. 不需要计算法向量 和光矢量 的点积, 而是通过和视角的角平分矢量 来计算 :
数量 通过 和 设置进一步计算。如果你想深入了解Unity中光照模型的计算, 可以下载其内置着色器的源码. 朗伯和Blinn-Phong表面函数都是在Lighting.cginc文件中计算的。而在 Unity5 中需要在遗产着色器(Legacy shaders)中寻找。
在Unity5中物理渲染
就像本文在前面提出的那样, Uniy4.x使用朗伯光照模型作为其默认着色器。Unity5中发生了改变, 其引入了 物理渲染 (PBR). 这个名字听起来相当有趣啊, 但是与其它光照模型没有什么不同。相比于朗伯反射, PBR提供了一个更加逼真的光线物体作用模型。术语physically 来源于,PBR考虑了材料的物理属性, 比如能量守恒以及光的散射。Unity5为艺术家和开发者提供了两种不同的方法,用于创建他们的PBR 材料: 金属工作流 和 镜面工作流。在前者中,一个材料对光的反射取决于其是什么样的金属(或者说,含有多少金属的量)。
简单来说就是,光是电磁波啊,其行为因接触到的是导体还是绝缘体而不同(电磁波唱由电场和磁场组成,但电场属性实际上比磁场属性更重要)。在镜面工作流中, 所提供的是镜面映射。尽管被当做两个不同的东西呈现出来, 金属材料和镜面材料实际上以不同的方式初始化同一个着色器;Marmoset 上有一个很好的例程展现了同样的材料如何通过金属和镜面工作流分别创建。这也是为什么当第一次接触 Unity5 着色器时会因为源代码中出现了同一事物却有两个工作流现象时会产生误解。 Joe Wilson 创建了一个相当清晰的例程来知道我们的艺术家:如果你想学习怎么通过 PBR 创建非常逼真的材质,这将是非常好的开始哟。如果需要更详细的技术信息,在 Unity5 博客里关于 PBR 的地方猛戳a very well done primer 。
Unity5 中新光照模型名字既简单又标准。取这个名字是因为现在 PBR 是每一个 Unity3D 中新建对象的初始材料。而且,每一个新建的着色器文件会被自动配置为一个 PBR 表面着色器:
Shader "Custom/NewShader" { Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Albedo (RGB)", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM // Physically based Standard lighting model, and enable shadows on all light types #pragma surface surf Standard fullforwardshadows // Use shader model 3.0 target, to get nicer looking lighting #pragma target 3.0 sampler2D _MainTex; struct Input { float2 uv_MainTex; }; half _Glossiness; half _Metallic; fixed4 _Color; void surf (Input IN, inout SurfaceOutputStandard o) { // Albedo comes from a texture tinted by color fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color; o.Albedo = c.rgb; // Metallic and smoothness come from slider variables o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = c.a; } ENDCG } FallBack "Diffuse" } Shader "Custom/NewShader" {Properties {_Color ("Color", Color) = (1,1,1,1)_MainTex ("Albedo (RGB)", 2D) = "white" {}_Glossiness ("Smoothness", Range(0,1)) = 0.5_Metallic ("Metallic", Range(0,1)) = 0.0}SubShader {Tags { "RenderType"="Opaque" }LOD 200CGPROGRAM// Physically based Standard lighting model, and enable shadows on all light types#pragma surface surf Standard fullforwardshadows // Use shader model 3.0 target, to get nicer looking lighting#pragma target 3.0sampler2D _MainTex;struct Input {float2 uv_MainTex;};half _Glossiness;half _Metallic;fixed4 _Color;void surf (Input IN, inout SurfaceOutputStandard o) {// Albedo comes from a texture tinted by colorfixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;o.Albedo = c.rgb;// Metallic and smoothness come from slider variableso.Metallic = _Metallic;o.Smoothness = _Glossiness;o.Alpha = c.a;}ENDCG}FallBack "Diffuse"}
14行将告知 Unity3D 该表面着色器将使用 PBR 光照模型。17行 意味着该着色器将使用高级特性,因而其将不同在落后的硬件上使用。同样的, SurfaceOutput 也不能同 PBR 一起使用;而是必须使用 SurfaceOutputStandard。
PBR 表面输出
在 SurfaceOutputStandard 中不光是 Albedo,Normal,Emission 和 Alpha 这些属性,还有三个新的:
-
半金属性: 物体该具有怎样的金属含量。同常为0或1,但对于一些奇怪的材料可以使用中间的值。这个将决定光如何在材料上发生反射;
-
半平滑性:决定表面平滑程度, 从0到1;
-
半吸收性:指定 AO 特效大小。
你应当通过 SurfaceOutputStandardSpecular 使用镜面流,其使用 float3 镜面替换了半金属性。注意当朗伯反射存在一个半镜面场,其在 PBR 中的镜面属性就是一个 float3。这符合镜面反射光波的 RGB 颜色值属性 。
Unity 中使用的着色技术
迄今为止已经介绍了四种不同的着色技术。为了避免混淆,可以参考下表中所示,顺序分别为:着色技术,表面着色器名,表面输出结构体名和内置着色器名称。
Unity4 及以下 | Unity5 及以上 | |
漫射 | Lambertian reflectance Lambert, SurfaceOutput Bumped Diffuse
| Physically Based Rendering (Metallic) Standard, SurfaceOutputStandard Standard |
反射 | Blinn–Phong reflection BlinnPhong, SurfaceOutput Bumped Specular
| Physically Based Rendering (Specular) Standard, SurfaceOutputStandardSpecular Standard (Specular setup) |
PBR 背后的方程非常复杂。如果你对背后的数学比较感兴趣,维基百科中的渲染方程和这篇文章 将是非常好的起点。
如果你导入 Unity3D 包 (包含了该例程中用到的着色器), 你将注意到内置 “Bumped Diffuse” 着色器是怎么产生一个与其最初实现“Simple Lambert”非常不同的结果。这是因为 Unity3D 的着色器增加了额外的特性,比如正常的映射。
结论
本文介绍了用于表面着色的自定义光照模型。通过一个关于如何修改获得不同特性的真实例子简单解释了朗伯和 Blinn-Phong 模型。有必要注意下单纯的漫射材料实际上在生活中是不存在的:即使是你所想到的最钝的材料也会有一些镜面反射。漫射材料在过去中非常普遍,因为计算镜面反射开销太大。
本文也介绍了什么是 PBR,以及在 Unity5 中如何使用。PBR 着色器与表面着色器没有什么区别,仅仅是带有了一个非常高级的光照模型。