Android|OpenGL ES3.1使用计算着色器(Compute Shader)

OpenGL ES3.1使用计算着色器(Compute Shader) 1.基本介绍 OpenGL ES从3.1版本开始支持计算着色器
工作模型有全局工作组和本地工作组,全局工作组包含由三维的本地工作组组成,本地工作组也由三个维度组成。本地工作组三个维度大小分别为:local_size_xlocal_size_ylocal_size_z
一个什么都不做的着色器最基本代码:

#version 310 es layout(local_size_x=32,local_size_y=32) in; //设置本地工作组大小,local_size_z未设置默认为1 void main(void){ //执行代码 }

2.创建、编译和链接计算着色器 创建计算着色器和顶点着色器、片元着色器过程差不多。
shader=glCreateShader(GL_COMPUTE_SHADER); //使用GL_COMPUTE glShaderSource(shader,1,sourece,NULL); glCompileShader(shader); program=glCreateProgram(); glAttachShader(program,shader); glLinkProgram(program);

3.执行计算着色器 当然要先使用glUseProgram(program)
3.1使用glDispatchCompute(GLuint num_groups_x,GLuint num_groups_y,GLuint num_groups_z)设置全局工作组大小并且执行计算着色器。
3.2使用glDispatchComputeIndirect(GLintptr indirect)使用存储在缓冲区对象参数来设置全局工作组大小并且执行计算着色器。
通过绑定GL_DISPATCH_INDIRECT_BUFFER缓冲对象来设置全局参数,缓冲对象由三个无符号整数(GLuint)来设置。indirect参数是当前绑定到 GL_DISPATCH_INDIRECT_BUFFER 目标的缓冲区的字节偏移量。
4.获取着色器属性 glGetProgramiv (GLuint program, GLenum pname, GLint *params)
pname:可为GL_MAX_COMPUTE_WORK_GROUP_SIZE
5.每个计算单元的位置
const uvec3 gl_WorkGroupSize; //本地工作组大小 in uvec3 gl_NumWorkGroups; //全局工作组大小 in uvec3 gl_LocalInvocationID; //表示当前执行单元在本地工作组中的位置 in uvec3 gl_WorkGroupID; //表示当前本地工作组在全局工作组中的位置 in uvec3 gl_GlobalInvocationID; //等于gl_WorkGroupID*gl_WorkGroupSize+gl_LocalInvocationID,所以它是当前执行单元的三维索引 in uint gl_LocalInvocationIndex; //它是gl_LocalInvocationID的一种扁平化方式,等于gl_LocalInvocationID.z*gl_WorkGroupSize.x*gl_WorkGroupSize.y+gl_LocalInvocationID.y*gl_WorkGroupSize.x+gl_LocalInvocationID.x

6.同步 同步类型有两种运行屏障(execution barrier)和内存屏障(memory barrier)
6.1运行屏障(execution barrier)
计算着色器程序中使用barrier()触发,如果计算着色器的一个请求遇到了barrier(),那么它就会停止运行,并等待同一本地工作组的所有请求到达为止,这一点在条件语句中使用尤为需要注意避免死锁。
6.2内存屏障(memory barrier)
最基本的版本就是memoryBarrier(),它可以保证着色器的请求内存的写入操作一定提交到内存端,而不是通过缓冲区(cache)或者调度队列之类的方式。它还可以给着色器编译器做出指示,让它不要对内存操作重排序,以免因此跨越屏障函数。
memoryBarrierAtomicCounter()会等待原子计数器更新,然后继续执行。
memoryBarrierBuffer()和memoryBarrierImage()会等待缓存和图像变量的写入操作完成,然后继续执行。
memoryBarrierShared()会等待带有shared限定符的变量更新,然后继续执行。
7.使用计算着色器写一个百万粒子发射器 7.1着色器代码
【Android|OpenGL ES3.1使用计算着色器(Compute Shader)】顶点着色器
#version 310 es precision mediump image2D; //OpenGL ES要求显示定义image2D的精度 layout (binding = 1,rgba32f) restrict uniform image2D position_buffer; uniform mat4 mvp; out float intensity; //粒子年龄值 void main() { ivec2 size=imageSize(position_buffer); vec4 pos=imageLoad(position_buffer,ivec2(gl_VertexID%size.y,gl_VertexID/size.y)); //将一维坐标转换为图像二维坐标 gl_Position=mvp*vec4(pos.xyz,1.0f); intensity=pos.w; }

片元着色器
#version 310 es precision mediump float; out vec4 color; //这个值来自顶点着色器中读取的粒子年龄值 in float intensity; void main(void){ //根据粒子的年龄,在热红色到冷蓝色之间的混合结果 color=mix(vec4(0.0f,0.2f,1.0f,1.0f),vec4(0.2f,0.05f,0.0f,1.0f),intensity); }

计算着色器
#version 310 es precision mediump image2D; //OpenGL ES要求显示定义image2D的精度 //uniform块中包含引力器的位置和质量 layout (std140,binding=0) uniform attractor_block{ vec4 attractor[64]; //xyz=position,w=mass }; //每块中粒子的数量为128 layout (local_size_x=128) in; //使用两个缓冲来包含粒子的位置和速度信息 layout (binding = 0,rgba32f) restrict uniform image2D velocity_buffer; layout (binding = 1,rgba32f) restrict uniform image2D position_buffer; //时间间隔 uniform float dt; void main(void){ //从缓存中读取当前的位置和速度 ivec2 size=imageSize(velocity_buffer); ivec2 p=ivec2(int(gl_GlobalInvocationID.x)/size.y,int(gl_GlobalInvocationID.x)%size.y); vec4 vel=imageLoad(velocity_buffer,p); vec4 pos=imageLoad(position_buffer,p); int i; //使用当前速度x时间来更新位置 pos.xyz += vel.xyz * dt; pos.w -= 0.0001 * dt; //对于每个引力器 for (i = 0; i < 4; i++) { //计算受力并更新速度 vec3 dist = (attractor[i].xyz - pos.xyz); vel.xyz += dt * dt * attractor[i].w * normalize(dist) / (dot(dist, dist) + 10.0); } //如果粒子已经过期,那么重置它 if (pos.w <= 0.0) { pos.xyz = vec3(0.0f); vel.xyz *= 0.01; pos.w += 1.0f; }//将新的位置和速度信息重新保存在缓存中 imageStore(position_buffer,p,pos); imageStore(velocity_buffer,p,vel); }

7.2粒子系统初始化
srand(clock()); glEnable(GL_BLEND); glBlendFunc(GL_ONE, GL_ONE); //激活计算着色器,绑定位置和速度缓存 compute_prog = GLTools::createComputeProgramAtAsset(aAssetManager,"compute.glsl"); //渲染设置 render_prog = GLTools::createProgramAtAsset(aAssetManager, "vertex.glsl","fragment.glsl"); glUseProgram(render_prog); /*//我在windows上模拟时需要创建、绑定vao才能正常运行,在android上没有vao也行,现在还不知道为什么 glGenVertexArrays(1,&vao); glBindVertexArray(vao); */ glGenTextures(2, tbos); glBindTexture(GL_TEXTURE_2D,position_tbo); glm::vec4 *pos = new glm::vec4[PARTICLE_COUNT]; for (int i = 0; i < PARTICLE_COUNT; ++i) { pos[i] = glm::vec4(random_vector(-1.0f, 1.0f), randomFloat()); } glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA32F,PARTICLE_GROUP_COUNT,PARTICLE_GROUP_SIZE,0,GL_RGBA,GL_FLOAT,pos); GLTools::getGLError(); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); delete[] pos; glBindTexture(GL_TEXTURE_2D,velocity_tbo); glm::vec4 *velocities = new glm::vec4[PARTICLE_COUNT]; for (int i = 0; i < PARTICLE_COUNT; ++i) { velocities[i] = glm::vec4(random_vector(-10.0f, 10.0f), 0.0f); } glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA32F,PARTICLE_GROUP_COUNT,PARTICLE_GROUP_SIZE,0,GL_RGBA,GL_FLOAT,velocities); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glGenBuffers(1, &attractor_buffer); glBindBuffer(GL_UNIFORM_BUFFER, attractor_buffer); glBufferData(GL_UNIFORM_BUFFER, 32 * sizeof(glm::vec4), NULL, GL_STATIC_DRAW);

