Unity的渲染路径
参考书籍:《Unity Shader 入门精要》
渲染路径主要用于和光源打交道,shader编写中需要为pass指定渲染路径,这样unity才会明白程序员想要使用的渲染路径,然后将光源和处理后的光照信息放到这些数据里,程序员才能在着色器中进行访问。
*大多数情况下,一个项目只使用一种渲染路径,因此我们可以为整个项目设置渲染时的渲染路径。在Unity的Edit->Project Settings->Player->Other Settings->Rendering Path中选择项目所需的渲染路径。默认情况下,该选择的是前向渲染路径。
但一个摄像机下可指定不同的渲染路径,同样在Rendering Path下指定。
利用pass标签下的LightMode来指定渲染路径,eg:
Pass{
Tag{"LightMode"="ForwardBase"}
}
上面的代码将告诉Unity,该Pass将使用前向渲染路径中的ForwardBase路径。
LightMode标签支持的渲染路径设置选项
标签名 | 描述 |
Always | 不管使用哪种渲染路径,该Pass总是会被渲染,但不会计算任何光照 |
ForwardBase | 用于前向渲染,该Pass会计算环境光,最重要的平行光,逐顶点/SH光源和Lightmaps |
ForwardAdd | 用于前向渲染。该Pass会计算额外的逐像素光源,每个Pass对应一个光源 |
Deferred | 用于延迟渲染。该Pass会渲染G缓冲(G-Buffer) |
ShadowCaster | 把物体的深度信息渲染到阴影映射纹理(shadowmap)或一张深度纹理中 |
PrepassBase | 用于遗留的延迟渲染。该Pass会渲染法线和高光反射的指数部分 |
PrepassFinal | 用于遗留的延迟渲染。该Pass通过合并纹理、光照和自发光来渲染得到最后的颜色 |
Vertex、VertexMRGBM和VertexLM | 用于遗留的顶点照明渲染 |
使用渲染路径的主要作用是为Unity内置的光照变量进行正确的赋值。
前向渲染路径
原理:每进行一次完整的前向渲染路径,我们需要渲染该对象的渲染图元,并计算两个缓冲区信息:一个是颜色缓冲区,一个是深度缓冲区,深度缓冲区决定一个片元是否可见,如果可见再更新颜色缓冲区中的颜色值。
前向渲染三种处理光照的方式:
1. 逐顶点处理
2. 逐像素处理
3. 球鞋函数处理(SH处理)
*决定一个光源使用哪种处理模式取决于它的类型和渲染模式。光源类型是指该光源是平行光还是点光源还是其他类型的光源。而光源的渲染模式指的是该光源是否是重要的。
Unity实际处理光源使用的判断规则如下:
- 场景中最亮的平行光总是按逐像素处理的
- 渲染模式被设置成Not Important的光源,会按逐顶点或者SH处理
- 渲染模式被设置成Important的光源,会按逐像素处理
- 如果根据以上规则得到的逐像素光源数量小于Quality Setting中的逐像素光源数量,会有更多的光源以逐像素的方式进行渲染。
前向渲染两种Pass浅析:(BasePass和Additional Pass)
- 在使用basePass的时候,通常需要加上#pragma multi_compile_fwdbase来保证我们在使用光照衰减等光照变量可以正确的被赋值,同理使用additional Pass的时候需要加上#pragma multi_compile_fwdadd
- BasePass计算的光照为一个逐像素的平行光以及所有逐顶点和SH光源,而Additional Pass计算的光照为其他影响该物体的逐像素光源每个光源执行一次pass
- BassPass默认支持阴影,而Additional Pass默认没有阴影效果,不过可以用#pragma multi_compile_fwdadd_fullshadows来代替#pragma multi_compile_fwdadd,为点光源和聚光灯开启阴影效果
- 在Additional Pass中一般会开启混合模式,因为每一个Additional Pass会和上一次光照效果进行叠加,从而得到最终的有多个光照的渲染效果。通常采用的混合模式为Blend One One
- 对于前向渲染来说,通常会定义一个BasePass和一个AdditionalPass,一个BasePass只会执行一次,而一个AdditionalPass没一个逐像素光源都会执行一次
延迟渲染
在前向渲染下的场景中光源数目较多造成性能瓶颈的情况下使用
原理:延迟渲染主要包含个Pass。在第一个Pass中,我们不进行任何光照计算,而是仅仅计算哪些片元是可见的,这主要是通过深度缓冲技术来实现,当发现一个片元是可见的,我们就把它的相关信息存储到G缓冲区中。然后,在第二个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缓冲中的信息
readGBuffer(pixel,materialInfo,pos,normal,light,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数目通常就是两个,这跟场景中包含的光源数目是没有关系的。所以延迟渲染的效率不依赖于场景的复杂度,而是和我们使用的屏幕空间的大小有关。这是因为,我们需要的信息都存储在缓冲区中,而这些缓冲区可以理解成一张张2D图像,我们的计算实际上就是在这些图像空间中进行的。
使用延迟渲染的注意事项:
- 对于延迟渲染路径来说,它最适合在场景中光源数目很多、如果使用前向渲染会造成性能瓶颈的情况下使用。
- 延迟渲染有一些缺陷:1. 不支持真正的抗锯齿功能。 2. 不能处理半透明物体。 3. 对显卡有一定要求,如果要使用延迟渲染的话,显卡必须支持MRT,Shader Mode 3.0以上,深度渲染纹理以及双面的模板缓冲。
光源的类型:
- 平行光
- 点光源
- 聚光灯
前向渲染的实例解析:
shader代码:
Shader "Custom/Chapter9-ForwardRendering"
{
Properties
{
//材质属性 BlinnPhong模型中需要的三个常量参数
_Diffuse("Diffuse",Color)=(1,1,1,1) //漫反射中所需的diffuse值(漫反射系数)
_Specular("Specular",Color)=(1,1,1,1) //高光反射中所需的specular值(高光反射系数)
_Gloss("Gloss",Range(8.0,256))=20 //高光反射中所需的材质光泽度
}
SubShader
{
//Base Pass
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;
float4 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=mul(v.normal,(float3x3)unity_WorldToObject);
o.worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;
return o;
}
fixed4 frag(v2f i):SV_Target{
//计算环境光
fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;
//利用兰伯特公式计算漫反射
fixed3 worldNormal=normalize(i.worldNormal);
fixed3 worldLightDir=normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffuse=_LightColor0.rgb*_Diffuse.rgb*saturate(dot(worldNormal,worldLightDir));
//计算高光反射
fixed3 viewDir=normalize(_WorldSpaceCameraPos-i.worldPos.xyz);
fixed3 halfDir=normalize(viewDir+worldLightDir);
fixed3 specular=_LightColor0.rgb*_Specular*pow(saturate(dot(worldNormal,halfDir)),_Gloss);
//计算光照衰减
fixed atten=1.0;
return fixed4(ambient+(diffuse+specular)*atten,1.0);
}
ENDCG
}
//Additional Pass
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;
float4 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=mul(v.normal,(float3x3)unity_WorldToObject);
o.worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;
return o;
}
fixed4 frag(v2f i):SV_Target{
//fixed3 ambient=UNITY_LIGHTMODEL_AMBIENT.xyz;无需计算环境光
fixed3 worldNormal=normalize(i.worldNormal);
//根据不同光源类型来计算光照方向
#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*saturate(dot(worldNormal,worldLightDir));
fixed3 viewDir=normalize(_WorldSpaceCameraPos-i.worldPos.xyz);
fixed3 halfDir=normalize(viewDir+worldLightDir);
fixed3 specular=_LightColor0.rgb*_Specular*pow(saturate(dot(worldNormal,halfDir)),_Gloss);
//根据不同光源类型来计算光照的衰减
#ifdef USING_DIRECTIONAL_LIGHT
fixed atten=1.0;
#else
float3 lightCoord=mul(unity_WorldToLight,float4(i.worldPos,1)).xyz;
fixed atten=tex2D(_LightTexture0,dot(lightCoord,lightCoord).rr).UNITY_ATTEN_CHANNEL;
#endif
return fixed4((diffuse+specular)*atten,1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}