キミを探す、夏 開発秘話

2021年11月5日(金) 22時43分0秒 | 435 view |

TL;DR

マジカルミライ2021プログラミングコンテストで、拙作「キミを探す、夏」が最優秀賞を受賞しました。本記事では作品の作り方やモチベーションについてブレイクダウンしながら解説していきたいと思います。

マジカルミライ2021プログラミングコンテストとは

このブログでも何回か取り扱っている、マジカルミライ2021に合わせてクリプトンと産総研で共同開催されたプログラミングコンテストです。
https://magicalmirai.com/2021/procon/
2020年から開催されて、今年2回目となるコンテストです。昨年に引き続き、今年も応募していました。
これまでの投稿はこちら
https://blog.utautattaro.com/tag/%E3%83%9E%E3%82%B8%E3%82%AB%E3%83%AB%E3%83%9F%E3%83%A9%E3%82%A4%20%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E3%82%B3%E3%83%B3%E3%83%86%E3%82%B9%E3%83%88/

コンテスト概要と審査基準

昨年と同様TextAliveApp APIを活用したリリックアプリを開発し、表現力を競うプログラミングコンテストでした。
審査基準は昨年と同様となっています。

クリエイティビティ
Webアプリケーションの演出が楽曲と同期して魅力的に見えるか
イノベーション
Webアプリケーションを支えるアイデアがユニークで、未来の創作文化を予感させるものであるか
完成度
Webアプリケーションが一般的なWebブラウザで正常に動作するか、実装が技術的に優れているか


今年は20作品の応募があり、その中から入選10作品が選ばれ、ユーザー投票をもとに受賞作品が発表されました。

成果物

「キミを探す、夏」という3Dフォトリアル没入型リリックアプリを開発しました


こちらからプレイできます:
https://magicalmirai.com/2021/procon/entry/entry01/

ソースコードも公開しました:


「マジカルミライ2021大阪」企画展ステージで紹介されました

「マジカルミライ2021東京」企画展ステージで最優秀賞作品として紹介されました


「キミを探す、夏」ができるまで

モチベーション

昨年も応募していて、入選止まりだったのが悔しく投稿しました。
昨年は初開催ということで、受賞を期待して発表会を視聴していたところ、受賞作品は作者からのコメントが発表されており、自分にはその連絡は来ていない!となって視聴中に落選を知りめちゃめちゃショックだったのが印象的です。
昨年の作品:


昨年は開発期間も1日で、締切直前に慌てて作って出したので、今回はもっとしっかり計画立てて開発しようと思い、実際には8月頭頃から開発していました


制作背景

今回はマジカルミライ2021楽曲コンテストのグランプリ及び準グランプリ作品計6曲が課題曲として設定されていました。提出するリリックアプリとしては一曲に絞って演出を作り込んでもよし、全楽曲に対応する形で作ってもよしというレギュレーションでコンテストが開催されました。

自分はすべての楽曲を聴いた上で、全楽曲に対応するような器用なアプリは作れないと早々に判断し、頭の中にビジョンが浮かんできた一曲に絞り込んで演出を作ろうと早々に決めました。
結果、シロクマ消しゴムさんの「夏をなぞって」が一番情景がイメージできたので、それに合わせて作り込みを開始しました


ラフ

今回は頭の中に浮かんできたビジョンをそのままのイメージで作ったので、ラフは起こさなかったです。歌詞と曲調から、学校を背景とした情景と、朝から昼になって夜になり、また朝になる感覚と、「キミ」と「僕」という存在がいることがイメージできたので、それに合わせてリリックアプリを作っています。


開発

開発環境

今回もすべてPlayCanvasで開発をしました。TextAlive App APIもCDNで公開されているので、すべてサーバーレスで開発ができ、DX(Develop Experience)が高いです。

これは凝った映像演出のために3Dレンダリングが必須であること、フォトリアルなイメージだったのでリッチなWebGLで書きたかったこと、なれた環境でやりたかったことなどからこの選択になりました。実際リリックアプリ開発に必用な開発環境構築ではPlayCanvasはプロジェクトを作成→External ScriptsからTextAlive App APIのjs読み込み→サンプルコピペで動作するのでおすすめです。

アセット

今回は舞台が教室なので、教室モデルを購入して利用しました。
購入したのはこちらの3Dモデルです お値段27USD:



UnityAsset Storeで販売されている3Dモデルを利用しました。技術的にも、規約的にも問題なくPlayCanvasで利用できるので、アセットを探すときにはおすすめです。
参考記事:

技術的こだわりポイント

開発したすべてを紹介すると多すぎるので、こだわりポイントを絞って紹介します

アプリ導入部のアニメーション

いきなりスタートして空間全体が見えてしまうのが嫌だったので、ぼんやりしたところから周囲を見渡して、物語が始まるような演出を入れています。

やり方としては、スタート時のカメラ(ユーザー操作不可)と、ウォークスルーカメラ(ユーザー操作可)の2つのカメラを用意し、最初はスタートカメラだけenabled:trueにします。

※後述するバッチング処理の影響で開発画面では一部オブジェクトがレンダリングされていません
そのあと、キーフレームアニメーションのように、振り向かせたいカメラのパスをentityのarrayとして一つのオブジェクトに格納します

分かりづらいですがpath1:左ななめした path2:後ろの黒板 path3:正面 となっています。
最後に指定されたpathを経由したキーフレームアニメーションをtweenライブラリを使って実装します

Firstcameraanimation.prototype.cameratween = function(pathpoint){
    let self = this;
    //位置をラープ
    this.startcameraentity.tween(this.startcameraentity.getLocalPosition()).to(pathpoint.localPosition,2.1,pc.CubicInOut)
      .loop(false) //繰り返さない
      .yoyo(false) //反復しない
      .on('update', function () {
          isTweenUpdate = true; //ラーピング開始
      })
      .on('complete',function(){
          isTweenUpdate = false; //ラーピング終了
          if(self.entity.children.length > pathindex){
            //次のパスポイントが存在したら再帰的にこの関数を実行して次のラープへ移る
            self.cameratween(self.entity.children[pathindex]);
            pathindex++;
        }else{
      //すべてのアニメーションが終了したらカメラを切り替える
            self.startcameraentity.enabled = false;
            self.walkthrowcamera.enabled = true;
        }
      })
      .start();
      //角度をラープ
      this.startcameraentity.tween(this.startcameraentity.getLocalEulerAngles()).rotate(pathpoint.getLocalEulerAngles().clone(),2.1,pc.CubicInOut)
      .loop(false)
      .yoyo(false)
      .start();
};


また寝ぼけて起きた感じを表現するためにポストエフェクトでbloomシェーダーを入れていますが、そのレベルもラーピングすることでぼんやり→くっきり感を表現しました。

MV in Lyric App


アプリケーションの3D空間内でYouTubeを通してMVを再生しています。

ちゃんと3D空間内にマッピングされていて、このアプリでは再現が難しいですが、オブジェクトに隠れると正しく隠れるようになります。
こちらはPlayCanvasのYouTube in 3D Scenesを参考にしています。
https://developer.playcanvas.com/ja/tutorials/youtube-in-3d-scenes/

一点誤算だったのが、
TextAlive App APIではデフォルトで楽曲のYouTube動画が挿入される仕組みとなっており、それをHTMLを変更して3Diframeのsourceに指定するつもりだったのですが、
player.createFromSongUrlでYouTubeのURLを指定した場合、歌詞のタイミングがずれたり歌詞が抜けたりするバグが有りました。
コンテスト公式ドキュメントで確認したところ、指定するURLはpiaproのURLとなっており、そちらを指定することでバグは解消されましたが、YouTube動画が差し込まれなくなったので、動画は別途YouTube APIを利用してミュートで差し込んでいます。ただYouTubeとpiaproで楽曲のイントロが微妙に違っていて、そのまま同タイミングで動画を流すとずれてしまうので、調整を行っています。また、YouTube iframeはクリックすると一時停止してしまいますがTextAliveとは結合していないため、アプリ側の楽曲は一時停止しないのでYouTube iframeはクリックしても一時停止しないように力技で防止しています。

//YouTube API読み込み
var tag = document.createElement('script');
tag.src = "https://www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);

//3diframe DOMを探してYouTube Playerインスタンスを割り当て
let iframe3d = document.getElementById("3diframe");
ytplayer = new YT.Player('3diframe', {
  height: '360',
  width: '640',
  videoId: '3wbZUkPxHEg',//本家MVのID
  playerVars: {
    'modestbranding': 1,
    'autohide': 1,
    'controls': 0,
    'showinfo': 0,
    'rel': 0                              
  },
  events: {
    'onReady': function(){
      isYtcanplay = true;
      //強制ミュート
      ytplayer.mute();
    },
    'onStateChange' : function(event){
      switch(event.data){
        case YT.PlayerState.PAUSED:
        //動画が一時停止されたときに即座に「playVideo()」を実行して
        //一時停止ができないようにする。
        ytplayer.playVideo();
      }
    }
  }
);

//in player.addListener()
onPlay() {
  self.isplaying = true;
  if(isYtcanplay){
    window.setTimeout(function(){
   //3900ミリ秒遅らせてからMV再生開始
      ytplayer.playVideo();
    }, 3900);
  }
}


大量ポリゴンとライティングの最適化処理

今回のアプリケーションは3Dモデルを大量に使うこともありパフォーマンス面が心配でした。

プロファイラから確認するとおよそ15万ポリゴンを描画していて、WebGLでやるにはかなり限界に近い数字です。
またライトも太陽のDirectional Lightと蛍光灯のspot lightの合計7灯たいていて、その全てがリアルタイムシャドウを要求するライトです

自分のPCはオンボードでないGPUが搭載されてかつRyzenのハイスペックなCPUが載ったゲーミングPCなので問題はないですが、多くの人に体験して貰う必要がある以上Intel製オンボードGPUのみしか乗っていないビジネスノートPCでも動作して貰う必要がありました。
今回は描画負荷軽減のためにstaticな要素(机や椅子、棚など動かないオブジェクト)はすべてstaticなバッチンググループにまとめて挿入し、cast shadowも不要なオブジェクトは外し、ライトも環境光の影響を受けない範囲にあるオブジェクトについて、ベイク可能なものはベイクしました。また画像リソースはすべてBASIC圧縮をかけています。
結果アプリ開始時のオーバーヘッドは10秒→1秒程度に収まり、ドローコール数は254→92まで減少、モバイルノートでのFPSは最大でも25FPS程度だったのが40-50まで安定して出るようになりました。
<最適化前>

<最適化後>

3Dフォント

このアプリ最大の演出であるサビの空から降ってくるフォントはPlayCanasのmesh fontサンプルを参考にしています:

チュートリアルも用意されています:
https://support.playcanvas.jp/hc/ja/articles/900006249963

meshfontを利用する場合、element-textと違って、動的にフォントのメッシュを作成するためプリレンダリングは不要なのですが、
リアルタイムにmeshfontを作成する場合、かなりのCPUリソースをもっていかれることがデバッグ中に判明し、一文字あたり大体1-2秒のオーバーヘッドがありました。
リリックアプリではかなり致命的なレイテンシだったので、今回は苦肉の策でinitialize時にmeshtextをプリレンダリングする超パワーコーディングな方法で解決しました。

楽曲が固定だったのでstaticにしてしまいましたが、本当は歌詞情報を抽出後によしなにやってくれるような処理のほうが望ましいですね。

表現的こだわりポイント

時間の表現

この作品はキミを探す僕の話です。誰もいない昼の教室で目覚めた僕の視点で物語がスタートします

このときは時計は13時頃で、蛍光灯もついていますね

そして夕方、夜になり、、、

最後は朝になります

この朝日が差し込む感じが最高に気に入ってます。教室で朝を迎えたことはありませんが(研究室なら何度もある)きっと朝の教室はこんな感じなんだろうなあというノスタルジーな気持ちになります。

掲示物・装飾

いくつかの掲示物は手書きで、奥さんにprocreateで描いてもらいました

味があってとっても気に入っています。

シロクマ消しゴム消しゴム

アプリを作っている中盤頃から、なんか宝探し要素を入れたいなと思い、作中のじゃまにならないような雰囲気で入れようと思っていました。
楽曲制作者のシロクマ消しゴムさんのアイコンが可愛かったので、許可をいただき使わせていただくことにしました。

作中では、歌詞に登場する「キミ」の数だけ、つまり9個教室内に隠れています。
答え合わせ用ページを作りました。鍵付きなので見たい方は以下URLから、パスワード「キミを見つけた」と入力して見てみてください。


作中で、机に歌詞が表示されるパートがありますが、一つ不自然に表示されない机があります。
その机と、開始時に目を覚ました机。その席に座る人物が一体どんな関係なのか。
この作品の意図を解説しきるのはナンセンスだなと思っているので、ぜひ想像してみてもらえたら嬉しいです。

作ってみて

TwitterやYouTube Liveのコメントでとても多くの反響をいただけてとても嬉しかったです。
正直作ってる最中は独りよがりなんじゃないかとかウケないんじゃないかとか、リリックアプリの最適解がない中模索しながら作っていました。
定期的に奥さんにレビューしてもらっていたんですが、「なんかわからないけど、どんな人でも体験したら胸がギューッとなると思う」という最高の褒め言葉を頂いたので自信持って作り切れました。
実際技術的にはPlayCanvasやTextAliveの機能をフル活用していただけで、自分がしたことはイメージを膨らませてそれをツールを使って落とし込んだだけです。
このように特筆したスキルが必要なわけでもなく、自分自身の発想とそれを実現可能なツールを知っていればどなたでも作れるので、他の参加者の方もおっしゃっていましたが、この作品を見て、自分も作ってみたい!と思ってくれる人が増えてくれたら嬉しいです。
これにて自分のリリックアプリ開発は一段落つけ、今後は自分はそういう人に支援する方向に回りたいなと思っています。
また、余談ですが自分は10年ほど前にボカロPになりたかった過去が合って、紆余曲折を経てプログラマーになったのですが、心のなかでどこか当時志していたのに道半ばになってしまったというもやもやがありました。なので最終的に、マジカルミライにこのような形で貢献できて、爪痕を残せたかなと思うので、個人的には大満足しています。

最後に、体験していただいた方、評価していただいた方、ありがとうございました!

avatar Ryotaro Tsuda @utautattaro

3D系Webデベロッパー。
エンジニアリングよりもデベロップメントやアートに興味があります。技術の根幹やコアも重要ですが、それで何を実現/表現するか?を考えていることの方が多いです。