ForgePatch/src/main/java/net/minecraftforge/common/model/animation/Clips.java

589 lines
20 KiB
Java

/*
* 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.common.model.animation;
import java.io.IOException;
import net.minecraft.client.renderer.TransformationMatrix;
import net.minecraft.client.renderer.model.IModelTransform;
import net.minecraft.client.renderer.model.IUnbakedModel;
import net.minecraft.client.renderer.model.ModelResourceLocation;
import net.minecraft.util.IStringSerializable;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.client.model.ModelLoader;
import net.minecraftforge.common.animation.Event;
import net.minecraftforge.common.animation.ITimeValue;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.common.model.TransformationHelper;
import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.function.Function;
import com.google.common.base.Objects;
import java.util.Optional;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import javax.annotation.Nullable;
/**
* Various implementations of IClip, and utility methods.
*/
public final class Clips
{
private static final Logger LOGGER = LogManager.getLogger();
/**
* Clip that does nothing.
*/
public static enum IdentityClip implements IClip, IStringSerializable
{
INSTANCE;
@Override
public IJointClip apply(IJoint joint)
{
return JointClips.IdentityJointClip.INSTANCE;
}
@Override
public Iterable<Event> pastEvents(float lastPollTime, float time)
{
return ImmutableSet.<Event>of();
}
@Override
public String getName()
{
return "identity";
}
}
/**
* Retrieves the clip from the model.
*/
@OnlyIn(Dist.CLIENT)
public static IClip getModelClipNode(ResourceLocation modelLocation, String clipName)
{
IUnbakedModel model = ModelLoader.defaultModelGetter().apply(modelLocation);
Optional<? extends IClip> clip = model.getClip(clipName);
if (clip.isPresent())
{
return new ModelClip(clip.get(), modelLocation, clipName);
}
LOGGER.error("Unable to find clip {} in the model {}", clipName, modelLocation);
// FIXME: missing clip?
return new ModelClip(IdentityClip.INSTANCE, modelLocation, clipName);
}
/**
* Wrapper for model clips; useful for debugging and serialization;
*/
public static final class ModelClip implements IClip
{
private final IClip childClip;
private final ResourceLocation modelLocation;
private final String clipName;
public ModelClip(IClip childClip, ResourceLocation modelLocation, String clipName)
{
this.childClip = childClip;
this.modelLocation = modelLocation;
this.clipName = clipName;
}
@Override
public IJointClip apply(IJoint joint)
{
return childClip.apply(joint);
}
@Override
public Iterable<Event> pastEvents(float lastPollTime, float time)
{
return childClip.pastEvents(lastPollTime, time);
}
@Override
public int hashCode()
{
return Objects.hashCode(modelLocation, clipName);
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
ModelClip other = (ModelClip) obj;
return Objects.equal(modelLocation, other.modelLocation) && Objects.equal(clipName, other.clipName);
}
}
/**
* Clip with custom parameterization of the time.
*/
public static final class TimeClip implements IClip
{
private final IClip childClip;
private final ITimeValue time;
public TimeClip(IClip childClip, ITimeValue time)
{
this.childClip = childClip;
this.time = time;
}
@Override
public IJointClip apply(final IJoint joint)
{
return new IJointClip()
{
private final IJointClip parent = childClip.apply(joint);
@Override
public TransformationMatrix apply(float time)
{
return parent.apply(TimeClip.this.time.apply(time));
}
};
}
@Override
public Iterable<Event> pastEvents(float lastPollTime, float time)
{
return childClip.pastEvents(this.time.apply(lastPollTime), this.time.apply(time));
}
@Override
public int hashCode()
{
return Objects.hashCode(childClip, time);
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
TimeClip other = (TimeClip) obj;
return Objects.equal(childClip, other.childClip) && Objects.equal(time, other.time);
}
}
/**
* Spherical linear blend between 2 clips.
*/
public static final class SlerpClip implements IClip
{
private final IClip from;
private final IClip to;
private final ITimeValue input;
private final ITimeValue progress;
public SlerpClip(IClip from, IClip to, ITimeValue input, ITimeValue progress)
{
this.from = from;
this.to = to;
this.input = input;
this.progress = progress;
}
@Override
public IJointClip apply(IJoint joint)
{
IJointClip fromClip = from.apply(joint);
IJointClip toClip = to.apply(joint);
return blendClips(joint, fromClip, toClip, input, progress);
}
@Override
public Iterable<Event> pastEvents(float lastPollTime, float time)
{
float clipLastPollTime = input.apply(lastPollTime);
float clipTime = input.apply(time);
return Iterables.mergeSorted(ImmutableSet.of(
from.pastEvents(clipLastPollTime, clipTime),
to.pastEvents(clipLastPollTime, clipTime)
), Ordering.<Event>natural());
}
@Override
public int hashCode()
{
return Objects.hashCode(from, to, input, progress);
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
SlerpClip other = (SlerpClip) obj;
return Objects.equal(from, other.from) &&
Objects.equal(to, other.to) &&
Objects.equal(input, other.input) &&
Objects.equal(progress, other.progress);
}
}
/*public static class AdditiveLerpClip implements IClip
{
private final IClip base;
private final IClip add;
private final IParameter progress;
public AdditiveLerpClip(IClip base, IClip add, IParameter progress)
{
this.base = base;
this.add = add;
this.progress = progress;
}
public IJointClip apply(IJoint joint)
{
throw new NotImplementedException("AdditiveLerpClip.apply");
}
}*/
private static IJointClip blendClips(final IJoint joint, final IJointClip fromClip, final IJointClip toClip, final ITimeValue input, final ITimeValue progress)
{
return new IJointClip()
{
@Override
public TransformationMatrix apply(float time)
{
float clipTime = input.apply(time);
return TransformationHelper.slerp(fromClip.apply(clipTime), toClip.apply(clipTime), MathHelper.clamp(progress.apply(time), 0, 1));
}
};
}
/**
* IModelState wrapper for a Clip, sampled at specified time.
*/
public static Pair<IModelTransform, Iterable<Event>> apply(final IClip clip, final float lastPollTime, final float time)
{
return Pair.of(new IModelTransform()
{
@Override
public TransformationMatrix getRotation()
{
return TransformationMatrix.identity();
}
@Override
public TransformationMatrix getPartTransformation(Object part)
{
if(!(part instanceof IJoint))
{
return TransformationMatrix.identity();
}
IJoint joint = (IJoint)part;
// TODO: Cache clip application?
TransformationMatrix jointTransform = clip.apply(joint).apply(time).compose(joint.getInvBindPose());
Optional<? extends IJoint> parent = joint.getParent();
while(parent.isPresent())
{
TransformationMatrix parentTransform = clip.apply(parent.get()).apply(time);
jointTransform = parentTransform.compose(jointTransform);
parent = parent.get().getParent();
}
return jointTransform;
}
}, clip.pastEvents(lastPollTime, time));
}
/**
* Clip + Event, triggers when parameter becomes non-negative.
*/
public static final class TriggerClip implements IClip
{
private final IClip clip;
private final ITimeValue parameter;
private final String event;
public TriggerClip(IClip clip, ITimeValue parameter, String event)
{
this.clip = clip;
this.parameter = parameter;
this.event = event;
}
@Override
public IJointClip apply(IJoint joint)
{
return clip.apply(joint);
}
@Override
public Iterable<Event> pastEvents(float lastPollTime, float time)
{
if(parameter.apply(lastPollTime) < 0 && parameter.apply(time) >= 0)
{
return Iterables.mergeSorted(ImmutableSet.of(
clip.pastEvents(lastPollTime, time),
ImmutableSet.of(new Event(event, 0))
), Ordering.<Event>natural());
}
return clip.pastEvents(lastPollTime, time);
}
}
/**
* Reference to another clip.
* Should only exist during debugging.
*/
public static final class ClipReference implements IClip, IStringSerializable
{
private final String clipName;
private final Function<String, IClip> clipResolver;
private IClip clip;
public ClipReference(String clipName, Function<String, IClip> clipResolver)
{
this.clipName = clipName;
this.clipResolver = clipResolver;
}
private void resolve()
{
if(clip == null)
{
if(clipResolver != null)
{
clip = clipResolver.apply(clipName);
}
if(clip == null)
{
throw new IllegalArgumentException("Couldn't resolve clip " + clipName);
}
}
}
@Override
public IJointClip apply(final IJoint joint)
{
resolve();
return clip.apply(joint);
}
@Override
public Iterable<Event> pastEvents(float lastPollTime, float time)
{
resolve();
return clip.pastEvents(lastPollTime, time);
}
@Override
public String getName()
{
return clipName;
}
@Override
public int hashCode()
{
resolve();
return clip.hashCode();
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
ClipReference other = (ClipReference) obj;
resolve();
other.resolve();
return Objects.equal(clip, other.clip);
}
}
public static enum CommonClipTypeAdapterFactory implements TypeAdapterFactory
{
INSTANCE;
private final ThreadLocal<Function<String, IClip>> clipResolver = new ThreadLocal<Function<String, IClip>>();
public void setClipResolver(@Nullable Function<String, IClip> clipResolver)
{
this.clipResolver.set(clipResolver);
}
@Override
@SuppressWarnings("unchecked")
@Nullable
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type)
{
if(type.getRawType() != IClip.class)
{
return null;
}
final TypeAdapter<ITimeValue> parameterAdapter = gson.getAdapter(ITimeValue.class);
return (TypeAdapter<T>)new TypeAdapter<IClip>()
{
@Override
public void write(JsonWriter out, IClip clip) throws IOException
{
// IdentityClip + ClipReference
if(clip instanceof IStringSerializable)
{
out.value("#" + ((IStringSerializable)clip).getName());
return;
}
else if(clip instanceof TimeClip)
{
out.beginArray();
out.value("apply");
TimeClip timeClip = (TimeClip)clip;
write(out, timeClip.childClip);
parameterAdapter.write(out, timeClip.time);
out.endArray();
return;
}
else if(clip instanceof SlerpClip)
{
out.beginArray();
out.value("slerp");
SlerpClip slerpClip = (SlerpClip)clip;
write(out, slerpClip.from);
write(out, slerpClip.to);
parameterAdapter.write(out, slerpClip.input);
parameterAdapter.write(out, slerpClip.progress);
out.endArray();
return;
}
else if(clip instanceof TriggerClip)
{
out.beginArray();
out.value("trigger_positive");
TriggerClip triggerClip = (TriggerClip)clip;
write(out, triggerClip.clip);
parameterAdapter.write(out, triggerClip.parameter);
out.value(triggerClip.event);
out.endArray();
return;
}
else if(clip instanceof ModelClip)
{
ModelClip modelClip = (ModelClip)clip;
out.value(modelClip.modelLocation + "@" + modelClip.clipName);
return;
}
// TODO custom clip writing?
throw new NotImplementedException("unknown Clip to json: " + clip);
}
@Override
public IClip read(JsonReader in) throws IOException
{
switch(in.peek())
{
case BEGIN_ARRAY:
in.beginArray();
String type = in.nextString();
IClip clip;
// TimeClip
if("apply".equals(type))
{
clip = new TimeClip(read(in), parameterAdapter.read(in));
}
else if("slerp".equals(type))
{
clip = new SlerpClip(read(in), read(in), parameterAdapter.read(in), parameterAdapter.read(in));
}
else if("trigger_positive".equals(type))
{
clip = new TriggerClip(read(in), parameterAdapter.read(in), in.nextString());
}
else
{
throw new IOException("Unknown Clip type \"" + type + "\"");
}
in.endArray();
return clip;
case STRING:
String string = in.nextString();
// IdentityClip
if(string.equals("#identity"))
{
return IdentityClip.INSTANCE;
}
// Clip reference
if(string.startsWith("#"))
{
return new ClipReference(string.substring(1), clipResolver.get());
}
// ModelClip
else
{
int at = string.lastIndexOf('@');
String location = string.substring(0, at);
String clipName = string.substring(at + 1, string.length());
ResourceLocation model;
if(location.indexOf('#') != -1)
{
model = new ModelResourceLocation(location);
}
else
{
model = new ResourceLocation(location);
}
return getModelClipNode(model, clipName);
}
default:
throw new IOException("expected Clip, got " + in.peek());
}
}
};
}
}
}