feat: add robot movement animation

This commit is contained in:
Youwen Wu 2024-02-26 00:59:23 -08:00
parent 3c7584cecf
commit 5f19b74d2f
3 changed files with 156 additions and 141 deletions

View file

@ -1,10 +1,19 @@
<script lang="ts">
import { createEventDispatcher, onDestroy } from 'svelte'
import { Camera, Vector2, Vector3, Quaternion } from 'three'
import {
Camera,
Vector2,
Vector3,
Quaternion,
Object3D,
type Object3DEventMap,
Group,
} from 'three'
import { useThrelte, useParent, useTask } from '@threlte/core'
export let object: Camera
export let object: Group<Object3DEventMap>
export let rotateSpeed = 1.0
export let shouldOrbit: boolean
$: if (object) {
// console.log(object)
@ -55,89 +64,53 @@
)
}
// domElement.addEventListener('pointerdown', onPointerDown)
// domElement.addEventListener('pointermove', onPointerMove)
// domElement.addEventListener('pointerleave', onPointerLeave)
// domElement.addEventListener('pointerup', onPointerUp)
onDestroy(() => {
// domElement.removeEventListener('pointerdown', onPointerDown)
// domElement.removeEventListener('pointermove', onPointerMove)
// domElement.removeEventListener('pointerleave', onPointerLeave)
// domElement.removeEventListener('pointerup', onPointerUp)
})
// This is basically your update function
useTask(delta => {
// the object's position is bound to the prop
if (!object) return
// camera is based on character so we rotation character first
// rotationQuat.setFromAxisAngle(axis, -rotateDelta.x * rotateSpeed * delta)
// object.quaternion.multiply(rotationQuat)
// then we calculate our ideal's
const offset = vectorFromObject(idealOffset)
const lookAt = vectorFromObject(idealLookAt)
// camera is based on character so we rotation character first
// rotationQuat.setFromAxisAngle(axis, rotateSpeed * delta)
// object.quaternion.multiply(rotationQuat)
// and how far we should move towards them
const t = 1.0 - Math.pow(0.001, delta)
currentPosition.lerp(offset, t)
currentLookAt.lerp(lookAt, t)
// then finally set the camera, a bit behind the model
$camera!.position.copy(currentPosition)
const behindOffset = currentPosition
.clone()
// typescript HACKS! never do this! How does this work? who knows!
const robotPosition = vectorFromObject(
object as unknown as { x: number; y: number; z: number }
)
const horizontalOffsetDistance = 12 // Distance behind the leading vector
const direction = new Vector3(0, 0, 1) // Default forward direction in Three.js is negative z-axis, so behind is positive z-axis
const verticalOffset = new Vector3(0, -2.8, 0)
// Calculate the offset vector
const offsetVector = direction
.normalize()
.multiplyScalar(8)
.setY(0.5)
.multiplyScalar(horizontalOffsetDistance)
.add(verticalOffset)
$camera!.position.copy(currentPosition).add(behindOffset)
// If the leading object is rotating, apply its rotation to the offset vector
const rotatedOffsetVector = offsetVector.applyQuaternion(object.quaternion)
// Calculate the trailing vector's position
const trailingVector = robotPosition.clone().sub(rotatedOffsetVector)
if (!shouldOrbit) {
// then finally set the camera, a bit behind the model
$camera!.position.copy(trailingVector)
// Rotate the offset around the Y-axis
$camera!.lookAt(currentLookAt)
}
})
function onPointerMove(event: PointerEvent) {
const { x, y } = event
if (pointerDown && !isOrbiting) {
// calculate distance from init down
const distCheck =
Math.sqrt(
Math.pow(x - rotateStart.x, 2) + Math.pow(y - rotateStart.y, 2)
) > 10
if (distCheck) {
isOrbiting = true
}
}
if (!isOrbiting) return
rotateEnd.set(x, y)
rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(rotateSpeed)
rotateStart.copy(rotateEnd)
invalidate()
dispatch('change')
}
function onPointerDown(event: PointerEvent) {
const { x, y } = event
rotateStart.set(x, y)
pointerDown = true
}
function onPointerUp() {
rotateDelta.set(0, 0)
pointerDown = false
isOrbiting = false
}
function onPointerLeave() {
rotateDelta.set(0, 0)
pointerDown = false
isOrbiting = false
}
function vectorFromObject(vec: { x: number; y: number; z: number }) {
const { x, y, z } = vec
const ideal = new Vector3(x, y, z)
@ -147,32 +120,4 @@
)
return ideal
}
function onKeyDown(event: KeyboardEvent) {
switch (event.key) {
case 'a':
rotateDelta.x = -2 * rotateSpeed
break
case 'd':
rotateDelta.x = 2 * rotateSpeed
break
default:
break
}
}
function onKeyUp(event: KeyboardEvent) {
switch (event.key) {
case 'a':
rotateDelta.x = 0
break
case 'd':
rotateDelta.x = 0
break
default:
break
}
}
</script>
<!-- <svelte:window on:keydown={onKeyDown} on:keyup={onKeyUp} /> -->

View file

@ -2,22 +2,37 @@
import { T, useTask } from '@threlte/core'
import { ContactShadows, Float, Grid, OrbitControls } from '@threlte/extras'
import Hornet from './models/Hornet.svelte'
import { onMount } from 'svelte'
import Controls from './Controls.svelte'
import type { Camera, Group, Object3D, Object3DEventMap } from 'three'
import {
Vector3,
type Camera,
type Group,
type Object3D,
type Object3DEventMap,
} from 'three'
import {
telemetryReadonlyStore,
telemetryStore,
} from '../../stores/telemetryStore'
import { get } from 'svelte/store'
import { Vector2 } from 'three'
import { SmoothMotionController } from './smoothMotionController'
import { onMount } from 'svelte'
// const rotate90 = () => {
// const originalRot = rot
// }
/* This is the root scene where the robot visualization is built.
It renders an infinite grid (it's not actually infinite, but we shouldn't run out
of space in realistic use), and a 3D model of the robot. The camera is locked
to the model, and the model is rotated to match the robot's orientation.
A PID controller is used to smoothly rotate the model to match the robot's
orientation and dampen out jittering. How does it work? Who knows!
75% percent of this was created while reading
https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation,
and rest was generated by AI!
The rest of this codebase is remarkably jank-free, but this visualization module
is the most esoteric and jank code ever written.
*/
// onMount(() => {
// setTimeout(rotate90, 5000)
// })
let shouldOrbit = true
// CONSTANTS
const maxAngularVelocity = 2 // Max angular velocity, in radians per second
@ -26,23 +41,8 @@
// Proportional control factor
const kP = 2 // Adjust this value based on responsiveness and stability needs
// simulate some turning for testing
// const simulateTurning = () => {
// let delay = Math.random() * 4500 + 500
// let randOffset = Math.random() * 170 * (Math.random() < 0.5 ? -1 : 1)
// telemetryStore.update({
// ...get(telemetryReadonlyStore),
// orientation: get(telemetryReadonlyStore)['orientation'] + randOffset,
// })
// setTimeout(simulateTurning, delay)
// }
// simulateTurning()
// Sync robot orientation with target rotation
let targetRot = 0
telemetryReadonlyStore.subscribe(value => {
targetRot = (value['orientation'] * Math.PI) / 180 // convert deg to rad
})
// Updates rotation to match target with PID controller (intended to be invoked in useTask)
let rot = 0 // (initial) rotation in radians
@ -80,38 +80,54 @@
}
}
// Assuming useTask is called every frame with the time delta
let robotPos: Vector3 = new Vector3(0, 0, 0)
const robotPosition = new Vector2(0, 0) // Initial position
const initialVelocity = { x: 0, y: 0 } // Initial velocity
// The smooth motion controller utilizes a cubic hermite spline to interpolate between
// the current simulation velocity and the robot's actual velocity
const controller = new SmoothMotionController(robotPosition, initialVelocity)
onMount(() => {
telemetryReadonlyStore.subscribe(value => {
targetRot = (value['orientation'] * Math.PI) / 180 // convert deg to rad
controller.setTargetVelocity({
x: value['chassis-x-speed'],
y: value['chassis-y-speed'],
})
shouldOrbit = value.gear === 'park' || value.gear === '-999'
if (shouldOrbit) {
robotPos = new Vector3(0, 0, 0)
controller.reset()
}
})
})
useTask(delta => {
if (!shouldOrbit) {
updateRotation(delta)
controller.update(delta)
robotPos.x = controller.getPosition().x
robotPos.z = controller.getPosition().y
}
})
let capsule: Group<Object3DEventMap>
let capRef: Camera
let capRef: Group<Object3DEventMap>
$: if (capsule) {
// typescript hacks because i dont know what im doing
capRef = capsule as unknown as Camera
capRef = capsule
}
</script>
<!-- <T.PerspectiveCamera makeDefault position={[-10, 10, 10]} fov={15}>
<T.PerspectiveCamera makeDefault position={[0, 8, -20]} fov={30} on:create>
<OrbitControls
autoRotate
enableZoom={true}
enableDamping
autoRotateSpeed={0.5}
target.y={1.5}
/>
</T.PerspectiveCamera> -->
<T.PerspectiveCamera makeDefault position={[0, 8, -20]} fov={75} on:create>
<OrbitControls
enableZoom={true}
enableDamping
autoRotateSpeed={5}
autoRotateSpeed={1.5}
target.y={1.5}
autoRotate
enableDamping
/>
<Controls bind:object={capRef} />
<Controls {shouldOrbit} bind:object={capRef} rotateSpeed={angularVelocity} />
</T.PerspectiveCamera>
<T.DirectionalLight intensity={0.8} position.x={5} position.y={10} />
@ -122,20 +138,20 @@
cellColor="#ffffff"
sectionColor="#ffffff"
sectionThickness={0}
fadeDistance={50}
fadeDistance={75}
cellSize={2}
infiniteGrid
/>
<ContactShadows scale={10} blur={2} far={2.5} opacity={0.5} />
<Float floatIntensity={1} floatingRange={[0, 0.5]}>
<!-- <T.Mesh > -->
<Hornet
position={[0, 1.7, 0]}
position.y={1.7}
position.z={robotPos.z}
position.x={robotPos.x}
scale={[0.8, 0.8, 0.8]}
bind:ref={capsule}
rotation.y={rot}
/>
<!-- <T.MeshStandardMaterial color="#F8EBCE" /> -->
<!-- </T.Mesh> -->
</Float>

View file

@ -0,0 +1,54 @@
import { Vector2 } from 'three'
interface Velocity {
x: number
y: number
}
export class SmoothMotionController {
private currentPosition: Vector2
private currentVelocity: Vector2
private targetVelocity: Velocity
private dampingFactor: number
constructor(
initialPosition: Vector2,
initialVelocity: Velocity,
dampingFactor: number = 0.1
) {
this.currentPosition = initialPosition
this.currentVelocity = new Vector2(initialVelocity.x, initialVelocity.y)
this.targetVelocity = { ...initialVelocity }
this.dampingFactor = dampingFactor
}
setTargetVelocity(velocity: Velocity) {
this.targetVelocity = velocity
}
update(delta: number) {
// Apply cubic interpolation to smoothly transition the current velocity towards the target velocity
this.currentVelocity.x +=
(this.targetVelocity.x - this.currentVelocity.x) *
this.dampingFactor *
delta
this.currentVelocity.y +=
(this.targetVelocity.y - this.currentVelocity.y) *
this.dampingFactor *
delta
// Update position based on the current velocity and the time delta
this.currentPosition.x += this.currentVelocity.x * delta * 3
this.currentPosition.y += this.currentVelocity.y * delta * 3
}
getPosition(): Vector2 {
return this.currentPosition
}
public reset() {
this.currentPosition = new Vector2(0, 0)
this.currentVelocity = new Vector2(0, 0)
this.targetVelocity = { x: 0, y: 0 }
}
}