いものやま。

雑多な知識の寄せ集め

Javaで動画作成ツールを作った話。(その1)

自分はニコニコ動画にいくつかボードゲーム動画を上げてるんだけど、動画を作るときに使っている自作ツールの話を少ししようかなと。

ちなみに、ツールの名前はMSC
Movie-create-tool by Script and Commandの頭文字をとってMSCーーというのは建前で、実際には大好きなGRANRODEOのModern Strange Cowboyからw

特徴

さて、このMSCがどんなツールなのかというと、以下のとおり:

  • Javaで書かれたコンソールアプリ
  • 独自仕様のスクリプトを書いて読み込ませることで、動画ファイル(Quick Time Movie形式)を生成する
    • ノード(画像、文字、プレイスホルダー)の表示をサポート
    • ノードのアクション(レイヤーへの追加・削除、前面・背面への移動、移動、フェード、文字列のセット、待ち)をサポート
    • ノードのグループ化をサポート
    • アクションの直列化、並列化をサポート
    • 複数レイヤーをサポート
    • 音声は未サポート
  • プレビュー機能あり
  • フレームワークはJMF(Java Media Framework)を使用

スクリプトを書いて動画を作るので、作業は基本的にテキストを打ち込むだけ。
あとは、プレビューで表示を確認したり、出来上がったらファイルに書き出したりすればいい。

この方法のいいところは、テキストエディタさえあればどこでも作業が出来るというところ。
それと、ボードゲーム動画のように、細かい要素を細かく動かす必要がある場合、プログラムを書くように動きをつけることが出来るので、比較的簡単に作ることが出来る。
(ただ、サブルーチンとかはサポートしていないので、そこまで使い勝手はよくない・・・DSLとして使えるようにするのが一番いいのだけど)

ちなみに、音声とQuick Time Movie形式以外の出力形式をサポートしていないので、実際には出力された動画をiMovieに読み込ませて、BGMやSEをつけたあと、MPEG4形式(コーデックはH.264)にエンコードして、アップをしている。

iMovieを使ったニコニコ動画向けのエンコードについては、自分の作った以下の動画を参照:

実例

例えば、自分がボードゲーム動画として最初にアップした動画が、以下:

この動画は、以下のようなスクリプトを読み込ませて作っている:

まず、全体的な設定をする、config.txt:

# コンフィギュレーション

> movie()
width: 800;
height: 600;
frame_rate: 25.0;
temp_dir: tmp;
<

> font(丸ゴ)
name: Hiragino Maru Gothic Pro;
size: 24;
color: ffffff;
<

> font(丸ゴ黒)
name: Hiragino Maru Gothic Pro;
size: 24;
color: 000000;
<

これで、以下を設定したことになる:

  • 動画の設定
    • 動画のサイズは800x600
    • フレームレートは25.0
    • 一時出力ディレクトリはtmp
  • フォントの設定
    • 「丸ゴ」というフォントとして、ヒラギノ丸ゴシックPro、サイズ24pt、色は白を使用する
    • 「丸ゴ黒」というフォントとして、ヒラギノ丸ゴシックPro、サイズ24pt、黒は黒を使用する

次に、動画に出現させるアイテムを定義する、item.txt:

# アイテム

#----------------------------------------
# レイヤー
#----------------------------------------
> layer(背景レイヤー) <
> layer(人物レイヤー) <
> layer(ボードレイヤー) <
> layer(移動レイヤー) <
> layer(効果レイヤー) <
> layer(会話背景レイヤー) <
> layer(会話レイヤー) <

#----------------------------------------
# 画像
#----------------------------------------

#------------------------------
# 一般
#------------------------------
> image(背景)
file: 画像/穂波部屋.png;
x: 0; y: 0; visible: true;
<

〜省略〜

> draw(黒塗り)
type: rect;
x: 0; y: 0; visible: true;
width: 800; height: 600;
color: 000000;
<

〜省略〜

#------------------------------
# 矢印
# x方向の間隔は53 / y方向の間隔は43
#------------------------------
> image(上矢印)
file: 画像/上矢印.png;
x: 582; y: 419; visible: false; <

〜省略〜

#------------------------------
# コマ
# x方向の間隔は53 / y方向の間隔は43
# 移動するときは、-5x-5で浮かせる
#------------------------------
> image(o1)
file: 画像/コマ/o1.png;
x: 582; y: 32; visible: false; <

