2026-09-17 11:00:11 +00:00
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
// WLCharacterBacklight.cs — 캐릭터 역광 패스 덧붙이기 + 가상 역광 방향 갱신 + 번짐(Bloom) 스코프
|
|
|
|
|
//
|
|
|
|
|
// 2026-09-17 PD 「우리 게임의 캐릭터가 조금 더 돋보이게 · 적절한 방법을 검토해 비주얼 완성도 강화」.
|
|
|
|
|
//
|
|
|
|
|
// ■ 어디서 불리나
|
|
|
|
|
// `WLCharacterLook.Apply(root)` 끝(= LH_M05 프리팹의 모든 인스턴스 · 섬 교체 모델 · 던전 PC)에서 `Apply(root)`.
|
|
|
|
|
// 재질 배열 **뒤에** `WL/CharBacklight` 런타임 재질 1장을 덧붙인다(서브메시 1개 = 메시 전체가 가산으로 한 번 더 그려진다).
|
|
|
|
|
// 피부 재질(이름 힌트)은 피부 투과를 켠 재질을, 나머지는 끈 재질을 쓴다. 몇 번 불러도 결과가 같다(멱등).
|
|
|
|
|
//
|
|
|
|
|
// ■ 가상 역광 방향
|
|
|
|
|
// 드라이버(숨은 오브젝트 1개)가 LateUpdate 마다 카메라에서 「뒤 + 위 + 옆」 방향을 계산해 전역 `_WLBacklightDir` 에 쓴다.
|
|
|
|
|
//
|
|
|
|
|
// ■ 번짐(Bloom)
|
|
|
|
|
// 설정의 씬이 떠 있을 때만 전역 Volume(Bloom 1개) + 메인 카메라 후처리/HDR 을 켜고, 벗어나면 원래 값으로 되돌린다.
|
|
|
|
|
//
|
|
|
|
|
// 🔴 본체 재질·셰이더 그래프·프리팹 0줄 · Debug.LogError / Debug.LogException 을 쓰지 않는다(813z 규칙).
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
using UnityEngine.Rendering;
|
|
|
|
|
using UnityEngine.Rendering.Universal;
|
|
|
|
|
using UnityEngine.SceneManagement;
|
|
|
|
|
|
|
|
|
|
namespace WL.Look.Character
|
|
|
|
|
{
|
|
|
|
|
public static class WLCharacterBacklight
|
|
|
|
|
{
|
|
|
|
|
// ── 진단(프로브가 읽는다)
|
|
|
|
|
public static int AppliedRenderers, SkinRenderers, MultiSubmeshRenderers;
|
|
|
|
|
public static string LastLog = "";
|
|
|
|
|
|
2026-09-17 13:45:45 +00:00
|
|
|
static Material s_rim, s_rimSkin, s_rimT2;
|
2026-09-17 11:00:11 +00:00
|
|
|
static readonly List<Renderer> s_buf = new List<Renderer>(16);
|
|
|
|
|
static readonly List<Material> s_tmp = new List<Material>(4);
|
|
|
|
|
|
2026-09-17 13:45:45 +00:00
|
|
|
public static bool IsOurs(Material m) { return m != null && (m == s_rim || m == s_rimSkin || m == s_rimT2); }
|
2026-09-17 11:00:11 +00:00
|
|
|
|
|
|
|
|
/// <summary>root 아래 캐릭터 렌더러에 역광 패스를 덧붙인다(설정이 꺼져 있으면 떼어낸다).</summary>
|
|
|
|
|
public static void Apply(GameObject root)
|
|
|
|
|
{
|
|
|
|
|
if (root == null) return;
|
|
|
|
|
var cfg = WLCharacterBacklightSettings.Instance;
|
|
|
|
|
if (cfg == null) return;
|
|
|
|
|
if (cfg.enabled_ == 0) { Remove(root); return; }
|
|
|
|
|
if (!EnsureMaterials(cfg)) { LastLog = "셰이더 WL/CharBacklight 없음 — 적용 0"; return; }
|
|
|
|
|
WLCharacterBacklightDriver.Ensure();
|
|
|
|
|
WLCharacterBacklightDriver.Register(root); // 나중에 붙는 무기·장비 렌더러까지 덮도록 2 s 마다 다시 맞춘다(실측: 던전 PC 의 무기 2개는 Awake 뒤에 붙는다)
|
|
|
|
|
|
|
|
|
|
int applied = 0, skin = 0, multi = 0;
|
|
|
|
|
s_buf.Clear();
|
|
|
|
|
root.GetComponentsInChildren<Renderer>(true, s_buf);
|
|
|
|
|
for (int i = 0; i < s_buf.Count; i++)
|
|
|
|
|
{
|
|
|
|
|
var r = s_buf[i];
|
|
|
|
|
if (r == null || r is ParticleSystemRenderer || r is TrailRenderer || r is LineRenderer) continue;
|
|
|
|
|
var mats = r.sharedMaterials;
|
|
|
|
|
if (mats == null || mats.Length == 0) continue;
|
|
|
|
|
|
|
|
|
|
bool match = false, isSkin = false;
|
|
|
|
|
s_tmp.Clear();
|
|
|
|
|
for (int m = 0; m < mats.Length; m++)
|
|
|
|
|
{
|
|
|
|
|
var mat = mats[m];
|
|
|
|
|
if (IsOurs(mat)) continue; // 이미 붙은 우리 재질은 걷어 내고 다시 붙인다(멱등)
|
|
|
|
|
s_tmp.Add(mat);
|
|
|
|
|
if (mat == null || mat.shader == null) continue;
|
|
|
|
|
if (string.IsNullOrEmpty(cfg.shaderNameFilter) || mat.shader.name == cfg.shaderNameFilter) match = true;
|
|
|
|
|
if (IsSkinName(cfg, mat.name)) isSkin = true;
|
|
|
|
|
}
|
|
|
|
|
if (!match)
|
|
|
|
|
{
|
|
|
|
|
if (s_tmp.Count != mats.Length) r.sharedMaterials = s_tmp.ToArray(); // 대상이 아닌데 붙어 있었으면 뗀다
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var want = isSkin ? s_rimSkin : s_rim;
|
|
|
|
|
bool same = mats.Length == s_tmp.Count + 1 && mats[mats.Length - 1] == want;
|
|
|
|
|
if (!same)
|
|
|
|
|
{
|
|
|
|
|
s_tmp.Add(want);
|
|
|
|
|
r.sharedMaterials = s_tmp.ToArray();
|
|
|
|
|
}
|
2026-09-17 13:23:02 +00:00
|
|
|
BrightenBase(cfg, r, s_tmp.Count > 0 ? s_tmp[0] : null, cfg.baseBrighten != 0);
|
2026-09-17 11:00:11 +00:00
|
|
|
applied++; if (isSkin) skin++;
|
|
|
|
|
if (SubMeshCount(r) > 1) multi++; // 서브메시가 여럿이면 마지막 서브메시만 덧그려진다(진단용)
|
|
|
|
|
}
|
|
|
|
|
AppliedRenderers = applied; SkinRenderers = skin; MultiSubmeshRenderers = multi;
|
|
|
|
|
LastLog = "역광 패스 " + applied + "개(피부 " + skin + " · 다중 서브메시 " + multi + ") @" + root.name;
|
|
|
|
|
if (cfg.verboseLog != 0) Debug.Log("[WL CharBacklight] " + LastLog);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>root 아래에서 우리 재질을 전부 뗀다.</summary>
|
|
|
|
|
public static void Remove(GameObject root)
|
|
|
|
|
{
|
|
|
|
|
if (root == null) return;
|
|
|
|
|
s_buf.Clear();
|
|
|
|
|
root.GetComponentsInChildren<Renderer>(true, s_buf);
|
|
|
|
|
for (int i = 0; i < s_buf.Count; i++)
|
|
|
|
|
{
|
|
|
|
|
var r = s_buf[i]; if (r == null) continue;
|
|
|
|
|
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]);
|
2026-09-17 13:23:02 +00:00
|
|
|
if (s_tmp.Count != mats.Length)
|
|
|
|
|
{
|
|
|
|
|
r.sharedMaterials = s_tmp.ToArray();
|
|
|
|
|
BrightenBase(null, r, s_tmp.Count > 0 ? s_tmp[0] : null, false); // 우리가 붙였던 렌더러만 본체 값도 되돌린다
|
2026-09-17 13:45:45 +00:00
|
|
|
RestoreMkTint(r, s_tmp.Count > 0 ? s_tmp[0] : null);
|
2026-09-17 15:02:13 +00:00
|
|
|
s_overlayRends.Remove(r);
|
2026-09-17 13:23:02 +00:00
|
|
|
}
|
2026-09-17 11:00:11 +00:00
|
|
|
}
|
|
|
|
|
LastLog = "역광 패스 제거 @" + root.name;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-17 13:23:02 +00:00
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
|
|
|
// 본체 밝히기 — 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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-17 13:45:45 +00:00
|
|
|
/// <summary>2등급 본체 색(MK Toon 그늘 색)을 재질 원래 값으로 되쓴다.</summary>
|
|
|
|
|
static void RestoreMkTint(Renderer r, Material baseMat)
|
|
|
|
|
{
|
|
|
|
|
if (r == null || baseMat == null || !baseMat.HasProperty(kGoochDark)) return;
|
|
|
|
|
if (s_mpb == null) s_mpb = new MaterialPropertyBlock();
|
|
|
|
|
r.GetPropertyBlock(s_mpb);
|
|
|
|
|
s_mpb.SetColor(kGoochDark, baseMat.GetColor(kGoochDark));
|
|
|
|
|
r.SetPropertyBlock(s_mpb);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
|
|
|
// 등급 — 2026-09-17 PD 「톤앤매너는 전체가 공유 · 플레이어 1등급 · 몬스터와 NPC 2등급 · 배경(프랍) 3등급」
|
|
|
|
|
// 공유하는 것 = 빛 방향(가상 역광·채움빛) · 림 색 · 그늘 색 계열(따뜻한 보랏빛). 등급은 **세기**만 다르다.
|
|
|
|
|
// 2등급 = 같은 덧그리기 패스(림 약하게 + 몸 전체 채움빛 약하게) + 본체 그늘을 조금 밝게(Toon = 그늘 최소 밝기/색 · MK Toon = _GoochDarkColor)
|
|
|
|
|
// 3등급 = 덧그리기 없음(드로우콜 0 추가) · 프랍이 공유하는 Toon 재질의 **런타임 복사본**에 그늘 값만 살짝(재질당 1장 → 배칭 유지)
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
|
|
|
public static int Tier2Roots, Tier2Renderers, Tier3Renderers, Tier3Materials;
|
|
|
|
|
static readonly int kGoochDark = Shader.PropertyToID("_GoochDarkColor");
|
2026-09-17 15:02:13 +00:00
|
|
|
static readonly HashSet<Renderer> s_overlayRends = new HashSet<Renderer>();
|
|
|
|
|
|
|
|
|
|
/// <summary>이 렌더러에 우리 덧그리기 패스가 붙어 있는가(피격 하이라이트가 덮기 방식으로 그릴 수 있는가).</summary>
|
|
|
|
|
public static bool HasOverlay(Renderer r) { return r != null && s_overlayRends.Contains(r); }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 몹 피격 점멸(`MobHitFlash`)이 렌더러의 프로퍼티 블록을 통째로 다시 쓸 때 불러, 2등급 본체 그늘 색을 **같은 블록**에 같이 써 넣는다
|
|
|
|
|
/// (따로 쓰면 맞을 때마다 지워져 그늘 색이 깜빡인다 · 블록의 소유자는 MobHitFlash 하나).
|
|
|
|
|
/// </summary>
|
|
|
|
|
public static void WriteTier2Props(Renderer r, MaterialPropertyBlock mpb)
|
|
|
|
|
{
|
|
|
|
|
if (r == null || mpb == null || !s_overlayRends.Contains(r)) return;
|
|
|
|
|
var cfg = WLCharacterBacklightSettings.Instance;
|
|
|
|
|
if (cfg == null || cfg.enabled_ == 0 || cfg.tiersEnabled == 0) return;
|
|
|
|
|
var baseMat = r.sharedMaterial;
|
|
|
|
|
if (baseMat == null) return;
|
|
|
|
|
if (baseMat.HasProperty(kGoochDark)) mpb.SetColor(kGoochDark, cfg.tier2MkShadowColor);
|
|
|
|
|
if (baseMat.HasProperty(kMinDark) && baseMat.HasProperty(kShadowCol))
|
|
|
|
|
{
|
|
|
|
|
mpb.SetFloat(kMinDark, Mathf.Clamp01(baseMat.GetFloat(kMinDark) + cfg.tier2MinimumDarknessAdd));
|
|
|
|
|
mpb.SetColor(kShadowCol, Color.Lerp(baseMat.GetColor(kShadowCol), cfg.tier2ShadowColor, cfg.tier2ShadowBlend));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-17 13:45:45 +00:00
|
|
|
|
|
|
|
|
public static void ApplyTier2(GameObject root)
|
|
|
|
|
{
|
|
|
|
|
if (root == null) return;
|
|
|
|
|
var cfg = WLCharacterBacklightSettings.Instance;
|
|
|
|
|
if (cfg == null) return;
|
|
|
|
|
if (cfg.enabled_ == 0 || cfg.tiersEnabled == 0) { Remove(root); return; }
|
|
|
|
|
if (!EnsureMaterials(cfg)) return;
|
|
|
|
|
|
|
|
|
|
int n = 0;
|
|
|
|
|
s_buf.Clear();
|
|
|
|
|
root.GetComponentsInChildren<Renderer>(false, s_buf);
|
|
|
|
|
for (int i = 0; i < s_buf.Count; i++)
|
|
|
|
|
{
|
|
|
|
|
var r = s_buf[i];
|
|
|
|
|
if (r == null || !(r is SkinnedMeshRenderer || r is MeshRenderer)) continue;
|
|
|
|
|
var mats = r.sharedMaterials;
|
|
|
|
|
if (mats == null || mats.Length == 0) continue;
|
|
|
|
|
s_tmp.Clear();
|
|
|
|
|
Material baseMat = null; bool hasT2 = false;
|
|
|
|
|
for (int m = 0; m < mats.Length; m++)
|
|
|
|
|
{
|
|
|
|
|
if (IsOurs(mats[m])) { if (mats[m] == s_rimT2) hasT2 = true; continue; }
|
|
|
|
|
s_tmp.Add(mats[m]);
|
|
|
|
|
if (baseMat == null && mats[m] != null) baseMat = mats[m];
|
|
|
|
|
}
|
|
|
|
|
if (baseMat == null || baseMat.renderQueue >= 2500) continue; // 불투명 본체만(반투명 이펙트 메시 제외)
|
|
|
|
|
if (!(hasT2 && mats.Length == s_tmp.Count + 1))
|
|
|
|
|
{
|
|
|
|
|
s_tmp.Add(s_rimT2);
|
|
|
|
|
r.sharedMaterials = s_tmp.ToArray();
|
|
|
|
|
}
|
|
|
|
|
TintTier2Base(cfg, r, baseMat);
|
2026-09-17 15:02:13 +00:00
|
|
|
s_overlayRends.Add(r);
|
2026-09-17 13:45:45 +00:00
|
|
|
n++;
|
|
|
|
|
}
|
2026-09-17 15:02:13 +00:00
|
|
|
if (s_overlayRends.Count > 512) s_overlayRends.RemoveWhere(x => x == null);
|
2026-09-17 13:45:45 +00:00
|
|
|
Tier2Renderers += n;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void TintTier2Base(WLCharacterBacklightSettings cfg, Renderer r, Material baseMat)
|
|
|
|
|
{
|
|
|
|
|
if (s_mpb == null) s_mpb = new MaterialPropertyBlock();
|
|
|
|
|
bool mk = baseMat.HasProperty(kGoochDark);
|
|
|
|
|
bool toon = baseMat.HasProperty(kMinDark) && baseMat.HasProperty(kShadowCol);
|
|
|
|
|
if (!mk && !toon) return;
|
|
|
|
|
r.GetPropertyBlock(s_mpb);
|
|
|
|
|
if (mk) s_mpb.SetColor(kGoochDark, cfg.tier2MkShadowColor);
|
|
|
|
|
if (toon)
|
|
|
|
|
{
|
|
|
|
|
s_mpb.SetFloat(kMinDark, Mathf.Clamp01(baseMat.GetFloat(kMinDark) + cfg.tier2MinimumDarknessAdd));
|
|
|
|
|
s_mpb.SetColor(kShadowCol, Color.Lerp(baseMat.GetColor(kShadowCol), cfg.tier2ShadowColor, cfg.tier2ShadowBlend));
|
|
|
|
|
}
|
|
|
|
|
r.SetPropertyBlock(s_mpb);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── 3등급(프랍) — 공유 재질 복사본으로 갈아끼운다
|
|
|
|
|
static readonly Dictionary<Material, Material> s_t3Copy = new Dictionary<Material, Material>();
|
|
|
|
|
static readonly HashSet<Material> s_t3Made = new HashSet<Material>();
|
|
|
|
|
static readonly HashSet<int> s_t3Seen = new HashSet<int>();
|
|
|
|
|
struct T3State { public Renderer r; public Material[] mats; }
|
|
|
|
|
static readonly List<T3State> s_t3Swapped = new List<T3State>(256);
|
|
|
|
|
|
|
|
|
|
/// <summary>3등급으로 갈아끼운 프랍 재질을 전부 원래대로(설정을 끄면 다음 검색 때 자동으로 불린다).</summary>
|
|
|
|
|
public static void RestoreTier3()
|
|
|
|
|
{
|
|
|
|
|
for (int i = 0; i < s_t3Swapped.Count; i++) if (s_t3Swapped[i].r != null) s_t3Swapped[i].r.sharedMaterials = s_t3Swapped[i].mats;
|
|
|
|
|
s_t3Swapped.Clear(); s_t3Seen.Clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static void ScanTier3()
|
|
|
|
|
{
|
|
|
|
|
var cfg = WLCharacterBacklightSettings.Instance;
|
|
|
|
|
if (cfg == null) return;
|
|
|
|
|
if (cfg.enabled_ == 0 || cfg.tiersEnabled == 0 || cfg.tier3ShaderNames == null || cfg.tier3ShaderNames.Length == 0)
|
|
|
|
|
{ if (s_t3Swapped.Count > 0) RestoreTier3(); return; }
|
|
|
|
|
if (s_t3Seen.Count > 20000) s_t3Seen.Clear();
|
|
|
|
|
for (int i = s_t3Swapped.Count - 1; i >= 0; i--) if (s_t3Swapped[i].r == null) s_t3Swapped.RemoveAt(i); // 씬이 내려가 사라진 것
|
|
|
|
|
var rends = Object.FindObjectsByType<MeshRenderer>(FindObjectsSortMode.None);
|
|
|
|
|
int swapped = 0;
|
|
|
|
|
for (int i = 0; i < rends.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
var r = rends[i];
|
|
|
|
|
if (r == null || !s_t3Seen.Add(r.GetInstanceID())) continue;
|
|
|
|
|
var mats = r.sharedMaterials;
|
|
|
|
|
if (mats == null || mats.Length == 0) continue;
|
|
|
|
|
bool any = false;
|
|
|
|
|
for (int m = 0; m < mats.Length; m++) if (IsTier3Source(cfg, mats[m])) { any = true; break; }
|
|
|
|
|
if (!any) continue;
|
|
|
|
|
// 캐릭터·몬스터·동물에 달린 것은 1·2등급이 맡는다
|
|
|
|
|
if (r.GetComponentInParent<Actor>() != null || r.GetComponentInParent<WLCharacterLook>() != null
|
|
|
|
|
|| r.GetComponentInParent<CryingSnow.FarmingIsland.PlayerController>() != null
|
|
|
|
|
|| r.GetComponentInParent<CryingSnow.FarmingIsland.AnimalController>() != null) continue;
|
|
|
|
|
s_t3Swapped.Add(new T3State { r = r, mats = (Material[])mats.Clone() });
|
|
|
|
|
for (int m = 0; m < mats.Length; m++) if (IsTier3Source(cfg, mats[m])) mats[m] = Tier3Copy(cfg, mats[m]);
|
|
|
|
|
r.sharedMaterials = mats;
|
|
|
|
|
swapped++;
|
|
|
|
|
}
|
|
|
|
|
Tier3Renderers += swapped; Tier3Materials = s_t3Copy.Count;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool IsTier3Source(WLCharacterBacklightSettings cfg, Material m)
|
|
|
|
|
{
|
|
|
|
|
if (m == null || m.shader == null || s_t3Made.Contains(m) || IsOurs(m)) return false;
|
|
|
|
|
if (!m.HasProperty(kMinDark) || !m.HasProperty(kShadowCol)) return false;
|
|
|
|
|
var n = m.shader.name;
|
|
|
|
|
for (int i = 0; i < cfg.tier3ShaderNames.Length; i++) if (cfg.tier3ShaderNames[i] == n) return true;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static Material Tier3Copy(WLCharacterBacklightSettings cfg, Material src)
|
|
|
|
|
{
|
|
|
|
|
Material c;
|
|
|
|
|
if (s_t3Copy.TryGetValue(src, out c) && c != null) return c;
|
|
|
|
|
c = new Material(src) { name = src.name + " (WL tier3)" };
|
|
|
|
|
c.SetFloat(kMinDark, Mathf.Clamp01(src.GetFloat(kMinDark) + cfg.tier3MinimumDarknessAdd));
|
|
|
|
|
c.SetColor(kShadowCol, Color.Lerp(src.GetColor(kShadowCol), cfg.tier3ShadowColor, cfg.tier3ShadowBlend));
|
|
|
|
|
s_t3Copy[src] = c; s_t3Made.Add(c);
|
|
|
|
|
return c;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>설정 값을 런타임 재질에 다시 밀어 넣는다(프로브에서 값을 바꾼 뒤 호출).</summary>
|
2026-09-17 11:00:11 +00:00
|
|
|
public static void RefreshValues()
|
|
|
|
|
{
|
|
|
|
|
var cfg = WLCharacterBacklightSettings.Instance;
|
|
|
|
|
if (cfg == null) return;
|
|
|
|
|
if (s_rim != null) Push(cfg, s_rim, false);
|
|
|
|
|
if (s_rimSkin != null) Push(cfg, s_rimSkin, true);
|
2026-09-17 13:45:45 +00:00
|
|
|
if (s_rimT2 != null) PushTier2(cfg, s_rimT2);
|
2026-09-17 11:00:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool EnsureMaterials(WLCharacterBacklightSettings cfg)
|
|
|
|
|
{
|
2026-09-17 13:45:45 +00:00
|
|
|
if (s_rim != null && s_rimSkin != null && s_rimT2 != null) return true;
|
2026-09-17 11:00:11 +00:00
|
|
|
var sh = Shader.Find("WL/CharBacklight");
|
|
|
|
|
if (sh == null) return false;
|
|
|
|
|
if (s_rim == null) s_rim = new Material(sh) { name = "WL_CharBacklight(runtime)" };
|
|
|
|
|
if (s_rimSkin == null) s_rimSkin = new Material(sh) { name = "WL_CharBacklight_Skin(runtime)" };
|
|
|
|
|
Push(cfg, s_rim, false);
|
|
|
|
|
Push(cfg, s_rimSkin, true);
|
2026-09-17 13:45:45 +00:00
|
|
|
if (s_rimT2 == null) s_rimT2 = new Material(sh) { name = "WL_CharBacklight_Tier2(runtime)" };
|
|
|
|
|
PushTier2(cfg, s_rimT2);
|
2026-09-17 11:00:11 +00:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-17 13:45:45 +00:00
|
|
|
/// <summary>2등급(몬스터·NPC) 재질 = 1등급과 같은 색·방향(톤 공유)에 세기만 낮춘다 · 얼굴빛/피부 투과 없음 · 몸 전체 채움빛 약하게.</summary>
|
|
|
|
|
static void PushTier2(WLCharacterBacklightSettings c, Material m)
|
|
|
|
|
{
|
|
|
|
|
Push(c, m, false);
|
|
|
|
|
m.SetFloat("_RimStrength", c.tier2RimStrength);
|
|
|
|
|
m.SetFloat("_RimOpacity", c.tier2RimOpacity);
|
|
|
|
|
m.SetFloat("_WrapStrength", c.wrapStrength * 0.5f);
|
|
|
|
|
m.SetFloat("_FaceStrength", 0f);
|
|
|
|
|
m.SetFloat("_FillStrength", c.tier2FillStrength);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-17 11:00:11 +00:00
|
|
|
static void Push(WLCharacterBacklightSettings c, Material m, bool skin)
|
|
|
|
|
{
|
|
|
|
|
m.SetColor("_RimColor", c.rimColor);
|
|
|
|
|
m.SetFloat("_RimStrength", c.rimStrength);
|
|
|
|
|
m.SetFloat("_RimThreshold", c.rimThreshold);
|
|
|
|
|
m.SetFloat("_RimThresholdFar", c.rimThresholdFar);
|
|
|
|
|
m.SetFloat("_RimNearDist", c.rimNearDistance);
|
|
|
|
|
m.SetFloat("_RimFarDist", c.rimFarDistance);
|
|
|
|
|
m.SetFloat("_RimSoftness", c.rimSoftness);
|
|
|
|
|
m.SetFloat("_RimDirectional", c.rimDirectional);
|
|
|
|
|
m.SetFloat("_RimOpacity", c.rimOpacity);
|
|
|
|
|
// 깊이 림은 URP 에셋이 카메라 깊이 텍스처를 만들 때만(꺼져 있으면 샘플 값이 쓰레기 → 프레넬만)
|
|
|
|
|
var urp = GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset;
|
|
|
|
|
bool depthOk = c.rimUseDepth != 0 && urp != null && urp.supportsCameraDepthTexture;
|
|
|
|
|
m.SetFloat("_UseDepthRim", depthOk ? 1f : 0f);
|
|
|
|
|
m.SetFloat("_RimWidth", c.rimWidth);
|
|
|
|
|
m.SetFloat("_RimDepthThreshold", c.rimDepthThreshold);
|
|
|
|
|
m.SetFloat("_FresnelRimNear", c.rimFresnelNear);
|
|
|
|
|
m.SetFloat("_FresnelRimFar", c.rimFresnelFar);
|
|
|
|
|
m.SetColor("_WrapColor", c.wrapColor);
|
|
|
|
|
m.SetFloat("_WrapStrength", c.wrapStrength);
|
|
|
|
|
m.SetFloat("_WrapPower", c.wrapPower);
|
|
|
|
|
m.SetColor("_SssColor", c.sssColor);
|
|
|
|
|
m.SetFloat("_SssStrength", skin ? c.sssStrength : 0f);
|
|
|
|
|
m.SetFloat("_SssPower", c.sssPower);
|
2026-09-17 13:23:02 +00:00
|
|
|
m.SetColor("_FaceColor", c.faceColor);
|
|
|
|
|
m.SetFloat("_FaceStrength", c.faceEnabled != 0 ? c.faceStrength : 0f);
|
|
|
|
|
m.SetFloat("_FaceWrap", c.faceWrap);
|
2026-09-17 11:00:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool IsSkinName(WLCharacterBacklightSettings c, string matName)
|
|
|
|
|
{
|
|
|
|
|
if (c.skinMaterialHints == null || string.IsNullOrEmpty(matName)) return false;
|
|
|
|
|
for (int i = 0; i < c.skinMaterialHints.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
var h = c.skinMaterialHints[i];
|
|
|
|
|
if (!string.IsNullOrEmpty(h) && matName.IndexOf(h, System.StringComparison.OrdinalIgnoreCase) >= 0) return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int SubMeshCount(Renderer r)
|
|
|
|
|
{
|
|
|
|
|
var smr = r as SkinnedMeshRenderer;
|
|
|
|
|
if (smr != null) return smr.sharedMesh != null ? smr.sharedMesh.subMeshCount : 0;
|
|
|
|
|
var mf = r.GetComponent<MeshFilter>();
|
|
|
|
|
return mf != null && mf.sharedMesh != null ? mf.sharedMesh.subMeshCount : 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>가상 역광 방향(전역 벡터) 갱신 + 번짐(Bloom) 스코프 — 숨은 오브젝트 1개.</summary>
|
|
|
|
|
public sealed class WLCharacterBacklightDriver : MonoBehaviour
|
|
|
|
|
{
|
|
|
|
|
static WLCharacterBacklightDriver s_inst;
|
|
|
|
|
static readonly int kDirId = Shader.PropertyToID("_WLBacklightDir");
|
|
|
|
|
|
|
|
|
|
// ── 진단
|
|
|
|
|
public static bool BloomHeld;
|
|
|
|
|
public static string BloomLog = "";
|
|
|
|
|
public static Vector3 LastDir;
|
|
|
|
|
|
|
|
|
|
// 번짐 스코프가 바꾼 것(되돌리기용)
|
|
|
|
|
GameObject _volumeGo; VolumeProfile _profile; Bloom _bloom;
|
|
|
|
|
Camera _cam; bool _camPostPrev, _camHdrPrev;
|
|
|
|
|
float _nextScopeCheck;
|
|
|
|
|
|
|
|
|
|
// 다시 맞출 캐릭터 루트(죽은 참조는 훑을 때 걷어 낸다)
|
|
|
|
|
static readonly List<GameObject> s_roots = new List<GameObject>(8);
|
|
|
|
|
float _nextRescan;
|
|
|
|
|
public static int Rescans;
|
|
|
|
|
|
|
|
|
|
public static void Register(GameObject root)
|
|
|
|
|
{
|
|
|
|
|
if (root == null || s_roots.Contains(root)) return;
|
|
|
|
|
s_roots.Add(root);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static void Ensure()
|
|
|
|
|
{
|
|
|
|
|
if (s_inst != null) return;
|
|
|
|
|
// 🔴 HideFlags.DontSave 를 쓰지 않는다 — Play 중 만든 DontSave 오브젝트는 Play 종료 뒤에도 에디터에 남는다(815c 장벽 고아 14개 실증).
|
|
|
|
|
if (!Application.isPlaying) return;
|
|
|
|
|
var go = new GameObject("~WL_CharBacklightDriver");
|
|
|
|
|
DontDestroyOnLoad(go);
|
|
|
|
|
s_inst = go.AddComponent<WLCharacterBacklightDriver>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void LateUpdate()
|
|
|
|
|
{
|
|
|
|
|
var cfg = WLCharacterBacklightSettings.Instance;
|
|
|
|
|
var cam = Camera.main;
|
|
|
|
|
if (cfg != null && cam != null)
|
|
|
|
|
{
|
|
|
|
|
// 카메라 기준 「뒤(카메라가 보는 쪽의 수평 성분) + 위 + 옆」 = 표면에서 가상 역광을 향하는 방향
|
|
|
|
|
var f = cam.transform.forward; f.y = 0f;
|
|
|
|
|
if (f.sqrMagnitude < 1e-4f) f = cam.transform.up; // 수직 내려다보기 예외
|
|
|
|
|
f.Normalize();
|
|
|
|
|
var d = f + Vector3.up * cfg.backlightUp + cam.transform.right * cfg.backlightSide;
|
|
|
|
|
if (d.sqrMagnitude < 1e-6f) d = Vector3.up;
|
|
|
|
|
d.Normalize();
|
|
|
|
|
LastDir = d;
|
|
|
|
|
Shader.SetGlobalVector(kDirId, new Vector4(d.x, d.y, d.z, 1f));
|
2026-09-17 13:23:02 +00:00
|
|
|
UpdateFaceLight(cfg, cam, f);
|
2026-09-17 11:00:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Time.unscaledTime >= _nextScopeCheck)
|
|
|
|
|
{
|
|
|
|
|
_nextScopeCheck = Time.unscaledTime + 0.5f;
|
|
|
|
|
UpdateBloomScope(cfg, cam);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-17 13:45:45 +00:00
|
|
|
if (cfg != null)
|
|
|
|
|
{
|
|
|
|
|
if (Time.unscaledTime >= _nextTier2)
|
|
|
|
|
{
|
|
|
|
|
_nextTier2 = Time.unscaledTime + Mathf.Max(0.5f, cfg.tier2ScanSeconds);
|
|
|
|
|
ScanTier2(cfg);
|
|
|
|
|
}
|
|
|
|
|
if (Time.unscaledTime >= _nextTier3)
|
|
|
|
|
{
|
|
|
|
|
_nextTier3 = Time.unscaledTime + Mathf.Max(1f, cfg.tier3ScanSeconds);
|
|
|
|
|
WLCharacterBacklight.ScanTier3();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-17 11:00:11 +00:00
|
|
|
if (Time.unscaledTime >= _nextRescan)
|
|
|
|
|
{
|
|
|
|
|
_nextRescan = Time.unscaledTime + 2f;
|
|
|
|
|
for (int i = s_roots.Count - 1; i >= 0; i--)
|
|
|
|
|
{
|
|
|
|
|
var root = s_roots[i];
|
|
|
|
|
if (root == null) { s_roots.RemoveAt(i); continue; }
|
|
|
|
|
if (!root.activeInHierarchy) continue;
|
|
|
|
|
WLCharacterBacklight.Apply(root); // 멱등 — 바뀐 게 없으면 재질 배열 대입 0
|
|
|
|
|
Rescans++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-17 13:45:45 +00:00
|
|
|
// ── 등급 검색 — 원본 Actor 를 역할로 나눠 2등급을 붙인다(PC 는 1등급 · 원본 0줄: 읽기만 한다)
|
|
|
|
|
float _nextTier2, _nextTier3;
|
|
|
|
|
static HashSet<string> s_t2Roles; static string[] s_t2RolesSrc;
|
|
|
|
|
public static int Tier2Scans, LastTier2Actors;
|
|
|
|
|
|
|
|
|
|
/// <summary>지금 바로 1·2·3등급을 다시 맞춘다(프로브의 끔/켬 비교용).</summary>
|
|
|
|
|
public static void ForceScan()
|
|
|
|
|
{
|
|
|
|
|
var cfg = WLCharacterBacklightSettings.Instance; if (cfg == null) return;
|
|
|
|
|
for (int i = s_roots.Count - 1; i >= 0; i--) if (s_roots[i] != null && s_roots[i].activeInHierarchy) WLCharacterBacklight.Apply(s_roots[i]);
|
|
|
|
|
ScanTier2(cfg);
|
|
|
|
|
WLCharacterBacklight.ScanTier3();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void ScanTier2(WLCharacterBacklightSettings cfg)
|
|
|
|
|
{
|
|
|
|
|
if (s_t2Roles == null || s_t2RolesSrc != cfg.tier2Roles)
|
|
|
|
|
{
|
|
|
|
|
s_t2RolesSrc = cfg.tier2Roles; s_t2Roles = new HashSet<string>();
|
|
|
|
|
if (cfg.tier2Roles != null) for (int i = 0; i < cfg.tier2Roles.Length; i++) if (!string.IsNullOrEmpty(cfg.tier2Roles[i])) s_t2Roles.Add(cfg.tier2Roles[i]);
|
|
|
|
|
}
|
|
|
|
|
int n = 0;
|
|
|
|
|
var actors = Object.FindObjectsByType<Actor>(FindObjectsSortMode.None);
|
|
|
|
|
for (int i = 0; i < actors.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
var a = actors[i];
|
|
|
|
|
if (a == null || !a.gameObject.activeInHierarchy) continue;
|
|
|
|
|
if (a.m_Role == eRole.PC)
|
|
|
|
|
{
|
|
|
|
|
// 룩 컴포넌트 없는 PC 프리팹도 1등급
|
|
|
|
|
if (!s_roots.Contains(a.gameObject) && a.GetComponentInChildren<WLCharacterLook>() == null) WLCharacterBacklight.Apply(a.gameObject);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (!s_t2Roles.Contains(a.m_Role.ToString())) continue;
|
|
|
|
|
WLCharacterBacklight.ApplyTier2(a.gameObject); n++;
|
|
|
|
|
}
|
|
|
|
|
var animals = Object.FindObjectsByType<CryingSnow.FarmingIsland.AnimalController>(FindObjectsSortMode.None);
|
|
|
|
|
for (int i = 0; i < animals.Length; i++) if (animals[i] != null) { WLCharacterBacklight.ApplyTier2(animals[i].gameObject); n++; }
|
|
|
|
|
LastTier2Actors = n; WLCharacterBacklight.Tier2Roots = n; Tier2Scans++;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-17 13:23:02 +00:00
|
|
|
// ── 얼굴 포인트 라이트 — 카메라에 가장 가까운 등록 캐릭터의 얼굴(머리) 렌더러 경계 중심을 전역 벡터로 넘긴다
|
|
|
|
|
static readonly int kFaceCenterId = Shader.PropertyToID("_WLFaceCenter");
|
|
|
|
|
static readonly int kFaceDirId = Shader.PropertyToID("_WLFaceLightDir");
|
|
|
|
|
static readonly Dictionary<GameObject, Renderer> s_faceRend = new Dictionary<GameObject, Renderer>();
|
|
|
|
|
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<Renderer>(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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-17 11:00:11 +00:00
|
|
|
void UpdateBloomScope(WLCharacterBacklightSettings cfg, Camera cam)
|
|
|
|
|
{
|
|
|
|
|
bool want = cfg != null && cfg.enabled_ != 0 && cfg.bloomEnabled != 0 && cam != null && InScope(cfg);
|
|
|
|
|
if (!want) { ReleaseBloom(); return; }
|
|
|
|
|
|
|
|
|
|
if (_volumeGo == null)
|
|
|
|
|
{
|
|
|
|
|
_volumeGo = new GameObject("~WL_CharBacklight_BloomVolume");
|
|
|
|
|
DontDestroyOnLoad(_volumeGo);
|
|
|
|
|
_profile = ScriptableObject.CreateInstance<VolumeProfile>();
|
|
|
|
|
_bloom = _profile.Add<Bloom>(true);
|
|
|
|
|
var v = _volumeGo.AddComponent<Volume>();
|
|
|
|
|
v.isGlobal = true; v.priority = 50f; v.weight = 1f;
|
|
|
|
|
v.sharedProfile = _profile;
|
|
|
|
|
}
|
|
|
|
|
_bloom.active = true;
|
|
|
|
|
_bloom.threshold.Override(cfg.bloomThreshold);
|
|
|
|
|
_bloom.intensity.Override(cfg.bloomIntensity);
|
|
|
|
|
_bloom.scatter.Override(cfg.bloomScatter);
|
|
|
|
|
_bloom.downscale.Override(cfg.bloomQuarter != 0 ? BloomDownscaleMode.Quarter : BloomDownscaleMode.Half);
|
|
|
|
|
_bloom.maxIterations.Override(Mathf.Clamp(cfg.bloomMaxIterations, 2, 8));
|
|
|
|
|
|
|
|
|
|
if (_cam != cam)
|
|
|
|
|
{
|
|
|
|
|
RestoreCamera();
|
|
|
|
|
var ad = cam.GetComponent<UniversalAdditionalCameraData>();
|
|
|
|
|
if (ad != null)
|
|
|
|
|
{
|
|
|
|
|
_cam = cam; _camPostPrev = ad.renderPostProcessing; _camHdrPrev = cam.allowHDR;
|
|
|
|
|
ad.renderPostProcessing = true;
|
|
|
|
|
if (cfg.bloomHdr != 0) cam.allowHDR = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
BloomHeld = true;
|
|
|
|
|
BloomLog = "번짐 켬 — 카메라 " + (_cam != null ? _cam.name : "없음") + " · 임계 " + cfg.bloomThreshold.ToString("F2") + " · 세기 " + cfg.bloomIntensity.ToString("F2");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool InScope(WLCharacterBacklightSettings cfg)
|
|
|
|
|
{
|
|
|
|
|
if (cfg.bloomScenes == null || cfg.bloomScenes.Length == 0) return false;
|
|
|
|
|
for (int i = 0; i < SceneManager.sceneCount; i++)
|
|
|
|
|
{
|
|
|
|
|
var s = SceneManager.GetSceneAt(i);
|
|
|
|
|
if (!s.isLoaded) continue;
|
|
|
|
|
for (int k = 0; k < cfg.bloomScenes.Length; k++)
|
|
|
|
|
if (!string.IsNullOrEmpty(cfg.bloomScenes[k]) && s.name == cfg.bloomScenes[k]) return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RestoreCamera()
|
|
|
|
|
{
|
|
|
|
|
if (_cam != null)
|
|
|
|
|
{
|
|
|
|
|
var ad = _cam.GetComponent<UniversalAdditionalCameraData>();
|
|
|
|
|
if (ad != null) ad.renderPostProcessing = _camPostPrev;
|
|
|
|
|
_cam.allowHDR = _camHdrPrev;
|
|
|
|
|
}
|
|
|
|
|
_cam = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ReleaseBloom()
|
|
|
|
|
{
|
|
|
|
|
if (!BloomHeld && _volumeGo == null) return;
|
|
|
|
|
RestoreCamera();
|
|
|
|
|
if (_volumeGo != null) Destroy(_volumeGo);
|
|
|
|
|
if (_profile != null) Destroy(_profile);
|
|
|
|
|
_volumeGo = null; _profile = null; _bloom = null;
|
|
|
|
|
BloomHeld = false;
|
|
|
|
|
BloomLog = "번짐 끔(스코프 밖 또는 설정 off)";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void OnDestroy()
|
|
|
|
|
{
|
|
|
|
|
ReleaseBloom();
|
|
|
|
|
if (s_inst == this) s_inst = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|