目录
· 1 渲染阴影
· 1.1 阴影设置
· 1.2 透传设置
· 1.3 阴影类
· 1.4 带有阴影的光照
· 1.5 创建阴影图集
· 1.6 阴影优先
· 1.7 渲染
· 1.8 Shadow Caster Pass
· 1.9 多灯光
· 2 采样阴影
· 2.1 阴影矩阵
· 2.2 存储每个灯光的阴影数据
· 2.3 阴影 HLSL文件
· 2.4 采样阴影
· 2.5 灯光衰减
· 3 级联阴影贴图
· 3.1 设置
· 3.2 渲染级联
· 3.3 球形剔除
· 3.4 采样级联
· 3.5 剔除阴影采样
· 3.6 最大距离
· 3.7 渐变阴影
· 3.8 级联渐变
· 4 阴影质量
· 4.1 深度偏差
· 4.2 级联数据
· 4.3 法线偏差
· 4.4 可配置的偏差
· 4.5 阴影花纹(Shadow Pancaking)
· 4.6 PCF过滤
· 4.7 级联混合
· 4.8 过渡抖动
· 4.9 剔除偏差
· 5 透明度
· 5.1 阴影模式
· 5.2 裁切阴影
· 5.3 抖动阴影
· 5.4 无阴影
· 5.5 不受光阴影投射器
· 5.6 接受阴影
本文重点:
1、渲染和采样阴影贴图
2、支持多个方向光阴影
3、使用cascaded阴影贴图
4、融合,渐变以及过滤阴影
这是自定义可编程渲染管线系列的第四章,增加对Cascaded阴影贴图的支持。
本教程是CatLikeCoding系列的一部分,原文地址见文章底部。
本篇教程示例使用Unity2019.2.14f1。
(防止光线到达它不应该到达的地方)
1 渲染阴影
当进行物体渲染时,表面和灯光信息足以计算光照。但是在两者之间可能存在某些阻碍光线的东西,导致在我们需要渲染的表面上投射了阴影。为了使阴影能够正常表现,就必须以某种方式让着色器知道阴影对象。这有很多种方法可以实现, 最常见的方法是生成一个阴影贴图,该贴图存储光在击中表面之前离开其源的距离。任何在同一个方向上更远的距离都不能被同一个光源照亮。Unity的RP使用这种方法,我们也会这样做。
1.1 阴影设置
在开始渲染阴影之前,我们首先要对阴影的质量做出一些定义,特别是要决定要渲染阴影的距离以及阴影贴图的大小。
虽然我们可以在摄像机可以看到的范围内渲染阴影,但这将需要大量的绘制和非常大的贴图才能充分覆盖该区域,这几乎是不切实际的。因此,我们为阴影引入最大距离,最小距离为零,默认情况下设置为100个单位。创建一个新的可序列化ShadowSettings类以包含此设置。此类纯粹是用于配置选项的容器,因此我们将为它提供一个公共的maxDistance字段。
对于贴图大小,我们将在ShadowSettings内嵌套一个TextureSize枚举类型。使用它来定义允许的纹理大小,所有大小均为256-8192(2的幂)。
然后为阴影贴图添加一个大小字段,默认值为1024。我们将使用单个纹理包含多个阴影贴图,因此将其命名为atlasSize。由于我们现在仅支持定向光源,因此我们现在也只处理定向阴影贴图。但是我们将来会支持其他类型的光源,它们将获得自己的阴影设置。因此,将atlasSize放在Directional结构中。这样,我们可以在检查器中自动获得hierarchical 配置。
将阴影设置字段添加到CustomRenderPipelineAsset。
(阴影设置)
构造后,将这些设置传递给CustomRenderPipeline实例。
并且要保持对他们的引用。
1.2 透传设置
从现在开始,当我们调用Render方法时,会将这些设置传递给camera renderer。这样的话,添加对运行时更改阴影设置的支持就会很容易了,但是在本教程中我们将不再处理。
然后将CameraRenderer.Render传递给Lighting.Setup以及自己的Cull方法。
我们需要在Cull中进行设置,因为阴影距离是通过culling参数设置的。
渲染距离摄像机看不到的阴影没有意义,因此请选择最大阴影距离和摄像机远剪切平面中的最小值。
为了使代码编译,我们还需要将阴影设置的参数添加到Lighting.Setup,但是我们暂时不会对其进行任何处理。
1.3 阴影类
尽管从逻辑上讲阴影是光照的一部分,但它们相当复杂,所以让我们创建一个专用于它们的新Shadows类。它以Lighting的精简存根副本开始,具有自己的缓冲区,上下文字段,剔除结果和设置,用于初始化字段的Setup方法以及ExecuteBuffer方法。
然后,所有Lighting需要做的就是跟踪Shadows实例,并在其自身的Setup方法中调用SetupLight之前的Setup方法。
1.4 带有阴影的光照
由于渲染阴影需要额外的工作,可能会减慢帧率,因此我们将限制可以存在的定向光阴影的数量,这与支持的定向光的数量无关。为此,向“ Shadows ”添加一个常量,该常量最初设置为一个。
我们不知道哪个可见光会产生阴影,因此必须对它们进行追踪。除此之外,稍后我们还将跟踪每个阴影光的更多数据,先定义一个内部ShadowedDirectionalLight结构,此结构现在仅包含索引并跟踪这些数组。
为了弄清楚哪些光会产生阴影,我们将添加一个带有光和可见光索引参数的公共ReserveDirectionalShadows方法。它的工作是在阴影图集中为灯光的阴影贴图保留空间,并存储渲染它们所需的信息。
由于阴影光的数量有限,我们必须追踪已保留的数量。在设置中将计数重置为零。然后检查是否在ReserveDirectionalShadows中尚未达到最大值。如果还有空间,存储灯光的可见索引并增加计数。
但是阴影只能保留给有阴影的灯光。如果灯光的阴影模式设置为无或阴影强度为零,则它没有阴影,应将其忽略。
除此之外,可见光最终可能不会影响任何投射阴影的对象,这可能是因为它们没有配置,或者是因为光线仅影响了超出最大阴影距离的对象。我们可以通过在剔除结果上调用GetShadowCasterBounds以获得可见光索引来进行检查。它具有边界的第二个输出参数(我们不需要),并返回边界是否有效。如果不是,则没有阴影可渲染,因此应将其忽略。
现在我们可以在Lighting.SetupDirectionalLight中保留阴影了。
1.5 创建阴影图集
保留阴影后,我们需要渲染它们。在SetupLights在Lighting.Render中完成之后,通过调用新的Shadows.Render方法来执行此操作。
Shadows.Render方法会将定向阴影的渲染委托给另一个RenderDirectionalShadows方法,但前提是有阴影的灯光存在。
通过将阴影投射对象绘制到纹理来完成创建阴影贴图。我们将使用_DirectionalShadowAtlas来引用定向阴影图集。从设置中检索整数形式的图集大小,然后以纹理标识符作为参数,在命令缓冲区上调用GetTemporaryRT,再加上其宽度和高度的大小(以像素为单位)。
它声明具有正方形的渲染纹理,但默认情况下是普通的ARGB纹理。我们需要一个阴影贴图,通过在调用中添加另外三个参数来指定阴影贴图。首先是深度缓冲区的位数。我们希望它尽可能高,所以让我们使用32。其次是filter 模式,为此我们使用默认的bilinear filtering。第三是渲染纹理类型,必须RenderTextureFormat.ShadowMap。尽管确切的格式取决于目标平台,但这为我们提供了适合渲染阴影贴图的纹理。
当获得临时渲染纹理时,我们还应该在完成处理后释放它。因此必须持有它,直到用相机完成渲染,然后才能通过使用缓冲区的纹理标识符调用ReleaseTemporaryRT来执行释放。如果我们有阴影的定向灯,创建一个新的公共
的Cleanup方法进行处理。
也给Lighting提供一个公共的Cleanup方法,该方法会将调用转发给Shadows。
然后,CameraRenderer可以在提交之前直接请求清除。
请求渲染纹理后,Shadows.Render还必须指示GPU渲染到该纹理而不是相机的目标。这是通过在缓冲区上调用SetRenderTarget,标识渲染纹理以及如何加载和存储其数据来完成的。我们不在乎它的初始状态,因为会立即清除它,因此我们将使用RenderBufferLoadAction.DontCare。纹理的目的是包含阴影数据,因此我们需要使用RenderBufferStoreAction.Store作为第三个参数。
完成后,我们可以像清除相机目标一样使用ClearRenderTarget,在这种情况下,只需关心深度缓冲区。通过执行缓冲区来完成。如果你至少有一个阴影定向光处于活动状态,那么你会看到阴影图集的clear动作显示在帧调试器中。
(清除2次渲染目标)
1.6 阴影优先
当我们在阴影图集之前设置常规摄影机时,最终会在渲染常规几何图形之前切换到阴影图集,这不是我们想要的。我们应该在调用CameraRenderer.Render中的CameraRenderer.Setup之前渲染阴影,这样常规渲染不会受到影响。
(阴影优先)
通过在设置照明之前开始采样并在清除照明对象之前立即结束采样,可以在帧调试器中将阴影条目嵌套在相机的内部。
(嵌套阴影)
1.7 渲染
为了为单个光源渲染阴影,我们将向Shadow添加一个变体RenderDirectionalShadows方法,该方法具有两个参数:第一个是阴影光索引,第二个是它在图集中的图块大小。然后,对由BeginSample和EndSample调用包装的其他RenderDirectionalShadows方法中的所有阴影光调用此方法。由于我们目前仅支持单个阴影光,因此其Tile大小等于图集大小。
要渲染阴影,我们需要一个ShadowDrawingSettings结构值。可以通过调用其构造函数方法,以及我们先前存储的剔除结果和适当的可见光索引,来创建配置正确的对象。
阴影贴图的原理是,我们从灯光的角度渲染场景,只存储深度信息。用结果标记出,光线在击中某物之前会传播多远。
但是,定向光被假定为无限远,没有真实位置。因此,我们要做的是找出与灯光方向匹配的视图和投影矩阵,并为我们提供一个剪辑空间立方体,该立方体与包含可见光阴影的摄像机可见区域重叠。这个不用自己去实现,我们可以使用culling results的ComputeDirectionalShadowMatricesAndCullingPrimitives方法为我们完成此工作,并为其传递9个参数。
第一个参数是可见光指数。接下来的三个参数是两个整数和一个Vector3,它们控制阴影级联。稍后我们将处理级联,因此现在使用零,一和零向量。然后是纹理尺寸,我们需要使用平铺尺寸。第六个参数是靠近平面的阴影,我们现在将其忽略并将其设置为零。
这些是输入参数,其余三个是输出参数。首先是视图矩阵,然后是投影矩阵,最后一个参数是ShadowSplitData结构。
拆分的数据包含有关应如何剔除投射对象的信息,我们需要将其复制到阴影设置中。而且,我们需要通过在缓冲区上调用SetViewProjectionMatrices来应用视图和投影矩阵。
最终通过执行缓冲区,然后在上下文中调用DrawShadows来调度阴影投射程序的绘制,并通过引用将阴影设置传递给它。
1.8 Shadow Caster Pass
此时,应该渲染阴影投射器了,但是图集仍然是空的。这是因为DrawShadows仅使用具有ShadowCaster传递的材质来渲染对象。因此,将第二个Pass块添加到我们的Lit着色器中,将其光照模式设置为ShadowCaster。使用相同的目标级别,为其提供实例化支持,以及_CLIPPING着色器功能。然后使用特殊的阴影投射器功能,这些功能将在新的ShadowCasterPass HLSL文件中定义。另外,由于只需要写深度,禁用颜色功能,因此可以在HLSL程序之前添加ColorMask 0。
通过复制LitPass并删除阴影投射器不需要的所有内容来创建ShadowCasterPass文件。因此,我们只需要剪切空间位置以及剪切的基色。片段函数没有任何返回值,因此如果没有语义,它将变为无效。它唯一能做的就是裁减片段。
现在,我们可以渲染阴影投射器。我创建了一个简单的测试场景,该场景在平面上包含一些不透明的对象,并带有一个定向光,该光具有启用了阴影的全部强度以进行尝试。灯光设置为使用硬阴影还是软阴影都没关系。
(阴影测试场景)
阴影尚未影响最终渲染的图像,但是我们已经可以通过帧调试器查看会将什么渲染到阴影图集中。通常将其可视化为单色纹理,随着距离的增加,颜色从白色变为黑色,但是当使用OpenGL时,颜色变为红色,而且是相反的。
(512的图集尺寸,最大距离为100)
将最大阴影距离设置为100,我们最终将所有内容渲染到纹理的一小部分。有效减小最大距离可以使阴影贴图放大相机前面的内容。
(最大距离从20变为10)
请注意,阴影投射器使用正交投影进行渲染,因为我们是针对定向光进行渲染。
1.9 多灯光
我们最多可以有四个定向灯,因此我们也可以支持四个定向灯的阴影。
作为快速测试,我使用了四个等效的定向灯,只是我将其Y旋转调整了90°增量。
(4个灯光叠加后的阴影投射器)
尽管最终我们正确地为所有灯光渲染了阴影投射器,但是当我们为每个灯光渲染整个图集时,它们都被叠加了。我们必须拆分图集,以便为每个光源提供自己的图块以进行渲染。
我们最多支持四个阴影灯,并在方形图集中为每个灯光提供一个方形Tile。因此,如果最后得到一个以上的阴影光,则需要将图块大小减半并将图集分成四个图块。在Shadows.RenderDirectionalShadows中确定分割量和tile大小,然后将每个灯光的分割量和平铺大小都传递给另一种方法。
我们可以通过调整渲染视口来渲染为单个图块。为此创建一个新方法,该方法具有一个磁贴索引并作为参数拆分。它首先计算图块偏移量,其中将以模为模的索引作为X偏移,将以该模除的索引作为Y偏移。这些是整数运算,但是我们最终定义了Rect,因此将结果存储为Vector2。
然后,使用Rect在缓冲区上调用SetViewPort,其偏移量根据切片大小缩放,该大小应成为第三个参数,可以立即成为浮点数。
设置矩阵时,调用RenderDirectionalShadows中的SetTileViewport。
(使用4个Tile来分别存储阴影信息)
2 采样阴影
现在,我们已经渲染了阴影投射器,但这还不会影响最终图像的生成。为了显示阴影,我们需要在CustomLit通道中对阴影贴图进行采样,然后使用它来确定是否对表面片段进行阴影处理。
2.1 阴影矩阵
对于每个片段,我们必须从阴影图集中的适当图块中采样深度信息。因此,我们需要找到给定世界空间位置的阴影纹理坐标。通过为每个阴影定向光创建一个阴影转换矩阵并将其发送到GPU来实现这一点。将_DirectionalShadowMatrices着色器属性标识符和静态矩阵数组添加到Shadows中,以实现此目的。
通过将灯光的阴影投影矩阵和RenderDirectionalShadows中的视图矩阵相乘,可以创建从世界空间到灯光空间的转换矩阵。
然后,在渲染完所有阴影光之后,通过调用缓冲区上的SetGlobalMatrixArray将矩阵发送到GPU。
但是,这忽略了我们使用阴影图集的事实。创建一个ConvertToAtlasMatrix,它接受一个light matrix,tile offset, 和split,并返回一个从世界空间转换为阴影图块空间的矩阵。
我们已经在SetTileViewport中计算了offset,因此使其返回该offset 。
然后调整RenderDirectionalShadows,以便它调用ConvertToAtlasMatrix。
如果使用反向Z buffer,我们在ConvertToAtlasMatrix中应该做的第一件事就是取反Z尺寸。我们可以通过SystemInfo.usesReversedZBuffer进行检查。
为什么Z缓冲区要反转?
最直观的是,0代表零深度,1代表最大深度。OpenGL就是这样做的。但是由于深度缓存器中精度的方式受到限制以及非线性存储的事实,我们通过反转来更好地利用这些位。其他图形API使用了反向方法。通常,我们不需要担心这个,除非我们明确使用Clip 空间。
其次,在立方体内部定义剪辑空间,其坐标从-1到1,中心为零。但是纹理坐标和深度从零到一。我们可以通过将XYZ尺寸缩放和偏移一半来将这种转换烘焙到矩阵中。使用矩阵乘法来执行此操作,但是它会导致大量与0之间的乘法,或者不必要的加法运算。因此,让我们直接调整矩阵。
最后,我们需要应用图块的偏移量和比例。再一次,我们可以直接执行此操作以避免大量不必要的计算。
2.2 存储每个灯光的阴影数据
要对光的阴影进行采样,我们需要知道其阴影图集中的Tile索引(如果有)。这是每个灯光必须存储的东西,因此让ReserveDirectionalShadows返回所需的数据。我们将提供两个值:阴影强度和阴影图块偏移量,打包在Vector2中。如果光线没有阴影,则结果为零向量。
让Lighting通过_DirectionalLightShadowData向量数组将此数据提供给着色器。
并将其也添加到Light HLSL文件的_CustomLight缓冲区中。
2.3 阴影 HLSL文件
我们还将创建一个专用的Shadows HLSL文件以进行阴影采样。在_CustomShadows缓冲区中,定义相同的最大阴影定向光计数以及_DirectionalShadowAtlas纹理以及_DirectionalShadowMatrices数组。
由于图集不是常规的纹理,因此我们可以通过TEXTURE2D_SHADOW宏对其进行定义,即使它对我们支持的平台没有影响,也要使其清晰可见。我们将使用一个特殊的SAMPLER_CMP宏来定义采样器状态,因为这确实定义了一种不同的方式来采样阴影贴图,因为常规的双线性过滤对深度数据没有意义。
实际上,只有一种合适的方法可以对阴影贴图进行采样,因此我们可以定义一个明确的采样器状态,而不是依赖Unity推导的渲染纹理状态。可以内联定义采样器状态,方法是在其名称中创建一个带有特定单词的状态。我们可以使用sampler_linear_clamp_compare。我们还为其定义一个简写的SHADOW_SAMPLER宏。
在LitPass中,Light之前包括“Shadows ”。
2.4 采样阴影
为了对阴影进行采样,需要了解每个光的阴影数据,因此让我们在Shadows中为它定义一个结构,特别是定向光。它包含strength 和tile offset,但是Shadows中的代码不知道它的存储位置。
我们还需要知道表面位置,因此将其添加到Surface结构中。
并在LitPassFragment中分配它。
向“Shadows”添加SampleDirectionalShadowAtlas函数,该函数通过SAMPLE_TEXTURE2D_SHADOW宏对阴影图集进行采样,并向其传递图集,阴影采样器以及阴影纹理空间中的位置(这是一个对应的参数)。
然后添加GetDirectionalShadowAttenuation函数,该函数在给定方向阴影数据和表面的情况下返回阴影衰减,该阴影应该在世界空间中定义。它使用tile offset 来检索正确的矩阵,将表面位置转换为阴影图块空间,然后对图集进行采样。
对阴影图集进行采样的结果是一个决定因素,仅考虑阴影,它确定有多少光到达表面。它是介于0到1范围内的值,称为衰减因子。如果片段完全被阴影覆盖,那么我们将得到零,而如果根本没有阴影,那么我们将得到一。之间的值表示片段被部分遮挡。
除此之外,无论是处于艺术原因或代表半透明表面的阴影,都可以降低灯光的阴影强度。当强度降低到零时,衰减完全不受阴影影响,应为1。因此,最终衰减是通过强度和采样衰减之间的线性插值找到的。
但是,当阴影强度为零时,根本就不需要对阴影进行采样,因为它们没有效果并且甚至没有被渲染。在这种情况下,就相当于一个不为人知的灯,它应该总是返回1。
在着色器中使用分支是个好主意吗?
分支曾经效率低下,但是现代GPU可以很好地处理它们。你要记住的是,片段的block是并行着色的。即使只有一个片段以一种特定的方式进行分支,即使所有其他片段都忽略了该代码路径的结果,整个Block还是会这样做。而这个案例,我们基于灯光的强度进行分支,至少在这一点上,所有片段都是相同的。
2.5 灯光衰减
我们把光的衰减存储在Light结构中。
向Light添加一个函数,以获取方向阴影数据。
然后将世界空间表面参数添加到GetDirectionalLight,让它检索方向阴影数据并使用GetDirectionalShadowAttenuation设置光源的衰减。
现在,照明中的GetLighting还必须将表面传递到GetDirectionalLight。现在应该在世界空间中定义表面,因此请相应地重命名参数。只要它们匹配,只有BRDF不在乎灯光和表面的空间。
使阴影起作用的最后一步是将衰减量纳入光线的强度中。
(一个带有阴影的灯光 最大距离为10 图集尺寸为512)
现在终于得到阴影,但它们看起来很糟糕。不应被阴影化的表面最终会被形成像素化带的阴影伪影所覆盖。这些是由于阴影贴图的有限分辨率导致的自我阴影化。使用不同的分辨率会更改伪影模式,但不会消除它们。这些表面最终会部分遮盖自身,但稍后我们将解决此问题。该效果使查看阴影贴图所覆盖的区域变得容易,因此我们暂时保留它们。
例如,我们可以看到阴影图仅覆盖可见区域的一部分,由最大阴影距离控制。更改最大值会增大或缩小区域。阴影贴图与光线方向对齐,而不与相机对齐。在最大距离之外,可以看到一些阴影,但是在超出地图边缘的地方对阴影进行采样时,一些阴影会变得奇怪。如果只有一个阴影光处于活动状态,则结果应该受到收紧限制,否则样本可能会越过tile边界,并且最终会使用来自另一个灯光的阴影来产生光。
(两盏带有阴影的灯光,都是一半的强度)
稍后我们将在最大距离处正确切除阴影,但目前这些无效阴影仍然可见。
3 级联阴影贴图
由于定向光会影响最大阴影距离范围内的所有物体,因此它们的阴影贴图最终会覆盖较大的区域。由于阴影贴图使用正交投影,因此阴影贴图中的每个纹理像素都具有固定的世界空间大小。如果该尺寸太大,则清晰可见单个阴影纹理,从而导致锯齿状的阴影边缘和小的阴影可能消失。可以通过增加图集大小来缓解这种情况,但仅限于一定程度。
使用透视相机时,较远的地方看起来较小。在某个视觉距离处,阴影贴图纹理像素将映射到单个显示像素,这意味着阴影分辨率在理论上是最佳的。距离相机越近,我们需要的阴影分辨率就越高,而距离更低的分辨率就足够了。这表明理想情况下,我们将根据阴影接收器的视距使用可变的阴影贴图分辨率。
级联阴影贴图(Cascaded Shadow Maps)是解决此问题的方法。这个想法是阴影投射器被渲染了不止一次,因此每个光在图集中会得到多个图块,称为级联(Cascaded )。第一个级联仅覆盖靠近相机的一小部分区域,而连续的级联会缩小以覆盖越来越大的具有相同像素数量的区域。然后,着色器对每个片段可用的最佳级联进行采样。
3.1 设置
Unity的阴影代码每个定向光最多支持四个级联。到目前为止,我们仅使用了单个级联,它涵盖了最大阴影距离之前的所有内容。为了支持更多功能,我们将在方向阴影设置中添加一个层叠计数滑块。虽然我们可以为每个定向光使用不同的量,但对所有阴影定向光使用相同的量是最有意义的。
每个级联覆盖阴影区域的一部分直至最大阴影距离。我们将通过为前三个级联添加比率滑块来使确切部分可配置。最后一个级联始终覆盖整个范围,因此不需要滑块。默认情况下,将级联计数设置为4,级联比率为0.1、0.25和0.5。这些比率应随着每个级联的增加而增加,但我们不会在UI中强制执行。
(Cascade 的数量和比率)
ComputeDirectionalShadowMatricesAndCullingPrimitives方法要求我们提供打包在Vector3中的比率,因此让我们在设置中添加一个方便的属性以该形式检索它们。
3.2 渲染级联
每个级联都需要其自己的变换矩阵,因此阴影的阴影矩阵阵列大小必须乘以每个光的最大级联数量(即4)。
在“Shadows”中也增加数组的大小。
完成此操作后,Unity将抱怨着色器的数组大小已更改,但无法使用新的大小。这是因为一旦着色器声明了固定数组,就无法在同一会话期间在GPU上更改其大小。我们需要重新启动Unity才能对其进行初始化。
完成后,将返回的Shadows.ReserveDirectionalShadows中的tile offset乘以配置的级联数量,因为每个定向光源现在将声明多个连续的tiles。
同样,使用的图块数量在RenderDirectionalShadows中成倍增加,这意味着我们最终可以得到总共16个图块,需要四分之一。
为什么不支持三分之一?
我们将自己限制为2的幂,这与对图集大小的限制相同。这样一来,整数除法始终是可行的,否则我们会遇到无法对准的问题。这意味着某些灯光配置不会使用所有可用的图块,从而浪费了纹理空间。如果这是一个问题,则可以添加对不需要为正方形的矩形图集的支持。但是,与纹理空间相比,你更有可能受到可渲染的图块数量的限制。
现在,RenderDirectionalShadows必须为每个级联绘制阴影。对于每个已配置的级联,将ComputeDirectionalShadowMatricesAndCullingPrimitives中的代码放入并包含DrawShadows的循环中。现在,ComputeDirectionalShadowMatricesAndCullingPrimitives 的第二个参数成为级联索引,然后是级联计数和级联比率。还要调整瓦片索引,使其成为光源的瓦片偏移量加上级联索引。
(1个核4个灯光带有4级级联,最大距离30,比率0.3,0.4,0.5)
3.3 球形剔除
Unity通过为其创建一个选择球来确定每个级联覆盖的区域。由于阴影投影是正交的且呈正方形,因此它们最终会紧密契合其剔除球,但还会覆盖周围的一些空间。这就是为什么可以在剔除区域之外看到一些阴影的原因。同样,光的方向与球无关,因此所有定向光最终都使用相同的剔除球。
(使用透明的球体来让剔除球可视化)
还需要这些球体来确定从哪个级联进行采样,因此我们需要将它们发送到GPU。为级联计数和级联的剔除球体数组添加一个标识符,并为球体数据添加一个静态数组。它们由四分量矢量定义,包含其XYZ位置及其在W分量中的半径。
级联的剔除球是ComputeDirectionalShadowMatricesAndCullingPrimitives输出的拆分数据的一部分。将其分配给RenderDirectionalShadows中循环中的球体数组。但是我们只需要对第一个光源执行此操作,因为所有光源的级联都是等效的。
我们需要着色器中的球体来检查表面碎片是否位于其中,这可以通过将距球体中心的平方距离与其半径进行比较来实现。因此,让我们存储平方半径,这样就不必在着色器中计算它了。
渲染级联后,将级联计数和球体发送到GPU。
3.4 采样级联
将级联计数和球形剔除数组添加到Shadows中。
级联指数是根据每个片段而不是每个光确定的。因此,让我们介绍一个包含它的全局ShadowData结构。稍后我们将向其中添加更多数据。还添加一个GetShadowData函数,该函数返回世界空间表面的阴影数据,最初级联索引始终设置为零。
将新数据作为参数添加到GetDirectionalShadowData中,以便通过将级联索引添加到灯光的阴影tile offset中来选择正确的tile索引。
还要向GetDirectionalLight添加相同的参数,以便它将数据转发到GetDirectionalShadowData。适当重命名方向阴影数据变量。
在GetLighting中获取阴影数据并将其传递。
(总是使用第一个 VS 总是使用最后一个 级联)
为了选择正确的级联,我们需要计算两点之间的平方距离。为此,我们为Common添加一个方便的功能。
遍历GetShadowData中的所有级联球形剔除,直到找到包含表面位置的球。一旦找到循环,请中断循环,然后将当前循环迭代器用作级联索引。这意味着如果片段位于所有区域之外,那么我们将获得无效索引,但是现在我们将忽略它。
(选择最合适的级联)
现在,我们得到具有更好像素纹理分布的阴影。由于存在自阴影伪影,因此级联之间的弯曲过渡边界也可见,尽管我们可以用级联索引(除以四)代替阴影衰减,使它们更容易被识别。
(级联索引阴影)
3.5 剔除阴影采样
如果我们超出了最后一个级联,则很可能没有有效的阴影数据,因此我们根本不应该采样阴影。一种简单的实现方法是在ShadowData中添加一个强度字段,默认情况下将其设置为1,如果最终超出最后一个级联,则将其设置为零。
然后将全局阴影强度计入GetDirectionalShadowData中的方向阴影强度。剔除最后一个级联之后的所有阴影。
此外,在GetDirectionalLight中恢复正确的衰减。
(剔除后的阴影,最大距离12)
3.6 最大距离
一些关于最大阴影距离的实验将揭示出,一些阴影投影器在最后一个级联的剔除球内突然消失。发生这种情况的原因是,最外面的剔除球并没有完全以配置的最大距离结束,而是超出了该范围。在最大距离较小的情况下,这种差异最为明显。
可以通过停止以最大距离采样阴影来修复阴影的超出。为了修复这个问题,我们必须在阴影中将最大距离发送到GPU。
最大距离是基于视图空间的深度,而不是距相机位置的距离。因此,要执行此剔除,我们需要知道表面的深度。为此,将一个字段添加到Surface。
可以在LitPassFragment中找到深度,方法是通过TransformWorldToView从世界空间转换为视图空间,并取负Z坐标。由于此转换只是相对于世界空间的旋转和偏移,因此视图空间和世界空间的深度相同。
现在,仅在表面深度小于最大距离时才执行此操作,而不是始终在GetShadowData中将强度初始化为一,否则将其设置为零。
(添加基于深度的剔除)
3.7 渐变阴影
突然在最大距离处截去阴影可能非常明显,因此让我们通过线性淡化阴影使过渡更加平滑。衰落从最大值开始的一段距离开始,直到我们在最大值达到零强度为止。为此,我们可以使用函数
钳位为0~1,其中d 是表面深度,m 是最大阴影距离,f 是淡入范围,表示为最大距离的一部分。
(f为0.1,02和0.5)
为距离淡入度添加一个滑块到阴影设置。由于淡入值和最大值都用作除数,因此它们不应为零,因此将其最小值设置为0.001。
将“shadow ”中的阴影距离标识符替换为距离值和淡入值两者。
将它们作为向量的XY分量发送到GPU时,请使用一个除以值的值,这样就可以避免在着色器中进行除法,因为乘法速度更快。
调整阴影中的_CustomShadows缓冲区以使其匹配。
现在我们可以使用(1-d s)f饱和来计算阴影强度,其中1/ m用于标度s,1/f用于新的衰减乘数f。为此创建一个FadedShadowStrength函数,并在GetShadowData中使用它。
(距离渐变)
3.8 级联渐变
我们也可以使用相同的方法在最后一个层叠的边缘处淡化阴影,而不是将其切除。为此添加一个层叠渐变阴影设置滑块。
唯一的区别是我们使用级联的距离和半径的平方,而不是线性深度和最大值。这意味着过渡变为非线性:
,其中r 为剔除球半径。差别不是很大,但是要保持配置的淡入淡出率不变,我们必须将f 替换为
。然后我们将其存储在阴影距离淡入矢量的Z分量中,再次取反。
(平方距离,f为0.1,0.2,0.5)
要执行级联渐隐,请检查我们是否仍在GetShadowData的循环内而处于最后一个级联中。如果是这样,请计算级联的淡入阴影强度,并将其作为最终强度。
(级联和距离双重渐变)
4 阴影质量
现在,我们已经具有功能性的级联阴影贴图,让我们集中精力改善阴影的质量。我们一直观察到的伪影被称为暗疮粉刺,这是由于与光的方向不完全对齐的表面的不正确的自阴影引起的。随着表面越来越接近平行于光的方向,粉刺变得更糟。
(阴影粉刺)
增加图集大小会减少纹理像素的世界空间大小,因此粉刺伪影会变小。但是,伪影的数量也会增加,因此无法通过简单地增加图集大小来解决该问题。
4.1 深度偏差
有多种减轻阴影痤疮的方法。最简单的方法是向阴影投射器的深度添加恒定的偏差,将其推离光线,从而不再发生不正确的自阴影。添加此技术的最快方法是在渲染时应用全局深度偏差,在DrawShadows之前在缓冲区上调用SetGlobalDepthBias,然后再将其设置回零。这是应用于剪辑空间的深度偏差,并且是非常小的值的倍数,具体取决于用于阴影贴图的确切格式。我们可以通过使用一个较大的值(例如50000)来了解其工作原理。还有一个第二个参数表示坡度比例偏差,但现在将其保持为零。
(常量的深度偏差)
恒定的偏差很简单,但只能消除正面朝上照亮的表面的伪影。去除所有粉刺需要更大的偏差,例如大一个数量级。
(更大的深度偏差)
但是,随着深度偏差将阴影投射器推离光线,采样阴影也会沿相同方向移动。偏差大到足以消除大多数痤疮不变的阴影,从而使阴影看起来与投射器分离,从而导致视觉伪影,称为彼得·潘宁(Peter-Panning)。
(偏差引起的 peter-panning)
另一种方法是应用斜率比例偏差,方法是对SetGlobalDepthBias的第二个参数使用非零值。此值用于缩放沿X和Y维度的最大绝对剪切空间深度导数。因此,对于正面照亮的表面,该值为零;当光线在至少两个维度中的至少一个以45°角入射时,该值为1;而当表面法线和光方向的点积达到零时,则为无穷大。因此,当需要更多时,偏差会自动增加,但没有上限。结果,只需要低得多的系数来消除粉刺,例如用3代替500000。
(斜率比偏差)
斜率比偏差是有效的,但不是直观的。实验是需要达成一个可接受的结果,而不是用Peter-Panning来替换粉刺。因此,让我们暂时禁用它,并寻找一种更直观和可预测的方法。
4.2 级联数据
因为痤疮的大小取决于世界空间纹理的大小,所以在所有情况下都可以使用的一致方法必须考虑到这一点。由于每个级联的texel大小不同,这意味着我们将不得不向GPU发送更多级联数据。为此,将一个通用级联数据矢量数组添加到Shadows。
将其与其他所有内容一起发送到GPU。
我们已经可以做的一件事是将级联半径平方的倒数放在这些向量的X分量中。这样,我们就不必在着色器中执行此计算。在新的SetCascadeData方法中执行此操作,同时存储拣选球并在RenderDirectionalShadows中调用它。将级联索引,剔除球和图块大小作为浮点传递给它。
将级联数据添加到Shadows中的_CustomShadows缓冲区中。
并在GetShadowData中使用新的预计算逆。
4.3 法线偏差
发生不正确的自遮影是因为阴影投射深度texel覆盖了多个片段,从而导致投射物体的体积从其表面戳出。因此,如果我们将投射器缩小得足够多,则该情况不再发生。但是,缩小阴影投射器将使阴影小于应有的阴影,并且可能会引入不应该存在的孔。
我们也可以做相反的事情:在采样阴影的同时对表面进行充气。然后,我们从表面取样一点,足够远以避免不正确的自阴影化。这将稍微调整阴影的位置,可能会导致沿边缘的未对准并添加错误的阴影,但是这些伪像往往不如Peter-Panning明显。
我们可以通过沿其法线向量稍微移动表面位置来实现此目的,以采样阴影。如果仅考虑一个维度,则等于世界空间纹理像素大小的偏移就足够了。通过将剔除球的直径除以图块大小,可以在SetCascadeData中找到纹理像素的大小。将其存储在级联数据向量的Y分量中。
但是,这并不总是满足的,因为纹理像素是正方形。在最坏的情况下,我们最终不得不沿着正方形的对角线偏移,因此让我们按√2进行缩放。
在着色器端,将全局阴影数据的参数添加到GetDirectionalShadowAttenuation。在计算阴影图块空间中的位置之前,将表面法线与偏移量相乘以找到法线偏差并将其添加到世界位置。
在GetDirectionalLight中将多余的数据传递给它。
(法向偏置等于texel大小)
4.4 可配置的偏差
法线偏差已经可以消除暗疮痤疮,而不会引入明显的新瑕疵,但不能消除所有阴影问题。例如,在墙壁下方的地板上可见不应该存在的阴影线。这不是自阴影导致的,而是阴影从墙上刺出来,影响了它下面的地板。添加一点斜率比例偏差可以解决这些问题,但是并没有完美的值。因此,我们将使用其现有的“Bias”滑块为每个光源配置它。将其字段添加到Shadows中的ShadowedDirectionalLight结构中。
可以通过它的shadowBias属性获得灯光的bias。将其添加到ReserveDirectionalShadows中的数据。
并使用它在RenderDirectionalShadows中配置斜率比例偏差。
我们还使用光源现有的“Normal Bias”滑块来调整我们应用的法线偏差。使ReserveDirectionalShadows返回一个Vector3,并将灯光的shadowNormalBias用于新的Z分量。
将新的法线偏差 添加到DirectionalShadowData,并将其应用于“Shadows”中的GetDirectionalShadowAttenuation。
并在Light中的GetDirectionalShadowData中对其进行配置。
现在,我们可以调整每个光源的两个偏差。默认值为0,斜率比例偏差为1,法向偏差为1。如果增加第一个,则可以减少第二个。但是请记住,我们对这些灯光设置的解释与其原始目的有所不同。它们曾经是剪辑空间深度偏差和世界空间收缩法线偏差。因此,当你创建新光源时,除非调整偏差,否则你会得到严重的Peter-Panning。
(都设置为0.6)
4.5 阴影花纹(Shadow Pancaking)
可能导致伪影的另一个潜在问题是Unity应用阴影平移。这个想法是当渲染定向光的阴影投射器时,近平面尽可能地向前移动。这可以提高深度精度,但是这意味着不在摄像机视线范围内的阴影投射器可以终止在近平面的前面,这会导致它们在不应该被投射时被修剪。
(阴影被裁切)
通过在ShadowCasterPassVertex中将顶点位置固定到近平面来解决此问题,可以有效地展平位于近平面前面的阴影投射器,将它们变成粘在近平面上的花纹。我们通过获取剪辑空间Z和W坐标的最大值或定义UNITY_REVERSED_Z时的最小值来做到这一点。要将正确的符号用于W坐标,请乘以UNITY_NEAR_CLIP_VALUE。
(收紧后的阴影)
这完全适用于完全位于近平面两侧的阴影投射器,但由于仅影响其某些顶点,因此与该平面交叉的阴影投射器会变形。对于小三角形而言,这并不明显,但是大三角形最终可能会变形很多,使其弯曲,并经常使它们沉入表面。
(非常长的立方体上,阴影产生了变形)
可以通过将近平面向后拉一点来缓解该问题。这就是灯光的“近平面”滑块的作用。为ShadowedDirectionalLight添加近平面偏移的字段。
然后将灯光的shadowNearPlane属性复制到它。
ComputeDirectionalShadowMatricesAndCullingPrimitives的最后一个参数来应用它,我们仍然将固定值设为零。
(near plane 偏移)
4.6 PCF过滤
到目前为止,我们仅对每个片段采样一次阴影贴图,且使用了硬阴影。阴影比较采样器使用特殊形式的双线性插值,在插值之前执行深度比较。这被称为百分比紧密过滤(percentage closer filtering 简称PCF),因为其中包含四个纹理像素,所以一般指是2×2 PCF过滤器。
但这不是我们过滤阴影贴图的唯一方法。我们也可以使用更大的滤镜,使阴影更柔和,更不易混叠,尽管准确性也较低。让我们添加对2×2、3×3、5×5和7×7过滤的支持。我们不会使用现有的柔和阴影模式来控制每个灯光。相反,我们将使所有定向光源使用相同的滤镜。为此,向ShadowSettings添加一个FilterMode枚举,并将“
Directional ”的过滤器选项默认设置为2×2。
(Filter 设置为PCF 2X2)
我们将为新的过滤器模式创建着色器变体。将具有三个关键字的静态数组添加到Shadows。
创建一个启用或禁用适当关键字的SetKeywords方法。在执行缓冲区之前,请在RenderDirectionalShadows中调用它。
较大的滤镜需要更多纹理样本。为此,我们需要知道着色器中的地图集大小和纹理像素大小。为此数据添加一个着色器标识符。
将尺寸存储在其X分量中,将纹理像素尺寸存储在其Y分量中。
在Lit的CustomLit传递中为三个关键字添加#pragma multi_compile指令,为与2×2过滤器匹配的no-word选项添加加号和下划线。
我们将使用Core RP库的Shadow / ShadowSamplingTent HLSL文件中定义的函数,因此将其包括在Shadows的顶部。如果定义了3×3关键字,则总共需要四个过滤器样本,我们将使用SampleShadow_ComputeSamples_Tent_3x3函数进行设置。我们只需要获取四个样本,因为每个样本都使用双线性2×2滤波器。在所有方向上偏移半个纹理像素的正方形覆盖了3×3像素的帐篷滤镜,其中心的权重大于边缘。
tent filter如何工作?
Bloom教程涵盖了利用双线性纹理采样的滤镜内核,而Depth of Field教程则包含了一个3×3tent filter的示例。
出于同样的原因,我们可以为5×5过滤器提供9个样本,为7×7过滤器提供16个样本,再加上适当命名的函数。
为阴影图块空间位置创建一个新的FilterDirectionalShadow函数。定义DIRECTIONAL_FILTER_SETUP时,需要多次采样,否则只需调用一次SampleDirectionalShadowAtlas就足够了。
过滤器设置功能具有四个参数。首先是float4的大小,前两个分量的X和Y纹素大小,Z和W的总纹理大小。然后是原始样本位置,然后是每个样本的权重和位置的输出参数。两者都定义为实数数组。之后,我们可以遍历所有样本,累积它们的权重进行调制。
在GetDirectionalShadowAttenuation中调用此新函数,而不是直接转到SampleDirectionalShadowAtlas。
(PCF 2x2, 3x3, 5x5, 和 7x7)
增大滤镜大小可使阴影更平滑,但也会导致粉刺再次出现。我们需要增加法向偏置以匹配滤波器尺寸。可以通过将纹理像素大小乘以1加上SetCascadeData中的过滤器模式来自动执行此操作。
除此之外,增加采样区域还意味着我们可以最终在级联的筛选范围之外进行采样。通过对球体的半径在平方之前减小过滤器尺寸,可以避免这种情况。
(具有缩放偏差的PCF 5x5和7x7)
4.7 级联混合
较柔和的阴影看起来更好,但它们也使级联之间的突然过渡更加明显。
(硬级联过渡 PCF 7X7)
通过在级联之间添加过渡区域,将两者融合在一起,可以使过渡不太明显(尽管不是完全隐藏)。我们已经有一个级联衰落因子,可以用于此目的。
首先,将级联混合值添加到Shadows中的ShadowData中,我们将使用它在相邻的级联之间进行插值。
最初将GetShadowData中的blend设置为1,指示选定的层叠处于完整强度。然后,当在循环中找到级联时,总是计算衰落因子。如果我们处在最后一个级联系数,则将其像以前一样加入强度,否则将其用于混合。
现在,在获取第一个阴影值之后,在GetDirectionalShadowAttenuation中检查级联混合是否小于一个。如果是这样,我们就处在过渡区域中,还必须从下一个级联中采样并在两个值之间进行插值。
(软级联转换)
请注意,级联渐隐率不仅适用于每个级联的可见部分,还适用于每个级联的整个半径。因此,请确保比例不会一直扩展到较低的级联。通常,这不是问题,因为你会希望保持过渡区域较小。
4.8 过渡抖动
尽管级联之间的混合看起来更好,但它也使我们必须在混合区域中采样阴影贴图的时间增加了一倍。一种替代方法是始终基于抖动模式从一个级联中采样。这看起来不太好,但是便宜很多,尤其是在使用大型过滤器时。
向“__Directional__ ”添加级联混合模式选项,支持Hard, Soft, Dither 方法。
(级联的融合模式)
为阴影添加soft 和dither级联混合关键字的静态数组。
调整SetKeywords,使其适用于任意关键字数组和索引,然后还设置级联混合关键字。
将所需的多重编译方向添加到CustomLit Pass中。
要执行抖动,我们需要一个抖动浮点值,可以将其添加到Surface。
有多种方法可以在LitPassFragment中生成抖动值。最简单的方法是使用Core RP Library中的InterleavedGradientNoise函数,该函数在给定屏幕空间XY位置的情况下生成旋转的平铺抖动模式。在片段函数中,其等于剪辑空间的XY位置。它还需要使用第二个参数对其进行动画处理,我们不需要该参数,并且可以将其保留为零。
在GetShadowData中设置级联索引之前,请在不使用soft blending 时将 cascade blend 设置为零。这样,整个分支将从这些着色器变体中消除。
当使用抖动混合时,如果我们不在上一个级联中,则当混合值小于抖动值时,跳到下一个级联。
(抖动级联)
抖动混合的可接受程度取决于渲染帧的分辨率。如果使用后效果弄脏了最终结果,则它可能会非常有效,例如,与temporal anti-aliasing和 animated dither pattern 结合使用。
(抖动放大)
4.9 剔除偏差
使用级联阴影贴图的一个缺点是,我们最终对每个光源渲染相同的阴影投射器不止一次。如果可以保证从较小的级联中覆盖某些阴影投射器,则可以尝试从较大的级联中剔除某些阴影投射器。Unity通过将拆分数据的shadowCascadeBlendCullingFactor设置为1来实现这一点。在将其应用到阴影设置之前,请在RenderDirectionalShadows中执行此操作。
(剔除偏差 0和1)
该值是一个因子,用于调制用于执行剔除的先前级联的半径。剔除时Unity是相当保守的,但是我们应该通过级联渐变比率将其降低,以确保过渡区域中的阴影投射器不会被剔除。
5 透明度
我们将通过考虑透明的阴影投射器来结束本教程。裁切,渐变和透明材质都可以接收阴影,就像不透明材质一样,但是目前只有剪辑材质本身会投射正确的阴影。透明对象的行为就像是实心阴影投射器一样。
(裁切和透明度的阴影表现)
5.1 阴影模式
我们可以通过几种方法来修改阴影投射器。由于涉及写入深度缓冲区,因此阴影是二进制的(无论是否存在),但这仍然给我们带来了一定的灵活性。它们可以打开并完全固定,裁减,抖动或完全关闭。可以独立于其他材质属性执行此操作,以支持最大的灵活性。因此,我们为其添加一个单独的_Shadows着色器属性。使用KeywordEnum属性为其创建一个关键字下拉菜单,默认情况下阴影处于打开状态。
(阴影开启)
为这些模式添加一个着色器功能,以替换现有的_CLIPPING功能。我们只需要三个变体,_SHADOWS_CLIP和_SHADOWS_DITHER不使用打开和关闭关键字。
在CustomShaderGUI中为阴影创建一个setter属性。
然后以预设方法适当设置阴影。对于不透明,将启用阴影,对剪辑将启用剪辑,将抖动用于淡入淡出和透明。
5.2 裁切阴影
在ShadowCasterPassFragment中,将_CLIPPED的检查替换为_SHADOWS_CLIP的检查。
现在可以为透明材质提供裁切过的阴影,这可能适用于其表面大部分是完全不透明或透明但需要alpha混合的表面。
(透明且裁切过的阴影)
请注意,裁剪的阴影不如实体阴影稳定,这是因为在视图移动时阴影矩阵会发生变化,导致片段移动一点。这可能会导致阴影贴图的纹理元素突然从裁切过渡到未裁切。
5.3 抖动阴影
抖动阴影的作用与修剪的阴影一样,只是条件不同。在这种情况下,我们从表面Alpha中减去一个抖动值,并基于此值进行裁剪。可以再次使用InterleavedGradientNoise函数。
(抖动阴影)
抖动可用于近似半透明的阴影投射器,但这是一种很粗糙的方法。强抖动阴影看起来会很糟糕,但是当使用较大的PCF滤镜时,它看起来似乎可以接受。
(PCF7X7的抖动)
由于抖动模式是每个纹理像素固定的,因此重叠的半透明阴影投射器不会投射组合的较暗阴影。该效果与大部分不透明的阴影投射器一样强。同样,由于生成的图案有噪声,因此当阴影矩阵发生变化时,它会遭受时间伪像的影响,这会使阴影看起来发抖。只要对象不移动,此方法就可以更好地用于其他具有固定投影的光源类型。对于半透明对象,通常使用剪裁阴影或根本不使用阴影更为实用。
5.4 无阴影
通过调整对象的MeshRenderer组件的“shadow casting”设置,可以关闭每个对象的阴影投射。但是,如果您想为所有使用相同材质的阴影禁用阴影,则是不切实际的,因此我们还将支持针对每种材质禁用阴影。我们通过禁用材质的ShadowCaster Pass来实现。
将SetShadowCasterPass方法添加到CustomShaderGUI,该方法首先检查_Shadows着色器属性是否存在。如果是这样,还通过其hasMixedValue属性检查是否所有选定的材质都设置为相同模式。如果没有模式或混合模式异常终止。否则,通过使用名称和启用状态作为参数,通过对所有材质调用SetShaderPassEnabled来启用或禁用所有材质的ShadowCaster传递。
确保正确设置传递的最简单方法是在通过GUI更改材质时始终调用SetShadowCasterPass。我们可以通过在OnGUI的开始处调用EditorGUI.BeginChangeCheck并在其结尾处调用EditorGUI.EndChangeCheck来实现。后一种方法返回自我们开始检查以来是否有所更改。如果是这样,请设置shadow caster pass。
5.5 不受光阴影投射器
尽管 unlit 的材质不受照明的影响,但你可能希望它们投射阴影。我们可以通过简单地将ShadowCaster通道从Lit复制到Unlit着色器来支持这一点。
(不受光,但是可以投射阴影)
5.6 接受阴影
最后,我们还可以使Lit的表面忽略阴影,这可能对全息图之类的东西或仅出于艺术目的有用。为此,将_RECEIVE_SHADOWS关键字toggle属性添加到Lit。
加上CustomLit传递中随附的着色器功能。
(Receiving shadows)
我们所要做的就是,在定义关键字时,将GetDirectionalShadowAttenuation中的阴影衰减强制为1。
(投影但不接受阴影)
欢迎扫描二维码,查看更多精彩内容。点击 阅读原文 可以跳转原教程。
本文翻译自 Jasper Flick的系列教程
原文地址:
https://catlikecoding.com/unity/tutorials
本文分享自微信公众号 - 壹种念头(OneDay1Idea)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。