React VRは、Facebookが開発したJavaScriptライブラリーで、WebVRアプリケーションを作成できます。A-Frame by Mozillaとの違いは、WebVRシーンの作成にHTMLではなく、JavaScriptを使うことです。
React VRはWebGLライブラリthree.jsとReact Nativeフレームワークを基礎としています。したがって、JSXタグや、<View>や<Text>などのReact Nativeのコンポーネント、FlexboxレイアウトといったReact Nativeのコンセプトを使えます。React VRシーンの作成手順を簡素化するために、3Dメッシュ、ライト、ビデオ、3Dシェープ、球面画像などをビルトインでサポートしています。
この記事では、React VRでパノラマ画像ビューワーを作ります。4つのエクイレクタングラー画像を使います。サンプルはReact Conf 2017でTheta S カメラを使って撮った写真です。ギャラリーには画像を切り替えるためのボタンを4つ備え、ボタンはマウスかVRヘッドセットで操作できます。ボタン用の画像、表示するエクイレクタングラー画像はここからダウンロードできます。最後に、ボタンの切り替えのアニメーションがReact VRでどう動くかを確認します。
開発には、デスクトップではChromeブラウザーを、VRデバイスの立体レンダリング機能をチェックするためにはSamsungのスマホとGear VRを使います。理論上では、WebVRが使えるモバイルブラウザーならGearVRやCardboard、Google Daydreamでアプリを立体視的にレンダリングできるはずです。しかし、APIだけでなくライブラリーも開発中なので、サポートはあてになりません。WebVRの機能をサポートしているブラウザーをまとめたサイトはこちらが参考になります。
開発設定とプロジェクトの構造
まずReact VRのCLIツールをインストールします。次に新しいReact VRプロジェクトを作り、関数オブジェクトをすべて新しいフォルダ「GDVR_REACTVR_SITEPOINT_GALLERY」に格納します。
npm install -g react-vr-cli
react-vr init GDVR_REACTVR_SITEPOINT_GALLERY
cd GDVR_REACTVR_SITEPOINT_GALLERY
ローカルな開発用サーバーを立ち上げます。npmスクリプトを実行後、Chromeで http://localhost:8081/vr/ を開きます。
npm start
階段と柱がある白黒の部屋と「hello」が現れれば成功です。
React VR CLIが生成する重要なファイルとフォルダーは以下の2つです。
- index.vr.js:アプリケーションのエントリーポイント。ファイルには、先ほどブラウザーに表示されたReact VRのデフォルトシーンのソースコード全体が含まれる
- static_assets:アプリケーションで使うアセットがすべて含まる。エクイレクタングラーイメージやボタンのグラフィックスを格納する
このプロジェクトでは3つのコンポーネントを作ります。
- 全球イメージのコードを保持するCanvasコンポーネント
- イメージを入れ替えるVRボタンを作るButtonコンポーネント
- 4つのボタンコンポーネントからUIを作るUIコンポーネント
3つのコンポーネントには、それぞれ独自のファイルがあります。ファイルを置くためのcomponentsフォルダを作ります。
次に、Canvasコンポーネントを作ります。index.vr.jsファイルを開き、あらかじめ用意されたサンプルコードを削除して以下の状態にします。
/* index.vr.js */
import React from 'react';
import {
AppRegistry,
View,
} from 'react-vr';
export default class GDVR_REACTVR_SITEPOINT_GALLERY extends React.Component {
render() {
return (
<View>
</View>
);
}
};
AppRegistry.registerComponent('GDVR_REACTVR_SITEPOINT_GALLERY', () => GDVR_REACTVR_SITEPOINT_GALLERY);
シーンに球面画像を加える
シーンに球面画像を加えるために、componentsフォルダーに新しいファイルCanvas.jsを作ります。
/* Canvas.js */
import React from 'react';
import {
asset,
Pano,
} from 'react-vr';
class Canvas extends React.Component {
constructor(props) {
super(props);
this.state = {
src: this.props.src,
}
}
render() {
return (
<Pano source={asset(this.state.src)}/>
);
}
};
export default Canvas;
最初の6行で、関数オブジェクトをインポートします。続いてCanvasコンポーネントを宣言し、JSXシンタックスでどうレンダリングするのか定義します。
JSXを学ぶなら、「ReactとJSX入門」がおすすめです。
JSXコードを見ると、CanvasコンポーネントはReact VR<Pano>コンポーネントのみを返すと分かります。パラメーターsource propsがあり、static_assetsフォルダから画像をロードするasset関数を使います。引数はコンストラクタ関数で初期化したstateを参照します。
この場合、Canvasコンポーネントにはパスを設定せず、画像のパスはすべてindex.vr.jsファイルで定義します。これが、state.srcオブジェクトがコンポーネントのpropsオブジェクトを参照する理由です。
stateとpropsを学ぶなら、React.ComponentのReactJSのドキュメントを調べてください。
index.vr.jsファイルを修正して、Canvasコンポーネントでシーンにレンダリングできるようにします。
/* index.vr.js */
import React from 'react';
import {
AppRegistry,
View,
} from 'react-vr';
import Canvas from './components/Canvas';
export default class GDVR_REACTVR_SITEPOINT_GALLERY extends React.Component {
constructor() {
super();
this.state = {
src: 'reactconf_00.jpg',
};
}
render() {
return (
<View>
<Canvas
src={this.state.src}
/>
</View>
);
}
};
AppRegistry.registerComponent('GDVR_REACTVR_SITEPOINT_GALLERY', () => GDVR_REACTVR_SITEPOINT_GALLERY);
すでに使ったReact VR関数オブジェクトのほかに、専用のCanvasコンポーネントをインポートします。次に、6行目でアプリケーションクラスを宣言します。
/* index.vr.js */
import Canvas from './components/Canvas';
<View>コンポーネントの子コンポーネントとして<Canvas>コンポーネントを加えます。srcは、Canvasコンポーネントで参照しているので、コンポーネントのpropsとして使います。ブラウザーで表示するとパノラマ画像が見え、インタラクションが可能になっているはずです。
4つのボタンを持つUIコンポーネントを作る
画像の入れ替えのトリガーとなる4つのボタンを作ります。まずは、UIコンポーネントとその子コンポーネントであるButtonコンポーネントを追加します。Buttonコンポーネントは以下の通りです。
/* Button.js */
import React from 'react';
import {
asset,
Image,
View,
VrButton,
} from 'react-vr';
class Button extends React.Component {
onButtonClick = () => {
this.props.onClick();
}
render () {
return (
<View
style={{
alignItems: 'center',
flexDirection: 'row',
margin: 0.0125,
width: 0.7,
}}
>
<VrButton
onClick={this.onButtonClick}
>
<Image
style={{
width: 0.7,
height: 0.7,
}}
source={asset(this.props.src)}
>
</Image>
</VrButton>
</View>
);
}
};
export default Button;
ボタンには、React VRの<VrButton>コンポーネントを使います。これは6行目でインポートしています。また、<VrButton>コンポーネントには外観がないので、ボタンにアセット画像を加えるために画像コンポーネントを使います。また、画像ソースを定義するのにpropを使います。このコンポーネントで2回使っているもう1つの機能はstyle propで、各ボタンとその画像にレイアウト値を追加します。また、<VrButton>はイベントリスナーonClickにも利用します。
シーンに4つのボタンを加えるために、UIの親コンポーネントを使います。これはあとでindex.vr.jsで子として追加します。UIコンポーネントを書く前に、エクイレクタングラー画像、ボタン画像、およびボタンの関係を定義するconfigオブジェクトを作ります。index.vr.jsファイルで、インポート文のすぐあとに定数を宣言します。
/* index.vr.js */
const Config = [
{
key: 0,
imageSrc: 'reactconf_00.jpg',
buttonImageSrc: 'button-00.png',
},
{
key: 1,
imageSrc: 'reactconf_01.jpg',
buttonImageSrc: 'button-01.png',
},
{
key: 2,
imageSrc: 'reactconf_02.jpg',
buttonImageSrc: 'button-02.png',
},
{
key: 3,
imageSrc: 'reactconf_03.jpg',
buttonImageSrc: 'button-03.png',
}
];
UIコンポーネントはconfigで定義した値を凝視イベントやクリックイベントの処理に使います。
/* UI.js */
import React from 'react';
import {
View,
} from 'react-vr';
import Button from './Button';
class UI extends React.Component {
constructor(props) {
super(props);
this.buttons = this.props.buttonConfig;
}
render () {
const buttons = this.buttons.map((button) =>
<Button
key={button.key}
onClick={()=>{
this.props.onClick(button.key);
}}
src={button.buttonImageSrc}
/>
);
return (
<View
style={{
flexDirection: 'row',
flexWrap: 'wrap',
transform: [
{rotateX: -12},
{translate: [-1.5, 0, -3]},
],
width: 3,
}}
>
{buttons}
</View>
);
}
};
export default UI;
画像のソースの設定で、index.vr.jsファイルに加えたconfigの値を使います。クリックイベントの処理でprop onClickを使います。これもすぐ、index.vr.jsファイルに追加します。configオブジェクトの定義と同数のボタンを作成して、JSXコードに追加し、シーンにレンダリングします。
/* UI.js */
const buttons = this.buttons.map((button) =>
<Button
key={button.key}
onClick={()=>{
this.props.onClick(button.key);
}}
src={button.buttonImageSrc}
/>
);
index.vr.jsファイルに定義されたシーンのUIコンポーネントを加えます。Canvasコンポーネントをインポートした直後に、UIコンポーネントをインポートします。
/* index.vr.js */
import UI from './components/UI';
シーンに<Canvas>コンポーネントを追加します。
/* index.vr.js */
<View>
<Canvas
src={this.state.src}
/>
<UI
buttonConfig={Config}
onClick={(key)=>{
this.setState({src: Config[key].imageSrc});
}}
/>
</View>
ブラウザーでコードをチェックすると、このままでは、クリックしても画像ソースは入れ替わりません。propの更新を検知するために、コンストラクタ関数のあとに、もう1つCanvasコンポーネントを加えます。
Reactコンポーネントのライフサイクルに興味があれば、React.Component in the React docsを読んでください。
/* Canvas.js */
componentWillReceiveProps(nextProps) {
this.setState({src: nextProps.src});
}
ブラウザーでボタン画像をクリックすると、球面画像が変化するはずです。今度は成功です。
ボタンの遷移にアニメーションを付加する
ユーザーインターアクションへのボタンの応答性向上のために、デフォルトの待機状態とホバー状態の間に、追加のホバー状態と遷移を加えます。実現するために、Animated libraryとEasing functionsを利用し、それぞれの遷移animateInとanimateOutに対して以下の関数を書きます。
/* Button.js */
import React from 'react';
import {
Animated,
asset,
Image,
View,
VrButton,
} from 'react-vr';
const Easing = require('Easing');
class Button extends React.Component {
constructor(props) {
super();
this.state = {
animatedTranslation: new Animated.Value(0),
};
}
animateIn = () => {
Animated.timing(
this.state.animatedTranslation,
{
toValue: 0.125,
duration: 100,
easing: Easing.in,
}
).start();
}
animateOut = () => {
Animated.timing(
this.state.animatedTranslation,
{
toValue: 0,
duration: 100,
easing: Easing.in,
}
).start();
}
onButtonClick = () => {
this.props.onClick();
}
render () {
return (
<Animated.View
style={{
alignItems: 'center',
flexDirection: 'row',
margin: 0.0125,
transform: [
{translateZ: this.state.animatedTranslation},
],
width: 0.7,
}}
>
<VrButton
onClick={this.onButtonClick}
onEnter={this.animateIn}
onExit={this.animateOut}
>
<Image
style={{
width: 0.7,
height: 0.7,
}}
source={asset(this.props.src)}
>
</Image>
</VrButton>
</Animated.View>
);
}
};
export default Button;
関数オブジェクトを加えたあと、遷移でアニメーションさせる値を保持する状態を定義します。
/* Button js */
constructor(props) {
super();
this.state = {
animatedTranslation: new Animated.Value(0),
};
}
2つのアニメーションを別々の関数で定義し、カーソルがボタンに入ったときと出たときに実行するアニメーションを指定します。
/* Button.js */
animateIn = () => {
Animated.timing(
this.state.animatedTranslation,
{
toValue: 0.125,
duration: 100,
easing: Easing.in,
}
).start();
}
animateOut = () => {
Animated.timing(
this.state.animatedTranslation,
{
toValue: 0,
duration: 100,
easing: Easing.in,
}
).start();
}
JSXコードでstate.animatedTranslation値を使うには、<Animated.view>を加えて<View>コンポーネントでアニメーションを可能にします。
/* Button.js */
<Animated.View
style={{
alignItems: 'center',
flexDirection: 'row',
margin: 0.0125,
transform: [
{translateZ: this.state.animatedTranslation},
],
width: 0.7,
}}
>
イベントリスナーonButtonEnterとonButtonExitがトリガーされたら、関数を呼びます。
/* Button.js */
<VrButton
onClick={this.onButtonClick}
onEnter={this.animateIn}
onExit={this.animateOut}
>
ブラウザーでコードをテストすると、各ボタンの垂直方向位置の間で遷移があるはずです。
アプリケーションの構築とテスト
WebVRをサポートするブラウザーでアプリを開き、http://localhost:8081/vr/index.htmlではなく、自分のIPアドレス、たとえば、http://192.168.1.100:8081/vr/index.htmlで開発用サーバーにアクセスしてください。そこでView in VRボタンをタップすると、フルスクリーン画面になり、立体視レンダリングが始まります。
To upload your app to a server, you can run the npm script npm run bundle, which will create a new folder build within the vr directory with the compiled files. On your web server you should have the following directory structure:
サーバーにアプリをアップロードします。npmスクリプトnpm run bundleを走らせると、vrディレクトリに、コンパイルされたファイルが含まれた新しいフォルダーbuildができます。
Web Server
├─ static_assets/
│
├─ index.html
├─ index.bundle.js
└─ client.bundle.js
追加のリソース
React VRで小規模なWebVRアプリケーションが完成しました。プロジェクトのソースコードはentire project code on GitHubにあります。
React VRには、このチュートリアルでは取り扱わなかったコンポーネントがいくつかあります。
- テキストレンダリングをするTextコンポーネント
- シーンに付け加える4つの異なったライト。AmbientLight、DirectionalLight、PointLight、Spotlight
- Soundコンポーネント。3Dシーン内に空間的な音を加える
- VideoコンポーネントまたはVideoPanoコンポーネントで動画を追加できる。特別なVideoControlコンポーネントでビデオプレーバックとボリュームを制御できる
- Modelコンポーネントで、アプリケーションにobjフォーマットで3Dモデルを加えられます。
- CylindricalPanelコンポーネント。子要素をシリンダーの内面に揃える。たとえば、ユーザーインターフェイス要素を揃える
- 3Dプリミティブの作成に、3つのコンポーネントが使える。sphereコンポーネント、planeコンポーネント、boxコンポーネントです。
React VRはまだ開発中なので、Carmel Developmer Previewブラウザーでしか動きません。React VRをもっと詳しく学びたければ、次のおもしろそうなリソースを参考にしてください。
- React VR ドキュメント
- GitHubのReact VR
- React VRReact VRリソース集
一般的なWebVRは、これらの記事が役に立ちます。
本記事はMoritz KrögerとTim Severienが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。
(原文:Building a Full-Sphere 3D Image Gallery with React VR)
[翻訳:関 宏也/編集:Livit]