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