using UnityEngine; /// /// 발판 아래에 깊이만 기록하는 사각형을 깔아 캐릭터를 가린다. /// "발판(=실제 창) 뒤에 서 있는" 그림을 만들 때 쓴다. 예: 창 밑에서 머리만 내미는 포즈. /// /// 왜 렌더 순서로 해결되지 않는가: /// 발판은 Unity 오브젝트가 아니라 진짜 데스크톱 창이고, 우리 창은 그 위에 떠 있는 /// 최상위 오버레이다. 그래서 "창 뒤에 그린다"가 물리적으로 불가능하다. /// 대신 그 영역에서 캐릭터를 아예 그리지 않으면, 우리 창은 그 자리에 투명한 채로 /// 남고 진짜 창이 비쳐 보인다. 결과가 같다. /// /// 방법은 깊이 마스크다. 색을 안 쓰고(ColorMask 0) 깊이만 쓰는 사각형을 /// 캐릭터보다 카메라 가까이, 캐릭터보다 먼저(Queue Geometry-100) 그리면 /// 그 뒤의 캐릭터 픽셀이 깊이 테스트에서 탈락한다. 알파를 0 으로 덮는 방법과 달리 /// 같은 자리에 그려진 다른 것(채팅창 등)을 건드리지 않는다. /// /// 사각형은 카메라 근평면 바로 앞에 둔다. 캐릭터 근처에 두면 무릎이나 코처럼 /// 튀어나온 부위가 사각형보다 앞에 놓여 일부만 가려지고, 그걸 피하려고 포즈마다 /// 깊이를 다시 재는 것은 낭비다. 원근 보정 덕분에 깊이를 당겨도 가려지는 화면 넓이는 /// 그대로이므로, 그냥 최대한 앞에 두면 어떤 포즈든 한 번에 해결된다. /// [DefaultExecutionOrder(100)] // WindowClimber(LateUpdate) 가 위치를 정한 뒤에 따라간다 public class PlatformOccluder : MonoBehaviour { [Header("참조 (비우면 씬에서 탐색)")] [SerializeField] WindowClimber climber; [SerializeField] Camera viewCamera; [Tooltip("MyCharacterAgent/PlatformOccluder 셰이더. " + "직접 참조해야 빌드에서 셰이더가 빠지지 않는다")] [SerializeField] Shader occluderShader; [Header("깊이")] // 사각형을 카메라 근평면 바로 앞에 둔다. 그러면 캐릭터의 어느 부위도 그보다 // 앞에 올 수 없으므로 포즈가 무엇이든 항상 전부 가려진다. // // 깊이를 바꿔도 가려지는 화면 영역은 그대로다 — ScreenToWorldPoint 가 원근을 // 보정하기 때문에, 깊이가 얕아지면 사각형이 작아지는 대신 화면에서 차지하는 // 넓이는 같다. 그래서 "최대한 앞"이 손해 없이 가장 안전한 선택이다. // // 3D 씬에 캐릭터 말고는 가릴 것이 없고, 채팅창과 포즈 메뉴는 ScreenSpaceOverlay // 캔버스라 깊이 테스트를 거치지 않는다. 잘못 가릴 대상이 없다. [Tooltip("카메라 근평면에서 얼마나 띄울지(월드 단위). " + "근평면보다 앞에 두면 사각형이 잘려나가 아무것도 못 가리므로 0 보다 커야 한다")] [SerializeField] float nearPlaneOffset = 0.01f; [Tooltip("발판 위쪽 경계를 얼마나 내릴지(픽셀). 양수면 발판선보다 아래부터 가린다. " + "머리가 살짝 더 보이게 하고 싶을 때 조절한다")] [SerializeField] float topOffset = 0f; /// 지금 가려야 하는지. CharacterMotionDirector 가 포즈에 따라 켜고 끈다. public bool Active { get; set; } MeshFilter meshFilter; MeshRenderer meshRenderer; Mesh mesh; readonly Vector3[] corners = new Vector3[4]; void Awake() { if (climber == null) climber = FindFirstObjectByType(); if (viewCamera == null) viewCamera = Camera.main; BuildQuad(); SetVisible(false); } void OnDestroy() { if (mesh != null) Destroy(mesh); if (meshRenderer != null && meshRenderer.material != null) Destroy(meshRenderer.material); } void LateUpdate() { if (meshRenderer == null) return; if (!Active || climber == null || viewCamera == null) { SetVisible(false); return; } var platform = climber.CurrentPlatform; if (platform == null) { // 공중에 있는 동안은 가릴 발판이 없다. SetVisible(false); return; } UpdateQuad(platform.Value); SetVisible(true); } /// 발판의 X 범위 × (발판선 아래 ~ 화면 아래)를 덮는 사각형으로 갱신한다. void UpdateQuad(DesktopPlatform platform) { float d = viewCamera.nearClipPlane + Mathf.Max(0.001f, nearPlaneOffset); float top = platform.Y - topOffset; corners[0] = viewCamera.ScreenToWorldPoint(new Vector3(platform.XMin, 0f, d)); corners[1] = viewCamera.ScreenToWorldPoint(new Vector3(platform.XMax, 0f, d)); corners[2] = viewCamera.ScreenToWorldPoint(new Vector3(platform.XMax, top, d)); corners[3] = viewCamera.ScreenToWorldPoint(new Vector3(platform.XMin, top, d)); // 정점을 월드 좌표로 넣으므로 트랜스폼은 원점/무회전으로 둔다. mesh.SetVertices(corners); mesh.RecalculateBounds(); } void BuildQuad() { transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity); transform.localScale = Vector3.one; mesh = new Mesh { name = "PlatformOccluderQuad" }; // 매 프레임 정점이 바뀌므로 표시해 둔다. mesh.MarkDynamic(); mesh.SetVertices(corners); mesh.SetTriangles(new[] { 0, 2, 1, 0, 3, 2 }, 0); meshFilter = gameObject.GetComponent(); if (meshFilter == null) meshFilter = gameObject.AddComponent(); meshFilter.sharedMesh = mesh; meshRenderer = gameObject.GetComponent(); if (meshRenderer == null) meshRenderer = gameObject.AddComponent(); meshRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; meshRenderer.receiveShadows = false; meshRenderer.lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off; meshRenderer.reflectionProbeUsage = UnityEngine.Rendering.ReflectionProbeUsage.Off; if (occluderShader == null) { Debug.LogWarning("[PlatformOccluder] 셰이더가 비어 있어 가림이 동작하지 않습니다. " + "MyCharacterAgent/PlatformOccluder 셰이더를 연결하세요."); return; } meshRenderer.material = new Material(occluderShader) { name = "PlatformOccluder (runtime)" }; } void SetVisible(bool value) { if (meshRenderer != null && meshRenderer.enabled != value) meshRenderer.enabled = value; } }