1 
2 module ws.file.obj;
3 
4 import std.conv;
5 
6 import ws.math.vector;
7 import ws.decode;
8 import ws.io;
9 import ws.string;
10 
11 
12 __gshared:
13 
14 
15 class MTL {
16 	public:
17 		class Material {
18 			this(string n){
19 				name = n;
20 			}
21 			string name;
22 			Vector!3 ambient;
23 			Vector!3 diffuse;
24 			Vector!3 specular;
25 			double dissolve;
26 			int illum;
27 			string mapDiffuse;
28 			string mapAmbient;
29 			string mapSpecular;
30 			string mapBump;
31 			string ext;
32 		};
33 		Material[] mtls;
34 
35 		void load(string path){
36 			Material currentMat;
37 			MTL currentMtl;
38 			currentMtl = this;
39 			try Decode.file(path, delegate(string command, string content, bool b){
40 				switch(command){
41 					case "newmtl":
42 						currentMat = new Material(content);
43 						currentMtl.mtls ~= currentMat;
44 						break;
45 							
46 					case "Ka":
47 						double[] v = toNumbers(content);
48 						for(size_t i=0; i<v.length; i++)
49 							currentMat.ambient[i] = v[i];
50 						break;
51 
52 					case "Kd":
53 						double[] v = toNumbers(content);
54 						for(size_t i=0; i<v.length; i++)
55 							currentMat.diffuse[i] = v[i];
56 						break;
57 
58 					case "Ks":
59 						double[] v = toNumbers(content);
60 						for(size_t i=0; i<v.length; i++)
61 							currentMat.specular[i] = v[i];
62 						break;
63 
64 					case "illum":
65 						currentMat.illum = to!int(content);
66 						break;
67 
68 					case "d":
69 						currentMat.dissolve = to!double(content);
70 						break;
71 
72 					case "map_Ka":
73 						currentMat.mapAmbient = content;
74 						break;
75 
76 					case "map_Kd":
77 						currentMat.mapDiffuse = content;
78 						break;
79 
80 					case "map_Ks":
81 						currentMat.mapSpecular = content;
82 						break;
83 					
84 					case "map_bump":
85 					case "bump":
86 						currentMat.mapBump = content;
87 						break;
88 					
89 					case "extern":
90 						currentMat.ext = content;
91 						break;
92 						
93 					default:
94 						break;//writeln("Unknown command: " ~ command);
95 
96 				}
97 
98 			});
99 			catch(Exception e)
100 				writeln("Failed to load MTL \"%\"\n", path, e);
101 		}
102 };
103 
104 
105 class OBJ {
106 		
107 	/+
108 	Decodes a wavefront obj file and stores it in this format:
109 	mtllibs:
110 	objects:
111 		name
112 		materials:
113 			name
114 			vertcount
115 			polygons:
116 				group
117 				smoothinggroup
118 				vertices:
119 					pos
120 					uvw
121 					normal
122 	+/
123 
124 	this(string name){
125 		path = name;
126 		dataVectors = [[], [], []];
127 
128 		currentObject = null;
129 		currentPath = "";
130 		if(name.findLast('/') < name.length)
131 			currentPath = name[0..name.findLast('/')+1];
132 
133 		currentObject = new OBJ.CObject("__default__");
134 		objects["__default__"] = currentObject;
135 	
136 		try Decode.file(name, delegate(string cmd, string content, bool block){
137 			switch(cmd){
138 				case "v":
139 					Vector!3 t;
140 					foreach(i, n; toNumbers(content)){
141 						if(i>2) break;
142 						t[i] = n;
143 					}
144 					dataVectors[0] ~= t;
145 				break;
146 			
147 				case "vt":
148 					Vector!3 t;
149 					foreach(i, n; toNumbers(content)){
150 						if(i>2) break;
151 						t[i] = n;
152 					}
153 					dataVectors[1] ~= t;
154 				break;
155 			
156 				case "vn":
157 					Vector!3 t;
158 					foreach(i, n; toNumbers(content)){
159 						if(i>2) break;
160 						t[i] = n;
161 					}
162 					dataVectors[2] ~= t;
163 				break;
164 			
165 				case "f":
166 					auto f = new OBJ.Polygon;
167 					currentMaterial.polygons ~= f;
168 					foreach(i, string s; content.split(' ')){
169 						if(s == "") continue;
170 						if(i > 2){
171 							// Convert all polygons with more than 3 vertices to triangles
172 							f.vertices ~= f.vertices[$-3];
173 							f.vertices ~= f.vertices[$-2];
174 							currentMaterial.vertcount += 2;
175 						}
176 						auto e = new OBJ.Vertex;
177 						f.vertices ~= e;
178 						string[] vertSplit = s.split('/');
179 						currentMaterial.vertcount++;
180 						for(size_t mode = 0; mode < 3; mode++){
181 							if(mode >= vertSplit.length || vertSplit[mode] == ""){
182 								// fill values that are not given
183 								e[mode] = (mode==1 ? Vector!3(e.pos[1], e.pos[0]+e.pos[2], 0)/100 : Vector!3(0,1,0));
184 							}else{
185 								size_t idx = to!size_t(vertSplit[mode]);
186 								if(idx<1 || idx>dataVectors[mode].length)
187 									throw new Exception(tostring("Model has dangerous % index (%)", (mode==0 ? "vertex" : (mode==1 ? "uvw" : "normal")), idx));
188 								else
189 									e[mode] = dataVectors[mode][idx-1];
190 							}
191 						}
192 					}
193 				break;
194 			
195 				case "mtllib":
196 					auto m = new MTL;
197 					m.load(currentPath ~ content);
198 					mtllibs ~= m;
199 				break;
200 			
201 				case "usemtl":
202 					if(content in currentObject.materials){
203 						currentMaterial = currentObject.materials[content];
204 						return;
205 					}
206 					currentMaterial = new OBJ.Material(content);
207 					currentObject.materials[content] = currentMaterial;
208 				break;
209 			
210 				case "o":
211 					if(content in objects){
212 						currentObject = objects[content];
213 						return;
214 					}
215 					currentObject = new OBJ.CObject(content);
216 					objects[content] = currentObject;
217 				break;
218 			
219 				case "g":
220 					currentGroup = content;
221 				break;
222 
223 				case "s":
224 					if(content == "off")
225 						currentSmooth = 0;
226 					else
227 						currentSmooth = to!long(content);
228 				break;
229 				
230 				default:
231 					options[cmd] = content;
232 					if(cmd !in unknownCommands){
233 						writeln("OBJ \"%\": Command not recognized: \"%\"", name, cmd);
234 						unknownCommands[cmd] = true;
235 					}
236 				}
237 		});
238 		catch(Exception e){
239 			throw new Exception("Failed to decode \"" ~ name ~ "\", " ~ e.toString);
240 		}
241 	}
242 
243 	static class Vertex {
244 		Vector!3 pos;
245 		Vector!3 uvw;
246 		Vector!3 normal;
247 		ref Vector!3 opIndex(long i){
248 			switch(i){
249 				case 0: return pos;
250 				case 1: return uvw;
251 				case 2: return normal;
252 				default: return pos;
253 			}
254 		}
255 	}
256 
257 	static class Polygon {
258 		string group;
259 		long smoothinggroup;
260 		Vertex[] vertices;
261 	}
262 
263 	static class Material {
264 		this(string n){
265 			name = n;
266 		}
267 		string name;
268 		int vertcount = 0;
269 		Polygon[] polygons;
270 	}
271 
272 	static class CObject {
273 		this(string n){
274 			name = n;
275 		}
276 		string name;
277 		Material[string] materials;
278 	}
279 
280 	MTL[] mtllibs;
281 
282 	CObject[string] objects;
283 
284 	bool[string] unknownCommands;
285 	string[string] options;
286 	Vector!3[][] dataVectors;
287 	Material currentMaterial;
288 	CObject currentObject;
289 	string currentGroup;
290 	long currentSmooth;
291 	string currentPath;
292 	string path;
293 	
294 }
295 
296 
297 
298 class DataOBJ {
299 	Vector!3[] vertices;
300 	Vector!3[] texCoords;
301 	Vector!3[] normals;
302 	Polygon[] polygons;
303 
304 	this(string name){
305 
306 		currentObject = null;
307 		currentPath = "";
308 		if(name.findLast('/') < name.length)
309 			currentPath = name[0..name.findLast('/')+1];
310 
311 		currentObject = new DataOBJ.CObject("__default__");
312 		objects["__default__"] = currentObject;
313 	
314 		try Decode.file(name, delegate(string cmd, string content, bool block){
315 			switch(cmd){
316 				case "v":
317 					Vector!3 t;
318 					foreach(i, n; toNumbers(content)){
319 						if(i>2) break;
320 						t[i] = n;
321 					}
322 					vertices ~= t;
323 				break;
324 			
325 				case "vt":
326 					Vector!3 t;
327 					foreach(i, n; toNumbers(content)){
328 						if(i>2) break;
329 						t[i] = n;
330 					}
331 					texCoords ~= t;
332 				break;
333 			
334 				case "vn":
335 					Vector!3 t;
336 					foreach(i, n; toNumbers(content)){
337 						if(i>2) break;
338 						t[i] = n;
339 					}
340 					normals ~= t;
341 				break;
342 			
343 				case "f":
344 					Polygon f = [];
345 					foreach(i, string s; content.split(' ')){
346 						if(s == "") continue;
347 						f ~= [-1, -1, -1];
348 						string[] vertSplit = s.split('/');
349 						currentMaterial.vertcount++;
350 						for(size_t mode = 0; mode < 3; mode++){
351 							if(mode < vertSplit.length && vertSplit[mode] != "")
352 								f[i][mode] = to!size_t(vertSplit[mode])-1;
353 						}
354 					}
355 					currentMaterial.polygons ~= f;
356 				break;
357 
358 				case "mtllib":
359 					auto m = new MTL;
360 					m.load(currentPath ~ content);
361 					mtllibs ~= m;
362 				break;
363 			
364 				case "usemtl":
365 					if(content in currentObject.materials){
366 						currentMaterial = currentObject.materials[content];
367 						return;
368 					}
369 					currentMaterial = new DataOBJ.Material(content);
370 					currentObject.materials[content] = currentMaterial;
371 				break;
372 			
373 				case "o":
374 				case "g":
375 					if(content in objects){
376 						currentObject = objects[content];
377 						return;
378 					}
379 					currentObject = new DataOBJ.CObject(content);
380 					objects[content] = currentObject;
381 				break;
382 			
383 				//case "g":
384 				//	currentGroup = content;
385 				//break;
386 
387 				case "s":
388 					if(content == "off")
389 						currentSmooth = 0;
390 					else
391 						currentSmooth = to!long(content);
392 				break;
393 				
394 				default:
395 					options[cmd] = content;
396 			}
397 		});
398 		catch(Exception e){
399 			throw new Exception("Failed to decode \"" ~ name ~ "\"", e);
400 		}
401 	}
402 	
403 	alias size_t[3][] Polygon; // [vertIndex, normalIndex, texIndex]
404 	
405 
406 	static class Material {
407 		this(string n){
408 			name = n;
409 		}
410 		string name;
411 		int vertcount = 0;
412 		Polygon[] polygons;
413 	}
414 
415 	static class CObject {
416 		this(string n){
417 			name = n;
418 		}
419 		string name;
420 		Material[string] materials;
421 	}
422 
423 	MTL[] mtllibs;
424 
425 	CObject[string] objects;
426 
427 	string[string] options;
428 	Material currentMaterial;
429 	CObject currentObject;
430 	string currentGroup;
431 	long currentSmooth;
432 	string currentPath;
433 	
434 }