学习OpenGL ES之加载OBJ

作者: handyTOOL | 来源:发表于2017-06-21 11:33 被阅读733次

    本系列所有文章目录

    获取示例代码


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

    相关文章

      网友评论

      • 幸运星铠甲勇士:看博主的文章真的是思路清晰,简洁明了,赏心悦目
      • 许一世烟花:请问楼主,如果想精确控制模型被渲染在屏幕上的位置,应该怎么做呢?
      • CoderSJun:博主 加载过后的位置不在中间是咋回事啊?还有 如果要加手势 能够按照手势翻转的话 有什么思路吗?
        handyTOOL:在不在中间受模型本来的顶点数据控制,例子里面的汽车模型我从网上下载的,可能就不在中间。手势反转有两个方案,一个是改变模型矩阵的值,根据手指移动距离改变旋转矩阵。另一个是改变观察矩阵的观察位置,比如向上滑动,就将观察位置围绕中心点向下旋转。前者适合只控制单个物体的情况,后者适合旋转整个场景的情况。
      • 蒲Annie_:这个正是我需要的 谢谢博主
      • knowIt:如何能获取得到模型的大小呢?不知道模型的大小,就又可能一开始显示就会超出视图范围
        handyTOOL:@knowIt 我没有看到过,之前看fbx格式的时候也没发现,都是自己算的包围盒大小。
        knowIt:@handyTOOL 这是个办法,obj文件中有没有相关大小的信息?
        handyTOOL:@knowIt 可以在读取顶点位置的时候统计出各个轴的最大和最小值,这样就可以计算出一个立方体的包围盒大小了。

      本文标题:学习OpenGL ES之加载OBJ

      本文链接:https://www.haomeiwen.com/subject/sdmzqxtx.html