3D地表生成及渲染 (VOXEL)


如果你不想在为了读完这篇文章消耗大量的线上时间, 把源码包(8K)拉回去慢慢琢磨吧:)

地表渲染效果演示 (3757 字节) 想 跟 着 云 风 的 讲 解 来 慢 慢 体 会 吗? 那 么 就 先 看 看 右 边 的 效 果 图, 来 个 感 性 的 认 识 吧. 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);
}

云风工作室制作