我的視頻課程(基礎):《(NDK)FFmpeg打造Android萬能音頻播放器》
我的視頻課程(進階):《(NDK)FFmpeg打造Android視頻播放器》
我的視頻課程(編碼直播推流):《Android視頻編碼和直播推流》
在前一篇博客我們知道了Android中OpenGL ES是什麼,然後知道了怎麼搭建一個OpenGL ES的運行環境,現在我們就來開始繪製我們自己想要的圖形了(繪製圖片會在後面講解,因爲繪製圖形是繪製圖片的基礎),我們最先開始繪製一個三角形,因爲三角形是很多圖形的基礎。
一、頂點座標系
在繪製之前,我們需要先了解Android中OpenGL ES的頂點座標系是怎樣的,如圖:
其中:中心座標(0,0)就是我們手機屏幕的中心,然後到最左邊是(-1,0)、最右邊是(1,0)、最上邊是(0,1)、最下邊是(0,-1)這樣就把我們的手機屏幕分成了一箇中心座標爲(0,0)上下左右長度分別爲1的矩形。不管我們的手機(具體來講是我們的GLSurfaceView)的大小是多少,都會映射到這個矩形中。這也是OpenGL中歸一化的處理方式,什麼都不管,反正都必須映射到這個範圍內就對了。
二、設置繪製的三角形所需要的三個頂點
因爲我們繪製的是三角形,所以我們需要三個頂點來確定我們的三角形的位置,比如我們要繪製如圖的三角形:
由圖我們知道要繪製的三角形的三個頂點座標分別爲:(-1,0)、(0,1)和(1,0)
三、本地化三角形頂點
所謂本地化就是跳出java VM(Java虛擬機)的約束(垃圾回收)範圍,使我們的頂點在程序運行時一直都有自己分配的內存地址,不會因爲java的GC而把頂點內存地址給回收掉,導致頂點不存在,從而引起OpenGL找不到頂點位置等錯誤,所以在OpenGL中我們需要把頂點座標給本地化。
這裏我們就需要分2個步驟來完成頂點的本地化:
3.1、用float數組來存儲我們的頂點座標,因爲頂點座標範圍是在(-1f~1f)之間的所有小數都可以,所以我們先創建頂點數組:
float[] vertexData = {
-1.0f, 0.0f,//三角形左下角
0.0f, 1.0f,//三角形右下角
1.0f, 0.0f//三角形頂點
};
3.2、然後根據頂點數組分配底層內存地址,因爲需要本地化,所以就和c/c++一樣需要我們手動分配內存地址,這裏用到了ByteBuffer這個類:
FloatBuffer vertexBuffer = ByteBuffer.allocateDirect(vertexData.length * 4)//分配內存空間(單位字節)
.order(ByteOrder.nativeOrder())//內存bit的排序方式和本地機器一致
.asFloatBuffer()//轉換成float的buffer,因爲我們是放float類型的頂點
.put(vertexData);//把數據放入內存中
vertexBuffer.position(0);//把索引指針指向開頭位置
首先用allocateDirect分配內存大小,其大小爲float數組長度乘以每一個float的大小,而float佔4個字節,所以就是:vertexData.length * 4;然後設置其在內存中的對齊方式(分大端和小端對其)這裏就和本地對齊方式一樣:order(ByteOrder.nativeOrder());然後設置是存儲float類型數據的內存空間:asFloatBuffer();最後再用float數組(vertexData)初始化內存中的數據:put(vertexData)。爲了能從開頭訪問這塊內存地址,還需要設置其position爲0:vertexBuffer.position(0);。
這樣我們的三角形的頂點內存地址就已經分配好了,並且做了本地持久化。
四、開始頂點着色器的編寫(shader)
OpenGL的操作需要我們自己編寫着色器(shader)程序給它,然後它會用GPU執行這個着色器程序,最終反饋執行結果給我們。我們用glsl語言來編寫着色器程序,其語法方式和c語言類似,這裏就不展開講了,當學會了OpenGL編程後,可以自己學習glsl語法,然後就可以根據自己的能力編寫“吊炸天”的效果了。
4.1、編寫頂點着色器(vertex_shader.glsl),位置我們放在:res/raw/路徑下
attribute vec4 av_Position;//用於在java代碼中獲取的屬性
void main(){
gl_Position = av_Position;//gl_Position是內置變量,opengl繪製頂點就是根據它的值繪製的,所以我們需要把我們自己的值賦值給它。
}
這段shader很短,但是足夠說明OpenGL中頂點座標的使用方法了:
首先解釋一下attribute vec4 av_Position這句的意思:
attribute是表示頂點屬性的,只能用在頂點座標裏面,然後在應用程序(java代碼)中可以獲取其變量,然後爲其賦值。vec4是一個包含4個值(x,y,z,w)的向量,x和y表示2d平面,加上z就是3d的圖像了,最後的w是攝像機的距離,因爲我們繪製的是2d圖形,所以最後z和w的值可以不用管,OpenGL會有默認值1。所以這句話的意思就是:聲明瞭一個名字叫av_Position的包含4個向量的attribute類型的變量,用於我們在java代碼中獲取並把我們的頂點(FloatBuffer vertexBuffer)值賦值給它。這樣OpenGL執行這段着色器代碼(程序)時,就有了具體的頂點數據,就會在相應的頂點之間繪製圖形(我們定義的三角形)了。
然後void main(){}是程序中函數,和c中是一樣的。
最後是gl_Position = av_Position,這裏的gl_Position是glsl中內置的最終頂點變量,我們要繪製的頂點就是傳遞給它。這段代碼就是把我們設置的頂點數據傳遞給gl_Position,然後OpenGL就知道在哪裏繪製頂點了。
五、片元着色器程序編寫(shader)
上面我們只是寫了我們的三角形繪製頂點的着色器程序,而三角形是 “頂點+顏色(樣式)” 組成的,所以我們就需要告訴OpenGL我們繪製的三角形是什麼顏色的,這就需要片元着色器程序了。
- 編寫片元着色器程序(fragment_shader.glsl)
precision mediump float;//聲明用中等精度的float
uniform vec4 af_Color;//用於在java層傳遞顏色數據
void main(){
gl_FragColor = af_Color;//gl_FragColor內置變量,opengl渲染的顏色就是獲取的它的值,這裏我們把我們自己的值賦值給它。
}
這裏的precision mediump float 表明用中等精度的float類型來保存變量,其他還可以設置高精度和低精度,一般中等精度就可以了,精度不同,執行的效率也會有差別。
然後這裏是用了uniform這個類型來聲明變量,uniform是用於應用程序(java代碼中)向頂點和片元着色器傳遞數據,和attribute的區別在於,attribute是隻能用在頂點着色器程序中,並且它裏面包含的是具體的頂點的數據,每次執行時都需要從頂點內存裏面獲取新的值,而uniform始終都是用同一個變量。vec4 af_Color也是聲明一個4個分量的變量af_Color,這個裏面保存的是顏色的值了(rgba四個分量)。
最後gl_FragColor也是glsl中內置的變量,用於最終渲染顏色的賦值,這裏我們就把我們自己的顏色賦值給gl_FragColor就行,是操作每一個像素的rgba。
通過第四步和第五步,我們已經設置好了頂點和顏色的着色器程序,接下來就可以讓OpenGL加載這2個着色器程序,然後執行裏面的代碼,最終繪製出我們想要的圖形(三角形)了。
六、加載並編譯着色器語言
6.1、通過GLES20.glCreateShader(shaderType)創建(頂點或片元)類型的代碼程序,如:
創建頂點類型的着色器代碼程序:
int vertexShader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER)
片元傳入:GLES20.GL_FRAGMENT_SHADER。
6.2、加載shader源碼並編譯shader
GLES20.glShaderSource(shader, source);//這裏更加我們創建的類型加載相應類型的着色器(如:頂點類型)
GLES20.glCompileShader(shader);//編譯我們自己寫的着色器代碼程序
6.3、實際創建並返回一個渲染程序(program)
int program = GLES20.glCreateProgram();//創建一個program程序
6.4、將着色器程序添加到渲染程序中
GLES20.glAttachShader(program, vertexShader);//把頂點着色器加入program程序中
GLES20.glAttachShader(program, fragmentShader);//把片元着色器加入program程序中
6.5、鏈接源程序
GLES20.glLinkProgram(program);//最終鏈接頂點和片元着色器,後面在program中就可以訪問頂點和片元着色器裏面的屬性了。
通過上面5個步驟,我們就將用glsl寫的着色器程序變成了我們可以在應用程序(java代碼)中可以獲取裏面的變量並操作變量的具體的程序(program)了。
七、接下來就是傳遞頂點座標和顏色值給着色器程序:
7.1、獲取頂點變量
int aPositionHandl = GLES20.glGetAttribLocation(programId, "av_Position");//獲取頂點屬性,後面會給它賦值(即:把我們的頂點賦值給它)
這裏的av_Position就是頂點着色器中的attribute變量,後續操作就可以用返回值aPositionHandl這個句柄了。
7.2、獲取顏色變量
int afColor = GLES20.glGetUniformLocation(program, "af_Color");//獲取片元變量,後面可以通過它設置片元要顯示的顏色。
這裏的af_Color就是片元着色器中的uniform變量。後面可以對它賦值來改變三角形的顏色。
7.3、開始執行着色器程序
GLES20.glUseProgram(programId);//開始繪製之前,先設置使用當前programId這個程序。
7.4、首先激活頂點屬性
GLES20.glEnableVertexAttribArray(aPositionHandl);//激活頂點屬性數組,激活後才能對它賦值
7.4、向頂點屬性傳遞頂點數組的值
GLES20.glVertexAttribPointer(aPositionHandl, 2, GLES20.GL_FLOAT, false, 8,
vertexBuffer);//現在就是把我們的頂點vertexBuffer賦值給頂點着色器裏面的變量。
第一參數就是我們的頂點屬性的句柄
第二個參數是我們用的幾個分量表示的一個點,這裏用的(x,y)2個分量,所以就填入2
第三個參數表示頂點的數據類型,因爲我們用的float類型,所以就填入GL_FLOAT類型
第四個參數是是否做歸一化處理,如果我們的座標不在(-1,1)之間,就需要,由於我們的座標是在(-1,1)之間,所以不需要,填入false
第五個參數是每個點所佔空間大小,因爲是(x,y)2個點,每個點是4個字節,所以一個點佔8個空間大小,這個設置好後,OpenGL才知道8個字節表示一個點,就能按照這個規則,依次取出所有的點的值。
第六個參數就是OpenGL要從哪個內存中取出這些點的數據。
7.4、最後繪製這些頂點
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);//繪製三角形,從我們的頂點數組裏面第0個位置開始,繪製頂點的個數爲3(因爲三角形只有三個頂點)
這裏是以頂點數組的方式來繪製圖形
第一個參數表示繪製的方式:GLES20.GL_TRIANGLES,單個三角形的方式,還有其他方式,我們後面會講解。
第二個參數表示從哪個位置開始繪製,因爲頂點座標裏面只有3個座標點,所以從0開始繪製。
第三個參數表示繪製多少個點,這裏顯然繪製三個點。
以上就是OpenGL的執行過程:
座標點(頂點或紋理)->編寫着色器程序->加載着色器程序並編譯生成program->獲取program中的變量->program變量賦值->最終繪製。
注:在加載着色器程序的時候還需要檢查是否加載成功等結果,還有繪製圖形時的清屏操作會在實例代碼中給出完整的例子。
八、核心代碼
8.1、加載着色器程序生成program(WlShaderUtil.java)
package com.ywl5320.opengldemo;
import android.content.Context;
import android.opengl.GLES20;
import android.util.Log;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class WlShaderUtil {
public static String readRawTxt(Context context, int rawId) {
InputStream inputStream = context.getResources().openRawResource(rawId);
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
StringBuffer sb = new StringBuffer();
String line;
try
{
while((line = reader.readLine()) != null)
{
sb.append(line).append("\n");
}
reader.close();
}
catch (Exception e)
{
e.printStackTrace();
}
return sb.toString();
}
public static int loadShader(int shaderType, String source)
{
int shader = GLES20.glCreateShader(shaderType);
if(shader != 0)
{
GLES20.glShaderSource(shader, source);
GLES20.glCompileShader(shader);
int[] compile = new int[1];
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compile, 0);
if(compile[0] != GLES20.GL_TRUE)
{
Log.d("ywl5320", "shader compile error");
GLES20.glDeleteShader(shader);
shader = 0;
}
}
return shader;
}
public static int createProgram(String vertexSource, String fragmentSource)
{
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
if(vertexShader == 0)
{
return 0;
}
int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
if(fragmentShader == 0)
{
return 0;
}
int program = GLES20.glCreateProgram();
if(program != 0)
{
GLES20.glAttachShader(program, vertexShader);
GLES20.glAttachShader(program, fragmentShader);
GLES20.glLinkProgram(program);
int[] linsStatus = new int[1];
GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linsStatus, 0);
if(linsStatus[0] != GLES20.GL_TRUE)
{
Log.d("ywl5320", "link program error");
GLES20.glDeleteProgram(program);
program = 0;
}
}
return program;
}
}
8.2、WlRender.java
package com.ywl5320.opengldemo;
import android.content.Context;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
public class WlRender implements GLSurfaceView.Renderer{
private Context context;
private final float[] vertexData ={
-1f, 0f,
0f, 1f,
1f, 0f
};
private FloatBuffer vertexBuffer;
private int program;
private int avPosition;
private int afColor;
public WlRender(Context context)
{
this.context = context;
vertexBuffer = ByteBuffer.allocateDirect(vertexData.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(vertexData);
vertexBuffer.position(0);
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
String vertexSource = WlShaderUtil.readRawTxt(context, R.raw.vertex_shader);
String fragmentSource = WlShaderUtil.readRawTxt(context, R.raw.fragment_shader);
program = WlShaderUtil.createProgram(vertexSource, fragmentSource);
if(program > 0)
{
avPosition = GLES20.glGetAttribLocation(program, "av_Position");
afColor = GLES20.glGetUniformLocation(program, "af_Color");
}
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES20.glViewport(0, 0, width, height);
}
@Override
public void onDrawFrame(GL10 gl) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
GLES20.glUseProgram(program);
GLES20.glUniform4f(afColor, 1f, 0f, 0f, 1f);
GLES20.glEnableVertexAttribArray(avPosition);
GLES20.glVertexAttribPointer(avPosition, 2, GLES20.GL_FLOAT, false, 8, vertexBuffer);
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);
}
}
這裏用到了通過uniform類型變量傳值的方式:
GLES20.glUniform4f(afColor, 1f, 0f, 0f, 1f);//分別設置片元變量的rgba四個值(前面的glUniform4f:表示這是uniform類型的變量的4個float類型的值)
給片元着色器中的顏色變量afColor設置argb的值爲:(1f, 0f, 0f,1f)——紅色
然後其他的代碼和上一篇博客一樣。
九、最終效果如下:
十、總結
通過本篇文章,我們瞭解了Android中OpenGL ES的頂點座標加載過程,在OpenGL ES中最複雜的圖像就是三角形,其他任意圖像都可以通過三角形來組合出來,這也爲我們後續的功能打下了基礎,務必好好理解裏面的流程和邏輯。
GitHub:Android-OpenGL-ES