1. 找到每条光线的角度
我们首先找出每条光线投射的角度。角度取决于三点:玩家面向的方向,摄像机的视野,还有正在绘画的列。
var angle = this .fov * (column / this .resolution - 0.5);
var ray = map.cast(player, player.direction + angle, this .range);
|
2. 通过网格跟踪每条光线
接下来,我们要检查每条光线经过的墙。这里的目标是最终得出一个数组,列出了光线离开玩家后经过的每面墙。
从玩家开始,我们找出最接近的横向(stepX)和纵向(stepY)网格坐标线。移到最近的地方然后检查是否有墙(inspect)。一直重复检查直到跟踪完每条线的所有长度。
function ray(origin) {
var stepX = step(sin, cos, origin.x, origin.y);
var stepY = step(cos, sin, origin.y, origin.x, true );
var nextStep = stepX.length2 < stepY.length2
? inspect(stepX, 1, 0, origin.distance, stepX.y)
: inspect(stepY, 0, 1, origin.distance, stepY.x);
if (nextStep.distance > range) return [origin];
return [origin].concat(ray(nextStep));
}
|
寻找网格交点很简单:只需要对 x 向下取整(1,2,3…),然后乘以光线的斜率(rise/run)得出 y。
var dx = run > 0 ? Math.floor(x + 1) - x : Math.ceil(x - 1) - x;
var dy = dx * (rise / run);
|
现在看出了这个算法的亮点没有?我们不用关心地图有多大!只需要关注网格上特定的点——与每帧的点数大致相同。样例中的地图是32×32,而32,000×32,000的地图一样跑得这么快!
3. 绘制一列
跟踪完一条光线后,我们就要画出它在路径上经过的所有墙。
var z = distance * Math.cos(angle);
var wallHeight = this .height * height / z;
|
我们通过墙高度的最大除以 z 来觉得它的高度。越远的墙,就画得越短。
额,
这里用 cos
是怎么回事?如果直接使用原来的距离,就会产生一种超广角的效果(鱼眼镜头)。为什么?想象你正面向一面墙,墙的左右边缘离你的距离比墙中心要远。于是原
本直的墙中心就会膨胀起来了!为了以我们真实所见的效果去渲染墙面,我们通过投射的每条光线一起构建了一个三角形,通过 cos 算出垂直距离。如图:
渲染出来
我们用摄像头对象 Camera 从玩家视角画出地图的每一帧。当我们从左往右扫过屏幕时它会负责渲染每一列。
在绘制墙壁之前,我们先渲染一个天空盒(skybox)——就是一张大的背景图,有星星和地平线,画完墙后我们还会在前景放个武器。
Camera.prototype.render = function (player, map) {
this .drawSky(player.direction, map.skybox, map.light);
this .drawColumns(player, map);
this .drawWeapon(player.weapon, player.paces);
};
|
摄像机最重要的属性是分辨率(resolution)、视野(fov)和射程(range)。
- 分辨率决定了每帧要画多少列,即要投射多少条光线。
- 视野决定了我们能看的宽度,即光线的角度。
- 射程决定了我们能看多远,即光线长度的最大值
|