首页 > 代码库 > 265行 JavaScript 代码实现第一人称引擎
265行 JavaScript 代码实现第一人称引擎
原文:A first-person engine in 265 lines(2014-6-11)
翻译:Jaward华仔
今天,让我们进入一个可以伸手触摸的世界吧。在这篇文章里,我们将从零开始快速完成一次第一人称探索。本文没有涉及复杂的数学计算,只用到了光线投射技术。你可能已经见识过这种技术了,比如《上古卷轴2 : 匕首雨》、《毁灭公爵3D》还有 Notch Persson 最近在 ludum dare 上的参赛作品。Notch 认为它够好,我就认为它够好! 【Demo (arrow keys / touch)】【Source】
用了光线投射就像开挂一样,作为一名懒得出油的程序员,我表示非常喜欢。你可以舒畅地浸入到3D环境中而不受“真3D”复杂性的束缚。举例来说,光线投射算法消耗线性时间,所以不用优化也可以加载一个巨大的世界,它执行的速度跟小型世界一样快。水平面被定义成简单的网格而不是多边形网面树,所以即使没有 3D 建模基础或数学博士学位也可以直接投入进去学习。
利用这些技巧很容易就可以做一些让人嗨爆的事情。15分钟之后,你会到处拍下你办公室的墙壁,然后检查你的 HR 文档看有没有规则禁止“工作场所枪战建模”。
玩家
我们从何处投射光线?这就是玩家对象(Palyer)的作用,只需要三个属性 x,y,direction。
function Player(x, y, direction) {
this.x = x;
this.y = y;
this.direction = direction;
}
地图
我们将地图存作简单的二维数组。数组中,0代表没墙,1代表有墙。你还可以做得更复杂些,比如给墙设任意高度,或者将多个墙数据的“故事(stories)”打包进数组。但作为我们的第一次尝试,用0-1就足够了。
function Map(size) {
this.size = size;
this.wallGrid = new Uint8Array(size * size);
}
投射一束光线
这里就是窍门:光线投射引擎不会一次性绘制出整个场景。相反,它把场景分成独立的列然后一条一条地渲染。每一列都代表从玩家特定角度投射出的一条光线。如果光线碰到墙壁,引擎会计算玩家到墙的距离然后在该列中画出一个矩形。矩形的高度取决于光线的长度——越远则越短。
绘画的光线越多,显示效果就会越平滑。
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.绘制一列
跟踪完一条光线后,我们就要画出它在路径上经过的所有墙。
【阅读全文】