캐릭터 로드
This commit is contained in:
152
Assets/02_Scripts/Editor/CharacterSetupMenu.cs
Normal file
152
Assets/02_Scripts/Editor/CharacterSetupMenu.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user