〜省略〜

> group(コマ)
item: o1, o2, o3, o4, o5, b1, b2, b3, b4, b5; <

> place(カウント)
font: 丸ゴ黒;
x: 541; y: 18; visible: true;
<

#------------------------------
# サイコロ
#------------------------------
> image(dl_1)
file: 画像/サイコロ/サイコロ1.png;
x: 372; y: 314; visible: true; <

〜省略〜

> group(dice)
item: dl_1, dl_2, dl_3, dl_4, dl_5, dl_6, 
    dr_2, dr_3, dr_4, dr_5, dr_6, dr_1; <

〜省略〜

#------------------------------
# 穂波アップ1
# x: 172; y: 0; / x: 12; y:0;
#------------------------------
> image(hu01)    # 真顔
file: 画像/穂波アップ/HOBDA01.png;
x: 172; y: 0; visible: false; <

> image(hu02)    # 微笑み
file: 画像/穂波アップ/HOBDA02.png;
x: 172; y: 0; visible: false; <

> image(hu03)    # 柔らかい笑み
file: 画像/穂波アップ/HOBDA04.png;
x: 172; y: 0; visible: false; <

〜省略〜

#----------------------------------------
# テキスト、プレースホルダー
#----------------------------------------
> text(会話穂波)
text: 穂波;
font: 丸ゴ;
x: 100; y: 426; visible: true;
<

> text(会話拓也)
text: 拓也;
font: 丸ゴ;
x: 100; y: 426; visible: true;
<

> text(会話ゆのは)
text: ゆのは;
font: 丸ゴ;
x: 95; y: 426; visible: true;
<

> text(会話前回)
text: 〜前回のおさらい〜;
font: 丸ゴ;
x: 30; y: 426; visible: false;
<

# 30文字、3段までOK
> place(会話)
font: 丸ゴ;
x: 10; y: 494; visible: true;
duration: 0.04;
<

そして最後に、実際のシナリオと動作を記述する、scenario.txt:

# シナリオ

> scene(アイテム追加)
背景レイヤー: > add() <;
人物レイヤー: > add() <;
ボードレイヤー: > add() <;
移動レイヤー: > add() <;
効果レイヤー: > add() <;
会話背景レイヤー: > add() <;
会話レイヤー: > add() <;

背景: > add() layer: 背景レイヤー; <;
黒塗り: > add() layer: 効果レイヤー; <;
会話バー:
    > add() layer: 会話背景レイヤー; < ,
    > fade() type: in; sec: 0.5; < ;
名前バー:
    > add() layer: 会話背景レイヤー; < ,
    > fade() type: in; sec: 0.5; <;
会話:
    > add() layer: 会話レイヤー; <;
<

> scene(intro1)
before: アイテム追加;
会話穂波:
    > add() layer: 会話レイヤー; <;
会話:
    > set()
        text: 「……拓也くん。;
        text:  …………拓也くん。」;
    <,
    > wait() sec: 0.5; <,
    > act() sec: 0.32; <,
    > wait() sec: 2; <,
    > act() <,
    > wait() sec: 2; <;
<

> scene(intro2)
before: intro1;
会話穂波: > remove() <;
会話拓也: > add() layer: 会話レイヤー; <;
会話:
    > set()
        text:  穂波のやわらかい声が聞こえてくる。;
        text:  昨日はたしか、雪かきのバイトでヘトヘトになって;
        text:  わかばちゃんの家に帰って寝たはず。;
    <,
    > wait() sec: 0.5; <,
    > act() sec: 0.76; <,
    > wait() sec: 2; <,
    > act() <,
    > wait() sec: 4; <,
    > set()
        text:  夢でも見てるのかな?;
    <,
    > act() <,
    > wait() sec: 4; <;
<

> scene(intro3)
before: intro2;
会話穂波: > add() layer: 会話レイヤー; <;
会話拓也: > remove() <;
会話:
    > set()
        text: 「……拓也くん?;
        text:  ………………起きないといたずらしちゃいますよ?」;
    <,
    > wait() sec: 0.5; <,
    > act() sec: 0.32; <,
    > wait() sec: 2; <,
    > act() <,
    > wait() sec: 4; <;
<

> scene(intro4)
before: intro3;
名前バー: > remove() <;
会話穂波: > remove() <;
会話:
    > set()
        text:  ガバッ!;
        text:  慌てて飛び起きる。;
    <,
    > wait() sec: 0.5; <,
    > act() sec: 0.2; <,
    > wait() sec: 1; <,
    > act() <,
    > wait() sec: 2; <;
<

> scene(intro5)
before: intro4;
hb02: > add() layer: 背景レイヤー; <;
会話バー: > remove() <;
会話: > remove() <;
黒塗り:
    > fade() type: out; sec: 1; <,
    > wait() sec: 2; <,
    > remove() <;
<

> scene(intro6)
before: intro5;
名前バー:
    > add()
        layer: 会話背景レイヤー;
        visible: false; <,
    > fade() type: in; sec: 0.5; <;
会話バー:
    > add()
        layer: 会話背景レイヤー;
        visible: false;
    <,
    > fade() type: in; sec: 0.5;
    <;
会話:
    > add() layer: 会話レイヤー; < +
    > set() <;
<

> scene(intro7)
before: intro6;
会話穂波: > add() layer: 会話レイヤー; <;
会話:
    > set()
        text: 「拓也くん、おはよう。」;
    <,
    > wait() sec: 0.5; <,
    > act() <,
    > wait() sec: 4; <;
<

〜以下略〜

ちなみに、ルール説明のために矢印をスタートからゴールへ動かしたり(3:35付近〜)、サイコロを振ったり(4:35付近〜など)、ゲーム中に駒を動かしたり(11:55付近〜など)というのは、以下のような感じ:

# 矢印をスタートからゴールへ動かす

> scene(move_explain)
before: rule1;
矢印:
    > add() layer: 移動レイヤー; <,
    > wait() sec: 0.7; <,
    > move() dx: 0; dy: -387; sec: 1; <,
    > wait() sec: 0.2; <,
    > move() dx: 53; dy: 0; sec: 0.5; <,
    > wait() sec: 0.2; <,
    > move() dx: 0; dy: 387; sec: 1; <,
    > wait() sec: 0.2; <,
    > move() dx: 53; dy: 0; sec: 0.5; <,
    > wait() sec: 0.2; <,
    > move() dx: 0; dy: -387; sec: 1; <,
    > wait() sec: 0.2; <,
    > move() dx: 40; dy: 0; sec: 0.5; <,
    > wait() sec: 3; <,
    > remove() <,
    > wait() sec: 1; <;
上矢印:
    > fade() type: in; sec: 0.2; <,
    > wait() sec: 1.5; <,
    > fade() type: out; sec: 0.2; <,
    > wait() sec: 2.4; <, # 0.5+0.2+1+0.2+0.5
    > fade() type: in; sec: 0.2; <,
    > wait() sec: 1; <,
    > fade() type: out; sec: 0.2; <;
右矢印:
    > wait() sec: 1.7; <,
    > fade() type: in; sec: 0.2; <,
    > wait() sec: 0.5; <,
    > fade() type: out; sec: 0.2; <,
    > wait() sec: 1; <,
    > fade() type: in; sec: 0.2; <,
    > wait() sec: 0.5; <,
    > fade() type: out; sec: 0.2; <,
    > wait() sec: 1; <,
    > fade() type: in; sec: 0.2; <,
    > wait() sec: 2; <,
    > fade() type: out; sec: 0.2; <;
下矢印:
    > wait() sec: 2.4; <, # 0.2+0.5+1+0.2+0.5
    > fade() type: in; sec: 0.5; <,
    > wait() sec: 1; <,
    > fade() type: out; sec: 0.2; <;
<
# ダイスロール

> scene(rule10)
before: rule9;
dice: > add() layer: 効果レイヤー; <;
dl_1: > wait() sec: 0.1; <, > add() layer: 効果レイヤー; <;
dl_2: > wait() sec: 0.2; <, > add() layer: 効果レイヤー; <;
dl_3: > wait() sec: 0.5; <, > add() layer: 効果レイヤー; <;
dl_4: > wait() sec: 0.4; <, > add() layer: 効果レイヤー; <;
dl_5: > wait() sec: 0.6; <, > add() layer: 効果レイヤー; <;
dl_6: > wait() sec: 0.3; <, > add() layer: 効果レイヤー; <;
dr_1: > wait() sec: 0.3; <, > add() layer: 効果レイヤー; <;
dr_2: > wait() sec: 0.1; <, > add() layer: 効果レイヤー; <;
dr_3: > wait() sec: 0.6; <, > add() layer: 効果レイヤー; <;
dr_4: > wait() sec: 0.2; <, > add() layer: 効果レイヤー; <;
dr_5: > wait() sec: 0.4; <, > add() layer: 効果レイヤー; <;
dr_6: > wait() sec: 0.5; <, > add() layer: 効果レイヤー; <;
<
### 駒を動かす

