using System; using System.Collections.Generic; using System.Linq; using SplashEdit.RuntimeCode; #if UNITY_EDITOR using UnityEditor; #endif using UnityEngine; namespace SplashEdit.RuntimeCode { public enum PSXSceneType { Exterior = 0, Interior = 1 } [ExecuteInEditMode] public class PSXSceneExporter : MonoBehaviour { /// /// Editor code sets this delegate so the Runtime assembly can convert /// audio without directly referencing the Editor assembly. /// Signature: (AudioClip clip, int sampleRate, bool loop) => byte[] adpcm /// public static Func AudioConvertDelegate; public float GTEScaling = 100.0f; public LuaFile SceneLuaFile; [Header("Fog Configuration")] [Tooltip("Enable distance fog. Fog color is also used as the GPU clear color.")] public bool FogEnabled = false; [Tooltip("Fog color (RGB). Also used as the sky/clear color.")] public Color FogColor = new Color(0.5f, 0.5f, 0.6f); [Tooltip("Fog density (1 = light haze, 10 = pea soup).")] [Range(1, 10)] public int FogDensity = 5; [Header("Scene Type")] [Tooltip("Exterior uses BVH frustum culling. Interior uses room/portal occlusion.")] public PSXSceneType SceneType = PSXSceneType.Exterior; [Header("Cutscenes")] [Tooltip("Cutscene clips to include in this scene's splashpack. Only these will be exported.")] public PSXCutsceneClip[] Cutscenes = new PSXCutsceneClip[0]; [Header("Loading Screen")] [Tooltip("Optional prefab containing a PSXCanvas to use as a loading screen when loading this scene.\n" + "The canvas may contain a PSXUIProgressBar named 'loading' which will be automatically\n" + "updated during scene load. If null, no loading screen is shown.")] public GameObject LoadingScreenPrefab; private PSXObjectExporter[] _exporters; private TextureAtlas[] _atlases; // Component arrays private PSXInteractable[] _interactables; private PSXAudioSource[] _audioSources; // Phase 3+4: World collision and nav regions private PSXCollisionExporter _collisionExporter; private PSXNavRegionBuilder _navRegionBuilder; // Phase 5: Room/portal system (interior scenes) private PSXRoomBuilder _roomBuilder; // Phase 6: UI canvases private PSXCanvasData[] _canvases; private PSXFontData[] _fonts; private PSXData _psxData; private Vector2 selectedResolution; private bool dualBuffering; private bool verticalLayout; private List prohibitedAreas; private Vector3 _playerPos; private Quaternion _playerRot; private float _playerHeight; private float _playerRadius; private float _moveSpeed; private float _sprintSpeed; private float _jumpHeight; private float _gravity; private BVH _bvh; public bool PreviewBVH = true; public int BVHPreviewDepth = 9999; /// /// Export with a file dialog (legacy workflow). /// public void Export() { ExportToPath(null); } /// /// Export to the given file path. If path is null, shows a file dialog. /// Called by the Control Panel pipeline for automated exports. /// public void ExportToPath(string outputPath) { #if UNITY_EDITOR _psxData = DataStorage.LoadData(out selectedResolution, out dualBuffering, out verticalLayout, out prohibitedAreas); _exporters = FindObjectsByType(FindObjectsSortMode.None); for (int i = 0; i < _exporters.Length; i++) { PSXObjectExporter exp = _exporters[i]; EditorUtility.DisplayProgressBar($"{nameof(PSXSceneExporter)}", $"Export {nameof(PSXObjectExporter)}", ((float)i) / _exporters.Length); exp.CreatePSXTextures2D(); exp.CreatePSXMesh(GTEScaling); } // Collect components _interactables = FindObjectsByType(FindObjectsSortMode.None); _audioSources = FindObjectsByType(FindObjectsSortMode.None); // Collect UI image textures for VRAM packing alongside 3D textures PSXUIImage[] uiImages = FindObjectsByType(FindObjectsSortMode.None); List uiTextures = new List(); foreach (PSXUIImage img in uiImages) { if (img.SourceTexture != null) { Utils.SetTextureImporterFormat(img.SourceTexture, true); PSXTexture2D tex = PSXTexture2D.CreateFromTexture2D(img.SourceTexture, img.BitDepth); tex.OriginalTexture = img.SourceTexture; img.PackedTexture = tex; uiTextures.Add(tex); } } EditorUtility.ClearProgressBar(); PackTextures(uiTextures); // Collect UI canvases after VRAM packing (so PSXUIImage.PackedTexture has valid VRAM coords) _canvases = PSXUIExporter.CollectCanvases(selectedResolution, out _fonts); PSXPlayer player = FindObjectsByType(FindObjectsSortMode.None).FirstOrDefault(); if (player != null) { player.FindNavmesh(); _playerPos = player.CamPoint; _playerHeight = player.PlayerHeight; _playerRadius = player.PlayerRadius; _moveSpeed = player.MoveSpeed; _sprintSpeed = player.SprintSpeed; _jumpHeight = player.JumpHeight; _gravity = player.Gravity; _playerRot = player.transform.rotation; } _bvh = new BVH(_exporters.ToList()); _bvh.Build(); // Phase 3: Build world collision soup _collisionExporter = new PSXCollisionExporter(); _collisionExporter.Build(_exporters, GTEScaling); if (_collisionExporter.MeshCount == 0) Debug.LogWarning("No collision meshes! Set CollisionType=Solid on your floor/wall objects."); // Phase 4+5: Room volumes are needed by BOTH the nav region builder // (for spatial room assignment) and the room builder (for triangle assignment). // Collect them early so both systems use the same room indices. PSXRoom[] rooms = null; PSXPortalLink[] portalLinks = null; if (SceneType == PSXSceneType.Interior) { rooms = FindObjectsByType(FindObjectsSortMode.None); portalLinks = FindObjectsByType(FindObjectsSortMode.None); } // Phase 4: Build nav regions _navRegionBuilder = new PSXNavRegionBuilder(); _navRegionBuilder.AgentRadius = _playerRadius; _navRegionBuilder.AgentHeight = _playerHeight; if (player != null) { _navRegionBuilder.MaxStepHeight = player.MaxStepHeight; _navRegionBuilder.WalkableSlopeAngle = player.WalkableSlopeAngle; _navRegionBuilder.CellSize = player.NavCellSize; _navRegionBuilder.CellHeight = player.NavCellHeight; } // Pass PSXRoom volumes so nav regions get spatial room assignment // instead of BFS connectivity. This ensures nav region roomIndex // matches the PSXRoomBuilder room indices used by the renderer. if (rooms != null && rooms.Length > 0) _navRegionBuilder.PSXRooms = rooms; _navRegionBuilder.Build(_exporters, _playerPos); if (_navRegionBuilder.RegionCount == 0) Debug.LogWarning("No nav regions! Enable 'Generate Navigation' on your floor meshes."); // Phase 5: Build room/portal system (for interior scenes) _roomBuilder = new PSXRoomBuilder(); if (SceneType == PSXSceneType.Interior) { if (rooms != null && rooms.Length > 0) { _roomBuilder.Build(rooms, portalLinks, _exporters, GTEScaling); if (portalLinks == null || portalLinks.Length == 0) Debug.LogWarning("Interior scene has rooms but no PSXPortalLink components! " + "Place PSXPortalLink objects between rooms for portal culling."); } else { Debug.LogWarning("Interior scene type but no PSXRoom volumes found! Place PSXRoom components."); } } ExportFile(outputPath); #endif } void PackTextures(List additionalTextures = null) { (Rect buffer1, Rect buffer2) = Utils.BufferForResolution(selectedResolution, verticalLayout); List framebuffers = new List { buffer1 }; if (dualBuffering) { framebuffers.Add(buffer2); } VRAMPacker tp = new VRAMPacker(framebuffers, prohibitedAreas); var packed = tp.PackTexturesIntoVRAM(_exporters, additionalTextures); _exporters = packed.processedObjects; _atlases = packed.atlases; } void ExportFile(string outputPath = null) { #if UNITY_EDITOR string path = outputPath; if (string.IsNullOrEmpty(path)) path = EditorUtility.SaveFilePanel("Select Output File", "", "output", "bin"); if (string.IsNullOrEmpty(path)) return; // Convert audio clips to ADPCM (Editor-only, before passing to Runtime writer) AudioClipExport[] audioExports = null; if (_audioSources != null && _audioSources.Length > 0) { var list = new List(); foreach (var src in _audioSources) { if (src.Clip != null) { if (AudioConvertDelegate == null) throw new InvalidOperationException("AudioConvertDelegate not set. Ensure PSXAudioConverter registers it."); byte[] adpcm = AudioConvertDelegate(src.Clip, src.SampleRate, src.Loop); list.Add(new AudioClipExport { adpcmData = adpcm, sampleRate = src.SampleRate, loop = src.Loop, clipName = src.ClipName }); } else { Debug.LogWarning($"Audio source on {src.gameObject.name} has no clip assigned."); list.Add(new AudioClipExport { adpcmData = null, sampleRate = src.SampleRate, loop = src.Loop, clipName = src.ClipName }); } } audioExports = list.ToArray(); } var scene = new PSXSceneWriter.SceneData { exporters = _exporters, atlases = _atlases, interactables = _interactables, audioClips = audioExports, collisionExporter = _collisionExporter, navRegionBuilder = _navRegionBuilder, roomBuilder = _roomBuilder, bvh = _bvh, sceneLuaFile = SceneLuaFile, gteScaling = GTEScaling, playerPos = _playerPos, playerRot = _playerRot, playerHeight = _playerHeight, playerRadius = _playerRadius, moveSpeed = _moveSpeed, sprintSpeed = _sprintSpeed, jumpHeight = _jumpHeight, gravity = _gravity, sceneType = SceneType, fogEnabled = FogEnabled, fogColor = FogColor, fogDensity = FogDensity, cutscenes = Cutscenes, audioSources = _audioSources, canvases = _canvases, fonts = _fonts, }; PSXSceneWriter.Write(path, in scene, (msg, type) => { switch (type) { case LogType.Error: Debug.LogError(msg); break; case LogType.Warning: Debug.LogWarning(msg); break; default: Debug.Log(msg); break; } }); #endif } void OnDrawGizmos() { Vector3 sceneOrigin = new Vector3(0, 0, 0); Vector3 cubeSize = new Vector3(8.0f * GTEScaling, 8.0f * GTEScaling, 8.0f * GTEScaling); Gizmos.color = Color.red; Gizmos.DrawWireCube(sceneOrigin, cubeSize); if (_bvh == null || !PreviewBVH) return; _bvh.DrawGizmos(BVHPreviewDepth); } } }