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