游戏开发论坛

 找回密码
 立即注册
搜索
查看: 4727|回复: 0

论简化三维流水线和逼近真实流水线快速构造引擎(三)

[复制链接]

20

主题

136

帖子

172

积分

注册会员

Rank: 2

积分
172
发表于 2007-9-22 23:01:00 | 显示全部楼层 |阅读模式
第一部分、基础设施
http://bbs.gameres.com/showthread.asp?threadid=93004
第二部分、光照和Gouraud着色
http://bbs.gameres.com/showthread.asp?threadid=94056
第三部分解决方案
http://pickup.mofile.com/9552663066148164
第四部分预告:裁剪、1/Z深度修正、深度缓存、碰撞和物理系统



第三部分、UV仿射贴图、材质混合、文件操作



图3-1、将光照和贴图进行了材质混合

图3-2、使用z缓冲来执行像素级排序,使物体得于正确渲染

        目前还没有涉及裁剪,因此对于一些离相机过近以至发生负投影的多边形被简单地剔除了。视景体的其它5个面则被简单地利用光栅化函数过滤了。这类似于硬件的工作模式,因为它足够简单并且可以假设我们可以在以后事先执行了裁剪,这样光栅化函数只是提供了判断的开销,这对于不执行这样的任务的灾难性后果来讲是值得的。
一、Bmp位图格式和读取位图信息
        为了把贴图放置到网格上,首先要解决的问题是使用什么图像格式和定义它的内存镜像格式。当前最简单的图像格式是bmp。我使用了它的8位和24位版本。8位因为信息量不足(仅仅256个定义长度),所以引入了调色板的形式。而24位色版本则提供了全部的RGB信息。执行贴图运算的时候,为了要立即得到有用的RGB信息,调色板索引应该在读取位图之后进行转换,和24位色版本一致。这是当前的硬件条件所允许和显示模式所决定的。因此,所有的各种图像格式最终都要转化成一致的内存镜像格式。
        对于文件操作,我使用了fstream流,这是C++的标准文件IO操作流,是STL(标准模板库)的一部分,现代编程需要程序员更快地解决问题,模板化为这一切作了大量的工作,C++并不意味着总是使用类结构,事实上,模板化范型编程正在成为C++编程的事实标准。设想,一个数组可能是包容字符的或者是浮点类型的数据集,而vector则替代了数组,没有指针操作,它使用了迭代器。原始的C++语言对于类内部类型是严格定义好的,假设有一个Cvector类,它可能只接受整型数据,但是vector接受任何你想要存放的类型,使用vector<type>作为类型符号。并且可以使用和指针相同的偏移模式进行迭代,它的size()子函数提供迭代限制。

fstream f;
f.open(filename.c_str(),ios_base::in|ios_base::binary);
if(!f)
{
        f.close();
        return -1;
}
//读取文件头
f.read((char*)&bmf.bmfh,sizeof(BITMAPFILEHEADER));
f.close();

ios_base枚举了fstream可能会用到的各种限定符,open()第二个参数需要指定打开类型,有一些有用的枚举:
ios_base::in 读操作                ios_base:ut 写操作        ios_base::app        追加写入
ios_base::ate        添入        ios_base::binary 二进制        ios_base::trunc        截断
读写操作子函数:
write() 写数据                        read()读数据                        get() 获取字符/字符串
getline() 获取一行字符串

位图格式:Bitmap File Format
1、镜像文件:
#define PALETTEENTRYS 256
typedef struct
{
        BITMAPFILEHEADER bmfh;
        BITMAPINFOHEADER bmih;
        PALETTEENTRY palette[PALETTEENTRYS];
        BYTE* bits;
        int size;
        int width;
        int height;
        int bitcount;
        int bpp;
        int pitch;
}BITMAPFILE;

        真实文件数据不包含BYTE* bits后面的数据,那是为了访问方便而人为加上的。
2、文件头BITMAPFILEHEADER

typedef struct tagBITMAPFILEHEADER {
        WORD    bfType;
        DWORD   bfSize;
        WORD    bfReserved1;
        WORD    bfReserved2;
        DWORD   bfOffBits;
} BITMAPFILEHEADER, FAR *LPBITMAPFILEHEADER, *PBITMAPFILEHEADER;

        有用的数据是bfType,应该为0x4d42,即“BM”,bfSize,指出文件头结构大小,bfOffBits:缓冲区数据的起始处相对于文件开头的偏移量
3、文件信息BITMAPINFOHEADER

typedef struct tagBITMAPINFOHEADER{
        DWORD      biSize;
        LONG       biWidth;
        LONG       biHeight;
        WORD       biPlanes;
        WORD       biBitCount;
        DWORD      biCompression;
        DWORD      biSizeImage;
        LONG       biXPelsPerMeter;
        LONG       biYPelsPerMeter;
        DWORD      biClrUsed;
        DWORD      biClrImportant;
} BITMAPINFOHEADER, FAR *LPBITMAPINFOHEADER, *PBITMAPINFOHEADER;

        biSize:该结构的大小,用于指出当前结构版本,biWidth:图像宽度,biHeight:图像高度,biPlanes:包含的调色板数,biBitCount:色深度,biSizeImage:图像数据区长度,biClrUsed:使用的调色板色数。biClrImportant:主要调色板色数。

4、调色板单元PALETTEENTRY结构

typedef struct tagPALETTEENTRY {
    BYTE        peRed;
    BYTE        peGreen;
    BYTE        peBlue;
    BYTE        peFlags;
} PALETTEENTRY, *PPALETTEENTRY, FAR *LPPALETTEENTRY;

peRed、peGreen、peBlue,8位长度的RGB信息,因为约定的关系,以上信息跟GDI的RGB次序是一致的,但是和显示屏RGB次序颠倒,为了使用方便,需要调整R和B的位置。最后一个标志符是向系统提示调色板的使用方式:

#define PC_RESERVED     0x01    /* palette index used for animation */
#define PC_EXPLICIT     0x02    /* palette index is explicit to device */
#define PC_NOCOLLAPSE   0x04    /* do not match color to system palette */

        以上三个标志符分别指定为:可变的、系统默认、自定义。基本上,我们总是使用自定义调色板。这个标志字符文件没有给出,作为保留段。所以由导入函数指定。
        缓冲区没有什么好神秘的,不同的位深的图像有不一致的像素占位。8位图像占据1个位长度用于指定调色板索引。24位图像占据3个位长度。主要的问题在于,由于历史原因,很多图像的y方向扫描线是颠倒的,也就是图像是自底向上指定的。


图3-3、图像自底向上指定

        当图像文件内定的高度信息是正值,便声明为以上形式,反之亦然。

        //翻转位图
if(bmf.height==Abs(bmf.height))
{
        BYTE* buf=(BYTE*)malloc(bmf.size);
        memcpy(buf,bmf.bits,bmf.size);
        memset(bmf.bits,0,bmf.size);
        for(int y=0;y<bmf.height;y++)
                memcpy(&bmf.bits[y*bmf.pitch],&buf[(bmf.height-y-1)*bmf.pitch],bmf.pitch);
        free(buf);
}


图3-4、图像最终呈现在窗口上

二、仿射纹理贴图
        对物体网格表面进行贴图可以简化成对单个多边形执行贴图的任务,附图呈现的是一个多边形,使用的采样对象的坐标值均转化为0-1的浮点数值,这意味着我们可以使用任何尺寸的位图作为采样对象,因为多边形使用的采样对象坐标都是一致的。当然,在最终光栅化阶段,我们需要对事实上的坐标进行插值。这一切对于网格操作和图像选取任务都是透明的。


图3-5、使用归一化坐标进行纹理采样

        当前考虑的是图像空间的采样,因此标准的插值算法是有效的。该插值过程和Gouraud着色渲染插值同步进行,我几乎是将Gouraud的插值代码重新拷贝了一遍,它就开始正常工作了,当然了,我修改了变量名,以便于它同采样点对应起来。注意,光栅化阶段的采样使用的并非归一化坐标,而是事实坐标,这样才能有足够的定点数进行插值。并且避免正式采样的时候还要转化一次坐标。
        仿射纹理映射和Gouraud着色的插值计算存在的同样一个问题是,插值过程是在图像空间进行的,而事实上,图像空间上的坐标点是经过透视变换的,在图像空间线性变化的数值,在实际相机空间中受z方向的数值变化影响而呈非线性变化,变换如图所示:

图3-6、透视变换中,z值得区别使得在视平面线性变化的点在实际空间非线性变化

        图形学有一句至理名言“如果它看起来是,那它就是了”。而事实上,这种情况是可以得到更正的,根据公式y=y’(d/z’),我们可以对d/z’进行插值计算,这样就能够得到y值变化在图像空间“扭曲的”而在相机视景体空间是线性的插值过程。后面将会接触到的z缓冲算法的一个变种就是基于这样的事实。
        尽管如此,目前我仍然使用了看起来笨拙但是更容易实现和高效的图像空间插值。当多边形在深度上相对于视平面平行面的跨度并不是很大,即多边形平面和视平面的倾斜夹角较小时,插值过程越精确。


图3-7、进行仿射纹理映射的结果图,就如我们在示意图假想的那样

        还有其它问题,第一、起点误差,这是扩展Bresenham算法引入的起点误差计算,我现在要处理的事情是如此之多,因此我一直没有对它进行额外编码。第二、双线性过滤,对纹理采样的时候,为了避免锯齿现象,应当在2D空间对当前采样点的周边进行加权计算以确定最终像素色值,很明显,这会占用处理周期。


三、流水线层次结构
基本层次结构:

对应关系:

        其中,若法线和顶点列表同步,而不是独立到多边形面序列,则通过顶点索引访问法线列表和光照列表。最终权衡,我决定放弃对3DS MAX的法线多边形面序列的兼容,对顶点法线做统一处理,使得每个顶点的光照只需计算一次来提高效率。


修改的对应关系:

        上例中将法线和光照和顶点列表对齐,这样每个顶点光照将只计算一次。但这种格式需要对多边形网格有一个约定,对于由平面组成的图形,需要分割位置一致但法向量不一致的顶点。
四、Alpha混合查找策略
        在讨论光线调制和Alpha混合的时候,我曾经提到,0-1的浮点取值使得光线乘法有意义并且不会导致越界。事实上,浮点运算有时候并不是我们想要的,而且,RGB的分量只有256的精度范围,在32位色格式中的A值也是使用了8位存储,使用浮点计算显然有些夸张。实践证明,光线乘法并不是单纯的乘法计算,而是一种类似将信息置入载波的调制过程。在对0-1的取值进行计算的时候隐含了除于1的降0位操作。由此可知,当对两个8位数值进行调制运算时,需要对其进行降8位操作,结果数值范围保持在8位以内。

        如上式所示,一种更容易理解的方式是,将分量转化成0-1的浮点数值,再将它逆乘,得到调制的结果RGB显示格式:
R=(int)(((float)R1/(float)0xFF)*((float)R2/(float)0xFF)*(float)0xFF)=R1*R2/0xFF
        显然,得出的结论一致,但是0-1浮点数值的相关运算是基于先验性的假设,这是一种不确定的推导。

for(int i=0;i<=0xFF;i++)
        for(int j=0;j<=0xFF;j++)
                alphatable[j]=i*j>>8;

        上述查找表将用于支持Alpha混合计算。例如,讨论纹理映射的时候,我忽略了这样一个细节,如何将光照的结果同贴图色混合起来。下面就是使用该查找表进行混合的实例代码:

bmpB=alphatable[bmpB][Bbase];
bmpG=alphatable[bmpG][Gbase];
bmpR=alphatable[bmpR][Rbase];

五、ASCII网格存储结构,msh和ase
        Msh是从MilkShape 3D导出的一种简易而高效的文本存储格式,但是它并不支持动画网格和骨骼。而MilkShape 3D的默认文本导出格式则支持。另外,3DS MAX导出的3DS格式导入到MilkShape 3D的时候,会出现坐标系错误,支持的顶点数有限,贴图坐标破坏以及其它未发现的问题,再者,直接使用3DS MAX的一些特性选项有时候会成为一种需求,总之,有必要直接从3DS MAX导出的文件中读取信息。
        但是3DS MAX导出的ASE文件有几个问题,第一,它仅支持三角形面级别的法线和贴图坐标,而通常我们均把法线、顶点光照、贴图坐标等同顶点列表对齐,这个问题在上面讨论流水线层次结构的时候已经提到。第二,它使用标签描述格式而非流格式,当然我们可以使用流格式的处理手段来读取它,但是这意味着随着对此格式文件的内部信息的需求的增加,将使得代码越来越混乱,最终我考虑了这样的策略,开发一种自定义的二进制文件版本,用于支持对任意输入格式的导出和文件级缓存工作,甚至到后来直接作为外部工具对所有开发包使用到的模型进行转换。这可以极大缓解处理ASE格式文本信息低效的现状。在实现上它应当没有问题,因为这种二进制格式是我们任意给定的,为了完成这项任务,它将同流水线的层次结构对齐起来,实现广泛的兼容性。这意味着,任何导入格式在这种二进制格式没有体现出来的特性,在流水线层次结构中也没有对应元素。一旦确定需要某种元素,这种特性立刻就会在流水线层次结构和二进制文本格式中同步更新。这样,我们使用了所有我们要求的外部信息而没有降低处理速度。另一方面,即使为了速度考虑而优化了ASE等文本格式的导入工作,上述的开发依然是必须的。只不过我们把它提前了,以便于我们不必在文本格式的低效性上大费脑筋。
Msh文件结构:

        关于这种格式,优点是简单易行,缺点是它没有支持更多的特性。当然,对于场景编辑,这种格式已经可以满足需求,而桢动画则使用多文件或者单文件多段存储,如果坚持要使用这种文件格式的话。
