1 | using System;
|
---|
2 | using System.Collections.Generic;
|
---|
3 | using System.Globalization;
|
---|
4 | using System.IO;
|
---|
5 |
|
---|
6 | namespace Oni.Dae.IO
|
---|
7 | {
|
---|
8 | internal class ObjReader
|
---|
9 | {
|
---|
10 | private struct ObjVertex : IEquatable<ObjVertex>
|
---|
11 | {
|
---|
12 | public int PointIndex;
|
---|
13 | public int TexCoordIndex;
|
---|
14 | public int NormalIndex;
|
---|
15 |
|
---|
16 | public ObjVertex(int pointIndex, int uvIndex, int normalIndex)
|
---|
17 | {
|
---|
18 | PointIndex = pointIndex;
|
---|
19 | TexCoordIndex = uvIndex;
|
---|
20 | NormalIndex = normalIndex;
|
---|
21 | }
|
---|
22 |
|
---|
23 | public static bool operator ==(ObjVertex v1, ObjVertex v2) =>
|
---|
24 | v1.PointIndex == v2.PointIndex
|
---|
25 | && v1.TexCoordIndex == v2.TexCoordIndex
|
---|
26 | && v1.NormalIndex == v2.NormalIndex;
|
---|
27 |
|
---|
28 | public static bool operator !=(ObjVertex v1, ObjVertex v2) =>
|
---|
29 | v1.PointIndex != v2.PointIndex
|
---|
30 | || v1.TexCoordIndex != v2.TexCoordIndex
|
---|
31 | || v1.NormalIndex != v2.NormalIndex;
|
---|
32 |
|
---|
33 | public bool Equals(ObjVertex v) => this == v;
|
---|
34 | public override bool Equals(object obj) => obj is ObjVertex && Equals((ObjVertex)obj);
|
---|
35 | public override int GetHashCode() => PointIndex ^ TexCoordIndex ^ NormalIndex;
|
---|
36 | }
|
---|
37 |
|
---|
38 | private class ObjFace
|
---|
39 | {
|
---|
40 | public string ObjectName;
|
---|
41 | public string[] GroupsNames;
|
---|
42 | public ObjVertex[] Vertices;
|
---|
43 | }
|
---|
44 |
|
---|
45 | private class ObjMaterial
|
---|
46 | {
|
---|
47 | private readonly string name;
|
---|
48 | private string textureFilePath;
|
---|
49 | private Material material;
|
---|
50 |
|
---|
51 | public ObjMaterial(string name)
|
---|
52 | {
|
---|
53 | this.name = name;
|
---|
54 | }
|
---|
55 |
|
---|
56 | public string Name => name;
|
---|
57 |
|
---|
58 | public string TextureFilePath
|
---|
59 | {
|
---|
60 | get { return textureFilePath; }
|
---|
61 | set { textureFilePath = value; }
|
---|
62 | }
|
---|
63 |
|
---|
64 | public Material Material
|
---|
65 | {
|
---|
66 | get
|
---|
67 | {
|
---|
68 | if (material == null && TextureFilePath != null)
|
---|
69 | CreateMaterial();
|
---|
70 |
|
---|
71 | return material;
|
---|
72 | }
|
---|
73 | }
|
---|
74 |
|
---|
75 | private void CreateMaterial()
|
---|
76 | {
|
---|
77 | var image = new Image
|
---|
78 | {
|
---|
79 | FilePath = TextureFilePath,
|
---|
80 | Name = name + "_img"
|
---|
81 | };
|
---|
82 |
|
---|
83 | var effectSurface = new EffectSurface(image);
|
---|
84 | var effectSampler = new EffectSampler(effectSurface);
|
---|
85 | var effectTexture = new EffectTexture
|
---|
86 | {
|
---|
87 | Sampler = effectSampler,
|
---|
88 | Channel = EffectTextureChannel.Diffuse,
|
---|
89 | TexCoordSemantic = "diffuse_TEXCOORD"
|
---|
90 | };
|
---|
91 |
|
---|
92 | material = new Material
|
---|
93 | {
|
---|
94 | Id = name,
|
---|
95 | Name = name,
|
---|
96 | Effect = new Effect
|
---|
97 | {
|
---|
98 | Id = name + "_fx",
|
---|
99 | DiffuseValue = effectTexture,
|
---|
100 | Parameters = {
|
---|
101 | new EffectParameter("surface", effectSurface),
|
---|
102 | new EffectParameter("sampler", effectSampler)
|
---|
103 | }
|
---|
104 | }
|
---|
105 | };
|
---|
106 | }
|
---|
107 | }
|
---|
108 |
|
---|
109 | private class ObjPrimitives
|
---|
110 | {
|
---|
111 | public ObjMaterial Material;
|
---|
112 | public readonly List<ObjFace> Faces = new List<ObjFace>(4);
|
---|
113 | }
|
---|
114 |
|
---|
115 | #region Private data
|
---|
116 | private static readonly string[] emptyStrings = new string[0];
|
---|
117 | private static readonly char[] whiteSpaceChars = new char[] { ' ', '\t' };
|
---|
118 | private static readonly char[] vertexSeparator = new char[] { '/' };
|
---|
119 |
|
---|
120 | private Scene mainScene;
|
---|
121 |
|
---|
122 | private readonly List<Vector3> points = new List<Vector3>();
|
---|
123 | private readonly List<Vector2> texCoords = new List<Vector2>();
|
---|
124 | private readonly List<Vector3> normals = new List<Vector3>();
|
---|
125 |
|
---|
126 | private int pointCount;
|
---|
127 | private int normalCount;
|
---|
128 | private int texCoordCount;
|
---|
129 |
|
---|
130 | private readonly Dictionary<Vector3, int> pointIndex = new Dictionary<Vector3, int>();
|
---|
131 | private readonly Dictionary<Vector3, int> normalIndex = new Dictionary<Vector3, int>();
|
---|
132 | private readonly Dictionary<Vector2, int> texCoordIndex = new Dictionary<Vector2, int>();
|
---|
133 | private readonly List<int> pointRemap = new List<int>();
|
---|
134 | private readonly List<int> normalRemap = new List<int>();
|
---|
135 | private readonly List<int> texCoordRemap = new List<int>();
|
---|
136 |
|
---|
137 | private readonly Dictionary<string, ObjMaterial> materials = new Dictionary<string, ObjMaterial>(StringComparer.Ordinal);
|
---|
138 |
|
---|
139 | private string currentObjectName;
|
---|
140 | private string[] currentGroupNames;
|
---|
141 | private readonly List<ObjPrimitives> primitives = new List<ObjPrimitives>();
|
---|
142 | private ObjPrimitives currentPrimitives;
|
---|
143 | #endregion
|
---|
144 |
|
---|
145 | public static Scene ReadFile(string filePath)
|
---|
146 | {
|
---|
147 | var reader = new ObjReader();
|
---|
148 | reader.ReadObjFile(filePath);
|
---|
149 | reader.ImportObjects();
|
---|
150 | return reader.mainScene;
|
---|
151 | }
|
---|
152 |
|
---|
153 | private void ReadObjFile(string filePath)
|
---|
154 | {
|
---|
155 | mainScene = new Scene();
|
---|
156 |
|
---|
157 | foreach (string line in ReadLines(filePath))
|
---|
158 | {
|
---|
159 | var tokens = line.Split(whiteSpaceChars, StringSplitOptions.RemoveEmptyEntries);
|
---|
160 |
|
---|
161 | switch (tokens[0])
|
---|
162 | {
|
---|
163 | case "o":
|
---|
164 | ReadObject(tokens);
|
---|
165 | break;
|
---|
166 |
|
---|
167 | case "g":
|
---|
168 | ReadGroup(tokens);
|
---|
169 | break;
|
---|
170 |
|
---|
171 | case "v":
|
---|
172 | ReadPoint(tokens);
|
---|
173 | break;
|
---|
174 |
|
---|
175 | case "vn":
|
---|
176 | ReadNormal(tokens);
|
---|
177 | break;
|
---|
178 |
|
---|
179 | case "vt":
|
---|
180 | ReadTexCoord(tokens);
|
---|
181 | break;
|
---|
182 |
|
---|
183 | case "f":
|
---|
184 | case "fo":
|
---|
185 | ReadFace(tokens);
|
---|
186 | break;
|
---|
187 |
|
---|
188 | case "mtllib":
|
---|
189 | ReadMtlLib(filePath, tokens);
|
---|
190 | break;
|
---|
191 |
|
---|
192 | case "usemtl":
|
---|
193 | ReadUseMtl(tokens);
|
---|
194 | break;
|
---|
195 | }
|
---|
196 | }
|
---|
197 | }
|
---|
198 |
|
---|
199 | private void ReadPoint(string[] tokens)
|
---|
200 | {
|
---|
201 | var point = new Vector3(
|
---|
202 | float.Parse(tokens[1], CultureInfo.InvariantCulture),
|
---|
203 | float.Parse(tokens[2], CultureInfo.InvariantCulture),
|
---|
204 | float.Parse(tokens[3], CultureInfo.InvariantCulture));
|
---|
205 |
|
---|
206 | AddPoint(point);
|
---|
207 |
|
---|
208 | pointCount++;
|
---|
209 | }
|
---|
210 |
|
---|
211 | private void AddPoint(Vector3 point)
|
---|
212 | {
|
---|
213 | int newIndex;
|
---|
214 |
|
---|
215 | if (pointIndex.TryGetValue(point, out newIndex))
|
---|
216 | {
|
---|
217 | pointRemap.Add(newIndex);
|
---|
218 | }
|
---|
219 | else
|
---|
220 | {
|
---|
221 | pointRemap.Add(points.Count);
|
---|
222 | pointIndex.Add(point, points.Count);
|
---|
223 | points.Add(point);
|
---|
224 | }
|
---|
225 | }
|
---|
226 |
|
---|
227 | private void ReadNormal(string[] tokens)
|
---|
228 | {
|
---|
229 | var normal = new Vector3(
|
---|
230 | float.Parse(tokens[1], CultureInfo.InvariantCulture),
|
---|
231 | float.Parse(tokens[2], CultureInfo.InvariantCulture),
|
---|
232 | float.Parse(tokens[3], CultureInfo.InvariantCulture));
|
---|
233 |
|
---|
234 | AddNormal(normal);
|
---|
235 |
|
---|
236 | normalCount++;
|
---|
237 | }
|
---|
238 |
|
---|
239 | private void AddNormal(Vector3 normal)
|
---|
240 | {
|
---|
241 | int newIndex;
|
---|
242 |
|
---|
243 | if (normalIndex.TryGetValue(normal, out newIndex))
|
---|
244 | {
|
---|
245 | normalRemap.Add(newIndex);
|
---|
246 | }
|
---|
247 | else
|
---|
248 | {
|
---|
249 | normalRemap.Add(normals.Count);
|
---|
250 | normalIndex.Add(normal, normals.Count);
|
---|
251 | normals.Add(normal);
|
---|
252 | }
|
---|
253 | }
|
---|
254 |
|
---|
255 | private void ReadTexCoord(string[] tokens)
|
---|
256 | {
|
---|
257 | var texCoord = new Vector2(
|
---|
258 | float.Parse(tokens[1], CultureInfo.InvariantCulture),
|
---|
259 | 1.0f - float.Parse(tokens[2], CultureInfo.InvariantCulture));
|
---|
260 |
|
---|
261 | AddTexCoord(texCoord);
|
---|
262 |
|
---|
263 | texCoordCount++;
|
---|
264 | }
|
---|
265 |
|
---|
266 | private void AddTexCoord(Vector2 texCoord)
|
---|
267 | {
|
---|
268 | int newIndex;
|
---|
269 |
|
---|
270 | if (texCoordIndex.TryGetValue(texCoord, out newIndex))
|
---|
271 | {
|
---|
272 | texCoordRemap.Add(newIndex);
|
---|
273 | }
|
---|
274 | else
|
---|
275 | {
|
---|
276 | texCoordRemap.Add(texCoords.Count);
|
---|
277 | texCoordIndex.Add(texCoord, texCoords.Count);
|
---|
278 | texCoords.Add(texCoord);
|
---|
279 | }
|
---|
280 | }
|
---|
281 |
|
---|
282 | private void ReadFace(string[] tokens)
|
---|
283 | {
|
---|
284 | var faceVertices = ReadVertices(tokens);
|
---|
285 |
|
---|
286 | if (currentPrimitives == null)
|
---|
287 | ReadUseMtl(emptyStrings);
|
---|
288 |
|
---|
289 | currentPrimitives.Faces.Add(new ObjFace
|
---|
290 | {
|
---|
291 | ObjectName = currentObjectName,
|
---|
292 | GroupsNames = currentGroupNames,
|
---|
293 | Vertices = faceVertices
|
---|
294 | });
|
---|
295 | }
|
---|
296 |
|
---|
297 | private ObjVertex[] ReadVertices(string[] tokens)
|
---|
298 | {
|
---|
299 | var vertices = new ObjVertex[tokens.Length - 1];
|
---|
300 |
|
---|
301 | for (int i = 0; i < vertices.Length; i++)
|
---|
302 | {
|
---|
303 | //
|
---|
304 | // Read a point/texture/normal index pair
|
---|
305 | //
|
---|
306 |
|
---|
307 | var indices = tokens[i + 1].Split(vertexSeparator);
|
---|
308 |
|
---|
309 | if (indices.Length == 0 || indices.Length > 3)
|
---|
310 | throw new InvalidDataException();
|
---|
311 |
|
---|
312 | //
|
---|
313 | // Extract indices from file: 0 means "not specified"
|
---|
314 | //
|
---|
315 |
|
---|
316 | int pointIndex = int.Parse(indices[0], CultureInfo.InvariantCulture);
|
---|
317 | int texCoordIndex = (indices.Length > 1 && indices[1].Length > 0) ? int.Parse(indices[1], CultureInfo.InvariantCulture) : 0;
|
---|
318 | int normalIndex = (indices.Length > 2 && indices[2].Length > 0) ? int.Parse(indices[2], CultureInfo.InvariantCulture) : 0;
|
---|
319 |
|
---|
320 | //
|
---|
321 | // Adjust for negative indices
|
---|
322 | //
|
---|
323 |
|
---|
324 | if (pointIndex < 0)
|
---|
325 | pointIndex = pointCount + pointIndex + 1;
|
---|
326 |
|
---|
327 | if (texCoordIndex < 0)
|
---|
328 | texCoordIndex = texCoordCount + texCoordIndex + 1;
|
---|
329 |
|
---|
330 | if (normalIndex < 0)
|
---|
331 | normalIndex = normalCount + normalIndex + 1;
|
---|
332 |
|
---|
333 | //
|
---|
334 | // Convert indices to internal representation: range 0..n and -1 means "not specified".
|
---|
335 | //
|
---|
336 |
|
---|
337 | pointIndex = pointIndex - 1;
|
---|
338 | texCoordIndex = texCoordIndex - 1;
|
---|
339 | normalIndex = normalIndex - 1;
|
---|
340 |
|
---|
341 | //
|
---|
342 | // Remap indices
|
---|
343 | //
|
---|
344 |
|
---|
345 | pointIndex = pointRemap[pointIndex];
|
---|
346 |
|
---|
347 | if (texCoordIndex < 0 || texCoordRemap.Count <= texCoordIndex)
|
---|
348 | texCoordIndex = -1;
|
---|
349 | else
|
---|
350 | texCoordIndex = texCoordRemap[texCoordIndex];
|
---|
351 |
|
---|
352 | if (normalIndex < 0 || normalRemap.Count <= normalIndex)
|
---|
353 | normalIndex = -1;
|
---|
354 | else
|
---|
355 | normalIndex = normalRemap[normalIndex];
|
---|
356 |
|
---|
357 | vertices[i] = new ObjVertex
|
---|
358 | {
|
---|
359 | PointIndex = pointIndex,
|
---|
360 | TexCoordIndex = texCoordIndex,
|
---|
361 | NormalIndex = normalIndex
|
---|
362 | };
|
---|
363 | }
|
---|
364 |
|
---|
365 | return vertices;
|
---|
366 | }
|
---|
367 |
|
---|
368 | private void ReadObject(string[] tokens)
|
---|
369 | {
|
---|
370 | currentObjectName = tokens[1];
|
---|
371 | }
|
---|
372 |
|
---|
373 | private void ReadGroup(string[] tokens)
|
---|
374 | {
|
---|
375 | currentGroupNames = tokens;
|
---|
376 | }
|
---|
377 |
|
---|
378 | private void ReadUseMtl(string[] tokens)
|
---|
379 | {
|
---|
380 | currentPrimitives = new ObjPrimitives();
|
---|
381 |
|
---|
382 | if (tokens.Length > 0)
|
---|
383 | materials.TryGetValue(tokens[1], out currentPrimitives.Material);
|
---|
384 |
|
---|
385 | primitives.Add(currentPrimitives);
|
---|
386 | }
|
---|
387 |
|
---|
388 | private void ReadMtlLib(string objFilePath, string[] tokens)
|
---|
389 | {
|
---|
390 | string materialLibraryFilePath = tokens[1];
|
---|
391 |
|
---|
392 | if (Path.GetExtension(materialLibraryFilePath).Length == 0)
|
---|
393 | materialLibraryFilePath += ".mtl";
|
---|
394 |
|
---|
395 | var dirPath = Path.GetDirectoryName(objFilePath);
|
---|
396 | var mtlFilePath = Path.Combine(dirPath, materialLibraryFilePath);
|
---|
397 |
|
---|
398 | if (!File.Exists(mtlFilePath))
|
---|
399 | {
|
---|
400 | Console.Error.WriteLine("Material file {0} does not exist", mtlFilePath);
|
---|
401 | return;
|
---|
402 | }
|
---|
403 |
|
---|
404 | ReadMtlFile(mtlFilePath);
|
---|
405 | }
|
---|
406 |
|
---|
407 | private void ReadMtlFile(string filePath)
|
---|
408 | {
|
---|
409 | var dirPath = Path.GetDirectoryName(filePath);
|
---|
410 |
|
---|
411 | ObjMaterial currentMaterial = null;
|
---|
412 |
|
---|
413 | foreach (string line in ReadLines(filePath))
|
---|
414 | {
|
---|
415 | var tokens = line.Split(whiteSpaceChars, StringSplitOptions.RemoveEmptyEntries);
|
---|
416 |
|
---|
417 | switch (tokens[0])
|
---|
418 | {
|
---|
419 | case "newmtl":
|
---|
420 | currentMaterial = new ObjMaterial(tokens[1]);
|
---|
421 | materials[currentMaterial.Name] = currentMaterial;
|
---|
422 | break;
|
---|
423 |
|
---|
424 | case "map_Kd":
|
---|
425 | string textureFilePath = Path.GetFullPath(Path.Combine(dirPath, tokens[1]));
|
---|
426 |
|
---|
427 | if (File.Exists(textureFilePath))
|
---|
428 | currentMaterial.TextureFilePath = textureFilePath;
|
---|
429 |
|
---|
430 | break;
|
---|
431 | }
|
---|
432 | }
|
---|
433 | }
|
---|
434 |
|
---|
435 | private void ImportObjects()
|
---|
436 | {
|
---|
437 | var inputs = new List<IndexedInput>();
|
---|
438 | IndexedInput positionInput, texCoordInput, normalInput;
|
---|
439 |
|
---|
440 | positionInput = new IndexedInput(Semantic.Position, new Source(points));
|
---|
441 | inputs.Add(positionInput);
|
---|
442 |
|
---|
443 | if (texCoords.Count > 0)
|
---|
444 | {
|
---|
445 | texCoordInput = new IndexedInput(Semantic.TexCoord, new Source(texCoords));
|
---|
446 | inputs.Add(texCoordInput);
|
---|
447 | }
|
---|
448 | else
|
---|
449 | {
|
---|
450 | texCoordInput = null;
|
---|
451 | }
|
---|
452 |
|
---|
453 | if (normals.Count > 0)
|
---|
454 | {
|
---|
455 | normalInput = new IndexedInput(Semantic.Normal, new Source(normals));
|
---|
456 | inputs.Add(normalInput);
|
---|
457 | }
|
---|
458 | else
|
---|
459 | {
|
---|
460 | normalInput = null;
|
---|
461 | }
|
---|
462 |
|
---|
463 | var geometry = new Geometry
|
---|
464 | {
|
---|
465 | Vertices = { positionInput }
|
---|
466 | };
|
---|
467 |
|
---|
468 | var geometryInstance = new GeometryInstance
|
---|
469 | {
|
---|
470 | Target = geometry
|
---|
471 | };
|
---|
472 |
|
---|
473 | foreach (var primitive in primitives.Where(p => p.Faces.Count > 0))
|
---|
474 | {
|
---|
475 | var meshPrimtives = new MeshPrimitives(MeshPrimitiveType.Polygons, inputs);
|
---|
476 |
|
---|
477 | foreach (var face in primitive.Faces)
|
---|
478 | {
|
---|
479 | meshPrimtives.VertexCounts.Add(face.Vertices.Length);
|
---|
480 |
|
---|
481 | foreach (var vertex in face.Vertices)
|
---|
482 | {
|
---|
483 | positionInput.Indices.Add(vertex.PointIndex);
|
---|
484 |
|
---|
485 | if (texCoordInput != null)
|
---|
486 | texCoordInput.Indices.Add(vertex.TexCoordIndex);
|
---|
487 |
|
---|
488 | if (normalInput != null)
|
---|
489 | normalInput.Indices.Add(vertex.NormalIndex);
|
---|
490 | }
|
---|
491 | }
|
---|
492 |
|
---|
493 | geometry.Primitives.Add(meshPrimtives);
|
---|
494 |
|
---|
495 | if (primitive.Material != null && primitive.Material.Material != null)
|
---|
496 | {
|
---|
497 | meshPrimtives.MaterialSymbol = "mat" + geometryInstance.Materials.Count;
|
---|
498 |
|
---|
499 | geometryInstance.Materials.Add(new MaterialInstance
|
---|
500 | {
|
---|
501 | Symbol = meshPrimtives.MaterialSymbol,
|
---|
502 | Target = primitive.Material.Material,
|
---|
503 | Bindings = { new MaterialBinding("diffuse_TEXCOORD", texCoordInput) }
|
---|
504 | });
|
---|
505 | }
|
---|
506 | }
|
---|
507 |
|
---|
508 | mainScene.Nodes.Add(new Node
|
---|
509 | {
|
---|
510 | Instances = { geometryInstance }
|
---|
511 | });
|
---|
512 | }
|
---|
513 |
|
---|
514 | private Vector3 ComputeFaceNormal(ObjVertex[] vertices)
|
---|
515 | {
|
---|
516 | if (vertices.Length < 3)
|
---|
517 | return Vector3.Up;
|
---|
518 |
|
---|
519 | var v1 = points[vertices[0].PointIndex];
|
---|
520 | var v2 = points[vertices[1].PointIndex];
|
---|
521 | var v3 = points[vertices[2].PointIndex];
|
---|
522 |
|
---|
523 | return Vector3.Normalize(Vector3.Cross(v2 - v1, v3 - v1));
|
---|
524 | }
|
---|
525 |
|
---|
526 | private static IEnumerable<string> ReadLines(string filePath)
|
---|
527 | {
|
---|
528 | using (var reader = File.OpenText(filePath))
|
---|
529 | {
|
---|
530 | for (var line = reader.ReadLine(); line != null; line = reader.ReadLine())
|
---|
531 | {
|
---|
532 | line = line.Trim();
|
---|
533 |
|
---|
534 | if (line.Length == 0)
|
---|
535 | continue;
|
---|
536 |
|
---|
537 | int commentStart = line.IndexOf('#');
|
---|
538 |
|
---|
539 | if (commentStart != -1)
|
---|
540 | {
|
---|
541 | line = line.Substring(0, commentStart).Trim();
|
---|
542 |
|
---|
543 | if (line.Length == 0)
|
---|
544 | continue;
|
---|
545 | }
|
---|
546 |
|
---|
547 | yield return line;
|
---|
548 | }
|
---|
549 | }
|
---|
550 | }
|
---|
551 | }
|
---|
552 | }
|
---|