渲染路径(Rendering Path)决定了光照是如何应用到Unity Shader中的,如果我们没有指定正确的渲染路径,那么一些光照变量很可能不会被正确赋值,我们计算出的效果也就很有可能是错误的
在Graphics中可以为整个项目设置渲染路径,默认为前向渲染
可以使用多个渲染路径,例如相机A使用前向渲染路径,而相机B使用延迟渲染路径。这时可以设置每个相机的渲染路径,以覆盖Project Settings中的设置
Pass {
Tags { "LightMode" ="ForwardBase" }
可以在每个Pass中使用标签来指定该Pass使用的渲染路径,LightMode支持的标签如下
标签名 | 描述 |
---|---|
Always | 不管使用哪种渲染路径,该Pass总是会被渲染,但不会计算任何光照 |
ForwardBase | 用于前向渲染。该Pass会计算环境光、最重要的平行光、逐顶点/SH光源和Lightmaps |
ForwardAdd | 用于前向渲染。该Pass会计算额外的逐像素光源,每个Pass对应一个光源 |
Deferred | 用于延迟渲染。该Pass会渲染G缓冲(G-buffer) |
ShadowCaster | 把物体的深度信息渲染到阴影映射纹理(shadowmap)或一张深度纹理中 |
Vertex、VertexLMRGBM和VertexLM | 用于遗留的顶点照明渲染 |
每进行一次完整的前向渲染,需要渲染该对象的渲染图元,并计算两个缓冲区的信息:一个是颜色缓冲区,一个是深度缓冲区。利用深度缓冲来决定一个片元是否可见,如果可见就在片元着色器中计算光照并更新颜色缓冲区中的颜色值,大致流程如下
下面的伪代码来描述大致过程:
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计算一个光源的光照结果,然后在帧缓冲中把这些光照结果混合起来得到最终的颜色值。假设场景中有N个物体,每个物体受M个光源的影响,那么要渲染整个场景一共需要N * M个Pass,光源数量对前向渲染影响很大
前向渲染路径有3种处理光照的方式:逐顶点处理、逐像素处理,球谐函数(Spherical Harmonics,SH)处理,Unity对这些光源进行一个重要度排序
前向渲染的两种Pass,必须添加 #pragma multi_compile_fwdbase 和 #pragmamulti_compile_fwdadd 这两个指令才可以在相关的Pass中得到一些正确的光照变量,例如光照衰减值等
在Additional Pass中还设置了混合模式。这是因为我们希望每个Additional Pass可以与上一次的光照结果在帧缓存中进行叠加,从而得到最终有多个光照的渲染效果
前向渲染可以使用的内置光照变量
名称 | 类型 | 描述 |
---|---|---|
_LightColor0 | float4 | 逐像素光源的颜色 |
_WorldSpaceLightPos0 | float4 | 该Pass逐像素光源的位置。如果该光源是平行光,那么w是0,其他光源类型w值为1 |
unity_WorldToLight | float4×4 | 从世界空间到光源空间的变换矩阵。可以用于采样cookie和光强衰减(attenuation)纹理 |
unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0 | float4 | 仅用于Base Pass。前4个非重要的点光源在世界空间中的位置 |
unity_4LightAtten0 | float4 | 仅用于Base Pass。存储了前4个非重要的点光源的衰减因子 |
unity_LightColor | half4[4] | 仅用于Base Pass。存储了前4个非重要的点光源的颜色 |
前向渲染可以使用的内置光照函数
函数名 | 描述 |
---|---|
float3 WorldSpaceLightDir(float4 v) | 仅可用于前向渲染。输入一个模型空间中的顶点位置,返回世界空间中从该点到光源的光照方向。内部实现使用了UnityWorldSpaceLightDir函数。没有归一化 |
float3 UnityWorldSpaceLightDir (float4 v) | 仅可用于前向渲染。输入一个世界空间中的顶点位置,返回世界空间中从该点到光源的光照方向。没有归一化 |
float3 ObjSpaceLightDir (float4 v) | 仅可用于前向渲染。输入一个模型空间中的顶点位置,返回模型空间中从该点到光源的光照方向。没有归一化 |
float3 Shade4PointLights (…) | 仅可用于前向渲染。计算四个点光源的光照,它的参数是已经打包进矢量的光照数据,如unity_4LightPosX0,unity_4LightPosY0, unity_4LightPosZ0、unity_LightColor和unity_4LightAtten0等。前向渲染通常会使用这个函数来计算逐顶点光照 |
使用前向渲染,在场景中添加两个胶囊体,1个平行光,4个点光源,前2个是Base Pass渲染平行光,后8个是Additional Pass渲染点光源,总共10个Pass
对点光源,聚光灯的衰减计算更复杂,涉及开根号、除法等计算量较大操作,因此Unity选择了使用一张纹理(_LightTexture0)作为查找表(Lookup Table,LUT),以在片元着色器中得到光源的衰减。在_LightTexture0上(0, 0)点表明了与光源位置重合的点的衰减值,而(1, 1)点表明了在光源空间中距离最远的点的衰减。为了得到某个顶点的光照衰减值,需要用unity_WorldToLight把该顶点从世界空间变换到光源空间,然后用这个顶点坐标的模的平方对_LightTexture0进行采样,得到衰减值
Shader实现
Shader "Unity Shaders Book/Chapter 9/Forward Rendering"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_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" }
Pass
{
// Base Pass只处理平行光,如果场景中有多个平行光,Unity会选择最亮的平行光传递给
// Base Pass进行逐像素处理,其他平行光按照逐顶点或在Additional Pass中按逐像素处理
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
// 这个指令保证我们在Shader中使用光照衰减等光照变量可以被正确赋值
#pragma multi_compile_fwdbase
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
};
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
// 使用Blinn-Phong光照模型
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
// 环境光只在Base Pass中计算一次
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 漫反射,使用_LightColor0来得到平行光的颜色和强度
fixed3 diffuse = tex2D(_MainTex, i.uv).rgb * _LightColor0.rgb * _Diffuse.rgb
* max(0, dot(worldNormal, worldLightDir));
// 高光
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);
// 衰减,平行光认为是没有衰减的,这里不写也行
float atten = 1.0;
return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}
ENDCG
}
Pass
{
// 去掉Base Pass中环境光、自发光、逐顶点光照、SH光照的部分,并添加一些对不同光源类型的支持
Tags { "LightMode"="ForwardAdd" }
// 设置混合模式,将光照结果在帧缓存中与之前的光照结果进行叠加。
Blend One One
CGPROGRAM
// 这个指令可以保证我们在Additional Pass中访问到正确的光照变量。
#pragma multi_compile_fwdadd
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#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);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
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)
// 通过unity_WorldToLight变换把顶点从世界空间转到灯光空间
float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;
// .rr操作相当于构建了一个二维采样坐标,UNITY_ATTEN_CHANNEL来得到衰减纹理中衰减值所在的分量
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"
}
除了前向渲染中使用的颜色缓冲和深度缓冲外,延迟渲染还会利用额外的缓冲区,这些缓冲区被统称为G缓冲(G-buffer),G是Geometry的缩写
延迟渲染包含了两个Pass。在第一个Pass中,不进行任何光照计算,仅仅计算哪些片元是可见的,这是通过深度缓冲技术来实现,当发现一个片元是可见的,会把物体的漫反射颜色、高光反射颜色、平滑度、法线、自发光和深度等信息渲染到屏幕空间的G缓冲区中。在第二个Pass中,利用G缓冲区的数据进行真正的光照计算,再存储到帧缓冲中
下面列出了 G 缓冲区中渲染目标 (RT0 - RT4) 的默认布局(RT,Render Texture)
RT0:格式是ARGB32,RGB通道用于存储漫反射颜色,A通道遮罩
RT1:格式是ARGB32,RGB通道用于存储高光反射颜色,A通道用于存储高光反射的指数部分
RT2:格式是ARGB2101010,RGB通道用于存储法线,A通道没有被使用
RT3:格式是ARGB32(非HDR)或ARGBHalf(HDR),用于存储自发光+lightmap+反射探针(reflection probes)
深度缓冲
模板缓冲
下面的伪代码描述大致过程
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);
}
}
}
}
Pass 2
{
// 利用G缓冲中的信息进行真正的光照计算
for (each pixel in the screen)
{
if (the pixel is valid)
{
// 如果该像素是有效的
// 读取它对应的G缓冲中的信息
readGBuffer(pixel, materialInfo, pos, normal);
// 根据读取到的信息进行光照计算
float4 color = Shading(materialInfo, pos, normal, lightDir, viewDir);
// 更新帧缓冲
writeFrameBuffer(pixel, color);
}
}
}
可以看出延迟渲染使用的Pass数目通常就两个,跟光源数目没有关系。即延迟渲染的效率不依赖于场景的复杂度,而是和屏幕空间的大小有关。因为我们需要的信息都存储在缓冲区中,而这些缓冲区可以理解成是一张张2D图像,我们的计算实际上就是在这些图像空间中进行的,光照计算相当于一个2D后处理
延迟渲染的一些缺点:
使用延迟渲染,场景内容和上面一样,在GBuffer部分2个Pass,相关信息存储到RT上,Light部分4个点光源和1个平行光
fixed4 frag(v2f i) : SV_Target
前向渲染中 SV_Target 就是颜色缓冲(ColorRT),延迟渲染需要多个RT,所以要定义一个新的输出数据结构 deferredOutput
下面Shader是延迟渲染的第一个Pass,存储GBuffer,第二个Pass计算光照,默认会使用Unity内置的Standard光照模型
Shader "MyCustom/DeferredGBuffer"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(1, 256)) = 20
}
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
Tags { "LightMode"="Deferred" }
CGPROGRAM
#pragma target 3.0
#pragma vertex vert
#pragma fragment frag
#pragma exclude_renderers nomrt
#pragma multi_compile __ UNITY_HDR_ON
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
};
// 延迟渲染输出的数据结构,需要4个Target
struct deferredOutput
{
// rgb存储漫反射颜色,a存储遮罩
float4 gBuffer0 : SV_Target0;
// rgb存储高光反射颜色,a存储gloss
float4 gBuffer1 : SV_Target1;
// rgb存储时世界空间法线,a没有使用
float4 gBuffer2 : SV_Target2;
// 自发光 + lightmap + reflection probes
float4 gBuffer3 : SV_Target3;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _Diffuse;
float4 _Specular;
float _Gloss;
v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
deferredOutput frag(v2f i)
{
deferredOutput o;
float3 color = tex2D(_MainTex, i.uv).rgb * _Diffuse.rgb;
o.gBuffer0.rgb = color;
o.gBuffer0.a = 1;
o.gBuffer1.rgb = _Specular.rgb;
o.gBuffer1.a = _Gloss / 256.0;
// 法线的取值范围 [-1, 1],转换到纹理的范围 [0, 1]
o.gBuffer2.rgb = i.worldNormal * 0.5 + 0.5;
o.gBuffer2.a = 1;
o.gBuffer3.rgb = 0;
o.gBuffer3.a = 1;
return o;
}
ENDCG
}
}
}
光照部分Shader
Shader "MyCustom/DeferredLighting"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Diffuse ("Diffuse", Color) = (1, 1, 1, 1)
_Specular ("Specular", Color) = (1, 1, 1, 1)
_Gloss ("Gloss", Range(1, 256)) = 20
}
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
Tags { "LightMode"="Deferred" }
// 第一个Pass经过了深度测试,光照部分就不需要了
ZWrite Off
// 把光照渲染到已经存在的GBuffer上
Blend One One
CGPROGRAM
#pragma target 3.0
// vert_deferred 是 UnityDeferredLibrary 中定义的顶点着色器
#pragma vertex vert_deferred
#pragma fragment frag
// 需要所有的灯光变体
#pragma multi_compile_lightpass
// 区分是否开启HDR
#pragma multi_compile __ UNITY_HDR_ON
#include "Lighting.cginc"
#include "UnityDeferredLibrary.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
sampler2D _CameraGBufferTexture0; // 漫反射颜色和遮罩
sampler2D _CameraGBufferTexture1; // 高光和平滑度
sampler2D _CameraGBufferTexture2; // 法线
sampler2D _CameraGBufferTexture3; // 自发光,lightmap,反射探针
fixed4 frag(unity_v2f_deferred i) : SV_Target
{
// 定义光照属性
float3 worldPos;
float2 uv;
half3 lightDir;
float atten;
// 衰减距离
float fadeDist;
// UnityDeferredLibrary中定义的方法,计算并返回上面定义的属性
UnityDeferredCalculateLightParams(i, worldPos, uv, lightDir, atten, fadeDist);
float3 lightColor = _LightColor.rgb * atten;
//采样GBuffer
float4 diffuse = tex2D(_CameraGBufferTexture0, uv);
float4 specular = tex2D(_CameraGBufferTexture1, uv);
// 转回 [-1, 1]
float4 worldNormal = normalize(tex2D(_CameraGBufferTexture2, uv) * 2 - 1);
// 漫反射
float3 diff = lightColor * diffuse.rgb * max(0, dot(worldNormal, lightDir));
// 高光
lightDir = normalize(lightDir);
float3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
float3 halfDir = normalize(lightDir + viewDir);
float hDotN = max(0, dot(halfDir, worldNormal));
float3 spec = lightColor * specular.rgb * pow(hDotN, specular.a * 50);
float4 color = 1;
color.rgb = diff + spec;
#ifdef UNITY_HDR_ON
return color;
#else
return exp2(-color);
#endif
}
ENDCG
}
//转码pass,主要是对于LDR转码
Pass
{
//使用深度测试,关闭剔除
ZTest Always
Cull Off
ZWrite Off
//修改模板缓冲区,避免破坏天空盒颜色
Stencil
{
//_StencilNonBackground是unity提供的天空盒遮蔽模板
ref[_StencilNonBackground]
readMask[_StencilNonBackground]
compback equal
compfront equal
}
CGPROGRAM
#pragma target 3.0
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _LightBuffer;
struct appdata
{
float4 pos : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.pos);
o.uv = v.uv;
return o;
}
fixed4 frag(v2f i): SV_Target
{
return -log2(tex2D(_LightBuffer, i.uv));
}
ENDCG
}
}
}
《Unity Shader入门精要》
《Unity ShaderLab新手宝典》
延迟着色渲染路径
Unity里的延迟渲染
【技术美术百人计划】图形 3.4 延迟渲染管线介绍