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);
// 啟用光源
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
代碼:
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:頂點的數量