/******************************************************************************* * 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.files.FileHandle; import com.badlogic.gdx.graphics.GL10; import com.badlogic.gdx.graphics.Mesh; 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.Material; 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) { String line; String[] tokens; char firstChar; // 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"); } } 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 = new Material("default"); 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; 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(""); } } @Override public StillModel load (FileHandle handle, ModelLoaderHints hints) { return loadObj(handle, hints.flipV); } }