OpenGL Android課程一:入門

翻譯文

原文標(biāo)題:OpenGL Android Lesson One: Getting Started
原文鏈接:http://www.learnopengles.com/android-lesson-one-getting-started/


這是在Android中使用OpenGL ES2的第一個(gè)教程。這一課中,我們將一步一步跟隨代碼,學(xué)習(xí)如何創(chuàng)建一個(gè)OpenGL ES 2并繪制到屏幕上。
我們還將了解什么是著色器,它們?nèi)绾喂ぷ鳎约霸鯓邮褂镁仃噷?chǎng)景轉(zhuǎn)換為您在屏幕上看到的圖像。最后,您需要在清單文件中添加您正在使用OpenGL ES 2的說明,以告知Android應(yīng)用市場(chǎng)支持的設(shè)備可見。

入門

我們將過一道下面所有的代碼并且解釋每一部分的作用。您可以跟著拷貝每一處的代碼片段來創(chuàng)建您自己的項(xiàng)目,您也可以在文章末尾下載這個(gè)已完成的項(xiàng)目。
在開發(fā)工具(如:Android Studio)中創(chuàng)建您的Android項(xiàng)目,名字不重要,這里由于這個(gè)課程我將MainActivity更名為LessonOneActivity。

我們來看這段代碼:

/** 保留對(duì)GLSurfaceView的引用*/
private GLSurfaceView mGLSurfaceView;

這個(gè)GLSurfaceView是一個(gè)特別的View,它為我們管理OpenGL界面并且將它繪制在Android View系統(tǒng)。它還添加了許多功能,使其更易于使用OpenGL,包括下面等等:

  • 它為OpenGL提供一個(gè)專用的著色線程,因此主線程不會(huì)停懈
  • 它支持連續(xù)或按需渲染
  • 它使用EGL (OpenGL和底層系統(tǒng)窗口之間的接口)來處理屏幕設(shè)置

GLSurfaceView使得在Android中設(shè)置和使用OpenGL相對(duì)輕松

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mGLSurfaceView = new GLSurfaceView(this);
    //檢測(cè)系統(tǒng)是否支持OpenGL ES 2.0
    final ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
    final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
    final boolean supportsEs2 = configurationInfo.reqGlEsVersion >= 0x20000;

    if (supportsEs2) {
        // 請(qǐng)求一個(gè)OpenGL ES 2.0兼容的上下文
        mGLSurfaceView.setEGLContextClientVersion(2);
        // 設(shè)置我們的Demo渲染器,定義在后面講
        mGLSurfaceView.setRenderer(new LessonOneRenderer());
    } else {
        // 如果您想同時(shí)支持ES 1.0和2.0的話,這里您可以創(chuàng)建兼容OpenGL ES 1.0的渲染器
        return;
    }
    setContentView(mGLSurfaceView);
}

onCreate()方法中是我們創(chuàng)建OpenGL上下文以及一切開始發(fā)生的重要部分。
在我們的onCreate()方法中,在調(diào)用super.onCreate()后我們首先創(chuàng)建了GLSurfaceView實(shí)例。
然后我們需要弄清楚系統(tǒng)是否支持OpenGL ES 2.為此,我們獲得一個(gè)ActivityManager實(shí)例,它允許我們與全局系統(tǒng)狀態(tài)進(jìn)行交互。
然后我們使用它獲取設(shè)備配置信息,它將告訴我們?cè)O(shè)備是否支持OpenGL ES 2。
我們也可以通過傳入不同的渲染器來支持OpenGL ES 1.x,盡管因?yàn)锳PI不同,我們需要編寫不同的代碼。對(duì)于本課我們僅僅關(guān)注支持OpenGL ES 2。

一旦我們知道設(shè)備是否支持OpenGL ES 2,我們告訴GLSurfaceView兼容OpenGL ES 2,然后傳入我們的自定義渲染器。無論何時(shí)調(diào)整界面或繪制新幀,系統(tǒng)都會(huì)調(diào)用此渲染器。

最后,我們調(diào)用setContentView()設(shè)置GLSurfaceView為顯示內(nèi)容,它告訴Android這個(gè)活動(dòng)內(nèi)容因該被我們的OpenGL界面填充。要入門OpenGL,就是這么簡(jiǎn)單。

@Override
protected void onResume() {
    super.onResume();
    //Activity 必須在onResume中調(diào)用GLSurfaceView的onResume方法
    mGLSurfaceView.onResume();
}

@Override
protected void onPause() {
    super.onPause();
    //Activity 必須在onPause中調(diào)用GLSurfaceView的onPause方法
    mGLSurfaceView.onPause();
}

GLSurfaceView要求我們?cè)贏ctivityonResume()onPause()的父方法被調(diào)用后分別調(diào)用它的onResume()onPause()方法。我們?cè)诖颂砑诱{(diào)用以完善我們的Activity。

可視化3D世界

在這部分,我們來看怎樣讓OpenGL ES 2工作,以及我們?nèi)绾卧谄聊簧侠L制東西。
在Activity中我們傳入自定義的GLSurfaceView.RendererGLSurfaceView,它將在這里定義。
這個(gè)渲染器有三個(gè)重要的方法,每當(dāng)系統(tǒng)事件發(fā)生時(shí),它們將會(huì)自動(dòng)被調(diào)用:

public void onSurfaceCreated(GL10 gl, EGLConfig config)

當(dāng)界面第一次被創(chuàng)建時(shí)調(diào)用,如果我們失去界面上下文并且之后由系統(tǒng)重建,也會(huì)被調(diào)用。

public void onSurfaceChanged(GL10 gl, int width, int height)

每當(dāng)界面改變時(shí)被調(diào)用;例如,從縱屏切換到橫屏,在創(chuàng)建界面后也會(huì)被調(diào)用。

public void onDrawFrame(GL10 gl)

每當(dāng)繪制新幀時(shí)被調(diào)用。

您可能注意到GL10的實(shí)例被傳入名字是gl。當(dāng)使用OpengGL ES 2繪制時(shí),我們不能使用它;
我們使用GLES20類的靜態(tài)方法來代替。這個(gè)GL10參數(shù)僅僅是在這里,因?yàn)橄嗤慕涌诒皇褂迷贠penGL ES 1.x。

在我們的渲染器可以顯示任何內(nèi)容之前,我們需要有些東西去顯示。在OpenGL ES 2,我們通過制定數(shù)字?jǐn)?shù)組傳遞內(nèi)容。這些數(shù)字可以表示位置、顏色或任何我們需要的。在這個(gè)Demo中,我們將顯示三個(gè)三角形。

// 新類成員
private final FloatBuffer mTriangle1Verticels;
private final FloatBuffer mTriangle2Verticels;
private final FloatBuffer mTriangle3Verticels;

/** 每個(gè)Float多少字節(jié)*/
private final int mBytePerFloat = 4;

/**
 * 初始Model數(shù)據(jù)
 */
public LessonOneRenderer() {
    // 這個(gè)三角形是紅色,藍(lán)色和綠色組成
    final float[] triangle1VerticesData = {
        // X, Y, Z,
        // R, G, B, A
        -0.5F, -0.25F, 0.0F,
        1.0F, 0.0F, 0.0F, 1.0F,

        0.5F, -0.25F, 0.0F,
        0.0F, 0.0F, 1.0F, 1.0F,

        0.0F, 0.559016994F, 0.0F,
        0.0F, 1.0F, 0.0F, 1.0F
    };
    ...
    // 初始化緩沖區(qū)
    mTriangle1Verticels = ByteBuffer.allocateDirect(triangle1VerticesData.length * mBytePerFloat).order(ByteOrder.nativeOrder()).asFloatBuffer();
    ...
    mTriangle1Verticels.put(triangle1VerticesData).position(0);
    ...
}

