Chapter9 更复杂的光照
- 一、Unity的渲染路径
- 1.渲染路径的概念
- 2.渲染路径的类型
- ①前向渲染路径
- a. 前向渲染路径的原理
- b. Unity中的前向渲染
- c. 两种Pass
- ②延迟渲染路径
- a. 延迟渲染路径的原理
- b. Unity中的延迟渲染
- c. 两种Pass
- ③顶点照明渲染路径
- 二、Unity的光源类型
- 1.光源类型
- ①平行光
- ②点光源
- ③聚光灯
- 2.前向渲染中处理不同的光源
- ①实践
- Base Pass
- Additional Pass
- 三、Unity光照衰减
- 1.用于光照衰减的纹理
- 2.数学公式计算
- 四、Unity的阴影
- 1.阴影如何实现
- ①Shadow Map的生成
- ②屏幕空间的阴影映射技术
- ③阴影映射
- 4.Unity Shader 使用内置宏和函数 来统一管理光照衰减和阴影
- ①光照衰减和阴影的影响
- ②内置宏 UNITY_LIGHT_ATTENUATION
- 5.透明物体阴影的办法
- ①透明物体阴影问题
- ②透明度测试 物体的阴影处理
- ③透明度混合 物体的阴影处理
- 五、标准的Unity Shader
一、Unity的渲染路径
1.渲染路径的概念
渲染路径是 Unity 处理光照信息的方式,它决定了光照是如何被应用到 Unity Shader 中的。简单来说,渲染路径就像一个“沟通桥梁”,它告诉 Unity 底层渲染引擎,开发者想要以哪种方式来处理光照,以及需要哪些光照信息。
2.渲染路径的类型
大多数情况下一个项目只使用一个渲染路径,在Player Setting中进行Rendering Path设置。也可以在每个摄像机中设置该摄像机的渲染路径。完成设置后,就可以在每个Pass中 使用 LightMode 标签 来指定Pass使用的渲染路径。
①前向渲染路径
a. 前向渲染路径的原理
- 它将每个光源的光照计算独立进行,并逐个应用到物体上
- 每进行一次完整的前向渲染,需要渲染该对象的渲染图元,计算两个缓冲区信息:颜色缓冲区(更新颜色缓存区中的颜色值)和 深度缓冲区(决定一个片元是否可见)
- 对每个逐像素光源都要进行一次这样的Pass渲染,如果有多个逐像素光源,就要进行多次。
b. Unity中的前向渲染
- 渲染一个物体时,Unity会计算哪些光源照亮了它,以及这些光源照亮该物体的方式
- 前向渲染中有三种照亮物体的方式:逐顶点处理、逐像素处理、球谐函数(SH)处理
- 决定光源用哪种处理模式取决于 类型 和 渲染模式
- 类型:指光源是平行光还是什么
- 渲染模式:指该光源是否重要,重要就是逐像素
c. 两种Pass
- 前向渲染路径通常包含两个 Pass
- Base Pass: 计算环境光、最重要的平行光(1个)、逐顶点/SH 光源和 Lightmaps
- 只会执行一次
- Additional Pass: 计算额外的逐像素光源,每个光源对应一个 Pass(不支持阴影)
- 每个逐像素光源会被调用一次
- 还开启和设置了渲染模式,每个Additional Pass可以与上一次的光照结果在帧缓存中进行叠加
②延迟渲染路径
a. 延迟渲染路径的原理
- 除了前向渲染用到的颜色缓冲和深度缓冲,延迟渲染还会用到G缓冲(G-buffer),存储了我们所关心的表面的其他信息(法线、位置、用于光照计算的材质属性)
b. Unity中的延迟渲染
- 若光源数目多,前向渲染会造成性能瓶颈,就适合延迟渲染;延迟渲染中每个光源都可以按逐像素的方式处理
- 缺点:
- 不支持真正的抗锯齿(anti-aliasing)
- 不能处理半透明物体
- 对显卡有要求
c. 两种Pass
- 第一个Pass:不进行任何光照计算,仅仅计算哪些片元是可见的(深度缓冲);如果该片元可见,就把相关信息存储到G缓冲区中
- 漫反射颜色、高光反射颜色、平滑度、法线、自发光和深度信息
- 第二个Pass:利用G缓冲区中的信息进行真正的光照计算
- 不依赖与场景复杂度,而是与屏幕空间的大小有关(缓冲区可以理解为2D图像,空间即为图像空间)
③顶点照明渲染路径
- 一种简单的渲染方式,它只使用逐顶点光照,不支持阴影、法线映射等高级光照效果
二、Unity的光源类型
- Unity中有4中类型:平行光、点光源、聚光灯和面光源(面光源仅在烘焙时用到)
1.光源类型
①平行光
没有具体位置,也没有衰减,所有点的方向都是一样的
②点光源
- 照亮的范围可以由面板中Range来调,有光照衰减(衰减可以由函数定义)
- 需要用 点光源的位置 - 某点位置 来得到该点的方向
③聚光灯
- 是一块锥形区域,半径由Range来调,角度由Spot Angle来调,有光照衰减(衰减可以由函数定义)
- 需要用 聚光灯的位置 - 某点位置 来得到该点的方向
2.前向渲染中处理不同的光源
- 如何在Shader中访问5个属性:位置、方向、颜色、强度、衰减
①实践
// Upgrade NOTE: replaced '_LightMatrix0' with 'unity_WorldToLight'
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unity Shaders Book/Chapter 9/Forward Rendering" {
Properties {
_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 {
// Pass for ambient light & first pixel light (directional light)
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
// Apparently need to add this declaration
#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 = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
fixed3 halfDir = normalize(worldLightDir + viewDir);
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 {
// Pass for other pixel lights
Tags { "LightMode"="ForwardAdd" }
Blend One One
CGPROGRAM
// Apparently need to add this declaration
#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 = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
fixed4 frag(v2f i) : SV_Target {
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 * max(0, dot(worldNormal, worldLightDir));
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
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)
float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;
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"
}
Base Pass
- #pragma multi_compile_fwdbase 保证我们在shader中使用 光照衰减 等光照变量时可以被正确使用
#pragma multi_compile_fwdbase
- 在Base Pass中计算环境光照后,在Additional Pass中就不会再计算(物体自发光也是)
Additional Pass
- 同样使用#pragma multi_compile_fwdadd 指令
- 开启了Blend 命令,设置了混合模式,希望Additional Pass计算得到的光照结果可以在帧缓存中与之前的光照结果进行叠加
Pass {
// Pass for other pixel lights
Tags { "LightMode"="ForwardAdd" }
Blend One One
CGPROGRAM
// Apparently need to add this declaration
#pragma multi_compile_fwdadd
- Additional Pass处理的光源类型可能是平行光、点光源或是聚光灯,在计算位置、方向、颜色、强度、衰减时,颜色和强度仍然可以使用 _LightColor0 来得到,由于位置、方向和衰减属性就需要根据光源类型分别计算
- 方向
如果是平行光,可以直接通过 _WorldSpaceLightPos0.xyz 来得到;如果是其他光源,_WorldSpaceLightPos0.xyz 表示的是光源在世界坐标中的位置,光源方向需要用这个位置减去世界空间下顶点的位置
#ifdef USING_DIRECTIONAL_LIGHT
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
#else
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
#endif
- 衰减
#ifdef USING_DIRECTIONAL_LIGHT
fixed atten = 1.0;
#else
#if defined (POINT)
float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;
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
三、Unity光照衰减
1.用于光照衰减的纹理
- 在Unity内部使用一张名为 _LightTexture0 的纹理来计算光源衰减(通常只关心对角线上的纹理颜色值),比如(0,0)点表示与光源重合的点的衰减值,(1,1)点表示了在光源中距离最远的点的衰减
- 为了对_LightTexture0纹理采样得到给定点到该光源的衰减值,先知道点在光源空间中的位置 ,_LightMatrix0 为把顶点从世界坐标变换到光源空间的矩阵,与世界空间中的顶点坐标相乘即可
- 再使用坐标的模的平方对衰减纹理进行采样
float3 lightCoord = mul(_LightMatrix0, float4(i.worldPos, 1)).xyz;
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
2.数学公式计算
float3 distance = length(_WorldSpaceLightPos0.xyz - i.worldPosition.xyz);
fixed atten = 1.0 / distance;
四、Unity的阴影
1.阴影如何实现
- 让一个物体向其他物体投影
- 在实时渲染中,经常使用Shadow Map,先把摄像机放在光源位置上,光源的阴影区域就是摄像机看不到的区域
①Shadow Map的生成
- 摄像机位置:将摄像机放置在与光源重合的位置
- ShadowCaster Pass:使用 LightMode 标签为 ShadowCaster 的 Pass 专门更新光源的阴影映射纹理。这个 Pass 渲染的目标不是帧缓存,而是阴影映射纹理(或深度纹理)
②屏幕空间的阴影映射技术
- 根据光源的阴影映射纹理和摄像机的深度纹理得到屏幕空间的阴影图
- 步骤
- 通过调用 LightMode 标签为 ShadowCaster 的 Pass 来得到 光源的阴影映射纹理 和 相机的深度纹理
- 根据 光源的阴影映射纹理 和 相机的深度纹理 得到屏幕空间的阴影图
- 若摄像机深度图中记录的表面深度 > 转换到阴影映射纹理中的深度值 → \rightarrow → 物体表面虽然可见,但出于该光源的阴影中
③阴影映射
- 让物体 接收 来自其他物体的投影
- 在Shader中队阴影映射纹理(包括屏幕空间的阴影图)进行采样,把采样结果与光照结果相乘得到阴影
- 让物体向其他 投射阴影
- 把该物体加到光源的阴影映射纹理计算中(通过为该物体执行 LightMode 标签为 ShadowCaster 的 Pass 来实现的)
4.Unity Shader 使用内置宏和函数 来统一管理光照衰减和阴影
①光照衰减和阴影的影响
- 两个对物体最终的渲染效果本质上是相同的,都是通过将衰减因子和阴影值与光照结果相乘得到最终结果
- 可以使用一个方法来同时计算这两个信息
②内置宏 UNITY_LIGHT_ATTENUATION
- 包含进需要的头文件 #include “AutoLight.cginc”
- 在v2f结构体中使用内置宏 SHADOW_COORDS 声明阴影坐标
struct v2f{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
SHADOW_COORDS(2);
};
- 在顶点着色器中使用内置宏 TRANSFER_SHADOW 计算并向片元着色器传递阴影坐标
v2f vert(a2v v){
v2f o;
...
TRANSFER_SHADOW(o);
return o;
}
- 在片元着色器中使用内置宏 UNITY_LIGHT_ATTENUATION 来计算光照衰减和阴影,有三个参数
- 第一个:用于存储光照衰减和阴影值相乘后的结果
- 第二个:结构体 v2f ,用于传递内置宏计算阴影值
- 第三个:世界空间的坐标,计算光源空间下的坐标,再对光照衰减纹理采样来得到光照衰减
fixed4 frag(v2f i) : SV_Target {
...
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}
- Base Pass 和 Additional Pass 代码得到统一,不需要在Base Pass里单独处理阴影,也不需要在Additional Pass中判断光源类型来处理光照衰减
5.透明物体阴影的办法
①透明物体阴影问题
- 透明物体的实现通常会使用透明度测试或透明度混合,需要小心设置这些物体的 Fallback
- 使用 VertexLit、Diffuse、Specular 等作为回调,往往无法得到正确的阴影(不透明物体可以,VertexLit中有ShadowCaster Pass),因为这些 Shader 中的 ShadowCaster Pass 没有进行透明度测试的计算。
②透明度测试 物体的阴影处理
- 需要提供一个有透明度测试功能的 ShadowCaster Pass
- Fallback “VertexLit” 改为 “Transparent/Cutout/VertexLit”
- Cude 的 Mesh Renderer 的Cast Shadows 设置为 Two Sided
③透明度混合 物体的阴影处理
- 因为透明度混合需要关闭深度写入,这会带来阴影生成的问题,所以所有内置的透明度混合的 Unity Shader 都没有包含阴影投射的 Pass,因此,这些半透明物体不会向其他物体投射阴影,也不会接收来自其他物体的阴影
- 可以使用一些 dirty trick 来强制为半透明物体生成阴影
- 把它们的 Fallback 设置为 VertexLit、Diffuse 等不透明物体使用的 Unity Shader
- 通过物体的 Mesh Renderer 组件上的 Cast Shadows 和 Receive Shadows 选项来控制是否需要向其他物体投射或接收阴影