本系列文章是对 metalkit.org 上面MetalKit内容的全面翻译和学习.c++
今天咱们将学习ambient occlusion环境光遮蔽.咱们将使用Shadows in Metal part 2
的playground代码.首先,让咱们添加一个新的对象类型-矩形盒子:github
struct Box {
float3 center;
float size;
Box(float3 c, float s) {
center = c;
size = s;
}
};
复制代码
下一步,让我为新的结构体再添加一个新的距离函数:函数
float distToBox(Ray r, Box b) {
float3 d = abs(r.origin - b.center) - float3(b.size);
return min(max(d.x, max(d.y, d.z)), 0.0) + length(max(d, 0.0));
}
复制代码
而后,更新咱们的场景:post
float distToScene(Ray r) {
Plane p = Plane(0.0);
float d2p = distToPlane(r, p);
Sphere s1 = Sphere(float3(0.0, 0.5, 0.0), 8.0);
Sphere s2 = Sphere(float3(0.0, 0.5, 0.0), 6.0);
Sphere s3 = Sphere(float3(10., -5., -10.), 15.0);
Box b = Box(float3(1., 1., -4.), 1.);
float dtb = distToBox(r, b);
float d2s1 = distToSphere(r, s1);
float d2s2 = distToSphere(r, s2);
float d2s3 = distToSphere(r, s3);
float dist = differenceOp(d2s1, d2s2);
dist = differenceOp(dist, d2s3);
dist = unionOp(dist, dtb);
dist = unionOp(d2p, dist);
return dist;
}
复制代码
咱们刚才作的是首先绘制一个半径为8
的球体,一个半径为6
的球体,并求出它们的差集.由于它们中心相同,因此小的那个看不到,除非咱们作个横截面.这就是为何咱们用到了第三个球体,大不少并且中心也不一样.咱们再取一次差集,就能看到第一个差集的结果.最后,咱们添加一个盒子,来让它更好看更多样.若是你如今运行playground你将看到相似的图像:学习
下一步,让咱们删除lighting() 和shadow() 函数,由于咱们再也不须要他们了.还有,删除Light结构体和内核中的两个实例.如今让咱们建立一个ambient occlusion环境光遮蔽
的替代函数:动画
float ao(float3 pos, float3 n) {
return n.y * 0.5 + 0.5;
}
复制代码
咱们在灯光中只用到了法线的y
份量,就像有一个正上方的灯光同样.在内核中,建立法线以后(在else
括号中),调用ao()
函数:ui
float o = ao(ray.origin, n);
col = col * o;
复制代码
只有一个基本(正上方)灯光时,没有阴影了.若是你如今运行playground你将看到相似的图像:spa
是时候来点真正的ambient occlusion环境光遮蔽了. Ambient环境光意味着灯光不是来自一个定义好的光源,而是意味着通常的背景光照. * Occlusion遮蔽*意思是多少环境光被阻挡了.咱们在曲面上取一个射线碰撞的点,观察它的周围.若是周围有一个物体,那颜色值阻挡场景中的大部分光源,因此这是一个暗区.若是周围没有东西,那就是亮区.对于处于中间状态的状况,咱们须要精确计算出多少光被阻塞了.介绍一下cone tracing圆锥追踪概念.翻译
cone tracing圆锥追踪
的想法就是在场景中使用一个圆锥体代替射线.若是圆锥与物体相交,咱们不单单能获得一个简单的true/false
的结果.咱们能够获得物体在该点处覆盖了多少圆锥体.可是咱们如何追踪一个圆锥呢?咱们可使用许多球体来作一个圆锥.试着想一下许多球体排成一行,一头小一头大.这就是咱们目前能近似获得的圆锥体.下面是咱们须要步骤:
由于咱们每步都把球体尺寸翻倍,这就意味着咱们只须要几步迭代就能够很快从曲面表面出来.这也给了咱们一个很棒的宽的圆锥.下面是完整的ao()
函数:
float ao(float3 pos, float3 n) {
float eps = 0.01;
pos += n * eps * 2.0;
float occlusion = 0.0;
for (float i=1.0; i<10.0; i++) {
float d = distToScene(Ray(pos, float3(0)));
float coneWidth = 2.0 * eps;
float occlusionAmount = max(coneWidth - d, 0.);
float occlusionFactor = occlusionAmount / coneWidth;
occlusionFactor *= 1.0 - (i / 10.0);
occlusion = max(occlusion, occlusionFactor);
eps *= 2.0;
pos += n * eps;
}
return max(0.0, 1.0 - occlusion);
}
复制代码
让咱们一行一行看看这些代码.首先,咱们定义了eps变量,它包含了圆锥半径和距离曲面的距离.而后,咱们移出去一点来避免咱们碰撞到咱们离开的表面.下一步,咱们定义occlusion遮蔽变量,初始化为nil(场景是彻底被照亮的).而后,咱们进入循环,每次迭代咱们拿到场景距离,将半径加倍以便知道圆锥的多少被遮蔽了,确保排队了灯光的负值,拿到遮蔽数量(比率)乘以圆锥宽度,给远处的遮蔽(能够从迭代次数获取远近)设置一个低的影响因子,保存当前最高的遮蔽值,将eps加倍并沿法线移动一样距离.而后返回一个值,它表明有多少光线到达了这个点.
如今让咱们建立个camera结构体.它须要一个位置.咱们只需储存一个射线来代替摄像机方向.最后rayDivergence给咱们一个因子,表明射线扩散了多少.
struct Camera {
float3 position;
Ray ray = Ray(float3(0), float3(0));
float rayDivergence;
Camera(float3 pos, Ray r, float div) {
position = pos;
ray = r;
rayDivergence = div;
}
};
复制代码
下一步,设置摄像机.须要一个摄像机位置,观察目标/朝向,视场和视图坐标:
Camera setupCam(float3 pos, float3 target, float fov, float2 uv, int x) {
uv *= fov;
float3 cw = normalize(target - pos );
float3 cp = float3(0.0, 1.0, 0.0);
float3 cu = normalize(cross(cw, cp));
float3 cv = normalize(cross(cu, cw));
Ray ray = Ray(pos, normalize(uv.x * cu + uv.y * cv + 0.5 * cw));
Camera cam = Camera(pos, ray, fov / float(x));
return cam;
}
复制代码
如今咱们只须要初始化摄像机.咱们让它环绕场景,朝向中心**(0,0,0)**.添加到内核,放在uv
变量建立后:
float3 camPos = float3(sin(time) * 10., 3., cos(time) * 10.);
Camera cam = setupCam(camPos, float3(0), 1.25, uv, width);
复制代码
而后删除ray变量,用cam.ray替换内核中用到它的地方.若是你如今运行playground你将看到相似的图像:
要看这份代码的动画效果,我在下面使用一个Shadertoy
嵌入式播放器.只要把鼠标悬浮在上面,并单击播放按钮就能看到动画:<译者注:这里不支持嵌入播放器,我用gif代替https://www.shadertoy.com/embed/4ltSWf>
源代码source code已发布在Github上.
下次见!