Feat: Avatar lebendiger — prozedurale Idle-Bewegung (Atmen, Wiegen, Kopf, Sprech-Nicken)
Über die Ruhepose gelegte Sinus-Bewegung auf Spine/Chest/Hips/Neck/Head/Arme; beim Sprechen (Audiopegel) nickt der Kopf stärker. Kein Animations-File. Frontend neu gebaut (dist). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
-3956
File diff suppressed because one or more lines are too long
+3922
File diff suppressed because one or more lines are too long
-564
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+5454
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -7,8 +7,8 @@
|
|||||||
<link rel="manifest" href="/manifest.webmanifest" />
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<title>Mission Control 2.0</title>
|
<title>Mission Control 2.0</title>
|
||||||
<script type="module" crossorigin src="/assets/index-BNlNRfcu.js"></script>
|
<script type="module" crossorigin src="/assets/index-eeooup-h.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-DuS2cUe8.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DueAT_Qc.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -11,21 +11,49 @@ import type { Emotion } from "@/lib/voice/sentiment"
|
|||||||
type LevelRef = MutableRefObject<{ current: number }> // audioLevel.current.current = Pegel 0..1
|
type LevelRef = MutableRefObject<{ current: number }> // audioLevel.current.current = Pegel 0..1
|
||||||
type EmotionRef = MutableRefObject<Emotion>
|
type EmotionRef = MutableRefObject<Emotion>
|
||||||
|
|
||||||
// VRMs laden in T-Pose (Bindepose, Arme waagerecht). Wir senken Ober-/Unterarme auf den
|
// Ruhepose (A-Pose) als Basis — VRMs laden sonst in T-Pose (Arme waagerecht).
|
||||||
// normalisierten Humanoid-Knoten zu einer ruhigen A-Pose ab — sieht sofort natürlich aus.
|
const REST: Record<string, [number, number, number]> = {
|
||||||
// (Echte Idle-Animation wäre die spätere Stufe.)
|
leftUpperArm: [0, 0, 1.2],
|
||||||
|
rightUpperArm: [0, 0, -1.2],
|
||||||
|
leftLowerArm: [0, -0.2, 0],
|
||||||
|
rightLowerArm: [0, 0.2, 0],
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBone(vrm: VRM, name: string, x: number, y: number, z: number) {
|
||||||
|
const b = vrm.humanoid?.getNormalizedBoneNode(name as any)
|
||||||
|
if (b) b.rotation.set(x, y, z)
|
||||||
|
}
|
||||||
|
|
||||||
function applyRestPose(vrm: VRM) {
|
function applyRestPose(vrm: VRM) {
|
||||||
const set = (name: any, x: number, y: number, z: number) => {
|
for (const [name, r] of Object.entries(REST)) setBone(vrm, name, r[0], r[1], r[2])
|
||||||
const b = vrm.humanoid?.getNormalizedBoneNode(name)
|
|
||||||
if (b) b.rotation.set(x, y, z)
|
|
||||||
}
|
|
||||||
set("leftUpperArm", 0, 0, 1.2) // Arm runter an die Seite
|
|
||||||
set("rightUpperArm", 0, 0, -1.2)
|
|
||||||
set("leftLowerArm", 0, -0.2, 0) // leichte Beugung
|
|
||||||
set("rightLowerArm", 0, 0.2, 0)
|
|
||||||
vrm.humanoid?.update()
|
vrm.humanoid?.update()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lebendige Idle-Animation: prozedural (kein Animations-File). Atmung, langsames Wiegen,
|
||||||
|
// Kopf-/Blickbewegung und Arm-Pendeln werden überlagert; beim Sprechen (level) nickt der Kopf
|
||||||
|
// stärker, damit der Avatar „mitgeht". Wird jede Frame über die Ruhepose gelegt.
|
||||||
|
function applyIdle(vrm: VRM, t: number, level: number) {
|
||||||
|
const breathe = Math.sin(t * 1.7) // ~0.27 Hz Atmung
|
||||||
|
const sway = Math.sin(t * 0.5)
|
||||||
|
const emphasis = Math.min(1, level * 1.4)
|
||||||
|
|
||||||
|
// Rumpf — Atmung + sanftes Wiegen
|
||||||
|
setBone(vrm, "spine", breathe * 0.025, sway * 0.03, Math.sin(t * 0.43) * 0.015)
|
||||||
|
setBone(vrm, "chest", breathe * 0.02, sway * 0.02, 0)
|
||||||
|
setBone(vrm, "hips", 0, Math.sin(t * 0.37) * 0.02, Math.sin(t * 0.29) * 0.01)
|
||||||
|
|
||||||
|
// Kopf/Hals — langsamer Blick + Atmung + Sprech-Nicken
|
||||||
|
setBone(vrm, "neck", Math.sin(t * 0.8) * 0.03 + emphasis * 0.06, Math.sin(t * 0.31) * 0.05, 0)
|
||||||
|
setBone(vrm, "head", Math.sin(t * 0.6) * 0.025 + emphasis * 0.05, Math.sin(t * 0.27) * 0.06, Math.sin(t * 0.5) * 0.02)
|
||||||
|
|
||||||
|
// Arme — Ruhepose + leichtes Pendeln
|
||||||
|
const arm = Math.sin(t * 0.8) * 0.04
|
||||||
|
setBone(vrm, "leftUpperArm", 0, 0, 1.2 + arm)
|
||||||
|
setBone(vrm, "rightUpperArm", 0, 0, -1.2 - arm)
|
||||||
|
setBone(vrm, "leftLowerArm", 0, -0.2 - Math.sin(t * 0.8) * 0.03, 0)
|
||||||
|
setBone(vrm, "rightLowerArm", 0, 0.2 + Math.sin(t * 0.8) * 0.03, 0)
|
||||||
|
}
|
||||||
|
|
||||||
const EXPRESSIONS = ["happy", "angry", "sad", "surprised", "relaxed"] as const
|
const EXPRESSIONS = ["happy", "angry", "sad", "surprised", "relaxed"] as const
|
||||||
const EMO_TO_EXPR: Record<Emotion, string | null> = {
|
const EMO_TO_EXPR: Record<Emotion, string | null> = {
|
||||||
neutral: null, happy: "happy", angry: "angry", sad: "sad", surprised: "surprised", relaxed: "relaxed",
|
neutral: null, happy: "happy", angry: "angry", sad: "sad", surprised: "surprised", relaxed: "relaxed",
|
||||||
@@ -66,12 +94,16 @@ function VrmModel({ url, audioLevel, emotion, onError }: {
|
|||||||
}
|
}
|
||||||
}, [url, onError])
|
}, [url, onError])
|
||||||
|
|
||||||
useFrame((_, delta) => {
|
useFrame((state, delta) => {
|
||||||
if (!vrm) return
|
if (!vrm) return
|
||||||
|
const target = audioLevel.current?.current ?? 0
|
||||||
|
|
||||||
|
// Lebendige Idle-/Sprech-Bewegung (über die Ruhepose gelegt).
|
||||||
|
applyIdle(vrm, state.clock.elapsedTime, target)
|
||||||
|
|
||||||
const em = vrm.expressionManager
|
const em = vrm.expressionManager
|
||||||
if (em) {
|
if (em) {
|
||||||
// Lippensync: 'aa' folgt geglättet dem Audiopegel.
|
// Lippensync: 'aa' folgt geglättet dem Audiopegel.
|
||||||
const target = audioLevel.current?.current ?? 0
|
|
||||||
const aa = (smooth.current.aa ?? 0) * 0.4 + target * 0.6
|
const aa = (smooth.current.aa ?? 0) * 0.4 + target * 0.6
|
||||||
smooth.current.aa = aa
|
smooth.current.aa = aa
|
||||||
em.setValue("aa", aa)
|
em.setValue("aa", aa)
|
||||||
|
|||||||
Reference in New Issue
Block a user