16、3D图形渲染技术全解析

3D图形渲染技术全解析

1. 3D裁剪的必要性与实现

在3D图形处理中,裁剪是确定多边形或对象是否在有限空间内的过程。3D裁剪体积通常是一个由六个面定义的金字塔或平截头体。虽然许多图形引擎不执行完整的3D裁剪,但这可能会导致问题,例如具有负Z值或零Z值的多边形进行透视投影时会出现除零错误和奇怪的失真。因此,至少需要对多边形进行远、近Z裁剪平面的裁剪。

为了过滤掉完全被裁剪的多边形,避免它们进入主多边形列表,我们可以编写一个函数 Clip_Object_3D() ,该函数有两种处理模式:
- CLIP_Z_MODE :仅裁剪对象多边形的Z范围。
- CLIP_XYZ_MODE :将每个多边形与完整的3D观察体积进行裁剪。

以下是 Clip_Object_3D() 函数的代码:

void Clip_Object_3D(object_ptr the_object, int mode)
{
    // this function clips an object in camera coordinates against the 3D viewing
    // volume. the function has two modes of operation. In CLIP_Z_MODE the
    // function performs only a simple z extend clip with the near and far clipping
    // planes. In CLIP_XYZ_MODE mode the function performs a full 3D clip
    int curr_poly;   // the current polygon being processed
    float x1,y1,z1,
          x2,y2,z2,
          x3,y3,z3,
          x4,y4,z4,  // working variables used to hold vertices
          x1_compare, // used to hold clipping points on x and y
          y1_compare,
          x2_compare,
          y2_compare,
          x3_compare,
          y3_compare,
          x4_compare,
          y4_compare;
    // test if trivial z clipping is being requested
    if (mode==CLIP_Z_MODE)
    {
        // attempt to clip each polygon against viewing volume
        for (curr_poly=0; curr_poly<the_object->num_polys; curr_poly++)
        {
            // extract z components
            z1=the_object->vertices_camera[the_object->polys[curr_poly].vertex_list[0]].z;
            z2=the_object->vertices_camera[the_object->polys[curr_poly].vertex_list[1]].z;
            z3=the_object->vertices_camera[the_object->polys[curr_poly].vertex_list[2]].z;
            // test if this is a quad
            if (the_object->polys[curr_poly].num_points==4)
            {
                // extract 4th z component
                z4=the_object->vertices_camera[the_object->polys[curr_poly].vertex_list[3]].z;
            } // end if quad
            else
                z4=z3;
            // perform near and far z clipping test
            if ( (z1<clip_near_z && z2<clip_near_z && z3<clip_near_z && z4<clip_near_z) ||
                 (z1>clip_far_z && z2>clip_far_z && z3>clip_far_z && z4>clip_far_z) )
            {
                // set clipped flag
                the_object->polys[curr_poly].clipped=1;
            } // end if clipped
        } // end for curr_poly
    } // end if CLIP_Z_MODE
    else
    {
        // CLIP_XYZ_MODE, perform full 3D viewing volume clip
        for (curr_poly=0; curr_poly<the_object->num_polys; curr_poly++)
        {
            // extract x,y and z components
            x1=the_object->vertices_camera[the_object->polys[curr_poly].vertex_list[0]].x;
            y1=the_object->vertices_camera[the_object->polys[curr_poly].vertex_list[0]].y;
            z1=the_object->vertices_camera[the_object->polys[curr_poly].vertex_list[0]].z;
            x2=the_object->vertices_camera[the_object->polys[curr_poly].vertex_list[1]].x;
            y2=the_object->vertices_camera[the_object->polys[curr_poly].vertex_list[1]].y;
            z2=the_object->vertices_camera[the_object->polys[curr_poly].vertex_list[1]].z;
            x3=the_object->vertices_camera[the_object->polys[curr_poly].vertex_list[2]].x;
            y3=the_object->vertices_camera[the_object->polys[curr_poly].vertex_list[2]].y;
            z3=the_object->vertices_camera[the_object->polys[curr_poly].vertex_list[2]].z;
            // test if this is a quad
            if (the_object->polys[curr_poly].num_points==4)
            {
                // extract 4th vertex
                x4=the_object->vertices_camera[the_object->polys[curr_poly].vertex_list[3]].x;
                y4=the_object->vertices_camera[the_object->polys[curr_poly].vertex_list[3]].y;
                z4=the_object->vertices_camera[the_object->polys[curr_poly].vertex_list[3]].z;
                // do clipping tests
                // perform near and far z clipping test first
                if (!((z1>clip_near_z || z2>clip_near_z || z3>clip_near_z || z4>clip_near_z) &&
                      (z1<clip_far_z || z2<clip_far_z || z3<clip_far_z || z4<clip_far_z)) )
                {
                    // set clipped flag
                    the_object->polys[curr_poly].clipped=1;
                    continue;
                } // end if clipped
                // pre-compute x comparison ranges
                x1_compare = (HALF_SCREEN_WIDTH*z1)/viewing_distance;
                x2_compare = (HALF_SCREEN_WIDTH*z2)/viewing_distance;
                x3_compare = (HALF_SCREEN_WIDTH*z3)/viewing_distance;
                x4_compare = (HALF_SCREEN_WIDTH*z4)/viewing_distance;
                // perform x test
                if (!((x1>-x1_compare || x2>-x1_compare || x3>-x3_compare || x4>-x4_compare) &&
                      (x1<x1_compare || x2<x2_compare || x3<x3_compare || x4<x4_compare))  )
                {
                    // set clipped flag
                    the_object->polys[curr_poly].clipped=1;
                    continue;
                } // end if clipped
                // pre-compute x comparison ranges
                y1_compare = (HALF_SCREEN_HEIGHT*z1)/viewing_distance;
                y2_compare = (HALF_SCREEN_HEIGHT*z2)/viewing_distance;
                y3_compare = (HALF_SCREEN_HEIGHT*z3)/viewing_distance;
                y4_compare = (HALF_SCREEN_HEIGHT*z4)/viewing_distance;
                // perform x test
                if (!((y1>-y1_compare || y2>-y1_compare || y3>-y3_compare || y4>-y4_compare) &&
                      (y1<y1_compare || y2<y2_compare || y3<y3_compare || y4<y4_compare))  )
                {
                    // set clipped flag
                    the_object->polys[curr_poly].clipped=1;
                    continue;
                } // end if clipped
            } // end if quad
            else
            {
                // must be triangle, perform clipping tests on only 3 vertices
                // do clipping tests
                // perform near and far z clipping test first
                if (!((z1>clip_near_z || z2>clip_near_z || z3>clip_near_z) &&
                      (z1<clip_far_z || z2<clip_far_z || z3<clip_far_z)) )
                {
                    // set clipped flag
                    the_object->polys[curr_poly].clipped=1;
                    continue;
                } // end if clipped
                // pre-compute x comparison ranges
                x1_compare = (HALF_SCREEN_WIDTH*z1)/viewing_distance;
                x2_compare = (HALF_SCREEN_WIDTH*z2)/viewing_distance;
                x3_compare = (HALF_SCREEN_WIDTH*z3)/viewing_distance;
                // perform x test
                if (!((x1>-x1_compare || x2>-x1_compare || x3>-x3_compare ) &&
                      (x1<x1_compare || x2<x2_compare || x3<x3_compare ))  )
                {
                    // set clipped flag
                    the_object->polys[curr_poly].clipped=1;
                    continue;
                } // end if clipped
                // pre-compute x comparison ranges
                y1_compare = (HALF_SCREEN_HEIGHT*z1)/viewing_distance;
                y2_compare = (HALF_SCREEN_HEIGHT*z2)/viewing_distance;
                y3_compare = (HALF_SCREEN_HEIGHT*z3)/viewing_distance;
                // perform x test
                if (!((y1>-y1_compare || y2>-y1_compare || y3>-y3_compare) &&
                      (y1<y1_compare || y2<y2_compare || y3<y3_compare ))  )
                {
                    // set clipped flag
                    the_object->polys[curr_poly].clipped=1;
                    continue;
                } // end if clipped
            } // end else triangle
        } // end for curr_poly
    } // end else clip everything
} // end Clip_Object_3D
2. 多边形列表的生成

