手记

[ - OpenGLES3.0 - ] 第一集 主线 - 打开新世界的大门

问:学OpenGL能干嘛? 答: 为所欲为。

说起OpenGLES,大家可能都敬而远之,其实它并没有想象中的那么可怕,当然也并没有那么容易
都0202年了,本系列使用OpenGLES3.0,这是一次有预谋的计划:


1.黑屏的实现

1.1 GLSurfaceView的使用

Android中OpenGL通过GLSurfaceView进行展现,实现Renderer接口
实现接口方法:onSurfaceCreatedonSurfaceChangedonDrawFrame

public class GLWorld extends GLSurfaceView implements GLSurfaceView.Renderer {
    private static final String TAG = "GLWorld";
    public GLWorld(Context context) {
        this(context,null);
    }

    public GLWorld(Context context, AttributeSet attrs) {
        super(context, attrs);
        setEGLContextClientVersion(3);//设置OpenGL ES 3.0 context
        setRenderer(this);//设置渲染器
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        Log.e(TAG, "onSurfaceCreated: " );
        GLES30.glClearColor(1.0f, 0.0f, 0.0f, 1.0f);//rgba
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        Log.e(TAG, "onSurfaceChanged: " );
        GLES30.glViewport(0, 0, width, height);//设置GL视口
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        Log.e(TAG, "onDrawFrame: " );
    }
}

日志如下: 默认onDrawFrame约隔16ms会不断刷新

2020-01-10 13:34:35.368 3934-4035/com.toly1994.tolygl E/GLWorld: onSurfaceCreated: 
2020-01-10 13:34:35.368 3934-4035/com.toly1994.tolygl E/GLWorld: onSurfaceChanged: 
2020-01-10 13:34:35.368 3934-4035/com.toly1994.tolygl E/GLWorld: onDrawFrame: 
2020-01-10 13:34:35.440 3934-4035/com.toly1994.tolygl E/GLWorld: onDrawFrame: 
2020-01-10 13:34:35.458 3934-4035/com.toly1994.tolygl E/GLWorld: onDrawFrame: 
2020-01-10 13:34:35.461 3934-4035/com.toly1994.tolygl E/GLWorld: onDrawFrame: 

1.2 GLWorld的使用

它就相当于一个View,现在直接放到setContentView中,便可以显示

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(GLWorld(this))
    }
}

2.从点线开始说起

现在你对OpenGLES的认识就像眼前的黑屏一样,一无所知
我们需要去点亮它,展现出一个新世界,大门即将打开,请扶好站稳。
这道光,你扎住了,就已经赢了一大半,抓不住,就…算了吧


2.1 GLPoint的编写

为了不混乱和方便使用,创建一个GLPoint类负责点的绘制测试

[1] 准备顶点着色代码和片段着色代码
[2] 准备顶点和颜色数据
[3] 加载着色器代码并初始化程序
[4] 绘制逻辑 (添加程序->启用顶点->绘制)

public class GLPoint {
    //顶点着色代码
    final String vsh = "#version 300 es\n" +
            "layout (location = 0) in vec3 aPosition; \n" +
            "layout (location = 1) in vec4 aColor;\n" +
            "\n" +
            "out vec4 color2frag;\n" +
            "\n" +
            "void main(){\n" +
            "    gl_Position = vec4(aPosition.x,aPosition.y, aPosition.z, 1.0);\n" +
            "    color2frag = aColor;\n" +
            "gl_PointSize=10.0;"+
            "}";
    
    //片段着色代码
    final String fsh = "#version 300 es\n" +
            "precision mediump float;\n" +
            "out vec4 outColor;\n" +
            "in vec4 color2frag;\n" +
            "\n" +
            "void main(){\n" +
            "    outColor = color2frag;\n" +
            "}";

    //顶点数组
    private final float vertexes[] = {   //以逆时针顺序
            0.0f, 0.0f, 0.0f,//原点
    };
    
    // 颜色数组
    private final float colors[] = new float[]{
            1.0f, 1.0f, 1.0f, 1.0f,//白色
    };

    private int program;
    private static final int VERTEX_DIMENSION = 3;
    private static final int COLOR_DIMENSION = 4;
    
    private FloatBuffer vertBuffer;
    private FloatBuffer colorBuffer;

    private  int aPosition =0;//位置的句柄
    private  int aColor =1;//颜色的句柄

    public GLPoint() {
        program = initProgram();
        vertBuffer= GLBuffer.getFloatBuffer(vertexes);
        colorBuffer= GLBuffer.getFloatBuffer(colors);
    }

