Metal入门资料022-阴影效果实现(下)

写在前面:

对Metal技术感兴趣的同学,可以关注我的专题:Metal专辑
也可以关注我个人的简书账号:张芳涛
所有的代码存储的Github地址是:Metal

正文

这一部分我们重点讲解soft shadows(柔光)效果实现,我们在第20部分的基础之上继续开发。我们需要设置一个具有球体,平面和光线的基本场景:

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太空中的法线是a float3,我们需要知道它在光线上的位置。假设光线刚好接触球体的左侧。法线应该是(-1, 0, 0),即指向左侧并远离球体。如果光线稍微移动到该点的右侧,则它位于球体内部(eg. -0.001)。如果光线稍微向左移动,则它在球体外部(eg. 0.001)。如果我们从左边减去我们得到左边的-0.001 - 0.001 = -0.002哪个点,那么这就是我们的x正常的坐标。然后我们重复这个yz。我们使用一个2D名为eps的矢量,因此我们可以根据需要在每种情况下使用所选择的各种坐标值轻松进行矢量调配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);
}
效果图

现在我们有法线,我们可以使用lighting()函数计算场景中每个像素的光照。首先,我们需要lightRay通过归一化光位置和当前光线来了解light()的方向。对于漫射照明,我们需要法线和lightRay两者之间的角度,即两者的点积。对于高光照明,我们需要在表面上进行反射,它们取决于我们正在观察的角度。不同之处在于,在这种情况下,我们首先将光线投射到场景中,从表面反射它,然后我们测量反射光线和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);
效果图

接下来,添加阴影!我们之前学到的shadow()函数方法派上用场了。我们将light(lightDir)的方向标准化,然后我们在distAlongRay沿着光线行进时不断更新:

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);
效果图

让我们在场景中得到一些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变量告诉我们,梁多少更宽阔的是,我们就往现场。细光束意味着锐利的阴影,而宽光束意味着柔和的阴影。我们从小开始,distAlongRay否则此时的表面会遮挡自己。然后,我们沿着射线走,因为我们没有为硬阴影,那么我们得到的距离现场,经过我们减去dist从eps(波束宽度),并把它eps。这给了我们覆盖的光束百分比。如果我们反转它(1 - beam width),我们得到光线中光束的百分比。我们采用这个新值的最小值light当我们沿着光线行进时,保持最黑暗的阴影。然后,我们再次沿着光线移动,并根据行进的距离和缩放比例增加光束宽度k。如果我们过了光明,我们就会突然出现。最后,我们希望避免光的负值,因此我们返回介于0.0和光值之间的最大值。现在让我们调整内核代码以使用新shadow()函数:

可能你已经注意到了,这次我们使用的是白色的灯光,并且使用衰减器(k)来获得各种(中间)光的值。EPS变量告诉我们,随着我们走进这个场景,光束就会变宽。我们从一个小的distAlongRay开始,否则这个时候的光线就会挡住物体自己。然后我们沿着光线行进,就像我们为硬阴影所做的那样,然后我们得到距离。我们从eps(光束宽度)里面减去dist之后再除以eps。这样我们就可以获取到覆盖的光束的百分比。如果我们将它反转(1 - 波束宽度),我们得到光线中光束的百分比。当我们沿着光线行进时,我们采用这个新值和光的最小值来保持最暗的阴影,然后,我们再次沿着光线移动并且增加光束宽度与行进距离成比例并且按k倍的比例缩放。最后还需要哦注意要避免才产生负值,我们需要作如下的工作:

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之内,我们确定我们有一个投射到了物体身上。如果我们没有投射到任何东西,只需将所有颜色都换成灰色,否则确定阴影值。最后,我们只需在场景前添加另一个(固定)光源,就可以更详细地看到阴影。

效果图

代码点击我

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