328 lines
13 KiB
C#
328 lines
13 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
public static Func<AudioClip, int, bool, byte[]> 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;
|
|
private PSXTriggerBox[] _triggerBoxes;
|
|
|
|
// 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<ProhibitedArea> 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;
|
|
|
|
/// <summary>
|
|
/// Export with a file dialog (legacy workflow).
|
|
/// </summary>
|
|
public void Export()
|
|
{
|
|
ExportToPath(null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Export to the given file path. If path is null, shows a file dialog.
|
|
/// Called by the Control Panel pipeline for automated exports.
|
|
/// </summary>
|
|
public void ExportToPath(string outputPath)
|
|
{
|
|
#if UNITY_EDITOR
|
|
_psxData = DataStorage.LoadData(out selectedResolution, out dualBuffering, out verticalLayout, out prohibitedAreas);
|
|
|
|
_exporters = FindObjectsByType<PSXObjectExporter>(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<PSXInteractable>(FindObjectsSortMode.None);
|
|
_audioSources = FindObjectsByType<PSXAudioSource>(FindObjectsSortMode.None);
|
|
_triggerBoxes = FindObjectsByType<PSXTriggerBox>(FindObjectsSortMode.None);
|
|
|
|
// Collect UI image textures for VRAM packing alongside 3D textures
|
|
PSXUIImage[] uiImages = FindObjectsByType<PSXUIImage>(FindObjectsSortMode.None);
|
|
List<PSXTexture2D> uiTextures = new List<PSXTexture2D>();
|
|
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<PSXPlayer>(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=Static 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<PSXRoom>(FindObjectsSortMode.None);
|
|
portalLinks = FindObjectsByType<PSXPortalLink>(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<PSXTexture2D> additionalTextures = null)
|
|
{
|
|
(Rect buffer1, Rect buffer2) = Utils.BufferForResolution(selectedResolution, verticalLayout);
|
|
|
|
List<Rect> framebuffers = new List<Rect> { 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<AudioClipExport>();
|
|
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,
|
|
triggerBoxes = _triggerBoxes,
|
|
};
|
|
|
|
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);
|
|
}
|
|
|
|
}
|
|
}
|