文章目录
- 不同光源
- 阴影
- 统一光源衰减与阴影
- 透明物体的阴影
不同光源
最常使用的光源属性:
- 光源的位置
- 光源方向(更具体说就是,到某点的方向)
- 颜色
- 强度
- 衰减(更具体说就是,到某点的衰减,与该点到光源的距离有关)
这些属性和光源的几何定义息息相关。
光源类型 | 位置 | 方向 | 颜色和强度 | 衰减 |
Unity代码 |
|
|
| |
平行光 | × | Transform的Rotation属性 | 面板控制 | 1.0(无衰减) |
点光源 | Transform的Position属性 | 点光源的位置减去某点的位置 | 面板控制 | 点光源球心处的光照强度最强,球体边界处的最弱,值为0 |
聚光灯 | Transform的Position属性 | 聚光灯的位置减去某点的位置 | 面板控制 | 锥形的顶点处光照强度最强,在锥形的边界处强度为0 |
阴影
让场景中可以产生阴影:
我们首先需要让平行光可以收集阴影信息。这需要在光源的Light组件中开启阴影。
让物体投射或接收阴影:
在Unity中,我们可以选择是否让一个物体投射或接收阴影。这是通过设置Mesh Renderer组件中的Cast Shadows和Receive Shadows属性来实现的。
一个物体接收来自其他物体的阴影,以及它向其他物体投射阴影是两个过程。
- 如果我们想要一个物体向其他物体投射阴影,就必须把该物体加入到光源的阴影映射纹理的计算中,从而让其他物体在对阴影映射纹理采样时可以得到该物体的相关信息。在Unity中,这个过程是通过为该物体
执行LightMode为ShadowCaster的Pass
来实现的。如果使用了屏幕空间的投影映射技术,Unity还会使用这个Pass产生一张摄像机的深度纹理。 - 如果我们想要一个物体接收来自其他物体的阴影,就必须在Shader中
对阴影映射纹理(包括屏幕空间的阴影图)进行采样,把采样结果和最后的光照结果相乘来产生阴影效果
。
我们的shader中没有定义这样一个Pass,但我们为它的Fallback指定了一个用于回调Unity Shader,即内置的Specular。虽然Specular本身也没有包含这样一个Pass,但是由于它的Fallback调用了VertexLit,它会继续回调,并最终回调到内置的VertexLit。
我们可以不依赖Fallback,而自行在SubShader中定义自己的LightMode为ShadowCaster的Pass。这种自定义的Pass可以让我们更加灵活地控制阴影的产生。但由于这个Pass的功能通常是可以在多个Unity Shader间通用的,因此直接Fallback是一个更加方便的用法。
在默认情况下,我们在计算光源的阴影映射纹理时会剔除掉物体的背面。但对于内置的平面来说,它只有一个面,因此在本例中当计算阴影映射纹理时,由于右侧的平面在光源空间下没有任何正面(frontface),因此就不会添加到阴影映射纹理中。我们可以将Cast Shadows设置为Two Sided来允许对物体的所有面都计算阴影信息。
实战:
以下shader代码是渲染一个不透明物体在平行光、点光源下的阴影:
Shader "ShaderBook/Chapter9/ForwardRender" {
Properties {
......
}
SubShader {
Tags { "RenderType"="Opaque" }
Pass {
// 环境光 & 第一个逐像素光照 (平行光)
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma multi_compile_fwdbase
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
//计算阴影时所用的宏都是在这个文件中声明
#include "AutoLight.cginc"
......
struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
//【阴影一】声明一个用于对阴影纹理采样的坐标:_ShadowCoord
SHADOW_COORDS(2)
//需要注意的是,这个宏的参数需要是下一个可用的插值寄存器的索引值,在上面的例子中就是2
};
v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
//【阴影二】在顶点着色器中计算上一步中声明的阴影纹理坐标
//根据平台不同而有所差异:把顶点坐标从模型空间变换到光源空间后存储到_ShadowCoord中
//TRANSFER_SHADOW会使用v.vertex或a.pos来计算坐标
//a2v结构体中的顶点坐标变量名必须是vertex.
//顶点着色器的输出结构体v2f必须命名为o,且v2f中的顶点位置变量必须命名为pos
TRANSFER_SHADOW(o);
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 * 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;
//【阴影三】使用_ShadowCoord对相关的纹理进行采样,得到阴影信息
fixed shadow = SHADOW_ATTENUATION(i);
return fixed4(ambient + (diffuse + specular) * atten * shadow, 1.0);
}
ENDCG
}
Pass {
// 其他逐像素光照
Tags { "LightMode"="ForwardAdd" }
Blend One One
CGPROGRAM
#pragma multi_compile_fwdadd
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "AutoLight.cginc"
......
v2f vert(a2v v) {
......
}
fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
//【不同光源的光照方向】
#ifdef USING_DIRECTIONAL_LIGHT
//如果当前前向渲染Pass处理的光源类型是平行光,那么Unity的底层渲染引擎就会定义USING_DIRECTIONAL_LIGHT
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
#else
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
#endif
//_LightColor0:该pass处理的逐像素光源的颜色(已经是颜色和强度的乘积了)
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)
//得到光源空间下的坐标:unity_WorldToLight(从世界空间到光源空间的变换矩阵)
float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;
//使用这个坐标的模的平方对衰减纹理进行采样得到衰减值
//使用宏UNITY_ATTEN_CHANNEL来得到衰减纹理中衰减值所在的分量
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
#elif defined (SPOT)
//得到光源空间下的坐标:unity_WorldToLight(从世界空间到光源空间的变换矩阵)
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"
}
统一光源衰减与阴影
UNITY_LIGHT_ATTENUATION
是Unity内置的用于计算光照衰减和阴影的宏。Unity针对不同光源类型、是否启用cookie等不同情况声明了多个版本的UNITY_LIGHT_ATTENUATION。
它接受3个参数:
- 第一个参数 atten:光照衰减和阴影值相乘后的结果。UNITY_LIGHT_ATTENUATION会帮我们声明这个变量。
- 第二个参数 v2f:这个参数会传递给SHADOW_ATTENUATION,用来计算阴影值。
- 第三个参数 i.worldPos:世界空间的坐标,这个参数会用于计算光源空间下的坐标,再对光照衰减纹理采样来得到光照衰减。
实战:
以下代码在base pass和add pass中都计算阴影。在add pass中不用再分情况计算衰减了。
Shader "Unity Shaders Book/Chapter 9/Attenuation And Shadow Use Build-in Functions" {
Properties {
......
}
SubShader {
......
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
......
fixed4 frag(v2f i) : SV_Target {
......
// UNITY_LIGHT_ATTENUATION not only compute attenuation, but also shadow infos
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}
ENDCG
}
Pass {
// Pass for other pixel lights
Tags { "LightMode"="ForwardAdd" }
Blend One One
CGPROGRAM
#pragma multi_compile_fwdadd_fullshadows
......
struct v2f {
......
SHADOW_COORDS(2)
};
v2f vert(a2v v) {
......
TRANSFER_SHADOW(o);
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);
// UNITY_LIGHT_ATTENUATION not only compute attenuation, but also shadow infos
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
return fixed4((diffuse + specular) * atten, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}
透明物体的阴影
AlphaTest
实战:在原AlphaTest代码的基础上添加了阴影
Shader "ShaderBook/Chapter8/AlphaTest" {
Properties {
......
}
SubShader {
......
Pass {
Tags { "LightMode"="ForwardBase" }
Cull Off
CGPROGRAM
#pragma multi_compile_fwdbase
......
#include "AutoLight.cginc"
......
struct v2f {
.......
//(使用TEXCOORD0、TEXCOORD1和TEXCOORD2修饰的变量)
//由于我们已经占用了3 个插值寄存器,因此SHADOW_COORDS中传入的参数是3
//阴影纹理坐标将占用第四个插值寄存器TEXCOORD3
SHADOW_COORDS(3)
};
v2f vert(a2v v) {
......
TRANSFER_SHADOW(o);
return o;
}
fixed4 frag(v2f i) : SV_Target {
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
return fixed4(ambient + diffuse * atten, 1.0);
}
ENDCG
}
}
//Transparent/Cutout/VertexLit中计算透明度测试时,使用了名为_Cutoff的属性来进行透明度测试
//因此,这要求我们的Shader中也必须提供名为_Cutoff的属性
FallBack "Transparent/Cutout/VertexLit"
}
我们可以将正方体的Mesh Renderer组件中的Cast Shadows属性设置为Two Sided,强制Unity在计算阴影映射纹理时计算所有面的深度信息。
AlphaBlend
由于透明度混合需要关闭深度写入,由此带来的问题也影响了阴影的生成。总体来说,要想为这些半透明物体产生正确的阴影,需要在每个光源空间下仍然严格按照从后往前的顺序进行渲染,这会让阴影处理变得非常复杂,而且也会影响性能。因此,在Unity中,所有内置的半透明Shader是不会产生任何阴影效果的。
当然,我们可以使用一些dirty trick来强制为半透明物体生成阴影,这可以通过把它们的Fallback设置为VertexLit、Diffuse这些不透明物体使用的Unity Shader,这样Unity就会在它的Fallback找到一个阴影投射的Pass。然后,我们可以通过物体的Mesh Renderer组件上的Cast Shadows和Receive Shadows选项来控制是否需要向其他物体投射或接收阴影。