CSharpGL(9)解析OBJ文件并用CSharpGL渲染
由于CSharpGL一直在更新,现在这个教程已经不适用最新的代码了。CSharpGL源码中包含10多个独立的Demo,更适合入门参考。
为了尽可能提升渲染效率,CSharpGL是面向Shader的,因此稍有难度。
最近研究shader,需要一些典型的模型来显示效果。我自己做了几个。
但是都不如这个茶壶更典型。
我搜罗半天,找到几个用*.obj格式存储的茶壶模型,于是不得不写个OBJ格式文件的解析器来读取和渲染这个茶壶了。
下载
这个OBJ解析器是CSharpGL的一部分,CSharpGL已在GitHub开源,欢迎对OpenGL有兴趣的同学加入(https://github.com/bitzhuwei/CSharpGL)
OBJ文件格式
OBJ文件格式是非常简单的。这种文件以纯文本的形式存储了模型的顶点、法线和纹理坐标和材质使用信息。OBJ文件的每一行,都有极其相似的格式。在OBJ文件中,每行的格式如下:
前缀 参数1 参数2 参数3 ...
其中,前缀标识了这一行所存储的信息类型。参数则是具体的数据。OBJ文件常见的的前缀有
v 表示本行指定一个顶点。 前缀后跟着3个单精度浮点数,分别表示该定点的X、Y、Z坐标值
vt 表示本行指定一个纹理坐标。此前缀后跟着两个单精度浮点数。分别表示此纹理坐标的U、V值
vn 表示本行指定一个法线向量。此前缀后跟着3个单精度浮点数,分别表示该法向量的X、Y、Z坐标值
f 表示本行指定一个表面(Face)。一个表面实际上就是一个三角形图元。此前缀行的参数格式后面将详细介绍。
usemtl 此前缀后只跟着一个参数。该参数指定了从此行之后到下一个以usemtl开头的行之间的所有表面所使用的材质名称。该材质可以在此OBJ文件所附属的MTL文件中找到具体信息。
mtllib 此前缀后只跟着一个参数。该参数指定了此OBJ文件所使用的材质库文件(*.mtl)的文件路径
现在,我们再来看一下OBJ文件的结构。在一个OBJ文件中,首先有一些以v、vt或vn前缀开头的行指定了所有的顶点、纹理坐标、法线的坐标。然后再由一些以f开头的行指定每一个三角形所对应的顶点、纹理坐标和法线的索引。在顶点、纹理坐标和法线的索引之间,使用符号"/"隔开的。一个f行可以以下面几种格式出现:
f 1 2 3 这样的行表示以第1、2、3号顶点组成一个三角形。
f 1/3 2/5 3/4 这样的行表示以第1、2、3号顶点组成一个三角形,其中第一个顶点的纹理坐标的索引值为3,第二个顶点的纹理坐标的索引值为5,第三个顶点的纹理坐标的索引值为4。
f 1/3/4 2/5/6 3/4/2 这样的行表示以第1、2、3号顶点组成一个三角形,其中第一个顶点的纹理坐标的索引值为3,其法线的索引值是4;第二个顶点的纹理坐标的索引值为5,其法线的索引值是6;第三个顶点的纹理坐标的索引值为6,其法线的索引值是2。
f 1//4 2//6 3//2这样的行表示以第1、2、3号顶点组成一个三角形,且忽略纹理坐标。其中第一个顶点的法线的索引值是4;第二个顶点的法线的索引值是6;第三个顶点的法线的索引值是2。
值得注意的是文件中的索引值是以1作为起点的,这一点与C语言中以0作为起点有很大的不同。在渲染的时候应注意将从文件中读取的坐标值减去1。
另外,一个OBJ文件里可能有多个模型,每个模型都是由(若干顶点属性信息+若干面信息)这样的顺序描述的。
解析器设计思路
代码并不复杂。
1 public class ObjFile 2 { 3 private List<ObjModel> models = new List<ObjModel>(); 4 5 public List<ObjModel> Models 6 { 7 get { return models; } 8 //set { models = value; } 9 } 10 11 public static ObjFile Load(string filename) 12 { 13 ObjFile file = new ObjFile(); 14 15 LoadModels(filename, file); 16 GenNormals(file); 17 OrganizeModels(file); 18 19 return file; 20 } 21 22 private static void OrganizeModels(ObjFile file) 23 { 24 List<ObjModel> models = new List<ObjModel>(); 25 foreach (var model in file.models) 26 { 27 var newModel = OrganizeModels(model); 28 models.Add(newModel); 29 } 30 31 file.models.Clear(); 32 file.models.AddRange(models); 33 } 34 35 private static ObjModel OrganizeModels(ObjModel model) 36 { 37 ObjModel result = new ObjModel(); 38 result.positionList = model.positionList; 39 40 result.normalList.AddRange(model.normalList); 41 42 bool hasUV = model.uvList.Count > 0; 43 if (hasUV) 44 { 45 result.uvList.AddRange(model.uvList); 46 } 47 48 for (int i = 0; i < model.innerFaceList.Count; i++) 49 { 50 var face = model.innerFaceList[i]; 51 var tuple = new Tuple<int, int, int>(face.vertex0.position, face.vertex1.position, face.vertex2.position); 52 result.faceList.Add(tuple); 53 if (face.vertex0.normal > 0) 54 result.normalList[face.vertex0.position - 1] = model.normalList[face.vertex0.normal - 1]; 55 if (face.vertex1.normal > 0) 56 result.normalList[face.vertex1.position - 1] = model.normalList[face.vertex1.normal - 1]; 57 if (face.vertex2.normal > 0) 58 result.normalList[face.vertex2.position - 1] = model.normalList[face.vertex2.normal - 1]; 59 60 if (hasUV) 61 { 62 if (face.vertex0.uv > 0) 63 result.uvList[face.vertex0.position - 1] = model.uvList[face.vertex0.uv - 1]; 64 if (face.vertex1.uv > 0) 65 result.uvList[face.vertex1.position - 1] = model.uvList[face.vertex1.uv - 1]; 66 if (face.vertex2.uv > 0) 67 result.uvList[face.vertex2.position - 1] = model.uvList[face.vertex2.uv - 1]; 68 } 69 70 result.faceList.Add(new Tuple<int, int, int>(face.vertex0.position, face.vertex1.position, face.vertex2.position)); 71 //result.faceList[i] = new Tuple<int, int, int>(face.vertex0.position, face.vertex1.position, face.vertex2.position); 72 } 73 74 //model.innerFaceList.Clear(); 75 76 return result; 77 } 78 79 private static void GenNormals(ObjFile file) 80 { 81 foreach (var model in file.models) 82 { 83 GenNormals(model); 84 } 85 } 86 87 private static void GenNormals(ObjModel model) 88 { 89 if (model.normalList.Count > 0) { return; } 90 91 var faceNormals = new vec3[model.innerFaceList.Count]; 92 model.normalList.AddRange(new vec3[model.positionList.Count]); 93 94 for (int i = 0; i < model.innerFaceList.Count; i++) 95 { 96 var face = model.innerFaceList[i]; 97 vec3 vertex0 = model.positionList[face.vertex0.position - 1]; 98 vec3 vertex1 = model.positionList[face.vertex1.position - 1]; 99 vec3 vertex2 = model.positionList[face.vertex2.position - 1]; 100 vec3 v1 = vertex0 - vertex2; 101 vec3 v2 = vertex2 - vertex1; 102 faceNormals[i] = v1.cross(v2); 103 } 104 105 for (int i = 0; i < model.positionList.Count; i++) 106 { 107 vec3 sum = new vec3(); 108 int shared = 0; 109 for (int j = 0; j < model.innerFaceList.Count; j++) 110 { 111 var face = model.innerFaceList[j]; 112 if (face.vertex0.position - 1 == i || face.vertex1.position - 1 == i || face.vertex2.position - 1 == i) 113 { 114 sum = sum + faceNormals[i]; 115 shared++; 116 } 117 } 118 if (shared > 0) 119 { 120 sum = sum / shared; 121 sum.Normalize(); 122 } 123 model.normalList[i] = sum; 124 } 125 126 } 127 128 private static void LoadModels(string filename, ObjFile file) 129 { 130 using (var sr = new StreamReader(filename)) 131 { 132 var model = new ObjModel(); 133 134 while (!sr.EndOfStream) 135 { 136 string line = sr.ReadLine(); 137 string[] parts = line.Split(separator, StringSplitOptions.RemoveEmptyEntries); 138 if (parts[0] == ("v")) 139 { 140 if (model.innerFaceList.Count > 0) 141 { 142 file.models.Add(model); 143 model = new ObjModel(); 144 } 145 146 vec3 position = new vec3(float.Parse(parts[1]), float.Parse(parts[2]), float.Parse(parts[3])); 147 model.positionList.Add(position); 148 } 149 else if (parts[0] == ("vt")) 150 { 151 vec2 uv = new vec2(float.Parse(parts[1]), float.Parse(parts[2])); 152 model.uvList.Add(uv); 153 } 154 else if (parts[0] == ("vn")) 155 { 156 vec3 normal = new vec3(float.Parse(parts[1]), float.Parse(parts[2]), float.Parse(parts[3])); 157 model.normalList.Add(normal); 158 } 159 else if (parts[0] == ("f")) 160 { 161 Triangle triangle = ParseFace(parts); 162 model.innerFaceList.Add(triangle); 163 } 164 } 165 166 file.models.Add(model); 167 } 168 } 169 170 private static Triangle ParseFace(string[] parts) 171 { 172 Triangle result = new Triangle(); 173 if (parts[1].Contains("//")) 174 { 175 for (int i = 1; i < 4; i++) 176 { 177 string[] indexes = parts[i].Split('/'); 178 int position = int.Parse(indexes[0]); int normal = int.Parse(indexes[1]); 179 result[i - 1] = new VertexInfo() { position = position, normal = normal, uv = -1 }; 180 } 181 } 182 else if (parts[1].Contains("/")) 183 { 184 int components = parts[1].Split('/').Length; 185 if (components == 2) 186 { 187 for (int i = 1; i < 4; i++) 188 { 189 string[] indexes = parts[i].Split('/'); 190 int position = int.Parse(indexes[0]); int uv = int.Parse(indexes[1]); 191 result[i - 1] = new VertexInfo() { position = position, normal = -1, uv = uv }; 192 } 193 } 194 else if (components == 3) 195 { 196 for (int i = 1; i < 4; i++) 197 { 198 string[] indexes = parts[i].Split('/'); 199 int position = int.Parse(indexes[0]); int uv = int.Parse(indexes[1]); int normal = int.Parse(indexes[2]); 200 result[i - 1] = new VertexInfo() { position = position, normal = normal, uv = uv, }; 201 } 202 } 203 } 204 else 205 { 206 for (int i = 1; i < 4; i++) 207 { 208 int position = int.Parse(parts[i]); 209 result[i - 1] = new VertexInfo() { position = position, normal = -1, uv = -1, }; 210 } 211 } 212 213 return result; 214 } 215 216 static readonly char[] separator = new char[] { ' ' }; 217 static readonly char[] separator1 = new char[] { '/' }; 218 } 219 220 class VertexInfo 221 { 222 public int position; 223 public int normal; 224 public int uv; 225 } 226 class Triangle 227 { 228 public VertexInfo vertex0; 229 public VertexInfo vertex1; 230 public VertexInfo vertex2; 231 232 public VertexInfo this[int index] 233 { 234 set 235 { 236 if (index == 0) 237 { 238 this.vertex0 = value; 239 } 240 else if (index == 1) 241 { 242 this.vertex1 = value; 243 } 244 else if (index == 2) 245 { 246 this.vertex2 = value; 247 } 248 else 249 { 250 throw new ArgumentException(); 251 } 252 } 253 } 254 }