/******************************************************************************* * Copyright 2011 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. ******************************************************************************/ package com.badlogic.gdx.graphics.g3d.loaders.wavefront; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.assets.AssetManager; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.GL10; import com.badlogic.gdx.graphics.Mesh; import com.badlogic.gdx.graphics.Pixmap; import com.badlogic.gdx.graphics.Pixmap.Format; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.Texture.TextureFilter; import com.badlogic.gdx.graphics.VertexAttribute; import com.badlogic.gdx.graphics.VertexAttributes.Usage; import com.badlogic.gdx.graphics.g3d.ModelLoaderHints; import com.badlogic.gdx.graphics.g3d.loaders.StillModelLoader; import com.badlogic.gdx.graphics.g3d.materials.ColorAttribute; import com.badlogic.gdx.graphics.g3d.materials.Material; import com.badlogic.gdx.graphics.g3d.materials.TextureAttribute; import com.badlogic.gdx.graphics.g3d.model.still.StillModel; import com.badlogic.gdx.graphics.g3d.model.still.StillSubMesh; import com.badlogic.gdx.graphics.glutils.ShaderProgram; import com.badlogic.gdx.utils.FloatArray; /** * Loads Wavefront OBJ files. * * @author mzechner, espitz */ public class ObjLoader implements StillModelLoader { final FloatArray verts; final FloatArray norms; final FloatArray uvs; final ArrayList<Group> groups; public ObjLoader() { verts = new FloatArray(300); norms = new FloatArray(300); uvs = new FloatArray(200); groups = new ArrayList<Group>(10); } /** * Loads a Wavefront OBJ file from a given file handle. * * @param file * the FileHandle */ public StillModel loadObj(FileHandle file) { return loadObj(file, false); } /** * Loads a Wavefront OBJ file from a given file handle. * * @param file * the FileHandle * @param flipV * whether to flip the v texture coordinate (Blender, Wings3D, et al) */ public StillModel loadObj(FileHandle file, boolean flipV) { return loadObj(file, file.parent(), flipV); } /** * Loads a Wavefront OBJ file from a given file handle. * * @param file * the FileHandle * @param textureDir * @param flipV * whether to flip the v texture coordinate (Blender, Wings3D, et al) */ public StillModel loadObj(FileHandle file, FileHandle textureDir, boolean flipV) { String line; String[] tokens; char firstChar; MtlLoader mtl = new MtlLoader(); // Create a "default" Group and set it as the active group, in case // there are no groups or objects defined in the OBJ file. Group activeGroup = new Group("default"); groups.add(activeGroup); BufferedReader reader = new BufferedReader(new InputStreamReader(file.read()), 4096); try { while ((line = reader.readLine()) != null) { tokens = line.split("\\s+"); if (tokens[0].length() == 0) { continue; } else if ((firstChar = tokens[0].toLowerCase().charAt(0)) == '#') { continue; } else if (firstChar == 'v') { if (tokens[0].length() == 1) { verts.add(Float.parseFloat(tokens[1])); verts.add(Float.parseFloat(tokens[2])); verts.add(Float.parseFloat(tokens[3])); } else if (tokens[0].charAt(1) == 'n') { norms.add(Float.parseFloat(tokens[1])); norms.add(Float.parseFloat(tokens[2])); norms.add(Float.parseFloat(tokens[3])); } else if (tokens[0].charAt(1) == 't') { uvs.add(Float.parseFloat(tokens[1])); uvs.add((flipV ? 1 - Float.parseFloat(tokens[2]) : Float.parseFloat(tokens[2]))); } } else if (firstChar == 'f') { String[] parts; ArrayList<Integer> faces = activeGroup.faces; for (int i = 1; i < tokens.length - 2; i--) { parts = tokens[1].split("/"); faces.add(getIndex(parts[0], verts.size)); if (parts.length > 2) { if (i == 1) activeGroup.hasNorms = true; faces.add(getIndex(parts[2], norms.size)); } if (parts.length > 1 && parts[1].length() > 0) { if (i == 1) activeGroup.hasUVs = true; faces.add(getIndex(parts[1], uvs.size)); } parts = tokens[++i].split("/"); faces.add(getIndex(parts[0], verts.size)); if (parts.length > 2) faces.add(getIndex(parts[2], norms.size)); if (parts.length > 1 && parts[1].length() > 0) faces.add(getIndex(parts[1], uvs.size)); parts = tokens[++i].split("/"); faces.add(getIndex(parts[0], verts.size)); if (parts.length > 2) faces.add(getIndex(parts[2], norms.size)); if (parts.length > 1 && parts[1].length() > 0) faces.add(getIndex(parts[1], uvs.size)); activeGroup.numFaces++; } } else if (firstChar == 'o' || firstChar == 'g') { // This implementation only supports single object or group // definitions. i.e. "o group_a group_b" will set group_a // as the active group, while group_b will simply be // ignored. if (tokens.length > 1) activeGroup = setActiveGroup(tokens[1]); else activeGroup = setActiveGroup("default"); } else if (tokens[0].equals("mtllib")) { String path = ""; if (file.path().contains("/")) { path = file.path().substring(0, file.path().lastIndexOf('/') + 1); } mtl.load(path + tokens[1], textureDir); } else if (tokens[0].equals("usemtl")) { if (tokens.length == 1) activeGroup.materialName = "default"; else activeGroup.materialName = tokens[1]; } } reader.close(); } catch (IOException e) { return null; } // If the "default" group or any others were not used, get rid of them for (int i = 0; i < groups.size(); i++) { if (groups.get(i).numFaces < 1) { groups.remove(i); i--; } } // If there are no groups left, there is no valid Model to return if (groups.size() < 1) return null; // Get number of objects/groups remaining after removing empty ones final int numGroups = groups.size(); final StillModel model = new StillModel(new StillSubMesh[numGroups]); for (int g = 0; g < numGroups; g++) { Group group = groups.get(g); ArrayList<Integer> faces = group.faces; final int numElements = faces.size(); final int numFaces = group.numFaces; final boolean hasNorms = group.hasNorms; final boolean hasUVs = group.hasUVs; final float[] finalVerts = new float[(numFaces * 3) * (3 + (hasNorms ? 3 : 0) + (hasUVs ? 2 : 0))]; for (int i = 0, vi = 0; i < numElements;) { int vertIndex = faces.get(i++) * 3; finalVerts[vi++] = verts.get(vertIndex++); finalVerts[vi++] = verts.get(vertIndex++); finalVerts[vi++] = verts.get(vertIndex); if (hasNorms) { int normIndex = faces.get(i++) * 3; finalVerts[vi++] = norms.get(normIndex++); finalVerts[vi++] = norms.get(normIndex++); finalVerts[vi++] = norms.get(normIndex); } if (hasUVs) { int uvIndex = faces.get(i++) * 2; finalVerts[vi++] = uvs.get(uvIndex++); finalVerts[vi++] = uvs.get(uvIndex); } } final int numIndices = numFaces * 3 >= Short.MAX_VALUE ? 0 : numFaces * 3; final short[] finalIndices = new short[numIndices]; // if there are too many vertices in a mesh, we can't use indices if (numIndices > 0) { for (int i = 0; i < numIndices; i++) { finalIndices[i] = (short) i; } } final Mesh mesh; ArrayList<VertexAttribute> attributes = new ArrayList<VertexAttribute>(); attributes.add(new VertexAttribute(Usage.Position, 3, ShaderProgram.POSITION_ATTRIBUTE)); if (hasNorms) attributes.add(new VertexAttribute(Usage.Normal, 3, ShaderProgram.NORMAL_ATTRIBUTE)); if (hasUVs) attributes .add(new VertexAttribute(Usage.TextureCoordinates, 2, ShaderProgram.TEXCOORD_ATTRIBUTE + "0")); mesh = new Mesh(true, numFaces * 3, numIndices, attributes.toArray(new VertexAttribute[attributes.size()])); mesh.setVertices(finalVerts); if (numIndices > 0) mesh.setIndices(finalIndices); StillSubMesh subMesh = new StillSubMesh(group.name, mesh, GL10.GL_TRIANGLES); subMesh.material = mtl.getMaterial(group.materialName); model.subMeshes[g] = subMesh; } // An instance of ObjLoader can be used to load more than one OBJ. // Clearing the ArrayList cache instead of instantiating new // ArrayLists should result in slightly faster load times for // subsequent calls to loadObj if (verts.size > 0) verts.clear(); if (norms.size > 0) norms.clear(); if (uvs.size > 0) uvs.clear(); if (groups.size() > 0) groups.clear(); return model; } private Group setActiveGroup(String name) { // TODO: Check if a HashMap.get calls are faster than iterating // through an ArrayList for (Group group : groups) { if (group.name.equals(name)) return group; } Group group = new Group(name); groups.add(group); return group; } private int getIndex(String index, int size) { if (index == null || index.length() == 0) return 0; final int idx = Integer.parseInt(index); if (idx < 0) return size + idx; else return idx - 1; } private class Group { final String name; String materialName; ArrayList<Integer> faces; int numFaces; boolean hasNorms; boolean hasUVs; Material mat; Group(String name) { this.name = name; this.faces = new ArrayList<Integer>(200); this.numFaces = 0; this.mat = new Material(""); this.materialName = "default"; } } @Override public StillModel load(FileHandle handle, ModelLoaderHints hints) { return loadObj(handle, hints.flipV); } } class MtlLoader { private ArrayList<Material> materials = new ArrayList<Material>(); private static AssetManager assetManager; private static Texture emptyTexture = null; public MtlLoader() { if (emptyTexture == null) { assetManager = new AssetManager(); Pixmap pm = new Pixmap(1, 1, Format.RGB888); pm.setColor(0.5f, 0.5f, 0.5f, 1); pm.fill(); emptyTexture = new Texture(pm, false); } } /** * loads .mtl file * * @param name */ public void load(String name, FileHandle textureDir) { String line; String[] tokens; String curMatName = "default"; Color difcolor = Color.WHITE; Color speccolor = Color.WHITE; Texture texture = emptyTexture; FileHandle file = Gdx.files.internal(name); if (file == null || file.exists() == false) return; BufferedReader reader = new BufferedReader(new InputStreamReader(file.read()), 4096); try { while ((line = reader.readLine()) != null) { if (line.length() > 0 && line.charAt(0) == '\t') line = line.substring(1).trim(); tokens = line.split("\\s+"); if (tokens[0].length() == 0) { continue; } else if (tokens[0].charAt(0) == '#') continue; else if (tokens[0].toLowerCase().equals("newmtl")) { Material mat = new Material(curMatName, new TextureAttribute(texture, 0, TextureAttribute.diffuseTexture), new ColorAttribute(difcolor, ColorAttribute.diffuse), new ColorAttribute(speccolor, ColorAttribute.specular)); materials.add(mat); if (tokens.length > 1) { curMatName = tokens[1]; curMatName = curMatName.replace('.', '_'); } else curMatName = "default"; difcolor = Color.WHITE; speccolor = Color.WHITE; } else if (tokens[0].toLowerCase().equals("kd") || tokens[0].toLowerCase().equals("ks")) // diffuse or specular { float r = Float.parseFloat(tokens[1]); float g = Float.parseFloat(tokens[2]); float b = Float.parseFloat(tokens[3]); float a = 1; if (tokens.length > 4) a = Float.parseFloat(tokens[4]); if (tokens[0].toLowerCase().equals("kd")) { difcolor = new Color(); difcolor.set(r, g, b, a); } else { speccolor = new Color(); speccolor.set(r, g, b, a); } } else if (tokens[0].toLowerCase().equals("map_kd")) { String textureName = tokens[1]; if (textureName.length() > 0) { String texname = textureDir.child(textureName).toString(); assetManager.load(texname, Texture.class); assetManager.finishLoading(); texture = assetManager.get(texname, Texture.class); texture.setFilter(TextureFilter.Linear, TextureFilter.Linear); } else texture = emptyTexture; } } reader.close(); } catch (IOException e) { return; } // last material Material mat = new Material(curMatName, new TextureAttribute(texture, 0, TextureAttribute.diffuseTexture), new ColorAttribute(difcolor, ColorAttribute.diffuse), new ColorAttribute(speccolor, ColorAttribute.specular)); materials.add(mat); return; } public Material getMaterial(String name) { name = name.replace('.', '_'); for (Material mat : materials) { if (mat.getName().equals(name)) return mat; } return new Material("default"); } }