OpenGL.Shader:3-GPU纹理动画,顶点/片元着色器再学习

2023-10-29 00:30

本文主要是介绍OpenGL.Shader:3-GPU纹理动画,顶点/片元着色器再学习,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

OpenGL.Shader:3-GPU纹理动画,顶点/片元着色器再学习

 

先放项目地址:https://github.com/MrZhaozhirong/NativeCppApp  还有本篇内容的效果图

这篇文章开始,正式开展OpengGL.Shader的知识。由浅析的效果到深入的理论一步步的去解剖GLSL。

继上一篇OpenGL.Shader:2文章,我们已经可以完成了一个正方体的贴图。如左上图所示,其中的基础知识点运用的是OpenGL.ES在Android上的简单实践:11-全景(索引-深度测试)我们简单看看一下Cpp版本的CubeIndex

CubeIndex::CubeIndex() {modelMatrix = new float[16];CELL::Matrix::setIdentityM(modelMatrix, 0);CUBE_VERTEX_DATA = new int8_t[60];int8_t * p = CUBE_VERTEX_DATA;p[0]=-1;   p[1]=1;    p[2]=1;    p[3]=0;   p[4]=0;p[5]=1;    p[6]=1;    p[7]= 1;   p[8]=1;   p[9]=0;p[10]=-1;  p[11]=-1;  p[12]= 1;  p[13]=0;  p[14]=1;p[15]=1;   p[16]=-1;  p[17]= 1;  p[18]=1;  p[19]=1;p[20]=-1;  p[21]= 1;  p[22]=-1;  p[23]=1;  p[24]=0;p[25]=1;   p[26]=1;   p[27]=-1;  p[28]=0;  p[29]=0;p[30]=-1;  p[31]=-1;  p[32]=-1;  p[33]=1;  p[34]=1;p[35]=1;   p[36]=-1;  p[37]=-1;  p[38]=0;  p[39]=1;p[40]=-1;  p[41]= 1;  p[42]=-1;  p[43]=0;  p[44]=0;p[45]=1;   p[46]=1;   p[47]=-1;  p[48]=1;  p[49]=0;p[50]=-1;  p[51]=1;   p[52]=1;   p[53]=0;  p[54]=1;p[55]=1;   p[56]=1;   p[57]= 1;  p[58]=1;  p[59]=1;//{//        //x,   y,  z    s, t,//        -1,   1,   1,   0, 0,  // 0 left top near//        1,   1,   1,    0, 1,  // 1 right top near//        -1,  -1,   1,   1, 0,  // 2 left bottom near//        1,  -1,   1,    1, 1,  // 3 right bottom near//        -1,   1,  -1,   1, 0,  // 4 left top far//        1,   1,  -1,    0, 0,  // 5 right top far//        -1,  -1,  -1,   1, 1,  // 6 left bottom far//        1,  -1,  -1,    1, 0,  // 7 right bottom far//        这样安排的纹理坐标点,四周是正常的,但是顶底是不正常,//        所以顶底要重新安排一组//        -1,   1,  -1,   0, 0,  // 8  left top far//        1,   1,  -1,    1, 0,  // 9  right top far//        -1,   1,   1,   0, 1,  // 10 left top near//        1,   1,   1,    1, 1,  // 11 right top near//};CUBE_INDEX = new int8_t[24];CUBE_INDEX[0 ]= 8;  CUBE_INDEX[1 ]= 9;  CUBE_INDEX[2 ]=10;  CUBE_INDEX[3 ]=11;CUBE_INDEX[4 ]= 6;  CUBE_INDEX[5 ]= 7;  CUBE_INDEX[6 ]=2;   CUBE_INDEX[7 ]=3;CUBE_INDEX[8 ]= 0;  CUBE_INDEX[9 ]= 1;  CUBE_INDEX[10]=2;   CUBE_INDEX[11]=3;CUBE_INDEX[12]= 4;  CUBE_INDEX[13]= 5;  CUBE_INDEX[14]=6;   CUBE_INDEX[15]=7;CUBE_INDEX[16]= 4;  CUBE_INDEX[17]= 0;  CUBE_INDEX[18]=6;   CUBE_INDEX[19]=2;CUBE_INDEX[20]= 1;  CUBE_INDEX[21]= 5;  CUBE_INDEX[22]=3;   CUBE_INDEX[23]=7;//{//    //top//    8,9,10,11,//    //bottom//    6,7,2,3//    //front//    0,1,2,3,//    //back//    4,5,6,7,//    //left//    4,0,6,2,//    //right//    1,5,3,7,//};
}CubeIndex::~CubeIndex() {delete [] CUBE_VERTEX_DATA;delete [] CUBE_INDEX;delete [] modelMatrix;
}void CubeIndex::bindData(CubeShaderProgram* shaderProgram) {glVertexAttribPointer(static_cast<GLuint>(shaderProgram->aPositionLocation),POSITION_COMPONENT_COUNT, GL_BYTE,GL_FALSE, STRIDE,CUBE_VERTEX_DATA);glEnableVertexAttribArray(static_cast<GLuint>(shaderProgram->aPositionLocation));glVertexAttribPointer(static_cast<GLuint>(shaderProgram->aTexUvLocation),TEXTURE_COORDINATE_COMPONENT_COUNT, GL_BYTE,GL_FALSE, STRIDE,&CUBE_VERTEX_DATA[POSITION_COMPONENT_COUNT]);glEnableVertexAttribArray(static_cast<GLuint>(shaderProgram->aTexUvLocation));
}void CubeIndex::draw() {// 正方体 六个面,每个面两个三角形,每个三角形三个点//glDrawElements(GL_TRIANGLES, 6*2*3, GL_UNSIGNED_BYTE, CUBE_INDEX );// 正方体 六个面,每个面四个点glDrawElements(GL_TRIANGLE_STRIP, 6*4, GL_UNSIGNED_BYTE, CUBE_INDEX );
}

