Build a Simple Path Tracer in TypeScript: From Geometry to Ray Tracing
This article walks through the fundamentals of computer graphics, explaining rasterization, geometry, and ray tracing, then shows how to implement a basic path‑tracing renderer in TypeScript using vectors, rays, spheres, a camera, and Web Workers to render on a canvas.
大厂技术 坚持周更 精选好文
计算机图形学是什么
Rasterization(光栅化)
计算机图形学对于前端来说可能就是 WebGL,光栅化只是其中一部分。它的主要工作是把三维空间的物体投影到二维平面上,这里所有计算都在 CPU 上完成。
将三维空间中的几何形体显示在屏幕上
游戏的首选(实时渲染)
Geometry(几何学)
几何学涉及在计算机中展示几何体,例如贝塞尔曲线可以用四个控制点生成平滑曲线。
Ray tracing(光线追踪)
光线追踪与光栅化是平行关系,都通过三维物体计算得到图像。光栅化使用近似方法,而光线追踪模拟真实的光线传播,效果更真实。
Animation / simulation(动画/模拟)
前端熟悉关键帧动画(CSS keyframes、AE、lottie),也可以使用 react‑spring 的质量弹簧系统实现更平滑的动画。
关键帧动画(keyframes, AE, lottie)
质量弹簧系统(react-spring)
计算机图形学可以归纳为四个步骤:几何、光栅化、动画和模拟。
Why 光线追踪
光栅化不能很好处理全局光照
光栅化只能处理直接光线和一次弹射,无法模拟光的多次反射和折射,导致全局光照效果差。
光栅化很快但是质量低
光栅化速度快,适用于实时渲染(如游戏),但真实感差;光线追踪质量高,适用于离线渲染(如动画电影),但速度慢。
光线追踪特别真实,但是比较慢
光线追踪:离线
光栅化:实时
大约需要 10k 的 CPU 时间去渲染一帧
坐标空间
实现光线追踪需要向量来描述坐标、法线和颜色。
位置 : x, y, z 法线 : 描述顶点朝向
颜色 : rgb 值
下面的代码实现了三维向量类,提供四则运算、点乘、长度等方法。
export default class Vec3 {
constructor(public e0 = 0, public e1 = 0, public e2 = 0) {}
static add(v1, v2) { /* ... */ }
static sub(v1, v2) { /* ... */ }
static mul(v1, v2) { /* ... */ }
static div(v1, v2) { /* ... */ }
static dot(v1, v2) { return v1.e0 * v2.e0 + v1.e1 * v2.e1 + v1.e2 * v2.e2; }
add(v) { return Vec3.add(this, v); }
sub(v) { return Vec3.sub(this, v); }
mul(v) { return Vec3.mul(this, v); }
div(v) { return Vec3.div(this, v); }
unitVec() { return this.div(this.length()); }
squaredLength() { return this.e0**2 + this.e1**2 + this.e2**2; }
length() { return Math.sqrt(this.squaredLength()); }
}光线
关于光线的三个观点
光以直线传播(波粒二象性)
光线交叉不会相互碰撞
光线从光源到眼睛(光路可逆)
光线类
光线由原点(origin)和方向(direction)组成,使用参数 t 可以得到任意时刻的位置。
export default class Ray {
constructor(public origin: Vec3, public direction: Vec3) {}
getPoint(t: number) { return this.origin.add(this.direction.mul(t)); }
reflect(hit) { return new Ray(hit.p, reflect(this.direction.unitVec(), hit.normal)); }
}球
球由中心点和半径定义。
怎样确定一个球
中心点
半径
export default class Sphere {
constructor(public center: Vec3, public radius: number) {}
hit(ray, t_min, t_max) {
const oc = Vec3.sub(ray.origin, this.center);
const a = Vec3.dot(ray.direction, ray.direction);
const b = Vec3.dot(oc, ray.direction) * 2;
const c = Vec3.dot(oc, oc) - this.radius**2;
const discriminant = b**2 - 4 * a * c;
if (discriminant > 0) {
let temp = (-b - Math.sqrt(discriminant)) / (2 * a);
if (temp > t_min && temp < t_max) {
const hit = new HitRecord();
hit.t = temp;
hit.p = ray.getPoint(temp);
hit.normal = hit.p.sub(this.center).div(this.radius);
return [hit, ray.reflect(hit)];
}
temp = (-b + Math.sqrt(discriminant)) / (2 * a);
if (temp > t_min && temp < t_max) {
const hit = new HitRecord();
hit.t = temp;
hit.p = ray.getPoint(temp);
hit.normal = hit.p.sub(this.center).div(this.radius);
return [hit, ray.reflect(hit)];
}
}
return undefined;
}
}碰撞记录
export default class HitRecord {
constructor(public t = 0, public p = new Vec3(0,0,0), public normal = new Vec3(0,0,0)) {}
}场景(HitList)
实现 HitableInterface,遍历所有可击中对象并返回最近的碰撞。
export default class HitList {
constructor(...list) { this.list = list; }
hit(ray, t_min, t_max) {
let closest = t_max, hit = undefined;
for (const obj of this.list) {
const result = obj.hit(ray, t_min, t_max);
if (result && result[0].t < closest) {
closest = result[0].t;
hit = result;
}
}
return hit;
}
}核心算法(路径追踪)
递归追踪光线,最多 50 次反射后返回黑色。
function trace(scene, r, step = 0) {
if (step > 50) return new Vec3(0,0,0);
const hit = scene.hit(r, 0.0000001, Infinity);
if (hit) {
return trace(scene, hit[1], step + 1).mul(0.5);
} else {
const unitDir = r.direction.unitVec();
const t = (unitDir.e1 + 1.0) * 0.5;
return Vec3.add(new Vec3(1,1,1).mul(1 - t), new Vec3(0.3,0.5,1).mul(t));
}
}从头开始渲染
使用 Snowpack 创建 TypeScript 项目,搭建 canvas、Web Worker、任务调度等结构,实现并行渲染。
Canvas 与样式
<div id="app">
<div class="processbar">
<div class="processline" id="processline"></div>
</div>
<canvas id="cv"></canvas>
</div>任务调度(initTasks、performTask)
将像素划分为若干任务,使用 Web Worker 并行计算,每完成一定数量像素后回传更新进度条。
渲染像素(RenderPixel)
export default function RenderPixel(v, width, height) {
const [r,g,b] = color(v.x/width, v.y/height);
v.r = Math.floor(r*255);
v.g = Math.floor(g*255);
v.b = Math.floor(b*255);
v.a = 255;
}颜色计算(color)
function color(_x, _y) {
const [x, y] = [_x, 1 - _y];
const r = camera.getRay(x, y);
const col = trace(world, r);
return [col.e0, col.e1, col.e2];
}相机
export default class Camera {
constructor(public origin, public leftBottom, public horizontal, public vertical) {}
getRay(x, y) {
return new Ray(this.origin, this.leftBottom.add(this.horizontal.mul(x)).add(this.vertical.mul(y)).sub(this.origin));
}
}场景构建
const ball1 = new Sphere(new Vec3(0,0,-1), 0.5);
const ball2 = new Sphere(new Vec3(1,0,-1), 0.5);
const ball3 = new Sphere(new Vec3(-1,0,-1), 0.5);
const earth = new Sphere(new Vec3(0,-100.5,-1), 100);
const world = new HitList(ball1, ball2, ball3, earth);光的反射
function reflect(v, n) { return v.sub(n.mul(Vec3.dot(v, n) * 2)); }优化思考
可以使用包围盒加速光线-物体相交检测、引入材质与纹理、使用随机终止概率降低噪声并通过多次采样去噪。
参考
https://sites.cs.ucsb.edu/~lingqi/teaching/games101.html
https://raytracing.github.io/books/RayTracingInOneWeekend.html
https://zhuanlan.zhihu.com/p/42218384
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
