OpenGL ES 绘制一个三角形(2)

OpenGL ES 绘制一个三角形(2)

简述

本节我们基于Android系统,使用OpenGL ES来实现绘制一个三角形。在OpenGL ES里,三角形是一个基础图形,其他的图形都可以使用三角形拼接而成,所以我们就的案例就基于这个开始。

在Android系统中,提供给上层应用的View都是通过Canvas的接口来绘制,虽然底层最终也是通过OpenGL ES来实现的,但是由于上层被封装了,我们无法通过这个来实现我们想要实现的demo,我们需要使用GLSurfaceView来实现。

GLSurfaceView继承自SurfaceView,我们知道SurfaceView和一般的View不同,会有自己的Surface,而GLSurfaceView则在SurfaceView的基础上,会初始化EGL的上下文环境。其实我们直接使用SurfaceView也是可以使用OpenGL ES的,只不过GLSurfaceView给我们提供了一些生命周期管理的辅助,在大多数场景使用起来更加方便。

GLSurfaceView提供的是EGL环境,我们想要绘制一个三角形所需要做的事如下:

  • 创建一个GLSurfaceView
  • 配置EGL(其实GLSurfaceView帮助我们做了大多数的事)
  • 使用OpenGL ES接口绘制图像
    • 配置顶点缓冲区
    • 实现顶点着色器和片段着色器
    • 调用drawCall
  • 交换缓冲区呈现图像

本节主要是实现demo,对OpenGL渲染大体流程有个感知,一些api的细节可以不需要关注,后续会对每个点会有更详细的介绍。

绘制一个三角形

配置OpenGL ES

在AndroidManifeast.xml里配置

主要就是配置一条

其中glEsVersion是版,我们这里用OpenGL ES 3.0来写demo。

复制代码
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
    <uses-feature android:glEsVersion="0x00030000" android:required="true"/>
    <application>
    // ...
    </activity>
</application>

自定义GLSurfaceView

GLSurfaceView通过setRenderer暴露一个Renderer,Renderer有三个接口onSurfaceCreated/onSurfaceChanged/onDrawFrame。

GLSurfaceView处理了EGL环境相关的逻辑,onDrawFrame则会控制VSync,在需要渲染的时候调用。

onSurfaceChanged是Surface变化的情况下会调用,而onSurfaceCreated则是Surface创建时回调,onDrawFrame和我们自定义View时候的onDraw有一些类似。

复制代码
public class DemoGLSurfaceView extends GLSurfaceView {
    public DemoGLSurfaceView(Context context) {
        super(context);
        init();
    }

    public DemoGLSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public void init() {
        // 设置版本
        setEGLContextClientVersion(3);
        Renderer renderer = new Renderer() {
            @Override
            public void onSurfaceCreated(GL10 gl, EGLConfig config) {
                // ...
            }

            @Override
            public void onSurfaceChanged(GL10 gl, int width, int height) {
                // 设定视口,类似相机,相机移动则渲染的图像相对位置变化。
                GLES30.glViewport(0, 0, width, height);
            }

            @Override
            public void onDrawFrame(GL10 gl) {
                // ...
            }
        };
        setRenderer(renderer);
    }
}

配置顶点缓冲区数据

由于我们只是要画一个固定的三角形,顶点缓冲区里的数据都是固定的,所以我们在onSurfaceCreated填充,只需要一次即可。

glGenBuffers是创建一个顶点缓冲区Buffer,第二个参数是一个int数组,创建的顶点缓冲区id会通过这个数组返回,后续使用这个id来使用这个buffer。

我们需要先调用glBindBuffer绑定buffer,然后再通过glBufferData将数据传到缓冲区中。

GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0)则是用来清除Buffer的绑定操作的,OpenGL的接口设计像是一个状态机,bind上一个Buffer才能对这个Buffer进行操作,如果需要操作其他Buffer则需要bind其他Buffer。

vertexArray有三个节点,是三个顶点的x,y,z坐标。OpenGL的坐标系是x,y,z都是(-1,1)。

复制代码
private float[] vertexArray = new float[] {
        -0.5f, -0.5f, 0.0f,
        0.5f, -0.5f, 0.0f,
        0.0f, 0.5f, 0.0f
};

@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
    // 清除背景颜色
    GLES30.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    // 创建顶点缓冲区
    int[] idBuffer = new int[1];
    GLES30.glGenBuffers(1, idBuffer, 0);
    vertexBufferId = idBuffer[0];

    // 将数据转化成ByteBuffer
    FloatBuffer vertexBuffer = ByteBuffer.allocateDirect(vertexArray.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
    vertexBuffer.put(vertexArray);
    vertexBuffer.position(0);

    GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vertexBufferId);
    // 顶点缓冲区数据填充
    GLES30.glBufferData(
            GLES30.GL_ARRAY_BUFFER,
            vertexArray.length * 4,
            vertexBuffer,
            GLES30.GL_STATIC_DRAW
    );
    GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0);

    // 初始化shader
    shaderProgramId = initShaderProgram(vertexShaderCode, fragmentShaderCode);
}

配置着色器

着色器是一段给GPU执行的程序,所以其实就是一段代码,我们需要调用对应接口来编译链接。

GLES30.glCreateShader创建一个Shader,参数表示着色器的类型,GL_VERTEX_SHADER为顶点着色器,GL_FRAGMENT_SHADER为片段着色器。