简单描述一下代码:

数组CUBE_VERTEX_DATA存放的是11个位置的点坐标(x,y,z)和纹理坐标数据(s,t),其中4和8是同一个位置但不同纹理坐标,同理5和9,0和10,1和11。为啥纹理是不一样呢,搞不懂的同学画个草图匹对一下纹理坐标的位置就知道了,这里不展开讨论。

CUBE_INDEX存放的是组成每个面的4个点位置的索引。以前我们画的是三角形(GL_TRIANGLES)这次我们再细微的优化,画的是三角带(GL_TRIANGLE_STRIP),省下了36-24=12个点。

别少看这12个点,接下来就开始进入Shader的第一个基础知识,着色器渲染流程。

 

渲染管道的执行流程

渲染一个正方体,你是否清楚的知道,渲染的执行流程是怎样?顶点着色器(VertexShader)被执行多少次?片元着色器(FragmentShader)又会被执行多少次?首先我们来看看下图:

如图所示,OpengGL的API和着色器工作流程:1,通过OpenGL客户端的API(就是我们编写的代码)把各种顶点数据传到内存/GPU显存;2、顶点着色器经过原始程序集之后,分配到对应的顶点数据;3、光栅化,即正方体经过MVP矩阵映射到屏幕上之后,变成了一个类似菱形的画图区域;4、片段着色器计算每个片元的渲染操作,确定这个正方体对应的点上究竟要显示什么颜色值;5、渲染画面并输出到帧缓冲区用于显示。

好了,哔哔了一堆理fei论hua。在这个例子上,我们的顶点着色器被执行多少次?答案就是glDrawXXXXX的count参数!当画的是三角形(GL_TRIANGLES)的时候,顶点着色器被执行36次;当画的是三角带(GL_TRIANGLE_STRIP)的时候,顶点着色器被执行24次。明白了上面所说的不要少看这些细微的差别,想象一下农药的王者峡谷,少则成百多则上千的渲染对象,每个对象的那怕减少10个渲染点,1k个对象就是减少1w次顶点着色器的执行次数,那性能得优化多少呢?

CubeShaderProgram::CubeShaderProgram()
{const char * vertexShaderResourceStr = const_cast<char *>(" uniform mat4    u_Matrix;\n\attribute vec4  a_Position;\n\attribute vec2  a_uv;\n\varying vec2    out_uv;\n\void main()\n\{\n\out_uv = a_uv;\n\gl_Position = u_Matrix * a_Position;\n\}");const char * fragmentShaderResourceStr= const_cast<char *>("precision mediump float;\n\uniform sampler2D _texture;\n\varying vec2      out_uv;\n\void main()\n\{\n\gl_FragColor = texture2D(_texture, out_uv);\n\}");programId = ShaderHelper::buildProgram(vertexShaderResourceStr, fragmentShaderResourceStr);uMatrixLocation     = glGetUniformLocation(programId, "u_Matrix");aPositionLocation   = glGetAttribLocation(programId, "a_Position");aTexUvLocation      = glGetAttribLocation(programId, "a_uv");uTextureUnit        = glGetUniformLocation(programId, "_texture");
}void CubeShaderProgram::setUniforms(float* matrix){glUniformMatrix4fv(uMatrixLocation, 1, GL_FALSE, matrix);
}
CubeShaderProgram::~CubeShaderProgram() {}

