5.1逐灯光的阴影数据5.2包含灯光5.3渲染所有的聚光灯

5.1逐灯光的阴影数据5.2包含灯光5.3渲染所有的聚光灯

5.1 逐灯光的阴影数据

5.2 包含灯光

5.3 渲染所有的阴影贴图

5.4 使用正确的阴影数据

5.5 阴影贴图集

6 动态平铺

6.1 计数 阴影 平铺

6.2 分隔阴影贴图

6.3 平铺为1就相当于没有

6.4 着色器关键字

本文重点:

1、渲染到纹理并从纹理中读取。

2、从灯光角度渲染。

3、为阴影投射器添加着色器通道。

4、采样阴影贴图。

5、支持硬阴影和软阴影的混合。

6、在一个图集中合并多达十六个阴影贴图。

这是涵盖Unity的可脚本化渲染管线的教程系列的第四部分。在其中,我们将添加对多达16个带阴影的聚光灯的支持。

本教程是CatLikeCoding系列的一部分,原文地址见文章底部。“原创”标识意为原创翻译而非原创教程。

本教程使用Unity 2018.3.0f2制作。

投影矩阵的性质_投影矩阵怎么求_unity 投影矩阵

(带有阴影的三个聚光灯)

1 一个带有阴影的聚光灯

阴影非常重要,既可以增加真实感,又可以使对象之间的空间关系更加明显。没有阴影,很难分辨是物体漂浮在表面上还是在表面上。

教程说明了阴影如何在Unity的默认渲染管线中工作,但是对于我们的单通道正向渲染器,这种完全相同的方法不起作用。但返回阅读以获取阴影贴图的相关知识仍然很有用。在本教程中,我们将仅限于聚光灯的阴影,因为阴影是最不复杂的。

我们首先要一个支持阴影的光,因此要创建一个包含几个对象和一个聚光灯的场景。平面对象对于接收阴影很有用。所有物体都使用我们的不透明Lit材质。

投影矩阵怎么求_投影矩阵的性质_unity 投影矩阵

(单个聚光灯 还没有阴影)

1.1 阴影贴图

有不少处理阴影的方法,但是我们将继续使用阴影贴图的默认方法。这意味着我们将从灯光的角度渲染场景。我们仅对此次渲染的深度信息感兴趣,因为它告诉我们光线在撞击表面之前到达的距离。任何更远的地方都在阴影中。

要使用阴影贴图,我们必须先创建阴影贴图,然后再使用普通相机进行渲染。为了以后能够对阴影贴图进行采样,我们必须渲染为单独的渲染纹理,而不是通常的帧缓冲区。向MyPipeline添加一个RenderTexture字段以保留对阴影贴图纹理的引用。

使用上下文作为参数,创建一个单独的方法来渲染阴影。它要做的第一件事就是控制渲染纹理。我们将通过调用静态RenderTexture.GetTemporary方法来实现。要么创建新的渲染纹理,要么重新使用尚未清理的旧纹理。因为我们很可能在每帧都需要阴影贴图,所以它会一直重复使用。

向RenderTexture.GetTemporary提供地图的宽度和高度,用于深度通道的位数unity 投影矩阵,最后是纹理格式。我们将从512×512的固定大小开始。深度通道将使用16位,因此它是高精度的。在创建阴影贴图时,请使用RenderTextureFormat.Shadowmap格式。

确保将纹理的滤镜模式设置为双线性,并将其环绕模式设置为钳制。

阴影贴图将在常规场景之前渲染,因此在设置常规摄影机之前但在剔除之后在Render中调用RenderShadows。

unity 投影矩阵_投影矩阵怎么求_投影矩阵的性质

另外,请确保在提交上下文后释放渲染纹理。如果此时有阴影贴图,则将其传递到RenderTexture.ReleaseTemporary方法并清除我们的字段。

unity 投影矩阵_投影矩阵的性质_投影矩阵怎么求

