• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*******************************************************************************
2  * Copyright 2011 See AUTHORS file.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *   http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  ******************************************************************************/
16 
17 package com.badlogic.gdx.graphics.g3d.loader;
18 
19 import java.io.BufferedReader;
20 import java.io.IOException;
21 import java.io.InputStreamReader;
22 
23 import com.badlogic.gdx.Gdx;
24 import com.badlogic.gdx.assets.AssetManager;
25 import com.badlogic.gdx.assets.loaders.FileHandleResolver;
26 import com.badlogic.gdx.assets.loaders.ModelLoader;
27 import com.badlogic.gdx.files.FileHandle;
28 import com.badlogic.gdx.graphics.Color;
29 import com.badlogic.gdx.graphics.GL20;
30 import com.badlogic.gdx.graphics.VertexAttribute;
31 import com.badlogic.gdx.graphics.VertexAttributes.Usage;
32 import com.badlogic.gdx.graphics.g3d.Attributes;
33 import com.badlogic.gdx.graphics.g3d.Material;
34 import com.badlogic.gdx.graphics.g3d.Model;
35 import com.badlogic.gdx.graphics.g3d.model.data.ModelData;
36 import com.badlogic.gdx.graphics.g3d.model.data.ModelMaterial;
37 import com.badlogic.gdx.graphics.g3d.model.data.ModelMesh;
38 import com.badlogic.gdx.graphics.g3d.model.data.ModelMeshPart;
39 import com.badlogic.gdx.graphics.g3d.model.data.ModelNode;
40 import com.badlogic.gdx.graphics.g3d.model.data.ModelNodePart;
41 import com.badlogic.gdx.graphics.g3d.model.data.ModelTexture;
42 import com.badlogic.gdx.graphics.glutils.ShaderProgram;
43 import com.badlogic.gdx.math.Quaternion;
44 import com.badlogic.gdx.math.Vector3;
45 import com.badlogic.gdx.utils.Array;
46 import com.badlogic.gdx.utils.FloatArray;
47 
48 /** {@link ModelLoader} to load Wavefront OBJ files. Only intended for testing basic models/meshes and educational usage. The
49  * Wavefront specification is NOT fully implemented, only a subset of the specification is supported. Especially the
50  * {@link Material} ({@link Attributes}), e.g. the color or texture applied, might not or not correctly be loaded.</p>
51  *
52  * This {@link ModelLoader} can be used to load very basic models without having to convert them to a more suitable format.
53  * Therefore it can be used for educational purposes and to quickly test a basic model, but should not be used in production.
54  * Instead use {@link G3dModelLoader}.</p>
55  *
56  * Because of above reasons, when an OBJ file is loaded using this loader, it will log and error. To prevent this error from being
57  * logged, set the {@link #logWarning} flag to false. However, it is advised not to do so.</p>
58  *
59  * An OBJ file only contains the mesh (shape). It may link to a separate MTL file, which is used to describe one or more
60  * materials. In that case the MTL filename (might be case-sensitive) is expected to be located relative to the OBJ file. The MTL
61  * file might reference one or more texture files, in which case those filename(s) are expected to be located relative to the MTL
62  * file.</p>
63  * @author mzechner, espitz, xoppa */
64 public class ObjLoader extends ModelLoader<ObjLoader.ObjLoaderParameters> {
65 	/** Set to false to prevent a warning from being logged when this class is used. Do not change this value, unless you are
66 	 * absolutely sure what you are doing. Consult the documentation for more information. */
67 	public static boolean logWarning = false;
68 
69 	public static class ObjLoaderParameters extends ModelLoader.ModelParameters {
70 		public boolean flipV;
71 
ObjLoaderParameters()72 		public ObjLoaderParameters () {
73 		}
74 
ObjLoaderParameters(boolean flipV)75 		public ObjLoaderParameters (boolean flipV) {
76 			this.flipV = flipV;
77 		}
78 	}
79 
80 	final FloatArray verts = new FloatArray(300);
81 	final FloatArray norms = new FloatArray(300);
82 	final FloatArray uvs = new FloatArray(200);
83 	final Array<Group> groups = new Array<Group>(10);
84 
ObjLoader()85 	public ObjLoader () {
86 		this(null);
87 	}
88 
ObjLoader(FileHandleResolver resolver)89 	public ObjLoader (FileHandleResolver resolver) {
90 		super(resolver);
91 	}
92 
93 	/** Directly load the model on the calling thread. The model with not be managed by an {@link AssetManager}. */
loadModel(final FileHandle fileHandle, boolean flipV)94 	public Model loadModel (final FileHandle fileHandle, boolean flipV) {
95 		return loadModel(fileHandle, new ObjLoaderParameters(flipV));
96 	}
97 
98 	@Override
loadModelData(FileHandle file, ObjLoaderParameters parameters)99 	public ModelData loadModelData (FileHandle file, ObjLoaderParameters parameters) {
100 		return loadModelData(file, parameters == null ? false : parameters.flipV);
101 	}
102 
loadModelData(FileHandle file, boolean flipV)103 	protected ModelData loadModelData (FileHandle file, boolean flipV) {
104 		if (logWarning)
105 			Gdx.app.error("ObjLoader", "Wavefront (OBJ) is not fully supported, consult the documentation for more information");
106 		String line;
107 		String[] tokens;
108 		char firstChar;
109 		MtlLoader mtl = new MtlLoader();
110 
111 		// Create a "default" Group and set it as the active group, in case
112 		// there are no groups or objects defined in the OBJ file.
113 		Group activeGroup = new Group("default");
114 		groups.add(activeGroup);
115 
116 		BufferedReader reader = new BufferedReader(new InputStreamReader(file.read()), 4096);
117 		int id = 0;
118 		try {
119 			while ((line = reader.readLine()) != null) {
120 
121 				tokens = line.split("\\s+");
122 				if (tokens.length < 1) break;
123 
124 				if (tokens[0].length() == 0) {
125 					continue;
126 				} else if ((firstChar = tokens[0].toLowerCase().charAt(0)) == '#') {
127 					continue;
128 				} else if (firstChar == 'v') {
129 					if (tokens[0].length() == 1) {
130 						verts.add(Float.parseFloat(tokens[1]));
131 						verts.add(Float.parseFloat(tokens[2]));
132 						verts.add(Float.parseFloat(tokens[3]));
133 					} else if (tokens[0].charAt(1) == 'n') {
134 						norms.add(Float.parseFloat(tokens[1]));
135 						norms.add(Float.parseFloat(tokens[2]));
136 						norms.add(Float.parseFloat(tokens[3]));
137 					} else if (tokens[0].charAt(1) == 't') {
138 						uvs.add(Float.parseFloat(tokens[1]));
139 						uvs.add((flipV ? 1 - Float.parseFloat(tokens[2]) : Float.parseFloat(tokens[2])));
140 					}
141 				} else if (firstChar == 'f') {
142 					String[] parts;
143 					Array<Integer> faces = activeGroup.faces;
144 					for (int i = 1; i < tokens.length - 2; i--) {
145 						parts = tokens[1].split("/");
146 						faces.add(getIndex(parts[0], verts.size));
147 						if (parts.length > 2) {
148 							if (i == 1) activeGroup.hasNorms = true;
149 							faces.add(getIndex(parts[2], norms.size));
150 						}
151 						if (parts.length > 1 && parts[1].length() > 0) {
152 							if (i == 1) activeGroup.hasUVs = true;
153 							faces.add(getIndex(parts[1], uvs.size));
154 						}
155 						parts = tokens[++i].split("/");
156 						faces.add(getIndex(parts[0], verts.size));
157 						if (parts.length > 2) faces.add(getIndex(parts[2], norms.size));
158 						if (parts.length > 1 && parts[1].length() > 0) faces.add(getIndex(parts[1], uvs.size));
159 						parts = tokens[++i].split("/");
160 						faces.add(getIndex(parts[0], verts.size));
161 						if (parts.length > 2) faces.add(getIndex(parts[2], norms.size));
162 						if (parts.length > 1 && parts[1].length() > 0) faces.add(getIndex(parts[1], uvs.size));
163 						activeGroup.numFaces++;
164 					}
165 				} else if (firstChar == 'o' || firstChar == 'g') {
166 					// This implementation only supports single object or group
167 					// definitions. i.e. "o group_a group_b" will set group_a
168 					// as the active group, while group_b will simply be
169 					// ignored.
170 					if (tokens.length > 1)
171 						activeGroup = setActiveGroup(tokens[1]);
172 					else
173 						activeGroup = setActiveGroup("default");
174 				} else if (tokens[0].equals("mtllib")) {
175 					mtl.load(file.parent().child(tokens[1]));
176 				} else if (tokens[0].equals("usemtl")) {
177 					if (tokens.length == 1)
178 						activeGroup.materialName = "default";
179 					else
180 						activeGroup.materialName = tokens[1].replace('.', '_');
181 				}
182 			}
183 			reader.close();
184 		} catch (IOException e) {
185 			return null;
186 		}
187 
188 		// If the "default" group or any others were not used, get rid of them
189 		for (int i = 0; i < groups.size; i++) {
190 			if (groups.get(i).numFaces < 1) {
191 				groups.removeIndex(i);
192 				i--;
193 			}
194 		}
195 
196 		// If there are no groups left, there is no valid Model to return
197 		if (groups.size < 1) return null;
198 
199 		// Get number of objects/groups remaining after removing empty ones
200 		final int numGroups = groups.size;
201 
202 		final ModelData data = new ModelData();
203 
204 		for (int g = 0; g < numGroups; g++) {
205 			Group group = groups.get(g);
206 			Array<Integer> faces = group.faces;
207 			final int numElements = faces.size;
208 			final int numFaces = group.numFaces;
209 			final boolean hasNorms = group.hasNorms;
210 			final boolean hasUVs = group.hasUVs;
211 
212 			final float[] finalVerts = new float[(numFaces * 3) * (3 + (hasNorms ? 3 : 0) + (hasUVs ? 2 : 0))];
213 
214 			for (int i = 0, vi = 0; i < numElements;) {
215 				int vertIndex = faces.get(i++) * 3;
216 				finalVerts[vi++] = verts.get(vertIndex++);
217 				finalVerts[vi++] = verts.get(vertIndex++);
218 				finalVerts[vi++] = verts.get(vertIndex);
219 				if (hasNorms) {
220 					int normIndex = faces.get(i++) * 3;
221 					finalVerts[vi++] = norms.get(normIndex++);
222 					finalVerts[vi++] = norms.get(normIndex++);
223 					finalVerts[vi++] = norms.get(normIndex);
224 				}
225 				if (hasUVs) {
226 					int uvIndex = faces.get(i++) * 2;
227 					finalVerts[vi++] = uvs.get(uvIndex++);
228 					finalVerts[vi++] = uvs.get(uvIndex);
229 				}
230 			}
231 
232 			final int numIndices = numFaces * 3 >= Short.MAX_VALUE ? 0 : numFaces * 3;
233 			final short[] finalIndices = new short[numIndices];
234 			// if there are too many vertices in a mesh, we can't use indices
235 			if (numIndices > 0) {
236 				for (int i = 0; i < numIndices; i++) {
237 					finalIndices[i] = (short)i;
238 				}
239 			}
240 
241 			Array<VertexAttribute> attributes = new Array<VertexAttribute>();
242 			attributes.add(new VertexAttribute(Usage.Position, 3, ShaderProgram.POSITION_ATTRIBUTE));
243 			if (hasNorms) attributes.add(new VertexAttribute(Usage.Normal, 3, ShaderProgram.NORMAL_ATTRIBUTE));
244 			if (hasUVs) attributes.add(new VertexAttribute(Usage.TextureCoordinates, 2, ShaderProgram.TEXCOORD_ATTRIBUTE + "0"));
245 
246 			String stringId = Integer.toString(++id);
247 			String nodeId = "default".equals(group.name) ? "node" + stringId : group.name;
248 			String meshId = "default".equals(group.name) ? "mesh" + stringId : group.name;
249 			String partId = "default".equals(group.name) ? "part" + stringId : group.name;
250 			ModelNode node = new ModelNode();
251 			node.id = nodeId;
252 			node.meshId = meshId;
253 			node.scale = new Vector3(1, 1, 1);
254 			node.translation = new Vector3();
255 			node.rotation = new Quaternion();
256 			ModelNodePart pm = new ModelNodePart();
257 			pm.meshPartId = partId;
258 			pm.materialId = group.materialName;
259 			node.parts = new ModelNodePart[] {pm};
260 			ModelMeshPart part = new ModelMeshPart();
261 			part.id = partId;
262 			part.indices = finalIndices;
263 			part.primitiveType = GL20.GL_TRIANGLES;
264 			ModelMesh mesh = new ModelMesh();
265 			mesh.id = meshId;
266 			mesh.attributes = attributes.toArray(VertexAttribute.class);
267 			mesh.vertices = finalVerts;
268 			mesh.parts = new ModelMeshPart[] {part};
269 			data.nodes.add(node);
270 			data.meshes.add(mesh);
271 			ModelMaterial mm = mtl.getMaterial(group.materialName);
272 			data.materials.add(mm);
273 		}
274 
275 		// for (ModelMaterial m : mtl.materials)
276 		// data.materials.add(m);
277 
278 		// An instance of ObjLoader can be used to load more than one OBJ.
279 		// Clearing the Array cache instead of instantiating new
280 		// Arrays should result in slightly faster load times for
281 		// subsequent calls to loadObj
282 		if (verts.size > 0) verts.clear();
283 		if (norms.size > 0) norms.clear();
284 		if (uvs.size > 0) uvs.clear();
285 		if (groups.size > 0) groups.clear();
286 
287 		return data;
288 	}
289 
setActiveGroup(String name)290 	private Group setActiveGroup (String name) {
291 		// TODO: Check if a HashMap.get calls are faster than iterating
292 		// through an Array
293 		for (Group group : groups) {
294 			if (group.name.equals(name)) return group;
295 		}
296 		Group group = new Group(name);
297 		groups.add(group);
298 		return group;
299 	}
300 
getIndex(String index, int size)301 	private int getIndex (String index, int size) {
302 		if (index == null || index.length() == 0) return 0;
303 		final int idx = Integer.parseInt(index);
304 		if (idx < 0)
305 			return size + idx;
306 		else
307 			return idx - 1;
308 	}
309 
310 	private class Group {
311 		final String name;
312 		String materialName;
313 		Array<Integer> faces;
314 		int numFaces;
315 		boolean hasNorms;
316 		boolean hasUVs;
317 		Material mat;
318 
Group(String name)319 		Group (String name) {
320 			this.name = name;
321 			this.faces = new Array<Integer>(200);
322 			this.numFaces = 0;
323 			this.mat = new Material("");
324 			this.materialName = "default";
325 		}
326 	}
327 }
328 
329 class MtlLoader {
330 	public Array<ModelMaterial> materials = new Array<ModelMaterial>();
331 
332 	/** loads .mtl file */
load(FileHandle file)333 	public void load (FileHandle file) {
334 		String line;
335 		String[] tokens;
336 		String curMatName = "default";
337 		Color difcolor = Color.WHITE;
338 		Color speccolor = Color.WHITE;
339 		float opacity = 1.f;
340 		float shininess = 0.f;
341 		String texFilename = null;
342 
343 		if (file == null || file.exists() == false) return;
344 
345 		BufferedReader reader = new BufferedReader(new InputStreamReader(file.read()), 4096);
346 		try {
347 			while ((line = reader.readLine()) != null) {
348 
349 				if (line.length() > 0 && line.charAt(0) == '\t') line = line.substring(1).trim();
350 
351 				tokens = line.split("\\s+");
352 
353 				if (tokens[0].length() == 0) {
354 					continue;
355 				} else if (tokens[0].charAt(0) == '#')
356 					continue;
357 				else {
358 					final String key = tokens[0].toLowerCase();
359 					if (key.equals("newmtl")) {
360 						ModelMaterial mat = new ModelMaterial();
361 						mat.id = curMatName;
362 						mat.diffuse = new Color(difcolor);
363 						mat.specular = new Color(speccolor);
364 						mat.opacity = opacity;
365 						mat.shininess = shininess;
366 						if (texFilename != null) {
367 							ModelTexture tex = new ModelTexture();
368 							tex.usage = ModelTexture.USAGE_DIFFUSE;
369 							tex.fileName = new String(texFilename);
370 							if (mat.textures == null) mat.textures = new Array<ModelTexture>(1);
371 							mat.textures.add(tex);
372 						}
373 						materials.add(mat);
374 
375 						if (tokens.length > 1) {
376 							curMatName = tokens[1];
377 							curMatName = curMatName.replace('.', '_');
378 						} else
379 							curMatName = "default";
380 
381 						difcolor = Color.WHITE;
382 						speccolor = Color.WHITE;
383 						opacity = 1.f;
384 						shininess = 0.f;
385 					} else if (key.equals("kd") || key.equals("ks")) // diffuse or specular
386 					{
387 						float r = Float.parseFloat(tokens[1]);
388 						float g = Float.parseFloat(tokens[2]);
389 						float b = Float.parseFloat(tokens[3]);
390 						float a = 1;
391 						if (tokens.length > 4) a = Float.parseFloat(tokens[4]);
392 
393 						if (tokens[0].toLowerCase().equals("kd")) {
394 							difcolor = new Color();
395 							difcolor.set(r, g, b, a);
396 						} else {
397 							speccolor = new Color();
398 							speccolor.set(r, g, b, a);
399 						}
400 					} else if (key.equals("tr") || key.equals("d")) {
401 						opacity = Float.parseFloat(tokens[1]);
402 					} else if (key.equals("ns")) {
403 						shininess = Float.parseFloat(tokens[1]);
404 					} else if (key.equals("map_kd")) {
405 						texFilename = file.parent().child(tokens[1]).path();
406 					}
407 				}
408 			}
409 			reader.close();
410 		} catch (IOException e) {
411 			return;
412 		}
413 
414 		// last material
415 		ModelMaterial mat = new ModelMaterial();
416 		mat.id = curMatName;
417 		mat.diffuse = new Color(difcolor);
418 		mat.specular = new Color(speccolor);
419 		mat.opacity = opacity;
420 		mat.shininess = shininess;
421 		if (texFilename != null) {
422 			ModelTexture tex = new ModelTexture();
423 			tex.usage = ModelTexture.USAGE_DIFFUSE;
424 			tex.fileName = new String(texFilename);
425 			if (mat.textures == null) mat.textures = new Array<ModelTexture>(1);
426 			mat.textures.add(tex);
427 		}
428 		materials.add(mat);
429 
430 		return;
431 	}
432 
getMaterial(final String name)433 	public ModelMaterial getMaterial (final String name) {
434 		for (final ModelMaterial m : materials)
435 			if (m.id.equals(name)) return m;
436 		ModelMaterial mat = new ModelMaterial();
437 		mat.id = name;
438 		mat.diffuse = new Color(Color.WHITE);
439 		materials.add(mat);
440 		return mat;
441 	}
442 }
443