[转帖]isometric 斜视角图形引擎介绍
为了帮助理解斜视角图形系统(isometric view),首先我们要复习一下砖块式图形系统(tiled graphics)。砖块图素大多是由一个矩形区域的象素集合所组成的。顾名思
义,砖块式图形系统就象组成地板的瓷砖,一块一块紧密的排列在一起。N个小砖块排列
在一起又组成一个图案。
砖块式图形引擎里,为了渲染一个场景,通常采用数组形式存储着砖块图素。经典的
渲染方法是从左上角开始,向右边绘制,到达最右端后,再从下一行的最左端开始。如此
反复。
斜视角图形系统,可以理解为用一种旋转视角绘制砖块图形场景的系统。X轴沿右下
方向递增,Y 轴沿左下方向递增。
如图:
砖块图形坐标: 斜视角图形坐标:
- X - 0 0
0123456789 / 1 * 1 \
0 ********** Y 2 * * 2 X
| 1 * ** * / 3 * * 3 \
Y 2 * **** * 4 * * 4
| 3 * ** * 5 * * 5
4 * * * * 6
5 ********** * * * * 7
* * * * * 8
* * * * 9
* *
* *
* *
* *
* *
*
因为我们的显示器是方的(废话:),所以在斜视角图形系统中你将看见如下的图形
------------------------
| \ 草地 / |
| \ / |
| \ / | (倾斜视角后的图形)
| \/ 湖泊 |
| 沙漠 \ |
| \ |
------------------------
现在我们已经粗略的解释了什么是斜视角图形系统,组成斜视角图形系统的图素(tile)
存在着 长度(height),宽度(width)和 深度(depth)这几个要素,其中长度和宽度
影响了斜视角图形系统的视角,而深度决定了图素间的层次关系。
作者认为斜视角图形系统采用 2:1 的横纵比是较合理的,我们将在下面的介绍中发现它
的优点。精确的说我采用的是 2.1:1的斜率,我们的宽度选择32个像素,那么长度就等
于 32/2.1=15.23,四舍五入后就成了: 32 X 15。如下图:
1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3
1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2
----------------------------------------------------------------
1| O O O O
2| O O O O O O O O
3| O O O O O O O O O O O O
4| O O O O O O O O O O O O O O O O
5| O O O O O O O O O O O O O O O O O O O O
6| O O O O O O O O O O O O O O O O O O O O O O O O
7| O O O O O O O O O O O O O O O O O O O O O O O O O O O O
8| O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O
9| O O O O O O O O O O O O O O O O O O O O O O O O O O O O
10| O O O O O O O O O O O O O O O O O O O O O O O O
11| O O O O O O O O O O O O O O O O O O O O
12| O O O O O O O O O O O O O O O O
13| O O O O O O O O O O O O
14| O O O O O O O O
15| O O O O
如果你仔细观察了上图,会发现32 X 15尺寸的图素可以很紧密的接合在一起只要向
右移动16个象素,向下移动8个象素就得到了下一个图素的启始位置。因此我们渲染好
的画面如下:
+-----------+
|/\/\ |
|\/\/\ |
|/\/\/\ |
|\/\/\/\ |
|/\/\/\/\ |
|\/\/\/\/\ |
|/\/\/\/\/\ |
+-----------+
这时有人会问了,按什么顺序画tile呢?请再看下面一张我们进行放大,并且加入坐标
的图例:
+-------------------+
| /\ |
| / 0 \ |
|/\ 0 /\ | Note:
| 0 \/ 1 \ |
| 1 /\ 0 /\ | / \
|\/ 1 \/ 2 \ | Y /\ X
| \ 1 /\ 0 /\ | / /\/\ \
| 1 \/ 2 \/ 3 \ | /\/\/\
| 2/ \ 1 /\ 0 / \ | \/\/\/
+-------------------+
我把它正过来,其坐标位置就想这样:
-X-
| 0 1 2 3
Y 1 X X X
| 2 X X X
3 X X X
从图上看,好象从左到右,从上到下的绘制屏幕是顺理成章的事。其实不然,这不是最好
的算法,我们准备采用从左到右,“从上到下”的方法,啊?!有的朋友肯定会说这不是
和上面的方法一样吗,不一样(没看见引号吗 ;)。
我来解释一下,其实从左到右是绝对正确的,而且彼此两列图素之间间隔32个象素,
但是从上到下画的时候请注意:彼此两行图素间隔不是15个象素,而是8个象素,而且
相邻的两行图素的横坐标相差16个象素,比如第0行的图素用0,0开始画起,那么第1
行的图素就应该从16,8开始画,同理第2行的图素就应该是从0,16坐标画。请结合上面
的图例再想想就清楚了 : )
因此我画图的顺序应该是:
+-----------------+
|0 /\ 1 /\ 2 /\|3
| / \/ \/ |
4|/\ 5 /\ 6 /\ 7 | 8
| \/ \/ \/|
|9 /\ 10 /\ 11 /\|12
|\/ \/ \/ |
13|/\14 /\15 /\16 |17
| \/ \/ \/|
|18 /\19 /\20 / |21
+-----------------+
大家也许会注意到:上图中每一行都在左边多画了一个只能看见一半的图素,如果不这做
的话,大家能想象的出来:在屏幕的边缘会出现一条丑陋的锯齿状黑边。多画一个图素就
可以补偿,对绘图速度没有太大的损失,所以我们采用这种办法。
是不是有些了解了? 那么如何在程序中实现这些具体步骤呢? OK, 我们在仔细观察拿
幅有坐标的图例看看:
+-------------------+
| /\ |
| / 0 \ |
|/\ 0 /\ | Note:
| 0 \/ 1 \ |
| 1 /\ 0 /\ | / \
|\/ 1 \/ 2 \ | Y /\ X
| \ 1 /\ 0 /\ | / /\/\ \
| 1 \/ 2 \/ 3 \ | /\/\/\
| 2/ \ 1 /\ 0 / \ | \/\/\/
+-------------------+
会发现:当我们在水平方向移动时,X坐标增加了1,而Y坐标却减少了1.我再来看看垂直方
向移动时坐标又是如何变化的:
\ 0,0
\ MAP CORDS: 1,0 <---- increase x
/ 1,1 <-- increase y
\ 2,1 <---- increase x
/ 2,2 <-- increase y
这和水平方向移动大不相同了,好象我们仅仅需要把X,Y不停的加1,就能实现垂直移动了.
1. 如果是奇数行,我只要X=X+1
2. 如果是偶数行,我只要Y=Y+1
哈哈,就这么简单!现在我们已经学会了如何渲染屏幕和如何跟踪坐标.不难吧 :-)下面我
们要来啃硬骨头了:在程序中实现斜视角图形引擎.
首先选择一个你擅长的数据结构来存储Map( 定长数组,可变数组,链表 ).我们的例子程序
将使用 10x10 的定长数组,并且针对一个区域具有层叠图素的能力,来实现有深度效果的
Map.
举个例子:如果你想在你的Map上实现一堵墙和一堵挂着蜡烛的墙,只要画一个墙和一
支蜡烛,然后在Map上同一个地方定义墙和蜡烛,那么就能得到层叠效果.而且你的蜡烛还
可以和别的物体进行组合.
所以我们的Map上一个"点"数据将定义成如下结构:
struct MAP_STRUCTURE {
char num_tiles; //最多十个图素
char tiles[10]; //每个图素的索引值
char height[10]; //每个图素的深度值
};
我们的Map定义如下:
MAP_STRUCTURE map[10][10]; //10x10的Map
如果我们有以下几种物体在Map上:
0) 草地
1) 墙
2) 高墙
看一下我们的地图
0 1 2 3 4 5 6 7 8 9
0 O O O O O O O O O O
1 O . . . . . . . . O
2 O . . . . . . . . O . = 草地 (0)
3 O . . o o o o . . O o = 墙 (1)
4 O . . o . . o . . O O = 高墙 (2)
5 O . . o . . o . . O
6 O . . o o o o . . O
7 O . . . . . . . . O
8 O . . . . . . . . O
9 O O O O O O O O O O
如何实现草地,墙,高墙呢,首先最容易实现的是"草地",只要画一个16x15的草地图案就可
以,我将用16x50的尺寸来画"墙",那么"高墙"就可以通过我们前面讲的层叠方法.用两堵"
墙"来实现一堵"高墙",第二堵"墙"的高度应该是第一堵"墙"的两倍.
map[0][0].num = 2;
map[0][0].tile[0] = 1;
map[0][0].height[0] = 0;
map[0][0].tile[1] = 1;
map[0][0].height[1] = 50;
...
map[1][1].num = 1;
map[1][1].tile[0] = 0;
map[1][1].height[0] = 0;
...
你可以想象的出来,在我们的绘图代码里,只要循环的绘制每一层的图素就可以了.由于每
个图素都有自己高度和宽度,所以我必须要选择一个绘图的起点,来满足绘制不同尺寸的图
素.本文作者认为最右下角的点坐标是最理想的起点.只要减去图素的高度和宽度就能到对
应屏幕上的起点.
我们可以用C实现:
(注意:这并不是一个"斜视角"的绘制方法,仅仅是为了让大家理解层叠的绘制方法)
for(i=0;i<10;i++) {
for(j=0;j<10;j++) {
for(k=0;k<map[i][j].num;k++) {
tile_to_draw = map[i][j].tile[k];
height_to_draw = map[i][j].tile[k];
width = block_width(tile_to_draw);
height = block_height(tile_to_draw);
block_draw(tile_to_draw,x-width,y-height-height_to_draw);
}
}
}
你不得不从上面的例子里适应这个坐标系统,这样有助于你理解"斜视角"的绘图方法.当我
们从自上而下,从左到右的绘制时当前图素将自动覆盖上一列甚至更远列的图素,所以我们
不需要用到象Z-Buffer那样的方法实现消隐.
为了实现"斜视角"图形系统,我们必须遵循下面的原则:
1. 启始坐标应该从(8,16)开始(该图素只能看见下半部分),在绘制图素前必须记得先减去
图素的高度和宽度
2. 我们用循环方式画每一列,一个 320x200 的屏幕最多能画25列,而实际上我们需要绘
制更多列,为了不裁剪掉上面几列较高的图素.因此我们选择画35列.
3. 在绘制屏幕时,我们必须建立一个坐标系,对应屏幕坐标.首先我们的循环判断是否是奇
数行,如果是则该行右移16个象素,接着在循环绘制该行的图素.最后Y坐标加1,如此循环
反复.在320x200的屏幕上水平横向有12个可视图素,其中有两个只能看见一半.垂直纵向
有13个可视图素.
4. 画图素利用上面我们已经讲过的结构.当右移32个象素时,意味着我们的相对坐标系
X+1,Y-1.完成水平一行的绘制后,Y+8移动到下一列,如果是奇数行则X+1,如果是偶数行则
Y+1,如此循环就得到了整个MAP.当按照上述方法完成你的引擎,你会发现,不管向哪个方向
移动精灵,都是要么"跳"32个象素,要么"跳"16个象素.显示非常不平滑.其实我们只要加
一个小小的改动:
首先我们仍然看看10x10的Map:
-X-
0123456789
| 0 ..........
Y 1 ..........
| 2 ..........
3 ..........
4 ..........
5 ..........
6 ..........
7 ..........
8 ..........
9 ..........
现在,我们把每一个图素定义成16x16的一个相对坐标系:
X0 X1
................ ................
................ ................
................ ................
................ ................
................ ................
................ ................
Y ................ ................
0 ................ ................
................ ................
................ ................
................ ................
................ ................
................ ................
................ ................
................ ................
................ ................
这样一来,Map仍然是10X10个图素,但是我们的精灵可以在(10x16)x(10x16)=(160x160)的
相对坐标里任意移动.我们这种坐标系统叫:精密坐标系统.
下面将要对我们的引擎做一些改动,以适应精密坐标系统
1. 我们定义变量:vx,vy表示160x160的坐标系统
2. vx,xy除以16就得到了实际的图素坐标:mx,my
mx=vx/16;my=vy/16
3. 我们还要定义几个变量:prestep_x,prestep_y,x_off,y_off都表示了偏移量,其中
x_off,y_off表示了相对Map坐标了偏移,prestep_x,prestep_y表示了相对屏幕坐标的偏
移.
其中的转换公式如下:
x_off = vx and 15
y_off = vy and 15
prestep_x = x_off - y_off
prestep_y = (x_off / 2) + (y_off / 2)
(这些函数公式是基于32x15的尺寸)
上面的方法介绍了在精密坐标系统中如何得出相对屏幕的偏移坐标.在原来绘制图素的坐
标上在相应的加上偏移量就可以得到图素偏移后的新位置.
到现在我们还剩最后一个问题了,就是精灵应该什么时候显示在 Map上,如果在所有Map场
景绘制完再显示,精灵会遮挡住某些不应该被遮挡的物体.所以我们只有在绘制Map场景时
把精灵也加进去,这样才能得到正确的画面.
因此我们还要引入图层(Layer)这个概念.有了图层我们就能顺利的解决了绘制的顺
序.
下面我们将定义一个绘制的顺序(针对Map上一个单位):
1. 首先要处理的草地等那些紧帖地面的图素.
2. 其次是是物体,请注意大物体如:高墙虽然有两堵墙组成但是我们把他作为一个物体放
在同一层.
如果你的引擎按这样的顺序的组织,我们将得到正确的视觉效果.并不需要用Zbuffer来处
理.
这样一来我要对原来定义的数据结构做一些调整:
struct MAP_STRUCTURE {
char num_tiles;
char tiles[10];
char height[10];
char layer[10]; // 图层
};
MAP_STRUCTURE map[10][10];
初始化...
map[0][0].layer[0] = 1;
map[0][0].layer[1] = 1;
...
map[1][1].layer[0] = 0;
...
以下是绘图函数的修改:
current_layer = 0;
max_layers = 0;
while(1) {
for(i=0;i<10;i++) {
for(j=0;j<10;j++) {
for(k=0;k<map[i][j].num;k++) {
if(map[i][j].layer == current_layer) {
// draw the tile
}
if(map[i][j].layer > max_layers)
max_layers = map[i][j].layer;
}
}
}
current_layer++;
if(current_layer >= max_layers)
break;
}
最后我们要讨论一下在绘制Map时怎样把精灵也绘制上去.其实我们只要在绘制Map时判
断一下精灵是否在当前Map位置上:
(sprite_x,sprite_y是"精密坐标系")
if(mx == sprite_x / 16 && my == sprite_y / 16) {
然后我们根据精灵在Map的偏移量绘制就可以了:
xo = sprite_x & 15;
yo = sprite_y & 15;
xx = xo - yo;
yy = (xo/2) + (yo/2);
block_draw(sprite_num,screenx-32+xx,screeny-16+yy);