user f2cd9878cb Feat: 배경 씬 웹 업로드 제외 기능 추가
- BackgroundSceneInfo에 excludeFromWeb 플래그 추가
- WebsiteBackgroundExporter에서 제외된 씬 필터링
- BackgroundSceneLoaderWindow UI 개선:
  - 컨텍스트 메뉴에 '웹사이트 업로드 제외' 토글 추가
  - 그리드 뷰: 제외된 씬에 빨간 X 표시
  - 리스트 뷰: 제외된 씬에 [제외] 텍스트 표시

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 02:52:43 +09:00

718 lines
25 KiB
C#

using System;
using System.IO;
using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Streamingle.Background.Editor
{
/// <summary>
/// 배경 씬 썸네일 캡처 도구
/// </summary>
public class BackgroundThumbnailCapture : EditorWindow
{
// 16:9 기본값
private const int DEFAULT_WIDTH = 1920;
private const int DEFAULT_HEIGHT = 1080;
private Camera _selectedCamera;
private int _width = DEFAULT_WIDTH;
private int _height = DEFAULT_HEIGHT;
private BackgroundSyncSettings _syncSettings;
private string _savePath;
private string _fileName;
private bool _useSceneCamera = true;
private Texture2D _previewTexture;
private BackgroundSceneDatabase _database;
private BackgroundSceneInfo _currentSceneInfo;
[MenuItem("Streamingle/Capture Background Thumbnail %#t")]
public static void ShowWindow()
{
var window = GetWindow<BackgroundThumbnailCapture>("썸네일 캡처");
window.minSize = new Vector2(350, 500);
window.Show();
window.Initialize();
}
private void Initialize()
{
// 데이터베이스 로드
_database = AssetDatabase.LoadAssetAtPath<BackgroundSceneDatabase>(
"Assets/Resources/Settings/BackgroundSceneDatabase.asset");
// 동기화 설정 로드
_syncSettings = AssetDatabase.LoadAssetAtPath<BackgroundSyncSettings>(
"Assets/Resources/Settings/BackgroundSyncSettings.asset");
// 설정에서 해상도 가져오기
if (_syncSettings != null)
{
_width = _syncSettings.thumbnailWidth;
_height = _syncSettings.thumbnailHeight;
}
// 현재 로드된 배경 씬 찾기
FindCurrentBackgroundScene();
// 카메라 찾기
FindSceneCamera();
}
private void OnEnable()
{
Initialize();
}
private void OnDestroy()
{
if (_previewTexture != null)
{
DestroyImmediate(_previewTexture);
}
}
private void FindCurrentBackgroundScene()
{
_currentSceneInfo = null;
_savePath = "";
_fileName = "";
if (_database == null) return;
const string BACKGROUND_PATH = "Assets/ResourcesData/Background";
// 현재 로드된 씬들 중 배경 씬 찾기
for (int i = 0; i < SceneManager.sceneCount; i++)
{
var scene = SceneManager.GetSceneAt(i);
if (string.IsNullOrEmpty(scene.path)) continue;
// 데이터베이스에서 찾기
var sceneInfo = _database.FindByPath(scene.path);
if (sceneInfo != null)
{
_currentSceneInfo = sceneInfo;
_savePath = Path.GetDirectoryName(sceneInfo.scenePath);
_fileName = sceneInfo.sceneName;
return;
}
// 데이터베이스에 없지만 Background 폴더에 있는 씬인 경우
// 데이터베이스를 갱신하고 다시 찾기
if (scene.path.Replace("\\", "/").Contains(BACKGROUND_PATH))
{
// 데이터베이스 갱신
RefreshDatabaseFromLoaderWindow();
// 다시 찾기
sceneInfo = _database.FindByPath(scene.path);
if (sceneInfo != null)
{
_currentSceneInfo = sceneInfo;
_savePath = Path.GetDirectoryName(sceneInfo.scenePath);
_fileName = sceneInfo.sceneName;
return;
}
// 여전히 없으면 수동으로 정보 설정
string scenePath = scene.path.Replace("\\", "/");
_savePath = Path.GetDirectoryName(scenePath);
_fileName = scene.name;
// 카테고리 추출 (폴더명)
string relativePath = scenePath.Substring(BACKGROUND_PATH.Length + 1);
string categoryName = relativePath.Split('/')[0];
// 새 씬 정보 생성 및 데이터베이스에 추가
_currentSceneInfo = new BackgroundSceneInfo
{
sceneName = scene.name,
scenePath = scenePath,
categoryName = categoryName,
thumbnailPath = ""
};
_database.scenes.Add(_currentSceneInfo);
EditorUtility.SetDirty(_database);
AssetDatabase.SaveAssets();
UnityEngine.Debug.Log($"새 배경 씬이 데이터베이스에 추가됨: {scene.name}");
return;
}
}
// 배경 씬을 못 찾으면 활성 씬 사용
var activeScene = SceneManager.GetActiveScene();
if (!string.IsNullOrEmpty(activeScene.path))
{
_savePath = Path.GetDirectoryName(activeScene.path);
_fileName = activeScene.name;
}
}
private void RefreshDatabaseFromLoaderWindow()
{
// BackgroundSceneLoaderWindow가 열려있으면 RefreshSceneList 호출
var loaderWindow = GetWindow<BackgroundSceneLoaderWindow>(false, null, false);
if (loaderWindow != null)
{
// 리플렉션으로 private 메서드 호출
var refreshMethod = typeof(BackgroundSceneLoaderWindow).GetMethod("RefreshSceneList",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (refreshMethod != null)
{
refreshMethod.Invoke(loaderWindow, null);
}
}
else
{
// 윈도우가 없으면 직접 갱신
RefreshSceneListDirectly();
}
// 데이터베이스 다시 로드
_database = AssetDatabase.LoadAssetAtPath<BackgroundSceneDatabase>(
"Assets/Resources/Settings/BackgroundSceneDatabase.asset");
}
/// <summary>
/// Scene 또는 scene 폴더를 실제 대소문자로 찾기
/// </summary>
private string FindSceneFolderWithCorrectCase(string parentFolder)
{
var subDirs = Directory.GetDirectories(parentFolder);
foreach (var dir in subDirs)
{
string dirName = Path.GetFileName(dir);
if (string.Equals(dirName, "Scene", StringComparison.OrdinalIgnoreCase))
{
return dir;
}
}
return null;
}
private void RefreshSceneListDirectly()
{
if (_database == null) return;
const string BACKGROUND_PATH = "Assets/ResourcesData/Background";
_database.scenes.Clear();
if (!Directory.Exists(BACKGROUND_PATH))
{
return;
}
// Background 하위 폴더들 검색
var backgroundFolders = Directory.GetDirectories(BACKGROUND_PATH);
foreach (var folderPath in backgroundFolders)
{
string folderName = Path.GetFileName(folderPath);
string sceneFolderPath = FindSceneFolderWithCorrectCase(folderPath);
if (sceneFolderPath == null)
{
sceneFolderPath = folderPath;
}
// .unity 파일 검색 (재귀적으로)
var sceneFiles = Directory.GetFiles(sceneFolderPath, "*.unity", SearchOption.AllDirectories);
foreach (var sceneFile in sceneFiles)
{
if (sceneFile.Contains("SkyBox")) continue;
string assetPath = sceneFile.Replace("\\", "/");
string sceneName = Path.GetFileNameWithoutExtension(sceneFile);
string sceneDir = Path.GetDirectoryName(sceneFile);
string thumbnailPath = FindThumbnail(sceneDir, sceneName);
var sceneInfo = new BackgroundSceneInfo
{
sceneName = sceneName,
scenePath = assetPath,
categoryName = folderName,
thumbnailPath = thumbnailPath
};
_database.scenes.Add(sceneInfo);
}
}
EditorUtility.SetDirty(_database);
AssetDatabase.SaveAssets();
}
private string FindThumbnail(string directory, string sceneName)
{
string[] extensions = { ".png", ".jpg", ".jpeg" };
foreach (var ext in extensions)
{
string thumbnailPath = Path.Combine(directory, sceneName + ext);
if (File.Exists(thumbnailPath))
{
return thumbnailPath.Replace("\\", "/");
}
}
return null;
}
private void FindSceneCamera()
{
_selectedCamera = null;
// 1. 씬에서 Main Camera 찾기
_selectedCamera = Camera.main;
// 2. Main Camera가 없으면 아무 카메라나 찾기
if (_selectedCamera == null)
{
var cameras = FindObjectsByType<Camera>(FindObjectsSortMode.None);
if (cameras.Length > 0)
{
_selectedCamera = cameras[0];
}
}
// 3. Scene View 카메라 사용 옵션
if (_selectedCamera == null)
{
_useSceneCamera = true;
}
}
private void OnGUI()
{
EditorGUILayout.Space(10);
EditorGUILayout.LabelField("배경 썸네일 캡처", EditorStyles.boldLabel);
EditorGUILayout.Space(5);
// 현재 씬 정보
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.BeginHorizontal();
if (_currentSceneInfo != null)
{
EditorGUILayout.BeginVertical();
EditorGUILayout.LabelField("현재 배경 씬:", _currentSceneInfo.sceneName);
EditorGUILayout.LabelField("카테고리:", _currentSceneInfo.categoryName);
EditorGUILayout.EndVertical();
}
else
{
EditorGUILayout.BeginVertical();
EditorGUILayout.LabelField("배경 씬을 찾을 수 없습니다.", EditorStyles.miniLabel);
EditorGUILayout.LabelField("배경 씬을 먼저 로드하세요.");
EditorGUILayout.EndVertical();
}
if (GUILayout.Button("새로고침", GUILayout.Width(70), GUILayout.Height(35)))
{
Initialize();
UnityEngine.Debug.Log("씬 정보가 새로고침되었습니다.");
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
EditorGUILayout.Space(10);
// 카메라 선택
EditorGUILayout.LabelField("카메라 설정", EditorStyles.boldLabel);
_useSceneCamera = EditorGUILayout.Toggle("Scene View 카메라 사용", _useSceneCamera);
if (!_useSceneCamera)
{
_selectedCamera = (Camera)EditorGUILayout.ObjectField("카메라", _selectedCamera, typeof(Camera), true);
// 카메라 목록 버튼
if (GUILayout.Button("씬에서 카메라 찾기"))
{
ShowCameraMenu();
}
}
EditorGUILayout.Space(10);
// 해상도 설정
EditorGUILayout.LabelField("해상도 설정", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
_width = EditorGUILayout.IntField("너비", _width);
_height = EditorGUILayout.IntField("높이", _height);
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("1920x1080 (권장)")) { _width = 1920; _height = 1080; }
if (GUILayout.Button("1280x720")) { _width = 1280; _height = 720; }
if (GUILayout.Button("960x540")) { _width = 960; _height = 540; }
EditorGUILayout.EndHorizontal();
EditorGUILayout.HelpBox("16:9 비율 썸네일이 권장됩니다.\n에디터에서는 1:1로 크롭되어 표시되고, 웹사이트에는 16:9 원본이 사용됩니다.", MessageType.Info);
EditorGUILayout.Space(10);
// 저장 경로
EditorGUILayout.LabelField("저장 설정", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
_savePath = EditorGUILayout.TextField("저장 경로", _savePath);
if (GUILayout.Button("...", GUILayout.Width(30)))
{
string path = EditorUtility.OpenFolderPanel("저장 폴더 선택", _savePath, "");
if (!string.IsNullOrEmpty(path))
{
// Assets 폴더 기준 상대 경로로 변환
if (path.StartsWith(Application.dataPath))
{
_savePath = "Assets" + path.Substring(Application.dataPath.Length);
}
else
{
_savePath = path;
}
}
}
EditorGUILayout.EndHorizontal();
_fileName = EditorGUILayout.TextField("파일 이름", _fileName);
EditorGUILayout.LabelField($"저장 위치: {_savePath}/{_fileName}.png", EditorStyles.miniLabel);
EditorGUILayout.Space(10);
// 프리뷰
EditorGUILayout.LabelField("프리뷰", EditorStyles.boldLabel);
if (GUILayout.Button("프리뷰 갱신", GUILayout.Height(25)))
{
CapturePreview();
}
if (_previewTexture != null)
{
float previewHeight = position.width * _previewTexture.height / _previewTexture.width;
var previewRect = GUILayoutUtility.GetRect(position.width - 20, Mathf.Min(previewHeight, 300));
GUI.DrawTexture(previewRect, _previewTexture, ScaleMode.ScaleToFit);
}
EditorGUILayout.Space(10);
// 캡처 버튼
GUI.enabled = !string.IsNullOrEmpty(_savePath) && !string.IsNullOrEmpty(_fileName);
GUI.backgroundColor = new Color(0.3f, 0.8f, 0.3f);
if (GUILayout.Button("썸네일 캡처 및 저장", GUILayout.Height(40)))
{
CaptureAndSave();
}
GUI.backgroundColor = Color.white;
GUI.enabled = true;
// 모든 배경 씬 썸네일 일괄 캡처
EditorGUILayout.Space(10);
EditorGUILayout.LabelField("일괄 캡처", EditorStyles.boldLabel);
if (GUILayout.Button("모든 배경 씬 썸네일 캡처", GUILayout.Height(30)))
{
CaptureAllBackgroundScenes();
}
// 썸네일 새로고침
EditorGUILayout.Space(10);
if (GUILayout.Button("배경 로더 썸네일 새로고침", GUILayout.Height(25)))
{
var loaderWindow = GetWindow<BackgroundSceneLoaderWindow>(false, null, false);
if (loaderWindow != null)
{
loaderWindow.RefreshAllThumbnails();
UnityEngine.Debug.Log("썸네일 캐시가 새로고침되었습니다.");
}
else
{
UnityEngine.Debug.LogWarning("배경 씬 로더 윈도우가 열려있지 않습니다.");
}
}
}
private void ShowCameraMenu()
{
var menu = new GenericMenu();
var cameras = FindObjectsByType<Camera>(FindObjectsSortMode.None);
foreach (var cam in cameras)
{
Camera capturedCam = cam;
menu.AddItem(new GUIContent(cam.name), _selectedCamera == cam, () =>
{
_selectedCamera = capturedCam;
});
}
if (cameras.Length == 0)
{
menu.AddDisabledItem(new GUIContent("카메라를 찾을 수 없습니다"));
}
menu.ShowAsContext();
}
private void CapturePreview()
{
if (_previewTexture != null)
{
DestroyImmediate(_previewTexture);
}
_previewTexture = CaptureImage();
}
private Texture2D CaptureImage()
{
Camera camera = GetCaptureCamera();
if (camera == null)
{
UnityEngine.Debug.LogError("캡처할 카메라를 찾을 수 없습니다.");
return null;
}
// RenderTexture 생성
RenderTexture rt = new RenderTexture(_width, _height, 24, RenderTextureFormat.ARGB32);
rt.antiAliasing = 8;
// 카메라 렌더링
RenderTexture prevRT = camera.targetTexture;
camera.targetTexture = rt;
camera.Render();
camera.targetTexture = prevRT;
// Texture2D로 변환
RenderTexture prevActive = RenderTexture.active;
RenderTexture.active = rt;
Texture2D texture = new Texture2D(_width, _height, TextureFormat.RGB24, false);
texture.ReadPixels(new Rect(0, 0, _width, _height), 0, 0);
texture.Apply();
RenderTexture.active = prevActive;
// 정리
DestroyImmediate(rt);
return texture;
}
private Camera GetCaptureCamera()
{
if (_useSceneCamera)
{
// Scene View 카메라 가져오기
var sceneView = SceneView.lastActiveSceneView;
if (sceneView != null)
{
return sceneView.camera;
}
}
return _selectedCamera;
}
private void CaptureAndSave()
{
Texture2D texture = CaptureImage();
if (texture == null) return;
// PNG로 인코딩
byte[] bytes = texture.EncodeToPNG();
DestroyImmediate(texture);
// 저장 경로 확인
string fullPath = Path.Combine(_savePath, _fileName + ".png");
// 디렉토리 확인
string directory = Path.GetDirectoryName(fullPath);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
// 파일 저장
File.WriteAllBytes(fullPath, bytes);
// 에셋 데이터베이스 갱신
AssetDatabase.Refresh();
// 텍스처 임포트 설정
string assetPath = fullPath.Replace("\\", "/");
TextureImporter importer = AssetImporter.GetAtPath(assetPath) as TextureImporter;
if (importer != null)
{
importer.textureType = TextureImporterType.Sprite;
importer.maxTextureSize = 1024;
importer.SaveAndReimport();
}
UnityEngine.Debug.Log($"썸네일 저장됨: {fullPath}");
// 프리뷰 업데이트
_previewTexture = AssetDatabase.LoadAssetAtPath<Texture2D>(assetPath);
// 데이터베이스 갱신
if (_database != null)
{
if (_currentSceneInfo != null)
{
_currentSceneInfo.thumbnailPath = assetPath;
EditorUtility.SetDirty(_database);
AssetDatabase.SaveAssets();
}
else
{
// _currentSceneInfo가 없으면 데이터베이스 전체 갱신
RefreshSceneListDirectly();
}
}
// 배경 씬 로더 윈도우 갱신
var loaderWindow = GetWindow<BackgroundSceneLoaderWindow>(false, null, false);
if (loaderWindow != null)
{
// 씬 목록 갱신 (리플렉션)
var refreshMethod = typeof(BackgroundSceneLoaderWindow).GetMethod("RefreshSceneList",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (refreshMethod != null)
{
refreshMethod.Invoke(loaderWindow, null);
}
// 썸네일 캐시 갱신
loaderWindow.RefreshThumbnail(assetPath);
}
EditorUtility.DisplayDialog("완료", $"썸네일이 저장되었습니다.\n{fullPath}", "확인");
}
private void CaptureAllBackgroundScenes()
{
if (_database == null)
{
EditorUtility.DisplayDialog("오류", "배경 씬 데이터베이스를 찾을 수 없습니다.", "확인");
return;
}
if (_database.scenes.Count == 0)
{
EditorUtility.DisplayDialog("오류", "배경 씬이 없습니다.", "확인");
return;
}
// 확인 다이얼로그
if (!EditorUtility.DisplayDialog("일괄 캡처",
$"총 {_database.scenes.Count}개의 배경 씬 썸네일을 캡처합니다.\n" +
"각 씬을 로드하고 카메라를 찾아 캡처합니다.\n\n" +
"진행하시겠습니까?",
"예", "아니오"))
{
return;
}
int successCount = 0;
int failCount = 0;
try
{
for (int i = 0; i < _database.scenes.Count; i++)
{
var sceneInfo = _database.scenes[i];
EditorUtility.DisplayProgressBar("썸네일 캡처 중...",
$"{sceneInfo.sceneName} ({i + 1}/{_database.scenes.Count})",
(float)i / _database.scenes.Count);
// 이미 썸네일이 있으면 스킵
if (!string.IsNullOrEmpty(sceneInfo.thumbnailPath) &&
File.Exists(sceneInfo.thumbnailPath))
{
successCount++;
continue;
}
// 씬 로드
var scene = UnityEditor.SceneManagement.EditorSceneManager.OpenScene(
sceneInfo.scenePath,
UnityEditor.SceneManagement.OpenSceneMode.Additive);
// 카메라 찾기
Camera cam = null;
var rootObjects = scene.GetRootGameObjects();
foreach (var obj in rootObjects)
{
cam = obj.GetComponentInChildren<Camera>();
if (cam != null) break;
}
if (cam == null)
{
cam = Camera.main;
}
if (cam != null)
{
// 캡처
_selectedCamera = cam;
_useSceneCamera = false;
Texture2D texture = CaptureImage();
if (texture != null)
{
string savePath = Path.GetDirectoryName(sceneInfo.scenePath);
string fullPath = Path.Combine(savePath, sceneInfo.sceneName + ".png");
byte[] bytes = texture.EncodeToPNG();
File.WriteAllBytes(fullPath, bytes);
DestroyImmediate(texture);
sceneInfo.thumbnailPath = fullPath.Replace("\\", "/");
successCount++;
}
else
{
failCount++;
}
}
else
{
UnityEngine.Debug.LogWarning($"카메라를 찾을 수 없음: {sceneInfo.sceneName}");
failCount++;
}
// 씬 언로드
UnityEditor.SceneManagement.EditorSceneManager.CloseScene(scene, true);
}
}
finally
{
EditorUtility.ClearProgressBar();
}
// 데이터베이스 저장
EditorUtility.SetDirty(_database);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
EditorUtility.DisplayDialog("완료",
$"썸네일 캡처 완료\n성공: {successCount}개\n실패: {failCount}개",
"확인");
}
}
}