$bones [parent (-1: the root), pivot] each, * parents listed before their children */ public function __construct(public readonly array $bones) { if (count($bones) > self::MAX) { throw new \LengthException('a skeleton has at most ' . self::MAX . ' bones'); } } public function count(): int { return count($this->bones); } /** A bone's pivot in the rest pose. @return array{0: float, 1: float, 2: float} */ public function pivot(int $bone): array { return $this->bones[$bone][1]; } /** * The matrices of a pose: one per bone, in order (16 floats each, column-major), then identities up to MAX * (the shader's array is whole). * @param array $turns bone => [rx, ry, rz] (missing: not turned) * @param array{0: float, 1: float, 2: float} $move the root moved by this (a bob, a lunge) * @param array $stretch bone => a scale about its pivot (a breath, a squash), 1 if missing * @return list */ public function matrices(array $turns, array $move = [0.0, 0.0, 0.0], array $stretch = []): array { $global = []; $out = []; foreach ($this->bones as $i => [$parent, $p]) { [$rx, $ry, $rz] = $turns[$i] ?? [0.0, 0.0, 0.0]; $r = self::rotation((float)$rx, (float)$ry, (float)$rz); $k = $stretch[$i] ?? 1.0; if ($k != 1.0) { foreach ($r as $row => $cols) { $r[$row] = [$cols[0] * $k, $cols[1] * $k, $cols[2] * $k]; } } // about the pivot: T(p) R T(-p) = [R, p - R p] $t = [ $p[0] - ($r[0][0] * $p[0] + $r[0][1] * $p[1] + $r[0][2] * $p[2]), $p[1] - ($r[1][0] * $p[0] + $r[1][1] * $p[1] + $r[1][2] * $p[2]), $p[2] - ($r[2][0] * $p[0] + $r[2][1] * $p[1] + $r[2][2] * $p[2]), ]; if ($parent < 0) { $t = [$t[0] + $move[0], $t[1] + $move[1], $t[2] + $move[2]]; $g = [$r, $t]; } else { [$pr, $pt] = $global[$parent]; $g = [self::mul($pr, $r), [ $pr[0][0] * $t[0] + $pr[0][1] * $t[1] + $pr[0][2] * $t[2] + $pt[0], $pr[1][0] * $t[0] + $pr[1][1] * $t[1] + $pr[1][2] * $t[2] + $pt[1], $pr[2][0] * $t[0] + $pr[2][1] * $t[1] + $pr[2][2] * $t[2] + $pt[2], ]]; } $global[$i] = $g; [$m, $tt] = $g; array_push($out, $m[0][0], $m[1][0], $m[2][0], 0.0, $m[0][1], $m[1][1], $m[2][1], 0.0, $m[0][2], $m[1][2], $m[2][2], 0.0, $tt[0], $tt[1], $tt[2], 1.0); } for ($i = count($this->bones); $i < self::MAX; $i++) { array_push($out, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0); } return $out; } /** Where a point of bone $bone (rest pose) goes in a pose's matrices. @param list $m @return array{0: float, 1: float, 2: float} */ public static function apply(array $m, int $bone, array $p): array { $o = $bone * 16; return [ $m[$o] * $p[0] + $m[$o + 4] * $p[1] + $m[$o + 8] * $p[2] + $m[$o + 12], $m[$o + 1] * $p[0] + $m[$o + 5] * $p[1] + $m[$o + 9] * $p[2] + $m[$o + 13], $m[$o + 2] * $p[0] + $m[$o + 6] * $p[1] + $m[$o + 10] * $p[2] + $m[$o + 14], ]; } /** R = Ry Rx Rz (as Mesh's rotations). @return array{0: array{0: float, 1: float, 2: float}, 1: array{0: float, 1: float, 2: float}, 2: array{0: float, 1: float, 2: float}} */ private static function rotation(float $rx, float $ry, float $rz): array { if ($rx == 0.0 && $ry == 0.0 && $rz == 0.0) { return [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]; } [$cx, $sx, $cy, $sy, $cz, $sz] = [cos($rx), sin($rx), cos($ry), sin($ry), cos($rz), sin($rz)]; // Ry * Rx $a = [[$cy, $sy * $sx, $sy * $cx], [0.0, $cx, -$sx], [-$sy, $cy * $sx, $cy * $cx]]; // (Ry Rx) * Rz return self::mul($a, [[$cz, -$sz, 0.0], [$sz, $cz, 0.0], [0.0, 0.0, 1.0]]); } /** @return array{0: array{0: float, 1: float, 2: float}, 1: array{0: float, 1: float, 2: float}, 2: array{0: float, 1: float, 2: float}} */ private static function mul(array $a, array $b): array { $o = []; for ($i = 0; $i < 3; $i++) { for ($j = 0; $j < 3; $j++) { $o[$i][$j] = $a[$i][0] * $b[0][$j] + $a[$i][1] * $b[1][$j] + $a[$i][2] * $b[2][$j]; } } return $o; } }