如果你不想在为了读完这篇文章消耗大量的线上时间, 把源码包(8K)拉回去慢慢琢磨吧:)
想 跟 着 云 风 的 讲 解 来 慢 慢 体 会 吗? 那 么 就 先 看 看 右 边 的 效 果 图,
来 个 感 性 的 认 识 吧. yeah! 这 就 是 我 们 要 达 到 的 效 果 ;-)
是 不 是 和 某 些 游 戏 里 采 用 的 Engine 的 效 果 不 大 一 样?
是 的, 我 们 不 准 备 使 用 多 边 形. 这 个 算 法 产 生 的 3D 地 表
比 用 多 边 形 产 生 出 来 的 更 平 滑, 在 英 文 里 我 们 称 其 为
voxel. 它 大 量 的 被 用 于 现 在 的 模 拟 飞 行 的 游 戏 中,
记 得 Commache I 就 是 因 为 采 用 这 个 技 术, 而 使 我 耳 目 一 新,
顿 时 爱 上 了 这 个 游 戏 :-)
闲 话 不 提 了, 我 在 学 习 3D 地 表 的 生 成 算 法 时, 有 幸 拜 读 了 一 段 程 序, 深 受 启 发. 原 本 早 就 应 该 将 其 介 绍 给 大 家, 一 直 没 有 时 间 写 这 篇 文 章. 这 两 天 没 有 更 新 主 页, 来 这 儿 访 问 的 朋 友 还 是 那 么 多, 真 有 点 不 好 意 思. 就 为 大 家 花 点 时 间, 再 写 篇 有 价 值 的 东 东 吧:-)
和 这 个 3D 地 表 有 关 的, 有 两 个 部 分:一 是 生 成 的 算 法; 二 是 显 示 的 算 法.地 表 和 其 它 的 物 体 不 同, 它 没 有 复 杂 的 3D 结 构, 所 以 可 以 不 用 多 边 形 去 描 述 它. 这 里 我 们 采 用 了 一 个 256 x 256 的 数 组 来 储 存 这 个 范 围 内 的 每 一 个 点 的 高 度. 而 地 表 的 光 泽, 也 是 预 先 算 好 的 ;) 同 样 储 存 在 一 个 256 x 256 的 数 组 了. 渲 染 的 方 法 是 利 用 坡 度 (即 和 周 围 点 的 高 度 差 来 决 定 的), 当 然 你 也 可 以 考 虑 点 的 绝 对 高 度, 比 如 在 绝 对 高 度 高 的 区 域 白 一 些, 以 造 成 一 种 山 顶 积 雪 的 感 觉, 这 就 是 你 自 己 的 发 挥 了. 在 生 成 地 表 时,第 一 步 是 决 定 外 形 的 概 况,这 里 采 用 的 是 随 机 的 方 式. 由 粗 到 细, 逐 步 细 化, 每 次 地 表 上 下 波 动 的 幅 度, 由 运 算 的 面 积 来 决 定. 而 在 实 际 运 用 时, 可 以 在 随 机 的 过 程 中, 加 入 一 些 限 制, 来 控 制 地 表 的 生 成. 第 二 步, 就 是 将 前 面 生 成 的 图 象 做 平 滑 处 理, 让 每 个 点 去 和 周 围 的 点 运 算, 取 平 均 值, 使 不 至 于 出 现 过 大 的 变 化, 这 个 过 程 多 重 复 几 次 (这 里 是 3 次), 就 可 以 得 到 上 佳 的 效 果 了 :-) 由 于 所 有 的 生 成 部 分 都 是 预 先 算 好, 所 以 可 以 不 考 虑 速 度 问 题.
显 示 时, 同 样 不 需 要 过 多 的 数 学 知 识. 我 们 由 近 及 远 的 画 出 地 表 就 可 以 了. 只 要 知 道, 距 离 视 点 越 远, 看 到 的 高 度 就 越 低, 利 用 实 际 高 度 和 距 离, 不 难 计 算 出 应 当 在 屏 幕 上 绘 制 的 高 度. 如 果 你 以 前 稍 微 研 究 过 3D Engine, 就 不 难 理 解 我 的 意 思. 而 出 于 地 表 3D 结 构 的 简 单, 远 处 的 部 分 是 不 会 遮 挡 住 近 处 的 部 分 的, 这 个 减 小 了 许 多 设 计 难 度. 只 是 在 处 理 视 线 和 我 们 生 成 的 地 图 的 x,y 轴 成 一 定 角 度 时, 需 要 使 用 一 点 三 角 知 识.
我 最 大 的 遗 憾 是, 目 前 还 没 有 搞 清 视 线 不 是 水 平 时 的 算 法. (如 果 使 用 多 边 形 产 生 地 表, 却 很 简 单, 这 个 是 另 一 篇 文 章 的 内 容 了)
也 许 解 说 的 太 简 单, 但 是 我 认 为 你如 果 能 欣 赏 一 下 源 代 码, 一 切 都 会 变 的 简 单. 原 来 的 程 序 已 经 是 很 清 晰 了, 但 作 者 还 是 加 入 了 少 许 优 化. 为 了 写 这 篇 教 学 性 质 的 文 章,云 风 又 将 程 序 重 写 了 一 遍 (使 用 的 Djgpp 编 译),更 是 添 加 了 非 常 详 细 的 中 文 注 解. 大 家 慢 慢 品 味 吧 :-)
#include <stdio.h> #include <dos.h> #include <go32.h> #include <conio.h> #include <stdlib.h> #include <math.h> #include <string.h> #include <sys/movedata.h> #include <sys/segments.h> // 将值限制在 0..255 之间 #define Clamp(x) ((x)<0 ? 0 : ((x)>255 ? 255 : (x))) // 取 x 的低字节位, 即对 255 取模 (HMap 和 CMap 都是 256 x 256 的数组) #define L(x) ((x)&0xff) typedef unsigned char byte; byte HMap[256][256]; // 地表高度数组 byte CMap[256][256]; // 色彩值数组 byte Video[320*200]; // 屏幕缓冲区 // 地表高度和色彩表的计算 void ComputeMap(void) { int p,i,j,k,k2,p2; // 从一个平坦的地表开始 HMap[0][0]=128; for ( p=256; p>1; p=p2 ) { p2=p/2; k=p*8+20; k2=k/2; for ( i=0; i<256; i+=p ) { for ( j=0; j<256; j+=p ) { int a,b,c,d; a=HMap[i][j]; b=HMap[ L(i+p) ][j]; c=HMap[i][ L(j+p) ]; d=HMap[ L(i+p)][ L(j+p) ]; HMap[i][ L(j+p2) ]= // 在 a,c 中点,以a,c平均高度为基准 Clamp(((a+c)>>1)+(rand()%k-k2)); // 产生一随机的高度 HMap[ L(i+p2) ][ L(j+p2) ]= // 在 a,b,c,d 区域中心,以平均高度 Clamp(((a+b+c+d)>>2)+(rand()%k-k2)); // 为基准,产生一随机高度 HMap[ L(i+p2) ][j]= // 在 a,b 中点,以a,b平均高度为基准 Clamp(((a+b)>>1)+(rand()%k-k2)); // 产生一随机的高度 } } } // 平滑处理 for ( k=0; k<3; k++ ) for ( i=0; i<256; i++ ) for ( j=0; j<256; j++ ) { HMap[i][j]=(HMap[ L(i+1) ][j]+HMap[i][ L(j+1) ]+ //将前后左右,四个点取 HMap[ L(i-1) ][j]+HMap[i][ L(j-1) ])/4; //平均值,这样做平滑 } // 颜色计算 (地表高度的衍生物) for ( i=0; i<256; i++ ) for ( j=0; j<256; j++ ) { k=128+(HMap[ L(i+1) ][ L(j+1) ]-HMap[i][j])*4; CMap[i][j]=Clamp(k); // 以坡度决定灰度 } } int lasty[320], // 画在指定列上的最后一个点 lastc[320]; // 最后一点的颜色 // 画地表的一个"部分"; 它能画出距离视点一定远处的图象 // 使用 lasty 数组中保存的上次画过的位置, 保正了这个部分不会 // 覆盖掉以前画的部分. x0,y0 和 x1,y1 和 xy 坐标描述 // 地表的高度, hy 是视点的高度, s 是由距离决定的比例因子. // x0,y0,x1,y1 是 16.16 的定点数, // 比例因子是 16.8 的定点值. void Line(int x0,int y0,int x1,int y1,int hy,int s) { int i,sx,sy; // 计算 xy 速度 sx=(x1-x0)/320; sy=(y1-y0)/320; for ( i=0; i<320; i++ ) { int c,y,h,u0,v0,u1,v1,a,b,h0,h1,h2,h3; // 计算 xy 坐标; a 和 b 将被定位于 // 一个 (0..255)(0..255) 的区间里面. u0=L(x0>>16); a=L(x0>>8); v0=L(y0>>16); b=L(y0>>8); u1=L(u0+1); v1=L(v0+1); // 由周围 4 个点来决定里面的高度 h0=HMap[v0][u0]; h2=HMap[v1][u0]; h1=HMap[v0][u1]; h3=HMap[v1][u1]; h0=(h0<<8)+a*(h1-h0); h2=(h2<<8)+a*(h3-h2); h=((h0<<8)+b*(h2-h0))>>16; // 由周围 4 个点来决定里面的颜色 (颜色值是 16.16 的定点数) h0=CMap[v0][u0]; h2=CMap[v1][u0]; h1=CMap[v0][u1]; h3=CMap[v1][u1]; h0=(h0<<8)+a*(h1-h0); h2=(h2<<8)+a*(h3-h2); c=((h0<<8)+b*(h2-h0)); // 使用比例因子计算屏幕高度 y=(((h-hy)*s)>>11)+100; // 画一列 if ( y<(a=lasty[i]) ) { unsigned char *b=Video+a*320+i; int sc,cc; if ( lastc[i]==-1 ) lastc[i]=c; sc=(c-lastc[i])/(a-y); cc=lastc[i]; if ( a>199 ) { b-=(a-199)*320; cc+=(a-199)*sc; a=199; } if ( y<0 ) y=0; while ( y>18; cc+=sc; b-=320; a--; } lasty[i]=y; } lastc[i]=c; // 进一步计算下一个 xy 坐标 x0+=sx; y0+=sy; } } float FOV=3.141592654/4; // 45 度宽的视角 // 画出从点 x0,y0 (16.16) 以 a 角 看到的图象 void View(int x0,int y0,float aa) { int d; int a,b,h,u0,v0,u1,v1,h0,h1,h2,h3; // 清除屏幕缓冲 memset(Video,0,320*200); // 初始化 last-y 和 last-color 数组 for ( d=0; d<320; d++ ) { lasty[d]=200; lastc[d]=-1; } // 计算视点高度变量 // 计算 xy 坐标; a 和 b 将被定位于 // 一个 (0..255)(0..255) 的区间里面. u0=(x0>>16)&0xFF; a=(x0>>8)&255; v0=(y0>>16)&0xFF; b=(y0>>8)&255; u1=(u0+1)&0xFF; v1=(v0+1)&0xFF; // 由周围 4 个点来决定里面的高度 h0=HMap[v0][u0]; h2=HMap[v1][u0]; h1=HMap[v0][u1]; h3=HMap[v1][u1]; h0=(h0<<8)+a*(h1-h0); h2=(h2<<8)+a*(h3-h2); h=((h0<<8)+b*(h2-h0))>>16; // 无覆盖的由近及远画地表 for ( d=0; d<100; d+=1+(d>>6) ) { Line(x0+d*65536*cos(aa-FOV),y0+d*65536*sin(aa-FOV), x0+d*65536*cos(aa+FOV),y0+d*65536*sin(aa+FOV), h-30,100*256/(d+1)); } // 将最终图象 blit 到屏幕 _movedatal(_my_ds(), (unsigned)Video, _dos_ds, 0xa0000, 16000); //320*200/4 } void main(void) { union REGS r; int i,k; float ss,sa,a,s; int x0,y0; // 进入 320x200x256 模式 r.w.ax=0x13; int386(0x10,&r,&r); // 设置前 64 个颜色为 64 级灰度 for ( i=0; i<64; i++ ) { outp(0x3C8,i); outp(0x3C9,i); outp(0x3C9,i); outp(0x3C9,i); } // 计算地图高度 ComputeMap(); // 主循环 // a = 角度 // x0,y0 = 当前坐标 // s = 固定速度 // ss = 当前向前/向后的速度 // sa = 旋转角速度 a=0; k=x0=y0=0; s=4096; ss=0; sa=0; while(k!=27) { // 画一帧 View(x0,y0,a); // 刷新位置/角度 x0+=ss*cos(a); y0+=ss*sin(a); a+=sa; // 处理用户输入 if ( kbhit() ) { if ( (k=getch())==0 ) k=-getch(); switch(k) { case -75: sa-=0.005; break; // 左 case -77: sa+=0.005; break; // 右 case -72: ss+=s; break; // 前 case -80: ss-=s; break; // 后 } } } // 退回到文本模式 r.w.ax=0x03; int386(0x10,&r,&r); } |