前兩節,我們初步了解骨骼系統的構造,以及如何透過座標變換來控制骨骼姿勢,本節來做一個更逼真的動作動畫:

6-15c.gif

這是從原來的T字形站姿,轉換到拳擊姿勢,怎麼做的呢?

從上一節的練習,我們知道Y Bot機器人有65個關節,任何一個關節的座標變換若有變動,機器人的姿勢就跟著改變。反過來說,一組關節的座標變換陣列資料,就定義了一個骨骼姿勢。

因此,骨骼模型的每個姿勢,必定反映在完整的關節座標變換陣列(jointTransforms)中。

也就是說,我們可以從 Mixamo 網站找一個拳擊姿勢,將對應的座標變換陣列抓出來,寫入程式,就能將新姿勢套用到原來的基礎模型(“Y Bot.usdz”)了。

驗證想法的完整程式如下:

// 6-15c 轉換姿勢(FromToByAnimation)
// Created by Heman Lu, 2025/09/06
// Minimum Requirement: macOS 15 or iPadOS 18 + Swift Playground 4.6

import SwiftUI
import RealityKit

let 拳擊姿勢: [Transform] =
[
    Transform(
        rotation: simd_quatf(real: 0.946, imag: simd_float3(-0.030, -0.322, -0.011)), 
        translation: simd_float3(-0.537, 92.86, 0.269)), 
    Transform(
        rotation: simd_quatf(real: 0.995, imag: simd_float3(-0.003, 0.096, -0.009)), 
        translation: simd_float3(0.0, 9.923, -1.227)), 
    Transform(
        rotation: simd_quatf(real: 1.0, imag: simd_float3(0.0239, 0.037, 0.002)), 
        translation: simd_float3(0.0, 11.732, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.996, imag: simd_float3(0.081, 0.038, 0.0)), 
        translation: simd_float3(0.0, 13.459, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.997, imag: simd_float3(0.074, -0.005, 0.009)), 
        translation: simd_float3(0.0, 15.028, 0.878)), 
    Transform(
        rotation: simd_quatf(real: 0.996, imag: simd_float3(0.047, 0.049, -0.059)), 
        translation: simd_float3(0.0, 10.322, 3.142)), 
    Transform(
        rotation: simd_quatf(real: 1.0, imag: simd_float3(0.0, 0.0, 0.0)), 
        translation: simd_float3(0.0, 18.475, 6.636)), 
    Transform(
        rotation: simd_quatf(real: -0.425, imag: simd_float3(-0.453, -0.547, 0.561)), 
        translation: simd_float3(6.106, 9.106, 0.757)), 
    Transform(
        rotation: simd_quatf(real: 0.799, imag: simd_float3(0.379, -0.155, 0.440)), 
        translation: simd_float3(0.0, 12.922, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.494, imag: simd_float3(0.0, 0.0, 0.869)), 
        translation: simd_float3(0.0, 27.404, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.964, imag: simd_float3(-0.223, 0.048, 0.134)), 
        translation: simd_float3(0.0, 27.614, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.96, imag: simd_float3(0.238, -0.031, 0.146)), 
        translation: simd_float3(-3.003, 3.789, 2.167)), 
    Transform(
        rotation: simd_quatf(real: 0.940, imag: simd_float3(0.038, -0.158, -0.299)), 
        translation: simd_float3(0.0, 4.745, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.821, imag: simd_float3(-0.163, -0.08, -0.542)), 
        translation: simd_float3(0.0, 4.382, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.99, imag: simd_float3(0.009, 0.126, 0.072)), 
        translation: simd_float3(0.0, 3.459, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.730, imag: simd_float3(0.677, 0.008, -0.090)), 
        translation: simd_float3(-2.822, 12.267, 0.232)), 
    Transform(
        rotation: simd_quatf(real: 0.506, imag: simd_float3(0.856, 0.0, -0.103)), 
        translation: simd_float3(0.0, 3.892, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.587, imag: simd_float3(0.804, 0.0, -0.097)), 
        translation: simd_float3(0.0, 3.415, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 1.0, imag: simd_float3(0.0, 0.004, 0.0)), 
        translation: simd_float3(0.0, 3.078, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.724, imag: simd_float3(0.685, 0.0, -0.083)), 
        translation: simd_float3(0.0, 12.776, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.563, imag: simd_float3(0.820, 0.0, -0.099)), 
        translation: simd_float3(0.0, 3.614, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.576, imag: simd_float3(0.812, 0.0, -0.098)), 
        translation: simd_float3(0.0, 3.46, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 1.0, imag: simd_float3(0.0, 0.005, 0.0)), 
        translation: simd_float3(0.0, 3.680, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.708, imag: simd_float3(0.703, -0.023, -0.062)), 
        translation: simd_float3(2.217, 12.147, -0.01)), 
    Transform(
        rotation: simd_quatf(real: 0.588, imag: simd_float3(0.803, 0.0, -0.097)), 
        translation: simd_float3(0.0, 3.601, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.615, imag: simd_float3(0.783, 0.0, -0.094)), 
        translation: simd_float3(0.0, 3.307, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 1.0, imag: simd_float3(0.0, 0.007, 0.0)), 
        translation: simd_float3(0.0, 3.66, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.680, imag: simd_float3(0.73, -0.04, -0.05)), 
        translation: simd_float3(4.726, 10.908, 0.226)), 
    Transform(
        rotation: simd_quatf(real: 0.646, imag: simd_float3(0.76, 0.027, -0.069)), 
        translation: simd_float3(0.0, 4.14, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.466, imag: simd_float3(0.879, 0.0, -0.106)), 
        translation: simd_float3(0.0, 2.595, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 1.0, imag: simd_float3(0.0, 0.004, 0.0)), 
        translation: simd_float3(0.0, 2.924, 0.0)), 
    Transform(
        rotation: simd_quatf(real: -0.470, imag: simd_float3(-0.470, 0.541, -0.514)), 
        translation: simd_float3(-6.106, 9.106, 0.757)), 
    Transform(
        rotation: simd_quatf(real: 0.761, imag: simd_float3(0.397, 0.188, -0.477)), 
        translation: simd_float3(0.0, 12.922, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.500, imag: simd_float3(0.0, 0.0, -0.866)), 
        translation: simd_float3(0.0, 27.405, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.953, imag: simd_float3(-0.251, 0.0981, -0.140)), 
        translation: simd_float3(0.0, 27.614, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.949, imag: simd_float3(0.277, 0.074, -0.132)), 
        translation: simd_float3(3.003, 3.789, 2.167)), 
    Transform(
        rotation: simd_quatf(real: 0.942, imag: simd_float3(0.014, -0.034, 0.333)), 
        translation: simd_float3(0.0, 4.74, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.860, imag: simd_float3(-0.025, 0.051, 0.506)), 
        translation: simd_float3(0.0, 4.382, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.989, imag: simd_float3(0.009, -0.127, -0.072)), 
        translation: simd_float3(0.0, 3.459, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.730, imag: simd_float3(0.676, -0.018, 0.102)), 
        translation: simd_float3(2.822, 12.267, 0.232)), 
    Transform(
        rotation: simd_quatf(real: 0.506, imag: simd_float3(0.856, 0.0, 0.104)), 
        translation: simd_float3(0.0, 3.89, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.587, imag: simd_float3(0.804, 0.0, 0.0979)), 
        translation: simd_float3(0.0, 3.415, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 1.0, imag: simd_float3(0.0, -0.007, 0.001)), 
        translation: simd_float3(0.0, 3.078, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.724, imag: simd_float3(0.685, 0.0, 0.083)), 
        translation: simd_float3(0.0, 12.776, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.564, imag: simd_float3(0.82, 0.0, 0.1)), 
        translation: simd_float3(0.0, 3.614, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.576, imag: simd_float3(0.812, 0.0, 0.099)), 
        translation: simd_float3(0.0, 3.46, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 1.0, imag: simd_float3(0.0, -0.007, -0.002)), 
        translation: simd_float3(0.0, 3.68, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.693, imag: simd_float3(0.718, 0.033, 0.055)), 
        translation: simd_float3(-2.217, 12.147, -0.01)), 
    Transform(
        rotation: simd_quatf(real: 0.630, imag: simd_float3(0.77, 0.0, 0.0939)), 
        translation: simd_float3(0.0, 3.601, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.615, imag: simd_float3(0.783, 0.0, 0.095)), 
        translation: simd_float3(0.0, 3.307, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 1.0, imag: simd_float3(0.0, -0.008, 0.0)), 
        translation: simd_float3(0.0, 3.66, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.678, imag: simd_float3(0.731, 0.066, 0.027)), 
        translation: simd_float3(-4.726, 10.908, 0.226)), 
    Transform(
        rotation: simd_quatf(real: 0.646, imag: simd_float3(0.758, 0.0, 0.092)), 
        translation: simd_float3(0.0, 4.137, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.466, imag: simd_float3(0.879, 0.0, 0.107)), 
        translation: simd_float3(0.0, 2.596, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 1.0, imag: simd_float3(0.0, -0.008, 0.002)), 
        translation: simd_float3(0.0, 2.924, 0.0)), 
    Transform(
        rotation: simd_quatf(real: -0.081, imag: simd_float3(-0.001, 0.234, 0.969)), 
        translation: simd_float3(9.124, -6.657, -0.055)), 
    Transform(
        rotation: simd_quatf(real: 0.96, imag: simd_float3(-0.28, 0.004, 0.016)), 
        translation: simd_float3(0.0, 40.599, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.846, imag: simd_float3(0.529, -0.038, -0.057)), 
        translation: simd_float3(0.0, 42.099, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.973, imag: simd_float3(0.228, -0.033, -0.015)), 
        translation: simd_float3(0.0, 15.722, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 1.0, imag: simd_float3(0.0, 0.0, 0.0)), 
        translation: simd_float3(0.0, 10.0, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.0728, imag: simd_float3(-0.131, -0.059, 0.987)), 
        translation: simd_float3(-9.125, -6.656, -0.055)), 
    Transform(
        rotation: simd_quatf(real: 0.967, imag: simd_float3(-0.25, 0.039, -0.017)), 
        translation: simd_float3(0.0, 40.599, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.688, imag: simd_float3(0.721, 0.046, 0.063)), 
        translation: simd_float3(0.0, 42.1, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 0.971, imag: simd_float3(0.236, 0.032, 0.015)), 
        translation: simd_float3(0.0, 15.722, 0.0)), 
    Transform(
        rotation: simd_quatf(real: 1.0, imag: simd_float3(0.0, 0.0, 0.0)), 
        translation: simd_float3(0.0, 1.0, 0.0))
]

struct 骨骼動畫: View {
    var body: some View {
        RealityView { 內容 in
            內容.add(座標軸())    // 共享程式6-6b
            
            if let 機器人模型 = try? await ModelEntity(named: "Y Bot.usdz") {
                機器人模型.position = [0, -0.9, 0]
                內容.add(機器人模型)
                print("機器人關節座標變換\\(機器人模型.jointTransforms.count)")
                print(機器人模型.jointTransforms)
                
                let 動畫 = FromToByAnimation(
                    jointNames: 機器人模型.jointNames,
                    to: JointTransforms(拳擊姿勢),
                    duration: 0.5,
                    bindTarget: .jointTransforms,
                    repeatMode: .autoReverse,
                    fillMode: .both,
                    trimStart: -0.5,
                    trimEnd: 1.5
                )
                try? 機器人模型.playAnimation(.generate(with: 動畫))
            }

            if let 天空盒 = try? await EnvironmentResource(named: "威尼斯清晨") {
                內容.environment = .skybox(天空盒)
            }
        } 
        .realityViewCameraControls(.orbit)
    }
}

import PlaygroundSupport 
PlaygroundPage.current.setLiveView(骨骼動畫())

最前面是從別的機器人模型(參考註解一)抓出來的一組65筆關節 Transform 資料,代表一個拳擊姿勢。

先觀察第一筆 Transform 資料,乃對應骨骼系統的根節點(mixamorig_Hips,臀部關節),從位移 translation.y = 92.86 可看出,機器人原始設計的座標單位是公分(而不是公尺),臀部關節離地面(內部座標原點)高 92.86 公分。

此範例執行時的主控台,會列印T字形站姿的座標陣列,其中根節點位移 translation.y = 99.79,兩相比較就可得知,從T字形站姿轉換到拳擊姿勢時,重心往下移約7公分,相當符合真實情境。

第二筆 Transform 資料,對應 “mixamorig_Hips/mixamorig_Spine”,也就是脊椎關節,是臀部關節的子節點,其位移 translation.y = 9.923,表示相對於臀部關節高出9.923公分。其他關節類推。