22.Approaching the Third Dimension: Raycasting
Let’s think for a moment about how to implement 3D graphics. One 3D graphics technique, ray-tracing, takes inspiration from the physical world. In the physical world, roughly, sources of illumination (the sun, electric lights, etc) shine in straight lines, which bounce around (potentially changing colour as they bounce) and some reach your eye. Simulating graphics this way is horribly inefficient, because you simulate all the light that never even reaches your eye. So ray-tracing inverts this idea, and instead projects virtual rays from your eyes, Superman-style (one ray per pixel on the screen), and bounces them around until it finds a light-source. Ray-tracing produces good quality graphics, but slowly — so it’s used in 3D films (e.g. Pixar’s films), but not in 3D games.
让我思考一会如何去实现3D绘图。一个叫做光线追踪的3D图形技术,受到物质世界的启发而产生。大致来说,在物质世界里,光源(太阳、灯光等)沿着直线照射,并四处反弹(当它们反弹时有可能改变颜色)并被人眼接收。按照这样的方式模拟图形是极其低效的,因为你模拟了所有从不会进入人眼的光线。于是光线追踪颠倒了这种思路,让人眼发射虚拟的光线,像超人一样(在屏幕上每一个像素产生一束光线),直到它遇到光源时进行反弹。光线追踪产生了高质量的图形效果——因此它被应用于3D电影中(比如皮克斯的电影),但是并没有在3D游戏中使用。
There are various ways to greatly increase the speed of this technique. One that we’ll look at in this post is to use virtual scenes where one ray per column of pixels is sufficient: this reduces the number of rays from, say, 800*600 down to just 600. This optimised version is called raycasting, and was used in a few early “3D” games such as Wolfenstein 3D:
有很多途径来显著地提高这种技术的速度。在这篇帖子中我们将看到的一种方式是使用虚拟场景,其中每一列像素产生一束光线就足够了:这降低了光线的数量,比如,从800乘600降低到了仅仅600.这种优化了的版本叫做光线投射,它被应用在一些早期的“3D”游戏中,比如德军司令部3D:
Note how there is only one solid wall in each vertical slice (column) of the image. So if for each column you send a ray to find which wall it hits, you can draw this seemingly 3D scene quite quickly.
注意,图像的每一个垂直面(列)上只有一个实心墙壁。因此如果对于每一列你发送一束光线去找到它撞到了哪些墙面,你便可以非常快速地绘制这种看起来类似3D的场景。
Cubes
立方体
The simplest type of scene that raycasting can be used with is a grid of squares. Lots of games use grids (or close enough for games): Bomberman, Pac-Man, etc. Raycasting in this grid allows you to see the grid from your character’s perspective. Here’s an example world that we’ll be using for our raycasting, as seen from above — the white dot is the player, light grey is empty, and the coloured or black sections are walls:
能够应用光线投射法的最简单的场景类型是网格。许多游戏使用网格(或者为了游戏而足够接近):炸弹人,吃蛋饼,等等。在这样的网格里使用光线投射法将允许你从角色的视角看到网格。这儿是一个例子,我们将用它来展示光线投射法,如同上面所了解的——白色的点是玩家,浅灰色是空地,彩色或黑色的部分是墙壁:
So now we need a method that can take a straight line, and figure out which squares the ray passes through, in order to find the first non-empty square that it will hit. This sounds familiar: we’ve already seen usedour line-drawing algorithm to implementline of sight calculations. The only change is that instead of projecting rays in a circle, we’ll project them in front of us, through an imaginary screen.
于是现在我们需要一个方法,它能够获得一条直线,并且能计算出光线穿过了哪些方格,从而找到它撞到的第一个非空方格。这听起来很熟悉:我们已经知道使用直线绘制算法去实现视线计算。唯一的改变是我们在自己面前发射光线,让它穿过一个假想的屏幕,而不是在一个圆圈中发射光线。
Here is a zoomed in portion of our world. The white square is the one that the player is standing in. Their eye is in the centre of the square, and is looking through a screen in front of them (facing at 45 degrees). We project lines from the eye, through the imaginary screen and see where they hit:
这儿是我们世界的一个放大部分。白色方格代表玩家所处的位置。眼睛在方格的中央,并且透过面前的屏幕看出去(面向45度角)。我们从眼睛发出直线,穿过假想的屏幕,并且看一看它们碰到了哪儿:
So most of the screen will be displaying the green wall, and the red wall will be visible on the far left of the screen. The line that escapes the image ends up hitting a black wall, so a small portion of black wall will be visible.
因此屏幕的大部分会显示绿色的墙面,而红色的墙面将显示在屏幕左边的角落。避开图像的直线将会最终撞击到黑色墙面,于是一小部分黑色的墙面将会可见。
Height
高度
We can use our line-following algorithm to find out which square we’ve hit, and uses that as the colour for the column. But if we just draw that colour from the top to the bottom of the image, we get this:
我们可以使用直线追踪算法去找到我们碰到了哪个方格,并且使用其颜色作为(像素)列的颜色。但是如果我们仅仅是从上到下绘制那种颜色的话,我们将看到:
That doesn’t look 3D! The key addition is to draw the right height in each column, by working out how high the wall is when projected on to the screen. For each ray that’s projected, we know the distance from the eye to the screen (always 0.5 for us), we know the height of the wall compared to where the eye hits (again, always 0.5 for us), so it just remains to use the distance of the wall from the eye (the length of the ray, which we get using Pythagoras) to calculate the height on the screen:
那看起来不是3D的!关键的改进是为每一列绘制正确的高度,这要计算出当墙面投射到屏幕上时它有多高。对于发射处的每一条光线,我们知道眼睛距离屏幕的距离(对我们来说始终是0.5),我们也知道视线所达墙壁的高度(同样,对我们来说始终是0.5),于是只剩下使用人眼与墙面的距离(光线的长度,使用勾股定理)去计算墙面在屏幕上的高度了。
To work out h, we can use the law of similar triangles. The small triangle on the left (with red sides 0.5 and h) is a scale replica of the larger triangle (with purple sides 1.5 and 0.5). When triangles are scale replicas, the ratio of any two sides is the same in each triangle. So:
为了计算出h,我们可以使用相似三角形法则。左边的小三角形(红边0.5和h)是与大三角形成比例的(紫色变1.5和0.5)。当三角形成比例时,在每个三角形里任意两边的比例是相同的。于是:
So in this case:
因此在这个例子里:
More generally, where the distance from the eye to the screen is screenDist, and the distance from the eye to the wall is rayLength:
更一般地,从眼睛到屏幕的距离表示为screenDist,而从眼睛到墙壁的距离表示为rayLength,则:
You can then multiply h(which will be between 0 and 0.5) by the height of the screen to get how high the wall is above the vertical-centre line. And because our eye is exactly halfway up the wall, the wall is the same “height” below the vertical-centre line. Here’s the code:
接下来你可以用屏幕的高度乘以h(值得范围从0到0.5)来得到墙壁在垂直中心线上有多高。同时因为人眼是完全对着墙壁中间的,所以墙面在垂直中心线的下部是相同的“高度”。代码如下:
double distX = r.getHitX() - eyeX; double distY = r.getHitY() - eyeY; double rayLength = Math.sqrt(distX * distX + distY * distY); height = (int)(((0.5 * screenDist) / rayLength) * img.getHeight()); img.drawLine(column, img.getHeight()/2 - height, column, img.getHeight()/2 + height);
We’re drawing on to an image named “img”. When we display this in Greenfoot, our previously block-colour picture now looks 3D:
我们在一个命名为“img”的图像上绘图。当我们在Greenfoot中显示它时,我们之前彩色方块的图片现在看起来有3D效果:
The effect is even more convincing when you can turn around, so goplay the scenario. Left and right turn your view, and the WASD keys move you around the grid (but not necessarily the way you’re facing).
当你转向的时候效果更加明显,因此去玩一下游戏剧本。用左右键旋转你的视角,用WASD键在网格中移动(但不是必定朝着你的方向前进)。