大多数图形引擎使用对象作为基本图形元素,但在某些时候会基于构成每个对象的多边形创建一个多边形列表。这样做的好处是,在进行最终渲染和扫描转换时,处理单个列表比处理多个对象的集合要容易得多。

为了生成多边形列表,我们使用一个数组来存储多边形,并定义了一个新的数据结构 facet_typ 来表示多边形:

// this structure holds a final polygon facet and is self contained
typedef struct facet_typ
{
    int num_points;  // number of vertices
    int color;       // color of polygon
    int shade;       // the final shade of color after lighting
    int shading;     // type of shading to use
    int two_sided;   // is the facet two sided
    int visible;     // is the facet transparent
    int clipped;     // has this poly been clipped
    int active;      // used to turn faces on and off
    point_3d vertex_list[MAX_POINTS_PER_POLY]; // the points that make
                                               // up the polygon facet
    float normal_length;  // holds pre-computed length of normal
} facet, *facet_ptr;

同时,我们编写了 Generate_Poly_List() 函数来生成多边形列表:

void Generate_Poly_List(object_ptr the_object,int mode)
{
    // this function is used to generate the final polygon list that will be
    // rendered. Object by object the list is built up
    int vertex,
        curr_vertex,
        curr_poly;
    // test if this is the first object to be inserted
    if (mode==RESET_POLY_LIST)
    {
        // reset number of polys to zero
        num_polys_frame=0;
        return;
    } // end if first
    // insert all visible polygons into polygon list
    for (curr_poly=0; curr_poly<the_object->num_polys; curr_poly++)
    {
        // test if this poly is visible, if so add it to poly list
        if (the_object->polys[curr_poly].visible &&
            !the_object->polys[curr_poly].clipped)
        {
            // add this poly to poly list
            // first copy data and vertices into an open slot in storage area
            world_poly_storage[num_polys_frame].num_points
                =the_object->polys[curr_poly].num_points;
            world_poly_storage[num_polys_frame].color
                =the_object->polys[curr_poly].color;
            world_poly_storage[num_polys_frame].shade
                = the_object->polys[curr_poly].shade;
            world_poly_storage[num_polys_frame].shading
                = the_object->polys[curr_poly].shading;
            world_poly_storage[num_polys_frame].two_sided
                = the_object->polys[curr_poly].two_sided;
            world_poly_storage[num_polys_frame].visible
                = the_object->polys[curr_poly].visible;
            world_poly_storage[num_polys_frame].clipped
                = the_object->polys[curr_poly].clipped;
            world_poly_storage[num_polys_frame].active
                = the_object->polys[curr_poly].active;
            // now copy vertices
            for (curr_vertex=0; curr_vertex<the_object->polys[curr_poly].num_points;
                 curr_vertex++)
            {
                // extract vertex number
                vertex=the_object->polys[curr_poly].vertex_list[curr_vertex];
                // extract x,y and z
                world_poly_storage[num_polys_frame].vertex_list[curr_vertex].x
                    = the_object->vertices_camera[vertex].x;
                world_poly_storage[num_polys_frame].vertex_list[curr_vertex].y
                    = the_object->vertices_camera[vertex].y;
                world_poly_storage[num_polys_frame].vertex_list[curr_vertex].z
                    = the_object->vertices_camera[vertex].z;
            } // end for curr_vertex
            // assign pointer to it
            world_polys[num_polys_frame] = &world_poly_storage[num_polys_frame];
            // increment number of polys
            num_polys_frame++;
        } // end if poly visible
    } // end for curr_poly
} // end Generate_Poly_List
3. 渲染与隐藏面移除

渲染和隐藏面移除是两个容易混淆的概念。渲染涉及如何绘制多边形,而隐藏面移除则是指移除隐藏的表面。在渲染过程中,某些多边形的部分可能会被其他多边形遮挡,但这些表面并没有被真正移除,只是在渲染过程中被省略或遮挡。

4. 画家算法与深度排序

画家算法的基本思想是,画家在画布上绘画时,先绘制远处的物体,然后绘制近处的物体以遮挡远处的物体。在3D图形中,我们可以使用类似的技术来绘制多边形列表。然而,由于多边形存在于3D空间中,并且有三个或四个顶点,这些顶点的Z值可能不同,因此在排序时需要选择合适的Z值。通常,使用平均Z值能在大多数情况下得到较好的结果。

为了对多边形列表进行Z排序,我们使用快速排序算法 qsort() ,并编写了比较函数 Poly_Compare()

int Poly_Compare(facet **arg1, facet **arg2)
{
    // this function compares the average z's of two polygons and is used by the
    // depth sort surface ordering algorithm
    float z1,z2;
    facet_ptr poly_1,poly_2;
    // dereference the poly pointers
    poly_1 = (facet_ptr)*arg1;
    poly_2 = (facet_ptr)*arg2;
    // compute z average of each polygon
    if (poly_1->num_points==3)
    {
        // compute average of 3 point polygon
        z1 = (float)0.33333*(poly_1->vertex_list[0].z+
                              poly_1->vertex_list[1].z+
                              poly_1->vertex_list[2].z);
    }
    else
    {
        // compute average of 4 point polygon
        z1 = (float)0.25*(poly_1->vertex_list[0].z+
                          poly_1->vertex_list[1].z+
                          poly_1->vertex_list[2].z+
                          poly_1->vertex_list[3].z);
    } // end else
    // now polygon 2
    if (poly_2->num_points==3)
    {
        // compute average of 3 point polygon
        z2 =(float)0.33333*(poly_2->vertex_list[0].z+
                            poly_2->vertex_list[1].z+
                            poly_2->vertex_list[2].z);
    }
    else
    {
        // compute average of 4 point polygon
        z2 = (float)0.25*(poly_2->vertex_list[0].z+
                          poly_2->vertex_list[1].z+
                          poly_2->vertex_list[2].z+
                          poly_2->vertex_list[3].z);
    } // end else
    // compare z1 and z2, such that polys will be sorted in descending Z order
    if (z1>z2)
        return(-1);
    else
        if (z1<z2)
            return(1);
        else
            return(0);
} // end Poly_Compare

然后编写了 Sort_Poly_List() 函数来对多边形列表进行排序:

void Sort_Poly_List(void)
{
    // this function does a simple z sort on the poly list to order surfaces
    // the list is sorted in descending order, i.e. farther polygons first
    qsort((void *)world_polys,num_polys_frame, sizeof(facet_ptr), Poly_Compare);
} // end Sort_Poly_List
5. 透视渲染最终图像

Draw_Poly_List() 函数用于绘制多边形列表,它通过遍历多边形指针列表,将多边形的顶点进行透视投影,并将投影后的2D坐标发送到三角形渲染函数:

void Draw_Poly_List(void)
{
    // this function draws the global polygon list generated by calls to
    // Generate_Poly_List
    int curr_poly,          // the current polygon
        is_quad=0;          // quadrilateral flag
    float x1,y1,z1,         // working variables
          x2,y2,z2,
          x3,y3,z3,
          x4,y4,z4;
    // draw each polygon in list
    for (curr_poly=0; curr_poly<num_polys_frame; curr_poly++)
    {
        // do Z clipping first before projection
        z1=world_polys[curr_poly]->vertex_list[0].z;
        z2=world_polys[curr_poly]->vertex_list[1].z;
        z3=world_polys[curr_poly]->vertex_list[2].z;
        // test if this is a quad
        if (world_polys[curr_poly]->num_points==4)
        {
            // extract vertex number and z component for clipping and projection
            z4=world_polys[curr_poly]->vertex_list[3].z;
            // set quad flag
            is_quad=1;
        } // end if quad
        else
            z4=z3;
#if 0
        // perform z clipping test
        if ( (z1<clip_near_z && z2<clip_near_z && z3<clip_near_z && z4<clip_near_z) ||
             (z1>clip_far_z && z2>clip_far_z && z3>clip_far_z && z4>clip_far_z) )
            continue;
#endif
        // extract points of polygon
        x1 = world_polys[curr_poly]->vertex_list[0].x;
        y1 = world_polys[curr_poly]->vertex_list[0].y;
        x2 = world_polys[curr_poly]->vertex_list[1].x;
        y2 = world_polys[curr_poly]->vertex_list[1].y;
        x3 = world_polys[curr_poly]->vertex_list[2].x;
        y3 = world_polys[curr_poly]->vertex_list[2].y;
        // compute screen position of points
        x1=(HALF_SCREEN_WIDTH  + x1*viewing_distance/z1);
        y1=(HALF_SCREEN_HEIGHT - ASPECT_RATIO*y1*viewing_distance/z1);
        x2=(HALF_SCREEN_WIDTH  + x2*viewing_distance/z2);
        y2=(HALF_SCREEN_HEIGHT - ASPECT_RATIO*y2*viewing_distance/z2);
        x3=(HALF_SCREEN_WIDTH  + x3*viewing_distance/z3);
        y3=(HALF_SCREEN_HEIGHT - ASPECT_RATIO*y3*viewing_distance/z3);
        // draw triangle
        Draw_Triangle_2D((int)x1,(int)y1,(int)x2,(int)y2,(int)x3,(int)y3,
                         world_polys[curr_poly]->shade);
        // draw second poly if this is a quad
        if (is_quad)
        {
            // extract the point
            x4 = world_polys[curr_poly]->vertex_list[3].x;
            y4 = world_polys[curr_poly]->vertex_list[3].y;
            // project to screen
            x4=(HALF_SCREEN_WIDTH  + x4*viewing_distance/z4);
            y4=(HALF_SCREEN_HEIGHT - ASPECT_RATIO*y4*viewing_distance/z4);
            // draw triangle
            Draw_Triangle_2D((int)x1,(int)y1,(int)x3,(int)y3,(int)x4,(int)y4,
                             world_polys[curr_poly]->shade);
        } // end if quad
    } // end for curr_poly
} // end Draw_Poly_List
6. Z缓冲算法

Z缓冲算法是一种在硬件和软件中都广泛使用的渲染技术。其基本思想是,如果确定了每个多边形的每个像素的Z坐标,就可以在像素级别进行排序。对于帧缓冲区中的每个像素,距离观察者最近或Z值最小的像素将被显示。

为了实现Z缓冲算法,我们需要一个与屏幕缓冲区尺寸相同的Z缓冲区,每个元素包含一个Z值。在实际实现中,我们使用“内存银行”方法来处理Z缓冲区,定义了一些全局变量:

int far *z_buffer,   // the current z buffer memory
    far *z_bank_1,   // memory bank 1 of z buffer
    far *z_bank_2;   // memory bank 2 of z buffer
unsigned int z_height    = 200,    // the height of the z buffer
               z_height_2  = 100,    // the height of half the z buffer
               z_bank_size = 64000L; // size of a z buffer bank in bytes

并编写了相关函数来创建、删除和初始化Z缓冲区:

int Create_Z_Buffer(unsigned int height)
{
    // this function allocates the z buffer in two banks
    // set global z buffer values
    z_height   = height;
    z_height_2 = height/2;
    z_bank_size = (height/2)*(unsigned int)640;
    // allocate the memory
    z_bank_1 = (int far *)_fmalloc(z_bank_size);
    z_bank_2 = (int far *)_fmalloc(z_bank_size);
    // return success or failure
    if (z_bank_1 && z_bank_2)
        return(1);
    else
        return(0);
} // end Create_Z_Buffer

void Delete_Z_Buffer(void)
{
    // this function frees up the memory used by the z buffer memory banks
    _ffree(z_bank_1);
    _ffree(z_bank_2);
} // end Delete_Z_Buffer

void Fill_Z_Buffer(int value)
{
    // this function fills the entire z buffer (both banks) with the sent value
    _asm
    {
        ; bank 1
        les di,z_bank_1       ; point es:di to z buffer bank 1
        mov ax,value          ; move the value into ax
        mov cx,z_bank_size    ; number of bytes to fill
        shr cx,1              ; convert to number of words
        rep stosw             ; move the value into z buffer
        ; bank 2
        les di,z_bank_2       ; point es:di to z buffer bank 1
        mov ax,value          ; move the value into ax (redundant)
        mov cx,z_bank_size    ; number of bytes to fill  (so is this)
        shr cx,1              ; convert to number of words
        rep stosw             ; move the value into z buffer
    } // end asm
} // end Fill_Z_Buffer

还编写了 Draw_Tri_3D_Z() Draw_TB_Tri_3D_Z() 函数来绘制Z缓冲三角形:

void Draw_Tri_3D_Z(int x1,int y1,int z1,
                   int x2,int y2,int z2,
                   int x3,int y3,int z3,
                   int color)
{
    // this function sorts the vertices, and splits the triangle into two
    // halves and draws them
    int temp_x,    // used for sorting
        temp_y,
        temp_z,
        new_x,     // used to compute new x and z at triangle splitting point
        new_z;
    // test for h lines and v lines
    if ((x1==x2 && x2==x3)  ||  (y1==y2 && y2==y3))
        return;
    // sort p1,p2,p3 in ascending y order
    if (y2<y1)
    {
        temp_x = x2;
        temp_y = y2;
        temp_z = z2;
        x2     = x1;
        y2     = y1;
        z2     = z1;
        x1     = temp_x;
        y1     = temp_y;
        z1     = temp_z;
    } // end if
    // now we know that p1 and p2 are in order
    if (y3<y1)
    {
        temp_x = x3;
        temp_y = y3;
        temp_z = y3;
        x3     = x1;
        y3     = y1;
        z3     = z1;
        x1     = temp_x;
        y1     = temp_y;
        z1     = temp_z;
    } // end if
    // finally test y3 against y2
    if (y3<y2)
    {
        temp_x = x3;
        temp_y = y3;
        temp_z = z3;
        x3     = x2;
        y3     = y2;
        z3     = z2;
        x2     = temp_x;
        y2     = temp_y;
        z2     = temp_z;
    } // end if
    // do trivial rejection tests
    if ( y3<poly_clip_min_y || y1>poly_clip_max_y ||
         (x1<poly_clip_min_x && x2<poly_clip_min_x && x3<poly_clip_min_x) ||
         (x1>poly_clip_max_x && x2>poly_clip_max_x && x3>poly_clip_max_x) )
        return;
    // test if top of triangle is flat
    if (y1==y2 || y2==y3)
    {
        Draw_TB_Tri_3D_Z(x1,y1,z1,x2,y2,z2,x3,y3,z3,color);
    } // end if
    else
    {
        // general triangle that needs to be broken up along long edge
        // compute new x,z at split point
        new_x = x1 + (int)((float)(y2-y1)*(float)(x3-x1)/(float)(y3-y1));
        new_z = z1 + (int)((float)(y2-y1)*(float)(z3-z1)/(float)(y3-y1));
        // draw each sub-triangle
        if (y2>=poly_clip_min_y && y1<poly_clip_max_y)
            Draw_TB_Tri_3D_Z(x1,y1,z1,new_x,y2,new_z,x2,y2,z2,color);
        if (y3>=poly_clip_min_y && y1<poly_clip_max_y)
            Draw_TB_Tri_3D_Z(x2,y2,z2,new_x,y2,new_z,x3,y3,z3,color);
    } // end else
} // end Draw_Tri_3D_Z

void Draw_TB_Tri_3D_Z(int x1,int y1, int z1,
                      int x2,int y2, int z2,
                      int x3,int y3, int z3,
                      int color)
{
    // this function draws a triangle that has a flat top
    float dx_right,    // the dx/dy ratio of the right edge of line
          dx_left,     // the dx/dy ratio of the left edge of line
          xs,xe,       // the starting and ending points of the edges
          height,      // the height of the triangle
          dx,          // general delta's
          dy,
          z_left,      // the z value of the left edge of current line
          z_right,     // the z value for the right edge of current line
          ay,          // interpolator constant
          b1y,         // the change of z with respect to y on the left edge
          b2y;         // the change of z with respect to y on the right edge
    int temp_x,        // used during sorting as temps
        temp_y,
        temp_z,
        xs_clip,       // used by clipping
        xe_clip,
        x,
        x_index,       // used as looping vars
        y_index;
    // change these two back to float and remove all *32 and >>5
    // if you don't want to use fixed point during horizontal z interpolation
    int z_middle,  // the z value of the middle between the left and right
        bx;        // the change of z with respect to x
    unsigned char far *dest_addr; // current image destination
    // test order of x1 and x2, note y1=y2.
    // test if top or bottom is flat and set constants appropriately
    if (y1==y2)
    {
        // perform computations for a triangle with a flat top
        if (x2 < x1)
        {
            temp_x = x2;
            temp_z = z2;
            x2     = x1;
            z2     = z1;
            x1     = temp_x;
            z1     = temp_z;
        } // end if swap
        // compute deltas for scan conversion
        height = y3-y1;
        dx_left  = (x3-x1)/height;
        dx_right = (x3-x2)/height;
        // compute deltas for z interpolation
        z_left  = z1;
        z_right = z2;
        // vertical interpolants
        ay  = 1/height;
        b1y = ay*(z3-z1);
        b2y = ay*(z3-z2);
        // set starting points
        xs = (float)x1;
        xe = (float)x2;
    } // end top is flat
    else
    { // bottom must be flat
        // test order of x3 and x2, note y2=y3.
        if (x3 < x2)
        {
            temp_x = x2;
            temp_z = z2;
            x2     = x3;
            z2     = z3;
            x3     = temp_x;
            z3     = temp_z;
        } // end if swap
        // compute deltas for scan conversion
        height = y3-y1;
        dx_left  = (x2-x1)/height;
        dx_right = (x3-x1)/height;
        // compute deltas for z interpolation
        z_left  = z1;
        z_right = z1;
        // vertical interpolants
        ay  = 1/height;
        b1y = ay*(z2-z1);
        b2y = ay*(z3-z1);
        // set starting points
        xs = (float)x1;
        xe = (float)x1;
    } // end else bottom is flat
    // perform y clipping
    // clip top
    if (y1<poly_clip_min_y)
    {
        // compute new xs and ys
        dy = (float)(-y1+poly_clip_min_y);
        xs = xs+dx_left*dy;
        xe = xe+dx_right*dy;
        // re-compute z_left and z_right to take into consideration
        // vertical shift down
        z_left  += b1y*dy;
        z_right += b2y*dy;
        // reset y1
        y1=poly_clip_min_y;
    } // end if top is off screen
    // clip bottom
    if (y3>poly_clip_max_y)
        y3=poly_clip_max_y;
    // compute starting address in video memory
    dest_addr = double_buffer+(y1<<8)+(y1<<6);
    // start z buffer at proper bank
    if (y1<z_height_2)
        z_buffer = z_bank_1+(y1<<8)+(y1<<6);
    else
    {
        temp_y = y1-z_height_2;
        z_buffer = z_bank_2+(temp_y<<8)+(temp_y<<6);
    } // end else
    // test if x clipping is needed
    if (x1>=poly_clip_min_x && x1<=poly_clip_max_x &&
        x2>=poly_clip_min_x && x2<=poly_clip_max_x &&
        x3>=poly_clip_min_x && x3<=poly_clip_max_x)
    {
        // draw the triangle
        for (y_index=y1; y_index<=y3; y_index++)
        {
            // test if we need to switch to z buffer bank two
            if (y_index==z_height_2)
                z_buffer = z_bank_2;
            // compute horizontal z interpolant
            z_middle = 32*z_left;
            // z_middle = z_left;
            bx = 32*(z_right - z_left)/(1+xe-xs);
            // bx = (z_right - z_left)/(1+xe-xs);
            // draw the line
            for (x_index=xs; x_index<=xe; x_index++)
            {
                // if current z_middle is less than z-buffer then replace
                // and update image buffer
                if (z_middle>>5 < z_buffer[x_index])
                {
                    // update z buffer
                    z_buffer[x_index]=(int)z_middle>>5;
                    // z_buffer[x_index]=(int)z_middle;
                    // write to image buffer
                    dest_addr[x_index] = color;
                    // update video buffer
                } // end if update buffer
                // update current z value
                z_middle += bx;
            } // end draw z buffered line
            // adjust starting point and ending point for scan conversion
            xs+=dx_left;
            xe+=dx_right;
            // adjust vertical z interpolants
            z_left  += b1y;
            z_right += b2y;
            // adjust video and z buffer offsets
            dest_addr += 320;
            z_buffer  += 320;
        } // end for
    } // end if no x clipping needed
    else
    {
        // clip x axis with slower version
        // draw the triangle
        for (y_index=y1; y_index<=y3; y_index++)
        {
            // test if we need to switch to z buffer bank two
            if (y_index==z_height_2)
                z_buffer = z_bank_2;
            // do x clip
            xs_clip  = (int)xs;
            xe_clip  = (int)xe;
            // compute horizontal z interpolant
            z_middle = 32*z_left;
            // z_middle = z_left;
            bx = 32*(z_right - z_left)/(1+xe-xs);
            // bx = 32*(z_right - z_left)/(1+xe-xs);
            // adjust starting point and ending point
            xs+=dx_left;
            xe+=dx_right;
            // adjust vertical z interpolants
            z_left  += b1y;
            z_right += b2y;
            // clip line
            if (xs_clip < poly_clip_min_x)
            {
                dx = (-xs_clip + poly_clip_min_x);
                xs_clip = poly_clip_min_x;
                // re-compute z_middle to take into consideration horizontal shift
                z_middle += 32*bx*dx;
                // z_middle += bx*dx;
            } // end if line is clipped on left
            if (xe_clip > poly_clip_max_x)
            {
                xe_clip = poly_clip_max_x;
            } // end if line is clipped on right
            // draw the line
            for (x_index=xs_clip; x_index<=xe_clip; x_index++)
            {
                // if current z_middle is less than z-buffer then replace
                // and update image buffer
                if (z_middle>>5 < z_buffer[x_index])
                {
                    // update z buffer
                    z_buffer[x_index]=(int)z_middle>>5;
                    // z_buffer[x_index]=(int)z_middle;
                    // write to image buffer
                    dest_addr[x_index] = color;
                    // update video buffer
                } // end if update z buffer
                // update current z value
                z_middle += bx;
            } // end draw z buffered line
            // adjust video and z buffer offsets
            dest_addr += 320;
            z_buffer  += 320;
        } // end for y_index
    } // end else x clipping needed
} // end Draw_TB_Tri_3D_Z
7. 垂直扫描线Z缓冲

垂直扫描线Z缓冲算法是一种按行处理而不是按像素处理的Z缓冲算法。在类似《黑暗力量》或《毁灭战士》的游戏中,所有墙壁都是垂直的,因此垂直列中的每个像素的Z值是恒定的。基于Z缓冲算法,可以推导出一种只计算每条垂直扫描线的Z值的算法,此时Z缓冲区只需要足够大来容纳与屏幕宽度相等的元素数量。

8. 二叉空间分割(BSP)

二叉空间分割(BSP)是一种3D渲染算法,它通过牺牲初始空间、计算量和一些几何约束来换取更快的运行时性能。其基本原理是,输入一组多边形,通过递归算法创建一个二叉树结构,该BSP树可以在运行时作为从任何视点绘制多边形的地图。

创建BSP树的大致算法如下:
1. 选择列表中的一个多边形作为分割平面,如果列表中没有多边形,则退出。
2. 创建两个子列表,分别连接到分割平面的前向和后向指针,将位于分割平面前面和后面的多边形分别放入这两个列表中。
3. 递归处理前向列表。
4. 递归处理后向列表。

为了实现BSP树,我们定义了存储墙的数据结构 wall_typ

typedef struct wall_typ
{
    int id;                  // used for debugging
    int color;               // color of wall
    point_3d wall_world[4];  // the points that make up the wall
    point_3d wall_camera[4]; // the final camera coordinates of the wall
    vector_3d normal;        // the outward normal to the wall used during
                             // creation of BSP only, after that it becomes
                             // invalid
    struct wall_typ *link;   // pointer to next wall
    struct wall_typ *front;  // pointer to walls in front
    struct wall_typ *back;   // pointer to walls behind
} wall, *wall_ptr;

并编写了 Build_Bsp_Tree() 函数来创建BSP树:

void Build_Bsp_Tree(wall_ptr root)
{
    // this function recursively builds the bsp tree from the sent wall list
    // note the function has some calls to Draw_Line() and a Time_Delay() at
    // the end, these are for illustrative purposes only for the demo interface
    // and should be removed if you wish to use this function in a real
    // application
    static wall_ptr next_wall,     // pointer to next wall to be processed
                    front_wall,    // the front wall
                    back_wall,     // the back wall
                    temp_wall;     // a temporary wall
    static float
        dot_wall_1,                // dot products for test wall
        dot_wall_2,
        wall_x0,wall_y0,wall_z0,   // working vars for test wall
        wall_x1,wall_y1,wall_z1,
        pp_x0,pp_y0,pp_z0,         // working vars for partitioning plane
        pp_x1,pp_y1,pp_z1,
        xi,zi;                     // points of intersection when the partitioning
                                   // plane cuts a wall in two
    static vector_3d test_vector_1,  // test vectors from the partitioning plane
                    test_vector_2;        // to the test wall to test the side
                                       // of the partitioning plane the test wall
                                       // lies on
    static int front_flag =0,        // flags if a wall is on the front or back
                   back_flag = 0,        // of the partitioning plane
                   index;                // looping index
    // SECTION 1 ////////////////////////////////////////////////////////////////
    // test if this tree is complete
    if (root==NULL)
        return;
    // the root is the partitioning plane, partition the polygons using it
    next_wall  = root->link;
    root->link = NULL;
    // extract top two vertices of partitioning plane wall for ease of calculations
    pp_x0 = root->wall_world[0].x;
    pp_y0 = root->wall_world[0].y;
    pp_z0 = root->wall_world[0].z;
    pp_x1 = root->wall_world[1].x;
    pp_y1 = root->wall_world[1].y;
    pp_z1 = root->wall_world[1].z;
    // highlight space partition green
    Draw_Line(pp_x0/WORLD_SCALE_X-SCREEN_TO_WORLD_X,
              pp_z0/WORLD_SCALE_Z-SCREEN_TO_WORLD_Z,
              pp_x1/WORLD_SCALE_X-SCREEN_TO_WORLD_X,
              pp_z1/WORLD_SCALE_Z-SCREEN_TO_WORLD_Z,
              10,
              video_buffer);
    // SECTION 2  ////////////////////////////////////////////////////////////////
    // test if all walls have been partitioned
    while(next_wall)
    {
        // test which side test wall is relative to partitioning plane
        // defined by root
        // first compute vectors from point on partitioning plane to point on
        // test wall
        Make_Vector_3D((point_3d_ptr)&root->wall_world[0],
                       (point_3d_ptr)&next_wall->wall_world[0],
                       (vector_3d_ptr)&test_vector_1);
        Make_Vector_3D((point_3d_ptr)&root->wall_world[0],
                       (point_3d_ptr)&next_wall->wall_world[1],
                       (vector_3d_ptr)&test_vector_2);
        // now dot each test vector with the surface normal and analyze signs
        dot_wall_1 = Dot_Product_3D((vector_3d_ptr)&test_vector_1,
                                    (vector_3d_ptr)&root->normal);
        dot_wall_2 = Dot_Product_3D((vector_3d_ptr)&test_vector_2,
                                    (vector_3d_ptr)&root->normal);
        // SECTION 3 ////////////////////////////////////////////////////////////////
        // perform the tests
        // case 0, the partitioning plane and the test wall have a point in common
        // this is a special case and must be accounted for, in the code
        // we will set a pair of flags and then the next case will handle
        // the actual insertion of the wall into BSP
        // reset flags
        front_flag = back_flag = 0;
        // determine if wall is tangent to endpoints of partitioning wall
        if (POINTS_EQUAL_3D(root->wall_world[0],next_wall->wall_world[0]) )
        {
            // p0 of partitioning plane is the same at p0 of test wall
            // we only need to see what side p1 of test wall is on
            if (dot_wall_2 > 0)
                front_flag = 1;
            else
                back_flag = 1;
        } // end if
        else
            if (POINTS_EQUAL_3D(root->wall_world[0],next_wall->wall_world[1]) )
            {
                // p0 of partitioning plane is the same at p1 of test wall
                // we only need to see what side p0 of test wall is on
                if (dot_wall_1 > 0)
                    front_flag = 1;
                else
                    back_flag = 1;
            } // end if
            else
                if (POINTS_EQUAL_3D(root->wall_world[1],next_wall->wall_world[0]) )
                {
                    // p1 of partitioning plane is the same at p0 of test wall
                    // we only need to see what side p1 of test wall is on
                    if (dot_wall_2 > 0)
                        front_flag = 1;
                    else
                        back_flag = 1;
                } // end if
                else
                    if (POINTS_EQUAL_3D(root->wall_world[1],next_wall->wall_world[1]) )
                    {
                        // p1 of partitioning plane is the same at p1 of test wall
                        // we only need to see what side p0 of test wall is on
                        if (dot_wall_1 > 0)
                            front_flag = 1;
                        else
                            back_flag = 1;
                    } // end if
        // SECTION 4 ////////////////////////////////////////////////////////////////
        // case 1 both signs are the same or the front or back flag has been set
        if ( (dot_wall_1 >= 0 && dot_wall_2 >= 0) || front_flag )
        {
            // highlight the wall blue
            Draw_Line(next_wall->wall_world[0].x/WORLD_SCALE_X-SCREEN_TO_WORLD_X,
                      next_wall->wall_world[0].z/WORLD_SCALE_Z-SCREEN_TO_WORLD_Z,
                      next_wall->wall_world[1].x/WORLD_SCALE_X-SCREEN_TO_WORLD_X,
                      next_wall->wall_world[1].z/WORLD_SCALE_Z-SCREEN_TO_WORLD_Z,
                      9,
                      video_buffer);
            // place this wall on the front list
            if (root->front==NULL)
            {
                // this is the first node
                root->front      = next_wall;
                next_wall        = next_wall->link;
                front_wall       = root->front;
                front_wall->link = NULL;
            } // end if
            else
            {
                // this is the nth node
                front_wall->link = next_wall;
                next_wall        = next_wall->link;
                front_wall       = front_wall->link;
                front_wall->link = NULL;
            } // end else
        } // end if both positive
        // SECTION 5 ////////////////////////////////////////////////////////////////
        else
            if ( (dot_wall_1 < 0 && dot_wall_2 < 0) || back_flag)
            {
                // highlight the wall red
                Draw_Line(next_wall->wall_world[0].x/WORLD_SCALE_X-SCREEN_TO_WORLD_X,
                          next_wall->wall_world[0].z/WORLD_SCALE_Z-SCREEN_TO_WORLD_Z,
                          next_wall->wall_world[1].x/WORLD_SCALE_X-SCREEN_TO_WORLD_X,
                          next_wall->wall_world[1].z/WORLD_SCALE_Z-SCREEN_TO_WORLD_Z,
                          12,
                          video_buffer);
                // place this wall on the back list
                if (root->back==NULL)
                {
                    // this is the first node
                    root->back      = next_wall;
                    next_wall       = next_wall->link;
                    back_wall       = root->back;
                    back_wall->link = NULL;
                } // end if
                else
                {
                    // this is the nth node
                    back_wall->link = next_wall;
                    next_wall       = next_wall->link;
                    back_wall       = back_wall->link;
                    back_wall->link = NULL;
                } // end else
            } // end if both negative
        // case 2 both signs are different
        // SECTION 6 ////////////////////////////////////////////////////////////////
        else
            if ( (dot_wall_1 < 0 && dot_wall_2 >= 0) ||
                 (dot_wall_1 >= 0 && dot_wall_2 < 0))
            {
                // the partitioning plane cuts the wall in half, the wall
                // must be split into two walls
                // extract top two vertices of test wall for ease of calculations
                wall_x0 = next_wall->wall_world[0].x;
                wall_y0 = next_wall->wall_world[0].y;
                wall_z0 = next_wall->wall_world[0].z;
                wall_x1 = next_wall->wall_world[1].x;
                wall_y1 = next_wall->wall_world[1].y;
                wall_z1 = next_wall->wall_world[1].z;
                // compute the point of intersection between the walls
                // note that x and z are the plane that the intersection takes place in
                Intersect_Lines(wall_x0,wall_z0,wall_x1,wall_z1,
                                pp_x0,pp_z0,pp_x1,pp_z1,
                                &xi,&zi);
                // here comes the tricky part, we need to split the wall in half and
                // create two walls. We'll do this by creating two new walls,
                // placing them on the appropriate front and back lists and
                // then deleting the original wall
                // process first wall
                // allocate the memory for the wall
                temp_wall = (wall_ptr)malloc(sizeof(wall));
                temp_wall->front = NULL;
                temp_wall->back  = NULL;
                temp_wall->link  = NULL;
                temp_wall->normal = next_wall->normal;
                temp_wall->id     = next_wall->id+1000; // add 1000 to denote a split
                // compute wall vertices
                for (index=0; index<4; index++)
                {
                    temp_wall->wall_world[index].x = next_wall->wall_world[index].x;
                    temp_wall->wall_world[index].y = next_wall->wall_world[index].y;
                    temp_wall->wall_world[index].z = next_wall->wall_world[index].z;
                } // end for index
                // now

#### 9. BSP树的遍历与相关处理
遍历BSP树时,我们使用修改后的中序遍历算法,根据视点相对于每个测试多边形的位置来引导遍历分支。以下是遍历BSP树并将多边形添加到全局多边形列表的函数`Bsp_Traverse()`:
```c
void Bsp_Traverse(wall_ptr root)
{
    // this function traverses the BSP tree and generates the polygon list used
    // by Draw_Polys() for the current global viewpoint (note the view angle is
    // irrelevant), also as the polygon list is being generated, only polygons
    // that are within the z extents are added to the polygon, in essence, the
    // function is performing Z clipping also, this is to minimize the amount
    // of polygons in the graphics pipeline that will have to be processed during
    // rendering
    // this function works by testing the viewpoint against the current wall
    // in the bsp, then depending on the side the viewpoint is the algorithm
    // proceeds. the search takes place as the rest using an "inorder" method
    // with hooks to process and add each node into the polygon list at the
    // right time
    static vector_3d test_vector;
    static float dot_wall,
                 z1,z2,z3,z4;
    // SECTION 1 ////////////////////////////////////////////////////////////////
    // is this a dead end?
    if (root==NULL)
        return;
    // test which side viewpoint is on relative to the current wall
    Make_Vector_3D((point_3d_ptr)&root->wall_world[0],
                   (point_3d_ptr)&view_point,
                   (vector_3d_ptr)&test_vector);
    // now dot test vector with the surface normal and analyze signs
    dot_wall = Dot_Product_3D((vector_3d_ptr)&test_vector,
                              (vector_3d_ptr)&root->normal);
    // SECTION 2 ////////////////////////////////////////////////////////////////
    // if the sign of the dot product is positive then the viewer is on the
    // front side of current wall, so recursively process the walls behind then
    // in front of this wall, else do the opposite
    if (dot_wall>0)
    {
        // viewer is in front of this wall
        // process the back wall sub tree
        Bsp_Traverse(root->back);
        // try to add this wall to the polygon list if it's within the Z extents
        z1=root->wall_camera[0].z;
        z2=root->wall_camera[1].z;
        z3=root->wall_camera[2].z;
        z4=root->wall_camera[3].z;
        // perform the z extents clipping test
        if ( (z1>clip_near_z && z1<clip_far_z ) || (z2>clip_near_z && z2<clip_far_z ) ||
             (z3>clip_near_z && z3<clip_far_z ) || (z4>clip_near_z && z4<clip_far_z ))
        {
            // first copy data and vertices into an open slot in storage area
            world_poly_storage[num_polys_frame].num_points = 4;
            world_poly_storage[num_polys_frame].color      = BSP_WALL_COLOR;
            world_poly_storage[num_polys_frame].shade      = root->color;
            world_poly_storage[num_polys_frame].shading    = 0;
            world_poly_storage[num_polys_frame].two_sided  = 1;
            world_poly_storage[num_polys_frame].visible    = 1;
            world_poly_storage[num_polys_frame].clipped    = 0;
            world_poly_storage[num_polys_frame].active     = 1;
            // now copy vertices
            world_poly_storage[num_polys_frame].vertex_list[0].x = root->wall_camera[0].x;
            world_poly_storage[num_polys_frame].vertex_list[0].y = root->wall_camera[0].y;
            world_poly_storage[num_polys_frame].vertex_list[0].z = root->wall_camera[0].z;
            world_poly_storage[num_polys_frame].vertex_list[1].x = root->wall_camera[1].x;
            world_poly_storage[num_polys_frame].vertex_list[1].y = root->wall_camera[1].y;
            world_poly_storage[num_polys_frame].vertex_list[1].z = root->wall_camera[1].z;
            world_poly_storage[num_polys_frame].vertex_list[2].x = root->wall_camera[2].x;
            world_poly_storage[num_polys_frame].vertex_list[2].y = root->wall_camera[2].y;
            world_poly_storage[num_polys_frame].vertex_list[2].z = root->wall_camera[2].z;
            world_poly_storage[num_polys_frame].vertex_list[3].x = root->wall_camera[3].x;
            world_poly_storage[num_polys_frame].vertex_list[3].y = root->wall_camera[3].y;
            world_poly_storage[num_polys_frame].vertex_list[3].z = root->wall_camera[3].z;
            // assign poly list pointer to it
            world_polys[num_polys_frame] = &world_poly_storage[num_polys_frame];
            // increment number of polys in this frame
            num_polys_frame++;
        } // end if polygon is visible
        // now process the front walls sub tree
        Bsp_Traverse(root->front);
    } // end if
    // SECTION 3 ////////////////////////////////////////////////////////////////
    else
    {
        // viewer is behind this wall
        // process the front wall sub tree
        Bsp_Traverse(root->front);
        // try to add this wall to the polygon list if it's within the Z extents
        z1=root->wall_camera[0].z;
        z2=root->wall_camera[1].z;
        z3=root->wall_camera[2].z;
        z4=root->wall_camera[3].z;
        // perform the z extents clipping test
        if ( (z1>clip_near_z && z1<clip_far_z ) || (z2>clip_near_z && z2<clip_far_z )  ||
             (z3>clip_near_z && z3<clip_far_z ) || (z4>clip_near_z && z4<clip_far_z ))
        {
            // first copy data and vertices into an open slot in storage area
            world_poly_storage[num_polys_frame].num_points = 4;
            world_poly_storage[num_polys_frame].color      = BSP_WALL_COLOR;
            world_poly_storage[num_polys_frame].shade      = root->color;
            world_poly_storage[num_polys_frame].shading    = 0;
            world_poly_storage[num_polys_frame].two_sided  = 1;
            world_poly_storage[num_polys_frame].visible    = 1;
            world_poly_storage[num_polys_frame].clipped    = 0;
            world_poly_storage[num_polys_frame].active     = 1;
            // now copy vertices, note that we don't use a structure copy, it's
            // not dependable
            world_poly_storage[num_polys_frame].vertex_list[0].x = root->wall_camera[0].x;
            world_poly_storage[num_polys_frame].vertex_list[0].y = root->wall_camera[0].y;
            world_poly_storage[num_polys_frame].vertex_list[0].z = root->wall_camera[0].z;
            world_poly_storage[num_polys_frame].vertex_list[1].x = root->wall_camera[1].x;
            world_poly_storage[num_polys_frame].vertex_list[1].y = root->wall_camera[1].y;
            world_poly_storage[num_polys_frame].vertex_list[1].z = root->wall_camera[1].z;
            world_poly_storage[num_polys_frame].vertex_list[2].x = root->wall_camera[2].x;
            world_poly_storage[num_polys_frame].vertex_list[2].y = root->wall_camera[2].y;
            world_poly_storage[num_polys_frame].vertex_list[2].z = root->wall_camera[2].z;
            world_poly_storage[num_polys_frame].vertex_list[3].x = root->wall_camera[3].x;
            world_poly_storage[num_polys_frame].vertex_list[3].y = root->wall_camera[3].y;
            world_poly_storage[num_polys_frame].vertex_list[3].z = root->wall_camera[3].z;
            // assign poly list pointer to it
            world_polys[num_polys_frame] = &world_poly_storage[num_polys_frame];
            // increment number of polys in this frame
            num_polys_frame++;
        } // end if polygon is visible
        // now process the front walls sub tree
        Bsp_Traverse(root->back);
    } // end else
} // end Bsp_Traverse

该函数的主要步骤如下:
1. 检查节点是否为空 :如果当前节点为空,则退出。
2. 计算视点位置 :通过点积计算视点相对于当前节点的位置。
3. 根据点积结果选择遍历方式 :如果点积为正,先递归处理后向墙子树,再尝试将当前墙添加到多边形列表,最后递归处理前向墙子树;否则,先递归处理前向墙子树,再进行类似操作,最后递归处理后向墙子树。

此外,还需要将墙的世界坐标转换为相机坐标,我们编写了 Bsp_World_To_Camera() 函数:

void Bsp_World_To_Camera(wall_ptr root)
{
    // this function traverses the bsp tree and converts the world coordinates
    // to camera coordinates using the global transformation matrix. note the
    // function is recursive and uses an inorder traversal, other traversals
    // such as preorder and postorder will work just as well...
    static int index; // looping variable
    // test if we have hit a dead end
    if (root==NULL)
        return;
    // transform back most sub-tree
    Bsp_World_To_Camera(root->back);
    // iterate thru all vertices of current wall and transform them into
    // camera coordinates
    for (index=0; index<4; index++)
    {
        // multiply the point by the viewing transformation matrix
        // x component
        root->wall_camera[index].x =
            root->wall_world[index].x * global_view[0][0] +
            root->wall_world[index].y * global_view[1][0] +
            root->wall_world[index].z * global_view[2][0] +
            global_view[3][0];
        // y component
        root->wall_camera[index].y =
            root->wall_world[index].x * global_view[0][1] +
            root->wall_world[index].y * global_view[1][1] +
            root->wall_world[index].z * global_view[2][1] +
            global_view[3][1];
        // z component
        root->wall_camera[index].z =
            root->wall_world[index].x * global_view[0][2] +
            root->wall_world[index].y * global_view[1][2] +
            root->wall_world[index].z * global_view[2][2] +
            global_view[3][2];
    } // end for index
    // transform front most sub-tree
    Bsp_World_To_Camera(root->front);
} // end Bsp_World_To_Camera

该函数使用中序遍历递归地将BSP树中所有墙的世界坐标转换为相机坐标。

对于BSP树中的墙,我们还编写了递归的平面着色函数 Bsp_Shade()

void Bsp_Shade(wall_ptr root)
{
    // this function shades the bsp tree and need only be called if the global
    // lightsource changes position
    static int index; // looping variable
    static float normal_length, // length of surface normal
                 intensity,     // intensity of light falling on surface being processed
                 dp;            // result of dot product
    // test if we have hit a dead end
    if (root==NULL)
        return;
    // shade the back most sub-tree
    Bsp_Shade(root->back);
    // compute the dot product between line of sight vector and normal to surface
    dp = Dot_Product_3D((vector_3d_ptr)&root->normal,(vector_3d_ptr)&light_source);
    // compute length of normal of surface normal, remember this function
    // doesn't need to be time critical since it is only called once at startup
    // or whenever the light source moves
    normal_length = Vector_Mag_3D((vector_3d_ptr)&root->normal);
    // cos 0 = (u.v)/|u||v| or
    intensity = ambient_light + (15*dp/normal_length);
    // test if intensity has overflowed
    if (intensity >15)
        intensity = 15;
    // intensity now varies from 0-1, 0 being black or grazing and 1 being
    // totally illuminated. use the value to index into color table
    root->color = BSP_WALL_SHADE - (int)(fabs(intensity));
    // shade the front most sub-tree
    Bsp_Shade(root->front);
} // end Bsp_Shade

该函数用于对BSP树中的墙进行着色,只有当全局光源位置改变时才需要调用。

同时,我们还编写了平移函数 Bsp_Translate() 和删除函数 Bsp_Delete()

void Bsp_Translate(wall_ptr root,int x_trans,int y_trans,int z_trans)
{
    // this function translates all the walls that make up the bsp world
    // note function is recursive, we don't really need this function, but
    // it's a good example of how we might perform transformations on the BSP
    // tree and similar tree like structures using recursion
    static int index; // looping variable
    // test if we have hit a dead end
    if (root==NULL)
        return;
    // translate back most sub-tree
    Bsp_Translate(root->back,x_trans,y_trans,z_trans);
    // iterate thru all vertices of current wall and translate them
    for (index=0; index<4; index++)
    {
        // perform translation
        root->wall_world[index].x+=x_trans;
        root->wall_world[index].y+=y_trans;
        root->wall_world[index].z+=z_trans;
    } // end for index
    // translate front most sub-tree
    Bsp_Translate(root->front,x_trans,y_trans,z_trans);
} // end Bsp_Translate

void Bsp_Delete(wall_ptr root)
{
    // this function recursively deletes all the nodes in the bsp tree and frees
    // the memory back to the OS.
    wall_ptr temp_wall; // a temporary wall
    // test if we have hit a dead end
    if (root==NULL)
        return;
    // delete back sub tree
    Bsp_Delete(root->back);
    // delete this node, but first save the front sub-tree
    temp_wall = root->front;
    // delete the memory
    free(root);
    // assign the root to the saved front most sub-tree
    root = temp_wall;
    // delete front sub tree
    Bsp_Delete(root);
} // end Bsp_Delete
10. 将BSP树添加到图形管线

将BSP树添加到图形管线的步骤如下:
1. 计算全局世界到相机的变换矩阵。
2. 将BSP树的世界坐标转换为相机坐标。
3. 重置多边形列表中的多边形数量。
4. 遍历BSP树,生成多边形列表。
5. 绘制多边形列表。
6. 显示双缓冲。

以下是实现这些步骤的 Bsp_View() 函数:

void Bsp_View(wall_ptr bsp_root)
{
    // this function is a self contained viewing processor that has its own event
    // loop, the display will continue to be generated until the ESC key is pressed
    int done=0;
    // install the isr keyboard driver
    Keyboard_Install_Driver();
    // change the light source direction
    light_source.x =(float)0.398636;
    light_source.y =(float)-0.374248;
    light_source.z =(float)0.8372275;
    // reset viewpoint to (0,0,0)
    view_point.x = 0;
    view_point.y = 0;
    view_point.z = 0;
    // main event loop
    while(!done)
    {
        // compute starting time of this frame
        starting_time = Timer_Query();
        // erase all objects
        Fill_Double_Buffer(0);
        // move viewpoint
        if (keyboard_state[MAKE_UP])
            view_point.y+=20;
        if (keyboard_state[MAKE_DOWN])
            view_point.y-=20;
        if (keyboard_state[MAKE_RIGHT])
            view_point.x+=20;
        if (keyboard_state[MAKE_LEFT])
            view_point.x-=20;
        if (keyboard_state[MAKE_KEYPAD_PLUS])
            view_point.z+=20;
        if (keyboard_state[MAKE_KEYPAD_MINUS])
            view_point.z-=20;
        if (keyboard_state[MAKE_Z])
            if ((view_angle.ang_x+=10)>360)
                view_angle.ang_x = 0;
        if (keyboard_state[MAKE_A])
            if ((view_angle.ang_x-=10)<0)
                view_angle.ang_x = 360;
        if (keyboard_state[MAKE_X])
            if ((view_angle.ang_y+=10)>360)
                view_angle.ang_y = 0;
        if (keyboard_state[MAKE_S])
            if ((view_angle.ang_y-=10)<0)
                view_angle.ang_y = 360;
        if (keyboard_state[MAKE_C])
            if ((view_angle.ang_z+=10)>360)
                view_angle.ang_z = 0;
        if (keyboard_state[MAKE_D])
            if ((view_angle.ang_z-=10)<0)
                view_angle.ang_z = 360;
        if (keyboard_state[MAKE_ESC])
            done=1;
        // now that user has possible moved viewpoint, create the global
        // world to camera transformation matrix
        Create_World_To_Camera();
        // now convert the bsp tree world coordinates into camera coordinates
        Bsp_World_To_Camera(bsp_root);
        // reset number of polygons in polygon list
        num_polys_frame = 0;
        // traverse the BSP tree and generate the polygon list
        Bsp_Traverse(bsp_root);
        // draw the polygon list generated by traversing the BSP tree
        Draw_Poly_List();
        // display double buffer
        Display_Double_Buffer(double_buffer,0);
        // lock onto 18 frames per second max
        while((Timer_Query()-starting_time)<1);
    } // end while
    // restore the old keyboard driver
    Keyboard_Remove_Driver();
} // end Bsp_View
11. BSP演示程序

BSP演示程序 BSPDEMO.EXE 允许使用鼠标绘制由墙组成的宇宙的顶视图。其主要操作规则如下:
- 最多绘制64条线(墙)。
- 线不能相交。
- 线可以有公共端点。

操作方式如下:
- 绘制墙 :将鼠标光标移动到所需位置,点击左键,然后移动到墙的端点并再次点击左键,即可绘制代表墙的线。若不想完成绘制,点击鼠标右键。
- 删除墙 :选择“Del Wall”,光标将变为带斜线的红色圆圈,将鼠标光标移到要删除的线上并点击左键。
- 清除编辑器 :点击“Clear”按钮。
- 创建BSP树 :按“Build BSP”,屏幕将显示创建过程,完成后按空格键继续。
- 打印BSP树 :点击“Print BSP”,程序将使用中序搜索打印BSP树的所有节点,输出将保存到 BSPDEMO.OUT 文件中。
- 查看3D BSP树宇宙 :构建BSP后,点击“View”按钮,可实时在宇宙中飞行,使用与其他3D程序相同的控制界面移动视点和视角,按“ESC”退出。

12. BSP的局限性

BSP树虽然在运行时确定多边形渲染顺序方面非常快速,但也存在一些局限性:
- 宇宙必须半静态 :多边形不能旋转,但在某些情况下可以平移。例如,在类似《毁灭战士》或《黑暗力量》的游戏中,墙通常移动较少,如果移动,通常是门,可以使用此技术支持。
- 不适合动态场景 :对于模拟器或有大量运动的游戏,由于每帧都需要重新生成BSP,因此不适合使用BSP树。

13. 技术的比较分析

在选择渲染技术时,需要根据游戏的具体情况进行综合考虑:
| 技术 | 适用场景 | 优点 | 缺点 |
| — | — | — | — |
| 画家算法和深度排序 | 主要包含凸对象和几何体的游戏,适合初学者 | 实现简单 | 处理复杂场景时可能出现排序错误 |
| Z缓冲 | 处理极其复杂的几何体和相交情况 | 能保证正确的渲染顺序 | 内存和计算开销大 |
| 二叉空间分割(BSP) | 玩家在静态环境中移动的游戏 | 运行时速度快 | 宇宙必须半静态,不适合动态场景 |

14. 向3D世界添加其他实体

我们的3D引擎目前完全基于多边形,但大多数3D游戏还包含其他类型的几何体,如点、线和2D精灵。

添加点

添加点到3D引擎很容易,只需添加新的点类型,对3D对象进行操作时,对这些点应用相同的操作。在渲染时,可以将每个点视为一个无限小的多边形,但为了避免减慢渲染管道,对于一些特效(如爆炸),可以在完成复杂的多边形渲染操作后再绘制点。

添加线

添加线的方式与添加点类似,但在渲染时,需要将线转换为特殊标记的多边形,并在 Draw_Poly_List() 函数中使用特殊过滤器进行扫描转换。

15. 3D游戏的动画模型

3D游戏的基本动画模型可以概括为以下循环:

graph LR
    A[擦除屏幕] --> B[变换3D对象]
    B --> C[绘制帧]
    C --> A

这个循环不断重复,实现3D游戏的动画效果。

综上所述,我们学习了多种3D图形渲染技术,包括画家算法、深度排序、Z缓冲和二叉空间分割(BSP),并通过实现演示程序了解了它们的优缺点和适用场景。在实际应用中,需要根据游戏的具体需求选择合适的技术,以达到最佳的性能和视觉效果。同时,还可以向3D世界中添加点、线等其他实体,丰富游戏的内容。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值