// ───────────────────────────────────────────────────────────────────────────── // WLHullOutline.shader — 화면 픽셀 단위 두께의 「굵은 검은 외곽선」 (WL-814t · #814) // // ■ 왜 새로 만드나 (§1 실측 결론) // 원본 `Shader Graphs/Toon` 의 `_Outline`/`_OUTLINESENABLED` 는 **머티리얼 안에 박힌 // 화면공간 에지검출 후처리**다(Depth+Normals 1텍셀 비교 → Overlay 블렌드). // · 두께 = `_CameraNormalsTexture` **1 텍셀 고정** — 머티리얼로 못 굵게 한다. // · `_Outline` 의 **알파는 무시**된다(Blend 노드가 float3 로 해석) — RGB 만 쓴다. // · `PixelOutlineSetup` 렌더러 피처가 꺼져 있으면(현재 `m_Active: 0`) 아무것도 안 나온다. // · 켜도 나오는 것은 「면 경계의 얇은 실선」이지 레퍼런스의 굵은 실루엣이 아니다. // → 레퍼런스(2D 도트)의 굵은 외곽선은 **메시 노멀 확장(inverted hull)** 으로만 나온다. // // ■ 무엇을 하나 // 앞면을 버리고(Cull Front) 뒷면을 화면 XY 로 `_OutlineWidth` **픽셀**만큼 밀어 그린다. // 두께가 화면 픽셀 단위라 거리·직교크기와 무관하게 일정하다(= 2D 도트 느낌). // // ■ 어떻게 쓰나 // 원본 머티리얼을 **건드리지 않는다**. 렌더러의 머티리얼 배열 뒤에 이 머티리얼을 한 장 // 더 붙이면(서브메시가 1개면 마지막 서브메시를 한 번 더 그린다) 외곽선이 된다. // 붙이고 떼는 것은 `WLReferenceLook` 이 런타임에만 한다(에셋·씬 무변경). // // ■ 모바일 비용 // 대상 렌더러당 드로우콜 +1(정점만 한 번 더). 픽셀 부하는 실루엣 띠 면적뿐. // ───────────────────────────────────────────────────────────────────────────── Shader "WL/HullOutline" { Properties { [HDR] _OutlineColor ("Outline Color", Color) = (0, 0, 0, 1) _OutlineWidth ("Outline Width (screen px)", Float) = 2.0 _OutlineDepthBias ("Depth Bias (m · 하드에지 저폴리의 내부 얼룩 제거)", Float) = 0.06 // ── 색 연동 외곽선(레퍼런스는 부위마다 선 색이 다르다: 머리칼=짙은 갈색 · 흰 올빼미=짙은 남색) _BaseMap ("Base Map (원본 머티리얼에서 복사)", 2D) = "white" {} _TintColor ("Tint Color (원본 _DiffuseColor)", Color) = (1, 1, 1, 1) _TintDarkness ("Tint Darkness", Range(0, 1)) = 0.28 _TintAmount ("0 = 균일 색 · 1 = 표면색 연동", Range(0, 1)) = 0.0 } SubShader { Tags { "RenderPipeline" = "UniversalPipeline" "RenderType" = "Opaque" "Queue" = "Geometry-1" } Pass { Name "WLHullOutline" Tags { "LightMode" = "SRPDefaultUnlit" } Cull Front ZWrite On ZTest LEqual ColorMask RGB HLSLPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_instancing #pragma target 3.0 #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); CBUFFER_START(UnityPerMaterial) float4 _OutlineColor; float _OutlineWidth; float _OutlineDepthBias; float4 _BaseMap_ST; float4 _TintColor; float _TintDarkness; float _TintAmount; CBUFFER_END struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float2 uv : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_VERTEX_OUTPUT_STEREO }; Varyings vert(Attributes input) { Varyings o = (Varyings)0; UNITY_SETUP_INSTANCE_ID(input); UNITY_TRANSFER_INSTANCE_ID(input, o); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); // 🔴 깊이 바이어스 — 껍데기를 카메라에서 조금 더 멀리 밀어 둔다. // 저폴리 하드에지 메시(머리카락·옷)는 같은 위치에서 노멀이 쪼개져 있어 // 그냥 밀면 실루엣 **안쪽**에 뒷면이 튀어나와 얼룩이 된다(실측). // 본체 면보다 항상 뒤에 있게 만들면 안쪽 얼룩만 깔끔히 사라진다. float3 posWS = TransformObjectToWorld(input.positionOS.xyz); float3 posVS = TransformWorldToView(posWS); posVS.z -= _OutlineDepthBias; // 뷰공간은 -Z 가 앞 → 빼면 멀어진다 float4 posCS = TransformWViewToHClip(posVS); float3 nWS = TransformObjectToWorldNormal(input.normalOS); float3 nVS = TransformWorldToViewDir(nWS, true); // 투영까지 태워야 직교/원근 모두에서 화면 방향이 맞는다 float2 nCS = mul((float3x3)UNITY_MATRIX_P, nVS).xy; // 화면(픽셀) 공간에서 정규화 → 방향이 대각이어도 두께가 일정하다 float2 dirPx = nCS * float2(_ScreenParams.x, _ScreenParams.y); float len = length(dirPx); dirPx = (len > 1e-5) ? dirPx / len : float2(0.0, 0.0); // 픽셀 → NDC(가로 2.0 이 화면 폭) → 클립(× w) float2 ndcPerPx = float2(2.0 / _ScreenParams.x, 2.0 / _ScreenParams.y); posCS.xy += dirPx * ndcPerPx * _OutlineWidth * posCS.w; o.positionCS = posCS; o.uv = TRANSFORM_TEX(input.uv, _BaseMap); return o; } half4 frag(Varyings input) : SV_Target { UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input); // 색 연동 — 원본 머티리얼의 BaseMap×DiffuseColor 를 어둡게 깐다. // BaseMap 이 없는 배경 머티리얼은 흰색이 바인딩되어 _TintColor(=_DiffuseColor) 만 남는다. half3 tinted = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv).rgb * _TintColor.rgb * _TintDarkness; half3 col = lerp(_OutlineColor.rgb, tinted, saturate(_TintAmount)); return half4(col, 1.0); } ENDHLSL } } Fallback Off }