🎡

3DボクセルをWebGLで企業サイトに組み込んだ際の考察過程

2023/07/05に公開

はじめまして。フロントエンドエンジニアの田島と申します。
ちょうど今から約1年ほど前に鹿児島にあるアプリファクトリーはるni株式会社というゲーム開発会社様にお声がけいただき、お仕事として企業サイトを制作致しました。
以下のツイートにて、サイト内での操作イメージを短くまとめた動画を添付しています。

サイトの制作方針として、はるni様が制作を得意とする3Dボクセルを中心に据える方針で企画が固まり、技術的にチャレンジングな仕事となりました。その際、私自身多くの方々の技術記事等を参考にさせていただいたため、自分も微力ながら制作の過程で得られた知見を共有できればと思い、記事に残すことにしました。
(ただし、WebGLやThree.jsの扱いに関して未熟な点も多分にあるため、各章でより良いアイデアをお持ちの方はコメント等で温かくご提案いただけますと幸いです。)

技術検証

陰影を事前にベイクするか、リアルタイムに演算するか

3Dのレンダリングのうち、陰影の処理は特に計算コストが大きなものとなります。その実装方針を2つに大別するなら、事前にベイク(焼き付け)するか、レンダリング時にリアルタイムに演算するかの2択になるかと思います。

例えば事前ベイクの手法を採用した場合、当ウェブサイトにある3Dモデルのテクスチャを陰影も含めてベイクして書き出した画像の一部は以下のようになります。

UV展開されたテクスチャを初めて見る方にとっては意味不明な画像かもしれませんが…
左下に見えるものが観覧車のフレームにあたる部分となっており、光が当たって明るい部分と当たらずに暗い部分が表現されている様子が見て取れます。このように、ライティングにより発生する陰影の結果を予めテクスチャに含めておくことで、レンダリング時のライティングの計算は省略してしまおうというのが事前ベイクの基本的な考え方です。

実際に、技術検証も兼ねて先に制作したティザーページではこの事前ベイクの手法を採用して実装していました。

この手法は、

  • 事前に時間をかけて陰影の計算ができるため、その質感が綺麗になる
  • レンダリング時は陰影の計算が不要なため負荷が小さくなる

といったメリットが有る一方で、以下のようなデメリットも懸念材料として抱えていました。

  1. 解像度の高いテクスチャ画像が必要になるため、アセットサイズが大きくなる
  2. テクスチャをGPUにロードする際に一定の時間を要する
  3. 3D空間内にアニメーションする要素があった場合、それ対してリアルタイムに連動する影を表現することができない

上記のティザーページのときはモデルの複雑度もそれほど大きくなかったためパフォーマンス上の影響が致命的ではなかったのですが、本番用のモデルで同様のアプローチを試みた場合、1, 2の問題が肥大化してきました。
具体的には、まず本番用のモデルに対して4096x4096のテクスチャが3~4枚ほど必要となることが見通しとして分かりました。1のアセットサイズについては、 avif や webp といった次世代フォーマットの画像を用いることで許容範囲と呼べるところまで削ることができたのですが、2の「GPUへのロード時間問題」については比較的スペックの高いマシンにおいても1〜2秒程度を要してしまっていました。また、この間はブラウザのレンダリングがフリーズするような挙動となるため、ユーザ体験としても好ましくありませんでした。

上記のようなパフォーマンスの問題に加え、3の「リアルタイムに連動する影を表現することができない」という件についても、表現をブラッシュアップするフェーズで必要となる可能性が高いと思われたため、事前ベイクをやめてリアルタイムレンダリングで実装する方針に変更しました。

リアルタイムレンダリングのためのパフォーマンスチューニング

リアルタイムレンダリングを行う方針に変更したため、やはり気になるのはレンダリング時のパフォーマンスです。
古いMacBook AirやAndroid端末を同時に起動しながら品質とパフォーマンスの丁度良い塩梅を探る作業を行いました。
前項でも述べましたが陰影の処理が特にボトルネックとなるため、シャドウマップの解像度調整には特に気を払いました。シャドウマップの解像度を高くするほど影をくっきりと描画することができますが、その分計算負荷も高くなります。

左がシャドウマップの解像度高、右が解像度低
幸い、今回の3Dデザインにおいては解像度が多少ぼやけた影であってもそれほど違和感なく馴染んでいたため、割と強めに落とすことができました。

また、Three.jsの実装上で用いるマテリアルとしては、事前ベイク向きの MeshBasicMaterial が使えなくなったため、リアルタイムに陰影を処理できるマテリアルのうち最も負荷の小さい MeshLambertMaterial を採用しました。今回の3Dデザインにおいて、高光沢な質感や金属的な表現などは不要であり、最小限の陰影表現のみが行えれば良いと判断したためです。

ちなみにこのあたりのThree.jsの実装におけるパフォーマンスチューニングのTipsは株式会社カブク様のThree.jsのパフォーマンスTipsという記事(主な情報ソースはThree.js作者のMr.doobさんのツイート)が非常に有用で参考になります。定期的にパフォーマンスチューニングのためのチェックリストとして使用させていただきました。

モデルサイズの圧縮

3Dボクセルはその性質上、ライティングによる陰影をベイクする必要がない場合、出力すべきテクスチャの情報量は極めて小さくなります。通常、UV展開された2次元のテクスチャ画像は不要となり、1次元のカラーパレットによりマテリアルを表現することができます。
そういった利点もあり、モデルデータの最適化(オブジェクト同士の結合など)やDraco圧縮を行った結果、アニメーション情報を含む3Dモデル全体の出力サイズは800kb程度となりました。
更にサーバーからの配信時にはBrotli圧縮が適用されることで、最終的な転送量を600kb程度まで減らすことができました。

インタラクション案の検討

ここからはトップページのインタラクション案に関する話に移ります。

初期案について

初期案として試みたのは、スクロール操作に連動して3Dの街をドローン映像のように周遊するアプローチです。

3Dの街の正面に門があったため、最初にそこをくぐるのが気持ち良い動きになるのではないかと直感的に思ったためです。

このインタラクションの実装としては、以下のようにBlender上で編集したカメラのアニメーションを書き出し、スクロール位置とアニメーションシーケンスを同期する手法を取りました。(アイデアの検証段階だったため、カメラアニメーションの手付けも私の方で行いながらトライ&エラーをしていたのですが、ジェットコースターのレールを設計しているような感覚があり少し面白かったです)

しかし、このアイデアのモックアップが概ね出来上がり、クライアントであるはるniさんに見せたところ「面白いけれど、3D酔いが起きやすいかもしれない」という指摘をいただきました。
私個人としてはそれほど感じていなかったのですが、3D酔いの起きやすさは個人差によるところが大きいという点と、企業サイトである以上その懸念が少しでもあるならば避けるべきだと考えたため、別案を検討することにしました。

3D酔いに関する考察

このときの議論の流れで、では何故このインタラクションだと3D酔いが起きてしまうのかという理由について仮説を話し合ったのですが、おそらく「ユーザが操作する方向は一定(下方向へのスクロール)であるにも関わらず、それに対するフィードバックが目まぐるしく変化するからではないか」と考えました。車の運転手よりも同乗者の方が車酔いを起こしやすいのと同じように、人間は次に起こるアクションが自分にとって予測しづらいとき、特に酔いが発生しやすいのではないかと考察しました。特に今回検討していた動きは、急なカーブなターンを含んでいたためそのポイントで酔いを引き起こしやすくなっていたものと考えられます。

第二案(本番案)について

第一案で得られた考察に基づき、別案を検討する際に考慮すべきは「ユーザ操作とそれに対するフィードバックのベクトルを一意に呼応させること」でした。
その条件を元にインタラクションを考えるとすればむしろシンプルとなり、3Dボクセルの上空を360度回転で周るように操作する案に落ち着きました。
こうして振り返ると、初回の案は随分トリッキーなインタラクションでユーザを選ぶ動きになっている印象があり、はるniさんの指摘は非常に的確なものであったと感じます。

2Dと3Dの境界を馴染ませる

インタラクションの大枠の方針が決まったため、詳細をブラッシュアップしていくフェーズとなります。今回特に気を払ったのは、2Dと3Dのインターフェースをいかに馴染ませるかという点です。
例えばトップページにおいては、3D空間内にある各スポットと、2D(DOM)上に配置されたカードUIが点線で繋がっています。

このとき、各スポット上で明滅する丸いアイコンは3D空間内に配置し(別の3Dオブジェクトの裏側に回るときは隠れるように)、カードと結ぶ点線は2D(DOM)上に配置する描写が最も自然であるように見えました。
また、丸いアイコンにマウスオーバーした際に現れる要素も最上位レイヤーに表示されるべきだと考えたため、2D(DOM)上に配置するようにしました。
まとめると、

  1. 3D空間内にある明滅するアイコン … WebGL
  2. 1とカードUIを結ぶ点線 … DOM
  3. 1をマウスオーバーした際に現れる要素 … DOM

といった具合に実装しています。

また、丸いアイコンは数がそれほど多いわけではないため大きな違いは出ないかと思いますが、パフォーマンス上の観点からポリゴンは使わずにポイントスプライトで実装しています。

なるべく省エネな実装を

サイトの下層ページにおいてもフッターエリアに到達すると3Dボクセルが現れ、グローバルナビゲーションのように機能するような構成をとりました。

つまりレイヤー的には、常に最下層に3D用のCanvasが敷かれているような実装となります。しかしながら、例えばお知らせ記事などを読んでいるときにも裏側にある3Dが60FPSで稼働しているとすれば、不要なマシン負荷を与えることになってしまいます。車のアイドリングストップのように、エネルギーが不要な場面では省エネモードに切り替えたいところです。

今回は、3D用のCanvasに対して以下の3つの状態を定義し、それらに応じてFPSを切り替えるような実装を行いました。

  1. 3D上でインタラクションやアニメーション等が行われており、アクティブに動いている状態 … 60FPS
  2. 3Dが画面上に表示されているものの、アクティブな動きはなくゆっくりと動いている状態 … 30FPS
  3. 3Dがページの裏に隠れている状態 … 0.5〜1FPS

3の挙動イメージについて、分かりやすさのためページを透かしてみると以下のような動きとなっています。


3D用のCanvasがページの裏に隠れるとレンダリングがほぼ静止する様子

ヘッドレスCMSとの連携

ヘッドレスCMSに入稿された画像をWebGL上で使用する

ウェブサイト内ある制作実績やお知らせ、ブログといった頻繁に更新が行われるページについてはmicroCMSと連携し、エンジニア以外の方でも更新可能な設計にしています。

このとき、制作実績のページにおいては3D空間内のモニターに実績用の画像を表示するような実装を行っていたため、microCMSから入稿された画像をWebGL上で扱う必要がありました。

しかし、microCMS上の画像をそのままWebGL上で扱おうとするとクロスドメインによるCanvas汚染のエラーが発生します。従って、同一ドメイン配下に画像リソースを移す必要がありました。
今回はSG(静的生成)の構成を採用していたため、生成時をフックとして実績ページ用の画像をウェブサイトと同じドメインのサーバーに転送する処理を行うことでエラーを回避しました。

WordPressからの移植

今回のサイトではCMSとしてmicroCMSを採用しましたが、以前のサイトではWordPressが使用されていたためコンテンツの移植を行う必要がありました。
基本的にはWordPressとmicroCMSのAPIをそれぞれ使用して移植を行っていくわけですが、WordPressのリッチエディタで入稿されたコンテンツについては同じ装飾状態を保ったままAPI経由でmicroCMS側のリッチエディタへ移植することができないため、microCMS社が紹介されている「リッチエディタを使いつつ一部はHTMLで入稿する」の構成をとりつつ、WordPressのリッチエディタコンテンツはHTMLとして移植するのが最適な落とし所になるかと思っています。

加えて、microCMSにはiframeを用いた拡張フィールドの機能があるため、これを使用してHTMLのプレビュー画面を横に並べたシンプルなエディタ画面をiframeで埋め込むことで、既存記事の再編集作業を多少安心して行いやすくなるかと考え、対応を行いました。

(ここにWordPressと同じリッチエディタをiframeで埋め込めば完璧なのでは?という不穏なことを一瞬考えましたが、思いつかなかったことにしました)

ユーザーテストによる改善

前章「インタラクション案の検討」の段階で、なるべく平易な操作方法になるよう工夫したつもりではいましたが、やはり3Dボクセルをトップページで大きく扱うという風変わりな構成を採用している以上、初見のユーザに対して適切に操作してもらえるかという懸念が個人的にはまだ完全に払拭しきれずにいました。
そこで、ウェブサイトが一度完成しリリースされた直後に知り合いの方にお声がけし、ウェブサイトを操作している様子を観察させてもらうことを依頼しました。
ウェブ関連の仕事をしている方とそうでない方にそれぞれお声がけさせていただいたのですが、特にウェブ関連の仕事をしていない方は、こちらが想定するイメージから離れた操作を頻繁に行っていました。
ユーザテストを経て改善した操作方法のうち、分かりやすい例を以下に挙げてみます。

縦方向スワイプ

元々、スマホ版においては横方向のスワイプで操作してもらうことを想定していたのですが、あるユーザの方はページを開いてすぐに縦方向へのスワイプを試みていました。通常のウェブサイトを閲覧するときの癖もあるのかもしれません。
アクセスした直後の第一印象に関わる重要な発見となったため、この時点でユーザテストをしてみて良かったと思いました。元々実装していた横方向へのスワイプ操作に加え、縦方向へのスワイプでも違和感なく回転操作が行えるように変更しました。

ズーム操作

その後しばらく3D空間内を周回した後、おもむろにスマホ画面をピンチイン・アウトするような操作をしていました。このような操作方法も私の頭にはなかったアイデアでしたが、確かに実現できた方が自由度が上がり操作性が良くなるイメージが得られたため、新たなインタラクションとして加えました。

スポットにズームインした後の戻り方

スポットのアイコンをタップしてズームインした後、俯瞰の状態に戻るためには左上にあるHOMEボタンを押してもらうことを想定していたのですが、なかなかそのボタンに気づくような気配がなく、代わりにここでも画面上をピンチアウトしたり、スワイプするような操作で戻りたがっている様子が見て取れました。(このような操作はウェブ関連の仕事をしている方にも共通していました。)
ズームインした後になかなか元の状態に戻れないのは強いストレスになり得ると思ったため、ピンチアウト・スワイプのいずれの操作においても正しく元の状態に戻ることができるように変更しました。

カード上でのマウスホイール

PC版の方では、トップページのカードの上にマウスカーソルを当て、マウスホイールを回すことでどうやらカードをめくりたがっているような様子が伺えました。当初は画面上でホイール操作を行うとカメラ位置が緩やかに回転する挙動のみを実装していたのですが、カード上にカーソルを当てながらマウスホイールを動かした際はカードが1枚単位でめくれて次のスポット位置まで進むというインタラクションを追加しました。

目の前で観察することの重要性

こうしたユーザテストを通して驚いたのは、実際に自分の目の前でユーザに操作してもらう様子を観察することで見えてくる情報量の豊かさです。これまでにも自分が携わった制作物などをメッセージ上のやりとりで友人に共有し、フィードバックをもらうというコミュニケーションはよく行っていたのですが、そのときに得られていた気付きとは質的にも量的にも全く異なるものであると感じました。
例えば前項の「スポットにズームインした後の戻り方」にて、押してほしいボタンになかなか気付いてもらえないという気づきを得ることができましたが、これがオンライン上でのコミュニケーションであり、なおかつユーザがそのボタンに気付かないまま結局やり過ごしてしまった場合、ユーザは「このボタンに気付きづらい」というフィードバックを挙げることもなく終わってしまっていたのかもしれないと思うのです。
対面でのユーザテストはお互いにコストがかかるため毎回のプロジェクトで実施するというのは現実的に難しいかもしれませんが、とりわけ今回のようなインタラクションやUIの操作性が大きな重要度を占める制作においては積極的に実施していきたいと感じました。
また、この場を借りて改めてユーザテストにご協力いただいた方々に御礼を述べたいと思います。ありがとうございました。

ChatGPTの恩恵〜番外編〜

今や散々取り上げられている話題なので敢えて言及する必要はないかもしれませんが、当プロジェクトにおいてもChatGPT(特にGPT-4)の登場により開発速度が加速しました。例えば3Dデザイナーの方からいただいたモデルデータをWeb用に最適化する際、Pythonスクリプトによる効率化が不可欠な管理状態となっていましたが、こうした軽微なスクリプトを出力してもらう用途としてChatGPTを非常に重宝しました。

また、以下のように複合的な背景知識を要する問いに対しても、割と納得感のある回答や、それに近いヒントを返してくれる点にも驚かされました。

Q.
Three.jsの実装において、アンチエイリアスを有効にするのと解像度を1.5倍にするのとではどちらが描画負荷の上昇が大きいですか?

A.
Three.jsでアンチエイリアスを有効にすることと解像度を1.5倍にすることは、どちらもGPUの負荷を高める処理ですが、一般的には解像度を1.5倍にする方が負荷が高いと言われています。

アンチエイリアスは、グラフィックスのエッジ部分のジャギーをなめらかにするための技術で、画面の全ピクセルに影響を与えるわけではありません。一方で、解像度を1.5倍にすると、画面の全ピクセル数が2.25倍になります(1.5 * 1.5 = 2.25)。これにより、テクスチャやシェーダーなどのグラフィック処理に関連するすべての計算が大幅に増加し、GPUの負荷が高まります。

特にパフォーマンスチューニングの段階において、壁打ち相手として積極的に活用しました。


以上となります。今後3Dボクセルやそれに類する3Dモデルを用いたウェブサイトを制作される方にとって、少しでも参考になれば幸いです。

サイト制作クレジット

3Dデザイン・文言執筆 … アプリファクトリーはるni 様
アートディレクション・ウェブデザイン … hydekick 様
写真撮影 … シティ ライツ カンパニー 様

Discussion