    private int initProgram() {
        int program;
        ////顶点shader代码加载
        int vertexShader = GLLoader.loadShader(GLES30.GL_VERTEX_SHADER, vsh);
        //片段shader代码加载
        int fragmentShader = GLLoader.loadShader(GLES30.GL_FRAGMENT_SHADER, fsh);
        program = GLES30.glCreateProgram();//创建空的OpenGL ES 程序
        GLES30.glAttachShader(program, vertexShader);//加入顶点着色器
        GLES30.glAttachShader(program, fragmentShader);//加入片元着色器
        GLES30.glLinkProgram(program);//创建可执行的OpenGL ES项目
        return program;
    }

    public void draw(){
        //清除颜色缓存和深度缓存
        GLES30.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
        // 将程序添加到OpenGL ES环境中
        GLES30.glUseProgram(program);
        //启用顶点句柄
        GLES30.glEnableVertexAttribArray(aPosition);
        //启用颜色句柄
        GLES30.glEnableVertexAttribArray(aColor);
        //准备坐标数据
        GLES30.glVertexAttribPointer(
                aPosition, VERTEX_DIMENSION,
                GLES30.GL_FLOAT, false,
                VERTEX_DIMENSION * 4, vertBuffer);

        //准备颜色数据
        GLES30.glVertexAttribPointer(
                aColor, COLOR_DIMENSION,
                GLES30.GL_FLOAT, false,
                COLOR_DIMENSION * 4, colorBuffer);

        //绘制点
        GLES30.glDrawArrays(GLES30.GL_POINTS, 0, vertexes.length / VERTEX_DIMENSION);
        
        //禁用顶点数组
        GLES30.glDisableVertexAttribArray(aPosition);
        GLES30.glDisableVertexAttribArray(aColor);
    }

}

2.2 缓冲数据

float数组需要通过FloatBuffer进行缓冲,以提高效率

/**
 * float数组缓冲数据
 *
 * @param vertexs 顶点
 * @return 获取浮点形缓冲数据
 */
public static FloatBuffer getFloatBuffer(float[] vertexs) {
    FloatBuffer buffer;
    ///每个浮点数:坐标个数* 4字节
    ByteBuffer qbb = ByteBuffer.allocateDirect(vertexs.length * 4);
    //使用本机硬件设备的字节顺序
    qbb.order(ByteOrder.nativeOrder());
    // 从字节缓冲区创建浮点缓冲区
    buffer = qbb.asFloatBuffer();
    // 将坐标添加到FloatBuffer
    buffer.put(vertexs);
    //设置缓冲区以读取第一个坐标
    buffer.position(0);
    return buffer;
}


2.3 使用

在我们的GLWorld中创建GLPoint对象,在onDrawFrame中绘制即可

public class GLWorld extends GLSurfaceView implements GLSurfaceView.Renderer {
    private static final String TAG = "GLWorld";
    private GLPoint glPoint;
    public GLWorld(Context context) {
        this(context,null);
    }
    
    public GLWorld(Context context, AttributeSet attrs) {
        super(context, attrs);
        setEGLContextClientVersion(3);//设置OpenGL ES 3.0 context
        setRenderer(this);//设置渲染器
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
         glPoint = new GLPoint();
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        GLES30.glViewport(0, 0, width, height);//设置GL视口
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        glPoint.draw();
    }
}

2.4 多点绘制

现在绘制点,通过更改顶点和颜色即可

    //顶点数组
    private final float vertexes[] = {   //以逆时针顺序
            0.0f, 0.0f,0.0f,//原点
            0.5f,0.0f,0.0f,
            0.5f,0.5f,0.0f,
            0.0f,0.5f,0.0f,

    };

    // 颜色数组
    private final float colors[] = new float[]{
            1.0f, 1.0f, 1.0f, 1.0f,//白色
            1.0f, 0.0f, 0.0f, 1.0f,//红色
            0.0f, 1.0f, 0.0f, 1.0f,//绿色
            0.0f, 0.0f, 1.0f, 1.0f,//蓝色
    };

2.5 画线

前面处理好了,划线比较简单

//绘制
GLES30.glLineWidth(10);
//GLES30.glDrawArrays(GLES30.GL_POINTS, 0, vertexes.length / VERTEX_DIMENSION);
//GLES30.glDrawArrays(GLES30.GL_LINES, 0, vertexes.length / VERTEX_DIMENSION);
//GLES30.glDrawArrays(GLES30.GL_LINE_LOOP, 0, vertexes.length / VERTEX_DIMENSION);
GLES30.glDrawArrays(GLES30.GL_LINE_STRIP, 0, vertexes.length / VERTEX_DIMENSION);

注:着色器的代码会有单独一篇进行讲解,此处暂不做解释。


3.线绘制与比例校正

可能你已经发现了,本应是个正方形,可现实成了矩形
原因在于视口的比例不对,现在是这样个坐标系:


3.1 GLLine添加顶点变换矩阵

在顶点着色器代码中添加用于变换的矩阵uMVPMatrix

//顶点着色代码
final String vsh = "#version 300 es\n" +
        "layout (location = 0) in vec3 aPosition; \n" +
        "layout (location = 1) in vec4 aColor;\n" +
        "uniform mat4 uMVPMatrix;\n" +
        "out vec4 color2frag;\n" +
        "\n" +
        "void main(){\n" +
        "    gl_Position = uMVPMatrix*vec4(aPosition.x,aPosition.y, aPosition.z, 1.0);\n" +
        "    color2frag = aColor;\n" +
        "gl_PointSize=10.0;"+
        "}";

---->[变换矩阵相关代码]----
private  int uMVPMatrix ;//顶点变换矩阵句柄

//构造方法中获取句柄
uMVPMatrix = GLES30.glGetUniformLocation(program, "uMVPMatrix");

public void draw(float[] mvpMatrix){
   //英雄所见
   GLES30.glUseProgram(program);
   GLES30.glUniformMatrix4fv(uMVPMatrix, 1, false,mvpMatrix, 0);

3.2 GLWorld添加变换矩阵逻辑

具体的变换细节,将在第六集第七集讲述,此处不做解释 现在视角就已经校正了

public class GLWorld extends GLSurfaceView implements GLSurfaceView.Renderer {
    private static final String TAG = "GLWorld";
    private GLLine line;
    //Model View Projection Matrix--模型视图投影矩阵
    private final float[] mMVPMatrix = new float[16];
    //投影矩阵 mProjectionMatrix
    private final float[] mProjectionMatrix = new float[16];
    //视图矩阵 mViewMatrix
    private final float[] mViewMatrix = new float[16];
    
    public GLWorld(Context context) {
        this(context, null);
    }
    public GLWorld(Context context, AttributeSet attrs) {
        super(context, attrs);
        setEGLContextClientVersion(3);//设置OpenGL ES 3.0 context
        setRenderer(this);//设置渲染器
    }
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        line = new GLLine();
    }
    
    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        GLES30.glViewport(0, 0, width, height);//设置GL视口
        float ratio = (float) width / height;
        //透视投影矩阵--截锥
        Matrix.frustumM(mProjectionMatrix, 0,
                -ratio, ratio, -1, 1,
                3, 7);
        // 设置相机位置(视图矩阵)
        Matrix.setLookAtM(mViewMatrix, 0,
                0, 0, 4,
                0f, 0f, 0f,
                0f, 1.0f, 0.0f);
    }
    
    @Override
    public void onDrawFrame(GL10 gl) {
        Matrix.multiplyMM(
                mMVPMatrix, 0,
                mProjectionMatrix, 0,
                mViewMatrix, 0);
        line.draw(mMVPMatrix);
    }
}

4.精简和优化

4.1 着色器shader的独立文件

着色器shader是OpenGL灵魂般的存在,所以直接写在代码里肯定不太好
一般放在assets文件夹里,另外值得一提的是AS的着色器代码高亮显示插件
个人习惯片段用.fsh的后缀名,顶点用.vsh的后缀名


4.2 读取资源文件并加载程序方法

这些通用的不变的操作可以提取出来进行复用

public static  int initProgramByAssets(Context ctx, String vertName, String fragName){
    int program=-1;
    ////顶点着色
    int vertexShader = loadShaderAssets(ctx, GLES30.GL_VERTEX_SHADER, vertName);
    //片元着色
    int fragmentShader = loadShaderAssets(ctx, GLES30.GL_FRAGMENT_SHADER, fragName);
    program = GLES30.glCreateProgram();//创建空的OpenGL ES 程序
    GLES30.glAttachShader(program, vertexShader);//加入顶点着色器
    GLES30.glAttachShader(program, fragmentShader);//加入片元着色器
    GLES30.glLinkProgram(program);//创建可执行的OpenGL ES项目
    return program;
}
//从sh脚本中加载shader内容的方法
private static int loadShaderAssets(Context ctx, int type, String name) {
    byte[] buf = new byte[1024];
    StringBuilder sb = new StringBuilder();
    int len;
    try (InputStream is = ctx.getAssets().open(name)) {
        while ((len = is.read(buf)) != -1) {
            sb.append(new String(buf, 0, len));
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return loadShader(type, sb.toString());
}

使用时直接加载即可,这样GLLine的内容就比较精简了。

program = GLLoader.initProgramByAssets(context, "base.vsh", "base.fsh");

这样也能正常表现,以后对着色器代码的修改就是家常便饭。

本篇为你介绍了OpenGLES的基础使用,旨在为你打开一扇OpenGLES的大门,其中很多细节一言蔽之,后面会一一道来。所以这一篇可以不求甚解,跑出来就算你成功了。下一篇将会为你介绍面的绘制和贴图,这是OpenGLES和图片关联起来的重要部分。

1人推荐
随时随地看视频
慕课网APP