本系列文章是对 metalkit.org 上面MetalKit内容的全面翻译和学习.html
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
如今咱们建立一个函数命名为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
函数,咱们重复空间填满整个屏幕,实际上建立了一个无限数量的球体,每个都带着本身的(重复的)射线.固然,咱们将只看被屏幕的x
和y
坐标以内的那些,然而,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,你应该会看到相似的图像:
下一步,咱们让场景动起来!咱们在part 12中已经看到如何发送uniforms变量好比time
到GPU
,因此咱们就再也不重复了.
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
到全部三个坐标,但咱们只让x
和y
起伏变化而z
保持直线.1.
部分只是为了阻止摄像机撞到最近的球体上.要看这份代码的动画效果,我在下面使用一个Shadertoy
嵌入式播放器.只要把鼠标悬浮在上面,并单击播放按钮就能看到动画:<译者注:这里不支持嵌入播放器,我用gif代替https://www.shadertoy.com/embed/XtcSDf>
感谢 Chris的协助. 源代码source code已发布在Github上.
下次见!