今天我们创建一个球体,后期会在此基础上实现一个全景视频播放器。之前的三篇笔记我们都是通过两种方式来实现OpenGL ES的学习,即一种方式是使用GLKit另一种方式不使用GLKit。这是为了方便理解OpenGL ES才这样做的,因为苹果对OpenGL ES进行了部分封装使其使用起来更方便了,但如果直接用GLKit学习不利于对OpenGL ES的理解所以前面我都是在实现同一个功能的时候用两种方式来实现。两种方式的区别基本上在前三篇的学习中已经讲解的差多了,基本没有其他区别了,所以以后的学习笔记不再使用两种方式来实现而是直接用GLKit来实现,下面进入正题。
学习的代码都在我的github仓库欢迎大家学习指教!
一、生成顶点数据
这次我们的代码也是在上一次学习笔记的基础之上修改来的,这里只讲不同点。上次实现的立方体和这次实现的球体已经很像了。只要传入的顶点数据不一样就可以了!其实学到这里你会发现OpenGL ES只是入门难,入门后里面的东西很简单。有点跑题了,现在用代码实现顶点数据。我们先要知道以下知识:
- 我们可以通过三角函数实现一个圆的坐标数据
- 如果我们把多个圆叠放在一起就可以实现一他圆柱体
- 如果我们把这多个叠放圆的半径也按三角函数的方式实现就可以实现一个球体
通过上面的分析我们大体已经知道怎么生成这个球体的数据了,全靠顶点来实现球体有点不现实,我们可以通过少量的顶点连接成的三角形来实现球体。顶点越多生成的球体越平滑但也不是没有极限,当顶点大于一定值的时候再多的顶点也看不出差别来反而会影响性能。所以我们这里有81 x 81个顶点来实现一个球体。生成顶点的方法如下:
#define kDevidCount 80
/**
绘制一个球的顶点
@param num 传入要生成的顶点的一层的个数(最后生成的顶点个数为 num * num)
@return 返回生成后的顶点
*/
- (Vertex *)getBallDevidNum:(GLint) num{
if (num % 2 == 1) {
return 0;
}
GLfloat delta = 2 * M_PI / num; // 分割的份数
GLfloat ballRaduis = 0.8; // 球的半径
GLfloat pointZ;
GLfloat pointX;
GLfloat pointY;
GLfloat textureY;
GLfloat textureX;
GLfloat textureYdelta = 1.0 / (num / 2);
GLfloat textureXdelta = 1.0 / num;
GLint layerNum = num / 2.0 + 1; // 层数
GLint perLayerNum = num + 1; // 要让点再加到起点所以num + 1
Vertex * cirleVertex = malloc(sizeof(Vertex) * perLayerNum * layerNum);
memset(cirleVertex, 0x00, sizeof(Vertex) * perLayerNum * layerNum);
// 层数
for (int i = 0; i < layerNum; i++) {
// 每层的高度(即pointY),为负数让其从下向上创建
pointY = -ballRaduis * cos(delta * i);
// 每层的半径
GLfloat layerRaduis = ballRaduis * sin(delta * i);
// 每层圆的点,
for (int j = 0; j < perLayerNum; j++) {
// 计算
pointX = layerRaduis * cos(delta * j);
pointZ = layerRaduis * sin(delta * j);
textureX = textureXdelta * j;
textureY = textureYdelta * i;
cirleVertex[i * perLayerNum + j] = (Vertex){pointX, pointY, pointZ, textureX, textureY};
}
}
return cirleVertex;
}
// 顶点数据索引
- (GLuint *)getBallVertexIndex:(GLint)num{
// 每层要多原点两次
GLint sizeNum = sizeof(GLuint) * (num + 1) * (num + 1);
GLuint * ballVertexIndex = malloc(sizeNum);
memset(ballVertexIndex, 0x00, sizeNum);
GLint layerNum = num / 2 + 1;
GLint perLayerNum = num + 1; // 要让点再加到起点所以num + 1
for (int i = 0; i < layerNum; i++) {
if (i + 1 < layerNum) {
for (int j = 0; j < perLayerNum; j++) {
// i * perLayerNum * 2每层的下标是原来的2倍
ballVertexIndex[(i * perLayerNum * 2) + (j * 2)] = i * perLayerNum + j;
// 后一层数据
ballVertexIndex[(i * perLayerNum * 2) + (j * 2 + 1)] = (i + 1) * perLayerNum + j;
}
} else {
for (int j = 0; j < perLayerNum; j++) {
// 后最一层数据单独处理
ballVertexIndex[i * perLayerNum * 2 + j] = i * perLayerNum + j;
}
}
}
return ballVertexIndex;
}
在原来设置VBO数据的方法里设置上面的方法来生成顶点数据和顶点索引数据。代码如下:
/**
设置顶点缓存VBO
*/
- (void)setupBufferVBO {
// 获取球的顶点和索引
_cirleVertex = [self getBallDevidNum:kDevidCount];
_vertextIndex = [self getBallVertexIndex:kDevidCount];
// 设置VBO
glGenBuffers(1, &_bufferVBO);
glBindBuffer(GL_ARRAY_BUFFER, _bufferVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(Vertex) * (kDevidCount + 1) * (kDevidCount / 2 + 1), _cirleVertex, GL_STATIC_DRAW);
glGenBuffers(1, &_bufferIndexVBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _bufferIndexVBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLuint) * kDevidCount * (kDevidCount + 1), _vertextIndex, GL_STATIC_DRAW);
glEnableVertexAttribArray(GLKVertexAttribPosition);
glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid *)NULL);
// 设置纹理坐标
glEnableVertexAttribArray(GLKVertexAttribTexCoord0);
glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLfloat *)NULL + 3);
// 释放顶点数据
free(_cirleVertex);
free(_vertextIndex);
}
二、修改渲染方法
由于顶点数据的大小变化了,渲染方法里也得修改新的顶点数据个数的计算方法,代码如下:
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
[self.effect prepareToDraw];
// 绘制一个球
glDrawElements(GL_TRIANGLE_STRIP, kDevidCount * (kDevidCount + 1), GL_UNSIGNED_INT, 0);
}
由于这次是用的GLKit所以代码会简单很多,渲染方法里的代码也很少,其他的代码可以看上一篇笔记。现在看一下运行结果。
球体.gif球体上面的纹理可能不太合适,大家可以在网上搜一个地球的纹理试一下效果会比现在好一点。
网友评论