配合vertexShaderResourceStr 继续加深上段话的理解。glDrawElements(GL_TRIANGLE_STRIP, 6*4, GL_UNSIGNED_BYTE, CUBE_INDEX );  触发顶点数据传送到顶点着色器程序,第一个顶点(索引0)attribute vec4  a_Position = {-1,1,1}  attribute vec2  a_uv = {0,0}; 经过自定义的逻辑计算之后,通过内置变量把相关数据传送到相应的片元着色器。 第二个顶点(索引1)attribute vec4  a_Position = {1,1,1}  attribute vec2  a_uv = {0,1};  到执行第三个第四个顶点,满足组成一个三角形带,就会触发片元着色,但是不等于就执行一次片元着色器程序!

所以到了片元着色,执行多少次片元着色器程序?这个还真说不准。what?!裤子都*了你跟我说这个?说是说不准,但我可以用张图表示明白。

第一个三角带触发的片元着色,其片元着色器程序的执行次数就取决于上图黄色区域中有多少个着色点。着色点和像素点差不多,但又有点区别,像素点是针对屏幕的,着色点是对gpu的渲染管道的,一个像素点可能包含大于1个的着色点。

如果把这个正方体的模型矩阵缩放一定比例,它在屏幕的显示就会变小,当前帧渲染的片元着色器执行次数就会减少;以现在这个摄像机位置的视图矩阵,打开深度检测之后,底部和背部的面是不会渲染的,所以对应的三角形带不会触发着色,自然对应的片元着色程序就没有执行了。

 

GPU纹理动画

理论知识介绍完毕,那么进入实战练习,开篇右侧的效果要怎么实现?有同学会提出这样的解决方案,随着时间的变化,不断的更新纹理,以带到动画的效果。这确实是一个可行的方案,但缺点也明显。如果周期的动画帧图太多,资源包占用物理空间会增加,操作内存->GPU显存的资源也会增多。    这里介绍另外一种更高效的方法,在着色器操作纹理动画的播放。正常的2D,2.5D游戏都是用这种方法实现人物的动作动画。

首先借助linux的 gettimeofday 函数能获取准备的应用运行时间,简单的封装成CELL::TimeCounter。然后在之前GLThread的renderOnDraw回调增加运行时间的参数。相关代码如下:

void *glThreadImpl(void *context)
{GLThread *glThread = static_cast<GLThread *>(context);CELL::TimeCounter tm;while(true){// ... ...double  second  =   tm.getElapsedTimeInMilliSec();if(glThread->isStart){//LOGD("GLThread onDraw.");glThread->mRender->renderOnDraw(second);}//tm.update();// 不update就是计算整个应用的运行时长// update之后,计算清零,用于获取代码间执行的时长}return 0;
}

然后加载以下这张资源图片到纹理缓冲区。

这是一张合成图,把一个周期的动画所需要的帧图都整齐的排列到一起。不一定要求横列数一样,但是必须要是满行满列的数目。看到这张图,我想大家应该都懂得接下来我要介绍的方法了,就是随着时间的变化,改变纹理坐标,来显示当前不同行列的纹理区域,在不替换纹理ID的前提下,达到显示动画的效果。

首先第一个问题,随时间的推移,怎么确定当前是在第几个帧图?

void NativeGLRender::renderOnDraw(double elpasedInMilliSec)
{if (mEglCore==NULL || mWindowSurface==NULL) {LOGW("Skipping drawFrame after shutdown");return;}mWindowSurface->makeCurrent();glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);double elpasedInSec = elpasedInMilliSec/1000; // 运行时间毫秒转为秒// 若以1秒为一个周期,播放完所有帧图,即当elpasedInSec==1,纹理位置索引是row*col==16// 若以2秒为一个周期,播放完所有帧图,即当elpasedInSec==2,纹理位置索引是row*col==16// 所以要用运行时间 / 周期时间 * (row*col)= 当前纹理索引int  cycleTimeInSec = 1;// 1秒后,纹理位置索引归0,所以要mod上(row*col)防止索引越界int    frame        = int(elpasedInSec/cycleTimeInSec * 16)%16;gpuAnimationProgram->ShaderProgram::userProgram();glActiveTexture(GL_TEXTURE0);glBindTexture(GL_TEXTURE_2D, animation_texure);glUniform1i(gpuAnimationProgram->uTextureUnit, 0);CELL::Matrix::multiplyMM(modelViewProjectionMatrix, viewProjectionMatrix, cube->modelMatrix);gpuAnimationProgram->setMVPUniforms(modelViewProjectionMatrix);gpuAnimationProgram->setAnimUniforms(4,4,frame);cube->bindData(gpuAnimationProgram);cube->draw();mWindowSurface->swapBuffers();
}

