Home | History | Annotate | Download | only in loader
      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 
     72 		public ObjLoaderParameters () {
     73 		}
     74 
     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 
     85 	public ObjLoader () {
     86 		this(null);
     87 	}
     88 
     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}. */
     94 	public Model loadModel (final FileHandle fileHandle, boolean flipV) {
     95 		return loadModel(fileHandle, new ObjLoaderParameters(flipV));
     96 	}
     97 
     98 	@Override
     99 	public ModelData loadModelData (FileHandle file, ObjLoaderParameters parameters) {
    100 		return loadModelData(file, parameters == null ? false : parameters.flipV);
    101 	}
    102 
    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 
    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 
    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 
    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 */
    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 
    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