[MetalKit]31-Shadows-in-Metal-part-1阴影1

772 阅读5分钟

本系列文章是对 metalkit.org 上面MetalKit内容的全面翻译和学习.

MetalKit系统文章目录


lighting and shadows光照和阴影Computer Graphics计算机图形学中一个相当重要的话题.本文是关于MetalShadow阴影系列文章的第一篇.我们将使用第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() 函数,它给出了到场景中任意物体的最近距离.注意在MSLfmod()函数使用的是trunc()而不是floor(),因为我们还想要使用负值,所以我们需要创建一个自定义的mod运算符,所以我们使用了GLSLmod()的定义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,你会看到类似的图像:

shadows_1.png

要产生阴影,我们需要第一-得到光源距离,第二-得到光源方向,第三-朝着该方向前进直到我们碰到光源或物体.所以让我们在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,你会看到类似的图像:

shadows_2.png

我们已经完成了前两步(灯光位置和方向),所以继续处理第三步-真实的阴影函数:

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,你会看到类似的图像:

shadows_3.png

现在每循环只前进一像素,性能很不好.我们可以通过加速沿射线方向的前进来改善性能.我们并不需要前进那么小的步长.我们可以大步前进只要不跨越我们的物体就行.我们可以安全地向任何方向步进一个到场景的距离而不是一个固定步长,这样我们可以快速路过空白区域!当找到到最近曲面的距离后,我们并不知道曲面的方向,所以实际上我们有了一个和场景中最近部分相交的圆的半径.我们可以追踪射线,它总是会遇到圆的边缘,当圆的半径变成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>

shadow1.mov.gif

这种类型的阴影被称为hard shadows硬阴影.下次我们将学习soft shadows软阴影,它看起来更真实更好看. 源代码source code已发布在Github上.

下次见!