既然我们知道结合带符号的距离函数的基础知识,您就可以使用它们来创建有趣的东西。 在本教程中,我们将使用它们来渲染二维阴影。 如果您还没有阅读我以前的符号距离字段(SDF)教程,那么我强烈建议您学习它们,从
创建简单形状的
教程开始。
[GIF在重新压缩期间生成了其他工件。]
基本配置
我创建了一个带有房间的简单配置,它使用了先前教程中介绍的技术。 早些时候,我没有提到我为vector2使用了
abs
函数来镜像相对于x和y轴的位置,并且没有反转图形的距离以交换内部和外部。
我们将把上一教程中的文件
2D_SDF.cginc复制到带有着色器的文件夹中,这将在本教程中编写。
Shader "Tutorial/037_2D_SDF_Shadows"{ Properties{ } SubShader{
如果我们仍然使用上一教程中的可视化技术,则该图将如下所示:
简单的阴影
为了创建清晰的阴影,我们在从样品位置到光源位置的整个空间中四处移动。 如果我们在途中找到一个物体,我们就决定应该对像素进行阴影处理,如果我们无障碍地到达源,则说它没有阴影。
我们首先计算光束的基本参数。 我们已经有了光束的起点(我们要渲染的像素的位置)和目标点(光源的位置)。 我们需要一个长度和一个归一化的方向。 可以通过从起点减去起点并将结果标准化来获得方向。 可以通过减去位置并将值传递给
length
方法来获得
length
。
float traceShadow(float2 position, float2 lightPosition){ float direction = normalise(lightPosition - position); float distance = length(lightPosition - position); }
然后,我们循环遍历射线。 我们将在define声明中设置循环的迭代,这将使我们能够在以后配置最大迭代次数,并且还允许编译器通过扩展循环来稍微优化着色器。
在循环中,我们需要现在的位置,因此我们在循环外部将其声明为初始值0。在循环中,我们可以通过将光束提前量乘以光束方向与基本位置相乘来计算样本的位置。 然后,在刚刚计算出的位置对有符号距离函数进行采样。
然后我们检查是否已经到了可以停止周期的地步。 如果带符号的距离函数的场景的距离接近1,则可以假定光束被图形遮挡并返回0。如果光束传播的距离大于与光源的距离,则可以假定到达光源时没有碰撞并返回值1。
如果返回不成功,则需要计算样本的下一个位置。 这是通过增加光束前进场景中的距离来完成的。 原因是场景中的距离为我们提供了距最近图形的距离,因此,如果将此值添加到光束中,我们可能无法发射比最近图形更远甚至更远的光束,这将导致阴影流动。
如果在样品库存完成(循环结束)时我们什么也没碰到并且没有到达光源,我们还需要返回该值。 由于这主要发生在形状旁边,因此在仍将像素视为阴影之前不久,这里我们使用0的返回值。
#define SAMPLES 32 float traceShadows(float2 position, float2 lightPosition){ float2 direction = normalize(lightPosition - position); float lightDistance = length(lightPosition - position); float rayProgress = 0; for(int i=0 ;i<SAMPLES; i++){ float sceneDist = scene(position + direction * rayProgress); if(sceneDist <= 0){ return 0; } if(rayProgress > lightDistance){ return 1; } rayProgress = rayProgress + sceneDist; } return 0; }
要使用此功能,我们在带有像素位置和光源位置的片段功能中调用它。 然后,我们将结果乘以任何颜色,以将其与光源的颜色混合。
我还使用了
第一个教程中有关距离场并带有符号的技术来可视化几何。 然后我只添加了折叠和几何。 在这里,我们只能使用加法运算,而不执行线性插值或类似的操作,因为在没有形状的地方,形状都是黑色的;在没有形状的地方,阴影都是黑色的。
fixed4 frag(v2f i) : SV_TARGET{ float2 position = i.worldPos.xz;
float2 lightPos; sincos(_Time.y, lightPos.x , lightPos.y ); float shadows = traceShadows(position, lightPos); float3 light = shadows * float3(.6, .6, 1); float sceneDistance = scene(position); float distanceChange = fwidth(sceneDistance) * 0.5; float binaryScene = smoothstep(distanceChange, -distanceChange, sceneDistance); float3 geometry = binaryScene * float3(0, 0.3, 0.1); float3 col = geometry + light; return float4(col, 1); }
柔和的阴影
从这些刺眼的阴影过渡到更柔和,更逼真是很容易的。 同时,着色器在计算上不会变得更加昂贵。
首先,我们只需获取绕过的每个样本到最近的场景对象的距离,然后选择最近的一个即可。 然后,在我们以前返回1的位置,可以将距离返回到最接近的数字。 为了使阴影的亮度不会太高并且不会导致产生奇怪的颜色,我们将通过
saturate
方法将其传递,将其限制为从0到1的间隔。在检查光源光束是否已经达到分布之后,我们在当前最接近的图形和下一个图形之间获得最小值。否则,我们可以获取超出光源范围的样本并得到奇怪的伪像。
float traceShadows(float2 position, float2 lightPosition){ float2 direction = normalize(lightPosition - position); float lightDistance = length(lightPosition - position); float rayProgress = 0; float nearest = 9999; for(int i=0 ;i<SAMPLES; i++){ float sceneDist = scene(position + direction * rayProgress); if(sceneDist <= 0){ return 0; } if(rayProgress > lightDistance){ return saturate(nearest); } nearest = min(nearest, sceneDist); rayProgress = rayProgress + sceneDist; } return 0; }
在此之后,我们注意到的第一件事是阴影中奇怪的“牙齿”。 之所以会出现这种情况,是因为从场景到光源的距离小于1。我试图以各种方式抵消这种情况,但是找不到解决方案。 相反,我们可以实现阴影的清晰度。 清晰度将是阴影功能中的另一个参数。 在循环中,我们将场景中的距离乘以清晰度,然后以2的清晰度,阴影的柔和灰色部分将变成一半。 使用锐度时,光源可以距图形至少1的距离除以锐度,否则会出现伪影。 因此,如果您使用20的清晰度,则距离应至少为0.05单位。
float traceShadows(float2 position, float2 lightPosition, float hardness){ float2 direction = normalize(lightPosition - position); float lightDistance = length(lightPosition - position); float rayProgress = 0; float nearest = 9999; for(int i=0 ;i<SAMPLES; i++){ float sceneDist = scene(position + direction * rayProgress); if(sceneDist <= 0){ return 0; } if(rayProgress > lightDistance){ return saturate(nearest); } nearest = min(nearest, hardness * sceneDist); rayProgress = rayProgress + sceneDist; } return 0; }
通过最小化此问题,我们注意到以下几点:即使在不应遮挡的区域中,在墙壁附近仍可见弱化。 另外,阴影的柔和度对于整个阴影似乎是相同的,并且在图形旁边不明显,并且在远离发射阴影的对象时更柔和。
我们将通过将场景中的距离除以光束传播来解决此问题。 由于这一点,我们将在光束开始处将距离分成非常小的数字,也就是说,我们仍将获得较高的值和美丽的清晰阴影。 当我们在射线的后续点中找到最接近射线的点时,最近的点将被较大的数除,从而使阴影更柔和。 由于这并不完全与最短距离有关,因此我们将变量重命名为
shadow
。
我们还将做一个较小的更改:由于我们用rayProgress除以,所以您不应该以0开始(除以零几乎总是一个坏主意)。 首先,您可以选择任何很小的数字。
float traceShadows(float2 position, float2 lightPosition, float hardness){ float2 direction = normalize(lightPosition - position); float lightDistance = length(lightPosition - position); float rayProgress = 0.0001; float shadow = 9999; for(int i=0 ;i<SAMPLES; i++){ float sceneDist = scene(position + direction * rayProgress); if(sceneDist <= 0){ return 0; } if(rayProgress > lightDistance){ return saturate(shadow); } shadow = min(shadow, hardness * sceneDist / rayProgress); rayProgress = rayProgress + sceneDist; } return 0; }
多种光源
在这种简单的单核实现中,获取多个光源的最简单方法是分别计算它们并添加结果。
fixed4 frag(v2f i) : SV_TARGET{ float2 position = i.worldPos.xz; float2 lightPos1 = float2(sin(_Time.y), -1); float shadows1 = traceShadows(position, lightPos1, 20); float3 light1 = shadows1 * float3(.6, .6, 1); float2 lightPos2 = float2(-sin(_Time.y) * 1.75, 1.75); float shadows2 = traceShadows(position, lightPos2, 10); float3 light2 = shadows2 * float3(1, .6, .6); float sceneDistance = scene(position); float distanceChange = fwidth(sceneDistance) * 0.5; float binaryScene = smoothstep(distanceChange, -distanceChange, sceneDistance); float3 geometry = binaryScene * float3(0, 0.3, 0.1); float3 col = geometry + light1 + light2; return float4(col, 1); }
源代码
二维SDF库(未更改,但在此处使用)
二维柔和阴影
Shader "Tutorial/037_2D_SDF_Shadows"{ Properties{ } SubShader{
这只是使用有符号距离字段的众多示例之一。 到目前为止,它们相当繁琐,因为所有形状都必须在着色器中注册或通过着色器属性传递,但是我对如何使它们更方便将来的教程有一些想法。