加载OBJ

获取示例代码,本文代码在分支chapter17中。


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文件模型数据的解析和渲染,下篇文章将重点介绍对材质的解析和使用。

Updated: