UE4でスマホ向け3D縦スクロール漫画を作ってみた

UE4

はじめに

この記事は、Unreal Engine 4 (UE4) その3 Advent Calendar 2020の10日目の記事です。

アドベントカレンダーも10日目ということで、本日は箸休めのつもりで気軽にご覧ください!

本記事では、私達が開発中のスマホ向けゲームで実装した、『3Dウェブトゥーン』という縦スクロール漫画機能についての事例を紹介します!

3Dウェブトゥーンとは

3Dウェブトゥーンは、私達が開発中のゲーム『謎と記憶のラビリンス』で実装しているカットシーン再生機能です。

某コンテストに応募した企画書では以下のように解説しています。

 

そして今回の実装結果はこちらです。

右側の動画はiPhone XS Max実機での再生動画ですが、最新版ソフトのビルドが間に合わず、古いバージョンのソフトになってしまいました…(スミマセン!)最新版ならここまで処理落ちしないはず…。

下準備

漫画を作るために必要なのは、なんと言ってもまずはネームです。今回は以下ものを用意しました。

仕様を決めよう

実装するにはまず仕様を決める必要があります。今回は下記のように仕様を策定しました!

3Dウェブトゥーンの仕様
  • 下から上にフリックするとスクロールして読み進めることができる。
  • コマの中の絵は3Dで表現する。
  • スクロールと連動してコマの中の3Dが動く。
  • コマ毎に「スクロール値がいくつからいくつまでの時にコマを動かす」という範囲を持っている。
  • 3Dが動くコマは同時に一つだけ(アクティブなコマが移り替わっていくイメージ)

大体こんな感じでいけそうですかね…?とりあえずこのまま行ってみましょう!

設計してみよう

仕様を決めたので、次はどのような機能を誰に持たせるか、アクター同士でどう連携するかといった、構造を設計していきます。

Widgetブループリント

  • 縦スクロール漫画を複数の子Widgetに分割する。
  • 親Widgetが子Widgetをまとめて保持する。
  • 漫画のコマごとにシーケンサーが存在する。
  • 子Widgetは自分の範囲のコマ数分のシーケンサーを保持する。

サブシステム

  • サブシステムのAPIは基本的にWidgetブループリントから呼ばれる。
  • 上図のように、縦スクロール漫画の仕組みはサブシステム側に持たせて、Widgetブループリントは演出を担当する。

とりあえず作ってみよう

コマの表現

通常の漫画ならば画像を表示するだけですが、今回はコマの中を3Dで表現します。

そのため、余白や枠線といったコマ割りをWidgetで表示しつつ、コマの中はScene Capture 2Dを用いて3Dビューをレンダーターゲットに描画したものを表示します。

Scene Capture 2Dレンダーターゲットをざっくり説明しますと、3Dをテクスチャに描画して、2Dとして使うことができる仕組みです。
別カメラで描画した映像をTexture化して使う – 凛(kagring)のUE4とUnityとQt勉強中ブログ

コマ割りを作る

今回はWidgetのImage要素を組み合わせて表現しました。

  • 余白は、Image要素の白塗りを上下左右に分解して配置します。
  • 枠線は、余白の内側4pxにImage要素の黒塗りを上下左右に分解して表示させています。
  • 斜線を配置する際は、枠線や余白を回転させて対応します。
  • 枠線の代わりにグラデーションで余白に切り替わる表現は、Image要素にマテリアルを割り当てます。

コマの中を作る

コマの中の動く映像は、一つ一つをシーケンサーで表現します。

スクロール操作と連動してコマの中のシーケンサーを動かすために、以下のように実装しました。

  • コマが保持するシーケンサーごとに、スクロールY座標がいくつからいくつまでのときに再生するかの情報を持っています。
  • スクロールY座標をもとに、子Widget達は各自で自身のシーケンサーの再生/停止を切り替え、再生位置をシークします。これによってカメラやキャラがスクロール位置と連動してアニメーションします。
  • シーケンサーにボイスを仕込んでおくと、再生位置になると一度だけ再生してくれます。
  • 吹き出しなどのWidgetアニメーションは、Wiget側でスクロールY座標を元に判定して再生します。

複数のコマ絵を表示する

縦スクロール漫画は画面内に複数のコマが同時に表示されるため、3Dビューが複数必要になってしまいます。

そこで、コマ数分のレンダーターゲットを使用して複数のコマに3Dを表示します。

コマが重なる部分

漫画表現でよくあるのが、コマの中の絵が枠を飛び出している表現です。

こちらはCustom Depthを使用して表現しました。

 

こちらを参考にさせていただきました。

発生した問題と解決方法

これで完成!といきたいところでしたが、いざテストしてみると出るわ出るわ、バグや仕様漏れが大量に襲いかかってきました…!

以降は、発生した数々の問題と、それらをどのように討伐していったかを紹介します!

シーケンサー未再生のコマが真っ暗な問題

スクロールしてコマがせり上がってきた際に、そのコマのシーケンサーが再生されるまでは、レンダーターゲットにはまだ何も書かれていません。

そのため、真っ暗に表示されてしまったり、バッファに残っている絵が表示されてしまったりと、動作が不定になってしまいます!

解決方法

漫画開始時の白フェード中に、裏で全てのコマのシーケンサーを順番に一瞬だけ再生し、各レンダーターゲットに絵を書き込んでいきます。

子Widgetの初期化時にシーケンサーを再生してキャプチャする処理を追加しました。

シーケンサーを再生してすぐ一時停止してキャプチャして停止します。

これで事前に「最初のフレームの画像」をレンダーターゲットに書き込むことができます。

口パクとセリフが合わない問題

スクロールのY座標を元にシーケンサーをシークがさせるため、再生速度はプレイヤーの操作次第になります。

セリフを喋った際に、口パクアニメーションはユーザーの操作次第で早くなったり遅くなったりして、音声と口パクがずれてしまいます

※音声はシーケンサーの速度に関係なく再生されます。

解決方法

シーケンサーの再生速度はユーザーの操作によって自由に変わるため、ここでは「音声再生位置に達したら口パクアニメーションだけ個別再生する」ことにします。

  • アニメーションブループリントに口パク用にスロットを用意し、ブレンドして再生します。
  • 口パクアニメーションはシーケンサーのイベントトリガーをきっかけに個別再生します。

結果は以下の通りです。これで音声と口パクが一致するようになりました!

 

口パクのブレンドについては下記リンクを参考にしました。

無駄な描画がある問題

今回の仕様は、コマの中はレンダーターゲットに描画したものをテクスチャとして表示します。

シーケンサーがアクティブなコマは表示内容が変わるため、毎フレームレンダーターゲットに描画することになります。

その結果、レンダーターゲットに1回描画、枠から飛び出すコマの場合はもう1回描画、さらにWidgetに隠れている3Dビューの無意味な描画を1回実行します。

その結果は…?

これでは使い物になりませんね…。

解決方法

アクティブなコマの表現をレンダーターゲットで行うと、Widgetに配置するのは楽なのですが、描画の回数が増えてしまいます。

そこで以下の対応を行います。

■アクティブなコマは3Dビューで表現する

コマが下からせり上がってきてアクティブになった際に、レンダーターゲットを非表示にして3Dビューが見えるようにします。

アクティブな範囲の外に出ると、再びレンダーターゲットの静止画を表示します。

こうすることで、シーケンサーの再生開始時と終了時だけ、レンダーターゲットのキャプチャ画像を更新すればよくなります。

■3Dビューの表示位置をコマの位置にずらし、追従させる

3Dビューはコマの位置とは関係なく、ウィンドウ全体に表示されます。当然、Widgetを上にスクロールしても、3Dビューは追従せずに留まってしまいます

Widgetの隙間から3Dビューを見ているだけなので当然っちゃ当然ですね…。

そこで、ポストプロセスマテリアルを使用して表示位置をコマの位置にずらし、追従させます。

  • ポストプロセスに入力されたシーン画像のUV値を操作することで、表示位置を動かすことができます。
  • UV値はマテリアルパラメータコレクションを使用して、ブループリントから指定できるようにします。

スクロール量が変わる度に値を更新すれば、追従させることができるはずです。

■コマの表示位置を算出する

3Dビューはウィンドウサイズによって比率が変わりますが、基準となるのは中央なので、中央の座標をレンダーターゲットに合わせるようにします。

色々計算して移動量を算出しています…

ちなみにレンダーターゲットのVisibilityをHiddenにすると座標が取得出来なくなってしまうため、Render Opacity = 0.0f で対応しました。

コマの表示位置の算出は、下記の記事を参考させていただきました。

これで3Dビューをコマの位置に追従させることができました!

 

早くフリックするとシーケンサーおかしくなる問題

スクロールの移動量は、ユーザーのフリック操作に委ねられているため、早くフリックした場合に前フレームからのY座標の移動幅が非常に大きくなることがあります。

その場合、子WidgetAのシーケンサー停止と、子WidgetBのシーケンサー再生の処理が同フレームで発生する場合があります。

シーケンサーの再生/停止を、子Widgetが各々で判定及び実行しているため、実行順によってはシーケンサーが再生されないことがあります。

この作りは危険…!

解決方法

この問題点は、子Widgetが各自で再生/停止を判定して各自が勝手にサブシステムに依頼していることです。

それを解決するには、再生/停止の管理はサブシステム側でやってもらいましょう。

子Widgetはあらかじめ、コマの再生停止に関する情報(構造体)をサブシステムに登録しておきます。

サブシステム側で再生/停止を一括管理します。

これで安心して管理できますね!

レンダーターゲット使いすぎ問題

今回の仕様では、漫画のコマ数分のレンダーターゲットを事前に用意しました。

レンダーターゲットを増やしていくとメモリ使用量が増えていきます。画面外のImage要素もVRAMに乗せるのであればそっちも喰うはず(ちゃんと検証しなくてスミマセン…)

ともかく、これでは漫画のコマ数が多ければ多いほど枚数を用意することになるので何とかする必要があります。

解決方法

漫画のコマ数分レンダーターゲットを用意するのはもうやめにします…。

ここは腹を括って、一画面に表示されるコマの最大許容数を決め、その枚数内でやりくりしていきましょう。

今回は一画面に5枚までとしました。

  1. アクティブなコマを基準に、前後2コマをレンダーターゲットに描画する。
  2. アクティブなコマが切り替わる際に、前後2枚のレンダーターゲットをチェックする。
  3. 一番遠くなったコマのレンダーターゲットを破棄し、新たに近づいたコマで上書きする。

この処理をサブシステムに実装します。

これで5枚でやりくりできるようになりました!エコですね!

おわりに

いろいろな不具合を乗り越えて、ようやく冒頭の動画のような、実用レベルの機能が実装出来ました!

ゲーム会社に勤務していた頃はバグが起きないように入念に設計していましたが、個人制作では行き当たりばったりで楽しく作っています!

ということで今回この記事の教訓は、入念に設計しないと痛い目を見る』『ゲーム制作は楽しいのが一番』でした!

明日のUnreal Engine 4 (UE4)AdventCalenderその3は、Kihoku_kさんによる「AutomationToolとか何か」です!
私も是非勉強させていただきます!