1、Unity的渲染路径
主要有3种:前向渲染路径、延迟渲染路径、顶点照明渲染路径
摄像机单独设置,若选择User Player Settings,则摄像机会使用Project Settings的设置;否则覆盖掉Project Settings的设置
,若当前显卡并不支持所选择的路径,则Unity会使用更低一级的渲染路径。
标签名 | 描述 |
Always | 不管使用哪种渲染路径,该Pass总是会被渲染,而不计算任何光照 |
ForwardBase | 用于前向渲染,该Pass会计算环境光、最重要的平行光、逐顶点/SH光源和Lightmaps |
ForwardAdd | 用于前向渲染,该Pass会计算额外的逐项素光源,每个Pass对应一个光源 |
Deferred | 用于延迟渲染,该Pass会渲染G缓冲(G-buffer) |
ShadowCaster | 把物体的深度信息渲染到阴影映射纹理(shadowmap)或一张深度纹理中 |
PrepassBase | 用于遗留的延迟渲染,该Pass会渲染法线和高光反射的指数部分 |
PrepassFinal | 用于遗留的延迟渲染,该Pass通过合并纹理、光照和自发光来渲染得到的最后的颜色 |
Vertex、VertexLMRGBM和VertexLM | 用于遗留的顶点照明渲染 |
1.1 前向渲染路径
前向渲染的原理
每进行一次完整的前向渲染,我们需要渲染该对象的渲染图元并计算两个缓冲区的信息:颜色缓冲区和深度缓冲区
伪代码:
Pass{
for(each primitive in this model)
for(each fragment covered by this primitive){
if(failed in depth test){
discard;
}else{
fixed4 color = Shading(materialInfo, pos, normal, lightDir, viewDir);
writeFrameBuffer(fragment, color);
}
}
}
对于每个逐像素光源就需要进行上面一次完整的渲染流程。如果一个物体在多个逐像素光源的影响区域内,该物体需要执行多个Pass,每个Pass计算一个逐像素光源的光照结果,然后在帧缓冲中把这些光照结果混合起来得到最终颜色值。N个物体M个光源,则需要N*M个Pass,如果有大量逐像素光照,则计算量大,通常会限制每个物体的逐项素光源数目。
Unity中的前向渲染
3种处理光照方式:逐顶点处理、逐像素处理、球谐函数处理。决定光源使用哪种处理模式取决于其类型和渲染模式。光源类型指的是该光源是平行光还是其他类型的光源,而光源的渲染模式指的是该光源是否是重要的。在Light组件中设置这些属性。一定数目的光源按照逐像素处理,至多4个光源按照逐顶点处理,其余的按照球谐函数处理
有如下规则:
-场景中最亮的平行光总是按照像素处理
-渲染模式被设置成Not Important的光源按逐顶点/SH处理
-设置成Important的光源按照逐像素处理
-如果以上规则得到的逐像素光源数量小于Quality Setting中的逐像素光源数量,则会有更多的光源以逐像素的方式进行渲染
Base Pass和Additional Pass的区别
可实现的光照效果 | 渲染设置 | 光照计算 | |
Base Pass | 光照纹理、环境光、自然光、阴影(平行光的阴影) | Tags{"LightMode" = "ForwardBase"} #pragma multi_compile_fwdbase | 一个逐像素的平行光 所有逐顶点和SH光源 |
Additional Pass | 默认情况不支持阴影 (可通过#pragma multi_compile_fwdadd_fullshadows开启) | Tags{"LightMode" = "ForwardAdd"} Blend One One #pragma multi_compile_fwdadd | 其他影响该物体的逐像素光源 每个光源执行一次Pass |
-Base Pass中支持一些光照特性,可以访问lightmap
-Base Pass中渲染的平行光默认支持阴影(若开启了光源的阴影功能),而Additional Pass中渲染的光源默认情况下没有,但可以通过编译指令开启点光源和聚光灯阴影效果
-环境光与自发光在Base Pass中计算,若在Addtional Pass计算则会叠加多次环境光和自发光
-在Additional Pass的渲染设置中开启和设置了混合模式,我们希望每个Additional Pass可以与上一次光照结果在帧缓存中进行叠加,从而得到最终的有多个光照的渲染效果。若没有开启和设置混合模式,则渲染结果会覆盖掉之前的渲染结果,看起来只受该光源影响。通常选择Blend One One混合模式
-通常定义Base Pass以及一个Additional Pass,Base Pass仅执行一次,而Additional Pass会根据影响该物体的其他逐像素光源的数目被多次调用,即每个逐像素光源会执行一次Additional Pass
内置的光照变量和函数
前向渲染可以使用的内置光照变量
名称 | 类型 | 描述 |
_LightColor0 | float4 | 该Pass处理的逐像素光源的颜色 |
_WorldSpaceLightPos0 | float4 | _WorldSpaceLightPos0.xyz是该Pass的逐像素光源的位置,若该光源是平行光,则_WorldSpaceLightPos0.w是0,其他光源类型w值为1 |
_LightMatrix0 | float4*4 | 世界空间到光源空间的变换矩阵,可用于采样cookie和光强衰减纹理 |
unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0 | float4 | 仅用于Base Pass,前4个非重要的点光源在世界空间中的位置 |
unity_3LightAtten0 | flaot4 | 仅用于Base Pass,存储了前四个非重要的点光源的衰减因子 |
unity_LightColor | half4[4] | 仅用于Base Pass,存储了前四个非重要的点光源的颜色 |
仅用于前向渲染路径的函数:WorldSpaceLightDir,UnityWorldSpaceLightDir,ObjSpaceLightDir
仅用于前向渲染的函数:
函数名 | 描述 |
float3 WorldSpaceLightDir(float4 v) | 输入模型空间顶点位置,返回世界空间从该点到光源的光照方向。未被归一化 |
float3 UnityWorldSpaceLightDir(float4 v) | 输入世界空间顶点位置,返回世界空间从该点到光源的光照方向。未被归一化 |
float3 ObjSpaceLightDir(float4 v) | 输入模型空间顶点位置,返回模型空间从该点到光源的光照方向。未被归一化 |
float3 Shade4PointLights(....) | 计算四个点光源的光照,其参数是已经打包进矢量的光照数据,通常是上表的内置变量。前向渲染使用这个函数计算逐顶点光照 |
1.2 顶点照明渲染路径
配置要求最少、运算性能最高、效果最差
如果选择使用顶点照明渲染路径,则Unity会只填充逐顶点相关的光源变量,意味着不可以使用一些逐像素光照变量
Unity中的顶点照明渲染
计算所有光源对该物体的照明,按逐顶点处理
可访问的内置变量和函数
名称 | 类型 | 描述 |
unity_LightColor | half4[8] | 光源颜色 |
unity_LightPosition | float4[8] | xyz是视角空间中的光源位置,若是平行光,则w为0,其他光源为1 |
unity_LightAtten | half4[8] | 光源衰减因子,聚光灯则x分量为cos(spotAngle/2),y分量为1/cos(spotAngle/4),若是其他类型光源x=-1,y=1,z是衰减的平方,w是光源范围开根号的结果 |
unity_SpotDirection | float4[8] | 若光源为聚光等的话,值为视角空间的聚光灯位置,若是其他类型则(0,0,1,0) |
内置函数
函数名 | 描述 |
float3 ShaderVertexLights(float4 vertex, float3 normal) | 输入模型空间中的顶点位置和法线,计算四个逐顶点光源的光照以及环境光。内部实现实际上调用了ShaderVertexLightsFull函数 |
float3 ShaderVertexLightsFull(float4 vertex, float3 normal, int lightCount, bool spotLight) | 输入模型空间中的顶点位置和法线,计算lightCount个光源的光照以及环境光,如果spotLight为true,则光源将被当成聚光灯处理,虽然结果更精确但更耗时,否则按点光源处理 |
1.3 延迟渲染路径
除了前向渲染中使用的颜色缓冲和深度缓冲外,延迟渲染还会利用额外的缓冲区统称为G缓冲(存储了例如法线、位置、用于光照计算的材质属性等)。
延迟渲染原理
第一个Pass计算哪些片元是可见的,通过深度缓冲技术实现,当发现一个片元课件就将其相关信息存储到G缓冲区中;
第二个Pass中利用G缓冲区的各个片元信息,如表面法线、视角方向、漫反射系数等进行真正的光照计算
Pass 1{
//光照计算需要运用的信息存储到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{
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);
}
}
(1)第一个Pass用于渲染G缓冲,在这个Pass中将物体的漫反射颜色、高光反射颜色、平滑度、法线、自发光和深度等信息渲染到屏幕空间的屏幕空间的G缓冲区中。此Pass只执行一次
(2)第二个Pass用于计算真正的光照模型,此Pass使用上一个Pass中渲染的数据来计算最终的光照颜色,再存储于帧缓冲中
延迟渲染的效率不依赖于场景的复杂度,而是和我们使用的屏幕空间的大小有关,因为信息都在缓冲区中,可理解成是2D图像
Unity中的渲染延迟
适合使用在场景中光源数目很多,如果使用前向渲染会造成性能瓶颈的情况下使用。缺点:
-不支持anti-aliasing
-不能处理半透明物体
-对显卡有一定要求
默认的G缓冲区包含以下渲染纹理:
格式 | RGB通道存储 | A通道存储 | |
RT0 | ARGB32 | 漫反射颜色 | 未使用 |
RT1 | ARGB32 | 高光反射颜色 | 高光反射指数部分 |
RT2 | ARGB2101010 | 法线 | 未使用 |
RT3 | ARGB32(非HDR)/ARGBHalf(HDR) | 自发光+lightmap+反射探针 |
深度缓冲和模板缓冲
第二个Pass中计算光照时默认情况下仅可使用Unity内置的Standard光照模型,若想使用其他的就需替换掉原有的Internal-DeferredShading.shader文件
可访问的内置变量和函数
名称 | 类型 | 描述 |
_LightColor | float4 | 光源颜色 |
_LightMatrix0 | float4*4 | 从世界空间到光源空间的变换矩阵,可用于采样cookie和光强衰减纹理 |
2、Unity的光源类型
2.1 光源类型有何影响
最常使用的光源属性有光源的位置、方向、颜色、强度以及衰减5个属性
平行光
其几何属性只有方向,到每一点方向一致,除此之外平行光无具体位置以及衰减的概念
点光源
球体半径;点光源位置;点光源方向(点光源位置-某点位置),随着物体逐渐远离点光源其接收到的光照强度会逐渐减小
球心处光照强度最强,向外递减到0
聚光灯
锥形区域的半径由Range定义;张开角度由Spot Angle决定;聚光灯位置;方向属性(聚光灯位置-某点位置);衰减,需要判定是否在椎体范围内
2.2 前向渲染中处理不同的光源类型
// Upgrade NOTE: replaced '_LightMatrix0' with 'unity_WorldToLight'
// Upgrade NOTE: replaced '_LightMatrix0' with 'unity_WorldToLight'
Shader "Custom/Chapter9_ForwardRendering" {
Properties {
_Diffuse("Diffuse", Color) = (1,1,1,1)
_Gloss("Gloss", Range(8.0, 256)) = 20
_Specular("Specular", Color) = (1,1,1,1)
}
SubShader {
Pass{
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#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 = UnityObjectToWorldDir(v.vertex);
return o;
}
fixed4 frag(v2f i) : SV_Target{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldPos = normalize(i.worldPos);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;//环境光计算一次即可,自发光也计算一次即可
// fixed3 diffuse = _Diffuse.rgb * _LightColor0.rgb * max(0, dot(worldNormal, worldLightDir));
fixed3 diffuse = _Diffuse.rgb * _LightColor0.rgb * saturate(dot(worldNormal, worldLightDir));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir = normalize(viewDir + worldLightDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
fixed atten = 1.0;
return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}
ENDCG
}
Pass{
Tags{"LightMode" = "ForwardAdd"}
Blend One One
CGPROGRAM
#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 = UnityObjectToWorldDir(v.vertex);
return o;
}
fixed4 frag(v2f i) : SV_Target{
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldPos = normalize(i.worldPos);
#ifdef USING_DIRECTIONAL_LIGHT
// fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
#else
// fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos).xyz - i.worldPos.xyz);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
#endif
// fixed3 diffuse = _Diffuse.rgb * _LightColor0.rgb * max(0, dot(worldNormal, worldLightDir));
fixed3 diffuse = _Diffuse.rgb * _LightColor0.rgb * saturate(dot(worldNormal, worldLightDir));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir = normalize(viewDir + worldLightDir);
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.0)).xyz;
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
#elif defined(SPOT)
float4 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1.0));
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 "Diffuse"
}
第一个Pass:
(1)使用#pragma multi_compile_fwdbase使得在Shader中使用光照衰减等光照变量可以被正确赋值
(2)Base Pass中计算一次环境光,在Additional Pass中不必再计算(物体自发光也只计算一次即可)
(3)Base Pass中处理场景中最重要的平行光。Unity将选择最亮的平行光传递给Base Pass进行逐像素处理,其他平行光将按照逐顶点或在Additional Pass中按逐像素方式处理。若没有任何平行光则Base Pass将当成全黑光源处理,对于Base Pass其处理的逐像素光源一定是平行光。可以使用_WorldSpaceLightPos0得到这个平行光的方向,_LightColor0是颜色和强度相乘的结果,衰减值无=1.0
第二个Pass:
(1)#pragma multi_compile_fwdadd指令可以保证我们在Additional Pass中访问到正确的光照变量。还是用Blend开启设置混合模式,希望Additional Pass计算得到的光照结果可以在帧缓存终于之前的光照结果进行叠加,若没有Blend命令,Additional Pass将直接覆盖掉之前的光照结果。
(2)修改针对去掉Base Pass中环境光、自发光、逐顶点光照和SH光照的部分,并添加一些对不同光源类型的支持。
平行光的方向可由_WorldSpaceLightPos0.xyz得到,而点、聚光灯_WorldSpaceLightPos0.xyz表示世界空间下的光源位置,想要得到方向就必须用这个位置减去世界空间下的顶点位置
(3)对于不同光源类型衰减值,Unity选择了使用一张纹理作为查找表LUT,以在片元着色器中得到光源的衰减。先得到光源空间下的坐标,然后该坐标对衰减纹理进行采样得到衰减值
3、Unity的光照衰减
3.1 用于光照衰减的纹理
若对该光源使用cookie,则衰减查找纹理是_LightTextureB0。(0,0)表明与光源位置重合的点的衰减值,(1,1)表明在光源空间中所关心的距离最远的点的衰减。
_LightMatrix0是世界空间变换到光源空间的变换矩阵
float3 lightCoord = mul(_LightMatrix0, float4(i.worldPosition, 1)).xyz;
然后利用光源空间中顶点距离的平方对纹理采样,使用宏UNITY_ATTEN_CHANNEL得到衰减纹理中衰减值所在的分量
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
在此放出作者在GITHUB上对dot(lightCoord, lightCoord).rr一句的解释:https://github.com/candycat1992/Unity_Shaders_Book/issues/47
3.2 使用数学公式计算衰减
4、Unity阴影
4.1 阴影是如何实现的
shadow map技术,将摄像机放在光源处与光源重合,有阴影的就是摄像机照不到的地方。
Unity需要一个额外的Pass专门更新光源的阴影映射纹理,此Pass就是LightMode = “ShadowCaster”的Pass。开启光源阴影效果后,底层引擎首先在LightMode中查找为ShadowCaster的Pass,若没有就在Fallback指定的UnityShader中继续寻找,若还没有就无法向其他物体投射阴影(但仍可接收)。
Unity中使用了屏幕空间的阴影映射技术。需要显卡支持MRT。
表面深度>转换到阴影纹理中的深度值,即说明该表面虽然可见但出于该光源阴影中。
阴影图是屏幕空间:将该表面坐标从模型空间变换到屏幕空间中
step1. 想要接收投影,就需要在Shader中对阴影映射纹理进行采样,将采样结果和最后的光照结果相乘产生阴影效果
step2. 要物体向其他物体投射阴影,将该物体加入到光源的阴影映射纹理计算中,从而让其他物体在对阴影映射纹理采样时可以得到该物体的相关信息。该过程通过ShadowCaster实现。
4.2 不透明物体的阴影
SHADOW_COORDS/TRANSFER_SHADOW/SHADOW_ATTENUATION三剑客:
SHADOW_COORDS声明了一个名为——ShadowCoord的阴影纹理坐标变量,TRANSFER_SHADOW根据平台不同有所差异。为了使宏可以进行相关计算,必须使a2f顶点坐标变量名为vertex,v2f必须名为v,v2f顶点位置变量必须命名为pos