• RuntimesUnity
  • Position and rotation of sprites in the skeleton

Hello,
Is it possible to get position and rotation of specific sprite (head sprite for example) at current frame in Unity?
I want to create a system that spawns separate sprites (body parts of a character) in a way that these sprites exactly match the current pose of the skeleton and applies physics to them.

  • Harald đã trả lời bài viết này.
    Related Discussions
    ...

    COOKIEMONSTER90210 Is it possible to get position and rotation of specific sprite (head sprite for example) at current frame in Unity?

    While "Sprite" is the wrong terminology here, I assume that you mean Attachment (to be even more precise, a Region Attachment).

    The location of the Attachment is determined by two things: the location of your Bone, plus the relative location of the RegionAttachment in local bone-space.

    You first need to get the position and rotation of the Bone which your desired Slot is attached to (you could also iterate over all Skeleton.Slots). As a quick reference you may want to check out how the BoneFollower component gets a world position for a bone (see the code here). Then you need to access the current Slot.Attachment, which you can then cast to RegionAttachment (with a type check to be sure). RegionAttachment then provides X and Y for relative position, Rotation for relative rotation and other properties like ScaleX, ScaleY, Width and Height.

    Just be aware that width and height of your attachments may have been reduced during whitespace stripping when Strip whitespace was enabled in the Atlas Export Settings during export of your skeleton.

    Depending on what exactly your are trying to achieve, you might also want to have a look at the SkeletonRagdoll example component, which is also demonstrated in its own example scene.

      Harald thank you very much for a quick reply! That’s all I need to know

      • Harald đã thích điều này.

      Glad to hear, thanks for getting back to us!

        5 ngày sau
        • Đã chỉnh sửa

        Harald
        Hi Harald
        I'm proceeding implementing the system described above. I have few more questions as I've run into some troubles. I would really appreciate it if you could help me.
        Due to performance issues I need to implement two versions of the system.
        First version: general approach. I'm given skeleton data (json), atlas and packed body parts (png file). I need to spawn all parts and match them with skeleton.
        Second version: a tricky one. I'm given an animation as a png sequence. I need to spawn all body parts at specific frame of animation and match them with corresponding png image of the sequence.

        I number my questions for convenience.
        -First version:

        1. I can't figure out how to correctly set world rotation of body part prefab. I've tried the code from the BoneFollower, but had no luck with it.
          Here is the code from BoneFollower:
          float halfRotation = Mathf.Atan2(slot.Bone.C, slot.Bone.A) * 0.5f;
          var q = default(Quaternion);
          q.z = Mathf.Sin(halfRotation);
          q.w = Mathf.Cos(halfRotation);
          bodyPart.transform.localRotation = q;

          And here is the one I'm currently using:
          bodyPart.transform.rotation = Quaternion.Euler(new Vector3(0, 0, slot.Bone.WorldRotationX - 90));
        2. I don't understand how should I set a pivot points of the body part sprites so they match pivot points of attachments. I'm using a copy of png pack from Spine to create prefabs of body parts.
        3. I try to get region attachment offset as you suggested above, but its value is always [0,0]. So the position of body parts that attached to the same bone is incorrect. (The hat of the zombie for example)
          Here is the code I'm using to set the position of body part prefabs:
          var regionAttachment = slot.Attachment as RegionAttachment;
          var attachmentOffset = new Vector2(regionAttachment.RegionOffsetX, regionAttachment.RegionOffsetY);
          var bonePosition = new Vector2(slot.Bone.WorldX, slot.Bone.WorldY);
          bodyPart.transform.position = bonePosition + attachmentOffset;

          All body part prefabs are simple game objects with sprite renderer and have no parent or children.

        -Second version. I've decided to stick to the following algorithm: calculate position/rotation for each body part prefab for each png file (frame of animation) -> store it as an asset -> load that asset into RAM in runtime -> when I need to spawn body parts, I just get a desired frame from that asset and spawn body part prefabs at saved positions with saved rotations + offset.

        1. How can I get positions and rotations that match specific png sequence frame? So, for example, I have a sequence of 30 png files. Each png file represents a frame of animation in Spine (I have 30 frames in Spine). Can I simply run the same animation for 30 frames, but with SkeletonAnimation prefab and store positions and rotations for each attachment (exactly as I do in the first version of the system) for each frame. Will those 30 frames of SkeletonAnimation exactly match with each of 30 frames of png sequence?

        Sorry for my English =)

        • Harald đã trả lời bài viết này.

          P.S.
          Looks like png sequence and SkeletonAnimation don't match, so I can't grab data from SkeletonAnimation and store it for matching body parts with frames of png sequence.
          Is it possible to get position/rotation for each attachment for each frame of Spine animation? (May be it's possible to calculate that data from SkeletonData.json somehow?)

          • Harald đã trả lời bài viết này.

            COOKIEMONSTER90210 -First version:
            I can't figure out how to correctly set world rotation of body part prefab. I've tried the code from the BoneFollower, but had no luck with it.
            Here is the code from BoneFollower:

            var q = default(Quaternion);
            q.z = Mathf.Sin(halfRotation);
            q.w = Mathf.Cos(halfRotation);
            bodyPart.transform.localRotation = q;```

            This code is from the if (skeletonTransformIsParent) branch, you should rather use the code from the connected else-branch here, which is similar to your code below.

            And here is the one I'm currently using:
            bodyPart.transform.rotation = Quaternion.Euler(new Vector3(0, 0, slot.Bone.WorldRotationX - 90));

            Did you perhaps forget to include Transform scale? It looks is if your skeleton is flipped horizontally, and this needs . Please see the linked code section above

            COOKIEMONSTER90210 I don't understand how should I set a pivot points of the body part sprites so they match pivot points of attachments. I'm using a copy of png pack from Spine to create prefabs of body parts.

            What do you mean by "a copy of png pack from Spine"? In general the pivot in Unity likely should be at 0.5, 0.5 to set it to the center of the attachment, as Spine uses the center as well. At least Unity displays 0.5, 0.5 for me in the Inspector when selecting a test Sprite.

            COOKIEMONSTER90210 I try to get region attachment offset as you suggested above, but its value is always [0,0].

            What property are you accessing with regionAttachment.RegionOffsetX, is this something you wrote? This property does not exist in the spine-unity runtime. As I mentioned above, you should access RegionAttachment.X and Y for relative position.

            COOKIEMONSTER90210 4. How can I get positions and rotations that match specific png sequence frame?
            [..] Can I simply run the same animation for 30 frames,

            I'm not sure what exactly you mean by "run the animation for 30 frames", but if you want to play it in Unity for 30 frames in Unity (with arbitrary and changing FPS), then no. You should set the AnimationState to a specific time, that's how you can precisely position the animations, then you can apply the AnimationState to the skeleton. Or simpler, if you have only a single Animation that should be matched, apply the Animation at a specific time to the skeleton. Similar to code in SkeletonBaker.cs:

            timeline.Apply(skeleton, lastTime, currentTime, null, 1, MixBlend.Setup, MixDirection.In);
            skeleton.UpdateWorldTransform(Skeleton.Physics.Update);

            Sorry for my English =)

            No need to apologize!

            COOKIEMONSTER90210 Looks like png sequence and SkeletonAnimation don't match, so I can't grab data from SkeletonAnimation and store it for matching body parts with frames of png sequence.

            Then likely you didn't match frame 30 in Spine with the Unity side. E.g. frame 30 at 60FPS in Spine is timepoint 0.5 seconds. If you set the animation to exactly 0.5 seconds in Unity, that should match.

            5 ngày sau

            I've finished this system, so may be the following code would be useful for someone.
            To fix weird rotation of separate body parts I added child game object with sprite renderer and deleted sprite renderer from initial parent game object of body part prefabs. Next I rotated this child object to match rotation of the bone in Spine. I thought that zero rotation of the bone in Spine is when the bone is positioned vertically. But it turned out that zero rotation is when the bone positioned horizontally in Spine. So, in my case some child objects in Unity must be rotated +90 degrees and some -90 to match bone-attachment rotation in Spine.

            Here is the code to spawn separate body parts and place them exactly as skeleton attachments are placed:

                     //Iterate through skeleton slots.
                     foreach (var slot in _skeletonAnimation.skeleton.Slots)
                     { 
                             var bone = slot.Bone;
                             
                             //Load and spawn corresponding body part prefab.
                             var prefab = Resources.Load(slot.Attachment.Name);
                             var bodyPart = Instantiate(prefab) as GameObject;
                             
                             //The position of region attachment is in bone's local coordinate system.
                             //So we must to transform this position into world coordinate system, using bone object. 
                             //Bone class has LocalToWorld method, so we can use it to do this transformation.
                             var regionAttachment = slot.Attachment as RegionAttachment;
                             bone.LocalToWorld(regionAttachment.X, regionAttachment.Y, out float x, out float y);
                             Vector3 attachmentWorldPos = new Vector3(x, y, 0) * _skeletonAnimation.transform.lossyScale.x;
                             Vector3 targetWorldPosition = attachmentWorldPos + _skeletonAnimation.transform.position;
                             
                             //Get target rotation
                             float boneWorldRotation = bone.WorldRotationX;
                             var targetRotation = Quaternion.Euler(0, 0, boneWorldRotation);
            
                             //Apply target position and rotation to spawned body part.
                             bodyPart.transform.SetPositionAndRotation(targetWorldPosition, targetRotation);
                     }
            • Harald đã trả lời bài viết này.

              And this is the method to spawn and place separate body parts so they will exactly match sprite sheet animation. In this case we can't access skeleton to get position and rotation of the bones and attachments. But we can serialize this data beforehand and load it into RAM at runtime. For each sprite sheet animation frame we must serialize frame name and position, rotation and draw order of each attachment in the skeleton. Then we can access this data by the frame name.

              This serialization method executes in Update. It must have reference to the skeleton animation object from the scene hierarchy. It writes the data from the skeleton frame by frame.


              void WriteAnimationData()
              {
                  //currentFrame and totalFrames are fields. 
                  //Current frame = 0. Total frames = amount of sprite sheet animation frames.
                  if (currentFrame <= totalFrames)
                  {
                      //In my case the sprite sheet animation has 30 frames. 
                      //So, every Update we must move the animation to the next frame before we capture the data.
                      //We can't set the animation to specific frame directly. We can only set the time of animation.
                      //The time for each frame can be calculated and set like that: 
                      var stepTime = 1f / 30f;
                      var currentTime = stepTime * currentFrame;
                      var state = _skeletonAnimation.state;
                      var currentTrack = state.GetCurrent(0);
                      currentTrack.AnimationStart = currentTime;
              
                      //For the test purposes I used simple serialization into a text file.
                      //In actual project it's better to use serializers like Protobuf or others.
                      //Each frame data is written as one text line "Frame Index|AttachmentName|X-Position|Y-Position|Rotation..."
                      string frameInfo = $"frame|{currentFrame}";
                      foreach (var slot in _skeletonAnimation.skeleton.Slots)
                      {  
                          var bone = slot.Bone;
                          var regionAttachment = slot.Attachment as RegionAttachment;
                          bone.LocalToWorld(regionAttachment.X, regionAttachment.Y, out float x, out float y);
               
                         frameInfo = String.Concat(frameInfo, $" {slot.Attachment.Name}|{x}|{y}|{bone.WorldRotationX}");
                      }
                      
                      using (TextWriter writer = new StreamWriter(path, true))
                      {
                          writer.WriteLine(frameInfo);
                          writer.Close();
                      }
                       
                      currentFrame++;
                  }
              }       

              Next we have to deserialize that text file in application:

              void ReadAnimationData()
              {
                  int count = 0;
                  foreach (var line in File.ReadLines(path))
                  {
                      //BodyPartSpawnData is a simple struct with attachment name, position and rotation fields.
                      var list = new List<BodyPartSpawnData>();
                      var split = line.Split(" ");
                      for (int i = 0; i < split.Length; i++)
                      {
                          if (i == 0)
                              continue;
                          
                          var attachmentInfoSplit = split[i].Split("|");
                          string attName = attachmentInfoSplit[0];
                         
                          Vector2 pos = new Vector2( float.Parse(attachmentInfoSplit[1]),float.Parse(attachmentInfoSplit[2]));
                          float rot = float.Parse(attachmentInfoSplit[3]);
              
                          BodyPartSpawnData spawnData = new BodyPartSpawnData(attName,pos,rot);
                          list.Add(spawnData);
                      }
                     
                      //spawnData is a dictionary with frame index as a key and BodyPartSpawnData as a value.
                      spawnData.Add(count, list);
                      count++;
                  }
              }

              And when we need to spawn body parts we need to execute this code:

                      //_sequenceRenderer is a reference to the SpriteRenderer of the object with sprite sheet animation.
                      var spriteName = _sequenceRenderer.sprite.name;
                      //In my case the last two characters of the frame name contain its index. 
                      //So I can parse them to int and get current frame index of sprite sheet animation.
                      int sequenceFrame = int.Parse(spriteName.Substring(spriteName.Length - 2));
              
                      //Get array of deserialized frame data by the frame index and iterate it.
                      foreach (var frameData in spawnData[sequenceFrame])
                      {
                          //In this test project I didn't serialize drawOrder, because the skeleton was in the scene. So I could calculate drawOrder from it.
                          //But it must be serialized in real project, if there is no reference to the skeleton.
                          int drawOrder =
                              _skeletonAnimation.Skeleton.DrawOrder.FindIndex(
                                  (slot => slot.Attachment.Name == frameData.bodyPartName));
                      
                          var prefab = Resources.Load(frameData.bodyPartName);
                          //Vector3.zero is the position of the skeleton when the data from it was captured and serialized.
                          Vector3 offset = _sequenceRenderer.transform.position - Vector3.zero;
                          Vector3 position = new Vector3(frameData.position.x, frameData.position.y, drawOrder * -0.1f) + offset;
                          var bodyPart = Instantiate(prefab, position,Quaternion.Euler(0,0,frameData.rotation)) as GameObject;
                      }

              It's pretty cool seeing him explode like that! 😆

              COOKIEMONSTER90210 I've finished this system, so may be the following code would be useful for someone.

              Thanks very much for sharing your code and insights, always much appreciated!
              Your video looks very cool indeed! 🙂

              COOKIEMONSTER90210 I thought that zero rotation of the bone in Spine is when the bone is positioned vertically. But it turned out that zero rotation is when the bone positioned horizontally in Spine. So, in my case some child objects in Unity must be rotated +90 degrees and some -90 to match bone-attachment rotation in Spine.

              Perhaps I misunderstood the setup you're describing here, but having to rotate anything manually by 90 or -90 degrees sounds like some property of the skeleton was not used when determining the Sprite orientation. Did you perhaps miss to add regionAttachment.Rotation on top of Bone.WorldRotationX when determining the attachment Sprite's rotation? Anyway, very glad to hear you've got a working solution!