OpenGL ES 入门

我只是一个虾纸丫 提交于 2020-11-24 13:36:12

写在前面

记录一下 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_SHADERGL_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);

原文出处:https://www.cnblogs.com/joy99/p/11048881.html

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!