自制Minecraft的初次尝试【01】

我对Minecraft十分热爱,同时也对用简单的方块表现世界的原理十分好奇。各种三维软件的原理必定特别深奥,而简单的Minecraft蕴含简单的原理,让我有了一探究竟的信心。
百科中有这样一行文字:
“游戏引擎 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、y、z轴的正方向——毕竟我们看不到身后的事物。方位角以z轴正半轴为始边,顺时针旋转为正角,逆时针旋转为负角。过视射线作一个平面垂直于xz平面,视射线与两平面交线的夹角就是仰角s,即视射线与它在xz平面上的投影的夹角。这里对球坐标的定义和地球经纬度的概念差不多。


如图,我们以这个方块右下角的顶点为例。首先以正方形中心为坐标原点建立极坐标系,依照定义,以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。还需继续努力!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值