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 |
} |