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
}