《Unity Shader入门精要》笔记(十一)
本文为《Unity Shader入门精要》第九章内容《更复杂的光照》的上半部分笔记,主要涉及渲染路径和光源类型。本文相关代码,详见:
原书代码,详见原作者github:
<hr/>
1. Unity的渲染路径
渲染路径决定了光照是如何应用到Unity Shader中的,它告诉Unity光源和处理后的光照信息从哪些数据中去取。
Unity 5.0之前,有3种渲染路径:前向渲染路径(Forward Rendering Path)、延迟渲染路径(Deferred Rendering Path)和顶点照明渲染路径(Vertex Lit Rendering Path)。
Unity5.0之后,顶点照明渲染路径已不建议使用(但Unity依然兼容);且新的延迟渲染路径代替了原来的延迟渲染路径(老版本的延迟渲染路径在Unity中依然兼容)。
大多数情况下一个项目只会使用一种渲染路径。修改项目渲染路径的操作:
菜单-Edit-Project Settings-Graphics-Tier Settings,可在对应的Tier下面去掉Use Defaults的勾选,选择想要的Rendering Path。
但有时会需要使用多个渲染路径来渲染不同的物体,可以使用多个相机,分别在Camera组件中设置不同的渲染路径,以覆盖Project Settings中的设置:
如果当前显卡不支持所选择的渲染路径,Unity会自动使用更低一级的渲染路径。比如:GPU不支持延迟渲染,则会使用前向渲染。
完成设置后,就可以在Unity Shader的Pass中设置标签LightMode:
Pass
{
Tags { &#34;LightMode&#34; = &#34;ForwardBase&#34; }
}
前向渲染除了ForwardBase,还有ForwardAdd。
LightMode标签支持的渲染路径设置选项:
Unity会根据Pass中LightMode标签的设置,对一些内置光照变量进行赋值。如果没有设置标签或设置了不正确的标签,当我们调用光照变量时,会得到不正确的结果。
1.1 前向渲染路径
前向渲染路径是传统的渲染方式,也是最常用的一种渲染路径。
1.1.1 原理
每进行一次完整的前向渲染,我们需要渲染该对象的图元,并计算两个缓冲区的信息:颜色缓冲区、深度缓冲区。
先根据深度缓冲来决定一个片元是否可见,如果可见再更新颜色缓冲区中的颜色值。
伪代码:
Pass
{
for (each primitive in this model)
{
for (each fragment covered by this primitive)
{
if (failed in depth test)
{
// 如果没有通过深度测试,说明该片元不可见
discard;
}
else
{
// 如果该片元可见,则进行光照计算
float4 color = Shading(materialInfo, pos, normal, lightDir, viewDir);
// 更新帧缓冲
writeFrameBuffer(fragment, color);
}
}
}
}
每个逐像素光源都需要进行上述渲染流程。如果一个物体存在多个逐像素光源的影响,则需要执行多个Pass,每个Pass计算一个逐像素光源的光照结果,然后在帧缓冲中把这些光照结果混合,得到最终的颜色值。
Pass执行次数 = N个物体 * M个光源
渲染引擎会限制每个物体的逐像素光照数目,以此来限制Pass执行的次数。
1.1.2 Unity中的前向渲染
Unity中,前向渲染有3种处理光照的方式:逐顶点处理、逐像素处理、球谐函数(Spherical Harmonics, SH)处理。
决定光源使用哪种处理方式,取决于它的类型和渲染模式。
[*]类型指该光源是平行光还是其他类型的光源;
[*]渲染模式指该光源是否是重要的(Important)。
如果一个光照被设置成Important,Unity就会以逐像素的方式处理该光源。
设置光照类型和渲染模式:
前向渲染中,渲染一个物体时,Unity会根据场景中各个光源的设置以及这些光源对物体的影响程度(距离远近、光照强度等)对这些光源进行重要度排序。
其中:
[*]一定数目的光源会按逐像素的方式处理;
[*]最多4个光源按逐顶点的方式处理;
[*]剩下的光源按球谐(SH)方式处理。
Unity使用的判断规则:
[*]场景中最亮的平行光总是按逐像素处理;
[*]渲染模式被设置成Not Important的光源,会按逐顶点或球谐(SH)处理;
[*]渲染模式被设置成Important的光源,会按逐像素处理;
[*]如果以上规则得到的逐像素光源数量小于Quality设置中的逐像素光源数量(Pixel Light Count),会有更多的光源以逐像素的方式进行渲染。
光照会在Unity Shader的Pass中进行计算。前向渲染有两种Pass:
[*]Base Pass
[*]Additional Pass
上图说明:
[*]使用#pragmamulti_compile_fwdbase和#pragma multi_compile_fwdadd,为了在Pass中得到一些正确的光照变量,如:光照衰减;
[*]Base Pass中渲染的平行光默认支持阴影(需要开启光源的阴影功能),而Additional Pass中渲染的光源默认没有阴影效果,即便设置了Shadow Type也不生效,但我们依然可以使用#pragma multi_compile_fwdadd_fullshadows编译指令代替#pragma multi_compile_fwdadd,为点光源和聚光灯开启阴影效果,但这需要Unity在内部使用更多的Shader变种;
[*]环境光和自发光对于一个物体来说,只希望计算一次,如果在Additoinal Pass中计算它们,会造成叠加计算多次,得不到我们想要的效果,所以会放在Base Pass中计算;
[*]Additional Pass中会计算不同的光源,不同的光源的光照效果需要混合,所以需要开启并设置混合模式,通常会使用Blend One One等混合指令;
[*]通常一个Unity Shader会定义一个Base Pass(双面渲染等情况会定义多次)和一个Additional Pass,但是一个Base Pass只会执行一次,而一个Additional Pass会根据影响该物体的其他逐像素光源的数目被调用多次,即:每个逐像素光源都会执行一次Additional Pass。
1.1.3 内置的光照变量和函数
前向渲染使用的内置光照变量:
前向渲染可以使用的内置光照函数:
上面列出的内置变量和函数只是一部分,后面学习中会用到其他一些变量和函数。
1.2 顶点照明渲染路径
顶点照明渲染路径是对硬件配置要求最少、运算性能最高,但同时得到效果最差的一种类型。它仅支持逐顶点的光源计算,不支持逐像素,所以无法得到:阴影、法线映射、高精度的高光反射等效果,它是前向渲染的一个子集。
1.2.1 Unity中的顶点照明渲染
顶点照明渲染路径通常在一个Pass中就可以完成对物体的渲染,在这个Pass中,我们会计算关心的光源对物体的照明,且是按照逐顶点处理的。
它是Unity中最快速的渲染路径,且具有最广泛的硬件支持(游戏机上不支持这种路径)。Unity 5以后被作为一个遗留的渲染路径,未来的版本中可能会被移除。
1.2.2 可访问的内置变量和函数
在Unity中一个顶点照明的Pass中最多可以访问8个逐顶点光源。
顶点照明渲染路径中可以使用的内置变量:
如果影响该物体的光源数目小于8,则数组中剩余的光源颜色会被设置成黑色。有些变量我们同样可以在前向渲染路径中使用,如:unity_LightColor,但这些变量的数组的维度和数值在不同渲染路径下的值是不同的。
顶点照明渲染路径中可以使用的内置函数:
1.3 延迟渲染路径
当场景中包含大量实时光源,使用前向渲染会造成性能急剧下降。每个光源都会执行一个Pass,重复地渲染一个物体会存在很多相同的重复计算。
在这样的环境下,本是一种更古老的渲染方法的延迟渲染,又流行了起来。除了前向渲染中使用的颜色缓冲和深度缓冲外,延迟渲染还使用了额外的缓冲区——G缓冲(Geometry Buffer,G-buffer)。G缓冲区存储了我们关心的表面(通常指离相机最近的表面)的其他信息,如:表面的法线、位置、用于光照计算的材质属性等。
1.3.1 原理
延迟渲染主要包含2个Pass。第一个Pass不进行任何光照计算,仅仅计算哪些片元可见,将可见的片元信息存储到G缓冲区。
伪代码:
Pass 1
{
// 第一个Pass不进行真正的光照计算
// 仅仅把光照计算需要的信息保存到G缓冲中
for (each primitive in this model)
{
for (each fragment covered by this primitive)
{
if (failed in depth test)
{
// 如果没有通过深度测试,则该片元不可见
discard;
}
else
{
// 如果该片元可见,把需要的信息存储到G缓冲中
writeGBuffer(materialInfo, pos, normal, lightDir, viewDir);
}
}
}
}
Pass 2
{
// 使用G缓冲中的信息进行真正的光照计算
for (each pixel in the screen)
{
if (the pixel is valid)
{
// 如果该像素是有效的,读取对应G缓冲中的信息
readGBuffer(pixel, materialInfo, pos, normal, lightDir, viewDir);
// 根据读取到的信息进行光照计算
float4 color = Shading(materialInfo, pos, normal, lightDir, viewDir);
// 更新帧缓冲
writeFrameBuffer(pixel, color);
}
}
}
延迟渲染使用的Pass数目通常是2个,与场景中包含的光源数目没有关系,和屏幕的大小有关。
1.3.2 Unity中的延迟渲染
目前Unity有2种延迟渲染路径:
[*]Unity 5之前使用的,遗留的延迟渲染路径;
[*]Unity 5及以后使用的延迟渲染路径。
延迟渲染路径需要一定的硬件支持。新旧延迟渲染路径之间的差别很小,只是使用了不同的技术来权衡不同的需求,比如:旧版的延迟渲染路径不支持Unity 5以后版本的基于物理的Standard Shader。
遗留的延迟渲染路径可参考官方文档:
http://docs.unity3d.com/Manual/RenderTech-DeferredLighting.html
下面仅讨论Unity 5以后的延迟渲染路径。
延迟渲染适合在光源数目众多、使用前向渲染造成性能瓶颈的情况下使用,每个光源都可以按逐像素处理。但它也有缺点:
[*]不支持真正的抗锯齿功能;
[*]不能处理半透明物体;
[*]对显卡有要求,必须支持MRT(Multiple Render Targets)、Shader Mode 3.0及以上、深度渲染纹理以及双面的模板缓冲。
延迟渲染需要两个Pass:
[*]第一个Pass用于渲染G缓冲
把物体的漫反射颜色、高光反射颜色、平滑度、法线、自发光和深度等信息渲染到屏幕空间的G缓冲区中对每个物体来说,这个Pass仅执行一次。
[*]第二个Pass用于计算真正的光照模型
使用上一个Pass中渲染的数据来计算最终的颜色光照颜色,再存储到帧缓冲中。
默认的G缓冲区包含了以下几个渲染纹理(Render Texture,RT):
[*]RT0
ARGB32格式,RBG通道存储漫反射颜色,A通道未被使用。
[*]RT1
ARGB32格式,RGB通道存储高光反射颜色,A通道存储高光反射的指数部分。
[*]RT2
ARGB2101010格式,RGB通道存储法线,A通道未被使用。
[*]RT3
ARGB32格式(非HDR)或ARGBHalf(HDR),用于存储自发光 + lightmap + 反射探针(reflection probes)。
[*]深度缓冲和模板缓冲
第二个Pass计算光照,默认情况下仅可以使用Unity内置的Standard光照模型。若想使用其他模型,需要替换掉原来的Internal-DeferredShading.shader文件,详见:http://docs.unity3d.com/Manual/RenderTech-DeferredShading.html。
1.3.3 可访问的内置变量和函数
延迟渲染路径中可以使用的内置变量:
这些变量可以在UnityDeferredLibrary.cginc文件中找到声明。
1.4 选择哪种渲染路径
Unity官方给出了4种渲染路径:
[*]前向渲染路径
[*]延迟渲染路径
[*]遗留的延迟渲染路径
[*]顶点照明渲染路径
的详细比较,包括特性比较(是否支持逐像素光照、半透明物体、实时阴影等)、性能比较以及平台支持,详见:http://docs.unity3d.com/Manual/RenderingPaths.html。
一般会根据游戏发布的平台来选择渲染路径,若当前显卡不支持所选渲染路径,则会自动使用更低一级的渲染路径。
本笔记中后续的学习主要使用Unity的前向渲染路径。
2. Unity的光源类型
Unity一共支持4种光源类型:平行光、点光源、聚光灯和面光源(area light)。面光源仅在烘焙时才有用,不在我们讨论的范围。
2.1 光源类型有什么影响
最常使用的光源属性有:光源的位置、方向(到某点的方向)、颜色、强度以及衰减(到某点的衰减,与点到光源的距离有关)。
2.1.1 平行光
平行光可以照亮的范围是没有限制的,它通常作为太阳这样的角色在场景中出现,它没有位置的概念,将它放到场景中任意位置都不影响光照的效果,它的几何属性只有方向,通过调整Transform组建中的Rotation属性来改变它的光源方向。
平行光没有衰减的概念,光照强度不会随距离发生改变。
2.1.2 点光源
点光源表示由一个点发出的、向所有方向延伸的光,它的照亮空间是有限的,由空间中的一个球体定义。
在Scene视图中开启光照才能预览光源是如何影响场景中的物体的:
点光源的属性有:位置(Position)、光照范围(Range)、方向、颜色强度等。对于方向属性,可以用点光源的位置减去某点的位置得到它到该点的方向,其他属性可以在面板中调整。
点光源会随距离增大而衰减,球心处的光照强度最强,边界处最弱(值为0),中间的衰减值可由一个函数定义。
2.1.3 聚光灯
聚光灯是这3种光源类型中最复杂的一种,照亮范围是有限的,相比点光源,不再是简单的球体,而是有空间中的一个锥形区域定义的。
聚光灯可用于表示由一个特定位置出发、向特定方向延伸的光,类比生活中的例子,比较像手电筒。
聚光灯有位置(Position)、光照范围(Range)、张开角度(Spot Angle)、方向、颜色强度等。对于方向属性,可以用聚光灯的位置减去某点的位置得到它到该点的方向。
聚光灯的衰减随物体的远离逐渐减小,锥形的顶点处光照强度最强,锥形的边界处强度为0,中间的衰减值可由一个函数定义,这个函数相比点光源衰减的计算公式更加复杂,因为需要判断一个点是否在椎体的范围内。
2.2 在前向渲染中处理不同的光源类型
接下来实现在前向渲染路径的基础上,在Unity Shader中访问它们的5个属性:位置、方向、颜色、强度以及衰减。
2.2.1 准备工作
完成如下准备工作:
[*]新建名为Scene_9_2_2_1的场景,并去掉天空盒子;
[*]新建名为ForwardRenderingMat的材质;
[*]新建名为Chapter9-ForwardRendering的Unity Shader,并赋给上一步的材质;
[*]在场景中创建一个胶囊体,并将第二步创建的材质赋给它;
[*]为了让胶囊体受到多个光源的影响,在场景中新建一个点光源,并将颜色设为绿色(和平行光区分开);
[*]保存场景。
详细操作详见《<Unity Shader入门精要>笔记(四)》里的案例常用操作说明。
2.2.2 编写Shader代码
将之前笔记中写过的Chapter6-BlinnPhong代码复制覆盖到Chapter9-ForwardRendering文件中,做部分修改。关于光源属性的访问代码已用注释说明,代码如下:
// 修改Shader命名
Shader &#34;Unity Shaders Book/Chapter 9/Forward Rendering&#34;
{
Properties
{
_Diffuse(&#34;Diffuse&#34;, Color) = (1, 1, 1, 1)
_Specular(&#34;Specular&#34;, Color) = (1, 1, 1, 1)
_Gloss(&#34;Gloss&#34;, Range(8.0, 256)) = 20
}
SubShader
{
Tags { &#34;RenderType&#34; = &#34;Opaque&#34; }
// Base pass
Pass
{
// 设置为ForwardBase,处理环境光和第一个逐像素光照(平行光)
Tags { &#34;LightMode&#34; = &#34;ForwardBase&#34; }
CGPROGRAM
// 执行该指令,让前向渲染路径的光照衰减等变量可以被正确赋值
#pragma multi_compile_fwdbase
#pragma vertex vert
#pragma fragment frag
#include &#34;Lighting.cginc&#34;
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.worldNormal);
// Unity会选择最亮的平行光传递给Base Pass进行逐像素处理
// 其他平行光会按照逐顶点或在Additional Pass中按逐像素的方式处理
// 对于Base Pass来说,处理逐像素光源类型一定是平行光
// 使用_WorldSpaceLightPos0得到这个平行光的方向
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
// 环境光只需要计算在Base Pass中计算一次即可
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 使用_LightColor0得到平行光的颜色和强度
// (_LightColor0已经是颜色和强度相乘后的结果)
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
// 因为平行光没有衰减,所以衰减值为1.0
fixed atten = 1.0;
return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}
ENDCG
}
// Additional pass
Pass
{
// 设置为ForwardAdd
Tags { &#34;LightMode&#34; = &#34;ForwardAdd&#34; }
// 开启混合模式,将帧缓冲中的颜色值和不同光照结果进行叠加
// (Blend One One并不是唯一,也可以使用其他Blend指令,比如:Blend SrcAlpha One)
Blend One One
// Additional pass中的顶点、片元着色器代码是根据Base Pass中的代码复制修改得到的
// 这些修改一般包括:去掉Base Pass中的环境光、自发光、逐顶点光照、SH光照的部分
CGPROGRAM
// 执行该指令,保证Additional Pass中访问到正确的光照变量
#pragma multi_compile_fwdadd
#pragma vertex vert
#pragma fragment frag
#include &#34;Lighting.cginc&#34;
#include &#34;AutoLight.cginc&#34;
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
// 去掉Base Pass中环境光
fixed3 worldNormal = normalize(i.worldNormal);
// 计算不同光源的方向
#ifdef USING_DIRECTIONAL_LIGHT
// 平行光方向可以直接通过_WorldSpaceLightPos0.xyz得到
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
#else
// 点光源或聚光灯,_WorldSpaceLightPos0表示世界空间下的光源位置
// 需要减去世界空间下的顶点位置才能得到光源方向
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
#endif
// 使用_LightColor0得到光源(可能是平行光、点光源或聚光灯)的颜色和强度
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
// 计算不同光源的衰减
#ifdef USING_DIRECTIONAL_LIGHT
// 平行光不衰减
fixed atten = 1.0;
#else
// 将片元的坐标由世界空间转到光源空间
float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;
// Unity选择使用一张纹理作为查找表(Lookup Table, LUT)
// 对衰减纹理进行采样得到衰减值
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
#endif
return fixed4((diffuse + specular) * atten, 1.0);
}
ENDCG
}
}
Fallback &#34;Specular&#34;
}
保存代码,得到如下效果:
注意:上述代码仅供学习了解,并不会用于真正的项目中。
2.3 实验:Base Pass和Additional Pass的调用
上一节给出了前向渲染中Unity是如何决定哪些光源是逐像素光,而哪些是逐顶点或球谐(SH)光。为了更直观地理解,接下来通过实验学习了解。
2.3.1 准备工作
完成如下准备工作:
[*]新建名为Scene_9_2_2_2的场景,并去掉天空盒子;
[*]将平行光的颜色调为绿色;
[*]在场景中创建一个胶囊体,将上一节的ForwardRenderingMat材质赋给该胶囊体;
[*]新建4个点光源,颜色调整成相同的红色;
[*]保存场景。
得到如下效果:
2.3.2 原理分析
当我们创建一个光源时,它的默认Render Mode是Auto,意味着Unity会自动为我们判断哪些光源会逐像素处理,哪些会逐顶点或球谐(SH)的方式处理。
在菜单-Edit-Project Settings-Quality-Pixel Light Count中,我们可以看到默认情况下,一个物体可以接收出除最亮的平行光外的4个逐像素光照:
而除了平行光,场景里刚好有4个点光源,它们的Render Mode为Auto,因此它们会在Additional Pass中以逐像素的方式被处理,每个光源调用一次Additional Pass。
2.3.3 使用FrameDebugger分析绘制过程
在菜单-Window-Analysis-Frame Debugger打开帧调试器,点击Enable按钮可以看到场景的渲染事件:
可以看到渲染胶囊体一共有6个关键事件,它们分别为:
[*]清除颜色、深度和模板缓冲;
[*]使用Base Pass将平行光的光照渲染到胶囊体;
[*]使用Additional Pass将第1个点光源的光照渲染到胶囊体;
[*]使用Additional Pass将第2个点光源的光照渲染到胶囊体;
[*]使用Additional Pass将第3个点光源的光照渲染到胶囊体;
[*]使用Additional Pass将第4个点光源的光照渲染到胶囊体;
Unity处理这些点光源是按照它们的重要度排序的,在我们的实验中,点光源的颜色和强度相同,所以距离就成了重要度的唯一衡量标准,离胶囊体最近的点光源会先被渲染。
如果将其中一个点光源移动到胶囊体不受其影响的位置:
将帧渲染器Disable再Enable,会看到关键的渲染事件变成了5个:
因为不对物体产生影响的光源,Unity不会调用相关的渲染事件。
如果我们将点光源的Render Mode设置为Not Important,这些点光源就不会以逐顶点和球谐的方式被处理,而因为我们的Base Pass中没有计算光照,所以这4个点光源不会对物体产生任何光照效果:
以上是关于Unity渲染路径和光源类型的学习笔记,下一个笔记我们将学习Unity的光照衰减和阴影。
写在最后
本文内容会同步发在笔者的公众号上,欢迎大家关注交流!
公众号:程序员叨叨叨(ID:i_coder)
页:
[1]