Skip to content

ずんだもんキャラクター設計

概要

ずんだもんの**顔のみ(バストアップ)**を画面右下に表示し、VOICEVOX の音素情報に基づいて口パクアニメーションを行う。

口パクアニメーション方式

母音ベース画像切り替え方式

日本語の母音(あ・い・う・え・お)に対応する口の形の画像を用意し、VOICEVOX から取得した音素タイミングに合わせて切り替える。

必要な画像素材

ファイル名母音口の形
mouth-a.pngあ (a)大きく開く
mouth-i.pngい (i)横に広げる
mouth-u.pngう (u)すぼめる
mouth-e.pngえ (e)中程度に開く
mouth-o.pngお (o)丸く開く
mouth-closed.png無音閉じる
mouth-n.pngん (N)閉じ気味

画像構成

キャラクター画像はベース(顔)を分離して管理する。

assets/zundamon/
├── face-base.png       # 口を除いた顔のベース画像
├── mouth-a.png         # 口パーツ: あ
├── mouth-i.png         # 口パーツ: い
├── mouth-u.png         # 口パーツ: う
├── mouth-e.png         # 口パーツ: え
├── mouth-o.png         # 口パーツ: お
├── mouth-closed.png    # 口パーツ: 閉じ
└── mouth-n.png         # 口パーツ: ん

コンポーネント設計

ZundamonCharacter.tsx

tsx
import { useCurrentFrame, useVideoConfig, Img, staticFile } from 'remotion';

interface PhonemeEntry {
  time: number;
  duration: number;
  vowel: 'a' | 'i' | 'u' | 'e' | 'o' | 'N' | 'silent';
}

interface Props {
  phonemes: PhonemeEntry[];
}

const MOUTH_MAP: Record<string, string> = {
  a: 'mouth-a.png',
  i: 'mouth-i.png',
  u: 'mouth-u.png',
  e: 'mouth-e.png',
  o: 'mouth-o.png',
  N: 'mouth-n.png',
  silent: 'mouth-closed.png',
};

export const ZundamonCharacter: React.FC<Props> = ({ phonemes }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
  const currentTime = frame / fps;

  // 現在時刻に対応する音素を取得
  const currentPhoneme = phonemes.find(
    p => currentTime >= p.time && currentTime < p.time + p.duration
  );

  const mouthImage = currentPhoneme
    ? MOUTH_MAP[currentPhoneme.vowel]
    : 'mouth-closed.png';

  return (
    <div style={{
      position: 'absolute',
      right: 40,
      bottom: 120,
      width: 200,
      height: 200,
    }}>
      {/* 顔ベース */}
      <Img
        src={staticFile('zundamon/face-base.png')}
        style={{ position: 'absolute', width: '100%', height: '100%' }}
      />
      {/* 口パーツ(アニメーション) */}
      <Img
        src={staticFile(`zundamon/${mouthImage}`)}
        style={{
          position: 'absolute',
          width: '40%',
          left: '30%',
          top: '60%',
        }}
      />
    </div>
  );
};

VOICEVOX 音素マッピング

VOICEVOX の accent_phrases から口パクデータへの変換ロジック:

typescript
function extractPhonemes(audioQuery: AudioQuery): PhonemeEntry[] {
  const phonemes: PhonemeEntry[] = [];
  let currentTime = 0;

  for (const phrase of audioQuery.accent_phrases) {
    for (const mora of phrase.moras) {
      // 子音部分(口は閉じ気味)
      if (mora.consonant_length) {
        phonemes.push({
          time: currentTime,
          duration: mora.consonant_length,
          vowel: 'silent',
        });
        currentTime += mora.consonant_length;
      }

      // 母音部分(口の形を変える)
      phonemes.push({
        time: currentTime,
        duration: mora.vowel_length,
        vowel: mora.vowel.toLowerCase() as PhonemeEntry['vowel'],
      });
      currentTime += mora.vowel_length;
    }

    // ポーズ(無音区間)
    if (phrase.pause_mora) {
      phonemes.push({
        time: currentTime,
        duration: phrase.pause_mora.vowel_length,
        vowel: 'silent',
      });
      currentTime += phrase.pause_mora.vowel_length;
    }
  }

  return phonemes;
}

アニメーション品質向上

補間処理

急激な口の形の変化を避けるため、フレーム間の補間を行う:

  • 口の開閉は 2-3 フレームかけてスムーズに遷移
  • 同じ母音が連続する場合は微細な揺れを追加
  • 無音区間が長い場合はまばたきアニメーションを追加

まばたき

  • 3-5秒に1回ランダムでまばたき
  • まばたきは 3フレーム(閉じ → 半開き → 開き)
  • 目の画像パーツも分離して管理
assets/zundamon/
├── eyes-open.png       # 通常
├── eyes-half.png       # 半開き
└── eyes-closed.png     # まばたき