> scene(replay14)
before: replay13;
o3:
    > remove() < + > add() layer: 移動レイヤー; <,
    > move() dx: -5; dy: -5; sec: 0.4; <, > wait() sec: 0.2; <,
    > move() dx: 0; dy: -43; sec: 0.4; <, > wait() sec: 0.2; <,
    > move() dx: 5; dy: 5; sec: 0.4; <, > wait() sec: 0.2; <;
カウント:
    > wait() sec: 1; <,
    > set() text: 1; < + > wait() sec: 0.6; <,
    > set() text:  ; <;
b4:
    > wait() sec: 1.6; <,
    > remove() < + > add() layer: 移動レイヤー; <,
    > move() x: 582; y: 419; sec: 0.4; <,
    > remove() < + > add() layer: ボードレイヤー; <;
b5:
    > wait() sec: 2; <,
    > remove() < + > add() layer: 移動レイヤー; <,
    > move() x: 542; y: 419; sec: 0.4; <
        + > fade() type: to; sec: 0.4; alpha: 0.5; <,
    > remove() < + > add() layer: ボードレイヤー; <,
    > wait() sec: 2; <;
<

> scene(replay15)
before: replay14;
会話:
    > act() <,
    > wait() sec: 4; <;
<

> scene(replay16)
before: replay15;
o2:
    > remove() < + > add() layer: 移動レイヤー; <,
    > move() dx: -5; dy: -5; sec: 0.4; <, > wait() sec: 0.2; <,
    > move() dx: 0; dy: -43; sec: 0.4; <, > wait() sec: 0.2; <,
    > move() dx: 0; dy: -43; sec: 0.4; <, > wait() sec: 0.2; <,
    > move() dx: 53; dy: 0; sec: 0.4; <, > wait() sec: 0.2; <,
    > move() dx: 5; dy: 5; sec: 0.4; <, > wait() sec: 0.2; <;
カウント:
    > wait() sec: 1; <,
    > set() text: 1; < + > wait() sec: 0.6; <,
    > set() text: 2; < + > wait() sec: 0.6; <,
    > set() text: 3; < + > wait() sec: 0.6; <,
    > set() text:  ; <;
b2:
    > wait() sec: 2.8; <,
    > remove() < + > add() layer: 移動レイヤー; <,
    > move() x: 582; y: 419; sec: 0.4; <,
    > remove() < + > add() layer: ボードレイヤー; <;
b4:
    > wait() sec: 3.2; <,
    > remove() < + > add() layer: 移動レイヤー; <,
    > move() x: 542; y: 376; sec: 0.4; <
        + > fade() type: to; sec: 0.4; alpha: 0.5; <,
    > remove() < + > add() layer: ボードレイヤー; <,
    > wait() sec: 2; <;
<

GUIでチマチマと動きをつけるとかなり大変だけど、スクリプトならサクッと書けるので、(比較的)楽チン。
(まぁ、いろいろと問題もあるんだけど・・・例えば、会話表示の適切な待ち時間を自分で指定しないといけなかったりとか)

ちなみに、scenario.txtは約3,000行。

そして、スクリプトを書いたら、あとはコマンドで動画を出力:

$ java -jar msc_console.jar -o output.mov config.txt item.txt scenario.txt
compile...
  configuration... OK
  item... OK
  scenario... OK
done.
output...
  scene progress... OK
  file output... OK
done.

これでoutput.movという名前で動画が書き出される。

ちなみに、JMFでの動画の出力方法がよく分からなかったので、かなり効率の悪い出力の仕方をしていて、そのせいでoutput.movのファイルサイズは1.7GBとかあったり(^^;
まぁ、実際にはH.264エンコードしてしまうので、そこまでアップ時のサイズはそれほどでもないんだけど。

今日はここまで!