1.2 阴影命令缓冲区

我们将为所有阴影工作使用单独的命令缓冲区,因此我们可以在帧调试器的单独部分中看到阴影和常规渲染。

投影矩阵的性质_投影矩阵怎么求_unity 投影矩阵

就像我们进行常规渲染一样,阴影渲染将在BeginSample和EndSample命令之间进行。

unity 投影矩阵_投影矩阵的性质_投影矩阵怎么求

1.3 设置 渲染目标

在渲染阴影之前,我们首先要告诉GPU渲染到阴影贴图。一种方便的方法是通过使用命令缓冲区和阴影贴图作为参数调用CoreUtils.SetRenderTarget。从清除贴图开始,请在BeginSample之前调用它,以便不显示帧调试器和额外的嵌套渲染阴影级别。

我们只关心深度通道,因此仅需要清除该通道。通过将ClearFlag.Depth添加为SetRenderTarget的第三个参数来表明这一点。

尽管不是必需的,但我们也可以更精确地了解纹理的负载和存储要求。我们不在乎它的来源,因为无论如何我们都可以清除它,我们可以使用RenderBufferLoadAction.DontCare指出。这使得基于图块的GPU变得更有效率。而且我们需要稍后从纹理中采样,因此需要将其保存在内存中,我们将使用RenderBufferStoreAction.Store进行指示。将它们添加为第三个和第四个参数。

现在,在常规摄影机渲染之前,阴影映射的清除动作会显示在帧调试器中。

unity 投影矩阵_投影矩阵的性质_投影矩阵怎么求

(清除阴影贴图)

1.4 配置视图和投影矩阵

我们的想法是从光源的角度进行渲染,这意味着我们将聚光灯当作照相机使用。因此,必须提供适当的视图和投影矩阵。我们可以通过使用灯光索引作为参数,在剔除结果上调用ComputeSpotShadowMatricesAndCullingPrimitives来检索这些矩阵。由于场景中只有一个聚光灯,因此我们只需提供零即可。视图和投影矩阵可通过两个输出参数使用。除此之外,还有第三个ShadowSplitData输出参数。我们不需要它,必须提供输出参数。

投影矩阵的性质_投影矩阵怎么求_unity 投影矩阵

一旦有了矩阵,就可以通过调用阴影命令缓冲区上的SetViewProjectionMatrices来设置它们,执行并清除它。

1.5 渲染阴影投射器

有了正确的矩阵,我们可以继续渲染所有的阴影对象。我们通过在上下文上调用DrawShadows来实现。该方法具有DrawShadowsSettings参考参数,我们可以通过构造函数方法创建该参数,该方法将剔除结果和光照索引作为参数。

投影矩阵怎么求_投影矩阵的性质_unity 投影矩阵

仅当我们的聚光灯的阴影类型设置为硬或软时,此方法才有效。如果将其设置为none,则Unity将报错,说它不是有效的阴影投射灯。

(灯光开启阴影)

2 阴影投射器通道

此时,受光照影响的所有对象都应渲染到阴影贴图中,但是帧调试器告诉我们这没有发生。这是因为DrawShadows使用ShadowCaster着色器通道,而我们的着色器当前没有这样的通道。

2.1 阴影包含文件

要创建阴影投射器通道,请复制Lit.hlsl文件并将其重命名为ShadowCaster.hlsl。我们只关心深度信息,因此请从新文件中删除与片段位置无关的所有内容。片段程序的输出仅为零。还重命名其传递函数,并包括防护定义。

投影矩阵的性质_unity 投影矩阵_投影矩阵怎么求

这足以渲染阴影,但是阴影投射者有可能与附近的地方相交,从而导致阴影中出现孔洞。为了防止这种情况,我们必须将顶点钳位到顶点程序中的附近。这是通过获取剪辑空间位置的Z坐标和W坐标的最大值来完成的。