那么,這些是什么意思?如果您曾經(jīng)使用過OpenGL 1, 您可能會(huì)習(xí)慣這樣做:

glBegin(GL_TRIANGLES);
glVertex3f(-0.5f, -0.25f, 0.0f);
glColor3f(1.0f, 0.0f, 0.0f);
...
glEnd();

這種方法在OpenGL ES 2中不起作用。我們不是通過一堆方法調(diào)用來定義點(diǎn),而是定義一個(gè)數(shù)組。讓我們?cè)賮砜纯次覀冞@個(gè)數(shù)組:

final float[] triangle1VerticesData = {
                // X, Y, Z,
                // R, G, B, A
                -0.5f, -0.25f, 0.0f,
                1.0f, 0.0f, 0.0f, 1.0f,
                ...
};

上面展示的代表三角形的一個(gè)點(diǎn)。我們已設(shè)置好前三個(gè)數(shù)字代表位置(X,Y,Z),隨后的四個(gè)數(shù)字代表顏色(紅,綠,藍(lán),透明度)。
您不必太擔(dān)心如何定義這個(gè)數(shù)組;只要記住當(dāng)我們想繪制東西在OpenGL ES 2時(shí),我們需要以塊的形式傳遞數(shù)據(jù),而不是一次傳遞一個(gè)。

了解緩沖區(qū)

// 初始化緩沖區(qū)
mTriangle1Verticels = ByteBuffer.allocateDirect(triangle1VerticesData.length * mBytePerFloat).order(ByteOrder.nativeOrder()).asFloatBuffer();
...

我們?cè)贏ndroid上使用Java進(jìn)行編碼,但OpengGL ES 2底層實(shí)現(xiàn)其實(shí)使用C語言編寫的。
在我們將數(shù)據(jù)傳遞給OpenGL之前,我們需要將其轉(zhuǎn)換成它能理解的形式。
Java和native系統(tǒng)可能不會(huì)以相同的順序存儲(chǔ)它們的字節(jié),因此我們使用一個(gè)特殊的緩沖類并創(chuàng)建一個(gè)足夠大的ByteBuffer來保存我們的數(shù)據(jù),并告訴它使用native字節(jié)順序存儲(chǔ)數(shù)據(jù)。
然后我們將它轉(zhuǎn)換成FloatBuffer,以便我們可以使用它來保存浮點(diǎn)數(shù)據(jù)。
最后,我們將數(shù)組復(fù)制到緩沖區(qū)。

這個(gè)緩沖區(qū)的東西看起來可能很混亂,單請(qǐng)記住,在將數(shù)據(jù)傳遞給OpenGL之前,我們需要做一個(gè)額外的步驟。我們現(xiàn)在的緩沖區(qū)已準(zhǔn)備好可以用于將數(shù)據(jù)傳入OpenGL。

另外,float緩沖區(qū)在Froyo上很慢,在Gingerbread上緩慢,因此您可能不希望經(jīng)常更換它們。

理解矩陣

// new class 定義

/**
 * 存儲(chǔ)view矩陣??梢哉J(rèn)為這是一個(gè)相機(jī),我們通過相機(jī)將世界空間轉(zhuǎn)換為眼睛空間
 * 它定位相對(duì)于我們眼睛的東西
 */
private float[] mViewMatrix = new float[16];

