From 60275c6cd9bbfc029f2228667d47c8a0cbf82357 Mon Sep 17 00:00:00 2001 From: dal4segno Date: Sat, 4 Apr 2026 10:51:10 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=B3=B4=EC=8A=A4=20=EA=B0=80=EB=A6=BC?= =?UTF-8?q?=20=EC=8B=9C=20=ED=94=8C=EB=A0=88=EC=9D=B4=EC=96=B4=20=EC=99=B8?= =?UTF-8?q?=EA=B3=BD=EC=84=A0=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PlayerOcclusionOutline: 보스(Enemy 레이어)가 플레이어를 가릴 때 외곽선 활성화 - Outline.shader: URP 법선 확장 외곽선 셰이더 (Fresnel 기반 알파) - 외곽선용 별도 SkinnedMeshRenderer를 자식 GameObject에 생성 - ObstacleTransparencyController: Enemy 레이어 장애물 숨김 제외 - PlayerCamera: PlayerOcclusionOutline 초기화 연동 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../Player/ObstacleTransparencyController.cs | 4 +- Assets/_Game/Scripts/Player/PlayerCamera.cs | 6 + .../Scripts/Player/PlayerOcclusionOutline.cs | 109 ++++++++++++++++++ .../Player/PlayerOcclusionOutline.cs.meta | 2 + Assets/_Game/Shaders.meta | 8 ++ Assets/_Game/Shaders/Outline.shader | 74 ++++++++++++ Assets/_Game/Shaders/Outline.shader.meta | 9 ++ 7 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 Assets/_Game/Scripts/Player/PlayerOcclusionOutline.cs create mode 100644 Assets/_Game/Scripts/Player/PlayerOcclusionOutline.cs.meta create mode 100644 Assets/_Game/Shaders.meta create mode 100644 Assets/_Game/Shaders/Outline.shader create mode 100644 Assets/_Game/Shaders/Outline.shader.meta diff --git a/Assets/_Game/Scripts/Player/ObstacleTransparencyController.cs b/Assets/_Game/Scripts/Player/ObstacleTransparencyController.cs index e6536005..9948f871 100644 --- a/Assets/_Game/Scripts/Player/ObstacleTransparencyController.cs +++ b/Assets/_Game/Scripts/Player/ObstacleTransparencyController.cs @@ -68,7 +68,9 @@ namespace Colosseum.Player private HashSet CollectHits(Vector3 origin, Vector3 direction, float maxDistance) { var hits = new HashSet(); - RaycastHit[] rayHits = Physics.SphereCastAll(origin, checkRadius, direction, maxDistance, occlusionMask, QueryTriggerInteraction.Ignore); + int enemyLayer = LayerMask.GetMask("Enemy"); + int mask = occlusionMask & ~enemyLayer; + RaycastHit[] rayHits = Physics.SphereCastAll(origin, checkRadius, direction, maxDistance, mask, QueryTriggerInteraction.Ignore); float targetY = targetTransform.position.y; diff --git a/Assets/_Game/Scripts/Player/PlayerCamera.cs b/Assets/_Game/Scripts/Player/PlayerCamera.cs index 94aafa82..8060d2ce 100644 --- a/Assets/_Game/Scripts/Player/PlayerCamera.cs +++ b/Assets/_Game/Scripts/Player/PlayerCamera.cs @@ -91,6 +91,12 @@ namespace Colosseum.Player transparencyController = cameraInstance.gameObject.AddComponent(); transparencyController.Initialize(target, collisionMask); + // 플레이어 외곽선 컨트롤러 연동 (보스 가림 시) + var outlineController = cameraInstance.gameObject.GetComponent(); + if (outlineController == null) + outlineController = cameraInstance.gameObject.AddComponent(); + outlineController.Initialize(cameraInstance.transform, target); + // 카메라 위치를 즉시 타겟 위치로 초기화 SnapToTarget(); } diff --git a/Assets/_Game/Scripts/Player/PlayerOcclusionOutline.cs b/Assets/_Game/Scripts/Player/PlayerOcclusionOutline.cs new file mode 100644 index 00000000..3b35984c --- /dev/null +++ b/Assets/_Game/Scripts/Player/PlayerOcclusionOutline.cs @@ -0,0 +1,109 @@ +using UnityEngine; + +namespace Colosseum.Player +{ + /// + /// 보스가 플레이어를 가릴 때 플레이어 외곽선을 표시하는 컨트롤러 + /// + public class PlayerOcclusionOutline : MonoBehaviour + { + [Header("Settings")] + [Min(0.001f)] [SerializeField] private float outlineWidth = 0.02f; + [SerializeField] private Color outlineColor = Color.white; + [Min(0.01f)] [SerializeField] private float checkRadius = 0.3f; + + private Transform cameraTransform; + private Transform playerTransform; + private SkinnedMeshRenderer originalRenderer; + private GameObject outlineHost; + private SkinnedMeshRenderer outlineRenderer; + private Material outlineMaterial; + private bool isShowingOutline; + + public void Initialize(Transform camera, Transform player) + { + cameraTransform = camera; + playerTransform = player; + + originalRenderer = player.GetComponentInChildren(); + if (originalRenderer == null) return; + + var shader = Shader.Find("Hidden/Colosseum/Outline"); + if (shader == null) + { + Debug.LogError("[PlayerOcclusionOutline] Shader 'Hidden/Colosseum/Outline' not found"); + return; + } + + outlineMaterial = new Material(shader); + outlineMaterial.SetFloat("_OutlineWidth", outlineWidth); + outlineMaterial.SetColor("_OutlineColor", outlineColor); + + if (originalRenderer.sharedMesh == null) + { + Debug.LogError("[PlayerOcclusionOutline] originalRenderer has no sharedMesh"); + return; + } + + outlineHost = new GameObject("OutlineRenderer"); + outlineHost.transform.SetParent(originalRenderer.transform, false); + outlineRenderer = outlineHost.AddComponent(); + outlineRenderer.sharedMesh = originalRenderer.sharedMesh; + outlineRenderer.bones = originalRenderer.bones; + outlineRenderer.rootBone = originalRenderer.rootBone; + outlineRenderer.sharedMaterials = new[] { outlineMaterial }; + outlineRenderer.updateWhenOffscreen = true; + outlineRenderer.enabled = false; + + Debug.Log($"[PlayerOcclusionOutline] Initialize: outlineRenderer on child {outlineHost.name}"); + } + + private void LateUpdate() + { + if (cameraTransform == null || playerTransform == null || outlineRenderer == null) + return; + + outlineMaterial.SetFloat("_OutlineWidth", outlineWidth); + outlineMaterial.SetColor("_OutlineColor", outlineColor); + + bool isOccluded = CheckBossOcclusion(); + + if (isOccluded && !isShowingOutline) + { + outlineRenderer.enabled = true; + isShowingOutline = true; + } + else if (!isOccluded && isShowingOutline) + { + outlineRenderer.enabled = false; + isShowingOutline = false; + } + } + + private bool CheckBossOcclusion() + { + Vector3 origin = cameraTransform.position; + Vector3 direction = playerTransform.position - origin; + float distance = direction.magnitude; + + if (distance < 0.01f) return false; + + int enemyLayer = LayerMask.GetMask("Enemy"); + + if (Physics.SphereCast(origin, checkRadius, direction.normalized, out RaycastHit hit, distance, enemyLayer, QueryTriggerInteraction.Ignore)) + { + return hit.collider.transform.IsChildOf(playerTransform) == false; + } + + return false; + } + + private void OnDestroy() + { + if (outlineHost != null) + Destroy(outlineHost); + if (outlineMaterial != null) + Destroy(outlineMaterial); + } + } +} diff --git a/Assets/_Game/Scripts/Player/PlayerOcclusionOutline.cs.meta b/Assets/_Game/Scripts/Player/PlayerOcclusionOutline.cs.meta new file mode 100644 index 00000000..fc073b52 --- /dev/null +++ b/Assets/_Game/Scripts/Player/PlayerOcclusionOutline.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2f13a11c6eb7427efa4bfdac57d1458c \ No newline at end of file diff --git a/Assets/_Game/Shaders.meta b/Assets/_Game/Shaders.meta new file mode 100644 index 00000000..8fb4ce5d --- /dev/null +++ b/Assets/_Game/Shaders.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c2031d8db9b9eec15ac21ab219326734 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Game/Shaders/Outline.shader b/Assets/_Game/Shaders/Outline.shader new file mode 100644 index 00000000..76220eb8 --- /dev/null +++ b/Assets/_Game/Shaders/Outline.shader @@ -0,0 +1,74 @@ +Shader "Hidden/Colosseum/Outline" +{ + Properties + { + _OutlineWidth("Width", Range(0.001, 0.1)) = 0.02 + _OutlineColor("Color", Color) = (1, 1, 1, 1) + } + + SubShader + { + Tags + { + "RenderType" = "Transparent" + "Queue" = "Overlay" + "RenderPipeline" = "UniversalPipeline" + } + + ZWrite Off + ZTest Always + Cull Front + Blend SrcAlpha OneMinusSrcAlpha + + Pass + { + HLSLPROGRAM + #pragma vertex vert + #pragma fragment frag + #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" + + CBUFFER_START(UnityPerMaterial) + float _OutlineWidth; + float4 _OutlineColor; + CBUFFER_END + + struct Attributes + { + float3 positionOS : POSITION; + float3 normalOS : NORMAL; + }; + + struct Varyings + { + float4 positionCS : SV_POSITION; + float outlineAlpha : TEXCOORD0; + }; + + Varyings vert(Attributes input) + { + Varyings output; + + float3 normalWS = TransformObjectToWorldNormal(input.normalOS); + float3 positionWS = TransformObjectToWorld(input.positionOS); + float3 viewDir = normalize(GetWorldSpaceViewDir(positionWS)); + + float NdotV = dot(normalWS, viewDir); + output.outlineAlpha = saturate(1.0 + NdotV); + + positionWS += normalWS * _OutlineWidth * output.outlineAlpha; + + output.positionCS = TransformWorldToHClip(positionWS); + return output; + } + + half4 frag(Varyings input) : SV_Target + { + float alpha = _OutlineColor.a * pow(input.outlineAlpha, 3.0); + return half4(_OutlineColor.rgb, alpha); + } + ENDHLSL + } + } + + FallBack Off +} diff --git a/Assets/_Game/Shaders/Outline.shader.meta b/Assets/_Game/Shaders/Outline.shader.meta new file mode 100644 index 00000000..f38a6bd4 --- /dev/null +++ b/Assets/_Game/Shaders/Outline.shader.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 38b277215185d2a71b39fc85259575aa +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + userData: + assetBundleName: + assetBundleVariant: