Android OpenGLES2.0(十四)——Obj格式3D模型載入
技術標籤:AndroidOpenGLES2.0
在博主《OpenGLES系列》文章中,最開始的幾篇講的就是OpenGL世界中各種形體的構建,但是那些形體都是規則的簡單形體,遇到複雜的形體,比如說一個人、一朵花,怎麼辦呢?自然是通過其他工具類似於Maya、3DMax等3D建模工具,做好模型匯出來,然後用OpenGLES載入匯出的模型檔案。模型的載入大同小異,本篇部落格是以Obj格式的3D模型為例。
模型檔案
本篇部落格例子中載入的是一個帽子,資源是在網上隨便找的一個。加載出來如圖所示:
格式如下:
# File exported by ZBrush version 4.2 # www.zbrush.com #Vertex Count 4898 #Face Count 4848 #Auto scale x=0.211538 y=0.211538 z=0.211538 #Auto offset x=-0.000000 y=-0.412507 z=-0.000000 v -0.62500745 3.93329608 0.0000001 v -0.00002446 3.32622414 1.33471741 v 1.47657442 2.55452877 1.37523436 v -1.01934254 3.90772931 0.00000007 ...省略若干行... g default f 990 991 987 986 f 991 874 873 987 f 972 971 991 990 f 971 55 874 991 f 987 992 988 986 ...省略若干行
載入這個模型檔案前,我們需要先知道這些資料代表的是什麼。針對這個檔案,#號開頭的,是描述模型檔案的相關資訊。以v開頭的,表示的是頂點座標。以f開頭的,表示一個面,後面跟的四個值是索引。一個v,後面的三個數,代表一個點的xyz,4個點組成了一個四邊形。
為什麼是4個點?不是說在OpenGLES中基本幾何是三角形麼?這樣問就有點尷尬了,因為模型檔案是我在網上隨便下的,自己選的模型,跪著也要加載出來。有什麼關係,一個四邊形不就是兩個三角形麼。
這個模型檔案只有v、f兩類資料,但是一個炫酷的模型,往往是包含很多資料的,主要的資料型別如下:
-
頂點資料(Vertex data):
- v 幾何體頂點(Geometric vertices)
- vt 貼圖座標點(Texture vertices)
- vn 頂點法線(Vertex normals)
- vp 引數空格頂點 (Parameter space vertices)
-
自由形態曲線(Free-form curve)/表面屬性(surface attributes):
- deg 度(Degree)
- bmat 基礎矩陣(Basis matrix)
- step 步尺寸(Step size)
- cstype 曲線或表面型別 (Curve or surface type)
-
元素(Elements):
- p 點(Point)
- l 線(Line)
- f 面(Face)
- curv 曲線(Curve)
- curv2 2D曲線(2D curve)
- surf 表面(Surface)
-
自由形態曲線(Free-form curve)/表面主體陳述(surface body statements):
- parm 引數值(Parameter values )
- trim 外部修剪迴圈(Outer trimming loop)
- hole 內部整修迴圈(Inner trimming loop)
- scrv 特殊曲線(Special curve)
- sp 特殊的點(Special point)
- end 結束陳述(End statement)
-
自由形態表面之間的連線(Connectivity between free-form surfaces):
- con 連線 (Connect)
-
成組(Grouping):
- g 組名稱(Group name)
- s 光滑組(Smoothing group)
- mg 合併組(Merging group)
- o 物件名稱(Object name)
-
顯示(Display)/渲染屬性(render attributes):
- bevel 導角插值(Bevel interpolation)
- c_interp 顏色插值(Color interpolation)
- d_interp 溶解插值(Dissolve interpolation)
- lod 細節層次(Level of detail)
- usemtl 材質名稱(Material name)
- mtllib 材質庫(Material library)
- shadow_obj 投射陰影(Shadow casting)
- trace_obj 光線跟蹤(Ray tracing)
- ctech 曲線近似技術(Curve approximation technique)
- stech 表面近似技術 (Surface approximation technique)
模型載入
知道了模型檔案的內容和格式,載入起來就不是什麼問題了:
public class ObjReader {
public static void read(InputStream stream,Obj3D obj3D){
ArrayList<Float> alv=new ArrayList<Float>();//原始頂點座標列表
ArrayList<Float> alvResult=new ArrayList<Float>();//結果頂點座標列表
ArrayList<Float> norlArr=new ArrayList<>();
float[] ab=new float[3],bc=new float[3],norl=new float[3];
try{
InputStreamReader isr=new InputStreamReader(stream);
BufferedReader br=new BufferedReader(isr);
String temps=null;
while((temps=br.readLine())!=null)
{
String[] tempsa=temps.split("[ ]+");
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")) {//此行為三角形面
int a=Integer.parseInt(tempsa[1])-1;
int b=Integer.parseInt(tempsa[2])-1;
int c=Integer.parseInt(tempsa[3])-1;
int d=Integer.parseInt(tempsa[4])-1;
//abc和acd兩個三角形組成的四邊形
alvResult.add(alv.get(a*3));
alvResult.add(alv.get(a*3+1));
alvResult.add(alv.get(a*3+2));
alvResult.add(alv.get(b*3));
alvResult.add(alv.get(b*3+1));
alvResult.add(alv.get(b*3+2));
alvResult.add(alv.get(c*3));
alvResult.add(alv.get(c*3+1));
alvResult.add(alv.get(c*3+2));
alvResult.add(alv.get(a*3));
alvResult.add(alv.get(a*3+1));
alvResult.add(alv.get(a*3+2));
alvResult.add(alv.get(c*3));
alvResult.add(alv.get(c*3+1));
alvResult.add(alv.get(c*3+2));
alvResult.add(alv.get(d*3));
alvResult.add(alv.get(d*3+1));
alvResult.add(alv.get(d*3+2));
//這裡也是因為下載模型檔案的坑。下了個出了頂點和麵啥也沒有的模型檔案
//為了有3d效果,給它加個光照,自己計算頂點法線
//用面法向量策略。按理說點法向量更適合這種光滑的3D模型,但是計算起來太複雜了,so
//既然主要講3D模型載入,就先用面法向量策略來吧
//通常3D模型裡面會包含法向量資訊的。
//法向量的計算,ABC三個空間點,他們的法向量為向量AB與向量BC的外積,所以有:
for (int i=0;i<3;i++){
ab[i]=alv.get(a*3+i)-alv.get(b*3+i);
bc[i]=alv.get(b*3+i)-alv.get(c*3+i);
}
norl[0]=ab[1]*bc[2]-ab[2]*bc[1];
norl[1]=ab[2]*bc[0]-ab[0]*bc[2];
norl[2]=ab[0]*bc[1]-ab[1]*bc[0];
//上面兩個三角形,傳入了6個頂點,這裡迴圈6次,簡單粗暴
for (int i=0;i<6;i++){
norlArr.add(norl[0]);
norlArr.add(norl[1]);
norlArr.add(norl[2]);
}
}
}
//這些就是比較熟悉的了,一切都為了能夠把資料給GPU
int size=alvResult.size();
float[] vXYZ=new float[size];
for(int i=0;i<size;i++){
vXYZ[i]=alvResult.get(i);
}
ByteBuffer byteBuffer=ByteBuffer.allocateDirect(4*size);
byteBuffer.order(ByteOrder.nativeOrder());
obj3D.vert=byteBuffer.asFloatBuffer();
obj3D.vert.put(vXYZ);
obj3D.vert.position(0);
obj3D.vertCount=size/3;
int vbSize=norlArr.size();
float[] vbArr=new float[size];
for(int i=0;i<size;i++){
vbArr[i]=norlArr.get(i);
}
ByteBuffer vb=ByteBuffer.allocateDirect(4*vbSize);
vb.order(ByteOrder.nativeOrder());
obj3D.vertNorl=vb.asFloatBuffer();
obj3D.vertNorl.put(vbArr);
obj3D.vertNorl.position(0);
}catch(Exception e){
e.printStackTrace();
}
}
public static class Obj3D{
public FloatBuffer vert;
public int vertCount;
public FloatBuffer vertNorl;
}
}
模型渲染
模型的渲染,和之前繪製各種形體也差不多了,往GPU傳資料就不用說了,為了讓3D模型呈現出立體效果,示例中,增加了簡單而不靠譜的光照。所以看得出來,雖然加載出來有立體效果,但是能看到比較明顯的網格。當然,光照不是本篇部落格的重點,在後續部落格裡面再詳細討論下光照的問題。
頂點Shader為:
attribute vec3 vPosition;
attribute vec2 vCoord;
uniform mat4 vMatrix;
varying vec2 textureCoordinate;
attribute vec3 vNormal; //法向量
varying vec4 vDiffuse; //用於傳遞給片元著色器的散射光最終強度
//返回散射光強度
vec4 pointLight(vec3 normal,vec3 lightLocation,vec4 lightDiffuse){
//變換後的法向量
vec3 newTarget=normalize((vMatrix*vec4(normal+vPosition,1)).xyz-(vMatrix*vec4(vPosition,1)).xyz);
//表面點與光源的方向向量
vec3 vp=normalize(lightLocation-(vMatrix*vec4(vPosition,1)).xyz);
return lightDiffuse*max(0.0,dot(newTarget,vp));
}
void main(){
gl_Position = vMatrix*vec4(vPosition,1);
textureCoordinate = vCoord;
vec4 at=vec4(1.0,1.0,1.0,1.0); //光照強度
vec3 pos=vec3(50.0,200.0,50.0); //光照位置
vDiffuse=pointLight(vNormal,pos,at);
}
片元Shader:
precision mediump float;
varying vec2 textureCoordinate;
uniform sampler2D vTexture;
varying vec4 vDiffuse;//接收從頂點著色器過來的散射光分量
void main() {
vec4 finalColor=vec4(1.0);
//給此片元顏色值
gl_FragColor=finalColor*vDiffuse+finalColor*vec4(0.15,0.15,0.15,1.0);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
著色器中散射光強度的計算,是根據散射光的公式來的,光照公式在Android OpenGLES2.0(一)——瞭解OpenGLES2.0光照中有講到。
編譯著色器,linkProgram,傳入從Obj檔案讀取的值,然後和渲染一個立方體一樣,渲染出模型就OK了。
原始碼
所有的程式碼全部在一個專案中,託管在Github上——Android OpenGLES 2.0系列部落格的Demo