Esteban님의 게시글 Getting started with React VR(CC BY) 전문을 번역했습니다.


개요

React VR은 웹 개발자가 React, 특히 React Native의 선언적 접근 방식을 사용하여 가상 현실(VR) 애플리케이션을 개발할 수 있도록 도와줍니다.

React VR은 Three.js를 사용하여 저수준의 WebVR과 WebGL API를 지원합니다. WebVR은 웹에서 VR 장치에 액세스하는 데 사용되는 API입니다. WebGL(Web Graphics Library)은 플러그인을 사용하지 않고도 호환 가능한 모든 웹 브라우저에서 3D 그래픽을 렌더링하는 데 사용되는 API입니다.

React VR은 Flexbox 레이아웃과 View, Image, Text와 같은 핵심 컴포넡르를 지원한다는 점에서 React Native와 유사합니다. 또한 React VR은 Pano, MeshPointLight와 같은 VR 컴포넌트를 더했습니다.

이 가이드에서 파노라마 이미지, 3D 객체, 버튼 및 Flexbox 레이아웃으로 장면(scene)을 만드는 방법을 배우기 위해 간단한 VR 앱을 만들어보겠습니다. 우리의 모의 앱은 React VR의 공식 샘플과 Mesh and Layout 샘플의 두 가지를 기반으로 합니다.

이 앱은 일부 버튼과 함께 큐브 맵에서 지구와 달의 3D 모델을 확대 및 축소하여 렌더링합니다. 다음은 완성된 앱의 모습입니다.

earth

모델, 크기 및 회전은 실제 지구와 달을 복제한 것은 아닙니다. 이는 React VR이 어떻게 동작하는지를 보여주기 위한 것입니다. 그 과정에서 중요한 3D 모델링 개념을 설명할 것입니다. React VR에 익숙해지면 자유롭게 태양을 추가하거나 더 많은 행성을 추가하는 등 여러가지를 만들 수 있습니다.

GitHub에서 최종 앱의 소스 코드를 찾을 수 있습니다.


시작하기

첫 번째 React VR 프로젝트를 시작하기 전에, React VR 응용 프로그램을 빌드하고 관리하는 데 사용되는 Node.js와 React VR CLI를 설치해야 합니다.

이미 Node.j가 설치되어 있다면, 버전이 4.0 이상인지 확인하세요.


프로젝트 생성하기

먼저 React VR CLI 툴을 설치해야 합니다. NPM을 하겠습니다.

npm install -g react-vr-cli

React VR CLI를 사용해서 EarthMoonVR이라는 이름으로 애플리케이션을 만들어 보겠습니다.

react-vr init EarthMoonVR

이렇게 하면 샘플 애플리케이션이 있는 EarthMoonVR 디렉토리가 만들어지고, 필요한 종속성도 설치되므로 시간이 조금 걸릴 수도 있습니다. Yarn으로 설치하면 작업 속도를 올릴 수 있습니다.

이렇게하면 샘플 응용 프로그램이있는 EarthMoonVR 디렉토리가 만들어지며 필요한 종속성도 설치되므로 시간이 오래 걸릴 수 있습니다. 원사를 설치하면 작업 속도가 빨라집니다.

(Yarn에 대해 더 알고 싶으면 이 가이드를 참고하세요.)

설치가 완료되면 해당 디렉토리로 이동합니다.

cd EarthMoonVR

샘플 애플리케이션을 테스트하기 위해, 로컬 개발 서버를 실행할 수 있습니다.

npm start

브라우저를 열고 http://localhost:8081/vr에 접속합니다. 앱을 빌드하고 초기화하는 데 시간이 오래 걸릴 수 있지만, 곧 다음과 같은 화면이 표시됩니다.

helloworld


React VR

이 샘플 앱의 디렉토리 구조는 다음과 같습니다.

+-node_modules
+-static_assets
+-vr
\-.gitignore
\-.watchmanconfig
\-index.vr.js
\-package.json
\-postinstall.js
\-rn-cli-config.js

응용 프로그램 코드가 들어있는 index.vr.js 파일과 이미지 및 3D 모델과 같은 외부 리소스 파일이 들어있는 static_assets 디렉토리를 강조하고 싶습니다.

여기서 프로젝트 구조에 대해 더 많이 알 수 있습니다.

다음은 index.vr.js 파일의 내용입니다.

import React from 'react';
import { AppRegistry, asset, StyleSheet, Pano, Text, View } from 'react-vr';

class EarthMoonVR extends React.Component {
  const textStyle = {
    backgroundColor:'blue',
    padding: 0.02,
    textAlign:'center',
    textAlignVertical:'center',
    fontSize: 0.8,
    layoutOrigin: [0.5, 0.5],
    transform: [{translate: [0, 0, -3]}]
  }
  render() {
    return (
      <View>
        <Pano source={asset('chess-world.jpg')}/>
        <Text
          style={textStyle}>
          hello
        </Text>
      </View>
    );
  }
};

AppRegistry.registerComponent('EarthMoonVR', () => EarthMoonVR);

React VR은 ES2015JSX를 사용한다는 것을 알 수 있습니다.

이 코드는 컴파일 (ES2015, JSX), 번들링 및 asset 로딩을 제공하는 Reactive Native 패키지 도구에 의해 전처리됩니다.

render 함수의 return 문에는 다음이 있습니다.

  • View 컴포넌트, 일반적으로 다른 컴포넌트의 컨테이너로 사용됩니다.
  • Pano 컴포넌트, 우리의 실세계를 형성하는 360 사진(chess-world.jpg)을 렌더링합니다.
  • Text 컴포넌트, 3D 공간에서 문자열을 렌더링하는 컴포넌트입니다.

Text 컴포넌트가 스타일 객체와 함께 어떻게 스타일되는지 주목해야 합니다. React VR의 모든 컴포넌트는 모양과 레이아웃을 제어하는 데 사용할 수있는 style 속성을 가지고 있습니다.

React VR은 Pano 또는 VrButton과 같은 특수 컴포넌트를 추가하는 것 외에도 컴포넌트, props, state, 라이프 사이클 메소드, 이벤트 및 flexbox 레이아웃과 같이, React, React Native와 같은 컨셉으로 작동합니다.

마지막으로, 루트 컴포넌트는 AppRegistry.registerComponent에 자체 등록해야 합니다. 이 때 앱이 비로소 번들링 되고 실행될 수 있습니다.

이제 코드가 하는 일을 알았으니 파노라마 이미지 에 다이빙할 수 있습니다.


파노라마 이미지

일반적으로 VR 앱 내부의 세계는 파노라마(Pano) 이미지로 구성됩니다. 이 이미지는 1000 미터 (React VR 거리 및 치수 단위는 미터 단위)의 구를 만들어 사용자를 중심에 배치합니다.

파노라마 이미지가 360 이미지 또는 구형(spherical) 파노라마라고도 불리는 이유는 위, 아래, 뒤 또는 옆의 모든 각도에서 이미지를 볼 수 있기 때문입니다.

360 파노라마의 두 가지 주요 형식은 등장방형 equirectangular입방체 cubic 입니다. React VR은 이 두 가지 모두를 지원합니다.

등장방형 파노라마

등장방형 파노라마는 종횡비가 2:1 인 단일 이미지로 이루어집니다. 즉, 폭이 높이의 두 배입니다.

이 이미지는 특별한 360 카메라로 생성됩니다. 등장방형 이미지의 훌륭한 소스는 Flickr입니다. equirectangular를 태그로 검색하면 됩니다. 예를 들어, 이것으로 검색해서 이 사진을 찾았습니다.

sample

이상하게 보이나요?

어쨌든, 이 사진(가능한한 가장 고화질)을 프로젝트의 static_assets 디렉토리로 다운로드하고 다음과 같이 render 함수를 바꿉니다.

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

Pano 컴포넌트의 source 속성은 이미지의 위치와 함께 uri 속성을 객체로 가지고 있습니다.

여기서 우리는 static_assets 디렉토리에서 sample_pano.jpg 파일을 보기 위해 uri 속성에 올바른 경로를 사용하여 객체를 반환하는 asset 함수를 사용하고 있습니다. 즉, 위의 코드는 다음과 같습니다.

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

브라우저를 새로고침하면 (로컬 서버가 여전히 실행 중이라고 가정) 다음과 같은 내용이 표시됩니다.

sample_pano2

페이지에 변화가 있을 때마다 새로 고침을 하지 않으려면 URL(http://localhost:8081/vr/?hotreload)에 ?hotreload를 더해 hot reloading 을 할 수 있습니다.

입방체 파노라마

Cubemaps는 다른 형식의 360 파노라마입니다. 이 형식은 우리 주변의 구체(sphere)를 채울 큐브의 여섯면에 여섯 개의 이미지를 사용합니다. 이는 스카이 박스(skybox)라고 알려져 있습니다.

기본 아이디어는 큐브를 렌더링하고 시청자가 움직일 때마다, 중앙에 시청자를 배치하는 것입니다.

예를 들어, 큐브의 면들을 나타내는이 이미지를 생각해 봅시다.

side_cube

React VR에서 이 큐브 맵을 사용하려면 Pano 컴포넌트의 소스 속성을 [+ x, -x, + y, -y, + z, -z] 순서로 표시되는 여섯 개의 개별 이미지 배열로 지정해야 합니다. 다음과 같이 말이죠.

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

2D 레이아웃에서 X축은 오른쪽을 가리키고, Y 축은 아래쪽을 가리킵니다. 즉, 왼쪽 위는 (0, 0)이고 오른쪽 아래는 요소의 폭과 높이가 될 것입니다.

그러나 3D 공간에서 React VR은 OpenGL에서 사용하는 것과 동일하게 오른손 좌표계를 사용합니다. 양수 X는 오른쪽을 가리키고 양수 Y는 위로 향하며 양수 Z는 사용자를 향합니다. 사용자의 기본 뷰가 원점에서 시작하기 때문인데, 이는 음의 Z 방향을 바라 보면서 시작한다는 의미입니다.

axis

React VR 좌표계에 대한 자세한 내용은 여기를 참조하세요.

이렇게 하면 큐빅맵(혹은 skybox)은 다음과 같이 보입니다.

cubicmap

Skybox는 Unity와 많이 사용되므로 다운로드할 수 있는 곳이 많이 있습니다. 예를 들어, 저는 이 페이지에서 사하라 사막을 다운 받았습니다. 이미지를 추출하고 코드를 다음과 같이 변경하면:

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

다음과 같은 결과를 볼 수 있습니다.

sahara

상단 및 하단 이미지가 잘 맞지 않는 것을 눈치챘나요? 상단 이미지를 시계 방향으로 90도 회전하고 하단 이미지를 시계 반대 방향으로 90도 회전시켜면 문제를 해결할 수 있습니다.

sahara_right

이제 우리 앱을 위한 스카이 박스 공간을 만들어 보겠습니다.

가장 좋은 프로그램은 Spacescape입니다. Spacescape는 Windows 및 Mac에서 사용할 수있는 별과 성운을 비롯한 우주 skybox를 만드는 무료 도구입니다.

이렇게 설장하면:

configuration

skybox에 6개의 이미지를 내보낼 수 있습니다.

export

코드를 다음과 같이 수정하면:

<Pano source={
  {
    uri: [
      '../static_assets/space_right.png',
      '../static_assets/space_left.png',
      '../static_assets/space_up.png',
      '../static_assets/space_down.png',
      '../static_assets/space_back.png',
      '../static_assets/space_front.png'
    ]
  }
}/>

다음과 같은 결과를 볼 수 있습니다.

star

이제 3D 모델에 대해 이야기해 보겠습니다.


스타일링과 버튼

새로운 컴포넌트인 버튼을 만들어 보겠습니다. ViewVrButton에 버튼으로 스타일을 지정을 지정하고, 이것들은 유용한 이벤트(onEnter와 같은)를 가지고 있어서 실제로 사용할 수 있습니다.

그러나 VrButton은 다른 상태 머신을 가지고 있고 onClickonLongClick 이벤트를 추가할 수 있기 때문에 VrButton을 사용하겠습니다.

또한, 스타일 시트를 사용하여 스타일 객체를 만들어 ID로 스타일을 참조할 것입니다.

button.js 파일에 포함 된 내용은 다음과 같습니다.

import React from 'react';
import {
  StyleSheet,
  Text,
  VrButton,
} from 'react-vr';

export default class Button extends React.Component {
  constructor() {
    super();
    this.styles = StyleSheet.create({
      button: {
        margin: 0.05,
        height: 0.4,
        backgroundColor: 'red',
      },
      text: {
        fontSize: 0.3,
        textAlign: 'center',
      },
    });
  }

  render() {
    return (
      <VrButton style={this.styles.button}
        onClick={() => this.props.callback()}>
        <Text style={this.styles.text}>
          {this.props.text}
        </Text>
      </VrButton>
    );
  }
}

VrButton은 모양이 없으므로 스타일을 지정해줘야 합니다. ImageText 컴포넌트를 래핑할 수도 있습니다. 또한 버튼을 컴포넌트의 속성으로 클릭했을 때 실행될 함수와 텍스트를 전달합니다.

이제 루트 컴포넌트의 render 메소드 안에서 Button 컴포넌트를 가져오고, View에 래핑된 두 개의 버튼을 추가하겠습니다.

...
import Button from './button.js';

class EarthMoonVR extends React.Component {
  ...

  render() {
    return (
      <View>
        ...

        <AmbientLight intensity={ 2.6 }  />

        <View>
          <Button text='+'  />
          <Button text='-'  />
        </View>

        ...
      </View>
    );
  }
};

이 버튼은 모델의 Z축 값을 변경하여 줌 컨트롤을 나타냅니다. 초기값은 -70 (지구의 Z축 값)으로 zoom이라는 상태 속성을 추가하고, 확대/축소 콜백 및 모델을 수정하여 사용합니다.

class EarthMoonVR extends React.Component {
  constructor() {
    super();
    this.state = {
      rotation: 130,
      zoom: -70,
    };
    ...
  }

  render() {
    const earthStyle = {
      transform: [
        {translate: [-25, 0, this.state.zoom]},
        {scale: 0.05 },
        {rotateY: this.state.rotation},
        {rotateX: 20},
        {rotateZ: -10}
      ]
    }
    const moonStyle = {
      transform: [
        {translate: [10, 10, this.state.zoom - 30]},
        {scale: 0.05},
        {rotateY: this.state.rotation / 3},
      ]
    }
    const earthSource = {
      mesh:asset('earth.obj'), mtl:asset('earth.mtl'), lit: true
    }
    const moonSource = {
      mesh:asset('moon.obj'), mtl:asset('moon.mtl'), lit: true
    }
    return (
      <View>

       ...

        <View>
          <Button text='+'
            callback={() => this.setState((prevState) => ({ zoom: prevState.zoom + 10 }) ) } />
          <Button text='-'
            callback={() => this.setState((prevState) => ({ zoom: prevState.zoom - 10 }) ) } />
        </View>

        <Mesh
          style={earthStyle}
          source={earthSource}
        />

        <Mesh
          style={moonStyle}
          source={moonSource}
        />
      </View>
    );
  }
};

이제 flexboxStyleSheet.create를 사용하여 버튼을 감싸는 View를 스타일링 해보겠습니다.

class EarthMoonVR extends React.Component {
  constructor() {
    super();
    ...
    this.styles = StyleSheet.create({
      menu: {
        flex: 1,
        flexDirection: 'column',
        width: 1,
        alignItems: 'stretch',
        transform: [{translate: [2, 2, -5]}],
      },
    });
    ...
  }

  render() {
    return (
      <View>
        ...

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

        ...

      </View>
    );
  }
};

flexbox 레이아웃에서 자식은 flexDirection: 'column' 으로 세로로 정렬하거나 flexDirection : 'row'로 가로로 배열 할 수 있습니다. 이 예제에서는 너비가 1 미터이고, stretch 정렬이 있는 한 열의 flexbox를 설정합니다. 즉, 요소가 컨테이너 너비를 차지한다는 의미입니다.

마지막으로 skybox 이미지를 render 메소드 밖으로 가져와 너무 붐비지 않게 할 수 있습니다.

import React from 'react';
import {
  AppRegistry,
  asset,
  StyleSheet,
  Pano,
  Text,
  View,
  Mesh,
  AmbientLight,
} from 'react-vr';
import Button from './button.js';

class EarthMoonVR extends React.Component {
  constructor() {
    super();
    this.state = {
      rotation: 130,
      zoom: -70,
    };
    this.lastUpdate = Date.now();
    this.spaceSkymap = [
      '../static_assets/space_right.png',
      '../static_assets/space_left.png',
      '../static_assets/space_up.png',
      '../static_assets/space_down.png',
      '../static_assets/space_back.png',
      '../static_assets/space_front.png'
    ];
    this.styles = StyleSheet.create({
      menu: {
        flex: 1,
        flexDirection: 'column',
        width: 1,
        alignItems: 'stretch',
        transform: [{translate: [2, 2, -5]}],
      },
    });

    this.rotate = this.rotate.bind(this);
  }

  componentDidMount() {
    this.rotate();
  }

  componentWillUnmount() {
    if (this.frameHandle) {
      cancelAnimationFrame(this.frameHandle);
      this.frameHandle = null;
    }
  }

  rotate() {
    const now = Date.now();
    const delta = now - this.lastUpdate;
    this.lastUpdate = now;

    this.setState({
        rotation: this.state.rotation + delta / 150
    });
    this.frameHandle = requestAnimationFrame(this.rotate);
  }

  render() {
    const earthStyle = {
      transform: [
        {translate: [-25, 0, this.state.zoom]},
        {scale: 0.05 },
        {rotateY: this.state.rotation},
        {rotateX: 20},
        {rotateZ: -10}
      ]
    }
    const moonStyle = {
      transform: [
        {translate: [10, 10, this.state.zoom - 30]},
        {scale: 0.05},
        {rotateY: this.state.rotation / 3},
      ]
    }
    const earthSource = {
      mesh:asset('earth.obj'), mtl:asset('earth.mtl'), lit: true
    }
    const moonSource = {
      mesh:asset('moon.obj'), mtl:asset('moon.mtl'), lit: true
    }

    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={earthStyle}
          source={earthSource}
        />

        <Mesh
          style={moonStyle}
          source={moonSource}
        />
      </View>
    );
  }
};

AppRegistry.registerComponent('EarthMoonVR', () => EarthMoonVR);

애플리케이션을 테스트하면 다음과 같은 버튼이 표시됩니다.

styling


결론

React VR은 빠르고 쉽게 VR 경험을 만들 수 있는 라이브러리입니다. A-Frame과 같이 더 완벽한 기능 셋과 더 큰 커뮤니티를 가진 대안도 있습니다. 그러나 360 파노라마, 3D 모델 및 간단한 애니메이션을 중심으로 VR 응용 프로그램을 만들고 싶다면, 그리고 React/React Native를 이미 알고 있다면 React VR을 사용하는 것이 가장 좋습니다. React VR이 좋은 발판이 된다면, 진보된 VR 플랫폼으로 마음껏 실험해 보세요.

GitHub에서 앱의 소스 코드를 찾을 수 있습니다.

읽어 주셔서 감사합니다!