目录 |
1 一个带有阴影的聚光灯 1.1 阴影贴图 1.2 阴影命令缓冲区 1.3 设置 渲染目标 1.4 配置视图和投影矩阵 1.5 渲染阴影投射器 2 阴影投射器通道 2.1 阴影包含文件 2.2 第二个通道 3 采样阴影贴图 3.1 从世界坐标到阴影坐标 3.2 采样深度 3.3 阴影淡化 4 阴影设置 4.1 阴影贴图大小 4.2 阴影偏差 4.3 阴影强度 4.4 软阴影 5 更多带有阴影的灯光 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制作。
(带有阴影的三个聚光灯)
1 一个带有阴影的聚光灯
阴影非常重要,既可以增加真实感,又可以使对象之间的空间关系更加明显。没有阴影,很难分辨是物体漂浮在表面上还是在表面上。
Rendering 7,Shadows教程说明了阴影如何在Unity的默认渲染管线中工作,但是对于我们的单通道正向渲染器,这种完全相同的方法不起作用。但返回阅读以获取阴影贴图的相关知识仍然很有用。在本教程中,我们将仅限于聚光灯的阴影,因为阴影是最不复杂的。
我们首先要一个支持阴影的光,因此要创建一个包含几个对象和一个聚光灯的场景。平面对象对于接收阴影很有用。所有物体都使用我们的不透明Lit材质。
(单个聚光灯 还没有阴影)
1.1 阴影贴图
有不少处理阴影的方法,但是我们将继续使用阴影贴图的默认方法。这意味着我们将从灯光的角度渲染场景。我们仅对此次渲染的深度信息感兴趣,因为它告诉我们光线在撞击表面之前到达的距离。任何更远的地方都在阴影中。
要使用阴影贴图,我们必须先创建阴影贴图,然后再使用普通相机进行渲染。为了以后能够对阴影贴图进行采样,我们必须渲染为单独的渲染纹理,而不是通常的帧缓冲区。向MyPipeline添加一个RenderTexture字段以保留对阴影贴图纹理的引用。
使用上下文作为参数,创建一个单独的方法来渲染阴影。它要做的第一件事就是控制渲染纹理。我们将通过调用静态RenderTexture.GetTemporary方法来实现。要么创建新的渲染纹理,要么重新使用尚未清理的旧纹理。因为我们很可能在每帧都需要阴影贴图,所以它会一直重复使用。
向RenderTexture.GetTemporary提供地图的宽度和高度,用于深度通道的位数,最后是纹理格式。我们将从512×512的固定大小开始。深度通道将使用16位,因此它是高精度的。在创建阴影贴图时,请使用RenderTextureFormat.Shadowmap格式。
确保将纹理的滤镜模式设置为双线性,并将其环绕模式设置为钳制。
阴影贴图将在常规场景之前渲染,因此在设置常规摄影机之前但在剔除之后在Render中调用RenderShadows。
另外,请确保在提交上下文后释放渲染纹理。如果此时有阴影贴图,则将其传递到RenderTexture.ReleaseTemporary方法并清除我们的字段。
1.2 阴影命令缓冲区
我们将为所有阴影工作使用单独的命令缓冲区,因此我们可以在帧调试器的单独部分中看到阴影和常规渲染。
就像我们进行常规渲染一样,阴影渲染将在BeginSample和EndSample命令之间进行。
1.3 设置 渲染目标
在渲染阴影之前,我们首先要告诉GPU渲染到阴影贴图。一种方便的方法是通过使用命令缓冲区和阴影贴图作为参数调用CoreUtils.SetRenderTarget。从清除贴图开始,请在BeginSample之前调用它,以便不显示帧调试器和额外的嵌套渲染阴影级别。
我们只关心深度通道,因此仅需要清除该通道。通过将ClearFlag.Depth添加为SetRenderTarget的第三个参数来表明这一点。
尽管不是必需的,但我们也可以更精确地了解纹理的负载和存储要求。我们不在乎它的来源,因为无论如何我们都可以清除它,我们可以使用RenderBufferLoadAction.DontCare指出。这使得基于图块的GPU变得更有效率。而且我们需要稍后从纹理中采样,因此需要将其保存在内存中,我们将使用RenderBufferStoreAction.Store进行指示。将它们添加为第三个和第四个参数。
现在,在常规摄影机渲染之前,阴影映射的清除动作会显示在帧调试器中。
(清除阴影贴图)
1.4 配置视图和投影矩阵
我们的想法是从光源的角度进行渲染,这意味着我们将聚光灯当作照相机使用。因此,必须提供适当的视图和投影矩阵。我们可以通过使用灯光索引作为参数,在剔除结果上调用ComputeSpotShadowMatricesAndCullingPrimitives来检索这些矩阵。由于场景中只有一个聚光灯,因此我们只需提供零即可。视图和投影矩阵可通过两个输出参数使用。除此之外,还有第三个ShadowSplitData输出参数。我们不需要它,必须提供输出参数。
一旦有了矩阵,就可以通过调用阴影命令缓冲区上的SetViewProjectionMatrices来设置它们,执行并清除它。
1.5 渲染阴影投射器
有了正确的矩阵,我们可以继续渲染所有的阴影对象。我们通过在上下文上调用DrawShadows来实现。该方法具有DrawShadowsSettings参考参数,我们可以通过构造函数方法创建该参数,该方法将剔除结果和光照索引作为参数。
仅当我们的聚光灯的阴影类型设置为硬或软时,此方法才有效。如果将其设置为none,则Unity将报错,说它不是有效的阴影投射灯。
(灯光开启阴影)
2 阴影投射器通道
此时,受光照影响的所有对象都应渲染到阴影贴图中,但是帧调试器告诉我们这没有发生。这是因为DrawShadows使用ShadowCaster着色器通道,而我们的着色器当前没有这样的通道。
2.1 阴影包含文件
要创建阴影投射器通道,请复制Lit.hlsl文件并将其重命名为ShadowCaster.hlsl。我们只关心深度信息,因此请从新文件中删除与片段位置无关的所有内容。片段程序的输出仅为零。还重命名其传递函数,并包括防护定义。
这足以渲染阴影,但是阴影投射者有可能与附近的地方相交,从而导致阴影中出现孔洞。