@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
    // 設(shè)置背景清理顏色為灰色
    GLES20.glClearColor(0.5F, 0.5F, 0.5F, 0.5F);

    // 將眼睛放到原點(diǎn)之后
    final float eyeX = 0.0F;
    final float eyeY = 0.0F;
    final float eyeZ = 1.5F;

    // 我們的眼睛望向哪
    final float lookX = 0.0F;
    final float lookY = 0.0F;
    final float lookZ = -5.0F;

    // 設(shè)置我們的向量,這是我們拿著相機(jī)時(shí)頭指向的方向
    final float upX = 0.0F;
    final float upY = 1.0F;
    final float upZ = 0.0F;

    // 可以這樣想象:我們?cè)跇蛏夏弥鄼C(jī)90°彎腰拍攝水平面下5米處的美人魚

    // 設(shè)置view矩陣,可以說這個(gè)矩陣代表相機(jī)的位置
    // 注意:在OpenGL 1中使用ModelView matrix,這是一個(gè)model和view矩陣的組合。
    //在OpenGL2中,我們選擇分別跟蹤這些矩陣
    Matrix.setLookAtM(mViewMatrix, 0, eyeX, eyeY, eyeZ, lookX, lookY, lookZ, upX, upY, upZ);
    ...
}

另一個(gè)有趣的話題是矩陣!無論您何時(shí)進(jìn)行3D編程,這些都將成為您最好的朋友。因此,您需要很好的了解他們。

當(dāng)我們的界面被創(chuàng)建,我們第一件事情是設(shè)置清理顏色為灰色。alpha部分也設(shè)置為灰色,但在我們本課程中沒有進(jìn)行alpha混合,因此該值未使用。我們只需要設(shè)置一次清理顏色,之后我們不會(huì)更改它。

我們第二件事情是設(shè)置view矩陣。我們使用了幾個(gè)不同種類的矩陣,它們都做了些重要的事情:

  1. model(模型)矩陣,該矩陣用于在“世界”中的某處放置模型。例如,您有一個(gè)模型車,你想將它放置在東邊一千米處,您將使用矩陣模型來做這件事。
  2. view (視圖)矩陣,該矩陣代表相機(jī)。如果我們想查看位于東邊一千米處的車,我們也需要向東移動(dòng)一千米(另一種思考方式是我們保持靜止,世界向西移動(dòng)一千米)。我們使用視圖矩陣來做到這點(diǎn)。
  3. projection(投影)矩陣。由于我們的屏幕是平面的,我們需要進(jìn)行最后的轉(zhuǎn)換,將我們的視圖“投影”到我們的屏幕上并獲得漂亮的3D視角。這就是投影矩陣的用途

可以在SongHo的OpenGL教程中找到很好的解釋。我建議您閱讀幾次直到您把握好這個(gè)想法為止;別擔(dān)心,我也閱讀了它好幾次!

在OpenGL 1中,模型和視圖矩陣被組合并且假設(shè)了攝像機(jī)處于(0,0,0)坐標(biāo)并面向Z軸方向。

我們不需要手動(dòng)構(gòu)建這些矩陣,Android有一個(gè)Matrix幫助類,它能為我們做繁重的工作。這里,我為攝像機(jī)創(chuàng)建了一個(gè)視圖矩陣,它位于原點(diǎn)后,朝向遠(yuǎn)處。

定義vertex(頂點(diǎn))和fragment(片段)著色器

final String vertexShader =
        "uniform mat4 u_MVPMatrix;    \n" + // 一個(gè)表示組合model、view、projection矩陣的常量
        "attribute vec4 a_Position;   \n" + // 我們將要傳入的每個(gè)頂點(diǎn)的位置信息
        "attribute vec4 a_Color;      \n" + // 我們將要傳入的每個(gè)頂點(diǎn)的顏色信息

        "varying vec4 v_Color;        \n" + // 他將被傳入片段著色器

        "void main()                  \n" + // 頂點(diǎn)著色器入口
        "{                            \n" +
        "   v_Color = a_Color;        \n" + // 將顏色傳遞給片段著色器
                                            // 它將在三角形內(nèi)插值
        "   gl_Position = u_MVPMatrix \n" + // gl_Position是一個(gè)特殊的變量用來存儲(chǔ)最終的位置
        "               * a_Position  \n" + // 將頂點(diǎn)乘以矩陣得到標(biāo)準(zhǔn)化屏幕坐標(biāo)的最終點(diǎn)
        "}                            \n";

