17、探索体素图形:从基础到优化

探索体素图形:从基础到优化

在计算机图形的世界里,体素图形是一种相对新颖且充满潜力的技术。它为我们带来了全新的视角,能够创建出独特而逼真的场景。本文将深入探讨体素图形的相关知识,包括体素数据库的生成、射线投射技术、高度映射方法,以及如何对体素引擎进行优化等内容。

1. 体素图形基础

体素图形本质上是一种用于生成山脉、海底等地形数据的技术。“体素”即“体积像素”,体素图形就是渲染具有体积的物体,并将其分解为小立方体或体积元素的科学。对于游戏开发者来说,我们希望利用这种技术,基于某种数据集以简单的方式生成 3D 地形。

2. 生成体素数据库

体素数据包含高度数据和颜色数据。获取体素数据主要有两种基本来源:扫描数据和算法生成数据。
- 使用扫描地形地图 :我们的体素引擎将使用 2D 位图作为体素数据。每个像素的颜色不仅代表该点体素数据的颜色,还代表从 0 到 255 的高度。不过,使用 2D 图像作为颜色和高度信息时,需要注意调色板的选择,以确保颜色和所需高度之间存在一一对应关系。
- 分形和随机地形地图 :除了扫描图像,还可以通过各种算法来参数化生成数据。例如,使用分形算法(如等离子分形)或随机数填充 2D 地形数组。但使用随机数时,需要确保随机值平滑变化,否则渲染出的数据会看起来像噪声或“草”。

3. 射线投射

在计算机图形中,有两种主要方法用于生成场景:合成法和基于光线和能量物理交互的方法(如射线追踪和辐射度)。射线投射是射线追踪的简化版本,它从视点向外投射射线,检测射线与场景中物体的交点。这种技术在一些游戏(如《德军总部 3D》)中被用于生成逼真的显示效果。

射线投射的工作流程如下:
1. 从视点向外投射射线。
2. 检测射线与场景中物体的交点。
3. 根据交点信息生成 3D 显示。

4. 高度映射

高度映射是一种通过从视点投射射线,检测射线与地面或地形的交点,从而生成 3D 显示的技术。具体步骤如下:
1. 从视点投射一系列射线,沿单个方向扫描。
2. 旋转一定角度后再次扫描,直到覆盖整个视野。
3. 根据扫描结果绘制适当的显示。

为了实现高度映射,我们需要执行两个嵌套的 for 循环:

for (column=0; column<320; column++)
    for (row=150; row<200; row++)
    {
        // 1. 从视点投射射线穿过当前列和行,并检测其与地形的交点
        // 2. 记录相交体素的高度和颜色
        // 3. 绘制体素条带
    } // end for row
5. 射线旋转与畸变修正

在生成扫描射线时,会遇到一些问题,例如玩家视点的位置和角度。为了解决这些问题,我们可以使用查找表。通过将一个圆细分为 1920 个小弧段,并创建 SIN 和 COSINE 查找表,我们可以快速生成不同方向的射线。

然而,射线扫描的圆形路径会导致渲染后的视图出现球形畸变。为了消除这种畸变,我们需要将每条射线的长度乘以相对于中心角度的反余弦(正割)值。

6. 查找体素高度

对于视频显示的每一列,需要投射一系列射线穿过每一行,并找到它们与地形的交点。我们可以使用相似三角形的原理来计算射线与地形的交点。具体公式如下:

ray_length = distance * height / delta
delta = height - (SCREEN_HEIGHT - row)
7. 条带缩放

在绘制体素条带时,我们可以根据体素与玩家视点的距离对条带进行缩放。物体的缩放比例与它到视平面的距离成反比,即:

scale = k/object(z)

其中,k 是缩放因子,object(z) 表示物体到视平面的 Z 距离。

8. 实现简单体素显示演示

我们可以编写一个简单的体素显示演示程序,使用 320x200 的 PCX 文件作为高度和颜色数据。程序的主要函数是 Draw_Terrain() ,它负责将当前视图渲染到双缓冲区中。

以下是 VOXEL.C 程序的主要代码:

// VOXEL.C - Single texture based, ray casted voxel engine
// I N C L U D E S ///////////////////////////////////////////////////////////
#include <io.h>
#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <dos.h>
#include <bios.h>
#include <fcntl.h>
#include <memory.h>
#include <malloc.h>
#include <math.h>
#include <string.h>
#include "black3.h"
#include "black4.h"
#include "black5.h"
// D E F I N E S /////////////////////////////////////////////////////////////
#define WORLD_X_SIZE   320  // width of universe
#define WORLD_Y_SIZE   200  // height of universe
// constants used to represent angles for the ray casting a 60 degree field of view
#define ANGLE_0        0
#define ANGLE_1        5
#define ANGLE_2        10
#define ANGLE_4        20
#define ANGLE_5        25
#define ANGLE_6        30
#define ANGLE_15       80
#define ANGLE_30       160
#define ANGLE_45       240
#define ANGLE_60       320
#define ANGLE_90       480
#define ANGLE_135      720
#define ANGLE_180      960
#define ANGLE_225      1200
#define ANGLE_270      1440
#define ANGLE_315      1680
#define ANGLE_360      1920
// there are 1920 degrees in our circle or 360/1920 is the conversion factor
// from real degrees to our degrees
#define ANGULAR_INCREMENT  ((float)0.1875)
// conversion constants from radians to degrees and viceversa
#define DEG_TO_RAD         ((float)3.1415926/(float)180)
#define RAD_TO_DEG         ((float)180/(float)3.1415926)
// G L O B A L S ////////////////////////////////////////////////////////////
pcx_picture image_pcx;        // general pcx image
int play_x=1000,              // the current world x of player
play_y=1000,              // the current world y of player
play_z=150,
play_ang=ANGLE_90,        // the current viewing angle of player
play_dist = 70,
mountain_scale=10;        // scaling factor for mountains
float play_dir_x,              // the direction the player is pointing in
play_dir_y,
play_dir_z,
cos_look[ANGLE_360],     // cosine look up table
sin_look[ANGLE_360],     // sin look up table
sphere_cancel[ANGLE_60]; // cancels fish eye distortion
// F U N C T I O N S ////////////////////////////////////////////////////////
void Line_VDB(int y1,int y2,int x,int color)
{
    // draw a vertical line, note that a memset function can no longer be
    // used since the pixel addresses are no longer contiguous in memory
    // note that the end points of the line must be on the screen
    unsigned char far *start_offset; // starting memory offset of line
    int index, // loop index
    temp;  // used for temporary storage during swap
    // make sure y2 > y1
    if (y1>y2)
    {
        temp = y1;
        y1   = y2;
        y2   = temp;
    } // end swap
    // compute starting position
    start_offset = double_buffer + ((y1<<8) + (y1<<6)) + x;
    for (index=0; index<=y2-y1; index++)
    {
        // set the pixel
        *start_offset = (unsigned char)color;
        // move downward to next line
        start_offset+=320;
    } // end for index
} // end Line_VDB
//////////////////////////////////////////////////////////////////////////////
int Initialize(char *filename)
{
    // this function builds all the look up tables for the terrain generator and
    // loads in the terrain texture map
    int ang;           // looping variable
    float rad_angle;   // current angle in radians
    // create sin and cos look up first
    for (ang=0; ang<ANGLE_360; ang++)
    {
        // compute current angle in radians
        rad_angle = (float)ang*ANGULAR_INCREMENT*DEG_TO_RAD;
        // now compute the sin and cos
        sin_look[ang] = sin(rad_angle);
        cos_look[ang] = cos(rad_angle);
    } // end for ang
    // create inverse cosine viewing distortion filter
    for (ang=0; ang<ANGLE_30; ang++)
    {
        // compute current angle in radians
        rad_angle = (float)ang*ANGULAR_INCREMENT*DEG_TO_RAD;
        // now compute the sin and cos
        sphere_cancel[ang+ANGLE_30] = 1/cos(rad_angle);
        sphere_cancel[ANGLE_30-ang] = 1/cos(rad_angle);
    } // end for ang
    // initialize the pcx structure
    PCX_Init((pcx_picture_ptr)&image_pcx);
    // load in the textures
    return(PCX_Load(filename, (pcx_picture_ptr)&image_pcx,1));
} // end Initialize
/////////////////////////////////////////////////////////////////////////////
void Draw_Terrain(int play_x,
                  int play_y,
                  int play_z,
                  int play_ang,
                  int play_dist)
{
    // this function draws the entire terrain based on the location and orientation
    // of the player's viewpoint
    int curr_ang,       // current angle being processed
    xr,yr,          // location of ray in world coords
    x_fine,y_fine,  // the texture coordinates the ray hit
    pixel_color,    // the color of textel
    ray,            // looping variable
    row,            // the current video row being processed
    row_inv,        // the inverted row to make upward positive
    scale,          // the scale of the current strip
    top,            // top of strip
    bottom;         // bottom of strip
    float ray_length;   // the length of the ray after distortion compensation
    // start the current angle off -30 degrees to the left of the player's
    // current viewing direction
    curr_ang = play_ang - ANGLE_30;
    // test for underflow
    if (curr_ang < 0)
        curr_ang+=ANGLE_360;
    // cast a series of rays for every column of the screen
    for (ray=1; ray<320; ray++)
    {
        // for each column compute the pixels that should be displayed
        // for each screen pixel, process from top to bottom
        for (row = 100; row<150; row++)
        {
            // compute length of ray
            row_inv = 200-row;
            // use the current height and distance to compute length of ray.
            ray_length = sphere_cancel[ray] * ((float)(play_dist*play_z)/
                                               (float)(play_z-row_inv));
            // rotate ray into position of sample
            xr = (int)((float)play_x + ray_length * cos_look[curr_ang]);
            yr = (int)((float)play_y - ray_length * sin_look[curr_ang]);
            // compute texture coords
            x_fine = xr % WORLD_X_SIZE;
            y_fine = yr % WORLD_Y_SIZE;
            // using texture index locate texture pixel in textures
            pixel_color = image_pcx.buffer[x_fine + (y_fine*320)];
            // draw the strip
            scale = (int)mountain_scale*pixel_color/(int)(ray_length+1);
            top        = 50+row-scale;
            bottom     = top + scale;
            Line_VDB(top,bottom,ray,pixel_color);
            //  Write_Pixel_DB(ray,50+row,pixel_color);
        } // end for row
        // move to next angle
        if (++curr_ang >= ANGLE_360)
            curr_ang=ANGLE_0;
    } // end for ray
} // end Draw_Terrain
// M A I N //////////////////////////////////////////////////////////////////
void main(int argc, char **argv)
{
    char buffer[80];
    int done=0; // exit flag
    float speed=0;  // speed of player
    // check to see if command line parms are correct
    if (argc<=2)
    {
        // not enough parms
        printf("\nUsage: voxopt.exe filename.pcx height");
        printf("\nExample: voxopt voxterr3.pcx 10\n");
        // return to DOS
        exit(1);
    } // end if
    // set the graphics mode to mode 13h
    Set_Graphics_Mode(GRAPHICS_MODE13);
    // create a double buffer
    Create_Double_Buffer(200);
    // create look up tables and load textures
    if (!Initialize(argv[1]))
    {
        printf("\nError loading file %s",argv[1]);
        exit(1);
    } // end if
    // install keyboard driver
    Keyboard_Install_Driver();
    // set scale of mountains
    mountain_scale = atoi(argv[2]);
    // draw the first frame
    Draw_Terrain(play_x,
                 play_y,
                 play_z,
                 play_ang,
                 play_dist);
    Display_Double_Buffer(double_buffer,0);
    // main event loop
    while(!done)
    {
        // reset velocity
        speed = 0;
        // test if user is hitting keyboard
        if (keys_active)
        {
            // what is user trying to do
            // change viewing distance
            if (keyboard_state[MAKE_F])
                play_dist+=10;
            if (keyboard_state[MAKE_C])
                play_dist-=10;
            // change viewing height
            if (keyboard_state[MAKE_U])
                play_z+=10;
            if (keyboard_state[MAKE_D])
                play_z-=10;
            // change viewing position
            if (keyboard_state[MAKE_RIGHT])
                if ((play_ang+=ANGLE_5) >= ANGLE_360)
                    play_ang-=ANGLE_360;
            if (keyboard_state[MAKE_LEFT])
                if ((play_ang-=ANGLE_5) < 0)
                    play_ang+=ANGLE_360;
            // move forward
            if (keyboard_state[MAKE_UP])
                speed=20;
            // move backward
            if (keyboard_state[MAKE_DOWN])
                speed=-20;
            // exit demo
            if (keyboard_state[MAKE_ESC])
                done=1;
            // compute trajectory vector for this view angle
            play_dir_x = cos_look[play_ang];
            play_dir_y = -sin_look[play_ang];
            play_dir_z = 0;
            // translate viewpoint
            play_x+=speed*play_dir_x;
            play_y+=speed*play_dir_y;
            play_z+=speed*play_dir_z;
            // draw the terrain
            Fill_Double_Buffer(0);
            Draw_Terrain(play_x,
                         play_y,
                         play_z,
                         play_ang,
                         play_dist);
            // draw tactical
            sprintf(buffer,"Height = %d Distance = %d     ",play_z,play_dist);
            Print_String_DB(0,0,10,buffer,0);
            sprintf(buffer,"Pos: X=%d, Y=%d, Z=%d    ",play_x,play_y,play_z);
            Print_String_DB(0,10,10,buffer,0);
            Display_Double_Buffer(double_buffer,0);
        } // end if
    } // end while
    // reset back to text mode
    Set_Graphics_Mode(TEXT_MODE);
    // remove the keyboard handler
    Keyboard_Remove_Driver();
} // end main

运行该程序时,需要提供两个参数:320x200x256 PCX 文件的文件名和渲染的缩放因子。例如:

VOXEL VOXTERR4.PCX 10

控制操作如下表所示:
| 按键 | 描述 |
| ---- | ---- |
| [UP{ARROW] | 右转 |
| [DOWN{{ARROW] | 左转 |
| [RIGHT{ARROW] | 向前移动 |
| [LEFT{ARROW] | 向后移动 |
| [U] | 增加高度 |
| [D] | 降低高度 |
| [F] | 远离视平面 |
| [C] | 靠近视平面 |
| [ESC] | 退出演示 |

9. 基于图块的体素引擎

为了创建更大的世界,我们可以使用图块系统。通过将世界划分为多个图块,并为每个图块分配一个纹理,我们可以在有限的内存中创建出更大的世界。

以下是实现基于图块的体素引擎的步骤:
1. 定义图块地图,使用一个简单的 2D 数组表示。
2. 加载图块的位图纹理。
3. 在 Draw_Terrain() 函数中添加额外的代码,以处理图块的查找和渲染。

以下是 VOXTILE.C 程序的主要代码:

// VOXTILE.C - CELL based, ray casted voxel engine
// I N C L U D E S ///////////////////////////////////////////////////////////
#include <io.h>
#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <dos.h>
#include <bios.h>
#include <fcntl.h>
#include <memory.h>
#include <malloc.h>
#include <math.h>
#include <string.h>
#include "black3.h"
#include "black4.h"
#include "black5.h"
// D E F I N E S /////////////////////////////////////////////////////////////
#define NUM_X_CELLS    32    // number of cells in x direction
#define NUM_Y_CELLS    32    // number of cells in y direction
#define CELL_WIDTH     64    // width of a cell in pixels
#define CELL_HEIGHT    64    // height of a cell in pixels
#define WORLD_X_SIZE   (NUM_X_CELLS*CELL_WIDTH)    // width of universe in pixels
#define WORLD_Y_SIZE   (NUM_Y_CELLS*CELL_HEIGHT)   // height of universe in pixels
// constants used to represent angles for the ray casting a 60 degree field of
// view
#define ANGLE_0        0
#define ANGLE_1        5
#define ANGLE_2        10
#define ANGLE_4        20
#define ANGLE_5        25
#define ANGLE_6        30
#define ANGLE_15       80
#define ANGLE_30       160
#define ANGLE_45       240
#define ANGLE_60       320
#define ANGLE_90       480
#define ANGLE_135      720
#define ANGLE_180      960
#define ANGLE_225      1200
#define ANGLE_270      1440
#define ANGLE_315      1680
#define ANGLE_360      1920
// there are 1920 degrees in our circle or 360/1920 is the conversion factor
// from real degrees to our degrees
#define ANGULAR_INCREMENT  ((float)0.1875)
// conversion constants from radians to degrees and viceversa
#define DEG_TO_RAD         ((float)3.1415926/(float)180)
#define RAD_TO_DEG         ((float)180/(float)3.1415926)
#define NUM_TEXTURES   4   // holds the number of textures in the system
// G L O B A L S  ////////////////////////////////////////////////////////////
pcx_picture image_pcx;        // general pcx image
sprite text_spr;              // this sprite holds the bitmap textures
// to drape over each cell
int play_x=1000,              // the current world x of player
play_y=1000,              // the current world y of player
play_z=150,               // the current world z of player, same as height
play_ang=ANGLE_90,        // the current viewing angle of player
play_dist = 70,
mountain_scale=10;        // scaling factor for mountains
float play_dir_x,             // the direction the player is pointing in
play_dir_y,
play_dir_z,
cos_look[ANGLE_360],     // cosine look up table
sin_look[ANGLE_360],     // sin look up table
sphere_cancel[ANGLE_60]; // cancels fish eye distortion
// this map holds the terrain texture cells
// each cell is really a bitmap texture, so by using cells and tiling much
// larger worlds can be constructed
char terrain_texture[] =
{ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
  1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
  1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
  1,0,0,3,3,0,0,0,0,0,0,1,1,1,0,0,0,1,0,0,3,0,0,0,0,0,0,0,3,0,0,1,
  1,0,0,3,3,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,3,0,0,0,0,0,0,0,3,3,0,1,
  1,0,0,3,3,0,0,0,0,0,0,2,2,1,0,0,0,0,0,0,3,0,0,3,3,0,0,0,3,3,0,1,
  1,0,0,0,3,0,0,2,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,3,3,0,0,0,0,3,0,1,
  1,0,0,0,0,0,0,2,0,2,2,2,0,0,3,0,3,3,3,0,0,0,0,3,3,0,0,0,0,0,0,1,
  1,0,0,0,0,0,0,2,0,0,0,0,0,0,3,0,3,3,3,0,0,0,0,3,0,0,0,0,0,0,0,1,
  1,0,0,2,0,0,0,2,0,0,0,0,0,0,3,0,3,3,0,0,0,0,0,0,0,2,0,0,2,0,0,1,
  1,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,0,0,0,0,0,0,0,0,0,1,
  1,0,0,2,0,0,1,1,1,0,0,2,0,2,0,0,1,0,0,0,2,2,2,0,0,0,0,0,0,0,0,1,
  1,0,0,0,0,2,1,1,1,0,0,0,0,0,0,0,1,0,0,0,2,0,2,0,0,0,0,0,0,0,0,1,
  1,0,0,0,0,0,0,1,3,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
  1,0,0,0,0,2,0,0,0,3,0,0,0,2,0,2,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
  1,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
  1,0,2,0,0,0,0,0,3,0,0,0,0,0,3,0,0,0,0,0,1,0,3,0,0,0,0,0,0,0,0,1,
  1,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,1,0,3,0,0,0,2,0,0,0,0,1,
  1,0,0,0,0,2,0,2,0,0,2,0,3,0,0,0,0,2,2,0,1,0,0,0,0,2,0,0,0,0,0,1,
  1,0,2,0,0,2,0,0,0,0,2,0,0,0,3,0,0,2,0,0,0,0,3,3,0,2,2,2,0,0,0,1,
  1,0,0,0,0,2,0,0,0,0,2,0,0,0,3,0,0,0,0,2,0,0,0,0,0,2,0,2,0,0,0,1,
  1,0,0,0,0,2,0,0,0,0,0,3,0,0,0,0,0,0,2,2,0,0,0,3,0,0,2,2,0,0,0,1,
  1,0,0,0,0,0,2,2,2,2,0,0,0,0,3,0,0,0,2,0,0,0,0,0,0,0,2,0,0,0,0,1,
  1,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,1,
  1,0,2,2,0,0,0,0,0,1,1,1,1,1,0,0,0,0,2,2,0,0,0,0,0,0,2,0,0,0,0,1,
  1,0,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,1,
  1,0,2,0,0,2,2,0,0,0,3,0,0,3,3,3,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,1,
  1,0,0,2,0,2,2,0,0,0,3,0,0,3,3,3,0,0,0,0,0,0,3,3,3,0,0,0,0,0,0,1,
  1,0,0,0,0,2,2,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,3,3,3,0,0,0,0,0,0,1,
  1,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,3,3,3,0,0,0,0,0,0,1,
  1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
  1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
};
// F U N C T I O N S ////////////////////////////////////////////////////////
void Line_VDB(int y1,int y2,int x,int color)
{
    // draw a vertical line, note that a memset function can no longer be
    // used since the pixel addresses are no longer contiguous in memory
    // note that the end points of the line must be on the screen
    unsigned char far *start_offset; // starting memory offset of line
    int index, // loop index
    temp;  // used for temporary storage during swap
    // make sure y2 > y1
    if (y1>y2)
    {
        temp = y1;
        y1   = y2;
        y2   = temp;
    } // end swap
    // compute starting position
    start_offset = double_buffer + ((y1<<8) + (y1<<6)) + x;
    for (index=0; index<=y2-y1; index++)
    {
        // set the pixel
        *start_offset = (unsigned char)color;
        // move downward to next line
        start_offset+=320;
    } // end for index
} // end Line_VDB
//////////////////////////////////////////////////////////////////////////////
void Initialize(void)
{
    // this function builds all the look up tables for the terrain generator
    int ang,           // looping variable
    index;         // looping variable
    float rad_angle;   // current angle in radians
    // create sin and cos look up first
    for (ang=0; ang<ANGLE_360; ang++)
    {
        // compute current angle in radians
        rad_angle = (float)ang*ANGULAR_INCREMENT*DEG_TO_RAD;
        // now compute the sin and cos
        sin_look[ang] = sin(rad_angle);
        cos_look[ang] = cos(rad_angle);
    } // end for ang
    // create inverse cosine viewing distortion filter
    for (ang=0; ang<ANGLE_30; ang++)
    {
        // compute current angle in radians
        rad_angle = (float)ang*ANGULAR_INCREMENT*DEG_TO_RAD;
        // now compute the sin and cos
        sphere_cancel[ang+ANGLE_30] = 1/cos(rad_angle);
        sphere_cancel[ANGLE_30-ang] = 1/cos(rad_angle);
    } // end for ang
    // load the texture tiles into memory
    // intialize the pcx structure
    PCX_Init((pcx_picture_ptr)&image_pcx);
    // load in the textures
    PCX_Load("voxtext.pcx", (pcx_picture_ptr)&image_pcx,1);
    // initialize the texture sprite
    Sprite_Init((sprite_ptr)&text_spr,0,0,64,64,0,0,0,0,0,0);
    // extract the bitmaps for the textures
    for (index=0; index<NUM_TEXTURES; index++)
        PCX_Get_Sprite((pcx_picture_ptr)&image_pcx,(sprite_ptr)&text_spr,index,index,0);
    // done with this PCX file so delete memory associated with it
    PCX_Delete((pcx_picture_ptr)&image_pcx);
} // end Initialize
/////////////////////////////////////////////////////////////////////////////
void Draw_Terrain(int play_x,
                  int play_y,
                  int play_z,
                  int play_ang,
                  int play_dist)
{
    // this function draws the entire terrain based on the location and orientation
    // of the player's viewpoint
    int curr_ang,               // current angle being processed
    xr,yr,                  // location of ray in world coords
    x_cell,y_cell,          // the cell the ray hit
    x_fine,y_fine,          // the texture coordinates the ray hit
    texture_index,          // which texture
    pixel_color,            // the color of textel
    ray,                    // looping variable
    row,                    // the current video row being processed
    row_inv,                // the inverted row to make upward positive
    scale,                  // the scale of the current strip
    top,                    // top of strip
    bottom;                 // bottom of strip
    float ray_length;           // the length of the ray after distortion compensation
    // start the current angle off -30 degrees to the left of the player's
    // current viewing direction
    curr_ang = play_ang - ANGLE_30;
    // test for underflow
    if (curr_ang < 0)
        curr_ang+=ANGLE_360;
    // cast a series of rays for every column of the screen
    for (ray=1; ray<320; ray++)
    {
        // for each column compute the pixels that should be displayed
        // for each screen pixel, process from top to bottom
        for (row = 100; row<150; row++)
        {
            // compute length of ray
            row_inv = 200-row;
            // use the current height and distance to compute length of ray.
            ray_length = sphere_cancel[ray] * ((float)(play_dist*play_z)/
                                               (float)(play_z-row_inv));
            // rotate ray into position of sample
            xr = (int)((float)play_x + ray_length * cos_look[curr_ang]);
            yr = (int)((float)play_y - ray_length * sin_look[curr_ang]);
            // compute cell of sampling tip
            x_cell = xr >> 5; //  / NUM_X_CELLS;
            y_cell = yr >> 5; //  / NUM_Y_CELLS;
            // compute texture coords
            x_fine = xr & 0x003F; // % CELL_WIDTH;
            y_fine = yr & 0x003F; // % CELL_HEIGHT;
            // locate texture index
            texture_index = terrain_texture[x_cell + (y_cell<<5)]; 
            // *NUM_X_CELLS];
            // using texture index locate texture pixel in textures
            pixel_color = text_spr.frames[texture_index][x_fine + (y_fine<<6)];
            // *CELL_WIDTH];
            // draw the strip
            scale = (int)mountain_scale*pixel_color/(int)(ray_length+1);
            top        = 50+row-scale;
            bottom     = top + scale;
            Line_VDB(top,bottom,ray,pixel_color);
        } // end for row
        // move to next angle
        if (++curr_ang >= ANGLE_360)
            curr_ang=ANGLE_0;
    } // end for ray
} // end Draw_Terrain
// M A I N //////////////////////////////////////////////////////////////////
void main(int argc, char **argv)
{
    char buffer[80]; // text output buffer
    int done=0;      // exit flag
    float speed=0;   // speed of player
    // check to see if command line parms are correct
    if (argc<=1)
    {
        // not enough parms
        printf("\nUsage: voxtile.exe height");
        printf("\nExample: voxtile 15\n");
        // return to DOS
        exit(1);
    } // end if
    // set scale of mountains
    mountain_scale = atoi(argv[1]);
    // set the graphics mode to mode 13h
    Set_Graphics_Mode(GRAPHICS_MODE13);
    // install keyboard driver
    Keyboard_Install_Driver();
    // build the look up tables and load textures
    Initialize();
    // create a double buffer
    Create_Double_Buffer(200);
    // display terrain manually first time
    Draw_Terrain(play_x,
                 play_y,
                 play_z,
                 play_ang,
                 play_dist);
    Display_Double_Buffer(double_buffer,0);
    // main event loop
    while(!done)
    {
        // reset velocity
        speed = 0;
        // test if user is hitting keyboard
        if (keys_active)
        {
            // what is user trying to do
            // change viewing distance
            if (keyboard_state[MAKE_F])
                play_dist+=10;
            if (keyboard_state[MAKE_C])
                play_dist-=10;
            // change viewing height
            if (keyboard_state[MAKE_U])
                play_z+=10;
            if (keyboard_state[MAKE_D])
                play_z-=10;
            // change viewing position
            if (keyboard_state[MAKE_RIGHT])
                if ((play_ang+=ANGLE_5) >= ANGLE_360)
                    play_ang-=ANGLE_360;
            if (keyboard_state[MAKE_LEFT])
                if ((play_ang-=ANGLE_5) < 0)
                    play_ang+=ANGLE_360;
            // move forward
            if (keyboard_state[MAKE_UP])
                speed=20;
            // move backward
            if (keyboard_state[MAKE_DOWN])
                speed=-20;
            // exit demo
            if (keyboard_state[MAKE_ESC])
                done=1;
            // compute trajectory vector for this view angle
            play_dir_x = cos_look[play_ang];
            play_dir_y = -sin_look[play_ang];
            play_dir_z = 0;
            // translate viewpoint
            play_x+=speed*play_dir_x;
            play_y+=speed*play_dir_y;
            play_z+=speed*play_dir_z;
            // draw the terrain
            Fill_Double_Buffer(0);
            Draw_Terrain(play_x,
                         play_y,
                         play_z,
                         play_ang,
                         play_dist);
            // print out tactical
            sprintf(buffer,"Height = %d Distance = %d     ",play_z,play_dist);
            Print_String_DB(0,0,10,buffer,0);
            sprintf(buffer,"Pos: X=%d, Y=%d, Z=%d    ",play_x,play_y,play_z);
            Print_String_DB(0,10,10,buffer,0);
            // show buffer
            Display_Double_Buffer(double_buffer,0);
        } // end if
    } // end while
    // reset back to text mode
    Set_Graphics_Mode(TEXT_MODE);
    // remove the keyboard handler
    Keyboard_Remove_Driver();
} // end main

运行 VOXTILE.EXE 时,只需在命令行中提供高度缩放因子。例如:

VOXTILE 15

控制操作与 VOXEL.EXE 相同。

10. 优化体素引擎

为了提高体素引擎的性能,我们可以进行一些优化。主要的优化方法包括:
- 代码分析 :通过分析 Draw_Terrain() 函数的嵌套循环结构,我们发现内循环的执行次数远远多于外循环,因此应该重点优化内循环。
- 内联所有函数 :将 Line_VDB() 函数内联,避免创建和销毁 16,000 个栈帧,从而提高渲染速度。
- 减少绘制次数 :采用从前到后的顺序绘制地形,只绘制每个像素一次,避免重复绘制。
- 使用移位和逻辑运算 :将乘法和除法运算替换为移位和逻辑运算,提高计算速度。
- 使用查找表 :创建查找表来存储一些常用的计算结果,减少重复计算。

以下是优化后的 VOXOPT.C 程序的主要代码:

// VOXOPT.C - Single texture based, ray casted voxel engine
// I N C L U D E S ///////////////////////////////////////////////////////////
#include <io.h>
#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <dos.h>
#include <bios.h>
#include <fcntl.h>
#include <memory.h>
#include <malloc.h>
#include <math.h>
#include <string.h>
#include "black3.h"
#include "black4.h"
#include "black5.h"
// D E F I N E S /////////////////////////////////////////////////////////////
#define WORLD_X_SIZE   320  // width of universe
#define WORLD_Y_SIZE   200  // height of universe
// constants used to represent angles for the ray casting a 60 degree field of view
#define ANGLE_0        0
#define ANGLE_1        5
#define ANGLE_2        10
#define ANGLE_4        20
#define ANGLE_5        25
#define ANGLE_10       50
#define ANGLE_6        30
#define ANGLE_15       80
#define ANGLE_30       160
#define ANGLE_45       240
#define ANGLE_60       320
#define ANGLE_90       480
#define ANGLE_135      720
#define ANGLE_180      960
#define ANGLE_225      1200
#define ANGLE_270      1440
#define ANGLE_315      1680
#define ANGLE_360      1920
// there are 1920 degrees in our circle or 360/1920 is the conversion factor
// from real degrees to our degrees
#define ANGULAR_INCREMENT  ((float)0.1875)
// conversion constants from radians to degrees and viceversa
#define DEG_TO_RAD         ((float)3.1415926/(float)180)
#define RAD_TO_DEG         ((float)180/(float)3.1415926)
// G L O B A L S  ////////////////////////////////////////////////////////////
pcx_picture image_pcx;        // general pcx image
int play_x=1000,              // the current world x of player
play_y=1000,              // the current world y of player
play_z=150,
play_ang=ANGLE_90,        // the current viewing angle of player
play_dist = 70,
mountain_scale=10;        // scaling factor for mountains
float play_dir_x,              // the direction the player is pointing in
play_dir_y,
play_dir_z,
cos_look[ANGLE_360],     // cosine look up table
sin_look[ANGLE_360],     // sin look up table
sphere_cancel[ANGLE_60], // cancels fish eye distortion
ray_length[100];         // holds ray length look up
// F U N C T I O N S ////////////////////////////////////////////////////////
int Initialize(char *filename)
{
    // this function builds all the look up tables for the terrain generator and
    // loads in the terrain texture map
    int ang,           // looping variable
    row,
    row_inv;
    float rad_angle;   // current angle in radians
    // create sin and cos look up first
    for (ang=0; ang<ANGLE_360; ang++)
    {
        // compute current angle in radians
        rad_angle = (float)ang*ANGULAR_INCREMENT*DEG_TO_RAD;
        // now compute the sin and cos
        sin_look[ang] = sin(rad_angle);
        cos_look[ang] = cos(rad_angle);
    } // end for ang
    // create inverse cosine viewing distortion filter
    for (ang=0; ang<ANGLE_30; ang++)
    {
        // compute current angle in radians
        rad_angle = (float)ang*ANGULAR_INCREMENT*DEG_TO_RAD;
        // now compute the sin and cos
        sphere_cancel[ang+ANGLE_30] = 1/cos(rad_angle);
        sphere_cancel[ANGLE_30-ang] = 1/cos(rad_angle);
    } // end for ang
    // create the pre-computed ray length array
    for (row = 100; row<150; row++)
    {
        // compute length of ray
        row_inv = 200-row;
        // use the current height and distance to compute length of ray.
        ray_length[row-100] = ((float)(play_dist*play_z)/(float)(play_z-row_inv));
    } // end for row
    // initialize the pcx structure
    PCX_Init((pcx_picture_ptr)&image_pcx);
    // load in the textures
    return(PCX_Load(filename, (pcx_picture_ptr)&image_pcx,1));
} // end Initialize
/////////////////////////////////////////////////////////////////////////////
void Draw_Terrain(int play_x,
                  int play_y,
                  int play_z,
                  int play_ang,
                  int play_dist)
{
    // this function draws the entire terrain based on the location and orientation
    // of the player's viewpoint
    int curr_ang,       // current angle being processed
    xr,yr,          // location of ray in world coords
    x_fine,y_fine,  // the texture coordinates the ray hit
    pixel_color,    // the color of textel
    ray,            // looping variable
    row,            // the current video row being processed
    scale,          // the scale of the current strip
    top,            // top of strip
    last_scale,
    last_top,
    diff,
    index;          // looping variable
    float ray_length_final;   // the length of the ray after distortion compensation
    unsigned char far *start_offset;  // used by inline line drawer
    // start the current angle off -30 degrees to the left of the player's
    // current viewing direction
    curr_ang = play_ang - ANGLE_30;
    // test for underflow
    if (curr_ang < 0)
        curr_ang+=ANGLE_360;
    // cast a series of rays for every column of the screen
    for (ray=1; ray<320; ray++)
    {
        // reset last top and scale
        last_scale = 0;
        last_top = 0;
        // for each column compute the pixels that should be displayed
        // for each screen pixel, process from top to bottom
        for (row = 149; row>=100; row--)
        {
            // use the current height and distance to compute length of ray.
            ray_length_final = sphere_cancel[ray] * ray_length[row-100];
            // rotate ray into position of sample
            xr = (int)((float)play_x + ray_length_final * cos_look[curr_ang]);
            yr = (int)((float)play_y - ray_length_final * sin_look[curr_ang]);
            // compute texture coords
            x_fine = xr % WORLD_X_SIZE;
            y_fine = yr % WORLD_Y_SIZE;
            // using texture index locate texture pixel in textures
            pixel_color = image_pcx.buffer[x_fine + (y_fine<<8) + (y_fine<<6)];
            // draw the strip
            scale = (int)mountain_scale*pixel_color/(int)(ray_length_final+1);
            // test if we need to draw this segment
            if (scale>=last_scale)
            {
                diff = scale-last_scale;
                top  = 50+row-scale;
                // compute starting position
                start_offset = double_buffer + ((top<<8) + (top<<6)) + ray;
                for (index=0; index<=diff; index++)
                {
                    // set the pixel
                    *start_offset = (unsigned char)pixel_color;
                    // move downward to next line
                    start_offset+=320;
                } // end for index
            } // end if scale
            last_scale = scale;
        } // end for row
        // move to next angle
        if (++curr_ang >= ANGLE_360)
            curr_ang=ANGLE_0;
    } // end for ray
} // end Draw_Terrain
// M A I N //////////////////////////////////////////////////////////////////
void main(int argc, char **argv)
{
    char buffer[80];
    int done=0; // exit flag
    float speed=0;  // speed of player
    // check to see if command line parms are correct
    if (argc<=2)
    {
        // not enough parms
        printf("\nUsage: voxel.exe filename.pcx height");
        printf("\nExample: voxel voxterr4.pcx 10\n");
        // return to DOS
        exit(1);
    } // end if
    // set the graphics mode to mode 13h
    Set_Graphics_Mode(GRAPHICS_MODE13);
    // create a double buffer
    Create_Double_Buffer(200);
    // create look up tables and load textures
    if (!Initialize(argv[1]))
    {
        printf("\nError loading file %s",argv[1]);
        exit(1);
    } // end if
    // install keyboard driver
    Keyboard_Install_Driver();
    // set scale of mountains
    mountain_scale = atoi(argv[2]);
    // draw the first frame
    Draw_Terrain(play_x,
                 play_y,
                 play_z,
                 play_ang,
                 play_dist);
    Display_Double_Buffer(double_buffer,0);
    // main event loop
    while(!done)

{
        // reset velocity
        speed = 0;
        // test if user is hitting keyboard
        if (keys_active)
        {
            // what is user trying to do
            // change viewing distance
            if (keyboard_state[MAKE_F])
                play_dist+=10;
            if (keyboard_state[MAKE_C])
                play_dist-=10;
            // change viewing height
            if (keyboard_state[MAKE_U])
                play_z+=10;
            if (keyboard_state[MAKE_D])
                play_z-=10;
            // change viewing position
            if (keyboard_state[MAKE_RIGHT])
                if ((play_ang+=ANGLE_10) >= ANGLE_360)
                    play_ang-=ANGLE_360;
            if (keyboard_state[MAKE_LEFT])
                if ((play_ang-=ANGLE_10) < 0)
                    play_ang+=ANGLE_360;
            // move forward
            if (keyboard_state[MAKE_UP])
                speed=20;
            // move backward
            if (keyboard_state[MAKE_DOWN])
                speed=-20;
            // exit demo
            if (keyboard_state[MAKE_ESC])
                done=1;
            // compute trajectory vector for this view angle
            play_dir_x = cos_look[play_ang];
            play_dir_y = -sin_look[play_ang];
            play_dir_z = 0;
            // translate viewpoint
            play_x+=speed*play_dir_x;
            play_y+=speed*play_dir_y;
            play_z+=speed*play_dir_z;
            // draw the terrain
            Fill_Double_Buffer(0);
            Draw_Terrain(play_x,
                         play_y,
                         play_z,
                         play_ang,
                         play_dist);
            // draw tactical
            sprintf(buffer,"Height = %d Distance = %d     ",play_z,play_dist);
            Print_String_DB(0,0,10,buffer,0);
            sprintf(buffer,"Pos: X=%d, Y=%d, Z=%d    ",play_x,play_y,play_z);
            Print_String_DB(0,10,10,buffer,0);
            Display_Double_Buffer(double_buffer,0);
        } // end if
    } // end while
    // reset back to text mode
    Set_Graphics_Mode(TEXT_MODE);
    // remove the keyboard handler
    Keyboard_Remove_Driver();
} // end main

`VOXOPT.EXE` 的控制和命令行参数与 `VOXEL.EXE` 相同,但由于使用了查找表,不能更改高度或查看距离。优化内容如下:
1. 内联线绘制函数
2. 使用查找表存储预计算的射线长度
3. 尽可能使用移位进行乘法运算
4. 以从前到后的顺序绘制每个像素,且仅绘制一次

#### 11. 添加前景对象
若要向体素显示中添加对象,可使用 Z 缓冲区技术。即在体素显示从后向前渲染时,将希望与视平面保持一定距离的精灵对象在适当的时间进行渲染,使剩余的体素地形遮挡精灵的相应部分。具体步骤如下:
1. 确定对象在体素世界中的位置和深度信息。
2. 在渲染体素地形的过程中,根据对象的深度信息,在合适的时机渲染对象。
3. 确保后续渲染的体素地形能够正确遮挡对象被覆盖的部分。

#### 总结
本文深入探讨了体素图形这一新颖的计算机图形技术。从体素图形的基础概念出发,详细介绍了体素数据库的生成方式,包括使用扫描地形地图和算法生成数据。接着阐述了射线投射和高度映射技术,这是实现体素图形显示的核心方法。通过射线旋转和畸变修正,解决了视角和渲染畸变的问题。同时,介绍了查找体素高度和条带缩放的方法,以及如何实现简单的体素显示演示。

为了创建更大的世界,引入了基于图块的体素引擎,通过图块系统有效管理内存。最后,对体素引擎进行了优化,包括代码分析、内联函数、减少绘制次数、使用移位和逻辑运算以及查找表等方法,显著提高了引擎的性能。

以下是体素图形实现的主要流程 mermaid 图:
```mermaid
graph LR
    A[体素图形基础] --> B[生成体素数据库]
    B --> C[射线投射]
    C --> D[高度映射]
    D --> E[射线旋转与畸变修正]
    E --> F[查找体素高度]
    F --> G[条带缩放]
    G --> H[实现简单体素显示演示]
    H --> I[基于图块的体素引擎]
    I --> J[优化体素引擎]
    J --> K[添加前景对象]

体素图形技术在游戏开发、虚拟场景创建等领域具有广阔的应用前景。随着技术的不断发展和优化,相信体素图形将为我们带来更加逼真和精彩的视觉体验。希望本文能为对体素图形感兴趣的开发者提供有价值的参考,激发更多的创新和实践。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值