先放项目地址: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(shaderProgram->aPositionLocation),
POSITION_COMPONENT_COUNT, GL_BYTE,
GL_FALSE, STRIDE,
CUBE_VERTEX_DATA);
glEnableVertexAttribArray(static_cast(shaderProgram->aPositionLocation));
glVertexAttribPointer(static_cast(shaderProgram->aTexUvLocation),
TEXTURE_COORDINATE_COMPONENT_COUNT, GL_BYTE,
GL_FALSE, STRIDE,
&CUBE_VERTEX_DATA[POSITION_COMPONENT_COUNT]);
glEnableVertexAttribArray(static_cast(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(" 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("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显存的资源也会增多。 这里介绍另外一种更高效的方法,在着色器操作纹理动画的播放。正常的2D,2.5D游戏都是用这种方法实现人物的动作动画。
首先借助linux的 gettimeofday 函数能获取准备的应用运行时间,简单的封装成CELL::TimeCounter。然后在之前GLThread的renderOnDraw回调增加运行时间的参数。相关代码如下:
void *glThreadImpl(void *context)
{
GLThread *glThread = static_cast(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 ("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("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)继续学习。