7.1 光线追踪:从像素反向射出一条线


7.1 光线追踪:从像素反向射出一条线

本节摘要:光栅化从三角形出发问"覆盖哪些像素",光线追踪反转提问:从每个像素射出一条射线,问"最先撞到什么"。一次求交解决可见性,递归弹射自然得到反射、折射与硬阴影。本节实现射线与球体、三角形的求交,搭建递归追踪的最小骨架,并与光栅化对照各自的本性。像素的旅程在此调转方向——原来出生的路还能倒着走。

学习目标

阅读完本节,你应当能够:

  1. 推导射线与球求交的判别式,判断相交并解出距离
  2. 用 Möller–Trumbore 算法求射线与三角形交点
  3. 描述递归光线追踪的弹射规则(阴影线、反射线、折射线)
  4. 对照光栅化说出两种范式的复杂度与适用场景差异

换一个方向问问题

光沿直线传播、遇面反弹——物理很简单,难的是光的量太大。Turner Whitted 1980 年的洞察:我们只关心最终进入眼睛的光,那就从眼睛反向追踪它——每个像素发出一条射线,找到最先撞到的表面,那里的颜色就是这个像素的颜色。可见性、阴影、反射在一次求交框架下全部统一:

主射线:像素 → 场景,最近交点即看见的表面 阴影射线:交点 → 光源,无阻挡则照亮(5.4 阴影图的暴力真解) 反射射线:交点按反射方向继续射,递归 折射射线:透明物体内部按折射方向继续,同样递归

射线与球:判别式的课堂

射线 P(t) = O + tD(O 起点、D 单位方向、t≥0 距离参数)。与球心 C、半径 r 的球求交:代入球方程 |P−C|² = r²,得到一元二次方程,判别式定生死:

import numpy as np def ray_sphere(O, D, C, r): # 返回最近正根 t(交点距离),无交返回 None oc = np.array(O) - np.array(C) b = 2 * np.dot(D, oc) c = np.dot(oc, oc) - r*r disc = b*b - 4*c # a=|D|²=1(D已单位化) if disc < 0: return None # 判别式为负:射线与球擦肩而过 sq = np.sqrt(disc) for t in ((-b - sq)/2, (-b + sq)/2): # 近根优先(可见性取最近) if t > 1e-4: # epsilon 防自交(阴影痤疮同款) return t return None # 球在射线背后 t = ray_sphere((0,0,0), (0,0,1), (0,0,5), 1.0) print(round(t, 3), ray_sphere((0,0,0), (0,1,0), (0,0,5), 1.0)) # 输出: 4.0 None 正对球心打出 4 米外交点;垂直方向的射线不相遇

射线与三角形:Möller–Trumbore

场景的主体是三角形网格。直接解"射线与平面相交再判点在三角形内"要走两步;Möller–Trumbore 一步到位,本质是 3.1 节叉积判内外的代数重排:

def ray_triangle(O, D, A, B, C): # Möller–Trumbore:一次求交 + 重心坐标免费奉送 e1, e2 = np.array(B)-np.array(A), np.array(C)-np.array(A) p = np.cross(D, e2) det = np.dot(e1, p) if abs(det) < 1e-9: # 射线与三角形平面平行 return None inv = 1 / det s = np.array(O) - np.array(A) u = np.dot(s, p) * inv # 重心坐标 u if u < 0 or u > 1: return None q = np.cross(s, e1) v = np.dot(D, q) * inv # 重心坐标 v if v < 0 or u + v > 1: return None t = np.dot(e2, q) * inv return (t, u, v) if t > 1e-4 else None hit = ray_triangle((0,0,0), (0,0,1), (-1,-1,3), (2,-1,3), (-1,2,3)) print((round(hit[0],3), round(hit[1],3), round(hit[2],3))) # 输出: (3.0, 0.333, 0.333) 距离 3,重心坐标可直接插值属性(老朋友 3.1)

朴素做法对每条射线遍历全部三角形,复杂度 O(像素×三角形),百万面模型上一帧要算百亿次求交。解药是空间加速结构:BVH(层次包围盒)把场景组织成一棵"不碰大盒就不进小盒"的树,单次查询从百万次求交降到几十次——现代光追(含 RTX 硬件)的效率全靠它,构建与更新 BVH 是光追引擎的核心工程。

递归追踪的最小骨架

def trace(O, D, depth=0): # 递归追踪:最近交点 → 局部光照 + 反射/折射递归 hit = nearest_intersection(O, D) # 遍历或 BVH 查询 if hit is None: return SKY_COLOR # 射向天空 P, N, mat = hit color = mat.emission # 自发光打底 for light in lights: L = normalize(light.pos - P) if ray_sphere(P, L, light.pos, light.r) is not None \ and not blocked(P, light.pos): # 阴影射线 color += mat.albedo * max(0, np.dot(N, L)) * light.power if depth < 3 and mat.reflective: # 反射射线递归 R = reflect_ray(D, N) # 5.1 的 reflect color += 0.4 * trace(P, R, depth+1) return color

对照光栅化:光栅化的并行单位是三角形(天然贴合 GPU),光追的并行单位是射线(各自独立、同样可并行但访存发散严重);光栅化擅长海量不透明表面,光追天生擅长镜面、玻璃、精确阴影这类"二次弹射"现象。两者是互补范式而非取代关系——硬件光追时代的主流引擎是混合体:光栅化打主力,光追补反射与阴影。

本节要点回顾

  • 反向提问:从像素射出射线,可见性、阴影、反射统一为求交问题
  • 球求交看判别式:负值擦肩、近根可见、epsilon 防自交
  • Möller–Trumbore:交点与重心坐标一次算清,直通属性插值
  • BVH 是效率命门:树状剔除把百万次求交压到几十次
  • 互补而非取代:光栅化管海量不透明,光追管弹射与镜面

7.2 节把"递归弹射"推广到随机采样所有光路——路径追踪,离线渲染真正的王座。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U