using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace Streamingle.Prop.Editor
{
///
/// 개별 프리펩 표시용 데이터
///
public class PrefabDisplayItem
{
public string prefabName; // 프리펩 이름
public string prefabPath; // 프리펩 경로
public string folderName; // 폴더 이름
public PropInfo parentProp; // 부모 PropInfo 참조
public string thumbnailPath; // 썸네일 경로
public string DisplayName => prefabName;
}
///
/// 프랍 브라우저 에디터 윈도우
///
public class PropBrowserWindow : EditorWindow
{
private const string PROP_PATH = "Assets/ResourcesData/Prop";
private const string DATABASE_PATH = "Assets/Resources/Settings/PropDatabase.asset";
private const string THUMBNAIL_FOLDER = "Thumbnail";
private const int THUMBNAIL_SIZE = 128;
private const int THUMBNAIL_CAPTURE_SIZE = 256;
private const int GRID_PADDING = 10;
private PropDatabase _database;
private List _filteredProps;
private List _displayItems; // 개별 프리펩 표시용
private Dictionary _thumbnailCache = new Dictionary();
private Vector2 _scrollPosition;
private string _searchFilter = "";
private int _viewMode = 0; // 0: 그리드, 1: 리스트
private static readonly string[] VIEW_MODE_OPTIONS = { "그리드", "리스트" };
private static readonly Color SELECTED_COLOR = new Color(0.3f, 0.6f, 1f, 0.3f);
private static readonly Color HOVER_COLOR = new Color(1f, 1f, 1f, 0.1f);
[MenuItem("Streamingle/Prop Browser %#p")]
public static void ShowWindow()
{
var window = GetWindow("프랍 브라우저");
window.minSize = new Vector2(400, 500);
window.Show();
}
private void OnEnable()
{
LoadOrCreateDatabase();
}
private void OnDisable()
{
ClearThumbnailCache();
}
private void OnGUI()
{
DrawToolbar();
DrawContent();
}
private void DrawToolbar()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
// 새로고침 버튼
if (GUILayout.Button("새로고침", EditorStyles.toolbarButton, GUILayout.Width(60)))
{
RefreshPropList();
}
GUILayout.Space(10);
// 검색
EditorGUILayout.LabelField("검색:", GUILayout.Width(35));
string newSearch = EditorGUILayout.TextField(_searchFilter, EditorStyles.toolbarSearchField, GUILayout.Width(150));
if (newSearch != _searchFilter)
{
_searchFilter = newSearch;
ApplyFilter();
}
if (GUILayout.Button("X", EditorStyles.toolbarButton, GUILayout.Width(20)))
{
_searchFilter = "";
ApplyFilter();
GUI.FocusControl(null);
}
GUILayout.FlexibleSpace();
// 뷰 모드
_viewMode = GUILayout.Toolbar(_viewMode, VIEW_MODE_OPTIONS, EditorStyles.toolbarButton, GUILayout.Width(100));
EditorGUILayout.EndHorizontal();
// 정보 바
EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
int totalPrefabCount = _database?.props.Sum(p => Math.Max(1, p.prefabPaths.Count)) ?? 0;
int filteredCount = _displayItems?.Count ?? 0;
string countText = string.IsNullOrEmpty(_searchFilter)
? $"총 {totalPrefabCount}개 프리펩"
: $"{filteredCount}/{totalPrefabCount}개 프리펩";
EditorGUILayout.LabelField(countText);
GUILayout.FlexibleSpace();
// 썸네일 생성
if (GUILayout.Button("썸네일 생성", GUILayout.Width(80)))
{
ShowThumbnailMenu();
}
// 웹 업로드
if (GUILayout.Button("웹 업로드", GUILayout.Width(80)))
{
ShowWebUploadMenu();
}
// 폴더 열기
if (GUILayout.Button("폴더", GUILayout.Width(40)))
{
EditorUtility.RevealInFinder(PROP_PATH);
}
EditorGUILayout.EndHorizontal();
}
private void ShowThumbnailMenu()
{
var menu = new GenericMenu();
menu.AddItem(new GUIContent("모든 프리펩 썸네일 생성 (개별)"), false, () => GenerateAllPrefabThumbnails());
menu.AddItem(new GUIContent("썸네일 없는 프리펩만 생성"), false, () => GenerateMissingPrefabThumbnails());
menu.AddSeparator("");
menu.AddItem(new GUIContent("폴더 단위로 생성 (기존 방식)/모든 폴더"), false, () => GenerateAllThumbnails());
menu.AddItem(new GUIContent("폴더 단위로 생성 (기존 방식)/선택한 폴더"), false, () => GenerateThumbnailsForSelected());
menu.ShowAsContext();
}
private void DrawContent()
{
if (_database == null || _displayItems == null)
{
EditorGUILayout.HelpBox("데이터베이스를 로드할 수 없습니다. 새로고침을 눌러주세요.", MessageType.Warning);
return;
}
if (_displayItems.Count == 0)
{
EditorGUILayout.HelpBox("프리펩이 없습니다.", MessageType.Info);
return;
}
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
if (_viewMode == 0)
{
DrawGridView();
}
else
{
DrawListView();
}
EditorGUILayout.EndScrollView();
}
private void DrawGridView()
{
float windowWidth = position.width - 30;
float itemWidth = THUMBNAIL_SIZE + GRID_PADDING;
float itemHeight = THUMBNAIL_SIZE + 40; // 폴더명 표시를 위해 높이 증가
int columnsCount = Mathf.Max(1, (int)(windowWidth / itemWidth));
int rowCount = Mathf.CeilToInt((float)_displayItems.Count / columnsCount);
float gridHeight = rowCount * itemHeight;
Rect gridRect = GUILayoutUtility.GetRect(windowWidth, gridHeight);
gridRect.x += GRID_PADDING;
for (int i = 0; i < _displayItems.Count; i++)
{
int row = i / columnsCount;
int col = i % columnsCount;
Rect itemRect = new Rect(
gridRect.x + col * itemWidth,
gridRect.y + row * itemHeight,
THUMBNAIL_SIZE,
itemHeight
);
DrawGridItem(_displayItems[i], itemRect);
}
}
private void DrawGridItem(PrefabDisplayItem item, Rect rect)
{
// 호버 효과
if (rect.Contains(Event.current.mousePosition))
{
EditorGUI.DrawRect(rect, HOVER_COLOR);
Repaint();
}
// 썸네일
var thumbnailRect = new Rect(rect.x + 2, rect.y + 2, THUMBNAIL_SIZE - 4, THUMBNAIL_SIZE - 4);
var thumbnail = GetThumbnailForItem(item);
if (thumbnail != null)
{
GUI.DrawTexture(thumbnailRect, thumbnail, ScaleMode.ScaleToFit);
}
else
{
EditorGUI.DrawRect(thumbnailRect, new Color(0.2f, 0.2f, 0.2f));
// 프리펩 미리보기
if (!string.IsNullOrEmpty(item.prefabPath))
{
var prefab = AssetDatabase.LoadAssetAtPath(item.prefabPath);
if (prefab != null)
{
var preview = AssetPreview.GetAssetPreview(prefab);
if (preview != null)
{
GUI.DrawTexture(thumbnailRect, preview, ScaleMode.ScaleToFit);
}
else
{
AssetPreview.SetPreviewTextureCacheSize(256);
GUI.Label(thumbnailRect, "Loading...", new GUIStyle(EditorStyles.centeredGreyMiniLabel) { alignment = TextAnchor.MiddleCenter });
}
}
}
else
{
GUI.Label(thumbnailRect, "No\nPrefab", new GUIStyle(EditorStyles.centeredGreyMiniLabel) { wordWrap = true, alignment = TextAnchor.MiddleCenter });
}
}
// 폴더 이름 배지 (프리펩 이름과 다를 경우)
if (!string.IsNullOrEmpty(item.folderName) && item.folderName != item.prefabName)
{
var folderBadgeRect = new Rect(rect.x + 4, rect.y + THUMBNAIL_SIZE - 18, THUMBNAIL_SIZE - 8, 14);
EditorGUI.DrawRect(folderBadgeRect, new Color(0, 0, 0, 0.7f));
GUI.Label(folderBadgeRect, item.folderName, new GUIStyle(EditorStyles.miniLabel) {
alignment = TextAnchor.MiddleCenter,
normal = { textColor = new Color(1, 1, 1, 0.9f) },
fontSize = 9,
clipping = TextClipping.Clip
});
}
// 프리펩 이름
var labelRect = new Rect(rect.x, rect.y + THUMBNAIL_SIZE, THUMBNAIL_SIZE, 35);
GUI.Label(labelRect, item.prefabName, new GUIStyle(EditorStyles.miniLabel) {
alignment = TextAnchor.UpperCenter,
wordWrap = true,
clipping = TextClipping.Clip
});
// 클릭 이벤트
if (Event.current.type == EventType.MouseDown && rect.Contains(Event.current.mousePosition))
{
if (Event.current.clickCount == 2)
{
// 더블클릭: 프리펩 선택
SelectPrefabInProject(item);
}
else if (Event.current.button == 0)
{
// 싱글클릭: 드래그 시작
HandleDragStartForItem(item);
}
else if (Event.current.button == 1)
{
// 우클릭: 컨텍스트 메뉴
ShowContextMenuForItem(item);
}
Event.current.Use();
}
}
private void DrawListView()
{
foreach (var item in _displayItems)
{
DrawListItem(item);
}
}
private void DrawListItem(PrefabDisplayItem item)
{
EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
// 썸네일 (작게)
var thumbnail = GetThumbnailForItem(item);
var thumbRect = GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
if (thumbnail != null)
{
GUI.DrawTexture(thumbRect, thumbnail, ScaleMode.ScaleToFit);
}
else
{
EditorGUI.DrawRect(thumbRect, new Color(0.2f, 0.2f, 0.2f));
// 프리펩 미리보기
if (!string.IsNullOrEmpty(item.prefabPath))
{
var prefab = AssetDatabase.LoadAssetAtPath(item.prefabPath);
if (prefab != null)
{
var preview = AssetPreview.GetAssetPreview(prefab);
if (preview != null)
{
GUI.DrawTexture(thumbRect, preview, ScaleMode.ScaleToFit);
}
}
}
}
GUILayout.Space(10);
// 프리펩 정보
EditorGUILayout.BeginVertical();
EditorGUILayout.LabelField(item.prefabName, EditorStyles.boldLabel);
// 폴더 이름 (다르면 표시)
if (!string.IsNullOrEmpty(item.folderName) && item.folderName != item.prefabName)
{
EditorGUILayout.LabelField($"폴더: {item.folderName}", EditorStyles.miniLabel);
}
EditorGUILayout.EndVertical();
GUILayout.FlexibleSpace();
// 버튼들
EditorGUILayout.BeginVertical();
if (GUILayout.Button("선택", GUILayout.Width(50)))
{
SelectPrefabInProject(item);
}
if (GUILayout.Button("...", GUILayout.Width(50)))
{
ShowContextMenuForItem(item);
}
EditorGUILayout.EndVertical();
EditorGUILayout.EndHorizontal();
// 드래그 처리
var lastRect = GUILayoutUtility.GetLastRect();
if (Event.current.type == EventType.MouseDown && lastRect.Contains(Event.current.mousePosition))
{
if (Event.current.button == 0)
{
HandleDragStartForItem(item);
}
}
}
private void HandleDragStart(PropInfo propInfo)
{
if (propInfo.prefabPaths.Count == 0) return;
var prefab = AssetDatabase.LoadAssetAtPath(propInfo.MainPrefabPath);
if (prefab == null) return;
DragAndDrop.PrepareStartDrag();
DragAndDrop.objectReferences = new UnityEngine.Object[] { prefab };
DragAndDrop.paths = new string[] { propInfo.MainPrefabPath };
DragAndDrop.StartDrag(propInfo.propName);
}
private void HandleDragStartForItem(PrefabDisplayItem item)
{
if (string.IsNullOrEmpty(item.prefabPath)) return;
var prefab = AssetDatabase.LoadAssetAtPath(item.prefabPath);
if (prefab == null) return;
DragAndDrop.PrepareStartDrag();
DragAndDrop.objectReferences = new UnityEngine.Object[] { prefab };
DragAndDrop.paths = new string[] { item.prefabPath };
DragAndDrop.StartDrag(item.prefabName);
}
private void SelectPrefabInProject(PrefabDisplayItem item)
{
if (!string.IsNullOrEmpty(item.prefabPath))
{
var prefab = AssetDatabase.LoadAssetAtPath(item.prefabPath);
if (prefab != null)
{
Selection.activeObject = prefab;
EditorGUIUtility.PingObject(prefab);
return;
}
}
// 프리펩이 없으면 폴더 선택
if (item.parentProp != null)
{
var folder = AssetDatabase.LoadAssetAtPath(item.parentProp.folderPath);
if (folder != null)
{
Selection.activeObject = folder;
EditorGUIUtility.PingObject(folder);
}
}
}
private void ShowContextMenuForItem(PrefabDisplayItem item)
{
var menu = new GenericMenu();
menu.AddItem(new GUIContent("프로젝트에서 선택"), false, () => SelectPrefabInProject(item));
if (item.parentProp != null)
{
menu.AddItem(new GUIContent("폴더 열기"), false, () => RevealInFinder(item.parentProp));
}
menu.AddSeparator("");
menu.AddItem(new GUIContent("씬에 배치"), false, () => InstantiatePrefabFromItem(item));
menu.AddSeparator("");
menu.AddItem(new GUIContent("이 프리펩 썸네일 생성"), false, () => GenerateThumbnailForItem(item));
menu.ShowAsContext();
}
private void InstantiatePrefabFromItem(PrefabDisplayItem item)
{
if (string.IsNullOrEmpty(item.prefabPath))
{
EditorUtility.DisplayDialog("오류", "프리펩이 없습니다.", "확인");
return;
}
var prefab = AssetDatabase.LoadAssetAtPath(item.prefabPath);
if (prefab == null)
{
EditorUtility.DisplayDialog("오류", "프리펩을 로드할 수 없습니다.", "확인");
return;
}
var instance = PrefabUtility.InstantiatePrefab(prefab) as GameObject;
if (instance != null)
{
if (SceneView.lastActiveSceneView != null)
{
instance.transform.position = SceneView.lastActiveSceneView.pivot;
}
Selection.activeGameObject = instance;
Undo.RegisterCreatedObjectUndo(instance, "Instantiate Prop");
}
}
private Texture2D GetThumbnailForItem(PrefabDisplayItem item)
{
if (string.IsNullOrEmpty(item.thumbnailPath))
return null;
if (_thumbnailCache.TryGetValue(item.thumbnailPath, out var cached))
return cached;
var texture = AssetDatabase.LoadAssetAtPath(item.thumbnailPath);
if (texture != null)
{
_thumbnailCache[item.thumbnailPath] = texture;
}
return texture;
}
private void ShowContextMenu(PropInfo propInfo)
{
var menu = new GenericMenu();
menu.AddItem(new GUIContent("프로젝트에서 선택"), false, () => SelectPropInProject(propInfo));
menu.AddItem(new GUIContent("폴더 열기"), false, () => RevealInFinder(propInfo));
menu.AddSeparator("");
// 프리펩 목록
if (propInfo.prefabPaths.Count > 0)
{
foreach (var prefabPath in propInfo.prefabPaths)
{
string prefabName = Path.GetFileNameWithoutExtension(prefabPath);
menu.AddItem(new GUIContent($"프리펩/{prefabName}"), false, () =>
{
var prefab = AssetDatabase.LoadAssetAtPath(prefabPath);
if (prefab != null)
{
Selection.activeObject = prefab;
EditorGUIUtility.PingObject(prefab);
}
});
}
}
else
{
menu.AddDisabledItem(new GUIContent("프리펩 없음"));
}
menu.AddSeparator("");
// 모델 목록
if (propInfo.modelPaths.Count > 0)
{
foreach (var modelPath in propInfo.modelPaths)
{
string modelName = Path.GetFileName(modelPath);
menu.AddItem(new GUIContent($"모델/{modelName}"), false, () =>
{
var model = AssetDatabase.LoadAssetAtPath(modelPath);
if (model != null)
{
Selection.activeObject = model;
EditorGUIUtility.PingObject(model);
}
});
}
}
menu.AddSeparator("");
menu.AddItem(new GUIContent("씬에 배치"), false, () => InstantiatePrefab(propInfo));
menu.AddSeparator("");
menu.AddItem(new GUIContent("썸네일 생성"), false, () => GenerateThumbnail(propInfo));
menu.ShowAsContext();
}
private void SelectPropInProject(PropInfo propInfo)
{
if (propInfo.prefabPaths.Count > 0)
{
var prefab = AssetDatabase.LoadAssetAtPath(propInfo.MainPrefabPath);
if (prefab != null)
{
Selection.activeObject = prefab;
EditorGUIUtility.PingObject(prefab);
return;
}
}
// 프리펩이 없으면 폴더 선택
var folder = AssetDatabase.LoadAssetAtPath(propInfo.folderPath);
if (folder != null)
{
Selection.activeObject = folder;
EditorGUIUtility.PingObject(folder);
}
}
private void RevealInFinder(PropInfo propInfo)
{
EditorUtility.RevealInFinder(propInfo.folderPath);
}
private void InstantiatePrefab(PropInfo propInfo)
{
if (propInfo.prefabPaths.Count == 0)
{
EditorUtility.DisplayDialog("오류", "프리펩이 없습니다.", "확인");
return;
}
var prefab = AssetDatabase.LoadAssetAtPath(propInfo.MainPrefabPath);
if (prefab == null)
{
EditorUtility.DisplayDialog("오류", "프리펩을 로드할 수 없습니다.", "확인");
return;
}
var instance = PrefabUtility.InstantiatePrefab(prefab) as GameObject;
if (instance != null)
{
// 씬 뷰 중앙에 배치
if (SceneView.lastActiveSceneView != null)
{
instance.transform.position = SceneView.lastActiveSceneView.pivot;
}
Selection.activeGameObject = instance;
Undo.RegisterCreatedObjectUndo(instance, "Instantiate Prop");
}
}
private Texture2D GetThumbnail(PropInfo propInfo)
{
if (string.IsNullOrEmpty(propInfo.thumbnailPath))
return null;
if (_thumbnailCache.TryGetValue(propInfo.thumbnailPath, out var cached))
return cached;
var texture = AssetDatabase.LoadAssetAtPath(propInfo.thumbnailPath);
if (texture != null)
{
_thumbnailCache[propInfo.thumbnailPath] = texture;
}
return texture;
}
private void ClearThumbnailCache()
{
_thumbnailCache.Clear();
}
private void LoadOrCreateDatabase()
{
_database = AssetDatabase.LoadAssetAtPath(DATABASE_PATH);
if (_database == null)
{
// 폴더 생성
if (!AssetDatabase.IsValidFolder("Assets/Resources"))
AssetDatabase.CreateFolder("Assets", "Resources");
if (!AssetDatabase.IsValidFolder("Assets/Resources/Settings"))
AssetDatabase.CreateFolder("Assets/Resources", "Settings");
_database = CreateInstance();
AssetDatabase.CreateAsset(_database, DATABASE_PATH);
AssetDatabase.SaveAssets();
}
RefreshPropList();
}
private void RefreshPropList()
{
if (_database == null) return;
_database.props.Clear();
if (!Directory.Exists(PROP_PATH))
{
UnityEngine.Debug.LogWarning($"Prop 폴더가 없습니다: {PROP_PATH}");
ApplyFilter();
return;
}
// Prop 하위 폴더들 검색 (Glb, Prop Prefab 제외)
var propFolders = Directory.GetDirectories(PROP_PATH)
.Where(f => !f.EndsWith("Glb") && !f.EndsWith("Prop Prefab"))
.ToList();
foreach (var folderPath in propFolders)
{
string propName = Path.GetFileName(folderPath);
string assetPath = folderPath.Replace("\\", "/");
// Assets/ 형식으로 변환
int assetsIndex = assetPath.IndexOf("Assets/");
if (assetsIndex >= 0)
{
assetPath = assetPath.Substring(assetsIndex);
}
var propInfo = new PropInfo
{
propName = propName,
folderPath = assetPath
};
// 파일 스캔
ScanPropFiles(propInfo, folderPath);
_database.props.Add(propInfo);
}
// 이름순 정렬
_database.props = _database.props.OrderBy(p => p.propName).ToList();
EditorUtility.SetDirty(_database);
AssetDatabase.SaveAssets();
ApplyFilter();
UnityEngine.Debug.Log($"[PropBrowser] {_database.props.Count}개 프랍 스캔 완료");
}
private void ScanPropFiles(PropInfo propInfo, string folderPath)
{
var allFiles = Directory.GetFiles(folderPath, "*.*", SearchOption.AllDirectories)
.Where(f => !f.EndsWith(".meta"))
.ToList();
foreach (var file in allFiles)
{
string ext = Path.GetExtension(file).ToLower();
string assetPath = file.Replace("\\", "/");
int assetsIndex = assetPath.IndexOf("Assets/");
if (assetsIndex >= 0)
{
assetPath = assetPath.Substring(assetsIndex);
}
if (ext == ".prefab")
{
propInfo.prefabPaths.Add(assetPath);
}
else if (ext == ".glb" || ext == ".fbx" || ext == ".obj")
{
propInfo.modelPaths.Add(assetPath);
}
else if (ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".tga" || ext == ".psd")
{
propInfo.textureCount++;
// 썸네일 찾기 (thumbnail 또는 _thumb 포함)
string fileName = Path.GetFileNameWithoutExtension(file).ToLower();
if (fileName.Contains("thumbnail") || fileName.Contains("_thumb"))
{
propInfo.thumbnailPath = assetPath;
}
}
else if (ext == ".mat")
{
propInfo.materialCount++;
}
}
// 프리펩 이름순 정렬
propInfo.prefabPaths = propInfo.prefabPaths.OrderBy(p => Path.GetFileName(p)).ToList();
}
private void ApplyFilter()
{
if (_database == null)
{
_filteredProps = new List();
_displayItems = new List();
return;
}
// 기존 필터링
if (string.IsNullOrEmpty(_searchFilter))
{
_filteredProps = new List(_database.props);
}
else
{
string filter = _searchFilter.ToLower();
_filteredProps = _database.props
.Where(p => p.propName.ToLower().Contains(filter) ||
p.prefabPaths.Any(pp => Path.GetFileNameWithoutExtension(pp).ToLower().Contains(filter)))
.ToList();
}
// 개별 프리펩 단위로 표시 아이템 생성
_displayItems = new List();
foreach (var prop in _filteredProps)
{
if (prop.prefabPaths.Count > 0)
{
// 각 프리펩을 개별 아이템으로 추가
foreach (var prefabPath in prop.prefabPaths)
{
string prefabName = Path.GetFileNameWithoutExtension(prefabPath);
// 검색 필터 추가 적용
if (!string.IsNullOrEmpty(_searchFilter))
{
string filter = _searchFilter.ToLower();
if (!prefabName.ToLower().Contains(filter) && !prop.propName.ToLower().Contains(filter))
continue;
}
// 개별 프리펩 썸네일 경로 찾기
string prefabThumbnailPath = $"{prop.folderPath}/{THUMBNAIL_FOLDER}/{prefabName}_thumbnail.png";
string prefabThumbnailFullPath = prefabThumbnailPath.Replace("Assets/", Application.dataPath + "/");
// 개별 썸네일이 없으면 폴더 썸네일 사용
string thumbnailToUse = File.Exists(prefabThumbnailFullPath) ? prefabThumbnailPath : prop.thumbnailPath;
_displayItems.Add(new PrefabDisplayItem
{
prefabName = prefabName,
prefabPath = prefabPath,
folderName = prop.propName,
parentProp = prop,
thumbnailPath = thumbnailToUse
});
}
}
else
{
// 프리펩이 없는 폴더도 표시
_displayItems.Add(new PrefabDisplayItem
{
prefabName = prop.propName,
prefabPath = null,
folderName = prop.propName,
parentProp = prop,
thumbnailPath = prop.thumbnailPath
});
}
}
// 이름순 정렬
_displayItems = _displayItems.OrderBy(d => d.prefabName).ToList();
}
#region Thumbnail Generation
///
/// 개별 프리펩에 대한 썸네일 생성
///
private bool GenerateThumbnailForItem(PrefabDisplayItem item)
{
if (string.IsNullOrEmpty(item.prefabPath) || item.parentProp == null)
{
UnityEngine.Debug.LogWarning($"[PropBrowser] {item.prefabName}: 프리펩 경로가 없습니다.");
return false;
}
var prefab = AssetDatabase.LoadAssetAtPath(item.prefabPath);
if (prefab == null)
{
UnityEngine.Debug.LogWarning($"[PropBrowser] {item.prefabName}: 프리펩을 로드할 수 없습니다.");
return false;
}
// 썸네일 폴더 경로
string thumbnailFolderPath = $"{item.parentProp.folderPath}/{THUMBNAIL_FOLDER}";
string thumbnailFileName = $"{item.prefabName}_thumbnail.png";
string thumbnailPath = $"{thumbnailFolderPath}/{thumbnailFileName}";
// 폴더 생성
if (!AssetDatabase.IsValidFolder(thumbnailFolderPath))
{
AssetDatabase.CreateFolder(item.parentProp.folderPath, THUMBNAIL_FOLDER);
}
var previewTexture = GeneratePreviewTexture(prefab);
if (previewTexture == null)
{
UnityEngine.Debug.LogWarning($"[PropBrowser] {item.prefabName}: 미리보기를 생성할 수 없습니다.");
return false;
}
try
{
byte[] pngData = previewTexture.EncodeToPNG();
string fullPath = thumbnailPath.Replace("Assets/", Application.dataPath + "/");
string directory = Path.GetDirectoryName(fullPath);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
File.WriteAllBytes(fullPath, pngData);
DestroyImmediate(previewTexture);
AssetDatabase.ImportAsset(thumbnailPath);
// item의 썸네일 경로 업데이트
item.thumbnailPath = thumbnailPath;
UnityEngine.Debug.Log($"[PropBrowser] {item.prefabName}: 썸네일 생성 완료");
return true;
}
catch (Exception ex)
{
UnityEngine.Debug.LogError($"[PropBrowser] {item.prefabName}: 썸네일 저장 실패 - {ex.Message}");
return false;
}
}
///
/// 모든 프리펩에 대해 개별 썸네일 생성
///
private void GenerateAllPrefabThumbnails()
{
// 모든 프리펩 아이템 수집 (필터 없이)
var allItems = new List();
foreach (var prop in _database.props)
{
foreach (var prefabPath in prop.prefabPaths)
{
allItems.Add(new PrefabDisplayItem
{
prefabName = Path.GetFileNameWithoutExtension(prefabPath),
prefabPath = prefabPath,
folderName = prop.propName,
parentProp = prop
});
}
}
if (allItems.Count == 0)
{
EditorUtility.DisplayDialog("알림", "생성할 프리펩이 없습니다.", "확인");
return;
}
if (!EditorUtility.DisplayDialog("확인",
$"모든 프리펩 ({allItems.Count}개)의 썸네일을 개별 생성합니다.\n계속하시겠습니까?",
"생성", "취소"))
{
return;
}
GeneratePrefabThumbnails(allItems, "모든 프리펩");
}
///
/// 썸네일이 없는 프리펩만 개별 썸네일 생성
///
private void GenerateMissingPrefabThumbnails()
{
var itemsWithoutThumbnail = new List();
foreach (var prop in _database.props)
{
foreach (var prefabPath in prop.prefabPaths)
{
string prefabName = Path.GetFileNameWithoutExtension(prefabPath);
string expectedThumbnailPath = $"{prop.folderPath}/{THUMBNAIL_FOLDER}/{prefabName}_thumbnail.png";
string fullPath = expectedThumbnailPath.Replace("Assets/", Application.dataPath + "/");
if (!File.Exists(fullPath))
{
itemsWithoutThumbnail.Add(new PrefabDisplayItem
{
prefabName = prefabName,
prefabPath = prefabPath,
folderName = prop.propName,
parentProp = prop
});
}
}
}
if (itemsWithoutThumbnail.Count == 0)
{
EditorUtility.DisplayDialog("알림", "모든 프리펩에 썸네일이 있습니다.", "확인");
return;
}
GeneratePrefabThumbnails(itemsWithoutThumbnail, $"썸네일 없는 {itemsWithoutThumbnail.Count}개 프리펩");
}
///
/// PrefabDisplayItem 리스트에 대해 썸네일 일괄 생성
///
private void GeneratePrefabThumbnails(List items, string description)
{
int successCount = 0;
int failCount = 0;
try
{
for (int i = 0; i < items.Count; i++)
{
var item = items[i];
EditorUtility.DisplayProgressBar("썸네일 생성 중",
$"{item.prefabName} ({i + 1}/{items.Count})",
(float)i / items.Count);
if (GenerateThumbnailForItem(item))
{
successCount++;
}
else
{
failCount++;
}
}
}
finally
{
EditorUtility.ClearProgressBar();
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
ClearThumbnailCache();
ApplyFilter(); // displayItems 다시 생성하여 썸네일 경로 반영
Repaint();
EditorUtility.DisplayDialog("완료",
$"{description} 썸네일 생성 완료\n성공: {successCount}개\n실패: {failCount}개",
"확인");
}
private void GenerateThumbnailsForSelected()
{
// 현재 선택된 프랍 (Selection에서)
var selectedObjects = Selection.objects;
var propsToGenerate = new List();
foreach (var obj in selectedObjects)
{
string path = AssetDatabase.GetAssetPath(obj);
if (string.IsNullOrEmpty(path)) continue;
var prop = _database.props.Find(p => path.StartsWith(p.folderPath));
if (prop != null && !propsToGenerate.Contains(prop))
{
propsToGenerate.Add(prop);
}
}
if (propsToGenerate.Count == 0)
{
EditorUtility.DisplayDialog("알림", "선택된 프랍이 없습니다.\n프로젝트 창에서 프랍 폴더나 프리펩을 선택해주세요.", "확인");
return;
}
GenerateThumbnails(propsToGenerate, $"선택한 {propsToGenerate.Count}개 프랍");
}
private void GenerateAllThumbnails()
{
if (!EditorUtility.DisplayDialog("확인",
$"모든 프랍 ({_database.props.Count}개)의 썸네일을 생성합니다.\n계속하시겠습니까?",
"생성", "취소"))
{
return;
}
GenerateThumbnails(_database.props, "모든 프랍");
}
private void GenerateMissingThumbnails()
{
var propsWithoutThumbnail = _database.props
.Where(p => string.IsNullOrEmpty(p.thumbnailPath) ||
!File.Exists(p.thumbnailPath.Replace("Assets/", Application.dataPath + "/")))
.ToList();
if (propsWithoutThumbnail.Count == 0)
{
EditorUtility.DisplayDialog("알림", "모든 프랍에 썸네일이 있습니다.", "확인");
return;
}
GenerateThumbnails(propsWithoutThumbnail, $"썸네일 없는 {propsWithoutThumbnail.Count}개 프랍");
}
private void GenerateThumbnails(List props, string description)
{
int successCount = 0;
int failCount = 0;
try
{
for (int i = 0; i < props.Count; i++)
{
var prop = props[i];
EditorUtility.DisplayProgressBar("썸네일 생성 중",
$"{prop.propName} ({i + 1}/{props.Count})",
(float)i / props.Count);
if (GenerateThumbnail(prop))
{
successCount++;
}
else
{
failCount++;
}
}
}
finally
{
EditorUtility.ClearProgressBar();
}
EditorUtility.SetDirty(_database);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
ClearThumbnailCache();
Repaint();
EditorUtility.DisplayDialog("완료",
$"{description} 썸네일 생성 완료\n성공: {successCount}개\n실패: {failCount}개",
"확인");
}
private bool GenerateThumbnail(PropInfo propInfo)
{
if (propInfo.prefabPaths.Count == 0)
{
UnityEngine.Debug.LogWarning($"[PropBrowser] {propInfo.propName}: 프리펩이 없어 썸네일을 생성할 수 없습니다.");
return false;
}
// 프리펩 로드
var prefab = AssetDatabase.LoadAssetAtPath(propInfo.MainPrefabPath);
if (prefab == null)
{
UnityEngine.Debug.LogWarning($"[PropBrowser] {propInfo.propName}: 프리펩을 로드할 수 없습니다.");
return false;
}
// 썸네일 폴더 경로
string thumbnailFolderPath = $"{propInfo.folderPath}/{THUMBNAIL_FOLDER}";
string thumbnailFileName = $"{propInfo.propName}_thumbnail.png";
string thumbnailPath = $"{thumbnailFolderPath}/{thumbnailFileName}";
// 폴더 생성
if (!AssetDatabase.IsValidFolder(thumbnailFolderPath))
{
AssetDatabase.CreateFolder(propInfo.folderPath, THUMBNAIL_FOLDER);
}
// 씬에서 직접 렌더링하여 썸네일 생성 (씬 조명 활용)
var previewTexture = GeneratePreviewTexture(prefab);
if (previewTexture == null)
{
UnityEngine.Debug.LogWarning($"[PropBrowser] {propInfo.propName}: 미리보기를 생성할 수 없습니다.");
return false;
}
// 텍스처 저장
try
{
// PNG로 저장
byte[] pngData = previewTexture.EncodeToPNG();
string fullPath = thumbnailPath.Replace("Assets/", Application.dataPath + "/");
string directory = Path.GetDirectoryName(fullPath);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
File.WriteAllBytes(fullPath, pngData);
DestroyImmediate(previewTexture);
// 에셋 임포트
AssetDatabase.ImportAsset(thumbnailPath);
// PropInfo 업데이트
propInfo.thumbnailPath = thumbnailPath;
UnityEngine.Debug.Log($"[PropBrowser] {propInfo.propName}: 썸네일 생성 완료");
return true;
}
catch (Exception ex)
{
UnityEngine.Debug.LogError($"[PropBrowser] {propInfo.propName}: 썸네일 저장 실패 - {ex.Message}");
return false;
}
}
private Texture2D GeneratePreviewTexture(GameObject prefab)
{
// 씬에서 프리펩을 소환하여 씬 조명을 활용
var tempInstance = PrefabUtility.InstantiatePrefab(prefab) as GameObject;
if (tempInstance == null)
{
tempInstance = Instantiate(prefab);
}
// 썸네일 촬영용 임시 위치 설정 (씬 외곽)
Vector3 capturePosition = new Vector3(10000f, 10000f, 10000f);
tempInstance.transform.position = capturePosition;
tempInstance.name = "[ThumbnailCapture_Temp]";
try
{
// 바운드 계산 - 모든 렌더러 포함
Bounds bounds = CalculateTotalBounds(tempInstance);
if (bounds.size == Vector3.zero)
{
DestroyImmediate(tempInstance);
return null;
}
// 카메라 설정
var cameraGo = new GameObject("[ThumbnailCamera_Temp]");
var camera = cameraGo.AddComponent();
camera.backgroundColor = new Color(0.15f, 0.15f, 0.15f, 0f);
camera.clearFlags = CameraClearFlags.SolidColor;
camera.orthographic = false;
camera.fieldOfView = 30f; // 좁은 FOV로 왜곡 최소화
camera.nearClipPlane = 0.01f;
camera.farClipPlane = 10000f;
// 카메라 위치 계산 - 바운더리 기반으로 전체가 보이도록
PositionCameraToFitBounds(camera, bounds);
// 렌더 텍스처
var rt = new RenderTexture(THUMBNAIL_CAPTURE_SIZE, THUMBNAIL_CAPTURE_SIZE, 24, RenderTextureFormat.ARGB32);
rt.antiAliasing = 4; // 안티앨리어싱 추가
camera.targetTexture = rt;
// 씬 조명이 없는 경우를 대비한 보조 조명
GameObject backupLightGo = null;
var existingLights = FindObjectsByType(FindObjectsSortMode.None);
bool hasSceneLight = existingLights.Any(l => l.enabled && l.gameObject.activeInHierarchy && l.type == LightType.Directional);
if (!hasSceneLight)
{
// 씬에 디렉셔널 라이트가 없으면 임시 조명 추가
backupLightGo = new GameObject("[ThumbnailLight_Temp]");
var light = backupLightGo.AddComponent();
light.type = LightType.Directional;
light.transform.rotation = Quaternion.Euler(50, -30, 0);
light.intensity = 1.5f;
light.color = new Color(1f, 0.98f, 0.95f); // 약간 따뜻한 색
// 보조 조명 (필/림 라이트)
var fillLightGo = new GameObject("[ThumbnailFillLight_Temp]");
var fillLight = fillLightGo.AddComponent();
fillLight.type = LightType.Directional;
fillLight.transform.rotation = Quaternion.Euler(30, 150, 0);
fillLight.intensity = 0.5f;
fillLight.color = new Color(0.8f, 0.85f, 1f); // 약간 차가운 색
fillLightGo.transform.SetParent(backupLightGo.transform);
}
// 렌더링
camera.Render();
// 텍스처 읽기
var texture = new Texture2D(THUMBNAIL_CAPTURE_SIZE, THUMBNAIL_CAPTURE_SIZE, TextureFormat.RGBA32, false);
RenderTexture.active = rt;
texture.ReadPixels(new Rect(0, 0, THUMBNAIL_CAPTURE_SIZE, THUMBNAIL_CAPTURE_SIZE), 0, 0);
texture.Apply();
RenderTexture.active = null;
// 정리
if (backupLightGo != null) DestroyImmediate(backupLightGo);
DestroyImmediate(cameraGo);
DestroyImmediate(rt);
DestroyImmediate(tempInstance);
return texture;
}
catch (Exception ex)
{
UnityEngine.Debug.LogError($"[PropBrowser] GeneratePreviewTexture 오류: {ex.Message}");
DestroyImmediate(tempInstance);
return null;
}
}
///
/// 오브젝트의 전체 바운드 계산 (모든 렌더러, 스킨드 메시, 파티클 포함)
///
private Bounds CalculateTotalBounds(GameObject obj)
{
var renderers = obj.GetComponentsInChildren(true);
var skinnedMeshRenderers = obj.GetComponentsInChildren(true);
var meshFilters = obj.GetComponentsInChildren(true);
Bounds bounds = new Bounds(obj.transform.position, Vector3.zero);
bool hasBounds = false;
// 렌더러 바운드 수집
foreach (var renderer in renderers)
{
// 파티클 시스템은 제외 (불필요하게 큰 바운드 생성)
if (renderer is ParticleSystemRenderer) continue;
if (!hasBounds)
{
bounds = renderer.bounds;
hasBounds = true;
}
else
{
bounds.Encapsulate(renderer.bounds);
}
}
// 바운드가 없으면 MeshFilter에서 직접 계산
if (!hasBounds)
{
foreach (var mf in meshFilters)
{
if (mf.sharedMesh == null) continue;
var meshBounds = mf.sharedMesh.bounds;
var worldBounds = new Bounds(
mf.transform.TransformPoint(meshBounds.center),
Vector3.Scale(meshBounds.size, mf.transform.lossyScale)
);
if (!hasBounds)
{
bounds = worldBounds;
hasBounds = true;
}
else
{
bounds.Encapsulate(worldBounds);
}
}
}
return bounds;
}
///
/// 카메라를 바운더리에 맞게 위치시켜 전체가 보이도록 함
///
private void PositionCameraToFitBounds(Camera camera, Bounds bounds)
{
// 대각선 방향에서 촬영 (앞쪽에서 45도 각도)
// Z+ 방향이 앞쪽이므로 Z를 양수로 설정하여 앞에서 촬영
Vector3 viewDirection = new Vector3(-0.7f, 0.5f, 1f).normalized;
// 바운드 크기 계산
float boundsSphereRadius = bounds.extents.magnitude;
// FOV 기반 거리 계산 - 전체가 화면에 들어오도록
float fov = camera.fieldOfView * Mathf.Deg2Rad;
float distance = boundsSphereRadius / Mathf.Sin(fov / 2f);
// 약간의 여백 추가 (1.2배)
distance *= 1.2f;
// 카메라 위치 설정
camera.transform.position = bounds.center + viewDirection * distance;
camera.transform.LookAt(bounds.center);
// 클리핑 평면 조정
camera.nearClipPlane = Mathf.Max(0.01f, distance - boundsSphereRadius * 2f);
camera.farClipPlane = distance + boundsSphereRadius * 2f;
}
#endregion
#region Web Upload
private void ShowWebUploadMenu()
{
var menu = new GenericMenu();
menu.AddItem(new GUIContent("웹사이트에 업로드"), false, () => UploadToWebsite());
menu.AddItem(new GUIContent("업로드 설정 열기"), false, () => WebsitePropExporter.ShowWindow());
menu.ShowAsContext();
}
private void UploadToWebsite()
{
if (_database == null || _database.props.Count == 0)
{
EditorUtility.DisplayDialog("오류", "업로드할 프랍이 없습니다.", "확인");
return;
}
// WebsitePropExporter 윈도우 열고 바로 업로드
var exporter = EditorWindow.GetWindow("프랍 업로드");
exporter.UploadToWebsite();
}
#endregion
}
}