OpenGL绘制不同形状需要先掌握OpenGL的坐标系,以及要清除shader的使用,这里我会以绘制三角形、四边形、圆形为例,来介绍OpenGL对形状的绘制

首先我们来了解一下,绘制形状的基本步骤:

  • 创建shader(着色器:顶点或片元)
  • 加载shader源码并编译shader
  • 检查是否编译成功
  • 创建一个渲染程序
  • 将着色器程序添加到渲染程序中
  • 链接源程序
  • 检查链接源程序是否成功
  • 得到着色器中的属性
  • 使用源程序
  • 使顶点属性数组有效
  • 为顶点属性赋值
  • 绘制图形

知道了基本步骤,我们就可以分步来实现。先看下效果:

1.基础知识

1.1 OpenGL着色器

OpenGL着色器Shader由glsl语言编写,我们可以通过在Java中传入参数来改变openGL的绘制细节。

以下为顶点着色器:

attribute vec4 vPosition; //4个方向变量的位置向量(xyzw)
attribute vec2 fPosition; //2个方向变量的位置向量(xy)

void main(){
    //赋值,gl_Position为固定写法
    gl_Position = vPosition;
}

以下为片元着色器:

precision mediump float; //中等精度
uniform vec4 f_Color; //4个变量的颜色向量(argb),可以从java层用uniform传入

void main() {
    //赋值,gl_FragColor为固定的写法
    gl_FragColor = f_Color;
}

1.2 OpenGL坐标系

如上所说,OpenGL着色器有顶点着色器、片元着色器,而它们对应了OpenGL的顶点坐标系、片元坐标系,绘制形状时需要了解OpenGL的坐标系是怎样的,其坐标系如下图所示:

从图中可以看出,OpenGL的顶点坐标系为归1化了的坐标系,也就是说不管显示的屏幕的大小是多大或多小,OpenGL都把屏幕的大小整成了-1到1之间;片元坐标系为0到1之间。

1.3 顶点内存数组和片元内存数组

首先,什么是顶点内存数组和片元内存数组,顶点内存数组和片元内存数组就是我们从java层传入的,如:

//顶点坐标
        float[] vertexData = {
                -1f, -1f,
                1f, -1f,
                -1f, 1f,
                1f, 1f
        };
		//纹理坐标
        float[] fragmentData = {
                0f, 1f,
                1f, 1f,
                0f, 0f,
                1f, 0f
        };

其次,怎么加载顶点内存数组和片元内存数组里的坐标,如下:

//读取顶点坐标
        FloatBuffer  vertexBuffer = ByteBuffer.allocateDirect(vertexData.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer().put(vertexData);
        vertexBuffer.position(0);

        //读取纹理坐标
        FloatBuffer  fragmentBuffer = ByteBuffer.allocateDirect(fragmentData.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer().put(fragmentData);
        fragmentBuffer.position(0);

然而,为什么要用allocateDirect,将顶点、片元的坐标存入jvm之外?

这样做,我的理解是在每次的绘制中,OpenGL都需要拿到顶点、片元的坐标,保存在JVM之外可以防止数据被java GC掉。

最后,怎么使用顶点坐标、片元坐标,如下伪代码:

//onSurfaceCreated中加载:

		//加载顶点着色器 shader
        String vertexSource = YShaderUtil.getRawResource(mContext, R.raw.screen_vert);
        //加载片元着色器 shader
        String fragmentSource = YShaderUtil.getRawResource(mContext, R.raw.screen_frag);
        //获取源程序
        program = YShaderUtil.createProgram(vertexSource, fragmentSource);
        //从渲染程序中得到着顶点色器中的属性
        vPosition = GLES20.glGetAttribLocation(program, "vPosition");
        //从渲染程序中得到片元着色器中的属性
        fPosition = GLES20.glGetAttribLocation(program, "fPosition");

//onDrawFrame中使能:

 		//使能顶点属性数组,使之有效
        GLES20.glEnableVertexAttribArray(vPosition);
        //使能之后,为顶点属性赋值,绑定顶点坐标
        GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 8, vertexBuffer);
        //使能片元属性数组,使之有效
        GLES20.glEnableVertexAttribArray(fPosition);
        //使能之后,为片元属性赋值,绑定纹理坐标
        GLES20.glVertexAttribPointer(fPosition, 2, GLES20.GL_FLOAT, false, 8, fragmentBuffer);

其中 vPosition、fPosition是从源程序(shader经过加载、编译、链接得到的源程序)中获取的,绑定之后就可以进行相应的绘制了。

1.4 Shader的加载

Shader需要编译成源程序后才可以获取到其中的变量值,然后才能被使能。以下是以代码加注释的方式对Shader的加载进行讲解:

/**
     * 获取 着色器 源程序
     * @param vertexSource 顶点着色器 内容 
     * @param fragmentSource 片元着色器 内容
     * @return 着色器 源程序
     */
    public static int createProgram(String vertexSource, String fragmentSource) {
        int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
        int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);

        if (vertexShader != 0 && fragmentShader != 0) {
            //创建一个渲染程序 program
            int program = GLES20.glCreateProgram();
            //将顶点着色器程序添加到渲染程序中
            GLES20.glAttachShader(program, vertexShader);
            //将片元着色器程序也添加到渲染程序中
            GLES20.glAttachShader(program, fragmentShader);
            //链接源程序
            GLES20.glLinkProgram(program);

            return program;
        }
        return 0;
    }

