LoginSignup
6

More than 5 years have passed since last update.

cairoのrustバインディングとffmpegで2Dアニメ作成ツールを作ってみた

Last updated at Posted at 2018-09-29

cairoのrust バインディングがあっさり使えたので、それをffmpegでエンコードして動画にするツールを作ってみました。
ソースコードは gist に貼りました。ここ
実用ではなくて私のrustの練習用です。

cairo のrustバインディング

cairoは2Dグラフィックスのライブラリです。
Rustからcairoを使う方法はこのページに丁寧に書いてあります。
Intro to Cairo Graphics in Rust
Ubuntu 18.04上で、このページの通りにやってcairoのライブラリのインストールからRustでのサンプルプログラムの実行まで問題なくできました。

プログラムの動作

とても単純です。
1フレームごとにcairoのグラフィックスライブラリで描画して、そのビットマップデータを子プロセスとして起動しておいたffmpegに食わせてエンコードしてmp4の動画ファイルを作ります。

プログラムの実行方法

全てソースコード内にハードコードしています。引数なしで実行させると、1280x720 30fps で10秒間の動画を作成します。これを変更するにはmain関数の定数を変更してください。
描画内容を変えるには関数draw()を編集してください。

作成できたmp4の動画ファイルをYouTubeに貼りました。
https://www.youtube.com/watch?v=JVZBuGC_HNU

描画する関数draw()は以下の通り。引数f に毎回インクリメントされるフレーム番号が入ります。
全面を白で塗りつぶし、扇形の描画、文字列の描画を行っています。

fn draw(surface: &ImageSurface, f: i32) {
    let cr = Context::new(surface);
    let width = surface.get_width() as f64;
    let height = surface.get_height() as f64;
    let dx = f as f64 * 1.0;

    cr.set_source_rgb(1.0, 1.0, 1.0);
    cr.paint();

    let cx = width / 2.0;
    let cy = height / 2.0;
    let r = cy;
    let cstart = -0.5 * PI;
    let cend = cstart + 2.0 * PI * ((f + 1) as f64) / 300.0;
    cr.move_to(cx, cy);
    cr.line_to(cx, 0.0);
    cr.arc(cx, cy, r, cstart, cend);
    cr.line_to(cx, cy);
    cr.set_source_rgba(0.0, 0.5, 0.0, 0.2);
    cr.fill();

    cr.select_font_face(
        "sans-serif",
        cairo::FontSlant::Normal,
        cairo::FontWeight::Normal,
    );
    cr.set_font_size(70.0);
    cr.move_to(600.0 - dx, 100.0);
    cr.set_source_rgb(0.0, 0.0, 1.0);
    cr.show_text("Hello, world! 1234567890");
    cr.fill();
}

子プロセスとしてffmpegを起動して、そこにピクセルデータを送り込むところは以下の通り。

fn make_cmdline(width: i32, height: i32, framerate: i32) -> String {
    format!(
        "ffmpeg -f rawvideo -pix_fmt bgra -s {width}x{height} -i - -pix_fmt yuv420p -r {framerate} -y out.mp4",
        width = width,
        height = height,
        framerate = framerate
    )
}

fn make_movie(width: i32, height: i32, framerate: i32, frames: i32) {
    let mut surface =
        ImageSurface::create(Format::ARgb32, width, height).expect("Couldn't create surface");
    let mut child = Command::new("/bin/sh")
        .args(&["-c", &make_cmdline(width, height, framerate)])
        .stdin(Stdio::piped())
        .spawn()
        .expect("failed to execute child");
    {
        // limited borrow of stdin
        let child_stdin = child.stdin.as_mut().expect("failed to get stdin");

        (0..frames).for_each(|f| {
            draw(&surface, f);
            let d = surface.get_data().expect("Failed to get_data");
            child_stdin.write_all(&d).expect("Failed to write to file");
        });
    }
    child.wait().expect("child process wasn't running");
}

fn main() {
    make_movie(1280, 720, 30, 300);
}

つまずいたところ

get_data()を使用可能にする条件

cairo::ImageSurface::get_data() がリファレンスカウントをチェックしていて、それが1より大きいとNoExclusive だという理由でエラーになります。描画のためにこのsurfaceのcairo::Contextを作るとそれでリファレンスカウンタが増えます。なので、get_data()を成功させるには、それまでにcairo::Contextをドロップさせなければなりません。
この関係がなかなかわからずに苦労しました。

なお、cairo::Contextは内部に可変の状態を持っているので意味的にはmutable のはずです。しかし、状態を変更する部分が全て unsafe に囲まれているのでRustコンパイラとしてはチェックができないためimmutableの扱いです。

ピクセルデータの順番

cairo::ImageSurface ではARGBの32bitでピクセルを扱います。get_data() ではこれを[u8]として取り出せます。これをこのままwrite_all()で書くのが一番簡単なのですが、そうすると32bit単位のデータを8bit単位で扱うことになるので、順番がひっくり返ります。ですが、ffmpeg ではピクセルデータのフォーマットの対応の幅が広くてBGRAの32bitも扱えるので、単にそのように指定するだけで済んでいます。
もしも順番を変えずに転送したい場合は[u8]で取り出したデータのraw pointerをunsafeな方法で[u32]にキャストして扱うことになると思います。

Future work

このツールは、時系列のデータの可視化に使えると思っています。GPSから取得した位置情報のログを地図データ上にプロットするとか。

8コアのマシンで実行してCPUの負荷状態を見たのですが、まだCPUを使い切っていません。マルチスレッド化して複数のCPUに描画を割り振るなど工夫の余地がたくさんあります。それは今後の楽しみに。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
6