1
0
forked from cgvr/DeltaVR

complete animating mouth scale based on amplitude timeline

This commit is contained in:
2026-02-03 12:26:31 +02:00
parent 5a3f566541
commit 3bae534a61
4 changed files with 47 additions and 43 deletions

View File

@@ -196,15 +196,20 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 8a215290e2c2e1d43983b61cb160e241, type: 3} m_Script: {fileID: 11500000, guid: 8a215290e2c2e1d43983b61cb160e241, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
mouthTransform: {fileID: 5759406807219530703} voicelinesFolder: CharacterVoicelines
mouthScalingMultiplier: 2.5 characterSpecificFolder: Chef
mouthMovementDuration: 0.25
voiceLineKeys: voiceLineKeys:
- Chef/Ulrich_Serve - Ulrich_Serve
- Chef/Ulrich_Get_Correctly - Ulrich_Get_Correctly
- Chef/Ulrich_Excellent - Ulrich_Excellent
- Chef/Ulrich_Terribly_Sorry_1 - Ulrich_Terribly_Sorry_1
- Chef/Ulrich_Enjoy - Ulrich_Enjoy
mouth: {fileID: 5759406807219530703}
minScaleY: 0.3
maxScaleY: 1
gain: 30
attack: 0.6
release: 0.2
fmodWhisperBridge: {fileID: 0} fmodWhisperBridge: {fileID: 0}
notepadText: {fileID: 0} notepadText: {fileID: 0}
notepad: {fileID: 0} notepad: {fileID: 0}

View File

@@ -335,7 +335,7 @@ public class AudioManager : MonoBehaviour
} }
instance.start(); instance.start();
// instance.release(); //instance.release();
return instance; return instance;
} }

View File

@@ -7,6 +7,9 @@ public abstract class NPCController : MonoBehaviour
{ {
protected Transform playerTransform; protected Transform playerTransform;
[Header("Voiceline Amplitude Timeline Config")]
public string voicelinesFolder = "CharacterVoicelines";
public string characterSpecificFolder;
public string[] voiceLineKeys; public string[] voiceLineKeys;
@@ -20,17 +23,14 @@ public abstract class NPCController : MonoBehaviour
public float attack = 0.6f; // faster opening public float attack = 0.6f; // faster opening
public float release = 0.2f; // slower closing public float release = 0.2f; // slower closing
[Header("Timeline Folder (StreamingAssets recommended)")]
public string timelineFolder = "CharacterVoicelines";
private float[] rmsCurve; private float[] rmsCurve;
private EventInstance currentVoicelineEvent; private EventInstance currentVoicelineEvent;
private bool isSpeaking; private bool isSpeaking;
private float smoothed; private float smoothed;
private int sampleRate = 50; // RMS samples per second (20ms windows)
// If you change RMS window in Python, update this // 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 // Start is called before the first frame update
@@ -62,7 +62,6 @@ public abstract class NPCController : MonoBehaviour
private void AnimateMouth() private void AnimateMouth()
{ {
// get FMOD timeline position // get FMOD timeline position
currentVoicelineEvent.getTimelinePosition(out int ms); currentVoicelineEvent.getTimelinePosition(out int ms);
float time = ms / 1000f; 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) private void OnTriggerEnter(Collider other)
{ {
if (other.gameObject.tag == "Player Head") if (other.gameObject.tag == "Player Head")
@@ -120,45 +136,28 @@ public abstract class NPCController : MonoBehaviour
} }
string key = voiceLineKeys[voiceLineId]; string key = voiceLineKeys[voiceLineId];
LoadCurve(key); // load RMS data 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()) if (!currentVoicelineEvent.isValid())
{ {
Debug.LogError("Failed to start dialogue event."); Debug.LogError("Failed to start dialogue event.");
return; return;
} }
isSpeaking = true;
//isSpeaking = true;
// Stop mouth on end // Stop mouth on end
currentVoicelineEvent.setCallback((type, inst, param) => float voicelineDuration = rmsCurve.Length * FRAME_DURATION;
{ Invoke(nameof(StopSpeaking), voicelineDuration);
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;
});
} }
// ---------------------------
// Load RMS Timeline (.txt) // ---------------------------
// --------------------------- // Load RMS Timeline (.txt)
// ---------------------------
private void LoadCurve(string key) 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"); string filePath = Path.Combine(folderPath, key + ".txt");
if (!File.Exists(filePath)) if (!File.Exists(filePath))