在OpenGL ES 2中任何我們想展示在屏幕中的東西都必須先經(jīng)過頂點(diǎn)和片段著色器,還好這些著色器并不像他們看起來的那么復(fù)雜。頂點(diǎn)著色器在每個(gè)頂點(diǎn)執(zhí)行操作,并把這些操作的結(jié)果使用在片段著色器做額外的每像素計(jì)算。

每個(gè)著色器基本由輸入(input)、輸出(output)和一個(gè)程序(program)組成。
首先我們定義一個(gè)統(tǒng)一(uniform),它是一個(gè)包含所有變換的組合矩陣。它是所有頂點(diǎn)的常量,用于將它們投影到屏幕上。
然后我們定義了位置和顏色屬性(attribute),這些屬性將從我們之前定義的緩存區(qū)中讀入,并指定每個(gè)頂點(diǎn)的位置和顏色。
接著我們定義了一個(gè)變量(varying),它負(fù)責(zé)在三角形中插值并傳遞到片段著色器。當(dāng)它運(yùn)行到片段著色器,它將為每個(gè)像素持有一個(gè)插值。

假設(shè)我們定義了一個(gè)三角形每個(gè)點(diǎn)都是紅色、綠色和藍(lán)色,我們調(diào)整它的大小讓它占用10像素屏幕。當(dāng)片段著色器運(yùn)行時(shí),它將為每像素包含一個(gè)不同的變量(varying)顏色。在某一點(diǎn)上,變量(varying)將是紅色,但是在紅色和藍(lán)色之間它可能是更紫的顏色。

除了設(shè)置顏色,我們還告訴OpenGL頂點(diǎn)在屏幕上的最終位置。然后我們定義片段著色器:

final String fragmentShader =
        "precision mediump float;       \n" + // 我們將默認(rèn)精度設(shè)置為中等,我們不需要片段著色器中的高精度
        "varying vec4 v_Color;          \n" + // 這是從三角形每個(gè)片段內(nèi)插的頂點(diǎn)著色器的顏色
        "void main()                    \n" + // 片段著色器入口
        "{                              \n" +
        "   gl_FragColor = v_Color;     \n" + // 直接將顏色傳遞
        "}                              \n";

這是個(gè)片段著色器,它會(huì)將東西放到屏幕上。在這個(gè)著色器中,我們得到的變量(varying)顏色來自頂點(diǎn)著色器,然后將它直接傳遞給OpenGL。該點(diǎn)已按像素插值,因?yàn)槠沃鲗⑨槍?duì)每個(gè)將要繪制的像素點(diǎn)運(yùn)行。

更多信息:OpenGL ES 2 API快速參考卡

將著色器加載到OpenGL

// 加載頂點(diǎn)著色器
int vertexShaderHandle = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
if (vertexShaderHandle != 0) {
    // 傳入頂點(diǎn)著色器源代碼
    GLES20.glShaderSource(vertexShaderHandle, vertexShader);
    // 編譯頂點(diǎn)著色器
    GLES20.glCompileShader(vertexShaderHandle);

    // 獲取編譯狀態(tài)
    final int[] compileStatus = new int[1];
    GLES20.glGetShaderiv(vertexShaderHandle, GLES20.GL_COMPILE_STATUS, compileStatus, 0);

    // 如果編譯失敗則刪除著色器
    if (compileStatus[0] == 0) {
        GLES20.glDeleteShader(vertexShaderHandle);
        vertexShaderHandle = 0;
    }
}

if (vertexShaderHandle == 0) {
    throw new RuntimeException("Error creating vertex shader.");
}

首先,我們創(chuàng)建一個(gè)著色器對(duì)象。如果成功,我們將得到這個(gè)對(duì)象的引用。
然后,我們使用這個(gè)引用傳入著色器源碼然后編譯它。
我們可以從OpenGL獲取編譯是否成功的狀態(tài),如果失敗我們可以使用GLES20.glGetShaderInfoLog(shader)找到原因。我們按照相同的步驟加載片段著色器。