然而,这由于剪辑空间的细节而变得复杂。最直观的方式是将近裁剪平面处的深度值视为零,并随着距离的增加而增加。但这实际上是除OpenGL API以外的所有方法的反函数,在近平面处该值为1。对于OpenGL,近平面值为-1。我们可以依靠通过Common.hlsl包含的UNITY_REVERSED_Z和UNITY_NEAR_CLIP_VALUE宏来处理所有情况。

投影矩阵的性质_unity 投影矩阵_投影矩阵怎么求

2.2 第二个通道

要将ShadowCaster传递添加到我们的Lit着色器中,我们复制它的通道块,并给第二个通道一个Tag块,在其中将LightMode设置为ShadowCaster。然后,使其包含ShadowCaster.hlsl而不是Lit.hlsl,并使用适当的顶点和片段函数。

unity 投影矩阵_投影矩阵的性质_投影矩阵怎么求

现在,我们的对象被渲染到阴影贴图中。由于此时对象不受多重光照的影响,因此GPU实例化非常有效。

投影矩阵的性质_投影矩阵怎么求_unity 投影矩阵

(所有的阴影投射器 渲染为2个DC)

通过选择Shadows.Draw条目,你可以看到阴影贴图的最终内容。由于它是仅深度的纹理,因此帧调试器将向我们显示深度信息,白色代表近距离而黑色代表远距离。

投影矩阵怎么求_投影矩阵的性质_unity 投影矩阵

(渲染阴影贴图)

因为阴影贴图是用聚光灯作为相机渲染的,所以其方向与灯光的方向匹配。如果光线已旋转,因此其局部向上方向在世界空间中指向下方,则阴影贴图也将上下颠倒。

3 采样阴影贴图

此时,我们已经有了一个阴影贴图,其中包含我们需要的所有数据,但是我们尚未使用它。下一步是在以正常方式渲染对象时对阴影贴图进行采样

3.1 从世界坐标到阴影坐标

阴影贴图中存储的深度信息对于渲染贴图时使用的剪辑空间有效。我们将其称为阴影空间。它与我们正常渲染对象时使用的空间不匹配。要知道片段相对于我们存储的阴影深度的位置,我们必须将片段的位置转换为阴影空间。

第一步是使阴影贴图本身可用于我们的着色器。我们通过一个着色器纹理变量来做到这一点,我们将其命名为_ShadowMap。在MyPipeline中跟踪其标识符。

通过在最后一次执行阴影命令缓冲区之前调用SetGlobalTexture,在RenderShadows的末尾将阴影映射全局绑定到此变量。

接下来,我们将添加一个着色器矩阵变量以将其从世界空间转换为阴影空间,名为_WorldToShadowMatrix。也要跟踪其标识符。

通过将我们在渲染阴影时使用的视图和投影矩阵相乘,然后通过SetGlobalMatrix将其传递到GPU,可以找到此矩阵。

但是,基于剪辑空间Z的尺寸是否反转,仍然存在差异,我们可以通过SystemInfo.usesReversedZBuffer进行检查。如果是这样技能特效,我们必须在相乘之前否定投影矩阵的Z分量行(索引为2的行)。我们可以通过直接调整矩阵的m20至m23字段来做到这一点。

投影矩阵的性质_unity 投影矩阵_投影矩阵怎么求

现在,我们有了从世界空间到阴影剪辑空间的转换矩阵。但是剪辑空间从-1变为1,而纹理坐标和深度从0变为1。我们可以通过将矩阵与矩阵进行额外的乘法运算,将矩阵范围和维度在所有维度上缩放和偏移一半,从而将变换转换为矩阵。我们可以使用Matrix4x4.TRS方法通过提供偏移,旋转和缩放来获得这样的矩阵。

但是由于它是一个简单的矩阵,所以我们也可以简单地从单位矩阵开始并设置适当的字段。

3.2 采样深度

