137 lines
5.6 KiB
C#
137 lines
5.6 KiB
C#
using UnityEngine;
|
||
|
||
/// <summary>
|
||
/// 발판 아래에 깊이만 기록하는 사각형을 깔아 캐릭터를 가린다.
|
||
/// "발판(=실제 창) 뒤에 서 있는" 그림을 만들 때 쓴다. 예: 창 밑에서 머리만 내미는 포즈.
|
||
///
|
||
/// 왜 렌더 순서로 해결되지 않는가:
|
||
/// 발판은 Unity 오브젝트가 아니라 진짜 데스크톱 창이고, 우리 창은 그 위에 떠 있는
|
||
/// 최상위 오버레이다. 그래서 "창 뒤에 그린다"가 물리적으로 불가능하다.
|
||
/// 대신 그 영역에서 캐릭터를 아예 그리지 않으면, 우리 창은 그 자리에 투명한 채로
|
||
/// 남고 진짜 창이 비쳐 보인다. 결과가 같다.
|
||
///
|
||
/// 방법은 깊이 마스크다. 색을 안 쓰고(ColorMask 0) 깊이만 쓰는 사각형을
|
||
/// 캐릭터보다 카메라 가까이, 캐릭터보다 먼저(Queue Geometry-100) 그리면
|
||
/// 그 뒤의 캐릭터 픽셀이 깊이 테스트에서 탈락한다. 알파를 0 으로 덮는 방법과 달리
|
||
/// 같은 자리에 그려진 다른 것(채팅창 등)을 건드리지 않는다.
|
||
/// </summary>
|
||
[DefaultExecutionOrder(100)] // WindowClimber(LateUpdate) 가 위치를 정한 뒤에 따라간다
|
||
public class PlatformOccluder : MonoBehaviour
|
||
{
|
||
[Header("참조 (비우면 씬에서 탐색)")]
|
||
[SerializeField] WindowClimber climber;
|
||
[SerializeField] Camera viewCamera;
|
||
|
||
[Tooltip("MyCharacterAgent/PlatformOccluder 셰이더. " +
|
||
"직접 참조해야 빌드에서 셰이더가 빠지지 않는다")]
|
||
[SerializeField] Shader occluderShader;
|
||
|
||
[Header("동작")]
|
||
[Tooltip("캐릭터보다 카메라 쪽으로 얼마나 앞에 둘지(월드 단위). " +
|
||
"너무 작으면 깊이 정밀도 문제로 지글거리고, 너무 크면 다른 것까지 가린다")]
|
||
[SerializeField] float depthBias = 0.05f;
|
||
|
||
[Tooltip("발판 위쪽 경계를 얼마나 내릴지(픽셀). 양수면 발판선보다 아래부터 가린다. " +
|
||
"머리가 살짝 더 보이게 하고 싶을 때 조절한다")]
|
||
[SerializeField] float topOffset = 0f;
|
||
|
||
/// <summary>지금 가려야 하는지. CharacterMotionDirector 가 포즈에 따라 켜고 끈다.</summary>
|
||
public bool Active { get; set; }
|
||
|
||
MeshFilter meshFilter;
|
||
MeshRenderer meshRenderer;
|
||
Mesh mesh;
|
||
readonly Vector3[] corners = new Vector3[4];
|
||
|
||
void Awake()
|
||
{
|
||
if (climber == null) climber = FindFirstObjectByType<WindowClimber>();
|
||
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);
|
||
}
|
||
|
||
/// <summary>발판의 X 범위 × (발판선 아래 ~ 화면 아래)를 덮는 사각형으로 갱신한다.</summary>
|
||
void UpdateQuad(DesktopPlatform platform)
|
||
{
|
||
// 캐릭터와 같은 평면보다 살짝 앞. 여기서 깊이를 써야 캐릭터가 탈락한다.
|
||
float d = Mathf.Max(0.01f, climber.CharacterDepth - depthBias);
|
||
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<MeshFilter>();
|
||
if (meshFilter == null) meshFilter = gameObject.AddComponent<MeshFilter>();
|
||
meshFilter.sharedMesh = mesh;
|
||
|
||
meshRenderer = gameObject.GetComponent<MeshRenderer>();
|
||
if (meshRenderer == null) meshRenderer = gameObject.AddComponent<MeshRenderer>();
|
||
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;
|
||
}
|
||
}
|