1 はじめに
スペースリーでエンジニアをしている長谷川です。 最近は3DのWebアプリの開発を担当していて、弊社でリリースしている「パノラマ変換3Dプレイヤー」のコンテンツ作成機能などを開発しています。
3DのWebアプリを開発する際には「React/Vue × WebGL系ライブラリ」という構成が代表的なケースの1つであり、弊社でもその構成を選択しました。 ところが、3D Webアプリはサービスの数としては多くないので保守運用まで考慮した設計ノウハウはまだまだ十分に共有されてはおらず、開発を進めるにあたって以下のような懸念点がありました。(詳細は次章)
- 「宣言的なReact/VueのWebアプリ」と「命令的なWebGL系ライブラリ」というスタイルの違うコードが混ざることでそれぞれの責任分担が曖昧になりやすい
- 「WebGL系ライブラリを用いた3Dアプリ」の保守運用を考慮した設計に関するノウハウが不足している
結論としては、これらの懸念はWebGLの宣言的なラッパーライブラリを使うことでおおよそ解決することができたので、記事として紹介します!
2 開発当初にあった懸念点
2.1 「宣言的なReact/VueのWebアプリ」と「命令的なWebGL系ライブラリ」というスタイルの違うコードが混ざることでそれぞれの責任分担が曖昧になりやすい
最近のWebアプリだとReact、VueやSvelteのような宣言的UIを用いたフレームワークで開発することが多いと思います。弊社のWebアプリもReactやVueを用いて、ロジックと状態の分離に励みながら宣言的なコードで実装しています。
一方で、WebGLはハードウェアに近い技術であり、命令的なコードで実装します。アプリ開発でWebGLを用いる際には、WebGLをJavaScriptから簡単に利用するためにThree.jsやBabylon.jsなどのライブラリが選択肢に上がると思いますが、それらのライブラリでは命令的なコードでの実装が必要になります。
そのため、Webアプリの宣言的UIを用いたコードと、WebGL由来(というかOpenGL ES由来)の命令的なコードの、2つのスタイルのコードを共存させる必要があります。 チーム開発、特に今回のような分野横断プロジェクトでバックグラウンドの異なるメンバーが集まる開発だと、明確な指針を設定しておかないと状態管理その他の役割が分散しがちです。そのため、適切な境界面を設定してそれぞれ領域の責任を明確にする必要があります。
2.2 「WebGL系ライブラリを用いた3Dアプリ」の保守運用を考慮した設計に関するノウハウが不足している
エンジニアの求人市場では、React/Vueエンジニアに比べてWebGL系エンジニアは少なく、チームに十分な人数のWebGL系エンジニアが在籍しているケースは稀です。 「保守運用を考慮したWebGLアプリケーションの設計ノウハウを持ったメンバー」となるとさらに採用が難しくなり、その結果、開発プロジェクトの初期はプロトタイピング多めで検討を進めながら社内に知見を貯めることになると思います。
弊社でもプロジェクト初期において、WebGL系のコードを書けるメンバーは何人かいましたが、サービスでWebGL系のアプリを運用した経験のあるメンバーはおらず、設計についてはかなり手探りで進めました。 (宣言的ラッパーライブラリ利用以外にも、Vue側から分離されたThree.jsのコードにおいて、TheCleanArchitectureの枠組みを踏襲したり、オブジェクト指向でVue側とは異なる枠組みで実装するパターンもやっていたが、チーム全員が設計を把握するのは難しく属人的になりがちだった)
3 宣言的なWebGLのラッパーライブラリの効果
先述のような問題に対して、Three.jsやBabylon.jsなどのライブラリを宣言的に扱えるようなラッパーライブラリがいくつか開発されています。 例えば、Three.jsだとReact Three FiberやA-Frameなどが人気かと思います。
ラッパーライブラリを使わずにThree.jsをそのままVueで扱おうとすると以下のようなコードが書けます。
<template> <div ref="containerRef" /> </template> ... const containerRef: Ref = ref(null); const camera = ... const scene = ... const geometry = ... const material = ... const renderer = new THREE.WebGLRenderer(...); renderer.setSize(width, height); renderer.setAnimationLoop( animation ); const animation = () => { camera.bar = ... ... renderer.render( scene, camera ); } containerRef.appendChild( renderer.domElement ); ...
A-Frameのような宣言的なラッパーライブラリを導入すると以下のようなコードになります。
<template> <a-scene ref="containerRef" ... > <a-entity :position="aframeCameraPosition" :rotation="aframeCameraRotation"> <a-camera ref="mainCamera" ... /> </a-entity> </a-entity> <a-box ... /> </a-entity> <a-scene/> </template> ... const aframeCameraPosition: Ref<Vector3> = ref({x: 0, y: 0, z: 0}) const aframeCameraRotation: Ref<Vector3> = ref({x: 0, y: 0, z: 0}) ...
このような宣言的なラッパーライブラリがもたらす境界面の変化を図で表すと、以下のようになります。 手続き的な実装と宣言的な実装の境界面を押し上げることで、Sceneや各GeometryオブジェクトやCamera, Lightなどを宣言的に扱えるようにしていると言えます。
これにより、WebGL系ライブラリを用いた3DアプリでもReact/Vueの保守運用の枠組みで扱える領域が広がり、対応できる開発メンバーを増やすことができます。 また、宣言的UIでの実装における状態とロジックの分離のノリで、3D系のロジックを分離するような実装にできるので、境界面についてチーム内で認識が合わせやすい状態になり1つ目の懸念点は解消されました。
2つ目の懸念点であった、"「WebGL系ライブラリを用いた3Dアプリ」の保守運用を考慮した設計に関するノウハウ不足"についても、他のWebアプリと同様の設計で扱えるようになるので、3Dアプリだからという点がネックになることもほぼなくなり、他のReact/Vueアプリの設計の知見がそのまま使えるようになりました。
4 まとめ
3DのWebアプリを「React/Vue × WebGL系ライブラリ」の構成で開発する際には、宣言的なラッパーライブラリを試してみることをおすすめします。よく言われる「実装が容易」というメリットだけではなくサービスの保守運用という観点でも大きなメリットを感じられると思います!
注意点として、パフォーマンスの問題など、ラッパーライブラリだと対応しきれない機能を扱いたいケースについては、Three.jsやBabylon.jsでそのまま書く必要があります。しかし、宣言的UIでのロジックの分離のメリットが活きてくるので、多くのケースでは設計方針を変えることなく対応できるはずです。
(技術の選択はチーム構成にも依存しますし、アプリケーションで求められている機能・性能も当然考慮が必要なので、参考ケースの1つとして捉えてもらえると幸いです!)
最後に
スペースリーでは開発メンバーを募集中です! 詳しくは採用サイトをご覧ください!