在Lit.hlsl中,为灯光数据添加一个缓冲区,并在其中定义float4x4 _WorldToShadowMatrix。

投影矩阵的性质_unity 投影矩阵_投影矩阵怎么求

纹理资源不是缓冲区的一部分。相反,它们是分别定义的。在这种情况下,我们可以使用TEXTURE2D_SHADOW宏定义_ShadowMap。

TEXTURE2D和TEXTURE2D_SHADOW有什么区别?

仅在OpenGL ES 2.0存在差异,因为它不支持阴影贴图的深度比较。但是我们不支持OpenGL ES 2.0,因此我们可以使用TEXTURE2D。但是可以使用TEXTURE2D_SHADOW来明确表明我们正在处理阴影数据。

这些宏是通过Core库中单独的API包含文件(根据我们通过Common.hlsl包含)在每个目标平台上定义的。

接下来,我们还必须定义用于对纹理进行采样的采样器状态。通常,这是通过SAMPLER宏完成的,但是我们将使用特殊的比较采样器,因此请使用SAMPLER_CMP。为了获得正确的采样器状态,我们必须给它起纹理的名称,并在其前面写上采样器。

什么是纹理Sampler?

在旧的GLSL代码中,我们使用sampler2D一起定义纹理和采样器状态。但是它们是两个独立的事物,并且都占用资源。采样器状态与纹理分开存在,这使得可以混合使用它们,通常重用同一采样器状态从多个纹理中采样。

在本例中,我们通过MyPipeline将采样器状态设置为使用钳位和双线性过滤。

在双线性插值发生之前,我们使用的比较采样器将为我们执行深度比较。与首先插值然后进行比较相比,这产生了更好的结果。

创建一个以世界位置为参数的ShadowAttenuation函数。它将返回光阴影的衰减因子。它需要做的第一件事是将世界位置转换为阴影位置。

投影矩阵怎么求_投影矩阵的性质_unity 投影矩阵

所产生的位置由齐次坐标定义,就像我们转换为剪辑空间时一样。但是我们需要规则的坐标,因此将XYZ分量除以其W分量。

现在,我们可以使用SAMPLE_TEXTURE2D_SHADOW宏对阴影贴图进行采样。它需要纹理,采样器状态和阴影位置作为参数。当位置的Z值小于阴影贴图中存储的值时,结果为1,这意味着该位置比投射阴影的对象更接近光。否则,它在阴影投射器后面,结果为零。因为采样器在双线性插值之前执行比较,所以阴影的边缘将在阴影贴图纹理之间混合。

3.3 阴影淡化

要影响照明,请在DiffuseLight函数中添加阴影衰减参数。将其与其他淡入淡出因子一起分解为漫射强度。

unity 投影矩阵_投影矩阵的性质_投影矩阵怎么求

阴影不适用于顶点照明2d游戏素材,因此在LitPassVertex中使用1进行阴影衰减。

unity 投影矩阵_投影矩阵怎么求_投影矩阵的性质

在LitPassFragment中,以世界位置作为参数调用ShadowAttenuation,并将结果传递给DiffuseLight。

unity 投影矩阵_投影矩阵怎么求_投影矩阵的性质

投影矩阵怎么求_投影矩阵的性质_unity 投影矩阵

(采样阴影)

阴影终于出现了,但伴有严重的尖刺。

4 阴影设置

有多种方法可以控制阴影的质量和外观。我们将添加一些支持,特别是阴影分辨率,深度偏差,强度和柔和阴影。这些和更多功能可以通过每个灯的检查器进行配置。

投影矩阵怎么求_unity 投影矩阵_投影矩阵的性质

(逐灯光的阴影设置)

4.1 阴影贴图大小

尽管灯光检查器可以选择其阴影分辨率,但这仅间接控制了阴影贴图的大小。实际尺寸至少通过Unity的默认管线通过质量设置来设置。我们使用自己的管线,因此将阴影映射大小配置选项添加到MyPipelineAsset。

