캐릭터 로드

This commit is contained in:
2026-08-26 03:39:36 +09:00
parent 8d1881749c
commit 7204afae54
31 changed files with 1750 additions and 41 deletions

View File

@@ -0,0 +1,152 @@
using System.Collections.Generic;
using System.Text;
using UnityEditor;
using UnityEngine;
/// <summary>
/// 씬에 배치한 FBX 캐릭터에 필요한 컴포넌트를 한 번에 붙이고, 흔한 함정을 진단한다.
/// 부스에서 받은 VRChat 아바타는 특히 리그 타입과 셰이더에서 걸리는 경우가 많다.
/// </summary>
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<Animator>();
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<SkinnedMeshRenderer>(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<string>();
foreach (var r in go.GetComponentsInChildren<Renderer>(true))
{
foreach (var m in r.sharedMaterials)
{
if (m != null && m.shader != null) shaders.Add(m.shader.name);
}
}
var nonUrp = new List<string>();
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<BlendShapeAvatar>();
if (avatar == null)
{
avatar = Undo.AddComponent<BlendShapeAvatar>(go);
log.AppendLine("+ BlendShapeAvatar 추가 (인스펙터에서 '자동 감지'를 눌러 매핑하세요)");
}
if (go.GetComponent<HeadLookAt>() == null)
{
Undo.AddComponent<HeadLookAt>(go);
log.AppendLine("+ HeadLookAt 추가");
}
// 5) 히트테스트용 콜라이더 — 이게 없으면 클릭 통과가 캐릭터를 인식하지 못한다
var col = go.GetComponent<CapsuleCollider>();
if (col == null)
{
col = Undo.AddComponent<CapsuleCollider>(go);
log.AppendLine("+ CapsuleCollider 추가 (클릭 판정용)");
}
FitCapsule(go, col);
EditorUtility.SetDirty(go);
Debug.Log(log.ToString());
Selection.activeObject = avatar;
}
/// <summary>렌더러 전체를 감싸도록 캡슐 콜라이더 크기를 맞춘다.</summary>
static void FitCapsule(GameObject go, CapsuleCollider col)
{
var renderers = go.GetComponentsInChildren<Renderer>(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;
}
}