前言
由于公司项目需要做一些3D内容的呈现,学习了关于OpenGL ES的内容。
OpenGL ES (OpenGL for Embedded Systems) 是OpenGL的子集,算是OpenGL的精简版,主要用于移动设备。安卓中用的也是ES版本。本文为了方便起见,所说的OpenGL都是指ES版本。
OpenGL是利用GPU做高并发的计算,让数百万像素的屏幕能快速的显示的正确的图像。
项目中绘制3D的工作总算是完成了,想到近期很火的挖矿、AI等都是用到GPU进行计算,我就想探究现在已掌握部分的OpenGL相关技术,能否用于除绘图外的通用计算。
在Android端使用OpenGL的compute shader加速计算_android studio compute shader-优快云博客
上面这篇是让我相信此可能性的文章,但是文章中的代码不完整,我用里面不完整的代码加上自己脑补里面欠缺的部分代码,无法算出预期的结果。而且要下载其提供的源码竟然需要50积分,实在太贵了~~
于是我花了很多时间找别的资料并尝试,最终完成了通用计算的目标。此篇文章就是整理我这段时间的学习心得,希望想学习这方面技术的人参考本文后能有所收获。
碍于篇幅,此文章也只列出那些关键代码,完整的代码分享在这
https://download.youkuaiyun.com/download/shouhengboy/91866995
意思意思的小积分,算是对我的小鼓励。如果不想付下载分,文中也有一些链接,可下载到能运行的项目代码,自行去下载学习。
正文
一、OpenGL基本纹理绘制
要实现计算的第一步,就是考虑数据要怎么"传进去"给GPU计算?答案是是使用"纹理"。
第一步就先简单的用OpenGL绘制纹理,并传入一些其他数据给他计算,做简单的图像处理。
下面这个系列的文章对我OpenGL的学习过程有很大的帮助,如果你没学过OpenGL的相关技术,可以先参考他的文章
https://blog.youkuaiyun.com/junzia/category_9269184.html
在安卓中最简单的 OpenGL 图像绘制,是用 GLSurfaceView 生成的 EGL 环境。
首先在layout中
<android.opengl.GLSurfaceView
android:id="@+id/glView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
然后在Activity中
GLSurfaceView glView = findViewById(R.id.glView);
glView.setEGLContextClientVersion(2);
glView.setRenderer(renderer);
glView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
其中,Renderer(渲染器)就是处理图像的关键部分,实现其 onSurfaceCreated、onSurfaceChanged、onDrawFrame 等接口方法,链接着色器代码,将图片"纹理"与相关"计算参数"传入着色器中进行计算,最终就能绘制出处理后的图片。
由于本人已经将创建着色器程序、链接参数等相关操作都封装过的,所以对 Renderer 中代码能直接复制分享的Java代码不多,建议直接参考上面分享的那个系列文章。
下面分享着色器代码
顶点着色器
attribute vec3 a_position;//顶点位置
attribute vec2 a_texCoord;//纹理顶点坐标
varying vec2 v_texCoord;//用于传递给片元着色器的变量
void main() {
gl_Position = vec4(a_position, 1);
v_texCoord = a_texCoord;
}
片元着色器
precision mediump float;
varying vec2 v_texCoord; //接收从顶点着色器过来的纹理顶点
uniform sampler2D u_sTexture; //纹理内容数据
uniform mat3 u_colorMatrix; // 色调转换矩阵
void main(){
vec3 inputPixel = texture2D(u_sTexture, v_texCoord).rgb; //取出纹理颜色
vec3 resultPixel = u_colorMatrix * inputPixel; // 计算色相旋转
gl_FragColor = vec4(resultPixel, 1.0); // 设置此片的颜色到gl_FragColor
}
其中,u_sTexture就是"纹理",u_colorMatrix就是"计算参数"。
在安卓中提到图片,一般用的就是 Bitmap 这个类。下面这句就是将Bitmap传入变成纹理。
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
此处省略一千个细节...
最终实现以下效果

