百科中有这样一行文字:
“游戏引擎 The Lightweight Java Game Library(LWJGL),基于OpenGL”
我搜索一番却没有什么收获。
以下是我自己的思考。
假设屏幕后面的世界中有一个方块,从眼睛(以下称“视点”)向某一个顶点作一条射线,那么射线与屏幕的交点就是该顶点应该在屏幕上显示的位置,我们要做的就是由该顶点的世界坐标得到它在屏幕上的xy坐标。一个明显的事实是,一条射线经过的任何顶点在屏幕上对应的位置相同。所以,我们可以由此得到启发,通过这条射线把顶点坐标转换成屏幕坐标。
过程如下:
世界坐标(x,y,z)
视点相对坐标(x-x0,y-y0,z-z0)
视点相对球坐标(r,s,d)
视线球坐标(r-r0,s-s0,d)
屏幕坐标(x,y)
为了简便,在计算时首先以视点为原点建立空间直角坐标系,将顶点坐标转换为与视点的相对坐标,同样地,射线的始端就位于原点。射线没有长度,我们最关心的是它的方向,这需要两个量来表示。一个简单的例子是,我们玩3D游戏(这里指Minecraft)时,为了移动视角,可以将鼠标上下左右移动,对应抬头、低头、左转、右转,同时视线的方向随之变动,我们就可以看向天球上任意一点。这表明,在空间中两个量可以确定射线的方向————方位角、仰角。那么我们就从基础做起吧——一个立方体线框的xuanzhua
如图,我们以这个方块右下角的顶点为例。首先以正方形中心为坐标原点建立极坐标系,依照定义,以x轴正半轴为始边,逆时针旋转为正角,顺时针旋转为负角,这就是这个方块的自身坐标系。绿线与水平蓝线的夹角即为方位角,利用三角函数求得xz坐标,然后加上正方形中心的坐标转换为世界坐标。
以下给出球坐标的计算公式,其中距离d是用来判断前后遮盖的,本程序用不到。
tanr=x/z
tans=y/sqrt(x*x+z*z)
d=sqrt(x*x+y*y+z*z)
由于本程序没有旋转视角的功能,所以略去转换玩家相对坐标和视线相对球坐标的过程,即视点位于世界原点,视线与z轴重合。
当最终得到方块顶点相对于视线的球坐标后,我们就要确定这一点在屏幕上的位置。首先,我们的视线必定垂直穿过屏幕的中心,视点与屏幕四角的连线形成了一个锥体。所有位于这个锥体中的顶点都会映射在屏幕上一个确定的位置,即视线与该顶点的连线交于屏幕上的那一点。
如果屏幕是弧形的,图形渲染就会变得十分简单,因为屏幕中图像随视角变化的变化是均匀的。但我们的屏幕是平直的,所以我们还需要进一步处理。大家玩Minecraft时应该会注意到,屏幕四角的图形好像被拉伸了一样,我们可以画一个图来解释这一现象。首先画一个正方形和它的内切圆,从圆心向与你相对的那条边作垂线,然后间隔相同的角度向两侧作射线,你会发现它们在正方形边上截得的线段由中间向两侧是逐渐变长的。
公式:x=tanr*x0/2*tan(S/2)
推导过程:设视点到屏幕的距离为d,视角为S,屏幕宽度为x0,视射线与屏幕的交点到屏幕中心的距离为x,视射线与中心视线的夹角为r
tan(S/2)=(1/2*x0)/d=x0/2*d
tanr=x/d
d=x0/2*tan(S/2)=x/tanr
x=tanr*x0/2*tan(S/2)
可见,此公式是假定屏幕中心为坐标原点,因此计算结果需要加上屏幕宽度或高度的一半。但是屏幕坐标系的y轴的正方向是向下的,所以需要把顶点的屏幕y坐标乘以-1,于是我干脆把顶点的世界y坐标就以其相反数赋值。
坐标转换的过程到此解释完毕,下面开始动手实践。
#include<stdio.h>
#include<math.h>
int main(void){
int i,j;
double xa,za,r=0;
struct POINT {
double x,y,z;
int xs,ys;
};
struct POINT table[8];
for(i=0;i<4;i++){
table[i].y=20;
}
for(i=4;i<8;i++){
table[i].y=40;
}
FILE *fp=NULL;
fp=fopen("block.txt","w");
if(fp==NULL){
return -1;
}
for(j=0;j<360;j++){
xa=14.1421356*cos(r); // 14.1421356=20/sqrt(2)=10*sqrt(2)
za=14.1421356*sin(r);
table[0].x=xa;
table[0].z=80+za;
table[1].x=-za;
table[1].z=80+xa;
table[2].x=-xa;
table[2].z=80-za;
table[3].x=za;
table[3].z=80-xa;
table[4].x=table[0].x;
table[4].z=table[0].z;
table[5].x=table[1].x;
table[5].z=table[1].z;
table[6].x=table[2].x;
table[6].z=table[2].z;
table[7].x=table[3].x;
table[7].z=table[3].z;
for(i=0;i<8;i++){
table[i].xs=(int)(table[i].x*482/table[i].z);
table[i].ys=(int)(table[i].y/sqrt(table[i].x*table[i].x+table[i].z*table[i].z)*340);
fprintf(fp,"%d,%d,",table[i].xs+200,table[i].ys+100);
// fprintf(fp,"xa=%f,za=%f,r=%f,i=%d,xs=%d,ys=%d\n",xa,za,r,i,table[i].xs,table[i].ys);
}
r+=0.0174532; // 0.0174532=2pi/360
}
fclose(fp);
fp=NULL;
return 0;
}
由于我现在只会写字符模式程序,对显示图形束手无策,所以我使用haribote操作系统(见于《30天自制操作系统》)的API来完成图形显示。以下程序中用block.txt的内容为数组p赋值。
#include "apilib.h"
int p[360*16]={};
void HariMain(void){
int win,buf,timer,i=0;
api_initmalloc();
buf = api_malloc(150 * 50);
win = api_openwin(buf, 512, 384, -1, "block");
api_boxfilwin(win+1, 5, 24, 506, 378, 0);
timer = api_alloctimer();
api_inittimer(timer, 128);
for (;;) {
api_boxfilwin(win+1, 5, 24, 506, 378, 0);
api_linewin(win+1,p[i],p[i+1],p[i+2],p[i+3],3);
api_linewin(win+1,p[i+2],p[i+3],p[i+4],p[i+5],4);
api_linewin(win+1,p[i+4],p[i+5],p[i+6],p[i+7],5);
api_linewin(win+1,p[i+6],p[i+7],p[i],p[i+1],6);
api_linewin(win+1,p[i+8],p[i+9],p[i+10],p[i+11],7);
api_linewin(win+1,p[i+10],p[i+11],p[i+12],p[i+13],8);
api_linewin(win+1,p[i+12],p[i+13],p[i+14],p[i+15],9);
api_linewin(win+1,p[i+14],p[i+15],p[i+8],p[i+9],10);
api_linewin(win+1,p[i],p[i+1],p[i+8],p[i+9],11);
api_linewin(win+1,p[i+2],p[i+3],p[i+10],p[i+11],12);
api_linewin(win+1,p[i+4],p[i+5],p[i+12],p[i+13],13);
api_linewin(win+1,p[i+6],p[i+7],p[i+14],p[i+15],14);
api_refreshwin(win, 5, 24, 506, 378);
api_settimer(timer, 1);
i+=16;
if(i>=360*16){
i-=360*16;
}
if (api_getkey(1) != 128) {
api_end();
}
}
}
下一步是改进api_linewin,加入抗锯齿功能,然后再编写一个填充不规则四边形的API。还需继续努力!