Skip to content

Commit 37d40ce

Browse files
committed
feat(motion-smoothing): add per-node interpolation, tween primitive, and visual offset
1 parent de45668 commit 37d40ce

32 files changed

Lines changed: 1641 additions & 2 deletions

client-fabric/src/main/java/com/moud/client/fabric/ClientMessageDispatcher.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
import com.moud.net.protocol.CursorState;
1919
import com.moud.net.protocol.EditorDiagnosticEvent;
2020
import com.moud.net.protocol.Message;
21+
import com.moud.client.fabric.scene.tween.ClientTweenPlayer;
2122
import com.moud.net.protocol.MatchmakerStatus;
23+
import com.moud.net.protocol.TweenCancel;
24+
import com.moud.net.protocol.TweenStart;
2225
import com.moud.net.protocol.MultiMeshData;
2326
import com.moud.net.protocol.RigidBodySnapshot;
2427
import com.moud.net.protocol.MeshPublish;
@@ -92,6 +95,16 @@ private void handleEngineMessage(Message message) {
9295
return;
9396
}
9497

98+
if (message instanceof TweenStart start) {
99+
ClientTweenPlayer.get().onStart(start);
100+
return;
101+
}
102+
103+
if (message instanceof TweenCancel cancel) {
104+
ClientTweenPlayer.get().onCancel(cancel);
105+
return;
106+
}
107+
95108
if (message instanceof MatchmakerStatus status) {
96109
String code = status.code() == null || status.code().isBlank() ? "matchmaker" : status.code();
97110
if (status.show()) {

client-fabric/src/main/java/com/moud/client/fabric/runtime/PlayRuntimeClient.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.moud.client.fabric.runtime;
22

33
import com.moud.client.fabric.physics.rapier.ClientRapierPhysics;
4+
import com.moud.client.fabric.scene.tween.ClientTweenPlayer;
45
import com.moud.client.fabric.render.VeilSceneRenderer;
56
import com.moud.core.util.MathUtils;
67
import com.moud.client.fabric.mixin.accessor.CameraAccessor;
@@ -247,6 +248,7 @@ public void travelFrame(MinecraftClient client, float tickDelta) {
247248
);
248249

249250
ClientRapierPhysics.get().tickRenderFrame();
251+
ClientTweenPlayer.get().tick(System.nanoTime());
250252

251253
clientScriptRuntime.syncAllNodes(characterBody, inputSnapshot, cameraState);
252254
clientScriptRuntime.frame(dt);

client-fabric/src/main/java/com/moud/client/fabric/scene/ClientSceneBus.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package com.moud.client.fabric.scene;
22

33

4+
import com.moud.client.fabric.scene.interp.InterpolationFeed;
5+
import com.moud.client.fabric.scene.tween.ClientTweenPlayer;
6+
import com.moud.client.fabric.scene.visual.VisualTransformRegistry;
47
import com.moud.net.protocol.SceneOp;
58
import com.moud.net.protocol.SceneSnapshot;
69
import com.moud.net.protocol.SceneSnapshotDelta;
@@ -59,6 +62,7 @@ public static void applySnapshot(SceneSnapshot snapshot) {
5962
synchronized (SCENE) {
6063
SCENE.applySnapshot(snapshot);
6164
}
65+
InterpolationFeed.onSnapshot(snapshot);
6266
VERSION.incrementAndGet();
6367
SNAPSHOT_VERSION.incrementAndGet();
6468
}
@@ -67,6 +71,7 @@ public static void applyDelta(SceneSnapshotDelta delta) {
6771
synchronized (SCENE) {
6872
SCENE.applyDelta(delta);
6973
}
74+
InterpolationFeed.onDelta(delta);
7075
VERSION.incrementAndGet();
7176
SNAPSHOT_VERSION.incrementAndGet();
7277
}
@@ -75,18 +80,53 @@ public static void applyOps(List<SceneOp> ops) {
7580
synchronized (SCENE) {
7681
SCENE.applyOps(ops);
7782
}
83+
feedTouchedNodes(ops);
7884
VERSION.incrementAndGet();
7985
}
8086

8187
public static void applyPhysicsOps(List<SceneOp> ops) {
8288
synchronized (SCENE) {
8389
SCENE.applyOps(ops);
8490
}
91+
feedTouchedNodes(ops);
8592
VERSION.incrementAndGet();
8693
PHYSICS_VERSION.incrementAndGet();
8794
MoudTickClock.onPhysicsBatchArrived();
8895
}
8996

97+
private static void feedTouchedNodes(List<SceneOp> ops) {
98+
if (ops == null || ops.isEmpty()) {
99+
return;
100+
}
101+
for (SceneOp op : ops) {
102+
long nodeId = touchedNodeId(op);
103+
if (nodeId <= 0L) {
104+
continue;
105+
}
106+
SceneSnapshot.NodeSnapshot node;
107+
synchronized (SCENE) {
108+
node = SCENE.getNode(nodeId);
109+
}
110+
if (node != null) {
111+
InterpolationFeed.onNodeRefreshed(node);
112+
} else {
113+
InterpolationFeed.onNodeRemoved(nodeId);
114+
}
115+
}
116+
}
117+
118+
private static long touchedNodeId(SceneOp op) {
119+
return switch (op) {
120+
case null -> 0L;
121+
case SceneOp.CreateNode ignored -> 0L;
122+
case SceneOp.QueueFree free -> free.nodeId();
123+
case SceneOp.Rename rename -> rename.nodeId();
124+
case SceneOp.SetProperty set -> set.nodeId();
125+
case SceneOp.RemoveProperty remove -> remove.nodeId();
126+
case SceneOp.Reparent reparent -> reparent.nodeId();
127+
};
128+
}
129+
90130
public static void markRestorePending() {
91131
RESET_VERSION.incrementAndGet();
92132
}
@@ -97,6 +137,9 @@ public static void clear() {
97137
}
98138
ClientLocalNodes.clearAll();
99139
ClientPropertyOverrides.clearAll();
140+
InterpolationFeed.onClear();
141+
ClientTweenPlayer.get().clear();
142+
VisualTransformRegistry.get().clearAll();
100143
VERSION.incrementAndGet();
101144
SNAPSHOT_VERSION.incrementAndGet();
102145
MoudTickClock.reset();

client-fabric/src/main/java/com/moud/client/fabric/scene/SceneNodeTransforms.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package com.moud.client.fabric.scene;
22

3+
import com.moud.client.fabric.scene.interp.NodeInterpolatorRegistry;
4+
import com.moud.client.fabric.scene.interp.SampledTransform;
5+
import com.moud.client.fabric.scene.visual.VisualTransform;
6+
import com.moud.client.fabric.scene.visual.VisualTransformRegistry;
37
import com.moud.core.util.ParseUtils;
48
import com.moud.net.protocol.SceneSnapshot;
59
import java.util.HashMap;
@@ -86,13 +90,44 @@ private static Pose localPose(SceneSnapshot.NodeSnapshot node) {
8690
}
8791
}
8892

93+
SampledTransform sampled = SAMPLE.get();
94+
sampled.reset();
95+
if (NodeInterpolatorRegistry.get().fillSampledTransform(node.nodeId(), sampled, System.nanoTime())) {
96+
if (sampled.hasX) x = sampled.x;
97+
if (sampled.hasY) y = sampled.y;
98+
if (sampled.hasZ) z = sampled.z;
99+
if (sampled.hasRx) rx = sampled.rx;
100+
if (sampled.hasRy) ry = sampled.ry;
101+
if (sampled.hasRz) rz = sampled.rz;
102+
if (sampled.hasSx) sx = Math.max(SCALE_EPS, sampled.sx);
103+
if (sampled.hasSy) sy = Math.max(SCALE_EPS, sampled.sy);
104+
if (sampled.hasSz) sz = Math.max(SCALE_EPS, sampled.sz);
105+
}
106+
107+
VisualTransform visual = VISUAL.get();
108+
visual.reset();
109+
if (VisualTransformRegistry.get().fill(node.nodeId(), visual)) {
110+
x += visual.dx;
111+
y += visual.dy;
112+
z += visual.dz;
113+
rx += visual.rxOff;
114+
ry += visual.ryOff;
115+
rz += visual.rzOff;
116+
sx = Math.max(SCALE_EPS, sx * visual.sxMul);
117+
sy = Math.max(SCALE_EPS, sy * visual.syMul);
118+
sz = Math.max(SCALE_EPS, sz * visual.szMul);
119+
}
120+
89121
pose.pos.set(x, y, z);
90122
pose.rot.set(quatFromEulerDeg(rx, ry, rz));
91123
pose.scale.set(sx, sy, sz);
92124
pose.inherit = ParseUtils.parseBool(inheritRaw, true);
93125
return pose;
94126
}
95127

