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; }
}
}