こんにちは。サイバーエージェントのAmeba Ownd(アメーバ・オウンド)というサービスでエンジニアをしている@fushikkyです。

最近までフロントエンドの開発を担当していましたが、現在はサーバーサイドを担当しています。ですが今回はVRの話をします。

OwndVRCaption

 

今流行りのVRをAmeba Owndで作られたWebサイトに適用して、「WebサイトのVR化」をエイプリルフールの施策として試みた事についてまとめます。施策の売りとしては、「自分の作ったWebサイトが自動でVR化される」というところですね。

Ameba Owndは、無料でオシャレなホームページやブログが開設できるホームページビルダーです。ポートフォリオやネットショップ作成など用途は幅広いので、興味のある方はよかったら使ってみてください。

https://www.amebaownd.com/

さて、Ameba Owndでは例年エイプリルフールにLPの内容をユニークにアレンジしているのですが(2015年, 2016年)、 今年は他に何か面白いことはできないかと考え、VRをWebサイトで実現したいと思い実装するに至りました。

Ameba Owndのスタッフブログにエイプリルフールの概要が記載されておりますので、よろしければご覧ください。

https://blog.amebaownd.com/posts/2225441

成果物はこちら(PC・スマートフォンで閲覧できます)

https://abematimes.com/?aprilfool=2017

https://awa-official.themedia.jp/?aprilfool=2017

WebVRについて

WebVRとは、Webブラウザ上でVRを実現する技術です。もう少し詳しく言えば、Webブラウザ上で3Dコンテンツを描画する技術であるWebGLを用いてVR体験を提供するものです。WebVRの利点として、特別なアプリをインストールせずともWebブラウザさえあればVR体験が可能になることが挙げられます。

WebVRに対応したライブラリはいくつか存在しますが、今回は特に時間がなかったこともあり(構想からエイプリルフールまで2週間弱)、HTMLのタグを記述するだけで簡単にWebVRが実装できる「A-Frame」を採用しました。

A-Frameとは

https://aframe.io/

HTMLのタグをシーンとして扱いWebVRを構築できるフレームワークです。簡単なものであればjavascriptの記述は必要ありません。こちらはThree.jsをベースに実装されています。A-Frameを使うだけで3Dの描画はもちろんの事、VR体験に必要なドラッグ操作や傾き検出などを行う機能が組み込まれています。

以下はA-Frame公式ページのサンプルコードです。

