我的OpenGL学习进阶之旅强烈推荐一款强大的 Android OpenGL ES 调试工具 GAPID并展示实战操作演练一步一步看如何使用GAPID调试工具

Posted 欧阳鹏

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了我的OpenGL学习进阶之旅强烈推荐一款强大的 Android OpenGL ES 调试工具 GAPID并展示实战操作演练一步一步看如何使用GAPID调试工具相关的知识,希望对你有一定的参考价值。

PS: 本文已同步发表在公众号【字节卷动https://mp.weixin.qq.com/s/fQ8OTf1k7cfC_HvmqlCx2w

一、需求描述

【字节流动】 推荐一款强大的 Android OpenGL ES 调试工具文章中有如下介绍:

很多OpenGL 开发者 心里可能会有疑问:

假如我看上了别人的一个 OpenGL 实现的效果,那我能不能用 GAPID 抓取到它的 shader 脚本源码来学习?
答案是肯定的。

编译完 shader 脚本生成的二进制代码,可以通过 GAPID 抓取到并反编译成原来的 shader 源码。总而言之就是,你的 shader 脚本实际上是在 GPU 上裸奔,尤其是对手机厂商来说。

shader 脚本在 GPU 层面上目前并没有有效的加密或混淆方法,比较通用的做法是将 shader 中的变量无意义化,比如用 var1、var2 等表示,或者将一个 shader 拆分成多个小 shader ,以达到降低可读性的目的。

二、 GAPID 是什么?

GAPID (Graphics API Debugger)是 Google 的一款开源且跨平台的图形开发调试工具,用于记录和检查应用程序对图形驱动程序的调用,支持 OpenGL ES 和 Vulkan 调试。

工具下载地址:https://github.com/google/gapid/releases

GAPID 的主要功能:

  • 查看 OpenGL ES 或 Vulkan 绘图接口的调用情况(调用顺序、流程)
  • 查看传入着色器程序的参数
  • 查看纹理,导出模型、贴图等资源
  • 查看、修改以及导出 shader 脚本

三、扩展阅读

四、实践一下

4.1 下载

在github官网下载https://github.com/google/gapid/releases


因为我是windows平台,所以我下载windows的安装包,如下所示:

下载到了电脑上

4.2 安装


点击【Next】

点击【Next】

点击【Install】

安装中

安装完毕

4.3 使用 GAPID

下载、安装好 GAPID 程序后,将 android 手机通过 USB 与电脑连接(同时需要关闭 AndroidStudio ),手机处于开发者选项中的 Debug 调试模式

注意, GAPID 支持 Android 5.0 及以上版本手机,待调试的 App 要求是 Debug 版本或者手机被 Root 掉了。

  • 启动 GAPID

4.3.1 设置adb路径


我电脑上 adb路径是在 C:\\Android\\SDK\\platform-tools,所以我们设置这个路径

设置完毕

4.3.2 采集

  • 点击 Capture a new trace 准备调试我们的程序
    设置好adb之后,进入如下所示的界面,我们点击 Capture a new trace 准备调试我们的程序

    可以参考官网介绍 https://gapid.dev/trace/


    我们来实践一下

  • 设置抓取的相关参数

  1. 选择Device,我们选中我们启动的模拟器
  2. 选择Typ, 我们选择 OpenGLES类型
  3. 选择要Application 调试的程序,我们选择 com.oyp.openglesdemo

    点击OK


选择输出目录,然后点击OK

勾选上下面的三个选项

抓取中

4.4 分析采集到的文件

4.4.1 打开 trace 文件

抓取完成后,我们点击【Open Trace】打开 trace 文件,然后 GAPID 程序的界面如下图所示。


左侧区域为抓取到的每一帧的绘制过程,区域 1 中展示的是其中一帧绘制过程

4.4.2 FrameBuffer选项

我们选择第2帧 Frame2 ,如下所示,可以看到帧的图像如下所示:

4.4.3 Geometry选项

选择 【Geometry】选项,可以看到如下所示的三角形

4.4.4 Shader选项

选择【Shader】面板,可以看到着色器,如下所示展示片段着色器:

#version 310 es

