using System.Collections.Generic; using System.Text; using UnityEditor; using UnityEngine; /// /// 씬에 배치한 FBX 캐릭터에 필요한 컴포넌트를 한 번에 붙이고, 흔한 함정을 진단한다. /// 부스에서 받은 VRChat 아바타는 특히 리그 타입과 셰이더에서 걸리는 경우가 많다. /// public static class CharacterSetupMenu { [MenuItem("Tools/Desktop Overlay/5. Setup Selected FBX As Character")] public static void SetupCharacter() { var go = Selection.activeGameObject; if (go == null || EditorUtility.IsPersistent(go)) { EditorUtility.DisplayDialog("캐릭터 셋업", "씬에 배치된 캐릭터 오브젝트를 선택한 뒤 실행하세요.\n" + "(프로젝트 창의 FBX 에셋이 아니라, 씬으로 끌어다 놓은 인스턴스입니다)", "확인"); return; } var log = new StringBuilder(); log.AppendLine($"[캐릭터 셋업] {go.name}"); log.AppendLine(); Undo.RegisterFullObjectHierarchyUndo(go, "Setup Character"); // 1) 휴머노이드 리그 확인 — 시선 추적과 애니메이션의 전제 var animator = go.GetComponentInChildren(); if (animator == null) { log.AppendLine("✗ Animator 가 없습니다."); log.AppendLine(" FBX 임포트 설정 > Rig > Animation Type = Humanoid 로 바꾸고 Apply 하세요."); } else if (!animator.isHuman) { log.AppendLine("✗ Animator 는 있지만 휴머노이드가 아닙니다."); log.AppendLine(" FBX 임포트 설정 > Rig > Animation Type = Humanoid 로 바꾸고 Apply 하세요."); log.AppendLine(" (시선 추적이 머리 본을 찾지 못합니다)"); } else { log.AppendLine("✓ 휴머노이드 리그 확인"); } // 2) 블렌드셰이프 확인 — 표정/립싱크의 전제 int shapeCount = 0; var renderers = go.GetComponentsInChildren(true); foreach (var r in renderers) { if (r.sharedMesh != null) shapeCount += r.sharedMesh.blendShapeCount; } if (shapeCount == 0) { log.AppendLine("✗ 블렌드셰이프가 하나도 없습니다. 표정과 립싱크가 불가능합니다."); log.AppendLine(" FBX 임포트 설정 > Model > Import BlendShapes 가 켜져 있는지 확인하세요."); } else { log.AppendLine($"✓ 블렌드셰이프 {shapeCount}개 ({renderers.Length}개 메시)"); } // 3) 셰이더 진단 — VRChat 아바타는 Built-in RP 용 셰이더를 쓰므로 URP 에서 분홍색이 된다 var shaders = new HashSet(); foreach (var r in go.GetComponentsInChildren(true)) { foreach (var m in r.sharedMaterials) { if (m != null && m.shader != null) shaders.Add(m.shader.name); } } var nonUrp = new List(); foreach (var s in shaders) { if (!s.StartsWith("Universal Render Pipeline/") && !s.StartsWith("Shader Graphs/") && !s.StartsWith("VRM10/Universal Render Pipeline/")) { nonUrp.Add(s); } } if (nonUrp.Count > 0) { log.AppendLine($"⚠ URP 가 아닌 셰이더 {nonUrp.Count}종 — 분홍색으로 보일 수 있습니다:"); foreach (var s in nonUrp) log.AppendLine(" " + s); log.AppendLine(" 해결: Window > Rendering > Render Pipeline Converter 로 일괄 변환하거나,"); log.AppendLine(" 머티리얼 셰이더를 Universal Render Pipeline/Lit 으로 직접 바꾸세요."); } else if (shaders.Count > 0) { log.AppendLine("✓ 모든 셰이더가 URP 호환"); } // 4) 컴포넌트 부착 var avatar = go.GetComponent(); if (avatar == null) { avatar = Undo.AddComponent(go); log.AppendLine("+ BlendShapeAvatar 추가 (인스펙터에서 '자동 감지'를 눌러 매핑하세요)"); } if (go.GetComponent() == null) { Undo.AddComponent(go); log.AppendLine("+ HeadLookAt 추가"); } // 5) 히트테스트용 콜라이더 — 이게 없으면 클릭 통과가 캐릭터를 인식하지 못한다 var col = go.GetComponent(); if (col == null) { col = Undo.AddComponent(go); log.AppendLine("+ CapsuleCollider 추가 (클릭 판정용)"); } FitCapsule(go, col); EditorUtility.SetDirty(go); Debug.Log(log.ToString()); Selection.activeObject = avatar; } /// 렌더러 전체를 감싸도록 캡슐 콜라이더 크기를 맞춘다. static void FitCapsule(GameObject go, CapsuleCollider col) { var renderers = go.GetComponentsInChildren(true); if (renderers.Length == 0) return; Bounds world = renderers[0].bounds; for (int i = 1; i < renderers.Length; i++) world.Encapsulate(renderers[i].bounds); Vector3 localCenter = go.transform.InverseTransformPoint(world.center); Vector3 scale = go.transform.lossyScale; // lossyScale 로 나눠 로컬 크기로 환산 (0 나누기 방지) float sx = Mathf.Approximately(scale.x, 0f) ? 1f : Mathf.Abs(scale.x); float sy = Mathf.Approximately(scale.y, 0f) ? 1f : Mathf.Abs(scale.y); float sz = Mathf.Approximately(scale.z, 0f) ? 1f : Mathf.Abs(scale.z); col.direction = 1; // Y 축 col.center = localCenter; col.height = world.size.y / sy; col.radius = Mathf.Max(world.size.x / sx, world.size.z / sz) * 0.5f; col.isTrigger = false; } }