目录
2、实验:Base Pass和Additional Pass的调用
9.1 Unity的渲染路径
渲染路径(Rendering Path)决定了光照是如何应用到Unity Shader中的。所以如果要和光源打交道,我们要给每个Pass指定渲染路径。这样光照计算才能被正确执行。
Unity5.0之前,主要有三种:向前渲染路径(Forward Rendering Path)、延迟渲染路径(Deferred Rendering Path)、顶点照明渲染路径(Vertex Lit Rendering Path)。5.0版本之后顶点照明渲染路径被抛弃了,延迟渲染路径被新版本的延迟渲染路径取代了,但也都可以对老版本进行兼容。
我们可以在Edit->Project Settings里面设置项目所需要的渲染路径,默认情况下是前向渲染路径。此外我们还可以在每个摄像机里单独设置使用的渲染路径,从而覆盖Project Settings中的设置,这样就可以使用多个渲染路径。默认情况下Camera使用的是Project Settings里的设置。
完成上面的设置后,我们就可以在每个Pass中使用LightMode标签来指定该Pass使用的渲染路径。不同类型的渲染路径可能包含多种标签设置。下面是Pass的LightMode标签支持的渲染路径设置选项。
如果没有指定渲染路径,那么有一些光照变量很可能不会被正确复制,计算出的效果也可能是错误的。
9.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数目也会很大。因此渲染引擎通常会限制每个物体的逐像素光照的数目。所以前向渲染存在的问题就是对性能消耗较高,尤其是场景中包含大量实时光源的时候。
2、Unity 中的前向渲染
一个Pass不仅仅可以用来计算逐像素光照,也可以计算逐顶点等其他光照。在渲染物体时,Unity会计算哪些光源照亮了它,以及光源照亮物体的方式。
在Unity中,前向渲染路径有三种处理光照的方式:逐顶点处理、逐像素处理、球谐函数处理(Spherical Harmonics,SH)处理。光源的类型和渲染模式决定了采用那种光照的处理方式。光源类型指的是该光源是平行光还是其他类型的光源,光源的渲染模式指的是该光源是否是重要的(Importance)。如果我们把一个光照的模式设置为Importance,说明这个光源很重要,就会当成一个逐像素的光源来处理。
在前向渲染中,Unity会对光源根据光源的设置、对物体的影响程度等,对光源进行一个重要度的排序。其中一定数目的光源会按逐像素的方式处理,然后最多有四个光源按逐顶点的方式处理,剩下的光源按SH方式处理。Unity使用的判断规则如下。
- 场景中最亮的平行光总是按逐像素处理。
- 渲染模式被设置为Not Important的光源,会按逐顶点或SH处理。
- 渲染模式被设置为Important的光源,会按逐像素处理。
- 如果根据上述规则得到的逐像素光源数量小于我们在Quality Setting中设置的逐像素光源数量(Pixel Light Count),就会再让一些比较重要的光源以逐像素的方式进行渲染。
前面的表有提到过,前向渲染的Pass有两种:Base Pass和Additional Pass。两种Pass进行的标签和渲染设置以及常规光照计算如下:
- 我们发现除了设置了LightMode的标签外,我们还使用了#pragma multi_compile_fwdbase和#pragma multi_compile_fwdadd的编译指令。这些编译指令保证Unity可以为相应类型的Pass生成所有需要的Shader变种,这些变种会处理不同条件下的渲染逻辑,如是否使用lightmap、当前使用哪种光源类型等。
- Base Pass渲染的平行光默认是支持阴影的(开启了光源的阴影功能),Additional Pass默认是不支持的,但我们可以使用#pragma multi_compile_fwdadd_fullshadows的编译指令,就可以为点光源和聚光灯开启阴影效果。
- 对于前向渲染,一个UnityShader通常会定义一个Base Pass(也可以被定义多次,如双面渲染)以及一个Additional Pass。一个Base Pass只会执行一次(定义多个Base Pass的情况除外),而Additional Pass会根据影响该物体的其他逐像素光源的数目被多次调用,即每个逐像素光源执行一次。
- Base Pass只会计算一次,所以环境光和自发光会在Base Pass中计算。如果放在Additional Pass会被多次计算造成多次环境光和自发光的叠加。
- Additonal Pass的渲染设置中还会开启和设置混合模式。因为这样每个Additional Pass才可以与上一次光照结果在帧缓存中叠加,得到有多个光照渲染的效果。如果没有开启,Additional Pass的渲染结果就会覆盖前面的渲染结果。一般设置的混合模式是Blend One One(第八章内容)
3、内置光照变量和函数
根据我们使用的渲染路径(Pass的LightMode标签),Unity会把不同的光照变量传给Shader。下面是一些前向渲染常用的光照变量和函数:
在Unity5.x某个版本升级后,_LightMatrix0变为unity_worldToLight。
9.1.2 顶点照明渲染路径
这种渲染路径对硬件配置要求最低、运算性能最高,但效果也是最差的。顶点照明渲染路径只能使用逐顶点的方式计算光照,所以对于阴影、法线映射、高精度的高光反射等逐像素才能得到的效果,都无法实现。
1、Unity中的顶点照明渲染
通常在一个Pass中就可以完成对物体的渲染。这个Pass中我们会计算所有光源对该物体的照明。这是Unity中最快速的渲染路径,也具有最广泛的硬件支持。
2、可访问的内置变量和函数
一个顶点照明的Pass中最多访问8个逐顶点光源。
9.1.3 延迟渲染路径
1、延迟渲染的原理
延迟渲染主要包含了两个Pass。第一个Pass不计算光照,只计算哪些片元是可见的(深度缓冲技术),如果可见九八它的相关信息存储到G缓冲区。然后在第二个Pass中,利用G缓冲区的各个片元信息(如表面法线、视角方向、漫反射系数等),进行真正的光照计算。把光照延迟到了第二个Pass中计算。
2、Unity中的延迟渲染(新版本)
延迟渲染适合在光源数目多、使用前向渲染造成性能瓶颈的情况下使用。而且延迟渲染路径的每个光源都可以按逐像素的方式处理。但也有一些缺点:
- 不支持真正的抗锯齿(anti-aliasing)功能
- 不能处理半透明物体
- 对显卡有一定要求
在Unity中使用延迟渲染时,我们也要提供两个Pass。
- 第一个Pass用于渲染G缓冲。我们会把物体的漫反射颜色、高光反射颜色、平滑度、法线、自发光和深度信息渲染到屏幕空间的G缓冲区中,对每个物体这个Pass只会执行一次。
- 第二个Pass用于计算真正的光照模型。这个Pass会使用上一个Pass中渲染的数据来计算最终光照,在存储到帧缓冲中。
G缓冲区包含了下面几个渲染纹理(Render Texture,RT)。
- RT0:ARGB32格式,RGB通道存储漫反射颜色,A通道没有使用。
- RT1:ARGB32格式,RGB通道存储高光反射颜色,A通道存储高光反射的指数部分。
- RT2:ARGB2101010格式,RGB通道用于存储法线,A通道没有使用。
- RT3:ARGB32(非HDR)格式 或 ARGBHalf(HDR)格式,用于存储自发光 + lightmap + 反射探针(reflection probes)。
- 深度缓冲和模板缓冲。
3、可访问的内置变量和函数
可以在UnityDeferredLibrary.cginc文件中找到对于的声明。
9.2 Unity的光源类型
Unit一共支持4种光源类型:平行光、点光源(point light)、聚光灯(spot light)、面光源(area light)。这节会学习如何在Unity中处理点光源和聚光灯。
9.2.1 光源类型的影响
最常用的光源属性有光源的位置、方向、颜色、强度以及衰减。这些属性与它们的几何定义息息相关。
1、平行光
最简单的一种光,没有确切的位置、范围没有限制、没有衰减的概念。
2、点光源
点光源照亮的空间有限,是一个球体。可以表示为由一个点发出的、向所有方向延伸的光。
方向属性要用点光源的位置减去某点的位置来得到它到该点的方向。点光源的衰减,随着物体远离点光源,光照强度也会逐渐减小。球心处最强,球面上为0。中间的衰减值可以由一个函数定义。
3、聚光灯
聚光灯的照亮空间是一块锥形区域。聚光灯可以表示由一个特点位置出发、向特点方向延伸的光。
如下图Range决定这个锥形的高,Spot Angle决定这个锥形的张开角度。聚光灯的衰减也是随着物体逐渐远离点光源而逐渐减小,在锥形的顶点处光照强度最强,在锥形的边界强度为0。
9.2.2 在前向渲染中处理不同的光源类型
1、实践
这节我们看一下如何在Unity Shader中访问它们的五个属性:位置、方向、颜色、强度、衰减。
先和之前一样新建场景、材质、Unity Shader、胶囊,把Shader赋给材质,把材质赋给胶囊体,关闭天空盒。为了让物体能受多个光源的影响,在平行光的基础上,新建了一个点光源,并设置为绿色以示区分。
Shader "MyShader/Chapter 9/Forward Rendering" {
Properties {
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(8.0, 256)) = 20
}
SubShader {
Tags { "RenderType"="Opaque" }
//Base Pass
Pass {
//第一个Pass用于渲染环境光或者说是第一个逐像素光照(平行光),也可以处理自发光,但这个案例没有。
//如果由多个平行光,Base Pass只会处理最亮的那一个,其他的在Additional Pass中处理
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
//下面这个指令可以保证文明在Shader中使用光照衰减等光照变量可以被正确赋值。
#pragma multi_compile_fwdbase
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
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);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
//计算环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//计算漫反射
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);
fixed atten = 1.0;//衰减指,因为平行光没有衰减,所有设置为1
return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}
ENDCG
}
//Additional pass
Pass {
//渲染其他的光照
Tags { "LightMode"="ForwardAdd" }
//设置混合模式,不唯一
Blend One One
CGPROGRAM
//同样的这个指令可以保证我们在Additional Pass中访问到正确的光照变量
#pragma multi_compile_fwdadd
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "AutoLight.cginc"
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);
//_WorldSpaceLightPos0.xyz表示的是世界空间下的光源位置
//使用USING_DIRECTIONAL_LIGHT 判断是不是平行光,如果是平行光直接调用函数就可以了,
//如果是点光源或聚光灯就要用_WorldSpaceLightPos0.xyz减去世界空间下的顶点位置
#ifdef USING_DIRECTIONAL_LIGHT
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
#else
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
#endif
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
#if defined (POINT)
//点光源计算衰减
float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
#elif defined (SPOT)
//聚光灯计算衰减
float4 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1));
fixed atten = (lightCoord.z > 0) * tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
#else
fixed atten = 1.0;
#endif
#endif
return fixed4((diffuse + specular) * atten, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
最终得到的效果如下,代码里面由关于衰减的计算,在9.3会具体讲到。
2、实验:Base Pass和Additional Pass的调用
还是使用上面这个物体。
我们可以在上面这张图的位置设置Pixel Light Count的数值,这代表了默认情况下一个物体可以接收除了最亮平行光外的四个逐像素光照。因此如果我们创建四个点光源的话,默认情况下光源的Render Mode是Auto,这时候这四个点光源都会在Additional Pass按逐像素方式处理。
如果我们把点光源设置为Not Important(如下图),这样点光源就不hi按照逐像素的方式处理,但我们在代码里还没有计算逐顶点和SH光源,所有这个光源就不会对物体造成任何影响。