新卒からのTAが内製ツールに動画撮影機能をつけてみた話

初めまして。

セガゲームス開発サポート部テクニカルアーティストの清水と申します。
新卒からテクニカルアーティスト(以後TA)業務に積極的に関わらせてもらい、今年で社会人歴TA歴共に3年目になります。

今回は、弊社で使われてる内製ツールHNDViewerにコマンドライン実行を組み込み、アセットのモーションGIF画像を自動生成できるようにした時の対応について紹介させていただきます。

概要

独自の内製ビューアに動画撮影機能を実装し、「モーションプレビュー一覧を半自動的に作成する環境を構築する」
という目的のもと、以下のようなコマンドをJenkinsで自動定期実行することにより、内製ビューアがGif画像を自動で生成できるようにしてみました。

HNDViewer.exe -cmd -exec=capturegif -out=D:\foruda\output -open=D:\directory\Narita_Ann_mojimoji.hn2v -width=600 -height=450

f:id:sgtech:20181023232429g:plain
モーションのGif画像 (C)SEGA

今回紹介する事例で取り組んだ内容は、3つの内容から成り立っています。
・内製3DCGモデルビューアHNDViewerにモデルのモーションに関するGIF画像を生成する機能を追加した。
・HNDViewerにコマンドライン実行機能を追加して、モデルのモーションに関するGIF画像を生成する機能をコマンドライン実行できるようにした。
・実装したコマンドラインをJenkinsから実行することで、モデルのモーションに関するGIF画像の生成を自動化した。

まず、今回紹介するアプリケーションについて、触れておきましょう。

f:id:sgtech:20181023232512p:plain
概要

HNDViewerとは?

弊社には、3D CGツールのデータをエクスポートして格納するための中間構造を扱うHNDというシステムがあります。
HNDViewerとは、このHNDを格納した「HNDファイル」のビューアツールです。

格納されている3DCGデータの表示・再生はもちろん、各ポリゴンやノードのパラメータを一覧することもできます。

f:id:sgtech:20181023232418p:plain
HNDViewer

Jenkinsとは?

Jenkinsとは、ソフトウェアの実行等の一連の作業を自動化できる自動実行ツールです。

jenkins.io

高い汎用性を持った自動実行ツールで、事前に設定した条件を満たすたびに指定したコマンドを自動で実行してくれます。

自動実行に指定できるのは基本的にbat処理などのコマンドライン実行可能な処理になります。

ここからは少し実装の話に入っていきます。

GIF画像の生成

今回はImageMagickを使用してgif画像を作成する方法を採用しました。

www.imagemagick.org

ImageMagickは、幅広い画像ファイルフォーマットに対応している、画像を操作したり表示したりするためのソフトウェアです。 コマンドラインツールを含んでおり、バッチ処理での画像の変換・編集などに必要な機能が揃っています。

ImageMagickを使用して指定した連番画像からGif画像を生成することができます。

HNDViewerには下記の3つの機能はすでに搭載されています。
・3DCGモデルを表示する
・モデルのモーションを再生する
・モデルを表示している画面のスクリーンショットを作成する
従ってGif画像を作成するために追加する必要のある機能は下記の2つです。
・モデルのモーションの連番画像を作成する機能
・ImageMagickに連番画像を受け渡してGIF画像を作成してもらう機能

まず、読み込んだモデルを1フレームずつ動かして表示している画面をキャプチャし、画像として一時フォルダへ保存します。 これをモーションの最終フレームまで行い、モデルのモーションの連番画像を作成します。

f:id:sgtech:20181023232423p:plain
連番画像の作成

モデルのモーションの連番画像の生成が完了したら、ImageMagickを利用してGIF画像を生成します。

キャプチャした連番画像をImageMagickのコマンドラインツールであるconvert.exeの引数に渡してGIF画像を作成します。

 convert.exe -delay 3.3 -loop 0 キャプチャした画像_*.png キャプチャした画像.gif  

これでGIF画像の生成は完了です。

GIF画像の生成処理が終了したことを確認したら、一時フォルダに保存してあるGIF画像の生成に使用した連番画像はすべて削除します。

これでモデルのモーションに関するGIF画像を作成する機能ができました。

コマンドライン実行の追加

モデルのモーションに関するGIF画像を作成する機能を実装したHNDViewerは、GUIツールでコマンドラインから処理を実行する機能は持たないため、次にHNDViewerにコマンドライン実行機能の追加をしていきます。

コマンドライン実行時の引数の入力方法について

入力する引数については以下のようなルールを採用しました。
・引数はキーとなる文字列とパラメータを「=」で接続したものを一つの引数として入力する。
・キーとなる文字列の頭には「/」もしくは「-」を付ける。
このような引数をプログラム側でパースしてパラメータを読み込みます。

つまり、以下の2種類のうち、いずれかの引数を入力するルールとします。

application.exe -cmd -exec=コマンド名 -out=出力ディレクトリ -open=出力ファイル -width=画像の横幅 -height=画像の縦幅  

または

application.exe /cmd /exec=コマンド名 /out=出力ディレクトリ /open=出力ファイル /width=画像の横幅 /height=画像の縦幅  

そして、次のようにプログラム側で引数を=でパースし、辞書にします。

Dictionary<string, string> ParseArgs(string[] args)  
{  
    Dictionary<string, string> ParsedArgsDictionary = new Dictionary<string, string>();  
    char[] splitters = { '=' };  
    foreach (string line in args)  
    {  
        string s = line;  
        if (s.StartsWith("/") || s.StartsWith("-"))  
        {  
            s = s.Remove(0, 1);  
        }  
        args = s.Split(splitters);  
        if (args.Length > 1)  
        {  
            ParsedArgsDictionary.Add(args[0], args[1]);  
        }  
        else  
        {  
            ParsedArgsDictionary.Add(args[0], "");  
        }  
    }  
    return ParsedArgsDictionary;  
}  

この引数の入力方法は以下のような利点があります。

・コマンドを使用する際にどのパラメーターにどの値を入力しようとしているのかユーザーに分かりやすい

例:通常のスペース区切りの場合

HNDViewer.exe capturegif D:\foruda\models D:\directory\model 640 320

capturegifはコマンド名であることは推測できます。

また、2つあるパスは入力先と出力先を表していると推測できます。

が、どちらが入力でどちらが出力かは読み取ることはできません。

残り2つの数値については何を表すのかは推測することすら困難です。

しかし、このルールを採用した場合

HNDViewer.exe -cmd -exec=capturegif -out=D:\foruda\models -open=D:\directory\model -width=640 -height=320

-exec=capturegifによって実行したい処理がcapturegifであることがわかる。

-outと-openで指定されていることによって入力と出力が一目でわかる。

-widthと-heightによってそれぞれの数値が幅と高さを表していることがわかる。

・アプリケーション側からのユーザーへ対しての通知内容を分かりやすくできる。

コマンドラインに渡す引数の順番を間違えるなどといったミスを事前に防ぐことができる。 各パラメータとパラメータが示すキーが対になっているため、アプリケーション側からどのパラメータに問題があったのかをユーザーに通知できる。

・アプリケーションに関連付けられたファイルを開く処理を明確に分けることができる。

第1引数に「-cmd」のようなコマンドライン実行であることを表すキー文字列が存在することによって、プログラム側は今回の実行がコマンドライン実行であるか否かを明確に判定することができます。

既存ツールへのコマンドライン実行の追加

コマンドライン実行時の処理の流れ

f:id:sgtech:20181023232408p:plain
コマンドライン実行の流れ

コマンドラインの確認

まず、アプリケーションのmain関数内でツール本来の処理に入る前に、 実行時の引数を確認する処理を追加します。

本来の処理に入る前に引数の内容を確認して、第一引数が「-cmd」または「/cmd」であった場合 コマンドラインモードで起動できるようにします。

この「コマンドラインモードで起動するか否か」の判定は、引数の有無で判断してしまうと、関連付けられたファイルを開くことができなくなってしまいますので、必ず引数の内容を確かめるようにします。
これは、アプリケーションが関連付けられたファイルから起動される際に、そのファイルのフルパスが引数1に入るためです。
アプリケーションに関連付けられたファイルがダブルクリックなどで開かれる場合、一般的に起動するアプリケーションには関連付けられたファイルのパスが第1引数に渡されます。
このパスの情報をもとにアプリケーションはファイルを開いた状態で起動することができます。
そのため、コマンドライン実行か否かの判定は引数の有無だけで判断しないようにします。

