1. 程式人生 > >【長文】在《 Ray Tracing from the Ground Up》的基礎上實現BART的動畫

【長文】在《 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檔案;還有前面提到的“動畫”內容。

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

前面吧啦吧啦了這麼多,終於說到重點了:我們要做的有哪些事情?
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檔案格式有什麼區別吧。

關於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,在viParseFile()函式讀到“}”時,g_xs_num–。
這個地方設定“g_xs_num–”,感覺有點草率。但是,小編髮現好像只有x和xs對應的“}”需要留到viParseFile()函式來解析。在kitchen、robots、museum三個場景中,只發現有“}”的只有x、xs和k三種描述指令。x、xs的資料是直接跟在指令之後的(x的資料只是animation name),後面的{}只是表示x、xs的作用範圍(物件),當碰到“}”時,說明作用範圍結束。而k指令不一樣:
其一,它對應的資料是用{}包起來的;
其二,它的資料格式不確定。
這裡的“格式不確定”指的是“並不是所有的animation都包含所有四種animation型別(scale、rot、transl、visibility)”(很多時候都只有其中的一種、兩種、三種)。所以,parseKeyFrames()中只有通過是否讀到“}”來判斷當前animation的資料是否結束。這樣一來k指令的“}”在parseKeyFrames()中完成了解析。這樣一來,解析檔案中剩餘的需要viParseFile()函式來解析的“}”只能是屬於x、xs的了。

5,在parseMesh()中輸出mesh的變形檔案.xs
經過上面的1、2、3、4,在解析mesh資料時,g_xs[]中儲存的就是當前mesh的完整的x、xs引數了。所以,在parseMesh()中輸出g_xs[]的內容即是當前mesh的變形檔案了。這裡需要提一下的是,由於g_xs[]儲存的資料包含x和xs資料,所以在按格式輸出時,需要根據type來判斷是x還是xs,以便確定對應的輸出格式。

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

    for(int j=g_xs_num; j>=1; j--){
        if(g_xs[j].type == 1){
            fprintf(fp_xs_new, "%s\n", g_xs[j].name);
        }
        if(g_xs[j].type == 2){
            for(int k=0; k<10; k++){
                fprintf(fp_xs_new, "%f ", g_xs[j].xs[k]);

            }
            fprintf(fp_xs_new, "\n");
        }
    }
    /*creat xs files---------end-------*/

3.2.6 將所有的animation資料儲存到animation list中

這裡先說明一下,前面生成.ply、.tn、.fm、.xs檔案的程式可以獨立渲染的主函式main()執行,不管在哪裡執行都可以,只要能夠生成這四類檔案即可。但是,animation資料的儲存就必須在渲染的主函式中執行,因為animation list是在渲染函式中需要用到的。這裡會引出一個問題,即:渲染主函式main()會再次呼叫viParseFile()解析所有的aff,那麼那些.ply、.tn、.fm、.xs豈不是會再生成一次。而寫之間又是以“at”的方式寫這些檔案的,這樣,.ply、.tn、.fm、.xs檔案中前後就有重複的兩份資料了。那麼是不是可以將之前寫檔案的方式改為“w”(覆蓋的方式)呢?貌似不能,因為x、xs資料是多次開啟檔案先後寫入的。所以,小編的做法是將前面新增的所有程式碼都用一個叫做“CREATE_PLY_FM_XS_TN_FILES”的巨集包起來的,而渲染主函式要用到的animation資料的全域性變數相關的程式碼是用一個叫做“KITCHEN_ANIMATION”的巨集包起來。為了是這些巨集能夠在多個檔案中使用,小編添加了個叫做Macro.h中的標頭檔案,然後在這個標頭檔案中定義所有的巨集。需要用到這些巨集的檔案只需要include這個標頭檔案即可。

關於animation資料的解析和animation list的生成,BART作者推薦的做法(這裡不能再說“小編的做法”了)在parseKeyFrames()函式中有提示。

小編是先在main.cpp中定義了全域性變數:
struct AnimationList* mAnimations;
然後按照BART作者在parseKeyFrames()函式中的提示,在parseKeyFrames()函式中的如下添加了幾行程式碼:
這裡寫圖片描述

關於“怎麼從mAnimations連結串列中獲取某個時刻的animation資料”:
1,定義一個Animation變數
Animation *anim;
2,通過呼叫BART提供的動畫演算法介面FindAnimation()
anim = FindAnimation(name, mAnimations);
將前面的連結串列mAnimations和需要查詢的animation名字name通過FindAnimation()函式傳給動畫演算法就可以得到對應的animation。
3,根據animation獲取當前t時刻的運動資料。
_GetTranslation(anim, t, trans);
_GetRotation(anim, t, rot);
_GetScale(anim, t, scale);
通過呼叫動畫演算法的如上三個介面可以得到t時刻的運動資料(具體是:根據t時刻前後兩個關鍵幀的資料進行曲線插值得到t時刻的資料)。此刻,這些資料就可以如同xs資料一樣設定到name對應的所有mesh了。注意,一個name可能對應著多個mesh的。比如:前面提到的“toycar”對應著整車的所有部件(mesh)。

3.3建立和RTGU程式碼之間的聯絡

如“第二部分”中提到,RTGU當前的那套程式碼是不能直接滿足需求的,所以我們需要增加和改動一些程式碼。其中“幾何部分”,我們已經在“3.2.2”中完成。

3.3.1 RTGU中“材質部分”程式碼的增改

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)的材質。

3.3.1.1 增加兩個BRDF:SV_Lambertian_FM、SV_GlossySpecular_FM

仿照原有的SV_Lambertian、SV_GlossySpecular,增加兩個新的BRDF:
SV_Lambertian_FM、SV_GlossySpecular_FM。

SV_Lambertian_FM類中的修改:
0,將SV_Lambertian類複製過來之後,將原來的“SV_Lambertian”全部替換成“SV_Lambertian_FM”。
1,SV_Lambertian中的成員變數float kd,在SV_Lambertian_FM中對應改成Texture* kd。
2,kd的賦值方法改成這樣:
inline void SV_Lambertian_FM::set_kd(Texture* t_ptr) {kd = t_ptr; }
3,f()、sample_f()、rho()中的“kd * (cd->get_color(sr))”改成這樣:
RGBColor c_kd = kd->get_color(sr);
RGBColor c_cd = cd->get_color(sr);
RGBColor(c_kd.r*c_cd.r, c_kd.g*c_cd.g, c_kd.b*c_cd.b)
4,如果編譯fail,簡單修一下

SV_GlossySpecular _FM類中的修改:
0,將SV_GlossySpecular類複製過來之後,將原來的“SV_GlossySpecular”全部替換成“SV_GlossySpecular _FM”。
1,SV_GlossySpecular中的成員變數float ks,在SV_GlossySpecular_FM中對應改成Texture* ks。
2,ks的賦值方法改成這樣:
inline void SV_GlossySpecular_FM::set_ks(Texture* t_ptr) {ks = t_ptr; }
3,f()、sample_f()、rho()中的“ks * (cs->get_color(sr))”改成這樣:

RGBColor c_ks = ks->get_color(sr);
RGBColor c_cs = cs->get_color(sr);
RGBColor(c_ks.r*c_cs.r, c_ks.g*c_cs.g, c_ks.b*c_cs.b)

4,如果編譯fail,簡單修一下

3.3.1.2 增加兩個Material:SV_Phong_FM、SV_GlossyReflector_FM

仿照原有的SV_Phong、SV_GlossyReflector,增加兩個新的Material:
SV_Phong_FM、SV_GlossyReflector_FM。

SV_Phong_FM類中的修改:
0,將SV_Phong類複製過來之後;將原來的“SV_Phong”全部替換成“SV_Phong_FM”;
1,將原來的“SV_Lambertian”全部替換成“SV_Lambertian_FM”,將原來的“SV_GlossySpecular”全部替換成“SV_GlossySpecular_FM”
2,賦值方法改成這樣:

inline void SV_Phong_FM::set_ka(Texture* t_ptr) {ambient_brdf->set_kd(t_ptr);}
inline void SV_Phong_FM::set_kd (Texture* t_ptr) {diffuse_brdf->set_kd(t_ptr);}
inline void SV_Phong_FM::set_ks (Texture* t_ptr) {specular_brdf->set_ks(t_ptr);}

3,如果編譯fail,簡單修一下

SV_GlossyReflector_FM類中的修改:
0,將SV_ GlossyReflector類複製過來之後;將原來的“SV_ GlossyReflector”全部替換成“SV_ GlossyReflector _FM”;
1,將原來的“SV_GlossySpecular”全部改成“SV_GlossySpecular_FM”,將原來的“SV_Phong”全部改成“SV_Phong_FM”
2,賦值方法改成這樣:
inline void SV_GlossyReflector_FM::set_kr(Texture* t_ptr) {
glossy_specular_brdf->set_ks(t_ptr);}
3,如果編譯fail,簡單修一下

3.3.2 RTGU程式碼中“紋理部分”程式碼的增改

RTGU中的紋理座標範圍是[0,1],而aff檔案的的有些紋理座標的絕對值有大於1而且有正有負。紋理座標的絕對值大於1,表示紋理圖片被重複的次數(這個“次數”不一定是整數);紋理座標為負數,表示反方向對映(若正數情況下,表示將圖片“從左至右,從下至上”對映到物體上;那麼,在負數情況下,表示將圖片“從右至左,從上至下”對映到物體上。前面的這些理解,小編是借鑑OpenGL的做法。
ImageTexture.cpp中的get_color()方法改成這樣:

RGBColor ImageTexture::get_color(const ShadeRec& sr) const {    
    int row;
    int column;
    double vv = - sr.v;//這裡是因為aff中的紋理座標和RTGU中的貌似是反向的
    double uu = sr.u;
    double tv, tu;

    if (mapping_ptr)
        mapping_ptr->get_texel_coordinates(sr.local_hit_point, hres, vres, row, column);
    else
    {
        if(abs(vv) > 100 || abs(uu) > 100)//只考慮絕對值小於100的紋理座標
        {vv = 0.0; uu = 0.0; }

        tv = abs(vv - floor(vv));//這兩句實現重複貼圖,同時考慮反向貼圖
        tu = abs(uu - floor(uu));

        row     = (int)(tv * (vres - 1));
        column  = (int)(tu * (hres - 1));
#if 1
        if(row < 0) {row = 0;}
        if(column < 0) {column = 0; }
#endif
    }
    return (image_ptr->get_color(row, column));
}  

另外,有個奇葩的問題:aff檔案提供的mesh資料中有些三角形的三個頂點壓根不能組成三角形。這種情況導致RTGU中無法計算法向量(即:得到法向量是(NaN, NaN, NaN))。為了避免這個問題,在MeshTriangle::compute_normal()函式計算完normal之後,新增這段程式碼:

    if(isnan(normal.x)){normal = Vector3D(1.0, 0.0, 0.0); }

3.3.3 RTGU程式碼中“運動/變換部分”程式碼的增改

RTGU是通過Instance類是實現物體的縮放、旋轉和平移的。但是其中的旋轉部分只是沿著座標軸旋轉,而aff檔案包含各種沿任意軸旋轉的情況,所以我們要增加“沿任意軸旋轉”的程式碼。
“沿任意軸旋轉”的原理參考PBRT-V3的“2.7.6 ROTATION AROUND AN ARBITRARY AXIS”章節。
在Instance類中新增rotate_axis()方法:

void
Instance::rotate_axis(const double theta, const Vector3D& axis) {
    double sin_theta = sin(theta * PI / 180.0);
    double cos_theta = cos(theta * PI / 180.0);
    Vector3D a(axis);
    a.normalize();

    Matrix axis_rotation_matrix; // temporary rotation matrix about z axis

    // Compute rotation of first basis vector
    axis_rotation_matrix.m[0][0] = a.x * a.x + (1 - a.x * a.x) * cos_theta;
    axis_rotation_matrix.m[0][1] = a.x * a.y * (1 - cos_theta) - a.z * sin_theta;
    axis_rotation_matrix.m[0][2] = a.x * a.z * (1 - cos_theta) + a.y * sin_theta;
    axis_rotation_matrix.m[0][3] = 0;

    // Compute rotations of second and third basis vectors
    axis_rotation_matrix.m[1][0] = a.x * a.y * (1 - cos_theta) + a.z * sin_theta;
    axis_rotation_matrix.m[1][1] = a.y * a.y + (1 - a.y * a.y) * cos_theta;
    axis_rotation_matrix.m[1][2] = a.y * a.z * (1 - cos_theta) - a.x * sin_theta;
    axis_rotation_matrix.m[1][3] = 0;

    axis_rotation_matrix.m[2][0] = a.x * a.z * (1 - cos_theta) - a.y * sin_theta;
    axis_rotation_matrix.m[2][1] = a.y * a.z * (1 - cos_theta) + a.x * sin_theta;
    axis_rotation_matrix.m[2][2] = a.z * a.z + (1 - a.z * a.z) * cos_theta;
    axis_rotation_matrix.m[2][3] = 0;

    forward_matrix = axis_rotation_matrix * forward_matrix;


    Matrix inv_axis_rotation_matrix; // temporary inverse rotation matrix about axis
    sin_theta = sin_theta * (-1);     // rotate in the opposite direction.
    cos_theta = cos_theta * (1);

    // Compute rotation of first basis vector
    inv_axis_rotation_matrix.m[0][0] = a.x * a.x + (1 - a.x * a.x) * cos_theta;
    inv_axis_rotation_matrix.m[0][1] = a.x * a.y * (1 - cos_theta) - a.z * sin_theta;
    inv_axis_rotation_matrix.m[0][2] = a.x * a.z * (1 - cos_theta) + a.y * sin_theta;
    inv_axis_rotation_matrix.m[0][3] = 0;

    // Compute rotations of second and third basis vectors
    inv_axis_rotation_matrix.m[1][0] = a.x * a.y * (1 - cos_theta) + a.z * sin_theta;
    inv_axis_rotation_matrix.m[1][1] = a.y * a.y + (1 - a.y * a.y) * cos_theta;
    inv_axis_rotation_matrix.m[1][2] = a.y * a.z * (1 - cos_theta) - a.x * sin_theta;
    inv_axis_rotation_matrix.m[1][3] = 0;

    inv_axis_rotation_matrix.m[2][0] = a.x * a.z * (1 - cos_theta) - a.y * sin_theta;
    inv_axis_rotation_matrix.m[2][1] = a.y * a.z * (1 - cos_theta) + a.x * sin_theta;
    inv_axis_rotation_matrix.m[2][2] = a.z * a.z + (1 - a.z * a.z) * cos_theta;
    inv_axis_rotation_matrix.m[2][3] = 0;

    inv_matrix = inv_matrix * inv_axis_rotation_matrix;
}

3.4 設定animation的引數

這裡算是承接“3.2.6”。

3.4.1 普通物體animation引數的設定

3.4.1.1 獲取物體animation引數

小編定義了get_animation()函式。在該函式中呼叫動畫演算法一些介面,從而獲取某個時刻t某個運動物體name的運動引數trans、rot、scale。
前面我們提到並不是所有的動畫都包含trans、rot、scale,所以我們用trans_flag、rot_flag、scale_flag來分別標記是否有對應的資料。即:trans_flag=1時,trans中才有資料;rot_flag=1時,rot中才有資料;scale_flag=1時,scale中才有資料。
函式中用到的animation連結串列“mAnimations”是全域性變數。

bool get_animation(char* name, double time,
                   double trans[3], double rot[4], double scale[3