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

228 lines
13 KiB
GLSL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// 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
}