using Godot; using System; using System.IO; using System.Text.Json; using System.Threading.Tasks; /// /// Handles loading and using frustum projection data from recorded JSON files /// public partial class FrustumLoader : Node { // Loaded frustum data public Projection LeftProjection { get; private set; } public Projection RightProjection { get; private set; } public float NearClip { get; private set; } public float FarClip { get; private set; } public DateTime RecordedAt { get; private set; } // Status properties public bool IsLoaded { get; private set; } = false; public Projection _invLeft { get; private set; } public Projection _invRight { get; private set; } // Path to the frustum data file private string _dataFilePath; /// /// Initialize the loader with a specific frustum data file path /// /// Path to the frustum data JSON file public FrustumLoader(string filePath = "recorded_data/frustum_data.json") { _dataFilePath = filePath; } /// /// Load frustum data from the specified file /// /// True if data was loaded successfully, false otherwise public bool LoadFrustumData() { try { if (!File.Exists(_dataFilePath)) { GD.PrintErr($"Frustum data file not found: {_dataFilePath}"); return false; } string jsonData = File.ReadAllText(_dataFilePath); var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; // Parse the JSON data var frustumJson = JsonSerializer.Deserialize(jsonData, options); // Convert to Godot Projection matrices LeftProjection = new Projection( new Vector4( frustumJson.LeftProjection.X[0], frustumJson.LeftProjection.X[1], frustumJson.LeftProjection.X[2], frustumJson.LeftProjection.X[3] ), new Vector4( frustumJson.LeftProjection.Y[0], frustumJson.LeftProjection.Y[1], frustumJson.LeftProjection.Y[2], frustumJson.LeftProjection.Y[3] ), new Vector4( frustumJson.LeftProjection.Z[0], frustumJson.LeftProjection.Z[1], frustumJson.LeftProjection.Z[2], frustumJson.LeftProjection.Z[3] ), new Vector4( frustumJson.LeftProjection.W[0], frustumJson.LeftProjection.W[1], frustumJson.LeftProjection.W[2], frustumJson.LeftProjection.W[3] ) ); RightProjection = new Projection( new Vector4( frustumJson.RightProjection.X[0], frustumJson.RightProjection.X[1], frustumJson.RightProjection.X[2], frustumJson.RightProjection.X[3] ), new Vector4( frustumJson.RightProjection.Y[0], frustumJson.RightProjection.Y[1], frustumJson.RightProjection.Y[2], frustumJson.RightProjection.Y[3] ), new Vector4( frustumJson.RightProjection.Z[0], frustumJson.RightProjection.Z[1], frustumJson.RightProjection.Z[2], frustumJson.RightProjection.Z[3] ), new Vector4( frustumJson.RightProjection.W[0], frustumJson.RightProjection.W[1], frustumJson.RightProjection.W[2], frustumJson.RightProjection.W[3] ) ); // Set other properties NearClip = frustumJson.NearClip; FarClip = frustumJson.FarClip; RecordedAt = DateTime.ParseExact(frustumJson.RecordedAt, "yyyy-MM-dd_HH-mm-ss", null); _invLeft = LeftProjection.Inverse(); _invRight = RightProjection.Inverse(); IsLoaded = true; GD.Print($"Frustum data loaded successfully from {_dataFilePath}"); GD.Print($"Recorded at: {RecordedAt}"); return true; } catch (Exception ex) { GD.PrintErr($"Error loading frustum data: {ex.Message}"); GD.PrintErr(ex.StackTrace); IsLoaded = false; return false; } } /// /// Load frustum data asynchronously /// public async Task LoadFrustumDataAsync() { return await Task.Run(() => LoadFrustumData()); } /// /// Project a 3D world position to screen space using the loaded frustum data /// /// The 3D position to project /// The world transform matrix /// Whether to use the left eye projection (true) or right eye projection (false) /// Screen space position (x, y in 0-1 range) public Vector2 ProjectToScreen(Vector3 worldPosition, Transform3D transformMatrix, bool useLeftEye = true) { if (!IsLoaded) { GD.PrintErr("Cannot project position: Frustum data not loaded"); return Vector2.Zero; } // Transform the world position to view space Vector3 viewPosition = transformMatrix.Inverse() * worldPosition; // Project the position using the appropriate projection matrix Projection projection = useLeftEye ? LeftProjection : RightProjection; Vector4 clipSpace = projection * new Vector4(viewPosition.X, viewPosition.Y, viewPosition.Z, 1.0f); // Perform perspective division to get normalized device coordinates float w = clipSpace.W; if (Mathf.Abs(w) < 0.0001f) { GD.PrintErr("Division by zero in perspective projection"); return Vector2.Zero; } // Convert from clip space to screen space (0-1 range) Vector2 screenPos = new Vector2( (clipSpace.X / w + 1.0f) * 0.5f, (clipSpace.Y / w + 1.0f) * 0.5f ); return screenPos; } public Vector2 ScreenPositionToViewAngles(Vector2 screenPos, bool useLeftEye = true) { if (!IsLoaded) { GD.PrintErr("Cannot convert to view angles: Frustum data not loaded"); return Vector2.Zero; } Vector2 ndcPos = new Vector2(screenPos.X * 2f - 1f, screenPos.Y * 2f - 1f); Projection invP = useLeftEye ? _invLeft : _invRight; // cached! Vector3 viewDir = UnprojectNDC(ndcPos, invP); float yawDeg = Mathf.RadToDeg(Mathf.Atan2(viewDir.X, -viewDir.Z)); float pitchDeg = Mathf.RadToDeg(Mathf.Atan2( viewDir.Y, Mathf.Sqrt(viewDir.X * viewDir.X + viewDir.Z * viewDir.Z))); // HACK HACK HACK // fudge factor to align the yaw angle with the expected range // This is a temporary fix and should be replaced with frustum data that works // 2f*6.17f I suspect is due appling the canting which is 6.17 degrees twice in the wrong direction. // Id guess its correctly applied, but if its the wrong direction, then it would be 2f*6.17f. // otherwise, it would be 6.17f missing. return new Vector2(yawDeg - (useLeftEye ? 1f : -1f) * 3.085f, pitchDeg); } private Vector3 UnprojectNDC(Vector2 ndcPos, Projection invProjection) { Vector4 eye = invProjection * new Vector4(ndcPos.X, ndcPos.Y, 1f, 1f); eye /= eye.W; // perspective divide return new Vector3(eye.X, eye.Y, eye.Z).Normalized(); } /// /// JSON structure for frustum data deserialization /// private class FrustumData { public ProjectionData LeftProjection { get; set; } public ProjectionData RightProjection { get; set; } public float NearClip { get; set; } public float FarClip { get; set; } public string RecordedAt { get; set; } } /// /// JSON structure for projection matrix data /// private class ProjectionData { public float[] X { get; set; } public float[] Y { get; set; } public float[] Z { get; set; } public float[] W { get; set; } } }