moeru-ai/airi · info

[mmd] gaze: no eye bone matched (左目/右目/両目); eye tracking dis

Error message

[mmd] gaze: no eye bone matched (左目/右目/両目); eye tracking disabled. Bones containing 目/eye:

What it means

createGazeController() in packages/stage-ui-mmd/src/composables/mmd/gaze.ts builds bone-based eye tracking by looking up standard MMD eye bones: 左目/eye_L/EyeLeft/LeftEye and 右目/eye_R/EyeRight/RightEye, with 両目/'Eyes' as a single-bone fallback. If none match, it logs every bone whose name contains 目 or eye (to help identify renames) and simply runs with zero eye bones: head-only gaze and idle saccades continue, eye rotation is disabled.

Source

Thrown at packages/stage-ui-mmd/src/composables/mmd/gaze.ts:81

 */
export function createGazeController(mesh: SkinnedMesh): GazeController {
  const eyeBones: GazeBone[] = []
  const left = findBone(mesh, LEFT_EYE_NAMES)
  const right = findBone(mesh, RIGHT_EYE_NAMES)
  if (left)
    eyeBones.push({ bone: left, rest: left.quaternion.clone() })
  if (right)
    eyeBones.push({ bone: right, rest: right.quaternion.clone() })

  // Fallback: a single both-eyes bone the eyeballs are skinned to directly.
  if (eyeBones.length === 0) {
    const both = findBone(mesh, BOTH_EYES_NAMES)
    if (both)
      eyeBones.push({ bone: both, rest: both.quaternion.clone() })
  }

  if (eyeBones.length === 0) {
    console.warn(
      '[mmd] gaze: no eye bone matched (左目/右目/両目); eye tracking disabled. '
      + 'Bones containing 目/eye:',
      mesh.skeleton.bones.filter(b => /目|eye/i.test(b.name)).map(b => b.name),
    )
  }

  const headBoneRaw = findBone(mesh, HEAD_NAMES)
  const headBone: GazeBone | undefined = headBoneRaw
    ? { bone: headBoneRaw, rest: headBoneRaw.quaternion.clone() }
    : undefined

  // Smoothed current aim, lerped toward the target each frame.
  const current: GazeOffset = { x: 0, y: 0 }
  const scratchEuler = new Euler()
  const scratchQuat = new Quaternion()

  // Idle saccade state.
  let saccade: GazeOffset = { x: 0, y: 0 }

View on GitHub (pinned to 677329427f)

Solutions

  1. Read the warn's printed bone list; if an eye bone exists under a different name, rename it to 左目/右目 in a PMX editor (PMXE/Blender+mmd_tools).
  2. Or extend LEFT_EYE_NAMES/RIGHT_EYE_NAMES/BOTH_EYE_NAMES in gaze.ts to cover your model's naming.
  3. If eyes are morph-driven, accept the degradation (head tracking still works) — no code change needed.

Example fix

// in packages/stage-ui-mmd/src/composables/mmd/gaze.ts
// before
const LEFT_EYE_NAMES = ['左目', 'eye_L', 'EyeLeft', 'LeftEye']

// after (cover your model's naming)
const LEFT_EYE_NAMES = ['左目', 'eye_L', 'EyeLeft', 'LeftEye', 'eyeInner_L', 'EYE.L']
Defensive patterns

Strategy: validation

Validate before calling

const hasEyeBones = mesh.skeleton.bones.some(b => ['左目', 'eye_L', 'EyeLeft', 'LeftEye'].includes(b.name))
|| mesh.skeleton.bones.some(b => ['右目', 'eye_R', 'EyeRight', 'RightEye'].includes(b.name))
|| mesh.skeleton.bones.some(b => ['両目', 'Eyes'].includes(b.name))
if (!hasEyeBones) console.info('gaze: head-only tracking for this model')

Type guard

function modelSupportsEyeGaze(mesh: SkinnedMesh): boolean {
  const names = new Set(mesh.skeleton.bones.map(b => b.name))
  return ['左目', 'eye_L', 'EyeLeft', 'LeftEye', '右目', 'eye_R', 'EyeRight', 'RightEye', '両目', 'Eyes'].some(n => names.has(n))
}

Prevention

When it happens

Trigger: Loading a PMX whose eye bones use non-standard names (e.g. 'eyeLeft001', 'EYE.L'), whose eyeballs are skinned directly to the head bone, or whose eyes are driven only by morphs instead of bones.

Common situations: Western-authored or blender-exported MMD-style models that translate standard Japanese bone names; heavily modified models; models rigged for morph-only blinking where eye bones were deleted.

Related errors


AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18). Data as JSON: /api/errors/80d8c88eb205d683. Report an issue: GitHub.