precision mediump float;
out mediump vec4 sk_FragColor;
in mediump vec4 vcolor_Stage0;
void main() 
    mediump vec4 outputColor_Stage0;
    
        outputColor_Stage0 = vcolor_Stage0;
    
    
        sk_FragColor = outputColor_Stage0;
    

选择【Programs】面板,如下所示:


vertex shader

#version 310 es

precision mediump float;
uniform highp vec4 sk_RTAdjust;
uniform highp vec2 uAtlasSizeInv_Stage0;
in highp vec2 inPosition;
in mediump vec4 inColor;
in mediump uvec2 inTextureCoords;
out highp vec2 vTextureCoords_Stage0;
flat out highp int vTexIndex_Stage0;
out mediump vec4 vinColor_Stage0;
void main() 
    highp ivec2 signedCoords = ivec2(int(inTextureCoords.x), int(inTextureCoords.y));
    highp int texIdx = 2 * (signedCoords.x & 1) + (signedCoords.y & 1);
    highp vec2 unormTexCoords = vec2(float(signedCoords.x / 2), float(signedCoords.y / 2));
    vTextureCoords_Stage0 = unormTexCoords * uAtlasSizeInv_Stage0;
    vTexIndex_Stage0 = texIdx;
    vinColor_Stage0 = inColor;
    highp vec2 pos2 = inPosition;
    gl_Position = vec4(pos2.x, pos2.y, 0.0, 1.0);
    gl_Position = vec4(gl_Position.x * sk_RTAdjust.x + gl_Position.w * sk_RTAdjust.y, gl_Position.y * sk_RTAdjust.z + gl_Position.w * sk_RTAdjust.w, 0.0, gl_Position.w);


fragment shader

#version 310 es

precision mediump float;
out mediump vec4 sk_FragColor;
uniform lowp sampler2D uTextureSampler_0_Stage0;
in highp vec2 vTextureCoords_Stage0;
flat in highp int vTexIndex_Stage0;
in mediump vec4 vinColor_Stage0;
void main() 
    mediump vec4 outputColor_Stage0;
    mediump vec4 outputCoverage_Stage0;
    
        outputColor_Stage0 = vinColor_Stage0;
        mediump vec4 texColor;
        
            texColor = texture(uTextureSampler_0_Stage0, vTextureCoords_Stage0);
        
        outputCoverage_Stage0 = texColor;
    
    
        sk_FragColor = outputColor_Stage0 * outputCoverage_Stage0;
    


uniforms 变量的值

4.4.5 【State】面板

我们可以在右边的【State】面板,进入 【Objects】–>【Shaders】,然后拿到当前着色器程序所对应的 shader 脚本源码,与代码中的程序一致。

右键点击【View Details】,可以查看详细的源代码

着色器详情,如下所示:

  • 查看顶点着色器代码

    查看到的顶点着色器详情如下:
#version 300 es
// 表示OpenGL ES着色器语言V3.00

// 使用in关键字,在顶点着色器中声明所有的输入顶点属性(Input Vertex Attribute)。
// 声明一个输入属性数组:一个名为vPosition的4分量向量
// 在图形编程中我们经常会使用向量这个数学概念,因为它简明地表达了任意空间中的位置和方向,并且它有非常有用的数学属性。
// 在GLSL中一个向量有最多4个分量,每个分量值都代表空间中的一个坐标,它们可以通过vec.x、vec.y、vec.z和vec.w来获取。
//注意vec.w分量不是用作表达空间中的位置的(我们处理的是3D不是4D),而是用在所谓透视除法(Perspective Division)上。
layout(location = 0) in vec4 vPosition;
void main()

	// 为了设置顶点着色器的输出,我们必须把位置数据赋值给预定义的gl_Position变量,它在幕后是vec4类型的。
	// 将vPosition输入属性拷贝到名为gl_Position的特殊输出变量
	// 每个顶点着色器必须在gl_Position变量中输出一个位置,这个位置传递到管线下一个阶段的位置
	gl_Position = vPosition;

可以看到,和我们的源代码一模一样,源代码地址为:https://github.com/ouyangpeng/OpenGLESDemo/blob/master/app/src/main/assets/vertex/vertex_shader_hello_triangle.glsl

  • 查看片段着色器代码

    查看到的片段着色器详情如下:
#version 300 es
// 表示OpenGL ES着色器语言V3.00

// 声明着色器中浮点变量的默认精度
precision mediump float;
// 声明一个输出变量fragColor,这是一个4分量的向量,
// 写入这个变量的值将被输出到颜色缓冲器
out vec4 fragColor;

void main()

	//在计算机图形中颜色被表示为有4个元素的数组:红色、绿色、蓝色和alpha(透明度)分量,
	//通常缩写为RGBA。当在OpenGL或GLSL中定义一个颜色的时候,
	//我们把颜色每个分量的强度设置在0.0到1.0之间。

	//比如说我们设置红为1.0f,绿为1.0f,我们会得到两个颜色的混合色,即黄色。
	//这三种颜色分量的不同调配可以生成超过1600万种不同的颜色!

	// 所有片段的着色器输出都是红色( 1.0, 0.0, 0.0, 1.0 )
	fragColor = vec4 ( 1.0, 0.0, 0.0, 1.0 );

	// 会输出橘黄色
	// fragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);

可以看到,和我们的源代码一模一样,源代码地址为:https://github.com/ouyangpeng/OpenGLESDemo/blob/master/app/src/main/assets/fragment/fragment_shader_hello_triangle.glsl

4.4.6 Commands面板

我们选择Draw 1帧,查看绘制第一帧的情况,可以发现调用了30个命令,如下所示:

正好对应的我们的源代码,如下所示:
绘制三角形的源代码

#include "NativeTriangle.h"

// 可以参考这篇讲解: https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/
// 我们在OpenGL中指定的所有坐标都是3D坐标(x、y和z)
// 由于我们希望渲染一个三角形,我们一共要指定三个顶点,每个顶点都有一个3D位置。
// 我们会将它们以标准化设备坐标的形式(OpenGL的可见区域)定义为一个float数组。
// https://learnopengl-cn.github.io/img/01/04/ndc.png

// https://developer.android.com/guide/topics/graphics/opengl#kotlin
// 在 OpenGL 中,形状的面是由三维空间中的三个或更多点定义的表面。
// 一个包含三个或更多三维点(在 OpenGL 中被称为顶点)的集合具有一个正面和一个背面。
// 如何知道哪一面为正面,哪一面为背面呢?这个问题问得好!答案与环绕(即您定义形状的点的方向)有关。
// 查看图片 : https://developer.android.com/images/opengl/ccw-winding.png
// 或者查看本地图片:Android_Java/Chapter_2/Hello_Triangle/ccw-winding.png
// 在此示例中,三角形的点按照使它们沿逆时针方向绘制的顺序定义。
// 这些坐标的绘制顺序定义了该形状的环绕方向。默认情况下,在 OpenGL 中,沿逆时针方向绘制的面为正面。
// 因此您看到的是该形状的正面(根据 OpenGL 解释),而另一面是背面。
//
// 知道形状的哪一面为正面为何如此重要呢?
// 答案与 OpenGL 的“面剔除”这一常用功能有关。
// 面剔除是 OpenGL 环境的一个选项,它允许渲染管道忽略(不计算或不绘制)形状的背面,从而节省时间和内存并缩短处理周期:
static GLfloat vVertices[] = 
        // 逆时针 三个顶点
        0.0f, 0.5f, 0.0f,            // 上角
        -0.5f, -0.5f, 0.0f,          // 左下角
        0.5f, -0.5f, 0.0f            // 右下角
;

void NativeTriangle::create() 
    GLUtils::printGLInfo();

    // Main Program
    VERTEX_SHADER = GLUtils::openTextFile(
            "vertex/vertex_shader_hello_triangle.glsl");
    FRAGMENT_SHADER = GLUtils::openTextFile(
            "fragment/fragment_shader_hello_triangle.glsl");

    mProgram = GLUtils::createProgram(&VERTEX_SHADER, &FRAGMENT_SHADER);
    if (!mProgram) 
        LOGD("Could not create program")
        return;
    
    // 设置清除颜色
    glClearColor(1.0f, 1.0f, 1.0f, 0.0f);


void NativeTriangle::draw() 
    // Clear the color buffer
    // 清除屏幕
    // 在OpenGL ES中,绘图中涉及多种缓冲区类型:颜色、深度、模板。
    // 这个例子,绘制三角形,只向颜色缓冲区中绘制图形。在每个帧的开始,我们用glClear函数清除颜色缓冲区
    // 缓冲区将用glClearColor指定的颜色清除。
    // 这个例子,我们调用了GLES30.glClearColor(1.0f, 1.0f, 1.0f, 0.0f); 因此屏幕清为白色。
    // 清除颜色应该由应用程序在调用颜色缓冲区的glClear之前设置。
    glClear(GL_COLOR_BUFFER_BIT);

    // Use the program object
    // 在glUseProgram函数调用之后,每个着色器调用和渲染调用都会使用这个程序对象(也就是之前写的着色器)了。
    // 当我们渲染一个物体时要使用着色器程序 , 将其设置为活动程序。这样就可以开始渲染了
    glUseProgram(mProgram);

    // Load the vertex data
    //  顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,
    //  它还的确意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。
    //  所以,我们必须在渲染前指定OpenGL该如何解释顶点数据。

    //  我们的顶点缓冲数据会被解析为下面这样子:https://learnopengl-cn.github.io/img/01/04/vertex_attribute_pointer.png
    //   . 位置数据被储存为32位(4字节)浮点值。
    //   . 每个位置包含3个这样的值。
    //   . 在这3个值之间没有空隙(或其他值)。这几个值在数组中紧密排列(Tightly Packed)。
    //   . 数据中第一个值在缓冲开始的位置。

    // 有了这些信息我们就可以使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)了:
    // Load the vertex data

    // 第一个参数指定我们要配置的顶点属性。因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入0。
    // 第二个参数指定顶点属性的大小。顶点属性是一个vec3,它由3个值组成,所以大小是3。
    // 第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec*都是由浮点数值组成的)。
    // 第四个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE。
    // 第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。我们设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。
    //      一旦我们有更多的顶点属性,我们就必须更小心地定义每个顶点属性之间的间隔,
    //      (译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。
    // 最后一个参数的类型是void*,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。
    glVertexAttribPointer(VERTEX_POS_INDX, 3, GL_FLOAT, GL_FALSE, 0, vVertices);

    // 现在我们已经定义了OpenGL该如何解释顶点数据,
    // 我们现在应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的。
    glEnableVertexAttribArray(VERTEX_POS_INDX);

    // glDrawArrays函数第一个参数是我们打算绘制的OpenGL图元的类型。我们希望绘制的是一个三角形,这里传递GL_TRIANGLES给它。
    // 第二个参数指定了顶点数组的起始索引,我们这里填0。
    // 最后一个参数指定我们打算绘制多少个顶点,这里是3(我们只从我们的数据中渲染一个三角形,它只有3个顶点长)。
    //        public static final int GL_POINTS                                  = 0x0000;
    //        public static final int GL_LINES                                   = 0x0001;
    //        public static final int GL_LINE_LOOP                               = 0x0002;
    //        public static final int GL_LINE_STRIP                              = 0x0003;
    //        public static final int GL_TRIANGLES                               = 0x0004;
    //        public static final int GL_TRIANGLE_STRIP                          = 0x0005;
    //        public static final int GL_TRIANGLE_FAN                            = 0x0006;
    glDrawArrays(GL_TRIANGLES, 0, 3);

    // 禁用 通用顶点属性数组
    glDisableVertexAttribArray(0);


void NativeTriangle::shutdown() 
    // Delete program object
    GLUtils::DeleteProgram(mProgram);


其中创建程序对象和链接着色器的代码,调用了如下的代码

/**
 * Loads the given source code as a shader of the given type.
 *
 * 负责 加载着色器源代码、编译并检查错误。他返回一个着色器对象
 */
