From 3bae534a6172895e9d0a74049419ef1f1b2e4273 Mon Sep 17 00:00:00 2001 From: henrisel Date: Tue, 3 Feb 2026 12:26:31 +0200 Subject: [PATCH] complete animating mouth scale based on amplitude timeline --- .../Characters/CafeWaiterNPC.prefab | 21 ++++--- .../_PROJECT/Scenes/DeltaBuilding_base.unity | 4 +- Assets/_PROJECT/Scripts/Audio/AudioManager.cs | 2 +- .../ModeGeneration/NPCs/NPCController.cs | 63 +++++++++---------- 4 files changed, 47 insertions(+), 43 deletions(-) diff --git a/Assets/_PROJECT/Prefabs/ModelGeneration/Characters/CafeWaiterNPC.prefab b/Assets/_PROJECT/Prefabs/ModelGeneration/Characters/CafeWaiterNPC.prefab index 36b4dde7..a8710d3e 100644 --- a/Assets/_PROJECT/Prefabs/ModelGeneration/Characters/CafeWaiterNPC.prefab +++ b/Assets/_PROJECT/Prefabs/ModelGeneration/Characters/CafeWaiterNPC.prefab @@ -196,15 +196,20 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8a215290e2c2e1d43983b61cb160e241, type: 3} m_Name: m_EditorClassIdentifier: - mouthTransform: {fileID: 5759406807219530703} - mouthScalingMultiplier: 2.5 - mouthMovementDuration: 0.25 + voicelinesFolder: CharacterVoicelines + characterSpecificFolder: Chef voiceLineKeys: - - Chef/Ulrich_Serve - - Chef/Ulrich_Get_Correctly - - Chef/Ulrich_Excellent - - Chef/Ulrich_Terribly_Sorry_1 - - Chef/Ulrich_Enjoy + - Ulrich_Serve + - Ulrich_Get_Correctly + - Ulrich_Excellent + - Ulrich_Terribly_Sorry_1 + - Ulrich_Enjoy + mouth: {fileID: 5759406807219530703} + minScaleY: 0.3 + maxScaleY: 1 + gain: 30 + attack: 0.6 + release: 0.2 fmodWhisperBridge: {fileID: 0} notepadText: {fileID: 0} notepad: {fileID: 0} diff --git a/Assets/_PROJECT/Scenes/DeltaBuilding_base.unity b/Assets/_PROJECT/Scenes/DeltaBuilding_base.unity index 9db4e308..d533c37b 100644 --- a/Assets/_PROJECT/Scenes/DeltaBuilding_base.unity +++ b/Assets/_PROJECT/Scenes/DeltaBuilding_base.unity @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20979ab09f46bdc8e3496951948e448188fd44271391e669a216f4f96d49006b -size 67958334 +oid sha256:c6a3626cb930a2243ce5bd952c3eb0c32c0140a05ac2f6ef7a4f2041ec625f75 +size 67958419 diff --git a/Assets/_PROJECT/Scripts/Audio/AudioManager.cs b/Assets/_PROJECT/Scripts/Audio/AudioManager.cs index ccc95a77..45810631 100644 --- a/Assets/_PROJECT/Scripts/Audio/AudioManager.cs +++ b/Assets/_PROJECT/Scripts/Audio/AudioManager.cs @@ -335,7 +335,7 @@ public class AudioManager : MonoBehaviour } instance.start(); - // instance.release(); + //instance.release(); return instance; } diff --git a/Assets/_PROJECT/Scripts/ModeGeneration/NPCs/NPCController.cs b/Assets/_PROJECT/Scripts/ModeGeneration/NPCs/NPCController.cs index 9782c4af..595d42c6 100644 --- a/Assets/_PROJECT/Scripts/ModeGeneration/NPCs/NPCController.cs +++ b/Assets/_PROJECT/Scripts/ModeGeneration/NPCs/NPCController.cs @@ -7,6 +7,9 @@ public abstract class NPCController : MonoBehaviour { protected Transform playerTransform; + [Header("Voiceline Amplitude Timeline Config")] + public string voicelinesFolder = "CharacterVoicelines"; + public string characterSpecificFolder; public string[] voiceLineKeys; @@ -20,17 +23,14 @@ public abstract class NPCController : MonoBehaviour public float attack = 0.6f; // faster opening public float release = 0.2f; // slower closing - [Header("Timeline Folder (StreamingAssets recommended)")] - public string timelineFolder = "CharacterVoicelines"; - private float[] rmsCurve; private EventInstance currentVoicelineEvent; private bool isSpeaking; private float smoothed; - private int sampleRate = 50; // RMS samples per second (20ms windows) // If you change RMS window in Python, update this - + private const float FRAME_DURATION = 0.02f; + private float sampleRate = 1f / FRAME_DURATION; // RMS samples per second (20ms windows) // Start is called before the first frame update @@ -62,7 +62,6 @@ public abstract class NPCController : MonoBehaviour private void AnimateMouth() { - // get FMOD timeline position currentVoicelineEvent.getTimelinePosition(out int ms); float time = ms / 1000f; @@ -88,6 +87,23 @@ public abstract class NPCController : MonoBehaviour } + + private void StopSpeaking() + { + isSpeaking = false; + smoothed = minScaleY; + + if (mouth != null) + { + var scale = mouth.localScale; + scale.y = minScaleY; + mouth.localScale = scale; + } + + currentVoicelineEvent.release(); + } + + private void OnTriggerEnter(Collider other) { if (other.gameObject.tag == "Player Head") @@ -120,45 +136,28 @@ public abstract class NPCController : MonoBehaviour } string key = voiceLineKeys[voiceLineId]; - LoadCurve(key); // load RMS data - Debug.Log("loaded timeline curve"); - - currentVoicelineEvent = AudioManager.Instance.PlayDialogue(key, gameObject); + currentVoicelineEvent = AudioManager.Instance.PlayDialogue(characterSpecificFolder + "/" + key, gameObject); if (!currentVoicelineEvent.isValid()) { Debug.LogError("Failed to start dialogue event."); return; } - - //isSpeaking = true; + isSpeaking = true; // Stop mouth on end - currentVoicelineEvent.setCallback((type, inst, param) => - { - if (type == EVENT_CALLBACK_TYPE.STOPPED) - { - isSpeaking = false; - smoothed = minScaleY; - - if (mouth != null) - { - Vector3 s = mouth.localScale; - s.y = minScaleY; - mouth.localScale = s; - } - } - return FMOD.RESULT.OK; - }); + float voicelineDuration = rmsCurve.Length * FRAME_DURATION; + Invoke(nameof(StopSpeaking), voicelineDuration); } -// --------------------------- -// Load RMS Timeline (.txt) -// --------------------------- + + // --------------------------- + // Load RMS Timeline (.txt) + // --------------------------- private void LoadCurve(string key) { - string folderPath = Path.Combine(Application.streamingAssetsPath, timelineFolder); + string folderPath = Path.Combine(Application.streamingAssetsPath, voicelinesFolder, characterSpecificFolder); string filePath = Path.Combine(folderPath, key + ".txt"); if (!File.Exists(filePath))