引言

本文主要讲解光线投射的原理——一种简单的伪3d渲染技术,广泛应用于早期的计算机图形渲染中。

具体来讲,什么是光线投射呢?

Untitled

简单讲就是将线转化成面的投影技术,如上图所示,从视点发出一簇光线最终投射在几条线上,通过光线投射技术的转化,可以得到右图所示的3D投影。更直观的场景如下图所示。

Untitled

Untitled

可以发现,人物在简单的二维平面上下左右移动,可以映射成三维空间的前进、后退、向左向右。

基本原理

光线投射的核心思想是从视点(通常是摄像机的位置)向物体投射光线,判断光线与物体的交点,从而确定该物体的三维特征,比如纹理、颜色和亮度。

更确切地说,我们需要找到光线打在物体上时,此时的光线传播距离,这个距离与三维视图中对应位置的高度有直接关系:光线的传播距离越远,物体在视角中的高度就越小;反之,光线的传播距离越近,物体在视角中的高度就越大。

设光线的传播距离为 ( d ),视角中的墙体高度为 ( h ),那么墙体高度的计算公式通常是:

$$
h = \frac{C}{d}
$$

其中,C 是一个常数,用于调整物体高度的比例。

投射光线

  1. 建立坐标系$(x,y)$
  2. 初始化视点位置 $(x_0,y_0)$和光线的方向$\theta_0$
  3. $\theta_0$的范围为$[-\pi,\pi]$,取逆时针方向为正方向

Untitled

Untitled

1
2
3
4
5
6
7
8
9
10
11
12
13
function castRay(xo,y0,angle) {
const stepSize = 0.1;


let x = x0;
let y = y0;

while (1) {
x += Math.cos(angle) * stepSize;
y += Math.sin(angle) * stepSize;
}

}

确定交点

如何计算光线和物体的交点,也就是光线打到物体上,传播了多少距离?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function castRay(x0,y0,angle) {
const stepSize = 0.1;
let distance = 0;
let hit = false;

let x = x0;
let y = y0;

while (1) {
x += Math.cos(angle) * stepSize;
y += Math.sin(angle) * stepSize;
distance += stepSize;

const mapX = Math.floor(x);
const mapY = Math.floor(y);


if (maze[mapY][mapX] === 1) {
hit = true;
break;
}
}

return { distance, hit };
}

绘制3D视图

Untitled

1
2
3
4
5
6
7
8
9
10
function draw3D(rayAngle) {

const ray = castRay(rayAngle);
const distance = ray.distance;
const wallHeight = FACTOR / distance;

ctx3D.fillStyle = ray.hit ? ray.color : 'rgba(255, 255, 255, 0.6)';
ctx3D.fillRect(i, (canvas3D.height / 2) - wallHeight / 2, 1, wallHeight);
}

边界处理

1. 光线传播距离的边界判断

离散的,所以会出现超出了墙壁,死循环。

Untitled

这时候步长stepSize可以设置小点,但是再怎么小无法避免不出现死循环,所以可以设置一个最远传播距离maxDepth。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
while (distance < maxDepth) {
x += Math.cos(angle) * stepSize;
y += Math.sin(angle) * stepSize;
distance += stepSize;

const mapX = Math.floor(x);
const mapY = Math.floor(y);


if (maze[mapY][mapX] === 1) {
hit = true;
break;
}
}

2. 鱼眼效应矫正

光线传播距离与物体高度成反比。

当我们在屏幕上绘制一个矩形(比如墙体)时,由于光线投射的角度不同,如果不进行校正,绘制出的墙体可能会出现两边比中心更高的效果,这就是所谓的鱼眼效应。

这种视觉上的扭曲是不符合现实的。所以要进行校对。

我们通过计算每个光线的实际距离和视线方向的夹角,用余弦函数校正距离:

1
2
const distance = ray.distance * Math.cos(rayAngle - player.dir * Math.PI / 2);
const wallHeight = (cellSize * 5) / (distance + 0.0001);

这样我们得到的距离是光线在视线方向上的投影距离,从而消除了由于角度不同带来的距离误差。

注意distance + 0.0001,防止分母为0。

实现

CodePen Demo

参考

1. Lode’s Computer Graphics Tutorial

2. Ray-Casting Tutorial For Game Development And Other Purposes