绘制圆柱体
获取示例代码,本文代码在分支chapter15中。
本文将要介绍如何使用代码绘制一个圆柱体,通过绘制圆柱体可以更好的掌握法线,UV,TriangleFan,TriangleStrip等相关知识。在绘制之前,先进行一些准备工作。
GLGeometry
为了更方便的进行顶点数据的管理,我创建了一个GLGeometry
类。
typedef enum : NSUInteger {
GLGeometryTypeTriangles,
GLGeometryTypeTriangleStrip,
GLGeometryTypeTriangleFan,
} GLGeometryType;
typedef struct {
GLfloat x;
GLfloat y;
GLfloat z;
GLfloat normalX;
GLfloat normalY;
GLfloat normalZ;
GLfloat u;
GLfloat v;
} GLVertex;
@interface GLGeometry () {
GLuint vbo;
BOOL vboValid;
}
@property (strong, nonatomic) NSMutableData *vertexData;
@end
@implementation GLGeometry
- (instancetype)initWithGeometryType:(GLGeometryType)geometryType
{
self = [super init];
if (self) {
self.geometryType = geometryType;
vboValid = NO;
self.vertexData = [NSMutableData data];
}
return self;
}
- (void)dealloc {
if (vboValid) {
glDeleteBuffers(1, &vbo);
}
}
- (void)appendVertex:(GLVertex)vertex {
void * pVertex = (void *)(&vertex);
NSUInteger size = sizeof(GLVertex);
[self.vertexData appendBytes:pVertex length:size];
}
- (GLuint)getVBO {
if (vboValid == NO) {
glGenBuffers(1, &vbo);
vboValid = YES;
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, [self.vertexData length], self.vertexData.bytes, GL_STATIC_DRAW);
}
return vbo;
}
- (int)vertexCount {
return [self.vertexData length] / sizeof(GLVertex);
}
这个类里我定义了描述顶点数据的结构体GLVertex
,描述顶点绘制方式的枚举GLGeometryType
,追加顶点数据的方法- (void)appendVertex:(GLVertex)vertex
,生成VBO的方法- (GLuint)getVBO
,获取顶点个数的方法- (int)vertexCount
。有了这些我们就可以很方便的构建3D几何体了。
分解圆柱体
如果我们有一个纸质的圆柱体模型,我们可以把它剪开成两个圆形和一个矩形。
所以我们可以将圆柱体看做三个几何体来绘制,绘制两个圆形和一个卷成桶状的矩形。我们将圆形半径定义为radius
,矩形高为height
,宽既是圆形的周长。
下面绘制的代码在
Cylinder
类中,Cylinder
继承自GLObject
。
绘制圆形
可以采取多边形逼近的方式绘制圆形,比如我们可以构建一个正36边形来表示一个圆。本文的代码就是利用这个原理来绘制圆的。定义构成圆形的边数为sideCount
。
- (GLGeometry *)topCircle {
if (_topCircle == nil) {
_topCircle = [[GLGeometry alloc] initWithGeometryType:GLGeometryTypeTriangleFan];
float y = self.height / 2.0;
// 中心点
GLVertex centerVertex = GLVertexMake(0, y, 0, 0, 1, 0, 0.5, 0.5);
[_topCircle appendVertex:centerVertex];
for (int i = self.sideCount; i >= 0; --i) {
GLfloat angle = i / (float)self.sideCount * M_PI * 2;
GLVertex vertex = GLVertexMake(cos(angle) * self.radius, y, sin(angle) * self.radius, 0, 1, 0, (cos(angle) + 1 ) / 2.0, (sin(angle) + 1 ) / 2.0);
[_topCircle appendVertex:vertex];
}
}
return _topCircle;
}
上面是Cylinder.m
中的代码,用来构建圆柱体上方的圆形。构建圆形是我使用的是TriangleFan,可以大大减少绘制需要的顶点数。首先添加圆心的顶点,然后围绕中心顶点,依次加入边上的顶点。上面的法线都是朝上的,既(0, 1, 0)
。UV和顶点的取值如下图所示。
图示为5条边的情况演示,第n条边的Angle等于2 * Pi * n / sideCount
,因为sin函数的范围是-1到1,所以使用(sin(angle) + 1 ) / 2.0
就可以得到0~1的uv范围。
下方的圆形和上方主要的区别就是y轴的位置和法线,它位于-height/2
处,法线向下。上方的圆形处于height/2
处,法线向上。
- (GLGeometry *)bottomCircle {
if (_bottomCircle == nil) {
_bottomCircle = [[GLGeometry alloc] initWithGeometryType:GLGeometryTypeTriangleFan];
float y = -self.height / 2.0;
// 中心点
GLVertex centerVertex = GLVertexMake(0, y, 0, 0, -1, 0, 0.5, 0.5);
[_bottomCircle appendVertex:centerVertex];
for (int i = 0; i <= self.sideCount; ++i) {
GLfloat angle = i / (float)self.sideCount * M_PI * 2;
GLVertex vertex = GLVertexMake(cos(angle) * self.radius, y, sin(angle) * self.radius, 0, -1, 0, (cos(angle) + 1 ) / 2.0, (sin(angle) + 1 ) / 2.0);
[_bottomCircle appendVertex:vertex];
}
}
return _bottomCircle;
}
细心的读者可能还会发现,循环的顺序也不一样,上面是 for (int i = self.sideCount; i >= 0; --i)
,下面是for (int i = 0; i <= self.sideCount; ++i)
。为什么要这样呢?因为我开启了剔除表面,glEnable(GL_CULL_FACE);
,并且剔除的是背面glCullFace(GL_BACK);
。剔除背面就意味着背面将不会被渲染,只有正面面向摄像机的时候我们才能看到它被渲染。那么OpenGL如何判断正面还是背面呢?
- (void)draw:(GLContext *)glContext {
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
...
}
Cull Face
默认情况下,投影到屏幕后顶点顺序为逆时针的面为正面。
图中右边的是逆时针,所以如果使用了Cull Face,我们只能看见右边的面。当然你也可以使用void glFrontFace(GLenum mode);
将顺时针改为正面。
因为我需要顶部圆形的上面一侧显示,所以必须保证从上往下看时,组成三角形的顶点顺序是逆时针的。底部的圆形则相反,从下往上看时,需要保证组成三角形的顶点顺序是逆时针的。
绘制中间的矩形
中间的矩形可以使用三角带来绘制。
- (GLGeometry *)middleCylinder {
if (_middleCylinder == nil) {
_middleCylinder = [[GLGeometry alloc] initWithGeometryType:GLGeometryTypeTriangleStrip];
float yUP = self.height / 2.0;
float yDOWN = -self.height / 2.0;
for (int i = 0; i <= self.sideCount; ++i) {
GLfloat angle = i / (float)self.sideCount * M_PI * 2;
GLKVector3 vertexNormal = GLKVector3Normalize(GLKVector3Make(cos(angle) * self.radius, 0, sin(angle) * self.radius));
GLVertex vertexUp = GLVertexMake(cos(angle) * self.radius, yUP, sin(angle) * self.radius, vertexNormal.x, vertexNormal.y, vertexNormal.z, i / (float)self.sideCount, 0);
GLVertex vertexDown = GLVertexMake(cos(angle) * self.radius, yDOWN, sin(angle) * self.radius, vertexNormal.x, vertexNormal.y, vertexNormal.z, i / (float)self.sideCount, 1);
[_middleCylinder appendVertex:vertexDown];
[_middleCylinder appendVertex:vertexUp];
}
}
return _middleCylinder;
}
可以把它看做self.sideCount
个矩形组成的几何体。只需要按照下图方向依次追加顶点即可。
注意添加顶点时候我使用的是从0到2Pi的方向,正如上图所示,这样才能保证顶点顺序是逆时针的。UV直接使用顶点在宽高上的比例即可。
绘制圆柱体
有了这三个几何体,就可以组合成一个圆柱体了。下面是绘制代码。
- (void)draw:(GLContext *)glContext {
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
[glContext setUniformMatrix4fv:@"modelMatrix" value:self.modelMatrix];
bool canInvert;
GLKMatrix4 normalMatrix = GLKMatrix4InvertAndTranspose(self.modelMatrix, &canInvert);
[glContext setUniformMatrix4fv:@"normalMatrix" value:canInvert ? normalMatrix : GLKMatrix4Identity];
[glContext bindTexture:self.diffuseTexture to:GL_TEXTURE0 uniformName:@"diffuseMap"];
[glContext drawGeometry:self.topCircle];
[glContext drawGeometry:self.bottomCircle];
[glContext drawGeometry:self.middleCylinder];
}
和之前唯一不同的是 [glContext drawGeometry:self.topCircle];
方法,这个是新增的用于绘制GLGeometry
的方法。实现如下:
- (void)drawGeometry:(GLGeometry *)geometry {
glBindBuffer(GL_ARRAY_BUFFER, [geometry getVBO]);
[self bindAttribs:NULL];
if (geometry.geometryType == GLGeometryTypeTriangleFan) {
glDrawArrays(GL_TRIANGLE_FAN, 0, [geometry vertexCount]);
} else if (geometry.geometryType == GLGeometryTypeTriangles) {
glDrawArrays(GL_TRIANGLES, 0, [geometry vertexCount]);
} else if (geometry.geometryType == GLGeometryTypeTriangleStrip) {
glDrawArrays(GL_TRIANGLE_STRIP, 0, [geometry vertexCount]);
}
}
主要就是根据不同的geometryType
绘制vbo,很好理解。最后回到ViewController
,利用三个圆柱体组装个锤子吧。
- (void)createCylinder {
GLKTextureInfo *metal1 = [GLKTextureLoader textureWithCGImage:[UIImage imageNamed:@"metal_01.png"].CGImage options:nil error:nil];
GLKTextureInfo *metal2 = [GLKTextureLoader textureWithCGImage:[UIImage imageNamed:@"metal_02.jpg"].CGImage options:nil error:nil];
GLKTextureInfo *metal3 = [GLKTextureLoader textureWithCGImage:[UIImage imageNamed:@"metal_03.png"].CGImage options:nil error:nil];
// 四边的圆柱体就是一个四方体
Cylinder * cylinder = [[Cylinder alloc] initWithGLContext:self.glContext sides:4 radius:0.9 height:1.2 texture:metal1];
cylinder.modelMatrix = GLKMatrix4MakeTranslation(0, 2, 0);
[self.objects addObject:cylinder];
Cylinder * cylinder2 = [[Cylinder alloc] initWithGLContext:self.glContext sides:16 radius:0.2 height:4.0 texture:metal3];
[self.objects addObject:cylinder2];
// 四边的圆柱体就是一个正方体
Cylinder * cylinder3 = [[Cylinder alloc] initWithGLContext:self.glContext sides:4 radius:0.41 height:0.3 texture:metal2];
cylinder3.modelMatrix = GLKMatrix4MakeTranslation(0, -2, 0);
[self.objects addObject:cylinder3];
}
最终效果图如下。
本文通过绘制圆柱体来介绍使用代码生成基本几何体的思路和方法。下篇文章中将介绍如何使用一张地形图片生成一个复杂的地形模型,敬请期待。