/* * 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.b3d; import java.io.FileNotFoundException; import java.io.IOException; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Random; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import javax.annotation.Nullable; import com.mojang.blaze3d.matrix.MatrixStack; import net.minecraft.client.renderer.Matrix4f; import net.minecraft.client.renderer.TransformationMatrix; import net.minecraft.client.renderer.Vector3f; import net.minecraft.client.renderer.model.*; import net.minecraft.client.renderer.texture.AtlasTexture; import net.minecraft.client.renderer.texture.MissingTextureSprite; import net.minecraft.client.renderer.vertex.DefaultVertexFormats; import net.minecraft.client.renderer.vertex.VertexFormatElement; import net.minecraftforge.client.model.*; import net.minecraftforge.common.model.*; import net.minecraftforge.resource.IResourceType; import net.minecraftforge.resource.ISelectiveResourceReloadListener; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.tuple.Triple; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import com.google.common.base.Objects; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.gson.JsonElement; import com.google.gson.JsonParser; import net.minecraft.block.BlockState; import net.minecraft.client.renderer.model.ItemCameraTransforms.TransformType; import net.minecraft.client.renderer.texture.TextureAtlasSprite; import net.minecraft.resources.IResource; import net.minecraft.resources.IResourceManager; import net.minecraft.util.Direction; import net.minecraft.util.ResourceLocation; import net.minecraft.util.math.MathHelper; import net.minecraftforge.client.model.b3d.B3DModel.Animation; import net.minecraftforge.client.model.b3d.B3DModel.Face; import net.minecraftforge.client.model.b3d.B3DModel.Key; import net.minecraftforge.client.model.b3d.B3DModel.Mesh; import net.minecraftforge.client.model.b3d.B3DModel.Node; import net.minecraftforge.client.model.b3d.B3DModel.Texture; import net.minecraftforge.client.model.b3d.B3DModel.Vertex; import net.minecraftforge.client.model.data.IDynamicBakedModel; import net.minecraftforge.client.model.data.IModelData; import net.minecraftforge.client.model.pipeline.BakedQuadBuilder; import net.minecraftforge.client.model.pipeline.IVertexConsumer; import net.minecraftforge.common.model.animation.IClip; import net.minecraftforge.common.model.animation.IJoint; import net.minecraftforge.common.property.Properties; /* * Loader for Blitz3D models. * To enable for your mod call instance.addDomain(modId). * If you need more control over accepted resources - extend the class, and register a new instance with ModelLoaderRegistry. */ // TODO: Implement as a new model loader public enum B3DLoader implements ISelectiveResourceReloadListener { INSTANCE; private static final Logger LOGGER = LogManager.getLogger(); private IResourceManager manager; private final Set enabledDomains = new HashSet<>(); private final Map cache = new HashMap<>(); @Override public void onResourceManagerReload(IResourceManager manager, Predicate resourcePredicate) { this.manager = manager; cache.clear(); } @SuppressWarnings("unchecked") public IUnbakedModel loadModel(ResourceLocation modelLocation) throws Exception { ResourceLocation file = new ResourceLocation(modelLocation.getNamespace(), modelLocation.getPath()); if(!cache.containsKey(file)) { IResource resource = null; try { try { resource = manager.getResource(file); } catch(FileNotFoundException e) { if(modelLocation.getPath().startsWith("models/block/")) resource = manager.getResource(new ResourceLocation(file.getNamespace(), "models/item/" + file.getPath().substring("models/block/".length()))); else if(modelLocation.getPath().startsWith("models/item/")) resource = manager.getResource(new ResourceLocation(file.getNamespace(), "models/block/" + file.getPath().substring("models/item/".length()))); else throw e; } B3DModel.Parser parser = new B3DModel.Parser(resource.getInputStream()); B3DModel model = parser.parse(); cache.put(file, model); } catch(IOException e) { cache.put(file, null); throw e; } finally { IOUtils.closeQuietly(resource); } } B3DModel model = cache.get(file); if(model == null) throw new ModelLoadingException("Error loading model previously: " + file); if(!(model.getRoot().getKind() instanceof Mesh)) { return new ModelWrapper(modelLocation, model, ImmutableSet.of(), true, true, 1); } return new ModelWrapper(modelLocation, model, ImmutableSet.of(model.getRoot().getName()), true, true, 1); } public static final class B3DState implements IModelTransform { @Nullable private final Animation animation; private final int frame; private final int nextFrame; private final float progress; @Nullable private final IModelTransform parent; public B3DState(@Nullable Animation animation, int frame) { this(animation, frame, frame, 0); } public B3DState(@Nullable Animation animation, int frame, IModelTransform parent) { this(animation, frame, frame, 0, parent); } public B3DState(@Nullable Animation animation, int frame, int nextFrame, float progress) { this(animation, frame, nextFrame, progress, null); } public B3DState(@Nullable Animation animation, int frame, int nextFrame, float progress, @Nullable IModelTransform parent) { this.animation = animation; this.frame = frame; this.nextFrame = nextFrame; this.progress = MathHelper.clamp(progress, 0, 1); this.parent = getParent(parent); } @Nullable private IModelTransform getParent(@Nullable IModelTransform parent) { if (parent == null) return null; else if (parent instanceof B3DState) return ((B3DState)parent).parent; return parent; } @Nullable public Animation getAnimation() { return animation; } public int getFrame() { return frame; } public int getNextFrame() { return nextFrame; } public float getProgress() { return progress; } @Nullable public IModelTransform getParent() { return parent; } @Override public TransformationMatrix func_225615_b_() { if(parent != null) { return parent.func_225615_b_(); } return TransformationMatrix.func_227983_a_(); } @Override public TransformationMatrix getPartTransformation(Object part) { // TODO make more use of Optional if(!(part instanceof NodeJoint)) { return TransformationMatrix.func_227983_a_(); } Node node = ((NodeJoint)part).getNode(); TransformationMatrix nodeTransform; if(progress < 1e-5 || frame == nextFrame) { nodeTransform = getNodeMatrix(node, frame); } else if(progress > 1 - 1e-5) { nodeTransform = getNodeMatrix(node, nextFrame); } else { nodeTransform = getNodeMatrix(node, frame); nodeTransform = TransformationHelper.slerp(nodeTransform,getNodeMatrix(node, nextFrame), progress); } if(parent != null && node.getParent() == null) { return parent.getPartTransformation(part).compose(nodeTransform); } return nodeTransform; } private static LoadingCache, Integer>, TransformationMatrix> cache = CacheBuilder.newBuilder() .maximumSize(16384) .expireAfterAccess(2, TimeUnit.MINUTES) .build(new CacheLoader, Integer>, TransformationMatrix>() { @Override public TransformationMatrix load(Triple, Integer> key) throws Exception { return getNodeMatrix(key.getLeft(), key.getMiddle(), key.getRight()); } }); public TransformationMatrix getNodeMatrix(Node node) { return getNodeMatrix(node, frame); } public TransformationMatrix getNodeMatrix(Node node, int frame) { return cache.getUnchecked(Triple.of(animation, node, frame)); } public static TransformationMatrix getNodeMatrix(@Nullable Animation animation, Node node, int frame) { TransformationMatrix ret = TransformationMatrix.func_227983_a_(); Key key = null; if(animation != null) key = animation.getKeys().get(frame, node); else if(node.getAnimation() != null) key = node.getAnimation().getKeys().get(frame, node); if(key != null) { Node parent = node.getParent(); if(parent != null) { // parent model-global current pose TransformationMatrix pm = cache.getUnchecked(Triple.of(animation, node.getParent(), frame)); ret = ret.compose(pm); // joint offset in the parent coords ret = ret.compose(new TransformationMatrix(parent.getPos(), parent.getRot(), parent.getScale(), null)); } // current node local pose ret = ret.compose(new TransformationMatrix(key.getPos(), key.getRot(), key.getScale(), null)); // this part moved inside the model // inverse bind of the current node /*Matrix4f rm = new TRSRTransformation(node.getPos(), node.getRot(), node.getScale(), null).getMatrix(); rm.invert(); ret = ret.compose(new TRSRTransformation(rm)); if(parent != null) { // inverse bind of the parent rm = new TRSRTransformation(parent.getPos(), parent.getRot(), parent.getScale(), null).getMatrix(); rm.invert(); ret = ret.compose(new TRSRTransformation(rm)); }*/ // TODO cache TransformationMatrix invBind = new NodeJoint(node).getInvBindPose(); ret = ret.compose(invBind); } else { Node parent = node.getParent(); if(parent != null) { // parent model-global current pose TransformationMatrix pm = cache.getUnchecked(Triple.of(animation, node.getParent(), frame)); ret = ret.compose(pm); // joint offset in the parent coords ret = ret.compose(new TransformationMatrix(parent.getPos(), parent.getRot(), parent.getScale(), null)); } ret = ret.compose(new TransformationMatrix(node.getPos(), node.getRot(), node.getScale(), null)); // TODO cache TransformationMatrix invBind = new NodeJoint(node).getInvBindPose(); ret = ret.compose(invBind); } return ret; } } static final class NodeJoint implements IJoint { private final Node node; public NodeJoint(Node node) { this.node = node; } @Override public TransformationMatrix getInvBindPose() { Matrix4f m = new TransformationMatrix(node.getPos(), node.getRot(), node.getScale(), null).func_227988_c_(); m.func_226600_c_(); TransformationMatrix pose = new TransformationMatrix(m); if(node.getParent() != null) { TransformationMatrix parent = new NodeJoint(node.getParent()).getInvBindPose(); pose = pose.compose(parent); } return pose; } @Override public Optional getParent() { // FIXME cache? if(node.getParent() == null) return Optional.empty(); return Optional.of(new NodeJoint(node.getParent())); } public Node getNode() { return node; } @Override public int hashCode() { return node.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (!super.equals(obj)) return false; if (getClass() != obj.getClass()) return false; NodeJoint other = (NodeJoint) obj; return Objects.equal(node, other.node); } } private static final class ModelWrapper implements IUnbakedModel { private final ResourceLocation modelLocation; private final B3DModel model; private final ImmutableSet meshes; private final ImmutableMap textures; private final boolean smooth; private final boolean gui3d; private final int defaultKey; public ModelWrapper(ResourceLocation modelLocation, B3DModel model, ImmutableSet meshes, boolean smooth, boolean gui3d, int defaultKey) { this(modelLocation, model, meshes, smooth, gui3d, defaultKey, buildTextures(model.getTextures())); } public ModelWrapper(ResourceLocation modelLocation, B3DModel model, ImmutableSet meshes, boolean smooth, boolean gui3d, int defaultKey, ImmutableMap textures) { this.modelLocation = modelLocation; this.model = model; this.meshes = meshes; this.textures = textures; this.smooth = smooth; this.gui3d = gui3d; this.defaultKey = defaultKey; } private static ImmutableMap buildTextures(List textures) { ImmutableMap.Builder builder = ImmutableMap.builder(); for(Texture t : textures) { String path = t.getPath(); String location = getLocation(path); if(!location.startsWith("#")) location = "#" + location; builder.put(path, location); } return builder.build(); } private static String getLocation(String path) { if(path.endsWith(".png")) path = path.substring(0, path.length() - ".png".length()); return path; } @Override public Collection func_225614_a_(Function p_225614_1_, Set> p_225614_2_) { return textures.values().stream().filter(loc -> !loc.startsWith("#")) .map(t -> new Material(AtlasTexture.LOCATION_BLOCKS_TEXTURE, new ResourceLocation(t))) .collect(Collectors.toList()); } @Override public Collection getDependencies() { return Collections.emptyList(); } @Nullable @Override public IBakedModel func_225613_a_(ModelBakery bakery, Function spriteGetter, IModelTransform modelTransform, ResourceLocation modelLocation) { ImmutableMap.Builder builder = ImmutableMap.builder(); TextureAtlasSprite missing = spriteGetter.apply(new Material(AtlasTexture.LOCATION_BLOCKS_TEXTURE, MissingTextureSprite.getLocation())); for(Map.Entry e : textures.entrySet()) { if(e.getValue().startsWith("#")) { LOGGER.fatal("unresolved texture '{}' for b3d model '{}'", e.getValue(), this.modelLocation); builder.put(e.getKey(), missing); } else { builder.put(e.getKey(), spriteGetter.apply(new Material(AtlasTexture.LOCATION_BLOCKS_TEXTURE, new ResourceLocation(e.getValue())))); } } builder.put("missingno", missing); return new BakedWrapper(model.getRoot(), modelTransform, smooth, gui3d, meshes, builder.build()); } public ModelWrapper retexture(ImmutableMap textures) { ImmutableMap.Builder builder = ImmutableMap.builder(); for(Map.Entry e : this.textures.entrySet()) { String path = e.getKey(); String loc = getLocation(path); // FIXME: Backward compatibilty: support finding textures that start with #, even though this is not how vanilla works if(loc.startsWith("#") && (textures.containsKey(loc) || textures.containsKey(loc.substring(1)))) { String alt = loc.substring(1); String newLoc = textures.get(loc); if(newLoc == null) newLoc = textures.get(alt); if(newLoc == null) newLoc = path.substring(1); builder.put(e.getKey(), newLoc); } else { builder.put(e); } } return new ModelWrapper(modelLocation, model, meshes, smooth, gui3d, defaultKey, builder.build()); } public ModelWrapper process(ImmutableMap data) { ImmutableSet newMeshes = this.meshes; int newDefaultKey = this.defaultKey; boolean hasChanged = false; if(data.containsKey("mesh")) { JsonElement e = new JsonParser().parse(data.get("mesh")); if(e.isJsonPrimitive() && e.getAsJsonPrimitive().isString()) { return new ModelWrapper(modelLocation, model, ImmutableSet.of(e.getAsString()), smooth, gui3d, defaultKey, textures); } else if (e.isJsonArray()) { ImmutableSet.Builder builder = ImmutableSet.builder(); for(JsonElement s : e.getAsJsonArray()) { if(s.isJsonPrimitive() && s.getAsJsonPrimitive().isString()) { builder.add(s.getAsString()); } else { LOGGER.fatal("unknown mesh definition '{}' in array for b3d model '{}'", s.toString(), modelLocation); return this; } } newMeshes = builder.build(); hasChanged = true; } else { LOGGER.fatal("unknown mesh definition '{}' for b3d model '{}'", e.toString(), modelLocation); return this; } } if(data.containsKey("key")) { JsonElement e = new JsonParser().parse(data.get("key")); if(e.isJsonPrimitive() && e.getAsJsonPrimitive().isNumber()) { newDefaultKey = e.getAsNumber().intValue(); hasChanged = true; } else { LOGGER.fatal("unknown keyframe definition '{}' for b3d model '{}'", e.toString(), modelLocation); return this; } } return hasChanged ? new ModelWrapper(modelLocation, model, newMeshes, smooth, gui3d, newDefaultKey, textures) : this; } @Override public Optional getClip(String name) { if(name.equals("main")) { return Optional.of(B3DClip.INSTANCE); } return Optional.empty(); } public IModelTransform getDefaultState() { return new B3DState(model.getRoot().getAnimation(), defaultKey, defaultKey, 0); } public ModelWrapper smoothLighting(boolean value) { if(value == smooth) { return this; } return new ModelWrapper(modelLocation, model, meshes, value, gui3d, defaultKey, textures); } public ModelWrapper gui3d(boolean value) { if(value == gui3d) { return this; } return new ModelWrapper(modelLocation, model, meshes, smooth, value, defaultKey, textures); } } private static final class BakedWrapper implements IDynamicBakedModel { private final Node node; private final IModelTransform state; private final boolean smooth; private final boolean gui3d; private final ImmutableSet meshes; private final ImmutableMap textures; private final LoadingCache cache; private ImmutableList quads; public BakedWrapper(final Node node, final IModelTransform state, final boolean smooth, final boolean gui3d, final ImmutableSet meshes, final ImmutableMap textures) { this(node, state, smooth, gui3d, meshes, textures, CacheBuilder.newBuilder() .maximumSize(128) .expireAfterAccess(2, TimeUnit.MINUTES) .build(new CacheLoader() { @Override public B3DState load(Integer frame) throws Exception { IModelTransform parent = state; Animation newAnimation = node.getAnimation(); if(parent instanceof B3DState) { B3DState ps = (B3DState)parent; parent = ps.getParent(); } return new B3DState(newAnimation, frame, frame, 0, parent); } })); } public BakedWrapper(Node node, IModelTransform state, boolean smooth, boolean gui3d, ImmutableSet meshes, ImmutableMap textures, LoadingCache cache) { this.node = node; this.state = state; this.smooth = smooth; this.gui3d = gui3d; this.meshes = meshes; this.textures = textures; this.cache = cache; } @Override public List getQuads(@Nullable BlockState state, @Nullable Direction side, Random rand, IModelData data) { if(side != null) return ImmutableList.of(); IModelTransform modelState = this.state; IModelTransform newState = data.getData(Properties.AnimationProperty); if(newState != null) { // FIXME: should animation state handle the parent state, or should it remain here? IModelTransform parent = this.state; if(parent instanceof B3DState) { B3DState ps = (B3DState)parent; parent = ps.getParent(); } if (parent == null) { modelState = newState; } else { modelState = new ModelTransformComposition(parent, newState); } } if(quads == null) { ImmutableList.Builder builder = ImmutableList.builder(); generateQuads(builder, node, this.state, ImmutableList.of()); quads = builder.build(); } // TODO: caching? if(this.state != modelState) { ImmutableList.Builder builder = ImmutableList.builder(); generateQuads(builder, node, modelState, ImmutableList.of()); return builder.build(); } return quads; } private void generateQuads(ImmutableList.Builder builder, Node node, final IModelTransform state, ImmutableList path) { ImmutableList.Builder pathBuilder = ImmutableList.builder(); pathBuilder.addAll(path); pathBuilder.add(node.getName()); ImmutableList newPath = pathBuilder.build(); for(Node child : node.getNodes().values()) { generateQuads(builder, child, state, newPath); } if(node.getKind() instanceof Mesh && meshes.contains(node.getName()) && state.getPartTransformation(Models.getHiddenModelPart(newPath)).isIdentity()) { Mesh mesh = (Mesh)node.getKind(); Collection faces = mesh.bake(new Function, Matrix4f>() { private final TransformationMatrix global = state.func_225615_b_(); private final LoadingCache, TransformationMatrix> localCache = CacheBuilder.newBuilder() .maximumSize(32) .build(new CacheLoader, TransformationMatrix>() { @Override public TransformationMatrix load(Node node) throws Exception { return state.getPartTransformation(new NodeJoint(node)); } }); @Override public Matrix4f apply(Node node) { return global.compose(localCache.getUnchecked(node)).func_227988_c_(); } }); for(Face f : faces) { List textures = null; if(f.getBrush() != null) textures = f.getBrush().getTextures(); TextureAtlasSprite sprite; if(textures == null || textures.isEmpty()) sprite = this.textures.get("missingno"); else if(textures.get(0) == B3DModel.Texture.White) sprite = ModelLoader.White.instance(); else sprite = this.textures.get(textures.get(0).getPath()); BakedQuadBuilder quadBuilder = new BakedQuadBuilder(sprite); quadBuilder.setContractUVs(true); quadBuilder.setQuadOrientation(Direction.getFacingFromVector(f.getNormal().getX(), f.getNormal().getY(), f.getNormal().getZ())); putVertexData(quadBuilder, f.getV1(), f.getNormal(), sprite); putVertexData(quadBuilder, f.getV2(), f.getNormal(), sprite); putVertexData(quadBuilder, f.getV3(), f.getNormal(), sprite); putVertexData(quadBuilder, f.getV3(), f.getNormal(), sprite); builder.add(quadBuilder.build()); } } } private final void putVertexData(IVertexConsumer consumer, Vertex v, Vector3f faceNormal, TextureAtlasSprite sprite) { // TODO handle everything not handled (texture transformations, bones, transformations, normals, e.t.c) ImmutableList vertexFormatElements = consumer.getVertexFormat().func_227894_c_(); for(int e = 0; e < vertexFormatElements.size(); e++) { switch(vertexFormatElements.get(e).getUsage()) { case POSITION: consumer.put(e, v.getPos().getX(), v.getPos().getY(), v.getPos().getZ(), 1); break; case COLOR: if(v.getColor() != null) { consumer.put(e, v.getColor().getX(), v.getColor().getY(), v.getColor().getZ(), v.getColor().getW()); } else { consumer.put(e, 1, 1, 1, 1); } break; case UV: // TODO handle more brushes if(vertexFormatElements.get(e).getIndex() < v.getTexCoords().length) { consumer.put(e, sprite.getInterpolatedU(v.getTexCoords()[0].getX() * 16), sprite.getInterpolatedV(v.getTexCoords()[0].getY() * 16), 0, 1 ); } else { consumer.put(e, 0, 0, 0, 1); } break; case NORMAL: if(v.getNormal() != null) { consumer.put(e, v.getNormal().getX(), v.getNormal().getY(), v.getNormal().getZ(), 0); } else { consumer.put(e, faceNormal.getX(), faceNormal.getY(), faceNormal.getZ(), 0); } break; default: consumer.put(e); } } } @Override public boolean isAmbientOcclusion() { return smooth; } @Override public boolean isGui3d() { return gui3d; } @Override public boolean func_230044_c_() { // TODO: Forge: Auto-generated method stub return false; } @Override public boolean isBuiltInRenderer() { return false; } @Override public TextureAtlasSprite getParticleTexture() { // FIXME somehow specify particle texture in the model return textures.values().asList().get(0); } @Override public boolean doesHandlePerspectives() { return true; } @Override public IBakedModel handlePerspective(TransformType cameraTransformType, MatrixStack mat) { return PerspectiveMapWrapper.handlePerspective(this, state, cameraTransformType, mat); } @Override public ItemOverrideList getOverrides() { // TODO handle items return ItemOverrideList.EMPTY; } } }