目标
上一节 已经实现一个球显示在窗口中央,这节的目标是显示多个球。
本节最终效果

先显示两个球
我们先来想像现实场景,假设你桌子有一个有显示器,此时你举起手机录屏,你能很直观认识到手机离你更近,显示器离你更远,你的眼睛就是那个摄像机,它发出的射线,肯定是先到手机,再到显示器。
现在我们代码做得事情就是,球就是"手机",背景(天空)就是"显示器",通过 intersect_sphere 我们可以计算出把"显示器"挡住的"手机"。
回到之前的代码,只显示一个球,也就是满足光线跟球相交时,就告诉 fragment shader 这里要应该显示某个颜色
cpp
if (intersect_sphere(ray, sphere) > 0) {
return vec4f(1, 0.76, 0.03, 1);
}
现在我们要显示两个球,所以先弄一个数组。需要注意到,这里用 constant 是因为 MSL(Metal Shading Language)规定 Program scope variable must reside in constant address space (程序作用域的变量,必须放在常量地址空间),总之就是你要是写个函数外的常量,那就用 constant 把它放到常量地址空间去。
cpp
constant u32 OBJECT_COUNT = 2;
constant Sphere scene[OBJECT_COUNT] = {
{ .center = vec3f(0., 0., -1.), .radius = 0.5 },
{ .center = vec3f(0., -100.5, -1.), .radius = 100. },
};
声明结束后,在 fragment shader 函数内循环匹配光线相交。
我们把离咱们最近的值定义为 closest_t,初始值给个 Metal 内置的常量 FLT_MAX,它表示 float 的最大值(因为我们用了 float 类型),然后循环通过调用 intersect_sphere 计算的值 t 去更新 closest_t(因为 intersect_sphere 没匹配到会返回 -1,所以很显然我们要判断 t > 0.,同时要再判断下这个 t 是比已知最近的还要近的值,也就是要满足 t < closest_t)。
cpp
fragment vec4f fragmentFn(Vertex in [[stage_in]], constant Uniforms &uniforms [[buffer(1)]]) {
// ...
let ray = Ray { origin, direction };
var closest_t = FLT_MAX;
for (u32 i = 0; i < OBJECT_COUNT; ++i) {
var t = intersect_sphere(ray, scene[i]);
if (t > 0. && t < closest_t) {
closest_t = t;
}
}
if (closest_t < FLT_MAX) {
return vec4f(1, 0.76, 0.03, 1);
}
return vec4f(sky_color(ray), 1);
}
于是就会显示

改颜色
这里因为我们设置的颜色是相同,所以连在一块根本分不清哪跟哪,所以我们可以让离得近得更亮,离得远的更暗,给原先设置的颜色再乘上一个值,saturate 这个是 MSL 内置的函数,作用是把小于 0 的转成 0,大于 1 的转成 1,在 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 0 , 1 ] [0, 1] </math>[0,1] 范围内的不变,等于讲,大的就乘多一点,小的就乘少一点,符合近得更亮,远得更暗的要求
cpp
fragment vec4f fragmentFn(Vertex in [[stage_in]], constant Uniforms &uniforms [[buffer(1)]]) {
// ...
let ray = Ray { origin, direction };
var closest_t = FLT_MAX;
for (u32 i = 0; i < OBJECT_COUNT; ++i) {
var t = intersect_sphere(ray, scene[i]);
if (t > 0. && t < closest_t) {
closest_t = t;
}
}
if (closest_t < FLT_MAX) {
return vec4f(1, 0.76, 0.03, 1) * saturate(1. - closest_t);
}
return vec4f(sky_color(ray), 1);
}
现在我们能看到这个效果

实现目标效果
其实到这一步,只是换个颜色,为了实现目标效果,我们直接用 closest_t 作为基础值,在它的基础上转成颜色向量
cpp
if (closest_t < FLT_MAX) {
return vec4f(saturate(closest_t) * 0.5);
}
这样就能实现最终效果
最后总结一下代码
cpp
#include <metal_stdlib>
#define let const auto
#define var auto
using namespace metal;
using vec2f = float2;
using vec3f = float3;
using vec4f = float4;
using u8 = uchar;
using i8 = char;
using u16 = ushort;
using i16 = short;
using i32 = int;
using u32 = uint;
using f16 = half;
using f32 = float;
using usize = size_t;
struct VertexIn {
vec2f position;
};
struct Vertex {
vec4f position [[position]];
};
struct Uniforms {
u32 width;
u32 height;
};
struct Ray {
vec3f origin;
vec3f direction;
};
struct Sphere {
vec3f center;
f32 radius;
};
constant u32 OBJECT_COUNT = 2;
constant Sphere scene[OBJECT_COUNT] = {
{ .center = vec3f(0., 0., -1.), .radius = 0.5 },
{ .center = vec3f(0., -100.5, -1.), .radius = 100. },
};
f32 intersect_sphere(const Ray ray, const Sphere sphere) {
let v = ray.origin - sphere.center;
let a = dot(ray.direction, ray.direction);
let b = dot(v, ray.direction);
let c = dot(v, v) - sphere.radius * sphere.radius;
let d = b * b - a * c;
if (d < 0.) {
return -1.;
}
let sqrt_d = sqrt(d);
let recip_a = 1. / a;
let mb = -b;
let t = (mb - sqrt_d) * recip_a;
if (t > 0.) {
return t;
}
return (mb + sqrt_d) * recip_a;
}
vec3f sky_color(Ray ray) {
let a = 0.5 * (normalize(ray.direction).y + 1);
return (1 - a) * vec3f(1) + a * vec3f(0.5, 0.7, 1);
}
vertex Vertex vertexFn(constant VertexIn *vertices [[buffer(0)]], uint vid [[vertex_id]]) {
return Vertex { vec4f(vertices[vid].position, 0, 1) };
}
fragment vec4f fragmentFn(Vertex in [[stage_in]], constant Uniforms &uniforms [[buffer(1)]]) {
let origin = vec3f(0);
let focus_distance = 1.0;
let aspect_ratio = f32(uniforms.width) / f32(uniforms.height);
var uv = in.position.xy / vec2f(f32(uniforms.width - 1), f32(uniforms.height - 1));
uv = (2 * uv - vec2f(1)) * vec2f(aspect_ratio, -1);
let direction = vec3f(uv, -focus_distance);
let ray = Ray { origin, direction };
var closest_t = FLT_MAX;
for (u32 i = 0; i < OBJECT_COUNT; ++i) {
var t = intersect_sphere(ray, scene[i]);
if (t > 0. && t < closest_t) {
closest_t = t;
}
}
if (closest_t < FLT_MAX) {
// return vec4f(1, 0.76, 0.03, 1);
// return vec4f(1, 0.76, 0.03, 1) * saturate(1. - closest_t);
return vec4f(saturate(closest_t) * 0.5);
}
return vec4f(sky_color(ray), 1);
}