点击查看【processon】
根据绘制需求的不同,cocos引擎将OpenGL绘制封装成了以下几种类型:
- TrianglesCommand:绘制一个或多个三角形
- QuadCommand
- 绘制一个或多个四边形,比如绘制一张图片
- QuadCommand
- PrimitiveCommand
- 绘制图元。点(GL_POINTS)、线(GL_LINES)、三角形(GL_TRIANGLES)。
- BatchCommand
- 批量顶点绘制。
- 比如从一张字符纹理,绘制一系列quad,组成一行字。
- 比如SpriteBatchNode,绘制大量相同quad
- CustomCommand
- 集成我们自己写的OpenGL绘制指令到cocos的绘制队列中
- GroupCommand
class CC_DLL RenderCommand { public: enum class Type { // 有哪些类型的RenderCommand UNKNOWN_COMMAND, // 预留类型 QUAD_COMMAND, // 绘制矩形区域 CUSTOM_COMMAND, // 自定义绘制命令,如stencil function, depth functions BATCH_COMMAND, // 封装TextureAtlas GROUP_COMMAND, // 包含1个或多个其他命令,可嵌套,通过一个queueID指向一个renderQueue MESH_COMMAND, // used to draw 3D meshes PRIMITIVE_COMMAND, // 绘制几何图元,如点、线、三角形 TRIANGLES_COMMAND // 绘制三角区域 };
protected:
Type _type; // 绘制指令类型
float _globalOrder; // global Z order
bool _isTransparent; // Transparent flag
/**
QuadCommand and TrianglesCommand could be auto batched
if there material ID is the same, however, if
a command is skip batching, it would be forced to draw
in a separate function call, and break the batch.
*/
bool _skipBatching; // true,则强制调用一个gl draw call,这会break the batch
/** Is the command been rendered on 3D pass. */
bool _is3D;
/** Depth from the model view matrix.*/
float _depth;
};
<a name="7j54P"></a>
# 二、TrianglesCommand
绘制三角形的指令,最常用到的绘制指令,比如绘制纹理,就是用的2个三角形构成一个的一个矩形区域。<br />封装的其实就是OpenGL通过顶点索引绘制三角形的指令glDrawElements:
```cpp
glDrawElements(GL_TRIANGLES, // 根据顶点索引缓冲中的顶点索引来绘制三角形
0, // 顶点索引数量,按顺序自动构建三角形
GL_UNSIGNED_INT, // 索引类型
(GLvoid*)(0 * sizeof(GLuint))) // 第一个索引在顶点索引缓冲中的偏移量
这里可以查看OpenGL如何渲染一张矩形纹理的代码。
数据结构
class TrianglesCommand : public RenderCommand
{
struct Triangles { // 顶点数据的封装
V3F_C4B_T2F* verts; // 一个顶点数据的数据结构:
// 1、顶点坐标:3 * sizeof(float)个字节大小
// x: float
// y: float
// z: float
// 2、颜色值:这是我们从应用程序代码传入给顶点着色器的顶点的颜色值,根据需要和纹理颜色值叠加
// 4 * sizeof(GLubyte)个字节大小
// r: GLubyte
// g: GLubyte
// b: GLubyte
// a: GLubyte
// 3、纹理坐标:顶点对应的纹理采样坐标,在片段着色器中要据此坐标进行纹理颜色值采样
// 2 * sizeof(GLfloat)个字节大小
// u: GLfloat,相当于x坐标
// v: GLfloat,相当于y坐标
unsigned short* indices; // 保存一些顶点的索引:通过这个索引就能在VBO管理的内存中找到对应的顶点数据
int vertCount; // 顶点数量
int indexCount; // 索引数量
};
protected:
// 这里面设计了一个MaterialID的概念,
// 相同MaterialID的绘制命令,以下数据都相同:
// GLProgramState: 包含glProgram,以及它所用到的uniforms、attribs、samplers。
// GLState: blend func
// 假设两条连续执行的绘制指令的MaterialID相同,
// 那么只需要一次gl draw call即可,减少了draw call,提高了渲染效率,这就是自动批处理(auto-batching)
uint32_t _materialID; // materialID
GLuint _textureID; // 使用到的纹理ID,glGenTextures生成的,注意和纹理单元区分(GL_TEXTUREi)
GLProgramState* _glProgramState; // 上面解释
BlendFunc _blendType; // 采用的缓和函数
Triangles _triangles; // 顶点数据 + 顶点索引数据
Mat4 _mv; // 模型视图矩阵,用于顶点坐标的变换,获得顶点的最终坐标
GLuint _alphaTextureID; // ANDROID ETC1 ALPHA supports.
};
class TrianglesCommand : public RenderCommand
{
void init(float globalOrder, // globalZOrder,用于全局绘制排序
GLuint textureID, // 使用到的纹理
GLProgramState* glProgramState, // 保存了一个GLProgram极其用到的所有数据:
// uniforms: 我们代码中传入给着色器的全局数据,该GLProgram的所有着色器都可以访问(CPU->GPU)
// attributes: 传入顶点着色器的顶点数据的格式(顶点属性),就是顶点数据的结构。
// textureUnits: 该GLProgram的片段着色器使用到的纹理单元
BlendFunc blendType, // 混合的类型,混合的知识点见:https://www.yuque.com/tvvhealth/cs/kahsp8
const Triangles& triangles, // 这条绘制命令需要用到的顶点数据
const Mat4& mv, // 用于变换顶点坐标的模型视图矩阵
uint32_t flags); // 用于区分3D还是2D
......; // init函数的其他重载版本
......; // 这里都是一些获取上面顶点数据的函数
protected:
void generateMaterialID() {
// 哈希算法生成materialID,影响因子包括:
// glProgramState:glProgram、uniforms、attributes
// blendFunc:混合函数(src + dst)
// textureID:片段着色器的采样纹理
struct {
void* glProgramState;
GLuint textureId;
GLenum blendSrc;
GLenum blendDst;
} hashMe;
// NOTE: Initialize hashMe struct to make the value of padding bytes be filled with zero.
// It's important since XXH32 below will also consider the padding bytes which probably
// are set to random values by different compilers.
memset(&hashMe, 0, sizeof(hashMe));
hashMe.textureId = _textureID;
hashMe.blendSrc = _blendType.src;
hashMe.blendDst = _blendType.dst;
hashMe.glProgramState = _glProgramState;
_materialID = XXH32((const void*)&hashMe, sizeof(hashMe), 0);
}
};
QuadCommand
绘制矩形的指令。绘制矩形其实就是绘制两个三角形,因此本质上它是一个TrianglesCommand(它的继承类)。
class CC_DLL QuadCommand : public TrianglesCommand
{
public:
void init( float globalOrder, // 同上TrianglesCommand的参数解释
GLuint textureID, // 同上TrianglesCommand的参数解释
GLProgramState* glProgramState, // 同上TrianglesCommand的参数解释
const BlendFunc& blendType, // 同上TrianglesCommand的参数解释
V3F_C4B_T2F_Quad* quads, // 一个矩形的数据结构,4个顶点数据构成
// {
// V3F_C4B_T2F tl; // 左顶
// V3F_C4B_T2F bl; // 左底
// V3F_C4B_T2F tr; // 右顶
// V3F_C4B_T2F br; // 右底
// };
ssize_t quadCount, // 同上TrianglesCommand的参数解释
const Mat4& mv, // 同上TrianglesCommand的参数解释
uint32_t flags); // 同上TrianglesCommand的参数解释
......; // init的重载版本
protected:
// 设一个矩形的结构如下,数字为顶点对应的索引
// 0------2
// | / |
// | / |
// |/ |
// 1------3
// 则绘制过程过程为:
// 三角形1:0 -> 1 -> 2,逆时针
// 三角形2:3 -> 2 -> 1,逆时针,统一为顺/逆时针方向,方便后续可能的glCullFace
//
// 假设这条指令要绘制3个矩形,如下;
// 0------2 4------6 8------10
// | / | | / | | / |
// | / | | / | | / |
// |/ | |/ | |/ |
// 1------3 5------7 9------11
// 绘制过程为:
// 矩形1:
// 三角形1:0 -> 1 -> 2
// 三角形2:3 -> 2 -> 1
// 矩形2:
// 三角形1:4 -> 5 -> 6
// 三角形2:7 -> 6 -> 5
// 矩形3:
// 三角形1:8 -> 9 -> 10
// 三角形2:11 -> 10 -> 9
// 根据上面的解释,我们计算出绘制n个矩形的顶点索引顺序,
// 而且不同的QuadCommand绘制n个矩形时的顶点索引是完全一样的。
// 因此我们可以提前创建一个索引池,支持最大连续绘制max个矩形。这样可以减少内存分配频率。
// 现在需要使用indices个顶点索引,函数将分配合适大小的索引量足以满足当前需求,
// 以及大概率满足后续需求
void reIndex(int indices);
......;
};
数据结构
class CC_DLL QuadCommand : public TrianglesCommand
{
protected:
int _indexSize; // 本条指令使用到的顶点索引数量
std::vector<GLushort*> _ownedIndices; // 由本指令负责释放的索引数组。
// shared across all instances
static int __indexCapacity; // 索引最大数量
static GLushort* __indices; // 当前正在共用的索引数组
};
重要函数
class CC_DLL QuadCommand : public TrianglesCommand
{
public:
void init( float globalOrder, // 同上TrianglesCommand的参数解释
GLuint textureID, // 同上TrianglesCommand的参数解释
GLProgramState* glProgramState, // 同上TrianglesCommand的参数解释
const BlendFunc& blendType, // 同上TrianglesCommand的参数解释
V3F_C4B_T2F_Quad* quads, // 一个矩形的数据结构,4个顶点数据构成
// {
// V3F_C4B_T2F tl; // 左顶
// V3F_C4B_T2F bl; // 左底
// V3F_C4B_T2F tr; // 右顶
// V3F_C4B_T2F br; // 右底
// };
ssize_t quadCount, // 同上TrianglesCommand的参数解释
const Mat4& mv, // 同上TrianglesCommand的参数解释
uint32_t flags); // 同上TrianglesCommand的参数解释
......; // init的重载版本
protected:
// 设一个矩形的结构如下,数字为顶点对应的索引
// 0------2
// | / |
// | / |
// |/ |
// 1------3
// 则绘制过程过程为:
// 三角形1:0 -> 1 -> 2,逆时针
// 三角形2:3 -> 2 -> 1,逆时针,统一为顺/逆时针方向,方便后续可能的glCullFace
//
// 假设这条指令要绘制3个矩形,如下;
// 0------2 4------6 8------10
// | / | | / | | / |
// | / | | / | | / |
// |/ | |/ | |/ |
// 1------3 5------7 9------11
// 绘制过程为:
// 矩形1:
// 三角形1:0 -> 1 -> 2
// 三角形2:3 -> 2 -> 1
// 矩形2:
// 三角形1:4 -> 5 -> 6
// 三角形2:7 -> 6 -> 5
// 矩形3:
// 三角形1:8 -> 9 -> 10
// 三角形2:11 -> 10 -> 9
// 根据上面的解释,我们计算出绘制n个矩形的顶点索引顺序,
// 而且不同的QuadCommand绘制n个矩形时的顶点索引是完全一样的。
// 因此我们可以提前创建一个索引池,支持最大连续绘制max个矩形。这样可以减少内存分配频率。
// 现在需要使用indices个顶点索引,函数将分配合适大小的索引量足以满足当前需求,
// 以及大概率满足后续需求
void reIndex(int indices);
......;
};
四、GroupCommand
GroupCommand指向一个RenderQueue,执行GroupCommand就是遍历RenderQueue中的RenderCommand。
init一个GroupCommand时,就已经在RenderQueue的stack栈顶压栈了一个新的RenderQueue,因此后续的RenderCommand都会进入这个新的RenderQueue。
class GroupCommand : public RenderCommand
{
public:
GroupCommand() {
_type = RenderCommand::Type::GROUP_COMMAND;
// 通过_renderQueueID指向绘制栈中闲置的RenderQueue
_renderQueueID = Director::getInstance()->getRenderer()->getGroupCommandManager()->getGroupID();
}
~GroupCommand(){
// 回收RenderQueue,重新变为限制的RenderQueue
Director::getInstance()->getRenderer()->getGroupCommandManager()->releaseGroupID(_renderQueueID);
}
void init(float globalOrder); // 重新初始化,重新分配一个闲置的RenderQueue
{
_globalOrder = globalOrder;
auto manager = Director::getInstance()->getRenderer()->getGroupCommandManager();
manager->releaseGroupID(_renderQueueID);
_renderQueueID = manager->getGroupID();
}
int _renderQueueID; // 当前指向的RenderQueue
};
五、CustomCommand
封装我们自己编写的OpenGL绘制指令。
class CC_DLL CustomCommand : public RenderCommand
{
void init( float globalZOrder, // 决定了这条命令的绘制队列中的执行位次。
const Mat4& modelViewTransform, // 以下两个参数,3D中才用到
uint32_t flags);
void init(float globalZOrder); // 2D中用这个就行了。
void execute(); // 执行的func
std::function<void()> func; // fuc里面写OpenGL绘制指令
......;
};
六、BatchCommand
TextureAtlas的封装执行,为TextureAtlas的绘制准备好了GLProgramState,包括:
- GLProgram:包含了编译链接好的顶点着色器、片段着色器等。
- 纹理(集):一般是一张合成纹理(由多个小纹理组成,由外部工具预生成),本质就是一个普通2D纹理。
- 混合函数(BlendFunc)
TextureAtlas的需求场景是,绘制大量的矩形区域,这些矩形区域都是从同一个纹理采样。比如从一张字符纹理中绘制多个子纹理组成文字,还有比如绘制大量的一样的草。
设计这个的好处是为了batching(批处理绘制),就是说将绘制的顶点数据集中起来,然后调用一次glDrawElements或glDrawArrays来完成所有的绘制,减少draw call。
class CC_DLL BatchCommand : public RenderCommand
{
// 参数解释同上
void init( float globalZOrder,
GLProgram* shader,
BlendFunc blendType,
TextureAtlas* textureAtlas, // 可以看成是纹理和quad顶点数据的集合
const Mat4& modelViewTransform,
uint32_t flags);
// 绘制大量的quad,
void execute()
{
// Set material
_shader->use();
_shader->setUniformsForBuiltins(_mv);
GL::bindTexture2D(_textureID);
GL::blendFunc(_blendType.src, _blendType.dst);
// Draw
_textureAtlas->drawQuads();
}
......; // 和上面的Command一样的成员
};
七、PrimitiveCommand
绘制图元的指令。
class PrimitiveCommand : public RenderCommand
{
void init( float globalOrder, // 同上TrianglesCommand的参数解释
GLuint textureID, // 同上TrianglesCommand的参数解释
GLProgramState* glProgramState, // 同上TrianglesCommand的参数解释
BlendFunc blendType, // 同上TrianglesCommand的参数解释
Primitive* primitive, // 图元数据,一个图元的数据结构如下:
// VertexData* _verts; // 顶点数据
// IndexBuffer* _indices; // 顶点索引(可选)
// int _start; // 绘制的起始顶点索引
// int _count; // 顶点数据数量
// int _type; // 图元类型
// GL_POINTS // 点
// GL_LINES // 线段
// GL_TRIANGLES // 三角形
const Mat4& mv, // 同上TrianglesCommand的参数解释
uint32_t flags); // 同上TrianglesCommand的参数解释
......; // 获取成员变量的函数
void execute() const; // 画点:glDrawArrays(GL_POINTS, startIndex, count);
// 画线:glDrawArrays(GL_LINES, startIndex, count);
// 画三角形:glDrawArrays(GL_TRIANGLES, startIndex, count);
......; // init的参数 作为成员变量
};