图形学31 Unity 的光源衰减和阴影
Posted 纸境止境
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了图形学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.vertex
或 a.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"
来为混合透明的物体生成阴影。
[图形学] Adaptive Shadow Map
reference: https://www.cs.cornell.edu/~kb/publications/ASM.pdf
概述
阴影贴图提供了一种快速方便的方法来识别场景中的阴影,但是会引入锯齿。本文介绍了自适应阴影贴图(ASM)作为此问题的解决方案。ASM通过解决眼睛视图和光源视图之间的像素大小不匹配来消除锯齿。通过将光源贴图(即光源的阴影图)存储与常规平面结构相反的分层网格结构,可以实现这一目标。在像素从相机视图转换为灯光视图时,可以改进ASM以在需要时创建更高分辨率的阴影图。这是通过评估阴影图像素对整体图像质量的贡献来完成的。
改进过程是视图驱动的、渐进式的,并且仅限于用户指定的内存占用来给你。我们可以证实ASM在保持交互式速率的同时能够明显提高阴影质量。
介绍
阴影提供了有关对象之间空间关系的重要信息。一种常用的阴影生成技术是阴影贴图。阴影图的做法是从光源视图生成深度图像,然后渲染时将相机视图中的点转换到灯光视图,最后将变换后的点的深度与阴影图中的深度进行比较,以确定是否可以从光源处看到变换后的点。如果变形点比阴影贴图中的对应点更靠近灯光,则认为该点处在光照中。否则,认为该点在阴影中。之后,此信息将用于对相机视图的图像进行着色。
阴影贴图的主要缺点之一是锯齿,如图1所示。
在图中,我们在一个简单场景中可以看到两个不同位置的视图:视点A和视点B。每张图片中的网格都显示了阴影的投影像素区域和当前视点所见的场景。由于视点A中光源像素的投影面积大致等于阴影图中的像素面积,因此这种情况下锯齿最轻微。相反,视点B非常接近场景几何体,因此该视图中阴影贴图像素的投影区域较大。这意味着使用来自阴影贴图的相对少量信息覆盖了视点B的大部分区域。结果是出现了非常明显的失真,在这里显示为锯齿状的边缘。
本文中,我们介绍了自适应阴影贴图(ASM),这是对常规阴影贴图的扩展,它解决了阴影贴图锯齿的主要原因:阴影贴图分辨率不足。我们使用分层网格结构,通过在需要时生成更高分辨率的阴影贴图,在视图驱动和渐进式的基础上提高图像质量。通过与最近最少使用(LRU)和基于优先级的内存管理方案相结合,限制该算法的内存使用率。
灯光的位置通常是人为指定的,以确保足够的阴影质量。因为传统的阴影贴图在灯光较远或者视野较广的时候效果比较差。ASM通过设置实际的位置和视野是用户避免此类约束。ASM的视图驱动特性还可以在交互期间生成高质量的阴影,而无需仔细调整常规的阴影图参数,或者使用更大的阴影图。
之前的工作
阴影生成算法的完整概述不在本文的讨论范围之内,引用[6]和[11]一起提供了较为流行的方法的全面概述。在[9]中介绍的常规阴影映射是用于生成阴影的常用技术。由于它是一种图像空间技术,因此在通用性,速度和易于实现方面均具有优势。
但是,消除阴影贴图中的锯齿一致都是难以解决的问题,百分比更近过滤(PCF)是解决混淆问题的方法,但是在锯齿严重的情况下,它只能通过模糊在掩盖锯齿。灯光缓冲区[3]通过使用带有阴影分析测试的平面阴影贴图,解决了非交互式光线跟踪器上下文中的锯齿问题。最近,陡峭深度图[5]通过使用抖动样本并对它们进行预过滤来解决锯齿问题。但是,它们不处理由于阴影贴图分辨率不足引起的锯齿。
自适应阴影贴图
ASM基于以下事实:高质量的阴影贴图不必具有统一的高分辨率。仅需对包含阴影边界的区域进行密集采样。在那些区域中,阴影图的分辨率应至少与相机视图中相应区域一样大,以避免产生锯齿。
ASM通过分层地细分普通阴影贴图,从而在视觉上重要的区域提供更高的分辨率。像传统的阴影贴图一样,ASM将一组变换的视点作为输入,并允许在该视点上进行阴影查询,如果特定点在光照中,则返回true,否则返回false。在软件系统中,ASM可以无缝替换传统的阴影贴图。
ASM具有三个主要特性:
● 它是视图驱动的,这意味着层次的网格结构会根据用户的视点进行更新。
● 它仅限于用户指定的内存限制。内存得到有效管理,ASM避免了具有相同视觉质量的常规阴影贴图所需内存使用的爆炸性增长。
● 它是渐进式的,这意味着一旦建立特定的视点,图像质量就会持续提高,直到达到用户指定的存储限制。
ASM被组织为树。ASM树中的每个节点都具有固定分辨率的阴影图,且阴影图被划分为固定数量的单元。这些单元中的每一个都可以包含另一个树节点。可以对树中每个单元执行两项操作。当确定与该单元相对应的阴影图区域的分辨率不够高而无法提供所需的图像质量时,可以为一个空单元分配一个新节点;包含节点的单元也可以删除该节点以及其后代。这是通过响应用户指定的内存限制来完成此操作。
创建节点
在任何时候,都有许多需要分配新节点的单元。在交互式应用程序中,并非总是能够满族所有这些需求。因此,我们使用成本效益度量来确定要满足的单元。仅当创建新节点(并因此获得更高分辨率的阴影贴图)时,它才会带来阴影质量的明显改善,这才是有益的。
我们通过计算跨深度不连续,且阴影贴图分辨率低于相机视图分辨率的单元内已转换的相机视图像素的数量,来量化此可感知的优势。我们使用图形硬件的mip映射功能来估算匹配相机视图分辨率的阴影贴图。第4.1节对此进行了详细说明。
创建新节点的成本是生成新阴影图所需的时间。使用相机视图像素大小和阴影视图像素大小的比率,新阴影图和相机视图分辨率匹配所需的分辨率为:
其中,N是阴影贴图单元中的像素数。
生成新阴影贴图的成本可以近似估算为:
该成本模型基于以下事实:硬件回读性能随着要回读的缓冲区的大小接近线性变化,随着回读大小变得非常小,效率变得越来越低。我们预处理执行较准测试,以评估给定硬件设置得a(每像素渲染成本)和b(恒定开销)。
新的阴影贴图的好处是可以解决分辨率与相机像素不匹配。一旦为所有预期单元格计算了成本效益比,便会根据该比例对单元格进行排序。为了保持一致的帧频率,仅在此列表中为性价比最高的单元生成的阴影图系欸但,直到超过给定的时间限制为止。
移除节点
由于ASM仅使用固定数量的内存,因此可能需要回收特定节点的内存。为此,我们在最后一帧中未使用的所有节点上使用LRU方案,删除最近最少使用的节点。如果没有这样的节点,意味着所有节点当前都是可见地。在这种情况下,仅当需要创建的新节点比现有节点有更大的收益时,我们才删除现有节点。
实现
这一章节讨论更多的技术细节和优化。
Mip-mapping
为了确定何时出现分辨率不匹配,算法必须计算像素的投影面积(即像素在世界空间中的覆盖面积)。对于交互速度的情况,在软件中执行此计算将过于昂贵,因此我们使用Mip-mapping来近似计算。
传统上使用Mip-mapping[10]是为了避免锯齿以及和纹理映射相关的伪影。当前的图形硬件实现了透视矫正的mip映射,该映射基于要渲染的像素的投影区域,在不同分辨率的纹理之间进行插值。我们使用此功能来快速估算投影像素区域,如下所示:算法将每个mip-map级别的分辨率放置在该级别的所有纹素中,因此,最小级别在每个纹理像素中包含1,第二小的级别包含2,第三小的级别包含4,以此类推。设置纹理坐标,以使得世界空间纹理大小统一。然后,使用此mip映射的纹理绘制每个多边形。回读帧时,每个像素都包含其三线性内插值的mip级别,这是其投影区域的合理近似值。各向异性过滤用于提高近似精度。
作为进一步的优化,mip-map级别仅在alpha通道中编码,而mip-map其余部分为白色。这使得回读可以与下面描述的几何体ID回读同时进行,从而避免了额外的渲染流程。
结合ID和深度对比
常规阴影贴图通过使用具有偏差因子的深度比较来检查变换后的像素是否处于光照中。然而,这种方法在靠近光边缘上的表面显示出为营,并且在具有变化的几何缩放的场景中存在困难。在[4]中提出了使用逐多边形ID来确定可见性,而不是深度比较。在许多情况下,此方法更好,但会导致沿网格边界产生的伪影。因此,我们结合使用每个多边形ID和深度比较来对变换后的像素确定可见性。如果ID测试失败,则使用常规的深度比较可以让我们避免沿多边形边界的伪影。我们的结果表明,尽管偏差的问题仍然存在,但这种简单的修改比仅使用每个多边形的ID或深度更稳定。
优化
可以使用类似于[2]中所描述的深度剔除技术来加速ASM的查询。存储最近查询的叶节点的高速缓存来进一步加速ASM的查找。如使用奔腾系列的SSE/SSE2之类的低级指令优化,可用于加速从相机视图到阴影图的像素重投影。
我们的方案需要随着单元格的完善,频繁地进行渲染和回读。由于在细分过程中为每个网格单元重绘整个场景效率不高,因此我们对层次结构最顶层的每个单元使用视锥剔除。
由于分析图像中所有像素的成本可能很高,因此我们的算法仅对一部分转换后的像素执行成本收益分析。此选择可能会导致收敛到准确解决方案的速度变慢。但是,在我们的实现中,我们发现只分析八分之一的像素即可提供良好的性能,并且不会显著地影响收敛速度。
结果
图2,3和4展示了我们对31000多边形场景进行交互式绘制地结果,其图像分辨率为512x512像素。我们的统计是在NVIDIA GeForce2 Ultrau图形卡的1 GHz Pentium |||上执行的。该场景具有三个不同的对象,可用于测试算法的不同层面。
光源是具有122° FOV的点光源。它放置在房间的天花板上,远离物体。第一个对象是20000多边形的兔子模型,它描述了该算法处理小三角形和多边形ID频繁变化情况的能力。另外两个对象是机器人和带有精细网格的雕塑,它们演示了算法发现和细化不同尺度的复杂阴影细节的能力。
测试中发现,传统的2048x2048像素阴影图(使用16MB存储空间,每个像素深度32位)平均每秒8.5帧,而我们的算法(也使用16MB内存)平均每秒4.9帧。图2和图3演示了ASM所实现的图像质量得到了明显改善。对物体进行特写,等效的常规阴影贴图大小非常大(图2中为65536x65536像素,图3中为524288x524288像素)。实际上,为交互式应用程序创建这样的阴影图是不可行的,因为这样不仅创建时间长,存储需求也很大。
我们的结果还论证了ASM能够适应广泛的视野。由于ASM具有视图驱动的特性,因此此时阴影贴图的大小可能会相对较小而FOV则相对较大。在我们的实验中,ASM的起始分辨率为512x512像素。
图4说明了该算法的内存管理。从左到右,我们显示了使用2048x2048像素的常规阴影贴图,使用8MB内存的ASM和使用16MB内存的ASM生成的图像。两个ASM图像之间的差异较小,但是与左侧的图像相比,两者均显示出图像质量的显著提升。为了突出显示从8MB到16MBASM的图像质量改进,我们放大了图像的每个部分。
ASM每帧大概使用203毫秒,而传统的2048x2048像素阴影映射使用117毫秒实现相同总内存使用量(16MB)。额外的时间花费在成本效益分析(30ms),节点创建(5ms),遍历层次结构进行查询(35ms)以及场景的额外渲染和回读以收集多边形ID信息上(16ms)。
以上是关于图形学31 Unity 的光源衰减和阴影的主要内容,如果未能解决你的问题,请参考以下文章