[MetalKit]Shadows in Metal part 2阴影2

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

MetalKit系统文章目录


在本系列的第二部分中,我们将学习soft shadows软阴影.我们将使用在Raymarching in Metal中的playground,因为它已经建立了3D物体.让我们建立一个基本场景,包含一个球体,一个平面,一个灯光和一个射线:

struct Ray {
    float3 origin;
    float3 direction;
    Ray(float3 o, float3 d) {
        origin = o;
        direction = d;
    }
};

struct Sphere {
    float3 center;
    float radius;
    Sphere(float3 c, float r) {
        center = c;
        radius = r;
    }
};

struct Plane {
    float yCoord;
    Plane(float y) {
        yCoord = y;
    }
};

struct Light {
    float3 position;
    Light(float3 pos) {
        position = pos;
    }
};

下一步,我们创建几个distance operation距离运算函数来帮助我们确定元素到场景之间的距离:

float unionOp(float d0, float d1) {
    return min(d0, d1);
}

float differenceOp(float d0, float d1) {//差集
    return max(d0, -d1);
}

float distToSphere(Ray ray, Sphere s) {
    return length(ray.origin - s.center) - s.radius;
}

float distToPlane(Ray ray, Plane plane) {
    return ray.origin.y - plane.yCoord;
}

下一步,我们创建一个distanceToScene()函数,它给出场景中到任意物体的最近距离.我们用这些函数来生成一个形状,它看起来像是一个带有几个洞的空心球体:

float distToScene(Ray r) {
    Plane p = Plane(0.0);
    float d2p = distToPlane(r, p);
    Sphere s1 = Sphere(float3(2.0), 1.9);
    Sphere s2 = Sphere(float3(0.0, 4.0, 0.0), 4.0);
    Sphere s3 = Sphere(float3(0.0, 4.0, 0.0), 3.9);
    Ray repeatRay = r;
    repeatRay.origin = fract(r.origin / 4.0) * 4.0;
    float d2s1 = distToSphere(repeatRay, s1);
    float d2s2 = distToSphere(r, s2);
    float d2s3 = distToSphere(r, s3);
    float dist = differenceOp(d2s2, d2s3);
    dist = differenceOp(dist, d2s1);
    dist = unionOp(d2p, dist);
    return dist;
}

目前我们写的都是旧代码,只是对Raymarching文章中的重构.让我们谈谈normals法线及为什么需要法线.如果我们有一个平板-比如我们的平面-它的法线总是(0,1,0)也就是指向上方.本例中却很繁琐.法线在3D空间是一个float3而且我们需要知道它在射线上的位置.假设射线刚好接触到球体的左侧.法线应是(-1,0,0),就是指向左边并远离球体.如果射线稍稍移动到该点的右边,在球体内部(如-0.001).如果射线稍稍移动到该点的左边,在球体外部(如0.001).如果我们从左边减去左边得到-0.001 - 0.001 = -0.002它指向左边,所以这就是我们法线的x坐标.然后对yz重复同样操作.我们使用一个名为eps2D向量,来让向量调和vector swizzling更容易操作,每次都使用选定的值0.001作为各个坐标值:

float3 getNormal(Ray ray) {
    float2 eps = float2(0.001, 0.0);
    float3 n = float3(distToScene(Ray(ray.origin + eps.xyy, ray.direction)) -
                      distToScene(Ray(ray.origin - eps.xyy, ray.direction)),
                      distToScene(Ray(ray.origin + eps.yxy, ray.direction)) -
                      distToScene(Ray(ray.origin - eps.yxy, ray.direction)),
                      distToScene(Ray(ray.origin + eps.yyx, ray.direction)) -
                      distToScene(Ray(ray.origin - eps.yyx, ray.direction)));
    return normalize(n);
}

最后,我们已经准备好看到图形了.我们再次使用Raymarching代码,放在已经添加了法线的内核函数的末尾,这样我们就可以给每个像素插值出颜色:

kernel void compute(texture2d<float, access::write> output [[texture(0)]],
                    constant float &time [[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;
    uv.y = -uv.y;
    Ray ray = Ray(float3(0., 4., -12), normalize(float3(uv, 1.)));
    float3 col = float3(0.0);
    for (int i=0; i<100; i++) {
        float dist = distToScene(ray);
        if (dist < 0.001) {
            col = float3(1.0);
            break;
        }
        ray.origin += ray.direction * dist;
    }
    float3 n = getNormal(ray);
    output.write(float4(col * n, 1.0), gid);
}

如果你现在运行playground你将看到类似的图像:

shadows_4.png

现在我们有了法线,我们可以用lighting()函数来计算场景中每个像素的光照.首先我们需要知道灯光的方向(lightRay光线),我们用规范化的灯光位置和当前射线来取得灯光方向.对diffuse漫反射光照我们需要知道法线和光线间的角度,也就是两者的点积.对specular高光光照我们需要在表面进行反射,它们依赖于我们寻找的角度.不同之处在于,本例中,我们首先发射一个射线到场景中,从表面反射回来,再测量反射线和lightRay光线间的角度.然后对这个值进行一个高次乘方运算来让它更锐利.最后我们返回混合光线:

float lighting(Ray ray, float3 normal, Light light) {
    float3 lightRay = normalize(light.position - ray.origin);
    float diffuse = max(0.0, dot(normal, lightRay));
    float3 reflectedRay = reflect(ray.direction, normal);
    float specular = max(0.0, dot(reflectedRay, lightRay));
    specular = pow(specular, 200.0);
    return diffuse + specular;
}

在内核函数中用下面几行替换最后一行:

Light light = Light(float3(sin(time) * 10.0, 5.0, cos(time) * 10.0));
float l = lighting(ray, n, light);
output.write(float4(col * l, 1.0), gid);

如果你现在运行playground你将看到类似的图像:

shadows_5.png

下一步,阴影!我们几乎从本系列的第一部分就开始使用shadow()函数到现在,只做过少许修改.我们规范化灯光方向(lightDir),并在步进射线时不断更新disAlongRay:

float shadow(Ray ray, Light light) {
    float3 lightDir = light.position - ray.origin;
    float lightDist = length(lightDir);
    lightDir = normalize(lightDir);
    float distAlongRay = 0.01;
    for (int i=0; i<100; i++) {
        Ray lightRay = Ray(ray.origin + lightDir * distAlongRay, lightDir);
        float dist = distToScene(lightRay);
        if (dist < 0.001) {
            return 0.0;
            break;
        }
        distAlongRay += dist;
        if (distAlongRay > lightDist) { break; }
    }
    return 1.0;
}

用下面几行替换内核函数中的最后一行:

float s = shadow(ray, light);
output.write(float4(col * l * s, 1.0), gid);

如果你现在运行playground你将看到类似的图像:

shadows_6.png

让我们给场景添加点soft shadows软阴影.在现实生活中,离物体越远阴影散布越大.例如,如果地板上有个立方体,在立方体的顶点我们得到清晰的阴影,但离立方体远的地方看起来像一个模糊的阴影.换句话说,我们从地板上的某点出发,向着灯光前进,要么撞到要么错过.硬阴影很简单:我们撞到了什么东西,这个点主在阴影中.软阴影则处于两者之间.用下面几行更新shadow()函数:

float shadow(Ray ray, float k, Light l) {
    float3 lightDir = l.position - ray.origin;
    float lightDist = length(lightDir);
    lightDir = normalize(lightDir);
    float eps = 0.1;
    float distAlongRay = eps * 2.0;
    float light = 1.0;
    for (int i=0; i<100; i++) {
        Ray lightRay = Ray(ray.origin + lightDir * distAlongRay, lightDir);
        float dist = distToScene(lightRay);
        light = min(light, 1.0 - (eps - dist) / eps);
        distAlongRay += dist * 0.5;
        eps += dist * k;
        if (distAlongRay > lightDist) { break; }
    }
    return max(light, 0.0);
}

你会注意到,我们这次从白色(1.0)灯光开始,通过使用一个衰减器(k)来得到不同的(中间的)灯光值.eps变量告诉我们当光线进入场景中时beam波束有多宽.窄波束意味着锐利的阴影,而宽波束意味着软阴影.我们从小distAlongRay到大开始,不然的话该点所在的曲面会投射阴影到自己身上.然后我们像硬阴影中那样沿射线前进,并得到离场景的距离,之后我们从eps(beam width波束宽度)中减掉dist并除以eps.这样给出了波束覆盖的百分比.如果我们颠倒它(1 - beam width)就得到了处于灯光中的百分比.当我们沿着射线前进时,我们取这个新的值和light值中的最小值,来让阴影保持最黑.然后再沿射线前进,并根据行进距离均匀地增加beam width波束宽度,并缩放k倍.如果超过了灯光,就跳出循环.最后,我们想要避免给灯光一个负值,所以我们返回0.0和灯光值之间的最大值.现在让我们用新的shadow()函数来改写内核函数:

float3 col = float3(1.0);
bool hit = false;
for (int i=0; i<200; i++) {
    float dist = distToScene(ray);
    if (dist < 0.001) {
        hit = true;
        break;
    }
    ray.origin += ray.direction * dist;
}
if (!hit) {
    col = float3(0.5);
} else {
    float3 n = getNormal(ray);
    Light light = Light(float3(sin(time) * 10.0, 5.0, cos(time) * 10.0));
    float l = lighting(ray, n, light);
    float s = shadow(ray, 0.3, light);
    col = col * l * s;
}
Light light2 = Light(float3(0.0, 5.0, -15.0));
float3 lightRay = normalize(light2.position - ray.origin);
float fl = max(0.0, dot(getNormal(ray), lightRay) / 2.0);
col = col + fl;
output.write(float4(col, 1.0), gid);

注意我们切换到了默认的白色.然后我们添加一个布尔值叫hit,它来告诉我们碰撞到物体没有.我们限定当我们到场景的距离在0.001之内就是碰撞了,如果我们没有碰到任何东西,则用灰色着色,否则确定阴影的数值.在最后我们只需要在场景前面添加另一个(固定的)光源,就能看到阴影的更多细节.如果你现在运行playground你将看到类似的图像:

shadows_7.png

要看这份代码的动画效果,我在下面使用一个Shadertoy嵌入式播放器.只要把鼠标悬浮在上面,并单击播放按钮就能看到动画:<译者注:简书不支持嵌入播放器,我用gif代替https://www.shadertoy.com/embed/XltSWf>

shadow2.mov.gif

源代码source code已发布在Github上.
下次见!

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

推荐阅读更多精彩内容