2026-04-27 캐릭터 립싱크 프로토타입

This commit is contained in:
2026-04-27 10:48:22 +09:00
parent 5d9418cf97
commit 6c6cf63668
11 changed files with 143 additions and 10 deletions

View File

@@ -60,12 +60,12 @@
"*.asset": "yaml",
"*.meta": "yaml",
"*.prefab": "yaml",
"*.unity": "yaml",
"*.unity": "yaml"
},
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": {
"*.sln": "*.csproj",
"*.slnx": "*.csproj"
},
"dotnet.defaultSolution": "VR_MyProject.slnx"
"dotnet.defaultSolution": "Shopping_UnityVR.slnx"
}

Binary file not shown.

View File

@@ -0,0 +1,109 @@
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);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 160aeff980f9a0546a56b85727e287f8

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c8d0283ff08da174784f1ec8827f28ec
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 7400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 410d5556fb8a2524a93097611687d170
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 7400000
userData:
assetBundleName:
assetBundleVariant: