A-Frameで雪が降りそそぐ街の景色を作ってみました。クリスマスツリーもね!
A-Frameで雪が降りそそぐ街の景色を作ってみました。クリスマスツリーもね!:
最近すっかり寒くなりましたね!
東京の雪はまだだけど、クリスマスも近いし、せっかくなので大きなクリスマスツリーがそびえ立つ街に、雪が降りそそぐ景色を作ってみました。
Christmas Scenery 2018 by A-Frame on Vimeo
実際はクリスマスツリー風の何かですが(笑)、雰囲気勝負です。フリープランのCODEPENで再現したかったので、objファイルなどの外部ファイルを使用せず、プリミティブオブジェクトだけで作成したので、少しマインクラフト風にも見えますね。
See the Pen Christmas Scenery 2018 by Takayasu.B (@untspringk) on CodePen.
今回使用したのは「A-Frame」というMozilla VRチームが手がけるTHREE.jsをベースにしたWebVRフレームワークです。ただ今回は、WebVRのためというよりも、デスクトップPC向けのWebGLコンテンツを作るにあたって、A-Frameの持つ利便性に注目してみました。
(※実際、最適化も何もしていない冒頭のサンプルはスマフォでは重すぎて、VRモードはおろか、動作すらしないと思います。。)
この記事は、冒頭で紹介したサンプル作品を制作する過程を紹介しながら、印象に残ったポイントをいくつかピックアップして、適宜解説するといった内容になります。とくにA-Frame Inspectorは、多少不便や不具合はあるものの、非常に便利な機能だと思いましたので、最初に簡単な使い方だけご紹介いたします。
※A-Frameの概要や使い方については、下記の記事で紹介してくださっています。
A-Frameを使った基本的なデモの作成 | MDN
新しいWebVRフレームワークA-Frame入門 - Qiita
VR表現をカンタンに実装できる?「A-Frame」であそんでみたよ! | 東京上野のWeb制作会社LIG
A-Frame Inspectorは、A-Frameのシーン上のオブジェクトを視覚的に管理できる機能で、A-Frameに組み込まれています。ただ、A-Frameの最新版(2018年12月現在、v0.8.0)に組み込まれたA-Frame Inspectorは、私の環境では正常に動作しないことが多々あったため、今回は一つ前のA-Frame(v0.7.0)で進めていきます。
まず、A-Frame公式の「Getting Started」へ行き、サンプルコードを実行してみましょう。実行前に読み込むA-Frameのバージョンを、0.7に変更します。
index.htmlをブラウザで実行すると、下図のように表示されます。これだけで3Dオブジェクトがスクリーンに表示されるだけでなく、マウスドラッグで視点を自由に変えることもできます。
さらに右下にあるメガネのアイコンをクリックすると全画面表示になり、VRモードになります。このページをスマフォで確認すると下図のような画面になり、GearVRやCardboard、ハコスコなどのVRゴーグルで、VRコンテンツを体験することができます。
javascriptを一切記述することなく、HTMLタグだけで、これだけのことができてしまうことから、A-Frameがいかに手軽にWebVRコンテンツを作成することができるかが分かると思います。ここで、A-Frame Inspectorを起動して、これらの3Dオブジェクトを編集してみます。
ちなみにA-Frameでは、A-Frameシーン(a-scene)上のオブジェクトを「エンティティ」と定義しているので、以後の記事中は、A-Frameの3Dオブジェクトのことをエンティティと記載します。
A-Frame Inspectorは、A-Frameで作成したシーン(ブラウザ)上で、[control] + [option] + [i](※Windowsは、[Ctrl] + [Alt] + [i])を同時に押すと起動し、上図のような画面に切り替わります。左側のScene Graph上のエンティティ名を適当に選択すると、画面右側にComponents panelが表示されます。それぞれの役割を簡単に説明します。
画面左側のパネルで、シーン(a-scene)上に配置されたエンティティ(entity)の階層(親子)関係を確認できるほか、エンティティの新規作成、編集、削除、複製などが可能です。
画面右側のパネルで、選択したエンティティの位置、回転、スケールなどの基本プロパティの他、付与されているコンポーネント(entity-component)の設定を追加、編集、削除が可能です。
A-Frameはエンティティに独自の機能を持ったコンポーネントを付与することで、そのエンティティの形状や性質を設定するEntity-Component-Systemを採用しています。Unityに似たシステムです。
画面のパネル以外の領域で、シーン上のエンティティを俯瞰して確認しつつ、移動、回転や拡大操作が可能です。Viewport上での操作一覧を下の表にまとめましたが、この操作一覧は、iMac、英文キーボード、左・中・右クリックが可能なマウスを使用した環境で検証したものなので、一部環境では動作しない可能性があります。
一部環境では動作しない操作があるかもしれません
Unityや3D制作オーサリングソフトに近い操作感なのですが、私の環境では、操作の取り消し([Ctrl] + [z])がうまく機能しなかったり、エンティティのロックができない?など、現段階ではまだ改善してほしいところが多々あります。とはいえ、現状でもかなり便利な機能だと思います。
Scene Graphのシリンダーオブジェクト(<a-cylinder>)を選択し、編集してからコードを取得してみます。
エディタに戻って、クリップボードにコピーしたコードをペーストして内容を確認します。
コードは(見やすいように適宜改行していますが、)上図のようになります。このA-Frame独自タグと属性に関しては後述しますが、A-Frame Inspector上で調整したシリンダーオブジェクトのy座標の値が、position属性の値に反映されているのが分かります。
index.htmlに記述してあった元の<a-cylinder>タグの内容を、A-Frame Inspectorからコピーしてきたコードに差し替えて、ページをリロードして表示を確認してみます。
上図のように、シリンダーオブジェクトが元にあった場所よりも上に配置されたのが確認できたと思います。このように3Dオブジェクトを視覚的に確認しながらコンテンツを作成できる仕組みが用意されているのは、とても助かりますね。
A-Frame Inspectorは、モーションキャプチャやコードの自動同期など、この他にも非常に多くの機能を有していますが、今回は作品に使用する部品の作成や、シーンの状況確認用として利用していきたいと思います。その他の機能に関しては、下記公式ページを参考にしてください。
Visual Inspector & Dev Tools – A-Frame
シーンに配置するアセットをA-Frame Inspectorで作成します。作品で使用するアセットは以下の3つです。とはいえ、ツリー以外は単なるBoxとSphereです。
先ほどサンプル用に記述したコードを削除して(※下図コード参照)、一旦シーン上のオブジェクトを空っぽにした後、A-Frame Inspectorを立ち上げて部品となる3Dオブジェクトを作成していきます。
一番大変なツリーを作成していきます。上図gifアニメーションは、A-Frame Inspectorで、エンティティの作成、編集を行なっている画面ですが、分かりづらいと思いますので、順を追って説明していきます。
画面左のScene Graphの上にある「+(プラス)」ボタンをクリックすると、Scene Graphのツリーにエンティティ(<a-entity>)が追加されます。この時点ではシーン上には何も表示されませんので、このエンティティに各種コンポーネント(Component)を追加して肉付けしていきます。
先ほど作成したエンティティを選択すると、画面右にComponent Panelが表示されますが、デフォルトでは位置情報などの最低限のプロパティしか設定されていません。パネル上にある「Add Component」メニューをクリックして、「geometry」と入力して、Geometryコンポーネントを追加します。
多くの設定項目がありますが、ここでは「Primitive」プロパティを「Box(デフォルト)」に設定すると、Viewport上に白色のBox(立方体)が表示されます。
続いて再度「Add Component」メニューから「material」と入力して、Materialコンポーネントを追加します。ここも多くの設定項目がありますが、ここでは「Color」プロパティを選択し、ポップアップされるカラーガイドウィンドウから緑色など適宜色を選択します。
移動、回転、拡大、それぞれのツールを切り替えながら、Viewport上のエンティティを変形することができます。またComponent PanelのPosition(位置)、Rotation(回転)、Scale(拡大縮小)プロパティのそれぞれの数値を変更することでも調整可能です。
今作成したエンティティと同じ設定のエンティティを複製するには、Scene Graphのエンティティ名の右横にあるクローンアイコンをクリックします。複製したエンティティはViewport上では全く同じ場所に生成されるので、位置を少しずらして調整していきます。
上述のフローを繰り返して、ようやく大きな樹(笑)が完成しました。出来はともかく、30分ほどでこのようなモデルが作成できるのは、視覚的に操作が可能なツールが用意されているからでしょう。コードだけで作成すると、より多くの時間を費やすことになると思います。
ここで注意したいのは、この状態ではまだコードに反映されたわけではないので、ブラウザをリロードしてしまうと作成前の状態に戻ってしまうことです。今回はInspectorとコードの自動同期(aframe-watcher)を導入していないので、Inspectorでエンティティを作成したら、都度そのコードをコピーして実際のコードにペーストして反映するようにしていきます。
上記コードを参照すると分かるように、大きな樹は大量のBoxエンティティ(<a-box>)で構成されています。これら大量のBoxエンティティを管理しやすいようにグループ化することができます。
これもUnityなどのオーサリングツールを使用したことがある人は直感的に分かると思いますが、要素(エンティティ)を入れ子にすることで、親子関係になり、子要素は親要素のプロパティの一部(位置、回転、拡大など)の影響を受けます。
このグループ化の操作はA-Frame Inspectorでは現状対応していない?ようなので、コードエディタ上でコードを編集します。
このグループ化作業のように、A-Frame Inspectorで一部操作が対応していない場合や、コードで編集した方が早い作業はコードエディタでおこなうなど、作業内容によって、臨機応変にツール切り替えて行っていけば、効率よく制作を進めることができるでしょう。
特にグループ化作業は重要で、ある程度Inspectorでエンティティを作成したら、エディタにコードを貼り付けてグループ化し、Inspectorに戻る。といったことを繰り返していくと良いと思います。いつか、A-Frame Inspector上でグループ化ができたり、コードの同期機能が安定すれば、この辺りの作業も必要なくなるかもしれません。
大きな樹そのままでも素朴で良いのですが、せっかくのクリスマスシーズンなので、樹のてっぺんや枝葉にオーナメント風なものを飾りたいと思います。
樹のてっぺんの飾りは星型が良かったのですが、あいにく用意されていなかったので、octahedron(八面体)を選びました。そのてっぺんの飾りの少し下あたりにライト(<a-light>)を配置します。ライトの種類(type)は「Point Light」です。Component PanelのLIGHTコンポーネントのtypeプロパティから選択できます。
Point Lightはその周辺のエンティティに対して光を当てる効果があるので、電飾ライトに向いています。明滅の効果は同じくLIGHTコンポーネントのintensityプロパティの値を増減させることで実現します。これにはアニメーションの実装が必要になるので、コードエディタで編集を行います。
A-Frame Animationは、アニメーションの対象となるエンティティの子要素のエンティティとして記述します。つまり上記コードのようにエンティティが入れ子の状態になります。
Animationエンティティのそれぞれの設定の詳細は今回は割愛しますが、上記コードで設定していることは、
少しアニメーションを取り入れただけですが、結構良い感じになりましたね。
オーナメントは単なるSphereです。MaterialコンポーネントのColorプロパティで色々な色を設定して、適当に配置しました。少しぐらいズレていても、見えない糸で吊るされている感じがして良いと思います。
ブリンクライトは前述のてっぺん飾りの下に追加したPoint Lightを複製して、ライトの色を変え、アニメーションの遅延実行(delay)の値を少し変えて、色々な光が交差していく感じを演出してみました。ライトは少し制御が難しいですが、視覚的に与える効果も大きいので、演出の作り込みには重要な作業ですね。
以上の作業を終え、章冒頭のgifアニメーションのような状態になりました。樹はアレですが、ライティングを加えることで結構良い感じになったと思います。
ここからは建物の生成、雪を降らす、カメラを回転移動と注視など、主にJavascriptを使用した実装が主になります。私がA-Frameの知見が少ないこともあって、A-Frameの良さが生かせてない実装になっています。また実装に関する説明もだいぶ省略していますので、実装の流れの参考程度にご覧いただければと思います。
建物と雪はこれまでのように手付けでエンティティを配置するわけではなく、Javascriptで動的にエンティティを配置します。Javascript使用して、A-Frameのエンティティを扱う場合は基本的に以下の原則があります。
まずは、建物のエンティティをBoxで作成し、DOM APIでJavascriptのオブジェクトとして取得します。このエンティティをサンプルとして、自動生成の際にサイズを変形、生成位置をランダムに設定します。
今回はBoxエンティティを、<a-box>ではなく、<a-entity>タグで作成しています。このタグのgeometry属性のprimitiveプロパティに「box」を指定することで、箱型のエンティティが作成されます。その他の設定については、公式ページのgeometryを参考にしてみてください。また、このエンティティを取得するために、<a-entity>タグのid属性に「building」を設定しました。
これをJavascriptで取得します。
次に、取得したオブジェクトをオリジナルとして、これをクローンしたエンティティを生成するためのベースとなるJavascriptのクラス「SpawnObject」クラスを作成します。
エンティティを複製してシーン上に配置するベースクラスです。
SpawnObjectクラスを継承して、建物用に特化したBuildingクラスを作成します。Buildingクラスは、シーン中央に配置されるクリスマスツリーの周囲に建物が生成されないようにするための処理や、クリスマスツリーから離れて配置される建物は、近くの建物に比べて色を徐々に暗くするなどの処理を行なっています。
雪の生成は建物の生成と同様に、オリジナルとなるエンティティ(a-sphere)を作成し、SpawnObjectクラスを継承した、Snowクラスで生成と降雪処理を実行します。
SnowクラスはSpawn Objectクラスを継承します。Buildingクラスと異なるのは、雪が降るモーションを実現するために、毎フレームごとに再計算された位置情報でエンティティを再描画する処理を追加する点です。
Cameraエンティティ(a-camera)は、移動や回転に関しては、Cameraエンティティ自体に設定するよりも、制御用の親エンティティに入れ込んだ方がやりやすいので、エンティティの構造を下図のように設定します。
これをいつものようにJavascriptで取得し、カメラ制御用のObject型変数を作成します。
これでカメラが画面中央を注視しながら、その周りをふよふよ周回するようになりました。
こういったムーディな(笑)コンテンツは、なんとなく音楽をつけたくなりますよね。無料プランのCODEPENではファイルをアップロードすることができないので諦めかけていたのですが、ファイル選択フォーム(<input type="file">)を使用すれば、ローカルからファイルをアップロードして、音楽を流せることを知りました。
画面左側の方でうっすらと「ファイルを選択」とありますので、お使いのPCからお気に入りの音楽を選択してみてください。全体的にスローモーションなコンテンツなので、アンビエントな曲調があうかな?と思います。
冒頭のVimeoの動画は、ISAo.さんの「星粒が降る夜」を使用させていただきました。とても素敵な曲です。
フリーBGM素材 『星粒が降る夜』 試聴ページ|フリーBGM DOVA-SYNDROME
本記事は以上になります。後半のスクリプティングの部分はろくな説明もなく、コードを掲載するだけの雑なつくりになってしまいましたが、折をみて、加筆修正したいと思います。
A-Frameは、今回の記事のために初めてちゃんと触ってみましたが、とても便利だと思いました。WebGLを素で扱えたり、THREE.jsやbabylon.jsなどのライブラリを使用して制作ができる人にとっては、A-Frameを使用する必要はあまりないかもしれませんが、手軽にコンテンツを作成・デバッグする目的でも役立ちそうです。
また、今回は使用しませんでしたが、A-FrameのEntity Component Systemを使いこなせば、描画処理(View)と、ロジック(Model)や描画制御(ViewModel)をうまく切り分けた実装が可能になるかと思いました。
とにかく、WebGLやWebVRコンテンツ制作の敷居を下げてくれているのは確実だと思いますので、今後これらの技術を使用したリッチなコンテンツが増えていくと良いなと思います。
それでは、また。良い年末を!
最近すっかり寒くなりましたね!
東京の雪はまだだけど、クリスマスも近いし、せっかくなので大きなクリスマスツリーがそびえ立つ街に、雪が降りそそぐ景色を作ってみました。
Christmas Scenery 2018 by A-Frame on Vimeo
実際はクリスマスツリー風の何かですが(笑)、雰囲気勝負です。フリープランのCODEPENで再現したかったので、objファイルなどの外部ファイルを使用せず、プリミティブオブジェクトだけで作成したので、少しマインクラフト風にも見えますね。
See the Pen Christmas Scenery 2018 by Takayasu.B (@untspringk) on CodePen.
今回使用したのは「A-Frame」というMozilla VRチームが手がけるTHREE.jsをベースにしたWebVRフレームワークです。ただ今回は、WebVRのためというよりも、デスクトップPC向けのWebGLコンテンツを作るにあたって、A-Frameの持つ利便性に注目してみました。
(※実際、最適化も何もしていない冒頭のサンプルはスマフォでは重すぎて、VRモードはおろか、動作すらしないと思います。。)
この記事は、冒頭で紹介したサンプル作品を制作する過程を紹介しながら、印象に残ったポイントをいくつかピックアップして、適宜解説するといった内容になります。とくにA-Frame Inspectorは、多少不便や不具合はあるものの、非常に便利な機能だと思いましたので、最初に簡単な使い方だけご紹介いたします。
※A-Frameの概要や使い方については、下記の記事で紹介してくださっています。
A-Frameを使った基本的なデモの作成 | MDN
新しいWebVRフレームワークA-Frame入門 - Qiita
VR表現をカンタンに実装できる?「A-Frame」であそんでみたよ! | 東京上野のWeb制作会社LIG
A-Frame Inspectorの使い方
A-Frame Inspectorは、A-Frameのシーン上のオブジェクトを視覚的に管理できる機能で、A-Frameに組み込まれています。ただ、A-Frameの最新版(2018年12月現在、v0.8.0)に組み込まれたA-Frame Inspectorは、私の環境では正常に動作しないことが多々あったため、今回は一つ前のA-Frame(v0.7.0)で進めていきます。まず、A-Frame公式の「Getting Started」へ行き、サンプルコードを実行してみましょう。実行前に読み込むA-Frameのバージョンを、0.7に変更します。
index.html
<html> <head> /* 読み込むA-Frameのバージョンを、0.7.0に変更 */ <script src="https://aframe.io/releases/0.7.0/aframe.min.js"></script> </head> <body> <a-scene> <a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9"></a-box> <a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E"></a-sphere> <a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D"></a-cylinder> <a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4"></a-plane> <a-sky color="#ECECEC"></a-sky> </a-scene> </body> </html>
さらに右下にあるメガネのアイコンをクリックすると全画面表示になり、VRモードになります。このページをスマフォで確認すると下図のような画面になり、GearVRやCardboard、ハコスコなどのVRゴーグルで、VRコンテンツを体験することができます。
javascriptを一切記述することなく、HTMLタグだけで、これだけのことができてしまうことから、A-Frameがいかに手軽にWebVRコンテンツを作成することができるかが分かると思います。ここで、A-Frame Inspectorを起動して、これらの3Dオブジェクトを編集してみます。
ちなみにA-Frameでは、A-Frameシーン(a-scene)上のオブジェクトを「エンティティ」と定義しているので、以後の記事中は、A-Frameの3Dオブジェクトのことをエンティティと記載します。
A-Frame Inspector概要
A-Frame Inspectorは、A-Frameで作成したシーン(ブラウザ)上で、[control] + [option] + [i](※Windowsは、[Ctrl] + [Alt] + [i])を同時に押すと起動し、上図のような画面に切り替わります。左側のScene Graph上のエンティティ名を適当に選択すると、画面右側にComponents panelが表示されます。それぞれの役割を簡単に説明します。
Scene Graph
画面左側のパネルで、シーン(a-scene)上に配置されたエンティティ(entity)の階層(親子)関係を確認できるほか、エンティティの新規作成、編集、削除、複製などが可能です。
Components panel
画面右側のパネルで、選択したエンティティの位置、回転、スケールなどの基本プロパティの他、付与されているコンポーネント(entity-component)の設定を追加、編集、削除が可能です。A-Frameはエンティティに独自の機能を持ったコンポーネントを付与することで、そのエンティティの形状や性質を設定するEntity-Component-Systemを採用しています。Unityに似たシステムです。
Viewport
画面のパネル以外の領域で、シーン上のエンティティを俯瞰して確認しつつ、移動、回転や拡大操作が可能です。Viewport上での操作一覧を下の表にまとめましたが、この操作一覧は、iMac、英文キーボード、左・中・右クリックが可能なマウスを使用した環境で検証したものなので、一部環境では動作しない可能性があります。
Viewport操作一覧
一部環境では動作しない操作があるかもしれません操作・キー | 概要 |
---|---|
左クリック | オブジェクトなど選択 |
左クリック + ドラッグ | 視点回転 |
右クリック + ドラッグ | 視点上下左右パン |
中クリック + ドラッグ | 視点高速前進・後退 |
w | 移動ツール |
e | 回転ツール |
r | 拡大ツール |
` | Scene Graph、Components Panel表示切り替え |
1 | Scene Graph表示切り替え |
2 | Components Panel表示切り替え |
A-Frame Inspectorで編集、コードを取得
Scene Graphのシリンダーオブジェクト(<a-cylinder>)を選択し、編集してからコードを取得してみます。エディタに戻って、クリップボードにコピーしたコードをペーストして内容を確認します。
index.html
<a-cylinder position="1 2.863505048034185 -3" radius="0.5" height="1.5" color="#FFC65D" material="" geometry=""> </a-cylinder>
index.htmlに記述してあった元の<a-cylinder>タグの内容を、A-Frame Inspectorからコピーしてきたコードに差し替えて、ページをリロードして表示を確認してみます。
上図のように、シリンダーオブジェクトが元にあった場所よりも上に配置されたのが確認できたと思います。このように3Dオブジェクトを視覚的に確認しながらコンテンツを作成できる仕組みが用意されているのは、とても助かりますね。
A-Frame Inspectorは、モーションキャプチャやコードの自動同期など、この他にも非常に多くの機能を有していますが、今回は作品に使用する部品の作成や、シーンの状況確認用として利用していきたいと思います。その他の機能に関しては、下記公式ページを参考にしてください。
Visual Inspector & Dev Tools – A-Frame
シーンで使用するアセット(部品)を作成する
シーンに配置するアセットをA-Frame Inspectorで作成します。作品で使用するアセットは以下の3つです。とはいえ、ツリー以外は単なるBoxとSphereです。- クリスマスツリー風の大きな樹
- 四角い建物
- 雪
先ほどサンプル用に記述したコードを削除して(※下図コード参照)、一旦シーン上のオブジェクトを空っぽにした後、A-Frame Inspectorを立ち上げて部品となる3Dオブジェクトを作成していきます。
index.html
<script src="https://aframe.io/releases/0.7.0/aframe.min.js"></script> <body> <a-scene id="viewport"> </a-scene> </body>
クリスマスツリー風な大きな樹を作成する
一番大変なツリーを作成していきます。上図gifアニメーションは、A-Frame Inspectorで、エンティティの作成、編集を行なっている画面ですが、分かりづらいと思いますので、順を追って説明していきます。
エンティティ(<a-entity>)の作成
画面左のScene Graphの上にある「+(プラス)」ボタンをクリックすると、Scene Graphのツリーにエンティティ(<a-entity>)が追加されます。この時点ではシーン上には何も表示されませんので、このエンティティに各種コンポーネント(Component)を追加して肉付けしていきます。
Geometryコンポーネントの追加
先ほど作成したエンティティを選択すると、画面右にComponent Panelが表示されますが、デフォルトでは位置情報などの最低限のプロパティしか設定されていません。パネル上にある「Add Component」メニューをクリックして、「geometry」と入力して、Geometryコンポーネントを追加します。多くの設定項目がありますが、ここでは「Primitive」プロパティを「Box(デフォルト)」に設定すると、Viewport上に白色のBox(立方体)が表示されます。
Materialコンポーネントの追加
続いて再度「Add Component」メニューから「material」と入力して、Materialコンポーネントを追加します。ここも多くの設定項目がありますが、ここでは「Color」プロパティを選択し、ポップアップされるカラーガイドウィンドウから緑色など適宜色を選択します。
エンティティの変形と複製
移動、回転、拡大、それぞれのツールを切り替えながら、Viewport上のエンティティを変形することができます。またComponent PanelのPosition(位置)、Rotation(回転)、Scale(拡大縮小)プロパティのそれぞれの数値を変更することでも調整可能です。今作成したエンティティと同じ設定のエンティティを複製するには、Scene Graphのエンティティ名の右横にあるクローンアイコンをクリックします。複製したエンティティはViewport上では全く同じ場所に生成されるので、位置を少しずらして調整していきます。
大きな樹のモデル完成、コードを取得
上述のフローを繰り返して、ようやく大きな樹(笑)が完成しました。出来はともかく、30分ほどでこのようなモデルが作成できるのは、視覚的に操作が可能なツールが用意されているからでしょう。コードだけで作成すると、より多くの時間を費やすことになると思います。
ここで注意したいのは、この状態ではまだコードに反映されたわけではないので、ブラウザをリロードしてしまうと作成前の状態に戻ってしまうことです。今回はInspectorとコードの自動同期(aframe-watcher)を導入していないので、Inspectorでエンティティを作成したら、都度そのコードをコピーして実際のコードにペーストして反映するようにしていきます。
index.html
/*(抜粋)*/ <a-entity id="tree_xmas_parts" position="0 1 0"> <a-entity id="xmas_parts1"> <a-box material="color:#4cca10" geometry="" position="-0.04 1.374 -0.284" scale="1 0.793 1"></a-box><a-box material="color:#4cca10" position="0.044 0.844 0.53" scale="1 0.43 1"></a-box><a-box material="color:#4cca10" position="-0.876 0.932 -0.275" scale="1.172 0.556 1" geometry=""></a-box><a-box material="color:#4cca10" position="-1.278 0.527 -0.275" scale="1.172 0.556 1"></a-box><a-box material="color:#4cca10" position="1.016 1.023 0" scale="1.063 0.59 1" geometry=""></a-box><a-box material="color:#4cca10" position="0.361 0.361 -0.47" scale="1.561 0.491 1"></a-box><a-box material="color:#4cca10" position="0.085 0.841 -0.977" scale="1.561 0.575 1"></a-box> <a-box id="node1" material="color:#a44523"></a-box> </a-entity> <a-entity id="xmas_parts2" position="-0.456 1.709 -0.266" rotation="0 -156.704 0"> <a-box material="color:#4cca10" position="-0.216 1.161 -0.359" scale="1 0.618 0.773"></a-box><a-box material="color:#4cca10" position="0.044 0.844 0.53" scale="1 0.43 1"></a-box><a-box material="color:#4cca10" position="-0.876 0.675 -0.275" scale="1.172 0.556 1"></a-box><a-box material="color:#4cca10" position="-1.064 0.527 -0.722" scale="1.172 0.556 1" geometry=""></a-box><a-box material="color:#4cca10" position="-0.031 0.205 -0.686" scale="1.172 0.556 1" rotation="0 -39.99245410013146 0"></a-box><a-box material="color:#4cca10" position="0.697 1.023 -0.137" scale="1.063 0.59 1"></a-box><a-box material="color:#4cca10" position="0.6 0.361 0.6" scale="1.231 0.491 1" rotation="0 -40.90918657234078 0"></a-box><a-box material="color:#4cca10" position="0.761 0.406 -0.435" scale="1.231 0.491 1"></a-box><a-box material="color:#4cca10" position="0.451 -0.396 -0.942" scale="1.231 0.491 1"></a-box><a-box material="color:#4cca10" position="-0.579 1.237 0.567" scale="0.894 0.329 0.573"></a-box> <a-box id="node2" material="color:#a44523" position="-0.181 0 -0.28" rotation="0 -13.349916626548183 0" scale="0.912 1.313 1"></a-box> </a-entity> <a-entity id="xmas_parts3" position="-0.456 3.408 -0.345" rotation="0 -172.575 0"> <a-box material="color:#4cca10" position="-0.26 1.374 -0.278" scale="0.613 0.631 0.432" geometry=""></a-box><a-box material="color:#4cca10" position="0.223 1.023 -0.532" scale="0.613 0.631 0.432"></a-box><a-box material="color:#4cca10" position="0.319 0.844 0.168" scale="0.656 0.43 0.514" rotation="0 -29.621918008263563 0"></a-box><a-box material="color:#4cca10" position="-0.224 0.932 -0.029" scale="0.761 0.556 0.609" rotation="0 -21.772396214971284 0" geometry=""></a-box><a-box material="color:#4cca10" position="-0.462 0.932 -0.665" scale="0.616 0.456 0.609" rotation="0 -124.15995420484938 0"></a-box><a-box material="color:#4cca10" position="-0.905 0.527 -0.555" scale="0.897 0.556 0.665" geometry=""></a-box><a-box material="color:#4cca10" position="-0.456 0.209 0.013" scale="0.897 0.43 0.665"></a-box><a-box material="color:#4cca10" position="0.382 0.722 -0.373" scale="1.063 0.411 0.719"></a-box><a-box material="color:#4cca10" position="0.775 0.376 -0.34" scale="0.897 0.381 0.517" geometry=""></a-box><a-box material="color:#4cca10" position="0.413 -0.004 0.14" scale="0.73 0.381 0.866" rotation="0 -45.664736271926614 0"></a-box><a-box material="color:#4cca10" position="0.403 0.227 -0.787" scale="0.897 0.381 0.517" rotation="0 31.168904055116787 0"></a-box><a-box material="color:#4cca10" position="-0.319 0.227 -0.881" scale="0.897 0.381 0.517" rotation="0 131.89488443911551 0"></a-box><a-box material="color:#4cca10" position="-0.345 0.841 0.233" scale="0.712 0.329 0.86"></a-box> <a-box id="node3" material="color:#a44523" position="-0.181 -0.062 -0.317" scale="0.596 1.472 0.455"></a-box> </a-entity> <a-entity id="xmas_parts4" position="-0.031 4.738 -0.294" rotation="0 90 0" scale="0.808 0.808 0.808"> <a-box material="color:#4cca10" position="-0.098 1.374 -0.278" scale="0.613 0.631 0.432"></a-box><a-box material="color:#4cca10" position="0.223 1.023 -0.532" scale="0.613 0.631 0.432"></a-box><a-box material="color:#4cca10" position="0.319 0.844 0.168" scale="0.656 0.43 0.514" rotation="0 -29.621918008263563 0"></a-box><a-box material="color:#4cca10" position="-0.224 0.932 -0.029" scale="0.761 0.556 0.609" rotation="0 -21.772396214971284 0"></a-box><a-box material="color:#4cca10" position="-0.462 0.932 -0.665" scale="0.616 0.456 0.609" rotation="0 -124.15995420484938 0"></a-box><a-box material="color:#4cca10" position="-0.68 0.527 -0.555" scale="0.897 0.556 0.665"></a-box><a-box material="color:#4cca10" position="-0.456 0.209 0.013" scale="0.897 0.43 0.665"></a-box><a-box material="color:#4cca10" position="0.382 0.722 -0.373" scale="1.063 0.411 0.719"></a-box><a-box material="color:#4cca10" position="0.775 0.376 -0.34" scale="0.897 0.381 0.517"></a-box><a-box material="color:#4cca10" position="0.413 -0.004 0.14" scale="0.73 0.381 0.866" rotation="0 -45.664736271926614 0"></a-box><a-box material="color:#4cca10" position="0.403 0.227 -0.787" scale="0.897 0.381 0.517" rotation="0 31.168904055116787 0"></a-box><a-box material="color:#4cca10" position="-0.319 0.227 -0.881" scale="0.897 0.381 0.517" rotation="0 131.89488443911551 0"></a-box><a-box material="color:#4cca10" position="-0.345 0.841 0.233" scale="0.712 0.329 0.86"></a-box> <a-box id="node4" material="color:#a44523" position="-0.181 -0.062 -0.317" scale="0.596 1.472 0.455"></a-box> </a-entity> </a-entity>
エンティティのグループ化
上記コードを参照すると分かるように、大きな樹は大量のBoxエンティティ(<a-box>)で構成されています。これら大量のBoxエンティティを管理しやすいようにグループ化することができます。これもUnityなどのオーサリングツールを使用したことがある人は直感的に分かると思いますが、要素(エンティティ)を入れ子にすることで、親子関係になり、子要素は親要素のプロパティの一部(位置、回転、拡大など)の影響を受けます。
このグループ化の操作はA-Frame Inspectorでは現状対応していない?ようなので、コードエディタ上でコードを編集します。
エンティティのグループ化の例
<a-entity position="0 1 0" /* 子要素は親要素の設定の影響を受ける */ > /* 子要素(エンティティ) */ <a-box></a-box> </a-entity>
特にグループ化作業は重要で、ある程度Inspectorでエンティティを作成したら、エディタにコードを貼り付けてグループ化し、Inspectorに戻る。といったことを繰り返していくと良いと思います。いつか、A-Frame Inspector上でグループ化ができたり、コードの同期機能が安定すれば、この辺りの作業も必要なくなるかもしれません。
ツリーに飾り付けとライティング
大きな樹そのままでも素朴で良いのですが、せっかくのクリスマスシーズンなので、樹のてっぺんや枝葉にオーナメント風なものを飾りたいと思います。
- 樹のてっぺんの飾り + ブリンク(明滅)ライト
- オーナメント(※単なるSphere)適量
- ブリンクライト4つぐらい
樹のてっぺんの飾りつけとライトの追加
樹のてっぺんの飾りは星型が良かったのですが、あいにく用意されていなかったので、octahedron(八面体)を選びました。そのてっぺんの飾りの少し下あたりにライト(<a-light>)を配置します。ライトの種類(type)は「Point Light」です。Component PanelのLIGHTコンポーネントのtypeプロパティから選択できます。
Point Lightはその周辺のエンティティに対して光を当てる効果があるので、電飾ライトに向いています。明滅の効果は同じくLIGHTコンポーネントのintensityプロパティの値を増減させることで実現します。これにはアニメーションの実装が必要になるので、コードエディタで編集を行います。
Animationの実装例
<a-light id="light_top_blink" type="point" intensity="1.89" position="-0.234 7 -0.175" scale="0.378 0.378 0.378" > <a-animation attribute="light.intensity" from="0" to="4" dur="2000" delay="0" direction="alternate" repeat="indefinite"> </a-animation> </a-light>
Animationエンティティのそれぞれの設定の詳細は今回は割愛しますが、上記コードで設定していることは、
- Lightエンティティのintensityの値を連続変化させたい
- 値を0から4まで、2000ms(2秒)かけて、遅延実行はしない
- 最後までアニメーションしたら逆再生、頭まで戻ったら普通に再生
- これらの動作を永遠に繰り返す
少しアニメーションを取り入れただけですが、結構良い感じになりましたね。
オーナメントとライトの追加
オーナメントは単なるSphereです。MaterialコンポーネントのColorプロパティで色々な色を設定して、適当に配置しました。少しぐらいズレていても、見えない糸で吊るされている感じがして良いと思います。ブリンクライトは前述のてっぺん飾りの下に追加したPoint Lightを複製して、ライトの色を変え、アニメーションの遅延実行(delay)の値を少し変えて、色々な光が交差していく感じを演出してみました。ライトは少し制御が難しいですが、視覚的に与える効果も大きいので、演出の作り込みには重要な作業ですね。
以上の作業を終え、章冒頭のgifアニメーションのような状態になりました。樹はアレですが、ライティングを加えることで結構良い感じになったと思います。
建物の生成
ここからは建物の生成、雪を降らす、カメラを回転移動と注視など、主にJavascriptを使用した実装が主になります。私がA-Frameの知見が少ないこともあって、A-Frameの良さが生かせてない実装になっています。また実装に関する説明もだいぶ省略していますので、実装の流れの参考程度にご覧いただければと思います。
建物と雪はこれまでのように手付けでエンティティを配置するわけではなく、Javascriptで動的にエンティティを配置します。Javascript使用して、A-Frameのエンティティを扱う場合は基本的に以下の原則があります。
- エンティティをDOM APIで、HTMLオブジェクトとして取得する
- A-Frame関連の設定は、HTML属性を通して設定の取得や設定をおこなう
- THREE.jsのAPIを使用できる
建物のエンティティ作成と動的生成
まずは、建物のエンティティをBoxで作成し、DOM APIでJavascriptのオブジェクトとして取得します。このエンティティをサンプルとして、自動生成の際にサイズを変形、生成位置をランダムに設定します。建物のエンティティを作成
<a-entity id="building" geometry="primitive: box; width: 1; height: 2; depth: 1;" material="shader: flat" position="0 -8 0" ></a-entity>
これをJavascriptで取得します。
Javascriptで建物エンティティを取得
const building = document.getElementById('building');
SpawnObjectクラス
エンティティを複製してシーン上に配置するベースクラスです。SpawnObjectクラス
class SpawnObject { constructor(original, viewport){ this.shape = original.cloneNode(); this.viewport = viewport; this.range = Utils.getViewportRange(viewport); this.viewport.appendChild(this.shape); this.shape.setAttribute('shadow', 'receive: false') this.setup(); } setup(options = { excludeRange: '0 0 0', expandRange: '1 1 1' }){ const { expandRange, excludeRange } = options; this.enabled = true; this.position = Utils.getVectorValueWithRange(this.range, expandRange, excludeRange); this.rotation = new THREE.Vector3(0, 0, 0); this.scale = new THREE.Vector3(1, 1, 1); this.acceleration = new THREE.Vector3(0, 0, 0); } update(){ if(!this.enabled){ return; } const velocity = this.acceleration; const position = this.position.add(velocity); this.render(); } render() { this.shape.setAttribute('position', this.position) this.shape.setAttribute('rotation',this.rotation); this.shape.setAttribute('scale', this.scale) } destroy() { this.enabled = false; } }
Buildingクラス
SpawnObjectクラスを継承して、建物用に特化したBuildingクラスを作成します。Buildingクラスは、シーン中央に配置されるクリスマスツリーの周囲に建物が生成されないようにするための処理や、クリスマスツリーから離れて配置される建物は、近くの建物に比べて色を徐々に暗くするなどの処理を行なっています。Buildingクラス
class Building extends SpawnObject { setup(){ super.setup({ excludeRange: '8 0 8', expandRange: '1.75 0 1.75', }); const { getRandom: r } = Utils; this.scale = new THREE.Vector3(r(2, 2.8), r(1, 2.4), r(2, 2.8)); this.position.y = this.scale.y; const { x, y, z } = this.position; let color = Math.round(255 - (Math.abs(z) * 2 + Math.abs(x) * 2)); color = (color <= 64 ? 64 : color).toString(16); this.shape.setAttribute('material', `shader: flat; color: #${color + color + color}`); this.update(); } }
雪を降らす
雪の生成は建物の生成と同様に、オリジナルとなるエンティティ(a-sphere)を作成し、SpawnObjectクラスを継承した、Snowクラスで生成と降雪処理を実行します。
Snowクラス
SnowクラスはSpawn Objectクラスを継承します。Buildingクラスと異なるのは、雪が降るモーションを実現するために、毎フレームごとに再計算された位置情報でエンティティを再描画する処理を追加する点です。Buildingクラス
class Snow extends SpawnObject { setup(){ super.setup(); const { getRandom: r, getCentralizedValue: c } = Utils; const scale = r(0.5, 2); this.position.y = r(this.range.y, this.range.y + 4); this.scale = new THREE.Vector3(scale, scale, scale); this.acceleration = new THREE.Vector3( c(r(0.01), 0.01), -r(0.01, 0.02), c(r(0.01), 0.01)); } update(){ super.update(); if(!this.enabled){ return; } if(this.position.y <= 0){ this.destroy(Math.random() * 1000) } } destroy(delay){ super.destroy(); setTimeout(() => this.setup(), delay); } }
中央を注視しながらカメラを周回させる
Cameraエンティティ(a-camera)は、移動や回転に関しては、Cameraエンティティ自体に設定するよりも、制御用の親エンティティに入れ込んだ方がやりやすいので、エンティティの構造を下図のように設定します。Cameraエンティティの設定
<a-entity id="camera_container" position="0 7 28" rotation="20 0 0" data-lookat="0 4 0" > <a-camera id="camera_body" data-aframe-default-camera look-controls wasd-controls ></a-camera> </a-entity>
Cameraオブジェクト
const Camera = { init(){ this.container = document.getElementById('camera_container'); this.body = document.getElementById('camera_body'); this.position = this.container.getAttribute('position'); this.positionDefault = { ...this.position }; this.rotation = this.container.getAttribute('rotation'); this.lookat = this.container.getAttribute('data-lookat') || '0 2 0'; this.lookat = new THREE.Vector3(...this.lookat.split(' ')); this.theta = 0; }, update() { const { position, positionDefault:pdef, rotation, lookat, theta } = this; position.x = Math.sin(theta * Math.PI / 180) * pdef.z; position.y = Math.sin(theta * 4 * Math.PI / 180) + pdef.y; position.z = Math.cos(theta * Math.PI / 180) * pdef.z; rotation.x = Math.atan2(lookat.y, position.y) / Math.PI * 180; rotation.y = theta; this.theta = (theta + 0.1) % 360; this.container.setAttribute('position', position); this.container.setAttribute('rotation', rotation); } };
音楽を流したい!
こういったムーディな(笑)コンテンツは、なんとなく音楽をつけたくなりますよね。無料プランのCODEPENではファイルをアップロードすることができないので諦めかけていたのですが、ファイル選択フォーム(<input type="file">)を使用すれば、ローカルからファイルをアップロードして、音楽を流せることを知りました。
画面左側の方でうっすらと「ファイルを選択」とありますので、お使いのPCからお気に入りの音楽を選択してみてください。全体的にスローモーションなコンテンツなので、アンビエントな曲調があうかな?と思います。
冒頭のVimeoの動画は、ISAo.さんの「星粒が降る夜」を使用させていただきました。とても素敵な曲です。
フリーBGM素材 『星粒が降る夜』 試聴ページ|フリーBGM DOVA-SYNDROME
最後に
本記事は以上になります。後半のスクリプティングの部分はろくな説明もなく、コードを掲載するだけの雑なつくりになってしまいましたが、折をみて、加筆修正したいと思います。A-Frameは、今回の記事のために初めてちゃんと触ってみましたが、とても便利だと思いました。WebGLを素で扱えたり、THREE.jsやbabylon.jsなどのライブラリを使用して制作ができる人にとっては、A-Frameを使用する必要はあまりないかもしれませんが、手軽にコンテンツを作成・デバッグする目的でも役立ちそうです。
また、今回は使用しませんでしたが、A-FrameのEntity Component Systemを使いこなせば、描画処理(View)と、ロジック(Model)や描画制御(ViewModel)をうまく切り分けた実装が可能になるかと思いました。
とにかく、WebGLやWebVRコンテンツ制作の敷居を下げてくれているのは確実だと思いますので、今後これらの技術を使用したリッチなコンテンツが増えていくと良いなと思います。
それでは、また。良い年末を!
コメント
コメントを投稿