Editor/HumanoidMocap/Inference/BodyMotionBuilder.cs
#nullable enable
using System;
using System.Collections.Generic;
using System.Numerics;
using HumanoidMocap.Mapping;
using HumanoidMocap.Motion;

namespace HumanoidMocap.Inference;
using Vector3=System.Numerics.Vector3;

public static class BodyMotionBuilder
{
    public static MotionDocument WorldRelative(SmplxSkeleton skeleton,GvhmrDecoder.Pose pose,GvhmrDecoder.Root root,
        MotionDocument cameraSource,bool contactIk)
    {
        if(root.Translation.Length!=pose.Frames||root.Orientation.Length!=pose.Frames||cameraSource.Frames.Count!=pose.Frames)
            throw new ArgumentException("World-motion tracks differ in length.");
        var document=cameraSource.Copy();var rest=skeleton.RestPose(pose.Betas.AsSpan(0,10));
        // World yaw has an arbitrary origin. Match its initial horizontal heading to
        // the camera-relative preview without changing the gravity-aligned up axis.
        var worldForward=Vector3.Transform(Vector3.UnitZ,root.Orientation[0]);worldForward.Y=0;
        var cameraForward=Vector3.Transform(Vector3.UnitZ,MotionDocument.Q(cameraSource.Frames[0].Rotations[0]));cameraForward.Y=0;
        var yaw=worldForward.LengthSquared()>1e-8f&&cameraForward.LengthSquared()>1e-8f
            ?Quaternion.CreateFromAxisAngle(Vector3.UnitY,MathF.Atan2(Vector3.Cross(worldForward,cameraForward).Y,Vector3.Dot(worldForward,cameraForward)))
            :Quaternion.Identity;
        var ground=float.PositiveInfinity;foreach(var joint in rest)ground=Math.Min(ground,joint.Y);
        // The reference model's origin is near the pelvis. Its world-space rest must
        // stand on the same zero plane as the processed clip, rather than below it.
        document.Bones[0].RestPosition=MotionDocument.A(rest[0]-Vector3.UnitY*ground);
        document.Space=MotionSpace.WorldRelative;document.MetricScaleCalibrated=false;
        for(var t=0;t<pose.Frames;t++)
        {
            var frame=document.Frames[t];frame.Positions[0]=MotionDocument.A(Vector3.Transform(rest[0]+root.Translation[t],yaw));
            frame.Rotations[0]=MotionDocument.A(Quaternion.Normalize(yaw*root.Orientation[t]));
            for(var j=1;j<22;j++)frame.Rotations[j]=MotionDocument.A(pose.BodyRotations[t*21+j-1]);
            if(contactIk)foreach(var j in new[]{0,1,2,4,5,7,8,10,11,13,14,16,17,18,19,20,21})frame.Evidence[j]=JointEvidence.GeneratedIk;
        }
        document.Diagnostics.Clear();
        document.Diagnostics.AddRange(new[]{
            "Estimated world-relative GVHMR motion under an explicitly stationary-camera assumption; camera motion has not been measured.",
            "Focal length, body shape and metric scale are model estimates, not calibrated measurements.",
            contactIk?"Upstream stationary-camera root/contact processing and two-iteration limb CCD applied. Limb transforms affected by IK are labeled GeneratedIk.":"Raw GVHMR gravity/velocity rollout, before contact/root correction.",
            "Stationary-joint predictions are not observed grip contacts or per-joint 3D confidence. Detailed fingers and object motion are not captured.",
            "Final target-proportion correction still runs after retargeting. Review foot plants, jumps and fast actions against the video."
        });
        document.Validate();return document;
    }
    static readonly BoneRole[] roles={BoneRole.Hips,BoneRole.UpperLegL,BoneRole.UpperLegR,BoneRole.Spine0,
        BoneRole.LowerLegL,BoneRole.LowerLegR,BoneRole.Spine1,BoneRole.FootL,BoneRole.FootR,BoneRole.Spine2,
        BoneRole.ToeL,BoneRole.ToeR,BoneRole.Neck,BoneRole.ClavicleL,BoneRole.ClavicleR,BoneRole.Head,
        BoneRole.UpperArmL,BoneRole.UpperArmR,BoneRole.LowerArmL,BoneRole.LowerArmR,BoneRole.HandL,BoneRole.HandR};
    /// <summary>Raw camera-relative GVHMR reconstruction. Contact/IK-corrected output is a
    /// separate stage. No detailed fingers, calibrated scale or measured joint confidence.</summary>
    public static MotionDocument CameraRelative(SmplxSkeleton skeleton,GvhmrDecoder.Pose pose,Vector3[] translations,
        IReadOnlyList<double> timestamps,string name,string video,string sha256,double fps,GvhmrDecoder.Camera camera)
    {
        if(translations.Length!=pose.Frames||timestamps.Count!=pose.Frames)throw new ArgumentException("Motion track lengths differ.");
        var rest=skeleton.RestPose(pose.Betas.AsSpan(0,10));var parents=SmplxSkeleton.Parents;
        var document=new MotionDocument{Name=name,SourceVideo=video,SourceSha256=sha256,SourceFps=fps,
            Backend="GVHMR / C# native CPU",ModelVersion="ee960bb6 / "+GvhmrTemporalNetwork.CheckpointSha256,
            Space=MotionSpace.CameraRelative,MetricScaleCalibrated=false};
        for(var j=0;j<22;j++)document.Bones.Add(new(){Name=roles[j].ToString(),Role=roles[j],Parent=parents[j],Group=j is 16 or 17 or 18 or 19 or 20 or 21?"arms":"body",
            RestPosition=MotionDocument.A(j==0?rest[j]:rest[j]-rest[parents[j]])});
        var cameraToDocument=Quaternion.CreateFromAxisAngle(Vector3.UnitX,MathF.PI);
        for(var t=0;t<pose.Frames;t++)
        {
            var frame=new MotionFrame{Time=timestamps[t],Positions=new float[22][],Rotations=new float[22][],Evidence=new JointEvidence[22]};
            for(var j=0;j<22;j++)
            {
                frame.Positions[j]=MotionDocument.A(j==0?Vector3.Transform(rest[0]+translations[t],cameraToDocument):rest[j]-rest[parents[j]]);
                frame.Rotations[j]=MotionDocument.A(j==0?Quaternion.Normalize(cameraToDocument*pose.CameraOrientation[t]):pose.BodyRotations[t*21+j-1]);
                frame.Evidence[j]=JointEvidence.Reconstructed;
            }
            document.Frames.Add(frame);
        }
        document.Cameras.Add(new(){Id="video",Source="Input video; focal length estimated from image diagonal",Calibrated=false,Synchronized=true,
            Intrinsics=new[]{camera.FocalLength,0,camera.CenterX,0,camera.FocalLength,camera.CenterY,0,0,1}});
        document.Diagnostics.AddRange(new[]{
            "Real ViTPose-H observations, HMR2 features and GVHMR temporal inference. C# port reference parity is incomplete.",
            "Camera-relative reconstruction with identity camera-angular-velocity conditioning assumption; camera motion has not been recovered.",
            "Focal length is estimated, not calibrated. Monocular body shape and scale are model estimates.",
            "Raw body output: upstream source contact/limb IK has not yet been applied. No world root-motion claim.",
            "GVHMR provides no detailed finger capture or per-joint 3D confidence. Raw 2D heatmap scores remain in reconstruction state."
        });
        document.Validate();return document;
    }
}