2026-04-08 20:31:14 +09:00

361 lines
14 KiB
C#

using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using YAMO.LutCycler;
namespace YAMO.LutCycler.Editor
{
public class LutBrowserWindow : EditorWindow
{
// ── Constants ─────────────────────────────────────────────────
private const string LUT_FOLDER = "Assets/YAMO/LutCycler/LUTs";
private const string PREFAB_FOLDER = "Assets/YAMO/LutCycler/Prefabs";
private const float CARD_PADDING = 6f;
private const float THUMB_RATIO = 0.55f; // height = width * ratio
// ── State ─────────────────────────────────────────────────────
private List<Texture2D> _allLuts = new List<Texture2D>();
private Vector2 _scrollPos;
private int _thumbSize = 130;
private bool _showFavsOnly = false;
private string _searchFilter = "";
// ── Styles (lazy) ─────────────────────────────────────────────
private GUIStyle _cardStyle;
private GUIStyle _nameStyle;
private bool _stylesReady;
// ── Menu ──────────────────────────────────────────────────────
[MenuItem("YAMO/LUT Browser", priority = 10)]
public static void ShowWindow()
{
var win = GetWindow<LutBrowserWindow>("LUT Browser");
win.minSize = new Vector2(300, 400);
}
[MenuItem("YAMO/Create LUT Controller", priority = 11)]
public static void CreateLutController()
{
// VolumeProfile asset
if (!AssetDatabase.IsValidFolder(PREFAB_FOLDER))
AssetDatabase.CreateFolder("Assets/YAMO/LutCycler", "Prefabs");
string profilePath = $"{PREFAB_FOLDER}/LUT_Profile.asset";
var profile = AssetDatabase.LoadAssetAtPath<VolumeProfile>(profilePath);
if (profile == null)
{
profile = ScriptableObject.CreateInstance<VolumeProfile>();
var cl = profile.Add<ColorLookup>(true);
cl.active = true;
AssetDatabase.CreateAsset(profile, profilePath);
AssetDatabase.SaveAssets();
}
// Scene GameObject
var go = new GameObject("LUT Controller");
var volume = go.AddComponent<Volume>();
volume.isGlobal = true;
volume.profile = profile;
var cycler = go.AddComponent<LutCycler>();
cycler.LoadFromFolder();
// Save as prefab
string prefabPath = $"{PREFAB_FOLDER}/LUT Controller.prefab";
PrefabUtility.SaveAsPrefabAssetAndConnect(go, prefabPath, InteractionMode.UserAction);
Undo.RegisterCreatedObjectUndo(go, "Create LUT Controller");
Selection.activeGameObject = go;
Debug.Log($"[LUT Browser] LUT Controller를 생성했습니다. ({prefabPath})");
}
// ── Lifecycle ─────────────────────────────────────────────────
private void OnEnable() => RefreshLuts();
private void OnFocus() => RefreshLuts();
private void RefreshLuts()
{
_allLuts.Clear();
if (!AssetDatabase.IsValidFolder(LUT_FOLDER))
{
AssetDatabase.CreateFolder("Assets/YAMO/LutCycler", "LUTs");
return;
}
var guids = AssetDatabase.FindAssets("t:Texture2D", new[] { LUT_FOLDER });
foreach (var guid in guids)
{
var tex = AssetDatabase.LoadAssetAtPath<Texture2D>(AssetDatabase.GUIDToAssetPath(guid));
if (tex != null) _allLuts.Add(tex);
}
Repaint();
}
// ── GUI ───────────────────────────────────────────────────────
private void OnGUI()
{
EnsureStyles();
var cycler = FindObjectOfType<LutCycler>();
DrawToolbar(cycler);
DrawStatusBar(cycler);
if (_allLuts.Count == 0)
{
EditorGUILayout.Space(12);
EditorGUILayout.HelpBox(
$"LUT 텍스처가 없습니다.\n{LUT_FOLDER} 폴더에 텍스처를 넣고 Refresh를 눌러주세요.",
MessageType.Info);
return;
}
var filtered = GetFilteredLuts(cycler);
if (filtered.Count == 0)
{
EditorGUILayout.Space(12);
EditorGUILayout.HelpBox("조건에 맞는 LUT이 없습니다.", MessageType.None);
return;
}
DrawGrid(filtered, cycler);
}
// ── Toolbar ───────────────────────────────────────────────────
private void DrawToolbar(LutCycler cycler)
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
if (GUILayout.Button("↻ Refresh", EditorStyles.toolbarButton, GUILayout.Width(68)))
RefreshLuts();
GUILayout.Space(4);
GUILayout.Label("크기", EditorStyles.miniLabel, GUILayout.Width(26));
_thumbSize = (int)GUILayout.HorizontalSlider(_thumbSize, 70, 220, GUILayout.Width(80));
GUILayout.Space(6);
bool newFavOnly = GUILayout.Toggle(
_showFavsOnly, "★ 즐겨찾기만",
EditorStyles.toolbarButton, GUILayout.Width(78));
if (newFavOnly != _showFavsOnly) { _showFavsOnly = newFavOnly; Repaint(); }
GUILayout.Space(4);
var newFilter = EditorGUILayout.TextField(_searchFilter, EditorStyles.toolbarSearchField, GUILayout.ExpandWidth(true));
if (newFilter != _searchFilter) { _searchFilter = newFilter; Repaint(); }
EditorGUILayout.EndHorizontal();
}
private void DrawStatusBar(LutCycler cycler)
{
EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
if (cycler != null)
{
var cur = cycler.GetCurrentTexture();
string curName = cur != null ? cur.name : "없음";
bool isFav = cur != null && cycler.IsFavorite(cur);
GUILayout.Label(
$"현재: {(isFav ? " " : "")}{curName} | 전체 {_allLuts.Count}개 | ★ {cycler.favorites.Count}개",
EditorStyles.miniLabel);
}
else
{
GUI.color = new Color(1f, 0.7f, 0.4f);
GUILayout.Label("⚠ 씬에 LutCycler 없음 — 썸네일 클릭은 Volume에 직접 적용됩니다.", EditorStyles.miniLabel);
GUI.color = Color.white;
}
EditorGUILayout.EndHorizontal();
}
// ── Grid ──────────────────────────────────────────────────────
private void DrawGrid(List<Texture2D> luts, LutCycler cycler)
{
float cardWidth = _thumbSize;
float cardHeight = _thumbSize * THUMB_RATIO + 22f; // thumb + name row
int columns = Mathf.Max(1, Mathf.FloorToInt((position.width - 16f) / (cardWidth + CARD_PADDING)));
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos);
GUILayout.Space(4);
for (int i = 0; i < luts.Count; i += columns)
{
EditorGUILayout.BeginHorizontal();
GUILayout.Space(4);
for (int j = 0; j < columns && i + j < luts.Count; j++)
{
DrawCard(luts[i + j], cycler, cardWidth, cardHeight);
GUILayout.Space(CARD_PADDING);
}
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
GUILayout.Space(CARD_PADDING);
}
GUILayout.Space(8);
EditorGUILayout.EndScrollView();
}
// ── Card ──────────────────────────────────────────────────────
private void DrawCard(Texture2D lut, LutCycler cycler, float cardWidth, float cardHeight)
{
bool isCurrent = cycler != null && cycler.GetCurrentTexture() == lut;
bool isFavorite = cycler != null && cycler.IsFavorite(lut);
EditorGUILayout.BeginVertical(GUILayout.Width(cardWidth));
// ── Thumbnail ──
float thumbH = _thumbSize * THUMB_RATIO;
Rect thumbRect = GUILayoutUtility.GetRect(cardWidth, thumbH, GUILayout.Width(cardWidth));
// Border (현재 선택 강조)
Color borderCol = isCurrent
? new Color(0.25f, 0.65f, 1.0f, 1f)
: new Color(0.25f, 0.25f, 0.25f, 1f);
int border = isCurrent ? 2 : 1;
EditorGUI.DrawRect(
new Rect(thumbRect.x - border, thumbRect.y - border,
thumbRect.width + border * 2, thumbRect.height + border * 2),
borderCol);
// Texture
GUI.DrawTexture(thumbRect, lut, ScaleMode.ScaleToFit);
// ★ 오버레이
if (isFavorite)
{
var starRect = new Rect(thumbRect.x + 2, thumbRect.y + 2, 18, 18);
GUI.color = Color.yellow;
GUI.Label(starRect, "★");
GUI.color = Color.white;
}
// 클릭 감지
if (Event.current.type == EventType.MouseDown && thumbRect.Contains(Event.current.mousePosition))
{
ApplyLutToScene(lut, cycler);
Event.current.Use();
}
// ── Name + Fav button ──
EditorGUILayout.BeginHorizontal(GUILayout.Width(cardWidth));
GUILayout.Label(lut.name, _nameStyle, GUILayout.Width(cardWidth - 26));
GUI.color = isFavorite ? Color.yellow : new Color(0.6f, 0.6f, 0.6f);
if (GUILayout.Button(isFavorite ? "★" : "☆", EditorStyles.miniButton, GUILayout.Width(22), GUILayout.Height(16)))
{
if (cycler != null)
{
Undo.RecordObject(cycler, "Toggle LUT Favorite");
cycler.ToggleFavorite(lut);
Repaint();
}
else
{
Debug.LogWarning("[LUT Browser] 즐겨찾기를 저장하려면 씬에 LutCycler가 필요합니다.");
}
}
GUI.color = Color.white;
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
// ── Apply ─────────────────────────────────────────────────────
private void ApplyLutToScene(Texture2D lut, LutCycler cycler)
{
// Play mode: LutCycler를 통해 적용
if (Application.isPlaying && cycler != null)
{
int idx = cycler.lutTextures.IndexOf(lut);
if (idx >= 0) cycler.ApplyAt(idx);
else ApplyDirectToVolume(lut, cycler != null ? cycler.GetComponent<Volume>() : FindObjectOfType<Volume>());
Repaint();
return;
}
// Edit mode: Volume Profile에 직접 적용
Volume volume = cycler != null
? cycler.GetComponent<Volume>()
: FindObjectOfType<Volume>();
if (cycler != null)
{
int idx = cycler.lutTextures.IndexOf(lut);
if (idx >= 0)
{
Undo.RecordObject(cycler, "Apply LUT");
cycler.ApplyAt(idx);
}
}
ApplyDirectToVolume(lut, volume);
Repaint();
}
private static void ApplyDirectToVolume(Texture2D lut, Volume volume)
{
if (volume == null || volume.profile == null) return;
if (!volume.profile.TryGet<ColorLookup>(out var cl))
cl = volume.profile.Add<ColorLookup>(true);
Undo.RecordObject(volume.profile, "Apply LUT");
cl.texture.value = lut;
cl.active = true;
EditorUtility.SetDirty(volume.profile);
}
// ── Filter ────────────────────────────────────────────────────
private List<Texture2D> GetFilteredLuts(LutCycler cycler)
{
return _allLuts
.Where(t => !_showFavsOnly || (cycler != null && cycler.IsFavorite(t)))
.Where(t => string.IsNullOrEmpty(_searchFilter)
|| t.name.IndexOf(_searchFilter, System.StringComparison.OrdinalIgnoreCase) >= 0)
.ToList();
}
// ── Style Init ────────────────────────────────────────────────
private void EnsureStyles()
{
if (_stylesReady) return;
_cardStyle = new GUIStyle(EditorStyles.helpBox)
{
padding = new RectOffset(2, 2, 2, 2)
};
_nameStyle = new GUIStyle(EditorStyles.miniLabel)
{
clipping = TextClipping.Clip,
wordWrap = false
};
_stylesReady = true;
}
}
}