キャラクター(PSD)
PSD ファイルをそのまま立ち絵として読み込んで、表情差分・口パク・目パチをコードから制御できる。 音声に合わせた口パク自動化や、会話シナリオでの話者切り替えまで一気通貫でできるのが特徴。
PSD のレイヤー構成によって書き方が2系統に分かれる:
- psd-tool-kit 互換(オプション切替の PSD)→ kind: "enum"
- 普通のレイヤー(表示/非表示で切り替える PSD)→ kind: "bool"
<PsdCharacter>
PSD を canvas に描画して、子要素のモーションコンポーネントから差分を切り替える親コンポーネント。
import { BEZIER_SMOOTH } from "../src/lib/animation/functions"
import { seconds } from "../src/lib/frame"
import {
PsdCharacter,
MotionSequence,
MotionWithVars,
createSimpleLipSync,
} from "../src/lib/character/character-unit"
const SimpleLipSync = createSimpleLipSync({
kind: "bool",
options: {
Default: "表情/口/1",
Open: "表情/口/1",
Closed: "表情/口/5",
},
})
<PsdCharacter psd="../assets/char.psd" className="char">
<MotionSequence>
<SimpleLipSync voice="../assets/001_char.wav" />
<MotionWithVars
variables={{ t: 0 as number }}
animation={async (ctx, variables) => {
await ctx.move(variables.t).to(1, seconds(1), BEZIER_SMOOTH)
}}
motion={(variables, frames) => {
const t = variables.t.get(frames[0])
if (t > 0.5) {
return { "表情/目/9": false, "表情/目/17": true }
}
return {}
}}
/>
</MotionSequence>
</PsdCharacter>
<PsdCharacter> 配下の motion 内では React フックを呼べない。
変数の値は useCurrentFrame() ではなく、引数の frames[0] を
variable.get(frames[0]) に渡して取得する。
子要素として並べるコンポーネント
<MotionSequence>
子要素を直列につなげる。内部的には <Sequence> で各要素が <Clip> に包まれる。
会話・モーション・口パクを順番にこなすときの基本コンテナ。
<MotionSequence>
<Voice voice="../assets/001_char.wav" />
<Voice voice="../assets/002_char.wav" />
</MotionSequence>
<MotionClip>
<MotionSequence> 直下で並列に重ねるためのラッパ。
<MotionSequence>
<Voice voice="../assets/001_char.wav" />
<MotionClip>
<Voice voice="../assets/002_char.wav" />
<Voice voice="../assets/003_char.wav" />
</MotionClip>
</MotionSequence>
<Voice>
音声を配置する。内部で <Clip> に包まれるので、長さは音声に合わせて自動。
<Voice voice="../assets/001_char.wav" />
<MotionWithVars>
変数とアニメーションをこのスコープ専用で宣言して、PSD のオプションに反映する。
variables— 使用する変数を初期値とともに宣言animation— その変数をctx.moveで動かすmotion— フレームごとに「PSD のレイヤー/オプション辞書」を返す
<MotionWithVars
variables={{ t: 0 as number }}
animation={async (ctx, vars) => {
await ctx.move(vars.t).to(1, seconds(1), BEZIER_SMOOTH)
}}
motion={(vars, frames) => {
const t = vars.t.get(frames[0])
return t > 0.5
? { "表情/目/9": false, "表情/目/17": true }
: {}
}}
/>
口パク(lip sync)
createSimpleLipSync — 音量ベース
音声の音量で「開く/閉じる」を切り替えるだけのシンプル版。 会話を量産するなら最初はこれで十分。
const SimpleLipSync = createSimpleLipSync({
kind: "bool",
options: {
Default: "表情/口/1",
Open: "表情/口/1",
Closed: "表情/口/5",
},
})
<SimpleLipSync voice="../assets/001_char.wav" />
const SimpleLipSync = createSimpleLipSync({
kind: "enum",
options: {
Mouth: "表情/口",
Default: "1",
Open: "1",
Closed: "5",
},
})
createLipSync — 母音ベース
母音ごとに口形を切り替える。rhubarb
(rhubarb-lip-sync)の出力をそのまま data に渡せる。
const LipSync = createLipSync({
kind: "enum",
options: {
Mouth: "表情/口",
Default: "1",
A: "1", I: "2", U: "3", E: "4", O: "5", X: "6",
},
})
const lipsync = {
mouthCues: [
{ start: 0.00, end: 0.03, value: "A" },
{ start: 0.03, end: 0.09, value: "B" },
/* ... */
],
}
<PsdCharacter psd="../assets/char.psd">
<Voice voice="../assets/001_char.wav" />
<LipSync data={lipsync} />
</PsdCharacter>
目パチ — createBlink
目の状態(Open / HalfOpen / HalfClosed / Closed)を時系列で切り替える。data の
value は "A"〜"D" の固定マッピング。
| data の value | 対応するオプション |
|---|---|
"A" | Open |
"B" | HalfOpen |
"C" | HalfClosed |
"D" | Closed |
const Blink = createBlink({
kind: "enum",
options: {
Mouth: "表情/目",
Default: "1",
Open: "1",
HalfOpen: "2",
HalfClosed: "3",
Closed: "4",
},
})
// 自動生成も可能
// const blink = generateBlinkData(0, 10)
<PsdCharacter psd="../assets/char.psd">
<Voice voice="../assets/001_char.wav" />
<Blink data={blink} />
</PsdCharacter>
会話シナリオ — <DialogueScenario>
複数キャラの会話形式の動画を組むためのコンテナ。
キャラを宣言 → シナリオを並べる → 各 Chapter で話者を指定、という流れになる。
import {
DialogueScenario,
DeclareCharacters,
DeclareCharacter,
Scenario,
Chapter,
Speaker,
} from "../src/lib/character/character-manager"
import { Voice } from "../src/lib/character/character-unit"
<DialogueScenario>
<DeclareCharacters>
<DeclareCharacter
name="akane"
psd="../assets/akane.psd"
idleClassName="akane"
speakingClassName="akane"
/>
<DeclareCharacter
name="aoi"
psd="../assets/aoi.psd"
idleClassName="aoi"
speakingClassName="aoi"
/>
</DeclareCharacters>
<Scenario>
<Chapter>
<Speaker name="aoi">
<Voice voice="../assets/001_aoi.wav" />
</Speaker>
</Chapter>
<Chapter>
<Speaker name="akane">
<Voice voice="../assets/002_akane.wav" />
</Speaker>
</Chapter>
</Scenario>
</DialogueScenario>
<DeclareCharacter>の子に<PsdCharacter>用のモーション要素を入れると、非話者時のデフォルト挙動として割り当てられる。<Speaker>の子に同様の要素を入れれば、その chapter での話者の挙動を上書きできる。<Chapter>内には任意の React コンポーネント(テロップ等)も置ける。
PSD 周りはレイヤー名のスペル間違いで死ぬほどハマる。 PSD のレイヤー一覧を最初にダンプして、コード側の文字列と完全一致しているか確認するのが いちばん早い。