学习OpenGL ES之绘制圆柱体

本系列所有文章目录

获取示例代码


本文将要介绍如何使用代码绘制一个圆柱体,通过绘制圆柱体可以更好的掌握法线,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];
}

最终效果图如下。


本文通过绘制圆柱体来介绍使用代码生成基本几何体的思路和方法。下篇文章中将介绍如何使用一张地形图片生成一个复杂的地形模型,敬请期待。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 【引言】 上一篇谈到C4D的凹凸贴图及其应用。最后说道,在现实使用中,尤其是在游戏引擎中,很少有使用凹凸贴图...
    DIGITALMAN阅读 39,172评论 19 189
  • 本系列所有文章目录[https://www.jianshu.com/p/df4c8f9bc08d] 获取示例代码[...
    handyTOOL阅读 7,528评论 2 12
  • 01 异地好友问我认不认识XX公司的人,该公司总部在上海,我刚好认识一个曾在这个公司工作过的朋友,朋友遂向我讲明缘...
    郑喜月阅读 590评论 0 5
  • 隐约雷鸣,阴霾天空。但盼风雨来,能留你在此。 隐约雷鸣,阴霾天空。即使天无雨,我亦留在此。 ...
    清浅一眸阅读 446评论 0 8
  • 如此清凉月,腾光照故人… 天空还未开明,乘坐的列车已经到站, 对家乡最深的记忆,最怕是莫过于隐匿在大山与天空云朵间...
    雲鹤爵阅读 233评论 0 0