Merge branch 'wl/gameplay/WL-814t-reference-look'

This commit is contained in:
깃 관리자 2026-09-13 00:03:39 +09:00
commit dabbdcb821
18 changed files with 1308 additions and 0 deletions

View File

@ -0,0 +1,224 @@
// WL-814t 캡처 — 레퍼런스 느낌 4요소 단독/단계/종합 (Play 중 한 프레임 안에서 전부 렌더)
// 한 프레임 안에서 찍는 이유: 구름 그림자(_Time)·포즈가 프레임마다 움직여 세션 간 비교가 노이즈투성이가 된다(814s 미확인⑤).
// run_script --file AgentScripts/WL814t_Capture.cs --entry WL814t_Capture.Run
using System.Collections.Generic;
using UnityEngine;
using WL.Look.Arena;
public static class WL814t_Capture
{
const string Dir = @"E:\NerdNavis\WL_wt\gameplay\Screenshots_WL\WL814t";
const int W = 1080, H = 1920;
const float Close = 3.4f; // 도트A 프리셋과 같은 직교 반높이 = 비교 기준
const float Wide = 10f; // 아레나 씬의 실제 카메라
static Camera s_cam;
static Vector3 s_camPos, s_charCenter;
static float s_camSize;
static System.Text.StringBuilder s_log;
public static string Run()
{
s_log = new System.Text.StringBuilder();
System.IO.Directory.CreateDirectory(Dir);
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) return "no camera";
s_camPos = s_cam.transform.position; s_camSize = s_cam.orthographicSize;
var pc = GameObject.Find("PC_LH_M05");
var rs = pc != null ? pc.GetComponentsInChildren<Renderer>(false) : new Renderer[0];
Bounds b = new Bounds(pc != null ? pc.transform.position : Vector3.zero, Vector3.zero);
bool first = true;
foreach (var r in rs) { if (r == null || !r.enabled) continue; if (first) { b = r.bounds; first = false; } else b.Encapsulate(r.bounds); }
s_charCenter = b.center;
s_log.AppendLine("charCenter=" + s_charCenter.ToString("F3") + " bodyH=" + b.size.y.ToString("F3")
+ " · 화면 캐릭터 키 @ortho3.4 = " + (b.size.y / (2f * Close) * H).ToString("F0") + " px · @ortho10 = " + (b.size.y / (2f * Wide) * H).ToString("F0") + " px");
var cfg = WLReferenceLookSettings.Instance;
if (cfg == null) return "no settings asset";
// ── ⓐ 지금(814s) — 아무것도 안 얹은 상태
Shot2("a_now", Nothing(cfg), Close, 0);
Shot2("a_now_wide", Nothing(cfg), Wide, 0);
Shot2("a_now_pixel", Nothing(cfg), Close, 240);
// ── 4요소 단독
Shot2("b_outline_only", Only(cfg, true, false, false, false), Close, 0);
Shot2("c_contrast_only", Only(cfg, false, true, false, false), Close, 0);
Shot2("d_mood_only", Only(cfg, false, false, true, false), Close, 0);
Shot2("e_rim_only", Only(cfg, false, false, false, true), Close, 0);
// ── Ⓐ 외곽선 4단 — 두께는 **아레나 실제 카메라(직교 10)** 기준 px (814u 실측 기준과 같은 축)
float[] ws = { 1f, 2f, 3f, 4f };
for (int i = 0; i < ws.Length; i++)
{
var o = Only(cfg, true, false, false, false);
o.outlineWidth = ws[i]; o.outlineWidthBackground = Mathf.Max(1f, ws[i] * 2.5f);
Shot2("o" + (i + 1) + "_outline_" + ws[i].ToString("F0") + "px", o, Close, 0);
}
// 외곽선 색 — 균일 검정 vs 표면색 연동(레퍼런스는 부위마다 선 색이 다르다)
{
var o = Only(cfg, true, false, false, false); o.outlineWidth = 1f;
o.outlineColor = Color.black; o.outlineTintAmount = 0f; Shot2("o5_color_uniformblack", o, Close, 0);
o.outlineTintAmount = 1f; Shot2("o6_color_surfacetint", o, Close, 0);
o.outlineColor = new Color(0.043f, 0.031f, 0.067f, 1f); o.outlineTintAmount = 0f; Shot2("o7_color_darkviolet", o, Close, 0);
}
// ── Ⓐ 면적 실측용 (직교 10 = 인게임 크기 · 외곽선을 마젠타로 칠해 픽셀을 센다)
MeasureOutlineArea(cfg, pc);
// ── Ⓑ 대비 3단
float[] mds = { 0.10f, 0.05f, 0.0f };
for (int i = 0; i < mds.Length; i++)
{
var o = Only(cfg, false, true, false, false); o.minimumDarkness = mds[i]; o.charShades = 0;
Shot2("c" + (i + 1) + "_darkness_" + mds[i].ToString("F2"), o, Close, 0);
}
// ── Ⓑ2 캐릭터 색단계 7 / 4 / 3 (레퍼런스2 = 부위당 2~3단)
int[] shades = { 0, 4, 3 };
for (int i = 0; i < shades.Length; i++)
{
var o = Only(cfg, false, true, false, false); o.charShades = shades[i];
Shot2("s" + (i + 1) + "_shades_" + (shades[i] == 0 ? 7 : shades[i]), o, Close, 0);
}
// ── Ⓒ 분위기 3단
float[] ms = { 0.35f, 0.65f, 1.0f };
for (int i = 0; i < ms.Length; i++)
{
var o = Only(cfg, false, false, true, false); o.moodStrength = ms[i];
Shot2("m" + (i + 1) + "_mood_" + ms[i].ToString("F2"), o, Close, 0);
}
// ── Ⓓ 림 3단
float[] rss = { 0.6f, 1.25f, 2.2f };
for (int i = 0; i < rss.Length; i++)
{
var o = Only(cfg, false, false, false, true); o.rimStrength = rss[i];
Shot2("r" + (i + 1) + "_rim_" + rss[i].ToString("F2"), o, Close, 0);
}
// ── ⓕ 종합안 (비픽셀)
var fin = WLReferenceLook.Opt.FromSettings(cfg);
Shot2("f_final", fin, Close, 0);
Shot2("f_final_wide", fin, Wide, 0);
// ── ⓕ2 종합안 + 도트 — 도트A 프리셋(orthoSize 3.4 · pixelHeight 240)
var fp = fin; fp.outlineWidth = cfg.outlineWidthPixelMode; fp.outlineWidthBackground = Mathf.Max(0.5f, cfg.outlineWidthPixelMode * 0.5f);
Shot2("f_final_pixel", fp, Close, 240);
var fp2 = fp; fp2.outlineWidth = cfg.outlineWidthPixelMode * 2f; fp2.outlineWidthBackground = cfg.outlineWidthPixelMode;
Shot2("f_final_pixel_w2", fp2, Close, 240);
Shot2("f_final_pixel_thick", fin, Close, 240); // 비픽셀 두께 그대로 → 저해상도에서 뭉개지는지
// ── 원복 확인
Shot2("z_restored_check", Nothing(cfg), Close, 0);
s_cam.transform.position = s_camPos; s_cam.orthographicSize = s_camSize;
s_log.AppendLine("DONE");
return s_log.ToString();
}
/// <summary>외곽선 띠가 캐릭터 면적의 몇 %를 먹는지 재기 위한 렌더 묶음(파이썬이 픽셀을 센다).</summary>
static void MeasureOutlineArea(WLReferenceLookSettings cfg, GameObject pc)
{
var rs = pc != null ? pc.GetComponentsInChildren<Renderer>(true) : new Renderer[0];
var on = new List<Renderer>();
foreach (var r in rs) if (r != null && r.enabled) on.Add(r);
// 배경만(캐릭터 끔)
WLReferenceLook.Restore();
foreach (var r in on) r.enabled = false;
s_cam.orthographicSize = Wide; s_cam.transform.position = s_charCenter - s_cam.transform.forward * 30f;
Shot("mx_bg", 0);
foreach (var r in on) r.enabled = true;
// 외곽선 없음
Shot2("mx_w0", Nothing(cfg), Wide, 0);
// 마젠타 외곽선 1~4 px (배경엔 안 넣는다 = 캐릭터만 센다)
float[] ws = { 1f, 2f, 3f, 4f };
for (int i = 0; i < ws.Length; i++)
{
var o = Only(cfg, true, false, false, false);
o.outlineWidth = ws[i]; o.outlineOnBackground = false;
o.outlineColor = new Color(1f, 0f, 1f, 1f); o.outlineTintAmount = 0f;
Shot2("mx_w" + ws[i].ToString("F0"), o, Wide, 0);
}
}
static WLReferenceLook.Opt Nothing(WLReferenceLookSettings cfg)
{
var o = WLReferenceLook.Opt.FromSettings(cfg);
o.outline = false; o.contrast = false; o.mood = false; o.rim = false;
return o;
}
static WLReferenceLook.Opt Only(WLReferenceLookSettings cfg, bool a, bool b, bool c, bool d)
{
var o = WLReferenceLook.Opt.FromSettings(cfg);
o.outline = a; o.contrast = b; o.mood = c; o.rim = d;
return o;
}
/// <summary>카메라를 먼저 맞춘 뒤(줌 비례 두께가 카메라를 읽는다) 적용 → 렌더.</summary>
static void Shot2(string name, WLReferenceLook.Opt o, float orthoSize, int pixelHeight)
{
s_cam.orthographicSize = orthoSize;
s_cam.transform.position = s_charCenter - s_cam.transform.forward * 30f;
WLReferenceLook.Apply(o, WLReferenceLookSettings.Instance);
Shot(name, pixelHeight);
}
/// <summary>한 장 렌더. pixelHeight 0 = 1080×1920 그대로, &gt;0 = 저해상도 RT 로 찍어 점보간 확대(도트).</summary>
static void Shot(string name, int pixelHeight)
{
int rw = W, rh = H;
if (pixelHeight > 0) { rh = pixelHeight; rw = Mathf.RoundToInt(pixelHeight * (float)W / H); }
var rt = new RenderTexture(rw, rh, 24, RenderTextureFormat.ARGB32);
rt.antiAliasing = 1;
rt.filterMode = pixelHeight > 0 ? FilterMode.Point : FilterMode.Bilinear;
rt.Create();
var prevTarget = s_cam.targetTexture;
s_cam.targetTexture = rt;
s_cam.Render();
s_cam.targetTexture = prevTarget;
var prevActive = RenderTexture.active;
RenderTexture.active = rt;
var src = new Texture2D(rw, rh, TextureFormat.RGB24, false);
src.ReadPixels(new Rect(0, 0, rw, rh), 0, 0);
src.Apply();
RenderTexture.active = prevActive;
Texture2D outTex = src;
if (pixelHeight > 0)
{
outTex = new Texture2D(W, H, TextureFormat.RGB24, false);
var sp = src.GetPixels32();
var dp = new Color32[W * H];
for (int y = 0; y < H; y++)
{
int sy = Mathf.Min(rh - 1, y * rh / H);
for (int x = 0; x < W; x++)
{
int sx = Mathf.Min(rw - 1, x * rw / W);
dp[y * W + x] = sp[sy * rw + sx];
}
}
outTex.SetPixels32(dp); outTex.Apply();
}
System.IO.File.WriteAllBytes(System.IO.Path.Combine(Dir, name + ".png"), outTex.EncodeToPNG());
s_log.AppendLine("shot " + name + " " + rw + "x" + rh + " · ortho " + s_cam.orthographicSize.ToString("F2") + " · " + WLReferenceLook.LastLog);
if (outTex != src) Object.Destroy(outTex);
Object.Destroy(src);
rt.Release(); Object.Destroy(rt);
}
}

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 269ec43cd1eb2794e9f12763ccdb9af5
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,179 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-7716237022740010770
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 558a8e2b6826cf840aae193990ba9f2e, type: 3}
m_Name: ShadowsMidtonesHighlights
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.ShadowsMidtonesHighlights
active: 1
shadows:
m_OverrideState: 1
m_Value: {x: 0.78, y: 0.58, z: 1.25, w: -0.06}
midtones:
m_OverrideState: 1
m_Value: {x: 1, y: 0.96, z: 1.05, w: 0}
highlights:
m_OverrideState: 1
m_Value: {x: 1.14, y: 1.02, z: 0.74, w: 0.03}
shadowsStart:
m_OverrideState: 0
m_Value: 0
shadowsEnd:
m_OverrideState: 0
m_Value: 0.3
highlightsStart:
m_OverrideState: 0
m_Value: 0.55
highlightsEnd:
m_OverrideState: 0
m_Value: 1
--- !u!114 &-6414152611911942192
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 0b2db86121404754db890f4c8dfe81b2, type: 3}
m_Name: Bloom
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.Bloom
active: 1
skipIterations:
m_OverrideState: 0
m_Value: 1
threshold:
m_OverrideState: 1
m_Value: 1.05
intensity:
m_OverrideState: 1
m_Value: 0.55
scatter:
m_OverrideState: 1
m_Value: 0.6
clamp:
m_OverrideState: 0
m_Value: 65472
tint:
m_OverrideState: 1
m_Value: {r: 1, g: 0.86, b: 0.6, a: 1}
highQualityFiltering:
m_OverrideState: 0
m_Value: 0
filter:
m_OverrideState: 0
m_Value: 0
downscale:
m_OverrideState: 0
m_Value: 0
maxIterations:
m_OverrideState: 0
m_Value: 6
dirtTexture:
m_OverrideState: 0
m_Value: {fileID: 0}
dimension: 1
dirtIntensity:
m_OverrideState: 0
m_Value: 0
--- !u!114 &-2512526436937386102
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 221518ef91623a7438a71fef23660601, type: 3}
m_Name: WhiteBalance
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.WhiteBalance
active: 1
temperature:
m_OverrideState: 1
m_Value: -22
tint:
m_OverrideState: 1
m_Value: 26
--- !u!114 &-875615482179838212
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 899c54efeace73346a0a16faa3afe726, type: 3}
m_Name: Vignette
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.Vignette
active: 1
color:
m_OverrideState: 1
m_Value: {r: 0.09, g: 0.03, b: 0.14, a: 1}
center:
m_OverrideState: 0
m_Value: {x: 0.5, y: 0.5}
intensity:
m_OverrideState: 1
m_Value: 0.28
smoothness:
m_OverrideState: 1
m_Value: 0.6
rounded:
m_OverrideState: 0
m_Value: 0
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d7fd9488000d3734a9e00ee676215985, type: 3}
m_Name: WL_ArenaMood
m_EditorClassIdentifier: Unity.RenderPipelines.Core.Runtime::UnityEngine.Rendering.VolumeProfile
components:
- {fileID: 1813646129596036978}
- {fileID: -2512526436937386102}
- {fileID: -7716237022740010770}
- {fileID: -6414152611911942192}
- {fileID: -875615482179838212}
--- !u!114 &1813646129596036978
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 66f335fb1ffd8684294ad653bf1c7564, type: 3}
m_Name: ColorAdjustments
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.ColorAdjustments
active: 1
postExposure:
m_OverrideState: 1
m_Value: 0.05
contrast:
m_OverrideState: 1
m_Value: 30
colorFilter:
m_OverrideState: 1
m_Value: {r: 0.95, g: 0.8, b: 1.08, a: 1}
hueShift:
m_OverrideState: 0
m_Value: 0
saturation:
m_OverrideState: 1
m_Value: 12

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 534beeaf638a948459a24ec36906d7f6
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7b6cfdda832d05f478e86a49991bde8f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5e200478fef3667448d80f66592afd9a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,46 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: af081c9d37b39dd40aa4c58f0397f23c, type: 3}
m_Name: WLReferenceLookSettings
m_EditorClassIdentifier: Assembly-CSharp::WL.Look.Arena.WLReferenceLookSettings
enabled_: 1
verboseLog: 0
outlineOn: 1
outlineWidth: 1
outlineWidthPixelMode: 1
outlineColor: {r: 0, g: 0, b: 0, a: 1}
outlineOnBackground: 1
outlineWidthBackground: 2.5
outlineScaleWithZoom: 1
outlineRefOrthoSize: 10
outlineTintAmount: 0
outlineTintDarkness: 0.28
outlineDepthBias: 0.06
contrastOn: 1
minimumDarkness: 0.12
shadowColorScale: 0.85
shadowTintAmount: 0.3
shadowTint: {r: 0.243, g: 0.106, b: 0.353, a: 1}
charShades: 0
moodOn: 1
moodStrength: 0.55
profile:
asset: {fileID: 11400000, guid: 534beeaf638a948459a24ec36906d7f6, type: 2}
keyLightColor: {r: 1, g: 0.78, b: 0.46, a: 1}
keyLightIntensity: 1.45
ambientColor: {r: 0.28, g: 0.17, b: 0.4, a: 1}
rimOn: 1
rimStrength: 1.6
rimPower: 3
rimUpBias: 0.35
rimColor: {r: 1, g: 0.82, b: 0.5, a: 1}
rimOnBackground: 0

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f662bae23dfde7b42a3b5966d68fbcf0
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1098,6 +1098,52 @@ Transform:
type: 3}
m_PrefabInstance: {fileID: 206788312}
m_PrefabAsset: {fileID: 0}
--- !u!1 &259214373
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 259214375}
- component: {fileID: 259214374}
m_Layer: 0
m_Name: WL_ReferenceLook
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &259214374
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 259214373}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4fd112583fc74eb40bbc4ba98ea96331, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::WL.Look.Arena.WLReferenceLook
characterRoot: {fileID: 711662063}
characterRootName: PC_LH_M05
--- !u!4 &259214375
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 259214373}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1001 &266873449
PrefabInstance:
m_ObjectHideFlags: 0
@ -14773,3 +14819,4 @@ SceneRoots:
- {fileID: 918041734}
- {fileID: 1166334010}
- {fileID: 2042308456}
- {fileID: 259214375}

View File

@ -0,0 +1,365 @@
// ─────────────────────────────────────────────────────────────────────────────
// 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);
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4fd112583fc74eb40bbc4ba98ea96331

View File

@ -0,0 +1,137 @@
// ─────────────────────────────────────────────────────────────────────────────
// WLReferenceLookSettings.cs — 「레퍼런스 느낌」 4요소의 값 단일 출처 (WL-814t · #814)
//
// PD 지시(2026-09-12) 「레퍼런스 게임 같은 느낌을 살릴 수 없을까?」 · 발주서 WL-814t §2-3.
// 레퍼런스 특징 = ⓐ 굵은 검은 외곽선 ⓑ 매우 어두운 그림자(강한 대비)
// ⓒ 보라~자홍 배경 + 금색/주황 조명 ⓓ 윤곽의 밝은 테(림라이트)
//
// ■ C45 — 값은 코드 상수가 아니라 이 에셋(`Resources/WL/WLReferenceLookSettings.asset`)
// ■ C8 — `enabled_ = 0` 이면 `WLReferenceLook` 이 **아무것도 하지 않는다** = 814s 상태 100 %
//
// 🔴 어셈블리 주의: Assets/WL/Look/ 에 .asmdef 를 만들지 말 것(Assembly-CSharp 단일).
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
namespace WL.Look.Arena
{
[CreateAssetMenu(fileName = "WLReferenceLookSettings", menuName = "WL/Reference Look Settings", order = 37)]
public sealed class WLReferenceLookSettings : ScriptableObject
{
public const string ResourcesPath = "WL/WLReferenceLookSettings";
private static WLReferenceLookSettings s_instance;
private static bool s_tried;
/// <summary>에셋이 없으면 null — 호출부는 전부 건너뛴다(C8 롤백).</summary>
public static WLReferenceLookSettings Instance
{
get
{
if (s_instance == null && !s_tried)
{
s_tried = true;
s_instance = Resources.Load<WLReferenceLookSettings>(ResourcesPath);
}
return s_instance;
}
}
[Header("마스터 (C8)")]
[Tooltip("0 이면 이 기능 전체가 꺼진다 = WL-814s 상태 100 %.")]
public int enabled_ = 1;
[Tooltip("1 이면 적용 결과를 콘솔에 1줄 남긴다.")]
public int verboseLog = 0;
// ── Ⓐ 외곽선 ────────────────────────────────────────────────────────
[Header("Ⓐ 외곽선 (inverted hull · 두께 단위 = 화면 픽셀 @1080×1920)")]
public bool outlineOn = true;
[Tooltip("캐릭터 외곽선 두께(px · 아레나 실제 카메라 직교 10 = 캐릭터 키 98~117 px 기준). " +
"🔴 814u 실측: 이 크기에서 1 px 띠가 이미 캐릭터 면적의 27 %, 3 px 이면 76 % 를 먹는다 → 상한 1(비교용 2).")]
public float outlineWidth = 1.0f;
[Tooltip("도트(픽셀) 모드에서 쓰는 캐릭터 외곽선 두께(px · 저해상도 RT 기준).")]
public float outlineWidthPixelMode = 1.0f;
[Tooltip("외곽선 색. 순검정보다 아주 어두운 중성/보라가 레퍼런스에 가깝다.")]
public Color outlineColor = new Color(0.043f, 0.031f, 0.067f, 1f);
[Tooltip("배경(바위·나무·건물)에도 외곽선을 넣을지.")]
public bool outlineOnBackground = true;
[Tooltip("배경 외곽선 두께(px @직교 10). 배경 물체는 화면에서 크므로 캐릭터보다 굵어도 된다.")]
public float outlineWidthBackground = 2.5f;
[Tooltip("켜면 두께를 카메라 줌(직교 크기)에 맞춰 비례시킨다 = 캐릭터 대비 굵기가 항상 같다(2D 스프라이트처럼).")]
public bool outlineScaleWithZoom = true;
[Tooltip("위 비례의 기준 직교 반높이 = 아레나 실제 카메라(10). 이 크기일 때 두께가 위 px 값 그대로다.")]
public float outlineRefOrthoSize = 10f;
[Tooltip("0 = 균일 색(검정) · 1 = 표면색 연동(머리칼=짙은 갈색, 옷=짙은 빨강 …). 레퍼런스는 부위마다 선 색이 다르다.")]
[Range(0f, 1f)] public float outlineTintAmount = 0f;
[Tooltip("색 연동일 때 표면색을 얼마나 어둡게 할지(작을수록 진하다).")]
[Range(0f, 1f)] public float outlineTintDarkness = 0.28f;
[Tooltip("껍데기를 카메라에서 뒤로 미는 양(m). 저폴리 하드에지의 실루엣 안쪽 얼룩을 없앤다.")]
public float outlineDepthBias = 0.06f;
// ── Ⓑ 명암 대비 ─────────────────────────────────────────────────────
[Header("Ⓑ 명암 대비 (Toon 램프)")]
public bool contrastOn = true;
[Tooltip("`_MinimumDarkness` — 그림자 바닥 밝기. 기본 0.2 · 낮을수록 어둡다.")]
[Range(0f, 0.5f)] public float minimumDarkness = 0.08f;
[Tooltip("`_ShadowDiffuseColor` 에 곱하는 계수. 1 미만이면 그림자가 더 진해진다.")]
[Range(0.2f, 1.5f)] public float shadowColorScale = 0.72f;
[Tooltip("그림자에 섞을 색조(보라). 0 이면 안 섞는다.")]
[Range(0f, 1f)] public float shadowTintAmount = 0.35f;
public Color shadowTint = new Color(0.243f, 0.106f, 0.353f, 1f);
[Tooltip("캐릭터 `_Shades`(색 단계 수). 0 이면 원래 값(배경과 동일 7) 유지. 레퍼런스2 = 부위당 2~3단.")]
public int charShades = 0;
// ── Ⓒ 색조·분위기 ───────────────────────────────────────────────────
[Header("Ⓒ 색조·분위기 (Volume + 라이트)")]
public bool moodOn = true;
[Tooltip("0=끔 · 0.5=은은 · 1=강함. Volume weight 와 라이트 보간에 함께 쓴다.")]
[Range(0f, 1f)] public float moodStrength = 0.75f;
[Tooltip("씬에 붙일 포스트 프로파일(없으면 Ⓒ 는 라이트만 바꾼다).")]
public VolumeProfileRef profile = new VolumeProfileRef();
[Tooltip("키라이트(Directional) 목표 색 — 레퍼런스의 금색/주황.")]
public Color keyLightColor = new Color(1f, 0.76f, 0.42f, 1f);
[Tooltip("키라이트 목표 세기.")]
public float keyLightIntensity = 1.15f;
[Tooltip("앰비언트 목표 색 — 레퍼런스의 보라~자홍.")]
public Color ambientColor = new Color(0.259f, 0.165f, 0.373f, 1f);
// ── Ⓓ 림라이트 ──────────────────────────────────────────────────────
[Header("Ⓓ 림라이트 (캐릭터 윤곽의 밝은 테)")]
public bool rimOn = true;
[Range(0f, 4f)] public float rimStrength = 1.25f;
[Range(0.5f, 8f)] public float rimPower = 3.0f;
[Range(0f, 1f)] public float rimUpBias = 0.35f;
public Color rimColor = new Color(1f, 0.82f, 0.5f, 1f);
[Tooltip("배경에도 림을 넣을지(레퍼런스는 캐릭터만).")]
public bool rimOnBackground = false;
/// <summary>인스펙터에서 프로파일을 물릴 수 있게 하는 얇은 래퍼(Volume 패키지 타입 직접 노출).</summary>
[System.Serializable]
public sealed class VolumeProfileRef
{
public UnityEngine.Rendering.VolumeProfile asset;
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: af081c9d37b39dd40aa4c58f0397f23c

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5cded1f5a214b5e46aec36dd921a61a6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,138 @@
// ─────────────────────────────────────────────────────────────────────────────
// WLHullOutline.shader — 화면 픽셀 단위 두께의 「굵은 검은 외곽선」 (WL-814t · #814)
//
// ■ 왜 새로 만드나 (§1 실측 결론)
// 원본 `Shader Graphs/Toon` 의 `_Outline`/`_OUTLINESENABLED` 는 **머티리얼 안에 박힌
// 화면공간 에지검출 후처리**다(Depth+Normals 1텍셀 비교 → Overlay 블렌드).
// · 두께 = `_CameraNormalsTexture` **1 텍셀 고정** — 머티리얼로 못 굵게 한다.
// · `_Outline` 의 **알파는 무시**된다(Blend 노드가 float3 로 해석) — RGB 만 쓴다.
// · `PixelOutlineSetup` 렌더러 피처가 꺼져 있으면(현재 `m_Active: 0`) 아무것도 안 나온다.
// · 켜도 나오는 것은 「면 경계의 얇은 실선」이지 레퍼런스의 굵은 실루엣이 아니다.
// → 레퍼런스(2D 도트)의 굵은 외곽선은 **메시 노멀 확장(inverted hull)** 으로만 나온다.
//
// ■ 무엇을 하나
// 앞면을 버리고(Cull Front) 뒷면을 화면 XY 로 `_OutlineWidth` **픽셀**만큼 밀어 그린다.
// 두께가 화면 픽셀 단위라 거리·직교크기와 무관하게 일정하다(= 2D 도트 느낌).
//
// ■ 어떻게 쓰나
// 원본 머티리얼을 **건드리지 않는다**. 렌더러의 머티리얼 배열 뒤에 이 머티리얼을 한 장
// 더 붙이면(서브메시가 1개면 마지막 서브메시를 한 번 더 그린다) 외곽선이 된다.
// 붙이고 떼는 것은 `WLReferenceLook` 이 런타임에만 한다(에셋·씬 무변경).
//
// ■ 모바일 비용
// 대상 렌더러당 드로우콜 +1(정점만 한 번 더). 픽셀 부하는 실루엣 띠 면적뿐.
// ─────────────────────────────────────────────────────────────────────────────
Shader "WL/HullOutline"
{
Properties
{
[HDR] _OutlineColor ("Outline Color", Color) = (0, 0, 0, 1)
_OutlineWidth ("Outline Width (screen px)", Float) = 2.0
_OutlineDepthBias ("Depth Bias (m · 하드에지 저폴리의 내부 얼룩 제거)", Float) = 0.06
// ── 색 연동 외곽선(레퍼런스는 부위마다 선 색이 다르다: 머리칼=짙은 갈색 · 흰 올빼미=짙은 남색)
_BaseMap ("Base Map (원본 머티리얼에서 복사)", 2D) = "white" {}
_TintColor ("Tint Color (원본 _DiffuseColor)", Color) = (1, 1, 1, 1)
_TintDarkness ("Tint Darkness", Range(0, 1)) = 0.28
_TintAmount ("0 = 균일 색 · 1 = 표면색 연동", Range(0, 1)) = 0.0
}
SubShader
{
Tags { "RenderPipeline" = "UniversalPipeline" "RenderType" = "Opaque" "Queue" = "Geometry-1" }
Pass
{
Name "WLHullOutline"
Tags { "LightMode" = "SRPDefaultUnlit" }
Cull Front
ZWrite On
ZTest LEqual
ColorMask RGB
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"
TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap);
CBUFFER_START(UnityPerMaterial)
float4 _OutlineColor;
float _OutlineWidth;
float _OutlineDepthBias;
float4 _BaseMap_ST;
float4 _TintColor;
float _TintDarkness;
float _TintAmount;
CBUFFER_END
struct Attributes
{
float4 positionOS : POSITION;
float3 normalOS : NORMAL;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
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);
// 🔴 깊이 바이어스 — 껍데기를 카메라에서 조금 더 멀리 밀어 둔다.
// 저폴리 하드에지 메시(머리카락·옷)는 같은 위치에서 노멀이 쪼개져 있어
// 그냥 밀면 실루엣 **안쪽**에 뒷면이 튀어나와 얼룩이 된다(실측).
// 본체 면보다 항상 뒤에 있게 만들면 안쪽 얼룩만 깔끔히 사라진다.
float3 posWS = TransformObjectToWorld(input.positionOS.xyz);
float3 posVS = TransformWorldToView(posWS);
posVS.z -= _OutlineDepthBias; // 뷰공간은 -Z 가 앞 → 빼면 멀어진다
float4 posCS = TransformWViewToHClip(posVS);
float3 nWS = TransformObjectToWorldNormal(input.normalOS);
float3 nVS = TransformWorldToViewDir(nWS, true);
// 투영까지 태워야 직교/원근 모두에서 화면 방향이 맞는다
float2 nCS = mul((float3x3)UNITY_MATRIX_P, nVS).xy;
// 화면(픽셀) 공간에서 정규화 → 방향이 대각이어도 두께가 일정하다
float2 dirPx = nCS * float2(_ScreenParams.x, _ScreenParams.y);
float len = length(dirPx);
dirPx = (len > 1e-5) ? dirPx / len : float2(0.0, 0.0);
// 픽셀 → NDC(가로 2.0 이 화면 폭) → 클립(× w)
float2 ndcPerPx = float2(2.0 / _ScreenParams.x, 2.0 / _ScreenParams.y);
posCS.xy += dirPx * ndcPerPx * _OutlineWidth * posCS.w;
o.positionCS = posCS;
o.uv = TRANSFORM_TEX(input.uv, _BaseMap);
return o;
}
half4 frag(Varyings input) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
// 색 연동 — 원본 머티리얼의 BaseMap×DiffuseColor 를 어둡게 깐다.
// BaseMap 이 없는 배경 머티리얼은 흰색이 바인딩되어 _TintColor(=_DiffuseColor) 만 남는다.
half3 tinted = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv).rgb * _TintColor.rgb * _TintDarkness;
half3 col = lerp(_OutlineColor.rgb, tinted, saturate(_TintAmount));
return half4(col, 1.0);
}
ENDHLSL
}
}
Fallback Off
}

View File

@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: c1901a3913628b24590d96fb77efd166
ShaderImporter:
externalObjects: {}
defaultTextures: []
nonModifiableTextures: []
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,102 @@
// ─────────────────────────────────────────────────────────────────────────────
// WLRimAdd.shader — 캐릭터 윤곽의 밝은 테(림라이트) (WL-814t · #814 · 요소 Ⓓ)
//
// ■ 왜 별도 패스인가
// 원본 `Toon.shadergraph` 에는 Fresnel 입력이 없고, 원본(`Assets/3DPixelArtEnvironment/**`)
// 수정은 금지다. 그래프 **복사본**을 만들어 Fresnel 을 더하는 대신, 같은 메시를 한 번 더
// 가산(Blend One One)으로 덧그리는 패스를 택했다 — 머티리얼/그래프를 하나도 안 건드리고
// 런타임에 켜고 끌 수 있고, 세기를 슬라이더 하나로 준다.
//
// ■ 무엇을 하나
// rim = pow(1 - saturate(dot(N, V)), _RimPower) — 시선과 수직인 가장자리에서 1.
// `_RimUpBias` 로 위쪽(레퍼런스의 무대조명) 윤곽만 더 밝게 줄 수 있다.
//
// ■ 주의
// Transparent 큐 · ZWrite Off · ZTest LEqual + Offset 으로 원본 면 위에 얹는다.
// ─────────────────────────────────────────────────────────────────────────────
Shader "WL/RimAdd"
{
Properties
{
[HDR] _RimColor ("Rim Color", Color) = (1, 0.85, 0.55, 1)
_RimPower ("Rim Power", Float) = 3.0
_RimStrength ("Rim Strength", Float) = 1.0
_RimUpBias ("Rim Up Bias (0=균일, 1=위쪽만)", Range(0, 1)) = 0.35
}
SubShader
{
Tags { "RenderPipeline" = "UniversalPipeline" "RenderType" = "Transparent" "Queue" = "Transparent" }
Pass
{
Name "WLRimAdd"
Tags { "LightMode" = "SRPDefaultUnlit" }
Cull Back
ZWrite Off
ZTest LEqual
Offset -1, -1
Blend One One
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"
CBUFFER_START(UnityPerMaterial)
float4 _RimColor;
float _RimPower;
float _RimStrength;
float _RimUpBias;
CBUFFER_END
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));
float rim = 1.0 - saturate(dot(n, v));
rim = pow(saturate(rim), max(_RimPower, 0.01));
// 위쪽 윤곽 가중(무대 조명 느낌)
float up = saturate(n.y * 0.5 + 0.5);
rim *= lerp(1.0, up, saturate(_RimUpBias));
return half4(_RimColor.rgb * (rim * _RimStrength), 0.0);
}
ENDHLSL
}
}
Fallback Off
}

View File

@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: e22defdb5476d4b4f93db440d73c4233
ShaderImporter:
externalObjects: {}
defaultTextures: []
nonModifiableTextures: []
userData:
assetBundleName:
assetBundleVariant: