光线与各种几何图形求交
在一些基础的光线追踪教程中,光线求交通常发生在世界空间(world space)
但在离线渲染器中(比如PBRT),光线求交发生在几何图形自己的局部空间(local/object/model space)
这是有好处的,比如说简化求交代码,相交表面的normal
和uv
也很容易计算
下面就记录一些几何求交的做法,自身经验有限,如有错误请指出
前提
- 这里采用的是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
- Physically Based Rendering: From Theory to Implementation
- The Ray Tracer Challenge