將頂點(diǎn)和片段著色器鏈接到一個(gè)程序中

// 創(chuàng)建一個(gè)程序?qū)ο蟛⒁梅胚M(jìn)去
int programHandle = GLES20.glCreateProgram();
if (programHandle != 0) {
    // 綁定頂點(diǎn)著色器到程序?qū)ο笾?    GLES20.glAttachShader(programHandle, vertexShaderHandle);
    // 綁定片段著色器到程序?qū)ο笾?    GLES20.glAttachShader(programHandle, fragmentShaderHandle);
    // 綁定屬性
    GLES20.glBindAttribLocation(programHandle, 0, "a_Position");
    GLES20.glBindAttribLocation(programHandle, 1, "a_Color");
    // 將兩個(gè)著色器連接到程序
    GLES20.glLinkProgram(programHandle);
    // 獲取連接狀態(tài)
    final int[] linkStatus = new int[1];
    GLES20.glGetProgramiv(programHandle, GLES20.GL_LINK_STATUS, linkStatus, 0);
    // 如果連接失敗,刪除這程序
    if (linkStatus[0] == 0) {
        GLES20.glDeleteProgram(programHandle);
        programHandle = 0;
    }
}

if (programHandle == 0) {
    throw new RuntimeException("Error creating program.");
}

在我們使用頂點(diǎn)和片段著色器之前,我們需要將它們綁定到一個(gè)程序中,它連接了頂點(diǎn)著色器的輸出和片段著色器的輸入。這也是讓我們從程序傳遞輸入并使用著色器繪制形狀的原因。

我們創(chuàng)建一個(gè)程序?qū)ο螅绻晒壎ㄖ?。我們想要將位置和顏色作為屬性傳遞進(jìn)去,因此我們需要綁定這些屬性。然后我們將著色器連接到一起。

// 新類成員
/** 這將用于傳遞變換矩陣*/
private int mMVPMatrixHandle;
/** 用于傳遞model位置信息*/
private int mPositionHandle;
/** 用于傳遞模型顏色信息*/
private int mColorHandle;
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
    ...
    // 設(shè)置程序引用,這將在之后傳遞值到程序時(shí)使用
    mMVPMatrixHandle = GLES20.glGetUniformLocation(programHandle, "u_MVPMatrix");
    mPositionHandle = GLES20.glGetAttribLocation(programHandle, "a_Position");
    mColorHandle = GLES20.glGetAttribLocation(programHandle, "a_Color");

    // 告訴OpenGL渲染的時(shí)候使用這個(gè)程序
    GLES20.glUseProgram(programHandle);
}

在我們成功連接程序后,我們還要完成幾個(gè)任務(wù),以便我們能實(shí)際使用它。
第一個(gè)任務(wù)是獲取引用,因?yàn)槲覀円獋鬟f數(shù)據(jù)到程序中。
然后我們要告訴OpenGL在繪制時(shí)使用我們這個(gè)程序。
由于本課我們僅使用了一個(gè)程序,我們可以將它放到onSurfaceCreated()方法中而不是onDrawFrame()

設(shè)置透視投影

// 新類成員
// 存放投影矩陣,用于將場(chǎng)景投影到2D視角
private float[] mProjectionMatrix = new float[16];

@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
    // 設(shè)置OpenGL界面和當(dāng)前視圖相同的尺寸
    GLES20.glViewport(0, 0, width, height);

    // 創(chuàng)建一個(gè)新的透視投影矩陣,高度保持不變,而高度根據(jù)縱橫比而變換
    final float ratio = (float) width / height;
    final float left = -ratio;
    final float right = ratio;
    final float bottom = -1.0F;
    final float top = 1.0F;
    final float near = 1.0F;
    final float far = 10.0F;

    Matrix.frustumM(mProjectionMatrix, 0, left, right, bottom, top, near, far);
}