また、第一引数が「-?」または「/?」であった場合も
コマンドラインモードに遷移するようにして最終的にユーザー対してヘルプが表示されるようにします。

 main(){  
        var args = Environment.GetCommandLineArgs();  
        bool consoleMode=false;  
        //第1引数を確認
        if(args[1].Equals("-cmd") || args[1].Equals("/cmd")){  
            //-cmdまたは/cmdの場合はコマンドライン実行用の処理に遷移するようにする。
            consoleMode=true;
        }
        if(args[1].Equals("-?") || args[1].Equals("/?")){  
            //-?または/?の場合もコマンドライン実行用の処理に遷移するようにする。
            //その後ヘルプを表示するようにする。
            consoleMode=true;
        }
        if(consoleMode){
            //コマンドライン実行用の処理へ遷移 
            Application.Run(new MainForm(consoleMode));  
        } 
        else{
            //通常起動用の処理へ遷移 
            Application.Run(new MainForm()); 
        }
    }

実行コマンドの確認

コマンドライン実行であることが確定した後に、実行しようとしているコマンドがアプリケーションに登録されたコマンドであるかを確認します。 コマンド名が指定されていない場合やアプリケーションに登録されていないコマンド名の場合は、ヘルプを表示して終了させます。

 //CommandListは実行できるコマンドのリスト

    //引数    
    Dictionary<string, string> argsDictionary = ParseArgs(args);
    //引数に実行するコマンド名が含まれているか否か
    if (argsDictionary.ContainsKey("exec")) {
    //実行しようとするコマンドがあるか否か
        if (CommandList.Contains(argsDictionary["exec"])) {
            //コマンドが存在するならば実行する
            ExecuteCommand(argsDictionary);
        }
        else {
            //コマンドが無い場合はヘルプを表示する。
            ShowHelp();
        }
    } else {
        //execが無いということはコマンドが指定されていないということなのでヘルプを表示する。
        ShowHelp();
    }

引数の確認

実行するコマンドが確定した後に実行に必要な引数が全てそろっているか、引数に入力されている値は有効かを確認します。 引数が足りていない場合や引数の内容に問題がある場合は、その旨エラーを出力し、最後にヘルプを表示して終了させます。

以下のコードは私が実装した「capturegif」コマンドの引数確認の例です。

 public Bool CheckCaptureGifImageArgs(Dictionary<string, string> argsDictionary)
    {
        //入力ファイルを確認する。
        if (!argsDictionary.ContainsKey("open")) {
            //引数にopneが存在しない場合は、openが無いことをユーザーに表示してヘルプを表示
            Console.WriteLine("引数[open]がありません。");
            ShowCaptureGifImageHelp();
            return false;
        }
        //入力ファイルが存在するかを確認する。
        string modelpath = argsDictionary.ContainsKey("open");
        if(!File.Exists(modelpath)){
            //opneで指定されたファイルが存在しない場合は、その旨をユーザーに表示してヘルプを表示
            Console.WriteLine("指定されたファイルmodelpathが存在しません。");
            ShowCaptureGifImageHelp();
            return false;
        }

        //出力フォルダを確認する。
        if (!argsDictionary.ContainsKey("out")) {
            //引数にoutが存在しない場合は、outが無いことをユーザーに表示してヘルプを表示
            Console.WriteLine("引数[out]がありません");
            ShowCaptureGifImageHelp();
            return false;
        }
        //出力フォルダが存在するかを確認する。
        string outputDirectory = argsDictionary.ContainsKey("out");
        if(!Directory.Exists(outputDirectory)){
            //outで指定されたフォルダが存在しない場合は、その旨をユーザーに表示してヘルプを表示
            Console.WriteLine("指定されたフォルダoutputDirectoryが存在しません。");
            ShowCaptureGifImageHelp();
            return false;
        }

        //生成するgif画像の大きさを確認する。
        if (argsDictionary.ContainsKey("width") && argsDictionary.ContainsKey("height")) {
            int w = 0;
            int h = 0;
            //widthとheightが数値に変換できるかを確認する。
            if (int.TryParse(argsDictionary["width"], out w) && int.TryParse(argsDictionary["height"], out h)) {
                SetGifImageSize(w, h);
            }
            else{
                //数値に変換できない場合は、その旨をユーザーに表示してヘルプを表示
                Console.WriteLine("widthとheightが数値に変換できませんでした。");
                ShowCaptureGifImageHelp();
                return false;               
            }
        }
        else{
            //引数にwidthとheightが存在しない場合は、その旨をユーザーに表示してヘルプを表示
            Console.WriteLine("引数[width]、[height]がありません。");
            ShowCaptureGifImageHelp();
            return false;
        }

        return true;
    }

処理の実行・アプリケーションの終了

ここまでの手順で、実行するコマンドが決まり、引数に問題ないことが確認できたので、処理を実行します。 処理が終了したら、アプリケーションを終了します。

これでGUIツールだったHNDViewerにコマンドライン実行機能を追加することができました。

既存の機能に対応するコマンド名と処理に必要な引数を決めておき、
設定したコマンド名が実行されたら、対応する既存の機能を実行するように処理を実装することで、コマンドラインからツールの任意の機能を実行できるようになります。

一例として、モデルのモーションに関するGIF画像の生成機能について、コマンド名と引数は以下のように設定しました。

コマンド名 : capturegif
引数

Key 内容
out 出力ディレクトリ
open モデルファイルのパス
width 画像の横幅
height 画像の高さ

複数の作業をバッチ処理にまとめることもできるようになりました。

jenkinsを用いた処理の自動実行

  ここまでで、コマンドライン実行でモデルのモーションに関するGIF画像を作成することができました。
この処理をjenkinsから実行してもらうことで、自動でモデルのモーションに関するGIF画像を作成できるようにします。

jenkinsを立ち上げて新規のジョブを作成し、
Windowsバッチコマンドの実行に、実装したモデルのモーションに関するGIF画像を作成するコマンドを設定します。
自動実行するタイミングは好きなタイミングを指定します。

f:id:sgtech:20181023232413p:plain
jenkinsのジョブが実行するコマンドを設定する。

jenkinsの導入についてはCEDEC2017で講演された以下のセッションの資料がわかりやすくてオススメです。

cedil.cesa.or.jp

これで自動でモデルのモーションに関するGIF画像を作成することができるようになりました。

最後に

いかがでしたでしょうか。

改めて振り返ってみると実装した各処理の内容自体は、特別優れた技術が使われているわけでもなく、普通な実装だなぁと自分でも感じる部分があります。
しかし、その普通な実装の積み重ねで、「アセットのアニメーションGIF画像の自動生成」という目的を達成することができました。

この処理の自動化を実現したことによって、 手作業を必要とせずに、常に最新のモデルのモーションに関するGIF画像を生成できるようになりました。
さらに生成されたGIF画像を用いて、手軽にモデルのモーションを確認できるようになり、作業の効率化に貢献できました。
社内での反響も上々です。

テクニカルアーティストとして実際に業務を経験するまでは、 作業の効率化とは最先端の高度な技術によって実現される「未だかつてない便利」なものだというイメージを持っていたのですが、 実際の業務経験を通して、作業の効率化というのは、案外小さな普通の技術でできている「ちょっとした便利」の組み合わせで成り立っているんだなぁと感じました。 これからもこうした作業の効率化にどんどん取り組んでいき、クリエイターがより快適にゲーム制作に注力できる環境の実現を目指して、頑張っていきたいと思います。

快適なゲーム制作環境の実現や快適な環境でのゲーム制作に興味がある方は、ぜひセガゲームスの採用情報にアクセス!

自己紹介(補足) 

新卒からTAという比較的珍しいキャリアパスを広めるべく過去2回CEDECで登壇などもいたしました。

若手テクニカルアーティストの業務効率改善への貢献、育成について話すラウンドテーブル http://cedec.cesa.or.jp/2017/session/VA/s58da6294d3416/

若手テクニカルアーティストの育成とその役割について話すラウンドテーブル https://2018.cedec.cesa.or.jp/session/detail/s5aafcb42e41b7

参加してくださった方はありがとうございます。
上記セッションについて興味を持たれた方には以下の記事がオススメです。

新卒からテクニカルアーティスト(TA)を育成するために必要なこと>>TAの素養ってなんだろう?
https://entry.cgworld.jp/column/post/201710-cedec-ta.html

TAを増やす改善策は「余裕をもつこと」~若手テクニカルアーティストが大いに語ったラウンドテーブル~CEDEC 2018レポート(3)
https://cgworld.jp/feature/201809-cedec-03tart.html

記事に取り上げてくださった

株式会社ボーンデジタル 尾形美幸様

株式会社ボーンデジタル 小村仁美様

にはこの場を借りて感謝申し上げます。

Powered by はてなブログ