Working towards mdx handler

This commit is contained in:
Retera 2019-11-29 01:41:09 -06:00
parent eb49c33eef
commit 6acbd13f27
17 changed files with 660 additions and 12 deletions

View File

@ -192,4 +192,29 @@ public class Layer extends AnimatedObject {
public long getByteLength() {
return 28 + super.getByteLength();
}
public FilterMode getFilterMode() {
return filterMode;
}
public int getFlags() {
return flags;
}
public int getTextureId() {
return textureId;
}
public int getTextureAnimationId() {
return textureAnimationId;
}
public long getCoordId() {
return coordId;
}
public float getAlpha() {
return alpha;
}
}

View File

@ -23,9 +23,12 @@ import com.google.common.io.LittleEndianDataOutputStream;
* and text MDL file formats.
*/
public class MdlxModel {
// Below, these can't call a function on a string to make their value because
// switch/case statements require the value to be compile-time defined in order
// to be legal, and it appears to only allow basic binary operators for that.
// Below, these can't call a function on a string to make their value
// because
// switch/case statements require the value to be compile-time defined in
// order
// to be legal, and it appears to only allow basic binary operators for
// that.
// I would love a clearer way to just type 'MDLX' in a character constant in
// Java for this
private static final int MDLX = ('M' << 24) | ('D' << 16) | ('L' << 8) | ('X');// War3ID.fromString("MDLX").getValue();
@ -52,10 +55,10 @@ public class MdlxModel {
private int version = 800;
private String name = "";
/**
* (Comment copied from Ghostwolf JS) To the best of my knowledge, this should
* always be left empty. This is probably a leftover from the Warcraft 3 beta.
* (WS game note: No, I never saw any animation files in the RoC 2001-2002 Beta.
* So it must be from the Alpha)
* (Comment copied from Ghostwolf JS) To the best of my knowledge, this
* should always be left empty. This is probably a leftover from the
* Warcraft 3 beta. (WS game note: No, I never saw any animation files in
* the RoC 2001-2002 Beta. So it must be from the Alpha)
*
* @member {string}
*/
@ -205,7 +208,7 @@ public class MdlxModel {
private <E extends MdlxBlock & Chunk> void loadDynamicObjects(final List<E> out,
final MdlxBlockDescriptor<E> constructor, final LittleEndianDataInputStream stream, final long size)
throws IOException {
throws IOException {
long totalSize = 0;
while (totalSize < size) {
final E object = constructor.create();
@ -456,7 +459,7 @@ public class MdlxModel {
private <E extends MdlxBlock> void loadNumberedObjectBlock(final List<E> out,
final MdlxBlockDescriptor<E> constructor, final String name, final MdlTokenInputStream stream)
throws IOException {
throws IOException {
stream.read(); // Don't care about the number, the array will grow
for (final String token : stream.readBlock()) {
@ -639,4 +642,12 @@ public class MdlxModel {
return 0;
}
public List<Long> getGlobalSequences() {
return globalSequences;
}
public List<Sequence> getSequences() {
return sequences;
}
}

View File

@ -101,4 +101,8 @@ public class Sequence implements MdlxBlock {
this.extent.writeMdl(stream);
stream.endBlock();
}
public long[] getInterval() {
return interval;
}
}

View File

@ -1,6 +1,7 @@
package com.etheller.warsmash.parsers.mdlx.timeline;
import java.io.IOException;
import java.util.Arrays;
import com.etheller.warsmash.parsers.mdlx.InterpolationType;
import com.etheller.warsmash.parsers.mdlx.MdlTokenInputStream;
@ -96,4 +97,28 @@ public class FloatArrayKeyFrame implements KeyFrame {
return size;
}
@Override
public long getTime() {
return time;
}
@Override
public boolean matchingValue(final KeyFrame other) {
if (other instanceof FloatArrayKeyFrame) {
final FloatArrayKeyFrame otherFrame = (FloatArrayKeyFrame) other;
return Arrays.equals(value, otherFrame.value);
}
return false;
}
@Override
public KeyFrame clone(final long time) {
final FloatArrayKeyFrame newKeyFrame = new FloatArrayKeyFrame(value.length);
System.arraycopy(value, 0, newKeyFrame.value, 0, value.length);
// if (inTan != null) {
System.arraycopy(inTan, 0, newKeyFrame.inTan, 0, inTan.length);
System.arraycopy(outTan, 0, newKeyFrame.outTan, 0, outTan.length);
// }
return newKeyFrame;
}
}

View File

@ -6,6 +6,7 @@ import com.etheller.warsmash.parsers.mdlx.InterpolationType;
import com.etheller.warsmash.parsers.mdlx.MdlTokenInputStream;
import com.etheller.warsmash.parsers.mdlx.MdlTokenOutputStream;
import com.etheller.warsmash.util.ParseUtils;
import com.etheller.warsmash.util.RenderMathUtils;
import com.google.common.io.LittleEndianDataInputStream;
import com.google.common.io.LittleEndianDataOutputStream;
@ -90,4 +91,26 @@ public class FloatKeyFrame implements KeyFrame {
return size;
}
@Override
public long getTime() {
return time;
}
@Override
public boolean matchingValue(final KeyFrame other) {
if (other instanceof FloatKeyFrame) {
final FloatKeyFrame otherFrame = (FloatKeyFrame) other;
return Math.abs(value - otherFrame.value) <= RenderMathUtils.EPSILON;
}
return false;
}
@Override
public KeyFrame clone(final long time) {
final FloatKeyFrame newKeyFrame = new FloatKeyFrame();
newKeyFrame.value = value;
newKeyFrame.inTan = inTan;
newKeyFrame.outTan = outTan;
return newKeyFrame;
}
}

View File

@ -18,4 +18,10 @@ public interface KeyFrame {
void writeMdl(MdlTokenOutputStream stream, InterpolationType interpolationType) throws IOException;
long getByteLength(InterpolationType interpolationType);
long getTime();
boolean matchingValue(KeyFrame other);
KeyFrame clone(long time);
}

View File

@ -148,4 +148,16 @@ public abstract class Timeline implements Chunk {
protected abstract KeyFrame newKeyFrame();
protected abstract int size();
public int getGlobalSequenceId() {
return globalSequenceId;
}
public List<KeyFrame> getKeyFrames() {
return keyFrames;
}
public InterpolationType getInterpolationType() {
return interpolationType;
}
}

View File

@ -90,4 +90,26 @@ public class UInt32KeyFrame implements KeyFrame {
return size;
}
@Override
public long getTime() {
return time;
}
@Override
public boolean matchingValue(final KeyFrame other) {
if (other instanceof UInt32KeyFrame) {
final UInt32KeyFrame otherFrame = (UInt32KeyFrame) other;
return value == otherFrame.value;
}
return false;
}
@Override
public KeyFrame clone(final long time) {
final UInt32KeyFrame newKeyFrame = new UInt32KeyFrame();
newKeyFrame.value = value;
newKeyFrame.inTan = inTan;
newKeyFrame.outTan = outTan;
return newKeyFrame;
}
}

View File

@ -0,0 +1,72 @@
package com.etheller.warsmash.util;
public class Interpolator {
public void interpolateScalar(final float[] out, final float[] a, final float[] b, final float[] c, final float[] d,
final float t, final int type) {
switch (type) {
case 0: {
out[0] = a[0];
break;
}
case 1: {
out[0] = RenderMathUtils.lerp(a[0], d[0], t);
break;
}
case 2: {
out[0] = RenderMathUtils.hermite(a[0], b[0], c[0], d[0], t);
break;
}
case 3: {
out[0] = RenderMathUtils.bezier(a[0], b[0], c[0], d[0], t);
break;
}
}
}
public void interpolateVector(final float[] out, final float[] a, final float[] b, final float[] c, final float[] d,
final float t, final int type) {
switch (type) {
case 0: {
System.arraycopy(a, 0, out, 0, a.length);
break;
}
case 1: {
out[0] = RenderMathUtils.lerp(a[0], d[0], t);
out[1] = RenderMathUtils.lerp(a[1], d[1], t);
out[2] = RenderMathUtils.lerp(a[2], d[2], t);
break;
}
case 2: {
out[0] = RenderMathUtils.hermite(a[0], b[0], c[0], d[0], t);
out[1] = RenderMathUtils.hermite(a[1], b[1], c[1], d[1], t);
out[2] = RenderMathUtils.hermite(a[2], b[2], c[2], d[2], t);
break;
}
case 3: {
out[0] = RenderMathUtils.bezier(a[0], b[0], c[0], d[0], t);
out[1] = RenderMathUtils.bezier(a[1], b[1], c[1], d[1], t);
out[2] = RenderMathUtils.bezier(a[2], b[2], c[2], d[2], t);
break;
}
}
}
public void interpolateQuaternion(final float[] out, final float[] a, final float[] b, final float[] c,
final float[] d, final float t, final int type) {
switch (type) {
case 0: {
System.arraycopy(a, 0, out, 0, a.length);
break;
}
case 1: {
RenderMathUtils.slerp(out, a, d, t);
break;
}
case 2:
case 3: {
RenderMathUtils.sqlerp(out, a, b, c, d, t);
break;
}
}
}
}

View File

@ -101,12 +101,14 @@ public enum RenderMathUtils {
}
public static void mul(final Matrix4 dest, final Matrix4 left, final Matrix4 right) {
dest.set(left); // TODO better performance here, remove the extra copying
dest.set(left); // TODO better performance here, remove the extra
// copying
dest.mul(right);
}
public static void mul(final Quaternion dest, final Quaternion left, final Quaternion right) {
dest.set(left); // TODO better performance here, remove the extra copying
dest.set(left); // TODO better performance here, remove the extra
// copying
dest.mul(right);
}
@ -352,4 +354,90 @@ public enum RenderMathUtils {
public static float distanceToPlane3(final Vector4 plane, final float px, final float py, final float pz) {
return (plane.x * px) + (plane.y * py) + (plane.z * pz) + plane.w;
}
public static float randomInRange(final float a, final float b) {
return (float) (a + Math.random() * (b - a));
}
public static float clamp(final float x, final float minVal, final float maxVal) {
return Math.min(Math.max(x, minVal), maxVal);
}
public static float lerp(final float a, final float b, final float t) {
return a + t * (b - a);
}
public static float hermite(final float a, final float b, final float c, final float d, final float t) {
final float factorTimes2 = t * t;
final float factor1 = factorTimes2 * (2 * t - 3) + 1;
final float factor2 = factorTimes2 * (t - 2) + t;
final float factor3 = factorTimes2 * (t - 1);
final float factor4 = factorTimes2 * (3 - 2 * t);
return (a * factor1) + (b * factor2) + (c * factor3) + (d * factor4);
}
public static float bezier(final float a, final float b, final float c, final float d, final float t) {
final float invt = 1 - t;
final float factorTimes2 = t * t;
final float inverseFactorTimesTwo = invt * invt;
final float factor1 = inverseFactorTimesTwo * invt;
final float factor2 = 3 * t * inverseFactorTimesTwo;
final float factor3 = 3 * factorTimes2 * invt;
final float factor4 = factorTimes2 * t;
return (a * factor1) + (b * factor2) + (c * factor3) + (d * factor4);
}
public static final float EPSILON = 0.000001f;
public static float[] slerp(final float[] out, final float[] a, final float[] b, final float t) {
final float ax = a[0], ay = a[1], az = a[2], aw = a[3];
float bx = b[0], by = b[1], bz = b[2], bw = b[3];
float omega, cosom, sinom, scale0, scale1;
// calc cosine
cosom = ax * bx + ay * by + az * bz + aw * bw;
// adjust signs (if necessary)
if (cosom < 0.0) {
cosom = -cosom;
bx = -bx;
by = -by;
bz = -bz;
bw = -bw;
}
// calculate coefficients
if ((1.0 - cosom) > EPSILON) {
// standard case (slerp)
omega = (float) Math.acos(cosom);
sinom = (float) Math.sin(omega);
scale0 = (float) (Math.sin((1.0 - t) * omega) / sinom);
scale1 = (float) (Math.sin(t * omega) / sinom);
}
else {
// "from" and "to" quaternions are very close
// ... so we can do a linear interpolation
scale0 = 1.0f - t;
scale1 = t;
}
// calculate final values
out[0] = scale0 * ax + scale1 * bx;
out[1] = scale0 * ay + scale1 * by;
out[2] = scale0 * az + scale1 * bz;
out[3] = scale0 * aw + scale1 * bw;
return out;
}
private static final float[] sqlerpHeap1 = new float[4];
private static final float[] sqlerpHeap2 = new float[4];
public static float[] sqlerp(final float[] out, final float[] a, final float[] b, final float[] c, final float[] d,
final float t) {
slerp(sqlerpHeap1, a, d, t);
slerp(sqlerpHeap2, b, c, t);
slerp(out, sqlerpHeap1, sqlerpHeap2, 2 * t * (1 - t));
return out;
}
}

View File

@ -33,7 +33,10 @@ public abstract class ModelView {
@Override
public abstract boolean equals(Object view);
// public boo
@Override
public abstract int hashCode();
// public boo
public void addSceneData(final ModelInstance instance, final Scene scene) {
if (this.model.ok && (scene != null)) {
SceneData data = this.sceneData.get(scene);

View File

@ -0,0 +1,6 @@
package com.etheller.warsmash.viewer5.handlers.mdx;
public class AnimatedObject {
public Model model;
public
}

View File

@ -0,0 +1,30 @@
package com.etheller.warsmash.viewer5.handlers.mdx;
import com.etheller.warsmash.parsers.mdlx.Layer.FilterMode;
public class Layer {
public Model model;
public com.etheller.warsmash.parsers.mdlx.Layer layer;
public int layerId;
public int priorityPlane;
public int filterMode;
public int textureId;
public int coordId;
public float alpha;
public Layer(final Model model, final com.etheller.warsmash.parsers.mdlx.Layer layer, final int layerId,
final int priorityPlane) {
super(model, layer);
this.model = model;
this.layer = layer;
this.layerId = layerId;
this.priorityPlane = priorityPlane;
final FilterMode filterMode2 = layer.getFilterMode();
this.filterMode = filterMode2.ordinal();
this.textureId = layer.getTextureId();
// this.coo
}
}

View File

@ -0,0 +1,16 @@
package com.etheller.warsmash.viewer5.handlers.mdx;
import java.util.List;
public class Material {
public final Model model;
public final String shader;
public final List<Layer> layers;
public Material(final Model model, final String shader, final List<Layer> layers) {
this.model = model;
this.shader = shader;
this.layers = layers;
}
}

View File

@ -0,0 +1,25 @@
package com.etheller.warsmash.viewer5.handlers.mdx;
import java.io.InputStream;
public class Model extends com.etheller.warsmash.viewer5.Model {
@Override
protected void lateLoad() {
// TODO Auto-generated method stub
}
@Override
protected void load(final InputStream src, final Object options) {
// TODO Auto-generated method stub
}
@Override
protected void error(final Exception e) {
// TODO Auto-generated method stub
}
}

View File

@ -0,0 +1,130 @@
package com.etheller.warsmash.viewer5.handlers.mdx;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.etheller.warsmash.parsers.mdlx.MdlxModel;
import com.etheller.warsmash.parsers.mdlx.Sequence;
import com.etheller.warsmash.parsers.mdlx.timeline.KeyFrame;
import com.etheller.warsmash.parsers.mdlx.timeline.Timeline;
import com.etheller.warsmash.util.War3ID;
import com.etheller.warsmash.viewer5.ModelInstance;
public abstract class Sd<TYPE> {
public MdlxModel model;
public int interpolationType;
public War3ID name;
public float[] defval;
public SdSequence<TYPE> globalSequence;
public List<SdSequence<TYPE>> sequences;
public static Map<War3ID, Integer> forcedInterpMap = new HashMap<>();
static {
forcedInterpMap.put(War3ID.fromString("KLAV"), 0);
forcedInterpMap.put(War3ID.fromString("KATV"), 0);
forcedInterpMap.put(War3ID.fromString("KPEV"), 0);
forcedInterpMap.put(War3ID.fromString("KP2V"), 0);
forcedInterpMap.put(War3ID.fromString("KRVS"), 0);
}
public static Map<War3ID, float[]> defVals = new HashMap<>();
static {
// LAYS
defVals.put(War3ID.fromString("KMTF"), new float[] { 0 });
defVals.put(War3ID.fromString("KMTA"), new float[] { 1 });
// TXAN
defVals.put(War3ID.fromString("KTAT"), new float[] { 0, 0, 0 });
defVals.put(War3ID.fromString("KTAR"), new float[] { 0, 0, 0, 1 });
defVals.put(War3ID.fromString("KTAS"), new float[] { 1, 1, 1 });
// GEOA
defVals.put(War3ID.fromString("KGAO"), new float[] { 1 });
defVals.put(War3ID.fromString("KGAC"), new float[] { 0, 0, 0 });
// LITE
defVals.put(War3ID.fromString("KLAS"), new float[] { 0 });
defVals.put(War3ID.fromString("KLAE"), new float[] { 0 });
defVals.put(War3ID.fromString("KLAC"), new float[] { 0, 0, 0 });
defVals.put(War3ID.fromString("KLAI"), new float[] { 0 });
defVals.put(War3ID.fromString("KLBI"), new float[] { 0 });
defVals.put(War3ID.fromString("KLBC"), new float[] { 0, 0, 0 });
defVals.put(War3ID.fromString("KLAV"), new float[] { 1 });
// ATCH
defVals.put(War3ID.fromString("KATV"), new float[] { 1 });
// PREM
defVals.put(War3ID.fromString("KPEE"), new float[] { 0 });
defVals.put(War3ID.fromString("KPEG"), new float[] { 0 });
defVals.put(War3ID.fromString("KPLN"), new float[] { 0 });
defVals.put(War3ID.fromString("KPLT"), new float[] { 0 });
defVals.put(War3ID.fromString("KPEL"), new float[] { 0 });
defVals.put(War3ID.fromString("KPES"), new float[] { 0 });
defVals.put(War3ID.fromString("KPEV"), new float[] { 1 });
// PRE2
defVals.put(War3ID.fromString("KP2S"), new float[] { 0 });
defVals.put(War3ID.fromString("KP2R"), new float[] { 0 });
defVals.put(War3ID.fromString("KP2L"), new float[] { 0 });
defVals.put(War3ID.fromString("KP2G"), new float[] { 0 });
defVals.put(War3ID.fromString("KP2E"), new float[] { 0 });
defVals.put(War3ID.fromString("KP2N"), new float[] { 0 });
defVals.put(War3ID.fromString("KP2W"), new float[] { 0 });
defVals.put(War3ID.fromString("KP2V"), new float[] { 1 });
// RIBB
defVals.put(War3ID.fromString("KRHA"), new float[] { 0 });
defVals.put(War3ID.fromString("KRHB"), new float[] { 0 });
defVals.put(War3ID.fromString("KRAL"), new float[] { 1 });
defVals.put(War3ID.fromString("KRCO"), new float[] { 0, 0, 0 });
defVals.put(War3ID.fromString("KRTX"), new float[] { 0 });
defVals.put(War3ID.fromString("KRVS"), new float[] { 1 });
// CAMS
defVals.put(War3ID.fromString("KCTR"), new float[] { 0, 0, 0 });
defVals.put(War3ID.fromString("KTTR"), new float[] { 0, 0, 0 });
defVals.put(War3ID.fromString("KCRL"), new float[] { 0 });
// NODE
defVals.put(War3ID.fromString("KGTR"), new float[] { 0, 0, 0 });
defVals.put(War3ID.fromString("KGRT"), new float[] { 0, 0, 0, 1 });
defVals.put(War3ID.fromString("KGSC"), new float[] { 1, 1, 1 });
}
public Sd(final MdlxModel model, final Timeline timeline) {
final List<Long> globalSequences = model.getGlobalSequences();
final int globalSequenceId = timeline.getGlobalSequenceId();
final List<KeyFrame> keyFrames = timeline.getKeyFrames();
final Integer forcedInterp = forcedInterpMap.get(timeline.getName());
this.model = model;
this.name = timeline.getName();
this.defval = defVals.get(timeline.getName());
this.globalSequence = null;
this.sequences = new ArrayList<>();
// Allow to force an interpolation type.
// The game seems to do this with visibility tracks, where the type is
// forced to None.
// It came up as a bug report by a user who used the wrong interpolation
// type.
this.interpolationType = forcedInterp != null ? forcedInterp : timeline.getInterpolationType().ordinal();
if (globalSequenceId != -1 && globalSequences.size() > 0) {
this.globalSequence = newSequenceTyped(this, 0, globalSequences.get(globalSequenceId).longValue(),
keyFrames, true);
}
else {
for (final Sequence sequence : model.getSequences()) {
final long[] interval = sequence.getInterval();
this.sequences.add(newSequenceTyped(this, interval[0], interval[1], keyFrames, false));
}
}
}
public int getValue(final TYPE out, final ModelInstance instance) {
if(this.globalSequence != null) {
return this.globalSequence
}
}
protected abstract SdSequence<TYPE> newSequenceTyped(final Sd<TYPE> parent, final long start, final long end,
final List<KeyFrame> keyframes, final boolean isGlobalSequence);
}

View File

@ -0,0 +1,150 @@
package com.etheller.warsmash.viewer5.handlers.mdx;
import java.util.ArrayList;
import java.util.List;
import com.etheller.warsmash.parsers.mdlx.timeline.KeyFrame;
import com.etheller.warsmash.util.RenderMathUtils;
public abstract class SdSequence<TYPE> {
private final Sd<TYPE> sd;
private final long start; // UInt32
private final long end; // UInt32
private final List<KeyFrame> keyframes;
private boolean constant;
public SdSequence(final Sd<TYPE> sd, final long start, final long end, final List<KeyFrame> keyframes,
final boolean isGlobalSequence) {
final TYPE defval = convertDefaultValue(sd.defval);
this.sd = sd;
this.start = start;
this.end = end;
this.keyframes = new ArrayList<>();
// When using a global sequence, where the first key is outside of the
// sequence's length, it becomes its constant value.
// When having one key in the sequence's range, and one key outside of
// it, results seem to be non-deterministic.
// Sometimes the second key is used too, sometimes not.
// It also differs depending where the model is viewed - the WE
// previewer, the WE itself, or the game.
// All three show different results, none of them make sense.
// Therefore, only handle the case where the first key is outside.
// This fixes problems spread over many models, e.g. HeroMountainKing
// (compare in WE and in Magos).
if (isGlobalSequence && keyframes.size() > 0 && keyframes.get(0).getTime() > end) {
this.keyframes.add(keyframes.get(0));
}
// Go over the keyframes, and add all of the ones that are in this
// sequence (start <= frame <= end).
for (int i = 0, l = keyframes.size(); i < l; i++) {
final KeyFrame keyFrame = keyframes.get(i);
final long frame = keyFrame.getTime();
if (frame >= start && frame <= end) {
this.keyframes.add(keyFrame);
}
}
final int keyframeCount = this.keyframes.size();
if (keyframeCount == 0) {
// if there are no keys, use the default value directly.
this.constant = true;
this.keyframes.add(createKeyFrame(start, defval));
}
else if (keyframeCount == 1) {
// If there's only one key, use it directly
this.constant = true;
}
else {
final KeyFrame firstFrame = this.keyframes.get(0);
// If all of the values in this sequence are the same, might as well
// make it constant.
boolean allFramesMatch = true;
for (final KeyFrame frame : this.keyframes) {
if (!frame.matchingValue(firstFrame)) {
allFramesMatch = false;
}
}
this.constant = allFramesMatch;
if (!this.constant) {
// If there is no opening keyframe for this sequence, inject one
// with the default value.
if (this.keyframes.get(0).getTime() != start) {
this.keyframes.add(0, createKeyFrame(start, defval));
}
// If there is no closing keyframe for this sequence, inject one
// with the default value.
if (this.keyframes.get(this.keyframes.size() - 1).getTime() != end) {
this.keyframes.add(this.keyframes.get(0).clone(end));
}
}
}
}
public int getValue(final TYPE out, final long frame) {
final int index = this.getKeyframe(frame);
final int size = keyframes.size();
if (index == -1) {
set(out, keyframes.get(0));
return 0;
}
else if (index == size) {
set(out, keyframes.get(size - 1));
return size - 1;
}
else {
final KeyFrame start = keyframes.get(index - 1);
final KeyFrame end = keyframes.get(index);
final float t = RenderMathUtils.clamp((frame - start.getTime()) / (end.getTime() - start.getTime()), 0, 1);
interpolate(out, start, end, t);
return index;
}
}
public int getKeyframe(final long frame) {
if (this.constant) {
return -1;
}
else {
final int l = keyframes.size();
if (frame < this.start) {
return -1;
}
else if (frame >= this.end) {
return 1;
}
else {
for (int i = 1; i < l; i++) {
final KeyFrame keyframe = keyframes.get(i);
if (keyframe.getTime() > frame) {
return i;
}
}
}
}
return -1;
}
protected abstract void set(TYPE out, KeyFrame frameForValue);
protected abstract TYPE convertDefaultValue(float[] defaultValue);
protected abstract KeyFrame createKeyFrame(long time, TYPE value);
protected abstract void interpolate(TYPE out, KeyFrame a, KeyFrame b, float t);
}