From 0d30350967959ebba89f3ed63cb3b4c21fed7328 Mon Sep 17 00:00:00 2001 From: CrushedPixel Date: Thu, 10 Sep 2015 00:49:53 +0200 Subject: [PATCH] Created AdvancedPositionKeyframeList, whose interpolation methods properly handle Spectator Keyframes using the SpectatorDataInterpolation --- .../replaymod/holders/AdvancedPosition.java | 9 + .../AdvancedPositionKeyframeList.java | 101 ++++++++++++ .../replaymod/interpolation/KeyframeList.java | 8 +- .../SpectatorDataInterpolation.java | 155 ++++++++++++++++++ 4 files changed, 269 insertions(+), 4 deletions(-) create mode 100644 src/main/java/eu/crushedpixel/replaymod/interpolation/AdvancedPositionKeyframeList.java create mode 100644 src/main/java/eu/crushedpixel/replaymod/interpolation/SpectatorDataInterpolation.java diff --git a/src/main/java/eu/crushedpixel/replaymod/holders/AdvancedPosition.java b/src/main/java/eu/crushedpixel/replaymod/holders/AdvancedPosition.java index 3b81f0da..551afcdc 100755 --- a/src/main/java/eu/crushedpixel/replaymod/holders/AdvancedPosition.java +++ b/src/main/java/eu/crushedpixel/replaymod/holders/AdvancedPosition.java @@ -62,6 +62,15 @@ public class AdvancedPosition extends Position { return new AdvancedPosition(position.x, position.y, position.z, pitch, yaw, roll); } + public void apply(AdvancedPosition toApply) { + this.x = toApply.getX(); + this.y = toApply.getY(); + this.z = toApply.getZ(); + this.pitch = toApply.getPitch(); + this.yaw = toApply.getYaw(); + this.roll = toApply.getRoll(); + } + @Override public AdvancedPosition newInstance() { return new AdvancedPosition(); diff --git a/src/main/java/eu/crushedpixel/replaymod/interpolation/AdvancedPositionKeyframeList.java b/src/main/java/eu/crushedpixel/replaymod/interpolation/AdvancedPositionKeyframeList.java new file mode 100644 index 00000000..77c2c02f --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/interpolation/AdvancedPositionKeyframeList.java @@ -0,0 +1,101 @@ +package eu.crushedpixel.replaymod.interpolation; + +import eu.crushedpixel.replaymod.holders.AdvancedPosition; +import eu.crushedpixel.replaymod.holders.Keyframe; +import eu.crushedpixel.replaymod.holders.SpectatorData; +import lombok.NoArgsConstructor; + +import java.util.HashSet; +import java.util.ListIterator; +import java.util.Set; + +@NoArgsConstructor +public class AdvancedPositionKeyframeList extends KeyframeList { + + private Set spectatorDataInterpolators; + + @Override + public AdvancedPosition getInterpolatedValueForPathPosition(float pathPosition, boolean linear) { + if(first() == null) return null; + if(size() == 1) return first().getValue(); + + @SuppressWarnings("unchecked") + AdvancedPosition toApply = first().getValue().newInstance(); + + if(previousCallLinear != (Boolean)linear) { + recalculate(linear); + } + + int keyframeIndex = (int)(pathPosition/size()); + float percentage = (pathPosition/size() - keyframeIndex)/(1f/size()); + AdvancedPosition current = get(keyframeIndex).getValue(); + + SpectatorDataInterpolation dataInterpolation = null; + if(current instanceof SpectatorData) { + for(SpectatorDataInterpolation interpolation : spectatorDataInterpolators) { + if(interpolation.contains((SpectatorData)current)) { + dataInterpolation = interpolation; + break; + } + } + } + + if(dataInterpolation == null) { + interpolation.applyPoint(pathPosition, toApply); + } else { + SpectatorData spectatorData = (SpectatorData)current; + int index = dataInterpolation.indexOf(spectatorData); + int size = dataInterpolation.size(); + float one = 1f/size; + dataInterpolation.applyPoint((index/size)+(percentage*one), toApply); + } + + return toApply; + } + + @Override + @SuppressWarnings("unchecked") + public void recalculate(boolean linear) { + previousCallLinear = linear; + + if(size() < 2) return; + + interpolation = linear ? first().getValue().getLinearInterpolator() : first().getValue().getCubicInterpolator(); + + for(Keyframe keyframe : this) { + interpolation.addPoint(keyframe.getValue()); + } + + interpolation.prepare(); + + spectatorDataInterpolators = new HashSet(); + + //find subsequent Spectator Keyframes with the same Entity ID + ListIterator> iterator = this.listIterator(); + while(iterator.hasNext()) { + Keyframe keyframe = iterator.next(); + SpectatorDataInterpolation spectatorInterpolation = null; + + boolean found = keyframe.getValue() instanceof SpectatorData; + + while(found) { + if(spectatorInterpolation == null) { + spectatorInterpolation = new SpectatorDataInterpolation(linear); + } + spectatorInterpolation.addPoint((SpectatorData)keyframe.getValue(), keyframe.getRealTimestamp()); + + if(iterator.hasNext()) { + keyframe = iterator.next(); + found = keyframe.getValue() instanceof SpectatorData; + } else { + found = false; + } + } + + if(spectatorInterpolation != null && spectatorInterpolation.size() > 1) { + spectatorInterpolation.prepare(); + spectatorDataInterpolators.add(spectatorInterpolation); + } + } + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/interpolation/KeyframeList.java b/src/main/java/eu/crushedpixel/replaymod/interpolation/KeyframeList.java index d1f63c9a..739e0115 100644 --- a/src/main/java/eu/crushedpixel/replaymod/interpolation/KeyframeList.java +++ b/src/main/java/eu/crushedpixel/replaymod/interpolation/KeyframeList.java @@ -11,11 +11,11 @@ import java.util.List; @NoArgsConstructor public class KeyframeList extends ArrayList> { - private static final KeyframeComparator KEYFRAME_COMPARATOR = new KeyframeComparator(); + protected static final KeyframeComparator KEYFRAME_COMPARATOR = new KeyframeComparator(); - private Boolean previousCallLinear = null; + protected Boolean previousCallLinear = null; - private Interpolation interpolation; + protected Interpolation interpolation; public KeyframeList(List> initial) { for(Keyframe kf : initial) { @@ -186,7 +186,7 @@ public class KeyframeList extends ArrayList * @param timestamp The value to use * @return A value between 0 and 1 */ - private float getPositionOnPath(int timestamp) { + protected float getPositionOnPath(int timestamp) { Keyframe previousKeyframe = getPreviousKeyframe(timestamp, true); Keyframe nextKeyframe = getNextKeyframe(timestamp, true); diff --git a/src/main/java/eu/crushedpixel/replaymod/interpolation/SpectatorDataInterpolation.java b/src/main/java/eu/crushedpixel/replaymod/interpolation/SpectatorDataInterpolation.java new file mode 100644 index 00000000..f3c671d7 --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/interpolation/SpectatorDataInterpolation.java @@ -0,0 +1,155 @@ +package eu.crushedpixel.replaymod.interpolation; + +import eu.crushedpixel.replaymod.holders.AdvancedPosition; +import eu.crushedpixel.replaymod.holders.Keyframe; +import eu.crushedpixel.replaymod.holders.SpectatorData; +import eu.crushedpixel.replaymod.holders.SpectatorDataThirdPersonInfo; +import eu.crushedpixel.replaymod.replay.ReplayHandler; +import org.apache.commons.lang3.tuple.Pair; + +import java.util.ArrayList; +import java.util.List; + +/** + * A SpectatorDataInterpolation object calculates Camera Paths between multiple Spectator Keyframes. + * It does not matter which SpectatingMethod these Keyframes have, the SpectatorDataInterpolation treats them specifically. + */ +public class SpectatorDataInterpolation { + + private Integer entityID = null; + + private List> points = new ArrayList>(); + private KeyframeList underlyingKeyframes; + + private final boolean linear; + + public SpectatorDataInterpolation(boolean linear) { + this.linear = linear; + underlyingKeyframes = new KeyframeList(); + } + + public int size() { + return points.size(); + } + + public boolean contains(SpectatorData spectatorData) { + return indexOf(spectatorData) != -1; + } + + public int indexOf(SpectatorData spectatorData) { + int i = 0; + for(Pair pair : points) { + if(pair.getValue().equals(spectatorData)) return i; + i++; + } + + return -1; + } + + public void prepare() { + //first, create an Interpolation instance to retreive a SpectatorDataThirdPersonInfo object for every + //position on the Spectator Path + Interpolation thirdPersonInfoInterpolation = + linear ? new GenericLinearInterpolation() : + new GenericSplineInterpolation(); + + double previousSmoothness = 0.5; + for(Pair pair : points) { + if(pair.getValue().getSpectatingMethod() == SpectatorData.SpectatingMethod.FIRST_PERSON) { + thirdPersonInfoInterpolation.addPoint(new SpectatorDataThirdPersonInfo(0, 0, 0, previousSmoothness)); + } else { + thirdPersonInfoInterpolation.addPoint(pair.getValue().getThirdPersonInfo()); + previousSmoothness = pair.getValue().getThirdPersonInfo().shoulderCamSmoothness; + } + } + thirdPersonInfoInterpolation.prepare(); + + //feed the underlying interpolator with AdvancedPosition Keyframes that are derived from the Spectator Keyframes + int i = 0; + int size = points.size(); + for(Pair pair : points) { + int timestamp = pair.getKey(); + int currentTimestamp = timestamp; + int nextTimestamp = timestamp; + + if(i+1 < points.size()) { + nextTimestamp = points.get(i+1).getKey(); + } + + int difference = nextTimestamp - timestamp; + + int smoothness = 0; + while(currentTimestamp + smoothness < nextTimestamp) { + currentTimestamp += smoothness; + + SpectatorDataThirdPersonInfo thirdPersonInfo = new SpectatorDataThirdPersonInfo(); + float percentage = (float)i/size; + percentage += ((currentTimestamp-timestamp)/(float)difference) * 1f/size; + + thirdPersonInfoInterpolation.applyPoint(percentage, thirdPersonInfo); + + smoothness = (int)(thirdPersonInfo.shoulderCamSmoothness*1000); + + //calculate the Position relative to the Entity + AdvancedPosition entityPosition = ReplayHandler.getEntityPositionTracker().getEntityPositionAtTimestamp(entityID, currentTimestamp); + if(entityPosition == null) continue; + + //first, rotate the camera pitch and yaw according to the settings + entityPosition.setYaw(entityPosition.getYaw() + thirdPersonInfo.shoulderCamYawOffset); + entityPosition.setPitch(entityPosition.getPitch() + thirdPersonInfo.shoulderCamPitchOffset); + + //next, move the camera point to fulfill the specified distance to the entity + entityPosition = entityPosition.getDestination(-1 * thirdPersonInfo.shoulderCamDistance); + underlyingKeyframes.add(new Keyframe(currentTimestamp, entityPosition)); + } + + i++; + } + + underlyingKeyframes.recalculate(linear); + } + + public void applyPoint(float position, AdvancedPosition toEdit) { + int keyframeIndex = (int)position*points.size(); + + //the progress between this and the next keyframe (between 0 and 1) + float partial = (position - ((float)keyframeIndex/points.size())) / (1f/points.size()); + + Pair pair = points.get(keyframeIndex); + Pair next = keyframeIndex < points.size()-1 ? points.get(keyframeIndex+1) : null; + + SpectatorData spectatorData = pair.getValue(); + + //decide whether the Entity is spectated in First Person Mode or not + boolean firstPerson = true; + + //firstPerson is false if either the SpectatorKeyframe + //or the following SpectatorKeyframe are no FIRST_PERSON Keyframes + //it's only true if the position value is between two FIRST_PERSON Keyframes + if(spectatorData.getSpectatingMethod() != SpectatorData.SpectatingMethod.FIRST_PERSON) { + firstPerson = false; + } else if(next != null) { + if(next.getValue().getSpectatingMethod() != SpectatorData.SpectatingMethod.FIRST_PERSON) firstPerson = false; + } + + if(firstPerson) { + int firstTimestamp = pair.getKey(); + int nextTimestamp = next == null ? pair.getKey() : next.getKey(); + + int diff = nextTimestamp - firstTimestamp; + int interpolatedTimestamp = firstTimestamp+(int)(partial*diff); + + AdvancedPosition pos = ReplayHandler.getEntityPositionTracker().getEntityPositionAtTimestamp(entityID, interpolatedTimestamp); + if(pos != null) + toEdit.apply(pos); + } else { + toEdit.apply(underlyingKeyframes.getInterpolatedValueForPathPosition(position, linear)); + } + } + + public void addPoint(SpectatorData spectatorData, int realTimestamp) { + if(entityID == null) entityID = spectatorData.getSpectatedEntityID(); + else if(entityID != spectatorData.getSpectatedEntityID()) throw new IllegalArgumentException(); + points.add(Pair.of(realTimestamp, spectatorData)); + } +} \ No newline at end of file