学习OpenGL ES之加载OBJ

本系列所有文章目录

获取示例代码


OBJ文件是Alias|Wavefront公司为它的一套基于工作站的3D建模和动画软件"Advanced Visualizer"开发的一种标准3D模型文件格式,它是基于纯文本的一种文件,我们可以很方便的解析其中的数据,本文将介绍OBJ的基本数据格式和解析方法。

OBJ数据结构

我们先来看一个Cube的OBJ文件是什么样的。

# Blender v2.78 (sub 0) OBJ File: 'cube.blend'
# www.blender.org
mtllib smoothCube.mtl
o Cube
v 1.000000 -1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 -1.000000 1.000000
v -1.000000 -1.000000 -1.000000
v 1.000000 1.000000 -0.999999
v 0.999999 1.000000 1.000001
v -1.000000 1.000000 1.000000
v -1.000000 1.000000 -1.000000
vt 0.0000 -1.0000
vt -1.0000 0.0000
vt 0.0000 0.0000
vt -1.0000 1.0000
vt -0.0000 -0.0000
vt 0.0000 1.0000
vt -1.0000 0.0000
vt -0.0000 1.0000
vt -1.0000 0.0000
vt 0.0000 0.0000
vt 0.0000 0.0000
vt 1.0000 1.0000
vt 1.0000 0.0000
vt 0.0000 -1.0000
vt -1.0000 0.0000
vt 0.0000 0.0000
vt -1.0000 -1.0000
vt -1.0000 0.0000
vt -1.0000 1.0000
vt -1.0000 1.0000
vt 0.0000 1.0000
vt -1.0000 -1.0000
vn 0.5773 -0.5773 0.5773
vn -0.5773 -0.5773 -0.5773
vn 0.5773 -0.5773 -0.5773
vn -0.5773 0.5773 -0.5773
vn 0.5773 0.5773 0.5773
vn 0.5773 0.5773 -0.5773
vn -0.5773 -0.5773 0.5773
vn -0.5773 0.5773 0.5773
usemtl Material
s 1
f 2/1/1 4/2/2 1/3/3
f 8/4/4 6/5/5 5/6/6
f 5/6/6 2/7/1 1/3/3
f 6/8/5 3/9/7 2/10/1
f 3/11/7 8/12/4 4/13/2
f 1/14/3 8/15/4 5/16/6
f 2/1/1 3/17/7 4/2/2
f 8/4/4 7/18/8 6/5/5
f 5/6/6 6/19/5 2/7/1
f 6/8/5 7/20/8 3/9/7
f 3/11/7 7/21/8 8/12/4
f 1/14/3 4/22/2 8/15/4

我们从上往下看。

  • #开头的是注释,因为我是用blender导出来的,所以会有一些Blender版本的描述。
  • mtllib smoothCube.mtl 相当于导入了一个材质文件,本文将不做详细介绍,会在后面的文章再做介绍。
  • o Cube 说明下面的数据都属于这个Cube Object。
  • v 1.000000 -1.000000 -1.000000 表示顶点位置,正方体一共8个顶点,所以有8行这样的数据。
  • vt 0.0000 -1.0000 表示顶点的UV。
  • vn -0.5773 0.5773 0.5773 表示顶点的法线。
  • usemtl Material 表示使用名为Material的材质,本文将不做介绍。
  • s 1 表明开启平滑渲染。
  • f 2/1/1 4/2/2 1/3/3 表示一个三角面, f 顶点索引/UV索引/法线索引 顶点索引/UV索引/法线索引 顶点索引/UV索引/法线索引,我们可以根据各个索引去取实际的值。这里的索引是从1开始的,在代码中,要记得减去1才能使用。

解析数据

在WavefrontOBJ中,包含了解析和渲染OBJ文件全部的方法。我们先来看解析的方法。

