发信人: nhyjq(人类·迷惑中) 
整理人: nhyjq(2003-01-05 15:46:18), 站内信件
 | 
 
 
版权:云风工作室
 
 如果你不想在为了读完这篇文章消耗大量的线上时间, 把源码包(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);
 }
 
 
 
  ---- ∵我是人类(♂)㊣,天蝎座
 ∴我冷静、深沉     
 My OICQ is 726556     | 
 
 
 |