vertexShaderCode字符串是我们配置的顶点着色器,gl_Position是出参,这里是直接做了透传。

fragmentShaderCode是片段着色器,gl_FragColor是出参,是颜色,而uniform vec4 vColor是统一变量,我们设置统一变量直接作为片段着色器的出参。

通过api编译连接后,我们会将它关联到一个Program上,initShaderProgram返回到就是program到id。

复制代码
private final String vertexShaderCode =
        "attribute vec4 vPosition;" +
                "void main() {" +
                "  gl_Position = vPosition;" +
                "}";


private final String fragmentShaderCode =
        "precision mediump float;" +
                "uniform vec4 vColor;" +
                "void main() {" +
                "  gl_FragColor = vColor;" +
                "}";

private int initShaderProgram(String vertexShaderCode, String fragmentShaderCode) {
    // 编译顶点着色器
    int vertexShader = GLES30.glCreateShader(GLES30.GL_VERTEX_SHADER);
    GLES30.glShaderSource(vertexShader, vertexShaderCode);
    GLES30.glCompileShader(vertexShader);

    // 编译片段着色器
    int fragmentShader = GLES30.glCreateShader(GLES30.GL_FRAGMENT_SHADER);
    GLES30.glShaderSource(fragmentShader, fragmentShaderCode);
    GLES30.glCompileShader(fragmentShader);

    // 链接着色器
    int program = GLES30.glCreateProgram();
    GLES30.glAttachShader(program, vertexShader);
    GLES30.glAttachShader(program, fragmentShader);
    GLES30.glLinkProgram(program);
    return program;
}

@Override
public void onDrawFrame(GL10 gl) {
    // ...
    // 使用编译好的着色器
    GLES30.glUseProgram(shaderProgramId);
    // ...
}

配置顶点布局/渲染

首先我们需要调用glClear清空屏幕,glUseProgram配置着色器程序,glBindBuffer绑定之前填充的Buffer。

属性需要通过glEnableVertexAttribArray使能才可使用,我们这里需要使能vPosition属性。

后续会使用glVertexAttribPointer告诉GPU顶点缓冲区布局情况,顶点缓冲区本质就是一段内存,不过没有glVertexAttribPointer,GPU并不知道怎么使用这个数据。

后面配置vColor作为颜色,(1,1,1,1)分别为RGBA,白色。

最后调用glDrawArrays来渲染三角形。

复制代码
@Override
public void onDrawFrame(GL10 gl) {
    // 清除屏幕
    GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
    // 使能着色器程序
    GLES30.glUseProgram(shaderProgramId);

    // 绑定Buffer
    GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vertexBufferId);
    // 获取vPosition属性
    int positionLocation = GLES30.glGetAttribLocation(shaderProgramId, "vPosition");
    // 属性需要使能才可使用
    GLES30.glEnableVertexAttribArray(positionLocation);
    // 告诉GPU顶点缓冲区的布局情况,即那些数据的意义是什么。
    // 这是CPU向GPU传数据的一种方式,我们这里是告诉GPU,我们前面bind的顶点缓冲区是什么数据。
    // 第一个参数是attr的id,第二个参数表示每一个顶点有几个数,第三个参数为数据类型,第四个是参数是否需要归一化
    // 第五个参数是步长,表示每个顶点占用了多少字节,0表示顶点都是紧凑的,GPU会通过计算来计算步长,最后一个参数表示offset。
    GLES30.glVertexAttribPointer(positionLocation, 3, GLES30.GL_FLOAT, false, 0, 0);

    // 配置统一变量,用于CPU和GPU通信的
    int colorLocation = GLES30.glGetUniformLocation(shaderProgramId, "vColor");
    GLES30.glUniform4f(colorLocation, 1.0f, 1.0f, 1.0f, 1.0f);

    // 调用DrawCall绘制三角形
    GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, 3);

    // 清除配置
    GLES30.glDisableVertexAttribArray(positionLocation);
    GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0);
    GLES30.glUseProgram(0);
}

效果

三角形之所以不是正三角形是因为屏幕是长方形的。

小结

本节通过OpenGL ES实现了一个三角形的渲染,对于每个接口使用只做了一个简单的介绍,想必首次学习OpenGL的同学会有很多疑问,比如怎么渲染多个目标,怎么实现渐变颜色等,我们的后续会对每一个点做更细节的学习,这一节主要是了解一下OpenGL的总体渲染流程,大概知道OpenGL接口是怎么工作的即可,后续的介绍也会基于本章的demo。

相关推荐
雨白4 小时前
Jetpack系列(二):Lifecycle与LiveData结合,打造响应式UI
android·android jetpack
kk爱闹5 小时前
【挑战14天学完python和pytorch】- day01
android·pytorch·python
每次的天空7 小时前
Android-自定义View的实战学习总结
android·学习·kotlin·音视频
恋猫de小郭7 小时前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
断剑重铸之日8 小时前
Android自定义相机开发(类似OCR扫描相机)
android
随心最为安8 小时前
Android Library Maven 发布完整流程指南
android
岁月玲珑8 小时前
【使用Android Studio调试手机app时候手机老掉线问题】
android·ide·android studio
还鮟13 小时前
CTF Web的数组巧用
android
小蜜蜂嗡嗡14 小时前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
aqi0014 小时前
FFmpeg开发笔记(七十一)使用国产的QPlayer2实现双播放器观看视频
android·ffmpeg·音视频·流媒体