feat: add robot movement animation
This commit is contained in:
parent
3c7584cecf
commit
5f19b74d2f
3 changed files with 156 additions and 141 deletions
|
@ -1,10 +1,19 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
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'
|
import { useThrelte, useParent, useTask } from '@threlte/core'
|
||||||
|
|
||||||
export let object: Camera
|
export let object: Group<Object3DEventMap>
|
||||||
export let rotateSpeed = 1.0
|
export let rotateSpeed = 1.0
|
||||||
|
export let shouldOrbit: boolean
|
||||||
|
|
||||||
$: if (object) {
|
$: if (object) {
|
||||||
// console.log(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
|
// This is basically your update function
|
||||||
useTask(delta => {
|
useTask(delta => {
|
||||||
// the object's position is bound to the prop
|
// the object's position is bound to the prop
|
||||||
if (!object) return
|
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
|
// then we calculate our ideal's
|
||||||
const offset = vectorFromObject(idealOffset)
|
const offset = vectorFromObject(idealOffset)
|
||||||
const lookAt = vectorFromObject(idealLookAt)
|
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
|
// and how far we should move towards them
|
||||||
const t = 1.0 - Math.pow(0.001, delta)
|
const t = 1.0 - Math.pow(0.001, delta)
|
||||||
currentPosition.lerp(offset, t)
|
currentPosition.lerp(offset, t)
|
||||||
currentLookAt.lerp(lookAt, t)
|
currentLookAt.lerp(lookAt, t)
|
||||||
|
|
||||||
// then finally set the camera, a bit behind the model
|
// typescript HACKS! never do this! How does this work? who knows!
|
||||||
$camera!.position.copy(currentPosition)
|
const robotPosition = vectorFromObject(
|
||||||
const behindOffset = currentPosition
|
object as unknown as { x: number; y: number; z: number }
|
||||||
.clone()
|
)
|
||||||
|
|
||||||
|
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()
|
.normalize()
|
||||||
.multiplyScalar(8)
|
.multiplyScalar(horizontalOffsetDistance)
|
||||||
.setY(0.5)
|
.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)
|
$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 }) {
|
function vectorFromObject(vec: { x: number; y: number; z: number }) {
|
||||||
const { x, y, z } = vec
|
const { x, y, z } = vec
|
||||||
const ideal = new Vector3(x, y, z)
|
const ideal = new Vector3(x, y, z)
|
||||||
|
@ -147,32 +120,4 @@
|
||||||
)
|
)
|
||||||
return ideal
|
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>
|
</script>
|
||||||
|
|
||||||
<!-- <svelte:window on:keydown={onKeyDown} on:keyup={onKeyUp} /> -->
|
|
||||||
|
|
|
@ -2,22 +2,37 @@
|
||||||
import { T, useTask } from '@threlte/core'
|
import { T, useTask } from '@threlte/core'
|
||||||
import { ContactShadows, Float, Grid, OrbitControls } from '@threlte/extras'
|
import { ContactShadows, Float, Grid, OrbitControls } from '@threlte/extras'
|
||||||
import Hornet from './models/Hornet.svelte'
|
import Hornet from './models/Hornet.svelte'
|
||||||
import { onMount } from 'svelte'
|
|
||||||
import Controls from './Controls.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 {
|
import {
|
||||||
telemetryReadonlyStore,
|
telemetryReadonlyStore,
|
||||||
telemetryStore,
|
telemetryStore,
|
||||||
} from '../../stores/telemetryStore'
|
} from '../../stores/telemetryStore'
|
||||||
import { get } from 'svelte/store'
|
import { get } from 'svelte/store'
|
||||||
|
import { Vector2 } from 'three'
|
||||||
|
import { SmoothMotionController } from './smoothMotionController'
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
|
||||||
// const rotate90 = () => {
|
/* This is the root scene where the robot visualization is built.
|
||||||
// const originalRot = rot
|
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(() => {
|
let shouldOrbit = true
|
||||||
// setTimeout(rotate90, 5000)
|
|
||||||
// })
|
|
||||||
|
|
||||||
// CONSTANTS
|
// CONSTANTS
|
||||||
const maxAngularVelocity = 2 // Max angular velocity, in radians per second
|
const maxAngularVelocity = 2 // Max angular velocity, in radians per second
|
||||||
|
@ -26,23 +41,8 @@
|
||||||
// Proportional control factor
|
// Proportional control factor
|
||||||
const kP = 2 // Adjust this value based on responsiveness and stability needs
|
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
|
// Sync robot orientation with target rotation
|
||||||
let targetRot = 0
|
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)
|
// Updates rotation to match target with PID controller (intended to be invoked in useTask)
|
||||||
let rot = 0 // (initial) rotation in radians
|
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 => {
|
useTask(delta => {
|
||||||
|
if (!shouldOrbit) {
|
||||||
updateRotation(delta)
|
updateRotation(delta)
|
||||||
|
|
||||||
|
controller.update(delta)
|
||||||
|
robotPos.x = controller.getPosition().x
|
||||||
|
robotPos.z = controller.getPosition().y
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let capsule: Group<Object3DEventMap>
|
let capsule: Group<Object3DEventMap>
|
||||||
let capRef: Camera
|
let capRef: Group<Object3DEventMap>
|
||||||
$: if (capsule) {
|
$: if (capsule) {
|
||||||
// typescript hacks because i dont know what im doing
|
capRef = capsule
|
||||||
capRef = capsule as unknown as Camera
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- <T.PerspectiveCamera makeDefault position={[-10, 10, 10]} fov={15}>
|
<T.PerspectiveCamera makeDefault position={[0, 8, -20]} fov={30} on:create>
|
||||||
<OrbitControls
|
<OrbitControls
|
||||||
autoRotate
|
autoRotateSpeed={1.5}
|
||||||
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}
|
|
||||||
target.y={1.5}
|
target.y={1.5}
|
||||||
autoRotate
|
autoRotate
|
||||||
|
enableDamping
|
||||||
/>
|
/>
|
||||||
<Controls bind:object={capRef} />
|
<Controls {shouldOrbit} bind:object={capRef} rotateSpeed={angularVelocity} />
|
||||||
</T.PerspectiveCamera>
|
</T.PerspectiveCamera>
|
||||||
|
|
||||||
<T.DirectionalLight intensity={0.8} position.x={5} position.y={10} />
|
<T.DirectionalLight intensity={0.8} position.x={5} position.y={10} />
|
||||||
|
@ -122,20 +138,20 @@
|
||||||
cellColor="#ffffff"
|
cellColor="#ffffff"
|
||||||
sectionColor="#ffffff"
|
sectionColor="#ffffff"
|
||||||
sectionThickness={0}
|
sectionThickness={0}
|
||||||
fadeDistance={50}
|
fadeDistance={75}
|
||||||
cellSize={2}
|
cellSize={2}
|
||||||
|
infiniteGrid
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ContactShadows scale={10} blur={2} far={2.5} opacity={0.5} />
|
<ContactShadows scale={10} blur={2} far={2.5} opacity={0.5} />
|
||||||
|
|
||||||
<Float floatIntensity={1} floatingRange={[0, 0.5]}>
|
<Float floatIntensity={1} floatingRange={[0, 0.5]}>
|
||||||
<!-- <T.Mesh > -->
|
|
||||||
<Hornet
|
<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]}
|
scale={[0.8, 0.8, 0.8]}
|
||||||
bind:ref={capsule}
|
bind:ref={capsule}
|
||||||
rotation.y={rot}
|
rotation.y={rot}
|
||||||
/>
|
/>
|
||||||
<!-- <T.MeshStandardMaterial color="#F8EBCE" /> -->
|
|
||||||
<!-- </T.Mesh> -->
|
|
||||||
</Float>
|
</Float>
|
||||||
|
|
|
@ -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 }
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue