[WL-814t] 외곽선 1px 상한(814u 면적 실측) + 표면색 연동 외곽선 + 면적 비교 캡처 (#814)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-09-13 00:00:15 +09:00
parent 9b8f7e968b
commit db34b0171b
5 changed files with 117 additions and 36 deletions

View File

@ -51,21 +51,25 @@ public static class WL814t_Capture
Shot2("d_mood_only", Only(cfg, false, false, true, 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); Shot2("e_rim_only", Only(cfg, false, false, false, true), Close, 0);
// ── Ⓐ 외곽선 4단 (PD: 굵게 · 상한을 올린 2/4/6/8 px) // ── Ⓐ 외곽선 4단 — 두께는 **아레나 실제 카메라(직교 10)** 기준 px (814u 실측 기준과 같은 축)
float[] ws = { 2f, 4f, 6f, 8f }; float[] ws = { 1f, 2f, 3f, 4f };
for (int i = 0; i < ws.Length; i++) for (int i = 0; i < ws.Length; i++)
{ {
var o = Only(cfg, true, false, false, false); 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); 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; var o = Only(cfg, true, false, false, false); o.outlineWidth = 1f;
o.outlineColor = Color.black; Shot2("o5_color_pureblack", o, Close, 0); o.outlineColor = Color.black; o.outlineTintAmount = 0f; Shot2("o5_color_uniformblack", o, Close, 0);
o.outlineColor = new Color(0.043f, 0.031f, 0.067f, 1f); Shot2("o6_color_darkviolet", 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단 // ── Ⓑ 대비 3단
float[] mds = { 0.10f, 0.05f, 0.0f }; float[] mds = { 0.10f, 0.05f, 0.0f };
for (int i = 0; i < mds.Length; i++) for (int i = 0; i < mds.Length; i++)
@ -118,6 +122,34 @@ public static class WL814t_Capture
return s_log.ToString(); 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) static WLReferenceLook.Opt Nothing(WLReferenceLookSettings cfg)
{ {
var o = WLReferenceLook.Opt.FromSettings(cfg); var o = WLReferenceLook.Opt.FromSettings(cfg);

View File

@ -15,13 +15,15 @@ MonoBehaviour:
enabled_: 1 enabled_: 1
verboseLog: 0 verboseLog: 0
outlineOn: 1 outlineOn: 1
outlineWidth: 6 outlineWidth: 1
outlineWidthPixelMode: 1 outlineWidthPixelMode: 1
outlineColor: {r: 0, g: 0, b: 0, a: 1} outlineColor: {r: 0, g: 0, b: 0, a: 1}
outlineOnBackground: 1 outlineOnBackground: 1
outlineWidthBackground: 2.5 outlineWidthBackground: 2.5
outlineScaleWithZoom: 1 outlineScaleWithZoom: 1
outlineRefOrthoSize: 3.4 outlineRefOrthoSize: 10
outlineTintAmount: 0
outlineTintDarkness: 0.28
outlineDepthBias: 0.06 outlineDepthBias: 0.06
contrastOn: 1 contrastOn: 1
minimumDarkness: 0.12 minimumDarkness: 0.12

View File

@ -42,6 +42,7 @@ namespace WL.Look.Arena
public bool outlineOnBackground; public bool outlineOnBackground;
public bool outlineScaleWithZoom; public bool outlineScaleWithZoom;
public float outlineRefOrthoSize, outlineDepthBias; public float outlineRefOrthoSize, outlineDepthBias;
public float outlineTintAmount, outlineTintDarkness;
public float minimumDarkness, shadowColorScale, shadowTintAmount; public float minimumDarkness, shadowColorScale, shadowTintAmount;
public Color shadowTint; public Color shadowTint;
public int charShades; public int charShades;
@ -59,6 +60,7 @@ namespace WL.Look.Arena
o.outlineColor = c.outlineColor; o.outlineOnBackground = c.outlineOnBackground; o.outlineColor = c.outlineColor; o.outlineOnBackground = c.outlineOnBackground;
o.outlineScaleWithZoom = c.outlineScaleWithZoom; o.outlineRefOrthoSize = c.outlineRefOrthoSize; o.outlineScaleWithZoom = c.outlineScaleWithZoom; o.outlineRefOrthoSize = c.outlineRefOrthoSize;
o.outlineDepthBias = c.outlineDepthBias; o.outlineDepthBias = c.outlineDepthBias;
o.outlineTintAmount = c.outlineTintAmount; o.outlineTintDarkness = c.outlineTintDarkness;
o.minimumDarkness = c.minimumDarkness; o.shadowColorScale = c.shadowColorScale; o.minimumDarkness = c.minimumDarkness; o.shadowColorScale = c.shadowColorScale;
o.shadowTintAmount = c.shadowTintAmount; o.shadowTint = c.shadowTint; o.shadowTintAmount = c.shadowTintAmount; o.shadowTint = c.shadowTint;
o.charShades = c.charShades; o.charShades = c.charShades;
@ -107,28 +109,21 @@ namespace WL.Look.Arena
OutlinedRenderers = 0; RimRenderers = 0; ContrastMaterials = 0; OutlinedRenderers = 0; RimRenderers = 0; ContrastMaterials = 0;
Material outlineChar = null, outlineBg = null, rimMat = null; Shader outlineShader = o.outline ? Shader.Find("WL/HullOutline") : null;
if (o.outline) // 줌 비례 — 직교 크기가 커지면(멀리서 보면) 화면 픽셀 두께를 같은 비율로 줄인다.
// 그래야 「캐릭터 키 대비 굵기」가 프레이밍과 무관하게 일정하다(2D 스프라이트처럼).
// 기준 = 아레나 실제 카메라(직교 10) → SO 의 px 값이 곧 「인게임에서 보이는 px」다.
float zoom = 1f;
{ {
var sh = Shader.Find("WL/HullOutline"); var c0 = Camera.main;
if (sh != null) 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;
// 그래야 「캐릭터 키 대비 굵기」가 프레이밍과 무관하게 일정하다(2D 스프라이트처럼).
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;
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);
}
} }
s_outlineShader = outlineShader; s_zoom = zoom; s_opt = o;
s_outlines.Clear();
Material rimMat = null;
if (o.rim) if (o.rim)
{ {
var sh = Shader.Find("WL/RimAdd"); var sh = Shader.Find("WL/RimAdd");
@ -173,7 +168,7 @@ namespace WL.Look.Arena
// Ⓐ 외곽선 · Ⓓ 림 — 머티리얼 배열 뒤에 덧붙인다(서브메시 1개면 한 번 더 그린다) // Ⓐ 외곽선 · Ⓓ 림 — 머티리얼 배열 뒤에 덧붙인다(서브메시 1개면 한 번 더 그린다)
var extra = new List<Material>(2); var extra = new List<Material>(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 (o.rim && rimMat != null && (isChar || o.rimOnBackground)) extra.Add(rimMat);
if (extra.Count > 0 && extra[0] != null) 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]); for (int i = 0; i < s_created.Count; i++) if (s_created[i] != null) DestroyAny(s_created[i]);
s_created.Clear(); s_created.Clear();
s_clones.Clear(); s_clones.Clear();
s_outlines.Clear();
OutlinedRenderers = 0; RimRenderers = 0; ContrastMaterials = 0; OutlinedRenderers = 0; RimRenderers = 0; ContrastMaterials = 0;
if (s_applied) LastLog = "restored(814s 상태)"; if (s_applied) LastLog = "restored(814s 상태)";
s_applied = false; s_applied = false;
@ -303,6 +299,32 @@ namespace WL.Look.Arena
return anim != null ? anim.gameObject : null; 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) static Material Make(Shader sh)
{ {
var m = new Material(sh); var m = new Material(sh);

View File

@ -48,8 +48,9 @@ namespace WL.Look.Arena
[Header("Ⓐ 외곽선 (inverted hull · 두께 단위 = 화면 픽셀 @1080×1920)")] [Header("Ⓐ 외곽선 (inverted hull · 두께 단위 = 화면 픽셀 @1080×1920)")]
public bool outlineOn = true; public bool outlineOn = true;
[Tooltip("캐릭터 외곽선 두께(px @직교 3.4). PD 지시 = 굵게. 레퍼런스2 비율 = 캐릭터 키의 1/80~1/60 ≈ 4.3~5.7 px.")] [Tooltip("캐릭터 외곽선 두께(px · 아레나 실제 카메라 직교 10 = 캐릭터 키 98~117 px 기준). " +
public float outlineWidth = 6.0f; "🔴 814u 실측: 이 크기에서 1 px 띠가 이미 캐릭터 면적의 27 %, 3 px 이면 76 % 를 먹는다 → 상한 1(비교용 2).")]
public float outlineWidth = 1.0f;
[Tooltip("도트(픽셀) 모드에서 쓰는 캐릭터 외곽선 두께(px · 저해상도 RT 기준).")] [Tooltip("도트(픽셀) 모드에서 쓰는 캐릭터 외곽선 두께(px · 저해상도 RT 기준).")]
public float outlineWidthPixelMode = 1.0f; public float outlineWidthPixelMode = 1.0f;
@ -60,14 +61,20 @@ namespace WL.Look.Arena
[Tooltip("배경(바위·나무·건물)에도 외곽선을 넣을지.")] [Tooltip("배경(바위·나무·건물)에도 외곽선을 넣을지.")]
public bool outlineOnBackground = true; public bool outlineOnBackground = true;
[Tooltip("배경 외곽선 두께(px). 저폴리 하드에지라 캐릭터보다 얇게 잡는다.")] [Tooltip("배경 외곽선 두께(px @직교 10). 배경 물체는 화면에서 크므로 캐릭터보다 굵어도 된다.")]
public float outlineWidthBackground = 2.5f; public float outlineWidthBackground = 2.5f;
[Tooltip("켜면 두께를 카메라 줌(직교 크기)에 맞춰 비례시킨다 = 캐릭터 대비 굵기가 항상 같다(2D 스프라이트처럼).")] [Tooltip("켜면 두께를 카메라 줌(직교 크기)에 맞춰 비례시킨다 = 캐릭터 대비 굵기가 항상 같다(2D 스프라이트처럼).")]
public bool outlineScaleWithZoom = true; public bool outlineScaleWithZoom = true;
[Tooltip("위 비례의 기준 직교 반높이. 이 크기일 때 두께가 위 px 값 그대로다.")] [Tooltip("위 비례의 기준 직교 반높이 = 아레나 실제 카메라(10). 이 크기일 때 두께가 위 px 값 그대로다.")]
public float outlineRefOrthoSize = 3.4f; 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). 저폴리 하드에지의 실루엣 안쪽 얼룩을 없앤다.")] [Tooltip("껍데기를 카메라에서 뒤로 미는 양(m). 저폴리 하드에지의 실루엣 안쪽 얼룩을 없앤다.")]
public float outlineDepthBias = 0.06f; public float outlineDepthBias = 0.06f;

View File

@ -29,6 +29,11 @@ Shader "WL/HullOutline"
[HDR] _OutlineColor ("Outline Color", Color) = (0, 0, 0, 1) [HDR] _OutlineColor ("Outline Color", Color) = (0, 0, 0, 1)
_OutlineWidth ("Outline Width (screen px)", Float) = 2.0 _OutlineWidth ("Outline Width (screen px)", Float) = 2.0
_OutlineDepthBias ("Depth Bias (m · 하드에지 저폴리의 내부 얼룩 제거)", Float) = 0.06 _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 SubShader
@ -53,22 +58,30 @@ Shader "WL/HullOutline"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap);
CBUFFER_START(UnityPerMaterial) CBUFFER_START(UnityPerMaterial)
float4 _OutlineColor; float4 _OutlineColor;
float _OutlineWidth; float _OutlineWidth;
float _OutlineDepthBias; float _OutlineDepthBias;
float4 _BaseMap_ST;
float4 _TintColor;
float _TintDarkness;
float _TintAmount;
CBUFFER_END CBUFFER_END
struct Attributes struct Attributes
{ {
float4 positionOS : POSITION; float4 positionOS : POSITION;
float3 normalOS : NORMAL; float3 normalOS : NORMAL;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_VERTEX_INPUT_INSTANCE_ID
}; };
struct Varyings struct Varyings
{ {
float4 positionCS : SV_POSITION; float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_VERTEX_OUTPUT_STEREO UNITY_VERTEX_OUTPUT_STEREO
}; };
@ -104,13 +117,18 @@ Shader "WL/HullOutline"
posCS.xy += dirPx * ndcPerPx * _OutlineWidth * posCS.w; posCS.xy += dirPx * ndcPerPx * _OutlineWidth * posCS.w;
o.positionCS = posCS; o.positionCS = posCS;
o.uv = TRANSFORM_TEX(input.uv, _BaseMap);
return o; return o;
} }
half4 frag(Varyings input) : SV_Target half4 frag(Varyings input) : SV_Target
{ {
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input); 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 ENDHLSL
} }