package org.terasology.asset.loaders; import com.google.common.collect.Lists; import gnu.trove.list.TFloatList; import gnu.trove.list.TIntList; import gnu.trove.list.array.TFloatArrayList; import gnu.trove.list.array.TIntArrayList; import org.terasology.asset.AssetLoader; import org.terasology.asset.AssetUri; import org.terasology.math.Vector3i; import org.terasology.rendering.primitives.Mesh; import javax.vecmath.Tuple3i; import javax.vecmath.Vector2f; import javax.vecmath.Vector3f; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.util.List; import java.util.logging.Logger; /** * Importer for Wavefront obj files. Supports core obj mesh data * * @author Immortius <immortius@gmail.com> */ public class ObjMeshLoader implements AssetLoader<Mesh> { private Logger logger = Logger.getLogger(getClass().getName()); @Override public Mesh load(InputStream stream, AssetUri uri, List<URL> urls) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); List<Vector3f> rawVertices = Lists.newArrayList(); List<Vector3f> rawNormals = Lists.newArrayList(); List<Vector2f> rawTexCoords = Lists.newArrayList(); List<Tuple3i[]> rawIndices = Lists.newArrayList(); // Gather data readMeshData(reader, rawVertices, rawNormals, rawTexCoords, rawIndices); // Process data TFloatList vertices = new TFloatArrayList(); TFloatList texCoord0 = new TFloatArrayList(); TFloatList normals = new TFloatArrayList(); TIntList indices = new TIntArrayList(); // Determine face format; if (rawIndices.size() == 0) { throw new IOException("No index data"); } processData(rawVertices, rawNormals, rawTexCoords, rawIndices, vertices, texCoord0, normals, indices); if (normals.size() != vertices.size() || texCoord0.size() / 2 != vertices.size() / 3) { throw new IOException("Mixed face format"); } return Mesh.buildMesh(uri, vertices, texCoord0, null, normals, null, indices); } private void processData(List<Vector3f> rawVertices, List<Vector3f> rawNormals, List<Vector2f> rawTexCoords, List<Tuple3i[]> rawIndices, TFloatList vertices, TFloatList texCoord0, TFloatList normals, TIntList indices) throws IOException { int vertCount = 0; for (Tuple3i[] face : rawIndices) { for (Tuple3i indexSet : face) { if (indexSet.x > rawVertices.size()) { throw new IOException("Vertex index out of range: " + indexSet.x); } Vector3f vertex = rawVertices.get(indexSet.x - 1); vertices.add(vertex.x); vertices.add(vertex.y); vertices.add(vertex.z); if (indexSet.y != -1) { if (indexSet.y > rawTexCoords.size()) { throw new IOException("TexCoord index out of range: " + indexSet.y); } Vector2f texCoord = rawTexCoords.get(indexSet.y - 1); texCoord0.add(texCoord.x); texCoord0.add(texCoord.y); } if (indexSet.z != -1) { if (indexSet.z > rawNormals.size()) { throw new IOException("Normal index out of range: " + indexSet.z); } Vector3f normal = rawNormals.get(indexSet.z - 1); normals.add(normal.x); normals.add(normal.y); normals.add(normal.z); } } for (int i = 0; i < face.length - 2; ++i) { indices.add(vertCount); indices.add(vertCount + i + 1); indices.add(vertCount + i + 2); } vertCount += face.length; } } private void readMeshData(BufferedReader reader, List<Vector3f> rawVertices, List<Vector3f> rawNormals, List<Vector2f> rawTexCoords, List<Tuple3i[]> rawIndices) throws IOException { String line = null; int lineNum = 0; try { while ((line = reader.readLine()) != null) { line = line.trim(); lineNum++; if (line.isEmpty()) continue; String[] prefixSplit = line.trim().split("\\s+", 2); String prefix = prefixSplit[0]; // Comment if ("#".equals(prefix)) continue; if (prefixSplit.length < 2) { throw new IOException(String.format("Incomplete statement")); } // JAVA7: Replace with switch // Object name if ("o".equals(prefix)) { // Just skip the name } // Vertex position else if ("v".equals(prefix)) { String[] floats = prefixSplit[1].trim().split("\\s+", 4); if (floats.length != 3) { throw new IOException("Bad statement"); } rawVertices.add(new Vector3f(Float.parseFloat(floats[0]), Float.parseFloat(floats[1]), Float.parseFloat(floats[2]))); } // Vertex texture coords else if ("vt".equals(prefix)) { String[] floats = prefixSplit[1].trim().split("\\s+", 4); if (floats.length < 2 || floats.length > 3) { throw new IOException("Bad statement"); } // Need to flip v coord, apparently rawTexCoords.add(new Vector2f(Float.parseFloat(floats[0]), 1 - Float.parseFloat(floats[1]))); } // Vertex normal else if ("vn".equals(prefix)) { String[] floats = prefixSplit[1].trim().split("\\s+", 4); if (floats.length != 3) { throw new IOException("Bad statement"); } rawNormals.add(new Vector3f(Float.parseFloat(floats[0]), Float.parseFloat(floats[1]), Float.parseFloat(floats[2]))); } // Material name (ignored) else if ("usemtl".equals(prefix)) { continue; } // Smoothing group (not supported) else if ("s".equals(prefix)) { if (!"off".equals(prefixSplit[1]) && !"0".equals(prefixSplit[1])) { logger.warning("Smoothing groups not supported in obj import yet"); } } // Face (polygon) else if ("f".equals(prefix)) { String[] elements = prefixSplit[1].trim().split("\\s+"); Tuple3i[] result = new Tuple3i[elements.length]; for (int i = 0; i < elements.length; ++i) { String[] parts = elements[i].split("/", 4); if (parts.length > 3) { throw new IOException("Bad Statement"); } result[i] = new Vector3i(Integer.parseInt(parts[0]), -1, -1); if (parts.length > 1 && !parts[1].isEmpty()) { result[i].y = Integer.parseInt(parts[1]); } if (parts.length > 2 && !parts[2].isEmpty()) { result[i].z = Integer.parseInt(parts[2]); } } rawIndices.add(result); } else { logger.warning(String.format("Skipping unsupported obj statement on line %d:\"%s\"", lineNum, line)); } } } catch (Exception e) { throw new IOException(String.format("Failed to process line %d:\"%s\"", lineNum, line), e); } } }