using System; using UnityEngine; // 보이스 진폭에 따라 입 관련 블렌드셰이프 그룹의 weight를 직접 제어 // LateUpdate에서 갱신해 Animator가 같은 프레임에 0으로 세팅한 값을 덮어씀 [RequireComponent(typeof(CharacterVoiceObject))] public class LipSync : MonoBehaviour { [Serializable] private struct LipShape { public string Name; [Range(0f, 100f)] public float MaxWeight; // amplitude=1일 때 도달할 weight } [Header("Refs")] [SerializeField] private SkinnedMeshRenderer _meshRenderer; // BMAC_OpenMouse_Big 클립의 입 관련 셰이프 프리셋 [Header("Mouth Preset (입 최대 시 weight)")] [SerializeField] private LipShape[] _shapes = { new() { Name = "Expression_SurpriesedMouth", MaxWeight = 50f }, new() { Name = "Expression_MouthSad_L", MaxWeight = 10f }, new() { Name = "Expression_MouthSad_R", MaxWeight = 10f }, new() { Name = "Expression_MouthWide_L", MaxWeight = 30f }, new() { Name = "Expression_MouthWide_R", MaxWeight = 30f }, new() { Name = "Expression_LipsOh", MaxWeight = 100f }, new() { Name = "Expression_LipsO", MaxWeight = 5f }, }; [Header("Tuning")] [SerializeField, Range(0f, 20f)] private float _amplitudeScale = 6f; // RMS → 0~1 매핑 배수 [SerializeField, Range(0f, 0.05f)] private float _noiseFloor = 0.005f; [SerializeField, Range(0f, 30f)] private float _smoothingSpeed = 15f; [SerializeField] private int _sampleSize = 256; private AudioSource _audioSource; private int[] _indices; private float[] _sampleBuffer; private float _currentAmplitude; private void Awake() { var voiceObj = GetComponent(); _audioSource = voiceObj != null ? voiceObj.VoiceSource : null; // 메시 자동 탐색 — 첫 번째 셰이프 이름을 가진 SkinnedMeshRenderer 사용 if (_meshRenderer == null && _shapes.Length > 0) { string probe = _shapes[0].Name; foreach (var smr in GetComponentsInChildren(true)) { if (smr.sharedMesh != null && smr.sharedMesh.GetBlendShapeIndex(probe) >= 0) { _meshRenderer = smr; break; } } } // 인덱스 캐시 _indices = new int[_shapes.Length]; if (_meshRenderer != null && _meshRenderer.sharedMesh != null) { var mesh = _meshRenderer.sharedMesh; for (int i = 0; i < _shapes.Length; i++) { _indices[i] = mesh.GetBlendShapeIndex(_shapes[i].Name); if (_indices[i] < 0) Debug.LogWarning($"[LipSync] 블렌드셰이프 없음: {_shapes[i].Name}", this); } } else { for (int i = 0; i < _indices.Length; i++) _indices[i] = -1; } if (_audioSource == null) Debug.LogWarning("[LipSync] CharacterVoiceObject.VoiceSource 미할당", this); _sampleBuffer = new float[_sampleSize]; } private void LateUpdate() { if (_audioSource == null || _meshRenderer == null) return; // PlayOneShot도 잡히도록 항상 샘플링 — 무음은 노이즈 플로어로 컷 _audioSource.GetOutputData(_sampleBuffer, 0); float sumSq = 0f; for (int i = 0; i < _sampleBuffer.Length; i++) sumSq += _sampleBuffer[i] * _sampleBuffer[i]; float rms = Mathf.Sqrt(sumSq / _sampleBuffer.Length); rms = Mathf.Max(0f, rms - _noiseFloor); float target = Mathf.Clamp01(rms * _amplitudeScale); _currentAmplitude = Mathf.Lerp(_currentAmplitude, target, Time.deltaTime * _smoothingSpeed); // Animator가 같은 프레임에 0으로 덮은 값을 LateUpdate에서 다시 씌움 for (int i = 0; i < _shapes.Length; i++) { if (_indices[i] < 0) continue; _meshRenderer.SetBlendShapeWeight(_indices[i], _currentAmplitude * _shapes[i].MaxWeight); } } }