1.5 Shader源程序的使用–绘制

//使用着色器源程序
        GLES20.glUseProgram(program);
        //绘制绿色
        GLES20.glUniform4f(fPosition, 1f, 0f, 1f, 1f);
        //使能顶点属性数组,使之有效
        GLES20.glEnableVertexAttribArray(vPosition);
        //使能之后,为顶点属性赋值,绑定顶点坐标
        GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 8, vertexBuffer);
        //绘制图形
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 3);//三角形,三个点

2.OpenGL绘制三角形

package com.york.media.opengl.demo.shape;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLES20;
import android.opengl.GLUtils;

import com.york.media.opengl.R;
import com.york.media.opengl.egl.YGLSurfaceView;
import com.york.media.opengl.egl.YShaderUtil;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

/**
 * author : York
 * date   : 2020/12/20 15:01
 * desc   : 绘制三角形的 Render
 */
public class YTriangleRender implements YGLSurfaceView.YGLRender {

    private final Context mContext;
    private FloatBuffer vertexBuffer;
    private int program;
    private int vPosition;
    private int fPosition;

    public YTriangleRender(Context mContext) {
        this.mContext = mContext;

        //顶点坐标
        float[] vertexData = {
                0f, 1f,
                -1f, -1f,
                1f, -1f
        };
        //读取顶点坐标
        vertexBuffer = ByteBuffer.allocateDirect(vertexData.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer().put(vertexData);
        vertexBuffer.position(0);

    }

    @Override
    public void onSurfaceCreated() {
        //加载顶点着色器 shader
        String vertexSource = YShaderUtil.getRawResource(mContext, R.raw.screen_vert);
        //加载片元着色器 shader
        String fragmentSource = YShaderUtil.getRawResource(mContext, R.raw.screen_frag_color);
        //获取源程序
        program = YShaderUtil.createProgram(vertexSource, fragmentSource);
        //从渲染程序中得到着顶点色器中的属性
        vPosition = GLES20.glGetAttribLocation(program, "vPosition");
        //从渲染程序中得到片元着色器中的属性
        fPosition = GLES20.glGetUniformLocation(program, "f_Color");
    }

    @Override
    public void onSurfaceChanged(int width, int height) {
        //设置窗口大小
        GLES20.glViewport(0, 0, width, height);
    }

    @Override
    public void onDrawFrame() {
        //清除屏幕,此处用的是红色
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
        GLES20.glClearColor(0f, 1f, 0f, 1f);
        //使用着色器源程序
        GLES20.glUseProgram(program);
        //绘制绿色
        GLES20.glUniform4f(fPosition, 1f, 0f, 1f, 1f);
        //使能顶点属性数组,使之有效
        GLES20.glEnableVertexAttribArray(vPosition);
        //使能之后,为顶点属性赋值,绑定顶点坐标
        GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 8, vertexBuffer);
        //绘制图形
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 3);//三角形

    }
}

3.OpenGL绘制四边形

package com.york.media.opengl.demo.shape;

import android.content.Context;
import android.opengl.GLES20;

import com.york.media.opengl.R;
import com.york.media.opengl.egl.YGLSurfaceView;
import com.york.media.opengl.egl.YShaderUtil;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

/**
 * author : York
 * date   : 2020/12/20 15:43
 * desc   : 绘制四边形的 Render
 */
public class YQuadrilateralRender implements YGLSurfaceView.YGLRender {

