本系列文章是对 metalkit.org 上面MetalKit内容的全面翻译和学习.c++
lighting and shadows光照和阴影是Computer Graphics计算机图形学
中一个至关重要的话题.本文是关于Metal
中Shadow阴影系列文章的第一篇.咱们将使用第15部分Using metal part 15
中playground的代码.让咱们创建一个基础场景:github
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()
函数来避免灯光亮度出现负值.用下面几行代码替换内核中的最后一行:post
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上.
下次见!