Skip to content

Commit 769dcfa

Browse files
authored
Animation Game Piece Intake [AARD-1973] (#1216)
2 parents de9a478 + f3673e5 commit 769dcfa

File tree

6 files changed

+103
-22
lines changed

6 files changed

+103
-22
lines changed

fission/src/mirabuf/EjectableSceneObject.ts

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,21 @@ class EjectableSceneObject extends SceneObject {
2121
private _deltaTransformation?: THREE.Matrix4
2222
private _ejectVelocity?: number
2323

24+
// Animation state
25+
private _animationStartTime = 0
26+
private _animationDuration = EjectableSceneObject._defaultAnimationDuration
27+
private _startTranslation?: THREE.Vector3
28+
private _startRotation?: THREE.Quaternion
29+
30+
private static _defaultAnimationDuration = 0.5
31+
32+
public static setAnimationDuration(duration: number) {
33+
EjectableSceneObject._defaultAnimationDuration = duration
34+
}
35+
public static getAnimationDuration() {
36+
return EjectableSceneObject._defaultAnimationDuration
37+
}
38+
2439
public get gamePieceBodyId() {
2540
return this._gamePieceBodyId
2641
}
@@ -49,15 +64,24 @@ class EjectableSceneObject extends SceneObject {
4964
)
5065
this._ejectVelocity = this._parentAssembly.ejectorPreferences.ejectorVelocity
5166

67+
// Record start transform at the game piece center of mass
68+
const gpBody = World.physicsSystem.getBody(this._gamePieceBodyId)
69+
this._startTranslation = new THREE.Vector3(0, 0, 0)
70+
this._startRotation = new THREE.Quaternion(0, 0, 0, 1)
71+
convertJoltMat44ToThreeMatrix4(gpBody.GetCenterOfMassTransform()).decompose(
72+
this._startTranslation,
73+
this._startRotation,
74+
new THREE.Vector3(1, 1, 1)
75+
)
76+
77+
this._animationDuration = EjectableSceneObject._defaultAnimationDuration
78+
this._animationStartTime = performance.now()
79+
5280
World.physicsSystem.disablePhysicsForBody(this._gamePieceBodyId)
5381

54-
// Checks if the gamepiece comes from a zone for persistent point score updates
55-
// because gamepieces removed by intake are not detected in the collision listener
82+
// Remove from any scoring zones
5683
const zones = [...World.sceneRenderer.sceneObjects.entries()]
57-
.filter(x => {
58-
const y = x[1] instanceof ScoringZoneSceneObject
59-
return y
60-
})
84+
.filter(x => x[1] instanceof ScoringZoneSceneObject)
6185
.map(x => x[1]) as ScoringZoneSceneObject[]
6286

6387
zones.forEach(x => {
@@ -69,26 +93,51 @@ class EjectableSceneObject extends SceneObject {
6993
}
7094

7195
public update(): void {
96+
const now = performance.now()
97+
const elapsed = (now - this._animationStartTime) / 1000
98+
const tRaw = elapsed / this._animationDuration
99+
const t = Math.min(tRaw, 1)
100+
101+
// ease-in curve for gradual acceleration
102+
const easedT = t * t
103+
72104
if (this._parentBodyId && this._deltaTransformation && this._gamePieceBodyId) {
73105
if (!World.physicsSystem.isBodyAdded(this._gamePieceBodyId)) {
74106
this._gamePieceBodyId = undefined
75107
return
76108
}
77109

78-
// I had a think and free wrote this matrix math on a whim. It worked first try and I honestly can't quite remember how it works... -Hunter
79110
const gpBody = World.physicsSystem.getBody(this._gamePieceBodyId)
80111
const posToCOM = convertJoltMat44ToThreeMatrix4(gpBody.GetCenterOfMassTransform()).premultiply(
81112
convertJoltMat44ToThreeMatrix4(gpBody.GetWorldTransform()).invert()
82113
)
83114

84115
const body = World.physicsSystem.getBody(this._parentBodyId)
85-
const bodyTransform = posToCOM
86-
.invert()
87-
.premultiply(
88-
this._deltaTransformation
89-
.clone()
90-
.premultiply(convertJoltMat44ToThreeMatrix4(body.GetWorldTransform()))
91-
)
116+
let desiredPosition = new THREE.Vector3(0, 0, 0)
117+
let desiredRotation = new THREE.Quaternion(0, 0, 0, 1)
118+
119+
// Compute target world transform
120+
const desiredTransform = this._deltaTransformation
121+
.clone()
122+
.premultiply(convertJoltMat44ToThreeMatrix4(body.GetWorldTransform()))
123+
124+
desiredTransform.decompose(desiredPosition, desiredRotation, new THREE.Vector3(1, 1, 1))
125+
126+
if (t < 1 && this._startTranslation && this._startRotation) {
127+
// gradual acceleration via easedT
128+
desiredPosition = new THREE.Vector3().lerpVectors(this._startTranslation, desiredPosition, easedT)
129+
desiredRotation = new THREE.Quaternion().copy(this._startRotation).slerp(desiredRotation, easedT)
130+
}
131+
// } else if (t >= 1) {
132+
// // snap instantly and re-enable physics
133+
// World.physicsSystem.enablePhysicsForBody(this._gamePieceBodyId)
134+
// }
135+
136+
// apply the transform
137+
desiredTransform.identity().compose(desiredPosition, desiredRotation, new THREE.Vector3(1, 1, 1))
138+
139+
const bodyTransform = posToCOM.clone().invert().premultiply(desiredTransform)
140+
92141
const position = new THREE.Vector3(0, 0, 0)
93142
const rotation = new THREE.Quaternion(0, 0, 0, 1)
94143
bodyTransform.decompose(position, rotation, new THREE.Vector3(1, 1, 1))

fission/src/systems/preferences/PreferenceTypes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export type IntakePreferences = {
9292
parentNode: string | undefined
9393
showZoneAlways: boolean
9494
maxPieces: number
95+
animationDuration: number
9596
}
9697

9798
export type EjectorPreferences = {
@@ -181,6 +182,7 @@ export function defaultRobotPreferences(): RobotPreferences {
181182
parentNode: undefined,
182183
showZoneAlways: false,
183184
maxPieces: 1,
185+
animationDuration: 0.5,
184186
},
185187
ejector: {
186188
deltaTransformation: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],

fission/src/test/PreferencesSystem.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ describe("Preference System Robot/Field", () => {
123123
parentNode: undefined,
124124
showZoneAlways: true,
125125
maxPieces: 3,
126+
animationDuration: 0.5,
126127
},
127128
ejector: {
128129
deltaTransformation: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
@@ -142,6 +143,7 @@ describe("Preference System Robot/Field", () => {
142143
parentNode: undefined,
143144
showZoneAlways: false,
144145
maxPieces: 1,
146+
animationDuration: 0.5,
145147
},
146148
ejector: {
147149
deltaTransformation: [1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],

fission/src/test/mirabuf/MirabufSceneObject.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ describe("MirabufSceneObject", () => {
188188
zoneDiameter: 1,
189189
showZoneAlways: false,
190190
maxPieces: 0,
191+
animationDuration: 0.5,
191192
})
192193
expect(instance.setEjectable(bodyId)).toBe(false)
193194
})
@@ -205,6 +206,7 @@ describe("MirabufSceneObject", () => {
205206
zoneDiameter: 1,
206207
showZoneAlways: false,
207208
maxPieces: 2,
209+
animationDuration: 0.5,
208210
})
209211
setPrivate(instance, "_ejectables", [])
210212
const bodyId = mockBodyId()

fission/src/ui/panels/configuring/assembly-config/interfaces/ConfigureGamepiecePickupInterface.tsx

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,14 @@ import { PAUSE_REF_ASSEMBLY_CONFIG } from "@/systems/physics/PhysicsSystem"
2323
import { Box } from "@mui/material"
2424
import { Switch } from "@mui/base/Switch"
2525
import Label, { LabelSize } from "@/ui/components/Label"
26+
import EjectableSceneObject from "@/mirabuf/EjectableSceneObject"
2627

2728
// slider constants
2829
const MIN_ZONE_SIZE = 0.1
2930
const MAX_ZONE_SIZE = 1.0
31+
const MIN_ANIMATION_DURATION = 0.1
32+
const MAX_ANIMATION_DURATION = 2.0
33+
const ANIMATION_DURATION_STEP = 0.05
3034

3135
/**
3236
* Saves ejector configuration to selected robot.
@@ -57,7 +61,8 @@ function save(
5761
selectedRobot: MirabufSceneObject,
5862
selectedNode?: RigidNodeId,
5963
showZoneAlways?: boolean,
60-
maxPieces?: number
64+
maxPieces?: number,
65+
animationDuration?: number
6166
) {
6267
if (!selectedRobot?.intakePreferences || !gizmo) {
6368
return
@@ -88,6 +93,7 @@ function save(
8893
}
8994

9095
selectedRobot.intakePreferences.maxPieces = maxPieces!
96+
selectedRobot.intakePreferences.animationDuration = animationDuration!
9197

9298
PreferencesSystem.savePreferences()
9399
}
@@ -106,15 +112,18 @@ const ConfigureGamepiecePickupInterface: React.FC<ConfigPickupProps> = ({ select
106112
const [zoneSize, setZoneSize] = useState<number>((MIN_ZONE_SIZE + MAX_ZONE_SIZE) / 2.0)
107113
const [showZoneAlways, setShowZoneAlways] = useState<boolean>(false)
108114
const [maxPieces, setMaxPieces] = useState<number>(selectedRobot.intakePreferences?.maxPieces || 1)
115+
const [animationDuration, setAnimationDuration] = useState<number>(
116+
selectedRobot.intakePreferences?.animationDuration || 0.5
117+
)
109118

110119
const gizmoRef = useRef<GizmoSceneObject | undefined>(undefined)
111120

112121
const saveEvent = useCallback(() => {
113122
if (gizmoRef.current && selectedRobot) {
114-
save(zoneSize, gizmoRef.current, selectedRobot, selectedNode, showZoneAlways, maxPieces)
123+
save(zoneSize, gizmoRef.current, selectedRobot, selectedNode, showZoneAlways, maxPieces, animationDuration)
115124
selectedRobot.updateIntakeSensor()
116125
}
117-
}, [selectedRobot, selectedNode, zoneSize, showZoneAlways, maxPieces])
126+
}, [selectedRobot, selectedNode, zoneSize, showZoneAlways, maxPieces, animationDuration])
118127

119128
useEffect(() => {
120129
ConfigurationSavedEvent.listen(saveEvent)
@@ -197,9 +206,11 @@ const ConfigureGamepiecePickupInterface: React.FC<ConfigPickupProps> = ({ select
197206
setSelectedNode(selectedRobot.intakePreferences.parentNode)
198207
setMaxPieces(selectedRobot.intakePreferences.maxPieces)
199208
setShowZoneAlways(selectedRobot.intakePreferences.showZoneAlways ?? false)
209+
setAnimationDuration(selectedRobot.intakePreferences.animationDuration ?? 0.5)
200210
} else {
201211
setSelectedNode(undefined)
202212
setShowZoneAlways(false)
213+
setAnimationDuration(0.5)
203214
}
204215
}, [selectedRobot])
205216

@@ -252,12 +263,22 @@ const ConfigureGamepiecePickupInterface: React.FC<ConfigPickupProps> = ({ select
252263
min={MIN_ZONE_SIZE}
253264
max={MAX_ZONE_SIZE}
254265
value={zoneSize}
255-
label="Zone Size"
256-
format={{ minimumFractionDigits: 2, maximumFractionDigits: 2 }}
257-
onChange={(_, vel: number | number[]) => {
258-
setZoneSize(vel as number)
259-
}}
266+
onChange={(_, v) => setZoneSize(typeof v === "number" ? v : v[0])}
260267
step={0.01}
268+
label="Intake Zone Diameter (m)"
269+
/>
270+
<Slider
271+
min={MIN_ANIMATION_DURATION}
272+
max={MAX_ANIMATION_DURATION}
273+
value={animationDuration ?? 0.5}
274+
onChange={(_, v) => {
275+
const val = typeof v === "number" ? v : v[0]
276+
setAnimationDuration(val)
277+
EjectableSceneObject.setAnimationDuration(val)
278+
}}
279+
step={ANIMATION_DURATION_STEP}
280+
label="Intake Animation Duration (s)"
281+
format={{ maximumFractionDigits: 2 }}
261282
/>
262283

263284
{/* Slider for adjusting max pieces the robot can intake */}
@@ -342,6 +363,7 @@ const ConfigureGamepiecePickupInterface: React.FC<ConfigPickupProps> = ({ select
342363
setZoneSize(0.5)
343364
setSelectedNode(selectedRobot?.rootNodeId)
344365
setMaxPieces(selectedRobot.intakePreferences?.maxPieces ?? 1)
366+
setAnimationDuration(0.5)
345367
}}
346368
/>
347369
</>

fission/src/util/debug/DebugPrint.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ export function threeVector3ToString(v: THREE.Vector3, units: number = 3) {
3939
return `(${v.x.toFixed(units)}, ${v.y.toFixed(units)}, ${v.z.toFixed(units)})`
4040
}
4141

42+
export function threeQuaternionToString(v: THREE.Quaternion, units: number = 3) {
43+
return `(${v.x.toFixed(units)}, ${v.y.toFixed(units)}, ${v.z.toFixed(units)}, ${v.w.toFixed(units)})`
44+
}
45+
4246
export function joltVec3ToString(v: Jolt.Vec3 | Jolt.RVec3, units: number = 3) {
4347
return `(${v.GetX().toFixed(units)}, ${v.GetY().toFixed(units)}, ${v.GetZ().toFixed(units)})`
4448
}

0 commit comments

Comments
 (0)