From 53e993f58ef298779fac887da696e8f1bf98247d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20R=C3=A1=C4=8Dek?= Date: Thu, 4 Sep 2025 18:01:23 +0200 Subject: [PATCH] feature: Added psxsplash installer, added basic BSP implementation (non-functional) --- Debug.unity | 872 ++++++++++++++++++++++++++ Debug.unity.meta | 7 + Editor/DependencyInstallerWindow.cs | 675 ++++++++++++++------ Editor/PSXSplashInstaller.cs | 203 ++++++ Editor/PSXSplashInstaller.cs.meta | 2 + Editor/SerialConnection.cs | 39 ++ Editor/SerialConnection.cs.meta | 2 + Editor/UniromConnection.cs | 22 + Editor/UniromConnection.cs.meta | 2 + Editor/UniromConnectionWindow.cs | 145 +++++ Editor/UniromConnectionWindow.cs.meta | 2 + Runtime/BSP.cs | 845 +++++++++++++++++++++++++ Runtime/BSP.cs.meta | 2 + Runtime/PSXData.cs | 22 + Runtime/PSXMesh.cs | 217 +++++-- Runtime/PSXObjectExporter.cs | 68 +- Runtime/PSXSceneExporter.cs | 14 +- 17 files changed, 2871 insertions(+), 268 deletions(-) create mode 100644 Debug.unity create mode 100644 Debug.unity.meta create mode 100644 Editor/PSXSplashInstaller.cs create mode 100644 Editor/PSXSplashInstaller.cs.meta create mode 100644 Editor/SerialConnection.cs create mode 100644 Editor/SerialConnection.cs.meta create mode 100644 Editor/UniromConnection.cs create mode 100644 Editor/UniromConnection.cs.meta create mode 100644 Editor/UniromConnectionWindow.cs create mode 100644 Editor/UniromConnectionWindow.cs.meta create mode 100644 Runtime/BSP.cs create mode 100644 Runtime/BSP.cs.meta diff --git a/Debug.unity b/Debug.unity new file mode 100644 index 0000000..8aefc5e --- /dev/null +++ b/Debug.unity @@ -0,0 +1,872 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 10 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 13 + m_BakeOnSceneLoad: 0 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 1 + m_PVRFilteringGaussRadiusAO: 1 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &283344192 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 283344197} + - component: {fileID: 283344196} + - component: {fileID: 283344195} + - component: {fileID: 283344194} + - component: {fileID: 283344193} + m_Layer: 0 + m_Name: f (1) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &283344193 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 283344192} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: bea0f31a495202580ac77bd9fd6e99f2, type: 3} + m_Name: + m_EditorClassIdentifier: + IsActive: 1 + bitDepth: 8 + luaFile: {fileID: 0} + previewNormals: 0 + normalPreviewLength: 0.5 +--- !u!65 &283344194 +BoxCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 283344192} + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_IsTrigger: 0 + m_ProvidesContacts: 0 + m_Enabled: 1 + serializedVersion: 3 + m_Size: {x: 1, y: 1, z: 1} + m_Center: {x: 0, y: 0, z: 0} +--- !u!23 &283344195 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 283344192} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!33 &283344196 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 283344192} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!4 &283344197 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 283344192} + serializedVersion: 2 + m_LocalRotation: {x: -0.1479161, y: 0.37224695, z: -0.33835596, w: 0.85150945} + m_LocalPosition: {x: -30.19757, y: 0, z: 8.341} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 47.226, z: -43.342} +--- !u!1 &1293128684 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1293128689} + - component: {fileID: 1293128688} + - component: {fileID: 1293128687} + - component: {fileID: 1293128686} + - component: {fileID: 1293128685} + m_Layer: 0 + m_Name: f + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1293128685 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1293128684} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: bea0f31a495202580ac77bd9fd6e99f2, type: 3} + m_Name: + m_EditorClassIdentifier: + IsActive: 1 + bitDepth: 8 + luaFile: {fileID: 0} + previewNormals: 0 + normalPreviewLength: 0.5 +--- !u!65 &1293128686 +BoxCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1293128684} + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_IsTrigger: 0 + m_ProvidesContacts: 0 + m_Enabled: 1 + serializedVersion: 3 + m_Size: {x: 1, y: 1, z: 1} + m_Center: {x: 0, y: 0, z: 0} +--- !u!23 &1293128687 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1293128684} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!33 &1293128688 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1293128684} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!4 &1293128689 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1293128684} + serializedVersion: 2 + m_LocalRotation: {x: -0.13355339, y: 0.35044792, z: -0.3301185, w: 0.8662399} + m_LocalPosition: {x: -30.19757, y: 0, z: 10.84006} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 44.053, z: -41.723} +--- !u!1 &1331845601 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1331845603} + - component: {fileID: 1331845602} + m_Layer: 0 + m_Name: Exp + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1331845602 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1331845601} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: ab5195ad94fd173cfb6d48ee06eaf245, type: 3} + m_Name: + m_EditorClassIdentifier: + GTEScaling: 100 + SceneLuaFile: {fileID: 0} + BSPPreviewDepth: 2 +--- !u!4 &1331845603 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1331845601} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: -31.5414, y: 0, z: 10.89769} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1337453585 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1337453587} + - component: {fileID: 1337453586} + m_Layer: 0 + m_Name: Directional Light + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!108 &1337453586 +Light: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1337453585} + m_Enabled: 1 + serializedVersion: 11 + m_Type: 1 + m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1} + m_Intensity: 1 + m_Range: 10 + m_SpotAngle: 30 + m_InnerSpotAngle: 21.80208 + m_CookieSize: 10 + m_Shadows: + m_Type: 2 + m_Resolution: -1 + m_CustomResolution: -1 + m_Strength: 1 + m_Bias: 0.05 + m_NormalBias: 0.4 + m_NearPlane: 0.2 + m_CullingMatrixOverride: + e00: 1 + e01: 0 + e02: 0 + e03: 0 + e10: 0 + e11: 1 + e12: 0 + e13: 0 + e20: 0 + e21: 0 + e22: 1 + e23: 0 + e30: 0 + e31: 0 + e32: 0 + e33: 1 + m_UseCullingMatrixOverride: 0 + m_Cookie: {fileID: 0} + m_DrawHalo: 0 + m_Flare: {fileID: 0} + m_RenderMode: 0 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingLayerMask: 1 + m_Lightmapping: 4 + m_LightShadowCasterMode: 0 + m_AreaSize: {x: 1, y: 1} + m_BounceIntensity: 1 + m_ColorTemperature: 6570 + m_UseColorTemperature: 0 + m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} + m_UseBoundingSphereOverride: 0 + m_UseViewFrustumForShadowCasterCull: 1 + m_ForceVisible: 0 + m_ShadowRadius: 0 + m_ShadowAngle: 0 + m_LightUnit: 1 + m_LuxAtDistance: 1 + m_EnableSpotReflector: 1 +--- !u!4 &1337453587 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1337453585} + serializedVersion: 2 + m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261} + m_LocalPosition: {x: 0, y: 3, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0} +--- !u!1 &1388445967 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1388445970} + - component: {fileID: 1388445969} + - component: {fileID: 1388445968} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &1388445968 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1388445967} + m_Enabled: 1 +--- !u!20 &1388445969 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1388445967} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 0 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &1388445970 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1388445967} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1392840323 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1392840328} + - component: {fileID: 1392840327} + - component: {fileID: 1392840326} + - component: {fileID: 1392840325} + - component: {fileID: 1392840324} + m_Layer: 0 + m_Name: f (2) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1392840324 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1392840323} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: bea0f31a495202580ac77bd9fd6e99f2, type: 3} + m_Name: + m_EditorClassIdentifier: + IsActive: 1 + bitDepth: 8 + luaFile: {fileID: 0} + previewNormals: 0 + normalPreviewLength: 0.5 +--- !u!65 &1392840325 +BoxCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1392840323} + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_IsTrigger: 0 + m_ProvidesContacts: 0 + m_Enabled: 1 + serializedVersion: 3 + m_Size: {x: 1, y: 1, z: 1} + m_Center: {x: 0, y: 0, z: 0} +--- !u!23 &1392840326 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1392840323} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!33 &1392840327 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1392840323} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!4 &1392840328 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1392840323} + serializedVersion: 2 + m_LocalRotation: {x: -0.409346, y: -0.7736037, z: 0.22623056, w: 0.42754278} + m_LocalPosition: {x: -30.19757, y: 2.041, z: 10.84006} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: -122.144, z: 55.77} +--- !u!1 &1513869424 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1513869429} + - component: {fileID: 1513869428} + - component: {fileID: 1513869427} + - component: {fileID: 1513869426} + - component: {fileID: 1513869425} + m_Layer: 0 + m_Name: f (3) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1513869425 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1513869424} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: bea0f31a495202580ac77bd9fd6e99f2, type: 3} + m_Name: + m_EditorClassIdentifier: + IsActive: 1 + bitDepth: 8 + luaFile: {fileID: 0} + previewNormals: 0 + normalPreviewLength: 0.5 +--- !u!65 &1513869426 +BoxCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1513869424} + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_IsTrigger: 0 + m_ProvidesContacts: 0 + m_Enabled: 1 + serializedVersion: 3 + m_Size: {x: 1, y: 1, z: 1} + m_Center: {x: 0, y: 0, z: 0} +--- !u!23 &1513869427 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1513869424} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!33 &1513869428 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1513869424} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!4 &1513869429 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1513869424} + serializedVersion: 2 + m_LocalRotation: {x: -0.09615398, y: 0.2551484, z: 0.3392829, w: 0.9003004} + m_LocalPosition: {x: -27.156, y: 0, z: 9.065} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: -20.259, y: 24.845, z: 36.791} +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 1388445970} + - {fileID: 1337453587} + - {fileID: 1293128689} + - {fileID: 283344197} + - {fileID: 1392840328} + - {fileID: 1513869429} + - {fileID: 1331845603} diff --git a/Debug.unity.meta b/Debug.unity.meta new file mode 100644 index 0000000..e43b038 --- /dev/null +++ b/Debug.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 14e1ffa687155c145bfed30e28f26bc1 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/DependencyInstallerWindow.cs b/Editor/DependencyInstallerWindow.cs index 34bba05..aa0a9e7 100644 --- a/Editor/DependencyInstallerWindow.cs +++ b/Editor/DependencyInstallerWindow.cs @@ -2,214 +2,521 @@ using UnityEngine; using UnityEditor; using System.Collections.Generic; using SplashEdit.RuntimeCode; +using System.IO; +using System.Linq; +using System.Threading.Tasks; -public class InstallerWindow : EditorWindow + +namespace SplashEdit.EditorCode { - // Cached status for MIPS toolchain binaries. - private Dictionary mipsToolStatus = new Dictionary(); - - // Cached status for optional tools. - private bool makeInstalled; - private bool gdbInstalled; - private string pcsxReduxPath; - - private bool isInstalling = false; - - [MenuItem("PSX/Toolchain & Build Tools Installer")] - public static void ShowWindow() + public class InstallerWindow : EditorWindow { - InstallerWindow window = GetWindow("Toolchain Installer"); - window.RefreshToolStatus(); - window.pcsxReduxPath = DataStorage.LoadData().PCSXReduxPath; - } + // Cached status for MIPS toolchain binaries. + private Dictionary mipsToolStatus = new Dictionary(); - /// - /// Refresh the cached statuses for all tools. - /// - private void RefreshToolStatus() - { - mipsToolStatus.Clear(); - foreach (var tool in ToolchainChecker.GetRequiredTools()) + // Cached status for optional tools. + private bool makeInstalled; + private bool gdbInstalled; + private string pcsxReduxPath; + + // PSXSplash related variables + private bool psxsplashInstalled = false; + private bool psxsplashInstalling = false; + private bool psxsplashFetching = false; + private string selectedVersion = "main"; + private Dictionary availableBranches = new Dictionary(); + private List availableReleases = new List(); + private bool showBranches = true; + private bool showReleases = false; + private Vector2 scrollPosition; + private Vector2 versionScrollPosition; + + private bool isInstalling = false; + + [MenuItem("PSX/Toolchain & Build Tools Installer")] + public static void ShowWindow() { - mipsToolStatus[tool] = ToolchainChecker.IsToolAvailable(tool); + InstallerWindow window = GetWindow("Toolchain Installer"); + window.RefreshToolStatus(); + window.pcsxReduxPath = DataStorage.LoadData().PCSXReduxPath; + window.CheckPSXSplashInstallation(); } - makeInstalled = ToolchainChecker.IsToolAvailable("make"); - gdbInstalled = ToolchainChecker.IsToolAvailable("gdb-multiarch"); - } - - private void OnGUI() - { - GUILayout.Label("Toolchain & Build Tools Installer", EditorStyles.boldLabel); - GUILayout.Space(5); - - if (GUILayout.Button("Refresh Status")) + /// + /// Refresh the cached statuses for all tools. + /// + private void RefreshToolStatus() { - RefreshToolStatus(); - } - GUILayout.Space(10); - - EditorGUILayout.BeginHorizontal(); - DrawToolchainColumn(); - DrawAdditionalToolsColumn(); - EditorGUILayout.EndHorizontal(); - } - - private void DrawToolchainColumn() - { - EditorGUILayout.BeginVertical("box", GUILayout.MaxWidth(position.width / 2 - 10)); - GUILayout.Label("MIPS Toolchain", EditorStyles.boldLabel); - GUILayout.Space(5); - - // Display cached status for each required MIPS tool. - foreach (var kvp in mipsToolStatus) - { - GUI.color = kvp.Value ? Color.green : Color.red; - GUILayout.Label($"{kvp.Key}: {(kvp.Value ? "Found" : "Missing")}"); - } - GUI.color = Color.white; - GUILayout.Space(5); - - if (GUILayout.Button("Install MIPS Toolchain")) - { - if (!isInstalling) - InstallMipsToolchainAsync(); - } - EditorGUILayout.EndVertical(); - } - - private void DrawAdditionalToolsColumn() - { - EditorGUILayout.BeginVertical("box", GUILayout.MaxWidth(position.width / 2 - 10)); - GUILayout.Label("Optional Tools", EditorStyles.boldLabel); - GUILayout.Space(5); - - // GNU Make status (required). - GUI.color = makeInstalled ? Color.green : Color.red; - GUILayout.Label($"GNU Make: {(makeInstalled ? "Found" : "Missing")} (Required)"); - GUI.color = Color.white; - GUILayout.Space(5); - if (GUILayout.Button("Install GNU Make")) - { - if (!isInstalling) - InstallMakeAsync(); - } - - GUILayout.Space(10); - - // GDB status (optional). - GUI.color = gdbInstalled ? Color.green : Color.red; - GUILayout.Label($"GDB: {(gdbInstalled ? "Found" : "Missing")} (Optional)"); - GUI.color = Color.white; - GUILayout.Space(5); - if (GUILayout.Button("Install GDB")) - { - if (!isInstalling) - InstallGDBAsync(); - } - - GUILayout.Space(10); - - // PCSX-Redux (manual install) - GUI.color = string.IsNullOrEmpty(pcsxReduxPath) ? Color.red : Color.green; - GUILayout.Label($"PCSX-Redux: {(string.IsNullOrEmpty(pcsxReduxPath) ? "Not Configured" : "Configured")} (Optional)"); - GUI.color = Color.white; - - GUILayout.BeginHorizontal(); - if (GUILayout.Button("Browse for PCSX-Redux")) - { - string selectedPath = EditorUtility.OpenFilePanel("Select PCSX-Redux Executable", "", ""); - if (!string.IsNullOrEmpty(selectedPath)) + mipsToolStatus.Clear(); + foreach (var tool in ToolchainChecker.GetRequiredTools()) { - pcsxReduxPath = selectedPath; - PSXData data = DataStorage.LoadData(); - data.PCSXReduxPath = pcsxReduxPath; - DataStorage.StoreData(data); + mipsToolStatus[tool] = ToolchainChecker.IsToolAvailable(tool); + } + + makeInstalled = ToolchainChecker.IsToolAvailable("make"); + gdbInstalled = ToolchainChecker.IsToolAvailable("gdb-multiarch"); + } + + private void CheckPSXSplashInstallation() + { + psxsplashInstalled = PSXSplashInstaller.IsInstalled(); + + if (psxsplashInstalled) + { + FetchPSXSplashVersions(); + } + else + { + availableBranches = new Dictionary(); + availableReleases = new List(); } } - if (!string.IsNullOrEmpty(pcsxReduxPath)) + + private async void FetchPSXSplashVersions() { - if (GUILayout.Button("Clear", GUILayout.Width(60))) + if (psxsplashFetching) return; + + psxsplashFetching = true; + try { - pcsxReduxPath = ""; - PSXData data = DataStorage.LoadData(); - data.PCSXReduxPath = pcsxReduxPath; - DataStorage.StoreData(data); + // Fetch latest from remote + await PSXSplashInstaller.FetchLatestAsync(); + + // Get all available versions + var branchesTask = PSXSplashInstaller.GetBranchesWithLatestCommitsAsync(); + var releasesTask = PSXSplashInstaller.GetReleasesAsync(); + + await Task.WhenAll(branchesTask, releasesTask); + + availableBranches = branchesTask.Result; + availableReleases = releasesTask.Result; + + // If no branches were found, add main as default + if (!availableBranches.Any()) + { + availableBranches["main"] = "latest"; + } + + // Select the first branch by default + if (availableBranches.Any() && string.IsNullOrEmpty(selectedVersion)) + { + selectedVersion = availableBranches.Keys.First(); + } + + Repaint(); + } + catch (System.Exception e) + { + UnityEngine.Debug.LogError($"Failed to fetch PSXSplash versions: {e.Message}"); + } + finally + { + psxsplashFetching = false; } } - GUILayout.EndHorizontal(); - EditorGUILayout.EndVertical(); - } - private async void InstallMipsToolchainAsync() - { - try + private void OnGUI() { - isInstalling = true; - EditorUtility.DisplayProgressBar("Installing MIPS Toolchain", - "Please wait while the MIPS toolchain is being installed...", 0f); - bool success = await ToolchainInstaller.InstallToolchain(); - EditorUtility.ClearProgressBar(); - if (success) + scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); + + GUILayout.Label("Toolchain & Build Tools Installer", EditorStyles.boldLabel); + GUILayout.Space(5); + + if (GUILayout.Button("Refresh Status")) { - EditorUtility.DisplayDialog("Installation Complete", "MIPS toolchain installed successfully.", "OK"); + RefreshToolStatus(); + CheckPSXSplashInstallation(); } - RefreshToolStatus(); // Update cached statuses after installation - } - catch (System.Exception ex) - { - EditorUtility.ClearProgressBar(); - EditorUtility.DisplayDialog("Installation Failed", $"Error: {ex.Message}", "OK"); - } - finally - { - isInstalling = false; - } - } + GUILayout.Space(10); - private async void InstallMakeAsync() - { - try - { - isInstalling = true; - EditorUtility.DisplayProgressBar("Installing GNU Make", - "Please wait while GNU Make is being installed...", 0f); - await ToolchainInstaller.InstallMake(); - EditorUtility.ClearProgressBar(); - EditorUtility.DisplayDialog("Installation Complete", "GNU Make installed successfully.", "OK"); - RefreshToolStatus(); - } - catch (System.Exception ex) - { - EditorUtility.ClearProgressBar(); - EditorUtility.DisplayDialog("Installation Failed", $"Error: {ex.Message}", "OK"); - } - finally - { - isInstalling = false; - } - } + EditorGUILayout.BeginHorizontal(); + DrawToolchainColumn(); + DrawAdditionalToolsColumn(); + DrawPSXSplashColumn(); + EditorGUILayout.EndHorizontal(); - private async void InstallGDBAsync() - { - try - { - isInstalling = true; - EditorUtility.DisplayProgressBar("Installing GDB", - "Please wait while GDB is being installed...", 0f); - await ToolchainInstaller.InstallGDB(); - EditorUtility.ClearProgressBar(); - EditorUtility.DisplayDialog("Installation Complete", "GDB installed successfully.", "OK"); - RefreshToolStatus(); + EditorGUILayout.EndScrollView(); } - catch (System.Exception ex) + + private void DrawToolchainColumn() { - EditorUtility.ClearProgressBar(); - EditorUtility.DisplayDialog("Installation Failed", $"Error: {ex.Message}", "OK"); + EditorGUILayout.BeginVertical("box", GUILayout.MaxWidth(position.width / 3 - 10)); + GUILayout.Label("MIPS Toolchain", EditorStyles.boldLabel); + GUILayout.Space(5); + + // Display cached status for each required MIPS tool. + foreach (var kvp in mipsToolStatus) + { + GUI.color = kvp.Value ? Color.green : Color.red; + GUILayout.Label($"{kvp.Key}: {(kvp.Value ? "Found" : "Missing")}"); + } + GUI.color = Color.white; + GUILayout.Space(5); + + if (GUILayout.Button("Install MIPS Toolchain")) + { + if (!isInstalling) + InstallMipsToolchainAsync(); + } + EditorGUILayout.EndVertical(); } - finally + + private void DrawAdditionalToolsColumn() { - isInstalling = false; + EditorGUILayout.BeginVertical("box", GUILayout.MaxWidth(position.width / 3 - 10)); + GUILayout.Label("Optional Tools", EditorStyles.boldLabel); + GUILayout.Space(5); + + // GNU Make status (required). + GUI.color = makeInstalled ? Color.green : Color.red; + GUILayout.Label($"GNU Make: {(makeInstalled ? "Found" : "Missing")} (Required)"); + GUI.color = Color.white; + GUILayout.Space(5); + if (GUILayout.Button("Install GNU Make")) + { + if (!isInstalling) + InstallMakeAsync(); + } + + GUILayout.Space(10); + + // GDB status (optional). + GUI.color = gdbInstalled ? Color.green : Color.red; + GUILayout.Label($"GDB: {(gdbInstalled ? "Found" : "Missing")} (Optional)"); + GUI.color = Color.white; + GUILayout.Space(5); + if (GUILayout.Button("Install GDB")) + { + if (!isInstalling) + InstallGDBAsync(); + } + + GUILayout.Space(10); + + // PCSX-Redux (manual install) + GUI.color = string.IsNullOrEmpty(pcsxReduxPath) ? Color.red : Color.green; + GUILayout.Label($"PCSX-Redux: {(string.IsNullOrEmpty(pcsxReduxPath) ? "Not Configured" : "Configured")} (Optional)"); + GUI.color = Color.white; + + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Browse for PCSX-Redux")) + { + string selectedPath = EditorUtility.OpenFilePanel("Select PCSX-Redux Executable", "", ""); + if (!string.IsNullOrEmpty(selectedPath)) + { + pcsxReduxPath = selectedPath; + PSXData data = DataStorage.LoadData(); + data.PCSXReduxPath = pcsxReduxPath; + DataStorage.StoreData(data); + } + } + if (!string.IsNullOrEmpty(pcsxReduxPath)) + { + if (GUILayout.Button("Clear", GUILayout.Width(60))) + { + pcsxReduxPath = ""; + PSXData data = DataStorage.LoadData(); + data.PCSXReduxPath = pcsxReduxPath; + DataStorage.StoreData(data); + } + } + GUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + } + + private void DrawPSXSplashColumn() + { + EditorGUILayout.BeginVertical("box", GUILayout.MaxWidth(position.width / 3 - 10)); + GUILayout.Label("PSXSplash", EditorStyles.boldLabel); + GUILayout.Space(5); + + // PSXSplash status + GUI.color = psxsplashInstalled ? Color.green : Color.red; + GUILayout.Label($"PSXSplash: {(psxsplashInstalled ? "Installed" : "Not Installed")}"); + GUI.color = Color.white; + + if (psxsplashFetching) + { + GUILayout.Label("Fetching versions..."); + } + else if (!psxsplashInstalled) + { + GUILayout.Space(5); + EditorGUILayout.HelpBox("Git is required to install PSXSplash. Make sure it's installed and available in your PATH.", MessageType.Info); + + // Show version selection even before installation + DrawVersionSelection(); + + if (GUILayout.Button("Install PSXSplash") && !psxsplashInstalling) + { + InstallPSXSplashAsync(); + } + } + else + { + GUILayout.Space(10); + + // Current version + EditorGUILayout.LabelField($"Current Version: {selectedVersion}", EditorStyles.boldLabel); + + // Version selection + DrawVersionSelection(); + + GUILayout.Space(10); + + // Refresh and update buttons + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("Refresh Versions")) + { + FetchPSXSplashVersions(); + } + + if (GUILayout.Button("Update PSXSplash")) + { + UpdatePSXSplashAsync(); + } + EditorGUILayout.EndHorizontal(); + } + + EditorGUILayout.EndVertical(); + } + + private void DrawVersionSelection() + { + EditorGUILayout.LabelField("Available Versions:", EditorStyles.boldLabel); + + versionScrollPosition = EditorGUILayout.BeginScrollView(versionScrollPosition, GUILayout.Height(200)); + + // Branches (with latest commits) + showBranches = EditorGUILayout.Foldout(showBranches, $"Branches ({availableBranches.Count})"); + if (showBranches && availableBranches.Any()) + { + foreach (var branch in availableBranches) + { + EditorGUILayout.BeginHorizontal(); + bool isSelected = selectedVersion == branch.Key; + if (GUILayout.Toggle(isSelected, "", GUILayout.Width(20)) && !isSelected) + { + selectedVersion = branch.Key; + if (psxsplashInstalled) + { + CheckoutPSXSplashVersionAsync(branch.Key); + } + } + GUILayout.Label($"{branch.Key} (Latest: {branch.Value})", EditorStyles.label); + EditorGUILayout.EndHorizontal(); + } + } + else if (showBranches) + { + GUILayout.Label("No branches available"); + } + + // Releases + showReleases = EditorGUILayout.Foldout(showReleases, $"Releases ({availableReleases.Count})"); + if (showReleases && availableReleases.Any()) + { + foreach (var release in availableReleases) + { + EditorGUILayout.BeginHorizontal(); + bool isSelected = selectedVersion == release; + if (GUILayout.Toggle(isSelected, "", GUILayout.Width(20)) && !isSelected) + { + selectedVersion = release; + if (psxsplashInstalled) + { + CheckoutPSXSplashVersionAsync(release); + } + } + GUILayout.Label(release, EditorStyles.label); + EditorGUILayout.EndHorizontal(); + } + } + else if (showReleases) + { + GUILayout.Label("No releases available"); + } + + EditorGUILayout.EndScrollView(); + } + + private async void InstallPSXSplashAsync() + { + try + { + psxsplashInstalling = true; + EditorUtility.DisplayProgressBar("Installing PSXSplash", "Cloning repository...", 0.3f); + + bool success = await PSXSplashInstaller.Install(); + + EditorUtility.ClearProgressBar(); + + if (success) + { + EditorUtility.DisplayDialog("Installation Complete", "PSXSplash installed successfully.", "OK"); + CheckPSXSplashInstallation(); + + // Checkout the selected version after installation + if (!string.IsNullOrEmpty(selectedVersion)) + { + await CheckoutPSXSplashVersionAsync(selectedVersion); + } + } + else + { + EditorUtility.DisplayDialog("Installation Failed", + "Failed to install PSXSplash. Make sure Git is installed and available in your PATH.", "OK"); + } + } + catch (System.Exception ex) + { + EditorUtility.ClearProgressBar(); + EditorUtility.DisplayDialog("Installation Failed", $"Error: {ex.Message}", "OK"); + } + finally + { + psxsplashInstalling = false; + } + } + + private async Task CheckoutPSXSplashVersionAsync(string version) + { + try + { + psxsplashInstalling = true; + EditorUtility.DisplayProgressBar("Checking Out Version", $"Switching to {version}...", 0.3f); + + bool success = await PSXSplashInstaller.CheckoutVersionAsync(version); + + EditorUtility.ClearProgressBar(); + + if (success) + { + EditorUtility.DisplayDialog("Checkout Complete", $"Switched to {version} successfully.", "OK"); + return true; + } + else + { + EditorUtility.DisplayDialog("Checkout Failed", + $"Failed to switch to {version}.", "OK"); + return false; + } + } + catch (System.Exception ex) + { + EditorUtility.ClearProgressBar(); + EditorUtility.DisplayDialog("Checkout Failed", $"Error: {ex.Message}", "OK"); + return false; + } + finally + { + psxsplashInstalling = false; + } + } + + private async void UpdatePSXSplashAsync() + { + try + { + psxsplashInstalling = true; + EditorUtility.DisplayProgressBar("Updating PSXSplash", "Pulling latest changes...", 0.3f); + + // Pull the latest changes + bool success = await PSXSplashInstaller.CheckoutVersionAsync(selectedVersion); + + EditorUtility.ClearProgressBar(); + + if (success) + { + EditorUtility.DisplayDialog("Update Complete", "PSXSplash updated successfully.", "OK"); + } + else + { + EditorUtility.DisplayDialog("Update Failed", + "Failed to update PSXSplash.", "OK"); + } + } + catch (System.Exception ex) + { + EditorUtility.ClearProgressBar(); + EditorUtility.DisplayDialog("Update Failed", $"Error: {ex.Message}", "OK"); + } + finally + { + psxsplashInstalling = false; + } + } + + private async void InstallMipsToolchainAsync() + { + try + { + isInstalling = true; + EditorUtility.DisplayProgressBar("Installing MIPS Toolchain", + "Please wait while the MIPS toolchain is being installed...", 0f); + bool success = await ToolchainInstaller.InstallToolchain(); + EditorUtility.ClearProgressBar(); + if (success) + { + EditorUtility.DisplayDialog("Installation Complete", "MIPS toolchain installed successfully.", "OK"); + } + RefreshToolStatus(); // Update cached statuses after installation + } + catch (System.Exception ex) + { + EditorUtility.ClearProgressBar(); + EditorUtility.DisplayDialog("Installation Failed", $"Error: {ex.Message}", "OK"); + } + finally + { + isInstalling = false; + } + } + + private async void InstallMakeAsync() + { + try + { + isInstalling = true; + EditorUtility.DisplayProgressBar("Installing GNU Make", + "Please wait while GNU Make is being installed...", 0f); + await ToolchainInstaller.InstallMake(); + EditorUtility.ClearProgressBar(); + EditorUtility.DisplayDialog("Installation Complete", "GNU Make installed successfully.", "OK"); + RefreshToolStatus(); + } + catch (System.Exception ex) + { + EditorUtility.ClearProgressBar(); + EditorUtility.DisplayDialog("Installation Failed", $"Error: {ex.Message}", "OK"); + } + finally + { + isInstalling = false; + } + } + + private async void InstallGDBAsync() + { + try + { + isInstalling = true; + EditorUtility.DisplayProgressBar("Installing GDB", + "Please wait while GDB is being installed...", 0f); + await ToolchainInstaller.InstallGDB(); + EditorUtility.ClearProgressBar(); + EditorUtility.DisplayDialog("Installation Complete", "GDB installed successfully.", "OK"); + RefreshToolStatus(); + } + catch (System.Exception ex) + { + EditorUtility.ClearProgressBar(); + EditorUtility.DisplayDialog("Installation Failed", $"Error: {ex.Message}", "OK"); + } + finally + { + isInstalling = false; + } } } -} +} \ No newline at end of file diff --git a/Editor/PSXSplashInstaller.cs b/Editor/PSXSplashInstaller.cs new file mode 100644 index 0000000..d21d335 --- /dev/null +++ b/Editor/PSXSplashInstaller.cs @@ -0,0 +1,203 @@ +using UnityEngine; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Collections.Generic; +using System.Threading.Tasks; +using System; + +namespace SplashEdit.EditorCode +{ + + public static class PSXSplashInstaller + { + public static readonly string RepoUrl = "https://github.com/psxsplash/psxsplash.git"; + public static readonly string InstallPath = "Assets/psxsplash"; + public static readonly string FullInstallPath; + + static PSXSplashInstaller() + { + FullInstallPath = Path.Combine(Application.dataPath, "psxsplash"); + } + + public static bool IsInstalled() + { + return Directory.Exists(FullInstallPath) && + Directory.EnumerateFileSystemEntries(FullInstallPath).Any(); + } + + public static async Task Install() + { + if (IsInstalled()) return true; + + try + { + // Create the parent directory if it doesn't exist + Directory.CreateDirectory(Application.dataPath); + + // Clone the repository + var result = await RunGitCommandAsync($"clone --recursive {RepoUrl} \"{FullInstallPath}\"", Application.dataPath); + return !result.Contains("error"); + } + catch (Exception e) + { + UnityEngine.Debug.LogError($"Failed to install PSXSplash: {e.Message}"); + return false; + } + } + + public static async Task> GetBranchesWithLatestCommitsAsync() + { + if (!IsInstalled()) return new Dictionary(); + + try + { + // Fetch all branches and tags + await RunGitCommandAsync("fetch --all", FullInstallPath); + + // Get all remote branches + var branchesOutput = await RunGitCommandAsync("branch -r", FullInstallPath); + var branches = branchesOutput.Split('\n') + .Where(b => !string.IsNullOrEmpty(b.Trim())) + .Select(b => b.Trim().Replace("origin/", "")) + .Where(b => !b.Contains("HEAD")) + .ToList(); + + var branchesWithCommits = new Dictionary(); + + // Get the latest commit for each branch + foreach (var branch in branches) + { + var commitOutput = await RunGitCommandAsync($"log origin/{branch} -1 --pretty=format:%h", FullInstallPath); + if (!string.IsNullOrEmpty(commitOutput)) + { + branchesWithCommits[branch] = commitOutput.Trim(); + } + } + + return branchesWithCommits; + } + catch (Exception e) + { + UnityEngine.Debug.LogError($"Failed to get branches: {e.Message}"); + return new Dictionary(); + } + } + + public static async Task> GetReleasesAsync() + { + if (!IsInstalled()) return new List(); + + try + { + await RunGitCommandAsync("fetch --tags", FullInstallPath); + var output = await RunGitCommandAsync("tag -l", FullInstallPath); + + return output.Split('\n') + .Where(t => !string.IsNullOrEmpty(t.Trim())) + .Select(t => t.Trim()) + .ToList(); + } + catch (Exception e) + { + UnityEngine.Debug.LogError($"Failed to get releases: {e.Message}"); + return new List(); + } + } + + public static async Task CheckoutVersionAsync(string version) + { + if (!IsInstalled()) return false; + + try + { + // If it's a branch name, checkout the branch + // If it's a commit hash, checkout the commit + var result = await RunGitCommandAsync($"checkout {version}", FullInstallPath); + var result2 = await RunGitCommandAsync("submodule update --init --recursive", FullInstallPath); + + return !result.Contains("error") && !result2.Contains("error"); + } + catch (Exception e) + { + UnityEngine.Debug.LogError($"Failed to checkout version: {e.Message}"); + return false; + } + } + + public static async Task FetchLatestAsync() + { + if (!IsInstalled()) return false; + + try + { + var result = await RunGitCommandAsync("fetch --all", FullInstallPath); + return !result.Contains("error"); + } + catch (Exception e) + { + UnityEngine.Debug.LogError($"Failed to fetch latest: {e.Message}"); + return false; + } + } + + private static async Task RunGitCommandAsync(string arguments, string workingDirectory) + { + var processInfo = new ProcessStartInfo + { + FileName = "git", + Arguments = arguments, + WorkingDirectory = workingDirectory, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using (var process = new Process()) + { + process.StartInfo = processInfo; + var outputBuilder = new System.Text.StringBuilder(); + var errorBuilder = new System.Text.StringBuilder(); + + process.OutputDataReceived += (sender, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + outputBuilder.AppendLine(e.Data); + }; + + process.ErrorDataReceived += (sender, e) => + { + if (!string.IsNullOrEmpty(e.Data)) + errorBuilder.AppendLine(e.Data); + }; + + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + // Wait for exit with timeout + var timeout = TimeSpan.FromSeconds(30); + if (await Task.Run(() => process.WaitForExit((int)timeout.TotalMilliseconds))) + { + process.WaitForExit(); // Ensure all output is processed + + string output = outputBuilder.ToString(); + string error = errorBuilder.ToString(); + + if (!string.IsNullOrEmpty(error)) + { + UnityEngine.Debug.LogError($"Git error: {error}"); + } + + return output; + } + else + { + process.Kill(); + throw new TimeoutException("Git command timed out"); + } + } + } + } +} \ No newline at end of file diff --git a/Editor/PSXSplashInstaller.cs.meta b/Editor/PSXSplashInstaller.cs.meta new file mode 100644 index 0000000..b514dbd --- /dev/null +++ b/Editor/PSXSplashInstaller.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 72d1da27a16f0794cb1ad49c00799e74 \ No newline at end of file diff --git a/Editor/SerialConnection.cs b/Editor/SerialConnection.cs new file mode 100644 index 0000000..86d4d61 --- /dev/null +++ b/Editor/SerialConnection.cs @@ -0,0 +1,39 @@ +using System.IO.Ports; + + +namespace SplashEdit.EditorCode +{ + public class SerialConnection + { + private static SerialPort serialPort; + + public SerialConnection(string portName, int baudRate) + { + serialPort = new SerialPort(portName, baudRate); + serialPort.ReadTimeout = 50; + serialPort.WriteTimeout = 50; + } + + public void Open() + { serialPort.Open(); } + + public void Close() + { serialPort.Close(); } + + public int ReadByte() + { return serialPort.ReadByte(); } + + public int ReadChar() + { return serialPort.ReadChar(); } + + public void Write(string text) + { serialPort.Write(text); } + + public void Write(char[] buffer, int offset, int count) + { serialPort.Write(buffer, offset, count); } + + public void Write(byte[] buffer, int offset, int count) + { serialPort.Write(buffer, offset, count); } + + } +} \ No newline at end of file diff --git a/Editor/SerialConnection.cs.meta b/Editor/SerialConnection.cs.meta new file mode 100644 index 0000000..43d5dbe --- /dev/null +++ b/Editor/SerialConnection.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 714bd2374b7a9a14686078e5eb431795 \ No newline at end of file diff --git a/Editor/UniromConnection.cs b/Editor/UniromConnection.cs new file mode 100644 index 0000000..bd753a5 --- /dev/null +++ b/Editor/UniromConnection.cs @@ -0,0 +1,22 @@ +namespace SplashEdit.EditorCode +{ + public class UniromConnection + { + + private SerialConnection serialConnection; + + public UniromConnection(int baudRate, string portName) + { + serialConnection = new SerialConnection(portName, baudRate); + } + + public void Reset() + { + serialConnection.Open(); + serialConnection.Write("REST"); + serialConnection.Close(); + } + + + } +} \ No newline at end of file diff --git a/Editor/UniromConnection.cs.meta b/Editor/UniromConnection.cs.meta new file mode 100644 index 0000000..0520c58 --- /dev/null +++ b/Editor/UniromConnection.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d8fbc734f42ab9d42a843b6718127da7 \ No newline at end of file diff --git a/Editor/UniromConnectionWindow.cs b/Editor/UniromConnectionWindow.cs new file mode 100644 index 0000000..3ec1041 --- /dev/null +++ b/Editor/UniromConnectionWindow.cs @@ -0,0 +1,145 @@ +using UnityEngine; +using UnityEditor; +using System.IO.Ports; +using System.Collections; +using SplashEdit.RuntimeCode; +using System.Runtime.InteropServices; + +namespace SplashEdit.EditorCode +{ + public class PSXConnectionConfigWindow : EditorWindow + { + + public PSXConnectionType connectionType = PSXConnectionType.REAL_HARDWARE; + + // REAL HARDWARE (Unirom) SETTINGS + private string[] portNames; + private int selectedPortIndex = 1; + private int[] baudRates = { 9600, 115200 }; + private int selectedBaudIndex = 0; + + + + + private string statusMessage = ""; + private MessageType statusType; + private Vector2 scrollPosition; + + [MenuItem("PSX/Console or Emulator Connection")] + public static void ShowWindow() + { + GetWindow("Serial Config"); + } + + private void OnEnable() + { + RefreshPorts(); + LoadSettings(); + } + + private void RefreshPorts() + { + portNames = SerialPort.GetPortNames(); + if (portNames.Length == 0) + { + portNames = new[] { "No ports available" }; + } + } + + private void OnGUI() + { + using (var scrollView = new EditorGUILayout.ScrollViewScope(scrollPosition)) + { + scrollPosition = scrollView.scrollPosition; + + EditorGUILayout.LabelField("Pick connection type", EditorStyles.boldLabel); + connectionType = (PSXConnectionType)EditorGUILayout.EnumPopup("Connection Type", connectionType); + + if (connectionType == PSXConnectionType.REAL_HARDWARE) + { + // Port selection + EditorGUILayout.LabelField("Select COM Port", EditorStyles.boldLabel); + selectedPortIndex = EditorGUILayout.Popup("Available Ports", selectedPortIndex, portNames); + + // Baud rate selection + EditorGUILayout.Space(); + EditorGUILayout.LabelField("Select Baud Rate", EditorStyles.boldLabel); + selectedBaudIndex = EditorGUILayout.Popup("Baud Rate", selectedBaudIndex, new[] { "9600", "115200" }); + + // Buttons + EditorGUILayout.Space(); + using (new EditorGUILayout.HorizontalScope()) + { + if (GUILayout.Button("Refresh Ports")) + { + RefreshPorts(); + } + + if (GUILayout.Button("Test Connection")) + { + TestConnection(); + } + } + } + + if (GUILayout.Button("Save settings")) + { + SaveSettings(); + + } + + // Status message + EditorGUILayout.Space(); + if (!string.IsNullOrEmpty(statusMessage)) + { + EditorGUILayout.HelpBox(statusMessage, statusType); + } + + } + } + + private void LoadSettings() + { + PSXData _psxData = DataStorage.LoadData(); + if (_psxData != null) + { + connectionType = _psxData.ConnectionType; + selectedBaudIndex = System.Array.IndexOf(baudRates, _psxData.BaudRate); + if (selectedBaudIndex == -1) selectedBaudIndex = 0; + + RefreshPorts(); + selectedPortIndex = System.Array.IndexOf(portNames, _psxData.PortName); + if (selectedPortIndex == -1) selectedPortIndex = 0; + } + } + + private void TestConnection() + { + if (portNames.Length == 0 || portNames[0] == "No ports available") + { + statusMessage = "No serial ports available"; + statusType = MessageType.Error; + return; + } + + UniromConnection connection = new UniromConnection(baudRates[selectedBaudIndex], portNames[selectedPortIndex]); + connection.Reset(); + + statusMessage = "Connection tested. If your PlayStation reset, it worked!"; + statusType = MessageType.Info; + Repaint(); + } + + private void SaveSettings() + { + PSXData _psxData = DataStorage.LoadData(); + _psxData.ConnectionType = connectionType; + _psxData.BaudRate = baudRates[selectedBaudIndex]; + _psxData.PortName = portNames[selectedPortIndex]; + DataStorage.StoreData(_psxData); + statusMessage = "Settings saved"; + statusType = MessageType.Info; + Repaint(); + } + } +} \ No newline at end of file diff --git a/Editor/UniromConnectionWindow.cs.meta b/Editor/UniromConnectionWindow.cs.meta new file mode 100644 index 0000000..9c02991 --- /dev/null +++ b/Editor/UniromConnectionWindow.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ade0bf0fd69f449458c5b43e0f48ddff \ No newline at end of file diff --git a/Runtime/BSP.cs b/Runtime/BSP.cs new file mode 100644 index 0000000..3ba84ed --- /dev/null +++ b/Runtime/BSP.cs @@ -0,0 +1,845 @@ +using System.Collections.Generic; +using UnityEngine; +using System.Diagnostics; +using SplashEdit.RuntimeCode; + +public class BSP +{ + private List _objects; + private Node root; + private const float EPSILON = 1e-6f; + private const int MAX_TRIANGLES_PER_LEAF = 256; + private const int MAX_TREE_DEPTH = 50; + private const int CANDIDATE_PLANE_COUNT = 15; + + // Statistics + private int totalTrianglesProcessed; + private int totalSplits; + private int treeDepth; + private Stopwatch buildTimer; + + public bool verboseLogging = false; + + // Store the triangle that was used for the split plane for debugging + private Dictionary splitPlaneTriangles = new Dictionary(); + + private struct Triangle + { + public Vector3 v0; + public Vector3 v1; + public Vector3 v2; + public Vector3 n0; + public Vector3 n1; + public Vector3 n2; + public Vector2 uv0; + public Vector2 uv1; + public Vector2 uv2; + public Plane plane; + public Bounds bounds; + public PSXObjectExporter sourceExporter; + public int materialIndex; // Store material index instead of submesh index + + public Triangle(Vector3 a, Vector3 b, Vector3 c, Vector3 na, Vector3 nb, Vector3 nc, + Vector2 uva, Vector2 uvb, Vector2 uvc, PSXObjectExporter exporter, int matIndex) + { + v0 = a; + v1 = b; + v2 = c; + n0 = na; + n1 = nb; + n2 = nc; + uv0 = uva; + uv1 = uvb; + uv2 = uvc; + sourceExporter = exporter; + materialIndex = matIndex; + + // Calculate plane + Vector3 edge1 = v1 - v0; + Vector3 edge2 = v2 - v0; + Vector3 normal = Vector3.Cross(edge1, edge2); + + if (normal.sqrMagnitude < 1e-4f) + { + plane = new Plane(Vector3.up, 0); + } + else + { + normal.Normalize(); + plane = new Plane(normal, v0); + } + + // Calculate bounds + bounds = new Bounds(v0, Vector3.zero); + bounds.Encapsulate(v1); + bounds.Encapsulate(v2); + } + + public void Transform(Matrix4x4 matrix) + { + v0 = matrix.MultiplyPoint3x4(v0); + v1 = matrix.MultiplyPoint3x4(v1); + v2 = matrix.MultiplyPoint3x4(v2); + + // Transform normals (using inverse transpose for correct scaling) + Matrix4x4 invTranspose = matrix.inverse.transpose; + n0 = invTranspose.MultiplyVector(n0).normalized; + n1 = invTranspose.MultiplyVector(n1).normalized; + n2 = invTranspose.MultiplyVector(n2).normalized; + + // Recalculate plane and bounds after transformation + Vector3 edge1 = v1 - v0; + Vector3 edge2 = v2 - v0; + Vector3 normal = Vector3.Cross(edge1, edge2); + + if (normal.sqrMagnitude < 1e-4f) + { + plane = new Plane(Vector3.up, 0); + } + else + { + normal.Normalize(); + plane = new Plane(normal, v0); + } + + bounds = new Bounds(v0, Vector3.zero); + bounds.Encapsulate(v1); + bounds.Encapsulate(v2); + } + } + + private class Node + { + public Plane plane; + public Node front; + public Node back; + public List triangles; + public bool isLeaf = false; + public Bounds bounds; + public int depth; + public int triangleSourceIndex = -1; + } + + public BSP(List objects) + { + _objects = objects; + buildTimer = new Stopwatch(); + } + + public void Build() + { + buildTimer.Start(); + + List triangles = ExtractTrianglesFromMeshes(); + totalTrianglesProcessed = triangles.Count; + + if (verboseLogging) + UnityEngine.Debug.Log($"Starting BSP build with {totalTrianglesProcessed} triangles"); + + if (triangles.Count == 0) + { + root = null; + return; + } + + // Calculate overall bounds + Bounds overallBounds = CalculateBounds(triangles); + + // Build tree recursively with depth tracking + root = BuildNode(triangles, overallBounds, 0); + + // Create modified meshes for all exporters + CreateModifiedMeshes(); + + buildTimer.Stop(); + + if (verboseLogging) + { + UnityEngine.Debug.Log($"BSP build completed in {buildTimer.Elapsed.TotalMilliseconds}ms"); + UnityEngine.Debug.Log($"Total splits: {totalSplits}, Max depth: {treeDepth}"); + } + } + + private List ExtractTrianglesFromMeshes() + { + List triangles = new List(); + + foreach (var meshObj in _objects) + { + if (!meshObj.IsActive) continue; + + MeshFilter mf = meshObj.GetComponent(); + Renderer renderer = meshObj.GetComponent(); + if (mf == null || mf.sharedMesh == null || renderer == null) continue; + + Mesh mesh = mf.sharedMesh; + Vector3[] vertices = mesh.vertices; + Vector3[] normals = mesh.normals.Length > 0 ? mesh.normals : new Vector3[vertices.Length]; + Vector2[] uvs = mesh.uv.Length > 0 ? mesh.uv : new Vector2[vertices.Length]; + Matrix4x4 matrix = meshObj.transform.localToWorldMatrix; + + // Handle case where normals are missing + if (mesh.normals.Length == 0) + { + for (int i = 0; i < normals.Length; i++) + { + normals[i] = Vector3.up; + } + } + + // Handle case where UVs are missing + if (mesh.uv.Length == 0) + { + for (int i = 0; i < uvs.Length; i++) + { + uvs[i] = Vector2.zero; + } + } + + // Process each submesh and track material index + for (int submesh = 0; submesh < mesh.subMeshCount; submesh++) + { + int materialIndex = Mathf.Min(submesh, renderer.sharedMaterials.Length - 1); + int[] indices = mesh.GetTriangles(submesh); + + for (int i = 0; i < indices.Length; i += 3) + { + int idx0 = indices[i]; + int idx1 = indices[i + 1]; + int idx2 = indices[i + 2]; + + Vector3 v0 = vertices[idx0]; + Vector3 v1 = vertices[idx1]; + Vector3 v2 = vertices[idx2]; + + // Skip degenerate triangles + if (Vector3.Cross(v1 - v0, v2 - v0).sqrMagnitude < 1e-4f) + continue; + + Vector3 n0 = normals[idx0]; + Vector3 n1 = normals[idx1]; + Vector3 n2 = normals[idx2]; + + Vector2 uv0 = uvs[idx0]; + Vector2 uv1 = uvs[idx1]; + Vector2 uv2 = uvs[idx2]; + + Triangle tri = new Triangle(v0, v1, v2, n0, n1, n2, uv0, uv1, uv2, meshObj, materialIndex); + tri.Transform(matrix); + triangles.Add(tri); + } + } + } + + return triangles; + } + + private Node BuildNode(List triangles, Bounds bounds, int depth) + { + if (triangles == null || triangles.Count == 0) + return null; + + Node node = new Node + { + triangles = new List(), + bounds = bounds, + depth = depth + }; + + treeDepth = Mathf.Max(treeDepth, depth); + + // Create leaf node if conditions are met + if (triangles.Count <= MAX_TRIANGLES_PER_LEAF || depth >= MAX_TREE_DEPTH) + { + node.isLeaf = true; + node.triangles = triangles; + + if (verboseLogging && depth >= MAX_TREE_DEPTH) + UnityEngine.Debug.LogWarning($"Max tree depth reached at depth {depth} with {triangles.Count} triangles"); + + return node; + } + + // Select the best splitting plane using multiple strategies + Triangle? splitTriangle = null; + if (!SelectBestSplittingPlane(triangles, bounds, out node.plane, out splitTriangle)) + { + // Fallback: create leaf if no good split found + node.isLeaf = true; + node.triangles = triangles; + + if (verboseLogging) + UnityEngine.Debug.Log($"Created leaf node with {triangles.Count} triangles (no good split found)"); + + return node; + } + + // Store the triangle that provided the split plane for debugging + if (splitTriangle.HasValue) + { + splitPlaneTriangles[node] = splitTriangle.Value; + } + + List frontList = new List(); + List backList = new List(); + List coplanarList = new List(); + + // Classify all triangles + foreach (var tri in triangles) + { + ClassifyTriangle(tri, node.plane, coplanarList, frontList, backList); + } + + // Handle cases where splitting doesn't provide benefit + if (frontList.Count == 0 || backList.Count == 0) + { + // If split doesn't separate geometry, create a leaf + node.isLeaf = true; + node.triangles = triangles; + + if (verboseLogging) + UnityEngine.Debug.Log($"Created leaf node with {triangles.Count} triangles (ineffective split)"); + + return node; + } + + // Distribute coplanar triangles to the side with fewer triangles + if (coplanarList.Count > 0) + { + if (frontList.Count <= backList.Count) + { + frontList.AddRange(coplanarList); + } + else + { + backList.AddRange(coplanarList); + } + } + + if (verboseLogging) + UnityEngine.Debug.Log($"Node at depth {depth}: {triangles.Count} triangles -> {frontList.Count} front, {backList.Count} back"); + + // Calculate bounds for children + Bounds frontBounds = CalculateBounds(frontList); + Bounds backBounds = CalculateBounds(backList); + + // Recursively build child nodes + node.front = BuildNode(frontList, frontBounds, depth + 1); + node.back = BuildNode(backList, backBounds, depth + 1); + + return node; + } + + private bool SelectBestSplittingPlane(List triangles, Bounds bounds, out Plane bestPlane, out Triangle? splitTriangle) + { + bestPlane = new Plane(); + splitTriangle = null; + int bestScore = int.MaxValue; + bool foundValidPlane = false; + + // Strategy 1: Try planes from triangle centroids + int candidatesToTry = Mathf.Min(CANDIDATE_PLANE_COUNT, triangles.Count); + for (int i = 0; i < candidatesToTry; i++) + { + Triangle tri = triangles[i]; + Plane candidate = tri.plane; + + int score = EvaluateSplitPlane(triangles, candidate); + if (score < bestScore && score >= 0) + { + bestScore = score; + bestPlane = candidate; + splitTriangle = tri; + foundValidPlane = true; + } + } + + // Strategy 2: Try axis-aligned planes through bounds center + if (!foundValidPlane || bestScore > triangles.Count * 3) + { + Vector3[] axes = { Vector3.right, Vector3.up, Vector3.forward }; + for (int i = 0; i < 3; i++) + { + Plane candidate = new Plane(axes[i], bounds.center); + int score = EvaluateSplitPlane(triangles, candidate); + if (score < bestScore && score >= 0) + { + bestScore = score; + bestPlane = candidate; + splitTriangle = null; + foundValidPlane = true; + } + } + } + + // Strategy 3: Try planes based on bounds extents + if (!foundValidPlane) + { + Vector3 extents = bounds.extents; + if (extents.x >= extents.y && extents.x >= extents.z) + bestPlane = new Plane(Vector3.right, bounds.center); + else if (extents.y >= extents.x && extents.y >= extents.z) + bestPlane = new Plane(Vector3.up, bounds.center); + else + bestPlane = new Plane(Vector3.forward, bounds.center); + + splitTriangle = null; + foundValidPlane = true; + } + + return foundValidPlane; + } + + private int EvaluateSplitPlane(List triangles, Plane plane) + { + int frontCount = 0; + int backCount = 0; + int splitCount = 0; + int coplanarCount = 0; + + foreach (var tri in triangles) + { + float d0 = plane.GetDistanceToPoint(tri.v0); + float d1 = plane.GetDistanceToPoint(tri.v1); + float d2 = plane.GetDistanceToPoint(tri.v2); + + // Check for NaN/infinity + if (float.IsNaN(d0) || float.IsNaN(d1) || float.IsNaN(d2) || + float.IsInfinity(d0) || float.IsInfinity(d1) || float.IsInfinity(d2)) + { + return int.MaxValue; + } + + bool front = d0 > EPSILON || d1 > EPSILON || d2 > EPSILON; + bool back = d0 < -EPSILON || d1 < -EPSILON || d2 < -EPSILON; + + if (front && back) + splitCount++; + else if (front) + frontCount++; + else if (back) + backCount++; + else + coplanarCount++; + } + + // Reject planes that would cause too many splits or imbalanced trees + if (splitCount > triangles.Count / 2) + return int.MaxValue; + + // Score based on balance and split count + return Mathf.Abs(frontCount - backCount) + splitCount * 2; + } + + private void ClassifyTriangle(Triangle tri, Plane plane, List coplanar, List front, List back) + { + float d0 = plane.GetDistanceToPoint(tri.v0); + float d1 = plane.GetDistanceToPoint(tri.v1); + float d2 = plane.GetDistanceToPoint(tri.v2); + + // Check for numerical issues + if (float.IsNaN(d0) || float.IsNaN(d1) || float.IsNaN(d2) || + float.IsInfinity(d0) || float.IsInfinity(d1) || float.IsInfinity(d2)) + { + coplanar.Add(tri); + return; + } + + bool front0 = d0 > EPSILON; + bool front1 = d1 > EPSILON; + bool front2 = d2 > EPSILON; + + bool back0 = d0 < -EPSILON; + bool back1 = d1 < -EPSILON; + bool back2 = d2 < -EPSILON; + + int fCount = (front0 ? 1 : 0) + (front1 ? 1 : 0) + (front2 ? 1 : 0); + int bCount = (back0 ? 1 : 0) + (back1 ? 1 : 0) + (back2 ? 1 : 0); + + if (fCount == 3) + { + front.Add(tri); + } + else if (bCount == 3) + { + back.Add(tri); + } + else if (fCount == 0 && bCount == 0) + { + coplanar.Add(tri); + } + else + { + totalSplits++; + SplitTriangle(tri, plane, front, back); + } + } + + private void SplitTriangle(Triangle tri, Plane plane, List front, List back) + { + // Get distances + float d0 = plane.GetDistanceToPoint(tri.v0); + float d1 = plane.GetDistanceToPoint(tri.v1); + float d2 = plane.GetDistanceToPoint(tri.v2); + + // Classify points + bool[] frontSide = { d0 > EPSILON, d1 > EPSILON, d2 > EPSILON }; + bool[] backSide = { d0 < -EPSILON, d1 < -EPSILON, d2 < -EPSILON }; + + // Count how many points are on each side + int frontCount = (frontSide[0] ? 1 : 0) + (frontSide[1] ? 1 : 0) + (frontSide[2] ? 1 : 0); + int backCount = (backSide[0] ? 1 : 0) + (backSide[1] ? 1 : 0) + (backSide[2] ? 1 : 0); + + // 2 points on one side, 1 on the other + if (frontCount == 2 && backCount == 1) + { + int loneIndex = backSide[0] ? 0 : (backSide[1] ? 1 : 2); + SplitTriangle2To1(tri, plane, loneIndex, true, front, back); + } + else if (backCount == 2 && frontCount == 1) + { + int loneIndex = frontSide[0] ? 0 : (frontSide[1] ? 1 : 2); + SplitTriangle2To1(tri, plane, loneIndex, false, front, back); + } + else + { + // Complex case - add to both sides (should be rare) + front.Add(tri); + back.Add(tri); + } + } + + private void SplitTriangle2To1(Triangle tri, Plane plane, int loneIndex, bool loneIsBack, + List front, List back) + { + Vector3[] v = { tri.v0, tri.v1, tri.v2 }; + Vector3[] n = { tri.n0, tri.n1, tri.n2 }; + Vector2[] uv = { tri.uv0, tri.uv1, tri.uv2 }; + + Vector3 loneVertex = v[loneIndex]; + Vector3 loneNormal = n[loneIndex]; + Vector2 loneUV = uv[loneIndex]; + + Vector3 v1 = v[(loneIndex + 1) % 3]; + Vector3 v2 = v[(loneIndex + 2) % 3]; + Vector3 n1 = n[(loneIndex + 1) % 3]; + Vector3 n2 = n[(loneIndex + 2) % 3]; + Vector2 uv1 = uv[(loneIndex + 1) % 3]; + Vector2 uv2 = uv[(loneIndex + 2) % 3]; + + Vector3 i1 = PlaneIntersection(plane, loneVertex, v1); + float t1 = CalculateInterpolationFactor(plane, loneVertex, v1); + Vector3 n_i1 = Vector3.Lerp(loneNormal, n1, t1).normalized; + Vector2 uv_i1 = Vector2.Lerp(loneUV, uv1, t1); + + Vector3 i2 = PlaneIntersection(plane, loneVertex, v2); + float t2 = CalculateInterpolationFactor(plane, loneVertex, v2); + Vector3 n_i2 = Vector3.Lerp(loneNormal, n2, t2).normalized; + Vector2 uv_i2 = Vector2.Lerp(loneUV, uv2, t2); + + // Desired normal: prefer triangle's plane normal, fallback to geometric normal + Vector3 desired = tri.plane.normal; + if (desired.sqrMagnitude < 1e-4f) + desired = Vector3.Cross(tri.v1 - tri.v0, tri.v2 - tri.v0).normalized; + if (desired.sqrMagnitude < 1e-4f) + desired = Vector3.up; + + // Helper: decide and swap b/c if necessary, then add triangle + void AddTriClockwise(List list, + Vector3 a, Vector3 b, Vector3 c, + Vector3 na, Vector3 nb, Vector3 nc, + Vector2 ua, Vector2 ub, Vector2 uc) + { + Vector3 cross = Vector3.Cross(b - a, c - a); + if (cross.z > 0f) // <-- assumes you're working in PS1 screen space (z forward) + { + // swap b <-> c + var tmpV = b; b = c; c = tmpV; + var tmpN = nb; nb = nc; nc = tmpN; + var tmpUv = ub; ub = uc; uc = tmpUv; + } + + list.Add(new Triangle(a, b, c, na, nb, nc, ua, ub, uc, tri.sourceExporter, tri.materialIndex)); + } + + if (loneIsBack) + { + // back: (lone, i1, i2) + AddTriClockwise(back, loneVertex, i1, i2, loneNormal, n_i1, n_i2, loneUV, uv_i1, uv_i2); + + // front: (v1, i1, i2) and (v1, i2, v2) + AddTriClockwise(front, v1, i1, i2, n1, n_i1, n_i2, uv1, uv_i1, uv_i2); + AddTriClockwise(front, v1, i2, v2, n1, n_i2, n2, uv1, uv_i2, uv2); + } + else + { + // front: (lone, i1, i2) + AddTriClockwise(front, loneVertex, i1, i2, loneNormal, n_i1, n_i2, loneUV, uv_i1, uv_i2); + + // back: (v1, i1, i2) and (v1, i2, v2) + AddTriClockwise(back, v1, i1, i2, n1, n_i1, n_i2, uv1, uv_i1, uv_i2); + AddTriClockwise(back, v1, i2, v2, n1, n_i2, n2, uv1, uv_i2, uv2); + } + } + + + private Vector3 PlaneIntersection(Plane plane, Vector3 a, Vector3 b) + { + Vector3 ba = b - a; + float denominator = Vector3.Dot(plane.normal, ba); + + // Check for parallel line (shouldn't happen in our case) + if (Mathf.Abs(denominator) < 1e-4f) + return a; + + float t = (-plane.distance - Vector3.Dot(plane.normal, a)) / denominator; + return a + ba * Mathf.Clamp01(t); + } + + private float CalculateInterpolationFactor(Plane plane, Vector3 a, Vector3 b) + { + Vector3 ba = b - a; + float denominator = Vector3.Dot(plane.normal, ba); + + if (Mathf.Abs(denominator) < 1e-4f) + return 0.5f; + + float t = (-plane.distance - Vector3.Dot(plane.normal, a)) / denominator; + return Mathf.Clamp01(t); + } + + private Bounds CalculateBounds(List triangles) + { + if (triangles == null || triangles.Count == 0) + return new Bounds(); + + Bounds bounds = triangles[0].bounds; + for (int i = 1; i < triangles.Count; i++) + { + bounds.Encapsulate(triangles[i].bounds); + } + + return bounds; + } + + // Add a method to create modified meshes after BSP construction + // Add a method to create modified meshes after BSP construction + private void CreateModifiedMeshes() + { + if (root == null) return; + + // Collect all triangles from the BSP tree + List allTriangles = new List(); + CollectTrianglesFromNode(root, allTriangles); + + // Group triangles by their source exporter and material index + Dictionary>> exporterTriangles = + new Dictionary>>(); + + foreach (var tri in allTriangles) + { + if (!exporterTriangles.ContainsKey(tri.sourceExporter)) + { + exporterTriangles[tri.sourceExporter] = new Dictionary>(); + } + + var materialDict = exporterTriangles[tri.sourceExporter]; + if (!materialDict.ContainsKey(tri.materialIndex)) + { + materialDict[tri.materialIndex] = new List(); + } + + materialDict[tri.materialIndex].Add(tri); + } + + // Create modified meshes for each exporter + foreach (var kvp in exporterTriangles) + { + PSXObjectExporter exporter = kvp.Key; + Dictionary> materialTriangles = kvp.Value; + + Mesh originalMesh = exporter.GetComponent().sharedMesh; + Renderer renderer = exporter.GetComponent(); + + Mesh modifiedMesh = new Mesh(); + modifiedMesh.name = originalMesh.name + "_BSP"; + + List vertices = new List(); + List normals = new List(); + List uvs = new List(); + List tangents = new List(); + List colors = new List(); + + // Create a list for each material's triangles + List> materialIndices = new List>(); + for (int i = 0; i < renderer.sharedMaterials.Length; i++) + { + materialIndices.Add(new List()); + } + + // Get the inverse transform to convert from world space back to object space + Matrix4x4 worldToLocal = exporter.transform.worldToLocalMatrix; + + // Process each material + foreach (var materialKvp in materialTriangles) + { + int materialIndex = materialKvp.Key; + List triangles = materialKvp.Value; + + // Add vertices, normals, and uvs for this material + for (int i = 0; i < triangles.Count; i++) + { + Triangle tri = triangles[i]; + + // Transform vertices from world space back to object space + Vector3 v0 = worldToLocal.MultiplyPoint3x4(tri.v0); + Vector3 v1 = worldToLocal.MultiplyPoint3x4(tri.v1); + Vector3 v2 = worldToLocal.MultiplyPoint3x4(tri.v2); + + int vertexIndex = vertices.Count; + vertices.Add(v0); + vertices.Add(v1); + vertices.Add(v2); + + // Transform normals from world space back to object space + Vector3 n0 = worldToLocal.MultiplyVector(tri.n0).normalized; + Vector3 n1 = worldToLocal.MultiplyVector(tri.n1).normalized; + Vector3 n2 = worldToLocal.MultiplyVector(tri.n2).normalized; + + normals.Add(n0); + normals.Add(n1); + normals.Add(n2); + + uvs.Add(tri.uv0); + uvs.Add(tri.uv1); + uvs.Add(tri.uv2); + + // Add default tangents and colors (will be recalculated later) + tangents.Add(new Vector4(1, 0, 0, 1)); + tangents.Add(new Vector4(1, 0, 0, 1)); + tangents.Add(new Vector4(1, 0, 0, 1)); + + colors.Add(Color.white); + colors.Add(Color.white); + colors.Add(Color.white); + + // Add indices for this material + materialIndices[materialIndex].Add(vertexIndex); + materialIndices[materialIndex].Add(vertexIndex + 1); + materialIndices[materialIndex].Add(vertexIndex + 2); + } + } + + // Assign data to the mesh + modifiedMesh.vertices = vertices.ToArray(); + modifiedMesh.normals = normals.ToArray(); + modifiedMesh.uv = uvs.ToArray(); + modifiedMesh.tangents = tangents.ToArray(); + modifiedMesh.colors = colors.ToArray(); + + // Set up submeshes based on materials + modifiedMesh.subMeshCount = materialIndices.Count; + for (int i = 0; i < materialIndices.Count; i++) + { + modifiedMesh.SetTriangles(materialIndices[i].ToArray(), i); + } + + // Recalculate important mesh properties + modifiedMesh.RecalculateBounds(); + modifiedMesh.RecalculateTangents(); + + // Assign the modified mesh to the exporter + exporter.ModifiedMesh = modifiedMesh; + } + } + // Helper method to collect all triangles from the BSP tree + private void CollectTrianglesFromNode(Node node, List triangles) + { + if (node == null) return; + + if (node.isLeaf) + { + triangles.AddRange(node.triangles); + } + else + { + CollectTrianglesFromNode(node.front, triangles); + CollectTrianglesFromNode(node.back, triangles); + } + } + + public void DrawGizmos(int maxDepth) + { + if (root == null) return; + + DrawNodeGizmos(root, 0, maxDepth); + } + + private void DrawNodeGizmos(Node node, int depth, int maxDepth) + { + if (node == null) return; + if (depth > maxDepth) return; + + Color nodeColor = Color.HSVToRGB((depth * 0.1f) % 1f, 0.8f, 0.8f); + Gizmos.color = nodeColor; + + if (node.isLeaf) + { + foreach (var tri in node.triangles) + { + DrawTriangleGizmo(tri); + } + } + else + { + DrawPlaneGizmo(node.plane, node.bounds); + + // Draw the triangle that was used for the split plane if available + if (splitPlaneTriangles.ContainsKey(node)) + { + Gizmos.color = Color.magenta; + DrawTriangleGizmo(splitPlaneTriangles[node]); + Gizmos.color = nodeColor; + } + + DrawNodeGizmos(node.front, depth + 1, maxDepth); + DrawNodeGizmos(node.back, depth + 1, maxDepth); + } + } + + private void DrawTriangleGizmo(Triangle tri) + { + Gizmos.DrawLine(tri.v0, tri.v1); + Gizmos.DrawLine(tri.v1, tri.v2); + Gizmos.DrawLine(tri.v2, tri.v0); + } + + private void DrawPlaneGizmo(Plane plane, Bounds bounds) + { + Vector3 center = bounds.center; + Vector3 normal = plane.normal; + + Vector3 tangent = Vector3.Cross(normal, Vector3.up); + if (tangent.magnitude < 0.1f) tangent = Vector3.Cross(normal, Vector3.right); + tangent = tangent.normalized; + + Vector3 bitangent = Vector3.Cross(normal, tangent).normalized; + + float size = Mathf.Max(bounds.size.x, bounds.size.y, bounds.size.z) * 0.5f; + tangent *= size; + bitangent *= size; + + Vector3 p0 = center - tangent - bitangent; + Vector3 p1 = center + tangent - bitangent; + Vector3 p2 = center + tangent + bitangent; + Vector3 p3 = center - tangent + bitangent; + + Gizmos.DrawLine(p0, p1); + Gizmos.DrawLine(p1, p2); + Gizmos.DrawLine(p2, p3); + Gizmos.DrawLine(p3, p0); + + Gizmos.color = Color.red; + Gizmos.DrawLine(center, center + normal * size * 0.5f); + } +} \ No newline at end of file diff --git a/Runtime/BSP.cs.meta b/Runtime/BSP.cs.meta new file mode 100644 index 0000000..3a55181 --- /dev/null +++ b/Runtime/BSP.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 15144e67b42b92447a546346e594155b \ No newline at end of file diff --git a/Runtime/PSXData.cs b/Runtime/PSXData.cs index c8075a0..6828f35 100644 --- a/Runtime/PSXData.cs +++ b/Runtime/PSXData.cs @@ -3,14 +3,36 @@ using UnityEngine; namespace SplashEdit.RuntimeCode { + + public enum PSXConnectionType + { + REAL_HARDWARE, // Unirom + EMULATOR // PCSX-Redux + } + [CreateAssetMenu(fileName = "PSXData", menuName = "Scriptable Objects/PSXData")] public class PSXData : ScriptableObject { + + // Texture packing settings public Vector2 OutputResolution = new Vector2(320, 240); public bool DualBuffering = true; public bool VerticalBuffering = true; public List ProhibitedAreas = new List(); + + // Connection settings + public PSXConnectionType ConnectionType = PSXConnectionType.REAL_HARDWARE; + + // Real hardware settings + public string PortName = "COM3"; + public int BaudRate = 0; + + // Emulator settings public string PCSXReduxPath = ""; + + + + } } \ No newline at end of file diff --git a/Runtime/PSXMesh.cs b/Runtime/PSXMesh.cs index dc926d4..0af0989 100644 --- a/Runtime/PSXMesh.cs +++ b/Runtime/PSXMesh.cs @@ -28,7 +28,7 @@ namespace SplashEdit.RuntimeCode public PSXVertex v1; public PSXVertex v2; - public int TextureIndex; + public int TextureIndex; public readonly PSXVertex[] Vertexes => new PSXVertex[] { v0, v1, v2 }; } @@ -82,77 +82,176 @@ namespace SplashEdit.RuntimeCode /// Height of the texture (default is 256). /// Optional transform to convert vertices to world space. /// A new PSXMesh containing the converted triangles. - public static PSXMesh CreateFromUnityRenderer(Renderer renderer, float GTEScaling, Transform transform, List textures) -{ - PSXMesh psxMesh = new PSXMesh { Triangles = new List() }; - Material[] materials = renderer.sharedMaterials; - Mesh mesh = renderer.GetComponent().sharedMesh; - - for (int submeshIndex = 0; submeshIndex < materials.Length; submeshIndex++) - { - int[] submeshTriangles = mesh.GetTriangles(submeshIndex); - Material material = materials[submeshIndex]; - Texture2D texture = material.mainTexture as Texture2D; - - // Find texture index instead of the texture itself - int textureIndex = -1; - if (texture != null) + public static PSXMesh CreateFromUnityRenderer(Renderer renderer, float GTEScaling, Transform transform, List textures) { - for (int i = 0; i < textures.Count; i++) + PSXMesh psxMesh = new PSXMesh { Triangles = new List() }; + Material[] materials = renderer.sharedMaterials; + Mesh mesh = renderer.GetComponent().sharedMesh; + + for (int submeshIndex = 0; submeshIndex < materials.Length; submeshIndex++) { - if (textures[i].OriginalTexture == texture) + int[] submeshTriangles = mesh.GetTriangles(submeshIndex); + Material material = materials[submeshIndex]; + Texture2D texture = material.mainTexture as Texture2D; + + // Find texture index instead of the texture itself + int textureIndex = -1; + if (texture != null) { - textureIndex = i; - break; + for (int i = 0; i < textures.Count; i++) + { + if (textures[i].OriginalTexture == texture) + { + textureIndex = i; + break; + } + } + } + + if (textureIndex == -1) + { + continue; + } + + // Get mesh data arrays + mesh.RecalculateNormals(); + Vector3[] vertices = mesh.vertices; + Vector3[] normals = mesh.normals; + Vector3[] smoothNormals = RecalculateSmoothNormals(mesh); + Vector2[] uv = mesh.uv; + + PSXVertex convertData(int index) + { + Vector3 v = Vector3.Scale(vertices[index], transform.lossyScale); + Vector3 wv = transform.TransformPoint(vertices[index]); + Vector3 wn = transform.TransformDirection(smoothNormals[index]).normalized; + Color c = PSXLightingBaker.ComputeLighting(wv, wn); + return ConvertToPSXVertex(v, GTEScaling, normals[index], uv[index], textures[textureIndex]?.Width, textures[textureIndex]?.Height, c); + } + + for (int i = 0; i < submeshTriangles.Length; i += 3) + { + int vid0 = submeshTriangles[i]; + int vid1 = submeshTriangles[i + 1]; + int vid2 = submeshTriangles[i + 2]; + + Vector3 faceNormal = Vector3.Cross(vertices[vid1] - vertices[vid0], vertices[vid2] - vertices[vid0]).normalized; + + if (Vector3.Dot(faceNormal, normals[vid0]) < 0) + { + (vid1, vid2) = (vid2, vid1); + } + + psxMesh.Triangles.Add(new Tri + { + v0 = convertData(vid0), + v1 = convertData(vid1), + v2 = convertData(vid2), + TextureIndex = textureIndex + }); } } + + return psxMesh; } - if (textureIndex == -1) + public static PSXMesh CreateFromUnityMesh(Mesh mesh, Renderer renderer, float GTEScaling, Transform transform, List textures) { - continue; - } + PSXMesh psxMesh = new PSXMesh { Triangles = new List() }; + Material[] materials = renderer.sharedMaterials; - // Get mesh data arrays - mesh.RecalculateNormals(); - Vector3[] vertices = mesh.vertices; - Vector3[] normals = mesh.normals; - Vector3[] smoothNormals = RecalculateSmoothNormals(mesh); - Vector2[] uv = mesh.uv; - - PSXVertex convertData(int index) - { - Vector3 v = Vector3.Scale(vertices[index], transform.lossyScale); - Vector3 wv = transform.TransformPoint(vertices[index]); - Vector3 wn = transform.TransformDirection(smoothNormals[index]).normalized; - Color c = PSXLightingBaker.ComputeLighting(wv, wn); - return ConvertToPSXVertex(v, GTEScaling, normals[index], uv[index], textures[textureIndex]?.Width, textures[textureIndex]?.Height, c); - } - - for (int i = 0; i < submeshTriangles.Length; i += 3) - { - int vid0 = submeshTriangles[i]; - int vid1 = submeshTriangles[i + 1]; - int vid2 = submeshTriangles[i + 2]; - - Vector3 faceNormal = Vector3.Cross(vertices[vid1] - vertices[vid0], vertices[vid2] - vertices[vid0]).normalized; - - if (Vector3.Dot(faceNormal, normals[vid0]) < 0) + // Ensure mesh has required data + if (mesh.normals == null || mesh.normals.Length == 0) { - (vid1, vid2) = (vid2, vid1); + mesh.RecalculateNormals(); } - psxMesh.Triangles.Add(new Tri { - v0 = convertData(vid0), - v1 = convertData(vid1), - v2 = convertData(vid2), - TextureIndex = textureIndex - }); - } - } + if (mesh.uv == null || mesh.uv.Length == 0) + { + Vector2[] uvs = new Vector2[mesh.vertices.Length]; + mesh.uv = uvs; + } - return psxMesh; -} + // Precompute smooth normals for the entire mesh + Vector3[] smoothNormals = RecalculateSmoothNormals(mesh); + + // Precompute world positions and normals for all vertices + Vector3[] worldVertices = new Vector3[mesh.vertices.Length]; + Vector3[] worldNormals = new Vector3[mesh.normals.Length]; + + for (int i = 0; i < mesh.vertices.Length; i++) + { + worldVertices[i] = transform.TransformPoint(mesh.vertices[i]); + worldNormals[i] = transform.TransformDirection(mesh.normals[i]).normalized; + } + + for (int submeshIndex = 0; submeshIndex < mesh.subMeshCount; submeshIndex++) + { + int materialIndex = Mathf.Min(submeshIndex, materials.Length - 1); + Material material = materials[materialIndex]; + Texture2D texture = material.mainTexture as Texture2D; + + // Find texture index + int textureIndex = -1; + if (texture != null) + { + for (int i = 0; i < textures.Count; i++) + { + if (textures[i].OriginalTexture == texture) + { + textureIndex = i; + break; + } + } + } + + int[] submeshTriangles = mesh.GetTriangles(submeshIndex); + + // Get mesh data arrays + Vector3[] vertices = mesh.vertices; + Vector3[] normals = mesh.normals; + Vector2[] uv = mesh.uv; + + PSXVertex convertData(int index) + { + Vector3 v = Vector3.Scale(vertices[index], transform.lossyScale); + + // Use precomputed world position and normal for consistent lighting + Vector3 wv = worldVertices[index]; + Vector3 wn = worldNormals[index]; + + // For split triangles, use the original vertex's lighting if possible + Color c = PSXLightingBaker.ComputeLighting(wv, wn); + + return ConvertToPSXVertex(v, GTEScaling, normals[index], uv[index], + textures[textureIndex]?.Width, textures[textureIndex]?.Height, c); + } + + for (int i = 0; i < submeshTriangles.Length; i += 3) + { + int vid0 = submeshTriangles[i]; + int vid1 = submeshTriangles[i + 1]; + int vid2 = submeshTriangles[i + 2]; + + Vector3 faceNormal = Vector3.Cross(vertices[vid1] - vertices[vid0], vertices[vid2] - vertices[vid0]).normalized; + + if (Vector3.Dot(faceNormal, normals[vid0]) < 0) + { + (vid1, vid2) = (vid2, vid1); + } + + psxMesh.Triangles.Add(new Tri + { + v0 = convertData(vid0), + v1 = convertData(vid1), + v2 = convertData(vid2), + TextureIndex = textureIndex + }); + } + } + + return psxMesh; + } /// /// Converts a Unity vertex into a PSXVertex by applying fixed-point conversion, shading, and UV mapping. diff --git a/Runtime/PSXObjectExporter.cs b/Runtime/PSXObjectExporter.cs index fe080e2..ac291fa 100644 --- a/Runtime/PSXObjectExporter.cs +++ b/Runtime/PSXObjectExporter.cs @@ -12,19 +12,30 @@ namespace SplashEdit.RuntimeCode public bool IsActive = true; - public List Textures { get; set; } = new List(); // Stores the converted PlayStation-style texture - public PSXMesh Mesh { get; protected set; } // Stores the converted PlayStation-style mesh + public List Textures { get; set; } = new List(); + public PSXMesh Mesh { get; protected set; } + [Header("Export Settings")] [FormerlySerializedAs("BitDepth")] - [SerializeField] private PSXBPP bitDepth = PSXBPP.TEX_8BIT; // Defines the bit depth of the texture (e.g., 4BPP, 8BPP) + [SerializeField] private PSXBPP bitDepth = PSXBPP.TEX_8BIT; [SerializeField] private LuaFile luaFile; + + [Header("BSP Settings")] + [SerializeField] private Mesh _modifiedMesh; // Mesh after BSP processing + [Header("Gizmo Settings")] [FormerlySerializedAs("PreviewNormals")] [SerializeField] private bool previewNormals = false; - [SerializeField] private float normalPreviewLength = 0.5f; // Length of the normal lines + [SerializeField] private float normalPreviewLength = 0.5f; private readonly Dictionary<(int, PSXBPP), PSXTexture2D> cache = new(); + public Mesh ModifiedMesh + { + get => _modifiedMesh; + set => _modifiedMesh = value; + } + private void OnDrawGizmos() { if (previewNormals) @@ -37,25 +48,19 @@ namespace SplashEdit.RuntimeCode Vector3[] vertices = mesh.vertices; Vector3[] normals = mesh.normals; - Gizmos.color = Color.green; // Normal color + Gizmos.color = Color.green; for (int i = 0; i < vertices.Length; i++) { - Vector3 worldVertex = transform.TransformPoint(vertices[i]); // Convert to world space - Vector3 worldNormal = transform.TransformDirection(normals[i]); // Transform normal to world space + Vector3 worldVertex = transform.TransformPoint(vertices[i]); + Vector3 worldNormal = transform.TransformDirection(normals[i]); Gizmos.DrawLine(worldVertex, worldVertex + worldNormal * normalPreviewLength); } } - } } - - /// - /// Converts the object's material texture into a PlayStation-compatible texture. - /// - /// public void CreatePSXTextures2D() { Renderer renderer = GetComponent(); @@ -71,14 +76,12 @@ namespace SplashEdit.RuntimeCode Texture mainTexture = mat.mainTexture; Texture2D tex2D = null; - // Check if it's already a Texture2D if (mainTexture is Texture2D existingTex2D) { tex2D = existingTex2D; } else { - // If not a Texture2D, try to convert tex2D = ConvertToTexture2D(mainTexture); } @@ -92,7 +95,7 @@ namespace SplashEdit.RuntimeCode else { tex = PSXTexture2D.CreateFromTexture2D(tex2D, bitDepth); - tex.OriginalTexture = tex2D; // Store reference to the original texture + tex.OriginalTexture = tex2D; cache.Add((tex2D.GetInstanceID(), bitDepth), tex); } Textures.Add(tex); @@ -104,10 +107,8 @@ namespace SplashEdit.RuntimeCode private Texture2D ConvertToTexture2D(Texture texture) { - // Create a new Texture2D with the same dimensions and format Texture2D texture2D = new Texture2D(texture.width, texture.height, TextureFormat.RGBA32, false); - // Read the texture pixels RenderTexture currentActiveRT = RenderTexture.active; RenderTexture.active = texture as RenderTexture; @@ -128,16 +129,35 @@ namespace SplashEdit.RuntimeCode return null; } - /// - /// Converts the object's mesh into a PlayStation-compatible mesh. - /// - public void CreatePSXMesh(float GTEScaling) + public void CreatePSXMesh(float GTEScaling, bool useBSP = false) { Renderer renderer = GetComponent(); if (renderer != null) { - Mesh = PSXMesh.CreateFromUnityRenderer(renderer, GTEScaling, transform, Textures); + if (useBSP && _modifiedMesh != null) + { + // Create a temporary GameObject with the modified mesh but same materials + GameObject tempGO = new GameObject("TempBSPMesh"); + tempGO.transform.position = transform.position; + tempGO.transform.rotation = transform.rotation; + tempGO.transform.localScale = transform.localScale; + + MeshFilter tempMF = tempGO.AddComponent(); + tempMF.sharedMesh = _modifiedMesh; + + MeshRenderer tempMR = tempGO.AddComponent(); + tempMR.sharedMaterials = renderer.sharedMaterials; + + Mesh = PSXMesh.CreateFromUnityRenderer(tempMR, GTEScaling, transform, Textures); + + // Clean up + GameObject.DestroyImmediate(tempGO); + } + else + { + Mesh = PSXMesh.CreateFromUnityRenderer(renderer, GTEScaling, transform, Textures); + } } } } -} +} \ No newline at end of file diff --git a/Runtime/PSXSceneExporter.cs b/Runtime/PSXSceneExporter.cs index ddf0ed0..5bd8914 100644 --- a/Runtime/PSXSceneExporter.cs +++ b/Runtime/PSXSceneExporter.cs @@ -32,6 +32,11 @@ namespace SplashEdit.RuntimeCode private Quaternion _playerRot; private float _playerHeight; + private BSP _bsp; + + public bool PreviewBSP = true; + public int BSPPreviewDepth = 9999; + public void Export() { _psxData = DataStorage.LoadData(out selectedResolution, out dualBuffering, out verticalLayout, out prohibitedAreas); @@ -42,7 +47,7 @@ namespace SplashEdit.RuntimeCode PSXObjectExporter exp = _exporters[i]; EditorUtility.DisplayProgressBar($"{nameof(PSXSceneExporter)}", $"Export {nameof(PSXObjectExporter)}", ((float)i) / _exporters.Length); exp.CreatePSXTextures2D(); - exp.CreatePSXMesh(GTEScaling); + exp.CreatePSXMesh(GTEScaling, true); } _navmeshes = FindObjectsByType(FindObjectsSortMode.None); @@ -66,6 +71,9 @@ namespace SplashEdit.RuntimeCode _playerRot = player.transform.rotation; } + _bsp = new BSP(_exporters.ToList()); + _bsp.Build(); + ExportFile(); } @@ -450,6 +458,10 @@ namespace SplashEdit.RuntimeCode Vector3 cubeSize = new Vector3(8.0f * GTEScaling, 8.0f * GTEScaling, 8.0f * GTEScaling); Gizmos.color = Color.red; Gizmos.DrawWireCube(sceneOrigin, cubeSize); + + if (_bsp == null || !PreviewBSP) return; + _bsp.DrawGizmos(BSPPreviewDepth); + } }