Project_WL/Assets/WL/Look/Character/Shaders/WLCharBacklight.shader

228 lines
13 KiB
Plaintext
Raw Permalink Normal View History

// ─────────────────────────────────────────────────────────────────────────────
// WLCharBacklight.shader — 캐릭터 역광(림) + 라이트 랩 + 피부 투과(SSS 흉내) 덧그리기 패스
//
// 2026-09-17 PD 「우리 게임의 캐릭터가 조금 더 돋보이게 · 역광 하이라이트로 입체감·배경 분리」
// + 팁 「메인 조명이 따뜻하면 역광은 차가운 색 · 피부는 역광에 붉게 비침(SSS) · Bloom 으로 번짐」.
//
// ■ 왜 별도 패스인가(814t `WL/RimAdd` 와 같은 이유)
// 캐릭터 본체는 원본 `Shader Graphs/Toon`(Assets/3DPixelArtEnvironment · 원본 0줄)이고 Fresnel 입력이 없다.
// → 같은 메시를 머티리얼 배열 뒤에 덧붙여 한 번 더 그린다(서브메시 1개 = 전체가 다시 그려진다).
// 본체 재질·그래프·프리팹 0줄 · 런타임에 켜고 끈다 · 모바일 비용 = 캐릭터 부위당 드로우 +1(화면 점유 작음) + 림 픽셀의 깊이 샘플 1회.
//
// ■ 무엇을 그리나
// ① 역광 림(차가운 색 · 또렷한 띠)
// · 화면 공간 깊이 림(기본): 가상 역광 쪽으로 몇 픽셀 옮긴 자리의 깊이가 훨씬 멀면 = 「역광을 받는 실루엣 가장자리」.
// → 굵기가 화면 픽셀로 일정(멀리서도 1 px 로 사라지지 않는다) · 머리카락 안쪽 가닥에는 안 생긴다(실측: 프레넬은 가닥마다 줄이 생겼다).
// · 프레넬 림(보조): 가까울 때만 섞어 띠에 두께감을 준다. 깊이 텍스처가 없으면(_UseDepthRim 0) 이것만 쓴다.
// ② 라이트 랩 = 역광 쪽의 넓고 옅은 Fresnel(빛이 얼굴 안쪽으로 살짝 스며든 느낌) — 가산
// ③ 피부 투과 = 피부 재질에만 림 안쪽에 따뜻한 붉은 띠(귀·볼 가장자리) — 가산
// 가상 역광 방향은 전역 벡터 `_WLBacklightDir`(C# 이 카메라에서 매 프레임 계산)로 받는다 → 카메라가 돌아도 항상 같은 쪽에 맺힌다.
// ─────────────────────────────────────────────────────────────────────────────
Shader "WL/CharBacklight"
{
Properties
{
[HDR] _RimColor ("Rim Color (차가운 역광)", Color) = (0.28, 0.62, 1.0, 1)
_RimStrength ("Rim Strength", Float) = 1.45
_RimOpacity ("Rim Opacity (1 = 바탕을 덮어 림 색 그대로 · 0 = 순수 가산 → 밝은 바탕 위에선 흰색이 된다)", Range(0, 1)) = 0.85
[Header(Screen space depth rim)]
_UseDepthRim ("Use Depth Rim (1 = 깊이 텍스처 있음)", Float) = 1
_RimWidth ("Rim Width (화면 짧은 변 대비 비율)", Range(0, 0.02)) = 0.0045
_RimDepthThreshold ("Depth Threshold (m · 이보다 멀면 실루엣)", Float) = 0.12
[Header(Fresnel rim)]
_RimThreshold ("Fresnel Threshold — 가까울 때(클수록 가는 띠)", Range(0, 1)) = 0.52
_RimThresholdFar ("Fresnel Threshold — 멀 때", Range(0, 1)) = 0.36
_RimNearDist ("Near Distance (m)", Float) = 3
_RimFarDist ("Far Distance (m)", Float) = 12
_RimSoftness ("Fresnel Softness", Range(0.001, 0.5)) = 0.05
_RimDirectional ("Directionality (0 = 둘레 전부 · 1 = 역광 쪽만)", Range(0, 1)) = 0.85
_FresnelRimNear ("Fresnel Rim Weight — 가까울 때(깊이 림과 섞는 양)", Range(0, 1)) = 0.6
_FresnelRimFar ("Fresnel Rim Weight — 멀 때", Range(0, 1)) = 0.0
[Header(Light wrap and SSS)]
[HDR] _WrapColor ("Light Wrap Color", Color) = (0.75, 0.85, 1.0, 1)
_WrapStrength ("Light Wrap Strength", Float) = 0.12
_WrapPower ("Light Wrap Power", Float) = 1.6
[HDR] _SssColor ("SSS Color (따뜻한 붉은빛)", Color) = (1.0, 0.32, 0.22, 1)
_SssStrength ("SSS Strength (피부만 > 0)", Float) = 0
_SssPower ("SSS Power", Float) = 1.7
[Header(Face point light)]
[HDR] _FaceColor ("Face Light Color", Color) = (1.0, 0.93, 0.85, 1)
_FaceStrength ("Face Light Strength (0 = 끔)", Float) = 0.3
_FaceWrap ("Face Light Wrap (0 = 또렷 · 1 = 둥글게 감싸는 빛)", Range(0, 1)) = 0.5
_FillStrength ("Body Fill Strength (위치 없는 채움빛 · 2등급용)", Float) = 0
[Header(Hit flash overlay)]
_HitFlash ("Hit Flash Opacity (렌더러별 프로퍼티 블록이 쓴다 · 0 = 없음)", Range(0, 1)) = 0
_HitFlashColor ("Hit Flash Color", Color) = (1, 1, 1, 1)
}
SubShader
{
Tags { "RenderPipeline" = "UniversalPipeline" "RenderType" = "Transparent" "Queue" = "Transparent" }
Pass
{
Name "WLCharBacklight"
Tags { "LightMode" = "SRPDefaultUnlit" }
Cull Back
ZWrite Off
ZTest LEqual
Offset -1, -1
// 미리 곱한 알파: 결과 = src.rgb + dst × (1 − src.a). 림은 a 로 바탕을 덮고(차가운 색이 그대로 보인다 — 실측: 순수 가산은 노란 머리·살색 위에서
// 흰 테가 됐다), 라이트 랩·피부 투과는 a = 0 이라 순수 가산으로 얹힌다.
Blend One OneMinusSrcAlpha
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"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"
CBUFFER_START(UnityPerMaterial)
float4 _RimColor;
float _RimStrength;
float _RimOpacity;
float _UseDepthRim;
float _RimWidth;
float _RimDepthThreshold;
float _RimThreshold;
float _RimThresholdFar;
float _RimNearDist;
float _RimFarDist;
float _RimSoftness;
float _RimDirectional;
float _FresnelRimNear;
float _FresnelRimFar;
float4 _WrapColor;
float _WrapStrength;
float _WrapPower;
float4 _SssColor;
float _SssStrength;
float _SssPower;
float4 _FaceColor;
float _FaceStrength;
float _FaceWrap;
float _FillStrength;
float _HitFlash;
float4 _HitFlashColor;
CBUFFER_END
// xyz = 표면에서 가상 역광 쪽을 가리키는 월드 방향(정규화) · w = 1 이면 유효(0 = 아직 안 들어옴 → 위쪽)
float4 _WLBacklightDir;
// 얼굴 포인트 라이트 — xyz = 얼굴 중심(월드) · w = 빛이 닿는 반지름(m · 0 = 없음) / 방향 = 표면에서 빛 쪽(카메라 앞·위·옆)
float4 _WLFaceCenter;
float4 _WLFaceLightDir;
struct Attributes
{
float4 positionOS : POSITION;
float3 normalOS : NORMAL;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float3 normalWS : TEXCOORD0;
float3 positionWS : TEXCOORD1;
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);
o.positionWS = TransformObjectToWorld(input.positionOS.xyz);
o.positionCS = TransformWorldToHClip(o.positionWS);
o.normalWS = TransformObjectToWorldNormal(input.normalOS);
return o;
}
half4 frag(Varyings input) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
float3 n = normalize(input.normalWS);
float3 v = normalize(GetWorldSpaceViewDir(input.positionWS));
float3 b = _WLBacklightDir.w > 0.5 ? normalize(_WLBacklightDir.xyz) : float3(0.0, 1.0, 0.0);
float fres = 1.0 - saturate(dot(n, v)); // 가장자리 1 · 정면 0
float toward = saturate(dot(n, b) * 0.5 + 0.5); // 역광 쪽을 보는 정도(반-램버트)
float dirMask = lerp(1.0, smoothstep(0.35, 0.85, toward), saturate(_RimDirectional));
float dist = distance(_WorldSpaceCameraPos, input.positionWS);
float farT = saturate((dist - _RimNearDist) / max(_RimFarDist - _RimNearDist, 0.01));
// ①-a 프레넬 림(보조)
float thr = lerp(_RimThreshold, _RimThresholdFar, farT);
float rimF = smoothstep(thr - _RimSoftness, thr + _RimSoftness, fres * dirMask);
// ①-b 화면 공간 깊이 림 — 역광 쪽으로 _RimWidth 만큼 옮긴 자리가 훨씬 멀면 실루엣 가장자리
float rimD = 0.0;
if (_UseDepthRim > 0.5)
{
float2 uv = GetNormalizedScreenSpaceUV(input.positionCS);
// 본체가 이 픽셀을 안 그렸으면(디졸브·알파 클립으로 잘려 나간 자리) 덧그리지 않는다 — 몬스터 사망 디졸브에 빛 자국이 남지 않게
float dBase = LinearEyeDepth(SampleSceneDepth(uv), _ZBufferParams);
clip(0.06 - (dBase - LinearEyeDepth(input.positionCS.z, _ZBufferParams)));
float3 bVS = TransformWorldToViewDir(b);
float2 dirSS = dot(bVS.xy, bVS.xy) > 1e-5 ? normalize(bVS.xy) : float2(0.7071, 0.7071);
float minSide = min(_ScaledScreenParams.x, _ScaledScreenParams.y);
float2 offs = dirSS * (_RimWidth * minSide) / _ScaledScreenParams.xy;
float dHere = LinearEyeDepth(input.positionCS.z, _ZBufferParams);
float dOff = LinearEyeDepth(SampleSceneDepth(uv + offs), _ZBufferParams);
rimD = step(_RimDepthThreshold, dOff - dHere);
// 역광을 등진 면(카메라 쪽 아래를 보는 면)은 뺀다 — 발밑·턱 아래에 줄이 생기지 않게
rimD *= smoothstep(0.15, 0.45, toward);
}
float wF = lerp(_FresnelRimNear, _FresnelRimFar, farT);
float rim = _UseDepthRim > 0.5 ? saturate(max(rimD, rimF * wF)) : rimF;
// ② 라이트 랩(넓고 옅게)
float wrap = pow(fres, max(_WrapPower, 0.01)) * toward * _WrapStrength;
// ③ 피부 투과 — 림 안쪽의 따뜻한 띠(림이 덮는 곳은 줄인다 → 바깥 차가움 · 안쪽 따뜻함의 두 톤)
float sss = pow(fres, max(_SssPower, 0.01)) * toward * _SssStrength * (1.0 - rim * 0.65);
// ④ 얼굴 포인트 라이트 — 얼굴 중심에서 반지름 안쪽만, 카메라 쪽 앞·위에서 비추는 부드러운 빛(가산 · 얼굴 그림(Unlit 데칼)은 이 위에 그려져 또렷하게 남는다)
float face = 0.0;
if (_WLFaceCenter.w > 0.001 && _FaceStrength > 0.0)
{
float fd = length(input.positionWS - _WLFaceCenter.xyz) / _WLFaceCenter.w;
float fall = saturate(1.0 - fd); fall = fall * fall * (3.0 - 2.0 * fall);
float3 fl = dot(_WLFaceLightDir.xyz, _WLFaceLightDir.xyz) > 1e-5 ? normalize(_WLFaceLightDir.xyz) : v;
float ndl = saturate(dot(n, fl) * (1.0 - 0.5 * _FaceWrap) + 0.5 * _FaceWrap);
face = fall * ndl * _FaceStrength * (1.0 - rim);
}
// ④-b 위치 없는 채움빛(2등급 = 몬스터·NPC) — 같은 빛 방향으로 몸 전체를 옅게
if (_FillStrength > 0.0)
{
float3 fl2 = dot(_WLFaceLightDir.xyz, _WLFaceLightDir.xyz) > 1e-5 ? normalize(_WLFaceLightDir.xyz) : v;
face += saturate(dot(n, fl2) * (1.0 - 0.5 * _FaceWrap) + 0.5 * _FaceWrap) * _FillStrength * (1.0 - rim);
}
float3 col = _RimColor.rgb * (rim * _RimStrength) + _WrapColor.rgb * wrap + _SssColor.rgb * sss + _FaceColor.rgb * face;
float a = rim * saturate(_RimOpacity);
// ⑤ 피격 하이라이트 — 본체 위에 흰색을 _HitFlash 만큼만 덮는다(완전히 하얗게 되지 않는다 · 2026-09-17 PD 「투명도 20 % 정도」)
float hf = saturate(_HitFlash);
col = col * (1.0 - hf) + _HitFlashColor.rgb * hf;
a = 1.0 - (1.0 - a) * (1.0 - hf);
return half4(col, a);
}
ENDHLSL
}
}
Fallback Off
}