7.3绘制
//执行计算着色器 GLTools::getGLError(); glUseProgram(compute_prog); glBindBuffer(GL_UNIFORM_BUFFER, attractor_buffer); for (int i = 0; i < MAX_ATTRACTORS; i++) { attractor_masses[i] = 0.5f + randomFloat()* 0.5f; }glBindBufferBase(GL_UNIFORM_BUFFER, 0, attractor_buffer); //更新引力器位置和质量的缓存 float time = clock()/float(CLOCKS_PER_SEC); glm::vec4* attractors = (glm::vec4*)glMapBufferRange(GL_UNIFORM_BUFFER, 0, 32 * sizeof(glm::vec4), GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT); for (int i = 0; i < 32; ++i) { attractors[i] = glm::vec4(sinf(time*float(i + 4)*7.5f*20.0f)*50.0f, cosf(time*float(i + 7)*3.9f*20.0f)*50.0f, sinf(time*float(i + 3)*5.3f*20.0f)*cosf(time*float(i + 5)*9.1f)*100.0f, attractor_masses[i]); } glUnmapBuffer(GL_UNIFORM_BUFFER); //设置时间间隔 glUniform1f(glGetUniformLocation(compute_prog, "dt"), 0.2f); glBindImageTexture(0, velocity_tbo, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F); glBindImageTexture(1, position_tbo, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F); glDispatchCompute(PARTICLE_COUNT, 1, 1); //确保计算着色器的写入操作已经完成 glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); // glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glUseProgram(render_prog); glDrawArrays(GL_POINTS, 0, PARTICLE_COUNT);

窗口大小改变时执行
aspect_ratio = (float)w / (float)h; //设置模型视图和投影矩阵 // 实时改变 MVP 矩阵 glm::mat4 projection = glm::perspective(glm::radians(45.0f), aspect_ratio, 0.1f, 1000.0f); glm::mat4 mvp = projection * glm::lookAt(glm::vec3(0, 0, 100.0f), glm::vec3(0, 0, 0), glm::vec3(0, 1.0f, 0)); GLuint t = glGetUniformLocation(render_prog, "mvp"); glUniformMatrix4fv(t, 1, GL_FALSE, glm::value_ptr(mvp));

杂项 使用的glm库是一个头文件库,只需要下载后复制到项目或者添加到环境变量就可以直接使用。
一些全局变量和静态函数:
GLuint compute_prog, render_prog; // TBOs union { struct { GLuint position_tbo; GLuint velocity_tbo; }; GLuint tbos[2]; }; // 吸引器 UBO GLuintattractor_buffer; GLuint vao; // Mass 吸引子的质量 float attractor_masses[MAX_ATTRACTORS]; float aspect_ratio = 1.0f; static float randomFloat() { float res; unsigned int tmp; static unsigned int seed = 0xFFFF0C59; seed *= 16807; tmp = seed ^ (seed >> 4) ^ (seed << 15); *((unsigned int *)&res) = (tmp >> 9) | 0x3F800000; return (res - 1.0f); }static glm::vec3 random_vector(float minmag = 0.0f, float maxmag = 1.0f) { glm::vec3 randomvec(randomFloat() * 2.0f - 1.0f, randomFloat() * 2.0f - 1.0f, randomFloat() * 2.0f - 1.0f); randomvec = normalize(randomvec); randomvec *= (randomFloat() * (maxmag - minmag) + minmag); return randomvec; }

