完成效果
2个动画分别使用不同的Plist,最终1个DC完成绘制
最终实现效果:
原理剖析
关于多重纹理,在江南百景图的技术分析里面也有提到
关于PASS是什么?请原谅我不知道,因为我知道Shader里面并没有PASS这玩意,很显然,这是game engine层面抽象出来的概念,同样MESH也是这个道理。
要合批,必须同时满足以下3个条件,缺一不可
- 相同的Blend
- 相同的Texture
- 相同的Shader
如何实现嘞?当然要从OpenGL程序的角度考虑这个问题。
最简单的绘制图片过程
理解OpenGL绘制图片的过程,对理解多重纹理如何实现非常重要!!!
以下都是伪代码,是为了方便理解
- vertex.shader
attribute vec4 a_position; attribute vec2 a_texCoord; attribute vec4 a_color; varying vec4 v_position; varying vec2 v_texCoord; varying vec4 v_fragmentColor; void main() { gl_Position = CC_PMatrix * a_position; v_fragmentColor = a_color; v_texCoord = a_texCoord; v_position = a_position; } 复制代码
- fragment.shader
varying vec2 v_texCoord; varying vec4 v_fragmentColor; varying vec4 v_position; uniform sampler2D texture1; void main() { gl_FragColor = v_fragmentColor * texture2D(texture1, v_texCoord); } 复制代码
- 逻辑代码
// 顶点数据 Vertices vertices=[ [color,position,texCoord], [color,position,texCoord] ]; // 获取属性 int positionLocation = glGetAttribLocation(shader, "a_positon"); // 启用这个属性 glEnableVertextAttribArray(positionLocation); // 从vertices中提取属于postion的数据,绑定到shader中定义的属性 // 我这么写隐藏了非常多的细节,主要是为了方便理解 glVertexAttriPointer(positionLocation, vertices, vertices.position); // ... texCoord, color 属性同理也按照上边的步骤进行设置即可 // 至此,GPU已经知道如何解析顶点数据了,其实这么描述不太准确,但是方便理解 int textureUnit = 0; // 其实texture->getName()就是这个东西 GLint textureName; // 分配纹理单元 glGenTextures(1, &textureName); // 激活纹理单元,注意这里是0号单元 // 如果要使用其他纹理单元,向下边这样累加就行,实际上cocos2dx就是这么玩的 glActiveTexture(GL_TEXTURE0 + textureUnit); // 绑定分配的纹理单元到激活的纹理单元,虽然这时是textureName啥都没得 glBindTexture(GL_TEXTURE2D, textureName); // 以上这几步,纯粹是为了建立映射关系 Bytes imageData="", imageWidth=100, imageHeight=100; glEnable(GL_TEXTURE_2D); // 将纹理数据绑定到上边的纹理单元:textureName // 这个api的用法不是这样子,这样写也是为了方便理解 glTexImage2D(GL_TEXTURE_2D,imageWidth,imageHeight,imageData); // 从fragment中获取texture1变量 int texture = glGetUniformLocation(shader, "texture1"); // 设置每个采样器使用哪个纹理单元,这里我们从头到尾一直都是在操作纹理单元0 glUniform1i(texture, textureUnit); // 完成最终的绘制 glDrawArrays(GL_TRIANGLES); 复制代码
以上的流程挺长的,但这个已经是最小的绘制图片流程了,总结下就是:
- glVertexAttriPointe将顶点数据正确的分配个vertex.shader中的attribute,通过varying将数据传递给fragment.shader
- 激活指定的纹理单元,并填充纹理数据
- 将fragment.shader中的uniform sampler2D,通过glUniform1i和刚刚操作的纹理单元建立映射,这样texture2D的的结果就是我们刚刚填充的纹理数据了
- draw
合批到底是什么?
理解了以上的图片绘制,我们再从OpenGL的角度理解下,到底什么是合批?
上边的绘制图片过程我们封装为一个drawImage
函数,如果我绘制2个图片,那么我们可能会这样做
drawImage(); drawImage(); 复制代码
调用了2次,也就触发了2次OpenGL的draw函数,但是如果一帧调用了大量的draw函数,就会产生性能压力,主要是gl的各种操作、CPU和GPU之间的数据交换,其实都是有代价的,所以你也会发现OpenGL出现了各种buffer的概念,有点类似缓存的味道。
那么有没有办法1个draw,绘制2个图片呢?
肯定有!我们如果查阅OpenGL的绘制形状,你会发现绘制三角形能满足我们的需求,因为图片也是由2个三角形组成,给你4个三角形,你肯定能拼凑出2个不同位置的四边形。
如果你仔细阅读源码,你也会发现,engine的绘制形状一定是GL_TRIANGLES
那该如何做呢?
只需要顶点数据一次传递4个三角形顶点即可!
这样,我们就一次draw,绘制了2个不同位置的图片,这个骚操作,在游戏引擎里面,称作合批,也可以理解为,使用最少的draw完成目标效果,前提是我们需要保证绘制结果正常。
多重纹理又是怎么回事
我们需要知道的是,多重纹理也是合批的一个手段,目的都是在通过一定的手段,达到合批效果。
多重纹理,顾名思义就是可以在多个预设的纹理之间切换,我们仔细观察下fragment.shader
gl_FragColor = v_fragmentColor * texture2D(texture1, v_texCoord); 复制代码
因为shader可以自己编写,如果我们想在多个纹理之间切换,那么shader可能就会是这样子
int unit=0; if( 0 == unit ){ gl_FragColor = v_fragmentColor * texture2D(texture0, v_texCoord); }else if( 1 == unit ){ gl_FragColor = v_fragmentColor * texture2D(texture1, v_texCoord); } 复制代码
那么问题就变成我们怎么控制unit达到切换纹理的效果。
我们首先会想到将unit定义为uniform,这样我们在代码中通过glUniform1i(unit, 3)
修改。
至此,你已经理解了,多重纹理的实现思路,本质上就是在fragment.shader中定义多个sampler2D
,在代码中修改shader中定义的变量,影响fragment采样不同的texture即可。
实现多重纹理如何合批
让我们再看下条件
- 相同的Blend: 这个就不用解释了,肯定得一致
- 相同的Shader:这个也不用解释了,肯定得一致
- 相同的Texture
问题就在相同的Texture上,多重纹理使用的是多个纹理,这怎么搞?
其实仔细思考下,这个条件其实对多纹理并没有多大意义。
因为不同组合的多纹理,我们可以生成不同的Shader实例,通过这个Shader实例,就可以判断,是否满足合批条件。
当然还有另外一个办法,Spire使用的shader实例是同一个,通过shader绑定的纹理Array的md5、hash值,也可以区分是否满足合批条件。
unit怎么传递呢?
上文说的uniform方式其实是有问题的。
2个图片的顶点数据最终是揉到了一个顶点数据里面,OpenGL在绘制的时候,如何在fragment里面知道当前绘制的顶点应该采用哪个 texture unit呢?
很显然unit其实是和顶点数据相关,所以必须放在顶点数据里面。
在creator中,你可以重新定义vertex format,但是在2dx中,并没有开放vertex format,那么unit数据应该放到哪里呢?
哈哈!一个2d engine,目前并没有采用positon.z,所以就hack到这里了!最重要的是这样子不用改渲染结构,算是勉强实现了需求。
至此,整个实现思路就完全剖析完毕,开始撸代码了。
扩展知识
获取fragment支持的最大纹理单元数量
GLint maxTextureUnits; glGetIntegerv(GL_MAX_TEXTURE_UNITS, &maxTextureUnits); 复制代码
实际测试发现GL_MAX_TEXTURE_UNITS
总是返回4,后来发现这个属性在OpenGL3中是废弃了,应该使用GL_MAX_TEXTURE_IMAGE_UNITS
属性 | 说明 | 取值 |
GL_MAX_TEXTURE_UNITS | 支持的常规纹理(纹理坐标集+纹理图像单元)单元数量,这个数值返回的总是4,因为在OpenGL3中被弃用了 | 4 |
GL_MAX_TEXTURE_IMAGE_UNITS | 片段着色器访问纹理贴图单元的数量 | 32 |
GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS | 32 | |
GL_MAX_TEXTURE_COORDS | 获取最大纹理坐标 | 8 |
GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS | 所有阶段着色器可以绑定的纹理单元,每个阶段可使用GL_MAX_TEXTURE_IMAGE_UNITS,一共6个阶段,所有32*6=192 | 192 |