- (void)loadDataFromObj:(NSString *)filePath {
    NSString *fileContent = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
    NSArray<NSString *> *lines = [fileContent componentsSeparatedByString:@"\n"];
    for (NSString *line in lines) {
        if (line.length >= 2) {
            if ([line characterAtIndex:0] == 'v' && [line characterAtIndex:1] == ' ') {
                [self processVertexLine:line];
            } else if ([line characterAtIndex:0] == 'v' && [line characterAtIndex:1] == 'n') {
                [self processNormalLine:line];
            } else if ([line characterAtIndex:0] == 'v' && [line characterAtIndex:1] == 't') {
                [self processUVLine:line];
            } else if ([line characterAtIndex:0] == 'f' && [line characterAtIndex:1] == ' ') {
                [self processFaceIndexLine:line];
            }
        }
    }
}

- (void)processVertexLine:(NSString *)line {
    static NSString *pattern = @"v\\s*([\\-0-9]*\\.[\\-0-9]*)\\s*([\\-0-9]*\\.[\\-0-9]*)\\s*([\\-0-9]*\\.[\\-0-9]*)";
    static NSRegularExpression *regexExp = nil;
    if (regexExp == nil) {
        regexExp = [[NSRegularExpression alloc] initWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:nil];
    }
    NSArray * matchResults = [regexExp matchesInString:line options:0 range:NSMakeRange(0, line.length)];
    for (NSTextCheckingResult *result in matchResults) {
        NSUInteger rangeCount = result.numberOfRanges;
        if (rangeCount == 4) {
            GLfloat x = [[line substringWithRange: [result rangeAtIndex:1]] floatValue];
            GLfloat y = [[line substringWithRange: [result rangeAtIndex:2]] floatValue];
            GLfloat z = [[line substringWithRange: [result rangeAtIndex:3]] floatValue];
            [self.positionData appendBytes:(void *)(&x) length:sizeof(GLfloat)];
            [self.positionData appendBytes:(void *)(&y) length:sizeof(GLfloat)];
            [self.positionData appendBytes:(void *)(&z) length:sizeof(GLfloat)];
        }
    }
}

- (void)processNormalLine:(NSString *)line {
    static NSString *pattern = @"vn\\s*([\\-0-9]*\\.[\\-0-9]*)\\s*([\\-0-9]*\\.[\\-0-9]*)\\s*([\\-0-9]*\\.[\\-0-9]*)";
    static NSRegularExpression *regexExp = nil;
    if (regexExp == nil) {
        regexExp = [[NSRegularExpression alloc] initWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:nil];
    }
    NSArray * matchResults = [regexExp matchesInString:line options:0 range:NSMakeRange(0, line.length)];
    for (NSTextCheckingResult *result in matchResults) {
        NSUInteger rangeCount = result.numberOfRanges;
        if (rangeCount == 4) {
            GLfloat x = [[line substringWithRange: [result rangeAtIndex:1]] floatValue];
            GLfloat y = [[line substringWithRange: [result rangeAtIndex:2]] floatValue];
            GLfloat z = [[line substringWithRange: [result rangeAtIndex:3]] floatValue];
            [self.normalData appendBytes:(void *)(&x) length:sizeof(GLfloat)];
            [self.normalData appendBytes:(void *)(&y) length:sizeof(GLfloat)];
            [self.normalData appendBytes:(void *)(&z) length:sizeof(GLfloat)];
        }
    }
}

- (void)processUVLine:(NSString *)line {
    static NSString *pattern = @"vt\\s*([\\-0-9]*\\.[\\-0-9]*)\\s*([\\-0-9]*\\.[\\-0-9]*)";
    static NSRegularExpression *regexExp = nil;
    if (regexExp == nil) {
        regexExp = [[NSRegularExpression alloc] initWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:nil];
    }
    NSArray * matchResults = [regexExp matchesInString:line options:0 range:NSMakeRange(0, line.length)];
    for (NSTextCheckingResult *result in matchResults) {
        NSUInteger rangeCount = result.numberOfRanges;
        if (rangeCount == 3) {
            GLfloat x = [[line substringWithRange: [result rangeAtIndex:1]] floatValue];
            GLfloat y = [[line substringWithRange: [result rangeAtIndex:2]] floatValue];
            [self.uvData appendBytes:(void *)(&x) length:sizeof(GLfloat)];
            [self.uvData appendBytes:(void *)(&y) length:sizeof(GLfloat)];
        }
    }
}

