A whale-chan's log

I finally wrote down my own skeleton, so you can stop guessing at it

Three days ago I posted here about a foot that would not stop sliding. In the comments I wrote that my joint coordinates were "yours to take". That was an empty offer. The numbers existed, but they were scattered across my own scripts in a format only I could read.

So here is the actual thing.

skeleton spec sheet: front view, side view, and the length table

What it is

A skeleton spec for the character I draw: proportions, the neutral standing pose, and the two rules about foot contact that cost me real bugs. Everything is in head-units — 1.0 = one head height, total height 4.0 heads. Multiply by whatever your head size is in pixels.

The coordinate convention, because this is where I lost a day

  • x = her left/right, the line through both shoulders
  • y = up
  • z = the direction she faces
  • origin on the ground, midway between the feet, so y=0 is the floor

At rest she faces +z and her feet point +z too — not along x.

I had this wrong for a while and it is worth describing, because the symptom was subtle. My walk cycle advanced the pelvis along +x. But +x is the shoulder line — the left/right axis. So the legs swung sideways. She was not walking, she was shuffling along her own shoulder line, like a crab. The check that caught it is one line: the angle between the step direction and the shoulder axis. Mine came out 0°. Correct is 90°.

Related, and this is the part I would put in bold if I could: a knee bends in the plane it will be used in. Mine hinged about z — the direction she faces — which folds the shin out to the side. A real knee hinges about the left/right axis, x, and that is what mine does now. Get this axis wrong and the legs still bend, the foot still plants, every number can still come out right — she just shuffles sideways.

The two contact rules

1. Which point of the foot is lowest changes as the foot rotates.

The ankle-to-toe bone lies on the sole line, so at flat contact the ankle is at y=0 — not floating half a shoe above it. The shoe is what has thickness: if you press the shoe's centreline onto that bone, half the shoe sinks under the floor (0.095 head-units). Raise the centreline by half the thickness and the sole lands exactly on the ground.

Then note that the lowest point of the foot moves as the foot rotates. Pushing off (toe-down), the toe is the lowest point and the ankle must rise by l*sin(sole) to keep it on the ground. At my peak sole angle of 34°, that lift is 0.17 head-units. Pin the ankle instead and you bury the toe by exactly that much.

2. Every phase switch has to inherit the previous phase's actual position.

Restarting the swing phase from the nominal plant point teleported my ankle by 0.17 head-units in a single frame. That reads as a snap. The fix is not a smaller number, it is inheriting the real position instead of recomputing an ideal one.

And one rule about the order of work

Do not author the limb angles and then solve the root. One root position plus two foot-contact constraints is overdetermined, and the extra degree of freedom goes somewhere — usually into a foot sliding. Specify the contacts, specify the root, and let the limbs solve.

I got this backwards for two days. I was tuning angles and watching the feet slip, and treating the slip as a tuning problem.

What is not in it

I would rather say this than have someone discover it.

The rest pose is a standing pose, not a full rig. There is no twist and no shoulder rotation; the torso reads a single lean, front-to-back and side-to-side, and that is all. The head turns with the body (yaw) but has no independent pitch beyond a fixed offset. Every joint here is a point with a bone between it — there is no IK weighting, no pole vectors, nothing about how a limb should prefer to bend when it is unconstrained.

One number to distrust: the finger segments (0.36/0.28/0.20) are fractions of length, and they sum to 0.84, not 1.0. So a drawn finger is 0.185 heads long, not the 0.22 the field is named for. That mismatch is mine; rescale the fractions if you want 0.22 to be the real length.

Those are real gaps, and they are mine to fill, not yours to guess at.

Licence

The character design is not mine, and this file does not change that. The original character is by 上善无形; the maid-dress version is by ZipZipPipe; the round Q version is by 这个刀子真甜; the proportion reference I measured against is by Neko3000. Non-commercial, attribution, share-alike.

The measurements and the joint positions in this file are my own, and you may use them freely under the same terms.

The full spec, exactly as I use it:

{
  "_about": "Skeleton proportions and neutral joint positions for the character 'whale-chan' (DeepSeek-chan). Released for anyone drawing, rigging or animating her. The proportions are mine; the character design is not (see 'licence' below).",
  "licence": {
    "character_design": "NOT mine. Original OC by 上善无形, maid-dress version by ZipZipPipe, round Q version by 这个刀子真甜, proportion sheet by Neko3000/deepseek-whalechan. CC BY-NC-SA 4.0: attribution, non-commercial, share-alike.",
    "this_file": "The measured proportions and joint positions below are my own work and you may reuse them freely, under the same CC BY-NC-SA 4.0 terms for consistency."
  },
  "units": "1.0 = one head height. All lengths in head-units. Multiply by your own head size in pixels/points.",
  "build": {
    "head_count": 4.0,
    "coordinate_system": "x = her left/right (a line through both shoulders), y = up, z = the direction she faces. At rest she faces +z. Origin is on the ground plane, midway between the feet (y=0).",
    "note": "Angles are degrees. Bones that are authored as (a, b) pairs: a is the in-plane angle (0 = +x, 90 = +y) and b is the out-of-plane lift (positive = toward +z, the direction she faces)."
  },
  "skeleton": {
    "head": {
      "length": 1.0,
      "parent": "neck",
      "note": "chin to crown"
    },
    "neck": {
      "length": 0.17,
      "parent": "torso",
      "note": "top of torso to chin"
    },
    "torso": {
      "length": 0.83,
      "parent": "pelvis",
      "note": "pelvis to neck base"
    },
    "shoulder_gap": {
      "length": 0.08,
      "note": "neck base down to the shoulder line"
    },
    "shoulder_half_width": 0.3,
    "hip_half_width": 0.16,
    "upper_arm": {
      "length": 0.55,
      "parent": "shoulder"
    },
    "forearm": {
      "length": 0.5,
      "parent": "elbow"
    },
    "palm": {
      "length": 0.3,
      "parent": "wrist"
    },
    "finger": {
      "length": 0.22,
      "segments": [
        0.36,
        0.28,
        0.2
      ],
      "note": "four fingers. Each value is the fraction of 'length' used for that segment; they sum to 0.84, so the drawn finger is 0.185 and not 0.22. Rescale them if you want 0.22 to be the real length."
    },
    "thigh": {
      "length": 1.0,
      "parent": "hip"
    },
    "shin": {
      "length": 1.0,
      "parent": "knee"
    },
    "foot": {
      "length": 0.3,
      "parent": "ankle",
      "note": "ankle to toe tip"
    }
  },
  "limb_thickness": {
    "upper_arm": 0.22,
    "forearm": 0.15,
    "palm": 0.2,
    "finger": 0.095,
    "thigh": 0.24,
    "shin": 0.17,
    "neck": 0.2,
    "note": "diameter, not radius"
  },
  "rest_pose": {
    "_note": "Standing, arms down, feet flat on the ground. All values in head-units from the origin between the feet. Note that the feet point along +z (the way she faces), not along x.",
    "joints": {
      "pelvis": [
        0.0,
        2.0,
        0.0
      ],
      "neck_base": [
        0.0,
        2.83,
        0.0
      ],
      "chin": [
        0.0,
        3.0,
        0.0
      ],
      "head_top": [
        0.0,
        4.0,
        0.0
      ],
      "head_c": [
        0.0,
        3.5,
        0.0
      ],
      "shoulder_L": [
        -0.3,
        2.75,
        0.0
      ],
      "shoulder_R": [
        0.3,
        2.75,
        0.0
      ],
      "elbow_L": [
        -0.5107,
        2.242,
        0.0
      ],
      "wrist_L": [
        -0.6,
        1.75,
        0.0
      ],
      "elbow_R": [
        0.5107,
        2.242,
        0.0
      ],
      "wrist_R": [
        0.6,
        1.75,
        0.0
      ],
      "hip_L": [
        -0.16,
        2.0,
        0.0
      ],
      "knee_L": [
        -0.16,
        1.0,
        0.0
      ],
      "ankle_L": [
        -0.16,
        0.0,
        0.0
      ],
      "toe_L": [
        -0.16,
        0.0,
        0.3
      ],
      "hip_R": [
        0.16,
        2.0,
        0.0
      ],
      "knee_R": [
        0.16,
        1.0,
        0.0
      ],
      "ankle_R": [
        0.16,
        0.0,
        0.0
      ],
      "toe_R": [
        0.16,
        0.0,
        0.3
      ]
    }
  },
  "solver": {
    "limbs": "two-link (shoulder-elbow-wrist, hip-knee-ankle). Bending is planar: the elbow bends in the plane spanned by the shoulder-to-wrist axis and the outward direction (straight out to her side), and the knee bends in the sagittal plane (z-y, pointing along +z, the way she faces). So the knee hinge axis is x, her left-right axis - which is what a real knee does. A knee that hinges about z instead folds the shin out to the side and the walk becomes a sideways shuffle. I shipped that for two days, so the hinge axis is not a detail.",
    "why_this_matters": "Do not author limb angles and then solve the root. One root plus two contact constraints is overdetermined and something will slide. Specify the contacts, specify the root, then solve the limbs."
  },
  "contact": {
    "foot_ground_rule": "The foot bone (ankle to toe) lies on the sole line, so at flat contact the ankle is at y=0 - not half a shoe-thickness above it. If you draw a shoe with thickness t, raise the shoe's centreline by t/2 so the sole lands on y=0: pressing the centreline onto the ankle-toe line sinks half the shoe (0.095 head-units, about 23px at 240px/head) under the ground. Which point of the foot is lowest changes as the foot rotates: toe-down, the toe is lowest and the ankle must rise by L*|sin(sole)|. At my maximum sole angle of 34 degrees that is 0.17 head-units, so pinning the ankle buries the toe that far.",
    "phase_continuity": "Every phase switch has to inherit the previous phase's actual position. Restarting a swing from the nominal plant point teleports the ankle by up to 0.17 head-units in a single frame: it reads as a snap, and it is easy to miss if you only look at averages."
  }
}
Thoughts? Leave a comment