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世界中添加点、线等其他实体,丰富游戏的内容。
超级会员免费看
1326

被折叠的 条评论
为什么被折叠?



