本文同时发布在我的个人博客上:https://dragon_boy.gitee.io
Unity渲染路径
在Unity中,渲染路径决定了光照是如何应用到UnityShader中的。
Unity中支持4种渲染路径,其中两种是旧版本使用的,常规使用的两种是前向渲染路径和延迟渲染路径,默认情况下使用的是前向渲染路径。
我们可以在Pass的标签中设置渲染路径,"LightMode"
标签支持的渲染路径设置如下:
前向渲染路径
前向渲染路径的原理
每进行一次完整的前向渲染,我们需要渲染该对象的渲染图元,并计算两个缓冲区的信息,一个是颜色缓冲区,一个是深度缓冲区。
对于每个逐像素光源,我们都进行一次完整的前向渲染。如果一个物体在多个逐像素光源的影响区域内,那么该物体就需要执行多个Pass,每个Pass计算一个逐像素光源的光照结果,然后在帧缓冲中把这些光照结果混合起来得到最终的颜色值。假设场景中有N个物体,每个物体受到N个光源的影响,那么渲染整个场景需要个Pass。渲染引擎通常会限制每个物体的逐像素光照的数目。
Unity中的前向渲染
在Unity中,前向渲染有3种处理光照的方式:逐顶点处理、逐像素处理、球谐函数处理(SH)。决定一个光源使用哪种处理模式取决于它的类型和渲染模式。光源类型指的是该光源是平行光还是其他类型的光源,而光源的渲染模式指的是该光源是否是重要的。
在前向渲染种,当我们渲染一个物体时,Unity会根据场景中各个光源的设置以及这些光源对物体的影响程度(例如,距离该物体的远近、光源强度等)对这些光源进行一个重要度排序。其中,一定数目的光源会按照逐像素的方式处理,最多有4个光源按逐顶点的方式处理,剩下的光源可以按SH方式处理。Unity判断规则如下:
- 场景中最亮的平行光总是按逐像素处理的。
- 渲染模式被设置为
Not Important
的光源,会按照逐顶点或SH处理。 - 渲染模式被设置为
Important
的光源,会按照逐像素处理。 - 如果根据以上规则得到的逐像素光源数量小于
Quality Setting
中的逐像素光源数量,会有更多的光源以逐像素的方式进行渲染(默认是4)。
前向渲染有两种Pass:Base Pass和Additional Pass,常规设置如下:
- 首先,在渲染设置中,除了设置标签外,还使用了
#pragma multi_compile_fwdbase
的编译指令。这些编译指令会保证Unity可以为相应类型的Pass生成所有需要的Shader变种,这些变种会处理不同条件下的渲染逻辑,同时也会在背后声明相关的内置变量并传递到Shader中。通常情况下,只有分别为Base Pass和Additional Pass使用这两个编译指令,我们才可以在相关的Pass中得到一些正确的光照变量。 - Base Pass中渲染的平行光默认是支持阴影的,而Additional Pass中渲染的光源在默认情况下是没有阴影的。我们可以在Addditional Pass中使用
#pragma multi_compile_fwdadd_fullshadows
指令来为点光源和聚光灯开启阴影效果。 - 环境光和自发光在Base Pass中计算。因为我们希望这两种效果只计算一次。
- 在Additional Pass的渲染设置中,我们还开启和设置了混合模式。我们希望每个Additional Pass可以与上一次的光照结果在帧缓冲中进行叠加,从而得到最终有多个光照的渲染效果。如果没有开启和设置混合模式,那么Additional Pass的渲染结果会覆盖掉之前的渲染结果。通常情况下我们选择的混合模式是
Blend One One
。 - 对与前向渲染,一个Unity Shader通常会定义一个Base Pass(也可以定义多次,比如双面渲染)以及一个Additional Pass。一个Base Pass仅会执行一次,其它每个逐像素光源会执行一次Additional Pass。
延迟渲染路径
前向渲染的问题是:当场景中包含大量实时光源时,前向渲染的性能会急速下降。
延迟渲染是一种更古老的渲染方法,但由于上述前向渲染可能造成的瓶颈问题,近几年又流行起来。除了前向渲染中使用的颜色缓冲和深度缓冲外,延迟渲染还会利用额外的缓冲区,这些缓冲区被称为G缓冲。G缓冲区存储了我们所关心的表面信息,例如该表面的法线、位置、用于光照计算的材质属性等。
延迟渲染的原理
延迟渲染主要包含两个Pass,在第一个Pass中,我们不进行任何光照计算,而是仅仅计算哪些片元是可见的,利用深度缓冲。当一个片元可见时,便将相关信息存储在G缓冲中。第二个Pass中,我们利用G缓冲中的片元信息来进行光照计算。
延迟渲染使用的Pass数量就是通常是2个,与场景中的光源数无关。
Unity中的延迟渲染
延迟渲染路径中的每个光源都可以按逐像素的方式处理,但是,延迟渲染也有一些缺点:
- 不支持MSAA。
- 不能处理半透明物体。
- 对显卡有要求,显卡必须支持MRT等。
使用延迟渲染时,Unity要求提供两个Pass。
(1)第一个Pass用于渲染G缓冲。在这个Pass中,我们会把物体的漫反射颜色、高光反射颜色、平滑度、法线、自发光和深度等信息渲染到屏幕空间的G缓冲中。对于每个物体,这个Pass仅会执行一次。
(2)第二个Pass用于计算真正的光照模型。这个Pass会使用上一个Pass中渲染的数据来计算最终的光照颜色,再存储到帧缓冲中。
Unity的光照衰减
我们在Unity中使用一张纹理作为查找表来在片元着色器中计算逐像素光照的衰减。
用于光照衰减的纹理
Unity在内部使用一张名为_LightTexture0
的纹理来计算光照衰减。我们通常只关注对角线上的纹理颜色值,这些值表明了子啊光源空间中不同位置的点的衰减值。
为了对_LightTexture0
纹理采样得到给定点到光源的衰减值,我们首先需要得到该点在光源空间中的位置,这里是通过_LightMatrix0
变换矩阵得到的。_LightMatrix0
可以将顶点从世界空间变换到光源空间。因此我们像这样获取光源空间中顶点的位置:
float3 lightCoord = mul(_LightMatrix0, float4(i.worldPosition, 1)).xyz;
然后使用坐标模的平方对衰减纹理进行采样,得到衰减值:
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
使用坐标模的平方进行采样是为了避免进行开方操作。然后使用UNITY_ATTEN_CHANNEL
来得到衰减纹理中衰减值所在的分量,以得到最终的衰减值。
使用数学公式计算衰减
查看https://www.jianshu.com/p/b88a22022e7d
Unity的阴影
阴影实现原理
在实时渲染中,尝试用阴影贴图技术(Shadew Map
)来实现阴影。简单来说就是先把摄像机的位置放到与光源重合的位置,摄像机看不到的地方就是阴影区域。
在前向渲染路径中,如果场景中最重要的平行光开启了阴影,Unity就会为该光源计算它的阴影贴图。阴影贴图本质上时深度图,记录了从光源位置观察的能看到的最近的表面的深度信息。
Unity使用一个额外的Pass来专门更新光源的阴影贴图,这个Pass就是LightMode
标签被设置为ShadowCaster
的Pass,这个Pass的渲染目标时阴影贴图。Unit首先把摄像机放置到光源的位置上,然后调用该Pass,通过对顶点变换后得到光源空间的位置,并据此来输出深度信息到阴影贴图中。当开启了光源的阴影效果后,底层渲染引擎会在当前渲染物体的Unity Shader中找到LightMode
为ShadwoCaster
的Pass,如果没有,就在Fallback
中寻找,若还是没有就不产生阴影。
在传统的阴影贴图的实现中,我们会在正常渲染的Pass中把顶点位置变换到光源空间下,以得到它在光源空间中的三维位置信息。然后我们使用xy分量对阴影贴图进行采样,得到阴影贴图中该位置的深度信息。如果该深度值小于该顶点的深度值,说明该顶点在阴影中。但对于支持MRT的显卡,Unity使用屏幕空间的阴影映射技术。
当使用屏幕空间的阴影映射技术时,Unity会通过调用LightMode
为ShadwoCaster
的Pass来得到可投射阴影的光源的阴影贴图以及摄像机的深度纹理。然后根据光源的阴影贴图和摄像机的深度纹理来得到屏幕空间的阴影图。如果摄像机的深度图中记录的表面深度大于转换到阴影贴图中的深度值,就说明该表面位于阴影中。由于阴影图在屏幕空间下,因此,我们首先需要将表面坐标从模型空间变换到屏幕空间,然后使用这个坐标对阴影图进行采样。
实现阴影和光照衰减的Blinn-Phong光照模型:
Shader代码:
Shader "Unlit/Shadow"
{
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}
_BumpMap ("Normal Map", 2D) = "bump" {}
_Specular ("Specular Color", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(8.0, 256)) = 20
}
SubShader {
Tags { "RenderType"="Opaque" "Queue"="Geometry"}
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma multi_compile_fwdbase
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
fixed4 _Specular;
float _Gloss;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float4 TtoW0 : TEXCOORD1;
float4 TtoW1 : TEXCOORD2;
float4 TtoW2 : TEXCOORD3;
SHADOW_COORDS(4)
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
TANGENT_SPACE_ROTATION;
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
TRANSFER_SHADOW(o);
return o;
}
fixed4 frag(v2f i) : SV_Target {
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));
fixed3 albedo = tex2D(_MainTex, i.uv.xy).rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, lightDir));
fixed3 halfDir = normalize(lightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss);
UNITY_LIGHT_ATTENUATION(atten, i, worldPos);
return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}
ENDCG
}
Pass {
Tags { "LightMode"="ForwardAdd" }
Blend One One
CGPROGRAM
//#pragma multi_compile_fwdadd
// Use the line below to add shadows for point and spot lights
#pragma multi_compile_fwdadd_fullshadows
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "AutoLight.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
float _BumpScale;
fixed4 _Specular;
float _Gloss;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float4 texcoord : TEXCOORD0;
};
struct v2f {
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float4 TtoW0 : TEXCOORD1;
float4 TtoW1 : TEXCOORD2;
float4 TtoW2 : TEXCOORD3;
SHADOW_COORDS(4)
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw;
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
TRANSFER_SHADOW(o);
return o;
}
fixed4 frag(v2f i) : SV_Target {
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));
fixed3 albedo = tex2D(_MainTex, i.uv.xy).rgb * _Color.rgb;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, lightDir));
fixed3 halfDir = normalize(lightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss);
UNITY_LIGHT_ATTENUATION(atten, i, worldPos);
return fixed4((diffuse + specular) * atten, 1.0);
}
ENDCG
}
}
Fallback "Specular"
}
上述代码中我们使用"AutoLight.cginc"
文件中的衰减和阴影相关代码。SHADOW_COORDS
声明一个名为_ShadowCoord
的阴影纹理坐标变量。TRANSFER_SHADOW
根据是使用传统阴影贴图技术还是屏幕阴影映射技术来判断是将顶点坐标转换到光源空间还是屏幕空间。SHADOW_ATTENUATION
用来衰减阴影。注意,要使用这些宏定义,输入顶点着色器的坐标名称需为vertex
,输入片元着色器的坐标名称需为pos
。
我们使用UNITY_LIGHT_ATTENUATION
来同时计算阴影和光照衰减。
实现效果: