1
0

feat: build articulated crow flight v2

This commit is contained in:
2026-08-19 01:12:08 -07:00
parent 747744fa6c
commit 94de2d8bee
10 changed files with 789 additions and 106 deletions
+412 -71
View File
@@ -1,5 +1,13 @@
/** A low-cost anonymous Tera crow, ready to perch or flap in flight. */
/**
* Procedural American-crow-scale actor with a readable rear flight silhouette.
*
* The rig remains code-only and cheap to clone, but the wing is no longer one
* flat polygon. Shoulder, elbow and wrist joints carry overlapping covert,
* secondary and primary feathers, so the renderer can describe a flap, glide,
* bank, tuck or perch without replacing geometry.
*/
import * as THREE from "three";
import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js";
import {
actorMesh,
disposeActor,
@@ -8,7 +16,25 @@ import {
type ActorRigBase,
} from "./common.ts";
export const CROW_METRICS = { bodyLength: 0.42, perchedHeight: 0.34, wingspan: 0.84 } as const;
export const CROW_METRICS = {
bodyLength: 0.48,
perchedHeight: 0.44,
wingspan: 1.02,
} as const;
export type CrowPoseState = "flap" | "glide" | "bank" | "tuck" | "perch";
export interface CrowPose {
state: CrowPoseState;
/** Cyclic flap phase in radians. */
phase?: number;
/** Pose strength in [0, 1]. */
amount?: number;
/** Signed bank request in [-1, 1]; positive banks right. */
bank?: number;
/** Tucks the feet as this reaches one. */
flight?: number;
}
export interface CrowMaterials {
feather: THREE.Material;
@@ -27,9 +53,18 @@ export interface CrowBuildOptions {
export interface CrowJoints {
body: THREE.Group;
head: THREE.Group;
/** Backwards-compatible aliases for the shoulder joints. */
wingLeft: THREE.Group;
wingRight: THREE.Group;
shoulderLeft: THREE.Group;
shoulderRight: THREE.Group;
elbowLeft: THREE.Group;
elbowRight: THREE.Group;
wristLeft: THREE.Group;
wristRight: THREE.Group;
tail: THREE.Group;
legLeft: THREE.Group;
legRight: THREE.Group;
}
export interface CrowRig extends ActorRigBase {
@@ -38,124 +73,430 @@ export interface CrowRig extends ActorRigBase {
export function createCrowMaterials(options: CrowBuildOptions = {}): CrowMaterials {
return {
// Wing sheets are intentionally thin. Both faces must render because a
// chase camera sees their backs while a flyover camera sees their fronts.
feather: new THREE.MeshStandardMaterial({ name: "crow.feather", color: options.featherColor ?? 0x111519, roughness: 0.62, metalness: 0.12, side: THREE.DoubleSide }),
sheen: new THREE.MeshStandardMaterial({ name: "crow.sheen", color: options.sheenColor ?? 0x1f2c36, roughness: 0.4, metalness: 0.28 }),
beak: new THREE.MeshStandardMaterial({ name: "crow.beak", color: 0x202327, roughness: 0.78 }),
eye: new THREE.MeshBasicMaterial({ name: "crow.eye", color: 0xd4b168, toneMapped: false }),
foot: new THREE.MeshStandardMaterial({ name: "crow.foot", color: 0x24272a, roughness: 0.9 }),
// A blue-violet highlight keeps the bird readable against black terrain
// without painting a naturally black animal grey.
feather: new THREE.MeshStandardMaterial({
name: "crow.feather",
color: options.featherColor ?? 0x111821,
roughness: 0.68,
metalness: 0.08,
emissive: 0x07111d,
emissiveIntensity: 0.24,
side: THREE.DoubleSide,
}),
sheen: new THREE.MeshPhysicalMaterial({
name: "crow.sheen",
color: options.sheenColor ?? 0x253547,
roughness: 0.34,
metalness: 0.22,
clearcoat: 0.28,
clearcoatRoughness: 0.42,
emissive: 0x0b1729,
emissiveIntensity: 0.32,
}),
beak: new THREE.MeshStandardMaterial({
name: "crow.beak",
color: 0x252a30,
roughness: 0.72,
metalness: 0.08,
}),
eye: new THREE.MeshBasicMaterial({ name: "crow.eye", color: 0xd9b86c, toneMapped: false }),
foot: new THREE.MeshStandardMaterial({ name: "crow.foot", color: 0x2b2f35, roughness: 0.9 }),
};
}
function wingGeometry(side: -1 | 1): THREE.BufferGeometry {
const s = side;
/** A small double-sided feather prism, rooted at z=0 and tapered along +Z. */
function featherGeometry(): THREE.BufferGeometry {
const outline: readonly [number, number][] = [
[-0.28, 0],
[-0.5, 0.46],
[-0.42, 0.76],
[-0.18, 0.95],
[0, 1],
[0.18, 0.95],
[0.42, 0.76],
[0.5, 0.46],
[0.28, 0],
];
const halfThickness = 0.012;
const positions: number[] = [];
for (const y of [halfThickness, -halfThickness]) {
for (const [x, z] of outline) positions.push(x, y, z);
}
const count = outline.length;
const indices: number[] = [];
// Top and bottom are triangle fans around the root-side midpoint.
for (let index = 1; index < count - 1; index += 1) {
indices.push(0, index, index + 1);
indices.push(count, count + index + 1, count + index);
}
for (let index = 0; index < count; index += 1) {
const next = (index + 1) % count;
indices.push(index, count + index, next, next, count + index, count + next);
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute(
"position",
new THREE.Float32BufferAttribute([
0, 0, 0.04,
s * 0.32, -0.015, 0.1,
s * 0.4, -0.03, 0.2,
s * 0.18, -0.015, -0.14,
0, 0, -0.16,
], 3),
);
geometry.setIndex([0, 1, 3, 1, 2, 3, 0, 3, 4]);
geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
geometry.setIndex(indices);
geometry.computeVertexNormals();
geometry.name = `crow.wing.${side < 0 ? "left" : "right"}`;
geometry.computeBoundingBox();
geometry.computeBoundingSphere();
geometry.name = "crow.feather-blade";
return geometry;
}
function beakGeometry(): THREE.BufferGeometry {
const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.Float32BufferAttribute([
-0.054, 0.025, 0,
0.054, 0.025, 0,
-0.043, -0.035, 0,
0.043, -0.035, 0,
0, -0.018, -0.19,
], 3));
geometry.setIndex([
0, 1, 4,
1, 3, 4,
3, 2, 4,
2, 0, 4,
0, 2, 3,
0, 3, 1,
]);
geometry.computeVertexNormals();
geometry.name = "crow.beak-wedge";
return geometry;
}
interface FeatherPlacement {
position: readonly [number, number, number];
scale: readonly [number, number, number];
rotation?: readonly [number, number, number];
}
/** Merge one articulated feather layer into one draw call. */
function featherBatch(
name: string,
material: THREE.Material,
placements: readonly FeatherPlacement[],
blade: THREE.BufferGeometry,
): THREE.Mesh {
const parts = placements.map((placement) => {
const matrix = new THREE.Matrix4().compose(
new THREE.Vector3(...placement.position),
new THREE.Quaternion().setFromEuler(new THREE.Euler(...(placement.rotation ?? [0, 0, 0]), "XYZ")),
new THREE.Vector3(...placement.scale),
);
return blade.clone().applyMatrix4(matrix);
});
const geometry = mergeGeometries(parts, false);
for (const part of parts) part.dispose();
if (!geometry) throw new Error(`crow: could not merge feather layer "${name}"`);
geometry.name = `${name}.geometry`;
geometry.computeBoundingBox();
geometry.computeBoundingSphere();
const mesh = actorMesh(name, geometry, material, { receiveShadow: false });
mesh.userData.featherCount = placements.length;
return mesh;
}
function addWing(
body: THREE.Group,
side: -1 | 1,
materials: CrowMaterials,
featherBlade: THREE.BufferGeometry,
): { shoulder: THREE.Group; elbow: THREE.Group; wrist: THREE.Group } {
const word = side < 0 ? "left" : "right";
const shoulder = namedGroup(`crow.wing.${word}`, [side * 0.09, 0.055, -0.035]);
const elbow = namedGroup(`crow.elbow.${word}`, [side * 0.175, 0, 0.012]);
const wrist = namedGroup(`crow.wrist.${word}`, [side * 0.165, -0.004, 0.045]);
shoulder.add(elbow);
elbow.add(wrist);
body.add(shoulder);
// Rounded scapular coverts bridge the torso to the articulated arm.
const coverts: FeatherPlacement[] = [];
for (let index = 0; index < 4; index += 1) {
coverts.push({
position: [side * (0.025 + index * 0.035), 0.018 - index * 0.004, -0.035 + index * 0.018],
scale: [0.09, 0.72, 0.18 + index * 0.012],
rotation: [0.06, side * (-0.05 - index * 0.035), 0],
});
}
shoulder.add(featherBatch(`crow.coverts.${word}`, materials.sheen, coverts, featherBlade));
// The secondaries overlap from elbow to wrist and create the broad inner
// trailing edge visible in the accepted rear-view direction.
const innerSecondaries: FeatherPlacement[] = [];
const outerSecondaries: FeatherPlacement[] = [];
for (let index = 0; index < 7; index += 1) {
const along = index / 6;
(index < 3 ? innerSecondaries : outerSecondaries).push({
position: [side * (0.015 + along * 0.155), -0.006 - along * 0.005, 0.018 + along * 0.014],
scale: [0.095 - along * 0.014, 0.72, 0.26 + along * 0.055],
rotation: [0.02 + along * 0.05, side * (-0.1 - along * 0.16), 0],
});
}
elbow.add(
featherBatch(`crow.secondaries-inner.${word}`, materials.sheen, innerSecondaries, featherBlade),
featherBatch(`crow.secondaries-outer.${word}`, materials.feather, outerSecondaries, featherBlade),
);
// Long, individually separated primaries fan from the wrist. Their small
// angular and length progression gives a clear fingertip silhouette.
const primaries: FeatherPlacement[] = [];
for (let index = 0; index < 9; index += 1) {
const along = index / 8;
primaries.push({
position: [side * (0.01 + along * 0.095), -0.014 - along * 0.003, 0.025 + along * 0.01],
scale: [0.074 - along * 0.014, 0.78, 0.34 - along * 0.075],
rotation: [0.015 + along * 0.035, side * (-0.2 - along * 0.42), side * (along - 0.5) * 0.035],
});
}
wrist.add(featherBatch(`crow.primaries.${word}`, materials.feather, primaries, featherBlade));
return { shoulder, elbow, wrist };
}
function addFoot(body: THREE.Group, side: -1 | 1, material: THREE.Material): THREE.Group {
const word = side < 0 ? "left" : "right";
const leg = namedGroup(`crow.leg.${word}`, [side * 0.052, -0.115, 0.015]);
leg.add(actorMesh(
`crow.shank.${word}`,
new THREE.CylinderGeometry(0.009, 0.007, 0.12, 7),
material,
{ position: [0, -0.052, 0.015], rotation: [0.2, 0, 0] },
));
const foot = namedGroup(`crow.foot.${word}`, [0, -0.108, -0.002]);
leg.add(foot);
for (let index = 0; index < 3; index += 1) {
const angle = (index - 1) * 0.42;
foot.add(actorMesh(
`crow.toe.${word}.${index}`,
new THREE.CylinderGeometry(0.0045, 0.003, 0.075, 6),
material,
{
position: [Math.sin(angle) * 0.025, -0.004, -0.028 - Math.cos(angle) * 0.006],
rotation: [Math.PI / 2, 0, -angle],
},
));
}
foot.add(actorMesh(
`crow.toe.${word}.rear`,
new THREE.CylinderGeometry(0.004, 0.003, 0.055, 6),
material,
{ position: [0, -0.005, 0.022], rotation: [Math.PI / 2, 0, 0] },
));
body.add(leg);
return leg;
}
function resolveCrow(root: THREE.Group, ownsMaterials: boolean): CrowRig {
const wingLeft = requireGroup(root, "crow.wing.left");
const wingRight = requireGroup(root, "crow.wing.right");
return {
root,
ownsMaterials,
joints: {
body: requireGroup(root, "crow.body"),
head: requireGroup(root, "crow.head"),
wingLeft: requireGroup(root, "crow.wing.left"),
wingRight: requireGroup(root, "crow.wing.right"),
wingLeft,
wingRight,
shoulderLeft: wingLeft,
shoulderRight: wingRight,
elbowLeft: requireGroup(root, "crow.elbow.left"),
elbowRight: requireGroup(root, "crow.elbow.right"),
wristLeft: requireGroup(root, "crow.wrist.left"),
wristRight: requireGroup(root, "crow.wrist.right"),
tail: requireGroup(root, "crow.tail"),
legLeft: requireGroup(root, "crow.leg.left"),
legRight: requireGroup(root, "crow.leg.right"),
},
};
}
export function buildCrow(options: CrowBuildOptions = {}): CrowRig {
const m = options.materials ?? createCrowMaterials(options);
const materials = options.materials ?? createCrowMaterials(options);
const featherBlade = featherGeometry();
const root = namedGroup("crow");
root.userData.kind = "actor";
root.userData.actorType = "anonymous-crow";
root.userData.forwardAxis = "-Z";
const body = namedGroup("crow.body", [0, 0.2, 0]);
root.userData.rigVersion = 2;
const body = namedGroup("crow.body", [0, 0.225, 0]);
root.add(body);
body.add(
actorMesh("crow.torso", new THREE.SphereGeometry(0.13, 12, 9), m.feather, {
rotation: [-0.16, 0, 0],
scale: [0.82, 1.08, 1.34],
actorMesh("crow.torso", new THREE.SphereGeometry(0.14, 18, 12), materials.feather, {
rotation: [-0.12, 0, 0],
scale: [0.82, 0.94, 1.58],
}),
actorMesh("crow.breast", new THREE.SphereGeometry(0.105, 10, 8), m.sheen, {
position: [0, 0.012, -0.105],
scale: [0.72, 1, 0.58],
actorMesh("crow.breast", new THREE.SphereGeometry(0.112, 16, 10), materials.sheen, {
position: [0, -0.006, -0.112],
rotation: [-0.18, 0, 0],
scale: [0.76, 1.03, 0.72],
}),
actorMesh("crow.mantle", new THREE.SphereGeometry(0.116, 16, 10), materials.sheen, {
position: [0, 0.057, 0.028],
scale: [0.82, 0.62, 1.12],
}),
actorMesh("crow.nape", new THREE.SphereGeometry(0.096, 16, 10), materials.sheen, {
position: [0, 0.086, -0.088],
rotation: [-0.18, 0, 0],
scale: [0.86, 0.74, 1.2],
}),
);
const head = namedGroup("crow.head", [0, 0.145, -0.105]);
// Small overlapping mantle feathers break up the old featureless pawn back.
const crownMantle: FeatherPlacement[] = [];
const lowerMantle: FeatherPlacement[] = [];
for (let row = 0; row < 3; row += 1) {
for (let column = -1; column <= 1; column += 1) {
(row === 0 ? crownMantle : lowerMantle).push({
position: [column * (0.036 - row * 0.004), 0.105 - row * 0.026, 0.02 + row * 0.045],
scale: [0.07 - row * 0.006, 0.65, 0.1 + row * 0.014],
rotation: [0.28, column * -0.08, 0],
});
}
}
body.add(
featherBatch("crow.mantle-feathers.crown", materials.sheen, crownMantle, featherBlade),
featherBatch("crow.mantle-feathers.lower", materials.feather, lowerMantle, featherBlade),
);
const head = namedGroup("crow.head", [0, 0.108, -0.15]);
body.add(head);
head.add(
actorMesh("crow.skull", new THREE.SphereGeometry(0.093, 12, 9), m.feather, { scale: [0.92, 1, 0.95] }),
actorMesh("crow.beak", new THREE.ConeGeometry(0.055, 0.18, 6), m.beak, {
position: [0, -0.018, -0.145],
rotation: [-Math.PI / 2, 0, 0],
scale: [0.72, 1, 0.68],
actorMesh("crow.skull", new THREE.SphereGeometry(0.098, 18, 12), materials.feather, {
scale: [0.88, 0.84, 1.14],
}),
actorMesh("crow.crown-sheen", new THREE.SphereGeometry(0.09, 16, 10, 0, Math.PI * 2, 0, Math.PI * 0.52), materials.sheen, {
position: [0, 0.012, -0.003],
scale: [0.92, 0.62, 0.96],
}),
actorMesh("crow.beak", beakGeometry(), materials.beak, { position: [0, -0.018, -0.073] }),
);
for (const side of [-1, 1] as const) {
const word = side < 0 ? "left" : "right";
head.add(
actorMesh(`crow.eye.${word}`, new THREE.SphereGeometry(0.011, 7, 5), m.eye, {
position: [side * 0.068, 0.02, -0.06],
actorMesh(`crow.eye-rim.${word}`, new THREE.SphereGeometry(0.016, 8, 6), materials.beak, {
position: [side * 0.068, 0.022, -0.058],
scale: [0.55, 1, 0.82],
}),
actorMesh(`crow.eye.${word}`, new THREE.SphereGeometry(0.008, 8, 6), materials.eye, {
position: [side * 0.076, 0.023, -0.063],
}),
);
const wing = namedGroup(`crow.wing.${word}`, [side * 0.075, 0.035, 0]);
body.add(wing);
wing.add(actorMesh(`crow.wing-mesh.${word}`, wingGeometry(side), m.feather, { receiveShadow: false }));
}
const tail = namedGroup("crow.tail", [0, -0.01, 0.13]);
addWing(body, -1, materials, featherBlade);
addWing(body, 1, materials, featherBlade);
const tail = namedGroup("crow.tail", [0, -0.025, 0.132]);
body.add(tail);
for (const side of [-1, 0, 1] as const) {
tail.add(
actorMesh(`crow.tail-feather.${side}`, new THREE.ConeGeometry(0.045, 0.25, 4), m.feather, {
position: [side * 0.04, -0.015, 0.12],
rotation: [Math.PI / 2, 0, 0],
scale: [0.72, 1, 0.35],
}),
);
const centralTail: FeatherPlacement[] = [];
const outerTail: FeatherPlacement[] = [];
for (let index = 0; index < 9; index += 1) {
const fan = (index - 4) / 4;
(Math.abs(fan) < 0.5 ? centralTail : outerTail).push({
position: [fan * 0.052, -Math.abs(fan) * 0.004, 0],
scale: [0.082, 0.8, 0.285 - Math.abs(fan) * 0.035],
rotation: [0.04, fan * -0.44, fan * -0.045],
});
}
for (const side of [-1, 1] as const) {
body.add(
actorMesh(`crow.foot.${side < 0 ? "left" : "right"}`, new THREE.CylinderGeometry(0.012, 0.009, 0.14, 6), m.foot, {
position: [side * 0.048, -0.14, -0.012],
}),
);
}
return resolveCrow(root, !options.materials);
tail.add(
featherBatch("crow.tail-feathers.central", materials.sheen, centralTail, featherBlade),
featherBatch("crow.tail-feathers.outer", materials.feather, outerTail, featherBlade),
);
addFoot(body, -1, materials.foot);
addFoot(body, 1, materials.foot);
featherBlade.dispose();
const rig = resolveCrow(root, options.materials === undefined);
poseCrow(rig, { state: "perch", amount: 1, flight: 0 });
return rig;
}
export function cloneCrow(source: CrowRig): CrowRig {
return resolveCrow(source.root.clone(true), false);
}
/** Pose a flap. `amount=0` folds the wings; `amount=1` is a broad flight stroke. */
function poseWing(
shoulder: THREE.Group,
elbow: THREE.Group,
wrist: THREE.Group,
side: -1 | 1,
pose: Required<CrowPose>,
): void {
const stroke = Math.sin(pose.phase);
const bankLift = pose.bank * side;
if (pose.state === "perch") {
elbow.position.x = side * 0.035;
wrist.position.x = side * 0.018;
shoulder.rotation.set(0.08, 0, side * 0.06);
elbow.rotation.set(0.08, 0, side * -0.1);
wrist.rotation.set(-0.06, 0, side * -0.06);
return;
}
if (pose.state === "tuck") {
elbow.position.x = side * 0.065;
wrist.position.x = side * 0.035;
shoulder.rotation.set(-0.05, 0, side * 0.16);
elbow.rotation.set(0.08, 0, side * -0.3);
wrist.rotation.set(-0.08, 0, side * -0.2);
return;
}
elbow.position.x = side * 0.175;
wrist.position.x = side * 0.165;
const flap = pose.state === "flap" ? stroke * 0.72 * pose.amount : 0;
const glideDihedral = pose.state === "glide" || pose.state === "bank" ? 0.09 : 0.15;
shoulder.rotation.set(-0.06 - Math.max(0, stroke) * 0.05, side * -0.08, side * (glideDihedral + flap + bankLift * 0.2));
elbow.rotation.set(0.025, side * (-0.1 - Math.max(0, -stroke) * 0.1), side * (-0.05 + flap * 0.22 + bankLift * 0.12));
wrist.rotation.set(-0.025, side * (-0.16 - pose.amount * 0.06), side * (-0.035 + flap * 0.12 + bankLift * 0.08));
}
/** Apply an explicit production pose while preserving the legacy rig contract. */
export function poseCrow(rig: CrowRig, value: CrowPose): void {
const pose: Required<CrowPose> = {
state: value.state,
phase: Number.isFinite(value.phase) ? value.phase ?? 0 : 0,
amount: THREE.MathUtils.clamp(Number.isFinite(value.amount) ? value.amount ?? 1 : 1, 0, 1),
bank: THREE.MathUtils.clamp(Number.isFinite(value.bank) ? value.bank ?? 0 : 0, -1, 1),
flight: THREE.MathUtils.clamp(Number.isFinite(value.flight) ? value.flight ?? 1 : 1, 0, 1),
};
poseWing(rig.joints.shoulderLeft, rig.joints.elbowLeft, rig.joints.wristLeft, -1, pose);
poseWing(rig.joints.shoulderRight, rig.joints.elbowRight, rig.joints.wristRight, 1, pose);
const flapBob = pose.state === "flap" ? Math.cos(pose.phase) * 0.045 * pose.amount : 0;
rig.joints.body.rotation.set(flapBob, 0, -pose.bank * 0.12);
rig.joints.head.rotation.set(-flapBob * 0.6, pose.bank * -0.08, pose.bank * 0.08);
rig.joints.tail.rotation.set(
pose.state === "tuck" ? -0.24 : pose.state === "perch" ? 0.18 : -0.03 - flapBob,
0,
-pose.bank * 0.16,
);
const tailSpread = pose.state === "perch"
? 0.78
: pose.state === "tuck"
? 0.62
: pose.state === "flap"
? 1.2
: 1.48 + Math.abs(pose.bank) * 0.18;
rig.joints.tail.scale.x = tailSpread;
const legTuck = pose.state === "perch" ? 0 : 0.22 + pose.flight * 0.62;
rig.joints.legLeft.rotation.x = legTuck;
rig.joints.legRight.rotation.x = legTuck;
rig.joints.legLeft.rotation.z = pose.state === "perch" ? -0.035 : 0.08;
rig.joints.legRight.rotation.z = pose.state === "perch" ? 0.035 : -0.08;
rig.root.userData.pose = pose.state;
}
/**
* Backwards-compatible flap helper. New adapters should prefer `poseCrow` so a
* glide, bank, tuck and perch is semantic rather than inferred from amplitude.
*/
export function poseCrowFlight(rig: CrowRig, phase: number, amount = 1): void {
const strength = THREE.MathUtils.clamp(amount, 0, 1);
const stroke = Math.sin(phase) * 0.72 * strength;
const spread = 0.2 + strength * 0.74;
rig.joints.wingLeft.rotation.z = spread + stroke;
rig.joints.wingRight.rotation.z = -spread - stroke;
rig.joints.wingLeft.rotation.x = -0.12 * strength;
rig.joints.wingRight.rotation.x = -0.12 * strength;
rig.joints.body.rotation.x = 0.08 * Math.cos(phase) * strength;
rig.joints.tail.rotation.x = -0.12 * Math.cos(phase) * strength;
poseCrow(rig, { state: amount <= 0.25 ? "glide" : "flap", phase, amount, flight: 1 });
}
export function disposeCrow(rig: CrowRig, options: { disposeMaterials?: boolean } = {}): void {
+3
View File
@@ -32,9 +32,12 @@ export {
cloneCrow,
createCrowMaterials,
disposeCrow,
poseCrow,
poseCrowFlight,
type CrowBuildOptions,
type CrowJoints,
type CrowMaterials,
type CrowPose,
type CrowPoseState,
type CrowRig,
} from "./crow.ts";