其实背后的数学道理也比较简单,已经写在注释里面,不懂的话,em ... 那也没办法了。之后就是一些模板代码:启动着色器,绑定纹理,绑定mvp矩阵,绑定顶点数据,启动渲染。

下一步就是分析本篇的主角:GPUAnimationProgram 

GPUAnimationProgram::GPUAnimationProgram()
{const char * vertexShaderResourceStr = const_cast<char *> ("uniform mat4    u_Matrix;\n\attribute vec4  a_Position;\n\uniform vec3    u_AnimInfor;\n\attribute vec2  a_uv;\n\varying vec2    out_uv;\n\void main()\n\{\n\float uS  =  1.0/u_AnimInfor.y;\n\float vS  =  1.0/u_AnimInfor.x;\n\out_uv    =  a_uv * vec2(uS,vS);\n\float  row  =  int(u_AnimInfor.z)/int(u_AnimInfor.y);\n\float  col  =  mod((u_AnimInfor.z), (u_AnimInfor.x));\n\out_uv.x    +=  float(col) * uS;\n\out_uv.y    +=  float(row) * vS;\n\gl_Position = u_Matrix * a_Position;\n\}");const char * fragmentShaderResourceStr= const_cast<char *>("precision mediump float;\n\uniform sampler2D _texture;\n\varying vec2      out_uv;\n\void main()\n\{\n\vec4 texture_color = texture2D(_texture, out_uv);\n\vec4 background_color = vec4(1.0, 1.0, 1.0, 1.0);\n\gl_FragColor = mix(background_color,texture_color, 0.9);\n\}");programId = ShaderHelper::buildProgram(vertexShaderResourceStr, fragmentShaderResourceStr);uMatrixLocation     = glGetUniformLocation(programId, "u_Matrix");uAnimInforLocation  = glGetUniformLocation(programId, "u_AnimInfor");aPositionLocation   = glGetAttribLocation(programId,  "a_Position");aTexUvLocation      = glGetAttribLocation(programId,  "a_uv");uTextureUnit        = glGetUniformLocation(programId, "_texture");
}void GPUAnimationProgram::setMVPUniforms(float* matrix){glUniformMatrix4fv(uMatrixLocation, 1, GL_FALSE, matrix);
}void GPUAnimationProgram::setAnimUniforms(int row,int col,int frame){glUniform3f(uAnimInforLocation, row, col, frame);
}

接下来开始着手 顶点着色器程序,跟着注释一行行的分析。

uniform vec3    u_AnimInfor;  //(1)
// 新增的一个自定义的输入变量,类似为vec3(x,y,z),
// 其中在客户端可以使用glUniform3f (GLint location, GLfloat v0, GLfloat v1, GLfloat v2); 指定其填充的元素值
// 这里代表(row,col,frame),其中row和col是固定数值,就是上方网格图的行列数,
// frame为动态变化的当前纹理索引位置,就是上方4*4网格图中,对应当前是哪个一格。
uniform mat4    u_Matrix;
attribute vec4  a_Position;
attribute vec2  a_uv;
varying vec2    out_uv;
void main()
{
      float uS  =  1.0/u_AnimInfor.y;
      float vS  =  1.0/u_AnimInfor.x;
      out_uv    =  a_uv * vec2(uS,vS); // (2)
      // 正常的输入纹理坐标是整张图的,换成合成图之后,我们需要根据行列的比例缩小其纹理坐标
      // 纹理的横坐标u,是要乘以 1/col,纵坐标v,是要乘以 1/row
      int  row  =  int(u_AnimInfor.z)/int(u_AnimInfor.y);
      float  col  =  mod((u_AnimInfor.z), (u_AnimInfor.x));
      // 然后计算当前索引位置具体是排在多少行多少列的位置。
      out_uv.x    +=  float(col) * uS; // 横坐标,偏移量是多少列
      out_uv.y    +=  float(row) * vS; //纵坐标,偏移是多少行
      gl_Position = u_Matrix * a_Position;
}

我想注释应该已经很清楚了,反正就是要注意纹理坐标的偏移计算,一开始我自己也混乱了几分钟。不过一意识到注意点之后就很好解决了。顶点着色器程序就分析到这里,然后就片元着色器程序。

