前言

先看看在iPhone上使用Cocos2d-x与OpenGL ES绘制出来的最终效果。

tess

图中绘制的都是圆环,或者光滑曲线构成的图形。不过本质还是绘制多边形,只要这些多边形的顶点足够多,那么就可以形成光滑的曲线。

在绘制过程中使用到的相关知识点:

  1. Cocos2d-x渲染机制
  2. OpenGL ES绘图基础
  3. Objectice-C与C++混编
  4. OpenGL Tessellation将多边形切分成多个三角形

问题的发现

在使用Cococs2d-x绘制填充颜色的多边形的时候,最先想到的就是使用DrawNode 。DrawNode提供以下接口:

1
2
// 绘制填充颜色的多边形,参数包括:多边形顶点坐标,定点数,以及需要填充的颜色
void drawSolidPoly(const Vec2 *poli, unsigned int numberOfPoints, const Color4F &color);

使用该接口绘制一个四分之一圆环的时候,想达到的效果如下👇

tess3

但是实际的效果却是这样的👇

tess2

查看源码

查看Cocos2d-x的DrawNode的源码:CCDrawNode.cpp

1
2
3
4
5
// drawSolidPoly接口的具体实现
void DrawNode::drawSolidPoly(const Vec2 *poli, unsigned int numberOfPoints, const Color4F &color)
{
drawPolygon(poli, numberOfPoints, color, 0.0, Color4F(0.0, 0.0, 0.0, 0.0));
}

问题的关键还是在drawPolygon接口,该接口绘制一般的多边形,它的实现原理很简单:根据顶点,将多边形划分成多个三角形,然后使用OpenGL绘制指令交由GPU绘制

来看看具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*------- drawPolygon具体实现 ----------*/
void DrawNode::drawPolygon(const Vec2 *verts, int count, const Color4F &fillColor, float borderWidth, const Color4F &borderColor)
{
CCASSERT(count >= 0, "invalid count value");

// 看是否需要绘制边框,本文分析的是无边框的情况
bool outline = (borderColor.a > 0.0f && borderWidth > 0.0f);

// 根据顶点数,计算底层需要绘制的三角形的个数:triangle_count
auto triangle_count = outline ? (3*count - 2) : (count - 2);
// 根据三角形的个数,算出需要传递给GPU的顶点个数,也就是乘3
auto vertex_count = 3*triangle_count;
ensureCapacity(vertex_count);

// 存放顶点数据的buffer
V2F_C4B_T2F_Triangle *triangles = (V2F_C4B_T2F_Triangle *)(_buffer + _bufferCount);
V2F_C4B_T2F_Triangle *cursor = triangles;

// 关键代码: 划分三角形的算法
for (int i = 0; i < count-2; i++)
{
V2F_C4B_T2F_Triangle tmp = {
{verts[0], Color4B(fillColor), __t(v2fzero)},
{verts[i+1], Color4B(fillColor), __t(v2fzero)},
{verts[i+2], Color4B(fillColor), __t(v2fzero)},
};

*cursor++ = tmp;
}

if(outline)
{
/* 此处省略800字,主要是绘制多边形的边框*/
}

_bufferCount += vertex_count;
_dirty = true;
}

具体分析下将多边形划分成三角形的步骤:

假设有5个顶点(1-5),那么可以分成 5 - 2 = 3 个三角形。以1号顶点为基础,这三个三角形分别是

①②③

①③④

①④⑤

那么当这些顶点构成的多边形是凹多边形时就会出现问题(覆盖了不需要绘制的区域),画个图解释一下:

tess4

假设将这个四分之一圆环分成10个顶点的多边形时,还是以刚说的划分三角形的算法,那么小圆弧上的点与①号点构成的三角形,就超出了绘制范围。其中最为明显的就是①⑥⑦这三个点构成的三角形。

使用Tessellation正确的划分三角形

经过刚刚的源码分析,可以发现出现渲染错误的原因就是:绘制凹多边形时,划分三角形的方法有问题。谷歌一番,其实OpenGL提供了Tessellation的方法,来正确的划分三角形;但是OpenGL ES暂不支持。不过2008年的时候,有一个第三方库提供了iOS上Tessellation的功能。

下载该源代码,然后修改Makefile,生成静态库,导入Cocos2d-x中。然后看看如何修改Cocos2d-x源码来实现绘制凹多边形的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/*-------- CCDrawNode.cpp中新增 drawPolygonUseTessllation 方法 ---------*/
void DrawNode::drawPolygonUseTessllation(const Vec2 *verts, int count, const Color4F &fillColor, float borderWidth, const Color4F &borderColor) {
CCASSERT(count >= 0, "invalid count value");

// 新建一个tessellation处理模块
GLUtesselator *tess = gluNewTess();
if (tess) {
totolTessVecPointNum = 0;
drawNum = 0;

// 绑定tessellation 回调函数
gluTessCallback(tess, GLU_TESS_BEGIN, (void (*)())tessBeginCB);
gluTessCallback(tess, GLU_TESS_END, (void (*)())tessEndCB);
gluTessCallback(tess, GLU_TESS_ERROR, (void (*)())tessErrorCB);
gluTessCallback(tess, GLU_TESS_VERTEX, (void (*)())tessVertexCB);

gluTessBeginPolygon(tess, 0);
gluTessBeginContour(tess);
// 向tessellation处理模块中,依次传入多边形顶点坐标
GLdouble *quad = new GLdouble[count * 3];
for (int i = 0; i < count; i++) {
quad[i * 3] = verts[i].x;
quad[i * 3 + 1] = verts[i].y;
quad[i * 3 + 2] = 0;
}

for (int i = 0; i < count; ++i) {
gluTessVertex(tess, quad + i * 3, quad + i * 3);
}
gluTessEndContour(tess);
gluTessEndPolygon(tess);

// 结束tessellation处理
gluDeleteTess(tess);

delete[] quad;
} else {
std::cout << "no tessellation";
exit(1);
}


V2F_C4B_T2F *triangles = (V2F_C4B_T2F *)(_buffer + _bufferCount);
V2F_C4B_T2F *cursor = triangles;

// 经过tessellation, 以及将顶点重新排序了,现在将重新排序后的顶点放入顶点buffer,以待GPU渲染
for (int i = 0; i < totolTessVecPointNum; i++)
{
V2F_C4B_T2F tmp = {tessVec2[i], Color4B(fillColor), __t(v2fzero)};
*cursor++ = tmp;
}

std::cout << "draw node is " << drawNum << std::endl;
_TSCommand.clear();
_TSNumber.clear();
_TSCommand.resize(drawNum);
_TSNumber.resize((drawNum));
for (int j = 0; j < drawNum; ++j) {
_TSCommand.push_back(tessSort[j][0]);
_TSNumber.push_back(tessSort[j][1]);
}

_bufferCount += totolTessVecPointNum;
_useTessellation = true;
_dirty = true;
}

Tessellation的主要工作就是:将多边形的顶点重新排序,然后依次指定这些顶点的绘制方式。有三种绘制方式:GL_TRIANGLES,GL_TRIANGLE_STRIP和GL_TRIANGLE_FAN

来看看Tessellation的回调函数:回调函数会在这些情况下发生:

  1. 重新指定一种绘制方式时 GLU_TESS_BEGIN
  2. 以该种绘制方式时,生成一个新的顶点时 GLU_TESS_VERTEX
  3. 结束该绘制方式时 GLU_TESS_END
  4. 发生错误时 GLU_TESS_ERROR
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
float tessVec2[5000][2];
int tessSort[100][2];
int totolTessVecPointNum = 0;
int currentDrawTessPointNum = 0;
int drawNum = 0;
GLenum tessType;

const char* getPrimitiveType(GLenum type) {
// 省略800字, 获取绘制方式
}

void tessBeginCB(GLenum which) {
currentDrawTessPointNum = 0;
switch(which)
{
case 0x0004:
tessType = 1;
break;
case 0x0005:
tessType = 2;
break;
case 0x0006:
tessType = 3;
break;
default:
std::cout << "error tess type" << std::endl;
}
std::cout << "glBegin(" << getPrimitiveType(which) << ");\n";
}

void tessEndCB() {
tessSort[drawNum][0] = tessType;
tessSort[drawNum][1] = currentDrawTessPointNum;
drawNum++;
std::cout << "glEnd();\n";
}

void tessVertexCB(const GLvoid *data) {
const GLdouble *ptr = (const GLdouble*)data;

GLfloat pos1 = *ptr;
GLfloat pos2 = *(ptr + 1);
GLfloat pos3 = *(ptr + 2);

std::cout << "pos1 is " << pos1 << ", pos2 is " << pos2 << ", pos3 is " << pos3 << std::endl;
tessVec2[totolTessVecPointNum][0] = pos1;
tessVec2[totolTessVecPointNum][1] = pos2;
totolTessVecPointNum++;
currentDrawTessPointNum++;
}

void tessErrorCB(GLenum errorCode) {
const GLubyte *errorStr;

errorStr = gluErrorString(errorCode);
std::cerr << "[ERROR]: " << errorStr << std::endl;
}

Cocos2d-x底层渲染

通过Tessellation之后,已经将多边形的顶点重新排序,并重新指定了绘制方式。现在这些绘制方式和顶点已经记录下来了,需要修改DrawNodedraw方法进行渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
void DrawNode::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
// 如果使用Tessellation,那么调用onDrawUseTessellation方法绘制
if (_useTessellation) {
_customCommand.init(_globalZOrder, transform, flags);
_customCommand.func = CC_CALLBACK_0(DrawNode::onDrawUseTessellation, this, transform, flags);
renderer->addCommand(&_customCommand);
return;
}
if(_bufferCount)
{
_customCommand.init(_globalZOrder, transform, flags);
_customCommand.func = CC_CALLBACK_0(DrawNode::onDraw, this, transform, flags);
renderer->addCommand(&_customCommand);
}

// 此处省略800字, cocos2d-x自带了绘制点和线的方法
}

/*----------- 真正的绘制方法 --------------*/
// onDrawUseTessellation仿造了DrawNode自带的onDraw方法,并共用底层的shader,以及CPU中的顶点buffer
void DrawNode::onDrawUseTessellation(const Mat4 &transform, uint32_t flags) {
// 基本设置,同onDraw方法
getGLProgramState()->apply(transform);
auto glProgram = this->getGLProgram();
glProgram->setUniformLocationWith1f(glProgram->getUniformLocation("u_alpha"), _displayedOpacity / 255.0);
GL::blendFunc(_blendFunc.src, _blendFunc.dst);


if (_dirty)
{
// 绑定顶点数据,顶点数据从CPU的_buffer缓存中,传递给GPU的GL_ARRAY_BUFFER
glBindBuffer(GL_ARRAY_BUFFER, _vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(V2F_C4B_T2F)*_bufferCapacity, _buffer, GL_STREAM_DRAW);

_dirty = false;
}

// 此处省略800字,一些基本的设置

int curIndex = 0;
int drawNumber = _TSCommand.size();
// 核心代码: 一个凹多边形可能需要调用多次drawArrays方法,并且绘制的方式不同
// 绘制的次数,以及每次的绘制方式,以及每次绘制时绘制哪些顶点 都在Tessellation步骤里面生成好了
// _TSCommand数组存储绘制方式,_TSNumber数组存储每次绘制的定点数
for (int i = 0; i < drawNumber; ++i) {
int curDrawType = _TSCommand[i];
int curVexNum = _TSNumber[i];
if (curDrawType == 1) {
glDrawArrays(GL_TRIANGLES, curIndex, curVexNum);
} else if (curDrawType == 2) {
glDrawArrays(GL_TRIANGLE_STRIP, curIndex, curVexNum);
} else {
glDrawArrays(GL_TRIANGLE_FAN, curIndex, curVexNum);
}
curIndex += curVexNum;
}

// 此处省略800字,一些收尾工作
}

到此,就实现了在iOS设备上使用Cocos2d-x和OpenGL ES绘制凹多边形。

PS

由于是在iOS上使用Cocos2d-x,所以使用过程中会需要Objective-C和C++混编,可以参考这个教程 - 混编ObjectiveC++

对了,还有一些基本的Cocos2d-x渲染机制,我学习的这系列博客 - coco2d-x sourcecode analysis

参考