(https://aframe.io/examples/)

<html>
  <head>
    <script src="https://aframe.io/releases/0.5.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>

たったこれだけのHTMLで、以下のような3Dの描画とVR体験が実現できます。

aframe-sample

簡単な解説
<head>タグ内でA-Frameのjsを読み込んでいます。<a-scece>タグの中に3Dオブジェクトとなるタグを記述していきます。ここで<a-box>は直方体、<a-sphere>は球、<a-cylinder>は円柱、<a-plane>は平面を描画するタグです。<a-sky>は3D空間の背景を描画するタグで、色を設定したり全天球画像を設定することもできます。これらのタグに位置や傾き、色情報などのプロパティを付加して3Dコンテンツを作成していきます。また、カメラが中央に配置されておりドラッグで視線位置を動かすことができます。より詳細な情報は公式ページのドキュメントを参照していただければと思います。

https://aframe.io/docs/0.5.0/introduction/

WebサイトをVR化する前に少し横道にそれまして、Ameba Owndの既存の構成を説明します。Ameba Owndは今までAngular.jsを用いたSPA(シングルページアプリケーション)で動作していたのですがSEOやパフォーマンスの観点から、Node + Reactで動くIsomorphic (SSR + SPA、現在はUniversal JSとも呼ばれる) の仕組みに置き換えるシステム刷新の移行期間中です。(一般ユーザーにも順次解放していく予定です)

システム刷新後のAmeba OwndでReact.jsを用いていた事が、VR化を行いやすかった点でもあります。

WebサイトをVR化

React.jsとA-Frameの橋渡しとなるのが「aframe-react」です。

https://github.com/aframevr/aframe-react

aframe-reactはReact.jsのコンポーネントとしてA-Frameのオブジェクトを使うことができるので、データバインディングが非常に楽に行えます。前述した<a-text><a-box><a-entity>コンポーネントを拡張したコンポーネントで、<a-entity>を用いた場合以下のように記述できます。

text

<!-- a-text を用いた場合-->
<a-text value="sample text"></a-text>
<!-- a-entity を用いた場合 -->
<a-entity text="value: sample text"></a-entity>

Image

<!-- a-image を用いた場合-->
<a-image src="image.jpg"></a-image>
<!-- a-entity を用いた場合 -->
<a-entity
 geometry="primitive: plane"
 material="src: image.jpg"
></a-entity>

aframe-reactでは<a-entity>のReactコンポーネントである
<Entity>を用います。

今までReact.jsで

<p>{'sample text'}</p>
<img src={'image.jpg'}>

のように書かれていた部分を以下のように書き換えるだけでA-Frameにデータバインディングを行えます。

<Entity
  text={{ value: 'sample text'}}
/>
<Entity
  geometry={{ primitive:'plane'}}
  material={{ src: 'image.jpg'}}
/>

ただ、このままではpositionがすべて初期値(0,0,0)になってしまうため、コンテンツに応じて描画位置を都度計算する必要があります。

今回はエイプリルフールということもあり、Webサイトとしての描画ロジックの正確さよりもVRとしてのインパクトが重視されました。結果としてVR表示で周りを見渡したときにカメラを取り囲むように画像を配置することになりました。

やったこと

  • サイトの最新記事から抽出した画像を球状に配置
  • 画像をクリック(注視) 時に記事詳細を描画するインタラクション

球状に並べるだけならライブラリを使えば簡単にできるようですが、細かいカスタマイズもしたかったことや、A-Frameの座標の扱いにも慣れたかったこともあるため、自前で座標変換をすることにしました。

A-Frameの座標系

A-Frameの座標系は以下のようになっています。

xyz

A-Frameで管理されるオブジェクトは、親オブジェクトからの相対座標系であるローカル座標系で位置が決定され、最上位のオブジェクトはワールド座標系で位置が決定されます。

  • ワールド座標系: 扱っている3D空間全体の座標系
  • ローカル座標系: 親オブジェクトに対する相対座標系

position, rotationを設定することで、オブジェクトの位置と向きを制御しています。position=”x y z” は座標位置、rotation=”x y z” にはそれぞれの軸に対する回転角を記述します。

三次元座標変換

球状に画像を並べる場合は、直行座標系(x, y, z)よりも極座標系(r, θ, φ)の方が管理がしやすいので、極座標系を直行座標系に変換する数式を使います。
polar

上記は以下の式で表せます。

xyz-eq

polar-range

上式を用いてrを固定値、θ,φを変化させることで球状に等間隔で画像を並べています。また、rotationが初期値のままだと、すべての画像が同じ方向を向いてしまうので、以下の図のようにカメラの方向を向くように傾きをつける必要があります。
camera-xyz
それぞれの軸に対する角度は以下の式で表せます。

rot

また、画像クリック時の詳細表示については、上述のように親オブジェクトの位置を継承するので、画像の親要素で位置を計算しておけば子オブジェクトは親からの相対座標で表現できます。

上記を踏まえた上でReact.jsのrender部分のDOM構造は以下のようになります。(y軸方向にm分割、x-z方向にn分割した場合)

/**
 * index.jsx
 */
import React from 'react';
import { Entity } from 'aframe-react';

...
render() {
  for (let i = 0; i < m; i++) {
    for (let j = 0; j < n; j++) {
      /* radianに変換 */
      const theta = Math.PI * i / m; 
      const phi = 2 * Math.PI * j / n;
      /* 座標計算 */
      const x = radius * Math.sin(theta) * Math.sin(phi);
      const y = radius * Math.cos(theta);
      const z = radius * Math.sin(theta) * Math.cos(phi);

      /* カメラの方向を向くように傾きをつける degree変換 */
      const rotX =  (theta - (Math.PI / 2)) * (180 / Math.PI);
      const rotY = phi * (180 / Math.PI);

      return (
        <Entity
          position={[x, y, z].join(' ')}
          rotation={[rotX, rotY, 0].join(' ')}
        >
          <Entity
            geometry={{ primitive: 'plane'}}
            material={{ src: 'image.jpg'}}
          />
        </Entity>
      );
    }
  }
}
...

インタラクション

VRモード時はクリックの操作ができないため、ページ中心にカーソルを配置しカーソルとオブジェクトが一定時間重なった時にクリックイベントを発生させて対応します。(以下ではこちらで用意した画像をテクスチャとして用いています。)
cursor

クリックイベント

onMouseenterイベントを発火させるためにcameraに<a-cursor>コンポーネントを追加しますa-cursorは標準ではドーナツ型(a-ring)です。
(showPostDetail: 詳細表示用の関数)

{/* 画像側 */}
<Entity
  onMouseenter={() => {showPostDetail(); }}
/>

{/* カメラ */}
<a-camera>
  <a-cursor/>
</a-camera>

アニメーションについて

浮遊感を出すために、少し画像を揺らしています。aframe-animation-componentを用いてx, y, z方向にランダムで移動させています。
(animPos: アニメーション後の座標)

<Entity
  animation={`property: position; dir: alternate; dur: 1000;
    easing: easeInSine; loop: true; to: ${[animPos.x, animPos.y, animPos.z].join(' ')}`}
/>

パフォーマンスについて

パフォーマンスの観点からすべての画像を一気に描画してしまうと処理が重くなってしまうので、カメラから見える初期位置から順に描画されるようにしました。 y軸方向にm分割、x-z方向にn分割した場合、カメラ初期注視位置は(m/2, n/2)の位置にあるコンポーネントに該当するので、中心座標からの座標距離から計算して描画開始時間tijは以下のように設定しました。

tmatrix

tij

また、クリック(注視)時にオブジェクト(HTMLタグ)自体の表示非表示を切り替えてしまうと、再レンダーが走ってしまうため描画がカクついて重くなってしまいます。ここではopacityの変更のみに留めています。 (showDetail: 表示非表示切り替えフラグ)

<Entity
  material={{ src: image.jpg, opacity: showDetail ? 1.0 : 0.0 }}
/>

フォントについて

A-Frameでのテキスト表示は、標準では英語フォントしかサポートされていません。そのため、日本語フォントを表示する場合は自前で用意する必要があります。aframe-text-geometry-component使えば自分で設定したフォントを設定できます。フォントはこちらのフリーフォント(ttfファイル)をダウンロードし、こちらのページからjsonに変換しました。

日本語フォントのデータは大変重いものなので(4~5MB)、スマートフォンでのダウンロードは行わないことにし、PCのみ表示することにしました。

フォントの設定方法

<a-assets>
  <a-asset-item id="font" src="font.json">
</a-assets>
{/* 文章 */}
<Entity
  text-geometry={{ value: 'sample text', font: '#font }}
/>

まとめ

A-Frame とReact.jsを連携させることで簡単にWebVRが実現できました。今回、Webサイトとしての描画ロジックの正確さよりもVRとしてのインパクトが重視される構成となりましたが、より最適な見せ方を吟味していけば新しいサイト閲覧の形が見出せるのではないかと思います。WebVRという技術が出てきたことでWebサイトは今後、既存のものとは全く新しい形に変化していくのかもしれません。そういった意味で、Ameba Ownd でWebサイトを作ってVRサイトを試してみてはいかがでしょうか。

fushikky
2013年新卒入社。Ameba事業本部にて複数のメディアサービスの運用を経験し、フロントエンジニアとしてAmebaOwndの立ち上げに携わる。現在は同サーバーサイドを担当している。趣味はアニメとブレイクダンス。