Skip to content

Instantly share code, notes, and snippets.

@yuripourre
Created April 3, 2022 02:21
Show Gist options
  • Save yuripourre/e1161bf3c58f8ae89e32fd1ef69d98f7 to your computer and use it in GitHub Desktop.
Save yuripourre/e1161bf3c58f8ae89e32fd1ef69d98f7 to your computer and use it in GitHub Desktop.
ObjLoader with Vertex Color
/*******************************************************************************
* 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.loader;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.assets.AssetManager;
import com.badlogic.gdx.assets.loaders.FileHandleResolver;
import com.badlogic.gdx.assets.loaders.ModelLoader;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.VertexAttribute;
import com.badlogic.gdx.graphics.VertexAttributes.Usage;
import com.badlogic.gdx.graphics.g3d.Attributes;
import com.badlogic.gdx.graphics.g3d.Material;
import com.badlogic.gdx.graphics.g3d.Model;
import com.badlogic.gdx.graphics.g3d.model.data.ModelData;
import com.badlogic.gdx.graphics.g3d.model.data.ModelMaterial;
import com.badlogic.gdx.graphics.g3d.model.data.ModelMesh;
import com.badlogic.gdx.graphics.g3d.model.data.ModelMeshPart;
import com.badlogic.gdx.graphics.g3d.model.data.ModelNode;
import com.badlogic.gdx.graphics.g3d.model.data.ModelNodePart;
import com.badlogic.gdx.graphics.g3d.model.data.ModelTexture;
import com.badlogic.gdx.graphics.glutils.ShaderProgram;
import com.badlogic.gdx.math.Quaternion;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.FloatArray;
/** {@link ModelLoader} to load Wavefront OBJ files. Only intended for testing basic models/meshes and educational usage. The
* Wavefront specification is NOT fully implemented, only a subset of the specification is supported. Especially the
* {@link Material} ({@link Attributes}), e.g. the color or texture applied, might not or not correctly be loaded.
* </p>
*
* This {@link ModelLoader} can be used to load very basic models without having to convert them to a more suitable format.
* Therefore it can be used for educational purposes and to quickly test a basic model, but should not be used in production.
* Instead use {@link G3dModelLoader}.
* </p>
*
* Because of above reasons, when an OBJ file is loaded using this loader, it will log and error. To prevent this error from being
* logged, set the {@link #logWarning} flag to false. However, it is advised not to do so.
* </p>
*
* An OBJ file only contains the mesh (shape). It may link to a separate MTL file, which is used to describe one or more
* materials. In that case the MTL filename (might be case-sensitive) is expected to be located relative to the OBJ file. The MTL
* file might reference one or more texture files, in which case those filename(s) are expected to be located relative to the MTL
* file.
* </p>
* @author mzechner, espitz, xoppa */
public class ObjLoader extends ModelLoader<ObjLoader.ObjLoaderParameters> {
/** Set to false to prevent a warning from being logged when this class is used. Do not change this value, unless you are
* absolutely sure what you are doing. Consult the documentation for more information. */
public static boolean logWarning = false;
public static class ObjLoaderParameters extends ModelLoader.ModelParameters {
public boolean flipV;
public boolean vertexColors;
public ObjLoaderParameters () {
}
public ObjLoaderParameters (boolean flipV) {
this.flipV = flipV;
}
public ObjLoaderParameters (boolean flipV, boolean vertexColors) {
this.flipV = flipV;
this.vertexColors = vertexColors;
}
}
final FloatArray verts = new FloatArray(300);
final FloatArray norms = new FloatArray(300);
final FloatArray uvs = new FloatArray(200);
final Array<Group> groups = new Array<Group>(10);
public ObjLoader () {
this(null);
}
public ObjLoader (FileHandleResolver resolver) {
super(resolver);
}
/** Directly load the model on the calling thread. The model with not be managed by an {@link AssetManager}. */
public Model loadModel (final FileHandle fileHandle, boolean flipV) {
return loadModel(fileHandle, new ObjLoaderParameters(flipV));
}
public Model loadModel (final FileHandle fileHandle, boolean flipV, boolean vertexColors) {
return loadModel(fileHandle, new ObjLoaderParameters(flipV, vertexColors));
}
@Override
public ModelData loadModelData (FileHandle file, ObjLoaderParameters parameters) {
return loadModelData(file, parameters != null && parameters.flipV, parameters != null && parameters.vertexColors);
}
protected ModelData loadModelData (FileHandle file, boolean flipV, boolean vertexColors) {
if (logWarning)
Gdx.app.error("ObjLoader", "Wavefront (OBJ) is not fully supported, consult the documentation for more information");
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);
int id = 0;
try {
while ((line = reader.readLine()) != null) {
tokens = line.split("\\s+");
if (tokens.length < 1) break;
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]));
if (vertexColors) {
// Use norms as colors to avoid more memory allocation
norms.add(Float.parseFloat(tokens[4]));
norms.add(Float.parseFloat(tokens[5]));
norms.add(Float.parseFloat(tokens[6]));
}
} 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;
Array<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 (vertexColors) {
faces.add(getIndex(parts[0], norms.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));
}
// Parse second vertex
parts = tokens[++i].split("/");
faces.add(getIndex(parts[0], verts.size));
if (vertexColors) {
faces.add(getIndex(parts[0], norms.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));
// Parse third vertex
parts = tokens[++i].split("/");
faces.add(getIndex(parts[0], verts.size));
if (vertexColors) {
faces.add(getIndex(parts[0], norms.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")) {
mtl.load(file.parent().child(tokens[1]));
} else if (tokens[0].equals("usemtl")) {
if (tokens.length == 1)
activeGroup.materialName = "default";
else
activeGroup.materialName = tokens[1].replace('.', '_');
}
}
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.removeIndex(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 ModelData data = new ModelData();
for (int g = 0; g < numGroups; g++) {
Group group = groups.get(g);
Array<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 || vertexColors) ? 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 || vertexColors) {
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;
}
}
Array<VertexAttribute> attributes = new Array<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"));
if (vertexColors) attributes.add(new VertexAttribute(Usage.ColorUnpacked, 3, ShaderProgram.COLOR_ATTRIBUTE));
String stringId = Integer.toString(++id);
String nodeId = "default".equals(group.name) ? "node" + stringId : group.name;
String meshId = "default".equals(group.name) ? "mesh" + stringId : group.name;
String partId = "default".equals(group.name) ? "part" + stringId : group.name;
ModelNode node = new ModelNode();
node.id = nodeId;
node.meshId = meshId;
node.scale = new Vector3(1, 1, 1);
node.translation = new Vector3();
node.rotation = new Quaternion();
ModelNodePart pm = new ModelNodePart();
pm.meshPartId = partId;
pm.materialId = group.materialName;
node.parts = new ModelNodePart[] {pm};
ModelMeshPart part = new ModelMeshPart();
part.id = partId;
part.indices = finalIndices;
part.primitiveType = GL20.GL_TRIANGLES;
ModelMesh mesh = new ModelMesh();
mesh.id = meshId;
mesh.attributes = attributes.toArray(VertexAttribute.class);
mesh.vertices = finalVerts;
mesh.parts = new ModelMeshPart[] {part};
data.nodes.add(node);
data.meshes.add(mesh);
ModelMaterial mm = mtl.getMaterial(group.materialName);
data.materials.add(mm);
}
// for (ModelMaterial m : mtl.materials)
// data.materials.add(m);
// An instance of ObjLoader can be used to load more than one OBJ.
// Clearing the Array cache instead of instantiating new
// Arrays 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 data;
}
private Group setActiveGroup (String name) {
// TODO: Check if a HashMap.get calls are faster than iterating
// through an Array
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 static class Group {
final String name;
String materialName;
Array<Integer> faces;
int numFaces;
boolean hasNorms;
boolean hasUVs;
Material mat;
Group (String name) {
this.name = name;
this.faces = new Array<Integer>(200);
this.numFaces = 0;
this.mat = new Material("");
this.materialName = "default";
}
}
}
class MtlLoader {
public Array<ModelMaterial> materials = new Array<ModelMaterial>();
/** loads .mtl file */
public void load (FileHandle file) {
String line;
String[] tokens;
ObjMaterial currentMaterial = new ObjMaterial();
if (file == null || !file.exists()) 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 {
final String key = tokens[0].toLowerCase();
if (key.equals("newmtl")) {
ModelMaterial mat = currentMaterial.build();
materials.add(mat);
if (tokens.length > 1) {
currentMaterial.materialName = tokens[1];
currentMaterial.materialName = currentMaterial.materialName.replace('.', '_');
} else {
currentMaterial.materialName = "default";
}
currentMaterial.reset();
} else if (key.equals("ka")) {
currentMaterial.ambientColor = parseColor(tokens);
} else if (key.equals("kd")) {
currentMaterial.diffuseColor = parseColor(tokens);
} else if (key.equals("ks")) {
currentMaterial.specularColor = parseColor(tokens);
} else if (key.equals("tr") || key.equals("d")) {
currentMaterial.opacity = Float.parseFloat(tokens[1]);
} else if (key.equals("ns")) {
currentMaterial.shininess = Float.parseFloat(tokens[1]);
} else if (key.equals("map_d")) {
currentMaterial.alphaTexFilename = file.parent().child(tokens[1]).path();
} else if (key.equals("map_ka")) {
currentMaterial.ambientTexFilename = file.parent().child(tokens[1]).path();
} else if (key.equals("map_kd")) {
currentMaterial.diffuseTexFilename = file.parent().child(tokens[1]).path();
} else if (key.equals("map_ks")) {
currentMaterial.specularTexFilename = file.parent().child(tokens[1]).path();
} else if (key.equals("map_ns")) {
currentMaterial.shininessTexFilename = file.parent().child(tokens[1]).path();
}
}
}
reader.close();
} catch (IOException e) {
return;
}
// last material
ModelMaterial mat = currentMaterial.build();
materials.add(mat);
return;
}
private Color parseColor (String[] tokens) {
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]);
}
return new Color(r, g, b, a);
}
public ModelMaterial getMaterial (final String name) {
for (final ModelMaterial m : materials)
if (m.id.equals(name)) return m;
ModelMaterial mat = new ModelMaterial();
mat.id = name;
mat.diffuse = new Color(Color.WHITE);
materials.add(mat);
return mat;
}
private static class ObjMaterial {
String materialName = "default";
Color ambientColor;
Color diffuseColor;
Color specularColor;
float opacity;
float shininess;
String alphaTexFilename;
String ambientTexFilename;
String diffuseTexFilename;
String shininessTexFilename;
String specularTexFilename;
public ObjMaterial () {
reset();
}
public ModelMaterial build () {
ModelMaterial mat = new ModelMaterial();
mat.id = materialName;
mat.ambient = ambientColor == null ? null : new Color(ambientColor);
mat.diffuse = new Color(diffuseColor);
mat.specular = new Color(specularColor);
mat.opacity = opacity;
mat.shininess = shininess;
addTexture(mat, alphaTexFilename, ModelTexture.USAGE_TRANSPARENCY);
addTexture(mat, ambientTexFilename, ModelTexture.USAGE_AMBIENT);
addTexture(mat, diffuseTexFilename, ModelTexture.USAGE_DIFFUSE);
addTexture(mat, specularTexFilename, ModelTexture.USAGE_SPECULAR);
addTexture(mat, shininessTexFilename, ModelTexture.USAGE_SHININESS);
return mat;
}
private void addTexture (ModelMaterial mat, String texFilename, int usage) {
if (texFilename != null) {
ModelTexture tex = new ModelTexture();
tex.usage = usage;
tex.fileName = texFilename;
if (mat.textures == null) mat.textures = new Array<ModelTexture>(1);
mat.textures.add(tex);
}
}
public void reset () {
ambientColor = null;
diffuseColor = Color.WHITE;
specularColor = Color.WHITE;
opacity = 1.f;
shininess = 0.f;
alphaTexFilename = null;
ambientTexFilename = null;
diffuseTexFilename = null;
shininessTexFilename = null;
specularTexFilename = null;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment