Re: 0x03. 从零开始的光线追踪实现-多球体着色

目标

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

本节最终效果

先显示两个球

我们先来想像现实场景,假设你桌子有一个有显示器,此时你举起手机录屏,你能很直观认识到手机离你更近,显示器离你更远,你的眼睛就是那个摄像机,它发出的射线,肯定是先到手机,再到显示器。

现在我们代码做得事情就是,球就是"手机",背景(天空)就是"显示器",通过 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);
}
相关推荐
肖永威11 小时前
macOS环境安装/卸载python实践笔记
笔记·python·macos
雨中风华16 小时前
Linux, macOS系统实现远程目录访问(等同于windows平台xFsRedir软件的目录重定向)
linux·windows·macos
有趣的杰克20 小时前
开源|macOS 菜单栏 AI 启动器 GroAsk:⌥Space 一键直达 ChatGPT / Claude / Gemini
人工智能·macos·chatgpt
疯狂敲代码的老刘20 小时前
JDK 1.6到25 全版本网盘合集 (Windows + Mac + Linux)
java·linux·windows·macos·jdk
jxy99981 天前
mac mini 安装java JDK 17
java·开发语言·macos
Figo_Cheung1 天前
Figo关于OpenClaw(MacOS)安装前环境变量设置保姆级教程
macos·性能优化·个人开发
未来侦察班1 天前
一晃13年过去了,苹果的Airdrop依然很坚挺。
macos·ios·苹果vision pro
普通网友2 天前
苹果笔记本(Mac)连接手机完全指南
macos·智能手机
Aftery的博客2 天前
Xcode运行报错:SDK does not contain ‘libarclite‘ at the path
macos·cocoa·xcode
楚轩努力变强2 天前
iOS 自动化环境配置指南 (Appium + WebDriverAgent)
javascript·学习·macos·ios·appium·自动化