diff --git a/Assets/WL/Look/Character/Resources/WL/WLCharacterBacklightSettings.asset b/Assets/WL/Look/Character/Resources/WL/WLCharacterBacklightSettings.asset
index de89d26c3..557dd38ee 100644
--- a/Assets/WL/Look/Character/Resources/WL/WLCharacterBacklightSettings.asset
+++ b/Assets/WL/Look/Character/Resources/WL/WLCharacterBacklightSettings.asset
@@ -17,12 +17,16 @@ MonoBehaviour:
shaderNameFilter: Shader Graphs/Toon
skinMaterialHints:
- Skin
- rimColor: {r: 0.28, g: 0.62, b: 1, a: 1}
- rimStrength: 1.45
+ baseBrighten: 1
+ baseMinimumDarkness: 0.38
+ baseShadowColor: {r: 0.46, g: 0.38, b: 0.44, a: 0}
+ baseCloudStrength: 0
+ rimColor: {r: 1, g: 0.96, b: 0.88, a: 1}
+ rimStrength: 1
rimUseDepth: 1
- rimWidth: 0.0045
+ rimWidth: 0.003
rimDepthThreshold: 0.12
- rimFresnelNear: 0.6
+ rimFresnelNear: 0.25
rimFresnelFar: 0
rimThreshold: 0.52
rimThresholdFar: 0.36
@@ -30,20 +34,30 @@ MonoBehaviour:
rimFarDistance: 12
rimSoftness: 0.05
rimDirectional: 0.85
- rimOpacity: 0.85
+ rimOpacity: 0.5
backlightUp: 0.7
backlightSide: 0.8
- wrapColor: {r: 0.75, g: 0.85, b: 1, a: 1}
- wrapStrength: 0.12
+ wrapColor: {r: 1, g: 0.95, b: 0.88, a: 1}
+ wrapStrength: 0.06
wrapPower: 1.6
sssColor: {r: 1, g: 0.32, b: 0.22, a: 1}
- sssStrength: 1.5
+ sssStrength: 1
sssPower: 1.7
+ faceEnabled: 1
+ faceColor: {r: 1, g: 0.93, b: 0.85, a: 1}
+ faceStrength: 0.16
+ faceRadiusScale: 3
+ faceWrap: 0.8
+ faceLightUp: 0.6
+ faceLightSide: -0.4
+ faceRendererHints:
+ - Face
+ - Head
bloomEnabled: 1
bloomScenes:
- Level01
- bloomThreshold: 1.05
- bloomIntensity: 0.55
+ bloomThreshold: 1.2
+ bloomIntensity: 0.4
bloomScatter: 0.55
bloomHdr: 1
bloomQuarter: 1
diff --git a/Assets/WL/Look/Character/Shaders/WLCharBacklight.shader b/Assets/WL/Look/Character/Shaders/WLCharBacklight.shader
index 3002007b6..106ea4595 100644
--- a/Assets/WL/Look/Character/Shaders/WLCharBacklight.shader
+++ b/Assets/WL/Look/Character/Shaders/WLCharBacklight.shader
@@ -48,6 +48,11 @@ Shader "WL/CharBacklight"
[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
}
SubShader
@@ -97,10 +102,16 @@ Shader "WL/CharBacklight"
float4 _SssColor;
float _SssStrength;
float _SssPower;
+ float4 _FaceColor;
+ float _FaceStrength;
+ float _FaceWrap;
CBUFFER_END
// xyz = 표면에서 가상 역광 쪽을 가리키는 월드 방향(정규화) · w = 1 이면 유효(0 = 아직 안 들어옴 → 위쪽)
float4 _WLBacklightDir;
+ // 얼굴 포인트 라이트 — xyz = 얼굴 중심(월드) · w = 빛이 닿는 반지름(m · 0 = 없음) / 방향 = 표면에서 빛 쪽(카메라 앞·위·옆)
+ float4 _WLFaceCenter;
+ float4 _WLFaceLightDir;
struct Attributes
{
@@ -172,7 +183,18 @@ Shader "WL/CharBacklight"
// ③ 피부 투과 — 림 안쪽의 따뜻한 띠(림이 덮는 곳은 줄인다 → 바깥 차가움 · 안쪽 따뜻함의 두 톤)
float sss = pow(fres, max(_SssPower, 0.01)) * toward * _SssStrength * (1.0 - rim * 0.65);
- float3 col = _RimColor.rgb * (rim * _RimStrength) + _WrapColor.rgb * wrap + _SssColor.rgb * sss;
+ // ④ 얼굴 포인트 라이트 — 얼굴 중심에서 반지름 안쪽만, 카메라 쪽 앞·위에서 비추는 부드러운 빛(가산 · 얼굴 그림(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);
+ }
+
+ float3 col = _RimColor.rgb * (rim * _RimStrength) + _WrapColor.rgb * wrap + _SssColor.rgb * sss + _FaceColor.rgb * face;
return half4(col, rim * saturate(_RimOpacity));
}
ENDHLSL
diff --git a/Assets/WL/Look/Character/WLCharacterBacklight.cs b/Assets/WL/Look/Character/WLCharacterBacklight.cs
index c0269239f..7906a5dc1 100644
--- a/Assets/WL/Look/Character/WLCharacterBacklight.cs
+++ b/Assets/WL/Look/Character/WLCharacterBacklight.cs
@@ -82,6 +82,7 @@ namespace WL.Look.Character
s_tmp.Add(want);
r.sharedMaterials = s_tmp.ToArray();
}
+ BrightenBase(cfg, r, s_tmp.Count > 0 ? s_tmp[0] : null, cfg.baseBrighten != 0);
applied++; if (isSkin) skin++;
if (SubMeshCount(r) > 1) multi++; // 서브메시가 여럿이면 마지막 서브메시만 덧그려진다(진단용)
}
@@ -102,11 +103,48 @@ namespace WL.Look.Character
var mats = r.sharedMaterials; if (mats == null) continue;
s_tmp.Clear();
for (int m = 0; m < mats.Length; m++) if (!IsOurs(mats[m])) s_tmp.Add(mats[m]);
- if (s_tmp.Count != mats.Length) r.sharedMaterials = s_tmp.ToArray();
+ if (s_tmp.Count != mats.Length)
+ {
+ r.sharedMaterials = s_tmp.ToArray();
+ BrightenBase(null, r, s_tmp.Count > 0 ? s_tmp[0] : null, false); // 우리가 붙였던 렌더러만 본체 값도 되돌린다
+ }
}
LastLog = "역광 패스 제거 @" + root.name;
}
+ // ─────────────────────────────────────────────────────────────────
+ // 본체 밝히기 — 2026-09-17 PD 「캐릭터가 어둡게 보인다 · 원한 건 더 밝고 화사하게」
+ // 본체(Toon) 재질의 그늘 최소 밝기·그늘 색·구름 그늘 세기를 **렌더러별 프로퍼티 블록**으로 덮는다(재질 에셋·공유 재질 0줄).
+ // 끌 때는 같은 속성을 재질의 원래 값으로 되써서 되돌린다(다른 시스템이 쓰는 블록 속성은 건드리지 않는다).
+ // ─────────────────────────────────────────────────────────────────
+ static MaterialPropertyBlock s_mpb;
+ static readonly int kMinDark = Shader.PropertyToID("_MinimumDarkness");
+ static readonly int kShadowCol = Shader.PropertyToID("_ShadowDiffuseColor");
+ static readonly int kCloud = Shader.PropertyToID("_Cloud_Strength");
+ public static int BrightenedRenderers;
+
+ static void BrightenBase(WLCharacterBacklightSettings cfg, Renderer r, Material baseMat, bool on)
+ {
+ if (r == null || baseMat == null) return;
+ if (!baseMat.HasProperty(kMinDark) || !baseMat.HasProperty(kShadowCol)) return;
+ if (s_mpb == null) s_mpb = new MaterialPropertyBlock();
+ r.GetPropertyBlock(s_mpb);
+ if (on && cfg != null)
+ {
+ s_mpb.SetFloat(kMinDark, cfg.baseMinimumDarkness);
+ s_mpb.SetColor(kShadowCol, cfg.baseShadowColor);
+ if (cfg.baseCloudStrength >= 0f && baseMat.HasProperty(kCloud)) s_mpb.SetFloat(kCloud, cfg.baseCloudStrength);
+ BrightenedRenderers++;
+ }
+ else
+ {
+ s_mpb.SetFloat(kMinDark, baseMat.GetFloat(kMinDark));
+ s_mpb.SetColor(kShadowCol, baseMat.GetColor(kShadowCol));
+ if (baseMat.HasProperty(kCloud)) s_mpb.SetFloat(kCloud, baseMat.GetFloat(kCloud));
+ }
+ r.SetPropertyBlock(s_mpb);
+ }
+
/// 설정 값을 런타임 재질 2장에 다시 밀어 넣는다(프로브에서 값을 바꾼 뒤 호출).
public static void RefreshValues()
{
@@ -153,6 +191,9 @@ namespace WL.Look.Character
m.SetColor("_SssColor", c.sssColor);
m.SetFloat("_SssStrength", skin ? c.sssStrength : 0f);
m.SetFloat("_SssPower", c.sssPower);
+ m.SetColor("_FaceColor", c.faceColor);
+ m.SetFloat("_FaceStrength", c.faceEnabled != 0 ? c.faceStrength : 0f);
+ m.SetFloat("_FaceWrap", c.faceWrap);
}
static bool IsSkinName(WLCharacterBacklightSettings c, string matName)
@@ -227,6 +268,7 @@ namespace WL.Look.Character
d.Normalize();
LastDir = d;
Shader.SetGlobalVector(kDirId, new Vector4(d.x, d.y, d.z, 1f));
+ UpdateFaceLight(cfg, cam, f);
}
if (Time.unscaledTime >= _nextScopeCheck)
@@ -249,6 +291,61 @@ namespace WL.Look.Character
}
}
+ // ── 얼굴 포인트 라이트 — 카메라에 가장 가까운 등록 캐릭터의 얼굴(머리) 렌더러 경계 중심을 전역 벡터로 넘긴다
+ static readonly int kFaceCenterId = Shader.PropertyToID("_WLFaceCenter");
+ static readonly int kFaceDirId = Shader.PropertyToID("_WLFaceLightDir");
+ static readonly Dictionary s_faceRend = new Dictionary();
+ public static Vector3 LastFaceCenter; public static float LastFaceRadius; public static string LastFaceLog = "";
+
+ void UpdateFaceLight(WLCharacterBacklightSettings cfg, Camera cam, Vector3 camForwardFlat)
+ {
+ if (cfg.enabled_ == 0 || cfg.faceEnabled == 0 || cfg.faceStrength <= 0f)
+ {
+ Shader.SetGlobalVector(kFaceCenterId, Vector4.zero); LastFaceRadius = 0f; return;
+ }
+ // 대상 = 카메라에 가장 가까운 활성 캐릭터(지금은 PC 하나)
+ Renderer best = null; float bestD = float.MaxValue;
+ for (int i = 0; i < s_roots.Count; i++)
+ {
+ var root = s_roots[i];
+ if (root == null || !root.activeInHierarchy) continue;
+ Renderer fr;
+ if (!s_faceRend.TryGetValue(root, out fr) || fr == null) { fr = FindFaceRenderer(cfg, root); s_faceRend[root] = fr; }
+ if (fr == null || !fr.enabled || !fr.gameObject.activeInHierarchy) continue;
+ float dsq = (fr.bounds.center - cam.transform.position).sqrMagnitude;
+ if (dsq < bestD) { bestD = dsq; best = fr; }
+ }
+ if (best == null) { Shader.SetGlobalVector(kFaceCenterId, Vector4.zero); LastFaceRadius = 0f; LastFaceLog = "얼굴 렌더러 없음"; return; }
+
+ var b = best.bounds;
+ float radius = Mathf.Max(0.05f, b.extents.magnitude * cfg.faceRadiusScale);
+ LastFaceCenter = b.center; LastFaceRadius = radius; LastFaceLog = best.name + " r=" + radius.ToString("F2");
+ Shader.SetGlobalVector(kFaceCenterId, new Vector4(b.center.x, b.center.y, b.center.z, radius));
+ // 빛 방향(표면 → 빛) = 카메라 쪽(앞) + 위 + 옆 — 역광의 반대편에서 얼굴을 받쳐 주는 빛
+ var l = -camForwardFlat + Vector3.up * cfg.faceLightUp + cam.transform.right * cfg.faceLightSide;
+ if (l.sqrMagnitude < 1e-6f) l = Vector3.up;
+ l.Normalize();
+ Shader.SetGlobalVector(kFaceDirId, new Vector4(l.x, l.y, l.z, 0f));
+ }
+
+ static Renderer FindFaceRenderer(WLCharacterBacklightSettings cfg, GameObject root)
+ {
+ if (cfg.faceRendererHints == null) return null;
+ var rends = root.GetComponentsInChildren(true);
+ for (int h = 0; h < cfg.faceRendererHints.Length; h++)
+ {
+ var hint = cfg.faceRendererHints[h];
+ if (string.IsNullOrEmpty(hint)) continue;
+ for (int i = 0; i < rends.Length; i++)
+ {
+ var r = rends[i];
+ if (r == null || r is ParticleSystemRenderer) continue;
+ if (r.name.IndexOf(hint, System.StringComparison.OrdinalIgnoreCase) >= 0) return r;
+ }
+ }
+ return null;
+ }
+
void UpdateBloomScope(WLCharacterBacklightSettings cfg, Camera cam)
{
bool want = cfg != null && cfg.enabled_ != 0 && cfg.bloomEnabled != 0 && cam != null && InScope(cfg);
diff --git a/Assets/WL/Look/Character/WLCharacterBacklightSettings.cs b/Assets/WL/Look/Character/WLCharacterBacklightSettings.cs
index 54cc3dbf9..d3519a4b4 100644
--- a/Assets/WL/Look/Character/WLCharacterBacklightSettings.cs
+++ b/Assets/WL/Look/Character/WLCharacterBacklightSettings.cs
@@ -24,19 +24,29 @@ namespace WL.Look.Character
[Tooltip("재질 이름에 이 글자가 들어 있으면 피부로 보고 피부 투과(SSS)를 켠 재질을 물린다.")]
public string[] skinMaterialHints = { "Skin" };
- [Header("① 역광 림 — 메인 조명(따뜻함)과 반대인 차가운 색")]
- [ColorUsage(false, true)] public Color rimColor = new Color(0.28f, 0.62f, 1f, 1f);
+ [Header("⓪ 본체 밝히기 — 2026-09-17 PD 「캐릭터가 어둡게 보인다 · 원한 건 더 밝고 화사하게」")]
+ [Tooltip("1 = 캐릭터 본체(Toon) 재질 값을 렌더러별 프로퍼티 블록으로 덮는다(재질 에셋 0줄 · 0 = 덮지 않음).")]
+ public int baseBrighten = 1;
+ [Tooltip("그늘의 최소 밝기(원본 0.2). 클수록 그늘이 밝다.")]
+ [Range(0f, 1f)] public float baseMinimumDarkness = 0.38f;
+ [Tooltip("그늘 색(원본 (0.216,0.209,0.371) = 어두운 남색). 밝고 따뜻하게.")]
+ public Color baseShadowColor = new Color(0.46f, 0.38f, 0.44f, 0f);
+ [Tooltip("구름 그늘 세기(원본은 지형과 같은 값). 0 = 캐릭터는 구름 그늘을 안 받는다(항상 화사). 음수 = 재질 값 그대로.")]
+ public float baseCloudStrength = 0f;
+
+ [Header("① 역광 림 — 2026-09-17 PD 「너무 푸른색 느낌」 → 태양색 계열의 따뜻한 흰빛(차가운 하늘색 (0.28,0.62,1) 에서 변경) · 「라이팅이 과하다」 → 약하게")]
+ [ColorUsage(false, true)] public Color rimColor = new Color(1f, 0.96f, 0.88f, 1f);
[Tooltip("림 밝기 배수(1 초과 = HDR · Bloom 이 집어낸다).")]
- public float rimStrength = 1.45f;
+ public float rimStrength = 1.0f;
[Header("①-b 화면 공간 깊이 림(기본) — 실루엣 바깥쪽에만 · 화면 픽셀 굵기 일정")]
[Tooltip("1 = 깊이 림 사용(URP 에셋의 Depth Texture 가 꺼져 있으면 자동으로 프레넬만 쓴다). 실측: 프레넬만 쓰면 머리카락 안쪽 가닥마다 줄이 생긴다.")]
public int rimUseDepth = 1;
[Tooltip("띠 굵기 = 화면 짧은 변 × 이 값(0.0045 → 1080 기준 ≈ 5 px).")]
- [Range(0f, 0.02f)] public float rimWidth = 0.0045f;
+ [Range(0f, 0.02f)] public float rimWidth = 0.003f;
[Tooltip("역광 쪽 이웃 픽셀이 이만큼(m) 더 멀면 실루엣으로 본다(작으면 머리카락 가닥에도 생긴다).")]
public float rimDepthThreshold = 0.12f;
[Tooltip("깊이 림에 섞는 프레넬 림의 양 — 가까울 때(두께감).")]
- [Range(0f, 1f)] public float rimFresnelNear = 0.6f;
+ [Range(0f, 1f)] public float rimFresnelNear = 0.25f;
[Tooltip("깊이 림에 섞는 프레넬 림의 양 — 멀 때(0 = 깊이 림만 · 가닥 줄 없음).")]
[Range(0f, 1f)] public float rimFresnelFar = 0f;
@@ -54,7 +64,7 @@ namespace WL.Look.Character
[Tooltip("0 = 둘레 전부 · 1 = 가상 역광 쪽 윤곽만.")]
[Range(0f, 1f)] public float rimDirectional = 0.85f;
[Tooltip("림이 바탕을 덮는 정도(1 = 림 색 그대로 · 0 = 순수 가산). 실측: 순수 가산은 노란 머리·살색 위에서 흰 테가 돼 차가운 색이 안 보였다.")]
- [Range(0f, 1f)] public float rimOpacity = 0.85f;
+ [Range(0f, 1f)] public float rimOpacity = 0.5f;
[Header("가상 역광 위치(카메라 기준) — 캐릭터 뒤 + 위 + 옆")]
[Tooltip("위쪽 성분(클수록 머리·어깨 윗선에 맺힌다).")]
@@ -63,23 +73,40 @@ namespace WL.Look.Character
public float backlightSide = 0.8f;
[Header("② 라이트 랩 — 빛이 안쪽으로 스며드는 옅은 번짐")]
- [ColorUsage(false, true)] public Color wrapColor = new Color(0.75f, 0.85f, 1f, 1f);
- public float wrapStrength = 0.12f;
+ [ColorUsage(false, true)] public Color wrapColor = new Color(1f, 0.95f, 0.88f, 1f);
+ public float wrapStrength = 0.06f;
public float wrapPower = 1.6f;
[Header("③ 피부 투과(SSS 흉내) — 피부 재질에만")]
[ColorUsage(false, true)] public Color sssColor = new Color(1f, 0.32f, 0.22f, 1f);
- public float sssStrength = 1.5f;
+ public float sssStrength = 1.0f;
public float sssPower = 1.7f;
- [Header("④ 번짐(Bloom) — 역광이 화사하게 번지게(전체 화면 패스 · 모바일 비용 있음)")]
+ [Header("④ 얼굴 포인트 라이트 — 얼굴 주변만 살짝 밝히는 빛(2026-09-17 PD 「얼굴 쪽을 살짝 포인트로 하이라이트」)")]
+ [Tooltip("0 = 끔.")]
+ public int faceEnabled = 1;
+ [ColorUsage(false, true)] public Color faceColor = new Color(1f, 0.93f, 0.85f, 1f);
+ [Tooltip("밝기(가산). 0.2 = 은은 · 0.5 = 뚜렷.")]
+ public float faceStrength = 0.16f;
+ [Tooltip("빛이 닿는 반지름 = 얼굴(머리) 렌더러 경계 크기 × 이 값. 3 = 얼굴에서 가장 밝고 발끝으로 갈수록 옅어지는 몸 전체 채움빛(「밝고 화사」).")]
+ public float faceRadiusScale = 3.0f;
+ [Tooltip("0 = 빛 방향을 향한 면만 · 1 = 둥글게 감싸는 빛.")]
+ [Range(0f, 1f)] public float faceWrap = 0.8f;
+ [Tooltip("빛 위치(카메라 기준): 위쪽 성분.")]
+ public float faceLightUp = 0.6f;
+ [Tooltip("빛 위치(카메라 기준): 옆 성분(− = 화면 왼쪽 · 역광의 반대쪽).")]
+ public float faceLightSide = -0.4f;
+ [Tooltip("얼굴 중심으로 쓸 렌더러 이름 힌트(앞에서부터 찾는다 · 못 찾으면 얼굴 빛 없음).")]
+ public string[] faceRendererHints = { "Face", "Head" };
+
+ [Header("⑤ 번짐(Bloom) — 역광이 화사하게 번지게(전체 화면 패스 · 모바일 비용 있음)")]
[Tooltip("0 = 카메라·볼륨을 건드리지 않는다.")]
public int bloomEnabled = 1;
[Tooltip("이 씬이 떠 있을 때만 켠다(전투 맵의 기존 후처리는 그대로).")]
public string[] bloomScenes = { "Level01" };
[Tooltip("이 밝기 이상만 번진다(HDR 1 초과 = 림·반짝임만).")]
- public float bloomThreshold = 1.05f;
- public float bloomIntensity = 0.55f;
+ public float bloomThreshold = 1.2f;
+ public float bloomIntensity = 0.4f;
[Range(0f, 1f)] public float bloomScatter = 0.55f;
[Tooltip("1 = 카메라 HDR 을 켠다(림만 골라 번지게 · 끄면 흰 거품·밝은 모래도 같이 번진다).")]
public int bloomHdr = 1;