我自己封装的android端OpenGL ES创建着色器的辅助函数:
#ifndef OPENGL_GLTOOLS_H #define OPENGL_GLTOOLS_H #ifdef ENABLE_GLES31 #include #elif defined(ENABLE_GLES32) #include #else #include #endif #include #include #include #define GLTOOLS_TAG "gltools" class GLTools {public: static void getGLError() { switch (glGetError()) { case GL_NO_ERROR: return ; case GL_INVALID_ENUM: LOGE("glError","GL Invalid Enum\n"); break; case GL_INVALID_VALUE: LOGE("glError", "GL Invalid Value\n"); break; case GL_INVALID_OPERATION: LOGE("glError", "GL Invalid Operation\n"); break; case GL_OUT_OF_MEMORY: LOGE("glError", "GL Out Of Memory\n"); break; case GL_INVALID_FRAMEBUFFER_OPERATION: LOGE("glError", "GL Invalid FrameBuffer Operation\n"); break; default: LOGE("glError", "err\n"); break; } } static char* readAsset(AAssetManager* pManager,const char* filename){ if(pManager== nullptr||filename== nullptr){ LOGE(GLTOOLS_TAG,"%s空指针异常!",filename); } AAsset* pAsset =AAssetManager_open(pManager,filename,AASSET_MODE_UNKNOWN); if(pAsset== nullptr){ LOGE(GLTOOLS_TAG,"打开文件:%s失败!",filename); return nullptr; } size_t size = AAsset_getLength(pAsset); char* pData= https://www.it610.com/article/nullptr; if( size> 0 ) { pData = https://www.it610.com/article/new char[size+1]; int iRet = AAsset_read( pAsset, pData, size); if( iRet <= 0 ) { AAsset_close(pAsset); delete [] pData; LOGE(GLTOOLS_TAG,"文件:%s读取失败!",filename); return nullptr; } pData[iRet]=0; }else{ LOGE(GLTOOLS_TAG,"文件:%s为空!",filename); return nullptr; } AAsset_close(pAsset); return pData; } static char*readFile(const char*filename){ if(filename == nullptr){ LOGE(GLTOOLS_TAG,"文件名错误!"); return nullptr; } FILE* fp=fopen(filename,"r"); if(fp== nullptr){ LOGE(GLTOOLS_TAG,"文件:%s打开失败!",filename); return nullptr; } fseek(fp,0,SEEK_END); long size=ftell(fp); fseek(fp,0,SEEK_SET); char* data=https://www.it610.com/article/new char[size+1]; data[size]=0; fread(data,size,1,fp); fclose(fp); return data; } static GLuint createShader(GLenum shaderType,const char* shaderCode){ GLuint shader=glCreateShader(shaderType); if(shader==0){ LOGE(GLTOOLS_TAG,"创建着色器失败!\n%s",shaderCode); return 0; } glShaderSource(shader,1,&shaderCode, nullptr); glCompileShader(shader); //编译结果 GLint status; //获取编译结果,并且将结果赋值给status glGetShaderiv(shader,GL_COMPILE_STATUS,&status); if(status!=GL_TRUE){ char buf[256]={}; //顶点着色器编译失败 glGetShaderInfoLog(shader,255, nullptr,buf); LOGE(GLTOOLS_TAG,"编译着色器失败->%s!\n%s",buf,shaderCode); glDeleteShader(shader); return 0; } return shader; } static GLuint createProgram(const char* vertexCode,const char* fragmentCode){ GLuint vertexShader=createShader(GL_VERTEX_SHADER,vertexCode); if(vertexShader==0){ LOGE(GLTOOLS_TAG,"顶点着色器创建失败!"); return 0; } GLuint fragmentShader=createShader(GL_FRAGMENT_SHADER,fragmentCode); if(fragmentShader==0){ LOGE(GLTOOLS_TAG,"片元着色器创建失败!"); return 0; } GLuint program=glCreateProgram(); if(program==0){ LOGE(GLTOOLS_TAG,"着色器程序创建失败!"); glDeleteShader(vertexShader); glDeleteShader(fragmentShader); return 0; } glAttachShader(program,vertexShader); glAttachShader(program,fragmentShader); glLinkProgram(program); //存储链接状态 GLint status; //获取链接状态信息 glGetProgramiv(program,GL_LINK_STATUS,&status); if(status!=GL_TRUE){ GLsizei num; //保存日志 GLchar *log; glGetProgramiv(program,GL_INFO_LOG_LENGTH,&num); log = new char[num+1]; //从日志缓存中取出关于program length个长度的日志,并保存在log中 glGetProgramInfoLog(program,num,&num,log); /* * 在这里查看日志 */ LOGE(GLTOOLS_TAG,"链接程序失败!->%s\n顶点着色器:\n%s\n片元着色器:\n%s",log,vertexCode,fragmentCode); delete[] log; glDeleteShader(vertexShader); glDeleteShader(fragmentShader); glDeleteProgram(program); return 0; } glDetachShader(program, vertexShader); glDeleteShader(vertexShader); glDetachShader(program, fragmentShader); glDeleteShader(fragmentShader); return program; } static GLuint createProgramAtAsset(AAssetManager *pManager, const char*vertexFileName, const char*fragmentFileName){ char*vCode=readAsset(pManager,vertexFileName); char*fCode=readAsset(pManager,fragmentFileName); if(vCode&&fCode){ GLuint program=createProgram(vCode,fCode); delete[] vCode; delete[] fCode; return program; }else{ return 0; }} static GLuint createProgramAtFile(const char*vertexFileName, const char*fragmentFileName){ char*vCode=readFile(vertexFileName); char*fCode=readFile(fragmentFileName); if(vCode&&fCode){ GLuint program=createProgram(vCode,fCode); delete[] vCode; delete[] fCode; return program; }else{ return 0; } } #if defined(ENABLE_GLES31)||defined(ENABLE_GLES32) static GLuint createComputeProgram(const char*calculationCode) { GLuint shader = createShader(GL_COMPUTE_SHADER, calculationCode); GLint status; if (shader == 0) { LOGE(GLTOOLS_TAG, "计算着色器创建失败!\n"); return 0; } GLuint program = glCreateProgram(); if (program == 0) { LOGE(GLTOOLS_TAG, "计算着色器程序创建失败!\n"); glDeleteShader(shader); return 0; } glAttachShader(program, shader); glLinkProgram(program); //存储链接状态 //获取链接状态信息 glGetProgramiv(program, GL_LINK_STATUS, &status); if (status != GL_TRUE) { GLsizei num; //保存日志 GLchar *log; glGetProgramiv(program, GL_INFO_LOG_LENGTH, &num); log = new char[num + 1]; //从日志缓存中取出关于program length个长度的日志,并保存在log中 glGetProgramInfoLog(program, num, &num, log); /* * 在这里查看日志 */ LOGE(GLTOOLS_TAG, "链接程序失败!->%s\n计算着色器:\n%s\n", log, shader); delete[] log; glDeleteShader(shader); glDeleteProgram(program); return 0; } glDetachShader(program, shader); glDeleteShader(shader); return program; } static GLuint createComputeProgramAtFile(const char*calculationPath) { char*fCode = readFile(calculationPath); if (fCode) { GLuint program = createComputeProgram(fCode); delete[] fCode; return program; } else { return 0; } } static GLuint createComputeProgramAtAsset(AAssetManager *pManager,const char*calculationPath) { char*fCode = readAsset(pManager,calculationPath); if (fCode) { GLuint program = createComputeProgram(fCode); delete[] fCode; return program; } else { return 0; } } #endif }; #endif //OPENGL_GLTOOLS_H

结语 你可以自己调整计算着色代码和初始化代码来实现一个你自己的粒子发射器

    推荐阅读