阴影贴图是正方形纹理,我们将允许从256×256到4096×.4096的2幂次方大小。若要仅使这些选项可用,请在MyPipelineAsset中定义一个ShadowMapSize枚举,并使用元素256、512、1024、2048和4096。数字不能用于枚举标签,因此请在每个数字前加上下划线。显示枚举的选项时,Unity编辑器将省略下划线。然后使用枚举为阴影贴图大小添加一个配置字段。

unity 投影矩阵_投影矩阵怎么求_投影矩阵的性质

默认情况下,枚举代表整数并从零开始。如果我们的枚举选项直接映射到相同的整数,则可以更加方便,这可以通过为它们分配显式值来实现。

投影矩阵的性质_unity 投影矩阵_投影矩阵怎么求

这意味着零不是有效的默认值,因此请将默认值设置为其他值。

投影矩阵的性质_投影矩阵怎么求_unity 投影矩阵

(阴影贴图设置为1024)

将阴影贴图的大小传递给管线的构造方法,并转换为整数。

并添加一个字段以跟踪MyPipeline的大小,并在构造函数中对其进行初始化。

投影矩阵怎么求_unity 投影矩阵_投影矩阵的性质

现在,在RenderShadows中获取渲染纹理时,我们将使用可变的阴影贴图大小。

投影矩阵的性质_投影矩阵怎么求_unity 投影矩阵

投影矩阵怎么求_unity 投影矩阵_投影矩阵的性质

unity 投影矩阵_投影矩阵怎么求_投影矩阵的性质

(阴影贴图大小为256和4096)

4.2 阴影偏差

阴影尖刺是由阴影贴图的纹理表面凸出引起的。有关更详细的说明,请参见。我们将支持减轻尖刺的最简单方法,即在渲染到阴影贴图时添加一个小的深度偏移。此阴影偏差是针对每个光源配置的,因此我们必须将其发送到GPU。通过_ShadowBias着色器属性进行此操作,因此请跟踪其标识符。

在RenderShadows中设置视图和投影矩阵时,还应设置阴影偏差。VisibleLight结构不包含此信息,但是它确实具有一个光场,该场保存对具有阴影偏差的Light组件的引用。

投影矩阵的性质_投影矩阵怎么求_unity 投影矩阵

在阴影投射器缓冲区中,将相应的变量添加到ShadowCaster.hlsl。clamping之前,使用它偏移剪辑空间位置的Z分量。如果Z反转,则应减去偏置,否则将其相加。

unity 投影矩阵_投影矩阵怎么求_投影矩阵的性质

阴影偏差应尽可能小,以防止阴影移得太远而导致平移。

unity 投影矩阵_投影矩阵的性质_投影矩阵怎么求

投影矩阵怎么求_unity 投影矩阵_投影矩阵的性质

(阴影偏差 0.05 VS 0.01)

4.3 阴影强度

由于我们仅使用单个光源并且没有任何环境照明,因此我们的阴影是完全黑色的。但是我们可以调低阴影衰减的强度,使其仅部分减弱光的贡献。就像所有阴影投射器都是半透明的一样。我们将通过_ShadowStrength属性将阴影强度发送到着色器,因此请跟踪其标识符。

在对阴影贴图进行采样时会使用阴影强度,因此将其与世界到阴影矩阵和阴影贴图本身一起设置。像深度偏差一样,我们可以从Light组件中检索它。

投影矩阵的性质_unity 投影矩阵_投影矩阵怎么求

将阴影强度添加到阴影缓冲区,然后使用它在1和ShadowAttenuation中的采样衰减之间进行插值。

投影矩阵怎么求_投影矩阵的性质_unity 投影矩阵

unity 投影矩阵_投影矩阵的性质_投影矩阵怎么求

(阴影强度设置为0.5)

4.4 软阴影

我们将支持的最终设置是在硬阴影和软阴影之间切换。目前正在使用硬阴影,这意味着阴影边缘的唯一平滑是由对阴影贴图进行采样时的双线性插值引起的。启用平滑阴影后,阴影过渡将模糊,表示阴影具有更大的半影。但是,与现实生活不同,半影是均匀的,而不是取决于光源,阴影投射器和阴影接收器之间的空间关系。

通过对阴影贴图进行多次采样来制作柔和的阴影,而距离原始样本位置较远的样本对最终值的贡献较小。我们将使用5×5Tent滤镜,需要9个纹理样本。为此,我们可以使用在核心库的Shadow / ShadowSamplingTent.hlsl包含文件中定义的函数。将其包含在Lit.hlsl中。

tent过滤器如何工作?

Bloom教程涵盖了利用双线性纹理采样的滤镜内核,而Depth of Field教程则包含了一个3×3tent滤镜的示例。

tent 过滤器要求我们知道阴影图的大小。我们将要使用的函数特别需要一个向量,该向量的四个分量中的地图的反转为和高度,正则反转为和高度。因此,将其添加到阴影缓冲区。

跟踪MyPipeline中的相应标识符。

并将其设置在RenderShadows的末尾。

unity 投影矩阵_投影矩阵的性质_投影矩阵怎么求

当定义_SHADOWS_SOFT shader关键字时,我们将用tent过滤器替换ShadowAttenuation函数中的阴影贴图的常规采样。

投影矩阵怎么求_投影矩阵的性质_unity 投影矩阵

我们用累积九个样本来创建5×5tent 过滤器,来代替一个样本。SampleShadow_ComputeSamples_Tent_5x5函数通过传递阴影贴图大小和阴影位置的XY坐标作为参数,为我们提供了要使用的权重和UV坐标。权重和UV是通过两个输出参数(float数组和float2数组)提供的,两个参数均具有9个元素。

投影矩阵的性质_unity 投影矩阵_投影矩阵怎么求

但是,该函数的输出参数使用real而不是float定义。那不是实际的数字类型,而是一个根据需要用于创建浮点数或half变体的宏。我们通常可以忽略这一点,但是为了防止某些平台出现编译器错误unity 投影矩阵,最好也对输出参数使用real。

现在,我们可以使用循环使用数组中的权重和UV坐标对阴影贴图进行九次采样。这是一个紧密的固定循环,因此着色器编译器将展开它。我们仍然需要阴影位置的Z坐标,因此可以使用它为每个阴影样本构造一个float3。

unity 投影矩阵_投影矩阵怎么求_投影矩阵的性质

要启用柔和阴影,我们必须为定义_SHADOWS_SOFT关键字时创建一个着色器通过变量。这是通过在我们的Lit着色器的默认通道中添加一个多编译编译指示来完成的。我们希望它生成两个变体,一个不带,另一个带定义的关键字。为此,我们编写了一个下划线来表示不带关键字的变体,后跟_SHADOWS_SOFT关键字。

最后,我们必须在RenderShadows的结尾根据灯光的shadows属性的值切换关键字。如果将其设置为LightShadows.Soft,则在阴影缓冲区上调用EnableShaderKeyword方法。否则,请调用DisableShaderKeyword。Unity使用关键字state来决定渲染时使用哪个pass变量。

unity 投影矩阵_投影矩阵的性质_投影矩阵怎么求

由于基于布尔值切换关键字是很常见的,因此我们也可以使用CoreUtils.SetKeyword方法执行相同的操作。

投影矩阵的性质_unity 投影矩阵_投影矩阵怎么求

unity 投影矩阵_投影矩阵怎么求_投影矩阵的性质

投影矩阵的性质_投影矩阵怎么求_unity 投影矩阵

(硬阴影与软阴影)

文章来源:https://blog.csdn.net/haog87/article/details/107777357