110 lines
4.1 KiB
C#
110 lines
4.1 KiB
C#
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<CharacterVoiceObject>();
|
|
_audioSource = voiceObj != null ? voiceObj.VoiceSource : null;
|
|
|
|
// 메시 자동 탐색 — 첫 번째 셰이프 이름을 가진 SkinnedMeshRenderer 사용
|
|
if (_meshRenderer == null && _shapes.Length > 0)
|
|
{
|
|
string probe = _shapes[0].Name;
|
|
foreach (var smr in GetComponentsInChildren<SkinnedMeshRenderer>(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);
|
|
}
|
|
}
|
|
}
|