React VRについて調べてみた

昨年、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
/* setStateによってnextLocationIdを出し分け、遷移を実現している */
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
// 作成したコンポーネントに対してカスタム3Dオブジェクトを当て込んでいる
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初心者なので、なにか間違っていたらご指摘ください。

参考

このエントリーをはてなブックマークに追加