unit AnimationrigSpine;

interface

uses SysUtils,Classes,Contnrs,StrUtils,math,
     Animationrig,AnimationrigMisc,AnimationrigState,AnimationrigPhysicsSpring,
     uSpineCommon,uSpineAnimation,uSpineSkeleton,uSpineAttachment,uSpineSkin;

type
  TAnimrigStateSpineRenderer = class(TAnimrigStateRenderer)
    protected
      fBoneCacheList : TStringList;
      fstate : pspAnimationState;
      fskeleton : pspSkeleton;
      function ExtractWorldMatrix(bone:pspBone) : TPhysicsMatrix4;
    public
      const SaveVersion = 1;
      AntiScale : single = 1.0;

      constructor Create;
      destructor Destroy; override;
      procedure Clear; override;

      procedure Update(deltatime:double; deltaphysics:double=-1); override;

      procedure SetAnimation(trackindex,usetrack: integer; const basename: string; loop:boolean; addanim:boolean; trackmix:TAnimrigMisc.MixBlend; duration:single; seekmode:boolean; loopbegin,loopend:single); override;
      procedure SetTrackBlendAlpha(trackindex,usetrack: integer; alpha1, alpha2, alpha3, alpha4: single; exists:boolean); override;
      procedure SetTrackSeekTime(trackindex,usetrack:integer; time1, time2, time3, time4: single; exists:boolean); override;
      procedure SetMixDuration(duration:single); override;
      procedure SetSkin(const skinname:string); override;
      function  GetBoneMatrix(const bonename:string):TAnimrigMisc.Matrix4; override;

      property AnimationState : pspAnimationState read fstate write fstate;
      property Skeleton : pspSkeleton read fskeleton write fskeleton;

      procedure SaveToBinary(ms:TMemoryStream);

      class procedure SpineAnimationStateSaveToBinary(state:pspAnimationState; ms:TMemoryStream);
      class function  SpineAnimationStateLoadFormBinary(state:pspAnimationState; ms:TMemoryStream):boolean;
  end;

  TAnimrigStateSpinePhysicsRenderer = class(TAnimrigStateSpineRenderer)
    public
      procedure Update(deltatime:double; deltaphysics:double=-1); override;
      procedure SetupPhysics(setting:TStringList); override;
      procedure ApplyPhysics(deltatime : double); override;
      procedure PreparePhysics; override;
      procedure AppeasePhysics; override;
      procedure AddPhysicsForce(const target:string; x,y,z : double); override;
  end;

implementation

{ TAnimrigStateRendererSpine }

procedure TAnimrigStateSpineRenderer.Clear;
begin
  inherited;

  fstate := nil;
  fskeleton := nil;
  fBoneCacheList.Clear;
end;

constructor TAnimrigStateSpineRenderer.Create; //(state: pspAnimationState; skeleton:pspSkeleton);
begin
  inherited ;

  fObjectType := anoSpine;
  fBoneCacheList := TStringList.Create;

  Clear;
end;

destructor TAnimrigStateSpineRenderer.Destroy;
begin
  fBoneCacheList.Free;

  inherited;
end;

function TAnimrigStateSpineRenderer.ExtractWorldMatrix(bone: pspBone): TPhysicsMatrix4;
var
  sx,sy : single;
begin
  result := TAnimrigMisc.IdentityMatrix;
  result._11 := bone.a;
  result._12 := bone.c;
  result._21 := bone.b;
  result._22 := bone.d;
  result._41 := (bone.worldX - skeleton.x) * AntiScale;
  result._42 := (bone.worldY - skeleton.y) * AntiScale;

  sx := 1.0;
  sy := 1.0;
  if skeleton.scaleX < 0.0 then sx := sx * -1.0;
  if Skeleton.scaleY < 0.0 then sy := sy * -1.0;
  if spBone_isYDown = 1 then sy := sy * -1.0;

  TAnimrigMisc.m4PushScale(result , sx,sy,1.0);
end;

function TAnimrigStateSpineRenderer.GetBoneMatrix(const bonename: string): TAnimrigMisc.Matrix4;
var
  bone : pspBone;
  i : integer;
  cachehit : boolean;
begin
  result := TAnimrigMisc.IdentityMatrix;
  if fskeleton = nil then exit;
  if fstate = nil then exit;

  // cache
  bone := nil;
  cachehit := false;
  for i:=0 to fBoneCacheList.Count-1 do
    if bonename = fBoneCacheList.Strings[i] then
    begin
      bone := pspBone(fBoneCacheList.Objects[i]);
      cachehit := true;
      break;
    end;
  if bone = nil then bone := spSkeleton_findBone(fskeleton,bonename);

  if not(cachehit) then fBoneCacheList.AddObject(bonename,pointer(bone));

  if bone = nil then exit;
  result := ExtractWorldMatrix(bone);
end;

procedure TAnimrigStateSpineRenderer.SaveToBinary(ms: TMemoryStream);
begin
  SpineAnimationStateSaveToBinary(fstate,ms);
end;

procedure TAnimrigStateSpineRenderer.SetAnimation(trackindex,usetrack: integer; const basename: string; loop, addanim:boolean; trackmix:TAnimrigMisc.MixBlend; duration:single; seekmode:boolean; loopbegin,loopend:single);
  procedure inSetEmpty(trackindex:integer; dt:single);
  begin
    if dt < 0.0 then dt := fstate.data.defaultMix;
    if addanim then
      spAnimationState_addEmptyAnimation(fstate,trackindex , dt , 0.0)
    else
      spAnimationState_setEmptyAnimation(fstate,trackindex , dt);
  end;
  procedure inSetAnimation(trackindex:integer; const name1,name2:string; dt:single);
  var
    anim : pspAnimation;
    track : pspTrackEntry;
    l : integer;
  begin
    // search
    anim := spSkeletonData_findAnimation(fstate.data.skeletonData,name1);
    if anim = nil then anim := spSkeletonData_findAnimation(fstate.data.skeletonData,name2);

    if anim = nil then
    begin
      // set/add empty
      inSetEmpty(trackindex , dt);
    end
    else
    begin
      // set/add animation
      l := 0;
      if loop then l := 1;
      // set animation
      if addanim then
        track := spAnimationState_addAnimation(fstate,trackindex,anim , l,0.0)
      else
        track := spAnimationState_setAnimation(fstate,trackindex,anim , l);

      if track <> nil then
      begin
        // mixblend
        if track.mixBlend >= SP_MIX_BLEND_REPLACE then   // ignore SETUP FIRST
        begin
          case trackmix of
            anmReplace : track.mixBlend := SP_MIX_BLEND_REPLACE;
            anmAdd     : track.mixBlend := SP_MIX_BLEND_ADD;
          end;
        end;
        // duration
        if duration < 0.0 then
        begin
          // use spine default mix
        end
        else
        begin
          track.mixDuration := max(dt,0.0);
        end;
        // seekmode
        if seekmode then track.timeScale := 0.0
                    else track.timeScale := 1.0;

        // loop area
        if loopbegin <> loopend then
        begin
          track.animationStart := loopbegin;
          track.animationEnd := loopend;
          // it often makes sense to set TrackEntry.Animation Last to the same value to prevent timeline keys before the start time from triggering.
          track.animationLast := loopbegin;
        end;
      end;
    end;
  end;
begin
  if fstate = nil then exit;

  // 1 animation blend layer
  if usetrack = 1 then
  begin
    if basename = 'empty' then
    begin
      inSetEmpty(trackindex+0 , duration);
    end
    else
    begin
      inSetAnimation(trackindex+0 , basename,basename+'@up' , duration);
    end;
  end;

  // 4 animations blend layer
  if usetrack = 4 then
  begin
    if basename = 'empty' then
    begin
      inSetEmpty(trackindex+0 , duration);
      inSetEmpty(trackindex+1 , duration);
      inSetEmpty(trackindex+2 , duration);
      inSetEmpty(trackindex+3 , duration);
    end
    else
    begin
      inSetAnimation(trackindex+0 , basename,basename+'@up' , duration);
      inSetAnimation(trackindex+1 , basename+'@right','' , duration);
      inSetAnimation(trackindex+2 , basename+'@down','' , duration);
      inSetAnimation(trackindex+3 , basename+'@left','' , duration);
    end;
  end;

  // 2 animations blend layer. use @up , @right.
  if usetrack = 2 then
  begin
    if basename = 'empty' then
    begin
      inSetEmpty(trackindex+0 , duration);
      inSetEmpty(trackindex+1 , duration);
    end
    else
    begin
      inSetAnimation(trackindex+0 , basename,basename+'@up' , duration);
      inSetAnimation(trackindex+1 , basename+'@right','' , duration);
    end;
  end;
end;

procedure TAnimrigStateSpineRenderer.Update(deltatime: double; deltaphysics:double);
var
  fpumask : cardinal;
  changed : boolean;
begin
  if fstate = nil then exit;
  if fskeleton = nil then exit;

  changed := false;
  fpumask := Get8087CW;
  if (fpumask and $3f) <> $3f then
  begin
    math.SetExceptionMask([exInvalidOp,exDenormalized,exZeroDivide,exOverflow,exUnderflow,exPrecision]);   // = $3F
    changed := true;
  end;

  spAnimationState_update(fstate , deltatime);
  spAnimationState_apply(fstate , fskeleton);    // *zero div
  spSkeleton_updateWorldTransform(fskeleton);    // x,y transform after , next call spSkeleton_updateWorldTransform

  if changed then Set8087CW(fpumask);
end;

procedure TAnimrigStateSpineRenderer.SetTrackBlendAlpha(trackindex,usetrack: integer; alpha1, alpha2, alpha3, alpha4: single; exists:boolean);
  procedure inSet(trackindex:integer; alpha:single);
  var
    track : pspTrackEntry;
  begin
    track := spAnimationState_getCurrent(fstate,trackindex);
    if track = nil then exit;

    track.alpha := alpha;
  end;
begin
  if fstate = nil then exit;

  // exists enable layer flag
  // if pass track alpha , check flag and return function.
  //if not(exists) then exit;

  // 1 animation blend layer
  if usetrack = 1 then
  begin
    inSet(trackindex+0 , alpha1);  // up
  end;

  // 4 animations blend layer
  if usetrack = 4 then
  begin
    inSet(trackindex+0 , alpha1);  // @up
    inSet(trackindex+1 , alpha2);  // @right
    inSet(trackindex+2 , alpha3);  // @down
    inSet(trackindex+3 , alpha4);  // @left
  end;

  // 2 animation blend layer
  // use @up , @right animation
  // @right , @down arer invert animation (-1)
  if usetrack = 2 then
  begin
    inSet(trackindex+0 , alpha1 - alpha3); // @up
    inSet(trackindex+1 , alpha2 - alpha4); // @right
  end;
end;

procedure TAnimrigStateSpineRenderer.SetTrackSeekTime(trackindex, usetrack: integer; time1, time2, time3, time4: single; exists: boolean);
  procedure inSet(trackindex:integer; time_:single);
  var
    track : pspTrackEntry;
  begin
    track := spAnimationState_getCurrent(fstate,trackindex);
    if track = nil then exit;

    track.trackTime := time_;
  end;
begin
  if fstate = nil then exit;

  // exists enable layer flag
  // if pass track alpha , check flag and return function.
  //if not(exists) then exit;

  // 1 animation seek time layer
  if usetrack = 1 then
  begin
    inSet(trackindex+0 , time1);  // up
  end;

  // 4 animations seek time layer
  if usetrack = 4 then
  begin
    inSet(trackindex+0 , time1);  // up
    inSet(trackindex+1 , time2);  // right
    inSet(trackindex+2 , time3);  // down
    inSet(trackindex+3 , time4);  // left
  end;
end;

class function TAnimrigStateSpineRenderer.SpineAnimationStateLoadFormBinary(state: pspAnimationState; ms: TMemoryStream): boolean;
var
  endpoint : int64;
  b : boolean;
begin
  result := false;

  endpoint := -1;
  try
    // head
    TAnimrigMisc.ReadHead(ms,'spinestate');
    // data size
    endpoint := TAnimrigMisc.ReadSkipBegin(ms);
    // state exists
    if state = nil then
    begin
      // skip
    end
    else
    begin
      // data exists
      b := TAnimrigMisc.ReadBool(ms);
      if b then
      begin
        // exists
      end
      else
      begin
        // null
      end;
    end;
    result := true;
  except
    //
  end;

  // end point
  TAnimrigMisc.JumpSkipEnd(ms,endpoint);
end;

class procedure TAnimrigStateSpineRenderer.SpineAnimationStateSaveToBinary(state: pspAnimationState; ms:TMemoryStream);
  procedure inTrack(track : pspTrackEntry);
  var
    endpoint : int64;
    s : string;
  begin
    // head
    TAnimrigMisc.WriteHead(ms , 'track');
    // data size
    endpoint := TAnimrigMisc.WriteSkipBegin(ms);

    // exists
    if track = nil then
    begin
      TAnimrigMisc.WriteBool(ms , false);
    end
    else
    begin
      TAnimrigMisc.WriteBool(ms , true);
      // animation (include <empty>)
      s := UTF8ToString(track.animation.name);
      TAnimrigMisc.WriteString(ms , s);
      // state
      TAnimrigMisc.WriteInteger(ms , track.loop);
      TAnimrigMisc.WriteFloat(ms , track.delay);
      TAnimrigMisc.WriteFloat(ms , track.trackTime);
      TAnimrigMisc.WriteFloat(ms , track.timeScale);
      TAnimrigMisc.WriteFloat(ms , track.alpha);
      TAnimrigMisc.WriteInteger(ms , integer(track.mixBlend));
      // next
      inTrack(pspTrackEntry(track.next));
    end;

    // write data size
    TAnimrigMisc.WriteSkipEnd(ms,endpoint);
  end;
var
  i : integer;
  endpoint : int64;
begin
  // head
  TAnimrigMisc.WriteHead(ms , 'spinestate');
  // data size
  endpoint := TAnimrigMisc.WriteSkipBegin(ms);

  // exists
  if state = nil then
  begin
    TAnimrigMisc.WriteBool(ms , false);
  end
  else
  begin
    TAnimrigMisc.WriteBool(ms , true);
    // version
    TAnimrigMisc.WriteInteger(ms , SaveVersion);
    // tracks
    TAnimrigMisc.WriteHead(ms , 'tracks');
    TAnimrigMisc.WriteInteger(ms , state.tracksCount);
    // track
    for i:=0 to state.tracksCount-1 do
    begin
      inTrack(state.tracks[i]);
    end;
  end;

  // write data size
  TAnimrigMisc.WriteSkipEnd(ms,endpoint);
end;

procedure TAnimrigStateSpineRenderer.SetMixDuration(duration: single);
begin
  if fstate = nil then exit;

  fstate.data.defaultMix := duration;
end;

procedure TAnimrigStateSpineRenderer.SetSkin(const skinname: string);
var
  skin : pspSkin;
begin
  if fskeleton = nil then exit;

  skin := spSkeletonData_findSkin(fskeleton.data,skinname);
  if skin <> nil then
  begin
    spSkeleton_setSkin(fskeleton,skin);
    spSkeleton_setToSetupPose(fskeleton);
  end;
end;

{ TAnimrigStateSpinePhysicsRenderer }

procedure TAnimrigStateSpinePhysicsRenderer.AddPhysicsForce(const target:string; x, y, z: double);
var
  p : pointer;
  g : TPhysicsGroup;
  v : TPhysicsVector3;
begin
  // safe check
  if fstate = nil then exit;
  if fskeleton = nil then exit;

  if target = 'none' then exit;

  v.x := x;
  v.y := y;
  v.z := z;
  for p in fPhysicsGroups do
  begin
    g := TPhysicsGroup(p);
    if (g.AliasName = target) or (target = 'all') then g.AddForce(v);
  end;
end;

procedure TAnimrigStateSpinePhysicsRenderer.AppeasePhysics;
var
  p : pointer;
  g : TPhysicsGroup;
begin
  // safe check
  if fstate = nil then exit;
  if fskeleton = nil then exit;

  for p in fPhysicsGroups do
  begin
    g := TPhysicsGroup(p);
    g.Appease;
  end;
end;

procedure TAnimrigStateSpinePhysicsRenderer.PreparePhysics;
  procedure inSpring(g:TPhysicsGroupSpringBone; skeleton:pspSkeleton);
  var
    p : pointer;
    b : TPhysicsNodeBone;
    sb : pspBone;
  begin
    for p in g.NodeList do
    begin
      b := TPhysicsNodeBone(p);
      if b.UserObject <> nil then
      begin
        sb := pspBone(b.UserObject);
        // set setup-pose param. usage param only.
        case g.Mode of
          gsmBone :
            begin
              sb.rotation := sb.data.rotation;
            end;
          gsmStrBone :
            begin
              sb.rotation := sb.data.rotation;
              sb.x := sb.data.x;
              sb.y := sb.data.y;
            end;
          gsmStrPoint,gsmPoint :
            begin
              sb.x := sb.data.x;
              sb.y := sb.data.y;
            end;
        end;
      end;
    end;
  end;
  procedure inElement(g:TPhysicsGroupElementBone; skeleton:pspSkeleton);
  var
    sb : pspBone;
  begin
    sb := pspBone(g.UserObject);
    if (sb = nil)then exit;

    sb.rotation := sb.data.rotation;
    sb.x := sb.data.x;
    sb.y := sb.data.y;
    sb.scaleX := sb.data.scaleX;
    sb.scaleY := sb.data.scaleY;
  end;
var
  p : Pointer;
  g : TPhysicsGroup;
begin
  // safe check
  if fstate = nil then exit;
  if fskeleton = nil then exit;

  for p in fPhysicsGroups do
  begin
    g := TPhysicsGroup(p);
    case g.GroupType of
      gtSpring : inSpring(TPhysicsGroupSpringBone(g),fskeleton);
      gtElement : inElement(TPhysicsGroupElementBone(g),fskeleton);
    end;
  end;
end;

procedure TAnimrigStateSpinePhysicsRenderer.SetupPhysics(setting:TStringList);
  // get setting value
  function inGetSettingFloat(index:integer; const n:string; def_,min_,max_:double):double;
  var
    sl : TStringList;
    s : string;
  begin
    sl := TAnimrigMisc.DivideString(',',setting.Values[n]);
    s := '';
    if index < sl.Count then s := sl.Strings[index];
    sl.Free;

    result := StrToFloatDef(s,def_);
    result := min(max(result,min_),max_);
  end;
  function inGetSettingString(const n:string; const def_:string):string;
  begin
    result := setting.Values[n];
    if result = '' then result := def_;
  end;
  // spring
  procedure inAddBone(g:TPhysicsGroupSpringBone; bone:pspBone; parent:TPhysicsNodeBone);
  var
    b : TPhysicsNodeBone;
    s : string;
    i : integer;
  begin
    // terminate code
    if parent <> nil then
    begin
      s := UTF8ToString(bone.data.name);
      if pos('@end',s) > 0 then exit;    // terminate code
      if pos('@',s) > 0 then exit;       // when other code exists, safe exit.
    end;

    b := TPhysicsNodeBone.Create(parent);
    b.UserObject := bone;
    b.LocalAngle := bone.data.rotation / 360.0;
    b.LocalLength := bone.data.length * AntiScale;
    b.LocalPosition.x := bone.data.x * AntiScale;
    b.LocalPosition.y := bone.data.y * AntiScale;
    b.LocalSizeX := bone.data.scaleX;
    b.LocalSizeY := bone.data.scaleY;

    g.AddBone(b);

    for i:=0 to bone.childrenCount-1 do
      inAddBone(g,pspBone(bone.children[i]),b);

    // create terminate bone
    if bone.childrenCount = 0 then
    begin
      parent := b;
      b := TPhysicsNodeBone.Create(parent);
      b.UserObject := nil;
      b.LocalAngle := 0;
      b.LocalLength := 100;
      b.LocalPosition.x := parent.LocalLength;
      b.LocalPosition.y := 0;
      g.AddBone(b);
    end;
  end;
  procedure inAddSpring(g:TPhysicsGroupSpringBone; bone:TPhysicsNodeBone; spring:TPhysicsSpringBone);
  var
    p : pointer;
    s : TPhysicsSpringBone;
  begin
    for p in bone.Children do
    begin
      s := TPhysicsSpringBone.Create(g,spring,bone,TPhysicsNodeBone(p));
      g.AddSpring(s);
      inAddSpring(g,TPhysicsNodeBone(p),s);
    end;
  end;
  procedure inAddSpringGroup(name,alias:string; bone : pspBone);
  var
    g : TPhysicsGroupSpringBone;
    root : pspbone;
    s : string;
  begin
    // need has parent
    root := pspBone(bone.parent);
    if root = nil then exit;

    if alias = '' then alias := name;

    // enable / disable
    if inGetSettingString(alias+'.enable','true') = 'false' then exit;

    g := TPhysicsGroupSpringBone.Create;
    g.Name := name;
    g.AliasName := alias;
    g.RootUserObject := root;

    s := inGetSettingString(alias+'.mode','bone');
    if s = 'strpoint' then g.Mode := gsmStrPoint;
    if s = 'point' then g.Mode := gsmPoint;
    if s = 'strbone' then g.Mode := gsmStrBone;
    if s = 'bone' then g.Mode := gsmBone;

    g.K := inGetSettingFloat(0,alias+'.k',g.K , 0.1,100*100);
    g.Damping := inGetSettingFloat(0,alias+'.damping',g.Damping , 0.1,100*100);
    g.Mass := inGetSettingFloat(0,alias+'.mass',g.Mass , 0.001,1000);
    g.Gravity := inGetSettingFloat(0,alias+'.gravity',g.Gravity , 0,10000*100);
    g.GravityVector.x := inGetSettingFloat(0,alias+'.gravityvector',g.GravityVector.x , -1000,+1000);
    g.GravityVector.y := inGetSettingFloat(1,alias+'.gravityvector',g.GravityVector.y , -1000,+1000);
    g.GravityVector.z := inGetSettingFloat(2,alias+'.gravityvector',g.GravityVector.z , -1000,+1000);
    g.GravityVector.Normalize;
    g.LimitVelocity := inGetSettingFloat(0,alias+'.limitvelocity',g.LimitVelocity, 1,$ffff);   // 100 = physics frame
    g.LimitAngleMin := inGetSettingFloat(0,alias+'.limitangle',g.LimitAngleMin , -$ff,0);
    g.LimitAngleMax := inGetSettingFloat(1,alias+'.limitangle',g.LimitAngleMax , 0,+$ff);
    g.LimitMoveRatio := inGetSettingFloat(0,alias+'.limitmove',g.LimitMoveRatio , 0,+$ff);

    g.Alpha := inGetSettingFloat(0,alias+'.alpha',g.Alpha , 0.0,1.0);
    g.ForceRatio := inGetSettingFloat(0,alias+'.force',g.ForceRatio , -10.0,10.0);
    g.ForceWiggle := inGetSettingFloat(0,alias+'.wiggle',g.ForceWiggle , 0.0,1.0);

    // privte
    g.ForceRotateRatio := ingetSettingFloat(0,alias+'.forcerotate',g.ForceRotateRatio , 0.0,10.0);

    inAddBone(g,bone,nil);
    inAddSpring(g,g.RootBone,nil);

    g.RootMatrix := ExtractWorldMatrix(root);
    g.SetupNatural;

    fPhysicsGroups.Add(g);
  end;
  // element spring
  procedure inAddElementGroup(name,alias:string; bone : pspBone);
    function inGetMask(const s:string):integer;
    var
      m : integer;
    begin
      if (s = 'none') then
      begin
        result := 0;
        exit;
      end;
      m := 0;
      if pos('x',s) > 0 then m := m + 1;
      if pos('y',s) > 0 then m := m + 2;
      if pos('z',s) > 0 then m := m + 4;
      result := m;
    end;
  var
    g : TPhysicsGroupElementBone;
    a : single;
  begin
    if alias = '' then alias := name;

    // enable / disable
    if inGetSettingString(alias+'.enable','true') = 'false' then exit;

    g := TPhysicsGroupElementBone.Create;
    g.Name := name;
    g.AliasName := alias;
    g.UserObject := bone;

    g.SpringPosition.Mask := inGetmask(inGetSettingString(alias+'.maskpos','xyz'));
    g.SpringScale.Mask := inGetmask(inGetSettingString(alias+'.maskscale','xyz'));
    g.SpringRotate.Mask := inGetmask(inGetSettingString(alias+'.maskrot','xyz'));

    g.K := inGetSettingFloat(0,alias+'.k',g.K , 0.1,100*100);
    g.Damping := inGetSettingFloat(0,alias+'.damping',g.Damping , 0.1,100*100);
    g.Mass := inGetSettingFloat(0,alias+'.mass',g.Mass , 0.001,1000);
    g.LimitVelocity := inGetSettingFloat(0,alias+'.limitvelocity',g.LimitVelocity, 1,$ffff);   // 100 = physics frame
    a := inGetSettingFloat(0,alias+'.alpha',1.0,0.0,+2.0);
    g.SpringPosition.Alpha := a;
    g.SpringScale.Alpha := a;
    g.SpringRotate.Alpha := a;

    fPhysicsGroups.Add(g);
  end;
var
  i : integer;
  s,name,op,alias : string;
  idx : integer;
  sl : TStringList;
begin
  // do not forget to clear PhysicsGroups
  inherited ;

  if fstate = nil then exit;
  if fskeleton = nil then exit;

  spSkeleton_updateWorldTransform(fskeleton);

  for i:=0 to fskeleton.bonesCount-1 do
  begin
    s := UTF8ToString(fskeleton.bones[i].data.name);

    name := '';
    idx := pos('@',s);
    if idx = 0 then continue;

    // name @ spring,aliasname
    op := '';
    alias := '';
    name := copy(s,1,idx-1);
    sl := TAnimrigMisc.DivideString(',' , copy(s,idx+1,4096));
    if sl.Count > 0 then op := sl.Strings[0];
    if sl.Count > 1 then alias := sl.Strings[1];
    sl.Free;

    if op = 'spring' then inAddSpringGroup(name,alias,fskeleton.bones[i]);
    if op = 'element' then inAddElementGroup(name, alias,fskeleton.bones[i]);
  end;
end;

procedure TAnimrigStateSpinePhysicsRenderer.Update(deltatime: double; deltaphysics:double);
var
  fpumask : cardinal;
  changed : boolean;
begin
  if fstate = nil then exit;
  if fskeleton = nil then exit;

  changed := false;
  fpumask := Get8087CW;
  if (fpumask and $3f) <> $3f then
  begin
    math.SetExceptionMask([exInvalidOp,exDenormalized,exZeroDivide,exOverflow,exUnderflow,exPrecision]);   // = $3F
    changed := true;
  end;

  spAnimationState_update(fstate , deltatime);
  PreparePhysics;                                // Prepare physics target bones. Set setup-pose.
  spAnimationState_apply(fstate , fskeleton);    // *zero div
  spSkeleton_updateWorldTransform(fskeleton);    // x,y transform after , next call spSkeleton_updateWorldTransform

  // physics
  if fPhysicsGroups.Count > 0 then
  begin
    if (deltaphysics < 0) then deltaphysics := deltatime;
    ApplyPhysics(deltaphysics);
  end;

  if changed then Set8087CW(fpumask);
end;

procedure TAnimrigStateSpinePhysicsRenderer.ApplyPhysics(deltatime: double);
  function inBlendRotate(r1,r2,alpha : single) : single;
  begin
    r2 := r2 - r1;
    r2 := r2 - (16384 - floor(16384.499999999996 - r2 / 360)) * 360;
    r2 := r1 + r2 * alpha;
    r2 := r2 - (16384 - floor(16384.499999999996 - r2 / 360)) * 360;

    result := r2;
  end;
  procedure inApplyBone(g:TPhysicsGroupSpringBone; skeleton:pspSkeleton);
  var
    p : pointer;
    b : TPhysicsNodeBone;
    sb : pspBone;
    alpha : single;
  begin
    alpha := g.Alpha;

    for p in g.NodeList do
    begin
      b := TPhysicsNodeBone(p);
      if b.UserObject <> nil then
      begin
        sb := pspBone(b.UserObject);
        case g.Mode of
          gsmBone :
            begin
              sb.rotation := inBlendRotate(sb.rotation , sb.rotation + (b.aLocalAngle - b.LocalAngle)*360 , alpha);
            end;
          gsmStrBone :
            begin
              sb.rotation := inBlendRotate(sb.rotation , sb.rotation  + (b.aLocalAngle - b.LocalAngle)*360 , alpha);
              sb.x := sb.x + (b.aLocalPosition.x - b.LocalPosition.x) * alpha / AntiScale;
              sb.y := sb.y + (b.aLocalPosition.y - b.LocalPosition.y) * alpha / AntiScale;
            end;
          gsmStrPoint,gsmPoint :
            begin
              sb.x := sb.x + (b.aLocalPosition.x - b.LocalPosition.x) * alpha / AntiScale;
              sb.y := sb.y + (b.aLocalPosition.y - b.LocalPosition.y) * alpha / AntiScale;
            end;
        end;
      end;
    end;
  end;
  procedure inUpdateMatrix(bone : pspBone);
  var
    i : integer;
  begin
    if bone = nil then exit;

    spBone_updateWorldTransform(bone);

    for i:=0 to bone.childrenCount-1 do
    begin
      inUpdateMatrix(pspBone(bone.children[i]));
    end;
  end;
  procedure inSpring(g : TPhysicsGroupSpringBone);
  var
    m : TPhysicsMatrix4;
  begin
    m := ExtractWorldMatrix( pspBone(g.RootUserObject) );
    TAnimrigMisc.m43PushMatrix(m,TransMatrix);
    g.RootMatrix := m;
    g.ExternalMatrix := PhysicsExternalMatrix;

    g.Update(deltatime);
    g.Apply;

    inApplyBone(g,fSkeleton);
    inUpdateMatrix(pspBone(g.RootBone.UserObject));
  end;
  procedure inElement(g : TPhysicsGroupElementBone);
  var
    sb : pspBone;
  begin
    sb := pspBone(g.UserObject);
    if sb = nil then exit;

    g.SpringPosition.Value.SetVector(sb.x, sb.y, 0);
    g.SpringScale.Value.SetVector(sb.scaleX, sb.scaleY, 0);
    g.SpringRotate.Value.SetVector(0, 0, sb.rotation/360.0);   // rotate : need 360deg = 1.0 scale

    g.Update(deltatime);
    g.Apply;

    sb.x := g.SpringPosition.aValue.x;
    sb.y := g.SpringPosition.aValue.y;
    sb.scaleX := g.SpringScale.aValue.x;
    sb.scaleY := g.SpringScale.aValue.y;
    sb.rotation := g.SpringRotate.aValue.z*360.0;
  end;
var
  i : integer;
  g : TPhysicsGroup;
  b : boolean;
begin
  b := false;
  if fSkeleton = nil then exit;

  for i:=0 to fPhysicsGroups.Count-1 do
  begin
    g := TPhysicsGroup(fPhysicsGroups[i]);

    case g.GroupType of
      gtSpring : inSpring(TPhysicsGroupSpringBone(g));
      gtElement :
        begin
          inElement(TPhysicsGroupElementBone(g));
          b := true;
        end;
    end;
  end;
  if b then spSkeleton_updateWorldTransform(fSkeleton);
end;

end.
