昨年、React VRがプレリリースされた。
VR開発に興味はあるものの、なかなか手を付けられなかったが、とりあえずしらべてみることにした。
React VRとは?
Oculusのブログを読むのが手っ取り早いが、WebVRをつかって、ブラウザ上でVR体験を可能にするには、平面での開発とは勝手が異なり、3Dや映像処理の知識がなければ難しい。
React VRは、それらを隠蔽し、従来のWeb開発に近い形で開発できるライブラリである。同じような思想のライブラリ(フレームワーク)として、A-Frameなどがあるが、React VRのメリットとしては Reactが使える ことにあると思う。例えば、チュートリアルで紹介されているコンポーネントは以下のような感じになる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class WelcomeToVR extends React.Component { render() { return ( <View> <Pano source={asset('chess-world.jpg')}/> <Text style={{ backgroundColor: '#777879', fontSize: 0.8, layoutOrigin: [0.5, 0.5], paddingLeft: 0.2, paddingRight: 0.2, textAlign: 'center', textAlignVertical: 'center', transform: [{translate: [0, 0, -3]}], }}> hello </Text> </View> ); } };
|
ビューに関してはおなじみ、このあたりはReact Nativeと同じような感じである。また、<View>
、<Text>
など、React Nativeでみたことのあるコンポーネントが並んでいる。
どんな仕組み?
仕組みとしては、以下の図のようなレイヤーになる。

これもブログを読むのが早いのだが、内部的には、Three.jsをOVRUIでVR用に変換している。また、ReactのコードはWebWorkerを使用して非同期に走るので、パフォーマンス的にも最適化されているとのこと。
自作VRをみるにはどうしたらいいの?
自作VRをみるには、Gear VRを使用し、Oculus StoreでCarmel Developer PreviewというWebVR APIが実装されたブラウザをインストール、その上でVRを動かす。
Carmelはまだプレビュー版なので、アドレスバーがでない
、2D web contentが表示されない
など未実装な部分がある。しかし、WebVRに特化するなら、最新のAPIが使えるし、最適化されているとのこと。また、優先的にこれらを改善していくとアナウンスもしており、FBの本気を感じる。
React VRは、Gear VRをもっていない場合でも、普段使用しているブラウザで開発ができる。ただし、もちろんVRができるわけではなく、自動的にWebGLへダウングレードされたUIを開発することとなる。
デバッグに関しては、Hot Reloading、NuclideのReact Dev Tools Inspectorをサポートしていて、React Native同様に、Webの世界の開発イテレーションをそのまま持ち込める(そもそもReact Nativeのようにネイティブの世界で動かしているわけでもない)。また、WebVRだけあって、Chrome Developer Toolsも使用できる。これらをみると、特にストレスなくデバッグサイクルがまわるはずだと考えた。
どんなことができるの?
今回は、いくつかサンプルを動かしてみようと思う。まずはGetting Startedのサンプルをみていくが、それぞれ以下のような感じである。コードは一部抜粋なので、興味のある人は参考リンクからとんでください。
Meshモデル

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| class MeshSample extends React.Component { constructor() { super(); this.state = {rotation: 0}; this.lastUpdate = Date.now(); this.rotate = this.rotate.bind(this); }
rotate() { const now = Date.now(); const delta = now - this.lastUpdate; this.lastUpdate = now; this.setState({rotation: this.state.rotation + delta / 20}); this.frameHandle = requestAnimationFrame(this.rotate); }
componentDidMount() { this.rotate(); }
componentWillUnmount() { if (this.frameHandle) { cancelAnimationFrame(this.frameHandle); this.frameHandle = null; } }
render() { return ( <View> <Pano source={asset('chess-world.jpg')}/> <Mesh style={{ transform: [ {translate: [0, -15, -70]}, {scale : 0.1 }, {rotateY : this.state.rotation}, {rotateX : -90} ], }} source={{mesh:asset('creature.obj'), mtl:asset('creature.mtl'), lit: true}} /> <PointLight style={{color:'white', transform:[{translate : [0, 400, 700]}]}} />
<Text style={style}>Creature</Text> </View> ); } };
|
レイアウト

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class LayoutSample extends React.Component { render() { return ( <View> <Pano source={asset('chess-world.jpg')} /> <View style={{ flex: 1, flexDirection: 'column', width: 2, alignItems: 'stretch', transform: [{translate: [-1, 1, -5]}], }}> <HighlightView text='Red' backgroundColor='red'/> <HighlightView text='Orange' backgroundColor='orange'/> <HighlightView text='Yellow' backgroundColor='yellow'/> <HighlightView text='Green' backgroundColor='green'/> <HighlightView text='Blue' backgroundColor='blue'/> </View> </View> ); } }
|
ツアー

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| class TourSample extends React.Component { render() { return ( <View> <View style={{transform:[{rotateY: rotation}]}}> <Pano source={asset(this.state.data.photos[this.state.nextLocationId].uri)}/> <NavButton key={tooltip.linkedPhotoId} isLoading={isLoading} onClickSound={asset(soundEffects.navButton.onClick.uri)} onEnterSound={asset(soundEffects.navButton.onEnter.uri)} onInput={() => { this.setState({nextLocationId: tooltip.linkedPhotoId}); }} rotateY={tooltip.rotationY} source={asset(this.state.data.nav_icon)} textLabel={tooltip.text} translateZ={this.translateZ} /> </View> </View> ); } };
|
Cube

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| function init(bundle, parent, options) {
const scene = new THREE.Scene(); const cubeModule = new CubeModule();
const vr = new VRInstance(bundle, 'CubeSample', parent, { cursorVisibility: 'visible', nativeModules: [ cubeModule ], scene: scene, });
const cube = new THREE.Mesh( new THREE.BoxGeometry(1, 1, 1), new THREE.MeshBasicMaterial() ); cube.position.z = -4; scene.add(cube);
cubeModule.init(cube);
vr.render = function(timestamp) { const seconds = timestamp / 1000; cube.position.x = 0 + (1 * (Math.cos(seconds))); cube.position.y = 0.2 + (1 * Math.abs(Math.sin(seconds))); };
vr.start(); return vr; }
|
他にも、以下の様なことができる。
パノラマ写真

1 2 3 4 5 6 7
| render() { return ( <View> <Pano source={asset('sample_pano.jpg')}/> </View> ); }
|
自分で6方位の面
を画像で定義

1 2 3 4 5 6 7 8 9 10 11 12 13 14
| render() { return ( <View> <Pano source={{ uri: [ '../static_assets/sample_right.jpg', '../static_assets/sample_left.jpg', '../static_assets/sample_top.jpg', '../static_assets/sample_bottom.jpg', '../static_assets/sample_back.jpg', '../static_assets/sample_front.jpg' ]}} /> </View> ); }
|

1 2 3 4 5 6 7 8 9 10 11 12 13 14
| render() { return ( <View> <Pano source={{ uri: [ '../static_assets/sahara_rt.jpg', '../static_assets/sahara_lf.jpg', '../static_assets/sahara_up.jpg', '../static_assets/sahara_dn.jpg', '../static_assets/sahara_bk.jpg', '../static_assets/sahara_ft.jpg' ]}} /> </View> ); }
|
ズームイン・アウト、アニメーション

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| render() { return ( <View> <Pano source={ {uri: this.spaceSkymap} }/> <AmbientLight intensity={ 2.6 } /> <View style={ this.styles.menu }> <Button text='+' callback={() => this.setState((prevState) => ({ zoom: prevState.zoom + 10 }) ) } /> <Button text='-' callback={() => this.setState((prevState) => ({ zoom: prevState.zoom - 10 }) ) } /> </View> <Mesh style={{ transform: [ {translate: [-25, 0, this.state.zoom]}, {scale: 0.05 }, {rotateY: this.state.rotation}, {rotateX: 20}, {rotateZ: -10} ] }} source={{mesh:asset('earth.obj'), mtl:asset('earth.mtl'), lit: true}} /> <Mesh style={{ transform: [ {translate: [10, 10, this.state.zoom - 30]}, {scale: 0.05}, {rotateY: this.state.rotation / 3}, ] }} source={{mesh:asset('moon.obj'), mtl:asset('moon.mtl'), lit: true}} /> </View> ); }
|
その他
- Styleは、React Nativeと同じような感じだが、flexboxをはじめとするCSSライクな構文が使える
- Three.jsでカスタムオブジェクトも作成可能。React Nativeと違い、JSだけ書けばいいと思うと楽
- 現時点ではAPIもけっこう限られているため、Three.jsに詳しくなければ、コンテンツの向き不向きをしっかり考えたほうが良さそう。
- ドキュメントはさくっと読み切れる量なので、事前に使えるAPIを把握しておくと良さそう
まとめ
ブラウザで見るぶんにはただのWebGLなのでそこまで感動はないが、これがそのままVRに展開できると考えるとすごいと思う。また、React VRによって宣言的にビューを作れるのも、たしかに開発効率が上がりそうである。Three.jsの時代と違い、適度に隠蔽されていて、コードが非常に読みやすいのは嬉しい。
WebVRに関しては過去にも度々話題になっていたが、効率的に開発できる手段が成熟していくと、これまでなかった可能性が広がりそうで面白い。
例えば、WebVRそのもののメリットとして、VR端末を持っていなくても、ブラウザからWebGLとしてアクセスできるため、VR専用コンテンツにする必要が無い(より多くのユーザーにアプローチできる)、従来のブラウザと同じようにシンプルに共有できる、などがある。向き不向きはあるが、ゲーム以外の、アクセスやシェアが重要になってくる、動画や画像ベースのコンテンツで有益なのではないだろうか。
以上、React VRについてしらべてみました。VR初心者なので、なにか間違っていたらご指摘ください。
参考