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

相关文章

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

学习hash总结

2014/1/29/   最近刚开始学hash,名字很陌生,但是hash的思想却很熟悉,以前早就做过此类的题,但是不知道这就是hash思想而已,说白了hash就是一个映射,往往灵活利用数组的下标来实现算法,hash的作用:1、判重;2、统计次数;

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

【机器学习】高斯过程的基本概念和应用领域以及在python中的实例

引言 高斯过程(Gaussian Process,简称GP)是一种概率模型,用于描述一组随机变量的联合概率分布,其中任何一个有限维度的子集都具有高斯分布 文章目录 引言一、高斯过程1.1 基本定义1.1.1 随机过程1.1.2 高斯分布 1.2 高斯过程的特性1.2.1 联合高斯性1.2.2 均值函数1.2.3 协方差函数(或核函数) 1.3 核函数1.4 高斯过程回归(Gauss

【学习笔记】 陈强-机器学习-Python-Ch15 人工神经网络(1)sklearn

系列文章目录 监督学习:参数方法 【学习笔记】 陈强-机器学习-Python-Ch4 线性回归 【学习笔记】 陈强-机器学习-Python-Ch5 逻辑回归 【课后题练习】 陈强-机器学习-Python-Ch5 逻辑回归(SAheart.csv) 【学习笔记】 陈强-机器学习-Python-Ch6 多项逻辑回归 【学习笔记 及 课后题练习】 陈强-机器学习-Python-Ch7 判别分析 【学

系统架构师考试学习笔记第三篇——架构设计高级知识(20)通信系统架构设计理论与实践

本章知识考点:         第20课时主要学习通信系统架构设计的理论和工作中的实践。根据新版考试大纲,本课时知识点会涉及案例分析题(25分),而在历年考试中,案例题对该部分内容的考查并不多,虽在综合知识选择题目中经常考查,但分值也不高。本课时内容侧重于对知识点的记忆和理解,按照以往的出题规律,通信系统架构设计基础知识点多来源于教材内的基础网络设备、网络架构和教材外最新时事热点技术。本课时知识

AI Toolkit + H100 GPU,一小时内微调最新热门文生图模型 FLUX

上个月,FLUX 席卷了互联网,这并非没有原因。他们声称优于 DALLE 3、Ideogram 和 Stable Diffusion 3 等模型,而这一点已被证明是有依据的。随着越来越多的流行图像生成工具(如 Stable Diffusion Web UI Forge 和 ComyUI)开始支持这些模型,FLUX 在 Stable Diffusion 领域的扩展将会持续下去。 自 FLU

如何用GPU算力卡P100玩黑神话悟空?

精力有限,只记录关键信息,希望未来能够有助于其他人。 文章目录 综述背景评估游戏性能需求显卡需求CPU和内存系统需求主机需求显式需求 实操硬件安装安装操作系统Win11安装驱动修改注册表选择程序使用什么GPU 安装黑神话悟空其他 综述 用P100 + PCIe Gen3.0 + Dell720服务器(32C64G),运行黑神话悟空画质中等流畅运行。 背景 假设有一张P100-