前向渲染和延迟渲染
前向渲染和延迟渲染总的来说是我们的两种主要的渲染方式。
我们在Unity的Project Settings中的Graphic界面能够找到渲染队列的设定:
我们也可以在Main Camera这里进行设置:
那这里我们首先介绍一下两种渲染(Forward Renderring\Deferred Renderring)的基本概念:
渲染就像去餐馆吃饭,每一桌就是一个需要渲染的对象,我们在吃饭前要先指指点点(渲染的设置),前向渲染就是比较朴素的餐馆运行模式:针对每一桌客人,我们都执行:客人下单,上菜,执行完一桌之后我们才去咨询下一桌,这样的话问题:作为餐馆的服务人员,我们来回跑了太多次(调用GPU进行渲染的指令),且这样计算的总开销时间较长(下一桌客人一定要等到上一桌客人的菜上完才能点菜);延迟渲染就是我们先总的收集所有客人的点餐情况,然后根据总的要求进行上菜,这样最大的改善就是我们大大减少了咨询客人点菜情况的时间,且减小了整体的计算时长。
两种渲染方式在多光源场景下的性能差异尤为明显:前向渲染的过程中我们要分别每个渲染对象单独地计算完光照效果之后再计算下一个对象,但是对于延迟渲染来说,整个场景的所有物体只用计算一次光照,本身光照计算就几乎是开销最大的部分,这样可以相当大一部分节省性能。
但是显然延迟渲染并不是十全十美的,你能统计全餐馆的下单情况的前提是你得有足够大的一个记事本,在计算机里这个记事本叫做:G-BUFFER,我们会把各个对象的诸多信息放入这个缓冲区,然后后续再在缓冲区中统一计算光照。因此,从这个角度来说,我们可以认为延迟渲染是前向渲染的以空间换时间的一种方式。
我们现在展开来说:
通俗地说,针对场景里的多光源,前向渲染会给光源分为三个优先级:最亮的一档就会采取逐像素渲染,而最不重要的一档我们会直接使用球谐函数来生成一个结果避免复杂运算,中间的一档(逐顶点渲染)则不会超过四个。其中每个光源并不是一定处以某个优先级,而是一个两种优先级的混合叠加态。
上述说的光源的优先级和限制的逐像素渲染光源数都是可以修改的:
这是前向渲染处理多光源的方法,那么对于延迟渲染来说呢?
延迟渲染是不支持抗锯齿的,这是一个非常致命的问题,同时还不支持半透明:
因此,根据不同的场景来选择不同的渲染策略才是最重要的。
阴影实现
还是那句话,现在并没有完美的生成阴影的算法,大多数都是有些缺陷的。
我们直接上代码来介绍吧:
Shader "Chapter3/chapter3_3_shadow"
{
Properties
{
// 定义主颜色属性,可在材质面板中调整
_MainColor ("Main Color", Color) = (1, 1, 1, 1)
}
SubShader
{
// -------- 基础Pass:处理主要光源的投影 --------
Pass
{
// 标签定义,用于指定此Pass的光照模式为"ForwardBase"
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
// 顶点着色器和片段着色器入口
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase // 多重编译,支持不同光照模型
#include "UnityCG.cginc" // 包含Unity常用工具函数和宏
#include "Lighting.cginc" // 包含Unity光照计算函数
#include "AutoLight.cginc" // 包含Unity自动化光照相关代码
// 定义顶点到片段的数据结构
struct v2f
{
float4 pos : SV_POSITION; // 裁剪空间的顶点位置
float3 normal : TEXCOORD0; // 法线,用于光照计算
float4 vertex : TEXCOORD1; // 模型空间的顶点位置
SHADOW_COORDS(2) // 使用Unity预定义宏存储阴影坐标
};
fixed4 _MainColor; // 主颜色变量
// 顶点着色器
v2f vert (appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex); // 转换顶点到裁剪空间
o.normal = v.normal; // 传递法线信息
o.vertex = v.vertex; // 传递顶点位置
TRANSFER_SHADOW(o) // 计算阴影坐标并存储
return o;
}
// 片段着色器
fixed4 frag (v2f i) : SV_Target
{
// 计算法线方向
float3 n = UnityObjectToWorldNormal(i.normal);
n = normalize(n);
// 计算世界空间中的光源方向
float3 l = WorldSpaceLightDir(i.vertex);
l = normalize(l);
// 将顶点从模型空间转换到世界空间
float4 worldPos = mul(unity_ObjectToWorld, i.vertex);
// Lambert光照模型:计算法线与光线夹角的点积
fixed ndotl = saturate(dot(n, l));
fixed4 color = _LightColor0 * _MainColor * ndotl;
// 叠加4个点光源的光照
color.rgb += Shade4PointLights(
unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0, // 点光源位置
unity_LightColor[0].rgb, unity_LightColor[1].rgb,
unity_LightColor[2].rgb, unity_LightColor[3].rgb, // 点光源颜色
unity_4LightAtten0, worldPos.rgb, n // 衰减和位置
) * _MainColor;
// 叠加环境光照
color += unity_AmbientSky;
// 使用Unity宏计算阴影衰减系数
UNITY_LIGHT_ATTENUATION(shadowmask, i, worldPos.rgb)
// 将阴影系数与颜色相乘,应用阴影效果
color.rgb *= shadowmask;
return color; // 返回最终颜色
}
ENDCG
}
// -------- 额外的Pass:处理其他逐像素灯光的投影 --------
Pass
{
// 标签定义,此Pass用于附加光源,模式为"ForwardAdd"
Tags{"LightMode" = "ForwardAdd"}
// 混合模式:相加混合
Blend One One
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdadd_fullshadows // 支持多重编译,包含完整阴影
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
// 定义顶点到片段的数据结构,与基础Pass一致
struct v2f
{
float4 pos : SV_POSITION;
float3 normal : TEXCOORD0;
float4 vertex : TEXCOORD1;
SHADOW_COORDS(2)
};
fixed4 _MainColor;
// 顶点着色器
v2f vert (appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.normal = v.normal;
o.vertex = v.vertex;
TRANSFER_SHADOW(o)
return o;
}
// 片段着色器
fixed4 frag (v2f i) : SV_Target
{
// 计算法线和光照方向
float3 n = UnityObjectToWorldNormal(i.normal);
n = normalize(n);
float3 l = WorldSpaceLightDir(i.vertex);
l = normalize(l);
// 转换顶点到世界空间
float4 worldPos = mul(unity_ObjectToWorld, i.vertex);
// Lambert光照计算
fixed ndotl = saturate(dot(n, l));
fixed4 color = _LightColor0 * _MainColor * ndotl;
// 叠加点光源的光照
color.rgb += Shade4PointLights(
unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
unity_LightColor[0].rgb, unity_LightColor[1].rgb,
unity_LightColor[2].rgb, unity_LightColor[3].rgb,
unity_4LightAtten0, worldPos.rgb, n
) * _MainColor;
// 使用阴影宏计算阴影系数
UNITY_LIGHT_ATTENUATION(shadowmask, i, worldPos.rgb)
// 应用阴影到颜色
color.rgb *= shadowmask;
return color; // 返回最终颜色
}
ENDCG
}
}
// 回退着色器,定义为Diffuse
FallBack "Diffuse"
}
我想我需要首先介绍两个Tags中的LightMode:ForwardBase和ForwardAdd。
在代码中我们用:
// 混合模式:相加混合
Blend One One
的混合模式把这两种前向渲染的结果进行结合就可以得到一个完整的生成阴影的方法。
现在我们来看具体代码:
// -------- 基础Pass:处理主要光源的投影 --------
Pass
{
// 标签定义,用于指定此Pass的光照模式为"ForwardBase"
Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
// 顶点着色器和片段着色器入口
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase // 多重编译,支持不同光照模型
#include "UnityCG.cginc" // 包含Unity常用工具函数和宏
#include "Lighting.cginc" // 包含Unity光照计算函数
#include "AutoLight.cginc" // 包含Unity自动化光照相关代码
// 定义顶点到片段的数据结构
struct v2f
{
float4 pos : SV_POSITION; // 裁剪空间的顶点位置
float3 normal : TEXCOORD0; // 法线,用于光照计算
float4 vertex : TEXCOORD1; // 模型空间的顶点位置
SHADOW_COORDS(2) // 使用Unity预定义宏存储阴影坐标
};
fixed4 _MainColor; // 主颜色变量
// 顶点着色器
v2f vert (appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex); // 转换顶点到裁剪空间
o.normal = v.normal; // 传递法线信息
o.vertex = v.vertex; // 传递顶点位置
TRANSFER_SHADOW(o) // 计算阴影坐标并存储
return o;
}
// 片段着色器
fixed4 frag (v2f i) : SV_Target
{
// 计算法线方向
float3 n = UnityObjectToWorldNormal(i.normal);
n = normalize(n);
// 计算世界空间中的光源方向
float3 l = WorldSpaceLightDir(i.vertex);
l = normalize(l);
// 将顶点从模型空间转换到世界空间
float4 worldPos = mul(unity_ObjectToWorld, i.vertex);
// Lambert光照模型:计算法线与光线夹角的点积
fixed ndotl = saturate(dot(n, l));
fixed4 color = _LightColor0 * _MainColor * ndotl;
// 叠加4个点光源的光照
color.rgb += Shade4PointLights(
unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0, // 点光源位置
unity_LightColor[0].rgb, unity_LightColor[1].rgb,
unity_LightColor[2].rgb, unity_LightColor[3].rgb, // 点光源颜色
unity_4LightAtten0, worldPos.rgb, n // 衰减和位置
) * _MainColor;
// 叠加环境光照
color += unity_AmbientSky;
// 使用Unity宏计算阴影衰减系数
UNITY_LIGHT_ATTENUATION(shadowmask, i, worldPos.rgb)
// 将阴影系数与颜色相乘,应用阴影效果
color.rgb *= shadowmask;
return color; // 返回最终颜色
}
ENDCG
}
我们来说新东西:
#pragma multi_compile_fwdbase // 多重编译,支持不同光照模型
这句指令就是允许我们的前向渲染根据场景中的光照动态地调整一些涉及着色器参数的关键字选择。
ForwardBase中主要分成三个部分:v2f,vert和frag。
v2f中我们可以看到熟悉的顶点坐标(裁剪空间和模型空间),法线和一个阴影的预定义宏,其中:
float4 vertex : TEXCOORD1; // 模型空间的顶点位置
SHADOW_COORDS(2) // 使用Unity预定义宏存储阴影坐标
我们法线模型空间的顶点位置居然使用了TEXCOORD1来存储,这不是纹理坐标的语义吗?是的这确实是,但是其实也没人规定你不可以用,只要合乎语法即可(但是有一种语义不可以用,是的就是我们之前提到过的系统值语义:这种语义存储的内容是被规定好的特殊阶段的特殊数据,如果不符合则整个渲染流程报错),阴影的预定义宏则是提前为将来生成的阴影坐标分配好了存储的坐标索引(2)。
TRANSFER_SHADOW(o) // 计算阴影坐标并存储
这一步就是计算阴影坐标的方法,Unity的着色器语言为我们封装成了一个函数。
// 将顶点从模型空间转换到世界空间
float4 worldPos = mul(unity_ObjectToWorld, i.vertex);
// Lambert光照模型:计算法线与光线夹角的点积
fixed ndotl = saturate(dot(n, l));
fixed4 color = _LightColor0 * _MainColor * ndotl;
// 叠加4个点光源的光照
color.rgb += Shade4PointLights(
unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0, // 点光源位置
unity_LightColor[0].rgb, unity_LightColor[1].rgb,
unity_LightColor[2].rgb, unity_LightColor[3].rgb, // 点光源颜色
unity_4LightAtten0, worldPos.rgb, n // 衰减和位置
) * _MainColor;
// 叠加环境光照
color += unity_AmbientSky;
// 使用Unity宏计算阴影衰减系数
UNITY_LIGHT_ATTENUATION(shadowmask, i, worldPos.rgb)
// 将阴影系数与颜色相乘,应用阴影效果
color.rgb *= shadowmask;
return color; // 返回最终颜色
这里为什么我们要采取兰伯特模型而不是更精准的冯模型呢?
我们根据兰伯特模型的公式计算出基本的漫反射光照颜色值之后,再叠加四个点光源的效果,这里我们用了Unity内置的Shade4PointLights函数:
综上所述,现在我们的漫反射光照颜色值是兰伯特光照模型和点光源效果的总和,我们再添加一个unity自带的:
最后再乘以一个Unity自带的阴影衰落因子就得到了ForwardBase输出的结果。
然后是我们的ForwardAdd部分:
// -------- 额外的Pass:处理其他逐像素灯光的投影 --------
Pass
{
// 标签定义,此Pass用于附加光源,模式为"ForwardAdd"
Tags{"LightMode" = "ForwardAdd"}
// 混合模式:相加混合
Blend One One
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdadd_fullshadows // 支持多重编译,包含完整阴影
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
// 定义顶点到片段的数据结构,与基础Pass一致
struct v2f
{
float4 pos : SV_POSITION;
float3 normal : TEXCOORD0;
float4 vertex : TEXCOORD1;
SHADOW_COORDS(2)
};
fixed4 _MainColor;
// 顶点着色器
v2f vert (appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.normal = v.normal;
o.vertex = v.vertex;
TRANSFER_SHADOW(o)
return o;
}
// 片段着色器
fixed4 frag (v2f i) : SV_Target
{
// 计算法线和光照方向
float3 n = UnityObjectToWorldNormal(i.normal);
n = normalize(n);
float3 l = WorldSpaceLightDir(i.vertex);
l = normalize(l);
// 转换顶点到世界空间
float4 worldPos = mul(unity_ObjectToWorld, i.vertex);
// Lambert光照计算
fixed ndotl = saturate(dot(n, l));
fixed4 color = _LightColor0 * _MainColor * ndotl;
// 叠加点光源的光照
color.rgb += Shade4PointLights(
unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
unity_LightColor[0].rgb, unity_LightColor[1].rgb,
unity_LightColor[2].rgb, unity_LightColor[3].rgb,
unity_4LightAtten0, worldPos.rgb, n
) * _MainColor;
// 使用阴影宏计算阴影系数
UNITY_LIGHT_ATTENUATION(shadowmask, i, worldPos.rgb)
// 应用阴影到颜色
color.rgb *= shadowmask;
return color; // 返回最终颜色
}
ENDCG
}
我们首先可以看到Blend One One,这是一种混合模式:
除此之外的ForwardAdd的代码几乎与ForwardBase的部分一模一样,我们都只采用兰伯特光照模型即可(其实具体的渲染模式和内部的光照模型并没有直接挂钩,不同的渲染模式主要是负责的职能不同而光照模型则是定义光照计算的方式不同)。
大体效果如图: