Android OpenGL 顯示基本圖形及相關概念解讀
在上一篇文章中,我們知道了如何在Android
開發一個OpenGL
模型顯示。但是並沒有對具體模型資料進行顯示,只是展示一個背景顏色而已,在本章中,我們學習如何將一個模型資料顯示成一個具體的3D
圖形。在Android
中開發OpenGL
程式非常簡單,但是卻有很多OpenGL
相關概念是必須要清楚的,瞭解這些相關概念才能寫出正確的程式碼,否則,你寫出來的程式可能會無緣無故崩潰,或者是畫出來的模型顯示不出來等等問題。
本文是建立在上一篇文章之上,只修改GLRender
類,其他部分保持不變,如果你沒有看上一篇文章,請先移步【Android OpenGL入門】
1 模型資料
前面我們說過,一個3D模型一般是由很多三角片(或四邊形)組成,因此,首先我們需要有三角形的點資料。既然是3D模型,自然每個點座標是在三維座標系中,因此,每個點需要3個數來表示。
我們定義一個三角形,需要9個數,如果我們有float
型別表示一個數,那麼定義一個三角形(三個點)如下:
private float[] mTriangleArray = {
0f, 1f, 0f,
-1f, -1f, 0f,
1f, -1f, 0f
};
此時,我們就有了一個三角形的3
個點資料了。但是,OpenGL
並不是對堆裡面的資料進行操作,而是在直接記憶體中(Direct Memory
),即操作的資料需要儲存到NIO
裡面的Buffer
物件中。而我們上面宣告的float[]
物件儲存在堆中,因此,需要我們將float[]
java.nio.Buffer
物件。
我們可以選擇在建構函式裡面,將float[]
物件轉為java.nio.Buffer
,如下所示:
private FloatBuffer mTriangleBuffer;
public GLRenderer() {
//先初始化buffer,陣列的長度*4,因為一個float佔4個位元組
ByteBuffer bb = ByteBuffer.allocateDirect(mTriangleArray.length * 4);
//以本機位元組順序來修改此緩衝區的位元組順序
bb.order(ByteOrder.nativeOrder());
mTriangleBuffer = bb.asFloatBuffer();
//將給定float[]資料從當前位置開始,依次寫入此緩衝區
mTriangleBuffer.put(mTriangleArray);
//設定此緩衝區的位置。如果標記已定義並且大於新的位置,則要丟棄該標記。
mTriangleBuffer.position(0);
}
注意,ByteBuffer
和FloatBuffer
以及IntBuffer
都是繼承自抽象類java.nio.Buffer
。
另外,OpenGL
在底層的實現是C
語言,與Java
預設的資料儲存位元組順序可能不同,即大端小端問題。因此,為了保險起見,在將資料傳遞給OpenGL
之前,我們需要指明使用本機的儲存順序。
此時,我們順利地將float[]
轉為了FloatBuffer
,後面繪製三角形的時候,直接通過成員變數mTriangleBuffer
即可。
2 矩陣變換
在現實世界中,我們要觀察一個物體可以通過如下幾種方式:
- 從不同位置去觀察。(檢視變換)
- 移動或旋轉物體,放縮物體(雖然實際生活中不能放縮,但是計算機世界是可以的)。(模型變換)
- 給物體拍照印成照片。可以做到“近大遠小”、裁剪只看部分等等透視效果。(投影變換)
- 只拍攝物體的一部分,使得物體在照片中只顯示部分。(視口變換)
上面所述效果,可以在OpenGL
中全部實現。有一點需要很清楚,就是OpenGL
的變換其實都是通過矩陣相乘來實現的。
2.1 模型變換和檢視變換
高中我們學過相對運動,就是說,改變觀測點的位置與改變物體位置都可以達到等效的運動效果。因此,在OpenGL
中,這兩種變換本質上用的是同一個函式。
在進行變換之前,我們需要聲明當前是使用哪種變換。在本節中,宣告使用模型檢視變換,而模型檢視變換在OpenGL
中對應標識為:GL10.GL_MODELVIEW
。通過glMatrixMode
函式來宣告:
gl.glMatrixMode(GL10.GL_MODELVIEW);
接下來你就可以對模型進行:平移、放縮、旋轉等操作啦。但是有一點值得注意的是,在此之前,你可能針對模型做了其他的操作,而我們知道,每次操作相當於一次矩陣相乘。OpenGL
中,使用“當前矩陣”表示要執行的變化,為了防止前面執行過變換“保留”在“當前矩陣”,我們需要把“當前矩陣”復位,即變為單位矩陣(對角線上的元素全為1),通過執行如下函式:
gl.glLoadIdentity();
此時,當前變換矩陣為單位矩陣,後面才可以繼續做變換,例如:
//繞(1,0,0)向量旋轉30度
gl.glRotatef(30, 1, 0, 0);
//沿x軸方向移動1個單位
gl.glTranslatef(1, 0, 0);
//x,y,z方向放縮0.1倍
gl.glScalef(0.1f, 0.1f, 0.1f);
上面的效果都是矩陣相乘實現,因此我們需要注意變換次序問題,舉個例子,假設“當前矩陣”為單位矩陣,然後乘以一個表示旋轉的矩陣R
,再乘以一個表示移動的矩陣T
,最後得到的矩陣,再與每個頂點相乘。假設表示模型所以頂點的矩陣為V
,則實際就是((RT)V)
,由矩陣乘法結合律,((RT)V)=(R(TV))
,這導致的就是,先移動再旋轉。即:
實際變換順序與程式碼中的順序是相反的
上面所講的都是改變物體的位置或方向來實現“相對運動”的,如果我們不想改變物體,而是改變觀察點,可以使用如下函式
/**
* gl: GL10型變數
* eyeX,eyeY,eyeZ: 觀測點座標(相機座標)
* centerX,centerY,centerZ:觀察位置的座標
* upX,upY,upZ :相機向上方向在世界座標系中的方向(即保證看到的物體跟期望的不會顛倒)
*/
GLU.gluLookAt(gl,eyeX,eyeY,eyeZ,centerX,centerY,centerZ,upX,upY,upZ);
2.2 投影變換
投影變換就是定義一個可視空間,可視空間之外的物體是看不到的(即不會再螢幕中)。在此之前,我們的三維座標中的三個座標軸取值為[-1,1]
,從現在開始,座標可以不再是從-1
到1
了!
OpenGL支援主要兩種投影變換:
- 透視投影
- 正投影
當然了,投影也是通過矩陣來實現的,如果想要設定為投影變換,跟前面類似:
gl.glMatrixMode(GL10.GL_PROJECTION);
gl.glLoadIdentity();
同樣的道理,glLoadIdentity()
函式也需要立即呼叫。
通過如下函式可將當前可視空間設定為透視投影空間:
gl.glFrustumf(left,right,bottom,top,near,far);
上面函式對應引數如下圖所示(圖片出自www.opengl.org
):
當然了,也可以通過另一個函式實現相同的效果:
GLU.gluPerspective(gl,fovy,aspect,near,far);
上面函式對應的引數如下圖所示(圖片出自www.opengl.org
):
而對於正投影來說,相當於觀察點處於無窮遠,當然了,這是一種理想狀態,但是有時使用正投影效率可能會更高。可以通過如下函式設定正投影:
gl.glOrthof(left,right,bottom,top,near,far);
上面函式對應的引數如下圖所示(圖片出自www.opengl.org
):
2.3 視口變換
我們可以選擇將影象繪製到螢幕視窗的那個區域,一般預設是在整個視窗中繪製,但是,如果你不希望在整個視窗中繪製,而是在視窗的某個小區域中繪製,你也可以自己定製:
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
gl.glViewport(0, 0, width, height);
}
每次視窗發生變化時,我們可以設定繪製區域,即在onSurfaceChanged
函式中呼叫glViewport
函式。
3 啟用相關功能及配置
3.1 glClearColor()
設定清屏顏色,每次清屏時,使用該顏色填充整個螢幕。使用例子:
gl.glClearColor(1.0f, 1.0f, 1.0f, 0f);
裡面引數分別代表RGBA
,取值範圍為[0,1]
而不是[0,255]
3.2 glDepthFunc()
OpenGL
中物體模型的每個畫素都有一個深度快取的值(在0
到1
之間,可以看成是距離),可以通過glClearDepthf
函式設定預設的“當前畫素”z
值。在繪製時,通過將待繪製的模型畫素點的深度值與“當前畫素”z
值進行比較,將符合條件的畫素繪製出來,不符合條件的不繪製。具體的“指定條件”可取以下值:
GL10.GL_NEVER
:永不繪製GL10.GL_LESS
:只繪製模型中畫素點的z
值<
當前畫素z值的部分GL10.GL_EQUAL
:只繪製模型中畫素點的z
值=
當前畫素z值的部分GL10.GL_LEQUAL
:只繪製模型中畫素點的z
值<=
當前畫素z值的部分GL10.GL_GREATER
:只繪製模型中畫素點的z
值>
當前畫素z值的部分GL10.GL_NOTEQUAL
:只繪製模型中畫素點的z
值!=
當前畫素z值的部分GL10.GL_GEQUAL
:只繪製模型中畫素點的z
值>=
當前畫素z值的部分GL10.GL_ALWAYS
:總是繪製
通過目標畫素與當前畫素在z
方向上值大小的比較是否滿足引數指定的條件,來決定在深度(z
方向)上是否繪製該目標畫素。
注意, 該函式只有啟用“深度測試”時才有效,通過
glEnable(GL_DEPTH_TEST)
開啟深度測試以及glDisable(GL_DEPTH_TEST)
關閉深度測試。
例子:
gl.glDepthFunc(GL10.GL_LEQUAL);
3.3 glClearDepthf()
給深度快取設定預設值。
快取中的每個畫素的深度值預設都是這個, 假設在 gl.glDepthFunc(GL10.GL_LEQUAL);
前提下:
- 如果指定“當前畫素值”為
1
時,我們知道,一個模型深度值取值和範圍為[0,1]
。這個時候你往裡面畫一個物體, 由於物體的每個畫素的深度值都小於等於1
, 所以整個物體都被顯示了出來。- 如果指定“當前畫素值”為
0
, 物體的每個畫素的深度值都大於等於0
, 所以整個物體都不可見。
如果指定“當前畫素值”為0.5
, 那麼物體就只有深度小於等於0.5
的那部分才是可見的
使用例子:
gl.glClearDepthf(1.0f);
3.3 glEnable(),glDisable()
glEnable()
啟用相關功能,glDisable()
關閉相關功能。
比如:
//啟用深度測試
gl.glEnable(GL10.GL_DEPTH_TEST);
//關閉深度測試
gl.glDisable(GL10.GL_DEPTH_TEST)
//開啟燈照效果
gl.glEnable(GL10.GL_LIGHTING);
// 啟用光源0
gl.glEnable(GL10.GL_LIGHT0);
// 啟用顏色追蹤
gl.glEnable(GL10.GL_COLOR_MATERIAL);
3.5 glHint()
如果OpenGL
在某些地方不能有效執行是,給他指定其他操作。
函式原型為:
void glHint(GLenum target,GLenum mod)
其中,target
:指定所控制行為的符號常量,可以是以下值(引自【OpenGL函式思考-glHint 】):
GL_FOG_HINT
:指定霧化計算的精度。如果OpenGL
實現不能有效的支援每個畫素的霧化計算,則GL_DONT_CARE
和GL_FASTEST
霧化效果中每個定點的計算。GL_LINE_SMOOTH_HINT
:指定反走樣線段的取樣質量。如果應用較大的濾波函式,GL_NICEST
在光柵化期間可以生成更多的畫素段。GL_PERSPECTIVE_CORRECTION_HINT
:指定顏色和紋理座標的差值質量。如果OpenGL
不能有效的支援透視修正引數差值,那麼GL_DONT_CARE
和GL_FASTEST
可以執行顏色、紋理座標的簡單線性差值計算。GL_POINT_SMOOTH_HINT
:指定反走樣點的取樣質量,如果應用較大的濾波函式,GL_NICEST
在光柵化期間可以生成更多的畫素段。GL_POLYGON_SMOOTH_HINT
:指定反走樣多邊形的取樣質量,如果應用較大的濾波函式,GL_NICEST
在光柵化期間可以生成更多的畫素段。
mod
:指定所採取行為的符號常量,可以是以下值:
GL_FASTEST
:選擇速度最快選項。GL_NICEST
:選擇最高質量選項。GL_DONT_CARE
:對選項不做考慮。
例子:
gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);
3.6 glEnableClientState()
當我們需要啟用頂點陣列(儲存每個頂點的座標資料)、頂點顏色陣列(儲存每個頂點的顏色)等等,就要通過glEnableClientState()
函式來開啟:
//以下兩步為繪製顏色與頂點前必做操作
// 允許設定頂點
//GL10.GL_VERTEX_ARRAY頂點陣列
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
// 允許設定顏色
//GL10.GL_COLOR_ARRAY顏色陣列
gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
3.7 glShadeModel()
設定著色器模式,有如下兩個選擇:
- GL10.GL_FLAT
- GL10.GL_SMOOTH(預設)
如果為每個頂點指定了頂點的顏色,此時:
GL_SMOOTH
:根據頂點的不同顏色,最終以漸變的形式填充圖形。GL_FLAT
:假設有n
個三角片,則取最後n
個頂點的顏色填充著n
個三角片。
使用例子:
gl.glShadeModel(GL10.GL_SMOOTH);
4 開始繪製
前面講了很多概念,但是其實都是非常值得學習的。有了這些基礎,我們才能理解如何寫OpenGL
,從上一篇文章中我們知道,開發OpenGL
大部分工作都是在Renderer
類上面,我直接粘Renderder
程式碼:
/**
* Package com.hc.opengl
* Created by HuaChao on 2016/7/28.
*/
public class GLRenderer implements GLSurfaceView.Renderer {
private float[] mTriangleArray = {
0f, 1f, 0f,
-1f, -1f, 0f,
1f, -1f, 0f
};
//三角形各頂點顏色(三個頂點)
private float[] mColor = new float[]{
1, 1, 0, 1,
0, 1, 1, 1,
1, 0, 1, 1
};
private FloatBuffer mTriangleBuffer;
private FloatBuffer mColorBuffer;
public GLRenderer() {
//點相關
//先初始化buffer,陣列的長度*4,因為一個float佔4個位元組
ByteBuffer bb = ByteBuffer.allocateDirect(mTriangleArray.length * 4);
//以本機位元組順序來修改此緩衝區的位元組順序
bb.order(ByteOrder.nativeOrder());
mTriangleBuffer = bb.asFloatBuffer();
//將給定float[]資料從當前位置開始,依次寫入此緩衝區
mTriangleBuffer.put(mTriangleArray);
//設定此緩衝區的位置。如果標記已定義並且大於新的位置,則要丟棄該標記。
mTriangleBuffer.position(0);
//顏色相關
ByteBuffer bb2 = ByteBuffer.allocateDirect(mColor.length * 4);
bb2.order(ByteOrder.nativeOrder());
mColorBuffer = bb2.asFloatBuffer();
mColorBuffer.put(mColor);
mColorBuffer.position(0);
}
@Override
public void onDrawFrame(GL10 gl) {
// 清除螢幕和深度快取
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
// 重置當前的模型觀察矩陣
gl.glLoadIdentity();
// 允許設定頂點
//GL10.GL_VERTEX_ARRAY頂點陣列
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
// 允許設定顏色
//GL10.GL_COLOR_ARRAY顏色陣列
gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
//將三角形在z軸上移動
gl.glTranslatef(0f, 0.0f, -2.0f);
// 設定三角形
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mTriangleBuffer);
// 設定三角形顏色
gl.glColorPointer(4, GL10.GL_FLOAT, 0, mColorBuffer);
// 繪製三角形
gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 3);
// 取消顏色設定
gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
// 取消頂點設定
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
//繪製結束
gl.glFinish();
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
float ratio = (float) width / height;
// 設定OpenGL場景的大小,(0,0)表示視窗內部視口的左下角,(w,h)指定了視口的大小
gl.glViewport(0, 0, width, height);
// 設定投影矩陣
gl.glMatrixMode(GL10.GL_PROJECTION);
// 重置投影矩陣
gl.glLoadIdentity();
// 設定視口的大小
gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10);
//以下兩句宣告,以後所有的變換都是針對模型(即我們繪製的圖形)
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// 設定白色為清屏
gl.glClearColor(1, 1, 1, 1);
}
}
效果如下:
5 幾個重要的函式
5.1 glVertexPointer()
其實就是設定一個指標,這個指標指向頂點陣列,後面繪製三角形(或矩形)根據這裡指定的頂點陣列來讀取資料。
函式原型如下:
void glVertexPointer(int size,int type,int stride,Buffer pointer)
其中:
size
: 每個頂點有幾個數值描述。必須是2,3 ,4 之一。type
: 陣列中每個頂點的座標型別。取值:GL_BYTE
,GL_SHORT
,GL_FIXED
,GL_FLOAT
。stride
:陣列中每個頂點間的間隔,步長(位元組位移)。取值若為0
,表示陣列是連續的pointer
:即儲存頂點的Buffer
5.2 glColorPointer()
跟上面類似,只是設定指向顏色陣列的指標。
函式原型:
void glColorPointer(
int size,
int type,
int stride,
java.nio.Buffer pointer
);
size
: 每種顏色元件的數量。 值必須為 3 或 4。type
: 顏色陣列中的每個顏色分量的資料型別。 使用下列常量指定可接受的資料型別:GL_BYTE
GL_UNSIGNED_BYTE
,GL_SHORT
GL_UNSIGNED_SHORT
,GL_INT
GL_UNSIGNED_INT
,GL_FLOAT
,或GL_DOUBLE
。stride
:連續顏色之間的位元組偏移量。 當偏移量為0時,表示資料是連續的。pointer
:即顏色的Buffer
5.3 glDrawArrays()
繪製數組裡面所有點構成的各個三角片。
函式原型:
void glDrawArrays(
int mode,
int first,
int count
);
其中:
mode
:有三種取值
GL_TRIANGLES
:每三個頂之間繪製三角形,之間不連線GL_TRIANGLE_FAN
:以V0 V1 V2
,V0 V2 V3
,V0 V3 V4
,……的形式繪製三角形GL_TRIANGLE_STRIP
:順序在每三個頂點之間均繪製三角形。這個方法可以保證從相同的方向上所有三角形均被繪製。以V0 V1 V2
,V1 V2 V3
,V2 V3 V4
,……的形式繪製三角形first
:從陣列快取中的哪一位開始繪製,一般都定義為0
count
:頂點的數量
相關資料: