using UnityEngine; /// /// 카메라를 직교 투영으로 바꾸고, 불러온 캐릭터 크기에 맞춰 자동으로 구도를 잡는다. /// /// 왜 직교 투영인가: /// 원근 투영에서는 화면 중앙을 벗어날수록 캐릭터를 비스듬히 옆에서 보게 되어 /// 형태가 왜곡된다. 데스크톱 캐릭터는 화면 어디로든 이동하므로 이 왜곡이 그대로 /// 드러난다. 직교 투영은 위치와 무관하게 항상 같은 각도로 보인다. /// /// 왜 자동인가: /// 사용자가 임의의 VRM 을 넣는 구조라 캐릭터 키를 미리 알 수 없다. 모델마다 /// 직교 크기를 손으로 맞추면 모델을 바꿀 때마다 다시 맞춰야 한다. /// public class CharacterCameraFit : MonoBehaviour { [Header("참조")] [Tooltip("비우면 같은 오브젝트의 Camera 또는 Camera.main")] [SerializeField] Camera targetCamera; [Tooltip("비우면 씬에서 탐색")] [SerializeField] VrmCharacterLoader loader; [Header("구도")] [Tooltip("직교 투영으로 강제한다. 끄면 현재 투영 방식을 유지한 채 크기만 맞춘다")] [SerializeField] bool forceOrthographic = true; [Tooltip("캐릭터가 화면 높이에서 차지할 비율. 0.30면 화면의 30%")] [Range(0.1f, 1f)] [SerializeField] float screenHeightFraction = 0.30f; [Tooltip("캐릭터가 처음에 화면 중앙에 오도록 카메라 위치도 맞춘다")] [SerializeField] bool centerOnCharacter = true; void Awake() { if (targetCamera == null) targetCamera = GetComponent(); if (targetCamera == null) targetCamera = Camera.main; if (loader == null) loader = FindFirstObjectByType(); if (loader != null) loader.Loaded += OnCharacterLoaded; } void OnDestroy() { if (loader != null) loader.Loaded -= OnCharacterLoaded; } void OnCharacterLoaded(ICharacterAvatar avatar) { if (avatar != null) Fit(avatar.Root); } /// 캐릭터를 감싸도록 카메라 크기와 위치를 맞춘다. public void Fit(Transform character) { if (targetCamera == null || character == null) return; var renderers = character.GetComponentsInChildren(true); if (renderers.Length == 0) { Debug.LogWarning("[CharacterCameraFit] 렌더러가 없어 구도를 잡지 못했습니다."); return; } Bounds bounds = renderers[0].bounds; for (int i = 1; i < renderers.Length; i++) bounds.Encapsulate(renderers[i].bounds); float characterHeight = Mathf.Max(0.01f, bounds.size.y); if (forceOrthographic) targetCamera.orthographic = true; if (targetCamera.orthographic) { // orthographicSize 는 화면에 보이는 세계 높이의 절반이다. // 보이는 높이 = 캐릭터 키 / 비율 이므로, 그 절반이 필요한 값이다. targetCamera.orthographicSize = characterHeight / (2f * screenHeightFraction); } else { // 원근이면 거리로 맞춘다. 왜곡은 남으므로 권장하지 않는다. float visibleHeight = characterHeight / screenHeightFraction; float halfFovRad = targetCamera.fieldOfView * 0.5f * Mathf.Deg2Rad; float distance = visibleHeight * 0.5f / Mathf.Tan(halfFovRad); Vector3 dir = targetCamera.transform.forward; targetCamera.transform.position = bounds.center - dir * distance; } if (centerOnCharacter) { // 카메라의 시선 방향(깊이)은 유지하고, 화면상 중앙만 캐릭터에 맞춘다. Vector3 camPos = targetCamera.transform.position; Vector3 toCharacter = bounds.center - camPos; Vector3 forward = targetCamera.transform.forward; // 깊이 성분만 남기고 나머지를 상쇄해 캐릭터가 화면 중앙에 오게 한다. Vector3 depthOnly = Vector3.Project(toCharacter, forward); targetCamera.transform.position = bounds.center - depthOnly; } // 직교 투영에서 near 가 캐릭터보다 앞에 있으면 잘려 보인다. if (targetCamera.orthographic && targetCamera.nearClipPlane > 0.05f) { targetCamera.nearClipPlane = 0.05f; } Debug.Log($"[CharacterCameraFit] 구도 적용. 캐릭터 키 {characterHeight:F2}m, " + $"직교={targetCamera.orthographic}, size={targetCamera.orthographicSize:F2}"); } }