OpenGL 3D 模型加載和渲染

博客原文連接:https://glumes.com/post/opengl/opengl-tutorial-import-3d-object/java

在使用 OpenGL 繪製時,咱們最多繪製的是一些簡單的圖形,好比三角形、圓形、立方體等,由於這些圖形的頂點數量很少,仍是能夠手動的寫出那些頂點的,可要是繪製一些複雜圖形該怎麼辦呢?git

這時候就可使用 OpenGL 來加載 3D 模型。先使用 3D 建模工具構建物體,而後再將物體導出成特定的文件格式,最終經過 OpenGL 渲染模型。github

例如以下的 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四、13 號以 "v" 開頭的行, 3 個頂點的紋理座標來自 十、十二、4 號以 「vt」 開頭的行,3 個頂點的法向量來自 一、十一、12 號以 「vn」 開頭的行。工具

若是頂點座標沒有法向量和紋理座標,那麼直接能夠忽略,用空格將三個頂點座標索引分開就行post

f 1 3 4
複製代碼

最後 OpenGL 在繪製時採用的是 GL_TRIANGLES,也就是由 ABCDEF 六個點繪製 ABC、DEF 兩個三角形,因此 "f" 開頭的行都表明繪製一個獨立的三角形,最終圖像由一個一個三角形拼接組成,而且彼此的點能夠分開。ui

加載 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 進行轉換等等,這就和以前的文章內容相同了。

若是隻是單純的導入了全部頂點,並決定了要繪製的顏色,就會出現相似上面的單一顏色的繪製狀況,事實上能夠經過修改片斷着色器來給 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…

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

相關文章
相關標籤/搜索