Project_WL/Assets/WL/Look/Arena/WLReferenceLook.cs

366 lines
18 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// WLReferenceLook.cs — 레퍼런스 느낌 4요소를 씬에 얹는다 (WL-814t · #814)
//
// Ⓐ 외곽선(inverted hull · 화면 픽셀 두께) Ⓑ 명암 대비(Toon 램프) Ⓒ 색조·분위기(Volume+라이트)
// Ⓓ 림라이트(가산 패스)
//
// ■ 원칙
// · 에셋(머티리얼·그래프·프리팹·원본 텍스처)을 **한 글자도 안 바꾼다**.
// 전부 런타임 인스턴스(머티리얼 복제 · 머티리얼 배열 뒤에 덧붙이기 · 임시 Volume)로만 한다.
// · `enabled_ = 0` 이면 아무 것도 하지 않는다 = WL-814s 상태 100 %(C8).
// · 되돌릴 수 있게 바꾸기 전 값을 전부 저장한다(OnDisable 에서 복구).
//
// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(813z 규칙).
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
namespace WL.Look.Arena
{
[DisallowMultipleComponent]
public class WLReferenceLook : MonoBehaviour
{
// ── 진단(프로브가 읽는다)
public static int OutlinedRenderers, RimRenderers, ContrastMaterials;
public static string LastLog = "";
public static bool IsApplied { get { return s_applied; } }
[Tooltip("캐릭터 루트(비우면 이름으로 찾는다).")]
public GameObject characterRoot;
[Tooltip("캐릭터 루트를 못 찾을 때 쓰는 이름.")]
public string characterRootName = "PC_LH_M05";
/// <summary>요소별 on/off + 세기. 캡처 스크립트가 단독 요소 비교에 쓴다.</summary>
public struct Opt
{
public bool outline, contrast, mood, rim;
public float outlineWidth, outlineWidthBackground;
public Color outlineColor;
public bool outlineOnBackground;
public bool outlineScaleWithZoom;
public float outlineRefOrthoSize, outlineDepthBias;
public float outlineTintAmount, outlineTintDarkness;
public float minimumDarkness, shadowColorScale, shadowTintAmount;
public Color shadowTint;
public int charShades;
public float moodStrength;
public float rimStrength, rimPower, rimUpBias;
public Color rimColor;
public bool rimOnBackground;
public static Opt FromSettings(WLReferenceLookSettings c)
{
var o = new Opt();
if (c == null) return o;
o.outline = c.outlineOn; o.contrast = c.contrastOn; o.mood = c.moodOn; o.rim = c.rimOn;
o.outlineWidth = c.outlineWidth; o.outlineWidthBackground = c.outlineWidthBackground;
o.outlineColor = c.outlineColor; o.outlineOnBackground = c.outlineOnBackground;
o.outlineScaleWithZoom = c.outlineScaleWithZoom; o.outlineRefOrthoSize = c.outlineRefOrthoSize;
o.outlineDepthBias = c.outlineDepthBias;
o.outlineTintAmount = c.outlineTintAmount; o.outlineTintDarkness = c.outlineTintDarkness;
o.minimumDarkness = c.minimumDarkness; o.shadowColorScale = c.shadowColorScale;
o.shadowTintAmount = c.shadowTintAmount; o.shadowTint = c.shadowTint;
o.charShades = c.charShades;
o.moodStrength = c.moodStrength;
o.rimStrength = c.rimStrength; o.rimPower = c.rimPower; o.rimUpBias = c.rimUpBias;
o.rimColor = c.rimColor; o.rimOnBackground = c.rimOnBackground;
return o;
}
}
// ── 저장된 원래 상태 ────────────────────────────────────────────────
struct RendState { public Renderer r; public Material[] mats; }
struct LightState { public Light l; public Color c; public float i; }
static readonly List<RendState> s_rends = new List<RendState>(200);
static readonly List<LightState> s_lights = new List<LightState>(8);
static readonly List<Material> s_created = new List<Material>(32);
static GameObject s_volumeGo;
static bool s_applied;
static Color s_ambient; static AmbientMode s_ambMode; static float s_ambIntensity;
static Terrain s_terrain; static Material s_terrainMat;
static Camera s_cam; static bool s_camPost; static bool s_camPostSaved;
static readonly Dictionary<Material, Material> s_clones = new Dictionary<Material, Material>();
const string kToon = "Shader Graphs/Toon";
const string kToonTerrain = "Shader Graphs/ToonTerrain";
void OnEnable()
{
var cfg = WLReferenceLookSettings.Instance;
if (cfg == null || cfg.enabled_ == 0) { LastLog = "enabled_=0 → 적용 0(814s 상태)"; return; }
Apply(Opt.FromSettings(cfg), cfg);
}
void OnDisable() { Restore(); }
// ─────────────────────────────────────────────────────────────────
/// <summary>4요소를 지정한 세기로 얹는다. 이미 얹혀 있으면 먼저 되돌린다(멱등).</summary>
public static void Apply(Opt o, WLReferenceLookSettings cfg)
{
Restore();
if (cfg == null) cfg = WLReferenceLookSettings.Instance;
var charRoot = FindCharacterRoot();
var rends = Object.FindObjectsByType<Renderer>(FindObjectsSortMode.None);
OutlinedRenderers = 0; RimRenderers = 0; ContrastMaterials = 0;
Shader outlineShader = o.outline ? Shader.Find("WL/HullOutline") : null;
// 줌 비례 — 직교 크기가 커지면(멀리서 보면) 화면 픽셀 두께를 같은 비율로 줄인다.
// 그래야 「캐릭터 키 대비 굵기」가 프레이밍과 무관하게 일정하다(2D 스프라이트처럼).
// 기준 = 아레나 실제 카메라(직교 10) → SO 의 px 값이 곧 「인게임에서 보이는 px」다.
float zoom = 1f;
{
var c0 = Camera.main;
if (c0 == null) { var cs0 = Object.FindObjectsByType<Camera>(FindObjectsSortMode.None); if (cs0.Length > 0) c0 = cs0[0]; }
if (o.outlineScaleWithZoom && c0 != null && c0.orthographic && c0.orthographicSize > 0.001f && o.outlineRefOrthoSize > 0.001f)
zoom = o.outlineRefOrthoSize / c0.orthographicSize;
}
s_outlineShader = outlineShader; s_zoom = zoom; s_opt = o;
s_outlines.Clear();
Material rimMat = null;
if (o.rim)
{
var sh = Shader.Find("WL/RimAdd");
if (sh != null)
{
rimMat = Make(sh);
rimMat.SetColor("_RimColor", o.rimColor);
rimMat.SetFloat("_RimPower", o.rimPower);
rimMat.SetFloat("_RimStrength", o.rimStrength);
rimMat.SetFloat("_RimUpBias", o.rimUpBias);
}
}
for (int i = 0; i < rends.Length; i++)
{
var r = rends[i];
if (r == null || r is ParticleSystemRenderer) continue;
var mats = r.sharedMaterials;
if (mats == null || mats.Length == 0 || mats[0] == null) continue;
bool isChar = charRoot != null && r.transform.IsChildOf(charRoot.transform);
// Toon 계열 불투명만 대상(얼굴 Unlit/Transparent · 물은 제외)
bool toon = false;
for (int m = 0; m < mats.Length; m++)
if (mats[m] != null && mats[m].shader != null && mats[m].shader.name == kToon) toon = true;
if (!toon) continue;
var newMats = mats;
bool touched = false;
// Ⓑ 대비 — 머티리얼 복제본으로 교체
if (o.contrast)
{
newMats = (Material[])mats.Clone();
for (int m = 0; m < newMats.Length; m++)
{
if (newMats[m] == null || newMats[m].shader == null || newMats[m].shader.name != kToon) continue;
newMats[m] = CloneForContrast(newMats[m], o, isChar);
touched = true;
}
}
// Ⓐ 외곽선 · Ⓓ 림 — 머티리얼 배열 뒤에 덧붙인다(서브메시 1개면 한 번 더 그린다)
var extra = new List<Material>(2);
if (o.outline && (isChar || o.outlineOnBackground)) extra.Add(OutlineFor(mats[0], isChar));
if (o.rim && rimMat != null && (isChar || o.rimOnBackground)) extra.Add(rimMat);
if (extra.Count > 0 && extra[0] != null)
{
if (!touched) newMats = (Material[])mats.Clone();
var list = new List<Material>(newMats);
for (int e = 0; e < extra.Count; e++) if (extra[e] != null) list.Add(extra[e]);
newMats = list.ToArray();
touched = true;
if (o.outline && (isChar || o.outlineOnBackground)) OutlinedRenderers++;
if (o.rim && rimMat != null && (isChar || o.rimOnBackground)) RimRenderers++;
}
if (touched)
{
s_rends.Add(new RendState { r = r, mats = mats });
r.sharedMaterials = newMats;
}
}
// Ⓑ — 터레인(화면 면적의 대부분)
if (o.contrast)
{
var t = Terrain.activeTerrain;
if (t != null && t.materialTemplate != null && t.materialTemplate.shader != null
&& t.materialTemplate.shader.name == kToonTerrain)
{
s_terrain = t; s_terrainMat = t.materialTemplate;
t.materialTemplate = CloneForContrast(t.materialTemplate, o, false);
}
}
// Ⓒ — 라이트 · 앰비언트 · Volume
if (o.mood && o.moodStrength > 0f)
{
float k = Mathf.Clamp01(o.moodStrength);
s_ambMode = RenderSettings.ambientMode;
s_ambient = RenderSettings.ambientLight;
s_ambIntensity = RenderSettings.ambientIntensity;
var lights = Object.FindObjectsByType<Light>(FindObjectsSortMode.None);
for (int i = 0; i < lights.Length; i++)
{
var l = lights[i];
if (l == null) continue;
s_lights.Add(new LightState { l = l, c = l.color, i = l.intensity });
if (l.type == LightType.Directional)
{
l.color = Color.Lerp(l.color, cfg != null ? cfg.keyLightColor : new Color(1f, .76f, .42f), k);
l.intensity = Mathf.Lerp(l.intensity, cfg != null ? cfg.keyLightIntensity : 1.15f, k);
}
}
RenderSettings.ambientMode = AmbientMode.Flat;
RenderSettings.ambientLight = Color.Lerp(
s_ambMode == AmbientMode.Flat ? s_ambient : RenderSettings.ambientSkyColor,
cfg != null ? cfg.ambientColor : new Color(.259f, .165f, .373f), k);
if (cfg != null && cfg.profile != null && cfg.profile.asset != null)
{
s_volumeGo = new GameObject("WL814t_ReferenceLook_Volume");
s_volumeGo.hideFlags = HideFlags.DontSave;
var v = s_volumeGo.AddComponent<Volume>();
v.isGlobal = true; v.priority = 100f;
v.sharedProfile = cfg.profile.asset;
v.weight = k;
s_cam = Camera.main;
if (s_cam == null) { var cs = Object.FindObjectsByType<Camera>(FindObjectsSortMode.None); if (cs.Length > 0) s_cam = cs[0]; }
if (s_cam != null)
{
var ac = s_cam.GetComponent<UnityEngine.Rendering.Universal.UniversalAdditionalCameraData>();
if (ac != null) { s_camPost = ac.renderPostProcessing; s_camPostSaved = true; ac.renderPostProcessing = true; }
}
}
}
s_applied = true;
LastLog = "Ⓐ" + (o.outline ? OutlinedRenderers.ToString() : "off")
+ " Ⓑ" + (o.contrast ? ContrastMaterials + "mat/md" + o.minimumDarkness.ToString("F2") : "off")
+ " Ⓒ" + (o.mood ? o.moodStrength.ToString("F2") : "off")
+ " Ⓓ" + (o.rim ? RimRenderers.ToString() : "off");
if (cfg != null && cfg.verboseLog != 0) Debug.Log("[WL814t ReferenceLook] " + LastLog);
}
/// <summary>바꾼 것을 전부 원래대로. 몇 번 불러도 안전.</summary>
public static void Restore()
{
for (int i = 0; i < s_rends.Count; i++)
if (s_rends[i].r != null) s_rends[i].r.sharedMaterials = s_rends[i].mats;
s_rends.Clear();
if (s_terrain != null && s_terrainMat != null) s_terrain.materialTemplate = s_terrainMat;
s_terrain = null; s_terrainMat = null;
for (int i = 0; i < s_lights.Count; i++)
if (s_lights[i].l != null) { s_lights[i].l.color = s_lights[i].c; s_lights[i].l.intensity = s_lights[i].i; }
if (s_lights.Count > 0)
{
RenderSettings.ambientMode = s_ambMode;
RenderSettings.ambientLight = s_ambient;
RenderSettings.ambientIntensity = s_ambIntensity;
}
s_lights.Clear();
if (s_volumeGo != null) { DestroyAny(s_volumeGo); s_volumeGo = null; }
if (s_camPostSaved && s_cam != null)
{
var ac = s_cam.GetComponent<UnityEngine.Rendering.Universal.UniversalAdditionalCameraData>();
if (ac != null) ac.renderPostProcessing = s_camPost;
}
s_camPostSaved = false; s_cam = null;
for (int i = 0; i < s_created.Count; i++) if (s_created[i] != null) DestroyAny(s_created[i]);
s_created.Clear();
s_clones.Clear();
s_outlines.Clear();
OutlinedRenderers = 0; RimRenderers = 0; ContrastMaterials = 0;
if (s_applied) LastLog = "restored(814s 상태)";
s_applied = false;
}
// ── helpers ──────────────────────────────────────────────────────
static GameObject FindCharacterRoot()
{
var go = GameObject.Find("PC_LH_M05");
if (go != null) return go;
var anim = Object.FindFirstObjectByType<Animator>();
return anim != null ? anim.gameObject : null;
}
static Shader s_outlineShader;
static float s_zoom = 1f;
static Opt s_opt;
static readonly Dictionary<Material, Material> s_outlines = new Dictionary<Material, Material>();
/// <summary>원본 머티리얼 1종당 외곽선 머티리얼 1장(색 연동을 하려면 BaseMap·DiffuseColor 가 원본별로 달라야 한다).</summary>
static Material OutlineFor(Material src, bool isChar)
{
if (s_outlineShader == null) return null;
Material m;
if (s_outlines.TryGetValue(src, out m) && m != null) return m;
m = Make(s_outlineShader);
m.SetColor("_OutlineColor", s_opt.outlineColor);
m.SetFloat("_OutlineWidth", (isChar ? s_opt.outlineWidth : s_opt.outlineWidthBackground) * s_zoom);
m.SetFloat("_OutlineDepthBias", s_opt.outlineDepthBias);
m.SetFloat("_TintAmount", s_opt.outlineTintAmount);
m.SetFloat("_TintDarkness", s_opt.outlineTintDarkness);
if (src != null)
{
if (src.HasProperty("_BaseMap")) { var t = src.GetTexture("_BaseMap"); if (t != null) m.SetTexture("_BaseMap", t); }
if (src.HasProperty("_DiffuseColor")) m.SetColor("_TintColor", src.GetColor("_DiffuseColor"));
}
s_outlines[src] = m;
return m;
}
static Material Make(Shader sh)
{
var m = new Material(sh);
m.hideFlags = HideFlags.HideAndDontSave;
s_created.Add(m);
return m;
}
static Material CloneForContrast(Material src, Opt o, bool isChar)
{
Material c;
// 캐릭터/배경이 같은 머티리얼을 공유하는 경우가 없어 키 하나로 충분하다
if (s_clones.TryGetValue(src, out c) && c != null) return c;
c = new Material(src);
c.hideFlags = HideFlags.HideAndDontSave;
c.name = src.name + "_814t";
if (c.HasProperty("_MinimumDarkness")) c.SetFloat("_MinimumDarkness", o.minimumDarkness);
if (c.HasProperty("_ShadowDiffuseColor"))
{
var s = c.GetColor("_ShadowDiffuseColor");
s = new Color(s.r * o.shadowColorScale, s.g * o.shadowColorScale, s.b * o.shadowColorScale, s.a);
if (o.shadowTintAmount > 0f) s = Color.Lerp(s, new Color(s.grayscale, s.grayscale, s.grayscale, s.a) * o.shadowTint, o.shadowTintAmount);
c.SetColor("_ShadowDiffuseColor", s);
}
if (isChar && o.charShades > 0 && c.HasProperty("_Shades")) c.SetFloat("_Shades", o.charShades);
s_clones[src] = c;
s_created.Add(c);
ContrastMaterials++;
return c;
}
static void DestroyAny(Object o)
{
if (o == null) return;
if (Application.isPlaying) Destroy(o); else DestroyImmediate(o);
}
}
}