[MetalKit]Shadows in Metal part 1阴影1

本系列文章是对 http://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上.
下次见!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容