366 lines
18 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|