【长文】在《 Ray Tracing from the Ground Up》的基础上实现BART的动画

第一部分:前言

本文是介绍在《Ray Tracing from the Ground Up》的那套代码的基础上怎么做出和BART官网上提供的视频差不多的动画。

大概一年前,小编写过一篇汇总性质的博文:
总结《Ray Tracing from the Ground Up》
https://blog.csdn.net/libing_zeng/article/details/72625390),算是读完了这本书。当时就想用光线追踪来做个动画试试看,但是这本书并没有尝试教我们怎么做。
后来决定读读《PBRT-V3》,因为感觉pbrt更为“高级”,读完之后应该会做动画了。读pbrt的过程可谓是充满心酸。pbrt讲的物理模拟,是教我们怎么更为真实地生成图形。pbrt的源代码算是商业级别的,不冗余,很抽象。代码和书都读得很辛苦,读完之后才发现pbrt“只是”其他光线追踪介绍书籍的“深入”,好像也没有教我们怎么做动画。
最近,决定回到刚读完《Ray Tracing from the Ground Up》的状态再次尝试用光线追踪来做个动画。为什么要回到一年前的状态呢?虽然花了不少时间分析pbrt的代码,但是自己还是觉得对《Ray Tracing from the Ground Up》的那套代码更为熟悉。另外,也是考虑到咱并不要求动画有pbrt涉及的那种真实程度。

也是在一年多前,知道了有BART这么个东东。关于BART,全称是“BART: A Benchmark for Animated Ray Tracing”,是对光线追踪动画算法的测试基准。也就是测试光线追踪的动画算法用的。那么用什么测试算法呢?作者提供了三个测试视频,这个些视频中覆盖了很多动画算法需要考虑的测试场景。用这些视频测试算法,然后根据测试结果给算法打个分。官网链接:(http://www.cse.chalmers.se/~uffe/BART/)。官网上提供了原论文、测试视频、源代码、动画描述文件(或者叫“脚本文件”吧)和原论文的一些其他论文。
小编铁定心一定要用光线追踪做出个动画,将BART官网上所有的内容看了好几遍,但总感觉是云里雾里。一方面,那些论文不是很好懂;另一方面,既然都有了源代码,我还有什么事情可以做呢?
后面发现:那些论文和我要做的事情压根没有关系;动画描述文件是对图形场景的描述(图形的几何信息、材质、纹理、运动等信息);源代码包含对描述文件的解析和动画算法(注意:其中不包含渲染部分)。
所以,为了做出和BART那三个测试视频差不多的的动画,我们需要关注的是:
1、描述文件;
2、源代码;
3、测试视频(主要是对比自己生成的动画)。

什么是动画?顾名思义,会动的画面。特点:1,有多张画面;2,这些画面中的某个部分是运动的(包含场景中所有物体静止,观看角度(相机)是运动的)。我们知道了怎么生成单张画面,所以接下来的就是搞清楚“怎么动”的问题了。这里针对接触过视频压缩的童鞋多说一句:视频压缩告诉我们去掉画面中的冗余信息,只保留运动信息;但是要注意“视频压缩”是“原始视频”的后期工作,而我们在这里要做的是压缩前的“原始视频”。
关于“怎么动”,这就涉及到“关键帧”。物体从A位置运动到B位置(这里的“位置”包含大小、角度、位置),物体在A、B位置处对应的两个画面是关键帧。aff文件中只会给出这两个位置的信息,那么物体从A位置到B位置的过程中的这些画面(称为“普通帧”吧)的位置信息怎么得到呢?这个过程一般是曲线的,所以可以通过对两个关键帧中物体的位置信息进行“曲线插值”得到普通帧中物体的位置信息。动画算法研究的主要关注点应该就是“插值算法”吧,anyway,这个不是我们这里要关心的,我们有这个概念就可以了。
总结一下做动画的思路:
一张一张地生成单个画面;考虑到画面中有些物体是运动的(也就是参数是变化的),所以在生成每一张画面前重新设置物体的参数(动画算法做的事情就是计算每个时间点运动物体的参数)。

到这里,我们默认:已经对《Ray Tracing from the Ground Up》比较熟悉;已经对aff文件比较熟悉;对“动画”的概念有简单的认识。如果不是,回头再看看:《Ray Tracing from the Ground Up》或者小编之前关于这本书所写的一些博文;按照BART官网上介绍地aff文件格式对比看看kitchen.aff及其所include的一些.aff文件;还有前面提到的“动画”内容。

先贴出自己做出的这三个动画视频。
kitchen:
https://v.youku.com/v_show/id_XMzY1NDI0MTQ5Mg==.html?spm=a2h3j.8428770.3416059.1
robots:
https://v.youku.com/v_show/id_XMzY1NDI0NDY1Mg==.html?spm=a2h3j.8428770.3416059.1
museum:(20180609,此刻优酷还在审核中。如果审核通过,应该和前面两个视频在一块)

第二部分:我们要做的事情

前面吧啦吧啦了这么多,终于说到重点了:我们要做的有哪些事情?
1、解析动画描述文件(.aff)。BART的源代码虽然提供了aff文件的解析程序,但它真的只是将aff文件信息单独地读入系统,得到的知识孤立的数据,完全忽略了aff文件中原有的数据结构关系。几何体几何体,材质是材质,纹理是纹理,运动是运动。几何体对应的是哪个材质哪个纹理怎么运动?这些在aff文件中是描述得很清楚的,但是在解析程序中完全没有体现。就像把一辆汽车拆成了一个一个零件堆在仓库里。所以,我们要做的是将这些零件组装起来,即,建立数据之间的关系。后面发现,这个是我们要做的主要事情(之一吧)。
2、建立和《Ray Tracing from the Ground Up》(RTGU)代码之间的联系。
几何部分:aff中给出的mesh信息并不是常见的ply文件格式,所以我们要将mesh信息转换成常见的ply文件格式(小编是直接转换之后信息以ply文件输出保存在硬盘上);
材质部分:aff文件中的fm信息是(amb_r amb_g amb_b diff_r diff_g diff_b spec_r spec_g spec_b Shine T index_of_refraction),而RTGU的程序中是ka、kd、ks,所以我们要增加对应的使用(amb_r amb_g amb_b) (diff_r diff_g diff_b) (spec_r spec_g spec_b)的材质(顺便提一下,RTGU原有的程序是不能改变纹理图片颜色的,使用新增的这些材质之后就可以改变纹理图片的颜色了。)
纹理部分:RTGU中的纹理坐标范围是[0,1],而aff文件的的有些纹理坐标的绝对值有大于1而且有正有负。纹理坐标的绝对值大于1,表示纹理图片被重复的次数(这个“次数”不一定是整数);纹理坐标为负数,表示反方向映射(若正数情况下,表示将图片“从左至右,从下至上”映射到物体上;那么,在负数情况下,表示将图片“从右至左,从上至下”映射到物体上。所以,RTGU中纹理映射这部分的代码是需要更改的。
运动部分:RTGU是通过Instance类是实现物体的缩放、旋转和平移的。但是其中的旋转部分只是沿着坐标轴旋转,而aff文件包含各种沿任意轴旋转的情况,所以我们要增加“沿任意轴旋转”的代码。
3、获取运动物体位置参数:解析完.aff文件之后,我们会将运动信息保留到全局变量中;设置物体参数前计算时间信息,然后根据时间信息和运动物体的名字,调用BART的源代码中提供动画算法的接口,从而获得当前时间点运动物体的位置参数。
4、生成动画:先是一张一张地生成画面(比如:kitchen是800张;robots是300张;museum是300张);将单张(单帧)画面合成动画(视频),我们用到是iMovie剪辑软件(其他视频剪辑软件应该也是可以的);压缩合成后的原始视频,我们是用HandBrake将原始视频转换成mp4的格式(可以将几百兆的原始视频压缩到几十兆)

第三部分:具体实现(以kitchen为例)

这里以生成kitchen动画为例,在RTGU代码的基础上完成具体代码的实现。robots动画的情况基本同kitchen;museum动画需要的一些其他东东后面再补充。

3.1 aff文件的结构

kitchen场景中运动的是toycar和camera。camera的行为比较单一,所以,此处我们以toycar为例看看aff文件的结构,看看aff文件是怎么将toycar的各个部件组装起来的和怎么设置各个部件的运动的。

首先说明一下:toycar的各个微小部件最初的中心都是在原点,而且尺寸可能很小。所以,需要进行缩放、旋转,然后平移到合适的位置,最终组装成toycar。

toycar由如下几个大的部件组成:车体car_body;四个轮子car_wheel。
车体中各个部件是统一运动的,所以,在组装完车体后,可以将车体看做一个整体。四个轮子则不一样。静态方面,左右轮子是关于整车的中轴对称的。动态方面,前后轮子的运动是不同的:整车运动时,前面两个轮子一方面要前后转动伴随整车的前进,另一方面要左右转动伴随整车前进方向的改变;后面两个轮子则只需要前后转动,无需左右转动。考虑到这两方面,四个轮子需要分开处理。

接下来,我们看看aff文件中是怎么描述toycar的这些关系的。
这里写图片描述

考虑到前轮是运动最为复杂部分,下面我们以前(左)轮为例看看aff的具体描述。
这里写图片描述这里写图片描述

从截图中,我们可以看到,一个前(左)轮的某个mesh(部件)从开始到最终呈现在图形中经过了如下阶段:
由mesh数据生成部件的几何图形(m:mesh);
设置部件的材质(含纹理)(fm,texture);
前后(旋转)的animation(x);
左右旋转的animation(x);
将部件平移到整车的合适位置(xs);
随着整车一起放大和上移(xs);
随着整车一起运动(x)。

3.2 组装数据

由于解析aff文件的过程是从上往下的,所以,在解析到x/xs/fm/texture数据时,我们并不知道这些数据是给那些部件(mesh)用的。而在解析到mesh时,由于独立解析x/xs/fm/texture数据已经完成,所以,我们怎么才能知道当前解析的mesh对应的x/xs/fm/texture数据是什么呢?

小编的做法是对每一个几何部件进行命名,然后生成对应的.ply、.fm、.xs(含x)、.tn(texture name)文件。
在创建每一个mesh时都去读取同名的.fm、.xs、.tn文件。(由于涉及到的mesh数目众多,按照RTGU那套代码在world::build()中逐一创建mesh,写build()的工作量有点大,所以,小编写了个函数来读取.ply、.fm、.xs、tn自动生成build()函数中的相应代码。

3.2.1 对几何部件进行命名

还是以左前轮为例。从开始解析kitchen.aff到解析到某个具体部件(mesh)依次打开了这些.aff文件:kitchen.aff、car.aff、car_front_wheel_left.aff、car_rotating_wheel_left.aff、car_wheel.aff。我们依次将这些文件的文件名依次包含到部件的命名中。另外在一个文件内部读到一个mesh或者include一个.aff(可能包含mesh)时进行计数。
最终得到左前轮的各个部件的命名是这样的:
这里写图片描述

下面看看在程序中的实现。

第〇步:
在parse.h中定义这样一个结构体FN:

    typedef struct File_name {
        char file_name[100];
        int sub_num;//number of meshes and inlude_files in this active file
    } FN;

在main.cpp中定义FN数组保存所有当前打开的文件的文件名和内部计数:
FN g_fn[20];//global variable for active file names
在main.cpp中定义当前打开的文件的总数:
int g_fn_num = 0;//number of active fn

第一步:
在main()函数中调用viParseFile()前将总的aff文件kitchen.aff的文件名数据到g_fn数组中(由于给定的文件名可能包含绝对路径,所以需要从包含绝对路径的文件名中分理出文件名);g_fn_num++。
在main()函数中解析完kitchen.aff(关闭文件)时,g_fn_num–。

第二步:
在viParseFile()中解析include、mesh、polygon(如果aff文件包含polygon而且你也有意对其进行命名)指令时,调用parseInclude()/parseMesh()/ parsePoly()函数前g_fn[g_fn_num].sub_num++;

第三步:
在parseInclude()函数中调用viParseFile()前将include的文件名数据到g_fn数组中;g_fn_num++。
在parseInclude()函数中解析完include的文件时,g_fn_num–。

在添加如上三处代码后,在解析mesh或者polygon(kitchen中没有polygon,暂不考虑)时g_fn[]数组中就已经写好了当前打开的所有文件的文件名及其内部计数的数据了。

第四步:
在parseMesh()函数中增加如下代码就可得到当前mesh部件的名字了。

    /*get current mesh name----begin----*/
    char str_mesh_name[100], str_sub_num[100];
    memset(str_mesh_name, 0x00, sizeof (str_mesh_name)); //clear string str_mesh_name
    for(int i=1; i<=g_fn_num; i++){
        if(i>1){
            strcat(str_mesh_name, "_");
        }
        strcat(str_mesh_name, g_fn[i].file_name);//connect strings
        strcat(str_mesh_name, "_");
        sprintf(str_sub_num, "%d", g_fn[i].sub_num);//transform int to string
        strcat(str_mesh_name, str_sub_num);
    }
    printf("current mesh: %s\n", str_mesh_name);
    /*get current mesh name----end----*/

3.2.2 将mesh数据输出为常见的ply文件

之所以做这个转换,还是为了用RTGU那套代码,另外确实看它不顺眼。童鞋不做这个转换,直接根据其给定的mesh数据创建三角形,然后进行加速肯定是可以的。但是,RTGU中现成的创建三角形、加速代码,为什么不用呢?

3.2.2.1 aff文件中的mesh和常见ply文件格式的区别

咱先对比一下aff文件中的mesh和常见ply文件格式有什么区别吧。

关于常见的ply文件,小编之前写过一篇博文:
Q81:“三角形网格”之“PLY文件”
https://blog.csdn.net/libing_zeng/article/details/61195502

关于aff文件中的mesh
这里写图片描述
为了将mesh数据转换成常见的ply文件,我们需要做三件事:
1,在三角形数据前添加数字“3”;
2,去掉三角形数据中的法向量坐标的索引和纹理坐标的索引;
3,将顶点坐标和纹理坐标对应起来。

3.2.2.2 填充vertices[][]和triangles[][]

前两件事so easy,而将顶点坐标和纹理坐标对应起来则需要特别处理了。
小编的做法是:在parseMesh()函数解析完所有数据之后,创建两个二维数组用于分别保存常见ply文件格式的顶点和三角形数据。
float vertices[num_verts][5];//顶点坐标+纹理坐标
int triangles[num_tris][4];//固定数字“3”+三个顶点坐标的索引

所以,接下来要做的事情就是将parseMesh()解析的顶点和三角形数据填到这两个数组中,然后将这两个数组输出生成ply文件。

小编是按照原三角形数据中顶点坐标索引的顺序来填vertices[][]和triangles[][]的。以上方截图中的三角形数据为例:
triangles 2
3 0 1 0 3 2 0 1 2//第一个三角形的三个顶点的索引分别是3 0 1
3 1 2 0 2 1 0 2 3//第二个三角形的三个顶点的索引分别是3 1 2
所以,小编填充vertices[][]的顺序是:
根据第一个三角形,依次填充vertices[3][]、vertices[0][]、vertices[1][];
根据第二个三角形,依次填充vertices[3][]、vertices[1][]、vertices[2][]。
发现其中vertices[3][]、vertices[1][]被填充了两次,是的,没有找到更好的办法。为了完整地填充vertices[][]数据,由于三角形共顶点的缘故,有的vertices[][]元素会被不止一次填充相同的数据。
而对于triangles[][]数据的填充,直接写固定数字“3”和原三角形数据中的顶点索引即可。

我们既然是依据原三角形数据来填充这两个数组的,那么就有必要看看parseMesh()是怎么解析和保存原三角形数据的。
parseMesh()是调用getTriangles()函数来解析原三角形数据的。在getTriangles()函数中的做法是:
其一,开辟了一坨空间来保存三角形数据的,然后用指针idx指向这坨空间:

    unsigned short *idx;
    int i,v[3],n[3],t[3];

    allocsize=3;//为单个三角形的顶点数据分配的空间数为3份
    if(norms) allocsize+=3;//如果有法向量,单个三角形的数据分配的空间数加3份
    if(txts) allocsize+=3; //如果有纹理坐标,单个三角形的数据分配的空间数加3份


    idx=(unsigned short *)malloc(num*allocsize*sizeof(unsigned short));
    //为num个三角形申请num个allocsize份空间

其二,依次解析三角形数据。将单个三角形的数据临时保存到v[3]、t[3]、n[3]中。
其三,将v[3]、t[3]、n[3]中的数据复制到指针idx指向的空间中:

        /* indices appear in this order: [texture] [normals] vertices. []=optional */
        for(w=0;w<3;w++)
        {
            if(txts) idx[i++]=t[w];
            if(norms) idx[i++]=n[w];
            idx[i++]=v[w];
        }

其四,“其二、其三”循环num次。

还是前面的三角形为例,看一下三角形数据是怎么保存的。
triangles 2
3 0 1 0 3 2 0 1 2
3 1 2 0 2 1 0 2 3//第二个三角形的三个顶点的索引分别是3 1 2
//第一个三角形的三个顶点的索引分别是3 0 1
//第一个三角形的三个顶点的法向量坐标的索引分别是0 3 2
//第一个三角形的三个顶点的纹理坐标的索引分别是0 1 2
idx[]中保存数据的顺序是纹理、法向量、顶点,所以第一个三角形的数据保存在idx[]中的位置是:
纹理部分(idx[0]=0、idx[1]=1、idx[2]=2)、法向量部分(idx[3]=0、idx[4]=3、idx[5]=2)、顶点部分(idx[6]=3、idx[7]=0、idx[8]=1)
第二个三角形的数据保存在idx[]中的位置是:
纹理部分(idx[9]=0、idx[10]=2、idx[11]=3)、法向量部分(idx[12]=0、idx[13]=2、idx[14]=1)、顶点部分(idx[15]=3、idx[16]=1、idx[17]=2)

知道解析后三角形数据在idx[]中的样子,接下来我们可以将idx[]中的数据填充到vertices[][]和triangles[][]了。
首先定义几个变量:

    int has_norms = 0;//标示idx[]数据中是否有法向量坐标的索引
    int has_txts = 0; //标示idx[]数据中是否有纹理坐标的索引
int idx_t_pos[3], idx_n_pos[3], idx_v_pos[3];
//这三个变量分别表示纹理坐标索引(3个)、法向量坐标索引(3个)、顶点索引(3个)在idx[]中的单个三角形数据中的位置

    if(norms){has_norms = 1; }
    if(txts){ has_txts = 1; }

    int idx_d = (has_txts + has_norms + 1) * 3;//indices dimension for each vertex
//idx[]中的单个三角形数据的个数

    //indices is obtained by "getTriangles(fp,&num_tris,&indices,verts,norms,txts)"
    int i = 0;
    for(int w=0; w<3; w++){
    //根据getTriangles()中保存数据的顺序:纹理、法向量、顶点,计算各类数据在单个三角形数据中的位置
        if(txts){ idx_t_pos[w] = i++;}
        if(norms){ idx_n_pos[w] = i++;}
        idx_v_pos[w] = i++;
    }

下面正式填充vertices[][]和triangles[][]:

    for(int i=0; i< num_tris; i++){
        triangles[i][0] = 3;
        triangles[i][1] = indices[i*idx_d+idx_v_pos[0]];
        triangles[i][2] = indices[i*idx_d+idx_v_pos[1]];
        triangles[i][3] = indices[i*idx_d+idx_v_pos[2]];

        vertices[indices[i*idx_d+idx_v_pos[0]]][0] = verts[indices[i*idx_d+idx_v_pos[0]]][0];
        vertices[indices[i*idx_d+idx_v_pos[0]]][1] = verts[indices[i*idx_d+idx_v_pos[0]]][1];
        vertices[indices[i*idx_d+idx_v_pos[0]]][2] = verts[indices[i*idx_d+idx_v_pos[0]]][2];

        vertices[indices[i*idx_d+idx_v_pos[1]]][0] = verts[indices[i*idx_d+idx_v_pos[1]]][0];
        vertices[indices[i*idx_d+idx_v_pos[1]]][1] = verts[indices[i*idx_d+idx_v_pos[1]]][1];
        vertices[indices[i*idx_d+idx_v_pos[1]]][2] = verts[indices[i*idx_d+idx_v_pos[1]]][2];

        vertices[indices[i*idx_d+idx_v_pos[2]]][0] = verts[indices[i*idx_d+idx_v_pos[2]]][0];
        vertices[indices[i*idx_d+idx_v_pos[2]]][1] = verts[indices[i*idx_d+idx_v_pos[2]]][1];
        vertices[indices[i*idx_d+idx_v_pos[2]]][2] = verts[indices[i*idx_d+idx_v_pos[2]]][2];

        if(txts){
            vertices[indices[i*idx_d+idx_v_pos[0]]][3] = txts[indices[i*idx_d+idx_t_pos[0]]][0];
            vertices[indices[i*idx_d+idx_v_pos[0]]][4] = txts[indices[i*idx_d+idx_t_pos[0]]][1];
            vertices[indices[i*idx_d+idx_v_pos[1]]][3] = txts[indices[i*idx_d+idx_t_pos[1]]][0];
            vertices[indices[i*idx_d+idx_v_pos[1]]][4] = txts[indices[i*idx_d+idx_t_pos[1]]][1];
            vertices[indices[i*idx_d+idx_v_pos[2]]][3] = txts[indices[i*idx_d+idx_t_pos[2]]][0];
            vertices[indices[i*idx_d+idx_v_pos[2]]][4] = txts[indices[i*idx_d+idx_t_pos[2]]][1];
        }
    }

3.2.2.3 输出常见的ply文件

ply文件的名字即是“3.2.1”中得到的名字,此处的变量是ply_file_name。
先写ply文件的header;再写vertices(直接输出前面填充的vertices[][]);最后写triangle(直接输出前面填充的triangles[][])

    /*creat ply files---------begin-------*/
    char new_file[200] = "/Users/libingzeng/CG/kitchen/ply/";
    strcat(ply_file_name, ".ply");  // connect strings
    strcat(new_file, ply_file_name);// new_file will be a whole path including file name for the file created.


    FILE *fp_new =fopen(new_file,"at");
    if(!fp_new)
    {
        printf("Error: could not open file: <%s>.\n",new_file);
        exit(1);
    }

    //write header for ply file.
    fprintf(fp_new, "ply\n");
    fprintf(fp_new, "format ascii 1.0\n");
    fprintf(fp_new, "comment author: Libing Zeng transforms this from .aff of bart\n");
    fprintf(fp_new, "element vertex %d\n", num_verts);
    fprintf(fp_new, "property float x\n");
    fprintf(fp_new, "property float y\n");
    fprintf(fp_new, "property float z\n");
    if(txts){
        fprintf(fp_new, "property float u\n");
        fprintf(fp_new, "property float v\n");
    }
    fprintf(fp_new, "element face %d\n", num_tris);
    fprintf(fp_new, "property list int int vertex_indices\n");
    fprintf(fp_new, "end_header\n");

    //write vertices data for the new file
    for(int j=0; j<num_verts; j++)
    {
        if(txts){
            fprintf(fp_new, "%f %f %f %f %f\n", vertices[j][0], vertices[j][1], vertices[j][2], vertices[j][3], vertices[j][4]);
        }
        else{
            fprintf(fp_new, "%f %f %f\n", vertices[j][0], vertices[j][1], vertices[j][2]);
        }
    }

    //write triangles data for the new file
    for(int k=0; k<num_tris; k++)
    {
        fprintf(fp_new, "%d %d %d %d\n", triangles[k][0], triangles[k][1], triangles[k][2], triangles[k][3]);
    }

    fclose(fp_new);
    /*creat ply files---------end-------*/

3.2.3 输出mesh的纹理图片名(texture name)文件.tn

单个mesh的纹理图片只有一个,所以直接将parseMesh()中读到的texturename输出即可。

    /*creat texture_name files---------begin-------*/
    char new_tn_file[200] = "/Users/libingzeng/CG/kitchen/tn/";
    strcat(tn_file_name, ".tn");  // connect strings
    strcat(new_tn_file, tn_file_name);// new_file will be a whole path including file name for the file created.
    FILE *fp_tn_new =fopen(new_tn_file,"at");
    if(!fp_tn_new)
    {
        printf("Error: could not open file: <%s>.\n",new_tn_file);
        exit(1);
    }

    fprintf(fp_tn_new, "%s ", texturename);

    fclose(fp_tn_new);
    /*creat fm files---------end-------*/

另外,在parseMesh()定义“texturename”处,最好将其初始化为空字符串,因为如果不初始化(系统给它一个随机值)
strcpy(texturename, “”);//initiate texturename for avoiding random initial value. 20180602

3.2.4 输出mesh的材质文件.fm

考虑到只有离mesh最近的那个fm数据会对mesh产生作用,所以只需用一个全局变量保存最近解析的fm数据,然后将这个数据输出到与mesh同名的.fm文件中即可。这里需要提出的是,在aff文件中“离mesh最近的那个fm”并不一定在m指令上面一行或者几行。因为要考虑“很多个mesh共一个fm”的情况,所以fm数据和当前mesh之间可能隔着很多个同fm的其他mesh的数据。

小编的做法是:
1,在parse.h中定义fm的结构体

    typedef struct Fm {
        float fm[12];
    } FM;

2,在main.cpp中定义全局变量
FM g_fm;//global variable for active material
3,在parseFill()函数解析完fm数据后,将数据保存到全局变量g_fm中

        g_fm.fm[0] = amb[X]; g_fm.fm[1] = amb[Y]; g_fm.fm[2] = amb[Z];
        g_fm.fm[3] = dif[X]; g_fm.fm[4] = dif[Y]; g_fm.fm[5] = dif[Z];
        g_fm.fm[6] = spc[X]; g_fm.fm[7] = spc[Y]; g_fm.fm[8] = spc[Z];
        g_fm.fm[9] = phong_pow; g_fm.fm[10] = t; g_fm.fm[11] = ior;

4,在parseMesh()中全局变量g_fm的内容直接输出到和mesh同名的.fm文件中

3.2.5 输出mesh的变形(静态变换xs和animation的名字)文件.xs
由“3.1 aff文件的结构”中的内容,我们知道mesh的变换(xs和x)有严格的顺序限制,所以我们必须保证输出到文件的xs、x数据的顺序和aff文件中描述的顺序是一致的。

小编的做法是:

1,在parse.h中定义结构体XS

    typedef struct Xs {
        int type;//1: for x; 2:for xs
        float xs[10];//for xs
        char name[100];//for x
} XS;

(对于x数据,只需要保存对应的animation name,具体设置animation参数时,根据name在animation list中查找对应的animation。关于animation信息的保存,后面再补充)

2,在main.cpp中定义全局变量

XS g_xs[20];//global variable for active transform (including x and xs)
int g_xs_num = 0;//number of active xs (including x)

3,分别在parseXform()函数解析完xs数据和x数据后,将解析后的数据保存到g_xs[]中;g_xs_num++

4,在viPar

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值