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_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}

View File

@@ -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;
float voicelineDuration = rmsCurve.Length * FRAME_DURATION;
Invoke(nameof(StopSpeaking), voicelineDuration);
}
if (mouth != null)
{
Vector3 s = mouth.localScale;
s.y = minScaleY;
mouth.localScale = s;
}
}
return FMOD.RESULT.OK;
});
}
// ---------------------------
// 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))