本系列文章是对 http://metalkit.org 上面MetalKit内容的全面翻译和学习.
lighting and shadows光照和阴影是Computer Graphics计算机图形学
中一个相当重要的话题.本文是关于Metal
中Shadow阴影系列文章的第一篇.我们将使用第15部分Using metal part 15
中playground的代码.让我们建立一个基础场景:
float differenceOp(float d0, float d1) {
return max(d0, -d1);
}
float distanceToRect( float2 point, float2 center, float2 size ) {
point -= center;
point = abs(point);
point -= size / 2.;
return max(point.x, point.y);
}
float distanceToScene( float2 point ) {
float d2r1 = distanceToRect( point, float2(0.), float2(0.45, 0.85) );
float2 mod = point - 0.1 * floor(point / 0.1);
float d2r2 = distanceToRect( mod, float2( 0.05 ), float2(0.02, 0.04) );
float diff = differenceOp(d2r1, d2r2);
return diff;
}
我们首先创建differenceOp()函数,它返回两个有符号距离间的差异.这为我们在物体表面雕刻出形状提供了便利.下一步,我们创建distanceToRect()函数,它确定一个给定的点是在四边形内部或外部.在1st
行,我们用给定的中心来偏移当前坐标系.在2nd
行我们得到当前点的对称坐标.在3rd
行我们得到到两边的距离.然后我们创建distanceToScene()函数,它给出了到场景中任意物体的最近距离.注意在MSL
中fmod()
函数使用的是trunc()
而不是floor()
,因为我们还想要使用负值,所以我们需要创建一个自定义的mod运算符,所以我们使用了GLSL
中mod()
的定义x - y * floor(x/y)
.我们需要modulus
运算来绘制大量小三角形,它们彼此距离0.1且互为镜像.最后,我们全这些函数来生成一个形状,它看起来有点像有窗户的高楼:
kernel void compute(texture2d<float, access::write> output [[texture(0)]],
constant float &timer [[buffer(0)]],
uint2 gid [[thread_position_in_grid]])
{
int width = output.get_width();
int height = output.get_height();
float2 uv = float2(gid) / float2(width, height);
uv = uv * 2.0 - 1.0;
float d2scene = distanceToScene(uv);
bool i = d2scene < 0.0;
float4 color = i ? float4( .1, .5, .5, 1. ) : float4( .7, .8, .8, 1. );
output.write(color, gid);
}
如果你现在运行playground,你会看到类似的图像:
要产生阴影,我们需要第一-得到光源距离,第二-得到光源方向,第三-朝着该方向前进直到我们碰到光源或物体.所以让我们在lightPos处创建一个光源,为了有趣我们将让它动起来.我们使用从主机(API
)代码传递过来的,原来的timeruniform参数.然后,我们得到任意给定点到lightPos
的距离,并根据到光源的距离给像素着色-只要不在物体内部.我们想让离光源近的颜色亮,远的颜色暗.我们用max()
函数来避免灯光亮度出现负值.用下面几行代码替换内核中的最后一行:
float2 lightPos = float2(1.3 * sin(timer), 1.3 * cos(timer));
float dist2light = length(lightPos - uv);
color *= max(0.0, 2. - dist2light );
output.write(color, gid);
如果你现在运行playground,你会看到类似的图像:
我们已经完成了前两步(灯光位置和方向),所以继续处理第三步-真实的阴影函数:
float getShadow(float2 point, float2 lightPos) {
float2 lightDir = lightPos - point;
float dist2light = length(lightDir);
for (float i=0.; i < 300.; i++) {
float distAlongRay = dist2light * (i / 300.);
float2 currentPoint = point + lightDir * distAlongRay;
float d2scene = distanceToScene(currentPoint);
if (d2scene <= 0.) { return 0.; }
}
return 1.;
}
让我们一行一行看看代码.我们首先得到从点指向灯光的方向.下一步,我们得出到灯光的距离,这样我们就知道了我们需要沿着灯光射线移动多远.然后,我们用一个循环来将射线分成许多小步.如果步数不够多,可能会跳过去我们的物体,这会导致阴影中出现"破洞".下一步,我们计算出当前沿射线前进了多远,并沿射线前进同样距离来找到空间中的采样点.然后,我们看看我们离平面上的那个点还有多远,并测试我们是否在物体内部.如果在,因为我们在阴影中就返回0,否则射线没有碰到任何物体就返回1.终于快到了观看阴影的时间了!在内核中,用下面几行替换最后一行:
float shadow = getShadow(uv, lightPos);
shadow = shadow * 0.5 + 0.5;
color *= shadow;
output.write(color, gid);
我们用0.5来衰减阴影效果,当然,也可以设置为其它值试试效果.如果你现在运行playground,你会看到类似的图像:
现在每循环只前进一像素,性能很不好.我们可以通过加速沿射线方向的前进来改善性能.我们并不需要前进那么小的步长.我们可以大步前进只要不跨越我们的物体就行.我们可以安全地向任何方向
步进一个到场景的距离而不是一个固定步长,这样我们可以快速路过空白区域!当找到到最近曲面的距离后,我们并不知道曲面的方向,所以实际上我们有了一个和场景中最近部分相交的圆的半径.我们可以追踪射线,它总是会遇到圆的边缘,当圆的半径变成0时就意味着它是和曲面的相交点.对了,这就是我们上次学习的raymarching技术!只需简单地用下面几行替换getShadow()函数中的内容:
float2 lightDir = normalize(lightPos - point);
float dist2light = length(lightDir);
float distAlongRay = 0.0;
for (float i=0.0; i < 80.; i++) {
float2 currentPoint = point + lightDir * distAlongRay;
float d2scene = distanceToScene(currentPoint);
if (d2scene <= 0.001) { return 0.0; }
distAlongRay += d2scene;
if (distAlongRay > dist2light) { break; }
}
return 1.;
在raymarching
中步长取决于到曲面的距离.在空白区域,它跳过一大段距离,可以跑得更长.但是,如果平行于物体并离得很近,距离就会很小,跳过的长度也很小.这就意味着射线跑得很慢.当使用固定步长时,它跑不远.用80或更多步,我就应该主可以不产生阴影中的"破洞"了.如果你再运行playground,图像看上去几乎没变,但阴影现在更快了.要看这份代码的动画效果,我在下面使用一个Shadertoy
嵌入式播放器.只要把鼠标悬浮在上面,并单击播放按钮就能看到动画:<译者注:简书不支持嵌入播放器,我用gif代替https://www.shadertoy.com/embed/lt3SzB>
这种类型的阴影被称为hard shadows硬阴影
.下次我们将学习soft shadows软阴影
,它看起来更真实更好看.
源代码source code已发布在Github上.
下次见!