OpenGL ES 入门
写在前面
记录一下 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 Getting started/05 Shaders/]
我这里记录一下,着色器的编译过程:
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(2, VAO, 0);
glGenBuffers(2, 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]);
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);
OpenGL ES 入门的更多相关文章
- OpenGL ES入门09-GLSL实现常见特效 [转]
本文转自简书,原文地址http://www.jianshu.com/p/e4a8c83cd37 本文是关于OpenGL ES的系统性学习过程,记录了自己在学习OpenGL ES时的收获. 这篇文章的目 ...
- OpenGL ES入门详解
http://blog.csdn.net/wangyuchun_799/article/details/7736928 1.决定你要支持的OpenGL ES的版本.目前,OpenGL ES包含1.1 ...
- IOS 中openGL使用教程1(openGL ES 入门篇 | 搭建openGL环境)
OpenGL版本 iOS系统默认支持OpenGl ES1.0.ES2.0以及ES3.0 3个版本,三者之间并不是简单的版本升级,设计理念甚至完全不同,在开发OpenGL项目前,需要根据业务需求选择合适 ...
- Android OpenGL ES 入门系列(二) --- 环境搭建
转载请注明出处 本文出自Hansion的博客 本章介绍如何使用GLSurfaceView和GLSurfaceView.Renderer完成在Activity中的最简单实现. 1.在AndroidMan ...
- Android OpenGL ES 入门系列(一) --- 了解OpenGL ES的前世今生
转载请注明出处 本文出自Hansion的博客 OpenGL ES (OpenGL for Embedded Systems) 是 OpenGL 三维图形 API 的子集,针对手机.PDA和游戏主机等嵌 ...
- iOS开发-OpenGL ES入门教程1
http://www.jianshu.com/p/750fde1d8b6a 这里是一篇新手教程,环境是Xcode7+OpenGL ES 2.0,目标写一个OpenGL ES的hello world.O ...
- IOS 中openGL使用教程2(openGL ES 入门篇 | 绘制一个多边形)
在上一篇我们学习了如何搭建IOS下openGL的开发环境,接下来我们来学习如何绘制一个多边形. 在2.0之前,es的渲染采用的是固定管线,何为固定管线,就是一套固定的模板流程,局部坐标变换 -> ...
- IOS 中openGL使用教程3(openGL ES 入门篇 | 纹理贴图(texture)使用)
在这篇文章中,我们将学习如何在openGL中使用纹理贴图. penGL中纹理可以分为1D,2D和3D纹理,我们在绑定纹理对象的时候需要指定纹理的种类.由于本文将以一张图片为例,因此我们为我们的纹理对象 ...
- IOS 中openGL使用教程4(openGL ES 入门篇 | 离屏渲染)
通常情况下,我们使用openGL将渲染好的图片绘制到屏幕上,但有时候我们不想显示处理结果,这时候就需要使用离屏渲染了. 正常情况下,我们将屏幕,也就是一个CAEAGLLayer对象作为渲染目标,离屏渲 ...
随机推荐
- C语言程序设计100例之(9):生理周期
例9 生理周期 问题描述 人生来就有三个生理周期,分别为体力.感情和智力周期,它们的周期长度为 23 天.28 天和33 天.每一个周期中有一天是高峰.在高峰这天,人会在相应的方面表现出色.例如 ...
- spring cloud 2.x版本 Gateway路由网关教程
前言 本文采用Spring cloud本文为2.1.8RELEASE,version=Greenwich.SR3 本文基于前两篇文章eureka-server.eureka-client.eureka ...
- 2019-6-5-WPF-拼音输入法
原文:2019-6-5-WPF-拼音输入法 title author date CreateTime categories WPF 拼音输入法 lindexi 2019-6-5 17:6:58 +08 ...
- .NET Core CSharp初级篇 1-2 循环与判断
.NET Core CSharp初级篇 1-2 本节内容循环与判断 循环 循环是一个在任何语言都是极为重要的语法,它可以用于很多东西,例如迭代数组等等.在C#中,语法层面的循环有:for , fore ...
- python基础(4):用户交互、if判断、while循环、break和continue
1. 用户交互 使⽤input()函数,可以让我们和计算机互动起来 语法: 内容 = input(提⽰信息) 这⾥可以直接获取到⽤户输入的内容 content = input("你吃了么?& ...
- PlayJava Day004
今日所学: /* 2019.08.19开始学习,此为补档. */ JDK 1.6:byte , int , short , char , enum JDK 1.7:byte , int , short ...
- JAVA----HelloWorld
1.步骤 将java代码编写到扩展名为.java的文件中(扩展名的查看) 新建文本文档,重命名为Test.java. 以记事本方式打开. 写入代码. public class Test{ ...
- Python-警告处理
python 下Warning的使用 起因是这样的,当我使用pymysql模块执行建表的sql语句时获,在控制台输出了红色的消息,但是程序并没有终止而是继续运行了 sql语句如下: CREATE TA ...
- Docker基础概念与安装
Docker是什么? Docker最初是dotCloud公司的创始人Solomon Hyks在法国期间发起的一个公司内部项目,它是基于dotCloud公司多年云服务技术的一次革新,并于2013年3月以 ...
- 定制Dynamics CRM标准导出功能:不能导出指定列的值
关注本人微信和易信公众号: 微软动态CRM专家罗勇 ,回复239或者20161203可方便获取本文,同时可以在第一间得到我发布的最新的博文信息,follow me!我的网站是 www.luoyong. ...