139 lines
6.9 KiB
Plaintext
139 lines
6.9 KiB
Plaintext
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 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
|
||
}
|