3DS MAX标准文本导出格式ASE的文件结构:

        如果你正在寻找它的几何体存放在哪的话,事实上,这不是描述具体信息层级关系的图示,而是类似XML的一种存储规范,它具有独立标签和嵌套标签,独立标签有名称和值表,而嵌套标签不仅可以嵌套子标签,还可以嵌套“子嵌套标签”,如此递归下去。这种格式固然带来很大的灵活性,但是非常浪费时间。需要对它们全部解套然后利用一致的算法,提供一个匹配标签名进行标签取出和迭代。
*3DSMAX_ASCIIEXPORT        200
*COMMENT "AsciiExport 版本  2.00 - Wed Sep 19 22:16:39 2007"
*SCENE {
        *SCENE_FILENAME "box.max"
        *SCENE_FIRSTFRAME 0
        *SCENE_LASTFRAME 100
        *SCENE_FRAMESPEED 30
        *SCENE_TICKSPERFRAME 160
        *SCENE_BACKGROUND_STATIC 0.00000000        0.00000000        0.00000000
        *SCENE_AMBIENT_STATIC 0.00000000        0.00000000        0.00000000
}
        第二行使用了双引号对值进行了限定,避免被分割成子单元。我们可以假设*SCENE块还可以内嵌子块,假如存在,便可将此子块作为独立单元进行递归分解。直到所有嵌套子块全部被遍历。
        算法很优雅,优雅的代价是慢。我可以开发一种不太优雅的较为快速的流式提取法,但是最后我决定使用文件级缓存和转换工具。在没有转换的情况下对所有导入格式进行二进制文件级缓存。缓存尽量写在当前文件夹的某个子文件夹中,仅当子文件夹不可访问的时候才写入系统临时文件夹。
        但是目前我还是把该解析算法列出来。作为一种参考和改进的基础。


元素定义:

typedef struct
{
        string name;
        vector<string> values;
        string itembody;
}ASEITEM;

struct ASEBLOCK
{
        string tag;
        vector<string> values;
        vector<ASEBLOCK> blocks;
        vector<ASEITEM> items;
        vector<string> blockbody;
};

进行数据递归分解:

ASEBLOCK ase_getblock(const vector<string>& lines)
{
        ASEBLOCK block;
        ASEBLOCK subblock;
        ASEITEM item;
        vector<string> strs;
        vector<string> inlinebody;
        int step=0;
        bool bracket=false;
        bool headerline=false;
        vector<string> headerinfo;
        for(int i=0;i<lines.size();i++)
        {
                strs=split_space(lines);
                //检查行类型
                //单元模式
                if(step==0&&strs[strs.size()-1]!=_T("{"))
                {
                        item.values.clear();
                        item.itembody=lines;
                        for(int j=0;j<strs.size();j++)
                        {
                                if(j==0)
                                        item.name=strs[0];
                                else
                                        item.values.push_back(trimquot(strs[j]));
                        }
                        block.items.push_back(item);
                }
                //块模式
                headerline=false;
                if(strs[strs.size()-1]==_T("{"))
                {
                        if(step==0)
                        {
                                headerline=true;
                                headerinfo=strs;
                        }
                        bracket=true;
                        step++;
                }
                if(strs[strs.size()-1]==_T("}"))
                {
                        bracket=true;
                        step--;
                        if(step==0)
                        {
                                //subblock=ase_getblock(inlinebody);
                                subblock.blockbody=inlinebody;
                                for(int j=0;j<headerinfo.size();j++)
                                {
                                        if(j==0)
                                                subblock.tag=headerinfo[0];
                                        if(j>0&&j!=headerinfo.size()-1)
                                                subblock.values.push_back(headerinfo[j]);
                                }
                                block.blocks.push_back(subblock);
                                inlinebody.clear();
                                bracket=false;
                        }
                }
                if(bracket&&(!headerline))
                        inlinebody.push_back(lines);
        }
        return block;
}

六、自定义二进制储存格式


typedef struct
{
        TCHAR sourcefile[64];
        int modelcount;
        int materialcount;
        int texturecount;
}LOSFILEHEADER;

typedef struct
{
        TCHAR modelname[64];
        int vertexcount;
        int indexcount;
        int tvertexcount;
        int tindexcount;
        int textureindex;
        int materialindex;
}LOSMODELINFO;


        这是一种非常紧凑的储存格式,并涵盖了每个阶段引擎所需的任何细节,因为它和OBJ结构体的描述基本上保持一一对应的关系。导出和导入工作都可以非常快速而简洁地完成。

性能测试比较


        由图表可见,两者完全没有可比性,即使是读取10兆的文件,LOS也可以在1秒钟左右完成全部读取工作,相比较而言,同样的信息量,ASE要占据50兆左右的空间和漫长的读取时间。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2025-6-21 08:28

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表