【图形学】31 Unity 的光源衰减和阴影

来源:《UNITY SHADER入门精要》

1、用于光照的衰减纹理

  如我们之前所用到的一样,我们在 Unity 内部使用了一张名为 _LightTesture0 的纹理来采样获得衰减值,这样就避免了复杂的数学计算。
  为了通过 _LightTesture0 纹理采样来获得衰减值,首先,我们必须将光源从 世界空间 变换到自己的 光源空间,为此,我们需要通过 LightMatrix0 变换矩阵得到:

float3 lightCoord = mul(_LightMatrix0, float4(i.worldPosition, 1)).xyz;

  然后我们可以使用这个坐标的模的平方对衰减纹理进行采样,得到衰减值:

fixed atten = (tex2D(_LightTexture0,  dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;

  我们使用了光源空间中顶点距离的平方,通过 dot 函数来得到来对纹理进行采样。之所以没有使用距离是因为这种方法可以避免开方操作。然后,我们使用了 UNITY_ATTEN_CHANNEL 来得到衰减纹理中衰减值所在的分量,以得到最终的衰减值。

2、阴影的产生

①传统的阴影映射纹理

  在实时渲染中,我们最常使用的是一种名为Shadow Map的技术。这种技术理解起来非常简单,它会首先把摄像机的位置放在与光源重合的位置上,那么场景中该光源的阴影区域就是那些摄像机看不到的地方。而Unity就是使用的这种技术。这一过程是在光源空间完成的。
  Unity 采用一种名为 Shadow Map 的技术。它会把摄像机的位置放在与光源重合的位置上,那么场景中该光源的阴影区域就是摄像机看不到的位置。

  在前向渲染中,如果场景中的平行光开启了阴影,Unity 就会为该光源计算它的 阴影映射纹理(shadowmap)。这张阴影映射纹理本质上也是一张深度图,它记录了从该光源位置出发、能看到的场景中距离它最近的表面位置(深度信息)。
  Unity 中,我们这个 Pass 的 Tags 设置为 ShadowCaster 的Pass。这个 Pass 渲染的结果不会给到 帧缓存,而会给到 阴影映射纹理(或深度纹理,因为本来就是通过深度值信息来判断谁会被打亮,谁会形成阴影)。

②屏幕空间的阴影映射技术(Screenspace Shadow Map)

  这个技术原本是 延迟渲染 中产生阴影的方法,现在可以作用于 前向渲染。所使用的平台显卡需要支持 MRT 才能使用。
  当使用了屏幕空间的阴影映射技术时,Unity首先会通过调用 LightMode 为 ShadowCaster 的 Pass 来得到可投射阴影的光源的阴影映射纹理以及摄像机的深度纹理。然后,根据光源的阴影映射纹理和摄像机的深度纹理来得到屏幕空间的阴影图。如果摄像机的深度图中记录的表面深度大于转换到阴影映射纹理中的深度值,就说明该表面虽然是可见的,但是却处于该光源的阴影中。通过这样的方式,阴影图就包含了屏幕空间中所有有阴影的区域。如果我们想要一个物体接收来自其他物体的阴影,只需要在Shader中对阴影图进行采样。由于阴影图是屏幕空间下的,因此,我们首先需要把表面坐标从 模型空间 变换到 屏幕空间 中,然后使用这个坐标对阴影图进行采样即可。

3、Unity产生阴影

①如何开启Unity中的阴影生成

  Unity 中要产生阴影,需要开启 cast Shadows 和 Receive Shadows 两个选项。

②Shader代码中调用阴影

  在我们之前写的产生光照的代码中,我们没有写有关阴影的代码,但是它依然能产生出阴影,这是为何?因为我们在最后添加了 Fallback "Specular" 它自动回调了内置的 Specular ,而 Specular 中又回调了 VertexLit ,其中就有 阴影产生的代码了。
  虽然我们可以自己书写一个 ShadowCaster 的 Pass。但由于这个 Pass 的功能通常可以在多个 UnityShader 中调用的,所以,直接 Fallback 是个更加方便的用法。

  或者我们直接**把 Fallback 设置为 VertexLit **就能直接在Shader中产生阴影了,当然,Fallback 中的 Specular 和 Diffuse 都有调用 VertexLit 了。

③双面阴影

  由于默认的,尽管 cast Shadow 已经被开启了,但是对于一个平面,只有正面对着光源的才会产生阴影,背面不会。若要产生,则将 cast Shadow 的选项设置为 Two Sided。

4、Unity接收阴影

  在我们普通的 前向渲染光照Shader 中,做出这样的修改:
(1)包含新的内置文件:#include "AutoLight.cginc"。
(2)在顶点着色器的输出结构体 v2f 中添加内置宏 SHADOW_COORDS。声明一个对阴影纹理坐标的采样。

struct v2f{
	float4 pos : SV_POSITION;
	float4 worldNormal : TEXCOORD0;
	float3 worldPos : TEXCOORD1;
	SHADOW_COORDS(2)
};

(3)定点着色器返回之前添加另一个内置宏 TRANSFER_SHADOW

v2f vert(a2v v){
	v2f o;
	...
	TRANSFER_SHADOW(o);
	
	return o;
}

(4)片元着色器中依然使用内置宏来处理,采用内置宏 SHADOW_ATTENUTATION

fixed shadow = SHADOW_ATTENUATION(i);

(5)最后在返回值的时候,把变量 shadow 和漫反射以及高光反射的颜色相乘。

  最后注意我们只在

  我们完全可以看到,这里所有的阴影的代码都是通过内置宏来实现的,我们可以在 AutoLight.cginc 中找到它们的声明(内容非常长,而且也根据平台的不同而做了许多相关的优化)。
  需要注意的是,由于这些宏中会使用上下文变量来进行相关计算,例如 TRANSFER SHADOW 会使用 v.vertexa.pos 来计算坐标,因此为了能够让这些宏正确工作,我们需要保证自定义的变量名和这些宏中使用的变量名相匹配。我们需要保证:v2f 结构体中的顶点坐标变量名必须是 vertex ,顶点着色器的输出结构体 v2f 必须命名为 v,且 v2f 中的顶点位置变量必须命名为 pos。

5、更新 Addtional Pass 中的阴影部分

  因为上面的代码我们只更新了 Base Pass 的部分,而 Base Pass 只调用了一次。所以这次我们将新的代码添加在 Addtional Pass 中。
  前面的步骤都和 Base Pass 一样的,唯一不一样的事情是最后的片元着色器 计算衰减的那一部分:

fixed frag(v2f i) : SV_Target{
	...
	UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
	return fixed4(ambient + (diffues + specualr) * atten, 1.0);
}

  UNITY_LIGHT_ATTENUATION 是 Unity 内置的用于计算光照衰减和阴影的宏,我们可以在内置的 AutoLight.cginc 里找到它的相关声明。它接受3个参数,它会将光照衰减和阴影值相乘后的结果存储到第一个参数中。注意到,我们并没有在代码中声明第一个参数 atten,这是因为 UNITY_LIGHT_ATTENUATION 会帮我们声明这个变量。它的第二个参数是结构体 v2f,这个参数会传递给9.4.2节中使用的SHADOW ATTENUATION,用来计算阴影值。而第三个参数是世界空间的坐标,正如我们在93节中看到的一样,这个参数会用于计算光源空间下的坐标,再对光照衰减纹理采样来得到光照衰减。我们强烈建议读者查阅 AutoLight.cginc 中 UNITY_LIGHT_ATTENUATION 的声明,读者可以发现,Unity 针对不同光源类型、是否启用cookie等不同情况声明了多个版本的 UNITY_LIGHT_ATTENUATION。这些不同版本的声明是保证我们可以通过这样一个简单的代码来得到正确结果的关键。

6、帧调试器查看阴影绘制

在这里插入图片描述

7、透明物体产生阴影

①透明度测试的物体产生阴影

  之前我们产生阴影,填写 Fallback "VertexLit",调用内置的宏定义就完事了。为了让透明度测试的物体得到正确的阴影效果,我们只需要在 UntiyShader中更改一行代码,Fallback "Transparent/Cutout/VetexLit"。我们可以在内置文件中找到该Unity Shader的代码,它的ShadowCaster Pass也计算了透明度测试,因此会把裁剪后的物体深度信息写入深度图和阴影映射纹理中。但需要注意的是,由于 Transparent//Cutout/VertexLit 中计算透明度测试时,使用了名为 _Cutoff 的属性来进行透明度测试,因此,这要求我们的Shader中也必须提供名为 _Cutoff 的属性。否则,同样无法得到正确的阴影结果。
  同时,我们也也应该在 Cast Shadows 属性中设置为 Two Sided,以获取正确的面别对光源的阴影。

②透明度混合的物体产生阴影

  与透明度测试的物体相比,想要为使用透明度混合的物体添加阴影是一件比较复杂的事情。事实上,所有内置的透明度混合的 Unity Shader,如 Transparent/VertexLit 等,都没有包含阴影投射的Pass。
  这意味着,这些半透明物体不会参与深度图和阴影映射纹理的计算,也就是说,它们不会向其他物体投射阴影,同样它们也不会接收来自其他物体的阴影。
  书中提到的 dirty trick 是通过调用不透明物体的 Fallback "VertexLit" 来为混合透明的物体生成阴影。

猜你喜欢

转载自blog.csdn.net/qq_40891541/article/details/127439619
今日推荐