361 lines
14 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|