onSurfaceChanged()方法至少被調(diào)用一次,每當(dāng)界面改變也會(huì)被調(diào)用。因?yàn)槲覀冃枰慨?dāng)界面改變的時(shí)候重置投影矩陣,那么onSurfaceChanged()方法中是個(gè)理想的地方。

繪制東西到屏幕上!

// 新類成員
// 存放模型矩陣,該矩陣用于將模型從對(duì)象空間(可以認(rèn)為每個(gè)模型開始都位于宇宙的中心)移動(dòng)到世界空間
private float[] mModelMatrix = new float[16];

@Override
public void onDrawFrame(GL10 gl) {
    GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);

    // 每10s完成一次旋轉(zhuǎn)
    long time = SystemClock.uptimeMillis() % 10000L;
    float angleDegrees = (360.0F / 10000.0F) * ((int)time);

    // 畫三角形
    Matrix.setIdentityM(mModelMatrix, 0);
    Matrix.rotateM(mModelMatrix, 0, angleDegrees, 0.0F, 0.0F, 1.0F);
    drawTriangle(mTriangle1Verticels);
    ...
}

這是實(shí)際顯示在屏幕上的內(nèi)容。我們清理屏幕,因此不會(huì)得到任何奇怪的鏡像效應(yīng)影響,我們希望我們的三角形在屏幕上能有平滑的動(dòng)畫,通常使用時(shí)間而不是幀率更好。

實(shí)際繪制在drawTriangle()方法中完成

// 新的類成員
/** 為最終的組合矩陣分配存儲(chǔ)空間,這將用來傳入著色器程序*/
private float[] mMVPMatrix = new float[16];

/** 每個(gè)頂點(diǎn)有多少字節(jié)組成,每次需要邁過這么一大步(每個(gè)頂點(diǎn)有7個(gè)元素,3個(gè)表示位置,4個(gè)表示顏色,7 * 4 = 28個(gè)字節(jié))*/
private final int mStrideBytes = 7 * mBytePerFloat;

/** 位置數(shù)據(jù)偏移量*/
private final int mPositionOffset = 0;

/** 一個(gè)元素的位置數(shù)據(jù)大小*/
private final int mPositionDataSize = 3;

/** 顏色數(shù)據(jù)偏移量*/
private final int mColorOffset = 3;

/** 一個(gè)元素的顏色數(shù)據(jù)大小*/
private final int mColorDataSize = 4;

/**
 * 從給定的頂點(diǎn)數(shù)據(jù)中繪制一個(gè)三角形
 * @param aTriangleBuffer 包含頂點(diǎn)數(shù)據(jù)的緩沖區(qū)
 */
private void drawTriangle(FloatBuffer aTriangleBuffer) {
    aTriangleBuffer.position(mPositionOffset);
    GLES20.glVertexAttribPointer(
            mPositionHandle, mPositionDataSize, GLES20.GL_FLOAT, false,
            mStrideBytes, aTriangleBuffer);

    GLES20.glEnableVertexAttribArray(mPositionHandle);

    // 傳入顏色信息
    aTriangleBuffer.position(mColorOffset);
    GLES20.glVertexAttribPointer(mColorHandle, mColorDataSize, GLES20.GL_FLOAT, false,
            mStrideBytes, aTriangleBuffer);

    GLES20.glEnableVertexAttribArray(mColorHandle);

    // 將視圖矩陣乘以模型矩陣,并將結(jié)果存放到MVP Matrix(model * view)
    Matrix.multiplyMM(mMVPMatrix, 0, mViewMatrix, 0, mModelMatrix, 0);

    // 將上面計(jì)算好的視圖模型矩陣乘以投影矩陣,并將結(jié)果存放到MVP Matrix(model * view * projection)
    Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mMVPMatrix, 0);

    GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mMVPMatrix, 0);
    GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);

}

您還記得我們最初創(chuàng)建渲染器時(shí)定義的那些緩沖區(qū)嗎?我們終于可以使用它們了。
我們需要使用GLES20.glVertexAttribPointer()來告訴OpenGL怎樣使用這些數(shù)據(jù)。

我們來看第一個(gè)使用

aTriangleBuffer.position(mPositionOffset);
GLES20.glVertexAttribPointer(
        mPositionHandle, mPositionDataSize, GLES20.GL_FLOAT, false,
        mStrideBytes, aTriangleBuffer);
GLES20.glEnableVertexAttribArray(mPositionHandle);

我們?cè)O(shè)置緩沖區(qū)的位置偏移,它位于緩沖區(qū)的開頭。然后我們告訴OpenGL使用這些數(shù)據(jù)并將其提供給頂點(diǎn)著色器并將其應(yīng)用到位置屬性(a_Position)。我們也需要告訴OpenGL每個(gè)頂點(diǎn)或邁幅之間有多少個(gè)元素。

注意:邁幅(Stride)需要定義為字節(jié)(byte),盡管每個(gè)頂點(diǎn)之間我們有7個(gè)元素(3個(gè)是位置,4個(gè)是顏色),但我們事實(shí)上有28個(gè)字節(jié),因?yàn)槊總€(gè)浮點(diǎn)數(shù)(float)就是4個(gè)字節(jié)(byte)。忘記此步驟您可能沒有任何錯(cuò)誤,但是你會(huì)想知道為什么您的屏幕上看不到任何內(nèi)容。

最終,我們使用了頂點(diǎn)屬性,往下我們使用了下一個(gè)屬性。再往后點(diǎn)我們構(gòu)建一個(gè)組合矩陣,將點(diǎn)投影到屏幕上。我們也可以在頂點(diǎn)著色器中執(zhí)行此操作,但是由于它只需要執(zhí)行一次我們也可以只緩存結(jié)果。
我們使用GLES20.glUniformMatrix4fv()方法將最終的矩陣傳入頂點(diǎn)著色器。
GLES20.glDrawArrays()將我們的點(diǎn)轉(zhuǎn)換為三角形并將其繪制在屏幕上。

總結(jié)

呼呼!這是重要的一課,如果您完成了本課,感謝您!
我們學(xué)習(xí)了怎樣創(chuàng)建OpenGL上下文,傳入形狀數(shù)據(jù),加載頂點(diǎn)和片段著色器,設(shè)置我們的轉(zhuǎn)換矩陣,最終放在一起。
如果一切順利,您因該看到了類似下面的截屏。


screenshot

這一課有很多需要消化的內(nèi)容,您可能需要多次閱讀這些步驟才能理解它。
OpenGL ES 2需要更多的設(shè)置才能開始,但是一旦您完成了這個(gè)過程幾次,您就會(huì)記住這個(gè)流程。

在Android市場(chǎng)上發(fā)布

當(dāng)開發(fā)的應(yīng)用我們不想在無法運(yùn)行這些應(yīng)用程序的人在市場(chǎng)上看到它們,否則當(dāng)應(yīng)用程序在其設(shè)備上崩潰時(shí),我們可能會(huì)收到大量糟糕的評(píng)論和評(píng)分。
要防止OpenGL ES 2 應(yīng)用程序出現(xiàn)在不支持它的設(shè)備上,你可以在清單文件中添加:

<uses-feature
    android:glEsVersion="0x00020000"
    android:required="true" />

這告訴市場(chǎng)您的app需要有OpenGL ES 2支持,不支持的設(shè)備將會(huì)隱藏您的app。

進(jìn)一步探索

嘗試更改動(dòng)畫速度,頂點(diǎn)或顏色,看看會(huì)發(fā)生什么!
可以在Github下載本課程源代碼:下載項(xiàng)目
本課的編譯版本也可以再Android市場(chǎng)下:google play 下載apk
“我”也編譯了個(gè)apk,方便大家下載:github download

教程目錄

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容