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