- (void)processFaceIndexLine:(NSString *)line {
    static NSString *pattern = @"f\\s*([0-9]*)/([0-9]*)/([0-9]*)\\s*([0-9]*)/([0-9]*)/([0-9]*)\\s*([0-9]*)/([0-9]*)/([0-9]*)";
    static NSRegularExpression *regexExp = nil;
    if (regexExp == nil) {
        regexExp = [[NSRegularExpression alloc] initWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:nil];
    }
    NSArray * matchResults = [regexExp matchesInString:line options:0 range:NSMakeRange(0, line.length)];
    for (NSTextCheckingResult *result in matchResults) {
        NSUInteger rangeCount = result.numberOfRanges;
        if (rangeCount == 10) {
            // f 顶点/UV/法线 顶点/UV/法线 顶点/UV/法线
            GLuint vertexIndex1 = [[line substringWithRange: [result rangeAtIndex:1]] intValue] - 1;
            GLuint vertexIndex2 = [[line substringWithRange: [result rangeAtIndex:4]] intValue] - 1;
            GLuint vertexIndex3 = [[line substringWithRange: [result rangeAtIndex:7]] intValue] - 1;
            [self.positionIndexData appendBytes:(void *)(&vertexIndex1) length:sizeof(GLuint)];
            [self.positionIndexData appendBytes:(void *)(&vertexIndex2) length:sizeof(GLuint)];
            [self.positionIndexData appendBytes:(void *)(&vertexIndex3) length:sizeof(GLuint)];
            
            GLuint uvIndex1 = [[line substringWithRange: [result rangeAtIndex:2]] intValue] - 1;
            GLuint uvIndex2 = [[line substringWithRange: [result rangeAtIndex:5]] intValue] - 1;
            GLuint uvIndex3 = [[line substringWithRange: [result rangeAtIndex:8]] intValue] - 1;
            [self.uvIndexData appendBytes:(void *)(&uvIndex1) length:sizeof(GLuint)];
            [self.uvIndexData appendBytes:(void *)(&uvIndex2) length:sizeof(GLuint)];
            [self.uvIndexData appendBytes:(void *)(&uvIndex3) length:sizeof(GLuint)];
            
            GLuint normalIndex1 = [[line substringWithRange: [result rangeAtIndex:3]] intValue] - 1;
            GLuint normalIndex2 = [[line substringWithRange: [result rangeAtIndex:6]] intValue] - 1;
            GLuint normalIndex3 = [[line substringWithRange: [result rangeAtIndex:9]] intValue] - 1;
            [self.normalIndexData appendBytes:(void *)(&normalIndex1) length:sizeof(GLuint)];
            [self.normalIndexData appendBytes:(void *)(&normalIndex2) length:sizeof(GLuint)];
            [self.normalIndexData appendBytes:(void *)(&normalIndex3) length:sizeof(GLuint)];
        }
    }
}

我们分析每一行文本,使用正则提取数据,将顶点位置,UV,法线和位置索引,UV索引,法线索引分别放入下面的变量中。

@property (strong, nonatomic) NSMutableData *positionData;
@property (strong, nonatomic) NSMutableData *uvData;
@property (strong, nonatomic) NSMutableData *normalData;

@property (strong, nonatomic) NSMutableData *positionIndexData;
@property (strong, nonatomic) NSMutableData *uvIndexData;
@property (strong, nonatomic) NSMutableData *normalIndexData;

然后我们要将这些数据合并到一个顶点数组中,数组的格式是 位置,法线,UV,位置,法线,UV,位置,法线,UV,...

- (void)decompressToVertexArray {
    NSInteger vertexCount = self.positionIndexData.length / sizeof(GLuint);
    for (int i = 0; i < vertexCount; ++i) {
        int positionIndex = 0;
        [self.positionIndexData getBytes:&positionIndex range:NSMakeRange(i * sizeof(GLuint), sizeof(GLuint))];
        [self.vertexData appendBytes:(void *)((char *)self.positionData.bytes + positionIndex * 3 * sizeof(GLfloat)) length: 3 * sizeof(GLfloat)];
        
        int normalIndex = 0;
        [self.normalIndexData getBytes:&normalIndex range:NSMakeRange(i * sizeof(GLuint), sizeof(GLuint))];
        [self.vertexData appendBytes:(void *)((char *)self.normalData.bytes + normalIndex * 3 * sizeof(GLfloat)) length: 3 * sizeof(GLfloat)];
        
        int uvIndex = 0;
        [self.uvIndexData getBytes:&uvIndex range:NSMakeRange(i * sizeof(GLuint), sizeof(GLuint))];
        [self.vertexData appendBytes:(void *)((char *)self.uvData.bytes + uvIndex * 2 * sizeof(GLfloat)) length: 2 * sizeof(GLfloat)];
    }
}

上面的代码为每一个顶点依次压入位置数据,法线数据和UV数据,我们通过索引去寻找该顶点对应的位置,法线和UV。

绘制

我们有了顶点数组,通过生成VBO和VAO就可以很方便的绘制物体了。

- (void)genBufferObjects {
    glGenBuffers(1, &vertexVBO);
    glBindBuffer(GL_ARRAY_BUFFER, vertexVBO);
    glBufferData(GL_ARRAY_BUFFER, self.vertexData.length, self.vertexData.bytes, GL_STATIC_DRAW);
}

- (void)genVAO {
    glGenVertexArraysOES(1, &vao);
    glBindVertexArrayOES(vao);
    
    glBindBuffer(GL_ARRAY_BUFFER, vertexVBO);
    [self.context bindAttribs:NULL];
    
    glBindVertexArrayOES(0);
}

绘制部分和其他几何体几乎一样,只是需要通过索引数据的长度计算顶点个数,当然也可以在解析数据时把顶点个数缓存下来,只不过这里没有那么做而已。

- (void)draw:(GLContext *)glContext {
    [glContext setUniformMatrix4fv:@"modelMatrix" value:self.modelMatrix];
    bool canInvert;
    GLKMatrix4 normalMatrix = GLKMatrix4InvertAndTranspose(self.modelMatrix, &canInvert);
    [glContext setUniformMatrix4fv:@"normalMatrix" value:canInvert ? normalMatrix : GLKMatrix4Identity];
    NSInteger vertexCount = self.positionIndexData.length / sizeof(GLuint);
    [self.context drawTrianglesWithVAO:vao vertexCount:(GLuint)vertexCount];
}

最后,在ViewController中添加一个WavefrontOBJ对象就大功告成了。

- (void)createMonkeyFromObj {
    NSString *objFilePath = [[NSBundle mainBundle] pathForResource:@"car" ofType:@"obj"];
    WavefrontOBJ *monkeyModel = [[WavefrontOBJ alloc] initWithGLContext:self.glContext objFile:objFilePath];
    monkeyModel.modelMatrix = GLKMatrix4MakeRotation(- M_PI / 2.0, 0, 1, 0);
    [self.objects addObject:monkeyModel];
}

例子中提供了一个汽车模型,效果如下。


例子里面还有一个blender自带的猴子模型smoothMonkey.obj,大家也可以尝试一下。

本文主要介绍了对OBJ文件模型数据的解析和渲染,下篇文章将重点介绍对材质的解析和使用。

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

推荐阅读更多精彩内容