OpenGL 3D 模型載入和渲染

glumes發表於2018-07-03

部落格原文連結:https://glumes.com/post/opengl/opengl-tutorial-import-3d-object/

在使用 OpenGL 繪製時,我們最多繪製的是一些簡單的圖形,比如三角形、圓形、立方體等,因為這些圖形的頂點數量不多,還是可以手動的寫出那些頂點的,可要是繪製一些複雜圖形該怎麼辦呢?

這時候就可以使用 OpenGL 來載入 3D 模型。先使用 3D 建模工具構建物體,然後再將物體匯出成特定的檔案格式,最終通過 OpenGL 渲染模型。

例如如下的 3D 模型檔案影象:

OpenGL 3D 模型載入和渲染

Obj 模型檔案

obj 模型檔案是眾多 3D 模型檔案中的一種,它的格式比較簡單,本質上就是文字檔案,只是格式固定了格式。

obj 檔案將頂點座標、三角形面、紋理座標等資訊以固定格式的文字字串表示。

擷取一小段 obj 檔案內容:

# Max2Obj Version 4.0 Mar 10th, 2001
#
# object (null) to come ...
#
v  -0.052045 11.934561 -0.071060
v  -0.052045 11.728649 1.039199
...
# 288 vertices
vt 0.000000 0.000000 0.000000
vt 1.000000 0.000000 0.000000
...
vt 1.000000 1.000000 0.000000
# 122 texture vertices
vn 0.000000 0.000000 -1.570796
vn 0.000000 0.000000 -1.570796
...
vn 0.000000 0.000000 1.570796
# 8 vertex normals
g (null)
f 1/10/1 14/12/11 13/4/11 
f 1/11/4 2/12/3 14/12/11
...
f 5/4/5 7/3/7 3/1/3
# 576 faces
g
複製程式碼
  • "#" 開頭的行表示註釋,載入過程中可以忽略
  • “v” 開頭的行用於存放頂點座標,後面三個數表示一個頂點的 x , y , z 座標 如:
v  -0.052045 11.934561 -0.071060
複製程式碼
  • "vt" 開頭的行表示存放頂點紋理座標,後面三個數表示紋理座標的 S,T,P 分量,其中 P 指的是深度紋理取樣,主要用於 3D 紋理的取樣,但使用的較少 如:
vt 0.000000 0.000000 0.000000
複製程式碼
  • "vn" 開頭的行用於存放頂點法向量,後面三個數分別表示一個頂點的法向量在 x 軸,y 軸,z 軸上的分量。 如:
vn 0.000000 0.000000 1.570796
複製程式碼
  • “g” 開頭的行表示一組的開始,後面的字串為此組的名稱。組就是由頂點組成的一些面的集合,只包含 “g” 的行表示一組的結束,與 “g” 開頭的行對應。
  • "f" 開頭的行表示組中的一個面,對於三角形圖形,後面有三組用空格分隔的資料,代表三角形的三個頂點。每組資料中包含 3 個數值,用 / 分隔,依次表示頂點座標資料索引頂點紋理座標資料索引頂點法向量資料索引,注意這裡都是指索引,而不是指具體資料,索引指向的是具體哪一行對應的座標 如:
f 1/10/1 14/12/11 13/4/12 
複製程式碼

如上資料代表了三個頂點,其中三角形 3 個頂點座標來自 1、14、13 號以 "v" 開頭的行, 3 個頂點的紋理座標來自 10、12、4 號以 “vt” 開頭的行,3 個頂點的法向量來自 1、11、12 號以 “vn” 開頭的行。

如果頂點座標沒有法向量和紋理座標,那麼直接可以忽略,用空格將三個頂點座標索引分開就行

f 1 3 4
複製程式碼

最後 OpenGL 在繪製時採用的是 GL_TRIANGLES,也就是由 ABCDEF 六個點繪製 ABC、DEF 兩個三角形,所以 "f" 開頭的行都代表繪製一個獨立的三角形,最終影象由一個一個三角形拼接組成,並且彼此的點可以分開。

載入 Obj 模型檔案

明白了 Obj 模型檔案代表的含義,接下來把它載入並用 OpenGL 進行渲染。

Obj 模型檔案實質上也就是文字檔案了,通過讀取每一行來進行載入即可,假設載入的模型檔案只有頂點座標,實際程式碼如下:

		// 載入所有的頂點座標資料,把 List 容器的 index 當成 索引
        ArrayList<Float> alv = new ArrayList<>();
        // 代表繪製影象的每一個小三角形的座標
        ArrayList<Float> alvResult = new ArrayList<>();
        // 最終要傳入給 OpenGL 的陣列
        float[] vXYZ;
        try {
            InputStream in = context.getResources().getAssets().open(fname);
            InputStreamReader isr = new InputStreamReader(in);
            BufferedReader br = new BufferedReader(isr);
            String temps = null;
			// 遍歷每一行來讀取內容
            while ((temps = br.readLine()) != null) {
	            // 正規表示式 用空格分開
                String[] tempsa = temps.split("[ ]+");
                // 先把所有的頂點座標加入到 List 中,這樣就有了索引
                if (tempsa[0].trim().equals("v")) {
                    alv.add(Float.parseFloat(tempsa[1]));
                    alv.add(Float.parseFloat(tempsa[2]));
                    alv.add(Float.parseFloat(tempsa[3]));
                } else if (tempsa[0].trim().equals("f")) {
                // 根據 f 指示的索引,找到對應的頂點座標,
                // 這裡 -1 的操作是因為 List 從 0 開始,f 開頭的行的索引從 1 開始
                // *3 是因為要跳過 3 的倍數個頂點
                    int index = Integer.parseInt(tempsa[1].split("/")[0]) - 1;
                    alvResult.add(alv.get(3 * index));
                    alvResult.add(alv.get(3 * index + 1));
                    alvResult.add(alv.get(3 * index + 2));

                    index = Integer.parseInt(tempsa[2].split("/")[0]) - 1;
                    alvResult.add(alv.get(3 * index));
                    alvResult.add(alv.get(3 * index + 1));
                    alvResult.add(alv.get(3 * index + 2));

                    index = Integer.parseInt(tempsa[3].split("/")[0]) - 1;
                    alvResult.add(alv.get((3 * index)));
                    alvResult.add(alv.get((3 * index + 1)));
                    alvResult.add(alv.get((3 * index + 2)));
                }
            }
            // 把面的座標轉換為最終要傳遞給 OpenGL 的陣列
            // 根據這個陣列,然後按照 GL_TRIANGLES 方式進行繪製
            int size = alvResult.size();
            vXYZ = new float[size];
            for (int i = 0; i < size; i++) {
                vXYZ[i] = alvResult.get(i);
            }
            return vXYZ;
        } catch (IOException e) {
            return null;
        }
複製程式碼

通過上面的函式就計算出了最終的頂點座標位置,並將此頂點座標位置傳入給 GPU ,通過 FloatBuffer 進行轉換等等,這就和之前的文章內容相同了。

OpenGL 3D 模型載入和渲染

如果只是單純的匯入了所有頂點,並決定了要繪製的顏色,就會出現類似上面的單一顏色的繪製情況,事實上可以通過修改片段著色器來給 3D 模型新增條紋著色效果。

利用著色器新增條紋著色效果

通過修改片段著色器來給 3D 形狀新增條紋著色效果。

precision mediump float;
varying  vec3 vPosition;  //頂點位置
void main() {
   vec4 bColor=vec4(0.678,0.231,0.129,0);//條紋的顏色
   vec4 mColor=vec4(0.763,0.657,0.614,0);//間隔的顏色
   float y=vPosition.y;
   y=mod((y+100.0)*4.0,4.0);
   if(y>1.8) {
     gl_FragColor = bColor;//給此片元顏色值
   } else {
     gl_FragColor = mColor;//給此片元顏色值
   }
// 預設使用單一顏色進行繪製
//   vec4 white = vec4(1,1,1,1);
//   gl_FragColor = white;
}
複製程式碼

實現的方式也是根據片段的 y 座標所在位置來決定該片段是取樣條紋的顏色還是間隔的顏色。

最後,載入 3D 模型就先了解到這了,如果想要載入更多效果,倒是可以繼續深挖,只是沒有 MAC 版本的 3ds Max 軟體,卻是少了一些樂趣~~

具體程式碼詳情,可以參考我的 Github 專案,求一波 star~~

github.com/glumes/Andr…

歡迎關注微信公眾號:【紙上淺談】,獲得最新文章推送~~

OpenGL 3D 模型載入和渲染

相關文章