返回

光线与各种几何图形求交

在一些基础的光线追踪教程中,光线求交通常发生在世界空间(world space)

但在离线渲染器中(比如PBRT),光线求交发生在几何图形自己的局部空间(local/object/model space)

这是有好处的,比如说简化求交代码,相交表面的normaluv也很容易计算

下面就记录一些几何求交的做法,自身经验有限,如有错误请指出

前提

  • 这里采用的是y轴朝上的右手坐标系,-z轴指向屏幕里,x轴在右手
  • 默认光线已经变换到对象的局部空间

使用几何体的变换矩阵和其逆矩阵,求交时将光线变换到对象空间

$$ Ray(t)=o+td (t > 0) $$

$$ Ray_{object}=M^{-1}Ray_{world} $$

矩形(Rectangle)

假设矩形高度为0,躺在xz平面上。控制矩形大小

忽略平行于平面的光线

if(is_zero(ray.direction.y)) return false;

使用高度计算相交点 $$ t=\frac{0 - o_{y}}{d_{y}} $$

const auto t = ray.inv_at<Axis::Y>(0);
const auto [x, y, z] = ray.at(t);

在矩形范围内的点才算相交成功

if(std::abs(x) > length || std::abs(z) > width) return false;

球(Sphere)

假设球心在原点半径控制球的大小

三维空间中球的一般方程 $$ x^{2}+y^{2}+z^{2}=r^{2} $$

将光线代入方程 $$ (o+td)^{2}=r^{2} $$

整理可得关于t的一元二次方程 $$ d^{2}t^{2}+2odt+o^{2}-r^{2}=0\ \begin{align*} & a=d^{2}\ & b=2od\ & c=o^{2}-r^{2} \end{align*} $$

假如光线的方向向量已经归一化,那么 $$ |\overrightarrow{d}|^{2}=1 $$

const auto a = 1;
const auto b = 2 * dot(ray.origin, ray.direction);
const auto c = dot(ray.origin, ray.origin) - radius * radius;

解一元二次方程,没有解则求交失败

const auto result = quadratic(a, b, c);
if(!result) return false;

有解则找到最近的交点

const auto [x1, x2] = result.value();
const auto t = min(x1, x2);

圆柱(Cylinder)

假设圆柱底面在xz平面,底面圆心在原点半径高度控制圆柱大小和长度,phi表示圆柱曲面的完整度

首先忽略y轴,求出相交点是否在圆柱底面

圆柱底面的一般方程 $$ x^{2}+z^{2}=r^{2} $$

将光线代入,同理可得 $$ (d_{x}^{2}+d_{z}^{2})t^{2}+2(o_{x}d_{x}+o_{z}d_{z})t+o_{x}^{2}+o_{z}^{2}-r^{2}=0 $$

日常解一元二次方程

$$ \begin{align*} & a = d_{x}^{2}+d_{z}^{2}\ & b = 2(o_{x}d_{x}+o_{z}d_{z})\ & c = o_{x}^{2}+o_{z}^{2}-r^{2} \end{align*} $$

const auto a = dx * dx + dz * dz;
const auto b = 2 * (ox * dx + oz * dz);
const auto c = ox * ox + oz * oz - radius * radius;

const auto result = quadratic(a, b, c);
if(!result) return false;

计算相交点高度判断是否在范围内,先从最小t开始

auto [x1, x2] = result.value();
if(x1 > x2) std::swap(x1, x2);

auto t = x1;
auto [x, y, z] = ray.at(t);
auto phi = std::atan2(z, x);
if(phi < 0) phi += 2 * PI<f32>;

假如第一个x1不符合条件,需要对x2继续进行判断

if(y < min_height || y > max_height || phi > max_angle)
{
    t = x2;
    auto [x, y, z] = ray.at(t);
    phi = std::atan2(z, x);
    if(phi < 0) phi += 2 * PI<f32>;
    if(y < min_height || y > max_height || phi > max_angle)
        return false;
}

小提示:当圆柱曲面不是封闭的时候,所求相交点有可能在圆柱内部,这时需要反转(flip)法线才能进行正确的着色

顶盖和底盖(Cap)

这时候圆柱求交算是基本完成了,但渲染的时候又发现一个问题,圆柱没有顶盖和底盖,也就是二维曲面并不是封闭的,因为圆柱体是一个退化的二维曲面

这时候相当于加入了两个圆盘(Disk)

如果在之前与圆柱曲面相交了,需要传入t与这次求交所得cap_t判断哪个最近

const auto intersect = [&](f32 cap_t)
{
    if(cap_t >= t || cap_t <= 0) return;

    if(!check_cap(ray, cap_t)) return;

    t = cap_t;
};

我们可以增加一个枚举变量cylinder_shape,来表示需要圆柱的顶面/底面,或者说我全都要也行

if(cylinder_shape & cylinder_top)
    intersect(ray.inv_at<Axis::Y>(max_height));

if(cylinder_shape & cylinder_bottom)
    intersect(ray.inv_at<Axis::Y>(min_height));

圆盘求交也很简单 $$ x^{2}+z^{2} \leq r^{2} $$

bool Cylinder::check_cap(const Ray3f& ray, f32 t) const
{
    const auto x = ray.at<Axis::X>(t);
    const auto z = ray.at<Axis::Z>(t);
    return (x * x + z * z) <= radius * radius;
}

Reference

  1. Physically Based Rendering: From Theory to Implementation
  2. The Ray Tracer Challenge
Licensed under CC BY-NC-SA 4.0
最后更新于 May 15, 2022