    private final Context mContext;
    private final FloatBuffer vertexBuffer;
    private int program;
    private int vPosition;
    private int fPosition;

    public YQuadrilateralRender(Context mContext) {
        this.mContext = mContext;
        //顶点坐标
        float[] vertexData = {
                -1f, 0f,
                0f, 1f,
                0f,-1f,
                1f, 0f
        };
        //读取顶点坐标
        vertexBuffer = ByteBuffer.allocateDirect(vertexData.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer().put(vertexData);
        vertexBuffer.position(0);
    }

    @Override
    public void onSurfaceCreated() {
        //加载顶点着色器 shader
        String vertexSource = YShaderUtil.getRawResource(mContext, R.raw.screen_vert);
        //加载片元着色器 shader
        String fragmentSource = YShaderUtil.getRawResource(mContext, R.raw.screen_frag_color);
        //获取源程序
        program = YShaderUtil.createProgram(vertexSource, fragmentSource);
        //从渲染程序中得到着顶点色器中的属性
        vPosition = GLES20.glGetAttribLocation(program, "vPosition");
        //从渲染程序中得到片元着色器中的属性
        fPosition = GLES20.glGetUniformLocation(program, "f_Color");
    }

    @Override
    public void onSurfaceChanged(int width, int height) {
        //设置窗口大小
        GLES20.glViewport(0, 0, width, height);
    }

    @Override
    public void onDrawFrame() {
        //清除屏幕,此处用的是红色
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
        GLES20.glClearColor(1f,0f, 0f, 1f);
        //使用着色器源程序
        GLES20.glUseProgram(program);
        //绘制绿色
        GLES20.glUniform4f(fPosition,0f,1f,1f,1f);
        //使能顶点属性数组,使之有效
        GLES20.glEnableVertexAttribArray(vPosition);
        //使能之后,为顶点属性赋值,绑定顶点坐标
        GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 8, vertexBuffer);
        //绘制图形
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);//四边形

    }
}

3.OpenGL绘制圆形

package com.york.media.opengl.demo.shape;

import android.content.Context;
import android.opengl.GLES20;

import com.york.media.opengl.R;
import com.york.media.opengl.egl.YGLSurfaceView;
import com.york.media.opengl.egl.YShaderUtil;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

/**
 * author : York
 * date   : 2020/12/20 16:33
 * desc   : 绘制圆形
 */
public class YCircularRender implements YGLSurfaceView.YGLRender {
    private final Context mContext;
    private final FloatBuffer vertexBuffer;
    private int program;
    private int vPosition;
    private int fPosition;

    public YCircularRender(Context mContext) {
        this.mContext = mContext;
        //顶点坐标
        float[] vertexData = new float[720];
        for (int i = 0; i < 720; i += 2) {
            // x value
            vertexData[i] = (float) Math.cos(i);
            // y value
            vertexData[i + 1] = (float) Math.sin(i);
        }
        //读取顶点坐标
        vertexBuffer = ByteBuffer.allocateDirect(vertexData.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer().put(vertexData);
        vertexBuffer.position(0);

    }

    @Override
    public void onSurfaceCreated() {
        //加载顶点着色器 shader
        String vertexSource = YShaderUtil.getRawResource(mContext, R.raw.screen_vert);
        //加载片元着色器 shader
        String fragmentSource = YShaderUtil.getRawResource(mContext, R.raw.screen_frag_color);
        //获取源程序
        program = YShaderUtil.createProgram(vertexSource, fragmentSource);
        //从渲染程序中得到着顶点色器中的属性
        vPosition = GLES20.glGetAttribLocation(program, "vPosition");
        //从渲染程序中得到片元着色器中的属性
        fPosition = GLES20.glGetUniformLocation(program, "f_Color");
    }

    @Override
    public void onSurfaceChanged(int width, int height) {
        //设置窗口大小
        GLES20.glViewport(0, 0, width, height);
    }

    @Override
    public void onDrawFrame() {
        //清除屏幕,此处用的是红色
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
        GLES20.glClearColor(0f, 0f, 1f, 1f);
        //使用着色器源程序
        GLES20.glUseProgram(program);
        //绘制绿色
        GLES20.glUniform4f(fPosition, .5f, .5f, 1f, 1f);
        //使能顶点属性数组,使之有效
        GLES20.glEnableVertexAttribArray(vPosition);
        //使能之后,为顶点属性赋值,绑定顶点坐标
        GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 0, vertexBuffer);
        //绘制图形
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 360);//圆形

    }
}

效果是这样的: