From db34b0171baddfd21b0e3bf067c56813b2f871b6 Mon Sep 17 00:00:00 2001 From: swrring Date: Sun, 13 Sep 2026 00:00:15 +0900 Subject: [PATCH] =?UTF-8?q?[WL-814t]=20=EC=99=B8=EA=B3=BD=EC=84=A0=201px?= =?UTF-8?q?=20=EC=83=81=ED=95=9C(814u=20=EB=A9=B4=EC=A0=81=20=EC=8B=A4?= =?UTF-8?q?=EC=B8=A1)=20+=20=ED=91=9C=EB=A9=B4=EC=83=89=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99=20=EC=99=B8=EA=B3=BD=EC=84=A0=20+=20=EB=A9=B4?= =?UTF-8?q?=EC=A0=81=20=EB=B9=84=EA=B5=90=20=EC=BA=A1=EC=B2=98=20(#814)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 --- AgentScripts/WL814t_Capture.cs | 46 +++++++++++-- .../WL/WLReferenceLookSettings.asset | 6 +- Assets/WL/Look/Arena/WLReferenceLook.cs | 64 +++++++++++++------ .../WL/Look/Arena/WLReferenceLookSettings.cs | 17 +++-- .../Character/Shaders/WLHullOutline.shader | 20 +++++- 5 files changed, 117 insertions(+), 36 deletions(-) diff --git a/AgentScripts/WL814t_Capture.cs b/AgentScripts/WL814t_Capture.cs index e51491d91..e5094a33f 100644 --- a/AgentScripts/WL814t_Capture.cs +++ b/AgentScripts/WL814t_Capture.cs @@ -51,21 +51,25 @@ public static class WL814t_Capture 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단 (PD: 굵게 · 상한을 올린 2/4/6/8 px) - float[] ws = { 2f, 4f, 6f, 8f }; + // ── Ⓐ 외곽선 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] * 0.45f); + 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 아주 어두운 보라 + // 외곽선 색 — 균일 검정 vs 표면색 연동(레퍼런스는 부위마다 선 색이 다르다) { - var o = Only(cfg, true, false, false, false); o.outlineWidth = 6f; o.outlineWidthBackground = 2.7f; - o.outlineColor = Color.black; Shot2("o5_color_pureblack", o, Close, 0); - o.outlineColor = new Color(0.043f, 0.031f, 0.067f, 1f); Shot2("o6_color_darkviolet", o, Close, 0); + 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++) @@ -118,6 +122,34 @@ public static class WL814t_Capture return s_log.ToString(); } + /// 외곽선 띠가 캐릭터 면적의 몇 %를 먹는지 재기 위한 렌더 묶음(파이썬이 픽셀을 센다). + static void MeasureOutlineArea(WLReferenceLookSettings cfg, GameObject pc) + { + var rs = pc != null ? pc.GetComponentsInChildren(true) : new Renderer[0]; + var on = new List(); + 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); diff --git a/Assets/WL/Look/Arena/Resources/WL/WLReferenceLookSettings.asset b/Assets/WL/Look/Arena/Resources/WL/WLReferenceLookSettings.asset index b33593832..e374ab347 100644 --- a/Assets/WL/Look/Arena/Resources/WL/WLReferenceLookSettings.asset +++ b/Assets/WL/Look/Arena/Resources/WL/WLReferenceLookSettings.asset @@ -15,13 +15,15 @@ MonoBehaviour: enabled_: 1 verboseLog: 0 outlineOn: 1 - outlineWidth: 6 + outlineWidth: 1 outlineWidthPixelMode: 1 outlineColor: {r: 0, g: 0, b: 0, a: 1} outlineOnBackground: 1 outlineWidthBackground: 2.5 outlineScaleWithZoom: 1 - outlineRefOrthoSize: 3.4 + outlineRefOrthoSize: 10 + outlineTintAmount: 0 + outlineTintDarkness: 0.28 outlineDepthBias: 0.06 contrastOn: 1 minimumDarkness: 0.12 diff --git a/Assets/WL/Look/Arena/WLReferenceLook.cs b/Assets/WL/Look/Arena/WLReferenceLook.cs index c1bf41ba3..e78d33b7e 100644 --- a/Assets/WL/Look/Arena/WLReferenceLook.cs +++ b/Assets/WL/Look/Arena/WLReferenceLook.cs @@ -42,6 +42,7 @@ namespace WL.Look.Arena 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; @@ -59,6 +60,7 @@ namespace WL.Look.Arena 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; @@ -107,28 +109,21 @@ namespace WL.Look.Arena OutlinedRenderers = 0; RimRenderers = 0; ContrastMaterials = 0; - Material outlineChar = null, outlineBg = null, rimMat = null; - if (o.outline) + Shader outlineShader = o.outline ? Shader.Find("WL/HullOutline") : null; + // 줌 비례 — 직교 크기가 커지면(멀리서 보면) 화면 픽셀 두께를 같은 비율로 줄인다. + // 그래야 「캐릭터 키 대비 굵기」가 프레이밍과 무관하게 일정하다(2D 스프라이트처럼). + // 기준 = 아레나 실제 카메라(직교 10) → SO 의 px 값이 곧 「인게임에서 보이는 px」다. + float zoom = 1f; { - var sh = Shader.Find("WL/HullOutline"); - if (sh != null) - { - // 줌 비례 — 직교 크기가 커지면(멀리서 보면) 화면 픽셀 두께를 같은 비율로 줄인다. - // 그래야 「캐릭터 키 대비 굵기」가 프레이밍과 무관하게 일정하다(2D 스프라이트처럼). - float zoom = 1f; - var c0 = Camera.main; - if (c0 == null) { var cs0 = Object.FindObjectsByType(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; - - outlineChar = Make(sh); outlineChar.SetColor("_OutlineColor", o.outlineColor); - outlineChar.SetFloat("_OutlineWidth", o.outlineWidth * zoom); - outlineChar.SetFloat("_OutlineDepthBias", o.outlineDepthBias); - outlineBg = Make(sh); outlineBg.SetColor("_OutlineColor", o.outlineColor); - outlineBg.SetFloat("_OutlineWidth", o.outlineWidthBackground * zoom); - outlineBg.SetFloat("_OutlineDepthBias", o.outlineDepthBias); - } + var c0 = Camera.main; + if (c0 == null) { var cs0 = Object.FindObjectsByType(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"); @@ -173,7 +168,7 @@ namespace WL.Look.Arena // Ⓐ 외곽선 · Ⓓ 림 — 머티리얼 배열 뒤에 덧붙인다(서브메시 1개면 한 번 더 그린다) var extra = new List(2); - if (o.outline && (isChar || o.outlineOnBackground)) extra.Add(isChar ? outlineChar : outlineBg); + 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) { @@ -289,6 +284,7 @@ namespace WL.Look.Arena 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; @@ -303,6 +299,32 @@ namespace WL.Look.Arena return anim != null ? anim.gameObject : null; } + static Shader s_outlineShader; + static float s_zoom = 1f; + static Opt s_opt; + static readonly Dictionary s_outlines = new Dictionary(); + + /// 원본 머티리얼 1종당 외곽선 머티리얼 1장(색 연동을 하려면 BaseMap·DiffuseColor 가 원본별로 달라야 한다). + 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); diff --git a/Assets/WL/Look/Arena/WLReferenceLookSettings.cs b/Assets/WL/Look/Arena/WLReferenceLookSettings.cs index 6ba8071b4..ca2262cd3 100644 --- a/Assets/WL/Look/Arena/WLReferenceLookSettings.cs +++ b/Assets/WL/Look/Arena/WLReferenceLookSettings.cs @@ -48,8 +48,9 @@ namespace WL.Look.Arena [Header("Ⓐ 외곽선 (inverted hull · 두께 단위 = 화면 픽셀 @1080×1920)")] public bool outlineOn = true; - [Tooltip("캐릭터 외곽선 두께(px @직교 3.4). PD 지시 = 굵게. 레퍼런스2 비율 = 캐릭터 키의 1/80~1/60 ≈ 4.3~5.7 px.")] - public float outlineWidth = 6.0f; + [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; @@ -60,14 +61,20 @@ namespace WL.Look.Arena [Tooltip("배경(바위·나무·건물)에도 외곽선을 넣을지.")] public bool outlineOnBackground = true; - [Tooltip("배경 외곽선 두께(px). 저폴리 하드에지라 캐릭터보다 얇게 잡는다.")] + [Tooltip("배경 외곽선 두께(px @직교 10). 배경 물체는 화면에서 크므로 캐릭터보다 굵어도 된다.")] public float outlineWidthBackground = 2.5f; [Tooltip("켜면 두께를 카메라 줌(직교 크기)에 맞춰 비례시킨다 = 캐릭터 대비 굵기가 항상 같다(2D 스프라이트처럼).")] public bool outlineScaleWithZoom = true; - [Tooltip("위 비례의 기준 직교 반높이. 이 크기일 때 두께가 위 px 값 그대로다.")] - public float outlineRefOrthoSize = 3.4f; + [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; diff --git a/Assets/WL/Look/Character/Shaders/WLHullOutline.shader b/Assets/WL/Look/Character/Shaders/WLHullOutline.shader index a93894fd5..f4ae7b2c7 100644 --- a/Assets/WL/Look/Character/Shaders/WLHullOutline.shader +++ b/Assets/WL/Look/Character/Shaders/WLHullOutline.shader @@ -29,6 +29,11 @@ Shader "WL/HullOutline" [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 @@ -53,22 +58,30 @@ Shader "WL/HullOutline" #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 }; @@ -104,13 +117,18 @@ Shader "WL/HullOutline" 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); - return half4(_OutlineColor.rgb, 1.0); + // 색 연동 — 원본 머티리얼의 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 }