##はじめに
WebGLやWebVRの練習で、雪が降る空間のようなものを作ってみようと思ったのですが、ちょうどクリスマスが近いし、せっかくなので大きなクリスマスツリーがある街に雪が降りそそぐ景色を作ってみました。
実際は全然クリスマスツリーに見えない残念な出来ですが、シーンに使用するツリーや雪などのアセットを、objファイルなどの3Dモデル素材を使用しないで、プリミティブジオメトリだけで作成しました。
また、コンテンツはデスクトップなどでのんびり眺める用の非VR版と、GearVR、Google Cardboardやハコスコなど向けの軽量なVR版を作成しました。
非VR版
See the Pen Christmas Scenery 2018 by Takayasu.B (@tspringkj) on CodePen.
VR版
VR版はスマートフォンでの閲覧に耐えうるように、要素数を減らすなど処理を軽量化、視点を変更するなどしました。QRを読み取って、CODEPEN上で表示された後、画面右下のVRアイコンをタップするとVRモードに切り替わります。
https://codepen.io/tspringkj/full/VqVweB
See the Pen VR - Christmas Scenery 2018 by Takayasu.B (@tspringkj) on CodePen.
###この記事について
この記事は、A-Frameについて詳細に解説するものではなく、冒頭で紹介したサンプル作品を制作する過程を紹介しながら、印象に残ったポイントをいくつかピックアップして、適宜解説するといった内容になります。
A-Frameは、Mozilla VRチームが手がけるthree.jsをベースにしたWebVRフレームワークで、独自のHTMLタグだけで手軽にWebVRやWebGLコンテンツが作成できるのが特徴です。また、JavaScriptでA-Frame APIだけでなく、three.jsのAPIも使用できるので、コンテンツの作りこみも可能です。
今回はWebVRのためというよりも、WebGLコンテンツを作成する上で、A-Frameの持つ利便性に注目してみました。とくにA-Frame Inspectorは、多少不便や不具合はあるものの、非常に便利な機能だと思いましたので、最初に簡単な使い方だけご紹介いたします。
##A-Frame クイックスタート
A-Frame Inspectorは、A-Frameのシーン上のオブジェクトを視覚的に管理できる機能で、A-Frameに組み込まれています。
ただ、A-Frameの最新版(2018年12月現在、v0.8.0)に組み込まれたA-Frame Inspectorは、私の環境では正常に動作しないことが多々あったため、今回は少し古いバージョンのA-Frame(v0.7.0)で進めていきます。
まずは手っ取り早くA-Frameのシンプルさを実感するために、A-Frame公式の「Getting Started」へ行き、サンプルコードを実行してみましょう。実行前に読み込むA-Frameのバージョンを、0.7.0に変更します。
<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>
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で扱うオブジェクトのことを「エンティティ」または「entity」と記載します。
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を採用しています。
####Viewport
画面のパネル以外の領域で、シーン上のエンティティを俯瞰して確認しつつ、移動、回転や拡大操作が可能です。Viewport上での操作一覧を下の表にまとめましたが、この操作一覧は、iMac、英文キーボード、左・中・右クリックが可能なマウスを使用した環境で検証したものなので、一部環境では動作しない可能性があります。
####Viewport操作一覧
一部環境では動作しない操作があるかもしれません
操作・キー | 概要 |
---|---|
左クリック | オブジェクトなど選択 |
左クリック + ドラッグ | 視点回転 |
右クリック + ドラッグ | 視点上下左右パン |
中クリック + ドラッグ | 視点高速前進・後退 |
w | 移動ツール |
e | 回転ツール |
r | 拡大ツール |
` | Scene Graph、Components Panel表示切り替え |
1 | Scene Graph表示切り替え |
2 | Components Panel表示切り替え |
Unityや3D制作オーサリングソフトに近い操作感なのですが、私の環境では、操作の取り消し([Ctrl] + [z])がうまく機能しなかったり、エンティティのロックができない?など、現段階ではまだ改善してほしいところが多々あります。とはいえ、現状でもかなり便利な機能だと思います。
A-Frame Inspector上でエンティティを編集してコードを取得する
Scene Graphのシリンダーオブジェクト(<a-cylinder>)を選択し、編集してからコードを取得してみます。
エディタに戻って、クリップボードにコピーしたコードをペーストして内容を確認します。
<a-cylinder
position="1 2.863505048034185 -3"
radius="0.5" height="1.5" color="#FFC65D"
material="" geometry="">
</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]
(https://aframe.io/docs/master/introduction/visual-inspector-and-dev-tools.html#sidebar)
シーンで使用するアセット(部品)を作成する
シーンに配置するアセットをA-Frame Inspectorで作成します。作品で使用するアセットは以下の3つです。とはいえ、ツリー以外は単なるBoxとSphereです。
- クリスマスツリー風の大きな樹
- 四角い建物
- 雪
アセットはOBJファイルなどの3Dモデルデータを読み込むのではなく、A-Frameが用意しているBoxやSphereなどのプリミティブなジオメトリ(geometry)を組み合わせて作成していきます。
###A-Frameのジオメトリ(geometry)について
A-Frameには、あらかじめいくつかの形状(geometry)が用意されています。
geometryの指定の仕方は2通りあります。
- タグ名で直接指定する(例. <a-box>, <a-plain>など)
- <a-entity>タグのgeometry属性のprimitiveプロパティで指定する
(例. <a-entity geometry="primitive: box">など )
今回は主に1の方法(タグ名指定)で進めていきますが、A-Frame Inspectorでエンティティを作成した時などは2の方法の記述になるなど、2の方法でコードが記述されている箇所も随所見受けられますが、指定方法が2種類あることに留意して、読み進めていただければと思います。
A-Frameで用意されているジオメトリ一覧は下表のようになります。
Primitive Geometry 一覧
形状 | タグ | geometry.primitive |
---|---|---|
立方体 | <a-box> | box |
円(平面) | <a-circle> | circle |
円錐 | <a-cone> | cone |
円柱 | <a-cylinder> | cylinder |
正十二面体 | <a-dodecahedron> | dodecahedron |
正八面体 | <a-octahedron> | octahedron |
正方形(平面) | <a-plane> | plane |
輪(平面) | <a-ring> | ring |
球体 | <a-sphere > | sphere |
四面体・四角錐 | <a-tetrahedron> | tetrahedron |
円環 | <a-torus> | torus |
円環結び目 | <a-torusKnot> | torusKnot |
三角形(平面) | <a-triangle> | triangle |
先ほどサンプル用に記述したコードを削除して(※下図コード参照)、一旦シーン上のオブジェクトを空っぽにした後、A-Frame Inspectorを立ち上げて部品となる3Dオブジェクトを作成していきます。
<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でエンティティを作成したら、都度そのコードをコピーして実際のコードにペーストして反映するようにしていきます。
/*(抜粋)*/
<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>
このグループ化作業のように、A-Frame Inspectorで一部操作が対応していない場合や、コードで編集した方が早い作業はコードエディタでおこなうなど、作業内容によって、臨機応変にツール切り替えて行っていけば、効率よく制作を進めることができるでしょう。
特にグループ化作業は重要で、ある程度Inspectorでエンティティを作成したら、エディタにコードを貼り付けてグループ化し、Inspectorに戻る。といったことを繰り返していくと良いと思います。いつか、A-Frame Inspector上でグループ化ができたり、コードの同期機能が安定すれば、この辺りの作業も必要なくなるかもしれません。
###ツリーに飾り付けとライトを追加する
大きな樹そのままでも素朴で良いのですが、せっかくのクリスマスシーズンなので、樹のてっぺんや枝葉にオーナメント風なものを飾りたいと思います。
- 樹のてっぺんの飾り + ブリンク(明滅)ライト
- オーナメント(※単なるSphere)適量
- ブリンクライト4つぐらい
####樹の頂上の飾りつけとライトの追加
樹の頂上の飾りは星型が良かったのですが、A-Frameの組み込みプリミティブジオメトリに用意されていなかったので、octahedron(八面体)を代替に選びました。そのてっぺんの飾りの少し下あたりにライト(<a-light>)を配置します。ライトの種類(type)は「Point Light」です。Component PanelのLIGHTコンポーネントのtypeプロパティから選択できます。
Point Lightはその周辺のエンティティに対して光を当てる効果があるので、電飾ライトに向いています。明滅の効果は同じくLIGHTコンポーネントのintensityプロパティの値を増減させることで実現します。これにはアニメーションの実装が必要になるので、コードエディタで編集を行います。
###A-Frameのアニメーションについて
A-Frame Animationは、まずAnimationエンティティ(<a-animation>)を作成し、その属性で各種設定を行なって行きます。
Animationエンティティの属性と概要一覧
属性 | 概要 | 初期値 |
---|---|---|
attribute | アニメーションさせたい対象(属性名)を指定 対象がコンポーネントのプロパティの場合 "コンポーネント名.プロパティ名" と指定 (例. light.intensity) |
rotation |
begin | アニメーションを開始させるイベント名 開始時はemit("イベント名")関数で実行する 数字を指定すると指定ミリ秒後に実行(delayと同じ) |
‘’ |
delay | 指定ミリ秒後に実行 | 0 |
direction | アニメーションの方向を、alternate, alternateReverse, normal, reverse から指定 |
normal |
dur | アニメーションの実行時間をミリ秒で指定 | 1000 |
easing | イージングを用意されているものから指定 | ease |
end | アニメーションを停止させるイベント名 使い方はbeginと一緒 |
‘’ |
fill | アニメーション停止時の要素の挙動を backwards, both, forwards, none から指定 (例. forwardsの場合はtoで指定した状態で停止) |
forwards |
from | アニメーション開始時の値 | ※現状の値 |
repeat | 繰り返し回数 無限に繰り返す場合は indefinite. |
0 |
to | アニメーション終了時の値。設定必須 | None |
Animationエンティティに設定できるプロパティ一に関する簡単な説明は上の表のようになります。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エンティティ(<a-light)がアニメーションさせたい対象となり、その子要素にAnimationエンティティ(<a-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を使用して設定する
今回の作品に関していえば、上記3つの流れに則るだけで実装することができました。
1に関しては、通常のWebサイト制作で、JavaScriptを使用してDOMを操作することに慣れている人なら、それほど難しいことではないと思います。ただ、2と3に関しては、A-Frame APIの(場合によっては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>
今回はBoxエンティティを、<a-box>ではなく、<a-entity>タグで作成しています。このタグのgeometry属性のprimitiveプロパティに「box」を指定することで、箱型のエンティティが作成されます。その他の設定については、公式ページのgeometryを参考にしてみてください。また、このエンティティを取得するために、<a-entity>タグのid属性に「building」を設定しました。
これをJavascriptで取得します。
const building = document.getElementById('building');
次に、取得したオブジェクトをオリジナルとして、これをクローンしたエンティティを生成するためのベースとなるJavascriptのクラス「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クラスは、シーン中央に配置されるクリスマスツリーの周囲に建物が生成されないようにするための処理や、クリスマスツリーから離れて配置される建物は、近くの建物に比べて色を徐々に暗くするなどの処理を行なっています。
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クラスと異なるのは、雪が降るモーションを実現するために、毎フレームごとに再計算された位置情報でエンティティを再描画する処理を追加する点です。
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エンティティ自体に設定するよりも、制御用の親エンティティに入れ込んだ方がやりやすいので、エンティティの構造を下図のように設定します。
<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>
これをいつものようにJavascriptで取得し、カメラ制御用のObject型変数を作成します。
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);
}
};
これでカメラが画面中央を注視しながら、その周りをふよふよ周回するようになりました。
##音楽を再生する
非VR版の方はBGMをつけるとより雰囲気が出るかな?と思い、音楽を再生したくなったのですが、フリープランのCODEPENではファイルをリンクさせることができなかったため、フォームからオーディオファイルをアップロードすることで、音楽を再生できるようにしてみました。
const AudioManager = {
/* setup()関数の引数
audioInputはinput要素。audioBodyはaudio要素です */
setup(audioInput, audioBody) {
audioBody.addEventListener('canplay', () => audioBody.play());
audioBody.addEventListener('error', e => console.error(e));
audioInput.addEventListener('change', e => {
const files = e.target.files;
audioBody.src = URL.createObjectURL(files[0]);
audioBody.load();
});
}
}
画面左下の方でうっすらと「ファイルを選択」と表示されたフォーム(input要素)が設置してありますので、クリックして、お使いのPCからmp3などのオーディオファイルを選択すると、音楽が再生されます。
##最後に
本記事は以上になります。後半のスクリプティングの部分はろくな説明もなく、コードを掲載するだけの雑なつくりになってしまいましたが、折をみて、加筆修正したいと思います。
A-Frameは、今回の記事のために初めてちゃんと触ってみましたが、とても便利だと思いました。WebGLを素で扱えたり、three.jsやbabylon.jsなどのライブラリを使用して制作ができる人にとっては、A-Frameを使用する必要はあまりないかもしれませんが、手軽にコンテンツを作成・デバッグする目的でも役立ちそうです。
また、今回は使用しませんでしたが、A-FrameのEntity Component Systemを使いこなせば、うまく役割を分けた実装が可能になるかと思いました。
とにかく、WebGLやWebVRコンテンツ制作の敷居を下げてくれているのは確実だと思いますので、今後これらの技術を使用したリッチなコンテンツが増えていくと良いなと思います。
それでは、良い年末を!