仙剑3吧 关注:86,753贴子:1,759,039
  • 19回复贴,共1

【转贴】仙剑奇侠传3模型文件格式分析

只看楼主收藏回复

转自新浪博客,作者微博id:叫我老汉。年代久远,有的图片已经没了,作者写的程序也没了。
前段时间特别喜欢看武林外传, 看到百度贴吧上面有人提议做一个武林外传的游戏。我就跟他们说美工可是大问题,建议他们从现成的游戏里面提取出来。于是我想到了研究仙剑奇侠转3模型文件的格式。把研究的过程在这里计个流水账吧。
仙剑3的资源打包在一个文件中。我对这些打包的东西没什么兴趣,而且网上已经有高手做了拆包的程序,所以就直接拿来用了。
拆包以后会出现很多的贴图文件,同时在模型文件夹下会出现很多.pol文件。估计这个就是模型文件了,因为pol很可能就是polygon的缩写。
模型文件的名称没有什么规律,但是幸运的是有一个叫box.pol的文件(在object目录下),这个很可能说明是一个方盒子的模型。方盒子模型的坐标会比较有特点,因而从这个模型着手分析可能会比较容易。
用16进制的方式打开这个文件进行初步观察,可以观察到如下的特征:


1、文件应该是没有压缩的,理由是文件中重复的字节很多,而且明显能看出很多的字符串。
2、文件的开头有个POLYd字样,应该是文件的合法性标识。打开其他的文件比较了一下,可以证实这个判断。
3、在文件开头有个Box02,可能是模型的名字。
4、紧跟模型名字有很多扩展名,作用不明,结束的位置也不清楚。
5、在这些扩展名之后紧跟了很多数据,含义未知。在这段区域中,每隔固定的长度都会出现 FFFF FFFF 字节。
6、在这段数据之后出现了一些重复的字节,之后是一个图像的文件名xiangzi.tga,应该是该模型的贴图。但是在众多资源中并没有找到这个贴图,现存疑。 这个字符串长度为11,但是在它前面没有指定这个长度的字节存在。其后又是一片空白的区域。
7、 最后存在一些数据区域。
看 到这个文件,我首先发现的就是有很多的 FFFF FFFF 字节存在。这个很可能成为分析这个模型文件的突破口。首先,一般的3D模型的坐标分量是用一个32位的float类型来表示的,而 FFFF FFFF 正好也是32位,这很可能说明这段区域包含了3D模型的顶点坐标。第二,FFFF FFFF 这个数据本身是没有意义的。在文件中它很可能是一个开始或结束的标示符。比如说,在nVidia的Demo中,3D模型就用同样的字节作为开始和结束的符 号。另外,FFFF FFFF 字节的数目刚好是16个,因为一个盒子通常有8个顶点,而16刚好是8的倍数,这也预示着这段数据同顶点的数据之间有一定的关系。但是问什么是顶点的2倍 而不正好是8呢,我感觉可能是前8个是顶点,后8个是贴图坐标。
现在已经假设 FFFF FFFF 是潜在的开始和结束标记,那么它到底是开始还是结束呢。如果假设它是起始标记,那么最后一个顶点的数据将如何结束呢?另外,不难发现,最后一个 FFFF FFFF 后面的数据和上一个 FFFF FFFF 后面的数据区别比较大:



因此我认为这里的 FFFF FFFF 不太可能是起始标记,而应该是结束的标记。
在 顶点数据区之后又很多的重复区域,其间包含了一个贴图文件名,之后又是一些重复的空闲区域。首先这个贴图文件名很可能分割了顶点数据和面的数据。这个判断 没有什么特别严谨的理由,只不过因为obj格式的模型也是这样, 才作出的估计。因为没有在这个数据区域中找到指明了文件名长短(11)的数据,所以我感觉,这个文件应该为贴图文件名分配了一段固定长的空间,而文件名的 长短就限制在这个空间之内,这就是为什么如果文件名没有占满这个空间,后面会出现很多重复空闲的区域。
初步判断在贴图文 件名之后是面的数据,除了上面提到的原因以外,还有几个因素可以肯定这个假设。首先,这些数据明显是以4个字为一个单位。这就说明,这段数据不太可能是浮 点类型的数据。我们知道,在3D模型中,每个面通常包含了组成这个面的3个顶点的编号,因而是整型的数据。虽然这里的数据是16位的,而不是整形通常的 32位,但是说不定它是用的short类型。另外一个重要的特点是,这个数据区域的开始有个明显的递增的趋势,这个也是面数据的一个特征。因为一个面的顶 点通常是连续建立的,因而编号递增变化。最后,这个区域中的数据,如果是short类型的话,刚好都不超过16,因为已知有16个顶点信息了,而这段数据 很可能就是顶点的编号。这也说明那16个点也许不是前8个顶点,后8个贴图坐标,而是一样的顶点信息。那为什么有16个呢
经过初步观察可以得到如下的结论或者说猜测:
1、文件开头有一个固定的标识。
2、之后是模型的名称和不明的数据
3、紧接着是顶点数据,每个顶点由 FFFF FFFF字节结束
4、然后是贴图文件名
5、最后是面数据,short型
到 这一步还有几个问题不太明了,第一,如果最后面是面数据的话,到底是triangle strip还是triangle list呢,不得而知。第二,为什么在第0x27那行会有一个连续的 0000 0000 呢,这个作为面数据没有什么意义。第三,每一个顶点数据包含了20个字节的信息,初步估计是3个坐标(12个字节)和2个贴图坐标(8个字节),但是如果 贴图坐标包含在顶点信息中,为什么会有16个顶点信息而非8个呢?不过看到这一步,只有写个程序试验一下了。
这个是伪代码:
打开文件,将文件指针调整到0x58的位置开始读取一个20个字节的数据
while(最后面是 FFFF FFFF)
{
以浮点的形式输出这个点的所有信息
再读取一个点
}
这个程序运行之后可以观察到,每个顶点输出的5个浮点数中,前两个浮点数是一个在0和1之间的数,而后面的3个数范围比较大。这说明前面的2个数应该是贴图坐标,而后面的是顶点的3个坐标。于是可以假定顶点数据的格式是这样的:



struct vertex
{
float u;
float v;
float x;
float y;
float z;
};

为什么把贴图坐标放在坐标之前呢,这个是比较奇怪的一点。
把 顶点取出之后,还要看一下这些顶点是不是构成一个盒子的造型。我用opengl简单地渲染了一下,确实。有了顶点的数据,需要分析面的数据了。刚才提到, 在面的数据中存在连续的顶点编号,这个是没意义的。但是这些数据只存在于数据区的前面,后面的数据都比较正常。所以我从后往前数,看到什么位置出现了不正 常。因为先前也提到,一般面的数据呈现一种递增的趋势,而且一般都从编号0开始,所以我认为0x280应该是面数据的开始。如果每个面包含3个顶点的话, 每个面应该占有6个字节,而从0x280到文件尾有60个字节。如果这些面是triangle list的话,那么刚好有10个面。但是,一个盒子应该有6个面,每个面应该由2个三角面所组成,这样算下来应该有12个面的数据,但是为什么这里只有 10个呢。另外注意到,紧接着面数据区前面的一个32位整型数据(0x27c)刚好是0xa,也就是10。会不会这个就是面的数目呢。我比对了其他的文 件,应该没错。
于是,面的数据应该是这样的:
int faceCount;
struct face
{
short v1;
short v2;
short v3;
};
按照这样的格式将模型输出成obj格式,然后用3D Max导入。发现模型是一个没有底面的盒子。这解释了为什么最后的面数据只有10个,而不是12个。因为在游戏中,为了加速渲染速度,模型需要尽量精简, 而一个盒子贴近地面的那一个面又通常不可见,因此模型中省略了底面。另外,模型中存在不少位置重复的点,现在看来是因为每个点的贴图坐标只能允许有一组, 所以如果要一个点有不同的贴图坐标,只能定义好几个点,这些点位置相同,但是贴图坐标不同。
初步拆解了这个模型文件之后,要进一步讨论它的适用型。把这个程序在其它的模型身上试验了一下,又成功拆开了j13.pol这个文件。 但是导入到3D Max之后,贴图坐标似乎有些不太正常。但是一开始我没有注意到这个问题。另外又试了几个文件,都出了问题。
如果要这个拆解的程序能够应用到更多的模型身上,必须要假设所有模型的顶点信息的起点都是0x58,并且包含贴图信息的数据区的长度是一致的。
我 打开那些不能拆解的模型文件,发现它们的顶点数据区的起始都不是ox58,也就是说我的第一个假设有问题。但是贴图信息的数据区长度似乎是一致的。另外一 个问题就是,这些问题文件包含的模型不止一个,它们都存在多个顶点数据区,面数据区和贴图数据区。这很可能说明,未知的文件头长度和模型文件所包含的模型 数目是相关的。
另外我注意到在0x8的位置上有个数,最开始我以为是指明模型名字长度的一个数值。但是比较box.pol文件,发现明显不 对。j13.pol的这个数值也是1,但是打不开的那些文件中这个数字就不是1了。这说明,这个数字表示模型中所含模型的个数。打开几个文件数了一下,证 实了这个猜想。




这个上接我这个博客的第一篇日志:仙剑奇侠传3模型文件格式分析(1)。实际上我真正看这个模型的时候好像是六月份。而写上一篇文章的时候在8月份。而这一篇拖了很久。我最开始想至少写3篇,因为虽然模型的扩展名是一样的,都是pol文件,但是实际上里面的数据有差别。比如说场景模型有个光照贴图的坐标,而在物体模型里面就没有。而现在主要分析的是物体的模型。我最初想写这个东西是因为我想要记录一下我当时思考的过程,结果倒是次要的。因为把仙剑奇侠转3的模型拆出来一看其实也就那么回事。但是现在隔的时间有点太长了,很多东西只能记得结论,当时怎么想的现在已经想不起来了。
上回说到发现一个模型文件可能会包含很多组模型,并且在头部0x08的位置有个数值记录了这个数目。这样通过分析拥有多个模型的文件和只有单个模型的文件可以得到模型的头部长。计算的方法是这样的:
假设整个文件有个公共的头部长度x,而每个模型有个自己的头长度为y,文件中包含的模型数目为c,假设一个只有1个模型的文件数据开始位置是a,一个有3个模型的文件数据开始是b,那么这样列个方程,就可以求出每个模型的头部长度。
x+y=a;
x+3y=b;
经过比较几个模型文件,我发现很多模型的数据开始的位置(0x58)都是1500 0000。这个是一个比较奇怪的现象,因为按照我先前的分析,这个应该是贴图坐标,但是这么多的模型的贴图坐标怎么可能这么一致呢?再发现,如果按照先前假设的,FFFF FFFF 是顶点信息的结束标志,那么为什么最后一个顶点(box.pol文件)的FFFF FFFF 后面紧跟着一个295C 7F3F 295C 7F3F ,而这个数据和前面一个顶点的295C 7F3F 00D7 233B 数据这么相像。所以我开始意识到,FFFF FFFF 应该不是顶点的结束标志。因为一开始我先入为主地认为顶点有结束标志,所以我认为在头部没有顶点的总数信息,但是现在看来应该是有的。由于已知这个文件中有16个顶点,所以一眼就能看到在顶点数据区前面,0x5c的位置上有个1000 0000 数值,这不正是顶点的数目么。这下也就说明顶点的数据开始于0x60,且0x5c是顶点的数目,FFFF FFFF 不是顶点数据的结束标志,顶点的结构应该是这样的:struct vertex
{
float x;
float y;
float z;
int FFFFFFFF;
float u;
float v;
}
通过和其他的文件比较可以发现,每个模型的头部有24字节长。每个顶点数据区域最开始有个头,其中最后4个字节是顶点的数目。整个文件的头有56字节长,第5个字节开始是文件包含的模型数目。
按照这个理论把程序改了一下,发现贴图坐标也正常了。
所以现在的结论是这样的:文件头
-文件标识
-不知道的内容
-文件所包含的模型数目
-不知道的内容
每个模型的头
每个模型的数据
-顶点的数据
--顶点数据区的头
--每个顶点的数据
---顶点位置坐标
---FFFF FFFF
---顶点贴图坐标
-贴图数据(固定长度)
-面数据
--面数据头(包含了面的数目)
--面的数据
按照这个理论实现的程序:
xj3Model.c
这个程序运行以后可以把仙剑3的模型转换成obj的格式。经过测试,可以转换90%的模型,还有很多2K左右的文件处理不了,估计是索引文件,我没有仔细研究了。导入到3D Max里面基本上就是这个样子:



总结:愣看文件格式需要耐心和运气,像我看这个模型格式的运气就在于有一个box.pol的文件,这个文件名给了我暗示。另外就是文件中存在的FFFF FFFF ,虽然我最开始的假设是错误的,但是些字节还是整个文件的一个突破口。不过真把模型拆出来觉得意思也不大,感觉这些模型做的挺普通的。有时间我会把拆场景模型的文章也补上。


回复
1楼2017-10-20 14:06


    回复
    来自Android客户端5楼2017-10-21 13:10
      上面那个xj3Model.c那个东西呢


      收起回复
      来自手机贴吧7楼2017-10-22 22:52
        #define _CRT_SECURE_NO_DEPRECATE

        #include <iostream>
        #include <vector>
        using namespace std;


        //定义文件头
        struct fileHeader
        {
        //文件合法性判断
        char valid[4];
        //未知
        int iDontKnow;
        //模型计数
        int polyCount;
        };


        //模型头
        struct polyHeader
        {
        char iDontKnow[52];
        };


        //顶点头
        struct vertexHeader
        {
        char iDontKnow[28];
        int vertexCount;
        };


        //顶点
        struct vertex
        {
        //顶点的坐标
        float x;
        float y;
        float z;
        //FFFF FFFF
        long e;
        //顶点的贴图坐标
        float u;
        float v;
        };


        //贴图数据区
        struct betweenVertexAndFace
        {
        //贴图数据区,固定长度
        char iDontKnow[144];
        };


        //面数据区头
        struct faceHeader
        {
        int iDontKnow[2];
        int whoCares;
        int faceCount;
        };


        //三角面数据
        struct face
        {
        short a;
        short b;
        short c;
        };


        //入口
        int main(int argc, char* argv[])
        {
        //建立文件头
        struct fileHeader theFileHeader;
        struct polyHeader tempPolyHeader;
        //显示指定的文件名
        printf("input:%s\n", argv[1]);


        //打开指定的文件
        FILE *fp;
        fp = fopen(argv[1], "rb");


        //读取文件头
        fread(&theFileHeader, sizeof(struct fileHeader), 1, fp);
        //如果文件合法
        if (theFileHeader.valid[0] == 'P'&&theFileHeader.valid[1] == 'O'&&theFileHeader.valid[2] == 'L'&&theFileHeader.valid[3] == 'Y')
        {
        printf("这应该是一个正确的仙三POL文件\n");
        }
        else
        {
        //不合法退出
        printf("这不是仙三POL文件\n");
        getchar();
        exit(0);
        }


        //读取模型头
        for (int i = 0; i<theFileHeader.polyCount; i++)
        {
        fread(&tempPolyHeader, sizeof(struct polyHeader), 1, fp);
        }


        //存储顶点和面数据的空间
        vector<struct vertex> vertexList;
        vector<struct face> faceList;


        //遍历全部的模型
        for (int i = 0; i<theFileHeader.polyCount; i++)
        {
        //清空存储空间
        vertexList.clear();
        faceList.clear();
        //得到顶点头
        struct vertexHeader tempVertexHeader;
        fread(&tempVertexHeader, sizeof(struct vertexHeader), 1, fp);
        printf("vertex count [%d]\n", tempVertexHeader.vertexCount);
        //遍历全部的顶点
        for (int e = 0; e<tempVertexHeader.vertexCount; e++)
        {
        //保存顶点的数据
        struct vertex tempVertex;
        fread(&tempVertex, sizeof(struct vertex), 1, fp);
        vertexList.push_back(tempVertex);
        }
        //读取贴图数据
        struct betweenVertexAndFace tempBetween;
        fread(&tempBetween, sizeof(struct betweenVertexAndFace), 1, fp);
        //得到面数据的头部
        struct faceHeader tempFaceHeader;
        fread(&tempFaceHeader, sizeof(struct faceHeader), 1, fp);
        printf("face count [%d]\n", tempFaceHeader.faceCount);
        //遍历所有的面
        for (int e = 0; e<tempFaceHeader.faceCount; e++)
        {
        //保存数据
        struct face tempFace;
        fread(&tempFace, sizeof(struct face), 1, fp);
        faceList.push_back(tempFace);
        }
        //生成obj格式的文件
        char outName[100] = { 0 };
        sprintf(outName, "%s%dOut.obj", argv[1], i);
        FILE *outFp;
        outFp = fopen(outName, "w");
        printf("%s\n", outName);
        for (int e = 0; e<vertexList.size(); e++)
        {
        fprintf(outFp, "v %f %f %f \n", vertexList[e].x, vertexList[e].y, vertexList[e].z);
        }


        fprintf(outFp, "\n\n");


        for (int e = 0; e<vertexList.size(); e++)
        {
        fprintf(outFp, "vt %f %f\n", vertexList[e].u, vertexList[e].v);
        }


        fprintf(outFp, "\n\ng model%d\n", i);


        for (int e = 0; e<faceList.size(); e++)
        {
        fprintf(outFp, "f %d/%d %d/%d %d/%d\n", faceList[e].a + 1, faceList[e].a + 1, faceList[e].b + 1, faceList[e].b + 1, faceList[e].c + 1, faceList[e].c + 1);
        }
        fprintf(outFp, "\n\ng\n\n");
        fclose(outFp);
        }
        fclose(fp);
        printf("导出成功!\n");
        getchar();
        }


        收起回复
        8楼2018-03-26 13:54
          不知不觉已经过去快三年了


          应用达人
          应用吧活动,去领取
          活动截止:2100-01-01
          去徽章馆》
          回复
          来自Android客户端9楼2020-06-27 13:51
            前来考古


            回复
            10楼2020-06-27 22:14