precision mediump float;
uniform sampler2D _texture;
varying vec2      out_uv;
void main()
{
   vec4 texture_color = texture2D(_texture, out_uv); // 求出正常的纹理着色值
   vec4 background_color = vec4(1.0, 1.0, 1.0, 1.0); // 另起一个白色的颜色值
   gl_FragColor = mix(background_color,texture_color, 0.9); // 两者颜色值进行混合 
   // 要不然完全透明的的正方体在黑色背景下是完全看不出轮廓
}

这里提一下,GLSL的内置函数:T mix(T x, T y, float a) 取x,y的线性混合,其计算公式是 x*(1-a)+y*a。

在一些很低的版本需要在OpenGL.ES的API启动混合功能,即:glEnable(GL_BLEND);           

混合的其他知识可以到 OpenGL.ES在Android上的简单实践:20-水印录制(预览+透明水印 表情 弹幕 gl_blend)继续学习。

这篇关于OpenGL.Shader:3-GPU纹理动画,顶点/片元着色器再学习的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/297021

相关文章

Go学习记录之runtime包深入解析

《Go学习记录之runtime包深入解析》Go语言runtime包管理运行时环境,涵盖goroutine调度、内存分配、垃圾回收、类型信息等核心功能,:本文主要介绍Go学习记录之runtime包的... 目录前言:一、runtime包内容学习1、作用:① Goroutine和并发控制:② 垃圾回收:③ 栈和

Android学习总结之Java和kotlin区别超详细分析

《Android学习总结之Java和kotlin区别超详细分析》Java和Kotlin都是用于Android开发的编程语言,它们各自具有独特的特点和优势,:本文主要介绍Android学习总结之Ja... 目录一、空安全机制真题 1:Kotlin 如何解决 Java 的 NullPointerExceptio

conda安装GPU版pytorch默认却是cpu版本

《conda安装GPU版pytorch默认却是cpu版本》本文主要介绍了遇到Conda安装PyTorchGPU版本却默认安装CPU的问题,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的... 目录一、问题描述二、网上解决方案罗列【此节为反面方案罗列!!!】三、发现的根本原因[独家]3.1 p

Kotlin Compose Button 实现长按监听并实现动画效果(完整代码)

《KotlinComposeButton实现长按监听并实现动画效果(完整代码)》想要实现长按按钮开始录音,松开发送的功能,因此为了实现这些功能就需要自己写一个Button来解决问题,下面小编给大... 目录Button 实现原理1. Surface 的作用(关键)2. InteractionSource3.

使用WPF实现窗口抖动动画效果

《使用WPF实现窗口抖动动画效果》在用户界面设计中,适当的动画反馈可以提升用户体验,尤其是在错误提示、操作失败等场景下,窗口抖动作为一种常见且直观的视觉反馈方式,常用于提醒用户注意当前状态,本文将详细... 目录前言实现思路概述核心代码实现1、 获取目标窗口2、初始化基础位置值3、创建抖动动画4、动画完成后

使用animation.css库快速实现CSS3旋转动画效果

《使用animation.css库快速实现CSS3旋转动画效果》随着Web技术的不断发展,动画效果已经成为了网页设计中不可或缺的一部分,本文将深入探讨animation.css的工作原理,如何使用以及... 目录1. css3动画技术简介2. animation.css库介绍2.1 animation.cs

重新对Java的类加载器的学习方式

《重新对Java的类加载器的学习方式》:本文主要介绍重新对Java的类加载器的学习方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、介绍1.1、简介1.2、符号引用和直接引用1、符号引用2、直接引用3、符号转直接的过程2、加载流程3、类加载的分类3.1、显示

判断PyTorch是GPU版还是CPU版的方法小结

《判断PyTorch是GPU版还是CPU版的方法小结》PyTorch作为当前最流行的深度学习框架之一,支持在CPU和GPU(NVIDIACUDA)上运行,所以对于深度学习开发者来说,正确识别PyTor... 目录前言为什么需要区分GPU和CPU版本?性能差异硬件要求如何检查PyTorch版本?方法1:使用命

Java学习手册之Filter和Listener使用方法

《Java学习手册之Filter和Listener使用方法》:本文主要介绍Java学习手册之Filter和Listener使用方法的相关资料,Filter是一种拦截器,可以在请求到达Servl... 目录一、Filter(过滤器)1. Filter 的工作原理2. Filter 的配置与使用二、Listen

Java进阶学习之如何开启远程调式

《Java进阶学习之如何开启远程调式》Java开发中的远程调试是一项至关重要的技能,特别是在处理生产环境的问题或者协作开发时,:本文主要介绍Java进阶学习之如何开启远程调式的相关资料,需要的朋友... 目录概述Java远程调试的开启与底层原理开启Java远程调试底层原理JVM参数总结&nbsMbKKXJx