Skip to content

Instantly share code, notes, and snippets.

@StillManic
Created February 20, 2015 19:06
Show Gist options
  • Save StillManic/b9d812148620ba7fe7f9 to your computer and use it in GitHub Desktop.
Save StillManic/b9d812148620ba7fe7f9 to your computer and use it in GitHub Desktop.
OBJModel
package net.minecraftforge.client.model.obj;
import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import javax.vecmath.Matrix4f;
import javax.vecmath.Vector2f;
import javax.vecmath.Vector3f;
import javax.vecmath.Vector4f;
import net.minecraft.block.state.IBlockState;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.block.model.BakedQuad;
import net.minecraft.client.renderer.block.model.ItemCameraTransforms;
import net.minecraft.client.renderer.block.model.ItemCameraTransforms.TransformType;
import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.renderer.texture.TextureMap;
import net.minecraft.client.renderer.texture.TextureUtil;
import net.minecraft.client.renderer.vertex.VertexFormat;
import net.minecraft.client.renderer.vertex.VertexFormatElement;
import net.minecraft.client.renderer.vertex.VertexFormatElement.EnumType;
import net.minecraft.client.renderer.vertex.VertexFormatElement.EnumUsage;
import net.minecraft.client.resources.IResource;
import net.minecraft.client.resources.IResourceManager;
import net.minecraft.client.resources.data.AnimationMetadataSection;
import net.minecraft.client.resources.model.IBakedModel;
import net.minecraft.item.ItemStack;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.client.GuiIngameForge;
import net.minecraftforge.client.event.RenderGameOverlayEvent;
import net.minecraftforge.client.event.TextureStitchEvent;
import net.minecraftforge.client.model.Attributes;
import net.minecraftforge.client.model.IFlexibleBakedModel;
import net.minecraftforge.client.model.IModel;
import net.minecraftforge.client.model.IModelPart;
import net.minecraftforge.client.model.IModelState;
import net.minecraftforge.client.model.IPerspectiveAwareModel;
import net.minecraftforge.client.model.ISmartBlockModel;
import net.minecraftforge.client.model.ISmartItemModel;
import net.minecraftforge.client.model.ModelLoader.White;
import net.minecraftforge.client.model.ModelLoaderRegistry;
import net.minecraftforge.client.model.TRSRTransformation;
import net.minecraftforge.fml.common.eventhandler.EventPriority;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.registry.GameRegistry;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.lwjgl.BufferUtils;
import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
public class OBJModel implements IModel {
public static boolean shouldLog = true;
public static final Logger logger = LogManager.getLogger(OBJModel.class);
OBJFlexibleModel flexModel;
MaterialLibrary matLib = new MaterialLibrary();
Material currentMaterial = new Material();
List<Face> faces = new ArrayList<Face>();
List<Vertex> vertices = new ArrayList<Vertex>();
List<TextureCoordinate> texCoords = new ArrayList<TextureCoordinate>();
List<Vector3f> normals = new ArrayList<Vector3f>();
List<Vector3f> colors = new ArrayList<Vector3f>();
List<ResourceLocation> textures = new ArrayList<ResourceLocation>();
private boolean isBlockModel = false;
public OBJModel() {}
@Override
public Collection<ResourceLocation> getDependencies()
{
return Collections.emptyList();
}
@Override
public Collection<ResourceLocation> getTextures()
{
return Collections.emptyList();
}
@Override
public IFlexibleBakedModel bake(IModelState state, VertexFormat format, Function<ResourceLocation, TextureAtlasSprite> bakedTextureGetter)
{
//TODO
flexModel = new OBJFlexibleModel(bakedTextureGetter, this);
return flexModel;
}
@Override
public IModelState getDefaultState()
{
return new OBJState();
}
public void isBlockModel(boolean isBlockModel)
{
this.isBlockModel = isBlockModel;
}
public static class OBJState implements IModelState
{
public OBJState() {}
public TRSRTransformation apply(IModelPart part)
{
return new TRSRTransformation(new Matrix4f());
}
}
public static class OBJFlexibleModel implements IFlexibleBakedModel, ISmartBlockModel, ISmartItemModel, IPerspectiveAwareModel
{
public Function<ResourceLocation, TextureAtlasSprite> bakedTextureGetter;
private OBJModel model = null;
private ByteBuffer buffer;
private VertexFormat format = Attributes.DEFAULT_BAKED_FORMAT;
public static final int BYTES_IN_INT = Integer.SIZE / Byte.SIZE;
public static final int VERTICES_IN_QUAD = 4;
public OBJFlexibleModel(Function<ResourceLocation, TextureAtlasSprite> bakedTextureGetter, OBJModel model)
{
this.bakedTextureGetter = bakedTextureGetter;
this.model = model;
this.buffer = BufferUtils.createByteBuffer(VERTICES_IN_QUAD * format.getNextOffset());
}
@Override
public boolean isAmbientOcclusion()
{
return false;
}
@Override
public boolean isGui3d()
{
return true;
// return model.isBlockModel;
}
@Override
public boolean isBuiltInRenderer()
{
return false;
}
@Override
public TextureAtlasSprite getTexture()
{
TextureAtlasSprite texture;
if (model.currentMaterial.getTexture().toString().equalsIgnoreCase(White.instance.getIconName())) texture = White.instance;
else texture = bakedTextureGetter.apply(model.currentMaterial.getTexture());
return texture;
}
@Override
public ItemCameraTransforms getItemCameraTransforms()
{
return ItemCameraTransforms.DEFAULT;
}
@Override
public IBakedModel handleItemState(ItemStack stack)
{
//TODO: how to handle different states?
return this;
}
@Override
public IBakedModel handleBlockState(IBlockState state)
{
//TODO: how to handle different states?
return this;
}
@Override
public List<BakedQuad> getFaceQuads(EnumFacing side)
{
return Collections.emptyList();
}
@Override
public List<BakedQuad> getGeneralQuads()
{
ImmutableList<BakedQuad> quads;
ImmutableList.Builder<BakedQuad> builder = ImmutableList.builder();
for (Face f : model.faces)
{
buffer.clear();
//TODO: figure out how to swap sprites for multiple textures per block
Material mat = model.matLib.getMaterial(f);
TextureAtlasSprite sprite;
if (mat.getTexture().toString().equalsIgnoreCase(White.instance.getIconName())) sprite = White.instance;
else sprite = bakedTextureGetter.apply(mat.getTexture());
Vertex[] vertices = f.getVertices();
putVertexData(vertices[0], sprite, 0, f);
putVertexData(vertices[1], sprite, 1, f);
putVertexData(vertices[2], sprite, 2, f);
putVertexData(vertices[3], sprite, 3, f);
buffer.flip();
int[] data = new int[VERTICES_IN_QUAD * format.getNextOffset() / BYTES_IN_INT];
buffer.asIntBuffer().get(data);
Vector3f normal = f.calcNormal();
builder.add(new BakedQuad(data, -1, EnumFacing.getFacingFromVector(normal.x, normal.y, normal.z)));
}
quads = builder.build();
return quads;
}
private void put(VertexFormatElement element, Number... ns)
{
Attributes.put(buffer, element, true, 0, ns);
}
@SuppressWarnings("unchecked")
private final void putVertexData(Vertex v, TextureAtlasSprite sprite, int index, Face f)
{
int oldPos = buffer.position();
Number[] nums = new Number[16];
for (int i = 0; i < nums.length; i++) nums[i] = 0f;
for (VertexFormatElement element : (List<VertexFormatElement>) format.getElements())
{
switch (element.getUsage())
{
case POSITION:
put(element, v.getPosition().x, v.getPosition().y, v.getPosition().z, 1);
break;
case COLOR:
//TODO: may change when implementing color-only
Material m = this.model.matLib.getMaterial(f);
if (m == null) put(element, 1, 1, 1, 0);
else
{
float r = m.color.x == 1f ? m.color.x : (int) (m.color.x * 255);
float g = m.color.y == 1f ? m.color.y : (int) (m.color.y * 255);
float b = m.color.z == 1f ? m.color.z : (int) (m.color.z * 255);
put(element, r, g, b, 0);
}
break;
case UV:
if (element.getIndex() < f.texCoords.length)
{
put(element,
sprite.getInterpolatedU(f.texCoords[index].coord.x * 16.0f),
sprite.getInterpolatedV(f.texCoords[index].coord.y * 16.0f),
0,
1
);
}
break;
default:
break;
}
}
buffer.position(oldPos + format.getNextOffset());
}
@Override
public VertexFormat getFormat()
{
return format;
}
@Override
public Pair<IBakedModel, Matrix4f> handlePerspective(TransformType cameraTransformType)
{
return Pair.of((IBakedModel) this, null);
}
}
public static class Parser
{
private IResourceManager manager;
private IResource from;
private InputStreamReader objStream;
private InputStreamReader mtlStream;
private BufferedReader objReader;
private BufferedReader mtlReader;
private List<String> objData = new ArrayList<String>();
private List<String> mtlData = new ArrayList<String>();
OBJModel model = new OBJModel();
public Parser(IResource from, IResourceManager manager) throws IOException
{
this.manager = manager;
this.from = from;
this.objStream = new InputStreamReader(from.getInputStream(), Charsets.UTF_8);
this.objReader = new BufferedReader(objStream);
}
public void readOBJToList() throws IOException
{
for (;;)
{
String currentLine = objReader.readLine();
if (currentLine == null) break;
if (currentLine.isEmpty() || currentLine.startsWith("#")) continue;
this.objData.add(currentLine);
}
}
public void readMTLToList(IResource from) throws IOException
{
mtlStream = new InputStreamReader(from.getInputStream(), Charsets.UTF_8);
mtlReader = new BufferedReader(mtlStream);
for (;;)
{
String currentLine = mtlReader.readLine();
if (currentLine == null) break;
if (currentLine.isEmpty() || currentLine.startsWith("#")) continue;
this.mtlData.add(currentLine);
}
}
public List<Face> getFaces()
{
List<Face> faces = new ArrayList<Face>();
for (Material m : model.matLib.materials.values()) faces.addAll(m.getFaces());
return faces;
}
public List<TextureCoordinate> getTextureCoordinates()
{
List<TextureCoordinate> coords = new ArrayList<TextureCoordinate>();
List<Face> faces = getFaces();
for (Face f : faces) coords.addAll(Arrays.asList(f.getTextureCoordinates()));
return coords;
}
public OBJModel parse() throws IOException
{
readOBJToList();
StringBuilder builder = new StringBuilder(from.getResourceLocation().getResourceDomain() + ":" + from.getResourceLocation().getResourcePath());
int i = builder.lastIndexOf("/");
for (String s : objData)
{
if (s.startsWith("mtllib") && mtlData.isEmpty())
{
String mtllibName = s.split(" ", 2)[1];
builder.replace(i + 1, builder.length(), mtllibName);
readMTLToList(manager.getResource(new ResourceLocation(builder.toString())));
break;
}
}
model.matLib.buildLibrary(this.mtlData);
for (String s : objData)
{
if (s.isEmpty() || s.startsWith("#")) continue;
String[] fields = s.split(" ", 2);
String key = fields[0];
String data = fields[1];
if (key.equalsIgnoreCase("usemtl")) model.currentMaterial = model.matLib.getMaterial(data);
else if (key.equalsIgnoreCase("v"))
{
String[] posStr = data.split(" ", 6);
Vector3f pos = new Vector3f(Float.parseFloat(posStr[0]), Float.parseFloat(posStr[1]), Float.parseFloat(posStr[2]));
Vector3f color = new Vector3f(0, 0, 0);
if (posStr.length > 4) color = new Vector3f(Float.parseFloat(posStr[3]), Float.parseFloat(posStr[4]), Float.parseFloat(posStr[5]));
model.vertices.add(new Vertex(pos, color));
}
else if (key.equalsIgnoreCase("vt"))
{
String[] posStr = data.split(" ", 3);
Vector2f pos = new Vector2f(Float.parseFloat(posStr[0]), Float.parseFloat(posStr[1]));
model.texCoords.add(new TextureCoordinate(pos));
}
else if (key.equalsIgnoreCase("vn"))
{
String[] posStr = data.split(" ", 3);
Vector3f pos = new Vector3f(Float.parseFloat(posStr[0]), Float.parseFloat(posStr[1]), Float.parseFloat(posStr[2]));
model.normals.add(pos);
}
else if (key.equalsIgnoreCase("f"))
{
String[] points = data.split(" ", 4);
List<Vertex> vs = new ArrayList<Vertex>();
List<TextureCoordinate> tcs = new ArrayList<TextureCoordinate>();
List<Vector3f> ns = new ArrayList<Vector3f>();
for (String p : points)
{
if (p.contains("//"))
{
String[] vnStr = p.split("//", 2);
int vert = Integer.parseInt(vnStr[0]);
int norm = Integer.parseInt(vnStr[1]);
//TODO: handle negative vertex indices!
if (vert < 0 || norm < 0)
{
logger.printf(Level.WARN, "OBJModel: Negative vertex index refrences for faces aren't supported yet! The model will not render properlly!", new Object[0]);
break;
}
else
{
if (vert > 0) vs.add(model.vertices.get(vert - 1));
if (norm > 0) ns.add(model.normals.get(norm - 1));
tcs.add(new TextureCoordinate(0, 1));
tcs.add(new TextureCoordinate(1, 1));
tcs.add(new TextureCoordinate(1, 0));
tcs.add(new TextureCoordinate(0, 0));
}
}
else if (p.contains("/"))
{
String[] vtStr = p.split("/", 3);
int vert = Integer.parseInt(vtStr[0]);
int texc = Integer.parseInt(vtStr[1]);
int norm = vtStr.length > 2 ? Integer.parseInt(vtStr[2]) : 0;
//TODO: handle negative vertex indices!
if (vert < 0 || texc < 0 || norm < 0)
{
logger.printf(Level.WARN, "OBJModel: Negative vertex index refrences for faces aren't supported yet! The model will not render properlly!", new Object[0]);
break;
}
else
{
if (vert > 0) vs.add(model.vertices.get(vert - 1));
if (texc > 0) tcs.add(model.texCoords.get(texc - 1));
if (norm > 0) ns.add(model.normals.get(norm - 1));
}
}
else
{
int vert = Integer.parseInt(p);
//TODO: handle negative vertex indices!
if (vert < 0)
{
logger.printf(Level.WARN, "OBJModel: Negative vertex index refrences for faces aren't supported yet! The model will not render properlly!", new Object[0]);
break;
}
else
{
if (vert > 0) vs.add(model.vertices.get(vert - 1));
tcs.add(new TextureCoordinate(0, 1));
tcs.add(new TextureCoordinate(1, 1));
tcs.add(new TextureCoordinate(1, 0));
tcs.add(new TextureCoordinate(0, 0));
ns.add(new Vector3f(0, 0, 0));
ns.add(new Vector3f(0, 0, 0));
ns.add(new Vector3f(0, 0, 0));
ns.add(new Vector3f(0, 0, 0));
}
}
}
if (vs.size() == 3) vs.add(vs.get(2));
if (tcs.size() == 3) tcs.add(tcs.get(2));
if (ns.size() == 3) ns.add(ns.get(2));
Face face = new Face(vs.toArray(new Vertex[4]), tcs.toArray(new TextureCoordinate[4]), ns.toArray(new Vector3f[4]));
model.currentMaterial.addFace(face);
}
}
model.faces = getFaces();
return model;
}
}
public static class MaterialLibrary
{
Map<String, Material> materials = new HashMap<String, Material>();
public MaterialLibrary()
{
materials.put("default", new Material());
}
public void buildLibrary(List<String> mtlData)
{
List<String> mat = new ArrayList<String>();
String[] matArray = null;
for (String s : mtlData)
{
String[] fields = s.split(" ", 2);
String key = fields[0];
String data = fields[1];
if (key.equalsIgnoreCase("newmtl"))
{
if (!mat.isEmpty())
{
String name = mat.get(0).split(" ", 2)[1];
matArray = new String[mat.size()];
mat.toArray(matArray);
materials.put(name, new Material(matArray));
mat.clear();
mat.add(s);
matArray = null;
}
else mat.add(s);
}
else mat.add(s);
}
if (!mat.isEmpty())
{
String name = mat.get(0).split(" ", 2)[1];
matArray = new String[mat.size()];
mat.toArray(matArray);
materials.put(name, new Material(matArray));
mat.clear();
matArray = null;
}
}
public Material getMaterial(String name)
{
if (name == null || !this.materials.containsKey(name)) return this.materials.get("default");
else return this.materials.get(name);
}
public Material getMaterial(Face f)
{
if (f != null)
{
for (Material m : this.materials.values()) if (m.faces.contains(f)) return m;
return null;
}
return null;
}
public void updateMaterial(String name, Material material)
{
if (name != null && !name.equals("default") && this.materials.containsKey(name) && material != null && material != this.materials.get("defualt"))
{
this.materials.replace(name, material);
}
}
public List<Face> getFacesContainingVertex(Vertex v)
{
List<Face> fcv = new ArrayList<Face>();
for (Material m : materials.values()) for (Face f : m.faces) if (Arrays.asList(f.verts).contains(v)) fcv.add(f);
return fcv;
}
}
public static class Material
{
private Vector3f kdColor;
private Vector3f kaColor;
private Vector3f ksColor;
private Vector3f color;
private ResourceLocation kdLoc;
private ResourceLocation kaLoc;
private ResourceLocation ksLoc;
private ResourceLocation texture;
private String name = "default";
private List<Face> faces = new ArrayList<Face>();
public Material()
{
this.name = "default";
this.texture = new ResourceLocation("missingno");
this.color = new Vector3f(0, 0, 0);
}
public Material(String[] matData)
{
for (String s : matData)
{
String[] fields = s.split(" ", 2);
String key = fields[0];
String data = fields[1];
if (key.equalsIgnoreCase("newmtl")) this.name = data;
else if (key.equalsIgnoreCase("Kd") || key.equalsIgnoreCase("Ka") || key.equalsIgnoreCase("Ks"))
{
String[] rgbStrings = data.split(" ", 3);
float[] rgb = new float[] {Float.parseFloat(rgbStrings[0]), Float.parseFloat(rgbStrings[1]), Float.parseFloat(rgbStrings[2])};
kdColor = key.equalsIgnoreCase("Kd") ? new Vector3f(rgb[0], rgb[1], rgb[2]) : kdColor;
kaColor = key.equalsIgnoreCase("Ka") ? new Vector3f(rgb[0], rgb[1], rgb[2]) : kaColor;
ksColor = key.equalsIgnoreCase("Ks") ? new Vector3f(rgb[0], rgb[1], rgb[2]) : ksColor;
}
else if (key.equalsIgnoreCase("map_Kd") || key.equalsIgnoreCase("map_Ka") || key.equalsIgnoreCase("map_Ks"))
{
String path;
if (data.contains(" "))
{
String[] dataSplit = data.split(" ");
path = dataSplit[dataSplit.length - 1];
}
else path = data;
String[] splitPath = path.split(":", 2);
ResourceLocation parsedLoc;
if (splitPath.length > 1 && splitPath[1] != null && !splitPath[1].isEmpty()) parsedLoc = new ResourceLocation(splitPath[0] + ":" + splitPath[1]);
else parsedLoc = new ResourceLocation(path);
kdLoc = key.equalsIgnoreCase("map_Kd") ? parsedLoc : kdLoc;
kaLoc = key.equalsIgnoreCase("map_Ka") ? parsedLoc : kaLoc;
ksLoc = key.equalsIgnoreCase("map_Ks") ? parsedLoc : ksLoc;
}
}
this.texture = kdLoc != null ? kdLoc : kaLoc != null ? kaLoc : ksLoc != null ? ksLoc : new ResourceLocation(White.instance.getIconName());
this.color = kdColor != null ? kdColor : kaColor != null ? kaColor : ksColor != null ? ksColor : this.color;
System.out.printf("Texture name is: %s%n", this.texture.toString());
System.out.printf("Color is: r: %s, g: %s, b: %s%n", this.color.x, this.color.y, this.color.z);
}
public ResourceLocation getTexture()
{
return this.texture;
}
public Vector3f getColor()
{
return this.color;
}
public String getName()
{
return this.name;
}
public void addFace(Face f)
{
if (!this.faces.contains(f)) this.faces.add(f);
}
public List<Face> getFaces()
{
return this.faces;
}
}
public static class Face
{
private Vertex[] verts = new Vertex[OBJFlexibleModel.VERTICES_IN_QUAD];
private TextureCoordinate[] texCoords = new TextureCoordinate[OBJFlexibleModel.VERTICES_IN_QUAD];
private Vector3f[] normals = new Vector3f[OBJFlexibleModel.VERTICES_IN_QUAD];
public Face(Vertex[] verts, TextureCoordinate[] texCoords, Vector3f[] normals)
{
this.verts = verts;
this.texCoords = texCoords;
this.normals = normals;
}
public void setVertices(Vertex[] verts)
{
this.verts = verts;
}
public Vertex[] getVertices()
{
return this.verts;
}
public void setTextureCoordinates(TextureCoordinate[] texCoords)
{
this.texCoords = texCoords;
}
public TextureCoordinate[] getTextureCoordinates()
{
return this.texCoords;
}
public void setNormals(Vector3f[] normals)
{
this.normals = normals;
}
public Vector3f[] getNormals()
{
return this.normals;
}
public boolean containsVertex(Vertex vertex)
{
return Arrays.asList(verts).contains(vertex);
}
public Vector3f calcNormal()
{
Vector3f normal = new Vector3f();
Vector3f v2sv1 = new Vector3f();
Vector3f v4sv1 = new Vector3f();
Vector3f v2sv3 = new Vector3f();
Vector3f v4sv3 = new Vector3f();
Vector3f tri124 = new Vector3f();
Vector3f tri324 = new Vector3f();
v2sv1.sub(verts[1].getPosition(), verts[0].getPosition());
v4sv1.sub(verts[3].getPosition(), verts[0].getPosition());
tri124.cross(v2sv1, v4sv1);
tri124.normalize();
v2sv3.sub(verts[1].getPosition(), verts[2].getPosition());
v4sv3.sub(verts[3].getPosition(), verts[2].getPosition());
tri324.cross(v2sv3, v4sv3);
tri324.normalize();
normal.set((tri124.getX() + tri324.getX()) / 2.0f, (tri124.getY() + tri324.getY()) / 2.0f, (tri124.getZ() + tri324.getZ()) / 2.0f);
normal.normalize();
return normal;
}
}
public static class Vertex
{
private Vector3f position;
private Vector3f color;
public Vertex(float x, float y, float z)
{
this(new Vector3f(x, y, z));
}
public Vertex(float x, float y, float z, float r, float g, float b)
{
this(new Vector3f(x, y, z), new Vector3f(r, g, b));
}
public Vertex(Vector3f position)
{
this(position, new Vector3f(-1, -1, -1));
}
public Vertex(Vector3f position, Vector3f color)
{
this.position = position;
this.color = color;
}
public void setPosition(Vector3f position)
{
this.position = position;
}
public Vector3f getPosition()
{
return this.position;
}
public void setColor(Vector3f color)
{
this.color = color;
}
public Vector3f getColor()
{
return this.color;
}
}
public static class TextureCoordinate
{
private Vector2f coord;
public TextureCoordinate(float u, float v)
{
this(new Vector2f(u, v));
}
public TextureCoordinate(Vector2f coord)
{
this.coord = coord;
}
public void setCoord(Vector2f coord)
{
this.coord = coord;
}
public Vector2f getCoord()
{
return this.coord;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment