一、延迟着色和前向渲染
很可惜的是没有什么前置,OpenGL 本是要写一篇延迟着色的笔记的,但是怎么看这都不属于OpenGL基础的范畴
先考虑最简单的情况:只有最多一个光源,这个时候当然就按照正常渲染流程就好了
→ 稍微复杂点:有多个光源,但是数量比较少(夜晚有路灯的街道),很容易想到在着色中 for 循环所有光源再叠加颜色的方法,确实之前 OpenGL 的 demo 就是这样做的,又或者是多个 PASS,一个 PASS 对应一个光照,并且按照混合的设置进行叠加,这些都没有太大的问题。不管如何,像这样在场景中根据所有光源照亮一个物体,之后再渲染下一个物体的方法都叫做前向渲染(Forward Rendering)
→ 终极情况:场景中有大量的光源(演出现场、舞台),上面的渲染方法在这种情况下就会束手无策,因为单从它的时间复杂度来讲就是平方级别的(,其中 n 为光源数量,m 为物体数量),这个时候延迟渲染(Deferred Rendering)就为了解决上述问题而诞生了,它大幅度地改变了我们之前渲染物体的方式
延迟渲染的过程:
- 离线!还是离线,先渲染场景一次以获取物体的各种几何信息(位置、颜色、法向量等),并储存在一系列叫做G缓冲(G-buffer)的纹理中,这个阶段也叫做几何处理阶段(Geometry Pass)
- 紧接着再第二个阶段使用G缓冲内的纹理数据:渲染一个屏幕大小的方形,并使用G缓冲中的几何数据对每一个片段计算场景的光照,这个阶段也叫做光照处理阶段(Lighting Pass)
这样总体时间复杂度就不再是平方级别,而更多取决于片段数量(,其中 n 为光源数量,p 为片段数量,一般都为分辨率大小)这一定值
二、Unity中的渲染路径
小明和小红同时来到的银行,小明在长期996的努(bo)力(xue)下,存了不少钱,因此她本次去银行的目的就是:储蓄,这样在银行柜员的指引下,小明成功办理了一张储蓄卡;而打工是不可能打工的小红则需要一些资金做自己奶茶店的开店准备,因此信誉良好的他本次去银行的目的就是:借款,这样在银行柜员的指引下,小明成功办理了一张信用卡
同样去银行,目的不同,银行就会提供不同的服务,对于存款和借款,整体的流程和操作都是不一样的,如果你没有提出你的要求,柜台的服务员也就无法直接给予你帮助
指明 Unity 渲染路径其实正是同样的道理,在 Unity 里,渲染路径(Rendering Path)决定了光照是如何运用到 UnityShader 中的,就像我们向银行提出我们的要求一样,我们也要为每个 Pass 指定它使用的渲染路径,这样 Unity 才能知道如何给予我们正确的光源和处理后的光照信息
Unity支持的渲染路径主要有三种:
- 前向渲染路径:对应第一节的前向渲染
- 延迟渲染路径:对应第一节的延迟着色
- 顶点照明渲染路径(VertexLitRenderingPath):前向渲染路径的一个子集,不支持逐像素光照,已基本废弃
关于渲染路径的设置:
Edit → ProjectSettings,或者针对不同的摄像机使用不同的渲染路径
三、LightMode标签
完成的上一节的渲染路径设置后,就可以在每个 Pass 中使用标签来指定该 Pass 的渲染路径了,这是通过设置 Pass 的 LightMode 标签实现的。不同类型的渲染路径可能会包含多种标签设置,如果该 Pass 对应的渲染路径和 Unity 设置的不同,该 Pass 就不会被渲染
LightMode 标签支持的路径渲染设置有:
- Always:无论 Unity 设置的哪种渲染路径,该 Pass 总会被渲染,不会计算任何光照
- ForwardBase:前向渲染,基本 Pass,会计算环境光、最重要的平行光、逐顶点/SH 光源和 Lightmaps
- ForwardAdd:前向渲染,额外 Pass:每个 Pass 对应一个光源,可以计算额外的逐像素光源
- Deferred:延迟渲染,渲染G缓冲
- ShadowCaster:用于把物体的深度信息渲染到 ShadowMap 中
- PrepassBase:Unity5版本之前的延迟渲染,会渲染法线和高光反射的指数部分,基本已废弃
- PrepassFinal:Unity5版本之前的延迟渲染,和 PrepassFinal 组合,通过合并纹理光照等属性来渲染得到最终的颜色,基本已废弃
- Vertex\VertexLMRGBM\VertexLM:定点照明渲染,基本已废弃
三种光照处理方式,性能从低到高,效果从好到差:
- 逐像素光照(Per-Pixel)
- 逐顶点光照(Per-Vertex)
- 球谐函数(Spherical Harmonics, SH)处理:及其高效的光照处理方式,但一般只应用漫反射
Unity 决定对一个光源使用哪种处理方式取决于光源的类型(平行光/点光源…)和它的渲染模式,在这里进行设置:
判断规则如下:
- 场景中最亮的平行光一定是逐像素处理的
- 渲染模式被设置为 Not Important 的光源会按照逐顶点或者 SH 处理
- 渲染模式被设置为 Important 的光源会按照逐像素处理
- 根据规则1-3,如果最终得到的逐像素光源数量小于 ProjectSettings → Quality 中的逐像素光源数量,那么会有更多的光源以逐像素的方式进行渲染
四、Unity中的前向渲染
完成了上一节的光照设置后,就可以来关心 Pass 本身了
前向渲染有两种 Pass:Base Pass 和 Addtional Pass,其渲染设置及常规光照计算如下:

关于里面的重点:
- 对于前向渲染来说,如果只定义了一个 Base Pass 和一个 Additional Pass,那么 Base Pass 会仅执行一次,而 Additional Pass 可能会被执行多次,即每个逐像素光照都会执行一次 Additional Pass
- 关于Bland One One,是为了将每个帧缓冲中的光照结果进行叠加,也因此,向自发光和环境光这种只需要被计算一次的光照都要放在 Bass Pass 中
- Base Pass 中渲染的平行光默认是支待阴影的,而 Additional Pass 中渲染的光源在默认情况下是没有阴影效果的(即便我们在它的 Light 组件中设置了有阴影的 Shadow Type),但我们可以在 Additional Pass 中使用 #pragma multi_compile_fwdadd_fullshadows 代替 #pragma multi_compile_fwdadd 编译指令,为点光源和聚光灯开启阴影效果,但这需要 Unity 在内部使用更多的 Shader 变种
回到最初,曾说过我们需要为每个 Pass 指定它使用的渲染路径,这样 Unity 才能知道如何给予我们正确的光源和处理后的光照信息,那么对于前向渲染 Unity 给了我们哪些“服务”呢?
名称 | 类型 | 描述 |
_LightColor0 | float4 | 该 Pass 处理的逐像素光源的颜色 |
_WorldSpaceLightPos0 | float4 | _WorldSpaceLightPos0.xyz 是该 Pass 处理的逐像素光源的位置,_WorldSpaceLightPos0.w 表示当前光照是否是平行光(是0否1) |
unity_WorldToLight |
float4×4 | 从世界空间到光源空间的变换矩阵,可以用于采样聚光灯影和光强衰减(attenuation)纹理 |
_LightTexture0 |
sampler2D | 点光源光照衰减纹理,用于获取光照的衰减值,对于聚光灯,其存储的不再是基于距离的衰减纹理,而是一张基于张角范围的衰减纹理 |
_LightTextureB0 | sampler2D | 聚光灯光照衰减纹理,用于获取光照的衰减值 |
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(...) | 计算4个点光源的光照,它的参数是已经打包进矢量的光照数据 |
五、关于光源处理方式、BassPass 和 AddPass 调用的验证
下面是一个拥有5个点光源,1个平行光的场景,场景中间有一个立方体,它的着色器算上了点光源:
着色器代码:
// Upgrade NOTE: replaced '_LightMatrix0' with 'unity_WorldToLight'
Shader "Jaihk662/PointLight1"
{
Properties
{
_DiffuseColor ("DiffuseColor", Color) = (1.0, 1.0, 1.0, 1.0)
_SpecularColor ("SpecularColor", Color) = (1.0, 1.0, 1.0, 1.0)
_MainTex ("MainTex", 2D) = "white" {}
_Gloss ("Gloss", Range(8.0, 256)) = 20
}
SubShader
{
LOD 200
//Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent"}
//如果场景中包含了多个平行光,Unity会选择最亮的那个传递给BasePass进行逐像素处理,其它平行光按照逐顶点或在AddPass中按逐像素处理(没有平行光默认全黑)
PASS
{
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma multi_compile_fwdbase
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
fixed4 _DiffuseColor;
fixed4 _SpecularColor;
sampler2D _MainTex;
float4 _MainTex_ST;
float _Gloss;
struct _2vert
{
float4 vertex: POSITION;
float3 normal: NORMAL;
float4 texcoord: TEXCOORD0;
};
struct vert2frag
{
float4 pos: SV_POSITION;
float3 wPos: TEXCOORD0;
float3 wNormal: TEXCOORD1;
float2 uv: TEXCOORD2;
};
vert2frag vert(_2vert v)
{
vert2frag o;
o.pos = UnityObjectToClipPos(v.vertex);
o.wNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
o.wPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
fixed4 frag(vert2frag i): SV_Target
{
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.wPos));
fixed3 wLightDir = normalize(UnityWorldSpaceLightDir(i.wPos));
fixed3 albedo = tex2D(_MainTex, i.uv) * _DiffuseColor.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(i.wNormal, wLightDir));
fixed3 reflectDir = normalize(reflect(-wLightDir, i.wNormal));
fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
PASS
{
Tags { "LightMode" = "ForwardAdd" }
Blend One One
CGPROGRAM
#pragma vertex vert
#pragma multi_compile_fwdadd
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc" //别忘了包含
fixed4 _DiffuseColor;
fixed4 _SpecularColor;
sampler2D _MainTex;
float4 _MainTex_ST;
float _Gloss;
struct _2vert
{
float4 vertex: POSITION;
float3 normal: NORMAL;
float4 texcoord: TEXCOORD0;
};
struct vert2frag
{
float4 pos: SV_POSITION;
float3 wPos: TEXCOORD0;
float3 wNormal: TEXCOORD1;
float2 uv: TEXCOORD2;
};
vert2frag vert(_2vert v)
{
vert2frag o;
o.pos = UnityObjectToClipPos(v.vertex);
o.wNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
o.wPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
fixed4 frag(vert2frag i): SV_Target
{
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.wPos));
#ifdef USING_DIRECTIONAL_LIGHT
fixed3 wLightDir = normalize(UnityWorldSpaceLightDir(i.wPos));
#else
fixed3 wLightDir = normalize(_WorldSpaceLightPos0.xyz - i.wPos.xyz);
#endif
fixed3 albedo = tex2D(_MainTex, i.uv) * _DiffuseColor.rgb;
fixed3 diffuse = _LightColor0.rgb * albedo * saturate(dot(i.wNormal, wLightDir));
fixed3 reflectDir = normalize(reflect(-wLightDir, i.wNormal));
fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(saturate(dot(reflectDir, viewDir)), _Gloss);
#ifdef USING_DIRECTIONAL_LIGHT
fixed atten = 1.0;
#else
float3 lightCoord = mul(unity_WorldToLight, float4(i.wPos, 1)).xyz;
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
#endif
return fixed4((diffuse + specular) * atten, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
由于仅在 AddPass 的片段着色器中计算的点光,并且没有 SH 和逐顶点的相关计算,因此这些光源都应是逐像素处理的,否则将不会表现于物体上
图片中的例子5个点光源都做了逐像素处理,这是因为它们的 RenderMode 都设置为了 Important:
也可以通过 FrameDebugger 工具查看当前的绘制过程:
如果这时将所有光源的 RenderMode 都设置为 Auto,那么就只能看到3个光源了:
而3正是上面 ProjectSettings → Quality 中最大逐像素光源数量,具体哪3个光源会被 Unity 当作 Important 逐像素处理,取决于光照的属性(位置、方向、颜色x强度、衰减)
当然了,手动设置光源 RenderMode 为 Not Important,或者物体脱离了点光源的有效照射范围,Unity 也不会为这个物体调用 Pass 来处理这个光源