psst
This commit is contained in:
496
Editor/PSXSceneValidatorWindow.cs
Normal file
496
Editor/PSXSceneValidatorWindow.cs
Normal file
@@ -0,0 +1,496 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using SplashEdit.RuntimeCode;
|
||||
|
||||
namespace SplashEdit.EditorCode
|
||||
{
|
||||
/// <summary>
|
||||
/// Scene Validator Window - Validates the current scene for PS1 compatibility.
|
||||
/// Checks for common issues that would cause problems on real hardware.
|
||||
/// </summary>
|
||||
public class PSXSceneValidatorWindow : EditorWindow
|
||||
{
|
||||
private Vector2 scrollPosition;
|
||||
private List<ValidationResult> validationResults = new List<ValidationResult>();
|
||||
private bool hasValidated = false;
|
||||
private int errorCount = 0;
|
||||
private int warningCount = 0;
|
||||
private int infoCount = 0;
|
||||
|
||||
// Filter toggles
|
||||
private bool showErrors = true;
|
||||
private bool showWarnings = true;
|
||||
private bool showInfo = true;
|
||||
|
||||
// PS1 Limits
|
||||
private const int MAX_RECOMMENDED_TRIS_PER_OBJECT = 100;
|
||||
private const int MAX_RECOMMENDED_TOTAL_TRIS = 400;
|
||||
private const int MAX_VERTEX_COORD = 32767; // signed 16-bit
|
||||
private const int MIN_VERTEX_COORD = -32768;
|
||||
private const int VRAM_WIDTH = 1024;
|
||||
private const int VRAM_HEIGHT = 512;
|
||||
|
||||
private static readonly Vector2 MinSize = new Vector2(500, 400);
|
||||
|
||||
public static void ShowWindow()
|
||||
{
|
||||
var window = GetWindow<PSXSceneValidatorWindow>("Scene Validator");
|
||||
window.minSize = MinSize;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
validationResults.Clear();
|
||||
hasValidated = false;
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
DrawHeader();
|
||||
DrawFilters();
|
||||
DrawResults();
|
||||
DrawFooter();
|
||||
}
|
||||
|
||||
private void DrawHeader()
|
||||
{
|
||||
EditorGUILayout.Space(5);
|
||||
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
GUILayout.Label("PS1 Scene Validator", EditorStyles.boldLabel);
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
if (GUILayout.Button("Validate Scene", GUILayout.Width(120)))
|
||||
{
|
||||
ValidateScene();
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(5);
|
||||
|
||||
// Summary bar
|
||||
if (hasValidated)
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope(EditorStyles.helpBox))
|
||||
{
|
||||
var errorStyle = new GUIStyle(EditorStyles.label);
|
||||
errorStyle.normal.textColor = errorCount > 0 ? Color.red : Color.green;
|
||||
GUILayout.Label($"✗ {errorCount} Errors", errorStyle);
|
||||
|
||||
var warnStyle = new GUIStyle(EditorStyles.label);
|
||||
warnStyle.normal.textColor = warningCount > 0 ? new Color(1f, 0.7f, 0f) : Color.green;
|
||||
GUILayout.Label($"⚠ {warningCount} Warnings", warnStyle);
|
||||
|
||||
var infoStyle = new GUIStyle(EditorStyles.label);
|
||||
infoStyle.normal.textColor = Color.cyan;
|
||||
GUILayout.Label($"ℹ {infoCount} Info", infoStyle);
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(5);
|
||||
}
|
||||
|
||||
private void DrawFilters()
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
GUILayout.Label("Show:", GUILayout.Width(40));
|
||||
showErrors = GUILayout.Toggle(showErrors, "Errors", EditorStyles.miniButtonLeft);
|
||||
showWarnings = GUILayout.Toggle(showWarnings, "Warnings", EditorStyles.miniButtonMid);
|
||||
showInfo = GUILayout.Toggle(showInfo, "Info", EditorStyles.miniButtonRight);
|
||||
GUILayout.FlexibleSpace();
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(5);
|
||||
}
|
||||
|
||||
private void DrawResults()
|
||||
{
|
||||
using (var scrollView = new EditorGUILayout.ScrollViewScope(scrollPosition))
|
||||
{
|
||||
scrollPosition = scrollView.scrollPosition;
|
||||
|
||||
if (!hasValidated)
|
||||
{
|
||||
EditorGUILayout.HelpBox("Click 'Validate Scene' to check for PS1 compatibility issues.", MessageType.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
if (validationResults.Count == 0)
|
||||
{
|
||||
EditorGUILayout.HelpBox("No issues found! Your scene looks ready for PS1 export.", MessageType.Info);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var result in validationResults)
|
||||
{
|
||||
if (result.Type == ValidationType.Error && !showErrors) continue;
|
||||
if (result.Type == ValidationType.Warning && !showWarnings) continue;
|
||||
if (result.Type == ValidationType.Info && !showInfo) continue;
|
||||
|
||||
DrawValidationResult(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawValidationResult(ValidationResult result)
|
||||
{
|
||||
MessageType msgType = result.Type switch
|
||||
{
|
||||
ValidationType.Error => MessageType.Error,
|
||||
ValidationType.Warning => MessageType.Warning,
|
||||
_ => MessageType.Info
|
||||
};
|
||||
|
||||
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
|
||||
{
|
||||
EditorGUILayout.HelpBox(result.Message, msgType);
|
||||
|
||||
if (result.RelatedObject != null)
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
GUILayout.Label("Object:", GUILayout.Width(50));
|
||||
|
||||
if (GUILayout.Button(result.RelatedObject.name, EditorStyles.linkLabel))
|
||||
{
|
||||
Selection.activeObject = result.RelatedObject;
|
||||
EditorGUIUtility.PingObject(result.RelatedObject);
|
||||
}
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
if (!string.IsNullOrEmpty(result.FixAction))
|
||||
{
|
||||
if (GUILayout.Button("Fix", GUILayout.Width(50)))
|
||||
{
|
||||
ApplyFix(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(2);
|
||||
}
|
||||
|
||||
private void DrawFooter()
|
||||
{
|
||||
EditorGUILayout.Space(10);
|
||||
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
if (GUILayout.Button("Select All With Errors"))
|
||||
{
|
||||
var errorObjects = validationResults
|
||||
.Where(r => r.Type == ValidationType.Error && r.RelatedObject != null)
|
||||
.Select(r => r.RelatedObject)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
Selection.objects = errorObjects;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateScene()
|
||||
{
|
||||
validationResults.Clear();
|
||||
errorCount = 0;
|
||||
warningCount = 0;
|
||||
infoCount = 0;
|
||||
|
||||
// Check for scene exporter
|
||||
ValidateSceneExporter();
|
||||
|
||||
// Check all PSX objects
|
||||
ValidatePSXObjects();
|
||||
|
||||
// Check textures and VRAM
|
||||
ValidateTextures();
|
||||
|
||||
|
||||
// Check Lua files
|
||||
ValidateLuaFiles();
|
||||
|
||||
// Overall scene stats
|
||||
ValidateSceneStats();
|
||||
|
||||
hasValidated = true;
|
||||
Repaint();
|
||||
}
|
||||
|
||||
private void ValidateSceneExporter()
|
||||
{
|
||||
var exporters = Object.FindObjectsOfType<PSXSceneExporter>();
|
||||
|
||||
if (exporters.Length == 0)
|
||||
{
|
||||
AddResult(ValidationType.Error,
|
||||
"No PSXSceneExporter found in scene. Add one via GameObject > PlayStation 1 > Scene Exporter",
|
||||
null, "AddExporter");
|
||||
}
|
||||
else if (exporters.Length > 1)
|
||||
{
|
||||
AddResult(ValidationType.Warning,
|
||||
$"Multiple PSXSceneExporters found ({exporters.Length}). Only one is needed per scene.",
|
||||
exporters[0].gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidatePSXObjects()
|
||||
{
|
||||
var exporters = Object.FindObjectsOfType<PSXObjectExporter>();
|
||||
|
||||
if (exporters.Length == 0)
|
||||
{
|
||||
AddResult(ValidationType.Info,
|
||||
"No objects marked for PSX export. Add PSXObjectExporter components to GameObjects you want to export.",
|
||||
null);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var exporter in exporters)
|
||||
{
|
||||
ValidateSingleObject(exporter);
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateSingleObject(PSXObjectExporter exporter)
|
||||
{
|
||||
var go = exporter.gameObject;
|
||||
|
||||
// Check for mesh
|
||||
var meshFilter = go.GetComponent<MeshFilter>();
|
||||
if (meshFilter == null || meshFilter.sharedMesh == null)
|
||||
{
|
||||
AddResult(ValidationType.Warning,
|
||||
$"'{go.name}' has no mesh. It will be exported as an empty object.",
|
||||
go);
|
||||
return;
|
||||
}
|
||||
|
||||
var mesh = meshFilter.sharedMesh;
|
||||
int triCount = mesh.triangles.Length / 3;
|
||||
|
||||
// Check triangle count
|
||||
if (triCount > MAX_RECOMMENDED_TRIS_PER_OBJECT)
|
||||
{
|
||||
AddResult(ValidationType.Warning,
|
||||
$"'{go.name}' has {triCount} triangles (recommended max: {MAX_RECOMMENDED_TRIS_PER_OBJECT}). Consider simplifying.",
|
||||
go);
|
||||
}
|
||||
|
||||
// Check vertex coordinates for GTE limits
|
||||
var vertices = mesh.vertices;
|
||||
var transform = go.transform;
|
||||
bool hasOutOfBounds = false;
|
||||
|
||||
foreach (var vert in vertices)
|
||||
{
|
||||
var worldPos = transform.TransformPoint(vert);
|
||||
// Check if fixed-point conversion would overflow (assuming scale factor)
|
||||
float scaledX = worldPos.x * 4096f; // FixedPoint<12> scale
|
||||
float scaledY = worldPos.y * 4096f;
|
||||
float scaledZ = worldPos.z * 4096f;
|
||||
|
||||
if (scaledX > MAX_VERTEX_COORD || scaledX < MIN_VERTEX_COORD ||
|
||||
scaledY > MAX_VERTEX_COORD || scaledY < MIN_VERTEX_COORD ||
|
||||
scaledZ > MAX_VERTEX_COORD || scaledZ < MIN_VERTEX_COORD)
|
||||
{
|
||||
hasOutOfBounds = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasOutOfBounds)
|
||||
{
|
||||
AddResult(ValidationType.Error,
|
||||
$"'{go.name}' has vertices that exceed PS1 coordinate limits. Move closer to origin or scale down.",
|
||||
go);
|
||||
}
|
||||
|
||||
// Check for renderer and material
|
||||
var renderer = go.GetComponent<MeshRenderer>();
|
||||
if (renderer == null)
|
||||
{
|
||||
AddResult(ValidationType.Info,
|
||||
$"'{go.name}' has no MeshRenderer. Will be exported without visual rendering.",
|
||||
go);
|
||||
}
|
||||
else if (renderer.sharedMaterial == null)
|
||||
{
|
||||
AddResult(ValidationType.Warning,
|
||||
$"'{go.name}' has no material assigned. Will use default colors.",
|
||||
go);
|
||||
}
|
||||
|
||||
// Check texture settings on exporter
|
||||
if (exporter.texture != null)
|
||||
{
|
||||
ValidateTexture(exporter.texture, go);
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateTextures()
|
||||
{
|
||||
var exporters = Object.FindObjectsOfType<PSXObjectExporter>();
|
||||
var textures = exporters
|
||||
.Where(e => e.texture != null)
|
||||
.Select(e => e.texture)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (textures.Count == 0)
|
||||
{
|
||||
AddResult(ValidationType.Info,
|
||||
"No textures assigned to any PSX objects. Scene will be vertex-colored only.",
|
||||
null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Rough VRAM estimation
|
||||
int estimatedVramUsage = 0;
|
||||
foreach (var tex in textures)
|
||||
{
|
||||
// Rough estimate: width * height * bits/8
|
||||
// This is simplified - actual packing is more complex
|
||||
int bitsPerPixel = 16; // Assume 16bpp worst case
|
||||
estimatedVramUsage += (tex.width * tex.height * bitsPerPixel) / 8;
|
||||
}
|
||||
|
||||
int vramTotal = VRAM_WIDTH * VRAM_HEIGHT * 2; // 16bpp
|
||||
int vramAvailable = vramTotal / 2; // Assume half for framebuffers
|
||||
|
||||
if (estimatedVramUsage > vramAvailable)
|
||||
{
|
||||
AddResult(ValidationType.Warning,
|
||||
$"Estimated texture VRAM usage ({estimatedVramUsage / 1024}KB) may exceed available space (~{vramAvailable / 1024}KB). " +
|
||||
"Consider using lower bit depths or smaller textures.",
|
||||
null);
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateTexture(Texture2D texture, GameObject relatedObject)
|
||||
{
|
||||
// Check power of 2
|
||||
if (!Mathf.IsPowerOfTwo(texture.width) || !Mathf.IsPowerOfTwo(texture.height))
|
||||
{
|
||||
AddResult(ValidationType.Warning,
|
||||
$"Texture '{texture.name}' dimensions ({texture.width}x{texture.height}) are not power of 2. May cause issues.",
|
||||
relatedObject);
|
||||
}
|
||||
|
||||
// Check max size
|
||||
if (texture.width > 256 || texture.height > 256)
|
||||
{
|
||||
AddResult(ValidationType.Warning,
|
||||
$"Texture '{texture.name}' is large ({texture.width}x{texture.height}). Consider using 256x256 or smaller.",
|
||||
relatedObject);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void ValidateLuaFiles()
|
||||
{
|
||||
var exporters = Object.FindObjectsOfType<PSXObjectExporter>();
|
||||
|
||||
foreach (var exporter in exporters)
|
||||
{
|
||||
if (exporter.LuaFile != null)
|
||||
{
|
||||
// Check if Lua file exists and is valid
|
||||
string path = AssetDatabase.GetAssetPath(exporter.LuaFile);
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
AddResult(ValidationType.Error,
|
||||
$"'{exporter.name}' references an invalid Lua file.",
|
||||
exporter.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateSceneStats()
|
||||
{
|
||||
var exporters = Object.FindObjectsOfType<PSXObjectExporter>();
|
||||
int totalTris = 0;
|
||||
|
||||
foreach (var exporter in exporters)
|
||||
{
|
||||
var mf = exporter.GetComponent<MeshFilter>();
|
||||
if (mf != null && mf.sharedMesh != null)
|
||||
{
|
||||
totalTris += mf.sharedMesh.triangles.Length / 3;
|
||||
}
|
||||
}
|
||||
|
||||
AddResult(ValidationType.Info,
|
||||
$"Scene statistics: {exporters.Length} objects, {totalTris} total triangles.",
|
||||
null);
|
||||
|
||||
if (totalTris > MAX_RECOMMENDED_TOTAL_TRIS)
|
||||
{
|
||||
AddResult(ValidationType.Warning,
|
||||
$"Total triangle count ({totalTris}) exceeds recommended maximum ({MAX_RECOMMENDED_TOTAL_TRIS}). " +
|
||||
"Performance may be poor on real hardware.",
|
||||
null);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddResult(ValidationType type, string message, GameObject relatedObject, string fixAction = null)
|
||||
{
|
||||
validationResults.Add(new ValidationResult
|
||||
{
|
||||
Type = type,
|
||||
Message = message,
|
||||
RelatedObject = relatedObject,
|
||||
FixAction = fixAction
|
||||
});
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case ValidationType.Error: errorCount++; break;
|
||||
case ValidationType.Warning: warningCount++; break;
|
||||
case ValidationType.Info: infoCount++; break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyFix(ValidationResult result)
|
||||
{
|
||||
switch (result.FixAction)
|
||||
{
|
||||
case "AddExporter":
|
||||
var go = new GameObject("PSXSceneExporter");
|
||||
go.AddComponent<PSXSceneExporter>();
|
||||
Undo.RegisterCreatedObjectUndo(go, "Create PSX Scene Exporter");
|
||||
Selection.activeGameObject = go;
|
||||
ValidateScene(); // Re-validate
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private enum ValidationType
|
||||
{
|
||||
Error,
|
||||
Warning,
|
||||
Info
|
||||
}
|
||||
|
||||
private class ValidationResult
|
||||
{
|
||||
public ValidationType Type;
|
||||
public string Message;
|
||||
public GameObject RelatedObject;
|
||||
public string FixAction;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user