- PropBrowserWindow: Unity 에디터용 프랍 브라우저 - 개별 프리펩 단위로 표시 (폴더별 묶음 X) - 씬 조명 기반 썸네일 생성 - 앞쪽에서 촬영하도록 카메라 각도 수정 - WebsitePropExporter: 웹 API 업로드 기능 - 개별 프리펩별 썸네일 URL 지원 - PropSyncSettings: API 및 Git URL 설정 - PropData: 프랍 데이터 구조체 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1395 lines
51 KiB
C#
1395 lines
51 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
namespace Streamingle.Prop.Editor
|
|
{
|
|
/// <summary>
|
|
/// 개별 프리펩 표시용 데이터
|
|
/// </summary>
|
|
public class PrefabDisplayItem
|
|
{
|
|
public string prefabName; // 프리펩 이름
|
|
public string prefabPath; // 프리펩 경로
|
|
public string folderName; // 폴더 이름
|
|
public PropInfo parentProp; // 부모 PropInfo 참조
|
|
public string thumbnailPath; // 썸네일 경로
|
|
|
|
public string DisplayName => prefabName;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 프랍 브라우저 에디터 윈도우
|
|
/// </summary>
|
|
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<PropInfo> _filteredProps;
|
|
private List<PrefabDisplayItem> _displayItems; // 개별 프리펩 표시용
|
|
private Dictionary<string, Texture2D> _thumbnailCache = new Dictionary<string, Texture2D>();
|
|
|
|
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<PropBrowserWindow>("프랍 브라우저");
|
|
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<GameObject>(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<GameObject>(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<GameObject>(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<GameObject>(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<GameObject>(item.prefabPath);
|
|
if (prefab != null)
|
|
{
|
|
Selection.activeObject = prefab;
|
|
EditorGUIUtility.PingObject(prefab);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 프리펩이 없으면 폴더 선택
|
|
if (item.parentProp != null)
|
|
{
|
|
var folder = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(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<GameObject>(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<Texture2D>(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<GameObject>(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<UnityEngine.Object>(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<GameObject>(propInfo.MainPrefabPath);
|
|
if (prefab != null)
|
|
{
|
|
Selection.activeObject = prefab;
|
|
EditorGUIUtility.PingObject(prefab);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 프리펩이 없으면 폴더 선택
|
|
var folder = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(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<GameObject>(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<Texture2D>(propInfo.thumbnailPath);
|
|
if (texture != null)
|
|
{
|
|
_thumbnailCache[propInfo.thumbnailPath] = texture;
|
|
}
|
|
|
|
return texture;
|
|
}
|
|
|
|
private void ClearThumbnailCache()
|
|
{
|
|
_thumbnailCache.Clear();
|
|
}
|
|
|
|
private void LoadOrCreateDatabase()
|
|
{
|
|
_database = AssetDatabase.LoadAssetAtPath<PropDatabase>(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<PropDatabase>();
|
|
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<PropInfo>();
|
|
_displayItems = new List<PrefabDisplayItem>();
|
|
return;
|
|
}
|
|
|
|
// 기존 필터링
|
|
if (string.IsNullOrEmpty(_searchFilter))
|
|
{
|
|
_filteredProps = new List<PropInfo>(_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<PrefabDisplayItem>();
|
|
|
|
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
|
|
|
|
/// <summary>
|
|
/// 개별 프리펩에 대한 썸네일 생성
|
|
/// </summary>
|
|
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<GameObject>(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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 모든 프리펩에 대해 개별 썸네일 생성
|
|
/// </summary>
|
|
private void GenerateAllPrefabThumbnails()
|
|
{
|
|
// 모든 프리펩 아이템 수집 (필터 없이)
|
|
var allItems = new List<PrefabDisplayItem>();
|
|
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, "모든 프리펩");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 썸네일이 없는 프리펩만 개별 썸네일 생성
|
|
/// </summary>
|
|
private void GenerateMissingPrefabThumbnails()
|
|
{
|
|
var itemsWithoutThumbnail = new List<PrefabDisplayItem>();
|
|
|
|
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}개 프리펩");
|
|
}
|
|
|
|
/// <summary>
|
|
/// PrefabDisplayItem 리스트에 대해 썸네일 일괄 생성
|
|
/// </summary>
|
|
private void GeneratePrefabThumbnails(List<PrefabDisplayItem> 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<PropInfo>();
|
|
|
|
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<PropInfo> 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<GameObject>(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>();
|
|
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<Light>(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>();
|
|
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<Light>();
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 오브젝트의 전체 바운드 계산 (모든 렌더러, 스킨드 메시, 파티클 포함)
|
|
/// </summary>
|
|
private Bounds CalculateTotalBounds(GameObject obj)
|
|
{
|
|
var renderers = obj.GetComponentsInChildren<Renderer>(true);
|
|
var skinnedMeshRenderers = obj.GetComponentsInChildren<SkinnedMeshRenderer>(true);
|
|
var meshFilters = obj.GetComponentsInChildren<MeshFilter>(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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 카메라를 바운더리에 맞게 위치시켜 전체가 보이도록 함
|
|
/// </summary>
|
|
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<WebsitePropExporter>("프랍 업로드");
|
|
exporter.UploadToWebsite();
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|