using System.Numerics;
namespace AcDream.Core.Vfx;
///
/// Retail's particle draw-frame facing law, ported verbatim from
/// CPhysicsPart::calc_draw_frame @0x0050DFA0:
///
///
/// draw = pos
/// switch (deg_mode):
/// 2: Frame::set_vector_heading(draw, viewer_heading) // face viewer, roll-free
/// 3/4/5: Frame::rotate_around_axis_to_vector(draw, X/Y/Z) // one free axis
/// else: authored orientation (mode 1, mode 0, out of range)
///
///
/// viewer_heading is the normalized part→viewer direction
/// (CPhysicsPart::UpdateViewerDistance @0x0050E030). The mode is the
/// FIRST degrade entry's DegradeMode
/// (GfxObjDegradeInfo::get_degrade @0x0051E4B0). Retail's
/// Always2D (mode != 1) affects only cell membership, not drawing —
/// the draw path is always the authored mesh with this facing applied.
/// Research: docs/research/2026-08-23-sky-default-script-port.md.
///
public static class RetailParticleFacing
{
/// True when the mode orients the part toward the viewer at all
/// (retail's deg_mode != 1 && (deg_mode - 2) <= 3).
public static bool Faces(uint degradeMode)
=> degradeMode >= 2u && degradeMode <= 5u;
///
/// Orients a particle quad per the retail law. Inputs are the particle's
/// authored orientation plus the local in-plane axes the renderer chose
/// for the sprite (unit vectors in sprite-local space); output is the
/// world-space direction pair for the quad's X/Y spans.
///
/// First degrade entry's mode.
/// The particle's authored world orientation.
/// Sprite-local in-plane X (unit).
/// Sprite-local in-plane Y (unit).
/// Normalized particle→viewer direction.
/// Basis used when the facing construction is
/// degenerate (viewer straight along world up) — the camera right.
/// Camera up, same degenerate fallback.
public static (Vector3 XDir, Vector3 YDir) OrientQuad(
uint degradeMode,
Quaternion orientation,
Vector3 localAxisX,
Vector3 localAxisY,
Vector3 toViewerUnit,
Vector3 fallbackRight,
Vector3 fallbackUp)
{
if (degradeMode == 2u)
return FaceViewerRollFree(toViewerUnit, fallbackRight, fallbackUp);
Vector3 worldX = Vector3.Transform(localAxisX, orientation);
Vector3 worldY = Vector3.Transform(localAxisY, orientation);
if (degradeMode < 3u || degradeMode > 5u)
return (worldX, worldY);
// Modes 3/4/5 spin the authored frame around its own local X/Y/Z so
// the sprite's face normal points at the viewer as far as the
// constraint allows (Frame::rotate_around_axis_to_vector).
Vector3 localAxis = degradeMode switch
{
3u => Vector3.UnitX,
4u => Vector3.UnitY,
_ => Vector3.UnitZ,
};
Vector3 axis = Vector3.Transform(localAxis, orientation);
Vector3 normal = Vector3.Cross(worldX, worldY);
if (normal.LengthSquared() < 1e-10f)
return (worldX, worldY);
normal = Vector3.Normalize(normal);
Vector3 targetInPlane = toViewerUnit - axis * Vector3.Dot(toViewerUnit, axis);
Vector3 normalInPlane = normal - axis * Vector3.Dot(normal, axis);
if (targetInPlane.LengthSquared() < 1e-8f
|| normalInPlane.LengthSquared() < 1e-8f)
{
return (worldX, worldY);
}
targetInPlane = Vector3.Normalize(targetInPlane);
normalInPlane = Vector3.Normalize(normalInPlane);
float cos = Math.Clamp(Vector3.Dot(normalInPlane, targetInPlane), -1f, 1f);
float sin = Vector3.Dot(Vector3.Cross(normalInPlane, targetInPlane), axis);
float angle = MathF.Atan2(sin, cos);
var spin = Quaternion.CreateFromAxisAngle(axis, angle);
return (Vector3.Transform(worldX, spin), Vector3.Transform(worldY, spin));
}
///
/// Mode 2 — Frame::set_vector_heading: the quad's face normal
/// points at the viewer with zero roll against world up (+Z). The quad's
/// X span stays horizontal; its Y span becomes the in-plane up.
///
private static (Vector3 XDir, Vector3 YDir) FaceViewerRollFree(
Vector3 toViewerUnit,
Vector3 fallbackRight,
Vector3 fallbackUp)
{
Vector3 right = Vector3.Cross(toViewerUnit, Vector3.UnitZ);
if (right.LengthSquared() < 1e-8f)
{
// Viewer straight above/below the part: heading is undefined —
// hold the camera plane, which retail's next frame resolves the
// same way once the direction tilts.
return (fallbackRight, fallbackUp);
}
right = Vector3.Normalize(right);
Vector3 up = Vector3.Cross(right, toViewerUnit);
return (right, up);
}
}