OpenGL ES 3.0 | 围绕HelloTriangle实战案例 展开 渲染流程分析

时间:2022-07-23
本文章向大家介绍OpenGL ES 3.0 | 围绕HelloTriangle实战案例 展开 渲染流程分析,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

案例运行(绘制一个三角形)的基本步骤

【可以先看看文末的代码,结合文章内容去看, 理解了整个流程之后再来看这个步骤,会容易很多】

  • 用EGL创建屏幕上的渲染表面(Android直接用一个GLSurfaceView
  • 加载顶点、片段着色器
  • 创建一个程序对象, 连接顶点、片段着色器, 并链接程序对象;
  • 设置视口;
  • 清除颜色缓冲区;
  • 渲染简单图元
  • 使颜色缓冲区的内容在EGL窗口表面(GLSurfaceView)中可见

着色器

  • 在OpenGL ES 3.0中, 除非加载有效的顶点和片段着色器,否则不会绘制任何几何形状;
  • OpenGL ES 3.0程序必须至少有 一个顶点着色器 和 一个片段着色器;
  • 着色器示例代码:
String vShaderStr =
         "#version 300 es             n"
         +   "in vec4 vPosition;           n"
         + "void main()                  n"
         + "{                            n"
         + "   gl_Position = vPosition;  n"
         + "}                            n";

顶点着色器

  • 第一行: 声明使用的着色器版本, #version 300 es 表示 OpenGL ES着色语言V3.00;
  • 这个顶点着色器声明一个输入属性数组——一个名为vPosition的4分量向量; Hello Triangle中的 Draw函数 将传入 要放在这个变量中的 每个顶点的位置。`
  • 着色器从它生命的main函数开始执行;
  • 实例着色器代码主题简单, vPosition输入属性 拷贝到 gl_Position的 特殊输出变量上; 每个顶点着色器 必须在 gl_Position变量中输出一个位置这个变量 定义 传递到管线下一个阶段的 `位置;

片段着色器

String fShaderStr =
         "#version 300 es                               n"
         + "precision mediump float;                        n"
         + "out vec4 fragColor;                         n"
         + "void main()                                  n"
         + "{                                            n"
         + "  fragColor = vec4 ( 0.0, 0.8, 1.0, 1.0 );  n"
         + "}                                            n";      
  • 第一行同顶点着色器(#version 300 es);
  • precision mediump float;声明 着色器中 浮点变量默认精度
  • 片段着色器 声明 一个输出变量fragColor,这是一个4分量的向量, 写入这个变量的值 将被 输出到 颜色缓冲区
  • 一般, 游戏或者应用程序不会像这个例子一样内嵌着色器源字符串 实际开发中, 着色器从某种文本或者数据文件中加载,然后加载到API。

编译和加载着色器

  • 以上是定义着色器源代码, 接着可以将着色器加载到OpenGL ES了;
  • 实例代码中, HelloTriangleRenderer.java的 LoadShader()负责 加载着色器源码、编译并检查其错误;
   ///
   // Create a shader object,
   // load the shader source, and
   // compile the shader,
   // finally,
   // return the shader`s id!
   // 【适用于 顶点着色器、片段着色器】
   //
   private int LoadShader ( int type, String shaderSrc )
   {
      int shader;
      int[] compiled = new int[1];

      // Create the shader object
      shader = GLES30.glCreateShader ( type );

      if ( shader == 0 )
      {
         return 0;
      }

      // Load the shader source
      // 加载 着色器代码
      GLES30.glShaderSource ( shader, shaderSrc );

      // Compile the shader
      // 编译 着色器代码
      GLES30.glCompileShader ( shader );

      // Check the compile status
      // 查看 着色器编译结果状态
      GLES30.glGetShaderiv ( shader, GLES30.GL_COMPILE_STATUS, compiled, 0 );

      //编译失败,则 报错 并 删除着色器实例
      if ( compiled[0] == 0 )
      {
         Log.e ( TAG, GLES30.glGetShaderInfoLog ( shader ) );
         GLES30.glDeleteShader ( shader );
         return 0;
      }

      //编译成功,则返回 着色器id
      return shader;
   }
  • GLES30.glCreateShader ( type );返回一个着色器对象, 这是一个OpenGL ES 3.0对象,可用于连接到程序对象glCreateShader ( type )指定着色器类型并创建着色器对象;
  • GLES30.glShaderSource ( shader, shaderSrc ); 把 着色器源码 加载到 着色器对象;
  • GLES30.glCompileShader ( shader ); 编译着色器;
  • GLES30.glGetShaderiv ( shader, GLES30.GL_COMPILE_STATUS, compiled, 0 ); 查看 着色器编译结果状态; 编译失败,则 报错(打印错误信息) 并 删除着色器实例; 编译成功,则返回 着色器id,后续 用于连接到程序对象

创建一个程序对象并链接着色器

  • 应用程序 为顶点和片段着色器 创建了 着色器对象 之后, 就需要 创建一个 程序对象;
  • 程序对象 可视为 最终链接的程序;
  • 不同的 着色器 编译为 一个 着色器对象之后, 它们必须连接到 一个 程序对象 并一起链接,才能绘制图形;
   ///
   // Initialize the shader and program object
   // 初始化 着色器 和 渲染管线程序
   //
   public void onSurfaceCreated ( GL10 glUnused, EGLConfig config )
   {
      String vShaderStr =
         "#version 300 es             n"
         +   "in vec4 vPosition;           n"
         + "void main()                  n"
         + "{                            n"
         + "   gl_Position = vPosition;  n"
         + "}                            n";

      String fShaderStr =
         "#version 300 es                               n"
         + "precision mediump float;                        n"
         + "out vec4 fragColor;                         n"
         + "void main()                                  n"
         + "{                                            n"
         + "  fragColor = vec4 ( 0.0, 0.8, 1.0, 1.0 );  n"
         + "}                                            n";

      int vertexShader;//加载好的 顶点着色器实例
      int fragmentShader;//加载好的 片段着色器实例
      int programObject;//自定义的渲染管线程序id
      int[] linked = new int[1];//存放链接成功 渲染管线程序id 的数组

      // Load the vertex/fragment shaders
      // 【调用 LoadShader() 】加载着色器实例
      vertexShader = LoadShader ( GLES30.GL_VERTEX_SHADER, vShaderStr );
      fragmentShader = LoadShader ( GLES30.GL_FRAGMENT_SHADER, fShaderStr );

      // Create the program object
      // 基于顶点着色器与片段着色器创建程序
      programObject = GLES30.glCreateProgram();

      //创建失败,就拜拜
      if ( programObject == 0 )
      {
         return;
      }

      //向程序中加入顶点着色器 片元着色器
      GLES30.glAttachShader ( programObject, vertexShader );
      GLES30.glAttachShader ( programObject, fragmentShader );

      // Bind vPosition to attribute 0
      GLES30.glBindAttribLocation ( programObject, 0, "vPosition" );

      // Link the program
      // 链接程序
      GLES30.glLinkProgram ( programObject );

      // Check the link status
      // 把链接成功的 渲染管线程序id 存入数组linked
      GLES30.glGetProgramiv ( programObject, GLES30.GL_LINK_STATUS, linked, 0 );

      //若链接失败则报错 并 删除程序
      if ( linked[0] == 0 )
      {
         Log.e ( TAG, "Error linking program:" );
         Log.e ( TAG, GLES30.glGetProgramInfoLog ( programObject ) );
         GLES30.glDeleteProgram ( programObject );
         return;
      }

      // Store the program object
      // 保存 链接成功的 渲染管线程序id 到 全局变量
      mProgramObject = programObject;

      //指定清除屏幕用的颜色
      GLES30.glClearColor ( 1.0f, 1.0f, 1.0f, 0.0f );
   }

至此,便完成了 编译着色器、检查编译错误、 创建程序对象、连接着色器、链接程序并检查链接错误等流程;

  • 程序对象 成功链接之后, 就可使用 程序对象 进行渲染了!
public void onDrawFrame ( GL10 glUnused )
   {
      // Set the viewport
      // viewport【窗口】
      GLES30.glViewport ( 0, 0, mWidth, mHeight );

      // Clear the color buffer
      GLES30.glClear ( GLES30.GL_COLOR_BUFFER_BIT );

      // Use the program object
      // 指定使用某套shader程序
      GLES30.glUseProgram ( mProgramObject );

      // Load the vertex data
      // vertex【顶点】
      // 将顶点位置数据【mVertices】传送进 渲染管线
      GLES30.glVertexAttribPointer ( 0, 3, GLES30.GL_FLOAT, false, 0, mVertices );
      //启用顶点位置数据
      GLES30.glEnableVertexAttribArray ( 0 );

//      GLES30.glDrawArrays ( GLES30.GL_TRIANGLES, 0, 3 );
//      绘制
      GLES30.glDrawArrays ( GLES30.GL_TRIANGLE_STRIP, 0, 3 );
   }
  • GLES30.glUseProgram ( mProgramObject ); 用于绑定程序对象,进行渲染; 程序对象调用glUseProgram ();之后, 所有后续的渲染 将用 链接到程序对象的 顶点着色器、片段着色器进行;

设置视口和清除颜色缓冲区

设置视口

  • onDrawFrame()方法用于绘制帧;
  • GLES30.glViewport ( 0, 0, mWidth, mHeight ); 通知OpenGL ES 用于绘制的2D渲染表面原点、宽度和高度 在OpenGL ES 中, 视口(Viewport) 定义所有 OpenGL ES 渲染操作 最终显示的 2D矩形 视口 由 原点坐标(x,y)和宽度、高度 定义;

清除颜色缓冲区

  • 设置视口之后,需要清除屏幕;
  • 在OpenGL ES中, 绘图中涉及多种缓冲区类型:颜色、深度、模板;
  • HelloTriangle案例中, 只向颜色缓冲区中绘制图形;
  • 在每个帧的开始, GLES30.glClear ( GLES30.GL_COLOR_BUFFER_BIT );清除颜色缓冲区 缓冲区将用GLES30.glClearColor();指定的颜色清除; onSurfaceCreated()的初始化代码中, 我们已经用GLES30.glClearColor ( 1.0f, 1.0f, 1.0f, 0.0f ); 指定清除屏幕用的颜色为( 1.0f, 1.0f, 1.0f, 0.0f )了,即白色, 因此屏幕清为白色; 清除颜色的设置, 应该由应用程序在调用颜色缓冲区GLES30.glClear()之前设置;

加载几何形状和绘制图元

加载几何形状

  • 清除颜色缓冲区、设置视口加载程序对象之后, 指定三角形的几何形状 三角形的顶点由mVerticesData数组中的3个坐标(x,y,z)指定;
private final float[] mVerticesData =
           { 0.0f, 0.5f, 0.0f,
             -0.5f, -0.5f, 0.0f,
             0.5f, -0.5f, 0.0f };

...

public HelloTriangleRenderer ( Context context )
   {
      mVertices = ByteBuffer.allocateDirect ( mVerticesData.length * 4 )
                  .order ( ByteOrder.nativeOrder() ).asFloatBuffer();
      mVertices.put ( mVerticesData ).position ( 0 );
   }

...

GLES30.glVertexAttribPointer ( 0, 3, GLES30.GL_FLOAT, false, 0, mVertices );
GLES30.glEnableVertexAttribArray ( 0 );
GLES30.glDrawArrays ( GLES30.GL_TRIANGLE_STRIP, 0, 3 );
  • 顶点位置需要加载到GL, 并连接到 顶点着色器源码中 声明的 vPosition属性; 默认vPosition变量输入属性位置0绑定 ——"layout(location = 0) in vec4 vPosition; n" 顶点着色器中的每个属性都有一个由无符号整数值唯一标志位置 使用GLES30.glVertexAttribPointer (); 顶点数据加载到 顶点变量值vPosition对应的输入属性位置 0上;

绘制图元

  • 通过GLES30.glDrawArrays ( GLES30.GL_TRIANGLE_STRIP, 0, 3 ); 真正告诉OpenGL ES 绘制的图元是什么; 可选的图元有三角形、直线或者条带等;

显示后台缓冲区

  • 最终最终一步, 将三角形绘制到帧缓冲区!

如何在屏幕上 真正显示帧缓冲区的内容 ——双缓冲区

项目代码

  • 说了这么多,最后直接上代码吧; 其实这个案例要在Android Studio中编辑并运行的话,流程也不复杂, OpenGL ES 在SDK中是有封装好的API的,直接可以调用了; 不像OpenCV一样还需要进行外接库项目、配置各种环境;
  • 本案例的话,只需要两个文件就可以直接运行了: 【当然,注意HelloTriangle 是一个Activity,需要在Manifest中注册】
// Book:      OpenGL(R) ES 3.0 Programming Guide, 2nd Edition
// Authors:   Dan Ginsburg, Budirijanto Purnomo, Dave Shreiner, Aaftab Munshi
// ISBN-10:   0-321-93388-5
// ISBN-13:   978-0-321-93388-1
// Publisher: Addison-Wesley Professional
// URLs:      http://www.opengles-book.com
//            http://my.safaribooksonline.com/book/animation-and-3d/9780133440133
//

// Hello_Triangle
//
//    This is a simple example that draws a single triangle with
//    a minimal vertex/fragment shader.  The purpose of this
//    example is to demonstrate the basic concepts of
//    OpenGL ES 3.0 rendering.

package com.lwp.openglorigintest;

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

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

import android.content.Context;
import android.opengl.GLES30;
import android.opengl.GLSurfaceView;
import android.util.Log;

public class HelloTriangleRenderer implements GLSurfaceView.Renderer
{
   // Member variables
   private int mProgramObject;//自定义渲染管线程序id
   private int mWidth;
   private int mHeight;
   private FloatBuffer mVertices;
   private static String TAG = "HelloTriangleRenderer";

   private final float[] mVerticesData =
           { 0.0f, 0.5f, 0.0f,
             -0.5f, -0.5f, 0.0f,
             0.5f, -0.5f, 0.0f };

   ///
   // Constructor
   //
   public HelloTriangleRenderer ( Context context )
   {
      mVertices = ByteBuffer.allocateDirect ( mVerticesData.length * 4 )
                  .order ( ByteOrder.nativeOrder() ).asFloatBuffer();
      mVertices.put ( mVerticesData ).position ( 0 );
   }

   ///
   // Create a shader object,
   // load the shader source, and
   // compile the shader,
   // finally,
   // return the shader`s id!
   // 【适用于 顶点着色器、片段着色器】
   //
   private int LoadShader ( int type, String shaderSrc )
   {
      int shader;
      int[] compiled = new int[1];

      // Create the shader object
      shader = GLES30.glCreateShader ( type );

      if ( shader == 0 )
      {
         return 0;
      }

      // Load the shader source
      // 加载 着色器代码
      GLES30.glShaderSource ( shader, shaderSrc );

      // Compile the shader
      // 编译 着色器代码
      GLES30.glCompileShader ( shader );

      // Check the compile status
      // 查看 着色器编译结果状态
      GLES30.glGetShaderiv ( shader, GLES30.GL_COMPILE_STATUS, compiled, 0 );

      //编译失败,则 报错 并 删除着色器实例
      if ( compiled[0] == 0 )
      {
         Log.e ( TAG, GLES30.glGetShaderInfoLog ( shader ) );
         GLES30.glDeleteShader ( shader );
         return 0;
      }

      //编译成功,则返回 着色器id
      return shader;
   }

   ///
   // Initialize the shader and program object
   // 初始化 着色器 和 渲染管线程序
   //
   public void onSurfaceCreated ( GL10 glUnused, EGLConfig config )
   {
      String vShaderStr =
         "#version 300 es             n"
         +   "in vec4 vPosition;           n"
         + "void main()                  n"
         + "{                            n"
         + "   gl_Position = vPosition;  n"
         + "}                            n";

      String fShaderStr =
         "#version 300 es                               n"
         + "precision mediump float;                        n"
         + "out vec4 fragColor;                         n"
         + "void main()                                  n"
         + "{                                            n"
         + "  fragColor = vec4 ( 0.0, 0.8, 1.0, 1.0 );  n"
         + "}                                            n";

      int vertexShader;//加载好的 顶点着色器实例
      int fragmentShader;//加载好的 片段着色器实例
      int programObject;//自定义的渲染管线程序id
      int[] linked = new int[1];//存放链接成功 渲染管线程序id 的数组

      // Load the vertex/fragment shaders
      // 【调用 LoadShader() 】加载着色器实例
      vertexShader = LoadShader ( GLES30.GL_VERTEX_SHADER, vShaderStr );
      fragmentShader = LoadShader ( GLES30.GL_FRAGMENT_SHADER, fShaderStr );

      // Create the program object
      // 基于顶点着色器与片段着色器创建程序
      programObject = GLES30.glCreateProgram();

      //创建失败,就拜拜
      if ( programObject == 0 )
      {
         return;
      }

      //向程序中加入顶点着色器 片元着色器
      GLES30.glAttachShader ( programObject, vertexShader );
      GLES30.glAttachShader ( programObject, fragmentShader );

      // Bind vPosition to attribute 0
      GLES30.glBindAttribLocation ( programObject, 0, "vPosition" );

      // Link the program
      // 链接程序
      GLES30.glLinkProgram ( programObject );

      // Check the link status
      // 把链接成功的 渲染管线程序id 存入数组linked
      GLES30.glGetProgramiv ( programObject, GLES30.GL_LINK_STATUS, linked, 0 );

      //若链接失败则报错 并 删除程序
      if ( linked[0] == 0 )
      {
         Log.e ( TAG, "Error linking program:" );
         Log.e ( TAG, GLES30.glGetProgramInfoLog ( programObject ) );
         GLES30.glDeleteProgram ( programObject );
         return;
      }

      // Store the program object
      // 保存 链接成功的 渲染管线程序id 到 全局变量
      mProgramObject = programObject;

      //指定清除屏幕用的颜色
      GLES30.glClearColor ( 1.0f, 1.0f, 1.0f, 0.0f );
   }

   // /
   // Draw a triangle using the shader pair created in onSurfaceCreated()
   // 使用onSurfaceCreated()中创建好的 着色器对(一对顶点、片段着色器) 画一个三角形
   //
   public void onDrawFrame ( GL10 glUnused )
   {
      // Set the viewport
      // viewport【窗口】
      GLES30.glViewport ( 0, 0, mWidth/2, mHeight/2 );

      // Clear the color buffer
      GLES30.glClear ( GLES30.GL_COLOR_BUFFER_BIT );

      // Use the program object
      // 指定使用某套shader程序
      GLES30.glUseProgram ( mProgramObject );

      // Load the vertex data
      // vertex【顶点】
      // 将顶点位置数据【mVertices】传送进 渲染管线
      GLES30.glVertexAttribPointer ( 0, 3, GLES30.GL_FLOAT, false, 0, mVertices );
      //启用顶点位置数据
      GLES30.glEnableVertexAttribArray ( 0 );

//      GLES30.glDrawArrays ( GLES30.GL_TRIANGLES, 0, 3 );
//      绘制
      GLES30.glDrawArrays ( GLES30.GL_TRIANGLE_STRIP, 0, 3 );
   }

   // /
   // Handle surface changes
   //
   public void onSurfaceChanged ( GL10 glUnused, int width, int height )
   {
      mWidth = width;
      mHeight = height;
   }
}
package com.lwp.openglorigintest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;

import android.app.Activity;
import android.app.ActivityManager;
import android.content.Context;
import android.content.pm.ConfigurationInfo;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.util.Log;

public class HelloTriangle extends AppCompatActivity {

    private final int CONTEXT_CLIENT_VERSION = 3;

    @Override
    protected void onCreate ( Bundle savedInstanceState )
    {
        super.onCreate ( savedInstanceState );
        mGLSurfaceView = new GLSurfaceView ( this );

        if ( detectOpenGLES30() )
        {
            // Tell the surface view we want to create an OpenGL ES 3.0-compatible
            // context, and set an OpenGL ES 3.0-compatible renderer.
            mGLSurfaceView.setEGLContextClientVersion ( CONTEXT_CLIENT_VERSION );
            mGLSurfaceView.setRenderer ( new HelloTriangleRenderer ( this ) );
        }
        else
        {
            Log.e ( "HelloTriangle", "OpenGL ES 3.0 not supported on device.  Exiting..." );
            finish();

        }

        setContentView ( mGLSurfaceView );
    }

    private boolean detectOpenGLES30()
    {
        ActivityManager am =
                ( ActivityManager ) getSystemService ( Context.ACTIVITY_SERVICE );
        ConfigurationInfo info = am.getDeviceConfigurationInfo();
        return ( info.reqGlEsVersion >= 0x30000 );
    }

    @Override
    protected void onResume()
    {
        // Ideally a game should implement onResume() and onPause()
        // to take appropriate action when the activity looses focus
        super.onResume();
        mGLSurfaceView.onResume();
    }

    @Override
    protected void onPause()
    {
        // Ideally a game should implement onResume() and onPause()
        // to take appropriate action when the activity looses focus
        super.onPause();
        mGLSurfaceView.onPause();
    }

    private GLSurfaceView mGLSurfaceView;
}

然后就可以运行了:


参考自:

  • 《OPENGL ES 3.0编程指南(第2版)》