LoginSignup
11
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に描画を割り振るなど工夫の余地がたくさんあります。それは今後の楽しみに。

11
6
0

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
  3. You can use dark theme
What you can do with signing up
11
6