128+
private static final ThreadLocal<SampledTransform> SAMPLE = ThreadLocal.withInitial(SampledTransform::new);
129+
private static final ThreadLocal<VisualTransform> VISUAL = ThreadLocal.withInitial(VisualTransform::new);
130+
96131
private static Quaternionf quatFromEulerDeg(float rxDeg, float ryDeg, float rzDeg) {
97132
return new Quaternionf().rotationXYZ(
98133
(float) Math.toRadians(Float.isFinite(rxDeg) ? rxDeg : 0.0f),
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.moud.client.fabric.scene.interp;
2+
3+
import com.moud.net.protocol.SceneSnapshot;
4+
import com.moud.net.protocol.SceneSnapshotDelta;
5+
import java.util.List;
6+
7+
public final class InterpolationFeed {
8+
9+
private InterpolationFeed() {
10+
}
11+
12+
public static void onSnapshot(SceneSnapshot snapshot) {
13+
if (snapshot == null) {
14+
return;
15+
}
16+
long now = System.nanoTime();
17+
List<SceneSnapshot.NodeSnapshot> nodes = snapshot.nodes();
18+
if (nodes == null) {
19+
return;
20+
}
21+
NodeInterpolatorRegistry registry = NodeInterpolatorRegistry.get();
22+
for (SceneSnapshot.NodeSnapshot node : nodes) {
23+
registry.onNodeUpdated(node, now);
24+
}
25+
}
26+
27+
public static void onDelta(SceneSnapshotDelta delta) {
28+
if (delta == null) {
29+
return;
30+
}
31+
long now = System.nanoTime();
32+
NodeInterpolatorRegistry registry = NodeInterpolatorRegistry.get();
33+
List<SceneSnapshot.NodeSnapshot> upserts = delta.upserts();
34+
if (upserts != null) {
35+
for (SceneSnapshot.NodeSnapshot node : upserts) {
36+
registry.onNodeUpdated(node, now);
37+
}
38+
}
39+
List<Long> removed = delta.removed();
40+
if (removed != null) {
41+
for (Long nodeId : removed) {
42+
if (nodeId != null) {
43+
registry.onNodeRemoved(nodeId);
44+
}
45+
}
46+
}
47+
}
48+
49+
public static void onNodeRefreshed(SceneSnapshot.NodeSnapshot node) {
50+
if (node == null) {
51+
return;
52+
}
53+
NodeInterpolatorRegistry.get().onNodeUpdated(node, System.nanoTime());
54+
}
55+
56+
public static void onNodeRemoved(long nodeId) {
57+
NodeInterpolatorRegistry.get().onNodeRemoved(nodeId);
58+
}
59+
60+
public static void onClear() {
61+
NodeInterpolatorRegistry.get().clear();
62+
}
63+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package com.moud.client.fabric.scene.interp;
2+
3+
import com.moud.core.interp.InterpPolicy;
4+
import com.moud.core.interp.InterpProperty;
5+
6+
public final class NodeInterpolator {
7+
8+
private final PropertySampleBuffer[] buffers = new PropertySampleBuffer[InterpProperty.COUNT];
9+
10+
private InterpPolicy policy;
11+
private long lastUpdateNanos;
12+
13+
public NodeInterpolator(InterpPolicy initial) {
14+
for (int i = 0; i < buffers.length; i++) {
15+
buffers[i] = new PropertySampleBuffer();
16+
}
17+
this.policy = initial;
18+
}
19+
20+
public InterpPolicy policy() {
21+
return policy;
22+
}
23+
24+
public void setPolicy(InterpPolicy next) {
25+
if (next != null) {
26+
this.policy = next;
27+
}
28+
}
29+
30+
public long lastUpdateNanos() {
31+
return lastUpdateNanos;
32+
}
33+
34+
public void push(InterpProperty property, float value, long nowNanos) {
35+
buffers[property.ordinal()].push(nowNanos, value);
36+
lastUpdateNanos = nowNanos;
37+
}
38+
39+
public boolean has(InterpProperty property) {
40+
return !buffers[property.ordinal()].isEmpty();
41+
}
42+
43+
public float sample(InterpProperty property, long sampleNanos) {
44+
PropertySampleBuffer buf = buffers[property.ordinal()];
45+
if (buf.isEmpty()) {
46+
return Float.NaN;
47+
}
48+
if (!policy.smooths()) {
49+
return buf.lastValue();
50+
}
51+
return buf.sample(sampleNanos, policy.mode());
52+
}
53+
54+
public void fill(SampledTransform out, long sampleNanos) {
55+
long lagNanos = (long) policy.lagMs() * 1_000_000L;
56+
long target = sampleNanos - lagNanos;
57+
if (!policy.smooths()) {
58+
target = sampleNanos;
59+
}
60+
61+
out.hasX = sampleInto(out, InterpProperty.X, target);
62+
out.hasY = sampleInto(out, InterpProperty.Y, target);
63+
out.hasZ = sampleInto(out, InterpProperty.Z, target);
64+
out.hasRx = sampleInto(out, InterpProperty.RX, target);
65+
out.hasRy = sampleInto(out, InterpProperty.RY, target);
66+
out.hasRz = sampleInto(out, InterpProperty.RZ, target);
67+
out.hasSx = sampleInto(out, InterpProperty.SX, target);
68+
out.hasSy = sampleInto(out, InterpProperty.SY, target);
69+
out.hasSz = sampleInto(out, InterpProperty.SZ, target);
70+
}
71+
72+
public void reset() {
73+
for (PropertySampleBuffer buf : buffers) {
74+
buf.reset();
75+
}
76+
lastUpdateNanos = 0L;
77+
}
78+
79+
private boolean sampleInto(SampledTransform out, InterpProperty p, long target) {
80+
PropertySampleBuffer buf = buffers[p.ordinal()];
81+
if (buf.isEmpty()) {
82+
return false;
83+
}
84+
float value = policy.smooths()
85+
? buf.sample(target, policy.mode())
86+
: buf.lastValue();
87+
switch (p) {
88+
case X -> out.x = value;
89+
case Y -> out.y = value;
90+
case Z -> out.z = value;
91+
case RX -> out.rx = value;
92+
case RY -> out.ry = value;
93+
case RZ -> out.rz = value;
94+
case SX -> out.sx = value;
95+
case SY -> out.sy = value;
96+
case SZ -> out.sz = value;
97+
}
98+
return true;
99+
}
100+
}

0 commit comments

Comments
 (0)