/* * Minecraft Forge * Copyright (c) 2016-2019. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation version 2.1 * of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ package net.minecraftforge.client.model.obj; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import joptsimple.internal.Strings; import net.minecraft.client.renderer.TransformationMatrix; import net.minecraft.client.renderer.Vector3f; import net.minecraft.client.renderer.Vector4f; import net.minecraft.client.renderer.model.*; import net.minecraft.client.renderer.texture.TextureAtlasSprite; import net.minecraft.client.renderer.vertex.VertexFormatElement; import net.minecraft.util.Direction; import net.minecraft.util.ResourceLocation; import net.minecraft.util.math.MathHelper; import net.minecraft.util.math.Vec2f; import net.minecraftforge.client.model.*; import net.minecraftforge.client.model.geometry.IModelGeometryPart; import net.minecraftforge.client.model.geometry.IMultipartModelGeometry; import net.minecraftforge.client.model.pipeline.BakedQuadBuilder; import net.minecraftforge.client.model.pipeline.IVertexConsumer; import net.minecraftforge.common.model.TransformationHelper; import org.apache.commons.lang3.tuple.Pair; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.io.IOException; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; public class OBJModel implements IMultipartModelGeometry { private static Vector4f COLOR_WHITE = new Vector4f(1, 1, 1, 1); private static Vec2f[] DEFAULT_COORDS = { new Vec2f(0, 0), new Vec2f(0, 1), new Vec2f(1, 1), new Vec2f(1, 0), }; private final Map parts = Maps.newHashMap(); private final List positions = Lists.newArrayList(); private final List texCoords = Lists.newArrayList(); private final List normals = Lists.newArrayList(); private final List colors = Lists.newArrayList(); public final boolean detectCullableFaces; public final boolean diffuseLighting; public final boolean flipV; public final boolean ambientToFullbright; public final ResourceLocation modelLocation; @Nullable public final String materialLibraryOverrideLocation; OBJModel(LineReader reader, ModelSettings settings) throws IOException { this.modelLocation = settings.modelLocation; this.detectCullableFaces = settings.detectCullableFaces; this.diffuseLighting = settings.diffuseLighting; this.flipV = settings.flipV; this.ambientToFullbright = settings.ambientToFullbright; this.materialLibraryOverrideLocation = settings.materialLibraryOverrideLocation; // for relative references to material libraries String modelDomain = modelLocation.getNamespace(); String modelPath = modelLocation.getPath(); int lastSlash = modelPath.lastIndexOf('/'); if (lastSlash >= 0) modelPath = modelPath.substring(0,lastSlash+1); // include the '/' else modelPath = ""; MaterialLibrary mtllib = MaterialLibrary.EMPTY; MaterialLibrary.Material currentMat = null; String currentSmoothingGroup = null; ModelGroup currentGroup = null; ModelObject currentObject = null; ModelMesh currentMesh = null; boolean objAboveGroup = false; if (materialLibraryOverrideLocation != null) { String lib = materialLibraryOverrideLocation; if (lib.contains(":")) mtllib = OBJLoader.INSTANCE.loadMaterialLibrary(new ResourceLocation(lib)); else mtllib = OBJLoader.INSTANCE.loadMaterialLibrary(new ResourceLocation(modelDomain, modelPath + lib)); } String[] line; while((line = reader.readAndSplitLine(true)) != null) { switch(line[0]) { case "mtllib": // Loads material library { if (materialLibraryOverrideLocation != null) break; String lib = line[1]; if (lib.contains(":")) mtllib = OBJLoader.INSTANCE.loadMaterialLibrary(new ResourceLocation(lib)); else mtllib = OBJLoader.INSTANCE.loadMaterialLibrary(new ResourceLocation(modelDomain, modelPath + lib)); break; } case "usemtl": // Sets the current material (starts new mesh) { String mat = Strings.join(Arrays.copyOfRange(line, 1, line.length), " "); MaterialLibrary.Material newMat = mtllib.getMaterial(mat); if (!Objects.equals(newMat, currentMat)) { currentMat = newMat; if (currentMesh != null && currentMesh.mat == null && currentMesh.faces.size() == 0) { currentMesh.mat = currentMat; } else { // Start new mesh currentMesh = null; } } break; } case "v": // Vertex positions.add(parseVector4To3(line)); break; case "vt": // Vertex texcoord texCoords.add(parseVector2(line)); break; case "vn": // Vertex normal normals.add(parseVector3(line)); break; case "vc": // Vertex color (non-standard) colors.add(parseVector4(line)); break; case "f": // Face { if (currentMesh == null) { currentMesh = new ModelMesh(currentMat, currentSmoothingGroup); if (currentObject != null) { currentObject.meshes.add(currentMesh); } else { if (currentGroup == null) { currentGroup = new ModelGroup(""); parts.put("", currentGroup); } currentGroup.meshes.add(currentMesh); } } int[][] vertices = new int[line.length-1][]; for(int i=0;i Strings.isNullOrEmpty(num) ? 0 : Integer.parseInt(num)).toArray(); if (vertex[0] < 0) vertex[0] = positions.size() + vertex[0]; else vertex[0]--; if (vertex.length > 1) { if (vertex[1] < 0) vertex[1] = texCoords.size() + vertex[1]; else vertex[1]--; if (vertex.length > 2) { if (vertex[2] < 0) vertex[2] = normals.size() + vertex[2]; else vertex[2]--; if (vertex.length > 3) { if (vertex[3] < 0) vertex[3] = colors.size() + vertex[3]; else vertex[3]--; } } } vertices[i] = vertex; } currentMesh.faces.add(vertices); break; } case "s": // Smoothing group (starts new mesh) { String smoothingGroup = "off".equals(line[1]) ? null : line[1]; if (!Objects.equals(currentSmoothingGroup, smoothingGroup)) { currentSmoothingGroup = smoothingGroup; if (currentMesh != null && currentMesh.smoothingGroup == null && currentMesh.faces.size() == 0) { currentMesh.smoothingGroup = currentSmoothingGroup; } else { // Start new mesh currentMesh = null; } } break; } case "g": { String name = line[1]; if (objAboveGroup) { currentObject = new ModelObject(currentGroup.name() + "/" + name); currentGroup.parts.put(name, currentObject); } else { currentGroup = new ModelGroup(name); parts.put(name, currentGroup); currentObject = null; } // Start new mesh currentMesh = null; break; } case "o": { String name = line[1]; if (objAboveGroup || currentGroup == null) { objAboveGroup = true; currentGroup = new ModelGroup(name); parts.put(name, currentGroup); currentObject = null; } else { currentObject = new ModelObject(currentGroup.name() + "/" + name); currentGroup.parts.put(name, currentObject); } // Start new mesh currentMesh = null; break; } } } } public static Vector3f parseVector4To3(String[] line) { switch (line.length) { case 1: return new Vector3f(0,0,0); case 2: return new Vector3f(Float.parseFloat(line[1]), 0, 0); case 3: return new Vector3f(Float.parseFloat(line[1]), Float.parseFloat(line[2]), 0); case 4: return new Vector3f(Float.parseFloat(line[1]), Float.parseFloat(line[2]), Float.parseFloat(line[3])); default: { Vector4f vec4 = parseVector4(line); return new Vector3f( vec4.getX() / vec4.getW(), vec4.getY() / vec4.getW(), vec4.getZ() / vec4.getW() ); } } } public static Vec2f parseVector2(String[] line) { switch (line.length) { case 1: return new Vec2f(0,0); case 2: return new Vec2f(Float.parseFloat(line[1]), 0); default: return new Vec2f(Float.parseFloat(line[1]), Float.parseFloat(line[2])); } } public static Vector3f parseVector3(String[] line) { switch (line.length) { case 1: return new Vector3f(0,0,0); case 2: return new Vector3f(Float.parseFloat(line[1]), 0, 0); case 3: return new Vector3f(Float.parseFloat(line[1]), Float.parseFloat(line[2]), 0); default: return new Vector3f(Float.parseFloat(line[1]), Float.parseFloat(line[2]), Float.parseFloat(line[3])); } } public static Vector4f parseVector4(String[] line) { switch (line.length) { case 1: return new Vector4f(0,0,0,1); case 2: return new Vector4f(Float.parseFloat(line[1]), 0, 0,1); case 3: return new Vector4f(Float.parseFloat(line[1]), Float.parseFloat(line[2]), 0,1); case 4: return new Vector4f(Float.parseFloat(line[1]), Float.parseFloat(line[2]), Float.parseFloat(line[3]),1); default: return new Vector4f(Float.parseFloat(line[1]), Float.parseFloat(line[2]), Float.parseFloat(line[3]), Float.parseFloat(line[4])); } } @Override public Collection getParts() { return parts.values(); } @Override public Optional getPart(String name) { return Optional.ofNullable(parts.get(name)); } private Pair makeQuad(int[][] indices, int tintIndex, Vector4f colorTint, Vector4f ambientColor, TextureAtlasSprite texture, TransformationMatrix transform) { boolean needsNormalRecalculation = false; for (int[] ints : indices) { needsNormalRecalculation |= ints.length < 3; } Vector3f faceNormal = new Vector3f(0,0,0); if (needsNormalRecalculation) { Vector3f a = positions.get(indices[0][0]); Vector3f ab = positions.get(indices[1][0]); Vector3f ac = positions.get(indices[2][0]); Vector3f abs = ab.copy(); abs.sub(a); Vector3f acs = ac.copy(); acs.sub(a); abs.cross(acs); abs.normalize(); faceNormal = abs; } Vector4f[] pos = new Vector4f[4]; Vector3f[] norm = new Vector3f[4]; BakedQuadBuilder builder = new BakedQuadBuilder(texture); builder.setQuadTint(tintIndex); Vec2f uv2 = new Vec2f(0, 0); if (ambientToFullbright) { int fakeLight = (int) ((ambientColor.getX() + ambientColor.getY() + ambientColor.getZ()) * 15 / 3.0f); uv2 = new Vec2f((fakeLight << 4) / 32767.0f, (fakeLight << 4) / 32767.0f); builder.setApplyDiffuseLighting(fakeLight == 0); } boolean hasTransform = !transform.isIdentity(); // The incoming transform is referenced on the center of the block, but our coords are referenced on the corner TransformationMatrix transformation = hasTransform ? transform.blockCenterToCorner() : transform; for(int i=0;i<4;i++) { int[] index = indices[Math.min(i,indices.length-1)]; Vector3f pos0 = positions.get(index[0]); Vector4f position = new Vector4f(pos0); Vec2f texCoord = index.length >= 2 && texCoords.size() > 0 ? texCoords.get(index[1]) : DEFAULT_COORDS[i]; Vector3f norm0 = !needsNormalRecalculation && index.length >= 3 && normals.size() > 0 ? normals.get(index[2]) : faceNormal; Vector3f normal = norm0; Vector4f color = index.length >= 4 && colors.size() > 0 ? colors.get(index[3]) : COLOR_WHITE; if (hasTransform) { normal = norm0.copy(); transformation.transformPosition(position); transformation.transformNormal(normal); }; Vector4f tintedColor = new Vector4f( color.getX() * colorTint.getX(), color.getY() * colorTint.getY(), color.getZ() * colorTint.getZ(), color.getW() * colorTint.getW()); putVertexData(builder, position, texCoord, normal, tintedColor, uv2, texture); pos[i] = position; norm[i] = normal; } builder.setQuadOrientation(Direction.getFacingFromVector(norm[0].getX(), norm[0].getY(),norm[0].getZ())); Direction cull = null; if (detectCullableFaces) { if (MathHelper.epsilonEquals(pos[0].getX(), 0) && // vertex.position.x MathHelper.epsilonEquals(pos[1].getX(), 0) && MathHelper.epsilonEquals(pos[2].getX(), 0) && MathHelper.epsilonEquals(pos[3].getX(), 0) && norm[0].getX() < 0) // vertex.normal.x { cull = Direction.WEST; } else if (MathHelper.epsilonEquals(pos[0].getX(), 1) && // vertex.position.x MathHelper.epsilonEquals(pos[1].getX(), 1) && MathHelper.epsilonEquals(pos[2].getX(), 1) && MathHelper.epsilonEquals(pos[3].getX(), 1) && norm[0].getX() > 0) // vertex.normal.x { cull = Direction.EAST; } else if (MathHelper.epsilonEquals(pos[0].getZ(), 0) && // vertex.position.z MathHelper.epsilonEquals(pos[1].getZ(), 0) && MathHelper.epsilonEquals(pos[2].getZ(), 0) && MathHelper.epsilonEquals(pos[3].getZ(), 0) && norm[0].getZ() < 0) // vertex.normal.z { cull = Direction.NORTH; // can never remember } else if (MathHelper.epsilonEquals(pos[0].getZ(), 1) && // vertex.position.z MathHelper.epsilonEquals(pos[1].getZ(), 1) && MathHelper.epsilonEquals(pos[2].getZ(), 1) && MathHelper.epsilonEquals(pos[3].getZ(), 1) && norm[0].getZ() > 0) // vertex.normal.z { cull = Direction.SOUTH; } else if (MathHelper.epsilonEquals(pos[0].getY(), 0) && // vertex.position.y MathHelper.epsilonEquals(pos[1].getY(), 0) && MathHelper.epsilonEquals(pos[2].getY(), 0) && MathHelper.epsilonEquals(pos[3].getY(), 0) && norm[0].getY() < 0) // vertex.normal.z { cull = Direction.DOWN; // can never remember } else if (MathHelper.epsilonEquals(pos[0].getY(), 1) && // vertex.position.y MathHelper.epsilonEquals(pos[1].getY(), 1) && MathHelper.epsilonEquals(pos[2].getY(), 1) && MathHelper.epsilonEquals(pos[3].getY(), 1) && norm[0].getY() > 0) // vertex.normal.y { cull = Direction.UP; } } return Pair.of(builder.build(), cull); } private void putVertexData(IVertexConsumer consumer, Vector4f position0, Vec2f texCoord0, Vector3f normal0, Vector4f color0, Vec2f uv2, TextureAtlasSprite texture) { ImmutableList elements = consumer.getVertexFormat().getElements(); for(int j=0;j meshes = Lists.newArrayList(); ModelObject(String name) { this.name = name; } @Override public String name() { return name; } @Override public void addQuads(IModelConfiguration owner, IModelBuilder modelBuilder, ModelBakery bakery, Function spriteGetter, IModelTransform modelTransform, ResourceLocation modelLocation) { for(ModelMesh mesh : meshes) { MaterialLibrary.Material mat = mesh.mat; if (mat == null) continue; TextureAtlasSprite texture = spriteGetter.apply(ModelLoaderRegistry.resolveTexture(mat.diffuseColorMap, owner)); int tintIndex = mat.diffuseTintIndex; Vector4f colorTint = mat.diffuseColor; for (int[][] face : mesh.faces) { Pair quad = makeQuad(face, tintIndex, colorTint, mat.ambientColor, texture, modelTransform.getRotation()); if (quad.getRight() == null) modelBuilder.addGeneralQuad(quad.getLeft()); else modelBuilder.addFaceQuad(quad.getRight(), quad.getLeft()); } } } @Override public Collection getTextures(IModelConfiguration owner, Function modelGetter, Set> missingTextureErrors) { return meshes.stream().map(mesh -> ModelLoaderRegistry.resolveTexture(mesh.mat.diffuseColorMap, owner)).collect(Collectors.toSet()); } } public class ModelGroup extends ModelObject { final Map parts = Maps.newHashMap(); ModelGroup(String name) { super(name); } public Collection getParts() { return parts.values(); } @Override public void addQuads(IModelConfiguration owner, IModelBuilder modelBuilder, ModelBakery bakery, Function spriteGetter, IModelTransform modelTransform, ResourceLocation modelLocation) { super.addQuads(owner, modelBuilder, bakery, spriteGetter, modelTransform, modelLocation); getParts().stream().filter(part -> owner.getPartVisibility(part)) .forEach(part -> part.addQuads(owner, modelBuilder, bakery, spriteGetter, modelTransform, modelLocation)); } @Override public Collection getTextures(IModelConfiguration owner, Function modelGetter, Set> missingTextureErrors) { Set combined = Sets.newHashSet(); combined.addAll(super.getTextures(owner, modelGetter, missingTextureErrors)); for (IModelGeometryPart part : getParts()) combined.addAll(part.getTextures(owner, modelGetter, missingTextureErrors)); return combined; } } private class ModelMesh { @Nullable public MaterialLibrary.Material mat; @Nullable public String smoothingGroup; public final List faces = Lists.newArrayList(); public ModelMesh(@Nullable MaterialLibrary.Material currentMat, @Nullable String currentSmoothingGroup) { this.mat = currentMat; this.smoothingGroup = currentSmoothingGroup; } } public static class ModelSettings { @Nonnull public final ResourceLocation modelLocation; public final boolean detectCullableFaces; public final boolean diffuseLighting; public final boolean flipV; public final boolean ambientToFullbright; @Nullable public final String materialLibraryOverrideLocation; public ModelSettings(@Nonnull ResourceLocation modelLocation, boolean detectCullableFaces, boolean diffuseLighting, boolean flipV, boolean ambientToFullbright, @Nullable String materialLibraryOverrideLocation) { this.modelLocation = modelLocation; this.detectCullableFaces = detectCullableFaces; this.diffuseLighting = diffuseLighting; this.flipV = flipV; this.ambientToFullbright = ambientToFullbright; this.materialLibraryOverrideLocation = materialLibraryOverrideLocation; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ModelSettings that = (ModelSettings) o; return equals(that); } public boolean equals(@Nonnull ModelSettings that) { return detectCullableFaces == that.detectCullableFaces && diffuseLighting == that.diffuseLighting && flipV == that.flipV && ambientToFullbright == that.ambientToFullbright && modelLocation.equals(that.modelLocation) && Objects.equals(materialLibraryOverrideLocation, that.materialLibraryOverrideLocation); } @Override public int hashCode() { return Objects.hash(modelLocation, detectCullableFaces, diffuseLighting, flipV, ambientToFullbright, materialLibraryOverrideLocation); } } }