前兩節,我們初步了解骨骼系統的構造,以及如何透過座標變換來控制骨骼姿勢,本節來做一個更逼真的動作動畫:
這是從原來的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公分。其他關節類推。