Quake 2 BSP 文件格式
(翻译自《Quake2 BSP file format》from http://www.flipcode.com/archives/Quake_2_BSP_File_Format.shtml )
引言
此文的目的是介绍一种在Quake 2游戏里面用于存储地图的文件格式,叫BSP。通常它的主要作用是渲染Quake 2世界的。虽然文件里还有关于游戏的其他一些信息(比如敌人,AI,等等),但是我不并打算把这些完整地介绍给你。如果你对这些部分非常感兴趣或者知晓这方面知识,请不吝将你的电子邮件发给我,地址在上面。同样的我将尽我可能把错误降到最低,如果你发现任何错误请不吝赐教。
除了清晰透彻地介绍BSP文件格式外,此文旨在阐述Quake渲染引擎的核心技术。因此我们假设读者已经熟悉了基本的3D图形知识,比如BSP树结构。
文章涉及到的Quake 2 BSP文件格式版本号为38(这也是Kingpin所使用的Quake 2引擎的版本号)。因此所有包含的信息都和这些文件有关。虽然Quake 1和Quake 3:竞技场的BSP文件格式和Quake 2的文件格式类似,但是并不完全兼容。尽管如此,因为大部分是相似的,所以这篇文章对于那些想要研究这一类文件格式的人来说,应该是有用的。
我所获得的关于Quake 2 BSP文件格式都是来源于大量的实践,在实践中我尝试通过我的引擎单独渲染Quake 2 BSP,这些引擎是 “类Quake”引擎,它们都是被作者通过网络公共渠道发行的。这些引擎有:Stefano Lanza的Twister引擎,Andrei Fortuna的The Arnfold 2和Alexey Goloshubin的Poly引擎。这些引擎在网上都是可以免费得到的。如果你觉得我还说漏了一些,你可以自行去核实。Id Software公司(译者注:就是Quake 2的发行公司)也公开了Quake 2工具的源代码,包括了QBSP地图文件编译器,它能生成BSP文件,但是我在我的实践中很少用到它。
法律
是否能在公共途径发布的程序中使用Id Software公司的文件格式,我对此有一点小小的疑问。为了写这篇文章,我联系了Id Software公司,解决了这个问题。权威人物John Carmark回复了我:
“我们的文件格式并不受法律保护,但是你不能使用任何已发行的工具,或者这些工具的派生品(除了GPL’d Quake 1工具)从事商业目的的活动。如果你完全靠自己写出来的,没有任何问题”
约定
为了让这篇文章更加易懂,我用C语法来呈现所有的文件结构。(同时假定结构域之间没有任何多余的字节)。为了简化和标准化变量意义,我用int32和uint32来代表signed和unsigned的32位整数(译者注:有符号和无符号整数);同样的,对于16位整数和8位整数也是同样的定义。下面两个结构在整个BSP文件中都使用了,它们用于存储顶点和向量:
struct point3f
{
float x;
float y;
float z;
};
struct point3s
{
int16 x;
int16 y;
int16 z;
};
第二个版本是为了节约内存,这可以用这精度不是那么重要的场合,比如bounding boxes,这个版本只用6个字节的空间,而第一个版本要用到12个。通常它们都是整形表示,而非浮点。
Rendering
为了介绍一些术语的用处,并揭示一些数据结构背后所隐含的意义,这一节先主要描述下渲染Quake 2环境的过程。
Quake地图被拆分成一个个的凸块(convex region),然后这些凸块构成了BSP树的树叶,在任何时刻,摄像机都在一个凸块所包含的一个层(level)上。
树叶和它的邻居(neigboring)叶子组织到一起,形成团簇(cluster);事实上,这些团簇是如何形成的取决于创建BSP文件的工具。对于每一个团簇,存储着潜在可见的其他所有团簇的一个列表。这就是我们所说的潜在可视集。(PVS)
渲染Quake地图的时候,首先遍历BSP树,判断摄像机在哪个叶子上。一旦我们找到摄像机的所在,我们就能知道它在哪个团簇里面。(不要忘了每个树叶都恰好包括在一个团簇里面)。这个团簇的PVS就开始生成一个列表,包含着相对于摄像机位置所有潜在可视的团簇。每个叶子都有自己的bounding boxes,用来和view frustum作快速的可视性判断。
BSP树
很多人错误地把BSP树和那些用在Quake或者其他类型引擎中的可视性算法联系在一起。正如上面所言,可见的面取决于预计算的PVS。BSP树主要作用是把地图划分成一个个区域,然后快速判断摄像机在哪个区域里面。因此,在Quake 中BSP基本上不用任何渲染算法。取而代之的是八叉树(Octree)或者K-D树等具有空间细分的数据结构。实际上BSP树非常简单,它们被Quake引擎那些不需要渲染的任务所使用。
传统上当讨论到BSP树时,人们习惯把叶子上存储的都是面数据的BSP树(基于叶子的BSP树leaf-based BSP trees)和把面数据放在中间节点的BSP树(基于节点的BSP树node-based BSP trees)区分开。这两种BSP树Quake都用到了。原因在于BSP树的用于不仅仅是渲染,而且还有碰撞检测。对于渲染来说,基于叶子的BSP树是很有用的,因为PVS能很好地帮上忙。。如果你不是对碰撞检测很有兴趣,那么那些在节点里的面数据你可以完全忽略。
文件头
据我所知所有的BSP文件都以little-endian字节顺序存储的,当在big-ending平台上载入内存时就需要转换。这就让Quake客户端在不同的平台上共享相同的地图文件。如果你想在一个big-endian平台上载入BSP文件,比如Macintosh或者是Unix系统,你需要在使用时格外小心地交换字节顺序。
Quake BSP文件是围绕目录结构(directory structure)组织的,所有的数据被包含在“自由浮动”(free floating)的团(lump)里面的。在文件的开始,会有一个目录,标识着每个团的开始位置的地址偏移和团的大小。一个8比特大小的结构标识了目录结构的地址偏移和大小,目录结构就紧跟在后面。
BSP文件头结构结构如下所示:
struct bsp_lump
{
uint32 offset; // offset (in bytes) of the data from the beginning of the file
uint32 length; // length (in bytes) of the data
};
struct bsp_header
{
uint32 magic; // magic number ("IBSP")
uint32 version; // version of the BSP format (38)
bsp_lump lump[19]; // directory of the lumps
};
BSP文件开始的4个字节是Id Software公司的BSP文件标识。用ASCII拼出来就是"IBSP"。紧接着就是一个32位无符号整数标识着数字版本。正如之前提到的,这个版本是十进制数38。
下面是一个表,包含各种团和它们的下标,这个下标就是头结构里面团数组的下标。被问号标识的团的用途是未知的(名字是从QBSP源代码里面派生而来的),没关系,它们对我们构造一个简单的Quake层渲染器没有任何的用处。
Index Name Description
0 Entities MAP entity text buffer
1 Planes Plane array
2 Vertices Vertex array
3 Visibility Compressed PVS data and directory for all clusters
4 Nodes Internal node array for the BSP tree
5 Texture Information Face texture application array
6 Faces Face array
7 Lightmaps Lightmaps
8 Leaves Internal leaf array of the BSP tree
9 Leaf Face Table Index lookup table for referencing the face array from a leaf
10 Leaf Brush Table ?
11 Edges Edge array
12 Face Edge Table Index lookup table for referencing the edge array from a face
13 Models ?
14 Brushes ?
15 Brush Sides ?
16 Pop ?
17 Areas ?
18 Area Portals ?
当结构有一个固定大小的时候,绝大多数的团是以结构数组的形式来存储的。比如顶点团就是一个point3f结构的数组。一个point3f就有12个字节,顶点个数可以由顶点团的大小去除以12得到。类似地,同样的计算可以应用于平面,节点,纹理信息,面,叶子和边团。
在下面的几节中,将介绍那些已知的团。
顶点团
顶点团是世界中所有顶点的列表。每个顶点都由3个浮点数表示的,占12个字节。你可以通过用12去除顶点团的大小来计算顶点的个数。
Quake有自己的坐标系统,在这个系统中,Z轴指向上。如果你修改了坐标去适应另外一套系统,别忘了去调整bounding boxes和平面方程。
边团
顶点共享面,边也如此。每条边都以一对顶点列表的下标的形式存储的。它们是两个16位整数,因此边的数量是边团的大小除以4。这里有点复杂的是,每条边都被两个面共享,因此对于一条边来说是没有固定的方向的。这个我们会在面边这一节中进一步谈到。
面团
面团是bsp_face结构的数组,它的格式如下所述:
struct bsp_face
{
uint16 plane; // index of the plane the face is parallel to
uint16 plane_side; // set if the normal is parallel to the plane normal
uint32 first_edge; // index of the first edge (in the face edge array)
uint16 num_edges; // number of consecutive edges (in the face edge array)
uint16 texture_info; // index of the texture info structure
uint8 lightmap_syles[4]; // styles (bit flags) for the lightmaps
uint32 lightmap_offset; // offset of the lightmap (in bytes) in the lightmap lump
};
Bsp_face结构的大小为20个字节。面的数量为面团的大小除以20。
Plane_side是用来判断顶点的法向量是否和面的法向量一致。这是很有必要的,在BSP树中有些面是共享同一个节点,也共享着同一法向量,尽管它们的法向量是不同的。如果plane_side非零,那么顶点的法向量就和平面的法向量相反。
纹理和光照贴图坐标的介绍将分别放在纹理那一节和光照贴图坐标这一节里面。
面边团
面数据里包含边面的下标,对应着边数组的下标,而不是直接去访问边数组。面边团是一个32位无符号整数数组。数组的元素个数可以通过用面边团大小除以4来得到。(注意这个不一定是边的数量)
边会被不同的资源索引,因此不可能会有一个确定的方向。如果边的下标是正的,那么第一个点就是边的起点;如果边的下标是负的,那么第二个点就是边的起点。(当然了,你使用这个负的下标时,直接去掉负号就行了)
平面团
平面团存储着一个bsp_plane结构的数组,这些平面是在BSP树中充当分割平面的。结构如下:
struct bsp_plane
{
point3f normal; // A, B, C components of the plane equation
float distance; // D component of the plane equation
uint32 type; // ?
};
每个bsp_plane结构有20个字节。所以这些平面的个数就是平面团的大小除以20。
X , y , z和法线的A,B,C常量,以及距离D常量,在面方程中的表示:
F(x, y, z) = Ax + By + Cz - D
位于平面上的点,代入方程F(x, y, z) = 0,位于平面前面的点,代入方程F(x, y, z) > 0,相应地位于平面后面的点代入方程F(x, y, z) < 0。这在遍历这个BSP树的时候很有用。
节点团
结点以数组的形式存储在节点团里面。第一个元素就是BSP树的根节点。Bsp_node结构如下:
struct bsp_node
{
uint32 plane; // index of the splitting plane (in the plane array)
int32 front_child; // index of the front child node or leaf
int32 back_child; // index of the back child node or leaf
point3s bbox_min; // minimum x, y and z of the bounding box
point3s bbox_max; // maximum x, y and z of the bounding box
uint16 first_face; // index of the first face (in the face array)
uint16 num_faces; // number of consecutive edges (in the face array)
};
每个结构都有28个字节,因此节点的个数就是节点团的大小除以28。
因为一个节点的儿子既有可能是节点,也有可能是树叶,所以如果下标是负数就表明这个儿子是树叶。因此正确的下标应该这样计算:-(index + 1)。这样第一个负数下标正好对应着0。
Bounding boxes是轴对齐的,8个坐标可以由最大值和最小值枚举出来,它们存储在bbox_min 和bbox_max域中。
正如前面所说,面表并不是用来渲染的,而是用来做碰撞检测的。
叶子团
叶子团是bsp_leaf结构的数组,这些叶子就是BSP树中的叶子。其结构如下:
struct bsp_leaf
{
uint32 brush_or; // ?
uint16 cluster; // -1 for cluster indicates no visibility information
uint16 area; // ?
point3s bbox_min; // bounding box minimums
point3s bbox_max; // bounding box maximums
uint16 first_leaf_face; // index of the first face (in the face leaf array)
uint16 num_leaf_faces; // number of consecutive edges (in the face leaf array)
uint16 first_leaf_brush; // ?
uint16 num_leaf_brushes; // ?
};
Bsp_leaf结构有28个字节。同样地叶子数量可以由叶子团的大小除以28来得到。
叶子被组织成团簇的形式,以存储PVS,团簇里是可视团数组的下标。具体信息请看可视化这一节。如果团簇给出的下标为-1,那么这个叶子没有可视化信息。(这种情况表明这个叶子是玩家不可到达的地点)
叶面团
叶子包含着叶面数组,对应着面数组的下标,而不是直接去访问面数组。叶面团是由16位无符号整数组成的。元素的个数是叶面团的大小除以2;这不一定是世界中面的总数。
纹理信息团
纹理信息结构标识着一个面如何被贴图的所有细节。Bsp_texinfo结构如下:
struct bsp_texinfo
{
point3f u_axis;
float u_offset;
point3f v_axis;
float v_offset;
uint32 flags;
uint32 value;
char texture_name[32];
uint32 next_texinfo;
};
bsp_texinfo结构的大小是76字节,所以纹理信息的数量就是纹理信息团的大小除以76。
纹理和面对应起来的,使用一种平面纹理映射策略。指定了两个纹理轴来代表一个平面,而不是对每个顶点指定纹理坐标。纹理坐标通过对顶点往平面上投影来计算出来。
这样也许增加了程序员的工作量,但是这样确减小了关卡设计者在不同面间的纹理对齐这样的负担。
纹理坐标( u , v )和顶点( x , y , z )对应的计算公式如下:
u = x * u_axis.x + y * u_axis.y + z * u_axis.z + u_offset
v = x * v_axis.x + y * v_axis.y + z * v_axis.z + v_offset
我的经验告诉我在需要的时候实时计算纹理坐标比一直把它们存储在内存中要好。
纹理的名字含有路径,但是没有后缀。如果你载入的是一个Quake 2地图,你可能会需要在后面加上扩展名wal,你可以在PAK文件中得到它们。如果你载入的是一个Kingpin地图你可能要加上tga后缀,它们不会出现在PAK文件中。如果你想知道关于WAL纹理的更多信息,请关注WAL纹理这一节。
可视化团
树叶被组织成团簇的形式来存储可视化信息。这是在PVS中节约空间的方法,因为相邻的叶子往往会有相似的潜在可视区域。开始的4个字节是一个32位无符号整数表明地图中团簇的数量,紧跟在后面的是一个bsp_vis_offset结构数组,元素数量就是团簇的数量。具体结构如下:
struct bsp_vis_offset
{
uint32 pvs; // offset (in bytes) from the beginning of the visibility lump
uint32 phs; // ?
};
可视化团的剩下部分就刚好是可视化信息。对于每一个团簇的可视化状态而言(可见或者是被遮挡)都是为了其他的团簇而存储的。团簇对它们自己而言始终是可见的。由于数据量是如此地巨大,因此这个数组被存储为位向量(一个元素占一位),0是游程编码。下面是一个C语言写的程序,来解压PVS到一个字节数组里(这是从Quake Specification文档改编而来的):
int v = offset;
memset(cluster_visible, 0, num_clusters);
for (int c = 0; c < num_clusters; v++) {
if (pvs_buffer[v] == 0) {
v++;
c += 8 * pvs_buffer[v];
} else {
for (uint8 bit = 1; bit != 0; bit *= 2, c++) {
if (pvs_buffer[v] & bit) {
cluster_visible[c] = 1;
}
}
}
光照贴图团
Quake环境用光照贴图来处理静态光照,是用低分辨率的位图结合纹理来渲染一个面的。光照贴图对于一个面来说是唯一的(这不同于纹理会被多个面使用)。光照贴图最大的尺寸是16×16,这是Quake创建工具制作出来的,它把面分割开来,不然的话,会需要更大的光照贴图。在Quake 1中,光照贴图被限制在灰度的范围内,而到了Quake 2的时候,允许24位真色彩光照。
光照贴图可以通过多纹理或者是多通道渲染来实现,而且在渲染时需要和面上的纹理进行叠加。
世界里所有的光照贴图被连续地存储在光照贴图团中。面存储着自己的光照贴图偏移,放置在bsp_face结构中的lightmap_offset中。具体的光照贴图信息是以每像素24位,从上到下,从左到右的顺序存放的。
光照贴图的长和宽不会明确的出现在任何地方,它需要我们来计算。如果max_u是一个面的所有u坐标中最大的,min_u是一个面的所有u坐标中最小的(同样适用于max_v和min_v),那么这个面的光照贴图的长和宽计算公式如下:
lightmap_width = ceil(max_u / 16) - floor(min_u / 16) + 1
lightmap_height = ceil(max_v / 16) - floor(min_v / 16) + 1
对于给一个面计算纹理坐标的相关信息,请查阅纹理信息团这一节。
WAL图像格式
Quake 2有自己的专有纹理2D图像存储格式,为WAL。它并不是BSP文件格式的一部分,它不是载入Quake 2地图信息的必需品,所以我没有打算去介绍它。注意到它不是用于Kingpin地图,因为它使用的是TGA文件格式。
WAL贴图存储的是8位索引颜色格式,指向一个具体的调色板,这个调色版被所有的纹理使用。(这个调色版以PAK文件格式存储的,作为Quake 2的一部分)。存储了4个mip-map层,伸缩因子为2。这是为软渲染而准备的,因为绝大多数的3D APIs能自动地为纹理创建mip-map层。每一帧的纹理动画被单独存储在一个WAL文件中,动画序列是由把下一个纹理名字存储起来为一个序列这样的方法形成的。纹理名字带有路径,但是没有后缀名。
WAL文件的文件头为wal_header,结构如下:
struct wal_header
{
char name[32]; // name of the texture
uint32 width; // width (in pixels) of the largest mipmap level
uint32 height; // height (in pixels) of the largest mipmap level
int32 offset[4]; // byte offset of the start of each of the 4 mipmap levels
char next_name[32]; // name of the next texture in the animation
uint32 flags; // ?
uint32 contents; // ?
uint32 value; // ?
};
纹理数据以8位每像素的RAW格式存储起来的,顺序是从上到下,从左到有。
实体
实体这一部分是ASCII文本缓存,在MAP文件中的实体也是同样的格式(不要忘了BSP文件是从MAP文件编译而来的)。实体是这些东西:玩家的出生点,光,生命值,弹药,和武器。
Quake 1中完整的实体列表(覆盖到大量的Quake 2实体)在Quake Specification文档中有介绍。