二、离屏渲染读取计算结果
上一步我们用是在 GLSurfaceView 中绘制图像,但如果要用于"纯运算",就需要脱离 View 来调用 OpenGL。
上面那个系列文章中,有一篇"利用EGL后台处理图像",说明了脱离 GLSurfaceView 来创建 EGL 环境的方法,可在脱离 View 的情况下使用 OpenGL。
然后,当 OpenGL 渲染结束后,也就是 glDrawArrays 方法后,我们还需要读取计算的结果,可以使用 glReadPixels 方法读出计算结果。
GLES20.glReadPixels(0, 0, w, h, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, mReadBuf);
最后那个 mReadBuf 是一个 Buffer,例如读整形可以用 IntBuffer,读浮点类型用 FloatBuffer,或是用 ByteBuffer 可读取任何类型的结果。
这里我们先用 IntBuffer 读取
然后创建 Bitmap
Bitmap outBmp = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
// 拷贝读出的结果到 Bitmap 中
outBmp.copyPixelsFromBuffer(mReadBuf);
最后将 Bitmap 放到 ImageView 中显示,得到与第一步中一样的显示效果。
三、OpenGL ES 3.0
接下来的 三 ~ 四 属于过渡阶段,是基于上述步骤一步一步"升级"过来的,实际使用有兼容性问题。但是由于升级过程中有一些知识点,所以我还是详细介绍了。
网上关于OpenGLES的文章,大部分都是用 OpenGLES2.0 的版本,基本所有手机都支持这个版本,并且如果只是用于绘图,这个版本也已经够用了。
但是,我的目标是用在通用计算,需要考虑各种数据格式。其中,最棘手的就是 float 类型。
OpenGL的纹理的颜色,是由 rgba 4个值组成,也就是 4 bytes,然后 float 也是 4 bytes,不过是特殊的数据格式(符号位+指数位+尾数位)。
能否把要计算输入的 float 传入,然后在着色器将 rgba 恢复成 float 数据,计算完成后再把 float 转回 rgba 再渲染输出。之后再用 FloatBuffer 读取数据,得到最终的计算结果?
抱着这个思路,我开始以下尝试。
GLSL语言中也没有类似 ByteBuffer 可以转换各种数据格式的东西,要在 rgba 与 float 两个数据格式间转换,势必用到"位运算"。
在GLSL中添加位运算的代码
vec4 inputPixel = imageLoad(u_inputImage, texelCoord);
// 将纹理颜色值 rgba 取出
// inputPixel.r 得到的值会是 float 0.0 ~ 1.0 间的值,原本是 255,得到的是 1.0,所以下方 * 255 是把原本byte复原回来
int ir = int(inputPixel.r * 255.0);
int ig = int(inputPixel.g * 255.0);
int ib = int(inputPixel.b * 255.0);
int ia = int(inputPixel.a * 255.0);
// 将原本的 4 bytes转成一个int
int ii = (ir & 0xFF) + ((ig & 0xFF) << 8) + ((ib & 0xFF) << 16) + ((ia & 0xFF) << 24);
然后创建程序时报了以下错误
ERROR: 0:17: '&' : bit-wise operator supported in GLSL ES 3.00 and above only
ERROR: 0:17: '<<' : bit-wise operator supported in GLSL ES 3.00 and above only
从上方的错误信息得知,要在GLSL中使用位运算需要 GLSL ES 3.00以上的版本。
3.0版本与2.0版本的语法上有一些差异,所以必须先掌握3.0版本的语法,下方列出我两个版本的着色器代码,对比其差异。
2.0版本
顶点着色器
attribute vec3 a_position;//顶点位置
attribute vec2 a_texCoord;//纹理顶点坐标
varying vec2 v_texCoord;//用于传递给片元着色器的变量
void main() {
gl_Position = vec4(a_position, 1);
v_texCoord = a_texCoord;
}
片元着色器
precision mediump float;
varying vec2 v_texCoord; //接收从顶点着色器过来的纹理顶点
uniform sampler2D u_sTexture; //纹理内容数据
uniform mat3 u_colorMatrix; // 色调转换矩阵
void main(){
vec3 inputPixel = texture2D(u_sTexture, v_texCoord).rgb; //取出纹理颜色
vec3 resultPixel = u_colorMatrix * inputPixel; // 计算色相旋转
gl_FragColor = vec4(resultPixel, 1.0); // 设置此片的颜色到gl_FragColor
}
3.0版本
顶点着色器
#version 300 es
layout (location = 0) in vec3 a_position;//顶点位置
layout (location = 1) in vec2 a_texCoord;//纹理顶点坐标
out vec2 v_texCoord;//用于传递给片元着色器的变量
void main() {
gl_Position = vec4(a_position, 1);
v_texCoord = a_texCoord;
}
片元着色器
#version 300 es
precision mediump float;
in vec2 v_texCoord; //接收从顶点着色器过来的纹理顶点
uniform sampler2D u_sTexture; //纹理内容数据
uniform mat3 u_colorMatrix; // 色调转换矩阵
out vec4 outColor; //输出显示颜色
void main(){
vec3 inputPixel = texture(u_sTexture, v_texCoord).rgb; //取出纹理颜色
vec3 resultPixel = u_colorMatrix * inputPixel; // 计算色相旋转
outColor = vec4(resultPixel, 1.0); // 设置此片的颜色到outColor
}
3.0版本主要变更
1. 着色器开头必须以 #version 300 es 开头
2. 传入顶点,由 attribute vec3 改成 layout (location = 0) in vec3
3. 顶点着色器传给片元着色器的参数,由 varying 改成 out 与 in
4. 获取纹理颜色由 texture2D() 改成 texture()
5. 片元着色器输出最终颜色,由固定变量 gl_FragColor = 颜色,改成先声明 out vec4 outColor; (变量名可改) 然后 outColor = 颜色
然后沿用最前面的 Renderer,只是创建程序时传入3.0版本的GLSL代码,得到与前面一样的显示效果,升级3.0的工作就算完成了。
四、执行通用计算(GLSL ES 3.0版本)
升级 GLSL ES 3.00 的代码后,再尝试加入位运算的相关代码,没有再报错了。接下来开始写正式的数据转换、计算的相关代码。
我们以计算以2为底的 log 值为例,计算大数组 float[1000000],利用 GPU 做并发计算。
片元着色器
#version 300 es
precision mediump float;
in vec2 v_texCoord; //接收从顶点着色器过来的纹理顶点
uniform sampler2D u_sTexture; //纹理内容数据
out vec4 outColor; //输出显示颜色
void main(){
vec4 inputPixel = texture(u_sTexture, v_texCoord); //纹理颜色值
// 将纹理颜色值 rgba 取出
// inputPixel.r 得到的值会是 float 0.0 ~ 1.0 间的值,原本是 255,得到的是 1.0,所以下方 * 255 是把原本byte复原回来
int ir = int(inputPixel.r * 255.0);
int ig = int(inputPixel.g * 255.0);
int ib = int(inputPixel.b * 255.0);
int ia = int(inputPixel.a * 255.0);
// 将原本的 4 bytes转成一个int
int ii = (ir & 0xFF) + ((ig & 0xFF) << 8) + ((ib & 0xFF) << 16) + ((ia & 0xFF) << 24);
// 得到真实传入的 float 值
float f = intBitsToFloat(ii);
// 计算以2为底的log值
f = log(f) / log(2.0);
// 把计算结果 float 转回 int
int oi = floatBitsToInt(f);
// 把输出的rgba转回 0.0~1.0 的 float
float r = float((oi) & 0xFF) / 255.0;
float g = float((oi >> 8) & 0xFF) / 255.0;
float b = float((oi >> 16) & 0xFF) / 255.0;
float a = float((oi >> 24) & 0xFF) / 255.0;
outColor = vec4(r, g, b, a);
}
然后,要把 float 数组传进去计算。
我们之前传入纹理数据时,是把图片 Bitmap 透过以下方法传入
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
然而,现在我们的数据源不是图片而是float数组,改用以下方法将数据传入。
FloatBuffer buffer = FloatBuffer.wrap(srcData);
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, srcWidth, srcHeight, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buffer);
其中,srcWidth、srcHeight 是纹理数据的宽高。我的数组长度是 1000 * 1000,所以宽高就定1000。
渲染完成后,再将数据读出。
FloatBuffer buf = FloatBuffer.allocate(1000000);
GLES20.glReadPixels(0, 0, data.width, data.height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buf);
float[] res = buf.array();
最后,我们得到了计算结果。

为了验证结果是否正确,我们用 java 的方式计算一下结果,对比两者的差异,并对比两者的耗时。
对比 OpenGL 与 java的计算结果,最大只有 1.9E-6 的计算误差,可以说计算结果完全可靠。
性能对比
这边用 Android SDK 中自带的模拟器来做性能对比(真机有兼容性问题,后面再说),计算 float[1000000] 的log2值100次。
Android 9(API28)
OpenGL:950 ms
Java:2200 ms
Android 15(API35)
OpenGL:1100 ms
Java:11000 ms
可看到利用OpenGL计算有2~10倍的性能提升,可能用模拟器跑Android 15对电脑性能挑战比较大,所以CPU运算速度较慢,但对GPU影响就没那么大。
然后进一步观察日志,可观察到OpenGL运算的主要耗时是在 glReadPixels 这个方法,从传入纹理数据到渲染 glDrawArrays 的耗时只有 1~2ms,再 glReadPixels 后耗时就到10ms。
所以OpenGL计算时,主要的性能瓶颈是在读取计算结果,实际计算(渲染)是一瞬间完成的。也就是如果算法复杂的话,性能提升是会更明显的。
兼容性问题
模拟器已经OK了,接下来用真机试试看,得到的计算结果全是0。
后来发现只要注释掉 f = log(f) / log(2.0); 这一句(也就是不计算),可以得到输出结果,按理说输出应该等于输入(只是把rgba转float再转回rgba),但是发现结果已经与输入不同了(1.0019379, 2.0039062 ...)。
光转换过程就发生了数据偏差,就算能运算结果也不可靠了。
五、计算着色器-图片处理
从OpenGL ES 3.1开始新增了计算着色器(Compute Shader),是用于执行通用计算的,我之前参考的那篇就是介绍使用计算着色器的
在Android端使用OpenGL的compute shader加速计算_android studio compute shader-优快云博客
但是,我参考上面这篇时无法顺利跑出正确的结果。经过多方的搜索,我在Google的官方文档中找到了相关资料。
将脚本迁移到 OpenGL ES 3.1 | Views | Android Developers
其中包括了可运行的示例源码(我上述举的色相旋转图片处理的例子就是从这来的)
经过一番学习,梳理官方代码的代码逻辑(还把kotlin转成我习惯的java),最后终于完成了我自己的计算着色器。
我们先用计算着色器来实现上面图片处理的功能
着色器
#version 310 es
layout(std430) buffer; // 官方的demo有,不确定干嘛用的,注释掉也能用
layout (local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
uniform layout (rgba8, binding = 0) readonly highp image2D u_inputImage;
uniform layout (rgba8, binding = 1) writeonly highp image2D u_outputImage;
uniform mat3 u_colorMatrix;
void main() {
ivec2 texelCoord = ivec2(gl_GlobalInvocationID.xy);
vec3 inputPixel = imageLoad(u_inputImage, texelCoord).rgb;
vec3 resultPixel = u_colorMatrix * inputPixel;
imageStore( u_outputImage, texelCoord, vec4(resultPixel, 1.0f));
}
对比与3.0版绘制的着色器差异
1. 计算着色器不区分顶点、片元着色器,不用传顶点坐标。数据是直接传到"工作组"中,用这句设置本地工作组大小 layout (local_size_x ... 。
2. 纹理数据由 uniform sampler2D 改成 uniform layout (rgba8, binding = 0) readonly highp image2D,可定义数据类型(rgba8 or other),纹理下标(binding = n),用于传入还是读出(readonl、writeonly)
3. 取当前位置的纹理值,之前是 texture(u_sTexture, v_texCoord) 改成先取得当前位置 ivec2 texelCoord = ivec2(gl_GlobalInvocationID.xy) 再读取当前位置的纹理值 imageLoad(u_inputImage, texelCoord)
4. 输出结果之前是声明 out vec2 v_texCoord; 后对 v_texCoord = 结果值 设置颜色;改成把颜色保存到输出纹理 imageStore(u_outputImage, texelCoord, 结果值);
接下来是java的部分
绑定输入图片纹理
GLES31.glGenTextures(1, mRotateTextureHandle, 0); // 生成纹理
GLES31.glBindTexture(GLES31.GL_TEXTURE_2D, mRotateTextureHandle[0]); // 绑定纹理
GLES31.glTexStorage2D(
GLES31.GL_TEXTURE_2D, 1, GLES31.GL_RGBA8,
width, height
); // 创建纹理存储区
GLUtils.texSubImage2D(
GLES31.GL_TEXTURE_2D, 0, 0, 0,
bitmap
); // 将bitmap数据更新到纹理中
GLES31.glActiveTexture(GLES31.GL_TEXTURE0 + INPUT_TEXTURE_UNIT_INDEX);
if (mRotateTextureHandle[0] != 0) {
GLES31.glBindImageTexture(
INPUT_TEXTURE_UNIT_INDEX, // unit
mRotateTextureHandle[0], // texture
0, // level
false, // layered
0, // layer
GLES31.GL_READ_ONLY, // access
GLES31.GL_RGBA8 // format
); // 绑定纹理到着色器中
}
GLES31.glTexStorage2D 以及 GLES31.glBindImageTexture 方法中的参数与GLSL中的这句互相对应
uniform layout (rgba8, binding = 0) readonly highp image2D u_inputImage;
int INPUT_TEXTURE_UNIT_INDEX = 0 对应 binding = 0,GLES31.GL_RGBA 对应 rgba8,GLES31.GL_READ_ONLY 对应 readonly
注:glActiveTexture 是在 ES 2.0 版本中处理多重纹理用的,我发现在 ES 3.1 的计算着色器中不加这个也行,计算着色器有 glBindImageTexture 的第一个参数 unit 去与 glsl 的 binding 对应。但是因为官方Demo有,我就先放在这。
绑定输出图片纹理
GLES31.glGenTextures(1, mOutputTextureHandle, 0);
GLES31.glBindTexture(GLES31.GL_TEXTURE_2D, mOutputTextureHandle[0]);
GLES31.glTexStorage2D(
GLES31.GL_TEXTURE_2D, 1, GLES31.GL_RGBA8,
width, height
);
GLES31.glActiveTexture(GLES31.GL_TEXTURE0 + OUTPUT_TEXTURE_UNIT_INDEX);
if (mOutputTextureHandle[0] != 0) {
GLES31.glBindImageTexture(
OUTPUT_TEXTURE_UNIT_INDEX, // OUTPUT_TEXTURE_UNIT_INDEX == 1
mOutputTextureHandle[0],
0,
false,
0,
GLES31.GL_WRITE_ONLY,
GLES31.GL_RGBA8
);
}
这里跟输入纹理逻辑差不多,只是不需要 glTexSubImage2D 传入数据,这个纹理是用来读数据用的。
绑定缓冲区
GLES31.glGenFramebuffers(1, mOffscreenBufferHandle, 0);
GLES31.glBindFramebuffer(GLES31.GL_FRAMEBUFFER, mOffscreenBufferHandle[0]);
GLES31.glFramebufferTexture2D(
GLES31.GL_FRAMEBUFFER,
GLES31.GL_COLOR_ATTACHMENT0,
GLES31.GL_TEXTURE_2D,
mOutputTextureHandle[0],
0
);
关键就是要把输出纹理 mOutputTextureHandle 加到缓冲区中。
然后计算,并等待计算结束
GLES31.glDispatchCompute(
roundUp(srcWidth, 8), // num_groups_x
roundUp(srcHeight, 8), // num_groups_y
1 // num_groups_z
);
GLES31.glMemoryBarrier(GLES31.GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
private int roundUp(int base, int divisor) {
return (int) Math.ceil((double) base/ divisor);
}
这里的 roundUp 就是计算工作组数量,假设我们有 1000 * 1000 的数据,由于我们设置了 local_size_x = 8, local_size_y = 8,也就是本地工作组的大小是 8 * 8,那么我们需要的工作组就是 1000 / 8,即 125 * 125 个工作组。
更多关于工作组的说明可以看这篇
GPGPU基础(五):使用compute shader进行通用计算及示例_compute shader 计算单元-优快云博客
计算完成后指定缓冲区读回
GLES31.glBindFramebuffer(GLES31.GL_FRAMEBUFFER, mOffscreenBufferHandle[0]);
GLES20.glReadPixels(0, 0, w, h, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, mReadBuf); // IntBuffer
// 把读出的数据转成Bitmap
Bitmap outBmp = BitmapStore.getInstance().getBitmap(w, h, Bitmap.Config.ARGB_8888);
outBmp.copyPixelsFromBuffer(mReadBuf);
最后将 Bitmap 放到 ImageView 中显示,得到与第一步中一样的显示效果。
至此,我们用计算着色器处理图片的工作就完成了。
六、计算着色器-通用计算
接下来,我们再尝试一下用这个计算着色器计算 log2 的值
着色器
#version 310 es
layout (local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
uniform layout (rgba8, binding = 0) readonly highp image2D u_inputImage;
uniform layout (rgba8, binding = 1) writeonly highp image2D u_outputImage;
void main(){
ivec2 texelCoord = ivec2(gl_GlobalInvocationID.xy);
vec4 inputPixel = imageLoad(u_inputImage, texelCoord);
// 将纹理颜色值 rgba 取出
// inputPixel.r 得到的值会是 float 0.0 ~ 1.0 间的值,原本是 255,得到的是 1.0,所以下方 * 255 是把原本byte复原回来
int ir = int(inputPixel.r * 255.0);
int ig = int(inputPixel.g * 255.0);
int ib = int(inputPixel.b * 255.0);
int ia = int(inputPixel.a * 255.0);
// 将原本的 4 bytes转成一个int
int ii = (ir & 0xFF) + ((ig & 0xFF) << 8) + ((ib & 0xFF) << 16) + ((ia & 0xFF) << 24);
// 得到真实传入的 float 值
float f = intBitsToFloat(ii);
// 计算以2为底的log值
f = log(f) / log(2.0);
// 把计算结果 float 转回 int
int oi = floatBitsToInt(f);
// 把输出的rgba转回 0.0~1.0 的 float
float r = float((oi) & 0xFF) / 255.0;
float g = float((oi >> 8) & 0xFF) / 255.0;
float b = float((oi >> 16) & 0xFF) / 255.0;
float a = float((oi >> 24) & 0xFF) / 255.0;
imageStore(u_outputImage, texelCoord, vec4(r, g, b, a));
}
java代码跟上一步处理图片的差不多,只是写入纹理数据不是 GLUtils.texSubImage2D ,而是 GLES31.glTexSubImage2D 传入 FloatBuffer
FloatBuffer buffer = FloatBuffer.wrap(srcData);
GLES31.glTexSubImage2D(GLES31.GL_TEXTURE_2D, 0, 0, 0, srcWidth, srcHeight, GLES31.GL_RGBA, GLES31.GL_UNSIGNED_BYTE, buffer);
然后读取计算结果也是读到 FloatBuffer
FloatBuffer buf = FloatBuffer.allocate(1000000);
GLES20.glReadPixels(0, 0, data.width, data.height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buf);
float[] res = buf.array();
最后的 res 就是我们要的计算结果

兼容性
计算着色器要求 OpenGL ES 3.1 以上才支持,我的 Android 9 模拟器不支持,Android 15 模拟器支持,我试了我手边大部分的真机都支持(原本期待10年前的Android5古董手机的结果,奈何不支持)。
下面这个方法可以取得手机的 OpenGL ES 版本
public static int getGlVersion(Context context) {
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
ConfigurationInfo info = am.getDeviceConfigurationInfo();
String versionStr = Integer.toHexString(info.reqGlEsVersion);
return Integer.parseInt(versionStr);
}
此值 >= 30001 的设备都能支持计算着色器。
另外,在部分真机中,首次渲染并读取数据时会读到全是0,第二次读取能正确读出,这是个小坑。
七、计算着色器-直接处理 float32
上面那一步是基于最早利用图片的 rgba8 版本改出来的,实际上计算着色器可以直接支持传入 float32 的,不需要 rgba 跟 float 互转了,所以我们再把上一步的代码再优化一下
着色器
#version 310 es
layout (local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
uniform layout (rgba32f, binding = 0) readonly highp image2D u_inputImage;
uniform layout (rgba32f, binding = 1) writeonly highp image2D u_outputImage;
void main(){
ivec2 texelCoord = ivec2(gl_GlobalInvocationID.xy);
vec4 inputPixel = imageLoad(u_inputImage, texelCoord);
float l2 = log(2.0);
float r = log(inputPixel.r) / l2;
float g = log(inputPixel.g) / l2;
float b = log(inputPixel.b) / l2;
float a = log(inputPixel.a) / l2;
imageStore(u_outputImage, texelCoord, vec4(r, g, b, a));
}
java中,也把原来的 GL_RGBA 改成 GL_RGBA32F
// 绑定输入纹理
GLES31.glBindTexture(GLES31.GL_TEXTURE_2D, fTexture[0]);
GLES31.glTexStorage2D(GLES31.GL_TEXTURE_2D, 1, GLES31.GL_RGBA32F, srcWidth, srcHeight);
FloatBuffer buffer = FloatBuffer.wrap(srcData);
GLES31.glTexSubImage2D(GLES31.GL_TEXTURE_2D, 0, 0, 0, srcWidth, srcHeight, GLES31.GL_RGBA, GLES31.GL_FLOAT, buffer); // 将数据写入纹理
GLES31.glBindImageTexture(0 /* binding */, fTexture[0], 0, false, 0, GLES31.GL_READ_ONLY /* readonly */, GLES31.GL_RGBA32F /* rgba32f */);
// 绑定输出纹理
GLES31.glBindTexture(GLES31.GL_TEXTURE_2D, fTexture[1]);
GLES31.glTexStorage2D(GLES31.GL_TEXTURE_2D, 1, GLES31.GL_RGBA32F, srcWidth, srcHeight);
GLES31.glBindImageTexture(1, fTexture[1], 0, false, 0, GLES31.GL_WRITE_ONLY, GLES31.GL_RGBA32F);
另外,目前计算数组的大小是 1000 * 1000,之前的 srcWidth、srcHeight 是 1000。
现在由于传入的纹理格式已经是 rgba32f 了,也就是 rgba 每一个分量都是一个 float,也就是一个像素(工作项)可以处理4个 float,所以此时 srcWidth、srcHeight 改成 500。
最后,读取数据
GLES20.glReadPixels(0, 0, data.widthF32, data.heightF32, GLES20.GL_RGBA, GLES20.GL_FLOAT, buf);
最终的计算结果与上一步的相同,并且运行效率还略高,这就是我目前阶段的最终版
性能对比
计算 float[1000000] 的log2值100次,计算着色器 VS java。
Android 15(API35)模拟器
OpenGL:950 ms
Java:11000 ms
荣耀V20(麒麟980,2018年中高端)
OpenGL:600~700 ms
Java:1700 ms
红米 Note 11T Pro(天玑8100,2022年中高端)
OpenGL:700~900 ms
Java:1135 ms
华为畅享9(骁龙450,2017年中低端)
OpenGL:5131 ms
Java:11616 ms
荣耀V9(麒麟960,2016中高端)
OpenGL:2100 ms
Java:4319 ms
硕王MX670pro(CPU手机里写骁龙8+,2022年中高端,不过非大品牌手机真实性存疑)
OpenGL:1050 ms
Java:3142 ms
从上述结果看,利用 OpenGL 进行通用计算,确实能有效提升运算速度,算法越复杂,效果会越明显。
八、多重纹理输入输出
上面部分已经实现了计算着色器的基本通用计算,之前只有单输入、单输出。实际上,计算着色器可以有多输入,多输出,可以支持更复杂的算法。
下方例子是传入两个数组,再把两数组的元素各自相加、相乘,并把两个结果输出
着色器
#version 310 es
layout (local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
uniform layout (rgba32f, binding = 0) readonly highp image2D u_inputImage1;
uniform layout (rgba32f, binding = 1) readonly highp image2D u_inputImage2;
uniform layout (rgba32f, binding = 2) writeonly highp image2D u_outputImage1;
uniform layout (rgba32f, binding = 3) writeonly highp image2D u_outputImage2;
void main(){
ivec2 texelCoord = ivec2(gl_GlobalInvocationID.xy);
vec4 inputPixel1 = imageLoad(u_inputImage1, texelCoord);
vec4 inputPixel2 = imageLoad(u_inputImage2, texelCoord);
// 第一个输出相加
float r1 = inputPixel1.r + inputPixel2.r;
float g1 = inputPixel1.g + inputPixel2.g;
float b1 = inputPixel1.b + inputPixel2.b;
float a1 = inputPixel1.a + inputPixel2.a;
// 第二个输出相乘
float r2 = inputPixel1.r * inputPixel2.r;
float g2 = inputPixel1.g * inputPixel2.g;
float b2 = inputPixel1.b * inputPixel2.b;
float a2 = inputPixel1.a * inputPixel2.a;
imageStore(u_outputImage1, texelCoord, vec4(r1, g1, b1, a1));
imageStore(u_outputImage2, texelCoord, vec4(r2, g2, b2, a2));
}
java绑定纹理
/** 缓冲区 */
private int[] fFrame = new int[1];
/** 4个纹理,2个输入2个输出 */
private int[] fTexture = new int[4];
public void onSurfaceCreated() {
// ...
GLES31.glGenTextures(fTexture.length, fTexture, 0);
GLES31.glGenFramebuffers(1, fFrame, 0);
}
private int lastWidth, lastHeight;
private void bindTexture() {
if(lastWidth != srcWidth || lastHeight != srcHeight) {
// 绑定输入纹理
GLES31.glBindTexture(GLES31.GL_TEXTURE_2D, fTexture[0]);
GLES31.glTexStorage2D(GLES31.GL_TEXTURE_2D, 1, GLES31.GL_RGBA32F, srcWidth, srcHeight);
GLES31.glBindImageTexture(0, fTexture[0], 0, false, 0, GLES31.GL_READ_ONLY, GLES31.GL_RGBA32F);
GLES31.glBindTexture(GLES31.GL_TEXTURE_2D, fTexture[1]);
GLES31.glTexStorage2D(GLES31.GL_TEXTURE_2D, 1, GLES31.GL_RGBA32F, srcWidth, srcHeight);
GLES31.glBindImageTexture(1, fTexture[1], 0, false, 0, GLES31.GL_READ_ONLY, GLES31.GL_RGBA32F);
// 绑定输出纹理
GLES31.glBindTexture(GLES31.GL_TEXTURE_2D, fTexture[2]);
GLES31.glTexStorage2D(GLES31.GL_TEXTURE_2D, 1, GLES31.GL_RGBA32F, srcWidth, srcHeight);
GLES31.glBindImageTexture(2, fTexture[2], 0, false, 0, GLES31.GL_WRITE_ONLY, GLES31.GL_RGBA32F);
GLES31.glBindTexture(GLES31.GL_TEXTURE_2D, fTexture[3]);
GLES31.glTexStorage2D(GLES31.GL_TEXTURE_2D, 1, GLES31.GL_RGBA32F, srcWidth, srcHeight);
GLES31.glBindImageTexture(3, fTexture[3], 0, false, 0, GLES31.GL_WRITE_ONLY, GLES31.GL_RGBA32F);
// 绑定缓冲区
GLES31.glBindFramebuffer(GLES31.GL_FRAMEBUFFER, fFrame[0]);
GLES31.glFramebufferTexture2D(GLES31.GL_FRAMEBUFFER, GLES31.GL_COLOR_ATTACHMENT0,
GLES31.GL_TEXTURE_2D, fTexture[2], 0); // GL_COLOR_ATTACHMENT0 用来读输出值1
GLES31.glFramebufferTexture2D(GLES31.GL_FRAMEBUFFER, GLES31.GL_COLOR_ATTACHMENT1,
GLES31.GL_TEXTURE_2D, fTexture[3], 0); // GL_COLOR_ATTACHMENT1 用来读输出值2
GLES31.glFramebufferTexture2D(GLES31.GL_FRAMEBUFFER, GLES31.GL_COLOR_ATTACHMENT2,
GLES31.GL_TEXTURE_2D, fTexture[0], 0); // GL_COLOR_ATTACHMENT2 用来读输入值1
lastWidth = srcWidth;
lastHeight = srcHeight;
}
GLES31.glBindTexture(GLES31.GL_TEXTURE_2D, fTexture[0]);
FloatBuffer buffer1 = FloatBuffer.wrap(srcData1);
GLES31.glTexSubImage2D(GLES31.GL_TEXTURE_2D, 0, 0, 0, srcWidth, srcHeight, GLES31.GL_RGBA, GLES31.GL_FLOAT, buffer1);
GLES31.glBindTexture(GLES31.GL_TEXTURE_2D, fTexture[1]);
FloatBuffer buffer2 = FloatBuffer.wrap(srcData2);
GLES31.glTexSubImage2D(GLES31.GL_TEXTURE_2D, 0, 0, 0, srcWidth, srcHeight, GLES31.GL_RGBA, GLES31.GL_FLOAT, buffer2);
}
读取计算结果
// 读输出1
FloatBuffer buf1 = FloatBuffer.wrap(data.dest1);
GLES31.glReadBuffer(GLES31.GL_COLOR_ATTACHMENT0);
GLES20.glReadPixels(0, 0, data.width, data.height, GLES20.GL_RGBA, GLES20.GL_FLOAT, buf1);
// 读输出2
FloatBuffer buf2 = FloatBuffer.wrap(data.dest2);
GLES31.glReadBuffer(GLES31.GL_COLOR_ATTACHMENT1);
GLES20.glReadPixels(0, 0, data.width, data.height, GLES20.GL_RGBA, GLES20.GL_FLOAT, buf2);
// 输入值也能按这种方式读
FloatBuffer buf3 = FloatBuffer.allocate(data.src1.length);
GLES31.glReadBuffer(GLES31.GL_COLOR_ATTACHMENT2);
GLES20.glReadPixels(0, 0, data.width, data.height, GLES20.GL_RGBA, GLES20.GL_FLOAT, buf3);
最终计算结果如下

总结
我们已经利用OpenGL的计算着色器实现了通用计算,对比java的计算确实有显著的性能提升。
目前主要的性能瓶颈是读取计算结果的过程,实际在GPU的计算是一瞬间完成的,所以要实际使用时要尽量在一次的计算任务中完成所有的事,减少CPU与GPU的数据交换。可以利用多纹理输入、输出来减少数据读取频率。
GPU中还可以在计算单元内共享数据,以处理更复杂的算法;这一点我目前还没尝试过,这是我下一步要研究的目标。
2966

被折叠的 条评论
为什么被折叠?



