寫在前面
記錄一下 OpenGL ES Android 開發的入門教程。邏輯性可能不那麼強,想到哪寫到哪。也可能自己的一些理解有誤。
參考資料:
LearnOpenGL CN
Android官方文件
《OpenGL ES應用開發實踐指南Android卷》
《OpenGL ES 3.0 程式設計指南第2版》
一、前言
目前android 4.3或以上支援opengles 3.0,但目前很多執行android 4.3系統的硬體能支援opengles 3.0的也是非常少的。不過,opengles 3.0是向後相容的,當程式發現硬體不支援opengles 3.0時則會自動呼叫opengles 2.0的API。Andorid 中使用 OpenGLES 有兩種方式,一種是基於Android框架API, 另一種是基於 Native Development Kit(NDK)使用 OpenGL。本文介紹Android框架介面。
二、繪製三角形例項
本文寫一個最基本的三角形繪製,來說明一下 OpenGL ES 的基本流程,以及注意點。
2.1 建立一個 Android 工程,在 AndroidManifest.xml 檔案中宣告使用 opengles3.0
<!-- Tell the system this app requires OpenGL ES 3.0. -->
<uses-feature android:glEsVersion="0x00030000" android:required="true" />
如果程式中使用了紋理壓縮的話,還需進行如下宣告,以防止不支援這些壓縮格式的裝置嘗試執行程式。
<supports-gl-texture android:name="GL_OES_compressed_ETC1_RGB8_texture" />
<supports-gl-texture android:name="GL_OES_compressed_paletted_texture" />
2.2 MainActivity 使用 GLSurfaceView
MainActivity.java 程式碼:
package com.sharpcj.openglesdemo;
import android.app.ActivityManager;
import android.content.Context;
import android.content.pm.ConfigurationInfo;
import android.opengl.GLSurfaceView;
import android.os.Build;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getSimpleName();
private GLSurfaceView mGlSurfaceView;
private boolean mRendererSet;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// setContentView(R.layout.activity_main);
if (!checkGlEsSupport(this)) {
Log.d(TAG, "Device is not support OpenGL ES 2");
return;
}
mGlSurfaceView = new GLSurfaceView(this);
mGlSurfaceView.setEGLContextClientVersion(2);
mGlSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0);
mGlSurfaceView.setRenderer(new MyRenderer(this));
setContentView(mGlSurfaceView);
mRendererSet = true;
}
@Override
protected void onPause() {
super.onPause();
if (mRendererSet) {
mGlSurfaceView.onPause();
}
}
@Override
protected void onResume() {
super.onResume();
if (mRendererSet) {
mGlSurfaceView.onResume();
}
}
/**
* 檢查裝置是否支援 OpenGLEs 2.0
*
* @param context 上下文環境
* @return 返回裝置是否支援 OpenGLEs 2.0
*/
public boolean checkGlEsSupport(Context context) {
final ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
final boolean supportGlEs2 = configurationInfo.reqGlEsVersion >= 0x20000
|| (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1
&& (Build.FINGERPRINT.startsWith("generic")
|| Build.FINGERPRINT.startsWith("unknown")
|| Build.MODEL.contains("google_sdk")
|| Build.MODEL.contains("Emulator")
|| Build.MODEL.contains("Andorid SDK built for x86")));
return supportGlEs2;
}
}
關鍵步驟:
- 建立一個 GLSurfaceView 物件
- 給GLSurfaceView 物件設定 Renderer 物件
- 呼叫
setContentView()
方法,傳入 GLSurfaceView 物件。
2.3 實現 SurfaceView.Renderer 介面中的方法
建立一個類,實現 GLSurfaceView.Renderer
介面,並實現其中的關鍵方法
package com.sharpcj.openglesdemo;
import android.content.Context;
import android.opengl.GLSurfaceView;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import static android.opengl.GLES30.*;
public class MyRenderer implements GLSurfaceView.Renderer {
private Context mContext;
private MyTriangle mTriangle;
public MyRenderer(Context mContext) {
this.mContext = mContext;
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
mTriangle = new MyTriangle(mContext);
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
glViewport(0, 0, width, height);
}
@Override
public void onDrawFrame(GL10 gl) {
glClear(GL_COLOR_BUFFER_BIT);
mTriangle.draw();
}
}
三個關鍵方法:
- onSurfaceCreated() - 在View的OpenGL環境被建立的時候呼叫。
- onSurfaceChanged() - 如果檢視的幾何形狀發生變化(例如,當裝置的螢幕方向改變時),則呼叫此方法。
- onDrawFrame() - 每一次View的重繪都會呼叫
glViewport(0, 0, width, height); 用於設定視口。
glCrearColor(1.0f, 1.0f, 1.0f, 1.0f) 方法用指定顏色(這裡是白色)清空螢幕。
在 onDrawFrame 中呼叫 glClearColor(GL_COLOR_BUFFER_BIT) ,擦除螢幕現有的繪製,並用之前的顏色清空螢幕。 該方法中一定要繪製一些東西,即便只是清空螢幕,因為該方法呼叫後會交換緩衝區,並顯示在螢幕上,否則可能會出現閃爍。該例子中將具體的繪製封裝在了 Triangle 類中的draw
方法中了。
注意:在 windows 版的 OpenGL 中,需要手動呼叫glfwSwapBuffers(window)
來交換緩衝區。
2.4 OpenGL ES 的關鍵繪製流程
建立 MyTriangle.java
類:
package com.sharpcj.openglesdemo;
import android.content.Context;
import com.sharpcj.openglesdemo.util.ShaderHelper;
import com.sharpcj.openglesdemo.util.TextResourceReader;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import static android.opengl.GLES30.*;
public class MyTriangle {
private final FloatBuffer mVertexBuffer;
static final int COORDS_PER_VERTEX = 3; // number of coordinates per vertex in this array
static final int COLOR_PER_VERTEX = 3; // number of coordinates per vertex in this array
static float triangleCoords[] = { // in counterclockwise order:
0.0f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // top
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // bottom left
0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f // bottom right
};
private Context mContext;
private int mProgram;
public MyTriangle(Context context) {
mContext = context;
// initialize vertex byte buffer for shape coordinates
mVertexBuffer = ByteBuffer.allocateDirect(triangleCoords.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
mVertexBuffer.put(triangleCoords); // add the coordinates to the FloatBuffer
mVertexBuffer.position(0); // set the buffer to read the first coordinate
String vertexShaderCode = TextResourceReader.readTextFileFromResource(mContext, R.raw.simple_vertex_glsl);
String fragmentShaderCode = TextResourceReader.readTextFileFromResource(mContext, R.raw.simple_fragment_glsl);
int vertexShader = ShaderHelper.compileVertexShader(vertexShaderCode);
int fragmentShader = ShaderHelper.compileFragmentShader(fragmentShaderCode);
mProgram = ShaderHelper.linkProgram(vertexShader, fragmentShader);
}
public void draw() {
if (!ShaderHelper.validateProgram(mProgram)) {
glDeleteProgram(mProgram);
return;
}
glUseProgram(mProgram); // Add program to OpenGL ES environment
// int aPos = glGetAttribLocation(mProgram, "aPos"); // get handle to vertex shader's vPosition member
mVertexBuffer.position(0);
glVertexAttribPointer(0, COORDS_PER_VERTEX, GL_FLOAT, false, (COORDS_PER_VERTEX + COLOR_PER_VERTEX) * 4, mVertexBuffer); // Prepare the triangle coordinate data
glEnableVertexAttribArray(0); // Enable a handle to the triangle vertices
// int aColor = glGetAttribLocation(mProgram, "aColor");
mVertexBuffer.position(3);
glVertexAttribPointer(1, COORDS_PER_VERTEX, GL_FLOAT, false, (COORDS_PER_VERTEX + COLOR_PER_VERTEX) * 4, mVertexBuffer); // Prepare the triangle coordinate data
glEnableVertexAttribArray(1);
// Draw the triangle
glDrawArrays(GL_TRIANGLES, 0, 3);
}
}
在該類中,我們使用了,兩個工具類:
TextResourceReader.java
, 用於讀取檔案的類容,返回一個字串,準確說,它與 OpenGL 本身沒有關係。
package com.sharpcj.openglesdemo.util;
import android.content.Context;
import android.content.res.Resources;
import android.util.Log;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class TextResourceReader {
private static String TAG = "TextResourceReader";
public static String readTextFileFromResource(Context context, int resourceId) {
StringBuilder body = new StringBuilder();
InputStream inputStream = null;
InputStreamReader inputStreamReader = null;
BufferedReader bufferedReader = null;
try {
inputStream = context.getResources().openRawResource(resourceId);
inputStreamReader = new InputStreamReader(inputStream);
bufferedReader = new BufferedReader(inputStreamReader);
String nextLine;
while ((nextLine = bufferedReader.readLine()) != null) {
body.append(nextLine);
body.append("\n");
}
} catch (IOException e) {
throw new RuntimeException("Could not open resource: " + resourceId, e);
} catch (Resources.NotFoundException nfe) {
throw new RuntimeException("Resource not found: " + resourceId, nfe);
} finally {
closeStream(inputStream);
closeStream(inputStreamReader);
closeStream(bufferedReader);
}
return body.toString();
}
private static void closeStream(Closeable c) {
if (c != null) {
try {
c.close();
} catch (IOException e) {
Log.e(TAG, e.getMessage());
}
}
}
}
ShaderHelper.java
著色器的工具類,這個跟 OpenGL 就有非常大的關係了。
package com.sharpcj.openglesdemo.util;
import android.util.Log;
import static android.opengl.GLES30.*;
public class ShaderHelper {
private static final String TAG = "ShaderHelper";
public static int compileVertexShader(String shaderCode) {
return compileShader(GL_VERTEX_SHADER, shaderCode);
}
public static int compileFragmentShader(String shaderCode) {
return compileShader(GL_FRAGMENT_SHADER, shaderCode);
}
private static int compileShader(int type, String shaderCode) {
final int shaderObjectId = glCreateShader(type);
if (shaderObjectId == 0) {
Log.w(TAG, "could not create new shader.");
return 0;
}
glShaderSource(shaderObjectId, shaderCode);
glCompileShader(shaderObjectId);
final int[] compileStatus = new int[1];
glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS, compileStatus, 0);
/*Log.d(TAG, "Results of compiling source: " + "\n" + shaderCode + "\n: "
+ glGetShaderInfoLog(shaderObjectId));*/
if (compileStatus[0] == 0) {
glDeleteShader(shaderObjectId);
Log.w(TAG, "Compilation of shader failed.");
return 0;
}
return shaderObjectId;
}
public static int linkProgram(int vertexShaderId, int fragmentShaderId) {
final int programObjectId = glCreateProgram();
if (programObjectId == 0) {
Log.w(TAG, "could not create new program");
return 0;
}
glAttachShader(programObjectId, vertexShaderId);
glAttachShader(programObjectId, fragmentShaderId);
glLinkProgram(programObjectId);
final int[] linkStatus = new int[1];
glGetProgramiv(programObjectId, GL_LINK_STATUS, linkStatus, 0);
/*Log.d(TAG, "Results of linking program: \n"
+ glGetProgramInfoLog(programObjectId));*/
if (linkStatus[0] == 0) {
glDeleteProgram(programObjectId);
Log.w(TAG, "Linking of program failed");
return 0;
}
return programObjectId;
}
public static boolean validateProgram(int programId) {
glValidateProgram(programId);
final int[] validateStatus = new int[1];
glGetProgramiv(programId, GL_VALIDATE_STATUS, validateStatus, 0);
/*Log.d(TAG, "Results of validating program: " + validateStatus[0]
+ "\n Log: " + glGetProgramInfoLog(programId));*/
return validateStatus[0] != 0;
}
}
著色器是 OpenGL 裡面非常重要的概念,這裡我先把程式碼貼上來,然後來講流程。
在 res/raw 資料夾下,我們建立了兩個著色器檔案。
頂點著色器,simple_vertex_shader.glsl
#version 330
layout (location = 0) in vec3 aPos; // 位置變數的屬性位置值為 0
layout (location = 1) in vec3 aColor; // 顏色變數的屬性位置值為 1
out vec3 vColor; // 向片段著色器輸出一個顏色
void main()
{
gl_Position = vec4(aPos.xyz, 1.0);
vColor = aColor; // 將ourColor設定為我們從頂點資料那裡得到的輸入顏色
}
片段著色器, simple_fragment_shader.glsl
#version 330
precision mediump float;
in vec3 vColor;
out vec4 FragColor;
void main()
{
FragColor = vec4(vColor, 1.0);
}
全部的程式碼就只這樣了,具體繪製過程下面來說。執行程式,我們看到效果如下:
三、OpenGL 繪製過程
一張圖說明 OpenGL 渲染過程:
我們看 MyTriangle.java
這個類。
要繪製三角形,我們肯定要定義三角形的頂點座標和顏色。(廢話,不然GPU怎麼知道用什麼顏色繪製在哪裡)。
首先我們定義了一個 float 型陣列:
static float triangleCoords[] = { // in counterclockwise order:
0.0f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // top
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // bottom left
0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f // bottom right
};
注意:這個陣列中,定義了 top, bottom left, bottom right 三個點。每個點包含六個資料,前三個數表示頂點座標,後三個點表示顏色的 RGB 值。
座標系統
可能注意到了,因為我們這裡繪製最簡單的平面二維影象,Z 軸座標都為 0 ,螢幕中的 X, Y 座標點都是在(-1,1)的範圍。我們沒有對視口做任何變換,設定的預設視口,此時的座標系統是以螢幕正中心為座標原點。 螢幕最左為 X 軸 -1 , 螢幕最右為 X 軸 +1。同理,螢幕最下方為 Y 軸 -1, 螢幕最上方為 Y 軸 +1。OpenGL 座標系統使用的是右手座標系,Z 軸正方向為垂直螢幕向外。
3.1 複製資料到本地記憶體
mVertexBuffer = ByteBuffer.allocateDirect(triangleCoords.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
mVertexBuffer.put(triangleCoords);
這一行程式碼,作用是將資料從 java 堆複製到本地堆。我們知道,在 java 虛擬機器記憶體模型中,陣列存在 java 堆中,受 JVM 垃圾回收機制影響,可能會被回收掉。所以我們要將資料複製到本地堆。
首先呼叫 ByteBuffer.allocateDirect()
分配一塊本地記憶體,一個 float 型別的數字佔 4 個位元組,所以分配的記憶體大小為 triangleCoords.length * 4 。
呼叫 order()
指定位元組緩衝區中的排列順序, 傳入 ByteOrder.nativeOrder() 保證作為一個平臺,使用相同的排序順序。
呼叫 asFloatBuffer()
可以得到一個反映底層位元組的 FloatBuffer 類的例項。
最後呼叫 put(triangleCoords)
把資料從 Android 虛擬機器堆記憶體中複製到本地記憶體。
3.2 編譯著色器並連結到程式
接下來,通過 TextResourceReader 工具類,讀取頂點著色器和片段著色器檔案的的內容。
String vertexShaderCode = TextResourceReader.readTextFileFromResource(mContext, R.raw.simple_vertex_shader);
String fragmentShaderCode = TextResourceReader.readTextFileFromResource(mContext, R.raw.simple_fragment_shader);
然後通過 ShaderHelper 工具類編譯著色器。然後連結到程式。
int vertexShader = ShaderHelper.compileVertexShader(vertexShaderCode);
int fragmentShader = ShaderHelper.compileFragmentShader(fragmentShaderCode);
mProgram = ShaderHelper.linkProgram(vertexShader, fragmentShader);
ShaderHelper.validateProgram(mProgram);
著色器
著色器是一個執行在 GPU 上的小程式。著色器的檔案其實定義了變數,並且包含 main 函式。關於著色器的詳細教程,請查閱:(LearnOpenGL CN 中的著色器教程)[https://learnopengl-cn.github.io/01%20Getting%20started/05%20Shaders/]
我這裡記錄一下,著色器的編譯過程:
3.2.1 建立著色器物件
int shaderObjectId = glCreateShader(type);`
建立一個著色器,並返回著色器的控制程式碼(類似java中的引用),如果返回了 0 ,說明建立失敗。GLES 中定義了常量,GL_VERTEX_SHADER
和 GL_FRAGMENT_SHADER
作為引數,分別建立頂點著色器和片段著色器。
3.2.2 編譯著色器
編譯著色器,
glShaderSource(shaderObjectId, shaderCode);
glCompileShader(shaderObjectId);
下面的程式碼,用於獲取編譯著色器的狀態結果。
final int[] compileStatus = new int[1];
glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS, compileStatus, 0);
Log.d(TAG, "Results of compiling source: " + "\n" + shaderCode + "\n: "
+ glGetShaderInfoLog(shaderObjectId));
if (compileStatus[0] == 0) {
glDeleteShader(shaderObjectId);
Log.w(TAG, "Compilation of shader failed.");
return 0;
}
親測上面的程式在我手上真機可以正常執行,在 genymotion 模擬器中執行報瞭如下錯誤:
JNI DETECTED ERROR IN APPLICATION: input is not valid Modified UTF-8: illegal start byte 0xfe
網上搜尋了一下,這個異常是由於Java虛擬機器內部的dalvik/vm/CheckJni.c中的checkUtfString函式丟擲的,並且JVM的這個介面明確是不支援四個位元組的UTF8字元。因此需要在呼叫函式之前,對介面傳入的字串進行過濾,過濾函式,可以上網搜到,這不是本文重點,所以我把這個 log 註釋掉了
Log.d(TAG, "Results of compiling source: " + "\n" + shaderCode + "\n: "
+ glGetShaderInfoLog(shaderObjectId));
3.2.3 將著色器連線到程式
編譯完著色器之後,需要將著色器連線到程式才能使用。
int programObjectId = glCreateProgram();
建立一個 program 物件,並返回控制程式碼,如果返回了 0 ,說明建立失敗。
glAttachShader(programObjectId, vertexShaderId);
glAttachShader(programObjectId, fragmentShaderId);
glLinkProgram(programObjectId);
將頂點著色器個片段著色器連結到 program 物件。下面的程式碼用於獲取連結的狀態結果:
final int[] linkStatus = new int[1];
glGetProgramiv(programObjectId, GL_LINK_STATUS, linkStatus, 0);
/*Log.d(TAG, "Results of linking program: \n"
+ glGetProgramInfoLog(programObjectId));*/
if (linkStatus[0] == 0) {
glDeleteProgram(programObjectId);
Log.w(TAG, "Linking of program failed");
return 0;
}
3.2.4 判斷 program 物件是否有效
在使用 program 物件之前,我們還做了有效性判斷:
glValidateProgram(programId);
final int[] validateStatus = new int[1];
glGetProgramiv(programId, GL_VALIDATE_STATUS, validateStatus, 0);
/*Log.d(TAG, "Results of validating program: " + validateStatus[0]
+ "\n Log: " + glGetProgramInfoLog(programId));*/
如果 validateStatus[0] == 0 , 則無效。
3.3 關聯屬性與頂點資料的陣列
首先呼叫glUseProgram(mProgram)
將 program 物件新增到 OpenGL ES 的繪製環境。
看如下程式碼:
mVertexData.position(0); // 移動指標到 0,表示從開頭開始讀取
// 告訴 OpenGL, 可以在緩衝區中找到 a_Position 對應的資料
int aPos = glGetAttribLocation(mProgram, "aPos");
glVertexAttribPointer(aPos, COORDS_PER_VERTEX, GL_FLOAT, false, (COORDS_PER_VERTEX + COLOR_PER_VERTEX) * 4, mVertexBuffer); // Prepare the triangle coordinate data
glEnableVertexAttribArray(aPos);
int aColor = glGetUniformLocation(mProgram, "aColor");
glVertexAttribPointer(1, COORDS_PER_VERTEX, GL_FLOAT, false, (COORDS_PER_VERTEX + COLOR_PER_VERTEX) * 4, mVertexBuffer); // Prepare the triangle coordinate data
glEnableVertexAttribArray(aColor);
在 OpenGL ES 2.0 中,我們通過如上程式碼,使用資料。呼叫 glGetAttribLocation()
方法,找到頂點和顏色對應的資料位置,第一個引數是 program 物件,第二個引數是著色器中的入參引數名。
然後呼叫 glVertexAttribPointer()
方法
引數如下(圖片擷取自《OpenGL ES應用開發實踐指南Android卷》):
最後呼叫glEnableVertexAttribArray(aPos);
使 OpenGL 能使用這個資料。
但是你發現,我們上面給的程式碼中並沒有呼叫 glGetAttribLocation()
方法尋找位置,這是因為,我使用的 OpenGLES 3.0 ,在 OpenGL ES 3.0 中,著色器程式碼中,新增了 layout(location = 0)
類似的語法支援。
#version 330
layout (location = 0) in vec3 aPos; // 位置變數的屬性位置值為 0
layout (location = 1) in vec3 aColor; // 顏色變數的屬性位置值為 1
out vec3 vColor; // 向片段著色器輸出一個顏色
void main()
{
gl_Position = vec4(aPos.xyz, 1.0);
vColor = aColor; // 將ourColor設定為我們從頂點資料那裡得到的輸入顏色
}
這裡已經指明瞭屬性在頂點陣列中對應的位置,所以在程式碼中,可以直接使用 0 和 1 來表示位置。
3.4 繪製圖形
最後呼叫 glDrawArrays(GL_TRIANGLES, 0, 3)
繪製出一個三角形。
glDrawArrays() 方法第一個引數指定繪製的型別, OpenGLES 中定義了一些常量,通常有 GL_TRIANGLES , GL_POINTS, GL_LINES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN 等等型別,具體每種型別代表的意思可以查閱API 文件。
四、 OpenGL 中的 VAO 和 VBO。
VAO : 頂點陣列物件
VBO :頂點緩衝物件
通過使用 VAO 和 VBO ,可以建立 VAO 與 VBO 的索引對應關係,一次寫入資料之後,每次使用只需要呼叫 glBindVertexArray
方法即可,避免重複進行資料的複製, 大大提高繪製效率。
int[] VBO = new int[2];
int[] VAO = new int[2];
glGenVertexArrays(0, VAO, 0);
glGenBuffers(0, VBO, 0);
glBindVertexArray(VAO[0]);
glBindBuffer(GL_ARRAY_BUFFER, VBO[0]);
glBufferData(GL_ARRAY_BUFFER, triangleCoords.length * 4, mVertexBuffer, GL_STATIC_DRAW);
glVertexAttribPointer(0, COORDS_PER_VERTEX, GL_FLOAT, false, (COORDS_PER_VERTEX + COLOR_PER_VERTEX) * 4, 0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, COORDS_PER_VERTEX, GL_FLOAT, false, (COORDS_PER_VERTEX + COLOR_PER_VERTEX) * 4, COORDS_PER_VERTEX * 4);
glEnableVertexAttribArray(1);
glBindVertexArray(VAO[0]);
glGenVertexArrays(1, VAO, 0);
glGenBuffers(1, VBO, 0);
glBindVertexArray(VAO[1]);
glBindBuffer(GL_ARRAY_BUFFER, VBO[1]);
glBufferData(GL_ARRAY_BUFFER, triangleCoords.length * 4, mVertexBuffer2, GL_STATIC_DRAW);
glVertexAttribPointer(0, COORDS_PER_VERTEX, GL_FLOAT, false, (COORDS_PER_VERTEX + COLOR_PER_VERTEX) * 4, 0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, COORDS_PER_VERTEX, GL_FLOAT, false, (COORDS_PER_VERTEX + COLOR_PER_VERTEX) * 4, COORDS_PER_VERTEX * 4);
glEnableVertexAttribArray(1);
glBindVertexArray(VAO[1]);
glBindVertexArray(VAO[0]);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(VAO[1]);
glDrawArrays(GL_TRIANGLES, 0, 3);