static GLuint loadShader(GLenum shaderType, const char** source) 
    // Create the shader object
    GLuint shader;
    FUN_BEGIN_TIME("GLUtils::loadShader")
        GLint compiled;
        // Create the shader object
        // shaderType 可以是  GL_VERTEX_SHADER  或者  GL_FRAGMENT_SHADER
        shader = glCreateShader(shaderType);
        if (shader == 0) 
            return 0;
        

        // Load the shader source
        glShaderSource(shader, 1, source, nullptr);

        // Compile the shader
        glCompileShader(shader);

        // Check the compile status
        glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);

        if (!compiled) 
            GLint infoLen = 0;

            glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen);

            if (infoLen > 1) 
                char *infoLog = (char *) malloc(sizeof(char) * infoLen);
                if (infoLog) 
                    // 检索信息日志
                    glGetShaderInfoLog(shader, infoLen, nullptr, infoLog);
                    LOGE("GLUtils::loadShader error compiling shader:\\n%s\\n", infoLog)

                    free(infoLog);
                
                // 删除Shader
                glDeleteShader(shader);
                return 0;
            
        FUN_END_TIME("GLUtils::loadShader")
        return shader;
    


GLuint GLUtils::createProgram(const char** vertexSource, const char** fragmentSource) 
    GLuint program = 0;
    FUN_BEGIN_TIME("GLUtils::createProgram")
        // Load the Vertex shader
        GLuint vertexShader = loadShader(GL_VERTEX_SHADER, vertexSource);
        if (vertexShader == 0) 
            return 0;
        
        // Load the Fragment shader
        GLuint fragmentShader = loadShader(GL_FRAGMENT_SHADER, fragmentSource);
        if (fragmentShader == 0) 
            return 0;
        

        // Create the program object
        program = glCreateProgram();
        if (program == 0) 
            return 0;
        

        // Bind the vertex shader to the program
        glAttachShader(program, vertexShader);
        checkGlError("glAttachShader");
        // Bind the fragment shader to the program.
        glAttachShader(program, fragmentShader);
        checkGlError("glAttachShader");
        // Link the program
        glLinkProgram(program);

        // Check the link status
        GLint linkStatus;
        glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);

        if (!linkStatus) 
            // Retrieve compiler error message when linking fails
            GLint infoLen = 0;
            glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLen);
            if (infoLen > 1) 
                char *infoLog = (char *) malloc(sizeof(char) * infoLen);
                if (infoLog) 
                    //获取信息
                    glGetProgramInfoLog(program, infoLen, nullptr, infoLog);
                    LOGE("GLUtils::createProgram error linking program:\\n%s\\n", infoLog)
                    free(infoLog);
                
            
            // 删除程序对象
            glDeleteProgram(program);
            return 0;
        
        // Free up no longer needed shader resources
        glDeleteShader(vertexShader);
        glDeleteShader(fragmentShader);
    FUN_END_TIME("GLUtils::createProgram")
    return program;

  • 对照分析一下

  • 加载顶点着色器和加载片段着色器的命令和源代码映射

  • 创建程序对象并链接着色器的命令和源代码映射
  • 绘制三角形的命令和源代码映射

设置清除颜色

调用 glViewport

绘制三角形的命令和源代码映射

除了绘制第一帧的时候需要创建程序对象和链接着色器等初始化操作,后续的帧绘制的时候,只需要调用draw方法,所以调用的命令就少了很多

以上是关于我的OpenGL学习进阶之旅强烈推荐一款强大的 Android OpenGL ES 调试工具 GAPID并展示实战操作演练一步一步看如何使用GAPID调试工具的主要内容,如果未能解决你的问题,请参考以下文章

我的OpenGL学习进阶之旅解决Android OpenGL ES 调试工具 GAPID 无法识别Android设备的问题

我的OpenGL学习进阶之旅 OpenGL ES 实现不停旋转图片,并保证图片不变形的效果

我的OpenGL学习进阶之旅 OpenGL ES 实现不停旋转图片,并保证图片不变形的效果

我的OpenGL学习进阶之旅学习OpenGL ES 3.0 的实战 Awsome Demo (中)

我的OpenGL学习进阶之旅学习OpenGL ES 3.0 的实战 Awsome Demo (中)

我的Android进阶之旅强烈推荐 一种优雅的方式实现RecyclerView条目多类型