[MetalKit]30-Raymarching-in-Metal射线行进

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

MetalKit系统文章目录c++


Raymarching射线步进 是一种用在实时图形的快速渲染方法.几何体一般不是传递到渲染器的,而是在着色器中用Signed Distance Fields (SDF) 函数来建立的,这个函数用来描述场景中一个点到物体的一个面之间的最短距离.当点在物体内部时SDF函数返回一个负数.SDFs很是有用,由于它让咱们减小了Ray Tracing射线追踪的采样数.相似于Ray Tracing射线追踪,在Raymarching中咱们也有从观察平面的每一个像素发出的射线,每条射线被用来肯定是否和某个物体相交.git

这两种技术的不一样在于,在射线追踪中是用严格的方程组来肯定相交的,而在Raymarching中相交是估算的.用SDFs咱们能够沿着射线步进直到咱们离某个物体过近.这种方法相比准确肯定相交来讲花费的计算不算多,当场景有不少物体而且光照很复杂时,准确肯定相交代价很大.Raymarching另外一大应用场景是体积渲染(雾,水,云),这些用Ray Tracing射线追踪很差作由于肯定和这些的相交很是困难.github

咱们能够用 Using MetalKit part 10中的playground来继续下去,下面会解释这些明显的改动.让咱们从两个基本构建块开始,这是咱们在内核用到的最小单元:一个射线和一个物体(球体).ide

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;
    }
};
复制代码

由于咱们是从第10部分开始写的,那咱们还要写一个SDF来计算从一个给定的点到球体的距离.与原有函数不一样之处在于,咱们如今的点是沿着射线marching步进的,因此咱们用射线位置来代替:函数

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

咱们须要作的是计算从一个给定点到一个圆(不是球体由于咱们尚未3D化)的距离,像这样:post

float dist(float2 point, float2 center, float radius) {
    return length(point - center) - radius;
}

...
float distToCircle = dist(uv, float2(0.), 0.5);
bool inside = distToCircle < 0.;
output.write(inside ? float4(1.) : float4(0.), gid);
...
复制代码

咱们如今须要有一个射线,并沿着它步进穿过场景,因此用下面几行替换内核中的最后三行:学习

Sphere s = Sphere(float3(0.), 1.);
Ray ray = Ray(float3(0., 0., -3.), normalize(float3(uv, 1.0)));
float3 col = float3(0.);
for (int i=0.; i<100.; i++) {
    float dist = distToSphere(ray, s);
    if (dist < 0.001) {
        col = float3(1.);
        break;
    }
    ray.origin += ray.direction * dist;
}
output.write(float4(col, 1.), gid);
复制代码

让咱们一行一行来看这些代码.咱们首先建立了一个球体和一个射线.注意射线的z值接近于0时,球体看起来更大由于射线离场景更近,相反,当它远离0,球体看上去更小了,缘由很明显-咱们用射线做为了隐性摄像机.下面咱们定义颜色来初始化一个纯黑色.如今raymarching最精华的地方来了!咱们循环必定次数(步数)来确保咱们行进足够细腻.咱们在这里用100,但你能够尝试一个更大数值的步数,来观察渲染图像的质量的改善,固然也会消耗更多的计算资源.在循环里,咱们计算当前位置沿射线到场景的距离,同时也检查咱们是否接触到了场景中的物体,若是接触到了就将其着色为白色并跳出循环,不然就更新射线位置向场景前进一些.动画

注意咱们规范化了射线方向来覆盖边缘状况,例如向量(1,1,1)(屏幕边角)的长度会是sqrt(1 * 1 + 1 * 1 + 1 * 1)即大约1.732.这意味着咱们须要向前移动射线位置大约1.73*dist,也就是大约咱们须要前进距离的两倍,这可能会让咱们由于超过射线交点而错过/穿过物体.为此,咱们规范化了方向,来确保它的长度始终是1.最后,咱们将颜色写入到输出纹理中.若是你如今运行playground,你应该会看到相似的图像:ui

raymarching1.png

如今咱们建立一个函数命名为distToScene,它接收一个射线做为参数,由于咱们如今卷尺的是找到包含多个物体的复杂场景中的最短距离.下一步,咱们移动球体相关的代码到新函数内,只返回到球体的距离(暂时).而后,咱们改变球体位置到(1,1,1),半径0.5,这意味着球体如今在0.5 ... 1.5范围内.这里有个巧妙的花招来作例子:若是咱们在0.0 ... 2.0内重复空间,则球体老是处于内部.下一步,咱们作个射线的本地副本,并对原始值取模.而后咱们用重复的射线代入distToSphere()函数.

float distToScene(Ray r) {
    Sphere s = Sphere(float3(1.), 0.5);
    Ray repeatRay = r;
    repeatRay.origin = fmod(r.origin, 2.0);
    return distToSphere(repeatRay, s);
}
复制代码

经过使用fmod函数,咱们重复空间填满整个屏幕,实际上建立了一个无限数量的球体,每个都带着本身的(重复的)射线.固然,咱们将只看被屏幕的xy坐标以内的那些,然而,z坐标将让咱们看到球体是如何进到无限深度的.在内核中,移除球体代码,将射线移到很远的位置,修改dist来给咱们留出到场景的距离,最后修改最后一行来显示更好看的颜色:

Ray ray = Ray(float3(1000.), normalize(float3(uv, 1.0)));
...
float dist = distToScene(ray);
...
output.write(float4(col * abs((ray.origin - 1000.) / 10.0), 1.), gid);
复制代码

咱们将颜色与射线位置相乘.除以10.0由于场景至关大,射线位置在大部分地方会大于1.0,这会让咱们看到纯白色.咱们用abs()由于屏幕左边的x小于0,它会让咱们看到纯黑色,因此咱们只需镜像上/下和左/右的颜色.最后,咱们偏移射线位置100,以匹配射线起点(摄像机).若是你如今运行playground,你应该会看到相似的图像:

raymarching2.png

下一步,咱们让场景动起来!咱们在part 12中已经看到如何发送uniforms变量好比timeGPU,因此咱们就再也不重复了.

float3 camPos = float3(1000. + sin(time) + 1., 1000. + cos(time) + 1., time);
Ray ray = Ray(camPos, normalize(float3(uv, 1.)));
...
float3 posRelativeToCamera = ray.origin - camPos;
output.write(float4(col * abs((posRelativeToCamera) / 10.0), 1.), gid);
复制代码

咱们添加time到全部三个坐标,但咱们只让xy起伏变化而z保持直线.1.部分只是为了阻止摄像机撞到最近的球体上.要看这份代码的动画效果,我在下面使用一个Shadertoy嵌入式播放器.只要把鼠标悬浮在上面,并单击播放按钮就能看到动画:<译者注:这里不支持嵌入播放器,我用gif代替https://www.shadertoy.com/embed/XtcSDf>

raymarching.mov.gif

感谢 Chris的协助. 源代码source code已发布在Github上.

下次见!

相关文章
相关标签/搜索