本文實現通過OpenGL在屏幕上畫出一寫簡單圖形(三角形、等腰三角形、矩形)。
開擼碼前先了解一些概念性的東西:
什么是OpenGL?
Open Graphics Library是用于渲染2D、3D矢量圖形的跨語言、跨平臺的應用程序編程接口。Android窗口怎么和OpenGL交互?
通過GLSurfaceView,它繼承至SurfaceView,它內嵌的surface專門負責OpenGL渲染。-
Android渲染機制,XML中的布局文件是怎么繪制到屏幕上的?
- XML中的布局首先通過LayoutInfalter解析成對應的對象
-
CPU把對象處理成多維圖形紋理。關于紋理需要清楚,擼碼時需要這個概念,比如一個3D的模型有好多好多紋理,再比如我們要渲染一個二維的三角形或者矩形只有一個紋理。圖片理解一下更直接,這里沒找到清晰的圖片,湊合看下,下面中的每個三角形就是一個紋理:
-
CPU通過OpenGLES接口調用GPU,GPU對圖像進行光柵化,光柵化就是把每個紋理處理成各個顏色的像素,再來張圖:
- 完成繪制
為什么說到了Android渲染機制?
從上面的渲染機制中可以看到GPU只要能確定紋理的位置和顏色信息,就可以把圖像繪制到屏幕,我們平時在XML中定義控件時,這些信息都是CPU根據我們的設置去完成后傳遞給GPU的,但是如果我們自己使用OpenGL去調用GPU的話,這些信息就需要我們自己確定后傳遞給GPU,OpenGL這里就為我們提供了頂點著色器和片元著色器,頂點著色器接收紋理的位置信息,片元著色器接受紋理的顏色信息,我們只需要擼碼完成這兩個著色器并傳遞給GPU,就可以渲染。頂點著色器和片元著色器怎么實現?
首先為什么問這個問題?因為著色器并不是Java類。
著色器其實是一段OpenGL語法(C風格的語法)寫一個小程序,我們需要把這段程序用String的形式傳遞給OpenGL。不過不要慌,這段小程序很簡單。
開始擼碼
- 要在項目中使用OpenGL,首先需要在
AndroidManifest
中添加:
<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
<application
...>
</application>
</manifest>
required
的意思是是否為必須的。
- 上面說到
GLSurfaceView
是Android窗口和OpenGL交互工具,自定義一個View繼承自GLSurfaceView
,而GLSurfaceView
需要展示的話依賴于GLSurfaceView.Renderer
,直接創建出這倆類:
public class GLViewRender implements GLSurfaceView.Renderer {
protected Context mContext;
public GLViewRender(Context context) {
this.mContext = context;
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
}
@Override
public void onDrawFrame(GL10 gl) {
}
}
public class GLView extends GLSurfaceView {
public GLView(Context context) {
this(context, null);
}
public GLView(Context context, AttributeSet attrs) {
super(context, attrs);
//設置OpenGL版本
setEGLContextClientVersion(2);
//設置Renderer(GLSurfaceView展示依賴于GLSurfaceView.Render)
setRenderer(new GLViewRender(context));
//設置刷新模式,RENDERMODE_WHEN_DIRTY:手動刷新
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
}
Render里的要實現的函數很簡單,看函數名就理解了。
GLView設置了Render。setRenderMode中刷新刷新模式選擇了RENDERMODE_WHEN_DIRTY
:
/**
* The renderer only renders
* when the surface is created, or when {@link #requestRender} is called.
*
* @see #getRenderMode()
* @see #setRenderMode(int)
* @see #requestRender()
*/
public final static int RENDERMODE_WHEN_DIRTY = 0;
我們只要在surface創建的時候刷新一次就可以了,所以requestRender也沒用到。
然后我們需要實現的就是在Render不同的回調中去進行繪制相關的操作。
-
著色器小程序,AS里可以先下載個插件,在寫OpenGL語法的時候就會有顏色提示,插件下載:
在res/raw下先創建頂點著色器程序base_vertex.vert
:
attribute vec4 vPosition;
void main(){
gl_Position=vPosition;
}
解釋一下,vPosition是自己定義的變量,gl_Position是OpenGL內置變量,一會會在Java程序中獲取vPosition變量,把頂點信息設置給它就可以了。
然后片元著色器base_frag.frag
uniform vec4 vColor;
void main(){
gl_FragColor=vColor;
}
理解方式同頂點著色器。
- 創建一個類Triangle,在這個類里進行繪制三角形的操作,這里要定義三角形頂點的位置,需要注意的是OpenGL的坐標系并不是左上為(0, 0),而是中間為(0, 0),寬高都為1的坐標系,很簡單不畫圖了,腦補一下左下(-1, -1),右上(1, 1)。Triangle類直接貼代碼了,注釋寫的很詳細:
public class Triangle {
int mProgram; //存放頂點著色器和片元著色器的程序的地址
// 頂點
static float triangleCoords[] = {
0.5f, 0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
};
// 顏色
float color[] = { 1.0f, 1.0f, 1.0f, 1.0f };
private FloatBuffer vertexBuffer; // 存放頂點的緩沖器,因為OpenGL接受參數為FloatBuffer,所以需要把triangleCoords[]中的數據copy到vertexBuffer
/**
* 構造函數
* @param context
*/
public Triangle(Context context) {
//float占4個字節
ByteBuffer bb = ByteBuffer.allocateDirect(triangleCoords.length * 4);
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(triangleCoords);
// position歸位
vertexBuffer.position(0);
//創建頂點著色器 并且在GPU進行編譯
int shader= GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
GLES20.glShaderSource(shader, Utils.readRawTextFile(context, R.raw.base_vertex));
GLES20.glCompileShader(shader);
//創建片元著色器 并且在GPU進行編譯
int fragmentShader=GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);
GLES20.glShaderSource(fragmentShader, Utils.readRawTextFile(context, R.raw.base_frag));
GLES20.glCompileShader(fragmentShader);
//將片元著色器和頂點著色器放到統一程序中
mProgram = GLES20.glCreateProgram();
GLES20.glAttachShader(mProgram, shader);
GLES20.glAttachShader(mProgram, fragmentShader);
//連接到著色器程序
GLES20.glLinkProgram(mProgram);
}
/**
* 開始渲染
*/
public void onDrawFrame() {
GLES20.glUseProgram(mProgram);
// 獲取頂點著色器中的vPosition變量
int mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
// 允許對變量讀寫
GLES20.glEnableVertexAttribArray(mPositionHandle);
// 這里參數有點多
// 最后一個vertexBuffer沒什么說的,頂點位置buffer;
// normalized意思是是否希望數據被標準化(歸一化),只表示方向不表示大小
// index、size、stride 這三個參數跟從vertexBuffer里取數據有關系
// index是開始位置
// size是每次取幾個
// stride是步長,也就指定在連續的頂點屬性之間的間隔,打個比方:如果傳1取值方式為0123、1234、2345……
GLES20.glVertexAttribPointer(0, 3, GLES20.GL_FLOAT, false,
0, vertexBuffer);
// 繪制
//這里的GL_TRIANGLES跟紋理渲染顏色的順序有關系。如果繪制花里胡哨的,需要用到
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);
// 禁止變量讀寫,對應上面的glEnableVertexAttribArray
GLES20.glDisableVertexAttribArray(mPositionHandle);
// 獲取片元著色器中的vColor變量
int mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
// 片元著色器設置顏色
GLES20.glUniform4fv(mColorHandle, 1, color, 0);
}
}
OpenGL的寫法是面向過程的,看久了面向對象的可能有點不習慣。上面這段代碼的主要思想就是定義位置、顏色,然后和剛才寫好的頂點著色器、片元著色器一起傳遞給OpenGL就可以了, 不過OpenGL有自己接受參數的規則,所以進行了一些轉換,把頂點坐標從數組轉成了FloatBuffer, 把著色器從raw里讀出來轉成字符串。這個Utils轉字符串的就不貼了,下面有工程地址,其實這里直接把那倆小程序copy出來用字符串方式傳遞也是一樣的:
//GLES20.glShaderSource(shader, Utils.readRawTextFile(context, R.raw.base_vertex));
String str = "attribute vec4 vPosition;\n" +
"void main(){\n" +
" gl_Position=vPosition;\n" +
"}";
GLES20.glShaderSource(shader, str);
在
GLViewRender
的onSurfaceCreated
中初始化Triangle
,在onDrawFrame
中調用Triangle
的onDrawFrame()
,代碼就不貼了,看工程吧。-
布局文件改為
GLView
。 運行結果:
運行時出來了,但是我們設置的三角形頂點坐標為:
// 頂點
static float triangleCoords[] = {
0.5f, 0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
};
所以應該是希望三角形是等腰三角形,但是因為自定義View的寬高都是match_parent
,設置的并不一樣,所以這里要對渲染出來的形狀進行適配,這里的適配并不是平時使用的縮放,涉及到一個3D的投影問題,是對其投影面進行縮放,因為這個三角形的z軸都是0,是個平面的,使用正交投影就可以解決。關于OpenGL的投影詳解,就不貼了,很多文章的內容都是一樣的。
解決方式:
在onSurfaceChanged
的時候通知Triangle
,并把View的寬高傳遞過來,在Trangle
里對投影面的縮放數據生成一個矩陣mProjectMatrix
,mProjectMatrix
是4x4的矩陣,至于為什么是4*4,需要去了解下OpenGl投影相關的計算:
public void onSurfaceChanged(int width, int height) {
//根據短的方向進行縮放
float aspectRatio = width > height ?
(float) width / (float) height :
(float) height / (float) width;
if (width > height) {
Matrix.orthoM(mProjectMatrix, 0, -aspectRatio, aspectRatio, -1, 1f, -1f, 1f);
} else {
Matrix.orthoM(mProjectMatrix, 0, -1, 1f, -aspectRatio, aspectRatio, -1f, 1f);
}
}
然后把mProjectMatrix
傳遞給頂點著色器:
/**
* 開始渲染
*/
public void onDrawFrame() {
...
int mMatrixHandler = GLES20.glGetUniformLocation(mProgram, "vMatrix");
GLES20.glUniformMatrix4fv(mMatrixHandler,1,false, mProjectMatrix,0);
...
}
修改頂點著色器程序:
attribute vec4 vPosition;
uniform mat4 vMatrix;
void main(){
gl_Position=vMatrix*vPosition;
}
哦了!
先了解OpenGL投影相關的知識,然后對照立方體Demo可以更好的理解