// WL785_Ramp.cs — #785 리본 가시성 보강 (PM · 2026-09-06 23:5x) // 문제: 리본 폭 방향 정점이 2개(손잡이=주황 · 칼끝=파랑)뿐이라 GPU 가 두 색을 선형 보간 → 중앙이 회색/연보라로 죽는다(실측: 연보라 띠). // 해결: 폭 방향 색을 정점색이 아니라 **램프 텍스처(v축)** 로 준다. 정점색은 흰색×알파만 나른다. // Play : unity command run_script --file AgentScripts/WL785_Ramp.cs --entry WL785_Ramp.Apply --args '[0.12, 0.30, 1.0]' (edgeSoft, tailSoft, intensity · 메모리만) // Edit : unity command run_script --file AgentScripts/WL785_Ramp.cs --entry WL785_Ramp.Bake --args '[0.12, 0.30, 1.0]' (PNG 저장 + 임포터 + 머티리얼 + 설정 SO 굽기) // Dump : unity command run_script --file AgentScripts/WL785_Ramp.cs --entry WL785_Ramp.Dump // 램프 규약: x = u(잔상 나이 · 1 = 머리) · y = v(칼날축 · 0 = 손잡이 · 1 = 칼끝). 64×64 RGBA32 · Clamp · Bilinear · 밉맵 없음 · 모바일 16 KB. using System.IO; using System.Text; using UnityEditor; using UnityEngine; public static class WL785_Ramp { const string TexPath = "Assets/WL/Combat/Textures/T_WL_BladeRibbonRamp.png"; const string MatPath = "Assets/WL/Combat/Materials/M_WL_BladeRibbon.mat"; const string SettingsPath = "Assets/WL/Settings/Resources/WL/SlashTrailSettings.asset"; const int Size = 64; // 폭(v) 단면: 손잡이 진주황 → 노랑 → 밝은 크림(코어) → 하늘색 → 칼끝 파랑. 회색 구간을 거치지 않는 색 경로. static Color Cross(float v) { var g = new Gradient(); g.SetKeys(new[] { new GradientColorKey(new Color(1.00f, 0.40f, 0.08f), 0.00f), new GradientColorKey(new Color(1.00f, 0.78f, 0.22f), 0.28f), new GradientColorKey(new Color(1.00f, 0.96f, 0.80f), 0.50f), new GradientColorKey(new Color(0.40f, 0.82f, 1.00f), 0.72f), new GradientColorKey(new Color(0.12f, 0.32f, 1.00f), 1.00f), }, new[] { new GradientAlphaKey(1f, 0f), new GradientAlphaKey(1f, 1f) }); return g.Evaluate(v); } public static Texture2D Build() { var tex = new Texture2D(Size, Size, TextureFormat.RGBA32, false, false); tex.name = "T_WL_BladeRibbonRamp"; tex.wrapMode = TextureWrapMode.Clamp; tex.filterMode = FilterMode.Bilinear; var px = new Color[Size * Size]; for (int y = 0; y < Size; y++) { float v = (y + 0.5f) / Size; var c = Cross(v); for (int x = 0; x < Size; x++) { float u = (x + 0.5f) / Size; // 1 = 머리(현재 칼 위치), 0 = 꼬리 float tailTint = Mathf.SmoothStep(0f, 1f, Mathf.Clamp01(u / 0.45f)); // 꼬리로 갈수록 청보라로 기움 + 약간 어둡게 var cc = Color.Lerp(new Color(0.45f, 0.30f, 1.00f), c, tailTint) * Mathf.Lerp(0.82f, 1f, tailTint); cc.a = 1f; px[y * Size + x] = cc; } } tex.SetPixels(px); tex.Apply(false, false); return tex; } static string ApplyLook(Material mat, Texture2D tex, float edgeSoft, float tailSoft, float intensity) { var s = WL.Combat.SlashTrailSettings.Instance; if (s == null) return "settings null"; // 정점색은 흰색만 (색은 램프가 담당) · 알파는 잔상축 그라디언트가 담당 var white = new Gradient(); white.SetKeys(new[] { new GradientColorKey(Color.white, 0f), new GradientColorKey(Color.white, 1f) }, new[] { new GradientAlphaKey(1f, 0f), new GradientAlphaKey(1f, 1f) }); s.gradientAlongBlade = white; var swing = new Gradient(); swing.SetKeys(new[] { new GradientColorKey(Color.white, 0f), new GradientColorKey(Color.white, 1f) }, new[] { new GradientAlphaKey(0.0f, 0f), new GradientAlphaKey(0.85f, 0.15f), new GradientAlphaKey(1f, 0.5f), new GradientAlphaKey(1f, 1f) }); s.gradientAlongSwing = swing; s.bladeGradientWeight = 1f; s.colorIntensity = intensity; s.useGradients = true; if (mat != null) { mat.SetTexture("_BaseMap", tex); mat.SetFloat("_EdgeSoftness", edgeSoft); mat.SetFloat("_TailSoftness", tailSoft); mat.SetFloat("_HeadSoftness", 0.03f); mat.SetFloat("_Intensity", 1f); } return "look applied: ramp=" + (tex != null ? tex.name : "null") + " edge=" + edgeSoft + " tail=" + tailSoft + " intensity=" + intensity + " bladeW=1 alpha(0→0.85@0.15→1@0.5)"; } /// Play 중 메모리만 (에셋 무수정): 런타임 램프 생성 → 머티리얼/설정 인스턴스에 적용. public static object Apply(float edgeSoft, float tailSoft, float intensity) { if (!Application.isPlaying) return "not playing (Edit 모드에서는 Bake 사용)"; var s = WL.Combat.SlashTrailSettings.Instance; if (s == null) return "settings null"; var mat = s.materialAdditive; var tex = Build(); return ApplyLook(mat, tex, edgeSoft, tailSoft, intensity); } /// Edit 모드: PNG 저장 · 임포터 · 머티리얼 텍스처 · 설정 SO 필드를 디스크에 굽는다. public static object Bake(float edgeSoft, float tailSoft, float intensity) { if (EditorApplication.isPlaying) return "ABORT: Play 중"; Directory.CreateDirectory(Path.GetDirectoryName(TexPath)); var tex = Build(); File.WriteAllBytes(TexPath, tex.EncodeToPNG()); Object.DestroyImmediate(tex); AssetDatabase.ImportAsset(TexPath, ImportAssetOptions.ForceUpdate); var imp = AssetImporter.GetAtPath(TexPath) as TextureImporter; if (imp == null) return "importer null " + TexPath; imp.textureType = TextureImporterType.Default; imp.sRGBTexture = true; imp.alphaSource = TextureImporterAlphaSource.None; imp.mipmapEnabled = false; imp.wrapMode = TextureWrapMode.Clamp; imp.filterMode = FilterMode.Bilinear; imp.maxTextureSize = 64; imp.textureCompression = TextureImporterCompression.Uncompressed; imp.isReadable = false; imp.SaveAndReimport(); var ramp = AssetDatabase.LoadAssetAtPath(TexPath); var mat = AssetDatabase.LoadAssetAtPath(MatPath); var s = AssetDatabase.LoadAssetAtPath(SettingsPath); if (ramp == null || mat == null || s == null) return "load fail ramp=" + (ramp != null) + " mat=" + (mat != null) + " settings=" + (s != null); // ApplyLook 은 Instance 를 쓰므로 에셋을 직접 채운다 var white = new Gradient(); white.SetKeys(new[] { new GradientColorKey(Color.white, 0f), new GradientColorKey(Color.white, 1f) }, new[] { new GradientAlphaKey(1f, 0f), new GradientAlphaKey(1f, 1f) }); var swing = new Gradient(); swing.SetKeys(new[] { new GradientColorKey(Color.white, 0f), new GradientColorKey(Color.white, 1f) }, new[] { new GradientAlphaKey(0.0f, 0f), new GradientAlphaKey(0.85f, 0.15f), new GradientAlphaKey(1f, 0.5f), new GradientAlphaKey(1f, 1f) }); s.gradientAlongBlade = white; s.gradientAlongSwing = swing; s.bladeGradientWeight = 1f; s.colorIntensity = intensity; s.useGradients = true; EditorUtility.SetDirty(s); mat.SetTexture("_BaseMap", ramp); mat.SetFloat("_EdgeSoftness", edgeSoft); mat.SetFloat("_TailSoftness", tailSoft); mat.SetFloat("_HeadSoftness", 0.03f); mat.SetFloat("_Intensity", 1f); EditorUtility.SetDirty(mat); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); return "baked: " + TexPath + " (" + ramp.width + "x" + ramp.height + ") · mat _BaseMap set · settings bladeW=1 intensity=" + intensity + " edge=" + edgeSoft + " tail=" + tailSoft; } // ── #788: 램프 색 × 원본 슬래시 스트릭(EricWang slash_L01 전치·흑백) 결합 텍스처 ───────────────── const string StreakPath = "Assets/WL/Combat/Textures/T_WL_SlashStreak.png"; const string CombinedPath = "Assets/WL/Combat/Textures/T_WL_BladeRibbonSlash.png"; const string BladeMatPath = "Assets/WL/Combat/Materials/M_WL_BladeRibbon.mat"; static Texture2D ReadPixels(Texture2D src) { var rt = RenderTexture.GetTemporary(src.width, src.height, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB); Graphics.Blit(src, rt); var prev = RenderTexture.active; RenderTexture.active = rt; var t = new Texture2D(src.width, src.height, TextureFormat.RGBA32, false); t.ReadPixels(new Rect(0, 0, src.width, src.height), 0, 0); t.Apply(); RenderTexture.active = prev; RenderTexture.ReleaseTemporary(rt); return t; } /// u(가로)=잔상 나이(1 머리) · v(세로)=칼날축. 색 = 램프 단면 · 알파 = 스트릭 밝기(코어 불투명 · 가장자리 갈라짐). public static Texture2D BuildCombined(float alphaGain, float alphaGamma, float floorAlpha) { var streakAsset = AssetDatabase.LoadAssetAtPath(StreakPath); Texture2D streak = streakAsset != null ? ReadPixels(streakAsset) : null; int W = 128, H = 64; var tex = new Texture2D(W, H, TextureFormat.RGBA32, false, false) { name = "T_WL_BladeRibbonSlash", wrapMode = TextureWrapMode.Clamp, filterMode = FilterMode.Bilinear }; var px = new Color[W * H]; for (int y = 0; y < H; y++) { float v = (y + 0.5f) / H; var c = Cross(v); for (int x = 0; x < W; x++) { float u = (x + 0.5f) / W; float tailTint = Mathf.SmoothStep(0f, 1f, Mathf.Clamp01(u / 0.45f)); var cc = Color.Lerp(new Color(0.45f, 0.30f, 1.00f), c, tailTint) * Mathf.Lerp(0.82f, 1f, tailTint); float s = 1f; if (streak != null) { var sp = streak.GetPixelBilinear(u, v); s = Mathf.Max(sp.r, sp.a); } // 스트릭 밝기를 알파로: 코어(밝음)는 불투명, 갈라진 가장자리는 투명. floorAlpha 로 띠의 바탕을 조금 남긴다. float a = Mathf.Clamp01(floorAlpha + (1f - floorAlpha) * Mathf.Pow(Mathf.Clamp01(s * alphaGain), alphaGamma)); // 밝은 코어는 살짝 백색화(참고 이미지의 흰 심) cc = Color.Lerp(cc, Color.white, Mathf.Clamp01((s - 0.75f) * 2f) * 0.5f); cc.a = a; px[y * W + x] = cc; } } tex.SetPixels(px); tex.Apply(false, false); if (streak != null) Object.DestroyImmediate(streak); return tex; } /// Play: 결합 텍스처를 리본 머티리얼(WL/Blade Ribbon)에 즉시 적용(메모리) + 떠 있는 트레일 렌더러를 그 머티리얼로. public static object ApplyCombined(float edgeSoft, float tailSoft, float alphaGain, float alphaGamma, float floorAlpha) { if (!Application.isPlaying) return "not playing"; var s = WL.Combat.SlashTrailSettings.Instance; if (s == null) return "settings null"; var mat = AssetDatabase.LoadAssetAtPath(BladeMatPath); if (mat == null) return "blade mat null"; s.materialAdditive = mat; var tex = BuildCombined(alphaGain, alphaGamma, floorAlpha); string r = ApplyLook(mat, tex, edgeSoft, tailSoft, 1f); int swapped = 0; foreach (var drv in Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None)) for (int i = 0; i < 4; i++) { var t = drv.GetTrail(i); if (t == null) continue; var mr = t.GetComponent(); if (mr == null) continue; mr.sharedMaterials = new[] { mat }; swapped++; } return r + " · combined " + tex.width + "x" + tex.height + " · 트레일 렌더러 교체=" + swapped; } /// Edit: 결합 텍스처 PNG 저장 + 임포터 + 머티리얼 + 설정 SO. public static object BakeCombined(float edgeSoft, float tailSoft, float alphaGain, float alphaGamma, float floorAlpha) { if (EditorApplication.isPlaying) return "ABORT: Play 중"; var tex = BuildCombined(alphaGain, alphaGamma, floorAlpha); File.WriteAllBytes(CombinedPath, tex.EncodeToPNG()); Object.DestroyImmediate(tex); AssetDatabase.ImportAsset(CombinedPath, ImportAssetOptions.ForceUpdate); var imp = AssetImporter.GetAtPath(CombinedPath) as TextureImporter; if (imp != null) { imp.textureType = TextureImporterType.Default; imp.sRGBTexture = true; imp.alphaSource = TextureImporterAlphaSource.FromInput; imp.alphaIsTransparency = false; imp.mipmapEnabled = false; imp.wrapMode = TextureWrapMode.Clamp; imp.filterMode = FilterMode.Bilinear; imp.maxTextureSize = 128; imp.textureCompression = TextureImporterCompression.Uncompressed; imp.isReadable = false; imp.SaveAndReimport(); } var ramp = AssetDatabase.LoadAssetAtPath(CombinedPath); var mat = AssetDatabase.LoadAssetAtPath(BladeMatPath); var s = AssetDatabase.LoadAssetAtPath(SettingsPath); if (ramp == null || mat == null || s == null) return "load fail"; var white = new Gradient(); white.SetKeys(new[] { new GradientColorKey(Color.white, 0f), new GradientColorKey(Color.white, 1f) }, new[] { new GradientAlphaKey(1f, 0f), new GradientAlphaKey(1f, 1f) }); var swing = new Gradient(); swing.SetKeys(new[] { new GradientColorKey(Color.white, 0f), new GradientColorKey(Color.white, 1f) }, new[] { new GradientAlphaKey(0.0f, 0f), new GradientAlphaKey(0.85f, 0.15f), new GradientAlphaKey(1f, 0.5f), new GradientAlphaKey(1f, 1f) }); s.materialAdditive = mat; s.gradientAlongBlade = white; s.gradientAlongSwing = swing; s.bladeGradientWeight = 1f; s.colorIntensity = 1f; s.useGradients = true; EditorUtility.SetDirty(s); mat.SetTexture("_BaseMap", ramp); mat.SetFloat("_EdgeSoftness", edgeSoft); mat.SetFloat("_TailSoftness", tailSoft); mat.SetFloat("_HeadSoftness", 0.03f); mat.SetFloat("_Intensity", 1f); EditorUtility.SetDirty(mat); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); return "baked " + CombinedPath + " (" + ramp.width + "x" + ramp.height + ") · mat/settings 갱신 · edge=" + edgeSoft + " tail=" + tailSoft + " gain=" + alphaGain + " gamma=" + alphaGamma + " floor=" + floorAlpha; } /// Edit: 램프 리본 기본 상태로 되돌린다 — Play 중 런타임 튜닝이 에셋 인스턴스에 남은 것(머티리얼 텍스처 dangling · burstMode · 클래스 오버라이드)을 명시적으로 굽는다. public static object RestoreRampMode(float edgeSoft, float tailSoft, float intensity, int burstMode) { if (EditorApplication.isPlaying) return "ABORT: Play 중"; string r = (string)Bake(edgeSoft, tailSoft, intensity); var s = AssetDatabase.LoadAssetAtPath(SettingsPath); var mat = AssetDatabase.LoadAssetAtPath(BladeMatPath); if (s == null || mat == null) return r + " | restore fail"; s.materialAdditive = mat; s.burstMode = (WL.Combat.BurstMode)burstMode; s.suppressMode = WL.Combat.SuppressMode.CrescentOnly; s.useGradients = true; s.drawRibbon = true; int n = 0; foreach (var cid in new[] { 10101, 10102, 10201, 10202, 10501, 10502 }) { var o = s.FindOverride(cid); if (o != null && o.overrideLook) { o.overrideLook = false; n++; } } EditorUtility.SetDirty(s); AssetDatabase.SaveAssets(); return r + " | restored: mat=" + s.materialAdditive.name + " tex=" + (mat.GetTexture("_BaseMap") != null ? mat.GetTexture("_BaseMap").name : "none") + " burst=" + s.burstMode + " overridesCleared=" + n; } public static object Dump() { var mat = AssetDatabase.LoadAssetAtPath(MatPath); var s = AssetDatabase.LoadAssetAtPath(SettingsPath); var sb = new StringBuilder(); if (mat != null) sb.AppendLine("mat tex=" + (mat.GetTexture("_BaseMap") != null ? mat.GetTexture("_BaseMap").name : "none") + " edge=" + mat.GetFloat("_EdgeSoftness") + " head=" + mat.GetFloat("_HeadSoftness") + " tail=" + mat.GetFloat("_TailSoftness") + " int=" + mat.GetFloat("_Intensity") + " blend=" + mat.GetFloat("_SrcBlend") + "/" + mat.GetFloat("_DstBlend")); if (s != null) sb.AppendLine("settings drawRibbon=" + s.drawRibbon + " useGradients=" + s.useGradients + " bladeW=" + s.bladeGradientWeight + " intensity=" + s.colorIntensity + " fade=" + s.fadeSeconds + " blade(0)=" + s.gradientAlongBlade.Evaluate(0f) + " blade(1)=" + s.gradientAlongBlade.Evaluate(1f) + " swingA(0.15)=" + s.gradientAlongSwing.Evaluate(0.15f).a + " swingA(1)=" + s.gradientAlongSwing.Evaluate(1f).a); var imp = AssetImporter.GetAtPath(TexPath) as TextureImporter; if (imp != null) sb.AppendLine("tex importer srgb=" + imp.sRGBTexture + " mip=" + imp.mipmapEnabled + " wrap=" + imp.wrapMode + " max=" + imp.maxTextureSize + " comp=" + imp.textureCompression); return sb.ToString(); } }