[WL-816zg] 섬 물가 스커트(완만한 모래 경사) (#816)
PD 결정(2026-09-15 「물은 진행」): 데모 연못처럼 섬 가장자리가 완만한 모래 경사로 물에 잠기게. - 신규 WLShoreSkirt.cs — 열린 타일(FI Island IsUnlocked · 한 변 IslandManager.IslandSize) 발자국의 바깥 테두리 선분까지의 유클리드 거리장(부호: 안 −/밖 +)으로 격자(셀 0.5 m) 띠 메시 1장 생성. y = smoothstep(d / skirtWidth) 로 윗면(0 − skirtTopDrop 0.04) → skirtBottomY −1.8 · 안쪽 d < −0.3 은 만들지 않음 · 격자 원점을 타일 가장자리에 맞춰 틈 0 · 볼록 모서리는 자동으로 둥긂. 정점색 = d 0~0.6 풀색(islandTopMaterial _DiffuseColor) → 모래색(softDirtColor) · 물 아래 ×0.85. 멱등(발자국·값 서명) · 드로우콜 +1 · 삼각형 수 로그. - 신규 Shaders/WLShoreSkirt.shader `WL/Shore Skirt` — URP 언릿 정점색 · 불투명 · 그림자 off · DepthOnly/DepthNormals 패스 포함(물 셰이더가 깊이로 물가 띠를 만들므로 깊이 프리패스 모드에서도 보이게) · SRP Batcher 호환 · target 2.0 · 텍스처 0. - WLIslandLookSettings §0-c: shoreSkirtEnabled 1 · skirtWidth 3.0 · skirtBottomY −1.8 · skirtGrassBand 0.6 · skirtSandBlend 0.6 · skirtCell 0.5 · skirtInnerKeep 0.3 · skirtTopDrop 0.04 · skirtTopMode 1(고정 skirtTopY 0) · skirtUnderwaterMul 0.85 · skirtGrassFallback · skirtMaxGridNodes 400000 · shoreSkirtMaterial(선택). 되돌리기 = shoreSkirtEnabled 0. - WLIslandLook.Apply: SpawnSeabed 다음에 SpawnShoreSkirt · 재훑기·RequestGrassRebuild(확장) 같은 훅에서 재생성 · 로그 항목. - 원본(FarmingIsland/3DPixelArtEnvironment/Script) 0줄 · ProjectSettings 0줄 · 유니티 미실행(컴파일은 Lead). Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
3752b0484c
commit
112eca1b72
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3cc6fee6fe84883494fb35f29859f927
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,204 @@
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// WLShoreSkirt.shader — 섬 둘레 물가 스커트(완만한 모래 경사) 전용 URP 언릿 정점색 (WL-816zg · #816)
|
||||||
|
//
|
||||||
|
// 왜 전용 셰이더인가:
|
||||||
|
// · 색은 전부 CPU 가 정점색으로 굽는다(풀 → 모래 → 물속 어둡기). 셰이더는 그 값을 그대로 내보낸다 = 텍스처 0 · 분기 0.
|
||||||
|
// · URP Unlit 은 정점색을 읽지 않고, Lit 계열은 조명·그림자 변형이 붙어 모바일에서 비싸다.
|
||||||
|
// · 물 셰이더(Farm_Water_Demo3)가 **깊이 텍스처**로 물가 흰 띠를 만든다 → URP 가 깊이 프리패스로 도는 설정에서도
|
||||||
|
// 경사가 깊이에 들어가야 하므로 DepthOnly · DepthNormals 패스를 함께 둔다(파이프라인이 요구할 때만 돈다 = 평소 비용 0).
|
||||||
|
//
|
||||||
|
// 모바일 제약 준수 (PD 상시 규칙 d):
|
||||||
|
// · geometry / tessellation 없음 · 불투명 · ZWrite On · 그림자 없음(ShadowCaster 패스 없음)
|
||||||
|
// · SRP Batcher 호환 (CBUFFER_START(UnityPerMaterial) — 모든 패스 동일)
|
||||||
|
// · #pragma target 2.0 · half 정밀도 · 텍스처 샘플 0회
|
||||||
|
//
|
||||||
|
// 정점색 규약 (WLShoreSkirt.cs 가 굽는다): RGB = 최종 색(리니어 파이프라인이면 리니어 값) · A = 미사용.
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
Shader "WL/Shore Skirt"
|
||||||
|
{
|
||||||
|
Properties
|
||||||
|
{
|
||||||
|
_Tint ("색 배율 (정점색에 곱함)", Color) = (1, 1, 1, 1)
|
||||||
|
[Enum(UnityEngine.Rendering.CullMode)] _Cull ("Cull", Float) = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
SubShader
|
||||||
|
{
|
||||||
|
Tags
|
||||||
|
{
|
||||||
|
"RenderPipeline" = "UniversalPipeline"
|
||||||
|
"RenderType" = "Opaque"
|
||||||
|
"Queue" = "Geometry"
|
||||||
|
"IgnoreProjector" = "True"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 본 패스: 정점색 그대로 ─────────────────────────────────────────
|
||||||
|
Pass
|
||||||
|
{
|
||||||
|
Name "ShoreSkirt"
|
||||||
|
Tags { "LightMode" = "UniversalForward" }
|
||||||
|
|
||||||
|
Blend Off
|
||||||
|
ZWrite On
|
||||||
|
ZTest LEqual
|
||||||
|
Cull [_Cull]
|
||||||
|
|
||||||
|
HLSLPROGRAM
|
||||||
|
#pragma target 2.0
|
||||||
|
#pragma vertex vert
|
||||||
|
#pragma fragment frag
|
||||||
|
#pragma multi_compile_instancing
|
||||||
|
|
||||||
|
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
|
||||||
|
|
||||||
|
CBUFFER_START(UnityPerMaterial)
|
||||||
|
half4 _Tint;
|
||||||
|
CBUFFER_END
|
||||||
|
|
||||||
|
struct Attributes
|
||||||
|
{
|
||||||
|
float4 positionOS : POSITION;
|
||||||
|
float4 color : COLOR;
|
||||||
|
UNITY_VERTEX_INPUT_INSTANCE_ID
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Varyings
|
||||||
|
{
|
||||||
|
float4 positionCS : SV_POSITION;
|
||||||
|
half4 color : COLOR;
|
||||||
|
UNITY_VERTEX_OUTPUT_STEREO
|
||||||
|
};
|
||||||
|
|
||||||
|
Varyings vert (Attributes IN)
|
||||||
|
{
|
||||||
|
Varyings OUT = (Varyings)0;
|
||||||
|
UNITY_SETUP_INSTANCE_ID(IN);
|
||||||
|
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
|
||||||
|
|
||||||
|
OUT.positionCS = TransformObjectToHClip(IN.positionOS.xyz);
|
||||||
|
OUT.color = half4(IN.color.rgb, 1.0) * _Tint;
|
||||||
|
return OUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
half4 frag (Varyings IN) : SV_Target
|
||||||
|
{
|
||||||
|
return half4(IN.color.rgb, 1.0);
|
||||||
|
}
|
||||||
|
ENDHLSL
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 깊이 전용 패스: 물 셰이더가 깊이 텍스처에서 경사를 본다 ───────
|
||||||
|
Pass
|
||||||
|
{
|
||||||
|
Name "DepthOnly"
|
||||||
|
Tags { "LightMode" = "DepthOnly" }
|
||||||
|
|
||||||
|
ZWrite On
|
||||||
|
ColorMask R
|
||||||
|
Cull [_Cull]
|
||||||
|
|
||||||
|
HLSLPROGRAM
|
||||||
|
#pragma target 2.0
|
||||||
|
#pragma vertex vertDepth
|
||||||
|
#pragma fragment fragDepth
|
||||||
|
#pragma multi_compile_instancing
|
||||||
|
|
||||||
|
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
|
||||||
|
|
||||||
|
CBUFFER_START(UnityPerMaterial)
|
||||||
|
half4 _Tint;
|
||||||
|
CBUFFER_END
|
||||||
|
|
||||||
|
struct AttributesDepth
|
||||||
|
{
|
||||||
|
float4 positionOS : POSITION;
|
||||||
|
UNITY_VERTEX_INPUT_INSTANCE_ID
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VaryingsDepth
|
||||||
|
{
|
||||||
|
float4 positionCS : SV_POSITION;
|
||||||
|
UNITY_VERTEX_OUTPUT_STEREO
|
||||||
|
};
|
||||||
|
|
||||||
|
VaryingsDepth vertDepth (AttributesDepth IN)
|
||||||
|
{
|
||||||
|
VaryingsDepth OUT = (VaryingsDepth)0;
|
||||||
|
UNITY_SETUP_INSTANCE_ID(IN);
|
||||||
|
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
|
||||||
|
OUT.positionCS = TransformObjectToHClip(IN.positionOS.xyz);
|
||||||
|
return OUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
half fragDepth (VaryingsDepth IN) : SV_Target
|
||||||
|
{
|
||||||
|
return IN.positionCS.z;
|
||||||
|
}
|
||||||
|
ENDHLSL
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 깊이+법선 패스: SSAO 등으로 URP 가 DepthNormals 프리패스를 돌릴 때도 깊이에서 빠지지 않게 ──
|
||||||
|
Pass
|
||||||
|
{
|
||||||
|
Name "DepthNormals"
|
||||||
|
Tags { "LightMode" = "DepthNormals" }
|
||||||
|
|
||||||
|
ZWrite On
|
||||||
|
Cull [_Cull]
|
||||||
|
|
||||||
|
HLSLPROGRAM
|
||||||
|
#pragma target 2.0
|
||||||
|
#pragma vertex vertNormals
|
||||||
|
#pragma fragment fragNormals
|
||||||
|
#pragma multi_compile_instancing
|
||||||
|
#pragma multi_compile_fragment _ _GBUFFER_NORMALS_OCT
|
||||||
|
|
||||||
|
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
|
||||||
|
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Packing.hlsl"
|
||||||
|
|
||||||
|
CBUFFER_START(UnityPerMaterial)
|
||||||
|
half4 _Tint;
|
||||||
|
CBUFFER_END
|
||||||
|
|
||||||
|
struct AttributesNormals
|
||||||
|
{
|
||||||
|
float4 positionOS : POSITION;
|
||||||
|
float3 normalOS : NORMAL;
|
||||||
|
UNITY_VERTEX_INPUT_INSTANCE_ID
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VaryingsNormals
|
||||||
|
{
|
||||||
|
float4 positionCS : SV_POSITION;
|
||||||
|
float3 normalWS : TEXCOORD0;
|
||||||
|
UNITY_VERTEX_OUTPUT_STEREO
|
||||||
|
};
|
||||||
|
|
||||||
|
VaryingsNormals vertNormals (AttributesNormals IN)
|
||||||
|
{
|
||||||
|
VaryingsNormals OUT = (VaryingsNormals)0;
|
||||||
|
UNITY_SETUP_INSTANCE_ID(IN);
|
||||||
|
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
|
||||||
|
OUT.positionCS = TransformObjectToHClip(IN.positionOS.xyz);
|
||||||
|
OUT.normalWS = TransformObjectToWorldNormal(IN.normalOS);
|
||||||
|
return OUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
half4 fragNormals (VaryingsNormals IN) : SV_Target
|
||||||
|
{
|
||||||
|
float3 n = normalize(IN.normalWS);
|
||||||
|
#if defined(_GBUFFER_NORMALS_OCT)
|
||||||
|
float2 octNormalWS = PackNormalOctQuadEncode(n); // [-1, +1]
|
||||||
|
float2 remapped = saturate(octNormalWS * 0.5 + 0.5); // [ 0, 1]
|
||||||
|
half3 packed = PackFloat2To888(remapped);
|
||||||
|
return half4(packed, 0.0);
|
||||||
|
#else
|
||||||
|
return half4(n, 0.0);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
ENDHLSL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Fallback Off
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b56bdf1cf90550693dba8b58ec78e88d
|
||||||
|
ShaderImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
defaultTextures: []
|
||||||
|
nonModifiableTextures: []
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -135,6 +135,7 @@ namespace WL.Look.Farm
|
||||||
// 🔴 816zf — 흙 데칼은 **풀보다 먼저** 만든다. 풀의 밭 제외가 이 마스크를 읽기 때문이다.
|
// 🔴 816zf — 흙 데칼은 **풀보다 먼저** 만든다. 풀의 밭 제외가 이 마스크를 읽기 때문이다.
|
||||||
if (cfg.softDirtEnabled != 0) SpawnSoftDirt(cfg, scene);
|
if (cfg.softDirtEnabled != 0) SpawnSoftDirt(cfg, scene);
|
||||||
if (cfg.seabedEnabled != 0) SpawnSeabed(cfg, scene);
|
if (cfg.seabedEnabled != 0) SpawnSeabed(cfg, scene);
|
||||||
|
if (cfg.shoreSkirtEnabled != 0) SpawnShoreSkirt(cfg, scene);
|
||||||
if (cfg.grassEnabled != 0) SpawnGrass(cfg, scene);
|
if (cfg.grassEnabled != 0) SpawnGrass(cfg, scene);
|
||||||
if (cfg.shoreFoamEnabled != 0) SpawnShoreFoam(cfg, scene);
|
if (cfg.shoreFoamEnabled != 0) SpawnShoreFoam(cfg, scene);
|
||||||
|
|
||||||
|
|
@ -145,6 +146,7 @@ namespace WL.Look.Farm
|
||||||
+ " · ReferenceLook " + (ReferenceLookApplied ? "on" : "off")
|
+ " · ReferenceLook " + (ReferenceLookApplied ? "on" : "off")
|
||||||
+ " · 풀 " + WLIslandGrass.LastLog
|
+ " · 풀 " + WLIslandGrass.LastLog
|
||||||
+ " · 흙데칼 " + WLSoftDirt.LastLog
|
+ " · 흙데칼 " + WLSoftDirt.LastLog
|
||||||
|
+ " · 물가스커트 " + WLShoreSkirt.LastLog
|
||||||
+ " · 둘레거품 " + WLShoreFoam.LastLog);
|
+ " · 둘레거품 " + WLShoreFoam.LastLog);
|
||||||
|
|
||||||
// 늦게 생기는 오브젝트(상인·작물·아이템)까지 다시 훑는다
|
// 늦게 생기는 오브젝트(상인·작물·아이템)까지 다시 훑는다
|
||||||
|
|
@ -175,6 +177,8 @@ namespace WL.Look.Farm
|
||||||
if (g2 != null) { g2.Rebuild(); GrassRebuilds++; }
|
if (g2 != null) { g2.Rebuild(); GrassRebuilds++; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 🔴 816zg — 물가 스커트도 같은 재훑기에서 다시 만든다(발자국이 그대로면 멱등 = 공짜).
|
||||||
|
if (cfg.shoreSkirtEnabled != 0) SpawnShoreSkirt(cfg, scene);
|
||||||
HookIslands(cfg, scene);
|
HookIslands(cfg, scene);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -283,6 +287,12 @@ namespace WL.Look.Farm
|
||||||
var f = Object.FindFirstObjectByType<WLShoreFoam>(FindObjectsInactive.Include);
|
var f = Object.FindFirstObjectByType<WLShoreFoam>(FindObjectsInactive.Include);
|
||||||
if (f != null) { f.cfg = cfg; f.Rebuild(); ShoreFoamRebuilds++; }
|
if (f != null) { f.cfg = cfg; f.Rebuild(); ShoreFoamRebuilds++; }
|
||||||
}
|
}
|
||||||
|
// 🔴 816zg — 섬이 확장되면 물가 스커트도 같은 훅으로 다시 만든다(발자국 변화 → 거리장 재계산).
|
||||||
|
if (cfg.shoreSkirtEnabled != 0)
|
||||||
|
{
|
||||||
|
var sk = Object.FindFirstObjectByType<WLShoreSkirt>(FindObjectsInactive.Include);
|
||||||
|
if (sk != null) { sk.cfg = cfg; sk.Rebuild(); ShoreSkirtRebuilds++; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int ShoreFoamRebuilds;
|
public static int ShoreFoamRebuilds;
|
||||||
|
|
@ -359,6 +369,29 @@ namespace WL.Look.Farm
|
||||||
SeabedLog = "바다 바닥판 y=" + go.transform.position.y.ToString("F2") + (found ? "(물 " + waterY.ToString("F2") + ")" : "(물 미발견 · 기본 -1)") + " · 깊이 " + cfg.seabedDepth + " m";
|
SeabedLog = "바다 바닥판 y=" + go.transform.position.y.ToString("F2") + (found ? "(물 " + waterY.ToString("F2") + ")" : "(물 미발견 · 기본 -1)") + " · 깊이 " + cfg.seabedDepth + " m";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool ShoreSkirtSpawned;
|
||||||
|
public static int ShoreSkirtRebuilds;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 816zg — 섬 둘레 완만한 모래 경사(물가 스커트). 바닥판·거품과 같은 자리에서 만들고 같은 훅으로 다시 만든다.
|
||||||
|
/// 열린 타일 발자국의 거리장으로 띠 메시 1장(드로우콜 +1)을 굽는다 — 본체는 <see cref="WLShoreSkirt"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static void SpawnShoreSkirt(WLIslandLookSettings cfg, Scene scene)
|
||||||
|
{
|
||||||
|
var s = Object.FindFirstObjectByType<WLShoreSkirt>(FindObjectsInactive.Include);
|
||||||
|
if (s == null)
|
||||||
|
{
|
||||||
|
var go = new GameObject(WLShoreSkirt.ObjectName);
|
||||||
|
SceneManager.MoveGameObjectToScene(go, scene);
|
||||||
|
go.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
|
||||||
|
s = go.AddComponent<WLShoreSkirt>();
|
||||||
|
s.cfg = cfg;
|
||||||
|
s.Rebuild(); // OnEnable 이 cfg 전에 돌 수 있다
|
||||||
|
}
|
||||||
|
else { s.cfg = cfg; s.Rebuild(); }
|
||||||
|
ShoreSkirtSpawned = true;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>섬 둘레 거품 띠 — 풀과 같은 자리에서 만들고 같은 훅으로 다시 만든다(816j2).</summary>
|
/// <summary>섬 둘레 거품 띠 — 풀과 같은 자리에서 만들고 같은 훅으로 다시 만든다(816j2).</summary>
|
||||||
public static void SpawnShoreFoam(WLIslandLookSettings cfg, Scene scene)
|
public static void SpawnShoreFoam(WLIslandLookSettings cfg, Scene scene)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -306,6 +306,54 @@ namespace WL.Look.Farm
|
||||||
[Tooltip("바닥판 색(언릿 · sRGB). 기본 = 데모 모래색.")]
|
[Tooltip("바닥판 색(언릿 · sRGB). 기본 = 데모 모래색.")]
|
||||||
public Color seabedColor = new Color(0.71f, 0.706f, 0.549f, 1f);
|
public Color seabedColor = new Color(0.71f, 0.706f, 0.549f, 1f);
|
||||||
|
|
||||||
|
// ───────────────────────────────── §0-c 물가 스커트 (WL-816zg · PD 「물은 진행」 2026-09-15)
|
||||||
|
[Header("§0-c — 섬 둘레 완만한 모래 경사(물가 스커트) — 데모 연못처럼 가장자리가 모래 경사로 물에 잠긴다")]
|
||||||
|
[Tooltip("1 = 열린 타일 발자국 바깥에 거리장 기반 모래 경사 메시 1장(드로우콜 +1)을 깐다 — 둥근 윤곽 · 타일 수직 옆면 가림 · " +
|
||||||
|
"흰 물가 띠는 물 셰이더가 깊이로 만든다. 0 = 없음(지금 상태 100 % 복귀).")]
|
||||||
|
public int shoreSkirtEnabled = 1;
|
||||||
|
|
||||||
|
[Tooltip("경사에 쓸 머티리얼(선택). 비우면 `WL/Shore Skirt` 셰이더로 런타임 머티리얼을 만든다(에셋 0줄). " +
|
||||||
|
"🔴 모바일 빌드에 이 셰이더가 들어가게 하려면 그 셰이더를 쓰는 머티리얼을 여기에 넣어 두는 것이 가장 확실하다(Always Included 대신).")]
|
||||||
|
public Material shoreSkirtMaterial;
|
||||||
|
|
||||||
|
[Tooltip("타일 가장자리에서 바깥(물 쪽)으로 뻗는 경사 폭(m).")]
|
||||||
|
public float skirtWidth = 3.0f;
|
||||||
|
|
||||||
|
[Tooltip("경사 바깥 끝의 높이(월드 y · m). 물(−1.0) 아래 · 바닥판(물 − seabedDepth = −1.8) 위 또는 같게.")]
|
||||||
|
public float skirtBottomY = -1.8f;
|
||||||
|
|
||||||
|
[Tooltip("타일 가장자리에서 이 거리(m)까지는 풀색(islandTopMaterial `_DiffuseColor`), 그 뒤 모래색(softDirtColor)으로 넘어간다.")]
|
||||||
|
public float skirtGrassBand = 0.6f;
|
||||||
|
|
||||||
|
[Tooltip("풀색 → 모래색으로 넘어가는 폭(m).")]
|
||||||
|
public float skirtSandBlend = 0.6f;
|
||||||
|
|
||||||
|
[Tooltip("메시 격자 칸(m). 🔴 타일 한 변(8 m)을 나누는 값이어야 정점이 타일 가장자리 선 위에 정확히 놓인다(0.5 · 0.25 · 1).")]
|
||||||
|
public float skirtCell = 0.5f;
|
||||||
|
|
||||||
|
[Tooltip("타일 안쪽으로 이 거리(m)까지만 정점을 둔다(가장자리 밑으로 살짝 들어가 틈을 막는다). 그 안쪽은 만들지 않는다(윗면 z-fight 방지).")]
|
||||||
|
public float skirtInnerKeep = 0.3f;
|
||||||
|
|
||||||
|
[Tooltip("타일 윗면보다 이만큼(m) 낮은 곳에서 경사를 시작한다 — 윗면과 같은 높이면 z-fight. 타일 옆면이 그만큼만 보인다.")]
|
||||||
|
public float skirtTopDrop = 0.04f;
|
||||||
|
|
||||||
|
[Tooltip("1 = 타일 윗면 높이를 아래 `skirtTopY` 고정값으로(Lead 실측 0 · 확장 애니메이션 중에도 흔들리지 않는다) · " +
|
||||||
|
"0 = 열린 타일 렌더러 bounds 윗면을 실측해 쓴다(로그에 두 값이 같이 찍힌다).")]
|
||||||
|
public int skirtTopMode = 1;
|
||||||
|
|
||||||
|
[Tooltip("타일 윗면 높이 고정값(월드 y · m). 실측(2026-09-15) = 0. 실측 모드에서 렌더러를 못 찾을 때의 대체값이기도 하다.")]
|
||||||
|
public float skirtTopY = 0f;
|
||||||
|
|
||||||
|
[Tooltip("물에 잠긴 정점(y < 물 높이)의 색 배율 — 젖은 모래·물속 어둡기.")]
|
||||||
|
public float skirtUnderwaterMul = 0.85f;
|
||||||
|
|
||||||
|
[Tooltip("풀색 대체값(sRGB). 기본은 `islandTopMaterial`(Farm_IslandTop_Demo3)의 `_DiffuseColor` 를 읽고, 없을 때만 이 값을 쓴다. " +
|
||||||
|
"기본 = 816ze 들판 실측 0.62/0.82/0.485.")]
|
||||||
|
public Color skirtGrassFallback = new Color(0.62f, 0.82f, 0.485f, 1f);
|
||||||
|
|
||||||
|
[Tooltip("거리장·메시 격자 정점 수 상한. 넘으면 칸을 두 배씩 키운다(섬이 아무리 커져도 비용이 묶인다).")]
|
||||||
|
public int skirtMaxGridNodes = 400000;
|
||||||
|
|
||||||
// ───────────────────────────────── §1 풀밭
|
// ───────────────────────────────── §1 풀밭
|
||||||
[Header("§1 — 섬 타일 위 풀밭 (데모와 같은 인스턴싱)")]
|
[Header("§1 — 섬 타일 위 풀밭 (데모와 같은 인스턴싱)")]
|
||||||
[Tooltip("0 이면 풀을 깔지 않는다.")]
|
[Tooltip("0 이면 풀을 깔지 않는다.")]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,395 @@
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// WLShoreSkirt.cs — 섬 둘레 완만한 모래 경사 테두리(물가 스커트) (WL-816zg · #816)
|
||||||
|
//
|
||||||
|
// PD 결정(2026-09-15 「물은 진행」) — 데모 연못처럼 섬 가장자리가 완만한 모래 경사로 물에 잠기게.
|
||||||
|
// · 둥근 윤곽 : 열린 타일 발자국의 **유클리드 거리장**으로 띠를 만든다 → 볼록 모서리가 저절로 둥글다.
|
||||||
|
// · 흰 물가 띠 : 물 셰이더(Farm_Water_Demo3)가 **깊이**로 만든다. 경사가 물 아래까지 이어져 얕은 물이 생긴다.
|
||||||
|
// · 타일 옆면 가림 : 경사가 타일 가장자리(윗면 − skirtTopDrop)에서 시작해 수직 옆면을 덮는다.
|
||||||
|
//
|
||||||
|
// ■ 만드는 법
|
||||||
|
// 1) 열린 타일(FI `Island` · IsUnlocked && activeInHierarchy · 한 변 = IslandManager.IslandSize)의 중심을 모은다.
|
||||||
|
// 2) 발자국의 **바깥 테두리 선분**(이웃 타일이 없는 변)만 골라 둔다. 격자 정점마다 그 선분들까지의 최단 거리가
|
||||||
|
// 부호 있는 거리 d(안쪽 −, 바깥 +)다 — 타일별 사각형 SDF 의 min 은 타일 이음매에서 「안쪽인데 0 근처」로 틀려
|
||||||
|
// 윗면 위에 조각이 생기므로 쓰지 않는다(WLSoftDirt.SignedDist 의 「부호 있는 거리 → 띠」 방식은 그대로).
|
||||||
|
// 3) −skirtInnerKeep ≤ d ≤ skirtWidth 인 격자(셀 skirtCell)만 만들고, y = smoothstep(d / skirtWidth) 로 윗면 → skirtBottomY.
|
||||||
|
// 격자 원점을 타일 가장자리에 맞춰 정점이 가장자리 선 위에 정확히 놓인다(틈 0 · 윗면 z-fight 0).
|
||||||
|
// 4) 색 = 정점색. d 0~skirtGrassBand 는 풀색(islandTopMaterial `_DiffuseColor`) → 모래색(softDirtColor) · 물 아래 × skirtUnderwaterMul.
|
||||||
|
// 셰이더 `WL/Shore Skirt` = 언릿 정점색 · URP · DepthOnly/DepthNormals 패스 포함(물 셰이더가 깊이 텍스처에서 경사를 봐야 한다).
|
||||||
|
// 메시 1장 · 드로우콜 +1 · 삼각형 수 = LastTriangles(로그에도 남긴다).
|
||||||
|
//
|
||||||
|
// ■ 되돌리기 — `shoreSkirtEnabled = 0` 이면 만들지 않는다(지금 상태 100 %).
|
||||||
|
//
|
||||||
|
// 🔴 FI 코드 0줄 · 씬 파일 0줄 · 에셋 0줄. 메시·머티리얼은 런타임 생성이다.
|
||||||
|
// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(ErrorLogHookManager 팝업).
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Rendering;
|
||||||
|
using FIIsland = CryingSnow.FarmingIsland.Island;
|
||||||
|
using FIIslandManager = CryingSnow.FarmingIsland.IslandManager;
|
||||||
|
|
||||||
|
namespace WL.Look.Farm
|
||||||
|
{
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
public sealed class WLShoreSkirt : MonoBehaviour
|
||||||
|
{
|
||||||
|
public const string ObjectName = "~WL_ShoreSkirt";
|
||||||
|
public const string ShaderName = "WL/Shore Skirt";
|
||||||
|
|
||||||
|
// ── 진단(프로브·보고가 읽는다)
|
||||||
|
public static int Rebuilds;
|
||||||
|
public static int LastTiles, LastVertices, LastTriangles;
|
||||||
|
public static string LastLog = "꺼짐";
|
||||||
|
|
||||||
|
public WLIslandLookSettings cfg;
|
||||||
|
|
||||||
|
MeshFilter _mf;
|
||||||
|
MeshRenderer _mr;
|
||||||
|
Mesh _mesh;
|
||||||
|
Material _inst;
|
||||||
|
bool _vertexColor; // 전용 셰이더를 찾았는가(아니면 URP Unlit 모래 단색 대체)
|
||||||
|
|
||||||
|
static readonly int IdBaseColor = Shader.PropertyToID("_BaseColor");
|
||||||
|
static readonly int IdColor = Shader.PropertyToID("_Color");
|
||||||
|
static readonly int IdDiffuseColor = Shader.PropertyToID("_DiffuseColor");
|
||||||
|
|
||||||
|
struct Seg { public float ax, az, bx, bz; }
|
||||||
|
readonly List<Vector3> _tiles = new List<Vector3>(); // x·z = 타일 중심 · y = 렌더러 윗면 실측
|
||||||
|
readonly List<Seg> _edges = new List<Seg>(); // 발자국 바깥 테두리 선분
|
||||||
|
readonly List<Vector3> _verts = new List<Vector3>();
|
||||||
|
readonly List<Color32> _cols = new List<Color32>();
|
||||||
|
readonly List<int> _tris = new List<int>();
|
||||||
|
|
||||||
|
// 마지막으로 만든 조건 — 같으면 다시 만들지 않는다(1초 폴링·재훑기에서 공짜로 돌게).
|
||||||
|
bool _dirty = true;
|
||||||
|
long _lastTileSig;
|
||||||
|
float _lastValSig = float.NaN;
|
||||||
|
|
||||||
|
void OnEnable() { Ensure(); _dirty = true; Rebuild(); }
|
||||||
|
void OnDisable() { if (_mr != null) _mr.enabled = false; }
|
||||||
|
|
||||||
|
void OnDestroy()
|
||||||
|
{
|
||||||
|
if (_inst != null) Destroy(_inst);
|
||||||
|
if (_mesh != null) Destroy(_mesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Ensure()
|
||||||
|
{
|
||||||
|
if (_mf == null) _mf = GetComponent<MeshFilter>();
|
||||||
|
if (_mf == null) _mf = gameObject.AddComponent<MeshFilter>();
|
||||||
|
if (_mr == null) _mr = GetComponent<MeshRenderer>();
|
||||||
|
if (_mr == null) _mr = gameObject.AddComponent<MeshRenderer>();
|
||||||
|
_mr.shadowCastingMode = ShadowCastingMode.Off;
|
||||||
|
_mr.receiveShadows = false;
|
||||||
|
_mr.lightProbeUsage = LightProbeUsage.Off;
|
||||||
|
_mr.reflectionProbeUsage = ReflectionProbeUsage.Off;
|
||||||
|
_mr.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion;
|
||||||
|
if (_mesh == null)
|
||||||
|
{
|
||||||
|
_mesh = new Mesh { name = "WL_ShoreSkirtMesh" };
|
||||||
|
_mesh.MarkDynamic();
|
||||||
|
_mesh.hideFlags = HideFlags.DontSave;
|
||||||
|
}
|
||||||
|
_mf.sharedMesh = _mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>다시 만든다(섬 확장·재훑기). 멱등 — 발자국·값이 그대로면 아무것도 하지 않는다.</summary>
|
||||||
|
public void Rebuild()
|
||||||
|
{
|
||||||
|
Ensure();
|
||||||
|
if (cfg == null) cfg = WLIslandLookSettings.Instance;
|
||||||
|
if (cfg == null || cfg.enabled_ == 0 || cfg.shoreSkirtEnabled == 0)
|
||||||
|
{ _mr.enabled = false; LastLog = "꺼짐"; return; }
|
||||||
|
|
||||||
|
float size = TileSize();
|
||||||
|
if (size <= 0.01f) { _mr.enabled = false; LastLog = "타일 크기 미확인(IslandManager 없음) — 대기"; return; }
|
||||||
|
float half = size * 0.5f;
|
||||||
|
|
||||||
|
long tileSig = CollectTiles(out float topMeasured);
|
||||||
|
LastTiles = _tiles.Count;
|
||||||
|
if (_tiles.Count == 0) { _mr.enabled = false; LastLog = "열린 타일 0 — 대기"; return; }
|
||||||
|
|
||||||
|
bool fixedTop = cfg.skirtTopMode != 0 || float.IsNaN(topMeasured);
|
||||||
|
float topY = fixedTop ? cfg.skirtTopY : topMeasured;
|
||||||
|
float waterY = FindWaterY(out bool waterFound);
|
||||||
|
|
||||||
|
Color grass = ToVertexSpace(GrassColor());
|
||||||
|
Color sand = ToVertexSpace(cfg.softDirtColor);
|
||||||
|
|
||||||
|
// 값이 바뀌면 다시 만든다(인스펙터에서 만져도 다음 폴링에 반영된다).
|
||||||
|
float valSig = cfg.skirtWidth * 7.1f + cfg.skirtBottomY * 13.3f + cfg.skirtGrassBand * 3.7f + cfg.skirtSandBlend * 29.3f
|
||||||
|
+ cfg.skirtCell * 5.9f + cfg.skirtInnerKeep * 11.9f + cfg.skirtTopDrop * 17.7f + cfg.skirtUnderwaterMul * 31.7f
|
||||||
|
+ cfg.skirtMaxGridNodes * 0.001f + topY * 37.3f + waterY * 41.1f
|
||||||
|
+ grass.r * 101f + grass.g * 103f + grass.b * 107f + sand.r * 109f + sand.g * 113f + sand.b * 127f;
|
||||||
|
|
||||||
|
bool same = !_dirty && _inst != null && tileSig == _lastTileSig && Mathf.Abs(_lastValSig - valSig) < 0.0001f;
|
||||||
|
if (same) { _mr.enabled = true; return; }
|
||||||
|
|
||||||
|
if (!EnsureMaterial()) { _mr.enabled = false; LastLog = "🔴 셰이더를 못 찾음(WL/Shore Skirt · URP Unlit 둘 다)"; return; }
|
||||||
|
|
||||||
|
BuildEdges(size, half);
|
||||||
|
float cell = BuildMesh(half, topY, waterY, grass, sand);
|
||||||
|
|
||||||
|
_mr.sharedMaterial = _inst;
|
||||||
|
_mr.enabled = true;
|
||||||
|
_lastTileSig = tileSig; _lastValSig = valSig; _dirty = false;
|
||||||
|
Rebuilds++;
|
||||||
|
|
||||||
|
LastLog = "물가 스커트 — 타일 " + _tiles.Count + " · 테두리 변 " + _edges.Count
|
||||||
|
+ " · 정점 " + LastVertices + " · 삼각형 " + LastTriangles
|
||||||
|
+ " · 폭 " + cfg.skirtWidth.ToString("F1") + " m · 윗면 y " + topY.ToString("F2")
|
||||||
|
+ (fixedTop ? "(고정 · 실측 " + (float.IsNaN(topMeasured) ? "없음" : topMeasured.ToString("F2"))
|
||||||
|
: "(실측 · 고정값 " + cfg.skirtTopY.ToString("F2")) + ")"
|
||||||
|
+ " → 바닥 " + cfg.skirtBottomY.ToString("F2") + " · 물 " + waterY.ToString("F2") + (waterFound ? "" : "(미발견·대체값)")
|
||||||
|
+ " · 격자 " + cell.ToString("F2") + " m · 셰이더 " + (_vertexColor ? ShaderName : "대체 URP Unlit(정점색 없음 · 모래 단색)")
|
||||||
|
+ " · 드로우콜 +1";
|
||||||
|
cfg.Log(LastLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// 입력 수집
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
static float TileSize()
|
||||||
|
{
|
||||||
|
var im = FIIslandManager.Instance;
|
||||||
|
return im != null ? im.IslandSize : 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>열린 타일 중심을 모은다. 반환 = 발자국 서명(순서 무관) — 확장으로 바뀌면 값이 바뀐다.</summary>
|
||||||
|
long CollectTiles(out float topMeasured)
|
||||||
|
{
|
||||||
|
_tiles.Clear();
|
||||||
|
topMeasured = float.NaN;
|
||||||
|
var scene = gameObject.scene;
|
||||||
|
var islands = Object.FindObjectsByType<FIIsland>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
|
||||||
|
long sig = 17;
|
||||||
|
for (int i = 0; i < islands.Length; i++)
|
||||||
|
{
|
||||||
|
var isl = islands[i];
|
||||||
|
if (isl == null || !isl.IsUnlocked || !isl.gameObject.activeInHierarchy) continue;
|
||||||
|
if (scene.IsValid() && isl.gameObject.scene != scene) continue;
|
||||||
|
var p = isl.transform.position;
|
||||||
|
float top = p.y;
|
||||||
|
var r = isl.GetComponent<Renderer>(); // 섬 본체 렌더러(FIIsland 와 같은 오브젝트 · SwapMaterials 와 같은 판별)
|
||||||
|
if (r != null) top = r.bounds.max.y;
|
||||||
|
if (float.IsNaN(topMeasured) || top > topMeasured) topMeasured = top;
|
||||||
|
_tiles.Add(new Vector3(p.x, top, p.z));
|
||||||
|
sig += ((long)Mathf.RoundToInt(p.x * 8f) * 73856093L) ^ ((long)Mathf.RoundToInt(p.z * 8f) * 19349663L);
|
||||||
|
}
|
||||||
|
return sig ^ ((long)_tiles.Count << 40);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>물 높이 — SpawnSeabed 와 같은 판별(씬의 `Water` 렌더러). 없으면 거품 띠의 대체값.</summary>
|
||||||
|
float FindWaterY(out bool found)
|
||||||
|
{
|
||||||
|
var scene = gameObject.scene;
|
||||||
|
var mrs = Object.FindObjectsByType<MeshRenderer>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||||
|
for (int i = 0; i < mrs.Length; i++)
|
||||||
|
{
|
||||||
|
var r = mrs[i];
|
||||||
|
if (r == null || r.gameObject.name != "Water") continue;
|
||||||
|
if (scene.IsValid() && r.gameObject.scene != scene) continue;
|
||||||
|
found = true;
|
||||||
|
return r.transform.position.y;
|
||||||
|
}
|
||||||
|
found = false;
|
||||||
|
return cfg.shoreFoamFallbackY;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>풀색 — 타일 윗면 머티리얼(Farm_IslandTop_Demo3)의 `_DiffuseColor`. 없으면 `_BaseColor`·`_Color` → 설정 대체값.</summary>
|
||||||
|
Color GrassColor()
|
||||||
|
{
|
||||||
|
var m = cfg.islandTopMaterial;
|
||||||
|
if (m != null)
|
||||||
|
{
|
||||||
|
if (m.HasProperty(IdDiffuseColor)) return m.GetColor(IdDiffuseColor);
|
||||||
|
if (m.HasProperty(IdBaseColor)) return m.GetColor(IdBaseColor);
|
||||||
|
if (m.HasProperty(IdColor)) return m.GetColor(IdColor);
|
||||||
|
}
|
||||||
|
return cfg.skirtGrassFallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 설정·머티리얼의 색은 sRGB 로 적힌 값이다(URP Unlit `_BaseColor` 와 같은 해석 — 바닥판·흙 데칼과 같은 숫자면 같은 색).
|
||||||
|
/// 정점색은 변환 없이 셰이더로 들어가므로 리니어 파이프라인에서는 리니어로 옮겨 넣는다.
|
||||||
|
/// </summary>
|
||||||
|
static Color ToVertexSpace(Color c)
|
||||||
|
{
|
||||||
|
return QualitySettings.activeColorSpace == ColorSpace.Linear ? c.linear : c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>전용 셰이더(정점색) → 없으면 URP Unlit 모래 단색 대체. 에셋은 만들지 않는다.</summary>
|
||||||
|
bool EnsureMaterial()
|
||||||
|
{
|
||||||
|
if (_inst != null) return true;
|
||||||
|
|
||||||
|
Shader sh = cfg.shoreSkirtMaterial != null ? cfg.shoreSkirtMaterial.shader : null;
|
||||||
|
if (sh == null) sh = Shader.Find(ShaderName);
|
||||||
|
if (sh == null) sh = Shader.Find("Universal Render Pipeline/Unlit");
|
||||||
|
if (sh == null) sh = Shader.Find("Unlit/Color");
|
||||||
|
if (sh == null) return false;
|
||||||
|
_vertexColor = sh.name == ShaderName;
|
||||||
|
|
||||||
|
_inst = cfg.shoreSkirtMaterial != null && cfg.shoreSkirtMaterial.shader == sh
|
||||||
|
? new Material(cfg.shoreSkirtMaterial) : new Material(sh);
|
||||||
|
_inst.name = "WL_ShoreSkirt(runtime)";
|
||||||
|
_inst.hideFlags = HideFlags.DontSave;
|
||||||
|
if (!_vertexColor)
|
||||||
|
{
|
||||||
|
// 대체 셰이더의 색 프로퍼티는 sRGB 해석 → 설정값을 그대로 넣는다(흙 데칼과 같은 색)
|
||||||
|
if (_inst.HasProperty(IdBaseColor)) _inst.SetColor(IdBaseColor, cfg.softDirtColor);
|
||||||
|
if (_inst.HasProperty(IdColor)) _inst.SetColor(IdColor, cfg.softDirtColor);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// 거리장 — 발자국 바깥 테두리 선분까지의 유클리드 거리(정확) · 부호 = 타일 안(−)/밖(+)
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
void BuildEdges(float size, float half)
|
||||||
|
{
|
||||||
|
_edges.Clear();
|
||||||
|
float tol = size * 0.25f; // 이웃 판정 허용 오차(타일 격자는 정수 배)
|
||||||
|
for (int i = 0; i < _tiles.Count; i++)
|
||||||
|
{
|
||||||
|
float cx = _tiles[i].x, cz = _tiles[i].z;
|
||||||
|
if (!HasTile(cx + size, cz, tol)) _edges.Add(new Seg { ax = cx + half, az = cz - half, bx = cx + half, bz = cz + half });
|
||||||
|
if (!HasTile(cx - size, cz, tol)) _edges.Add(new Seg { ax = cx - half, az = cz - half, bx = cx - half, bz = cz + half });
|
||||||
|
if (!HasTile(cx, cz + size, tol)) _edges.Add(new Seg { ax = cx - half, az = cz + half, bx = cx + half, bz = cz + half });
|
||||||
|
if (!HasTile(cx, cz - size, tol)) _edges.Add(new Seg { ax = cx - half, az = cz - half, bx = cx + half, bz = cz - half });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasTile(float x, float z, float tol)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _tiles.Count; i++)
|
||||||
|
if (Mathf.Abs(_tiles[i].x - x) <= tol && Mathf.Abs(_tiles[i].z - z) <= tol) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Inside(float x, float z, float half)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _tiles.Count; i++)
|
||||||
|
if (Mathf.Abs(x - _tiles[i].x) <= half && Mathf.Abs(z - _tiles[i].z) <= half) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>발자국 테두리까지의 부호 있는 거리(m). **음수 = 타일 안 · 양수 = 바깥(물 쪽)** · 테두리 선 위 = 0.</summary>
|
||||||
|
float SignedDist(float x, float z, float half)
|
||||||
|
{
|
||||||
|
float best = float.MaxValue;
|
||||||
|
for (int i = 0; i < _edges.Count; i++)
|
||||||
|
{
|
||||||
|
var s = _edges[i];
|
||||||
|
float ex = s.bx - s.ax, ez = s.bz - s.az;
|
||||||
|
float t = Mathf.Clamp01(((x - s.ax) * ex + (z - s.az) * ez) / (ex * ex + ez * ez));
|
||||||
|
float dx = x - (s.ax + ex * t), dz = z - (s.az + ez * t);
|
||||||
|
float d2 = dx * dx + dz * dz;
|
||||||
|
if (d2 < best) best = d2;
|
||||||
|
}
|
||||||
|
float d = Mathf.Sqrt(best);
|
||||||
|
return Inside(x, z, half) ? -d : d;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
// 메시 — 격자(셀 skirtCell) · 정점마다 d → 높이(smoothstep)·정점색
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
float BuildMesh(float half, float topY, float waterY, Color grass, Color sand)
|
||||||
|
{
|
||||||
|
float cell = Mathf.Max(0.1f, cfg.skirtCell);
|
||||||
|
float width = Mathf.Max(cell, cfg.skirtWidth);
|
||||||
|
float keep = Mathf.Max(0f, cfg.skirtInnerKeep);
|
||||||
|
float top = topY - Mathf.Max(0f, cfg.skirtTopDrop);
|
||||||
|
float bottom = cfg.skirtBottomY;
|
||||||
|
float band = Mathf.Max(0f, cfg.skirtGrassBand);
|
||||||
|
float blend = Mathf.Max(0.01f, cfg.skirtSandBlend);
|
||||||
|
float uw = Mathf.Clamp01(cfg.skirtUnderwaterMul);
|
||||||
|
|
||||||
|
float minX = float.MaxValue, minZ = float.MaxValue, maxX = float.MinValue, maxZ = float.MinValue;
|
||||||
|
for (int i = 0; i < _tiles.Count; i++)
|
||||||
|
{
|
||||||
|
var t = _tiles[i];
|
||||||
|
if (t.x - half < minX) minX = t.x - half;
|
||||||
|
if (t.x + half > maxX) maxX = t.x + half;
|
||||||
|
if (t.z - half < minZ) minZ = t.z - half;
|
||||||
|
if (t.z + half > maxZ) maxZ = t.z + half;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 격자 정점 수 상한 — 넘으면 칸을 키운다(모바일 안전 · 섬이 아무리 커져도 비용이 묶인다)
|
||||||
|
int maxNodes = Mathf.Max(1000, cfg.skirtMaxGridNodes);
|
||||||
|
int mc, nx, nz;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
mc = Mathf.CeilToInt((width + cell * 2f) / cell);
|
||||||
|
nx = Mathf.CeilToInt((maxX - minX) / cell - 0.0001f) + 2 * mc + 1;
|
||||||
|
nz = Mathf.CeilToInt((maxZ - minZ) / cell - 0.0001f) + 2 * mc + 1;
|
||||||
|
if ((long)nx * nz <= maxNodes) break;
|
||||||
|
cell *= 2f;
|
||||||
|
}
|
||||||
|
// 원점 = 발자국 최소 가장자리에서 정수 칸만큼 바깥 → 칸이 타일 한 변을 나누면 정점이 모든 타일 가장자리 선 위에 놓인다
|
||||||
|
float x0 = minX - mc * cell, z0 = minZ - mc * cell;
|
||||||
|
|
||||||
|
var dist = new float[nx * nz];
|
||||||
|
var index = new int[nx * nz];
|
||||||
|
for (int j = 0; j < nz; j++)
|
||||||
|
{
|
||||||
|
float z = z0 + j * cell;
|
||||||
|
for (int i = 0; i < nx; i++) dist[j * nx + i] = SignedDist(x0 + i * cell, z, half);
|
||||||
|
}
|
||||||
|
|
||||||
|
_verts.Clear(); _cols.Clear(); _tris.Clear();
|
||||||
|
float outerKeep = width + cell * 2f; // 사각형 네 귀가 전부 있어야 하므로 바깥으로 두 칸 여유
|
||||||
|
for (int j = 0; j < nz; j++)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < nx; i++)
|
||||||
|
{
|
||||||
|
int n = j * nx + i;
|
||||||
|
float d = dist[n];
|
||||||
|
if (d < -keep || d > outerKeep) { index[n] = -1; continue; }
|
||||||
|
|
||||||
|
float t = Mathf.Clamp01(d / width);
|
||||||
|
t = t * t * (3f - 2f * t); // smoothstep — 윗면에서도 바닥에서도 접선이 수평(완만)
|
||||||
|
float y = top + (bottom - top) * t;
|
||||||
|
index[n] = _verts.Count;
|
||||||
|
_verts.Add(new Vector3(x0 + i * cell, y, z0 + j * cell));
|
||||||
|
|
||||||
|
float k = Mathf.Clamp01((d - band) / blend); // 풀 띠(0~band) → blend 폭에서 모래로
|
||||||
|
k = k * k * (3f - 2f * k);
|
||||||
|
var c = Color.LerpUnclamped(grass, sand, k);
|
||||||
|
if (y < waterY) { c.r *= uw; c.g *= uw; c.b *= uw; }
|
||||||
|
c.a = 1f;
|
||||||
|
_cols.Add(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int j = 0; j < nz - 1; j++)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < nx - 1; i++)
|
||||||
|
{
|
||||||
|
int n = j * nx + i;
|
||||||
|
int ia = index[n], ib = index[n + 1], ic = index[n + nx], id = index[n + nx + 1];
|
||||||
|
if (ia < 0 || ib < 0 || ic < 0 || id < 0) continue;
|
||||||
|
// 네 귀가 전부 경사 바깥(d ≥ width)이면 바닥과 같은 높이의 평판 — 만들지 않는다
|
||||||
|
float dm = Mathf.Min(Mathf.Min(dist[n], dist[n + 1]), Mathf.Min(dist[n + nx], dist[n + nx + 1]));
|
||||||
|
if (dm >= width) continue;
|
||||||
|
// 바닥판(SpawnSeabed)과 같은 감김 — (x0,z0)→(x0,z1)→(x1,z1) 이 위를 향한다
|
||||||
|
_tris.Add(ia); _tris.Add(ic); _tris.Add(id);
|
||||||
|
_tris.Add(ia); _tris.Add(id); _tris.Add(ib);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_mesh.Clear();
|
||||||
|
_mesh.indexFormat = _verts.Count > 65000 ? IndexFormat.UInt32 : IndexFormat.UInt16;
|
||||||
|
_mesh.SetVertices(_verts);
|
||||||
|
_mesh.SetColors(_cols);
|
||||||
|
_mesh.SetTriangles(_tris, 0);
|
||||||
|
_mesh.RecalculateNormals(); // DepthNormals 패스용
|
||||||
|
_mesh.RecalculateBounds();
|
||||||
|
|
||||||
|
LastVertices = _verts.Count;
|
||||||
|
LastTriangles = _tris.Count / 3;
|
||||||
|
return cell;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 00f6070a704765f47af4936dfb74e80d
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Loading…
Reference in New Issue