593 lines
22 KiB
Java
593 lines
22 KiB
Java
/*
|
|
* Minecraft Forge
|
|
* Copyright (c) 2016.
|
|
*
|
|
* 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.animation;
|
|
|
|
import java.io.FileNotFoundException;
|
|
import java.io.IOException;
|
|
import java.io.InputStreamReader;
|
|
import java.util.Collections;
|
|
import java.util.EnumSet;
|
|
import java.util.Iterator;
|
|
import java.util.Map;
|
|
import java.util.NoSuchElementException;
|
|
import java.util.TreeMap;
|
|
|
|
import javax.vecmath.AxisAngle4f;
|
|
import javax.vecmath.Matrix4f;
|
|
import javax.vecmath.Quat4f;
|
|
import javax.vecmath.Vector3f;
|
|
|
|
import org.apache.logging.log4j.Level;
|
|
|
|
import net.minecraft.client.renderer.block.model.BlockPart;
|
|
import net.minecraft.client.resources.IResource;
|
|
import net.minecraft.client.resources.IResourceManager;
|
|
import net.minecraft.util.ResourceLocation;
|
|
import net.minecraft.util.math.MathHelper;
|
|
import net.minecraftforge.client.model.animation.ModelBlockAnimation.Parameter.Interpolation;
|
|
import net.minecraftforge.client.model.animation.ModelBlockAnimation.Parameter.Type;
|
|
import net.minecraftforge.client.model.animation.ModelBlockAnimation.Parameter.Variable;
|
|
import net.minecraftforge.common.animation.Event;
|
|
import net.minecraftforge.common.model.IModelState;
|
|
import net.minecraftforge.common.model.TRSRTransformation;
|
|
import net.minecraftforge.common.model.animation.IClip;
|
|
import net.minecraftforge.common.model.animation.IJoint;
|
|
import net.minecraftforge.common.model.animation.IJointClip;
|
|
import net.minecraftforge.common.model.animation.JointClips;
|
|
import net.minecraftforge.common.util.JsonUtils;
|
|
import net.minecraftforge.fml.common.FMLLog;
|
|
|
|
import com.google.common.base.Optional;
|
|
import com.google.common.collect.ImmutableCollection;
|
|
import com.google.common.collect.ImmutableList;
|
|
import com.google.common.collect.ImmutableMap;
|
|
import com.google.common.collect.ImmutableMultimap;
|
|
import com.google.common.collect.Maps;
|
|
import com.google.common.collect.Sets;
|
|
import com.google.common.collect.UnmodifiableIterator;
|
|
import com.google.gson.Gson;
|
|
import com.google.gson.GsonBuilder;
|
|
import com.google.gson.JsonParseException;
|
|
import com.google.gson.annotations.SerializedName;
|
|
|
|
public class ModelBlockAnimation
|
|
{
|
|
private final ImmutableMap<String, ImmutableMap<String, float[]>> joints;
|
|
private final ImmutableMap<String, MBClip> clips;
|
|
private transient ImmutableMultimap<Integer, MBJointWeight> jointIndexMap;
|
|
|
|
public ModelBlockAnimation(ImmutableMap<String, ImmutableMap<String, float[]>> joints, ImmutableMap<String, MBClip> clips)
|
|
{
|
|
this.joints = joints;
|
|
this.clips = clips;
|
|
}
|
|
|
|
public ImmutableMap<String, MBClip> getClips()
|
|
{
|
|
return clips;
|
|
}
|
|
|
|
public ImmutableCollection<MBJointWeight> getJoint(int i)
|
|
{
|
|
if(jointIndexMap == null)
|
|
{
|
|
ImmutableMultimap.Builder<Integer, MBJointWeight> builder = ImmutableMultimap.builder();
|
|
for(Map.Entry<String, ImmutableMap<String, float[]>> info : joints.entrySet())
|
|
{
|
|
ImmutableMap.Builder<Integer, float[]> weightBuilder = ImmutableMap.builder();
|
|
for(Map.Entry<String, float[]> e : info.getValue().entrySet())
|
|
{
|
|
weightBuilder.put(Integer.parseInt(e.getKey()), e.getValue());
|
|
}
|
|
ImmutableMap<Integer, float[]> weightMap = weightBuilder.build();
|
|
for(Map.Entry<Integer, float[]> e : weightMap.entrySet())
|
|
{
|
|
builder.put(e.getKey(), new MBJointWeight(info.getKey(), weightMap));
|
|
}
|
|
}
|
|
jointIndexMap = builder.build();
|
|
}
|
|
return jointIndexMap.get(i);
|
|
}
|
|
|
|
protected static class MBVariableClip
|
|
{
|
|
private final Variable variable;
|
|
@SuppressWarnings("unused")
|
|
private final Type type;
|
|
private final Interpolation interpolation;
|
|
private final float[] samples;
|
|
|
|
public MBVariableClip(Variable variable, Type type, Interpolation interpolation, float[] samples)
|
|
{
|
|
this.variable = variable;
|
|
this.type = type;
|
|
this.interpolation = interpolation;
|
|
this.samples = samples;
|
|
}
|
|
}
|
|
|
|
protected static class MBClip implements IClip
|
|
{
|
|
private final boolean loop;
|
|
@SerializedName("joint_clips")
|
|
private final ImmutableMap<String, ImmutableList<MBVariableClip>> jointClipsFlat;
|
|
private transient ImmutableMap<String, MBJointClip> jointClips;
|
|
@SerializedName("events")
|
|
private final ImmutableMap<String, String> eventsRaw;
|
|
private transient TreeMap<Float, Event> events;
|
|
|
|
public MBClip(boolean loop, ImmutableMap<String, ImmutableList<MBVariableClip>> clips, ImmutableMap<String, String> events)
|
|
{
|
|
this.loop = loop;
|
|
this.jointClipsFlat = clips;
|
|
this.eventsRaw = events;
|
|
}
|
|
|
|
private void initialize()
|
|
{
|
|
if(jointClips == null)
|
|
{
|
|
ImmutableMap.Builder<String, MBJointClip> builder = ImmutableMap.builder();
|
|
for (Map.Entry<String, ImmutableList<MBVariableClip>> e : jointClipsFlat.entrySet())
|
|
{
|
|
builder.put(e.getKey(), new MBJointClip(loop, e.getValue()));
|
|
}
|
|
jointClips = builder.build();
|
|
events = Maps.newTreeMap();
|
|
if (!eventsRaw.isEmpty())
|
|
{
|
|
TreeMap<Float, String> times = Maps.newTreeMap();
|
|
for (String time : eventsRaw.keySet())
|
|
{
|
|
times.put(Float.parseFloat(time), time);
|
|
}
|
|
float lastTime = Float.POSITIVE_INFINITY;
|
|
if (loop)
|
|
{
|
|
lastTime = times.firstKey();
|
|
}
|
|
for (Map.Entry<Float, String> entry : times.descendingMap().entrySet())
|
|
{
|
|
float time = entry.getKey();
|
|
float offset = lastTime - time;
|
|
if (loop)
|
|
{
|
|
offset = 1 - (1 - offset) % 1;
|
|
}
|
|
events.put(time, new Event(eventsRaw.get(entry.getValue()), offset));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public IJointClip apply(IJoint joint)
|
|
{
|
|
initialize();
|
|
if(joint instanceof MBJoint)
|
|
{
|
|
MBJoint mbJoint = (MBJoint)joint;
|
|
//MBJointInfo = jointInfos.
|
|
MBJointClip clip = jointClips.get(mbJoint.getName());
|
|
if(clip != null) return clip;
|
|
}
|
|
return JointClips.IdentityJointClip.INSTANCE;
|
|
}
|
|
|
|
@Override
|
|
public Iterable<Event> pastEvents(final float lastPollTime, final float time)
|
|
{
|
|
initialize();
|
|
return new Iterable<Event>()
|
|
{
|
|
public Iterator<Event> iterator()
|
|
{
|
|
return new UnmodifiableIterator<Event>()
|
|
{
|
|
private Float curKey;
|
|
private Event firstEvent;
|
|
private float stopTime;
|
|
{
|
|
if(lastPollTime >= time)
|
|
{
|
|
curKey = null;
|
|
}
|
|
else
|
|
{
|
|
float fractTime = time - (float)Math.floor(time);
|
|
float fractLastTime = lastPollTime - (float)Math.floor(lastPollTime);
|
|
// swap if not in order
|
|
if(fractLastTime > fractTime)
|
|
{
|
|
float tmp = fractTime;
|
|
fractTime = fractLastTime;
|
|
fractLastTime = tmp;
|
|
}
|
|
// need to wrap around, swap again
|
|
if(fractTime - fractLastTime > .5f)
|
|
{
|
|
float tmp = fractTime;
|
|
fractTime = fractLastTime;
|
|
fractLastTime = tmp;
|
|
}
|
|
|
|
stopTime = fractLastTime;
|
|
|
|
curKey = events.floorKey(fractTime);
|
|
if(curKey == null && loop && !events.isEmpty())
|
|
{
|
|
curKey = events.lastKey();
|
|
}
|
|
if(curKey != null)
|
|
{
|
|
float checkCurTime = curKey;
|
|
float checkStopTime = stopTime;
|
|
if(checkCurTime >= fractTime) checkCurTime--;
|
|
if(checkStopTime >= fractTime) checkStopTime--;
|
|
float offset = fractTime - checkCurTime;
|
|
Event event = events.get(curKey);
|
|
if(checkCurTime < checkStopTime)
|
|
{
|
|
curKey = null;
|
|
}
|
|
else if(offset != event.offset())
|
|
{
|
|
firstEvent = new Event(event.event(), offset);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public boolean hasNext()
|
|
{
|
|
return curKey != null;
|
|
}
|
|
|
|
@Override
|
|
public Event next()
|
|
{
|
|
if(curKey == null)
|
|
{
|
|
throw new NoSuchElementException();
|
|
}
|
|
Event event;
|
|
if(firstEvent == null)
|
|
{
|
|
event = events.get(curKey);
|
|
}
|
|
else
|
|
{
|
|
event = firstEvent;
|
|
firstEvent = null;
|
|
}
|
|
curKey = events.lowerKey(curKey);
|
|
if(curKey == null && loop)
|
|
{
|
|
curKey = events.lastKey();
|
|
}
|
|
if(curKey != null)
|
|
{
|
|
float checkStopTime = stopTime;
|
|
while(curKey + events.get(curKey).offset() < checkStopTime) checkStopTime--;
|
|
while(curKey + events.get(curKey).offset() >= checkStopTime + 1) checkStopTime++;
|
|
if(curKey <= checkStopTime)
|
|
{
|
|
curKey = null;
|
|
}
|
|
}
|
|
return event;
|
|
}
|
|
};
|
|
}
|
|
};
|
|
}
|
|
|
|
protected static class MBJointClip implements IJointClip
|
|
{
|
|
private final boolean loop;
|
|
private final ImmutableList<MBVariableClip> variables;
|
|
|
|
public MBJointClip(boolean loop, ImmutableList<MBVariableClip> variables)
|
|
{
|
|
this.loop = loop;
|
|
this.variables = variables;
|
|
EnumSet<Variable> hadVar = Sets.newEnumSet(Collections.<Variable>emptyList(), Variable.class);
|
|
for(MBVariableClip var : variables)
|
|
{
|
|
if(hadVar.contains(var.variable))
|
|
{
|
|
throw new IllegalArgumentException("duplicate variable: " + var);
|
|
}
|
|
hadVar.add(var.variable);
|
|
}
|
|
}
|
|
|
|
public TRSRTransformation apply(float time)
|
|
{
|
|
time -= Math.floor(time);
|
|
Vector3f translation = new Vector3f(0, 0, 0);
|
|
Vector3f scale = new Vector3f(1, 1, 1);
|
|
AxisAngle4f rotation = new AxisAngle4f(0, 0, 0, 0);
|
|
for(MBVariableClip var : variables)
|
|
{
|
|
int length = loop ? var.samples.length : (var.samples.length - 1);
|
|
float timeScaled = time * length;
|
|
int s1 = MathHelper.clamp((int)Math.round(Math.floor(timeScaled)), 0, length - 1);
|
|
float progress = timeScaled - s1;
|
|
int s2 = s1 + 1;
|
|
if(s2 == length && loop) s2 = 0;
|
|
float value = 0;
|
|
switch(var.interpolation)
|
|
{
|
|
case LINEAR:
|
|
if(var.variable == Variable.ANGLE)
|
|
{
|
|
float v1 = var.samples[s1];
|
|
float v2 = var.samples[s2];
|
|
float diff = ((v2 - v1) % 360 + 540) % 360 - 180;
|
|
value = v1 + diff * progress;
|
|
}
|
|
else
|
|
{
|
|
value = var.samples[s1] * (1 - progress) + var.samples[s2] * progress;
|
|
}
|
|
break;
|
|
case NEAREST:
|
|
value = var.samples[progress < .5f ? s1 : s2];
|
|
break;
|
|
}
|
|
switch(var.variable)
|
|
{
|
|
case X:
|
|
translation.x = value;
|
|
break;
|
|
case Y:
|
|
translation.y = value;
|
|
break;
|
|
case Z:
|
|
translation.z = value;
|
|
break;
|
|
case XROT:
|
|
rotation.x = value;
|
|
break;
|
|
case YROT:
|
|
rotation.y = value;
|
|
break;
|
|
case ZROT:
|
|
rotation.z = value;
|
|
break;
|
|
case ANGLE:
|
|
rotation.angle = (float)Math.toRadians(value);
|
|
break;
|
|
case SCALE:
|
|
scale.x = scale.y = scale.z = value;
|
|
break;
|
|
case XS:
|
|
scale.x = value;
|
|
break;
|
|
case YS:
|
|
scale.y = value;
|
|
break;
|
|
case ZS:
|
|
scale.z = value;
|
|
break;
|
|
}
|
|
}
|
|
Quat4f rot = new Quat4f();
|
|
rot.set(rotation);
|
|
return TRSRTransformation.blockCenterToCorner(new TRSRTransformation(translation, rot, scale, null));
|
|
}
|
|
}
|
|
}
|
|
|
|
protected static class MBJoint implements IJoint
|
|
{
|
|
private final String name;
|
|
private final TRSRTransformation invBindPose;
|
|
|
|
public MBJoint(String name, BlockPart part)
|
|
{
|
|
this.name = name;
|
|
if(part.partRotation != null)
|
|
{
|
|
float x = 0, y = 0, z = 0;
|
|
switch(part.partRotation.axis)
|
|
{
|
|
case X:
|
|
x = 1;
|
|
case Y:
|
|
y = 1;
|
|
case Z:
|
|
z = 1;
|
|
}
|
|
Quat4f rotation = new Quat4f();
|
|
rotation.set(new AxisAngle4f(x, y, z, 0));
|
|
Matrix4f m = new TRSRTransformation(
|
|
TRSRTransformation.toVecmath(part.partRotation.origin),
|
|
rotation,
|
|
null,
|
|
null).getMatrix();
|
|
m.invert();
|
|
invBindPose = new TRSRTransformation(m);
|
|
}
|
|
else
|
|
{
|
|
invBindPose = TRSRTransformation.identity();
|
|
}
|
|
}
|
|
|
|
public TRSRTransformation getInvBindPose()
|
|
{
|
|
return invBindPose;
|
|
}
|
|
|
|
public Optional<? extends IJoint> getParent()
|
|
{
|
|
return Optional.absent();
|
|
}
|
|
|
|
public String getName()
|
|
{
|
|
return name;
|
|
}
|
|
}
|
|
|
|
protected static class MBJointWeight
|
|
{
|
|
private final String name;
|
|
private final ImmutableMap<Integer, float[]> weights;
|
|
|
|
public MBJointWeight(String name, ImmutableMap<Integer, float[]> weights)
|
|
{
|
|
this.name = name;
|
|
this.weights = weights;
|
|
}
|
|
|
|
public String getName()
|
|
{
|
|
return name;
|
|
}
|
|
|
|
public ImmutableMap<Integer, float[]> getWeights()
|
|
{
|
|
return weights;
|
|
}
|
|
}
|
|
|
|
protected static class Parameter
|
|
{
|
|
public static enum Variable
|
|
{
|
|
@SerializedName("offset_x")
|
|
X,
|
|
@SerializedName("offset_y")
|
|
Y,
|
|
@SerializedName("offset_z")
|
|
Z,
|
|
@SerializedName("axis_x")
|
|
XROT,
|
|
@SerializedName("axis_y")
|
|
YROT,
|
|
@SerializedName("axis_z")
|
|
ZROT,
|
|
@SerializedName("angle")
|
|
ANGLE,
|
|
@SerializedName("scale")
|
|
SCALE,
|
|
@SerializedName("scale_x")
|
|
XS,
|
|
@SerializedName("scale_y")
|
|
YS,
|
|
@SerializedName("scale_z")
|
|
ZS;
|
|
}
|
|
|
|
public static enum Type
|
|
{
|
|
@SerializedName("uniform")
|
|
UNIFORM;
|
|
}
|
|
|
|
public static enum Interpolation
|
|
{
|
|
@SerializedName("linear")
|
|
LINEAR,
|
|
@SerializedName("nearest")
|
|
NEAREST;
|
|
}
|
|
}
|
|
|
|
public TRSRTransformation getPartTransform(IModelState state, BlockPart part, int i)
|
|
{
|
|
ImmutableCollection<MBJointWeight> infos = getJoint(i);
|
|
if(!infos.isEmpty())
|
|
{
|
|
Matrix4f m = new Matrix4f(), tmp;
|
|
float weight = 0;
|
|
for(MBJointWeight info : infos)
|
|
{
|
|
if(info.getWeights().containsKey(i))
|
|
{
|
|
ModelBlockAnimation.MBJoint joint = new ModelBlockAnimation.MBJoint(info.getName(), part);
|
|
Optional<TRSRTransformation> trOp = state.apply(Optional.of(joint));
|
|
if(trOp.isPresent() && trOp.get() != TRSRTransformation.identity())
|
|
{
|
|
float w = info.getWeights().get(i)[0];
|
|
tmp = trOp.get().getMatrix();
|
|
tmp.mul(w);
|
|
m.add(tmp);
|
|
weight += w;
|
|
}
|
|
}
|
|
}
|
|
if(weight > 1e-5)
|
|
{
|
|
m.mul(1f / weight);
|
|
return new TRSRTransformation(m);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Load armature associated with a vanilla model.
|
|
*/
|
|
public static ModelBlockAnimation loadVanillaAnimation(IResourceManager manager, ResourceLocation armatureLocation)
|
|
{
|
|
try
|
|
{
|
|
IResource resource = null;
|
|
try
|
|
{
|
|
resource = manager.getResource(armatureLocation);
|
|
}
|
|
catch(FileNotFoundException e)
|
|
{
|
|
// this is normal. FIXME: error reporting?
|
|
return defaultModelBlockAnimation;
|
|
}
|
|
ModelBlockAnimation mba = mbaGson.fromJson(new InputStreamReader(resource.getInputStream(), "UTF-8"), ModelBlockAnimation.class);
|
|
//String json = mbaGson.toJson(mba);
|
|
return mba;
|
|
}
|
|
catch(IOException e)
|
|
{
|
|
FMLLog.log(Level.ERROR, e, "Exception loading vanilla model animation %s, skipping", armatureLocation);
|
|
return defaultModelBlockAnimation;
|
|
}
|
|
catch(JsonParseException e)
|
|
{
|
|
FMLLog.log(Level.ERROR, e, "Exception loading vanilla model animation %s, skipping", armatureLocation);
|
|
return defaultModelBlockAnimation;
|
|
}
|
|
}
|
|
|
|
private static final Gson mbaGson = new GsonBuilder()
|
|
.registerTypeAdapter(ImmutableList.class, JsonUtils.ImmutableListTypeAdapter.INSTANCE)
|
|
.registerTypeAdapter(ImmutableMap.class, JsonUtils.ImmutableMapTypeAdapter.INSTANCE)
|
|
.setPrettyPrinting()
|
|
.enableComplexMapKeySerialization()
|
|
.disableHtmlEscaping()
|
|
.create();
|
|
|
|
private static final ModelBlockAnimation defaultModelBlockAnimation = new ModelBlockAnimation(ImmutableMap.<String, ImmutableMap<String, float[]>>of(), ImmutableMap.<String, ModelBlockAnimation.MBClip>of());
|
|
}
|