React VRについて調べてみた

MMM Corporation
mmmuser

昨年、React VRがプレリリースされた。

VR開発に興味はあるものの、なかなか手を付けられなかったが、とりあえずしらべてみることにした。

React VRとは?

Oculusのブログを読むのが手っ取り早いが、WebVRをつかって、ブラウザ上でVR体験を可能にするには、平面での開発とは勝手が異なり、3Dや映像処理の知識がなければ難しい。

React VRは、それらを隠蔽し、従来のWeb開発に近い形で開発できるライブラリである。同じような思想のライブラリ(フレームワーク)として、A-Frameなどがあるが、React VRのメリットとしては Reactが使える ことにあると思う。例えば、チュートリアルで紹介されているコンポーネントは以下のような感じになる。

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モデル

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>
    );
  }
};

レイアウト

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>
    );
  }
}

ツアー

/* 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

// 作成したコンポーネントに対してカスタム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;
}

他にも、以下の様なことができる。

パノラマ写真

render() {
  return (
    <View>
      <Pano source={asset('sample_pano.jpg')}/>
    </View>
  );
}

自分で6方位のを画像で定義

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>
  );
}

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>
  );
}

ズームイン・アウト、アニメーション

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初心者なので、なにか間違っていたらご指摘ください。

参考

AUTHOR
デロイト トーマツ ウェブサービス株式会社(DWS)
デロイト トーマツ ウェブサービス株式会社(DWS)
デロイト トーマツ ウェブサービス株式会社はアマゾン ウェブ サービス(AWS)に 専門性や実績を認定された公式パートナーです。
記事URLをコピーしました