Compare commits

17 Commits

Author SHA1 Message Date
jongjae0305 c335995a9a feat: update song selection, score UI, and song creator features
- SongSelectManager/SongDetailPanel: 곡 선택 및 상세 패널 개선
- SongCreatorManager: 곡 생성 기능 추가
- FinalScoreLabel/ScoreManager: 결과 화면 점수 UI 업데이트
- MarqueeText: 마퀴 텍스트 컴포넌트 개선
- NoteData/SongController: 노트 데이터 및 컨트롤러 보완

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 17:29:50 +09:00
whdwo798 72dad1ce4c docs: update project handoff 2026-05-29 00:53:49 +09:00
whdwo798 fb59fc36f7 feat: polish result screen UI 2026-05-29 00:51:32 +09:00
whdwo798 b46ccddbdb feat: polish game HUD scoring and results 2026-05-29 00:32:21 +09:00
jongjae0305 c4330aa544 docs: add NAS config template 2026-05-28 19:03:34 +09:00
jongjae0305 03105a4f85 feat: polish VR gameplay and sync tools 2026-05-28 19:01:20 +09:00
jongjae0305 ee34d79a66 docs: update html code review docs 2026-05-26 19:12:06 +09:00
jongjae0305 abd3c9bb36 fix: widen note lanes and clear warnings 2026-05-26 18:54:56 +09:00
jongjae0305 182d2c90b9 fix: stabilize VR UI and song playback 2026-05-26 18:21:58 +09:00
jongjae0305 5e5e918c10 docs: update project handoff status 2026-05-26 17:18:02 +09:00
jongjae0305 10e9ebae45 feat: improve VR menu pointer and BeatSaber flow 2026-05-26 17:13:02 +09:00
whdwo798 58838f0acb docs: add annotated code & code review HTML documents
학습용 HTML 2개 추가 — 전체 15개 스크립트 줄 단위 주석본(annotated_code.html)과
설계 분석/퀴즈 문서(code_review.html). LiberationSans SDF Fallback 에셋 교체.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 00:18:32 +09:00
whdwo798 2f6aff7691 feat: Game scene — SongController bridges custom map to VRBeatsKit
- SongController: loads MP3 + Beat Saber JSON map, runs countdown (3→2→1→GO),
  spawns cubes via VR_BeatManager.Spawn() synced to audioSource.time
- NoteData → SpawnEventInfo mapping: position/lineLayer → x/y, colorType → ColorSide,
  cutDirection → Direction enum
- travelTimeOverride on SpawnEventInfo: each cube's travel time is back-calculated
  from remaining time at spawn moment, so simultaneous notes arrive at hit zone together
  regardless of frame-level spawn delay
- AudioManager: add PlayClip(AudioClip) and CurrentTime property
- VR_BeatManager: respect travelTimeOverride when non-zero
- Settings.asset: targetTravelTime 0.5 → 1.8 for natural Beat Saber approach feel
- SceneBuilder ④: auto-builds Game.unity from SaberStyle, wires SongController refs,
  registers in Build Settings
- LiberationSans SDF fallback updated with NanumGothic for Korean text support
- Remove unused SampleScene

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 14:32:25 +09:00
whdwo798 64ef3d64ec feat: SongSelect UI polish — marquee title, button states, font
- MarqueeText: scrolling title for long song names (RectMask2D clipped)
- SongDetailPanel: difficulty selected = green (img.color direct set)
- SongSelectManager: ALL/OWNED tab active state via img.color
- Card layout: DestroyImmediate + correct rebuild order to fix zero-size title bug
- NanumGothic SDF fallback configured on LiberationSans for Korean support

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 13:31:04 +09:00
whdwo798 1f1100bbd8 feat: NanumGothic font + SongSelect UI fixes
- Add NanumGothic.ttf and NanumGothic SDF.asset for Korean text support
- SongSelectManager: cardFont SerializeField for card title/artist rendering
- SongDetailPanel: difficulty selected color changed from blue to green

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 10:42:04 +09:00
whdwo798 58c88dafff feat: SongSelect panel — NAS song list with download/play in Menu.unity
- Add DownloadManager, SongLibrary, SongSelectManager, SongDetailPanel scripts
- Rebuild SongSelect panel inside Menu.unity using VRBeatsKit style:
  left scroll list (ALL/OWNED tabs) + right detail panel (diff buttons, Download/Delete/Play)
- SceneBuilder replaced: only ③ Menu — Rebuild SongSelect Panel remains
- All UI text in English

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 01:22:16 +09:00
whdwo798 4dad9e5d5b feat: SongCreator 씬 완성 — Beat Sage URL 지원, info.dat 메타데이터 자동 추출
- BeatSageUploader: audio_url 지원(UploadFromUrl), PollAndDownload 공통화, ZIP 500 오류 3회 재시도
- BeatSageConverter: info.dat 파싱(SongMetadata), BPM 자동 감지 → 노트 타이밍 변환에 적용
- SongCreatorManager: title/BPM 필수 입력 제거, 난이도 4개 자동 선택, GenerateFlowFromUrl 버그 수정
- NasPublisher: audioPath null 허용(URL 흐름에서 로컬 파일 없는 경우 스킵)
- .gitignore/.gitattributes 초기 설정

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 23:37:34 +09:00
2581 changed files with 389991 additions and 116461 deletions
+12
View File
@@ -0,0 +1,12 @@
# Unity recommended line endings
* text=auto eol=lf
# Force binary for Unity binary assets
*.png binary
*.jpg binary
*.wav binary
*.mp3 binary
*.ogg binary
*.mp4 binary
*.fbx binary
*.asset binary
+40
View File
@@ -0,0 +1,40 @@
# Unity
/Library/
/Temp/
/Obj/
/Build/
/Builds/
/Logs/
/UserSettings/
# IDE / Generated
*.csproj
*.csproj.user
*.slnx
*.sln
.vscode/
.vs/
# Claude Code local settings
.claude/
# Credentials — never commit
.env
/env
/cookies.txt
/Assets/StreamingAssets/nas_config.json
/Assets/StreamingAssets/nas_config.json.meta
# Local tool output
/Captures/
/tools/unity-mcp-server/node_modules/
/Assets/_Recovery/
/Assets/_Recovery.meta
# Local video sources / superseded test clips
/Assets/img/*.mkv
/Assets/img/*.mkv.meta
/Assets/img/No Copyright Neon Lights Modern Animated Loop Background - Free Footage - Motion Stock Footage.mp4
/Assets/img/No Copyright Neon Lights Modern Animated Loop Background - Free Footage - Motion Stock Footage.mp4.meta
/Assets/img/neon_background_unity.mp4
/Assets/img/neon_background_unity.mp4.meta
+15
View File
@@ -0,0 +1,15 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d7fd9488000d3734a9e00ee676215985, type: 3}
m_Name: DefaultVolumeProfile
m_EditorClassIdentifier: Unity.RenderPipelines.Core.Runtime::UnityEngine.Rendering.VolumeProfile
components: []
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0767462997e881e4980faede0fe3cc8a
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 967f9dfcbece854419d004baa2dd052d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+946
View File
@@ -0,0 +1,946 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace VRBeats.EditorTools
{
[InitializeOnLoad]
internal static class UnityCodexBridgeServer
{
private const int PreferredPort = 19744;
private const int MaxPortAttempts = 5;
private const string AutoStartPrefKey = "VRBeats.CodexBridge.AutoStart";
private const int MaxLogs = 250;
private static readonly ConcurrentQueue<BridgeJob> Jobs = new ConcurrentQueue<BridgeJob>();
private static readonly List<LogEntry> Logs = new List<LogEntry>();
private static readonly object LogLock = new object();
private static TcpListener _listener;
private static Thread _serverThread;
private static bool _running;
private static int _port = PreferredPort;
private static int _logIndex;
static UnityCodexBridgeServer()
{
if (IsBackgroundEditorProcess())
return;
Application.logMessageReceived -= OnLogMessageReceived;
Application.logMessageReceived += OnLogMessageReceived;
EditorApplication.update -= ProcessJobs;
EditorApplication.update += ProcessJobs;
EditorApplication.quitting -= StopServer;
EditorApplication.quitting += StopServer;
AssemblyReloadEvents.beforeAssemblyReload -= StopServer;
AssemblyReloadEvents.beforeAssemblyReload += StopServer;
if (EditorPrefs.GetBool(AutoStartPrefKey, true))
StartServer();
}
[MenuItem("Tools/Codex Bridge/Start Server")]
private static void StartServerMenu()
{
StartServer();
}
[MenuItem("Tools/Codex Bridge/Stop Server")]
private static void StopServerMenu()
{
StopServer();
}
[MenuItem("Tools/Codex Bridge/Auto Start")]
private static void ToggleAutoStart()
{
bool enabled = !EditorPrefs.GetBool(AutoStartPrefKey, true);
EditorPrefs.SetBool(AutoStartPrefKey, enabled);
if (enabled)
StartServer();
}
[MenuItem("Tools/Codex Bridge/Auto Start", true)]
private static bool ValidateToggleAutoStart()
{
Menu.SetChecked("Tools/Codex Bridge/Auto Start", EditorPrefs.GetBool(AutoStartPrefKey, true));
return true;
}
[MenuItem("Tools/Codex Bridge/Capture Game View")]
private static void CaptureGameViewMenu()
{
BridgeResponse response = CaptureGameView(new Dictionary<string, string>());
Debug.Log("[CodexBridge] Capture result: " + response.Body);
}
[MenuItem("Tools/Codex Bridge/Open Sync Calibration")]
private static void OpenSyncCalibrationMenu()
{
SyncCalibrationOverlay.Open();
}
private static void StartServer()
{
if (IsBackgroundEditorProcess())
return;
if (_running)
return;
try
{
_listener = CreateListener();
_running = true;
_serverThread = new Thread(ServerLoop)
{
IsBackground = true,
Name = "UnityCodexBridgeServer"
};
_serverThread.Start();
Debug.Log("[CodexBridge] Listening on http://127.0.0.1:" + _port);
}
catch (Exception ex)
{
_running = false;
try
{
if (_listener != null)
_listener.Stop();
}
catch
{
// Ignore cleanup failures after a failed bind.
}
finally
{
_listener = null;
}
Debug.LogWarning("[CodexBridge] Failed to start: " + ex.Message);
}
}
private static void StopServer()
{
_running = false;
try
{
if (_listener != null)
_listener.Stop();
}
catch
{
// Ignore shutdown races.
}
_listener = null;
if (_serverThread != null && _serverThread.IsAlive)
_serverThread.Join(200);
_serverThread = null;
}
private static TcpListener CreateListener()
{
Exception lastException = null;
for (int i = 0; i < MaxPortAttempts; i++)
{
int port = PreferredPort + i;
TcpListener listener = null;
try
{
listener = new TcpListener(IPAddress.Loopback, port);
listener.Server.ExclusiveAddressUse = true;
listener.Start();
_port = port;
return listener;
}
catch (Exception ex)
{
lastException = ex;
try
{
if (listener != null)
listener.Stop();
}
catch
{
// Ignore cleanup failures while trying fallback ports.
}
}
}
throw lastException ?? new SocketException();
}
private static bool IsBackgroundEditorProcess()
{
string commandLine = Environment.CommandLine;
return Application.isBatchMode ||
commandLine.IndexOf("AssetImportWorker", StringComparison.OrdinalIgnoreCase) >= 0 ||
commandLine.IndexOf("-batchMode", StringComparison.OrdinalIgnoreCase) >= 0;
}
private static void ServerLoop()
{
while (_running)
{
try
{
TcpClient client = _listener.AcceptTcpClient();
ThreadPool.QueueUserWorkItem(_ => HandleClient(client));
}
catch
{
if (_running)
Thread.Sleep(100);
}
}
}
private static void HandleClient(TcpClient client)
{
using (client)
{
try
{
client.ReceiveTimeout = 5000;
client.SendTimeout = 5000;
BridgeRequest request = ReadRequest(client.GetStream());
BridgeResponse response;
if (request == null)
{
response = BridgeResponse.Json(400, "{\"ok\":false,\"error\":\"invalid_request\"}");
}
else if (request.Method == "OPTIONS")
{
response = BridgeResponse.Json(204, string.Empty);
}
else
{
BridgeJob job = new BridgeJob(request);
Jobs.Enqueue(job);
if (!job.Done.Wait(TimeSpan.FromSeconds(10)))
response = BridgeResponse.Json(504, "{\"ok\":false,\"error\":\"unity_main_thread_timeout\"}");
else
response = job.Response;
}
WriteResponse(client.GetStream(), response);
}
catch (Exception ex)
{
try
{
WriteResponse(client.GetStream(),
BridgeResponse.Json(500, "{\"ok\":false,\"error\":" + JsonString(ex.Message) + "}"));
}
catch
{
// Client has already gone away.
}
}
}
}
private static BridgeRequest ReadRequest(Stream stream)
{
StreamReader reader = new StreamReader(stream, Encoding.UTF8, false, 4096, true);
string requestLine = reader.ReadLine();
if (string.IsNullOrEmpty(requestLine))
return null;
string[] requestParts = requestLine.Split(' ');
if (requestParts.Length < 2)
return null;
int contentLength = 0;
string line;
while (!string.IsNullOrEmpty(line = reader.ReadLine()))
{
int separatorIndex = line.IndexOf(':');
if (separatorIndex <= 0)
continue;
string headerName = line.Substring(0, separatorIndex).Trim();
string headerValue = line.Substring(separatorIndex + 1).Trim();
if (string.Equals(headerName, "Content-Length", StringComparison.OrdinalIgnoreCase))
int.TryParse(headerValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out contentLength);
}
string body = string.Empty;
if (contentLength > 0)
{
char[] buffer = new char[contentLength];
int read = reader.ReadBlock(buffer, 0, contentLength);
body = new string(buffer, 0, read);
}
Uri uri = new Uri("http://127.0.0.1" + requestParts[1]);
return new BridgeRequest(requestParts[0].ToUpperInvariant(), uri.AbsolutePath, ParseQuery(uri.Query), body);
}
private static Dictionary<string, string> ParseQuery(string query)
{
Dictionary<string, string> values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (string.IsNullOrEmpty(query))
return values;
string trimmed = query[0] == '?' ? query.Substring(1) : query;
string[] pairs = trimmed.Split('&');
foreach (string pair in pairs)
{
if (string.IsNullOrEmpty(pair))
continue;
int separatorIndex = pair.IndexOf('=');
string key = separatorIndex >= 0 ? pair.Substring(0, separatorIndex) : pair;
string value = separatorIndex >= 0 ? pair.Substring(separatorIndex + 1) : string.Empty;
values[Uri.UnescapeDataString(key)] = Uri.UnescapeDataString(value.Replace("+", " "));
}
return values;
}
private static void WriteResponse(Stream stream, BridgeResponse response)
{
if (response == null)
response = BridgeResponse.Json(500, "{\"ok\":false,\"error\":\"null_response\"}");
byte[] body = Encoding.UTF8.GetBytes(response.Body ?? string.Empty);
string headers =
"HTTP/1.1 " + response.StatusCode + " " + StatusText(response.StatusCode) + "\r\n" +
"Content-Type: " + response.ContentType + "; charset=utf-8\r\n" +
"Content-Length: " + body.Length.ToString(CultureInfo.InvariantCulture) + "\r\n" +
"Access-Control-Allow-Origin: http://localhost\r\n" +
"Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n" +
"Access-Control-Allow-Headers: Content-Type\r\n" +
"Connection: close\r\n\r\n";
byte[] headerBytes = Encoding.UTF8.GetBytes(headers);
stream.Write(headerBytes, 0, headerBytes.Length);
if (body.Length > 0)
stream.Write(body, 0, body.Length);
}
private static string StatusText(int statusCode)
{
switch (statusCode)
{
case 200: return "OK";
case 204: return "No Content";
case 400: return "Bad Request";
case 404: return "Not Found";
case 405: return "Method Not Allowed";
case 500: return "Internal Server Error";
case 504: return "Gateway Timeout";
default: return "OK";
}
}
private static void ProcessJobs()
{
while (Jobs.TryDequeue(out BridgeJob job))
{
try
{
job.Response = Execute(job.Request);
}
catch (Exception ex)
{
job.Response = BridgeResponse.Json(500,
"{\"ok\":false,\"error\":" + JsonString(ex.Message) + "}");
}
finally
{
job.Done.Set();
}
}
}
private static BridgeResponse Execute(BridgeRequest request)
{
switch (request.Path)
{
case "/health":
case "/state":
return GetHealth();
case "/capture":
return CaptureGameView(request.Query);
case "/logs":
return GetLogs(request.Query);
case "/scene/roots":
return GetSceneRoots();
case "/scene/objects":
return GetSceneObjects(request.Query);
case "/object":
return GetObjectDetails(request.Query);
case "/play":
return SetPlayState(true, false);
case "/pause":
return SetPlayState(true, true);
case "/stop":
return SetPlayState(false, false);
case "/sync/open":
SyncCalibrationOverlay.Open();
return BridgeResponse.Json(200, "{\"ok\":true,\"opened\":\"sync_calibration\"}");
case "/transform":
if (request.Method != "POST")
return BridgeResponse.Json(405, "{\"ok\":false,\"error\":\"method_not_allowed\"}");
return SetTransform(request.Body);
default:
return BridgeResponse.Json(404, "{\"ok\":false,\"error\":\"not_found\"}");
}
}
private static BridgeResponse GetHealth()
{
Scene scene = SceneManager.GetActiveScene();
string body =
"{\"ok\":true" +
",\"bridge\":\"unity-codex-bridge\"" +
",\"port\":" + _port.ToString(CultureInfo.InvariantCulture) +
",\"unityVersion\":" + JsonString(Application.unityVersion) +
",\"projectPath\":" + JsonString(Directory.GetCurrentDirectory()) +
",\"scene\":" + JsonString(scene.IsValid() ? scene.name : string.Empty) +
",\"isPlaying\":" + Bool(EditorApplication.isPlaying) +
",\"isPaused\":" + Bool(EditorApplication.isPaused) +
"}";
return BridgeResponse.Json(200, body);
}
private static BridgeResponse CaptureGameView(Dictionary<string, string> query)
{
int width = GetInt(query, "width", 1280, 320, 4096);
int height = GetInt(query, "height", 720, 180, 4096);
string relativePath = "Captures/latest.png";
string absolutePath = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), relativePath));
Directory.CreateDirectory(Path.GetDirectoryName(absolutePath));
Camera camera = ResolveCaptureCamera();
string cameraName = camera != null ? camera.name : string.Empty;
bool usedCameraRender = false;
if (camera != null)
{
RenderTexture previousTarget = camera.targetTexture;
RenderTexture previousActive = RenderTexture.active;
RenderTexture renderTexture = RenderTexture.GetTemporary(width, height, 24, RenderTextureFormat.ARGB32);
Texture2D texture = null;
try
{
camera.targetTexture = renderTexture;
RenderTexture.active = renderTexture;
camera.Render();
texture = new Texture2D(width, height, TextureFormat.RGB24, false);
texture.ReadPixels(new Rect(0, 0, width, height), 0, 0);
texture.Apply();
File.WriteAllBytes(absolutePath, texture.EncodeToPNG());
usedCameraRender = true;
}
finally
{
camera.targetTexture = previousTarget;
RenderTexture.active = previousActive;
RenderTexture.ReleaseTemporary(renderTexture);
if (texture != null)
UnityEngine.Object.DestroyImmediate(texture);
}
}
if (!usedCameraRender)
ScreenCapture.CaptureScreenshot(relativePath);
string body =
"{\"ok\":true" +
",\"path\":" + JsonString(absolutePath) +
",\"relativePath\":" + JsonString(relativePath) +
",\"width\":" + width.ToString(CultureInfo.InvariantCulture) +
",\"height\":" + height.ToString(CultureInfo.InvariantCulture) +
",\"camera\":" + JsonString(cameraName) +
",\"mode\":" + JsonString(usedCameraRender ? "camera_render" : "screen_capture") +
"}";
return BridgeResponse.Json(200, body);
}
private static Camera ResolveCaptureCamera()
{
Camera main = Camera.main;
if (main != null && main.isActiveAndEnabled)
return main;
Camera[] cameras = UnityEngine.Object.FindObjectsByType<Camera>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
foreach (Camera camera in cameras)
{
if (camera != null && camera.isActiveAndEnabled)
return camera;
}
return main != null ? main : (cameras.Length > 0 ? cameras[0] : null);
}
private static BridgeResponse GetLogs(Dictionary<string, string> query)
{
int count = GetInt(query, "count", 80, 1, MaxLogs);
List<LogEntry> snapshot;
lock (LogLock)
{
int start = Mathf.Max(0, Logs.Count - count);
snapshot = Logs.GetRange(start, Logs.Count - start);
}
StringBuilder sb = new StringBuilder();
sb.Append("{\"ok\":true,\"logs\":[");
for (int i = 0; i < snapshot.Count; i++)
{
if (i > 0)
sb.Append(',');
LogEntry entry = snapshot[i];
sb.Append('{');
sb.Append("\"index\":").Append(entry.Index.ToString(CultureInfo.InvariantCulture)).Append(',');
sb.Append("\"time\":").Append(JsonString(entry.Time)).Append(',');
sb.Append("\"type\":").Append(JsonString(entry.Type)).Append(',');
sb.Append("\"message\":").Append(JsonString(entry.Message)).Append(',');
sb.Append("\"stackTrace\":").Append(JsonString(entry.StackTrace));
sb.Append('}');
}
sb.Append("]}");
return BridgeResponse.Json(200, sb.ToString());
}
private static BridgeResponse GetSceneRoots()
{
Scene scene = SceneManager.GetActiveScene();
GameObject[] roots = scene.IsValid() ? scene.GetRootGameObjects() : Array.Empty<GameObject>();
StringBuilder sb = new StringBuilder();
sb.Append("{\"ok\":true,\"scene\":").Append(JsonString(scene.IsValid() ? scene.name : string.Empty));
sb.Append(",\"roots\":[");
for (int i = 0; i < roots.Length; i++)
{
if (i > 0)
sb.Append(',');
AppendGameObjectSummary(sb, roots[i]);
}
sb.Append("]}");
return BridgeResponse.Json(200, sb.ToString());
}
private static BridgeResponse GetSceneObjects(Dictionary<string, string> query)
{
string filter = GetString(query, "query", string.Empty);
int limit = GetInt(query, "limit", 120, 1, 500);
string lowerFilter = filter.ToLowerInvariant();
Transform[] transforms = UnityEngine.Object.FindObjectsByType<Transform>(FindObjectsInactive.Include, FindObjectsSortMode.None);
StringBuilder sb = new StringBuilder();
sb.Append("{\"ok\":true,\"objects\":[");
int written = 0;
foreach (Transform transform in transforms)
{
if (transform == null || transform.gameObject == null)
continue;
if (EditorUtility.IsPersistent(transform.gameObject))
continue;
if (!transform.gameObject.scene.IsValid())
continue;
string path = GetHierarchyPath(transform.gameObject);
if (!string.IsNullOrEmpty(lowerFilter) &&
path.ToLowerInvariant().IndexOf(lowerFilter, StringComparison.Ordinal) < 0)
continue;
if (written > 0)
sb.Append(',');
AppendGameObjectSummary(sb, transform.gameObject);
written++;
if (written >= limit)
break;
}
sb.Append("],\"count\":").Append(written.ToString(CultureInfo.InvariantCulture)).Append('}');
return BridgeResponse.Json(200, sb.ToString());
}
private static BridgeResponse GetObjectDetails(Dictionary<string, string> query)
{
string path = GetString(query, "path", string.Empty);
GameObject go = FindGameObjectByPath(path);
if (go == null)
return BridgeResponse.Json(404, "{\"ok\":false,\"error\":\"object_not_found\",\"path\":" + JsonString(path) + "}");
StringBuilder sb = new StringBuilder();
sb.Append("{\"ok\":true,\"object\":");
AppendGameObjectDetails(sb, go);
sb.Append('}');
return BridgeResponse.Json(200, sb.ToString());
}
private static BridgeResponse SetPlayState(bool play, bool pause)
{
EditorApplication.isPlaying = play;
EditorApplication.isPaused = pause;
return GetHealth();
}
private static BridgeResponse SetTransform(string body)
{
string path = ExtractString(body, "path");
if (string.IsNullOrEmpty(path))
return BridgeResponse.Json(400, "{\"ok\":false,\"error\":\"missing_path\"}");
GameObject go = FindGameObjectByPath(path);
if (go == null)
return BridgeResponse.Json(404, "{\"ok\":false,\"error\":\"object_not_found\",\"path\":" + JsonString(path) + "}");
Undo.RecordObject(go.transform, "Codex Bridge Set Transform");
if (TryExtractVector3(body, "position", out Vector3 position))
go.transform.position = position;
if (TryExtractVector3(body, "localPosition", out Vector3 localPosition))
go.transform.localPosition = localPosition;
if (TryExtractVector3(body, "rotationEuler", out Vector3 rotationEuler))
go.transform.eulerAngles = rotationEuler;
if (TryExtractVector3(body, "localRotationEuler", out Vector3 localRotationEuler))
go.transform.localEulerAngles = localRotationEuler;
if (TryExtractVector3(body, "scale", out Vector3 scale))
go.transform.localScale = scale;
EditorUtility.SetDirty(go.transform);
StringBuilder sb = new StringBuilder();
sb.Append("{\"ok\":true,\"object\":");
AppendGameObjectDetails(sb, go);
sb.Append('}');
return BridgeResponse.Json(200, sb.ToString());
}
private static void AppendGameObjectSummary(StringBuilder sb, GameObject go)
{
sb.Append('{');
sb.Append("\"name\":").Append(JsonString(go.name)).Append(',');
sb.Append("\"path\":").Append(JsonString(GetHierarchyPath(go))).Append(',');
sb.Append("\"activeSelf\":").Append(Bool(go.activeSelf)).Append(',');
sb.Append("\"activeInHierarchy\":").Append(Bool(go.activeInHierarchy)).Append(',');
sb.Append("\"scene\":").Append(JsonString(go.scene.IsValid() ? go.scene.name : string.Empty)).Append(',');
sb.Append("\"position\":").Append(VectorJson(go.transform.position)).Append(',');
sb.Append("\"rotationEuler\":").Append(VectorJson(go.transform.eulerAngles)).Append(',');
sb.Append("\"scale\":").Append(VectorJson(go.transform.localScale));
sb.Append('}');
}
private static void AppendGameObjectDetails(StringBuilder sb, GameObject go)
{
AppendGameObjectSummary(sb, go);
sb.Length -= 1;
Component[] components = go.GetComponents<Component>();
sb.Append(",\"components\":[");
for (int i = 0; i < components.Length; i++)
{
if (i > 0)
sb.Append(',');
Component component = components[i];
sb.Append(JsonString(component != null ? component.GetType().FullName : "Missing Script"));
}
sb.Append("]}");
}
private static GameObject FindGameObjectByPath(string path)
{
if (string.IsNullOrEmpty(path))
return null;
string[] parts = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 0)
return null;
Scene scene = SceneManager.GetActiveScene();
if (!scene.IsValid())
return null;
foreach (GameObject root in scene.GetRootGameObjects())
{
if (root.name != parts[0])
continue;
Transform current = root.transform;
for (int i = 1; i < parts.Length && current != null; i++)
{
current = FindChildByName(current, parts[i]);
}
if (current != null)
return current.gameObject;
}
return null;
}
private static Transform FindChildByName(Transform parent, string childName)
{
for (int i = 0; i < parent.childCount; i++)
{
Transform child = parent.GetChild(i);
if (child.name == childName)
return child;
}
return null;
}
private static string GetHierarchyPath(GameObject go)
{
Stack<string> parts = new Stack<string>();
Transform current = go.transform;
while (current != null)
{
parts.Push(current.name);
current = current.parent;
}
return string.Join("/", parts.ToArray());
}
private static void OnLogMessageReceived(string condition, string stackTrace, LogType type)
{
lock (LogLock)
{
Logs.Add(new LogEntry
{
Index = ++_logIndex,
Time = DateTime.Now.ToString("HH:mm:ss", CultureInfo.InvariantCulture),
Type = type.ToString(),
Message = condition ?? string.Empty,
StackTrace = stackTrace ?? string.Empty
});
if (Logs.Count > MaxLogs)
Logs.RemoveAt(0);
}
}
private static int GetInt(Dictionary<string, string> query, string key, int fallback, int min, int max)
{
if (!query.TryGetValue(key, out string value) ||
!int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed))
return fallback;
return Mathf.Clamp(parsed, min, max);
}
private static string GetString(Dictionary<string, string> query, string key, string fallback)
{
return query.TryGetValue(key, out string value) ? value : fallback;
}
private static string ExtractString(string json, string key)
{
if (string.IsNullOrEmpty(json))
return string.Empty;
Match match = Regex.Match(json,
"\"" + Regex.Escape(key) + "\"\\s*:\\s*\"((?:\\\\.|[^\"])*)\"",
RegexOptions.CultureInvariant);
return match.Success ? Regex.Unescape(match.Groups[1].Value) : string.Empty;
}
private static bool TryExtractVector3(string json, string key, out Vector3 value)
{
value = Vector3.zero;
if (string.IsNullOrEmpty(json))
return false;
Match objectMatch = Regex.Match(json,
"\"" + Regex.Escape(key) + "\"\\s*:\\s*\\{(?<body>.*?)\\}",
RegexOptions.Singleline | RegexOptions.CultureInvariant);
if (!objectMatch.Success)
return false;
string objectBody = objectMatch.Groups["body"].Value;
if (!TryExtractFloat(objectBody, "x", out float x) ||
!TryExtractFloat(objectBody, "y", out float y) ||
!TryExtractFloat(objectBody, "z", out float z))
return false;
value = new Vector3(x, y, z);
return true;
}
private static bool TryExtractFloat(string json, string key, out float value)
{
value = 0f;
Match match = Regex.Match(json,
"\"" + Regex.Escape(key) + "\"\\s*:\\s*(?<num>-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?)",
RegexOptions.CultureInvariant);
return match.Success &&
float.TryParse(match.Groups["num"].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out value);
}
private static string VectorJson(Vector3 value)
{
return "{\"x\":" + FloatJson(value.x) +
",\"y\":" + FloatJson(value.y) +
",\"z\":" + FloatJson(value.z) + "}";
}
private static string FloatJson(float value)
{
if (float.IsNaN(value) || float.IsInfinity(value))
return "0";
return value.ToString("0.#####", CultureInfo.InvariantCulture);
}
private static string Bool(bool value)
{
return value ? "true" : "false";
}
private static string JsonString(string value)
{
if (value == null)
return "null";
StringBuilder sb = new StringBuilder(value.Length + 2);
sb.Append('"');
foreach (char c in value)
{
switch (c)
{
case '\\':
sb.Append("\\\\");
break;
case '"':
sb.Append("\\\"");
break;
case '\n':
sb.Append("\\n");
break;
case '\r':
sb.Append("\\r");
break;
case '\t':
sb.Append("\\t");
break;
default:
if (c < 32)
sb.Append("\\u").Append(((int)c).ToString("x4", CultureInfo.InvariantCulture));
else
sb.Append(c);
break;
}
}
sb.Append('"');
return sb.ToString();
}
private sealed class BridgeRequest
{
public readonly string Method;
public readonly string Path;
public readonly Dictionary<string, string> Query;
public readonly string Body;
public BridgeRequest(string method, string path, Dictionary<string, string> query, string body)
{
Method = method;
Path = path;
Query = query;
Body = body ?? string.Empty;
}
}
private sealed class BridgeResponse
{
public readonly int StatusCode;
public readonly string ContentType;
public readonly string Body;
private BridgeResponse(int statusCode, string contentType, string body)
{
StatusCode = statusCode;
ContentType = contentType;
Body = body;
}
public static BridgeResponse Json(int statusCode, string body)
{
return new BridgeResponse(statusCode, "application/json", body);
}
}
private sealed class BridgeJob
{
public readonly BridgeRequest Request;
public readonly ManualResetEventSlim Done = new ManualResetEventSlim(false);
public BridgeResponse Response;
public BridgeJob(BridgeRequest request)
{
Request = request;
}
}
private sealed class LogEntry
{
public int Index;
public string Time;
public string Type;
public string Message;
public string StackTrace;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b3aaf23c3d6f42b5b1f68100b9b0f682
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+507
View File
@@ -0,0 +1,507 @@
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using VRBeats;
using VRBeats.ScriptableEvents;
public static class VRBeatSaberSceneBuilder
{
private const string MenuScene = "Assets/VRBeatsKit/Scenes/Menu.unity";
// ─────────────────────────────────────────────
// ④ Build Game Scene
// SaberStyle 복제 → Game.unity 생성
// PlayableManager 제거, SongController + 카운트다운 캔버스 추가
// ─────────────────────────────────────────────
[MenuItem("Tools/VRBeatSaber/④ Build Game Scene")]
public static void BuildGameScene()
{
const string saberStylePath = "Assets/VRBeatsKit/Scenes/SaberStyle.unity";
const string gamePath = "Assets/Scenes/Game.unity";
// SaberStyle → Game.unity 복제 (이미 있으면 그냥 열기)
if (!AssetDatabase.LoadAssetAtPath<Object>(gamePath))
{
if (!AssetDatabase.CopyAsset(saberStylePath, gamePath))
{
Debug.LogError("[SceneBuilder] SaberStyle.unity 복제 실패");
return;
}
AssetDatabase.Refresh();
}
var scene = EditorSceneManager.OpenScene(gamePath, OpenSceneMode.Single);
// PlayableManager 제거 (PlayableDirector는 유지)
var pm = Object.FindFirstObjectByType<PlayableManager>();
if (pm != null)
Object.DestroyImmediate(pm);
// SongController GO 생성
var scGO = new GameObject("SongController");
var songController = scGO.AddComponent<SongController>();
var cubePrefab = AssetDatabase.LoadAssetAtPath<Spawneable>(
"Assets/VRBeatsKit/Prefabs/Spawneable/VR_BeatCube.prefab");
var onLevelComplete = AssetDatabase.LoadAssetAtPath<GameEvent>(
"Assets/VRBeatsKit/GameEvents/OnLevelComplete.asset");
// 카운트다운 캔버스 생성
var canvasGO = new GameObject("CountdownCanvas");
var canvas = canvasGO.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = 100;
canvasGO.AddComponent<CanvasScaler>();
canvasGO.AddComponent<GraphicRaycaster>();
var countdownGO = new GameObject("CountdownText");
countdownGO.transform.SetParent(canvasGO.transform, false);
var cRect = countdownGO.AddComponent<RectTransform>();
cRect.anchorMin = new Vector2(0.5f, 0.5f);
cRect.anchorMax = new Vector2(0.5f, 0.5f);
cRect.pivot = new Vector2(0.5f, 0.5f);
cRect.anchoredPosition = Vector2.zero;
cRect.sizeDelta = new Vector2(400f, 200f);
var cTmp = countdownGO.AddComponent<TextMeshProUGUI>();
cTmp.text = "";
cTmp.fontSize = 120f;
cTmp.color = Color.white;
cTmp.alignment = TextAlignmentOptions.Center;
cTmp.fontStyle = FontStyles.Bold;
countdownGO.SetActive(false);
// SongController 필드 연결
var scSO = new SerializedObject(songController);
scSO.FindProperty("cubePrefab") .objectReferenceValue = cubePrefab;
scSO.FindProperty("onLevelComplete") .objectReferenceValue = onLevelComplete;
scSO.FindProperty("countdownText") .objectReferenceValue = cTmp;
scSO.ApplyModifiedPropertiesWithoutUndo();
// Build Settings 에 Game.unity 추가
var scenes = EditorBuildSettings.scenes;
bool exists = System.Array.Exists(scenes, s => s.path == gamePath);
if (!exists)
{
var newList = new EditorBuildSettingsScene[scenes.Length + 1];
System.Array.Copy(scenes, newList, scenes.Length);
newList[scenes.Length] = new EditorBuildSettingsScene(gamePath, true);
EditorBuildSettings.scenes = newList;
}
EditorSceneManager.MarkSceneDirty(scene);
EditorSceneManager.SaveScene(scene);
Debug.Log("[SceneBuilder] ✓ Game.unity 생성 완료");
}
// ─────────────────────────────────────────────
// ③ Menu — Rebuild SongSelect Panel
//
// Canvas(SongSelect) size: 105.885 × 68.223
// BG child covers full canvas (stretch anchors)
// BG local coord origin = center
// X: -52.94 ~ +52.94
// Y: -34.11 ~ +34.11
// ─────────────────────────────────────────────
[MenuItem("Tools/VRBeatSaber/③ Menu — Rebuild SongSelect Panel")]
public static void RebuildSongSelectPanel()
{
var scene = EditorSceneManager.OpenScene(MenuScene, OpenSceneMode.Single);
var songSelectGO = GameObject.Find("SongSelect");
if (songSelectGO == null) { Debug.LogError("[SceneBuilder] 'SongSelect' not found."); return; }
var bgTransform = songSelectGO.transform.Find("BG");
if (bgTransform == null) { Debug.LogError("[SceneBuilder] 'SongSelect/BG' not found."); return; }
// Clear BG children
for (int i = bgTransform.childCount - 1; i >= 0; i--)
Object.DestroyImmediate(bgTransform.GetChild(i).gameObject);
// Create/reuse SongSystem root GO for SongLibrary (must be root for DontDestroyOnLoad)
var sysGO = GameObject.Find("SongSystem");
if (sysGO == null) sysGO = new GameObject("SongSystem");
var oldLib = sysGO.GetComponent<SongLibrary>();
if (oldLib != null) Object.DestroyImmediate(oldLib);
var songLibrary = sysGO.AddComponent<SongLibrary>();
// Add/replace SongSelectManager + DownloadManager on SongSelect GO
var oldSSM = songSelectGO.GetComponent<SongSelectManager>();
if (oldSSM != null) Object.DestroyImmediate(oldSSM);
var oldDM = songSelectGO.GetComponent<DownloadManager>();
if (oldDM != null) Object.DestroyImmediate(oldDM);
var downloadManager = songSelectGO.AddComponent<DownloadManager>();
var songSelectManager = songSelectGO.AddComponent<SongSelectManager>();
var bg = bgTransform;
// ── Header ──────────────────────────────────────────
CreateLabel(bg, "Title", "SONG SELECT",
new Vector2(0f, 28.5f), new Vector2(100f, 9f), 8.5f,
Color.white, TextAlignmentOptions.Center);
CreateDivider(bg, "DivHeader", new Vector2(0f, 23.5f), new Vector2(104f, 0.5f));
var tabAllBtn = CreateStyledButton(bg, "TabAll", "ALL", new Vector2(-18f, 19.5f), new Vector2(30f, 7f), 5f);
var tabOwnedBtn = CreateStyledButton(bg, "TabOwned", "OWNED", new Vector2( 14f, 19.5f), new Vector2(30f, 7f), 5f);
CreateDivider(bg, "DivTabs", new Vector2(0f, 15.5f), new Vector2(104f, 0.5f));
// ── Content area: Y from 15 to -34.11, height ~49 ───
// ListPanel (left half)
var listPanelGO = new GameObject("ListPanel");
listPanelGO.transform.SetParent(bg, false);
SetRect(listPanelGO, new Vector2(-26.6f, -9.4f), new Vector2(52.7f, 49f));
// Vertical divider
CreateDivider(bg, "DivVertical", new Vector2(0.1f, -9.4f), new Vector2(0.5f, 49f));
// DetailPanel (right half, hidden until card clicked)
var detailPanelGO = new GameObject("DetailPanel");
detailPanelGO.transform.SetParent(bg, false);
SetRect(detailPanelGO, new Vector2(26.6f, -9.4f), new Vector2(52.7f, 49f));
// ── ListPanel contents ───────────────────────────────
RectTransform scrollContent;
GameObject loadingOverlay;
GameObject errorOverlay;
TMP_Text errorText;
BuildScrollList(listPanelGO.transform,
out scrollContent, out loadingOverlay, out errorOverlay, out errorText);
// ── DetailPanel contents ─────────────────────────────
var detailPanelComp = detailPanelGO.AddComponent<SongDetailPanel>();
Button btnNormal, btnHard, btnExpert, btnExpertPlus;
Button downloadBtn, deleteBtn, playBtn, closeBtn;
GameObject progressGroup;
Slider progressSlider;
TMP_Text progressText;
TMP_Text titleTmp, artistTmp, infoTmp;
BuildDetailPanelUI(detailPanelGO.transform,
out titleTmp, out artistTmp, out infoTmp,
out btnNormal, out btnHard, out btnExpert, out btnExpertPlus,
out downloadBtn, out deleteBtn, out playBtn, out closeBtn,
out progressGroup, out progressSlider, out progressText);
detailPanelGO.SetActive(false);
// ── Wire SongDetailPanel refs ────────────────────────
var dpSO = new SerializedObject(detailPanelComp);
dpSO.FindProperty("titleText") .objectReferenceValue = titleTmp;
dpSO.FindProperty("artistText") .objectReferenceValue = artistTmp;
dpSO.FindProperty("infoText") .objectReferenceValue = infoTmp;
dpSO.FindProperty("btnNormal") .objectReferenceValue = btnNormal;
dpSO.FindProperty("btnHard") .objectReferenceValue = btnHard;
dpSO.FindProperty("btnExpert") .objectReferenceValue = btnExpert;
dpSO.FindProperty("btnExpertPlus") .objectReferenceValue = btnExpertPlus;
dpSO.FindProperty("downloadButton") .objectReferenceValue = downloadBtn;
dpSO.FindProperty("deleteButton") .objectReferenceValue = deleteBtn;
dpSO.FindProperty("playButton") .objectReferenceValue = playBtn;
dpSO.FindProperty("closeButton") .objectReferenceValue = closeBtn;
dpSO.FindProperty("progressGroup") .objectReferenceValue = progressGroup;
dpSO.FindProperty("progressSlider") .objectReferenceValue = progressSlider;
dpSO.FindProperty("progressText") .objectReferenceValue = progressText;
dpSO.FindProperty("gameSceneName") .stringValue = "Game";
dpSO.ApplyModifiedPropertiesWithoutUndo();
// ── Wire SongSelectManager refs ──────────────────────
var smSO = new SerializedObject(songSelectManager);
smSO.FindProperty("tabAllBtn") .objectReferenceValue = tabAllBtn.GetComponent<Button>();
smSO.FindProperty("tabOwnedBtn") .objectReferenceValue = tabOwnedBtn.GetComponent<Button>();
smSO.FindProperty("cardContainer") .objectReferenceValue = scrollContent;
smSO.FindProperty("detailPanel") .objectReferenceValue = detailPanelComp;
smSO.FindProperty("downloadManager").objectReferenceValue = downloadManager;
smSO.FindProperty("loadingOverlay") .objectReferenceValue = loadingOverlay;
smSO.FindProperty("errorOverlay") .objectReferenceValue = errorOverlay;
smSO.FindProperty("errorText") .objectReferenceValue = errorText;
smSO.ApplyModifiedPropertiesWithoutUndo();
EditorSceneManager.MarkSceneDirty(scene);
EditorSceneManager.SaveScene(scene);
Debug.Log("[SceneBuilder] ✓ SongSelect panel rebuilt in Menu.unity");
}
// ─────────────────────────────────────────────
// ListPanel: ScrollRect + overlays
// ─────────────────────────────────────────────
private static void BuildScrollList(Transform parent,
out RectTransform scrollContent,
out GameObject loadingOverlay,
out GameObject errorOverlay,
out TMP_Text errorText)
{
// ScrollRect (fills parent)
var scrollGO = new GameObject("Scroll");
scrollGO.transform.SetParent(parent, false);
StretchFull(scrollGO);
// Viewport with Mask
var vpGO = new GameObject("Viewport");
vpGO.transform.SetParent(scrollGO.transform, false);
StretchFull(vpGO);
var vpImg = vpGO.AddComponent<Image>();
vpImg.color = new Color(0f, 0f, 0f, 0.01f);
vpGO.AddComponent<Mask>().showMaskGraphic = false;
// Content with VerticalLayoutGroup + ContentSizeFitter
var contentGO = new GameObject("Content");
contentGO.transform.SetParent(vpGO.transform, false);
var contentRect = contentGO.AddComponent<RectTransform>();
contentRect.anchorMin = new Vector2(0f, 1f);
contentRect.anchorMax = new Vector2(1f, 1f);
contentRect.pivot = new Vector2(0.5f, 1f);
contentRect.anchoredPosition = Vector2.zero;
contentRect.sizeDelta = Vector2.zero;
var vlg = contentGO.AddComponent<VerticalLayoutGroup>();
vlg.spacing = 1.5f;
vlg.padding = new RectOffset(2, 2, 2, 2);
vlg.childForceExpandWidth = true;
vlg.childForceExpandHeight = false;
vlg.childControlWidth = true;
vlg.childControlHeight = true;
var csf = contentGO.AddComponent<ContentSizeFitter>();
csf.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
csf.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
// ScrollRect component
var sr = scrollGO.AddComponent<ScrollRect>();
sr.content = contentRect;
sr.viewport = vpGO.GetComponent<RectTransform>();
sr.horizontal = false;
sr.vertical = true;
sr.movementType = ScrollRect.MovementType.Clamped;
sr.scrollSensitivity = 30f;
sr.inertia = true;
sr.decelerationRate = 0.135f;
// Loading overlay
loadingOverlay = new GameObject("LoadingOverlay");
loadingOverlay.transform.SetParent(parent, false);
StretchFull(loadingOverlay);
loadingOverlay.AddComponent<Image>().color = new Color(0.10f, 0.18f, 0.22f, 0.92f);
CreateLabel(loadingOverlay.transform, "Text", "Loading...",
Vector2.zero, new Vector2(40f, 10f), 5f, Color.white, TextAlignmentOptions.Center);
// Error overlay (hidden by default)
errorOverlay = new GameObject("ErrorOverlay");
errorOverlay.transform.SetParent(parent, false);
StretchFull(errorOverlay);
errorOverlay.AddComponent<Image>().color = new Color(0.10f, 0.18f, 0.22f, 0.92f);
var errLblGO = CreateLabel(errorOverlay.transform, "ErrorText", "",
Vector2.zero, new Vector2(48f, 20f), 4.5f, new Color(1f, 0.5f, 0.5f), TextAlignmentOptions.Center);
errorText = errLblGO.GetComponent<TMP_Text>();
errorOverlay.SetActive(false);
scrollContent = contentRect;
}
// ─────────────────────────────────────────────
// DetailPanel UI
// Local space: 52.7 × 49 → X: ±26.35, Y: ±24.5
// ─────────────────────────────────────────────
private static void BuildDetailPanelUI(Transform parent,
out TMP_Text titleTmp, out TMP_Text artistTmp, out TMP_Text infoTmp,
out Button btnNormal, out Button btnHard, out Button btnExpert, out Button btnExpertPlus,
out Button downloadBtn, out Button deleteBtn, out Button playBtn, out Button closeBtn,
out GameObject progressGroup, out Slider progressSlider, out TMP_Text progressText)
{
// Close button (top-right)
var closeBtnGO = CreateStyledButton(parent, "CloseBtn", "✕",
new Vector2(21f, 20.5f), new Vector2(8f, 7f), 5.5f);
closeBtn = closeBtnGO.GetComponent<Button>();
// Song info
var titleGO = CreateLabel(parent, "TitleText", "---",
new Vector2(-3f, 18.5f), new Vector2(38f, 8f), 6.5f,
Color.white, TextAlignmentOptions.MidlineLeft);
titleTmp = titleGO.GetComponent<TMP_Text>();
titleTmp.overflowMode = TextOverflowModes.Ellipsis;
var artistGO = CreateLabel(parent, "ArtistText", "",
new Vector2(0f, 12f), new Vector2(50f, 6f), 5f,
new Color(1f, 1f, 1f, 0.8f), TextAlignmentOptions.Center);
artistTmp = artistGO.GetComponent<TMP_Text>();
var infoGO = CreateLabel(parent, "InfoText", "",
new Vector2(0f, 7f), new Vector2(50f, 5f), 4.2f,
new Color(1f, 1f, 1f, 0.6f), TextAlignmentOptions.Center);
infoTmp = infoGO.GetComponent<TMP_Text>();
CreateDivider(parent, "Div1", new Vector2(0f, 4f), new Vector2(50f, 0.4f));
// Difficulty section
CreateLabel(parent, "LblDifficulty", "DIFFICULTY",
new Vector2(-16f, 1.5f), new Vector2(26f, 5f), 4.2f,
new Color(1f, 1f, 1f, 0.65f), TextAlignmentOptions.MidlineLeft);
var btnNormalGO = CreateStyledButton(parent, "BtnNormal", "Normal", new Vector2(-12f, -5f), new Vector2(22f, 7f), 4.5f);
var btnHardGO = CreateStyledButton(parent, "BtnHard", "Hard", new Vector2( 12f, -5f), new Vector2(22f, 7f), 4.5f);
var btnExpertGO = CreateStyledButton(parent, "BtnExpert", "Expert", new Vector2(-12f, -14f), new Vector2(22f, 7f), 4.5f);
var btnExpertPlusGO = CreateStyledButton(parent, "BtnExpertPlus", "Expert+", new Vector2( 12f, -14f), new Vector2(22f, 7f), 4.5f);
btnNormal = btnNormalGO .GetComponent<Button>();
btnHard = btnHardGO .GetComponent<Button>();
btnExpert = btnExpertGO .GetComponent<Button>();
btnExpertPlus = btnExpertPlusGO.GetComponent<Button>();
CreateDivider(parent, "Div2", new Vector2(0f, -18.5f), new Vector2(50f, 0.4f));
// Action buttons
var downloadBtnGO = CreateStyledButton(parent, "DownloadBtn", "Download",
new Vector2(-6f, -21.5f), new Vector2(34f, 7f), 5f);
var deleteBtnGO = CreateStyledButton(parent, "DeleteBtn", "Delete",
new Vector2(-6f, -21.5f), new Vector2(34f, 7f), 5f);
var playBtnGO = CreateStyledButton(parent, "PlayBtn", "Play",
new Vector2(19f, -21.5f), new Vector2(16f, 7f), 5f);
downloadBtn = downloadBtnGO.GetComponent<Button>();
deleteBtn = deleteBtnGO .GetComponent<Button>();
playBtn = playBtnGO .GetComponent<Button>();
// Make delete button red-tinted
var delImg = deleteBtnGO.GetComponent<Image>();
if (delImg != null) delImg.color = new Color(0.9f, 0.3f, 0.3f, 0.3f);
// Progress group (hidden by default)
progressGroup = new GameObject("ProgressGroup");
progressGroup.transform.SetParent(parent, false);
SetRect(progressGroup, new Vector2(0f, -21.5f), new Vector2(50f, 7f));
var pTextGO = CreateLabel(progressGroup.transform, "ProgressText", "--- 0%",
new Vector2(-13f, 0f), new Vector2(22f, 6f), 4f,
new Color(1f, 1f, 1f, 0.85f), TextAlignmentOptions.MidlineLeft);
progressText = pTextGO.GetComponent<TMP_Text>();
progressSlider = CreateSlider(progressGroup.transform, "ProgressSlider",
new Vector2(18f, 0f), new Vector2(18f, 4.5f));
progressGroup.SetActive(false);
}
// ─────────────────────────────────────────────
// Helpers — UI factory
// ─────────────────────────────────────────────
private static GameObject CreateStyledButton(Transform parent, string goName, string label,
Vector2 pos, Vector2 size, float fontSize)
{
var go = new GameObject(goName);
go.transform.SetParent(parent, false);
SetRect(go, pos, size);
var img = go.AddComponent<Image>();
img.color = new Color(1f, 1f, 1f, 0.12f);
var btn = go.AddComponent<Button>();
btn.targetGraphic = img;
var c = btn.colors;
c.normalColor = Color.white;
c.highlightedColor = new Color(0.96f, 0.96f, 0.96f, 1f);
c.pressedColor = new Color(0.78f, 0.78f, 0.78f, 1f);
c.selectedColor = new Color(0.96f, 0.96f, 0.96f, 1f);
c.fadeDuration = 0.1f;
btn.colors = c;
var textGO = new GameObject("Text");
textGO.transform.SetParent(go.transform, false);
StretchFull(textGO);
var tmp = textGO.AddComponent<TextMeshProUGUI>();
tmp.text = label;
tmp.alignment = TextAlignmentOptions.Center;
tmp.fontSize = fontSize;
tmp.color = Color.white;
return go;
}
private static GameObject CreateLabel(Transform parent, string goName, string text,
Vector2 pos, Vector2 size, float fontSize,
Color? color = null, TextAlignmentOptions align = TextAlignmentOptions.MidlineLeft)
{
var go = new GameObject(goName);
go.transform.SetParent(parent, false);
SetRect(go, pos, size);
var tmp = go.AddComponent<TextMeshProUGUI>();
tmp.text = text;
tmp.fontSize = fontSize;
tmp.color = color ?? Color.white;
tmp.alignment = align;
return go;
}
private static void CreateDivider(Transform parent, string goName, Vector2 pos, Vector2 size)
{
var go = new GameObject(goName);
go.transform.SetParent(parent, false);
SetRect(go, pos, size);
var img = go.AddComponent<Image>();
img.color = new Color(1f, 1f, 1f, 0.18f);
img.raycastTarget = false;
}
private static Slider CreateSlider(Transform parent, string goName, Vector2 pos, Vector2 size)
{
var go = new GameObject(goName);
go.transform.SetParent(parent, false);
SetRect(go, pos, size);
var bgGO = new GameObject("Background");
bgGO.transform.SetParent(go.transform, false);
StretchFull(bgGO);
bgGO.AddComponent<Image>().color = new Color(1f, 1f, 1f, 0.15f);
var fillArea = new GameObject("Fill Area");
fillArea.transform.SetParent(go.transform, false);
StretchFull(fillArea);
var fill = new GameObject("Fill");
fill.transform.SetParent(fillArea.transform, false);
StretchFull(fill);
fill.AddComponent<Image>().color = new Color(0.3f, 0.8f, 0.3f, 0.9f);
var slider = go.AddComponent<Slider>();
slider.fillRect = fill.GetComponent<RectTransform>();
slider.minValue = 0f;
slider.maxValue = 1f;
slider.value = 0f;
slider.interactable = false;
return slider;
}
// ─────────────────────────────────────────────
// Utils
// ─────────────────────────────────────────────
private static void SetRect(GameObject go, Vector2 pos, Vector2 size)
{
var r = go.GetComponent<RectTransform>();
if (r == null) r = go.AddComponent<RectTransform>();
r.anchorMin = new Vector2(0.5f, 0.5f);
r.anchorMax = new Vector2(0.5f, 0.5f);
r.pivot = new Vector2(0.5f, 0.5f);
r.anchoredPosition = pos;
r.sizeDelta = size;
}
private static void StretchFull(GameObject go)
{
var r = go.GetComponent<RectTransform>();
if (r == null) r = go.AddComponent<RectTransform>();
r.anchorMin = Vector2.zero;
r.anchorMax = Vector2.one;
r.offsetMin = r.offsetMax = Vector2.zero;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 424dfcaa538598f4785c3cb3be62ec8a
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 052faaac586de48259a63d0c4782560b
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 11500000, guid: 8404be70184654265930450def6a9037, type: 3}
generateWrapperCode: 0
wrapperCodePath:
wrapperClassName:
wrapperCodeNamespace:
+34
View File
@@ -0,0 +1,34 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fcf7219bab7fe46a1ad266029b2fee19, type: 3}
m_Name: Readme
m_EditorClassIdentifier:
icon: {fileID: 2800000, guid: 727a75301c3d24613a3ebcec4a24c2c8, type: 3}
title: URP Empty Template
sections:
- heading: Welcome to the Universal Render Pipeline
text: This template includes the settings and assets you need to start creating with the Universal Render Pipeline.
linkText:
url:
- heading: URP Documentation
text:
linkText: Read more about URP
url: https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@latest
- heading: Forums
text:
linkText: Get answers and support
url: https://forum.unity.com/forums/universal-render-pipeline.383/
- heading: Report bugs
text:
linkText: Submit a report
url: https://unity3d.com/unity/qa/bug-reporting
loadedLayout: 1
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8105016687592461f977c054a80ce2f2
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 0
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8e716445493484943b18d1b34e247068
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: dab0572fcafe87d4b80a838fd874234b
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 09ef50fbbdfb41e4784b699dcffd4c2b
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 7cc4d650e2829344e9ce8611f760fcea
guid: b4ad11828534b3d45b83f18f0b4bbe7f
folderAsset: yes
DefaultImporter:
externalObjects: {}
+88
View File
@@ -0,0 +1,88 @@
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class SongMetadata
{
public string title;
public string artist;
public float bpm;
}
[System.Serializable]
public class BeatSageInfoDat
{
public string _songName;
public string _songAuthorName;
public float _beatsPerMinute;
}
[System.Serializable]
public class BeatSageRoot
{
public string _version;
public List<BeatSageNote> _notes;
}
[System.Serializable]
public class BeatSageNote
{
public float _time;
public int _lineIndex;
public int _lineLayer;
public int _type;
public int _cutDirection;
}
public static class BeatSageConverter
{
private static readonly bool LogConversions = false;
public static List<NoteData> Convert(string rawJson, float bpm)
{
var result = new List<NoteData>();
var root = JsonUtility.FromJson<BeatSageRoot>(rawJson);
if (root?._notes == null)
{
Debug.LogWarning("[BeatSageConverter] Parse failed or no notes.");
return result;
}
foreach (var note in root._notes)
{
// Only process normal notes (0=red, 1=blue); skip bombs (3) etc.
if (note._type != 0 && note._type != 1) continue;
result.Add(new NoteData
{
time = (note._time * 60f) / bpm,
position = note._lineIndex,
lineLayer = note._lineLayer,
colorType = note._type,
cutDirection = note._cutDirection,
});
}
if (LogConversions)
Debug.Log($"[BeatSageConverter] Converted {result.Count} notes.");
return result;
}
public static string ToMapJson(List<NoteData> notes)
{
return JsonUtility.ToJson(new MapData { target = notes }, true);
}
public static SongMetadata ParseInfoDat(string json)
{
var info = JsonUtility.FromJson<BeatSageInfoDat>(json);
if (info == null) return null;
return new SongMetadata
{
title = (info._songName ?? "").Trim(),
artist = (info._songAuthorName ?? "").Trim(),
bpm = info._beatsPerMinute,
};
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1eca2ce555fd76e44ab91d0aea717fad
+320
View File
@@ -0,0 +1,320 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
public class BeatSageUploader : MonoBehaviour
{
private const string BASE_URL = "https://beatsage.com";
private const string CREATE_EP = "/beatsaber_custom_level_create";
private const string HEARTBEAT_EP = "/beatsaber_custom_level_heartbeat/{0}";
private const string DOWNLOAD_EP = "/beatsaber_custom_level_download/{0}";
private const float POLL_INTERVAL = 5f;
private const float POLL_TIMEOUT = 300f;
private static readonly Dictionary<string, string> DiffNames = new()
{
{ "normal", "Normal" },
{ "hard", "Hard" },
{ "expert", "Expert" },
{ "expertplus", "ExpertPlus" },
};
private static readonly Dictionary<string, string> DatFileNames = new()
{
{ "normal", "Normal.dat" },
{ "hard", "Hard.dat" },
{ "expert", "Expert.dat" },
{ "expertplus", "ExpertPlus.dat" },
};
public string CurrentStatus { get; private set; } = "";
public SongMetadata LastMetadata { get; private set; }
// Upload from local file path
public IEnumerator Upload(
string audioPath,
List<string> difficulties,
float bpm,
Action<float> onProgress,
Action<Dictionary<string, List<NoteData>>> onComplete,
Action<string> onError)
{
SetStatus("[1/4] Uploading audio...");
string levelId = null;
yield return CreateLevel(audioPath, difficulties, id => levelId = id, onError);
if (levelId == null) yield break;
onProgress?.Invoke(0.15f);
yield return PollAndDownload(levelId, difficulties, bpm, onProgress, onComplete, onError);
}
// Upload from direct audio URL (Beat Sage downloads it server-side)
public IEnumerator UploadFromUrl(
string audioUrl,
List<string> difficulties,
float bpm,
Action<float> onProgress,
Action<Dictionary<string, List<NoteData>>> onComplete,
Action<string> onError)
{
SetStatus("[1/4] Sending URL to Beat Sage...");
string levelId = null;
yield return CreateLevelFromUrl(audioUrl, difficulties, id => levelId = id, onError);
if (levelId == null) yield break;
onProgress?.Invoke(0.15f);
yield return PollAndDownload(levelId, difficulties, bpm, onProgress, onComplete, onError);
}
// Shared poll + download + convert phase
private IEnumerator PollAndDownload(
string levelId,
List<string> difficulties,
float bpm,
Action<float> onProgress,
Action<Dictionary<string, List<NoteData>>> onComplete,
Action<string> onError)
{
SetStatus("[2/4] Generating beatmap...");
bool ready = false;
float elapsed = 0f;
while (!ready && elapsed < POLL_TIMEOUT)
{
yield return new WaitForSeconds(POLL_INTERVAL);
elapsed += POLL_INTERVAL;
bool error = false;
yield return PollHeartbeat(levelId,
status =>
{
ready = string.Equals(status, "generated", StringComparison.OrdinalIgnoreCase)
|| string.Equals(status, "done", StringComparison.OrdinalIgnoreCase);
error = string.Equals(status, "error", StringComparison.OrdinalIgnoreCase);
},
onError);
if (error) { onError?.Invoke("Beat Sage generation failed (error status)"); yield break; }
onProgress?.Invoke(0.15f + Mathf.Clamp01(elapsed / POLL_TIMEOUT) * 0.6f);
SetStatus($"[2/4] Generating... {(int)elapsed}s elapsed");
}
if (!ready) { onError?.Invoke("Beat Sage timeout (>5 min)"); yield break; }
SetStatus("[3/4] Downloading result...");
byte[] zipBytes = null;
yield return DownloadZip(levelId, b => zipBytes = b, onError);
if (zipBytes == null) yield break;
onProgress?.Invoke(0.9f);
SetStatus("[3/4] Converting map data...");
Dictionary<string, List<NoteData>> maps = null;
try { maps = ExtractAndConvert(zipBytes, difficulties, bpm); }
catch (Exception e) { onError?.Invoke($"Conversion failed: {e.Message}"); yield break; }
onProgress?.Invoke(1f);
SetStatus("[3/4] Conversion complete.");
onComplete?.Invoke(maps);
}
private IEnumerator CreateLevelFromUrl(string audioUrl, List<string> difficulties,
Action<string> onSuccess, Action<string> onError)
{
var mappedDiffs = new List<string>();
foreach (string d in difficulties)
if (DiffNames.TryGetValue(d, out var n)) mappedDiffs.Add(n);
if (mappedDiffs.Count == 0)
{
onError?.Invoke("No supported difficulties selected.");
yield break;
}
var form = new List<IMultipartFormSection>
{
new MultipartFormDataSection("audio_url", audioUrl),
new MultipartFormDataSection("audio_metadata_title", " "),
new MultipartFormDataSection("audio_metadata_artist", " "),
new MultipartFormDataSection("difficulties", string.Join(",", mappedDiffs)),
new MultipartFormDataSection("modes", "Standard"),
new MultipartFormDataSection("events", "DotBlocks,Obstacles,Bombs"),
new MultipartFormDataSection("environment", "DefaultEnvironment"),
new MultipartFormDataSection("system_tag", "v2"),
};
using var req = UnityWebRequest.Post(BASE_URL + CREATE_EP, form);
req.SetRequestHeader("Accept", "*/*");
req.SetRequestHeader("User-Agent", "VRBeatSaber/1.0");
yield return req.SendWebRequest();
if (req.result != UnityWebRequest.Result.Success)
{
onError?.Invoke($"Level create (URL) failed: {req.error}");
yield break;
}
string levelId = ParseJsonString(req.downloadHandler.text, "id");
if (string.IsNullOrEmpty(levelId))
{
onError?.Invoke($"Failed to parse levelId. Response: {req.downloadHandler.text}");
yield break;
}
onSuccess?.Invoke(levelId);
}
private IEnumerator CreateLevel(string audioPath, List<string> difficulties,
Action<string> onSuccess, Action<string> onError)
{
byte[] audioBytes = File.ReadAllBytes(audioPath);
string fileName = Path.GetFileName(audioPath);
var mappedDiffs = new List<string>();
foreach (string d in difficulties)
if (DiffNames.TryGetValue(d, out var n)) mappedDiffs.Add(n);
if (mappedDiffs.Count == 0)
{
onError?.Invoke("No supported difficulties selected (use Normal/Hard/Expert/ExpertPlus).");
yield break;
}
var form = new List<IMultipartFormSection>
{
new MultipartFormFileSection("audio_file", audioBytes, fileName, "audio/mpeg"),
new MultipartFormDataSection("audio_metadata_title", " "),
new MultipartFormDataSection("audio_metadata_artist", " "),
new MultipartFormDataSection("difficulties", string.Join(",", mappedDiffs)),
new MultipartFormDataSection("modes", "Standard"),
new MultipartFormDataSection("events", "DotBlocks,Obstacles,Bombs"),
new MultipartFormDataSection("environment", "DefaultEnvironment"),
new MultipartFormDataSection("system_tag", "v2"),
};
using var req = UnityWebRequest.Post(BASE_URL + CREATE_EP, form);
req.SetRequestHeader("Accept", "*/*");
req.SetRequestHeader("User-Agent", "VRBeatSaber/1.0");
yield return req.SendWebRequest();
if (req.result != UnityWebRequest.Result.Success)
{
onError?.Invoke($"Level create request failed: {req.error}");
yield break;
}
string levelId = ParseJsonString(req.downloadHandler.text, "id");
if (string.IsNullOrEmpty(levelId))
{
onError?.Invoke($"Failed to parse levelId. Response: {req.downloadHandler.text}");
yield break;
}
onSuccess?.Invoke(levelId);
}
private IEnumerator PollHeartbeat(string levelId,
Action<string> onStatus, Action<string> onError)
{
using var req = UnityWebRequest.Get(BASE_URL + string.Format(HEARTBEAT_EP, levelId));
yield return req.SendWebRequest();
if (req.result != UnityWebRequest.Result.Success)
{
onError?.Invoke($"Heartbeat check failed: {req.error}");
yield break;
}
onStatus?.Invoke(ParseJsonString(req.downloadHandler.text, "status") ?? "");
}
private IEnumerator DownloadZip(string levelId,
Action<byte[]> onSuccess, Action<string> onError)
{
string url = BASE_URL + string.Format(DOWNLOAD_EP, levelId);
for (int attempt = 1; attempt <= 3; attempt++)
{
using var req = UnityWebRequest.Get(url);
yield return req.SendWebRequest();
if (req.result == UnityWebRequest.Result.Success)
{
onSuccess?.Invoke(req.downloadHandler.data);
yield break;
}
// 500 오류는 Beat Sage 처리 지연일 수 있으므로 재시도
if (req.responseCode == 500 && attempt < 3)
{
SetStatus($"[3/4] Server error, retrying ({attempt}/3)...");
yield return new WaitForSeconds(5f);
continue;
}
onError?.Invoke($"ZIP download failed: {req.error} (HTTP {req.responseCode})");
yield break;
}
}
private Dictionary<string, List<NoteData>> ExtractAndConvert(
byte[] zipBytes, List<string> difficulties, float fallbackBpm)
{
var result = new Dictionary<string, List<NoteData>>();
using var ms = new MemoryStream(zipBytes);
using var archive = new ZipArchive(ms, ZipArchiveMode.Read);
// Read info.dat first to get auto-detected BPM and metadata
float bpm = fallbackBpm;
foreach (var e in archive.Entries)
{
if (!string.Equals(e.Name, "info.dat", StringComparison.OrdinalIgnoreCase)) continue;
using var r = new StreamReader(e.Open(), Encoding.UTF8);
var meta = BeatSageConverter.ParseInfoDat(r.ReadToEnd());
if (meta != null)
{
LastMetadata = meta;
if (meta.bpm > 0) bpm = meta.bpm;
}
break;
}
foreach (string diff in difficulties)
{
if (!DatFileNames.TryGetValue(diff, out string datName)) continue;
ZipArchiveEntry entry = null;
foreach (var e in archive.Entries)
if (string.Equals(e.Name, datName, StringComparison.OrdinalIgnoreCase))
{ entry = e; break; }
if (entry == null) { Debug.LogWarning($"[BeatSageUploader] {datName} not found — skipped."); continue; }
using var reader = new StreamReader(entry.Open(), Encoding.UTF8);
result[diff] = BeatSageConverter.Convert(reader.ReadToEnd(), bpm);
}
return result;
}
private static string ParseJsonString(string json, string key)
{
string search = $"\"{key}\":\"";
int start = json.IndexOf(search, StringComparison.Ordinal);
if (start < 0) return null;
start += search.Length;
int end = json.IndexOf('"', start);
return end > start ? json.Substring(start, end - start) : null;
}
private void SetStatus(string msg) => CurrentStatus = msg;
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 313c2722c0b3ff845a6d014c821e3660
+110
View File
@@ -0,0 +1,110 @@
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
// Editor/PC-only helper — auto-injects at runtime, no need to place in scene.
// On Quest builds this entire class is stripped.
//
// Features:
// 1. Replaces TrackedDeviceGraphicRaycaster → GraphicRaycaster (enables mouse clicks)
// 2. Keeps worldCamera up to date on all World Space canvases
// 3. ESC key navigates back
public class DesktopUIMode : MonoBehaviour
{
#if !UNITY_ANDROID || UNITY_EDITOR
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
private static void AutoCreate()
{
if (FindFirstObjectByType<DesktopUIMode>() != null) return;
new GameObject("[DesktopUIMode]").AddComponent<DesktopUIMode>();
}
private static readonly System.Collections.Generic.Dictionary<string, string> BackMap =
new()
{
{ "SongSelect", "Menu" },
{ "SongCreator", "Menu" },
{ "MapEditorScene", "SongCreator" },
{ "Game", "SongSelect" },
};
private void Awake()
{
if (FindObjectsByType<DesktopUIMode>(FindObjectsSortMode.None).Length > 1)
{ Destroy(gameObject); return; }
DontDestroyOnLoad(gameObject);
SceneManager.sceneLoaded += OnSceneLoaded;
PatchCanvases();
}
private void OnDestroy() => SceneManager.sceneLoaded -= OnSceneLoaded;
private void OnSceneLoaded(Scene s, LoadSceneMode m) => StartCoroutine(PatchAfterFrame());
private System.Collections.IEnumerator PatchAfterFrame()
{ yield return null; PatchCanvases(); }
private void Update()
{
RefreshCanvasCameras();
if (Keyboard.current?.escapeKey.wasPressedThisFrame == true) GoBack();
}
private static void PatchCanvases()
{
foreach (var canvas in FindObjectsByType<Canvas>(FindObjectsSortMode.None))
{
if (canvas.renderMode != RenderMode.WorldSpace) continue;
var tracked = canvas.GetComponent("TrackedDeviceGraphicRaycaster");
if (tracked != null)
{
DestroyImmediate(tracked);
if (canvas.GetComponent<GraphicRaycaster>() == null)
canvas.gameObject.AddComponent<GraphicRaycaster>();
}
}
RemoveDuplicateAudioListeners();
RefreshCanvasCameras();
}
private static void RemoveDuplicateAudioListeners()
{
var listeners = FindObjectsByType<AudioListener>(FindObjectsSortMode.None);
if (listeners.Length <= 1) return;
AudioListener keep = null;
foreach (var al in listeners)
if (al.gameObject.scene.name != "DontDestroyOnLoad") { keep = al; break; }
keep ??= listeners[0];
foreach (var al in listeners)
if (al != keep) DestroyImmediate(al);
}
private static void RefreshCanvasCameras()
{
Camera cam = Camera.main;
if (cam == null)
foreach (var c in FindObjectsByType<Camera>(FindObjectsSortMode.None))
if (c.enabled && c.gameObject.scene.name != "DontDestroyOnLoad") { cam = c; break; }
cam ??= FindFirstObjectByType<Camera>();
if (cam == null) return;
foreach (var canvas in FindObjectsByType<Canvas>(FindObjectsSortMode.None))
if (canvas.renderMode == RenderMode.WorldSpace && canvas.worldCamera != cam)
canvas.worldCamera = cam;
}
private static void GoBack()
{
if (BackMap.TryGetValue(SceneManager.GetActiveScene().name, out string target))
SceneManager.LoadScene(target);
}
#endif
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c0afc29d40bc9cc4486fc0c8078d2cb7
+211
View File
@@ -0,0 +1,211 @@
using System;
using System.Collections;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
public class DownloadManager : MonoBehaviour
{
[SerializeField] private string baseUrl = "http://whdwo798.synology.me/beatsaber";
private static string CacheRoot => Path.Combine(Application.persistentDataPath, "beatsaber");
private static string LegacyCacheRoot => Path.Combine(Application.temporaryCachePath, "beatsaber");
// ── Public API ───────────────────────────────────────────
public void FetchSongsList(Action<SongsList> onSuccess, Action<string> onError = null)
{
StartCoroutine(GetText($"{baseUrl}/songs.json", json =>
{
SongsList list = JsonUtility.FromJson<SongsList>(json);
if (list == null)
onError?.Invoke("songs.json 파싱 실패");
else
onSuccess?.Invoke(list);
}, onError));
}
public void DownloadSong(SongInfo song, string difficulty,
Action<float> onProgress, Action onComplete, Action<string> onError = null)
{
StartCoroutine(DownloadSongCoroutine(song, difficulty, onProgress, onComplete, onError));
}
public void DeleteSong(string songId)
{
string dir = SongDir(songId);
if (Directory.Exists(dir))
{
Directory.Delete(dir, recursive: true);
Debug.Log($"[DownloadManager] 삭제: {songId}");
}
string legacyDir = LegacySongDir(songId);
if (Directory.Exists(legacyDir))
Directory.Delete(legacyDir, recursive: true);
}
public void DeleteDifficulty(SongInfo song, string difficulty)
{
TryMigrateLegacySong(song.id);
string path = MapPath(song, difficulty);
if (path != null && File.Exists(path))
File.Delete(path);
string songDir = SongDir(song.id);
if (Directory.Exists(songDir) && Directory.GetFileSystemEntries(songDir).Length == 0)
Directory.Delete(songDir);
}
public bool IsSongDownloaded(string songId)
{
TryMigrateLegacySong(songId);
return File.Exists(AudioPath(songId));
}
public bool IsDifficultyDownloaded(SongInfo song, string difficulty)
{
TryMigrateLegacySong(song.id);
string path = MapPath(song, difficulty);
return path != null && File.Exists(path);
}
public string AudioPath(string songId)
=> Path.Combine(SongDir(songId), $"{songId}.mp3");
public string MapPath(SongInfo song, string difficulty)
{
DifficultyInfo info = song.difficulties.Get(difficulty);
if (info == null || string.IsNullOrEmpty(info.mapFile)) return null;
string fileName = Path.GetFileName(info.mapFile);
if (string.IsNullOrEmpty(fileName)) return null;
return Path.Combine(SongDir(song.id), fileName);
}
// ── 내부 구현 ─────────────────────────────────────────────
private IEnumerator DownloadSongCoroutine(SongInfo song, string difficulty,
Action<float> onProgress, Action onComplete, Action<string> onError)
{
TryMigrateLegacySong(song.id);
string songDir = Path.GetFullPath(SongDir(song.id));
Directory.CreateDirectory(songDir);
// 1단계: 오디오 (70%)
string audioPath = Path.Combine(songDir, $"{song.id}.mp3");
if (!File.Exists(audioPath))
{
bool failed = false;
yield return DownloadFile(
$"{baseUrl}/{song.audioFile}", audioPath,
p => onProgress?.Invoke(p * 0.7f),
err => { onError?.Invoke(err); failed = true; });
if (failed) yield break;
}
// 2단계: 맵 파일 (30%)
DifficultyInfo diffInfo = song.difficulties.Get(difficulty);
if (diffInfo == null)
{
onError?.Invoke($"난이도 '{difficulty}' 없음");
yield break;
}
if (string.IsNullOrEmpty(diffInfo.mapFile))
{
onError?.Invoke($"'{difficulty}' 맵 파일 정보 없음 — Creator에서 곡을 다시 생성해주세요");
yield break;
}
string mapPath = MapPath(song, difficulty);
if (mapPath != null) mapPath = Path.GetFullPath(mapPath);
if (mapPath == null)
{
onError?.Invoke($"'{difficulty}' 맵 경로 계산 실패");
yield break;
}
if (!File.Exists(mapPath))
{
bool failed = false;
yield return DownloadFile(
$"{baseUrl}/{diffInfo.mapFile}", mapPath,
p => onProgress?.Invoke(0.7f + p * 0.3f),
err => { onError?.Invoke(err); failed = true; });
if (failed) yield break;
}
onProgress?.Invoke(1f);
onComplete?.Invoke();
Debug.Log($"[DownloadManager] 완료: {song.title} ({difficulty})");
}
private IEnumerator DownloadFile(string url, string savePath,
Action<float> onProgress, Action<string> onError)
{
using var req = UnityWebRequest.Get(url);
req.downloadHandler = new DownloadHandlerFile(savePath);
req.SendWebRequest();
while (!req.isDone)
{
onProgress?.Invoke(req.downloadProgress);
yield return null;
}
if (req.result != UnityWebRequest.Result.Success)
{
if (File.Exists(savePath)) File.Delete(savePath);
onError?.Invoke($"다운로드 실패: {url} — {req.error}");
}
}
private IEnumerator GetText(string url, Action<string> onSuccess, Action<string> onError)
{
using var req = UnityWebRequest.Get(url);
yield return req.SendWebRequest();
if (req.result != UnityWebRequest.Result.Success)
onError?.Invoke($"요청 실패: {url} — {req.error}");
else
onSuccess?.Invoke(req.downloadHandler.text);
}
private static string SongDir(string songId)
=> Path.Combine(CacheRoot, songId);
private static string LegacySongDir(string songId)
=> Path.Combine(LegacyCacheRoot, songId);
private static void TryMigrateLegacySong(string songId)
{
string sourceDir = LegacySongDir(songId);
string targetDir = SongDir(songId);
if (Directory.Exists(targetDir) || !Directory.Exists(sourceDir))
return;
CopyDirectory(sourceDir, targetDir);
Directory.Delete(sourceDir, recursive: true);
Debug.Log($"[DownloadManager] 기존 캐시를 영구 저장소로 이동: {songId}");
}
private static void CopyDirectory(string sourceDir, string targetDir)
{
Directory.CreateDirectory(targetDir);
foreach (string file in Directory.GetFiles(sourceDir))
{
string targetFile = Path.Combine(targetDir, Path.GetFileName(file));
File.Copy(file, targetFile, overwrite: true);
}
foreach (string dir in Directory.GetDirectories(sourceDir))
{
string targetSubDir = Path.Combine(targetDir, Path.GetFileName(dir));
CopyDirectory(dir, targetSubDir);
}
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a8efd2469f7355140ae71425ecc638e0
+130
View File
@@ -0,0 +1,130 @@
using UnityEngine;
using UnityEngine.Video;
public class Game360VideoBackground : MonoBehaviour
{
[SerializeField] private VideoClip videoClip;
[SerializeField] private int renderTextureSize = 2048;
[SerializeField] private bool muteVideoAudio = true;
[SerializeField, Range(0f, 360f)] private float skyboxRotationDegrees = 0f;
[SerializeField, Range(0f, 8f)] private float skyboxExposure = 1f;
private GameObject videoPlayerObject;
private Material skyboxMaterial;
private Material previousSkybox;
private RenderTexture renderTexture;
private VideoPlayer videoPlayer;
private void Awake()
{
if (videoClip == null)
{
Debug.LogWarning("[Game360VideoBackground] videoClip is not assigned.");
return;
}
CreateSkyboxMaterial();
CreateVideoPlayer();
}
private void OnDestroy()
{
if (videoPlayer != null)
{
videoPlayer.prepareCompleted -= OnVideoPrepared;
videoPlayer.errorReceived -= OnVideoError;
}
if (renderTexture != null)
{
renderTexture.Release();
Destroy(renderTexture);
}
RenderSettings.skybox = previousSkybox;
DynamicGI.UpdateEnvironment();
if (skyboxMaterial != null)
Destroy(skyboxMaterial);
if (videoPlayerObject != null)
Destroy(videoPlayerObject);
}
private void CreateSkyboxMaterial()
{
renderTexture = new RenderTexture(renderTextureSize, renderTextureSize / 2, 0, RenderTextureFormat.ARGB32)
{
name = "Game360VideoRenderTexture",
wrapMode = TextureWrapMode.Clamp,
filterMode = FilterMode.Bilinear,
};
renderTexture.Create();
previousSkybox = RenderSettings.skybox;
skyboxMaterial = new Material(ResolveSkyboxShader())
{
name = "Game360VideoMaterial",
};
skyboxMaterial.SetTexture("_MainTex", renderTexture);
skyboxMaterial.SetFloat("_ImageType", 0f);
skyboxMaterial.SetFloat("_Mapping", 0f);
skyboxMaterial.SetFloat("_Layout", 0f);
ApplySkyboxSettings();
RenderSettings.skybox = skyboxMaterial;
DynamicGI.UpdateEnvironment();
}
private void CreateVideoPlayer()
{
videoPlayerObject = new GameObject("[360 Video Skybox Player]");
videoPlayerObject.transform.SetParent(transform, false);
videoPlayer = videoPlayerObject.AddComponent<VideoPlayer>();
videoPlayer.playOnAwake = false;
videoPlayer.isLooping = true;
videoPlayer.waitForFirstFrame = true;
videoPlayer.renderMode = VideoRenderMode.RenderTexture;
videoPlayer.targetTexture = renderTexture;
videoPlayer.clip = videoClip;
videoPlayer.audioOutputMode = muteVideoAudio
? VideoAudioOutputMode.None
: VideoAudioOutputMode.Direct;
videoPlayer.prepareCompleted += OnVideoPrepared;
videoPlayer.errorReceived += OnVideoError;
videoPlayer.Prepare();
}
private static Shader ResolveSkyboxShader()
{
return Shader.Find("Skybox/Panoramic")
?? Shader.Find("Skybox/6 Sided")
?? Shader.Find("Standard");
}
private void OnVideoPrepared(VideoPlayer source)
{
source.Play();
}
private void OnValidate()
{
ApplySkyboxSettings();
}
private void ApplySkyboxSettings()
{
if (skyboxMaterial == null)
return;
skyboxMaterial.SetFloat("_Exposure", skyboxExposure);
skyboxMaterial.SetFloat("_Rotation", skyboxRotationDegrees);
DynamicGI.UpdateEnvironment();
}
private static void OnVideoError(VideoPlayer source, string message)
{
Debug.LogWarning($"[Game360VideoBackground] VideoPlayer error: {message}");
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3e381cd99de84f67b9f83c19a032dc24
+6
View File
@@ -0,0 +1,6 @@
// Static container — passes selected song/difficulty between scenes
public static class GameSession
{
public static SongInfo SelectedSong;
public static string SelectedDifficulty;
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4794ac1142dcc254fa53e2c8d7c1512a
+24
View File
@@ -0,0 +1,24 @@
using UnityEngine;
public static class GlobalSyncSettings
{
private const string AudioOffsetMsKey = "VRBeats.GlobalAudioOffsetMs";
public static float AudioOffsetMs
{
get => PlayerPrefs.GetFloat(AudioOffsetMsKey, 0.0f);
set
{
PlayerPrefs.SetFloat(AudioOffsetMsKey, Mathf.Clamp(value, -300.0f, 300.0f));
PlayerPrefs.Save();
}
}
public static float AudioOffsetSeconds => AudioOffsetMs / 1000.0f;
public static void Reset()
{
PlayerPrefs.DeleteKey(AudioOffsetMsKey);
PlayerPrefs.Save();
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a2e8c518ec2f4a03a6d820774b475ce0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+80
View File
@@ -0,0 +1,80 @@
using System.Collections;
using TMPro;
using UnityEngine;
[RequireComponent(typeof(TMP_Text))]
public class MarqueeText : MonoBehaviour
{
public float speed = 14f;
public float pauseStart = 1.8f;
public float pauseEnd = 0.9f;
private TMP_Text _label;
private RectTransform _rect;
private Coroutine _scrollRoutine;
private void Awake()
{
_label = GetComponent<TMP_Text>();
_rect = GetComponent<RectTransform>();
}
private IEnumerator Start()
{
yield return null;
Refresh();
}
private void OnDisable()
{
StopScrolling();
}
public void Refresh()
{
if (!isActiveAndEnabled || _label == null || _rect == null || transform.parent == null)
return;
StopScrolling();
SetX(0f);
_label.ForceMeshUpdate();
float textW = _label.preferredWidth;
float containerW = ((RectTransform)transform.parent).rect.width;
float dist = textW - containerW;
if (dist > 1f)
_scrollRoutine = StartCoroutine(ScrollLoop(dist));
}
private IEnumerator ScrollLoop(float dist)
{
while (true)
{
SetX(0f);
yield return new WaitForSeconds(pauseStart);
float x = 0f;
while (x > -dist)
{
x = Mathf.MoveTowards(x, -dist, speed * Time.deltaTime);
SetX(x);
yield return null;
}
yield return new WaitForSeconds(pauseEnd);
}
}
private void SetX(float x) =>
_rect.anchoredPosition = new Vector2(x, _rect.anchoredPosition.y);
private void StopScrolling()
{
if (_scrollRoutine == null)
return;
StopCoroutine(_scrollRoutine);
_scrollRoutine = null;
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: aec41476b82385047a8cec63612a6698
+93
View File
@@ -0,0 +1,93 @@
using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class MenuSyncButtonInjector : MonoBehaviour
{
private const string MenuSceneName = "Menu";
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
private static void AutoCreate()
{
if (FindFirstObjectByType<MenuSyncButtonInjector>() != null)
return;
GameObject go = new GameObject("[MenuSyncButtonInjector]");
DontDestroyOnLoad(go);
go.AddComponent<MenuSyncButtonInjector>();
}
private void Awake()
{
SceneManager.sceneLoaded += OnSceneLoaded;
StartCoroutine(InjectAfterFrame());
}
private void OnDestroy()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
}
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
StartCoroutine(InjectAfterFrame());
}
private IEnumerator InjectAfterFrame()
{
yield return null;
if (SceneManager.GetActiveScene().name != MenuSceneName)
yield break;
if (GameObject.Find("SyncButton") != null)
yield break;
Button sourceButton = FindButtonByText("CreateSong");
if (sourceButton == null)
sourceButton = FindButtonByText("음악만들기");
if (sourceButton == null)
yield break;
Button syncButton = Instantiate(sourceButton, sourceButton.transform.parent);
syncButton.gameObject.name = "SyncButton";
foreach (VRBeats.LoadSceneButton loader in syncButton.GetComponents<VRBeats.LoadSceneButton>())
{
loader.enabled = false;
Destroy(loader);
}
syncButton.onClick.RemoveAllListeners();
syncButton.onClick.AddListener(SyncCalibrationOverlay.Open);
RectTransform sourceRect = sourceButton.GetComponent<RectTransform>();
RectTransform syncRect = syncButton.GetComponent<RectTransform>();
syncRect.anchoredPosition = sourceRect.anchoredPosition + new Vector2(0.0f, -22.0f);
TextMeshProUGUI tmp = syncButton.GetComponentInChildren<TextMeshProUGUI>(true);
if (tmp != null)
tmp.text = "SYNC";
Text text = syncButton.GetComponentInChildren<Text>(true);
if (text != null)
text.text = "SYNC";
}
private static Button FindButtonByText(string text)
{
foreach (Button button in FindObjectsByType<Button>(FindObjectsSortMode.None))
{
TextMeshProUGUI tmp = button.GetComponentInChildren<TextMeshProUGUI>(true);
if (tmp != null && tmp.text.Trim() == text)
return button;
Text legacyText = button.GetComponentInChildren<Text>(true);
if (legacyText != null && legacyText.text.Trim() == text)
return button;
}
return null;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: efc5a20a7b4749bfb60d95ac0f0b2180
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+290
View File
@@ -0,0 +1,290 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using UnityEngine;
using UnityEngine.Networking;
public class NasPublisher : MonoBehaviour
{
[Header("NAS Connection")]
[SerializeField] private string nasBaseUrl = "http://192.168.55.3:5000";
[SerializeField] private string nasAccount = "admin";
[SerializeField] private string nasRootPath = "/web/beatsaber";
[Header("Static Server URL (for reading songs.json)")]
[SerializeField] private string staticBaseUrl = "http://whdwo798.synology.me/beatsaber";
private string _sid = "";
private string _synoToken = "";
private string _password = "";
private void Awake() => LoadConfig();
private void LoadConfig()
{
string path = Path.Combine(Application.streamingAssetsPath, "nas_config.json");
if (!File.Exists(path))
{
NormalizeSettings();
return;
}
var cfg = JsonUtility.FromJson<NasConfig>(File.ReadAllText(path));
if (cfg == null) return;
_password = cfg.password ?? "";
if (!string.IsNullOrWhiteSpace(cfg.host)) nasBaseUrl = cfg.host.Trim();
if (!string.IsNullOrWhiteSpace(cfg.account)) nasAccount = cfg.account.Trim();
if (!string.IsNullOrWhiteSpace(cfg.rootPath)) nasRootPath = cfg.rootPath.Trim();
if (!string.IsNullOrWhiteSpace(cfg.staticUrl)) staticBaseUrl = cfg.staticUrl.Trim();
NormalizeSettings();
}
[Serializable] private class NasConfig
{
public string host = "";
public string account = "";
public string rootPath = "";
public string staticUrl = "";
public string password = "";
}
public IEnumerator Publish(
SongInfo song,
string audioPath,
Dictionary<string, List<NoteData>> maps,
Action<float> onProgress,
Action onComplete,
Action<string> onError)
{
NormalizeSettings();
bool failed = false;
void OnErr(string e) { onError?.Invoke(e); failed = true; }
yield return Login(OnErr);
if (string.IsNullOrEmpty(_sid)) yield break;
onProgress?.Invoke(0.1f);
if (!string.IsNullOrEmpty(audioPath))
{
yield return UploadFile(audioPath, $"{nasRootPath}/music", $"{song.id}.mp3", OnErr);
if (failed) { yield return Logout(); yield break; }
}
onProgress?.Invoke(0.4f);
int total = maps.Count, done = 0;
foreach (var kv in maps)
{
string fileName = $"Map_{song.id}_{kv.Key}.json";
byte[] bytes = Encoding.UTF8.GetBytes(BeatSageConverter.ToMapJson(kv.Value));
AssignMapFile(song, kv.Key, fileName);
yield return UploadBytes(bytes, fileName, $"{nasRootPath}/maps", OnErr);
if (failed) { yield return Logout(); yield break; }
done++;
onProgress?.Invoke(0.4f + (float)done / total * 0.3f);
}
yield return PatchSongsJson(song, OnErr);
if (failed) { yield return Logout(); yield break; }
onProgress?.Invoke(0.95f);
yield return Logout();
onProgress?.Invoke(1f);
onComplete?.Invoke();
Debug.Log($"[NasPublisher] Upload complete: '{song.title}'");
}
private IEnumerator Login(Action<string> onError)
{
if (string.IsNullOrWhiteSpace(_password))
{
onError?.Invoke("NAS password missing. Create Assets/StreamingAssets/nas_config.json with host, account, rootPath, staticUrl, and password.");
yield break;
}
string url = $"{nasBaseUrl}/webapi/auth.cgi" +
$"?api=SYNO.API.Auth&version=6&method=login" +
$"&account={UnityWebRequest.EscapeURL(nasAccount)}" +
$"&passwd={UnityWebRequest.EscapeURL(_password)}" +
$"&session=FileStation&format=sid&enable_syno_token=yes";
UnityWebRequest req;
try
{
req = UnityWebRequest.Get(url);
}
catch (UriFormatException e)
{
onError?.Invoke($"DSM login URL invalid: '{nasBaseUrl}' — {e.Message}");
yield break;
}
string resp;
using (req)
{
yield return req.SendWebRequest();
if (req.result != UnityWebRequest.Result.Success)
{
onError?.Invoke($"DSM login failed: {req.error}");
yield break;
}
resp = req.downloadHandler.text;
_sid = ParseJsonString(resp, "sid");
_synoToken = ParseJsonString(resp, "synotoken");
}
if (string.IsNullOrEmpty(_sid))
onError?.Invoke($"DSM sid parse failed. Check NAS account/password/permissions. Response: {Shorten(resp)}");
}
private IEnumerator Logout()
{
NormalizeSettings();
string url = $"{nasBaseUrl}/webapi/auth.cgi" +
$"?api=SYNO.API.Auth&version=1&method=logout&session=FileStation&_sid={_sid}";
using var req = UnityWebRequest.Get(url);
yield return req.SendWebRequest();
_sid = "";
}
private IEnumerator UploadFile(string localPath, string nasFolder,
string fileName, Action<string> onError)
{
yield return UploadBytes(File.ReadAllBytes(localPath), fileName, nasFolder, onError);
}
private IEnumerator UploadBytes(byte[] bytes, string fileName,
string nasFolder, Action<string> onError)
{
NormalizeSettings();
string uploadUrl = $"{nasBaseUrl}/webapi/entry.cgi" +
$"?api=SYNO.FileStation.Upload&version=2&method=upload" +
$"&_sid={UnityWebRequest.EscapeURL(_sid)}";
string boundary = Guid.NewGuid().ToString("N");
const string CRLF = "\r\n";
using var body = new MemoryStream();
void WriteText(string s) { var b = Encoding.UTF8.GetBytes(s); body.Write(b, 0, b.Length); }
void WriteField(string name, string value)
{
WriteText($"--{boundary}{CRLF}");
WriteText($"Content-Disposition: form-data; name=\"{name}\"{CRLF}{CRLF}");
WriteText(value + CRLF);
}
WriteField("path", nasFolder);
WriteField("create_parents", "true");
WriteField("overwrite", "true");
WriteText($"--{boundary}{CRLF}");
WriteText($"Content-Disposition: form-data; name=\"file\"; filename=\"{fileName}\"{CRLF}");
WriteText($"Content-Type: application/octet-stream{CRLF}{CRLF}");
body.Write(bytes, 0, bytes.Length);
WriteText(CRLF + $"--{boundary}--{CRLF}");
using var req = new UnityWebRequest(uploadUrl, "POST");
req.uploadHandler = new UploadHandlerRaw(body.ToArray());
req.downloadHandler = new DownloadHandlerBuffer();
req.SetRequestHeader("Content-Type", $"multipart/form-data; boundary={boundary}");
if (!string.IsNullOrEmpty(_synoToken))
req.SetRequestHeader("X-SYNO-TOKEN", _synoToken);
yield return req.SendWebRequest();
if (req.result != UnityWebRequest.Result.Success)
{
onError?.Invoke($"Upload failed ({fileName}): {req.error}");
yield break;
}
if (req.downloadHandler.text.Contains("\"success\":false"))
onError?.Invoke($"Upload rejected ({fileName}): {req.downloadHandler.text}");
}
private IEnumerator PatchSongsJson(SongInfo newSong, Action<string> onError)
{
NormalizeSettings();
SongsList list = null;
using (var req = UnityWebRequest.Get($"{staticBaseUrl}/songs.json"))
{
yield return req.SendWebRequest();
if (req.result == UnityWebRequest.Result.Success)
list = JsonUtility.FromJson<SongsList>(req.downloadHandler.text);
}
list ??= new SongsList { version = "1.0", songs = new List<SongInfo>() };
int idx = list.songs.FindIndex(s => s.id == newSong.id);
if (idx >= 0) list.songs[idx] = newSong;
else list.songs.Add(newSong);
yield return UploadBytes(
Encoding.UTF8.GetBytes(JsonUtility.ToJson(list, true)),
"songs.json", nasRootPath, onError);
}
private static string ParseJsonString(string json, string key)
{
if (string.IsNullOrEmpty(json) || string.IsNullOrEmpty(key))
return null;
Match match = Regex.Match(
json,
$"\"{Regex.Escape(key)}\"\\s*:\\s*\"(?<value>(?:\\\\.|[^\"])*)\"");
return match.Success
? Regex.Unescape(match.Groups["value"].Value)
: null;
}
private static void AssignMapFile(SongInfo song, string diff, string fileName)
{
var info = song.difficulties.Get(diff);
if (info != null) info.mapFile = $"maps/{fileName}";
}
private void OnValidate()
{
NormalizeSettings();
}
private void NormalizeSettings()
{
nasBaseUrl = NormalizeBaseUrl(nasBaseUrl);
staticBaseUrl = NormalizeBaseUrl(staticBaseUrl);
nasAccount = nasAccount?.Trim() ?? "";
nasRootPath = NormalizeRootPath(nasRootPath);
}
private static string NormalizeBaseUrl(string value)
{
return (value ?? "").Trim().TrimEnd('/');
}
private static string NormalizeRootPath(string value)
{
value = (value ?? "").Trim().Replace('\\', '/');
if (string.IsNullOrEmpty(value))
return "/";
return value.StartsWith("/") ? value.TrimEnd('/') : "/" + value.TrimEnd('/');
}
private static string Shorten(string value, int maxLength = 240)
{
if (string.IsNullOrEmpty(value) || value.Length <= maxLength)
return value ?? "";
return value.Substring(0, maxLength) + "...";
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2379e0d70040c994089638264e6e9934
+80
View File
@@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using UnityEngine;
[Serializable]
public class NoteData
{
public float time;
public int position; // column 0-3
public int lineLayer; // row 0-2
public int colorType; // 0=red, 1=blue
public int cutDirection; // 0-8 (see Beat Saber spec)
}
[Serializable]
public class MapData
{
public List<NoteData> target;
public ForcedResultData forcedResult;
}
[Serializable]
public class ForcedResultData
{
public bool enabled;
public int totalNotes;
public int perfect;
public int great;
public int good;
public int miss;
public int maxCombo;
}
[Serializable]
public class SongsList
{
public string version;
public List<SongInfo> songs;
}
[Serializable]
public class SongInfo
{
public string id;
public string title;
public string artist;
public float bpm;
public int duration;
public string audioFile;
public long audioSize;
public string coverImage;
public DifficultyMap difficulties;
public string addedAt;
}
[Serializable]
public class DifficultyMap
{
public DifficultyInfo normal;
public DifficultyInfo hard;
public DifficultyInfo expert;
public DifficultyInfo expertplus;
public DifficultyInfo Get(string key) => key switch
{
"normal" => normal,
"hard" => hard,
"expert" => expert,
"expertplus" => expertplus,
_ => null
};
}
[Serializable]
public class DifficultyInfo
{
public string mapFile;
public long mapSize;
public int noteCount;
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 16355c5f50bd642439e8ce4f61be6b92
+234
View File
@@ -0,0 +1,234 @@
using System.Collections;
using System.Collections.Generic;
using System.IO;
using TMPro;
using UnityEngine;
using UnityEngine.Networking;
using VRBeats;
using VRBeats.ScriptableEvents;
public class SongController : MonoBehaviour
{
[SerializeField] private Spawneable cubePrefab;
[SerializeField] private GameEvent onLevelComplete;
[SerializeField] private TMP_Text countdownText;
private const float LaneSpacing = 0.42f;
private const float LayerSpacing = 0.34f;
private const float HorizontalCenter = 1.5f;
private const float VerticalCenter = 1f;
private const float VerticalOffset = 0.22f;
private AudioManager _audio;
private ScoreManager _scoreManager;
private float _clipLength;
private static string CacheRoot =>
Path.Combine(Application.persistentDataPath, "beatsaber");
private void Start()
{
_audio = FindFirstObjectByType<AudioManager>();
_scoreManager = FindFirstObjectByType<ScoreManager>();
StartCoroutine(LoadAndPlay());
}
private void Update()
{
if (_audio != null && _scoreManager != null && _clipLength > 0.0f)
_scoreManager.SetSongProgress(_audio.CurrentTime, _clipLength);
}
private IEnumerator LoadAndPlay()
{
SongInfo song = GameSession.SelectedSong;
string diff = GameSession.SelectedDifficulty;
if (song == null || string.IsNullOrEmpty(diff))
{
Debug.LogError("[SongController] No song/difficulty selected");
yield break;
}
// Load audio clip from local cache
string audioPath = Path.Combine(CacheRoot, song.id, song.id + ".mp3");
AudioClip clip;
using (var req = UnityWebRequestMultimedia.GetAudioClip("file://" + audioPath, AudioType.MPEG))
{
yield return req.SendWebRequest();
if (req.result != UnityWebRequest.Result.Success)
{
Debug.LogError($"[SongController] Audio load failed: {req.error}");
yield break;
}
clip = DownloadHandlerAudioClip.GetContent(req);
}
_clipLength = clip.length;
// Load and parse map
DifficultyInfo diffInfo = song.difficulties.Get(diff);
if (diffInfo == null)
{
Debug.LogError($"[SongController] Difficulty '{diff}' not found");
yield break;
}
string mapPath = Path.Combine(CacheRoot, song.id, Path.GetFileName(diffInfo.mapFile));
if (!File.Exists(mapPath))
{
Debug.LogError($"[SongController] Map file missing: {mapPath}");
yield break;
}
MapData map = JsonUtility.FromJson<MapData>(File.ReadAllText(mapPath));
if (map == null)
{
Debug.LogError("[SongController] Map parse failed");
yield break;
}
if (map.target == null)
map.target = new List<NoteData>();
if (IsForcedResultMap(map))
{
_scoreManager?.SetTotalNotes(Mathf.Max(0, map.forcedResult.totalNotes));
yield return StartCoroutine(Countdown());
_audio.PlayClip(clip);
yield return new WaitForSeconds(Mathf.Min(Mathf.Max(0.2f, _clipLength), 0.75f));
_scoreManager?.ApplyForcedResult(
map.forcedResult.totalNotes,
map.forcedResult.perfect,
map.forcedResult.great,
map.forcedResult.good,
map.forcedResult.miss,
map.forcedResult.maxCombo);
_scoreManager?.CompleteSong();
onLevelComplete?.Invoke();
yield break;
}
map.target.Sort(CompareNotes);
if (_clipLength <= 0.0f)
{
float lastNoteTime = map.target.Count > 0 ? map.target[map.target.Count - 1].time : 0.0f;
_clipLength = Mathf.Max(song.duration, lastNoteTime + 1.0f);
}
_scoreManager?.SetTotalNotes(map.target.Count);
yield return StartCoroutine(Countdown());
_audio.PlayClip(clip);
StartCoroutine(SpawnRoutine(map.target));
yield return StartCoroutine(WaitForCompletion(_clipLength, map.target));
}
private IEnumerator Countdown()
{
if (countdownText == null) yield break;
countdownText.gameObject.SetActive(true);
string[] labels = { "3", "2", "1", "GO!" };
float[] durations = { 1f, 1f, 1f, 0.6f };
for (int i = 0; i < labels.Length; i++)
{
countdownText.text = labels[i];
yield return new WaitForSeconds(durations[i]);
}
countdownText.gameObject.SetActive(false);
}
private IEnumerator SpawnRoutine(List<NoteData> notes)
{
float travelTime = VR_BeatManager.instance.GameSettings.TargetTravelTime;
foreach (NoteData note in notes)
{
float adjustedNoteTime = note.time + GlobalSyncSettings.AudioOffsetSeconds;
float spawnAt = Mathf.Max(0f, adjustedNoteTime - travelTime);
yield return new WaitUntil(() => _audio.CurrentTime >= spawnAt);
SpawnNote(note);
}
}
private void SpawnNote(NoteData note)
{
float x = MapLaneX(note.position);
float y = MapLayerY(note.lineLayer);
// 스폰 시점의 실제 남은 시간으로 역산 → 동시 노트가 프레임 차이 나도 같은 타이밍에 도착
float remaining = note.time + GlobalSyncSettings.AudioOffsetSeconds - _audio.CurrentTime;
float travelTime = Mathf.Max(0.05f, remaining);
var info = new SpawnEventInfo
{
position = new Vector3(x, y, 0f),
colorSide = note.colorType == 0 ? ColorSide.Left : ColorSide.Right,
hitDirection = MapCutDirection(note.cutDirection),
useSpark = false,
speed = 2f,
travelTimeOverride = travelTime,
};
VR_BeatManager.instance.Spawn(cubePrefab, info);
}
private static int CompareNotes(NoteData a, NoteData b)
{
int timeCompare = a.time.CompareTo(b.time);
if (timeCompare != 0)
return timeCompare;
int positionCompare = a.position.CompareTo(b.position);
if (positionCompare != 0)
return positionCompare;
return a.lineLayer.CompareTo(b.lineLayer);
}
private static bool IsForcedResultMap(MapData map)
=> map?.forcedResult != null && map.forcedResult.enabled;
private static float MapLaneX(int position)
{
int lane = Mathf.Clamp(position, 0, 3);
return (lane - HorizontalCenter) * LaneSpacing;
}
private static float MapLayerY(int lineLayer)
{
int layer = Mathf.Clamp(lineLayer, 0, 2);
return VerticalOffset + (layer - VerticalCenter) * LayerSpacing;
}
// Beat Saber cutDirection → VRBeats Direction
// BS: 0=Up 1=Down 2=Left 3=Right 4=UpperLeft 5=UpperRight 6=LowerLeft 7=LowerRight 8=Any
private static readonly Direction[] CutDirMap =
{
Direction.Up,
Direction.Down,
Direction.Left,
Direction.Right,
Direction.UpperLeft,
Direction.UpperRight,
Direction.LowerLeft,
Direction.LowerRight,
Direction.Center,
};
private static Direction MapCutDirection(int cut)
=> (cut >= 0 && cut < CutDirMap.Length) ? CutDirMap[cut] : Direction.Center;
private IEnumerator WaitForCompletion(float clipLength, List<NoteData> notes)
{
float lastNoteTime = notes.Count > 0 ? notes[notes.Count - 1].time : 0.0f;
float resultTime = Mathf.Min(clipLength, lastNoteTime + GlobalSyncSettings.AudioOffsetSeconds + 0.35f);
yield return new WaitUntil(() => _audio.CurrentTime >= resultTime);
yield return new WaitForSeconds(0.35f);
_scoreManager?.CompleteSong();
onLevelComplete?.Invoke();
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ca3b401467e927148b0face4c03b0062
+424
View File
@@ -0,0 +1,424 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using TMPro;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class SongCreatorManager : MonoBehaviour
{
[Header("Audio Source")]
[SerializeField] private TMP_Dropdown audioDropdown;
[SerializeField] private Button refreshBtn;
[SerializeField] private TMP_Text inputPathHint;
[Header("Audio — Local File")]
[SerializeField] private Button filePickerBtn;
[SerializeField] private TMP_Text addStatusText;
[Header("Audio — URL")]
[SerializeField] private TMP_InputField urlInput;
[SerializeField] private Button urlDownloadBtn;
[Header("Metadata")]
[SerializeField] private TMP_InputField titleInput;
[SerializeField] private TMP_InputField artistInput;
[SerializeField] private TMP_InputField bpmInput;
[Header("Difficulty")]
[SerializeField] private Toggle toggleNormal;
[SerializeField] private Toggle toggleHard;
[SerializeField] private Toggle toggleExpert;
[SerializeField] private Toggle toggleExpertPlus;
[Header("Actions")]
[SerializeField] private Button generateButton;
[SerializeField] private Button manualEditorButton;
[SerializeField] private Button backButton;
[SerializeField] private string menuSceneName = "Menu";
[Header("Progress")]
[SerializeField] private GameObject progressGroup;
[SerializeField] private TMP_Text statusText;
[SerializeField] private Slider progressSlider;
[Header("References")]
[SerializeField] private BeatSageUploader beatSageUploader;
[SerializeField] private NasPublisher nasPublisher;
private static readonly Color NeonBg = new Color(0.05f, 0.82f, 0.95f, 0.42f);
private static readonly Color DarkButtonBg = new Color(0.09f, 0.22f, 0.27f, 0.66f);
private static readonly Color DisabledBg = new Color(0.06f, 0.12f, 0.15f, 0.48f);
private static readonly Color ButtonText = new Color(0.92f, 1.0f, 1.0f, 1.0f);
private static readonly Color MutedText = new Color(0.66f, 0.80f, 0.84f, 0.76f);
private static readonly Color NeonOutline = new Color(0.25f, 0.96f, 1.0f, 0.42f);
private static string InputPath =>
Path.Combine(Application.persistentDataPath, "input");
private readonly List<string> audioFiles = new();
private string _pendingFilePath;
private void OnValidate()
{
ApplyButtonStyles();
}
private void Start()
{
ApplyButtonStyles();
Directory.CreateDirectory(InputPath);
if (inputPathHint != null)
inputPathHint.text = $"Path: {InputPath}";
refreshBtn?.onClick.AddListener(RefreshAudioList);
generateButton?.onClick.AddListener(OnGenerateClicked);
backButton?.onClick.AddListener(() => SceneManager.LoadScene(menuSceneName));
filePickerBtn?.onClick.AddListener(OnFilePickerClicked);
urlDownloadBtn?.onClick.AddListener(OnUrlDownloadClicked);
if (progressGroup != null) progressGroup.SetActive(false);
RefreshAudioList();
}
private void Update()
{
if (_pendingFilePath != null)
{
CopyToInput(_pendingFilePath);
_pendingFilePath = null;
}
}
private void RefreshAudioList()
{
audioFiles.Clear();
audioDropdown?.ClearOptions();
var options = new List<string>();
foreach (string f in Directory.GetFiles(InputPath, "*.mp3"))
{
audioFiles.Add(f);
options.Add(Path.GetFileNameWithoutExtension(f));
}
if (options.Count == 0) options.Add("-- no .mp3 files --");
audioDropdown?.AddOptions(options);
}
private void OnGenerateClicked()
{
string directUrl = urlInput != null ? urlInput.text.Trim() : "";
bool hasUrl = !string.IsNullOrEmpty(directUrl);
bool hasFile = audioFiles.Count > 0;
if (!hasUrl && !hasFile) { SetStatus("No audio source. Add a file or enter a URL."); return; }
// BPM input is optional — Beat Sage auto-detects from audio; use as fallback only
float.TryParse(bpmInput?.text, out float bpmHint);
var diffs = new List<string> { "normal", "hard", "expert", "expertplus" };
if (hasUrl)
{
if (!Uri.TryCreate(directUrl, UriKind.Absolute, out var uri) ||
(uri.Scheme != "http" && uri.Scheme != "https"))
{ SetStatus("Invalid URL."); return; }
StartCoroutine(GenerateFlowFromUrl(uri.AbsoluteUri, bpmHint, diffs));
}
else
{
StartCoroutine(GenerateFlow(audioFiles[audioDropdown.value], bpmHint, diffs));
}
}
private IEnumerator GenerateFlowFromUrl(string audioUrl, float bpm, List<string> diffs)
{
SetInteractable(false);
if (progressGroup != null) progressGroup.SetActive(true);
Dictionary<string, List<NoteData>> maps = null;
bool failed = false;
yield return beatSageUploader.UploadFromUrl(
audioUrl, diffs, bpm,
onProgress: p =>
{
if (progressSlider != null) progressSlider.value = p * 0.8f;
SetStatus($"{beatSageUploader.CurrentStatus} ({(int)(p * 80)}%)");
},
onComplete: result => maps = result,
onError: err => { SetStatus($"Error: {err}"); failed = true; });
if (failed) { SetInteractable(true); yield break; }
SongInfo song = BuildSongInfo(audioUrl, bpm, maps);
yield return nasPublisher.Publish(
song, null, maps,
onProgress: p =>
{
if (progressSlider != null) progressSlider.value = 0.8f + p * 0.2f;
SetStatus($"[4/4] Uploading to NAS... ({(int)((0.8f + p * 0.2f) * 100)}%)");
},
onComplete: () =>
{
if (progressSlider != null) progressSlider.value = 1f;
SetStatus($"Done! '{song.title}' created successfully.");
},
onError: err => { SetStatus($"NAS upload failed: {err}"); failed = true; });
SetInteractable(true);
}
private IEnumerator GenerateFlow(string audioPath, float bpm, List<string> diffs)
{
SetInteractable(false);
if (progressGroup != null) progressGroup.SetActive(true);
Dictionary<string, List<NoteData>> maps = null;
bool failed = false;
yield return beatSageUploader.Upload(
audioPath, diffs, bpm,
onProgress: p =>
{
if (progressSlider != null) progressSlider.value = p * 0.8f;
SetStatus($"{beatSageUploader.CurrentStatus} ({(int)(p * 80)}%)");
},
onComplete: result => maps = result,
onError: err => { SetStatus($"Error: {err}"); failed = true; });
if (failed) { SetInteractable(true); yield break; }
SongInfo song = BuildSongInfo(audioPath, bpm, maps);
yield return nasPublisher.Publish(
song, audioPath, maps,
onProgress: p =>
{
if (progressSlider != null) progressSlider.value = 0.8f + p * 0.2f;
SetStatus($"[4/4] Uploading to NAS... ({(int)((0.8f + p * 0.2f) * 100)}%)");
},
onComplete: () =>
{
if (progressSlider != null) progressSlider.value = 1f;
SetStatus($"Done! '{song.title}' created successfully.");
},
onError: err => { SetStatus($"NAS upload failed: {err}"); failed = true; });
SetInteractable(true);
}
private SongInfo BuildSongInfo(string audioPath, float fallbackBpm,
Dictionary<string, List<NoteData>> maps)
{
// Prefer values from info.dat (auto-detected by Beat Sage); UI inputs override if non-empty
var meta = beatSageUploader != null ? beatSageUploader.LastMetadata : null;
string uiTitle = titleInput?.text.Trim() ?? "";
string uiArtist = artistInput?.text.Trim() ?? "";
float.TryParse(bpmInput?.text, out float uiBpm);
string title = !string.IsNullOrEmpty(uiTitle) ? uiTitle : (meta?.title ?? "");
string artist = !string.IsNullOrEmpty(uiArtist) ? uiArtist : (meta?.artist ?? "");
float bpm = (meta != null && meta.bpm > 0) ? meta.bpm : (uiBpm > 0 ? uiBpm : fallbackBpm);
// Fallback id from filename if title is still empty
if (string.IsNullOrEmpty(title) && !string.IsNullOrEmpty(audioPath))
title = Path.GetFileNameWithoutExtension(audioPath);
if (string.IsNullOrEmpty(title))
title = $"song_{DateTime.Now:yyyyMMdd_HHmmss}";
string id = title.ToLower().Replace(" ", "_");
var diffMap = new DifficultyMap();
foreach (var kv in maps)
{
var info = new DifficultyInfo { noteCount = kv.Value.Count };
switch (kv.Key)
{
case "normal": diffMap.normal = info; break;
case "hard": diffMap.hard = info; break;
case "expert": diffMap.expert = info; break;
case "expertplus": diffMap.expertplus = info; break;
}
}
return new SongInfo
{
id = id,
title = title,
artist = artist,
bpm = bpm,
audioFile = $"music/{id}.mp3",
difficulties = diffMap,
addedAt = DateTime.Now.ToString("yyyy-MM-dd"),
};
}
private void SetStatus(string msg) { if (statusText != null) statusText.text = msg; }
private void SetInteractable(bool value)
{
if (generateButton != null) generateButton.interactable = value;
if (audioDropdown != null) audioDropdown.interactable = value;
if (refreshBtn != null) refreshBtn.interactable = value;
if (filePickerBtn != null) filePickerBtn.interactable = value;
if (urlDownloadBtn != null) urlDownloadBtn.interactable = value;
ApplyButtonStyles();
}
private void OnFilePickerClicked()
{
#if UNITY_EDITOR
string path = UnityEditor.EditorUtility.OpenFilePanel("Select audio file", "", "mp3");
if (!string.IsNullOrEmpty(path)) CopyToInput(path);
#elif UNITY_STANDALONE_WIN
var t = new Thread(() =>
{
var dlg = new System.Windows.Forms.OpenFileDialog { Filter = "MP3|*.mp3" };
if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
_pendingFilePath = dlg.FileName;
});
t.SetApartmentState(ApartmentState.STA);
t.Start();
#else
SetAddStatus($"Copy file via ADB:\n{InputPath}");
#endif
}
private void CopyToInput(string srcPath)
{
try
{
string dest = Path.Combine(InputPath, Path.GetFileName(srcPath));
File.Copy(srcPath, dest, overwrite: true);
RefreshAudioList();
string nameNoExt = Path.GetFileNameWithoutExtension(srcPath);
int idx = audioFiles.FindIndex(f => Path.GetFileNameWithoutExtension(f) == nameNoExt);
if (idx >= 0 && audioDropdown != null) audioDropdown.value = idx;
SetAddStatus($"Added: {Path.GetFileName(srcPath)}");
}
catch (Exception e) { SetAddStatus($"File copy failed: {e.Message}"); }
}
private void OnUrlDownloadClicked()
{
string url = urlInput != null ? urlInput.text.Trim() : "";
if (string.IsNullOrEmpty(url)) { SetAddStatus("Please enter a URL."); return; }
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) ||
(uri.Scheme != "http" && uri.Scheme != "https"))
{
SetAddStatus($"Invalid URL: must start with http:// or https://");
return;
}
// Streaming service URLs cannot be downloaded directly
string host = uri.Host.ToLower();
if (host.Contains("youtube.com") || host.Contains("youtu.be") ||
host.Contains("spotify.com") || host.Contains("soundcloud.com") ||
host.Contains("music.apple.com"))
{
SetAddStatus("Streaming URLs not supported.\nUse a direct .mp3 download link or Browse File.");
return;
}
StartCoroutine(DownloadFromUrl(uri.AbsoluteUri));
}
private IEnumerator DownloadFromUrl(string url)
{
SetAddStatus("Downloading...");
if (urlDownloadBtn != null) urlDownloadBtn.interactable = false;
ApplyButtonStyles();
string fileName;
try
{
string uriPath = new Uri(url).AbsolutePath;
fileName = Path.GetFileName(uriPath);
if (string.IsNullOrEmpty(fileName) || !fileName.EndsWith(".mp3", StringComparison.OrdinalIgnoreCase))
fileName = "download.mp3";
}
catch { fileName = "download.mp3"; }
string savePath = Path.GetFullPath(Path.Combine(InputPath, fileName));
using var req = UnityWebRequest.Get(url);
req.downloadHandler = new DownloadHandlerFile(savePath);
yield return req.SendWebRequest();
if (urlDownloadBtn != null) urlDownloadBtn.interactable = true;
ApplyButtonStyles();
if (req.result == UnityWebRequest.Result.Success)
{
RefreshAudioList();
int idx = audioFiles.FindIndex(
f => Path.GetFileNameWithoutExtension(f) == Path.GetFileNameWithoutExtension(fileName));
if (idx >= 0 && audioDropdown != null) audioDropdown.value = idx;
SetAddStatus($"Downloaded: {fileName}");
}
else
{
if (File.Exists(savePath)) File.Delete(savePath);
SetAddStatus($"Download failed: {req.error}");
}
}
private void SetAddStatus(string msg) { if (addStatusText != null) addStatusText.text = msg; }
private void ApplyButtonStyles()
{
ApplyCreatorButtonStyle(generateButton, true);
ApplyCreatorButtonStyle(urlDownloadBtn, true);
ApplyCreatorButtonStyle(refreshBtn, false);
ApplyCreatorButtonStyle(filePickerBtn, false);
ApplyCreatorButtonStyle(backButton, false);
}
private static void ApplyCreatorButtonStyle(Button btn, bool primary)
{
if (btn == null)
return;
Color bg = btn.interactable ? (primary ? NeonBg : DarkButtonBg) : DisabledBg;
if (btn.targetGraphic is Image img)
{
img.color = bg;
img.raycastTarget = true;
}
var colors = btn.colors;
colors.normalColor = bg;
colors.highlightedColor = btn.interactable
? new Color(0.10f, 0.95f, 1.0f, primary ? 0.58f : 0.48f)
: DisabledBg;
colors.pressedColor = btn.interactable
? new Color(0.02f, 0.58f, 0.72f, 0.80f)
: DisabledBg;
colors.selectedColor = colors.highlightedColor;
colors.disabledColor = DisabledBg;
colors.fadeDuration = 0.08f;
btn.colors = colors;
TMP_Text label = btn.GetComponentInChildren<TMP_Text>(true);
if (label != null)
{
label.color = btn.interactable ? ButtonText : MutedText;
label.raycastTarget = false;
}
Outline outline = btn.GetComponent<Outline>() ?? btn.gameObject.AddComponent<Outline>();
outline.enabled = btn.interactable;
outline.effectColor = NeonOutline;
outline.effectDistance = new Vector2(0.0f, -0.28f);
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ad9984c644076724bb5507e3e9e73ed5
+384
View File
@@ -0,0 +1,384 @@
using System;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class SongDetailPanel : MonoBehaviour
{
[Header("곡 정보")]
[SerializeField] private TMP_Text titleText;
[SerializeField] private TMP_Text artistText;
[SerializeField] private TMP_Text infoText;
[Header("난이도 버튼")]
[SerializeField] private Button btnNormal;
[SerializeField] private Button btnHard;
[SerializeField] private Button btnExpert;
[SerializeField] private Button btnExpertPlus;
[Header("액션 버튼")]
[SerializeField] private Button downloadButton;
[SerializeField] private Button deleteButton;
[SerializeField] private Button playButton;
[SerializeField] private Button closeButton;
[Header("진행률")]
[SerializeField] private GameObject progressGroup;
[SerializeField] private Slider progressSlider;
[SerializeField] private TMP_Text progressText;
[Header("씬 이름")]
[SerializeField] private string gameSceneName = "Game";
private static readonly Color NeonBg = new Color(0.05f, 0.82f, 0.95f, 0.42f);
private static readonly Color DarkButtonBg = new Color(0.09f, 0.22f, 0.27f, 0.66f);
private static readonly Color DisabledBg = new Color(0.06f, 0.12f, 0.15f, 0.48f);
private static readonly Color DangerBg = new Color(0.52f, 0.16f, 0.22f, 0.72f);
private static readonly Color ButtonText = new Color(0.92f, 1.0f, 1.0f, 1.0f);
private static readonly Color MutedText = new Color(0.66f, 0.80f, 0.84f, 0.76f);
private static readonly Color NeonOutline = new Color(0.25f, 0.96f, 1.0f, 0.42f);
private SongInfo currentSong;
private string selectedDifficulty;
private DownloadManager downloadManager;
private SongSelectManager selectManager;
private MarqueeText titleMarquee;
private MarqueeText artistMarquee;
private readonly (string key, Func<SongDetailPanel, Button> btn)[] diffSlots =
{
("normal", p => p.btnNormal),
("hard", p => p.btnHard),
("expert", p => p.btnExpert),
("expertplus", p => p.btnExpertPlus),
};
private void Awake()
{
HideDifficultyLabel();
titleMarquee = ConfigureMarqueeText(titleText, 5.0f, 7.2f);
artistMarquee = ConfigureMarqueeText(artistText, 3.4f, 4.4f);
ConfigureOneLineText(infoText, 3.2f, 4.2f, TextAlignmentOptions.MidlineLeft);
ConfigureButtonText(btnNormal, 3.2f, 4.0f);
ConfigureButtonText(btnHard, 3.2f, 4.0f);
ConfigureButtonText(btnExpert, 3.2f, 4.0f);
ConfigureButtonText(btnExpertPlus, 3.0f, 3.8f);
ConfigureButtonText(downloadButton, 3.5f, 4.4f);
ConfigureButtonText(deleteButton, 3.5f, 4.4f);
ConfigureButtonText(playButton, 3.5f, 4.4f);
ConfigureButtonText(closeButton, 5.2f, 6.4f);
}
// ── Public API ───────────────────────────────────────────
public void Show(SongInfo song, DownloadManager dm, SongSelectManager sm)
{
currentSong = song;
downloadManager = dm;
selectManager = sm;
selectedDifficulty = null;
titleText.text = song.title;
artistText.text = song.artist;
infoText.text = song.duration > 0
? $"BPM {Mathf.RoundToInt(song.bpm)} {FormatDuration(song.duration)}"
: $"BPM {Mathf.RoundToInt(song.bpm)}";
titleMarquee?.Refresh();
artistMarquee?.Refresh();
RefreshUI();
}
// ── UI 갱신 ──────────────────────────────────────────────
private void RefreshUI()
{
bool downloaded = SongLibrary.Instance.IsSongDownloaded(currentSong.id);
foreach (var (key, getBtn) in diffSlots)
{
Button btn = getBtn(this);
bool exists = currentSong.difficulties.Get(key) != null;
btn.interactable = downloaded && exists;
btn.onClick.RemoveAllListeners();
if (downloaded && exists)
{
string captured = key;
btn.onClick.AddListener(() => SelectDifficulty(captured));
}
}
UpdateDiffColors();
downloadButton.gameObject.SetActive(!downloaded);
deleteButton.gameObject.SetActive(downloaded);
downloadButton.interactable = !downloaded;
deleteButton.interactable = downloaded;
playButton.interactable = downloaded && selectedDifficulty != null;
progressGroup.SetActive(false);
UpdateActionButtonStyles(downloaded);
downloadButton.onClick.RemoveAllListeners();
downloadButton.onClick.AddListener(OnDownloadClicked);
deleteButton.onClick.RemoveAllListeners();
deleteButton.onClick.AddListener(OnDeleteClicked);
playButton.onClick.RemoveAllListeners();
playButton.onClick.AddListener(OnPlayClicked);
if (closeButton != null)
{
closeButton.onClick.RemoveAllListeners();
closeButton.onClick.AddListener(() => gameObject.SetActive(false));
}
}
private void SelectDifficulty(string difficulty)
{
selectedDifficulty = difficulty;
playButton.interactable = true;
UpdateDiffColors();
UpdateActionButtonStyles(true);
}
private void UpdateDiffColors()
{
foreach (var (key, getBtn) in diffSlots)
{
Button btn = getBtn(this);
bool selected = key == selectedDifficulty;
ApplyButtonStyle(btn, selected ? NeonBg : DarkButtonBg, selected, btn.interactable, false);
}
}
// ── 다운로드 ──────────────────────────────────────────────
private void OnDownloadClicked()
{
StartCoroutine(DownloadAllCoroutine());
}
private IEnumerator DownloadAllCoroutine()
{
var diffs = new List<string>();
foreach (var (key, _) in diffSlots)
if (currentSong.difficulties.Get(key) != null)
diffs.Add(key);
if (diffs.Count == 0) yield break;
SetInteractable(false);
progressGroup.SetActive(true);
downloadButton.gameObject.SetActive(false);
deleteButton.gameObject.SetActive(false);
playButton.gameObject.SetActive(false);
int totalSteps = diffs.Count;
int doneSteps = 0;
bool failed = false;
foreach (string diff in diffs)
{
bool stepDone = false;
downloadManager.DownloadSong(
currentSong, diff,
onProgress: p =>
{
float overall = (doneSteps + p) / totalSteps;
progressSlider.value = overall;
progressText.text = $"{diffs[Mathf.Min(doneSteps, diffs.Count - 1)].ToUpper()} {(int)(overall * 100)}%";
},
onComplete: () =>
{
SongLibrary.Instance.MarkDownloaded(currentSong.id, diff);
doneSteps++;
stepDone = true;
},
onError: err =>
{
Debug.LogError($"[SongDetailPanel] {err}");
failed = true;
stepDone = true;
});
yield return new WaitUntil(() => stepDone);
if (failed) break;
}
SetInteractable(true);
progressGroup.SetActive(false);
playButton.gameObject.SetActive(true);
selectManager.RefreshCards();
RefreshUI();
if (!failed)
Debug.Log($"[SongDetailPanel] '{currentSong.title}' 전체 다운로드 완료");
}
// ── 삭제 ─────────────────────────────────────────────────
private void OnDeleteClicked()
{
downloadManager.DeleteSong(currentSong.id);
SongLibrary.Instance.MarkSongRemoved(currentSong.id);
selectedDifficulty = null;
selectManager.RefreshCards();
RefreshUI();
}
// ── 플레이 ───────────────────────────────────────────────
private void OnPlayClicked()
{
GameSession.SelectedSong = currentSong;
GameSession.SelectedDifficulty = selectedDifficulty;
SceneManager.LoadScene(gameSceneName);
}
// ── 유틸 ─────────────────────────────────────────────────
private void SetInteractable(bool value)
{
downloadButton.interactable = value;
deleteButton.interactable = value;
playButton.interactable = value && selectedDifficulty != null;
foreach (var (_, getBtn) in diffSlots)
getBtn(this).interactable = value;
}
private static string FormatDuration(int seconds)
=> $"{seconds / 60}:{seconds % 60:D2}";
private void HideDifficultyLabel()
{
Transform label = transform.Find("LblDifficulty");
if (label != null)
label.gameObject.SetActive(false);
}
private void UpdateActionButtonStyles(bool downloaded)
{
ApplyButtonStyle(downloadButton, NeonBg, true, !downloaded, false);
ApplyButtonStyle(deleteButton, DangerBg, true, downloaded, true);
ApplyButtonStyle(playButton, NeonBg, true, playButton.interactable, false);
ApplyButtonStyle(closeButton, DarkButtonBg, false, true, false);
}
private static void ApplyButtonStyle(Button btn, Color activeBg, bool outlined, bool enabled, bool danger)
{
if (btn == null)
return;
Color bg = enabled ? activeBg : DisabledBg;
if (btn.targetGraphic is Image img)
img.color = bg;
var colors = btn.colors;
colors.normalColor = bg;
colors.highlightedColor = enabled
? (danger ? new Color(0.72f, 0.23f, 0.30f, 0.86f) : new Color(0.10f, 0.95f, 1.0f, 0.58f))
: DisabledBg;
colors.pressedColor = enabled
? (danger ? new Color(0.42f, 0.10f, 0.15f, 0.92f) : new Color(0.02f, 0.58f, 0.72f, 0.80f))
: DisabledBg;
colors.selectedColor = colors.highlightedColor;
colors.disabledColor = DisabledBg;
colors.fadeDuration = 0.08f;
btn.colors = colors;
TMP_Text label = btn.GetComponentInChildren<TMP_Text>();
if (label != null)
label.color = enabled ? ButtonText : MutedText;
Outline outline = btn.GetComponent<Outline>() ?? btn.gameObject.AddComponent<Outline>();
outline.enabled = outlined && enabled;
outline.effectColor = danger ? new Color(1.0f, 0.35f, 0.42f, 0.34f) : NeonOutline;
outline.effectDistance = new Vector2(0.0f, -0.28f);
}
private static MarqueeText ConfigureMarqueeText(TMP_Text text, float minSize, float maxSize)
{
if (text == null)
return null;
RectTransform textRect = text.rectTransform;
Transform originalParent = textRect.parent;
int siblingIndex = textRect.GetSiblingIndex();
string maskName = $"{text.name}Mask";
Transform existingMask = originalParent != null ? originalParent.Find(maskName) : null;
RectTransform maskRect;
if (existingMask != null)
{
maskRect = existingMask as RectTransform;
if (textRect.parent != existingMask)
textRect.SetParent(existingMask, false);
}
else
{
var mask = new GameObject(maskName);
mask.transform.SetParent(originalParent, false);
mask.transform.SetSiblingIndex(siblingIndex);
maskRect = mask.AddComponent<RectTransform>();
maskRect.anchorMin = textRect.anchorMin;
maskRect.anchorMax = textRect.anchorMax;
maskRect.pivot = textRect.pivot;
maskRect.anchoredPosition = textRect.anchoredPosition;
maskRect.sizeDelta = textRect.sizeDelta;
maskRect.localRotation = textRect.localRotation;
maskRect.localScale = textRect.localScale;
mask.AddComponent<RectMask2D>();
textRect.SetParent(mask.transform, false);
}
textRect.anchorMin = new Vector2(0f, 0f);
textRect.anchorMax = new Vector2(0f, 1f);
textRect.pivot = new Vector2(0f, 0.5f);
textRect.anchoredPosition = Vector2.zero;
textRect.localRotation = Quaternion.identity;
textRect.localScale = Vector3.one;
textRect.sizeDelta = new Vector2(260.0f, 0f);
ConfigureOneLineText(text, minSize, maxSize, TextAlignmentOptions.MidlineLeft);
text.overflowMode = TextOverflowModes.Overflow;
text.raycastTarget = false;
MarqueeText marquee = text.GetComponent<MarqueeText>() ?? text.gameObject.AddComponent<MarqueeText>();
marquee.speed = 9f;
marquee.pauseStart = 1.25f;
marquee.pauseEnd = 0.8f;
return marquee;
}
private static void ConfigureButtonText(Button btn, float minSize, float maxSize)
{
if (btn == null)
return;
TMP_Text label = btn.GetComponentInChildren<TMP_Text>();
ConfigureOneLineText(label, minSize, maxSize, TextAlignmentOptions.Center);
if (label != null)
label.raycastTarget = false;
}
private static void ConfigureOneLineText(TMP_Text text, float minSize, float maxSize, TextAlignmentOptions alignment)
{
if (text == null)
return;
text.enableAutoSizing = true;
text.fontSizeMin = minSize;
text.fontSizeMax = maxSize;
text.alignment = alignment;
text.overflowMode = TextOverflowModes.Ellipsis;
text.textWrappingMode = TextWrappingModes.NoWrap;
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d27acdc84ca9a6241894ce7ee9f3c3fa
+140
View File
@@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
public class SongLibrary : MonoBehaviour
{
public static SongLibrary Instance { get; private set; }
private const string FileName = "song_library.json";
private static string SavePath => Path.Combine(Application.persistentDataPath, FileName);
private LibraryData _data = new LibraryData();
private void Awake()
{
if (Instance != null) { Destroy(gameObject); return; }
Instance = this;
DontDestroyOnLoad(gameObject);
Load();
}
// ── Public API ───────────────────────────────────────────
public void MarkDownloaded(string songId, string difficulty)
{
LibraryEntry entry = GetOrCreate(songId);
if (!entry.difficulties.Contains(difficulty))
entry.difficulties.Add(difficulty);
entry.lastAccessedAt = DateTime.UtcNow.ToString("o");
Save();
}
public void MarkDifficultyRemoved(string songId, string difficulty)
{
LibraryEntry entry = Find(songId);
if (entry == null) return;
entry.difficulties.Remove(difficulty);
if (entry.difficulties.Count == 0)
_data.entries.Remove(entry);
Save();
}
public void MarkSongRemoved(string songId)
{
_data.entries.RemoveAll(e => e.songId == songId);
Save();
}
public void TouchSong(string songId)
{
LibraryEntry entry = Find(songId);
if (entry == null) return;
entry.lastAccessedAt = DateTime.UtcNow.ToString("o");
Save();
}
public bool IsSongDownloaded(string songId)
=> Find(songId) != null;
public bool IsDifficultyDownloaded(string songId, string difficulty)
=> Find(songId)?.difficulties.Contains(difficulty) ?? false;
public List<LibraryEntry> GetAll()
=> _data.entries;
public void ValidateWithFileSystem(DownloadManager dm, List<SongInfo> songs)
{
bool dirty = false;
foreach (SongInfo song in songs)
{
LibraryEntry entry = Find(song.id);
if (entry == null) continue;
if (!dm.IsSongDownloaded(song.id))
{
_data.entries.Remove(entry);
dirty = true;
continue;
}
entry.difficulties.RemoveAll(d => !dm.IsDifficultyDownloaded(song, d));
if (entry.difficulties.Count == 0)
{
_data.entries.Remove(entry);
dirty = true;
}
}
if (dirty) Save();
}
// ── 내부 구현 ─────────────────────────────────────────────
private LibraryEntry Find(string songId)
=> _data.entries.Find(e => e.songId == songId);
private LibraryEntry GetOrCreate(string songId)
{
LibraryEntry entry = Find(songId);
if (entry != null) return entry;
entry = new LibraryEntry { songId = songId };
_data.entries.Add(entry);
return entry;
}
private void Load()
{
if (!File.Exists(SavePath)) return;
try
{
string json = File.ReadAllText(SavePath);
_data = JsonUtility.FromJson<LibraryData>(json) ?? new LibraryData();
}
catch (Exception e)
{
Debug.LogWarning($"[SongLibrary] 로드 실패, 초기화: {e.Message}");
_data = new LibraryData();
}
}
private void Save()
{
File.WriteAllText(SavePath, JsonUtility.ToJson(_data, true));
}
}
[Serializable]
public class LibraryData
{
public List<LibraryEntry> entries = new List<LibraryEntry>();
}
[Serializable]
public class LibraryEntry
{
public string songId;
public List<string> difficulties = new List<string>();
public string lastAccessedAt;
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 765cf3a9cd9c14943be42e1cee050abd
+328
View File
@@ -0,0 +1,328 @@
using System.Collections.Generic;
using System.IO;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class SongSelectManager : MonoBehaviour
{
[SerializeField] private Button tabAllBtn;
[SerializeField] private Button tabOwnedBtn;
[SerializeField] private RectTransform cardContainer;
[SerializeField] private SongDetailPanel detailPanel;
[SerializeField] private DownloadManager downloadManager;
[SerializeField] private GameObject loadingOverlay;
[SerializeField] private GameObject errorOverlay;
[SerializeField] private TMP_Text errorText;
private static readonly Color TabActiveBg = new Color(0.05f, 0.82f, 0.95f, 0.42f);
private static readonly Color TabInactiveBg = new Color(0.09f, 0.22f, 0.27f, 0.66f);
private static readonly Color TabActiveText = new Color(0.92f, 1.0f, 1.0f, 1.0f);
private static readonly Color TabInactiveText = new Color(0.72f, 0.86f, 0.90f, 0.82f);
private static readonly Color TabActiveOutline = new Color(0.25f, 0.96f, 1.0f, 0.55f);
private static string CachePath =>
Path.Combine(Application.persistentDataPath, "songs_cache.json");
private List<SongInfo> allSongs = new List<SongInfo>();
private bool showingOwned = false;
private TMP_FontAsset _cardFont;
private void Start()
{
// NanumGothic SDF를 직접 로드 — Resources 경로에 있어야 함
_cardFont = Resources.Load<TMP_FontAsset>("Fonts & Materials/NanumGothic SDF");
if (_cardFont == null)
_cardFont = Resources.Load<TMP_FontAsset>("Fonts & Materials/LiberationSans SDF");
tabAllBtn .onClick.AddListener(() => SwitchTab(false));
tabOwnedBtn.onClick.AddListener(() => SwitchTab(true));
detailPanel.gameObject.SetActive(false);
SetTabVisual(false);
FetchSongs();
}
private void SwitchTab(bool owned)
{
showingOwned = owned;
SetTabVisual(owned);
RefreshCards();
}
private void SetTabVisual(bool owned)
{
ApplyTabStyle(tabAllBtn, !owned);
ApplyTabStyle(tabOwnedBtn, owned);
}
private static void ApplyTabStyle(Button btn, bool active)
{
if (btn == null)
return;
Color bg = active ? TabActiveBg : TabInactiveBg;
if (btn.targetGraphic is Image img)
img.color = bg;
var colors = btn.colors;
colors.normalColor = bg;
colors.highlightedColor = active
? new Color(0.10f, 0.95f, 1.0f, 0.58f)
: new Color(0.14f, 0.34f, 0.40f, 0.72f);
colors.pressedColor = active
? new Color(0.02f, 0.58f, 0.72f, 0.72f)
: new Color(0.08f, 0.20f, 0.24f, 0.82f);
colors.selectedColor = colors.highlightedColor;
colors.disabledColor = new Color(0.05f, 0.10f, 0.12f, 0.45f);
btn.colors = colors;
TMP_Text label = btn.GetComponentInChildren<TMP_Text>();
if (label != null)
label.color = active ? TabActiveText : TabInactiveText;
Outline outline = btn.GetComponent<Outline>() ?? btn.gameObject.AddComponent<Outline>();
outline.enabled = active;
outline.effectColor = TabActiveOutline;
outline.effectDistance = new Vector2(0.0f, -0.35f);
}
private void FetchSongs()
{
loadingOverlay.SetActive(true);
errorOverlay .SetActive(false);
downloadManager.FetchSongsList(
onSuccess: list =>
{
allSongs = list.songs ?? new List<SongInfo>();
AddLocalForcedRankDummies(allSongs);
SaveCache(new SongsList { version = list.version, songs = allSongs });
SongLibrary.Instance.ValidateWithFileSystem(downloadManager, allSongs);
loadingOverlay.SetActive(false);
RefreshCards();
},
onError: _ =>
{
SongsList cached = LoadCache();
loadingOverlay.SetActive(false);
if (cached != null)
{
allSongs = cached.songs;
RefreshCards();
}
else
{
errorOverlay.SetActive(true);
errorText.text = "Failed to connect to server\nPlease check your internet connection";
}
});
}
public void RefreshCards()
{
// DestroyImmediate to avoid deferred-destroy interfering with layout
for (int i = cardContainer.childCount - 1; i >= 0; i--)
DestroyImmediate(cardContainer.GetChild(i).gameObject);
List<SongInfo> songs = showingOwned
? allSongs.FindAll(s => SongLibrary.Instance.IsSongDownloaded(s.id))
: allSongs;
foreach (SongInfo song in songs)
SpawnCard(song);
// Order matters: layout first → card gets size → then canvas update → anchored children recalculate
LayoutRebuilder.ForceRebuildLayoutImmediate(cardContainer);
Canvas.ForceUpdateCanvases();
}
private void SpawnCard(SongInfo song)
{
bool downloaded = SongLibrary.Instance.IsSongDownloaded(song.id);
var card = new GameObject(song.title);
card.transform.SetParent(cardContainer, false);
var le = card.AddComponent<LayoutElement>();
le.preferredHeight = 13f;
le.flexibleWidth = 1f;
var bg = card.AddComponent<Image>();
bg.color = new Color(1f, 1f, 1f, 0.06f);
var btn = card.AddComponent<Button>();
btn.targetGraphic = bg;
var bc = btn.colors;
bc.normalColor = new Color(1f, 1f, 1f, 0.06f);
bc.highlightedColor = new Color(0.4f, 0.75f, 1f, 0.25f);
bc.pressedColor = new Color(0.3f, 0.60f, 0.9f, 0.45f);
bc.fadeDuration = 0.1f;
btn.colors = bc;
float textLeftInset = downloaded ? 12f : 5f;
// Title — RectMask2D 컨테이너 안에서 마퀴 스크롤
var titleMask = new GameObject("TitleMask");
titleMask.transform.SetParent(card.transform, false);
var tmr = titleMask.AddComponent<RectTransform>();
tmr.anchorMin = new Vector2(0f, 0.5f);
tmr.anchorMax = new Vector2(1f, 1f);
tmr.offsetMin = new Vector2(textLeftInset, 0f);
tmr.offsetMax = new Vector2(-3f, 0f);
titleMask.AddComponent<RectMask2D>();
var titleGO = new GameObject("Title");
titleGO.transform.SetParent(titleMask.transform, false);
var tr = titleGO.AddComponent<RectTransform>();
tr.anchorMin = new Vector2(0f, 0f);
tr.anchorMax = new Vector2(0f, 1f);
tr.pivot = new Vector2(0f, 0.5f);
tr.anchoredPosition = Vector2.zero;
tr.sizeDelta = new Vector2(500f, 0f);
var tTmp = titleGO.AddComponent<TextMeshProUGUI>();
if (_cardFont != null) tTmp.font = _cardFont;
tTmp.text = song.title;
tTmp.fontSize = 5f;
tTmp.color = Color.white;
tTmp.alignment = TextAlignmentOptions.MidlineLeft;
tTmp.overflowMode = TextOverflowModes.Overflow;
tTmp.textWrappingMode = TextWrappingModes.NoWrap;
titleGO.AddComponent<MarqueeText>();
// Artist
var artistGO = new GameObject("Artist");
artistGO.transform.SetParent(card.transform, false);
var ar = artistGO.AddComponent<RectTransform>();
ar.anchorMin = new Vector2(0f, 0.04f);
ar.anchorMax = new Vector2(1f, 0.48f);
ar.offsetMin = new Vector2(textLeftInset, 0f);
ar.offsetMax = new Vector2(-3f, 0f);
var aTmp = artistGO.AddComponent<TextMeshProUGUI>();
if (_cardFont != null) aTmp.font = _cardFont;
aTmp.text = song.artist;
aTmp.fontSize = 4f;
aTmp.enableAutoSizing = true;
aTmp.fontSizeMin = 2.8f;
aTmp.fontSizeMax = 4f;
aTmp.color = new Color(1f, 1f, 1f, 0.6f);
aTmp.alignment = TextAlignmentOptions.MidlineLeft;
aTmp.overflowMode = TextOverflowModes.Ellipsis;
aTmp.textWrappingMode = TextWrappingModes.NoWrap;
// Downloaded check mark
if (downloaded)
{
var checkGO = new GameObject("OwnedCheck");
checkGO.transform.SetParent(card.transform, false);
var cr = checkGO.AddComponent<RectTransform>();
cr.anchorMin = new Vector2(0f, 0f);
cr.anchorMax = new Vector2(0f, 1f);
cr.pivot = new Vector2(0f, 0.5f);
cr.anchoredPosition = new Vector2(3.0f, 0f);
cr.sizeDelta = new Vector2(6f, 0f);
Color checkColor = new Color(0.36f, 1.0f, 0.58f, 0.95f);
CreateCheckStroke(checkGO.transform, "ShortStroke", new Vector2(1.8f, 7.1f), new Vector2(1.5f, 0.35f), 42.0f, checkColor);
CreateCheckStroke(checkGO.transform, "LongStroke", new Vector2(3.25f, 7.85f), new Vector2(3.7f, 0.35f), -45.0f, checkColor);
}
SongInfo captured = song;
btn.onClick.AddListener(() => OnCardClicked(captured));
}
private void OnCardClicked(SongInfo song)
{
detailPanel.gameObject.SetActive(true);
detailPanel.Show(song, downloadManager, this);
}
private static void CreateCheckStroke(Transform parent, string name, Vector2 anchoredPosition,
Vector2 size, float rotationZ, Color color)
{
var stroke = new GameObject(name);
stroke.transform.SetParent(parent, false);
var rect = stroke.AddComponent<RectTransform>();
rect.anchorMin = new Vector2(0f, 0f);
rect.anchorMax = new Vector2(0f, 0f);
rect.pivot = new Vector2(0.5f, 0.5f);
rect.anchoredPosition = anchoredPosition;
rect.sizeDelta = size;
rect.localRotation = Quaternion.Euler(0f, 0f, rotationZ);
var img = stroke.AddComponent<Image>();
img.color = color;
img.raycastTarget = false;
}
private static void SaveCache(SongsList list)
{
try { File.WriteAllText(CachePath, JsonUtility.ToJson(list, true)); }
catch { }
}
private static SongsList LoadCache()
{
if (!File.Exists(CachePath)) return null;
try { return JsonUtility.FromJson<SongsList>(File.ReadAllText(CachePath)); }
catch { return null; }
}
private static void AddLocalForcedRankDummies(List<SongInfo> songs)
{
string root = Path.Combine(Application.persistentDataPath, "beatsaber");
AddLocalForcedRankDummy(songs, root, "dummy_rank_m", "M", 10);
AddLocalForcedRankDummy(songs, root, "dummy_rank_splus", "S+", 100);
AddLocalForcedRankDummy(songs, root, "dummy_rank_s", "S", 100);
AddLocalForcedRankDummy(songs, root, "dummy_rank_a", "A", 100);
AddLocalForcedRankDummy(songs, root, "dummy_rank_b", "B", 100);
AddLocalForcedRankDummy(songs, root, "dummy_rank_c", "C", 100);
AddLocalForcedRankDummy(songs, root, "dummy_rank_d", "D", 100);
AddLocalForcedRankDummy(songs, root, "dummy_rank_f", "F", 100);
AddLocalForcedRankDummy(songs, root, "dummy_rank_splus_border", "S+ 98%", 100);
AddLocalForcedRankDummy(songs, root, "dummy_rank_f_zero", "F 0%", 20);
}
private static void AddLocalForcedRankDummy(List<SongInfo> songs, string root, string id, string title, int noteCount)
{
if (songs.Exists(song => song.id == id))
return;
string songDir = Path.Combine(root, id);
string audioPath = Path.Combine(songDir, $"{id}.mp3");
string mapFile = $"Map_{id}_forced.json";
string mapPath = Path.Combine(songDir, mapFile);
if (!File.Exists(audioPath) || !File.Exists(mapPath))
return;
long audioSize = new FileInfo(audioPath).Length;
long mapSize = new FileInfo(mapPath).Length;
DifficultyInfo info = new DifficultyInfo
{
mapFile = mapFile,
mapSize = mapSize,
noteCount = noteCount
};
songs.Insert(0, new SongInfo
{
id = id,
title = title,
artist = "Forced Rank Dummy",
bpm = 120.0f,
duration = 1,
audioFile = $"dummy/{id}.mp3",
audioSize = audioSize,
coverImage = "",
difficulties = new DifficultyMap
{
normal = info,
hard = info,
expert = info,
expertplus = info
},
addedAt = "2026-05-29"
});
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 46403458ee5537142ad1e0b2ce7d3995
+341
View File
@@ -0,0 +1,341 @@
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using UnityEngine.XR;
using XRCommonUsages = UnityEngine.XR.CommonUsages;
using XRInputDevice = UnityEngine.XR.InputDevice;
public class SyncCalibrationOverlay : MonoBehaviour
{
private const float TickInterval = 1.0f;
private const int MaxSamples = 8;
private static Scene pendingReturnScene;
private readonly List<float> samples = new();
private readonly List<(Canvas canvas, bool enabled)> hiddenCanvases = new();
private AudioSource audioSource = null;
private AudioClip tickClip = null;
private RectTransform sweepDot = null;
private RectTransform visualPulse = null;
private TextMeshProUGUI offsetText = null;
private TextMeshProUGUI sampleText = null;
private TextMeshProUGUI guideText = null;
private float lastTickTime = 0.0f;
private float nextTickTime = 0.0f;
private float pendingVisualPulseTime = -1.0f;
private float visualPulseTimer = 0.0f;
private bool previousPrimary = false;
private bool previousSecondary = false;
private Scene returnScene;
private Scene syncScene;
private bool canvasesRestored = false;
public static void Open()
{
if (FindFirstObjectByType<SyncCalibrationOverlay>() != null)
return;
pendingReturnScene = SceneManager.GetActiveScene();
Scene calibrationScene = SceneManager.CreateScene("SyncCalibration");
SceneManager.SetActiveScene(calibrationScene);
GameObject overlay = new GameObject("[SyncCalibrationOverlay]");
overlay.AddComponent<SyncCalibrationOverlay>();
}
private void Awake()
{
syncScene = gameObject.scene;
returnScene = pendingReturnScene;
HideExistingCanvases();
BuildView();
tickClip = CreateTickClip();
audioSource = gameObject.AddComponent<AudioSource>();
audioSource.playOnAwake = false;
audioSource.spatialBlend = 0.0f;
float now = Time.unscaledTime;
lastTickTime = now;
nextTickTime = now + 0.8f;
UpdateTexts();
}
private void OnDestroy()
{
RestoreCanvases();
}
private void Update()
{
float now = Time.unscaledTime;
if (now >= nextTickTime)
{
lastTickTime = nextTickTime;
nextTickTime += TickInterval;
audioSource.PlayOneShot(tickClip);
pendingVisualPulseTime = lastTickTime + GlobalSyncSettings.AudioOffsetSeconds;
}
if (pendingVisualPulseTime > 0.0f && now >= pendingVisualPulseTime)
{
visualPulseTimer = 0.18f;
pendingVisualPulseTime = -1.0f;
}
UpdateMetronomeVisual(now);
HandleInput(now);
}
private void HandleInput(float now)
{
bool primary = GetRightButton(XRCommonUsages.primaryButton) || IsKeyboardPressed(Key.Space);
bool secondary = GetRightButton(XRCommonUsages.secondaryButton) || IsKeyboardPressed(Key.Escape);
if (primary && !previousPrimary)
CaptureSample(now);
if (secondary && !previousSecondary)
Close();
previousPrimary = primary;
previousSecondary = secondary;
}
private void CaptureSample(float now)
{
float nearestTick = Mathf.Abs(now - lastTickTime) <= Mathf.Abs(now - nextTickTime)
? lastTickTime
: nextTickTime;
float offsetMs = Mathf.Clamp((now - nearestTick) * 1000.0f, -300.0f, 300.0f);
samples.Add(offsetMs);
if (samples.Count > MaxSamples)
samples.RemoveAt(0);
float sum = 0.0f;
for (int i = 0; i < samples.Count; i++)
sum += samples[i];
GlobalSyncSettings.AudioOffsetMs = sum / samples.Count;
UpdateTexts();
}
private void AdjustOffset(float deltaMs)
{
GlobalSyncSettings.AudioOffsetMs += deltaMs;
samples.Clear();
UpdateTexts();
}
private void ResetOffset()
{
GlobalSyncSettings.Reset();
samples.Clear();
UpdateTexts();
}
private void Close()
{
RestoreCanvases();
if (returnScene.IsValid() && returnScene.isLoaded)
SceneManager.SetActiveScene(returnScene);
if (syncScene.IsValid() && syncScene.isLoaded)
SceneManager.UnloadSceneAsync(syncScene);
else
Destroy(gameObject);
}
private void BuildView()
{
Camera camera = Camera.main ?? FindFirstObjectByType<Camera>();
GameObject canvasObject = new GameObject("SyncCalibrationCanvas");
canvasObject.transform.SetParent(transform, false);
Canvas canvas = canvasObject.AddComponent<Canvas>();
canvas.renderMode = RenderMode.WorldSpace;
canvas.sortingOrder = 400;
canvas.worldCamera = camera;
canvasObject.AddComponent<GraphicRaycaster>();
RectTransform canvasRect = canvasObject.GetComponent<RectTransform>();
canvasRect.sizeDelta = new Vector2(1080.0f, 660.0f);
canvasObject.transform.localScale = Vector3.one * 0.0028f;
if (camera != null)
{
Transform camTransform = camera.transform;
canvasObject.transform.position = camTransform.position + camTransform.forward * 1.9f;
canvasObject.transform.rotation = Quaternion.LookRotation(canvasObject.transform.position - camTransform.position, camTransform.up);
}
Image background = CreateImage("Panel", canvasRect, new Vector2(0, 0), new Vector2(1020, 620), new Color(0.02f, 0.06f, 0.09f, 0.92f));
background.raycastTarget = false;
TextMeshProUGUI title = CreateText("Title", canvasRect, "SYNC CALIBRATION", new Vector2(0, 260), new Vector2(920, 58), 42, Color.white, TextAlignmentOptions.Center);
title.fontStyle = FontStyles.Bold;
guideText = CreateText("Guide", canvasRect, "", new Vector2(0, 180), new Vector2(920, 92), 26, new Color(0.77f, 0.9f, 1.0f, 1.0f), TextAlignmentOptions.Center);
Image bar = CreateImage("BeatBar", canvasRect, new Vector2(0, 82), new Vector2(760, 10), new Color(0.25f, 0.85f, 1.0f, 0.28f));
bar.raycastTarget = false;
sweepDot = CreateImage("BeatDot", canvasRect, new Vector2(-380, 82), new Vector2(34, 34), new Color(0.35f, 0.95f, 1.0f, 1.0f)).rectTransform;
visualPulse = CreateImage("VisualPulse", canvasRect, new Vector2(0, 82), new Vector2(140, 140), new Color(0.35f, 0.95f, 1.0f, 0.0f)).rectTransform;
offsetText = CreateText("Offset", canvasRect, "", new Vector2(0, 0), new Vector2(720, 74), 54, Color.white, TextAlignmentOptions.Center);
offsetText.fontStyle = FontStyles.Bold;
sampleText = CreateText("Samples", canvasRect, "", new Vector2(0, -72), new Vector2(760, 44), 24, new Color(0.65f, 0.78f, 0.84f, 1.0f), TextAlignmentOptions.Center);
CreateButton(canvasRect, "-10ms", new Vector2(-300, -165), () => AdjustOffset(-10.0f));
CreateButton(canvasRect, "+10ms", new Vector2(-100, -165), () => AdjustOffset(10.0f));
CreateButton(canvasRect, "RESET", new Vector2(100, -165), ResetOffset);
CreateButton(canvasRect, "BACK", new Vector2(300, -165), Close);
CreateText("Footer", canvasRect, "A / Space: capture beat B / Esc: back", new Vector2(0, -270), new Vector2(860, 40), 22, new Color(0.58f, 0.7f, 0.75f, 1.0f), TextAlignmentOptions.Center);
}
private void UpdateMetronomeVisual(float now)
{
float phase = Mathf.InverseLerp(lastTickTime, nextTickTime, now);
if (sweepDot != null)
sweepDot.anchoredPosition = new Vector2(Mathf.Lerp(-380.0f, 380.0f, phase), 82.0f);
if (visualPulse == null)
return;
visualPulseTimer = Mathf.Max(0.0f, visualPulseTimer - Time.unscaledDeltaTime);
float alpha = visualPulseTimer / 0.18f;
visualPulse.sizeDelta = Vector2.one * Mathf.Lerp(190.0f, 80.0f, alpha);
Image image = visualPulse.GetComponent<Image>();
if (image != null)
image.color = new Color(0.35f, 0.95f, 1.0f, alpha * 0.52f);
}
private void UpdateTexts()
{
float offset = GlobalSyncSettings.AudioOffsetMs;
if (offsetText != null)
offsetText.text = $"{offset:+0;-0;0} ms";
if (sampleText != null)
sampleText.text = $"samples {samples.Count}/{MaxSamples} global offset saved";
if (guideText != null)
guideText.text = "Tick 소리가 들리는 순간 A / Space를 누르세요.\n파란 원이 박자와 겹치면 보정이 맞습니다.";
}
private void HideExistingCanvases()
{
hiddenCanvases.Clear();
foreach (Canvas canvas in FindObjectsByType<Canvas>(FindObjectsSortMode.None))
{
hiddenCanvases.Add((canvas, canvas.enabled));
canvas.enabled = false;
}
}
private void RestoreCanvases()
{
if (canvasesRestored)
return;
canvasesRestored = true;
foreach ((Canvas canvas, bool enabled) in hiddenCanvases)
{
if (canvas != null)
canvas.enabled = enabled;
}
}
private static TextMeshProUGUI CreateText(string name, RectTransform parent, string value, Vector2 position, Vector2 size, int fontSize, Color color, TextAlignmentOptions alignment)
{
GameObject go = new GameObject(name);
go.transform.SetParent(parent, false);
RectTransform rect = go.AddComponent<RectTransform>();
rect.anchoredPosition = position;
rect.sizeDelta = size;
TextMeshProUGUI text = go.AddComponent<TextMeshProUGUI>();
text.text = value;
text.fontSize = fontSize;
text.color = color;
text.alignment = alignment;
text.textWrappingMode = TextWrappingModes.Normal;
text.overflowMode = TextOverflowModes.Overflow;
text.raycastTarget = false;
return text;
}
private static Image CreateImage(string name, RectTransform parent, Vector2 position, Vector2 size, Color color)
{
GameObject go = new GameObject(name);
go.transform.SetParent(parent, false);
RectTransform rect = go.AddComponent<RectTransform>();
rect.anchoredPosition = position;
rect.sizeDelta = size;
Image image = go.AddComponent<Image>();
image.color = color;
return image;
}
private static void CreateButton(RectTransform parent, string label, Vector2 position, UnityEngine.Events.UnityAction action)
{
Image image = CreateImage(label + "Button", parent, position, new Vector2(168, 58), new Color(0.07f, 0.18f, 0.24f, 0.96f));
Button button = image.gameObject.AddComponent<Button>();
button.onClick.AddListener(action);
ColorBlock colors = button.colors;
colors.normalColor = new Color(0.07f, 0.18f, 0.24f, 0.96f);
colors.highlightedColor = new Color(0.13f, 0.38f, 0.48f, 1.0f);
colors.pressedColor = new Color(0.08f, 0.72f, 0.85f, 1.0f);
colors.selectedColor = colors.highlightedColor;
button.colors = colors;
TextMeshProUGUI labelText = CreateText(label + "Text", image.rectTransform, label, Vector2.zero, new Vector2(154, 48), 24, Color.white, TextAlignmentOptions.Center);
labelText.fontStyle = FontStyles.Bold;
}
private static bool GetRightButton(InputFeatureUsage<bool> usage)
{
var devices = new List<XRInputDevice>();
InputDevices.GetDevicesWithCharacteristics(InputDeviceCharacteristics.Controller | InputDeviceCharacteristics.Right, devices);
if (devices.Count == 0)
return false;
devices[0].TryGetFeatureValue(usage, out bool pressed);
return pressed;
}
private static bool IsKeyboardPressed(Key key)
{
Keyboard keyboard = Keyboard.current;
return keyboard != null && keyboard[key].isPressed;
}
private static AudioClip CreateTickClip()
{
const int sampleRate = 48000;
const float duration = 0.055f;
int sampleCount = Mathf.CeilToInt(sampleRate * duration);
float[] data = new float[sampleCount];
for (int i = 0; i < sampleCount; i++)
{
float t = (float)i / sampleRate;
float envelope = Mathf.Exp(-t * 62.0f);
float high = Mathf.Sin(2.0f * Mathf.PI * 1760.0f * t);
float click = i < 80 ? 1.0f - (float)i / 80.0f : 0.0f;
data[i] = Mathf.Clamp((high * 0.75f + click * 0.35f) * envelope, -1.0f, 1.0f);
}
AudioClip clip = AudioClip.Create("SyncTick", sampleCount, 1, sampleRate, false);
clip.SetData(data, 0);
return clip;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 90f8d18d467240d8bb84178048a5aa91
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+512
View File
@@ -0,0 +1,512 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using UnityEngine.XR;
namespace VRBeats
{
[RequireComponent(typeof(LineRenderer))]
public class VRPointerController : MonoBehaviour
{
[SerializeField] private bool isRightHand = true;
[SerializeField] private float maxDistance = 50f;
[SerializeField] private bool debugLogging = false;
[SerializeField] private float scrollSpeed = 2.4f;
[SerializeField] private float scrollDeadZone = 0.15f;
[SerializeField] private float dragScrollSpeed = 1.25f;
[SerializeField] private float dragClickThreshold = 0.025f;
private LineRenderer _line;
private bool _prevTrigger;
private Selectable _currentHover;
private ScrollRect _dragScrollRect;
private Selectable _triggerPressSelectable;
private Vector2 _dragStartLocalPoint;
private float _dragStartNormalizedPosition;
private float _dragMaxNormalizedDelta;
private static readonly Color NormalColor = new Color(1f, 1f, 1f, 0.8f);
private static readonly Color HoverColor = new Color(0.3f, 0.8f, 1f, 1f);
private float _deviceLogTimer;
// 버튼별 이전 상태
private bool _prevGrip;
private bool _prevPrimary;
private bool _prevSecondary;
private bool _prevThumbstick;
private void Awake()
{
_line = GetComponent<LineRenderer>();
_line.positionCount = 2;
_line.startWidth = 0.005f;
_line.endWidth = 0.001f;
_line.useWorldSpace = true;
// enabled 상태는 OnEnable/OnDisable이 관리 — Awake에서 건드리지 않음
}
private void Start()
{
// Awake 이후 reflection으로 isRightHand가 설정되므로 Start에서 로그
if (debugLogging)
Debug.Log($"[VRPointer] Start — {gameObject.name} / isRightHand={isRightHand}");
}
private void OnEnable()
{
if (_line != null) _line.enabled = true;
}
private void OnDisable()
{
if (_line != null) _line.enabled = false;
}
private void Update()
{
// 3초마다 연결된 디바이스 목록 출력
if (debugLogging)
{
_deviceLogTimer += Time.deltaTime;
if (_deviceLogTimer >= 3f)
{
_deviceLogTimer = 0f;
LogConnectedDevices();
}
}
bool trigger = GetButton(CommonUsages.triggerButton);
bool grip = GetButton(CommonUsages.gripButton);
bool primary = GetButton(CommonUsages.primaryButton);
bool secondary = GetButton(CommonUsages.secondaryButton);
bool thumbstick = GetButton(CommonUsages.primary2DAxisClick);
bool triggerDown = trigger && !_prevTrigger;
bool triggerUp = !trigger && _prevTrigger;
bool gripDown = grip && !_prevGrip;
bool primaryDown = primary && !_prevPrimary;
bool secondaryDown = secondary && !_prevSecondary;
bool thumbstickDown = thumbstick && !_prevThumbstick;
string hand = isRightHand ? "R" : "L";
if (debugLogging)
{
if (triggerDown) Debug.Log($"[VRPointer:{hand}] 검지 트리거 눌림");
if (gripDown) Debug.Log($"[VRPointer:{hand}] 그립(중지) 눌림");
if (primaryDown) Debug.Log($"[VRPointer:{hand}] {(isRightHand ? "A" : "X")} 버튼 눌림");
if (secondaryDown) Debug.Log($"[VRPointer:{hand}] {(isRightHand ? "B" : "Y")} 버튼 눌림");
if (thumbstickDown) Debug.Log($"[VRPointer:{hand}] 조이스틱 클릭 눌림");
}
Ray ray = new Ray(transform.position, transform.forward);
float selectableHitDist = maxDistance;
float scrollHitDist = maxDistance;
Selectable hit = FindSelectableUnderRay(ray, ref selectableHitDist);
ScrollRect scrollRect = FindScrollRectUnderRay(ray, ref scrollHitDist);
float hitDist = Mathf.Min(selectableHitDist, scrollHitDist);
bool beganScrollDrag = false;
if (triggerDown)
beganScrollDrag = TryBeginScrollDrag(scrollRect, hit, ray);
if (_dragScrollRect != null && trigger)
UpdateScrollDrag(ray);
else if (_dragScrollRect == null)
HandleScroll(scrollRect);
// 호버 변화 로그
if (debugLogging && hit != _currentHover)
{
Debug.Log(hit != null
? $"[VRPointer] HOVER → {hit.gameObject.name}"
: $"[VRPointer] HOVER → (없음)");
}
UpdateHoverState(hit);
if (triggerUp && _dragScrollRect != null)
EndScrollDrag(hand, ray);
// 검지 트리거 또는 A/X 버튼으로 클릭.
// ScrollRect 위의 검지 트리거는 드래그/클릭 판별을 위해 release 시점에 처리한다.
if ((triggerDown && !beganScrollDrag) || primaryDown)
{
if (_currentHover != null)
{
string btn = triggerDown && !beganScrollDrag ? "검지 트리거" : (isRightHand ? "A" : "X");
if (debugLogging)
Debug.Log($"[VRPointer:{hand}] CLICK [{btn}] → {_currentHover.gameObject.name}");
Click(_currentHover);
}
else if (debugLogging)
{
Debug.Log($"[VRPointer:{hand}] CLICK — 레이 아래 버튼 없음 " +
$"pos={transform.position:F2} fwd={transform.forward:F2}");
DebugRaycastAttempt(new Ray(transform.position, transform.forward));
}
}
DrawLine(hitDist);
_prevTrigger = trigger;
_prevGrip = grip;
_prevPrimary = primary;
_prevSecondary = secondary;
_prevThumbstick = thumbstick;
}
private void LogConnectedDevices()
{
var all = new List<InputDevice>();
InputDevices.GetDevices(all);
if (all.Count == 0)
{
Debug.LogWarning($"[VRPointer] 연결된 XR 디바이스 없음 ({gameObject.name})");
return;
}
var chars = InputDeviceCharacteristics.Controller |
(isRightHand ? InputDeviceCharacteristics.Right : InputDeviceCharacteristics.Left);
var matched = new List<InputDevice>();
InputDevices.GetDevicesWithCharacteristics(chars, matched);
Debug.Log($"[VRPointer] 전체 디바이스 {all.Count}개 | " +
$"{(isRightHand ? "" : "")} 컨트롤러 {matched.Count}개 ({gameObject.name})");
}
private void UpdateHoverState(Selectable hit)
{
if (hit == _currentHover) return;
var es = EventSystem.current;
if (_currentHover != null)
ExecuteEvents.Execute(_currentHover.gameObject,
new PointerEventData(es), ExecuteEvents.pointerExitHandler);
_currentHover = hit;
if (_currentHover != null)
ExecuteEvents.Execute(_currentHover.gameObject,
new PointerEventData(es), ExecuteEvents.pointerEnterHandler);
}
private static void Click(Selectable sel)
{
var es = EventSystem.current;
if (es == null)
{
Debug.LogWarning("[VRPointer] EventSystem.current is null — click 실패");
return;
}
var eventData = new PointerEventData(es);
ExecuteEvents.Execute(sel.gameObject, eventData, ExecuteEvents.pointerDownHandler);
ExecuteEvents.Execute(sel.gameObject, eventData, ExecuteEvents.pointerUpHandler);
ExecuteEvents.Execute(sel.gameObject, eventData, ExecuteEvents.pointerClickHandler);
var btn = sel.GetComponent<Button>();
if (btn != null) btn.onClick.Invoke();
}
private void DrawLine(float hitDist)
{
Color c = _currentHover != null ? HoverColor : NormalColor;
_line.startColor = c;
_line.endColor = new Color(c.r, c.g, c.b, 0f);
_line.SetPosition(0, transform.position);
_line.SetPosition(1, transform.position + transform.forward * hitDist);
}
private static void DebugRaycastAttempt(Ray ray)
{
var all = Selectable.allSelectablesArray;
Debug.Log($"[VRPointer:DEBUG] Selectable 총 {all.Length}개 | ray.origin={ray.origin:F2} ray.dir={ray.direction:F2}");
foreach (Selectable sel in all)
{
if (!sel.gameObject.activeInHierarchy) { Debug.Log($" SKIP(비활성) {sel.gameObject.name}"); continue; }
if (!sel.interactable) { Debug.Log($" SKIP(interactable=false) {sel.gameObject.name}"); continue; }
var rt = sel.GetComponent<RectTransform>();
if (rt == null) { Debug.Log($" SKIP(RectTransform없음) {sel.gameObject.name}"); continue; }
Vector3[] c = new Vector3[4];
rt.GetWorldCorners(c);
Vector3 normal = rt.forward;
if (Vector3.Dot(normal, ray.direction) >= 0f) normal = -normal;
Plane plane = new Plane(normal, c[0]);
bool hit = plane.Raycast(ray, out float dist);
if (!hit) { Debug.Log($" MISS(Plane.Raycast=false) {sel.gameObject.name} | rtPos={rt.position:F2} normal={normal:F2}"); continue; }
bool inRect = IsPointInRect(ray.GetPoint(dist), c);
Debug.Log($" {(inRect ? "HIT" : "MISS(InRect=false)")} {sel.gameObject.name} | dist={dist:F2} inRect={inRect}");
}
}
private static Selectable FindSelectableUnderRay(Ray ray, ref float maxDist)
{
Selectable closest = null;
float closestDist = maxDist;
foreach (Selectable sel in Selectable.allSelectablesArray)
{
if (!sel.gameObject.activeInHierarchy || !sel.interactable) continue;
if (!IsOnEnabledCanvas(sel)) continue;
var rt = sel.GetComponent<RectTransform>();
if (rt == null) continue;
Vector3[] corners = new Vector3[4];
rt.GetWorldCorners(corners);
// Plane.Raycast은 normal 쪽(앞면)에서 레이가 출발해야 true 반환.
// rt.forward가 카메라 반대를 향할 수 있으므로 레이 원점 방향으로 노말 보정.
Vector3 normal = rt.forward;
if (Vector3.Dot(normal, ray.direction) >= 0f)
normal = -normal;
Plane plane = new Plane(normal, corners[0]);
if (!plane.Raycast(ray, out float dist)) continue;
if (dist >= closestDist || dist <= 0f) continue;
if (!IsPointInRect(ray.GetPoint(dist), corners)) continue;
closestDist = dist;
closest = sel;
}
maxDist = closestDist;
return closest;
}
private static bool IsPointInRect(Vector3 p, Vector3[] c)
{
Vector3 toP = p - c[0];
Vector3 right = c[3] - c[0];
Vector3 up = c[1] - c[0];
float r = Vector3.Dot(toP, right) / right.sqrMagnitude;
float u = Vector3.Dot(toP, up) / up.sqrMagnitude;
return r >= 0f && r <= 1f && u >= 0f && u <= 1f;
}
private static ScrollRect FindScrollRectUnderRay(Ray ray, ref float maxDist)
{
ScrollRect closest = null;
float closestDist = maxDist;
var all = Object.FindObjectsByType<ScrollRect>(FindObjectsSortMode.None);
foreach (ScrollRect scroll in all)
{
if (!scroll.isActiveAndEnabled) continue;
if (!IsOnEnabledCanvas(scroll)) continue;
RectTransform rt = scroll.viewport != null
? scroll.viewport
: scroll.GetComponent<RectTransform>();
if (rt == null) continue;
Vector3[] corners = new Vector3[4];
rt.GetWorldCorners(corners);
Vector3 normal = rt.forward;
if (Vector3.Dot(normal, ray.direction) >= 0f)
normal = -normal;
Plane plane = new Plane(normal, corners[0]);
if (!plane.Raycast(ray, out float dist)) continue;
if (dist >= closestDist || dist <= 0f) continue;
if (!IsPointInRect(ray.GetPoint(dist), corners)) continue;
closestDist = dist;
closest = scroll;
}
if (closest != null)
maxDist = closestDist;
return closest;
}
private static bool IsOnEnabledCanvas(Component component)
{
Canvas[] canvases = component.GetComponentsInParent<Canvas>(true);
if (canvases.Length == 0)
return true;
for (int i = 0; i < canvases.Length; i++)
{
Canvas canvas = canvases[i];
if (canvas == null)
continue;
if (!canvas.enabled || !canvas.gameObject.activeInHierarchy)
return false;
}
return true;
}
private void HandleScroll(ScrollRect scrollRect)
{
if (!CanScrollVertically(scrollRect))
return;
Vector2 axis = GetAxis(CommonUsages.primary2DAxis);
if (Mathf.Abs(axis.y) < scrollDeadZone)
return;
scrollRect.verticalNormalizedPosition = Mathf.Clamp01(
scrollRect.verticalNormalizedPosition + axis.y * scrollSpeed * Time.deltaTime);
}
private bool TryBeginScrollDrag(ScrollRect scrollRect, Selectable pressSelectable, Ray ray)
{
if (!CanScrollVertically(scrollRect))
return false;
if (!TryGetScrollLocalPoint(scrollRect, ray, out Vector2 localPoint, out _))
return false;
_dragScrollRect = scrollRect;
_triggerPressSelectable = pressSelectable;
_dragStartLocalPoint = localPoint;
_dragStartNormalizedPosition = scrollRect.verticalNormalizedPosition;
_dragMaxNormalizedDelta = 0f;
return true;
}
private void UpdateScrollDrag(Ray ray)
{
if (_dragScrollRect == null)
return;
if (!TryGetScrollLocalPoint(_dragScrollRect, ray, out Vector2 localPoint, out float viewportHeight))
return;
float deltaY = localPoint.y - _dragStartLocalPoint.y;
float normalizedDelta = deltaY / viewportHeight * dragScrollSpeed;
_dragMaxNormalizedDelta = Mathf.Max(_dragMaxNormalizedDelta, Mathf.Abs(normalizedDelta));
_dragScrollRect.verticalNormalizedPosition = Mathf.Clamp01(
_dragStartNormalizedPosition - normalizedDelta);
}
private void EndScrollDrag(string hand, Ray ray)
{
bool shouldClick = _dragMaxNormalizedDelta < dragClickThreshold;
ScrollRect scrollRect = _dragScrollRect;
Selectable pressSelectable = _triggerPressSelectable;
float startNormalizedPosition = _dragStartNormalizedPosition;
ClearScrollDrag();
if (!shouldClick)
return;
if (scrollRect != null)
scrollRect.verticalNormalizedPosition = startNormalizedPosition;
if (pressSelectable != null && pressSelectable.isActiveAndEnabled && pressSelectable.interactable)
{
if (debugLogging)
Debug.Log($"[VRPointer:{hand}] CLICK [검지 트리거] → {pressSelectable.gameObject.name}");
Click(pressSelectable);
}
else if (debugLogging)
{
Debug.Log($"[VRPointer:{hand}] CLICK — 레이 아래 버튼 없음 " +
$"pos={transform.position:F2} fwd={transform.forward:F2}");
DebugRaycastAttempt(ray);
}
}
private void ClearScrollDrag()
{
_dragScrollRect = null;
_triggerPressSelectable = null;
_dragStartLocalPoint = Vector2.zero;
_dragStartNormalizedPosition = 0f;
_dragMaxNormalizedDelta = 0f;
}
private static bool CanScrollVertically(ScrollRect scrollRect)
{
if (scrollRect == null || !scrollRect.vertical)
return false;
RectTransform viewport = scrollRect.viewport != null
? scrollRect.viewport
: scrollRect.GetComponent<RectTransform>();
if (viewport == null || scrollRect.content == null)
return true;
return scrollRect.content.rect.height > viewport.rect.height + 1f;
}
private static bool TryGetScrollLocalPoint(ScrollRect scrollRect, Ray ray, out Vector2 localPoint, out float viewportHeight)
{
localPoint = Vector2.zero;
viewportHeight = 1f;
RectTransform rt = scrollRect.viewport != null
? scrollRect.viewport
: scrollRect.GetComponent<RectTransform>();
if (rt == null)
return false;
Vector3[] corners = new Vector3[4];
rt.GetWorldCorners(corners);
Vector3 normal = rt.forward;
if (Vector3.Dot(normal, ray.direction) >= 0f)
normal = -normal;
Plane plane = new Plane(normal, corners[0]);
if (!plane.Raycast(ray, out float dist) || dist <= 0f)
return false;
Vector3 local = rt.InverseTransformPoint(ray.GetPoint(dist));
localPoint = new Vector2(local.x, local.y);
viewportHeight = Mathf.Max(1f, rt.rect.height);
return true;
}
private bool GetButton(InputFeatureUsage<bool> usage)
{
var chars = InputDeviceCharacteristics.Controller |
(isRightHand
? InputDeviceCharacteristics.Right
: InputDeviceCharacteristics.Left);
var devices = new List<InputDevice>();
InputDevices.GetDevicesWithCharacteristics(chars, devices);
if (devices.Count == 0) return false;
devices[0].TryGetFeatureValue(usage, out bool pressed);
return pressed;
}
private Vector2 GetAxis(InputFeatureUsage<Vector2> usage)
{
var chars = InputDeviceCharacteristics.Controller |
(isRightHand
? InputDeviceCharacteristics.Right
: InputDeviceCharacteristics.Left);
var devices = new List<InputDevice>();
InputDevices.GetDevicesWithCharacteristics(chars, devices);
if (devices.Count == 0) return Vector2.zero;
devices[0].TryGetFeatureValue(usage, out Vector2 axis);
return axis;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: eec74ab726176d74491e9be716a7a609
+114
View File
@@ -0,0 +1,114 @@
using UnityEngine;
using UnityEngine.SceneManagement;
namespace VRBeats
{
// 모든 씬에서 자동 실행.
// Game 씬: VRPointerController를 비활성 상태로 추가 → VR_InteractorController가 게임오버 시 활성화.
// 나머지 씬: 바로 활성 상태로 추가.
public class VRPointerSetup : MonoBehaviour
{
private static VRPointerSetup instance;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void ResetStatics()
{
instance = null;
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void AutoInject()
{
if (instance != null)
return;
var go = new GameObject("[VRPointerSetup]");
go.AddComponent<VRPointerSetup>();
}
private void Awake()
{
if (instance != null && instance != this)
{
Destroy(gameObject);
return;
}
instance = this;
DontDestroyOnLoad(gameObject);
}
private void OnEnable()
{
SceneManager.sceneLoaded += OnSceneLoaded;
}
private void OnDisable()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
}
private void Start()
{
SetupActiveScene();
}
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
SetupScene(scene);
}
private static void SetupActiveScene()
{
SetupScene(SceneManager.GetActiveScene());
}
private static void SetupScene(Scene scene)
{
bool isGameScene = scene.name == "Game";
SetupControllers(disabledByDefault: isGameScene);
}
private static void SetupControllers(bool disabledByDefault)
{
foreach (var go in FindObjectsByType<GameObject>(FindObjectsSortMode.None))
{
string name = go.name;
bool isRight = name.Contains("Right");
bool isLeft = name.Contains("Left");
if (!isRight && !isLeft) continue;
if (!name.Contains("Controller") && !name.Contains("Hand")) continue;
if (go.GetComponent<LineRenderer>() == null) continue;
DisableToolkitPointerComponents(go);
if (go.GetComponent<VRPointerController>() != null) continue;
var pointer = go.AddComponent<VRPointerController>();
var field = typeof(VRPointerController)
.GetField("isRightHand",
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance);
field?.SetValue(pointer, isRight);
// Game 씬에서는 게임오버 전까지 비활성
if (disabledByDefault)
pointer.enabled = false;
}
}
private static void DisableToolkitPointerComponents(GameObject go)
{
var rayInteractor = go.GetComponent<UnityEngine.XR.Interaction.Toolkit.Interactors.XRRayInteractor>();
if (rayInteractor != null)
rayInteractor.enabled = false;
var lineVisual = go.GetComponent<UnityEngine.XR.Interaction.Toolkit.Interactors.Visuals.XRInteractorLineVisual>();
if (lineVisual != null)
lineVisual.enabled = false;
}
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 38f89babd4e99734aac47edbc4f87aa3
+31
View File
@@ -0,0 +1,31 @@
using UnityEngine;
// Automatically spawns the XR Interaction Simulator when running in the Editor or on PC.
// Add this to any persistent GameObject in the Menu scene (e.g. VR_Manager).
//
// Setup:
// 1. Attach this script to VR_Manager (or any root object) in Menu.unity
// 2. In Package Manager → XR Interaction Toolkit → Samples → import "XR Interaction Simulator"
// 3. Drag the imported prefab into the SimulatorPrefab field:
// Assets/Samples/XR Interaction Toolkit/<version>/XR Interaction Simulator/XR Interaction Simulator.prefab
//
// Controls (XR Interaction Simulator):
// Right-click drag — rotate head
// G + mouse move — move right controller
// Shift+G — left controller
// Space — trigger (UI click)
public class XRSimulatorLoader : MonoBehaviour
{
[SerializeField] private GameObject simulatorPrefab;
private void Awake()
{
#if !UNITY_ANDROID || UNITY_EDITOR
if (simulatorPrefab != null)
Instantiate(simulatorPrefab);
else
Debug.LogWarning("[XRSimulatorLoader] simulatorPrefab is not assigned.\n" +
"Import the XR Interaction Simulator sample via Package Manager and assign the prefab.");
#endif
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 39a535ec9b2d18e489709431e0c25086
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 709f11a7f3c4041caa4ef136ea32d874
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+893
View File
@@ -0,0 +1,893 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-9167874883656233139
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5485954d14dfb9a4c8ead8edb0ded5b1, type: 3}
m_Name: LiftGammaGain
m_EditorClassIdentifier:
active: 1
lift:
m_OverrideState: 1
m_Value: {x: 1, y: 1, z: 1, w: 0}
gamma:
m_OverrideState: 1
m_Value: {x: 1, y: 1, z: 1, w: 0}
gain:
m_OverrideState: 1
m_Value: {x: 1, y: 1, z: 1, w: 0}
--- !u!114 &-8270506406425502121
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 70afe9e12c7a7ed47911bb608a23a8ff, type: 3}
m_Name: SplitToning
m_EditorClassIdentifier:
active: 1
shadows:
m_OverrideState: 1
m_Value: {r: 0.5, g: 0.5, b: 0.5, a: 1}
highlights:
m_OverrideState: 1
m_Value: {r: 0.5, g: 0.5, b: 0.5, a: 1}
balance:
m_OverrideState: 1
m_Value: 0
--- !u!114 &-7750755424749557576
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 60f3b30c03e6ba64d9a27dc9dba8f28d, type: 3}
m_Name: OutlineVolumeComponent
m_EditorClassIdentifier:
active: 1
Enabled:
m_OverrideState: 1
m_Value: 0
--- !u!114 &-7743500325797982168
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: ccf1aba9553839d41ae37dd52e9ebcce, type: 3}
m_Name: MotionBlur
m_EditorClassIdentifier:
active: 1
mode:
m_OverrideState: 1
m_Value: 0
quality:
m_OverrideState: 1
m_Value: 0
intensity:
m_OverrideState: 1
m_Value: 0
clamp:
m_OverrideState: 1
m_Value: 0.05
--- !u!114 &-7274224791359825572
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 0fd9ee276a1023e439cf7a9c393195fa, type: 3}
m_Name: TestAnimationCurveVolumeComponent
m_EditorClassIdentifier:
active: 1
testParameter:
m_OverrideState: 1
m_Value:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0.5
value: 10
inSlope: 0
outSlope: 10
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 15
inSlope: 10
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
--- !u!114 &-6335409530604852063
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 66f335fb1ffd8684294ad653bf1c7564, type: 3}
m_Name: ColorAdjustments
m_EditorClassIdentifier:
active: 1
postExposure:
m_OverrideState: 1
m_Value: 0
contrast:
m_OverrideState: 1
m_Value: 0
colorFilter:
m_OverrideState: 1
m_Value: {r: 1, g: 1, b: 1, a: 1}
hueShift:
m_OverrideState: 1
m_Value: 0
saturation:
m_OverrideState: 1
m_Value: 0
--- !u!114 &-6288072647309666549
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 29fa0085f50d5e54f8144f766051a691, type: 3}
m_Name: FilmGrain
m_EditorClassIdentifier:
active: 1
type:
m_OverrideState: 1
m_Value: 0
intensity:
m_OverrideState: 1
m_Value: 0
response:
m_OverrideState: 1
m_Value: 0.8
texture:
m_OverrideState: 1
m_Value: {fileID: 0}
--- !u!114 &-5520245016509672950
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 97c23e3b12dc18c42a140437e53d3951, type: 3}
m_Name: Tonemapping
m_EditorClassIdentifier:
active: 1
mode:
m_OverrideState: 1
m_Value: 0
neutralHDRRangeReductionMode:
m_OverrideState: 1
m_Value: 2
acesPreset:
m_OverrideState: 1
m_Value: 3
hueShiftAmount:
m_OverrideState: 1
m_Value: 0
detectPaperWhite:
m_OverrideState: 1
m_Value: 0
paperWhite:
m_OverrideState: 1
m_Value: 300
detectBrightnessLimits:
m_OverrideState: 1
m_Value: 1
minNits:
m_OverrideState: 1
m_Value: 0.005
maxNits:
m_OverrideState: 1
m_Value: 1000
--- !u!114 &-5139089513906902183
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5a00a63fdd6bd2a45ab1f2d869305ffd, type: 3}
m_Name: OasisFogVolumeComponent
m_EditorClassIdentifier:
active: 1
Density:
m_OverrideState: 1
m_Value: 0
StartDistance:
m_OverrideState: 1
m_Value: 0
HeightRange:
m_OverrideState: 1
m_Value: {x: 0, y: 50}
Tint:
m_OverrideState: 1
m_Value: {r: 1, g: 1, b: 1, a: 1}
SunScatteringIntensity:
m_OverrideState: 1
m_Value: 2
--- !u!114 &-4463884970436517307
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fb60a22f311433c4c962b888d1393f88, type: 3}
m_Name: PaniniProjection
m_EditorClassIdentifier:
active: 1
distance:
m_OverrideState: 1
m_Value: 0
cropToFit:
m_OverrideState: 1
m_Value: 1
--- !u!114 &-1410297666881709256
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 6bd486065ce11414fa40e631affc4900, type: 3}
m_Name: ProbeVolumesOptions
m_EditorClassIdentifier:
active: 1
normalBias:
m_OverrideState: 1
m_Value: 0.33
viewBias:
m_OverrideState: 1
m_Value: 0
scaleBiasWithMinProbeDistance:
m_OverrideState: 1
m_Value: 0
samplingNoise:
m_OverrideState: 1
m_Value: 0.1
animateSamplingNoise:
m_OverrideState: 1
m_Value: 1
leakReductionMode:
m_OverrideState: 1
m_Value: 1
minValidDotProductValue:
m_OverrideState: 1
m_Value: 0.1
occlusionOnlyReflectionNormalization:
m_OverrideState: 1
m_Value: 1
intensityMultiplier:
m_OverrideState: 1
m_Value: 1
skyOcclusionIntensityMultiplier:
m_OverrideState: 1
m_Value: 1
--- !u!114 &-1216621516061285780
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 0b2db86121404754db890f4c8dfe81b2, type: 3}
m_Name: Bloom
m_EditorClassIdentifier:
active: 1
skipIterations:
m_OverrideState: 1
m_Value: 1
threshold:
m_OverrideState: 1
m_Value: 0.9
intensity:
m_OverrideState: 1
m_Value: 0
scatter:
m_OverrideState: 1
m_Value: 0.7
clamp:
m_OverrideState: 1
m_Value: 65472
tint:
m_OverrideState: 1
m_Value: {r: 1, g: 1, b: 1, a: 1}
highQualityFiltering:
m_OverrideState: 1
m_Value: 0
downscale:
m_OverrideState: 1
m_Value: 0
maxIterations:
m_OverrideState: 1
m_Value: 6
dirtTexture:
m_OverrideState: 1
m_Value: {fileID: 0}
dimension: 1
dirtIntensity:
m_OverrideState: 1
m_Value: 0
--- !u!114 &-1170528603972255243
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 221518ef91623a7438a71fef23660601, type: 3}
m_Name: WhiteBalance
m_EditorClassIdentifier:
active: 1
temperature:
m_OverrideState: 1
m_Value: 0
tint:
m_OverrideState: 1
m_Value: 0
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d7fd9488000d3734a9e00ee676215985, type: 3}
m_Name: DefaultVolumeProfile
m_EditorClassIdentifier:
components:
- {fileID: -9167874883656233139}
- {fileID: 1918650496244738858}
- {fileID: 853819529557874667}
- {fileID: 1052315754049611418}
- {fileID: -1170528603972255243}
- {fileID: -8270506406425502121}
- {fileID: -5520245016509672950}
- {fileID: 7173750748008157695}
- {fileID: 1666464333004379222}
- {fileID: 9001657382290151224}
- {fileID: -6335409530604852063}
- {fileID: -1216621516061285780}
- {fileID: 3959858460715838825}
- {fileID: -7743500325797982168}
- {fileID: 4644742534064026673}
- {fileID: -4463884970436517307}
- {fileID: -6288072647309666549}
- {fileID: 7518938298396184218}
- {fileID: -1410297666881709256}
- {fileID: -7750755424749557576}
- {fileID: -5139089513906902183}
--- !u!114 &853819529557874667
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 06437c1ff663d574d9447842ba0a72e4, type: 3}
m_Name: ScreenSpaceLensFlare
m_EditorClassIdentifier:
active: 1
intensity:
m_OverrideState: 1
m_Value: 0
tintColor:
m_OverrideState: 1
m_Value: {r: 1, g: 1, b: 1, a: 1}
bloomMip:
m_OverrideState: 1
m_Value: 1
firstFlareIntensity:
m_OverrideState: 1
m_Value: 1
secondaryFlareIntensity:
m_OverrideState: 1
m_Value: 1
warpedFlareIntensity:
m_OverrideState: 1
m_Value: 1
warpedFlareScale:
m_OverrideState: 1
m_Value: {x: 1, y: 1}
samples:
m_OverrideState: 1
m_Value: 1
sampleDimmer:
m_OverrideState: 1
m_Value: 0.5
vignetteEffect:
m_OverrideState: 1
m_Value: 1
startingPosition:
m_OverrideState: 1
m_Value: 1.25
scale:
m_OverrideState: 1
m_Value: 1.5
streaksIntensity:
m_OverrideState: 1
m_Value: 0
streaksLength:
m_OverrideState: 1
m_Value: 0.5
streaksOrientation:
m_OverrideState: 1
m_Value: 0
streaksThreshold:
m_OverrideState: 1
m_Value: 0.25
resolution:
m_OverrideState: 1
m_Value: 4
chromaticAbberationIntensity:
m_OverrideState: 1
m_Value: 0.5
--- !u!114 &1052315754049611418
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 558a8e2b6826cf840aae193990ba9f2e, type: 3}
m_Name: ShadowsMidtonesHighlights
m_EditorClassIdentifier:
active: 1
shadows:
m_OverrideState: 1
m_Value: {x: 1, y: 1, z: 1, w: 0}
midtones:
m_OverrideState: 1
m_Value: {x: 1, y: 1, z: 1, w: 0}
highlights:
m_OverrideState: 1
m_Value: {x: 1, y: 1, z: 1, w: 0}
shadowsStart:
m_OverrideState: 1
m_Value: 0
shadowsEnd:
m_OverrideState: 1
m_Value: 0.3
highlightsStart:
m_OverrideState: 1
m_Value: 0.55
highlightsEnd:
m_OverrideState: 1
m_Value: 1
--- !u!114 &1666464333004379222
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3eb4b772797da9440885e8bd939e9560, type: 3}
m_Name: ColorCurves
m_EditorClassIdentifier:
active: 1
master:
m_OverrideState: 1
m_Value:
<length>k__BackingField: 2
m_Loop: 0
m_ZeroValue: 0
m_Range: 1
m_Curve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 1
outSlope: 1
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 1
inSlope: 1
outSlope: 1
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
red:
m_OverrideState: 1
m_Value:
<length>k__BackingField: 2
m_Loop: 0
m_ZeroValue: 0
m_Range: 1
m_Curve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 1
outSlope: 1
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 1
inSlope: 1
outSlope: 1
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
green:
m_OverrideState: 1
m_Value:
<length>k__BackingField: 2
m_Loop: 0
m_ZeroValue: 0
m_Range: 1
m_Curve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 1
outSlope: 1
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 1
inSlope: 1
outSlope: 1
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
blue:
m_OverrideState: 1
m_Value:
<length>k__BackingField: 2
m_Loop: 0
m_ZeroValue: 0
m_Range: 1
m_Curve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 1
outSlope: 1
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 1
value: 1
inSlope: 1
outSlope: 1
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
hueVsHue:
m_OverrideState: 1
m_Value:
<length>k__BackingField: 0
m_Loop: 1
m_ZeroValue: 0.5
m_Range: 1
m_Curve:
serializedVersion: 2
m_Curve: []
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
hueVsSat:
m_OverrideState: 1
m_Value:
<length>k__BackingField: 0
m_Loop: 1
m_ZeroValue: 0.5
m_Range: 1
m_Curve:
serializedVersion: 2
m_Curve: []
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
satVsSat:
m_OverrideState: 1
m_Value:
<length>k__BackingField: 0
m_Loop: 0
m_ZeroValue: 0.5
m_Range: 1
m_Curve:
serializedVersion: 2
m_Curve: []
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
lumVsSat:
m_OverrideState: 1
m_Value:
<length>k__BackingField: 0
m_Loop: 0
m_ZeroValue: 0.5
m_Range: 1
m_Curve:
serializedVersion: 2
m_Curve: []
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
--- !u!114 &1918650496244738858
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e021b4c809a781e468c2988c016ebbea, type: 3}
m_Name: ColorLookup
m_EditorClassIdentifier:
active: 1
texture:
m_OverrideState: 1
m_Value: {fileID: 0}
dimension: 1
contribution:
m_OverrideState: 1
m_Value: 0
--- !u!114 &3959858460715838825
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c01700fd266d6914ababb731e09af2eb, type: 3}
m_Name: DepthOfField
m_EditorClassIdentifier:
active: 1
mode:
m_OverrideState: 1
m_Value: 0
gaussianStart:
m_OverrideState: 1
m_Value: 10
gaussianEnd:
m_OverrideState: 1
m_Value: 30
gaussianMaxRadius:
m_OverrideState: 1
m_Value: 1
highQualitySampling:
m_OverrideState: 1
m_Value: 0
focusDistance:
m_OverrideState: 1
m_Value: 10
aperture:
m_OverrideState: 1
m_Value: 5.6
focalLength:
m_OverrideState: 1
m_Value: 50
bladeCount:
m_OverrideState: 1
m_Value: 5
bladeCurvature:
m_OverrideState: 1
m_Value: 1
bladeRotation:
m_OverrideState: 1
m_Value: 0
--- !u!114 &4251301726029935498
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 74955a4b0b4243bc87231e8b59ed9140, type: 3}
m_Name: TestVolume
m_EditorClassIdentifier:
active: 1
param:
m_OverrideState: 1
m_Value: 123
--- !u!114 &4644742534064026673
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 81180773991d8724ab7f2d216912b564, type: 3}
m_Name: ChromaticAberration
m_EditorClassIdentifier:
active: 1
intensity:
m_OverrideState: 1
m_Value: 0
--- !u!114 &7173750748008157695
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 899c54efeace73346a0a16faa3afe726, type: 3}
m_Name: Vignette
m_EditorClassIdentifier:
active: 1
color:
m_OverrideState: 1
m_Value: {r: 0, g: 0, b: 0, a: 1}
center:
m_OverrideState: 1
m_Value: {x: 0.5, y: 0.5}
intensity:
m_OverrideState: 1
m_Value: 0
smoothness:
m_OverrideState: 1
m_Value: 0.2
rounded:
m_OverrideState: 1
m_Value: 0
--- !u!114 &7518938298396184218
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c5e1dc532bcb41949b58bc4f2abfbb7e, type: 3}
m_Name: LensDistortion
m_EditorClassIdentifier:
active: 1
intensity:
m_OverrideState: 1
m_Value: 0
xMultiplier:
m_OverrideState: 1
m_Value: 1
yMultiplier:
m_OverrideState: 1
m_Value: 1
center:
m_OverrideState: 1
m_Value: {x: 0.5, y: 0.5}
scale:
m_OverrideState: 1
m_Value: 1
--- !u!114 &9001657382290151224
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: cdfbdbb87d3286943a057f7791b43141, type: 3}
m_Name: ChannelMixer
m_EditorClassIdentifier:
active: 1
redOutRedIn:
m_OverrideState: 1
m_Value: 100
redOutGreenIn:
m_OverrideState: 1
m_Value: 0
redOutBlueIn:
m_OverrideState: 1
m_Value: 0
greenOutRedIn:
m_OverrideState: 1
m_Value: 0
greenOutGreenIn:
m_OverrideState: 1
m_Value: 100
greenOutBlueIn:
m_OverrideState: 1
m_Value: 0
blueOutRedIn:
m_OverrideState: 1
m_Value: 0
blueOutGreenIn:
m_OverrideState: 1
m_Value: 0
blueOutBlueIn:
m_OverrideState: 1
m_Value: 100
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ab09877e2e707104187f6f83e2f62510
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
+143
View File
@@ -0,0 +1,143 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bf2edee5c58d82540a51f03df9d42094, type: 3}
m_Name: Mobile_RPAsset
m_EditorClassIdentifier:
k_AssetVersion: 13
k_AssetPreviousVersion: 13
m_RendererType: 1
m_RendererData: {fileID: 0}
m_RendererDataList:
- {fileID: 11400000, guid: 65bc7dbf4170f435aa868c779acfb082, type: 2}
m_DefaultRendererIndex: 0
m_RequireDepthTexture: 0
m_RequireOpaqueTexture: 0
m_OpaqueDownsampling: 0
m_SupportsTerrainHoles: 1
m_SupportsHDR: 1
m_HDRColorBufferPrecision: 0
m_MSAA: 1
m_RenderScale: 0.8
m_UpscalingFilter: 0
m_FsrOverrideSharpness: 0
m_FsrSharpness: 0.92
m_EnableLODCrossFade: 1
m_LODCrossFadeDitheringType: 1
m_ShEvalMode: 0
m_LightProbeSystem: 0
m_ProbeVolumeMemoryBudget: 1024
m_ProbeVolumeBlendingMemoryBudget: 256
m_SupportProbeVolumeGPUStreaming: 0
m_SupportProbeVolumeDiskStreaming: 0
m_SupportProbeVolumeScenarios: 0
m_SupportProbeVolumeScenarioBlending: 0
m_ProbeVolumeSHBands: 1
m_MainLightRenderingMode: 1
m_MainLightShadowsSupported: 1
m_MainLightShadowmapResolution: 1024
m_AdditionalLightsRenderingMode: 1
m_AdditionalLightsPerObjectLimit: 4
m_AdditionalLightShadowsSupported: 0
m_AdditionalLightsShadowmapResolution: 2048
m_AdditionalLightsShadowResolutionTierLow: 256
m_AdditionalLightsShadowResolutionTierMedium: 512
m_AdditionalLightsShadowResolutionTierHigh: 1024
m_ReflectionProbeBlending: 1
m_ReflectionProbeBoxProjection: 1
m_ReflectionProbeAtlas: 1
m_ShadowDistance: 50
m_ShadowCascadeCount: 1
m_Cascade2Split: 0.25
m_Cascade3Split: {x: 0.1, y: 0.3}
m_Cascade4Split: {x: 0.067, y: 0.2, z: 0.467}
m_CascadeBorder: 0.2
m_ShadowDepthBias: 1
m_ShadowNormalBias: 1
m_AnyShadowsSupported: 1
m_SoftShadowsSupported: 0
m_ConservativeEnclosingSphere: 1
m_NumIterationsEnclosingSphere: 64
m_SoftShadowQuality: 2
m_AdditionalLightsCookieResolution: 1024
m_AdditionalLightsCookieFormat: 1
m_UseSRPBatcher: 1
m_SupportsDynamicBatching: 0
m_MixedLightingSupported: 1
m_SupportsLightCookies: 1
m_SupportsLightLayers: 1
m_DebugLevel: 0
m_StoreActionsOptimization: 0
m_UseAdaptivePerformance: 1
m_ColorGradingMode: 0
m_ColorGradingLutSize: 32
m_AllowPostProcessAlphaOutput: 0
m_UseFastSRGBLinearConversion: 1
m_SupportDataDrivenLensFlare: 1
m_SupportScreenSpaceLensFlare: 1
m_GPUResidentDrawerMode: 0
m_SmallMeshScreenPercentage: 0
m_GPUResidentDrawerEnableOcclusionCullingInCameras: 0
m_ShadowType: 1
m_LocalShadowsSupported: 0
m_LocalShadowsAtlasResolution: 256
m_MaxPixelLights: 0
m_ShadowAtlasResolution: 256
m_VolumeFrameworkUpdateMode: 0
m_VolumeProfile: {fileID: 11400000, guid: 10fc4df2da32a41aaa32d77bc913491c, type: 2}
apvScenesData:
obsoleteSceneBounds:
m_Keys: []
m_Values: []
obsoleteHasProbeVolumes:
m_Keys: []
m_Values:
m_PrefilteringModeMainLightShadows: 3
m_PrefilteringModeAdditionalLight: 4
m_PrefilteringModeAdditionalLightShadows: 0
m_PrefilterXRKeywords: 1
m_PrefilteringModeForwardPlus: 1
m_PrefilteringModeDeferredRendering: 0
m_PrefilteringModeScreenSpaceOcclusion: 0
m_PrefilterDebugKeywords: 1
m_PrefilterWriteRenderingLayers: 1
m_PrefilterHDROutput: 1
m_PrefilterAlphaOutput: 0
m_PrefilterSSAODepthNormals: 1
m_PrefilterSSAOSourceDepthLow: 1
m_PrefilterSSAOSourceDepthMedium: 0
m_PrefilterSSAOSourceDepthHigh: 1
m_PrefilterSSAOInterleaved: 0
m_PrefilterSSAOBlueNoise: 1
m_PrefilterSSAOSampleCountLow: 1
m_PrefilterSSAOSampleCountMedium: 0
m_PrefilterSSAOSampleCountHigh: 1
m_PrefilterDBufferMRT1: 1
m_PrefilterDBufferMRT2: 1
m_PrefilterDBufferMRT3: 1
m_PrefilterSoftShadowsQualityLow: 1
m_PrefilterSoftShadowsQualityMedium: 1
m_PrefilterSoftShadowsQualityHigh: 1
m_PrefilterSoftShadows: 0
m_PrefilterScreenCoord: 1
m_PrefilterScreenSpaceIrradiance: 0
m_PrefilterNativeRenderPass: 1
m_PrefilterUseLegacyLightmaps: 0
m_PrefilterBicubicLightmapSampling: 0
m_PrefilterReflectionProbeRotation: 0
m_PrefilterReflectionProbeBlending: 0
m_PrefilterReflectionProbeBoxProjection: 0
m_PrefilterReflectionProbeAtlas: 0
m_ShaderVariantLogLevel: 0
m_ShadowCascades: 0
m_Textures:
blueNoise64LTex: {fileID: 2800000, guid: e3d24661c1e055f45a7560c033dbb837, type: 3}
bayerMatrixTex: {fileID: 2800000, guid: f9ee4ed84c1d10c49aabb9b210b0fc44, type: 3}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5e6cbd92db86f4b18aec3ed561671858
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
+52
View File
@@ -0,0 +1,52 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: de640fe3d0db1804a85f9fc8f5cadab6, type: 3}
m_Name: Mobile_Renderer
m_EditorClassIdentifier:
debugShaders:
debugReplacementPS: {fileID: 4800000, guid: cf852408f2e174538bcd9b7fda1c5ae7,
type: 3}
hdrDebugViewPS: {fileID: 4800000, guid: 573620ae32aec764abd4d728906d2587, type: 3}
probeVolumeSamplingDebugComputeShader: {fileID: 7200000, guid: 53626a513ea68ce47b59dc1299fe3959,
type: 3}
probeVolumeResources:
probeVolumeDebugShader: {fileID: 0}
probeVolumeFragmentationDebugShader: {fileID: 0}
probeVolumeOffsetDebugShader: {fileID: 0}
probeVolumeSamplingDebugShader: {fileID: 0}
probeSamplingDebugMesh: {fileID: 0}
probeSamplingDebugTexture: {fileID: 0}
probeVolumeBlendStatesCS: {fileID: 0}
m_RendererFeatures: []
m_RendererFeatureMap:
m_UseNativeRenderPass: 1
postProcessData: {fileID: 11400000, guid: 41439944d30ece34e96484bdb6645b55, type: 2}
m_AssetVersion: 2
m_OpaqueLayerMask:
serializedVersion: 2
m_Bits: 4294967295
m_TransparentLayerMask:
serializedVersion: 2
m_Bits: 4294967295
m_DefaultStencilState:
overrideStencilState: 0
stencilReference: 0
stencilCompareFunction: 8
passOperation: 2
failOperation: 0
zFailOperation: 0
m_ShadowTransparentReceive: 0
m_RenderingMode: 0
m_DepthPrimingMode: 0
m_CopyDepthMode: 0
m_AccurateGbufferNormals: 0
m_IntermediateTextureMode: 0
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 65bc7dbf4170f435aa868c779acfb082
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
+143
View File
@@ -0,0 +1,143 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bf2edee5c58d82540a51f03df9d42094, type: 3}
m_Name: PC_RPAsset
m_EditorClassIdentifier:
k_AssetVersion: 13
k_AssetPreviousVersion: 13
m_RendererType: 1
m_RendererData: {fileID: 0}
m_RendererDataList:
- {fileID: 11400000, guid: f288ae1f4751b564a96ac7587541f7a2, type: 2}
m_DefaultRendererIndex: 0
m_RequireDepthTexture: 1
m_RequireOpaqueTexture: 1
m_OpaqueDownsampling: 1
m_SupportsTerrainHoles: 1
m_SupportsHDR: 1
m_HDRColorBufferPrecision: 0
m_MSAA: 1
m_RenderScale: 1
m_UpscalingFilter: 0
m_FsrOverrideSharpness: 0
m_FsrSharpness: 0.92
m_EnableLODCrossFade: 1
m_LODCrossFadeDitheringType: 1
m_ShEvalMode: 0
m_LightProbeSystem: 0
m_ProbeVolumeMemoryBudget: 1024
m_ProbeVolumeBlendingMemoryBudget: 256
m_SupportProbeVolumeGPUStreaming: 0
m_SupportProbeVolumeDiskStreaming: 0
m_SupportProbeVolumeScenarios: 0
m_SupportProbeVolumeScenarioBlending: 0
m_ProbeVolumeSHBands: 1
m_MainLightRenderingMode: 1
m_MainLightShadowsSupported: 1
m_MainLightShadowmapResolution: 2048
m_AdditionalLightsRenderingMode: 1
m_AdditionalLightsPerObjectLimit: 4
m_AdditionalLightShadowsSupported: 1
m_AdditionalLightsShadowmapResolution: 2048
m_AdditionalLightsShadowResolutionTierLow: 256
m_AdditionalLightsShadowResolutionTierMedium: 512
m_AdditionalLightsShadowResolutionTierHigh: 1024
m_ReflectionProbeBlending: 1
m_ReflectionProbeBoxProjection: 1
m_ReflectionProbeAtlas: 1
m_ShadowDistance: 50
m_ShadowCascadeCount: 4
m_Cascade2Split: 0.25
m_Cascade3Split: {x: 0.1, y: 0.3}
m_Cascade4Split: {x: 0.12299999, y: 0.2926, z: 0.53599995}
m_CascadeBorder: 0.107758604
m_ShadowDepthBias: 0.1
m_ShadowNormalBias: 0.5
m_AnyShadowsSupported: 1
m_SoftShadowsSupported: 1
m_ConservativeEnclosingSphere: 1
m_NumIterationsEnclosingSphere: 64
m_SoftShadowQuality: 3
m_AdditionalLightsCookieResolution: 2048
m_AdditionalLightsCookieFormat: 3
m_UseSRPBatcher: 1
m_SupportsDynamicBatching: 0
m_MixedLightingSupported: 1
m_SupportsLightCookies: 1
m_SupportsLightLayers: 1
m_DebugLevel: 0
m_StoreActionsOptimization: 0
m_UseAdaptivePerformance: 1
m_ColorGradingMode: 0
m_ColorGradingLutSize: 32
m_AllowPostProcessAlphaOutput: 0
m_UseFastSRGBLinearConversion: 0
m_SupportDataDrivenLensFlare: 1
m_SupportScreenSpaceLensFlare: 1
m_GPUResidentDrawerMode: 0
m_SmallMeshScreenPercentage: 0
m_GPUResidentDrawerEnableOcclusionCullingInCameras: 0
m_ShadowType: 1
m_LocalShadowsSupported: 0
m_LocalShadowsAtlasResolution: 256
m_MaxPixelLights: 0
m_ShadowAtlasResolution: 256
m_VolumeFrameworkUpdateMode: 0
m_VolumeProfile: {fileID: 11400000, guid: 10fc4df2da32a41aaa32d77bc913491c, type: 2}
apvScenesData:
obsoleteSceneBounds:
m_Keys: []
m_Values: []
obsoleteHasProbeVolumes:
m_Keys: []
m_Values:
m_PrefilteringModeMainLightShadows: 3
m_PrefilteringModeAdditionalLight: 4
m_PrefilteringModeAdditionalLightShadows: 0
m_PrefilterXRKeywords: 1
m_PrefilteringModeForwardPlus: 1
m_PrefilteringModeDeferredRendering: 0
m_PrefilteringModeScreenSpaceOcclusion: 1
m_PrefilterDebugKeywords: 1
m_PrefilterWriteRenderingLayers: 0
m_PrefilterHDROutput: 1
m_PrefilterAlphaOutput: 0
m_PrefilterSSAODepthNormals: 0
m_PrefilterSSAOSourceDepthLow: 1
m_PrefilterSSAOSourceDepthMedium: 1
m_PrefilterSSAOSourceDepthHigh: 1
m_PrefilterSSAOInterleaved: 1
m_PrefilterSSAOBlueNoise: 0
m_PrefilterSSAOSampleCountLow: 1
m_PrefilterSSAOSampleCountMedium: 0
m_PrefilterSSAOSampleCountHigh: 1
m_PrefilterDBufferMRT1: 1
m_PrefilterDBufferMRT2: 1
m_PrefilterDBufferMRT3: 0
m_PrefilterSoftShadowsQualityLow: 0
m_PrefilterSoftShadowsQualityMedium: 0
m_PrefilterSoftShadowsQualityHigh: 0
m_PrefilterSoftShadows: 0
m_PrefilterScreenCoord: 1
m_PrefilterScreenSpaceIrradiance: 0
m_PrefilterNativeRenderPass: 1
m_PrefilterUseLegacyLightmaps: 0
m_PrefilterBicubicLightmapSampling: 0
m_PrefilterReflectionProbeRotation: 0
m_PrefilterReflectionProbeBlending: 0
m_PrefilterReflectionProbeBoxProjection: 0
m_PrefilterReflectionProbeAtlas: 0
m_ShaderVariantLogLevel: 0
m_ShadowCascades: 0
m_Textures:
blueNoise64LTex: {fileID: 2800000, guid: e3d24661c1e055f45a7560c033dbb837, type: 3}
bayerMatrixTex: {fileID: 2800000, guid: f9ee4ed84c1d10c49aabb9b210b0fc44, type: 3}
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4b83569d67af61e458304325a23e5dfd
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
+95
View File
@@ -0,0 +1,95 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: de640fe3d0db1804a85f9fc8f5cadab6, type: 3}
m_Name: PC_Renderer
m_EditorClassIdentifier:
debugShaders:
debugReplacementPS: {fileID: 4800000, guid: cf852408f2e174538bcd9b7fda1c5ae7,
type: 3}
hdrDebugViewPS: {fileID: 4800000, guid: 573620ae32aec764abd4d728906d2587, type: 3}
probeVolumeSamplingDebugComputeShader: {fileID: 7200000, guid: 53626a513ea68ce47b59dc1299fe3959,
type: 3}
probeVolumeResources:
probeVolumeDebugShader: {fileID: 4800000, guid: e5c6678ed2aaa91408dd3df699057aae,
type: 3}
probeVolumeFragmentationDebugShader: {fileID: 4800000, guid: 03cfc4915c15d504a9ed85ecc404e607,
type: 3}
probeVolumeOffsetDebugShader: {fileID: 4800000, guid: 53a11f4ebaebf4049b3638ef78dc9664,
type: 3}
probeVolumeSamplingDebugShader: {fileID: 4800000, guid: 8f96cd657dc40064aa21efcc7e50a2e7,
type: 3}
probeSamplingDebugMesh: {fileID: -3555484719484374845, guid: 57d7c4c16e2765b47a4d2069b311bffe,
type: 3}
probeSamplingDebugTexture: {fileID: 2800000, guid: 24ec0e140fb444a44ab96ee80844e18e,
type: 3}
probeVolumeBlendStatesCS: {fileID: 7200000, guid: b9a23f869c4fd45f19c5ada54dd82176,
type: 3}
m_RendererFeatures:
- {fileID: 7833122117494664109}
m_RendererFeatureMap: ad6b866f10d7b46c
m_UseNativeRenderPass: 1
postProcessData: {fileID: 11400000, guid: 41439944d30ece34e96484bdb6645b55, type: 2}
m_AssetVersion: 2
m_OpaqueLayerMask:
serializedVersion: 2
m_Bits: 4294967295
m_TransparentLayerMask:
serializedVersion: 2
m_Bits: 4294967295
m_DefaultStencilState:
overrideStencilState: 0
stencilReference: 1
stencilCompareFunction: 3
passOperation: 2
failOperation: 0
zFailOperation: 0
m_ShadowTransparentReceive: 1
m_RenderingMode: 2
m_DepthPrimingMode: 0
m_CopyDepthMode: 0
m_AccurateGbufferNormals: 0
m_IntermediateTextureMode: 0
--- !u!114 &7833122117494664109
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f62c9c65cf3354c93be831c8bc075510, type: 3}
m_Name: ScreenSpaceAmbientOcclusion
m_EditorClassIdentifier:
m_Active: 1
m_Settings:
AOMethod: 0
Downsample: 0
AfterOpaque: 0
Source: 1
NormalSamples: 1
Intensity: 0.4
DirectLightingStrength: 0.25
Radius: 0.3
Samples: 1
BlurQuality: 0
Falloff: 100
SampleCount: -1
m_BlueNoise256Textures:
- {fileID: 2800000, guid: 36f118343fc974119bee3d09e2111500, type: 3}
- {fileID: 2800000, guid: 4b7b083e6b6734e8bb2838b0b50a0bc8, type: 3}
- {fileID: 2800000, guid: c06cc21c692f94f5fb5206247191eeee, type: 3}
- {fileID: 2800000, guid: cb76dd40fa7654f9587f6a344f125c9a, type: 3}
- {fileID: 2800000, guid: e32226222ff144b24bf3a5a451de54bc, type: 3}
- {fileID: 2800000, guid: 3302065f671a8450b82c9ddf07426f3a, type: 3}
- {fileID: 2800000, guid: 56a77a3e8d64f47b6afe9e3c95cb57d5, type: 3}
m_Shader: {fileID: 4800000, guid: 0849e84e3d62649e8882e9d6f056a017, type: 3}
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f288ae1f4751b564a96ac7587541f7a2
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
+63
View File
@@ -0,0 +1,63 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-7893295128165547882
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 0b2db86121404754db890f4c8dfe81b2, type: 3}
m_Name: Bloom
m_EditorClassIdentifier:
active: 1
m_AdvancedMode: 0
threshold:
m_OverrideState: 0
m_Value: 1
min: 0
intensity:
m_OverrideState: 1
m_Value: 15
min: 0
scatter:
m_OverrideState: 0
m_Value: 0.7
min: 0
max: 1
clamp:
m_OverrideState: 0
m_Value: 65472
min: 0
tint:
m_OverrideState: 0
m_Value: {r: 1, g: 1, b: 1, a: 1}
hdr: 0
showAlpha: 0
showEyeDropper: 1
highQualityFiltering:
m_OverrideState: 0
m_Value: 0
dirtTexture:
m_OverrideState: 0
m_Value: {fileID: 0}
dirtIntensity:
m_OverrideState: 0
m_Value: 0
min: 0
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d7fd9488000d3734a9e00ee676215985, type: 3}
m_Name: SampleSceneProfile
m_EditorClassIdentifier:
components:
- {fileID: -7893295128165547882}
@@ -0,0 +1,15 @@
fileFormatVersion: 2
guid: 10fc4df2da32a41aaa32d77bc913491c
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 0
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 168243
packageName: VR Beats Kit
packageVersion: 2.0
assetPath: Assets/Settings/SampleSceneProfile.asset
uploadId: 546658
@@ -0,0 +1,433 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 2ec995e51a6e251468d2a3fd8a686257, type: 3}
m_Name: UniversalRenderPipelineGlobalSettings
m_EditorClassIdentifier:
m_ShaderStrippingSetting:
m_Version: 0
m_ExportShaderVariants: 1
m_ShaderVariantLogLevel: 0
m_StripRuntimeDebugShaders: 1
m_URPShaderStrippingSetting:
m_Version: 0
m_StripUnusedPostProcessingVariants: 1
m_StripUnusedVariants: 1
m_StripScreenCoordOverrideVariants: 1
m_ShaderVariantLogLevel: 0
m_ExportShaderVariants: 1
m_StripDebugVariants: 1
m_StripUnusedPostProcessingVariants: 1
m_StripUnusedVariants: 1
m_StripScreenCoordOverrideVariants: 1
supportRuntimeDebugDisplay: 0
m_EnableRenderGraph: 0
m_Settings:
m_SettingsList:
m_List:
- rid: 6852985685364965376
- rid: 6852985685364965377
- rid: 6852985685364965378
- rid: 6852985685364965379
- rid: 6852985685364965380
- rid: 6852985685364965381
- rid: 6852985685364965382
- rid: 6852985685364965383
- rid: 6852985685364965384
- rid: 6852985685364965385
- rid: 6852985685364965386
- rid: 6852985685364965387
- rid: 6852985685364965388
- rid: 6852985685364965389
- rid: 6852985685364965390
- rid: 6852985685364965391
- rid: 6852985685364965392
- rid: 6852985685364965393
- rid: 6852985685364965394
- rid: 8712630790384254976
- rid: 3348792947937902592
- rid: 3348792947937902593
- rid: 3348792947937902594
- rid: 3348792947937902595
- rid: 3348792947937902596
- rid: 3348792947937902597
- rid: 3348792947937902598
- rid: 3348792947937902599
- rid: 3348792947937902600
- rid: 3348792947937902601
- rid: 3348792947937902602
- rid: 3348792947937902603
- rid: 3348792947937902604
- rid: 3348792947937902605
m_RuntimeSettings:
m_List: []
m_AssetVersion: 10
m_ObsoleteDefaultVolumeProfile: {fileID: 0}
m_RenderingLayerNames:
- Light Layer default
- Light Layer 1
- Light Layer 2
- Light Layer 3
- Light Layer 4
- Light Layer 5
- Light Layer 6
- Light Layer 7
m_ValidRenderingLayers: 0
lightLayerName0: Light Layer default
lightLayerName1: Light Layer 1
lightLayerName2: Light Layer 2
lightLayerName3: Light Layer 3
lightLayerName4: Light Layer 4
lightLayerName5: Light Layer 5
lightLayerName6: Light Layer 6
lightLayerName7: Light Layer 7
apvScenesData:
obsoleteSceneBounds:
m_Keys: []
m_Values: []
obsoleteHasProbeVolumes:
m_Keys: []
m_Values:
references:
version: 2
RefIds:
- rid: 3348792947937902592
type: {class: RayTracingRenderPipelineResources, ns: UnityEngine.Rendering.UnifiedRayTracing, asm: Unity.UnifiedRayTracing.Runtime}
data:
m_Version: 1
m_GeometryPoolKernels: {fileID: 7200000, guid: 98e3d58cae7210c4786f67f504c9e899, type: 3}
m_CopyBuffer: {fileID: 7200000, guid: 1b95b5dcf48d1914c9e1e7405c7660e3, type: 3}
m_CopyPositions: {fileID: 7200000, guid: 1ad53a96b58d3c3488dde4f14db1aaeb, type: 3}
m_BitHistogram: {fileID: 7200000, guid: 8670f7ce4b60cef43bed36148aa1b0a2, type: 3}
m_BlockReducePart: {fileID: 7200000, guid: 4e034cc8ea2635c4e9f063e5ddc7ea7a, type: 3}
m_BlockScan: {fileID: 7200000, guid: 4d6d5de35fa45ef4a92119397a045cc9, type: 3}
m_BuildHlbvh: {fileID: 7200000, guid: 2d70cd6be91bd7843a39a54b51c15b13, type: 3}
m_RestructureBvh: {fileID: 7200000, guid: 56641cb88dcb31a4398a4997ef7a7a8c, type: 3}
m_Scatter: {fileID: 7200000, guid: a2eaeefdac4637a44b734e85b7be9186, type: 3}
- rid: 3348792947937902593
type: {class: UniversalRenderPipelineEditorAssets, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
data:
m_DefaultSettingsVolumeProfile: {fileID: 11400000, guid: eda47df5b85f4f249abf7abd73db2cb2, type: 2}
- rid: 3348792947937902594
type: {class: UniversalRenderPipelineRuntimeTerrainShaders, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
data:
m_Version: 0
m_TerrainDetailLit: {fileID: 4800000, guid: f6783ab646d374f94b199774402a5144, type: 3}
m_TerrainDetailGrassBillboard: {fileID: 4800000, guid: 29868e73b638e48ca99a19ea58c48d90, type: 3}
m_TerrainDetailGrass: {fileID: 4800000, guid: e507fdfead5ca47e8b9a768b51c291a1, type: 3}
- rid: 3348792947937902595
type: {class: URPTerrainShaderSetting, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
data:
m_Version: 0
m_IncludeTerrainShaders: 1
- rid: 3348792947937902596
type: {class: PostProcessData/ShaderResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
data:
stopNanPS: {fileID: 4800000, guid: 1121bb4e615ca3c48b214e79e841e823, type: 3}
subpixelMorphologicalAntialiasingPS: {fileID: 4800000, guid: 63eaba0ebfb82cc43bde059b4a8c65f6, type: 3}
gaussianDepthOfFieldPS: {fileID: 4800000, guid: 5e7134d6e63e0bc47a1dd2669cedb379, type: 3}
bokehDepthOfFieldPS: {fileID: 4800000, guid: 2aed67ad60045d54ba3a00c91e2d2631, type: 3}
cameraMotionBlurPS: {fileID: 4800000, guid: 1edcd131364091c46a17cbff0b1de97a, type: 3}
paniniProjectionPS: {fileID: 4800000, guid: a15b78cf8ca26ca4fb2090293153c62c, type: 3}
lutBuilderLdrPS: {fileID: 4800000, guid: 65df88701913c224d95fc554db28381a, type: 3}
lutBuilderHdrPS: {fileID: 4800000, guid: ec9fec698a3456d4fb18cf8bacb7a2bc, type: 3}
bloomPS: {fileID: 4800000, guid: 5f1864addb451f54bae8c86d230f736e, type: 3}
temporalAntialiasingPS: {fileID: 4800000, guid: 9c70c1a35ff15f340b38ea84842358bf, type: 3}
LensFlareDataDrivenPS: {fileID: 4800000, guid: 6cda457ac28612740adb23da5d39ea92, type: 3}
LensFlareScreenSpacePS: {fileID: 4800000, guid: 701880fecb344ea4c9cd0db3407ab287, type: 3}
scalingSetupPS: {fileID: 4800000, guid: e8ee25143a34b8c4388709ea947055d1, type: 3}
easuPS: {fileID: 4800000, guid: 562b7ae4f629f144aa97780546fce7c6, type: 3}
uberPostPS: {fileID: 4800000, guid: e7857e9d0c934dc4f83f270f8447b006, type: 3}
finalPostPassPS: {fileID: 4800000, guid: c49e63ed1bbcb334780a3bd19dfed403, type: 3}
m_ShaderResourcesVersion: 0
- rid: 3348792947937902597
type: {class: URPReflectionProbeSettings, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Universal.Runtime}
data:
version: 1
useReflectionProbeRotation: 0
- rid: 3348792947937902598
type: {class: ScreenSpaceAmbientOcclusionDynamicResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
data:
m_BlueNoise256Textures:
- {fileID: 2800000, guid: 36f118343fc974119bee3d09e2111500, type: 3}
- {fileID: 2800000, guid: 4b7b083e6b6734e8bb2838b0b50a0bc8, type: 3}
- {fileID: 2800000, guid: c06cc21c692f94f5fb5206247191eeee, type: 3}
- {fileID: 2800000, guid: cb76dd40fa7654f9587f6a344f125c9a, type: 3}
- {fileID: 2800000, guid: e32226222ff144b24bf3a5a451de54bc, type: 3}
- {fileID: 2800000, guid: 3302065f671a8450b82c9ddf07426f3a, type: 3}
- {fileID: 2800000, guid: 56a77a3e8d64f47b6afe9e3c95cb57d5, type: 3}
m_Version: 0
- rid: 3348792947937902599
type: {class: OnTilePostProcessResource, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
data:
m_Version: 0
m_UberPostShader: {fileID: 4800000, guid: fe4f13c1004a07d4ea1e30bfd0326d9e, type: 3}
- rid: 3348792947937902600
type: {class: UniversalRenderPipelineRuntimeXRResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
data:
m_xrOcclusionMeshPS: {fileID: 4800000, guid: 4431b1f1f743fbf4eb310a967890cbea, type: 3}
m_xrMirrorViewPS: {fileID: 4800000, guid: d5a307c014552314b9f560906d708772, type: 3}
m_xrMotionVector: {fileID: 4800000, guid: f89aac1e4f84468418fe30e611dff395, type: 3}
- rid: 3348792947937902601
type: {class: ScreenSpaceAmbientOcclusionPersistentResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
data:
m_Shader: {fileID: 4800000, guid: 0849e84e3d62649e8882e9d6f056a017, type: 3}
m_Version: 0
- rid: 3348792947937902602
type: {class: PostProcessData/TextureResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
data:
blueNoise16LTex:
- {fileID: 2800000, guid: 81200413a40918d4d8702e94db29911c, type: 3}
- {fileID: 2800000, guid: d50c5e07c9911a74982bddf7f3075e7b, type: 3}
- {fileID: 2800000, guid: 1134690bf9216164dbc75050e35b7900, type: 3}
- {fileID: 2800000, guid: 7ce2118f74614a94aa8a0cdf2e6062c3, type: 3}
- {fileID: 2800000, guid: 2ca97df9d1801e84a8a8f2c53cb744f0, type: 3}
- {fileID: 2800000, guid: e63eef8f54aa9dc4da9a5ac094b503b5, type: 3}
- {fileID: 2800000, guid: 39451254daebd6d40b52899c1f1c0c1b, type: 3}
- {fileID: 2800000, guid: c94ad916058dff743b0f1c969ddbe660, type: 3}
- {fileID: 2800000, guid: ed5ea7ce59ca8ec4f9f14bf470a30f35, type: 3}
- {fileID: 2800000, guid: 071e954febf155243a6c81e48f452644, type: 3}
- {fileID: 2800000, guid: 96aaab9cc247d0b4c98132159688c1af, type: 3}
- {fileID: 2800000, guid: fc3fa8f108657e14486697c9a84ccfc5, type: 3}
- {fileID: 2800000, guid: bfed3e498947fcb4890b7f40f54d85b9, type: 3}
- {fileID: 2800000, guid: d512512f4af60a442ab3458489412954, type: 3}
- {fileID: 2800000, guid: 47a45908f6db0cb44a0d5e961143afec, type: 3}
- {fileID: 2800000, guid: 4dcc0502f8586f941b5c4a66717205e8, type: 3}
- {fileID: 2800000, guid: 9d92991794bb5864c8085468b97aa067, type: 3}
- {fileID: 2800000, guid: 14381521ff11cb74abe3fe65401c23be, type: 3}
- {fileID: 2800000, guid: d36f0fe53425e08499a2333cf423634c, type: 3}
- {fileID: 2800000, guid: d4044ea2490d63b43aa1765f8efbf8a9, type: 3}
- {fileID: 2800000, guid: c9bd74624d8070f429e3f46d161f9204, type: 3}
- {fileID: 2800000, guid: d5c9b274310e5524ebe32a4e4da3df1f, type: 3}
- {fileID: 2800000, guid: f69770e54f2823f43badf77916acad83, type: 3}
- {fileID: 2800000, guid: 10b6c6d22e73dea46a8ab36b6eebd629, type: 3}
- {fileID: 2800000, guid: a2ec5cbf5a9b64345ad3fab0912ddf7b, type: 3}
- {fileID: 2800000, guid: 1c3c6d69a645b804fa232004b96b7ad3, type: 3}
- {fileID: 2800000, guid: d18a24d7b4ed50f4387993566d9d3ae2, type: 3}
- {fileID: 2800000, guid: c989e1ed85cf7154caa922fec53e6af6, type: 3}
- {fileID: 2800000, guid: ff47e5a0f105eb34883b973e51f4db62, type: 3}
- {fileID: 2800000, guid: fa042edbfc40fbd4bad0ab9d505b1223, type: 3}
- {fileID: 2800000, guid: 896d9004736809c4fb5973b7c12eb8b9, type: 3}
- {fileID: 2800000, guid: 179f794063d2a66478e6e726f84a65bc, type: 3}
filmGrainTex:
- {fileID: 2800000, guid: 654c582f7f8a5a14dbd7d119cbde215d, type: 3}
- {fileID: 2800000, guid: dd77ffd079630404e879388999033049, type: 3}
- {fileID: 2800000, guid: 1097e90e1306e26439701489f391a6c0, type: 3}
- {fileID: 2800000, guid: f0b67500f7fad3b4c9f2b13e8f41ba6e, type: 3}
- {fileID: 2800000, guid: 9930fb4528622b34687b00bbe6883de7, type: 3}
- {fileID: 2800000, guid: bd9e8c758250ef449a4b4bfaad7a2133, type: 3}
- {fileID: 2800000, guid: 510a2f57334933e4a8dbabe4c30204e4, type: 3}
- {fileID: 2800000, guid: b4db8180660810945bf8d55ab44352ad, type: 3}
- {fileID: 2800000, guid: fd2fd78b392986e42a12df2177d3b89c, type: 3}
- {fileID: 2800000, guid: 5cdee82a77d13994f83b8fdabed7c301, type: 3}
smaaAreaTex: {fileID: 2800000, guid: d1f1048909d55cd4fa1126ab998f617e, type: 3}
smaaSearchTex: {fileID: 2800000, guid: 51eee22c2a633ef4aada830eed57c3fd, type: 3}
m_TexturesResourcesVersion: 0
- rid: 3348792947937902603
type: {class: VrsRenderPipelineRuntimeResources, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime}
data:
m_TextureComputeShader: {fileID: 7200000, guid: cacb30de6c40c7444bbc78cb0a81fd2a, type: 3}
m_VisualizationShader: {fileID: 4800000, guid: 620b55b8040a88d468e94abe55bed5ba, type: 3}
m_VisualizationLookupTable:
m_Data:
- {r: 0.785, g: 0.23, b: 0.2, a: 1}
- {r: 1, g: 0.8, b: 0.8, a: 1}
- {r: 0.4, g: 0.2, b: 0.2, a: 1}
- {r: 0.51, g: 0.8, b: 0.6, a: 1}
- {r: 0.6, g: 0.8, b: 1, a: 1}
- {r: 0.2, g: 0.4, b: 0.6, a: 1}
- {r: 0.8, g: 1, b: 0.8, a: 1}
- {r: 0.2, g: 0.4, b: 0.2, a: 1}
- {r: 0.125, g: 0.22, b: 0.36, a: 1}
m_ConversionLookupTable:
m_Data:
- {r: 0.785, g: 0.23, b: 0.2, a: 1}
- {r: 1, g: 0.8, b: 0.8, a: 1}
- {r: 0.4, g: 0.2, b: 0.2, a: 1}
- {r: 0.51, g: 0.8, b: 0.6, a: 1}
- {r: 0.6, g: 0.8, b: 1, a: 1}
- {r: 0.2, g: 0.4, b: 0.6, a: 1}
- {r: 0.8, g: 1, b: 0.8, a: 1}
- {r: 0.2, g: 0.4, b: 0.2, a: 1}
- {r: 0.125, g: 0.22, b: 0.36, a: 1}
- rid: 3348792947937902604
type: {class: RenderingDebuggerRuntimeResources, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime}
data:
m_version: 0
- rid: 3348792947937902605
type: {class: LightmapSamplingSettings, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime}
data:
m_Version: 1
m_UseBicubicLightmapSampling: 0
- rid: 6852985685364965376
type: {class: URPShaderStrippingSetting, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
data:
m_Version: 0
m_StripUnusedPostProcessingVariants: 1
m_StripUnusedVariants: 1
m_StripScreenCoordOverrideVariants: 1
- rid: 6852985685364965377
type: {class: UniversalRenderPipelineEditorShaders, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
data:
m_AutodeskInteractive: {fileID: 4800000, guid: 0e9d5a909a1f7e84882a534d0d11e49f, type: 3}
m_AutodeskInteractiveTransparent: {fileID: 4800000, guid: 5c81372d981403744adbdda4433c9c11, type: 3}
m_AutodeskInteractiveMasked: {fileID: 4800000, guid: 80aa867ac363ac043847b06ad71604cd, type: 3}
m_DefaultSpeedTree7Shader: {fileID: 4800000, guid: 0f4122b9a743b744abe2fb6a0a88868b, type: 3}
m_DefaultSpeedTree8Shader: {fileID: -6465566751694194690, guid: 9920c1f1781549a46ba081a2a15a16ec, type: 3}
m_DefaultSpeedTree9Shader: {fileID: -6465566751694194690, guid: cbd3e1cc4ae141c42a30e33b4d666a61, type: 3}
- rid: 6852985685364965378
type: {class: UniversalRendererResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
data:
m_Version: 0
m_CopyDepthPS: {fileID: 4800000, guid: d6dae50ee9e1bfa4db75f19f99355220, type: 3}
m_CameraMotionVector: {fileID: 4800000, guid: c56b7e0d4c7cb484e959caeeedae9bbf, type: 3}
m_StencilDeferredPS: {fileID: 4800000, guid: e9155b26e1bc55942a41e518703fe304, type: 3}
m_ClusterDeferred: {fileID: 4800000, guid: 222cce62363a44a380c36bf03b392608, type: 3}
m_StencilDitherMaskSeedPS: {fileID: 4800000, guid: 8c3ee818f2efa514c889881ccb2e95a2, type: 3}
m_DBufferClear: {fileID: 4800000, guid: f056d8bd2a1c7e44e9729144b4c70395, type: 3}
- rid: 6852985685364965379
type: {class: UniversalRenderPipelineDebugShaders, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
data:
m_DebugReplacementPS: {fileID: 4800000, guid: cf852408f2e174538bcd9b7fda1c5ae7, type: 3}
m_HdrDebugViewPS: {fileID: 4800000, guid: 573620ae32aec764abd4d728906d2587, type: 3}
m_ProbeVolumeSamplingDebugComputeShader: {fileID: 7200000, guid: 53626a513ea68ce47b59dc1299fe3959, type: 3}
- rid: 6852985685364965380
type: {class: UniversalRenderPipelineRuntimeShaders, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
data:
m_Version: 0
m_FallbackErrorShader: {fileID: 4800000, guid: e6e9a19c3678ded42a3bc431ebef7dbd, type: 3}
m_BlitHDROverlay: {fileID: 4800000, guid: a89bee29cffa951418fc1e2da94d1959, type: 3}
m_CoreBlitPS: {fileID: 4800000, guid: 93446b5c5339d4f00b85c159e1159b7c, type: 3}
m_CoreBlitColorAndDepthPS: {fileID: 4800000, guid: d104b2fc1ca6445babb8e90b0758136b, type: 3}
m_SamplingPS: {fileID: 4800000, guid: 04c410c9937594faa893a11dceb85f7e, type: 3}
m_TerrainDetailLit: {fileID: 0}
m_TerrainDetailGrassBillboard: {fileID: 0}
m_TerrainDetailGrass: {fileID: 0}
- rid: 6852985685364965381
type: {class: UniversalRenderPipelineRuntimeTextures, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
data:
m_Version: 1
m_BlueNoise64LTex: {fileID: 2800000, guid: e3d24661c1e055f45a7560c033dbb837, type: 3}
m_BayerMatrixTex: {fileID: 2800000, guid: f9ee4ed84c1d10c49aabb9b210b0fc44, type: 3}
m_DebugFontTex: {fileID: 2800000, guid: 26a413214480ef144b2915d6ff4d0beb, type: 3}
- rid: 6852985685364965382
type: {class: Renderer2DResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
data:
m_Version: 0
m_LightShader: {fileID: 4800000, guid: 3f6c848ca3d7bca4bbe846546ac701a1, type: 3}
m_ProjectedShadowShader: {fileID: 4800000, guid: ce09d4a80b88c5a4eb9768fab4f1ee00, type: 3}
m_SpriteShadowShader: {fileID: 4800000, guid: 44fc62292b65ab04eabcf310e799ccf6, type: 3}
m_SpriteUnshadowShader: {fileID: 4800000, guid: de02b375720b5c445afe83cd483bedf3, type: 3}
m_GeometryShadowShader: {fileID: 4800000, guid: 19349a0f9a7ed4c48a27445bcf92e5e1, type: 3}
m_GeometryUnshadowShader: {fileID: 4800000, guid: 77774d9009bb81447b048c907d4c6273, type: 3}
m_CopyDepthPS: {fileID: 4800000, guid: d6dae50ee9e1bfa4db75f19f99355220, type: 3}
m_DefaultLitMaterial: {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, type: 2}
m_DefaultUnlitMaterial: {fileID: 2100000, guid: 9dfc825aed78fcd4ba02077103263b40, type: 2}
m_DefaultMaskMaterial: {fileID: 2100000, guid: 15d0c3709176029428a0da2f8cecf0b5, type: 2}
m_DefaultMesh2DLitMaterial: {fileID: 2100000, guid: 9452ae1262a74094f8a68013fbcd1834, type: 2}
- rid: 6852985685364965383
type: {class: UniversalRenderPipelineEditorMaterials, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
data:
m_DefaultMaterial: {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2}
m_DefaultParticleMaterial: {fileID: 2100000, guid: e823cd5b5d27c0f4b8256e7c12ee3e6d, type: 2}
m_DefaultLineMaterial: {fileID: 2100000, guid: e823cd5b5d27c0f4b8256e7c12ee3e6d, type: 2}
m_DefaultTerrainMaterial: {fileID: 2100000, guid: 594ea882c5a793440b60ff72d896021e, type: 2}
m_DefaultDecalMaterial: {fileID: 2100000, guid: 31d0dcc6f2dd4e4408d18036a2c93862, type: 2}
m_DefaultSpriteMaterial: {fileID: 2100000, guid: 9dfc825aed78fcd4ba02077103263b40, type: 2}
- rid: 6852985685364965384
type: {class: URPDefaultVolumeProfileSettings, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
data:
m_Version: 0
m_VolumeProfile: {fileID: 11400000, guid: ab09877e2e707104187f6f83e2f62510, type: 2}
- rid: 6852985685364965385
type: {class: RenderGraphSettings, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
data:
m_Version: 0
m_EnableRenderCompatibilityMode: 0
- rid: 6852985685364965386
type: {class: GPUResidentDrawerResources, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.GPUDriven.Runtime}
data:
m_Version: 0
m_InstanceDataBufferCopyKernels: {fileID: 7200000, guid: f984aeb540ded8b4fbb8a2047ab5b2e2, type: 3}
m_InstanceDataBufferUploadKernels: {fileID: 7200000, guid: 53864816eb00f2343b60e1a2c5a262ef, type: 3}
m_TransformUpdaterKernels: {fileID: 7200000, guid: 2a567b9b2733f8d47a700c3c85bed75b, type: 3}
m_WindDataUpdaterKernels: {fileID: 7200000, guid: fde76746e4fd0ed418c224f6b4084114, type: 3}
m_OccluderDepthPyramidKernels: {fileID: 7200000, guid: 08b2b5fb307b0d249860612774a987da, type: 3}
m_InstanceOcclusionCullingKernels: {fileID: 7200000, guid: f6d223acabc2f974795a5a7864b50e6c, type: 3}
m_OcclusionCullingDebugKernels: {fileID: 7200000, guid: b23e766bcf50ca4438ef186b174557df, type: 3}
m_DebugOcclusionTestPS: {fileID: 4800000, guid: d3f0849180c2d0944bc71060693df100, type: 3}
m_DebugOccluderPS: {fileID: 4800000, guid: b3c92426a88625841ab15ca6a7917248, type: 3}
- rid: 6852985685364965387
type: {class: STP/RuntimeResources, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime}
data:
m_setupCS: {fileID: 7200000, guid: 33be2e9a5506b2843bdb2bdff9cad5e1, type: 3}
m_preTaaCS: {fileID: 7200000, guid: a679dba8ec4d9ce45884a270b0e22dda, type: 3}
m_taaCS: {fileID: 7200000, guid: 3923900e2b41b5e47bc25bfdcbcdc9e6, type: 3}
- rid: 6852985685364965388
type: {class: ProbeVolumeBakingResources, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime}
data:
m_Version: 1
dilationShader: {fileID: 7200000, guid: 6bb382f7de370af41b775f54182e491d, type: 3}
subdivideSceneCS: {fileID: 7200000, guid: bb86f1f0af829fd45b2ebddda1245c22, type: 3}
voxelizeSceneShader: {fileID: 4800000, guid: c8b6a681c7b4e2e4785ffab093907f9e, type: 3}
traceVirtualOffsetCS: {fileID: -6772857160820960102, guid: ff2cbab5da58bf04d82c5f34037ed123, type: 3}
traceVirtualOffsetRT: {fileID: -5126288278712620388, guid: ff2cbab5da58bf04d82c5f34037ed123, type: 3}
skyOcclusionCS: {fileID: -6772857160820960102, guid: 5a2a534753fbdb44e96c3c78b5a6999d, type: 3}
skyOcclusionRT: {fileID: -5126288278712620388, guid: 5a2a534753fbdb44e96c3c78b5a6999d, type: 3}
renderingLayerCS: {fileID: -6772857160820960102, guid: 94a070d33e408384bafc1dea4a565df9, type: 3}
renderingLayerRT: {fileID: -5126288278712620388, guid: 94a070d33e408384bafc1dea4a565df9, type: 3}
- rid: 6852985685364965389
type: {class: ProbeVolumeGlobalSettings, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime}
data:
m_Version: 1
m_ProbeVolumeDisableStreamingAssets: 0
- rid: 6852985685364965390
type: {class: ProbeVolumeDebugResources, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime}
data:
m_Version: 1
probeVolumeDebugShader: {fileID: 4800000, guid: 3b21275fd12d65f49babb5286f040f2d, type: 3}
probeVolumeFragmentationDebugShader: {fileID: 4800000, guid: 3a80877c579b9144ebdcc6d923bca303, type: 3}
probeVolumeSamplingDebugShader: {fileID: 4800000, guid: bf54e6528c79a224e96346799064c393, type: 3}
probeVolumeOffsetDebugShader: {fileID: 4800000, guid: db8bd7436dc2c5f4c92655307d198381, type: 3}
probeSamplingDebugMesh: {fileID: -3555484719484374845, guid: 20be25aac4e22ee49a7db76fb3df6de2, type: 3}
numbersDisplayTex: {fileID: 2800000, guid: 73fe53b428c5b3440b7e87ee830b608a, type: 3}
- rid: 6852985685364965391
type: {class: IncludeAdditionalRPAssets, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime}
data:
m_version: 0
m_IncludeReferencedInScenes: 0
m_IncludeAssetsByLabel: 0
m_LabelToInclude:
- rid: 6852985685364965392
type: {class: ShaderStrippingSetting, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime}
data:
m_Version: 0
m_ExportShaderVariants: 1
m_ShaderVariantLogLevel: 0
m_StripRuntimeDebugShaders: 1
- rid: 6852985685364965393
type: {class: ProbeVolumeRuntimeResources, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime}
data:
m_Version: 1
probeVolumeBlendStatesCS: {fileID: 7200000, guid: a3f7b8c99de28a94684cb1daebeccf5d, type: 3}
probeVolumeUploadDataCS: {fileID: 7200000, guid: 0951de5992461754fa73650732c4954c, type: 3}
probeVolumeUploadDataL2CS: {fileID: 7200000, guid: 6196f34ed825db14b81fb3eb0ea8d931, type: 3}
- rid: 6852985685364965394
type: {class: RenderGraphGlobalSettings, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime}
data:
m_version: 0
m_EnableCompilationCaching: 1
m_EnableValidityChecks: 1
- rid: 8712630790384254976
type: {class: RenderGraphUtilsResources, ns: UnityEngine.Rendering.RenderGraphModule.Util, asm: Unity.RenderPipelines.Core.Runtime}
data:
m_Version: 0
m_CoreCopyPS: {fileID: 4800000, guid: 12dc59547ea167a4ab435097dd0f9add, type: 3}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 18dc0cd2c080841dea60987a38ce93fa
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 41a64c21c57316b4ba71b8433b329dce
guid: d9717e3bceea9e1459f32361d592dc77
folderAsset: yes
DefaultImporter:
externalObjects: {}
@@ -0,0 +1,6 @@
{
"host": "http://192.168.55.3:5000",
"publicHost": "http://whdwo798.synology.me",
"account": "",
"password": ""
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 5f6f642b36f74dc5a0f44793fa605c2e
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8e7e8f5a82a3a134e91c54efd2274ea9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 1b8d251f9af63b746bf2f7ffe00ebb9b
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 168243
packageName: VR Beats Kit
packageVersion: 2.0
assetPath: Assets/TextMesh Pro/Documentation/TextMesh Pro User Guide 2016.pdf
uploadId: 546658
@@ -0,0 +1,15 @@
fileFormatVersion: 2
guid: 6e59c59b81ab47f9b6ec5781fa725d2c
timeCreated: 1484171296
licenseType: Pro
TextScriptImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 168243
packageName: VR Beats Kit
packageVersion: 2.0
assetPath: Assets/TextMesh Pro/Fonts/LiberationSans - OFL.txt
uploadId: 546658
@@ -0,0 +1,26 @@
fileFormatVersion: 2
guid: e3265ab4bf004d28a9537516768c1c75
timeCreated: 1484171297
licenseType: Pro
TrueTypeFontImporter:
serializedVersion: 2
fontSize: 16
forceTextureCase: -2
characterSpacing: 1
characterPadding: 0
includeFontData: 1
use2xBehaviour: 0
fontNames: []
fallbackFontReferences: []
customCharacters:
fontRenderingMode: 0
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 168243
packageName: VR Beats Kit
packageVersion: 2.0
assetPath: Assets/TextMesh Pro/Fonts/LiberationSans.ttf
uploadId: 546658
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f6c6fe0f3c5912a43a8a6707e336d2ea
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
Binary file not shown.
@@ -0,0 +1,21 @@
fileFormatVersion: 2
guid: d2abafb75e12afe48bb523e2ac30b43b
TrueTypeFontImporter:
externalObjects: {}
serializedVersion: 4
fontSize: 16
forceTextureCase: -2
characterSpacing: 0
characterPadding: 1
includeFontData: 1
fontNames:
- NanumGothic
fallbackFontReferences: []
customCharacters:
fontRenderingMode: 0
ascentCalculationMode: 1
useLegacyBoundsCalculation: 0
shouldRoundAdvanceValue: 1
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,16 @@
fileFormatVersion: 2
guid: e73a58f6e2794ae7b1b7e50b7fb811b0
timeCreated: 1484172806
licenseType: Pro
NativeFormatImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 168243
packageName: VR Beats Kit
packageVersion: 2.0
assetPath: Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF -
Drop Shadow.mat
uploadId: 546658
File diff suppressed because one or more lines are too long
@@ -0,0 +1,16 @@
fileFormatVersion: 2
guid: 2e498d1c8094910479dc3e1b768306a4
timeCreated: 1484171803
licenseType: Pro
NativeFormatImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 168243
packageName: VR Beats Kit
packageVersion: 2.0
assetPath: Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF -
Fallback.asset
uploadId: 546658
@@ -0,0 +1,16 @@
fileFormatVersion: 2
guid: 79459efec17a4d00a321bdcc27bbc385
timeCreated: 1484172856
licenseType: Pro
NativeFormatImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 168243
packageName: VR Beats Kit
packageVersion: 2.0
assetPath: Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF -
Outline.mat
uploadId: 546658
@@ -0,0 +1,15 @@
fileFormatVersion: 2
guid: 8f586378b4e144a9851e7b34d9b748ee
timeCreated: 1484171803
licenseType: Pro
NativeFormatImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 168243
packageName: VR Beats Kit
packageVersion: 2.0
assetPath: Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF.asset
uploadId: 546658

Some files were not shown because too many files have changed in this diff Show More