React VRで太陽系を創ろう、3次元モデリングとアニメーションの基礎(前編)【がっつりReact!シリーズ 第2回】
サブシリーズ【がっつりReactシリーズ】として、React関連技術を紹介していきます。前回記事ではReact VRの紹介と、VRコンテンツの作成手順を示しました。引き続きReact VRを紹介していきます
ソフト道場のがっつり試し食い!
- 2017年09月29日公開
はじめに
お久しぶりです。上原です。 お待たせしました【がっつりReact!シリーズ】React VR紹介第二段です。
前回記事「VRコンテンツをWebアプリのように開発するReact VR登場」では、React VRの紹介と、VRコンテンツの作成手順を示しました。引き続きReact VRを紹介していきます。
お題:太陽系を作る
唐突ですが、みなさんもお住まいの「太陽系」をVR体験可能な3Dモデルとして作ってみましょう。まずは前回でやりかたを示したように、React VRプロジェクトを作ります。「光あれ」みたいなことをつぶやきながらやると気分が盛り上がるかもしれません。
$ react-vr init SpaceTour
$ cd SpaceTour
$ rm static_assets/chess-world.jpg
$ mkdir src
$ mkdir src/components
最初に地球を作りましょう。React VRで用意されているSphere(球体)コンポーネントを使用し、Reactのファンクショナルコンポーネントとして地球の3Dモデルを定義します。
[src/components/Earth.js]
import React from 'react';
import { Sphere, asset } from 'react-vr';
const Earth = props => <Sphere texture={asset('2k_earth_daymap.jpg')} {...props} />;
export default Earth;
ここで使用している地球のテクスチャファイルは、Creative Commonsライセンスでモデルを配布している以下のサイトから今回入手します。
後で使うものも含めて、上記サイトから[DOWNLOAD]のボタンを押して、以下の4つの画像ファイルを<PROJECT>/static_assetsフォルダ配下に保存しておきます。
- 2k_earth_daymap.jpg
- 2k_moon.jpg
- 2k_stars_milky_way.jpg
- 2k_sun.jpg
先程の地球のモデルを配置するために、<PROJECT>/index.vr.jsを以下のように編集します。
[index.vr.js]
import React from 'react';
import { AppRegistry, asset, View } from 'react-vr';
import Earth from './src/components/Earth';
export default class SpaceTour extends React.Component {
render() {
return (
<View>
<Earth />
</View>
);
}
}
AppRegistry.registerComponent('SpaceTour', () => SpaceTour);
その上で
$ npm start
でReact VRを開発モードで起動し、URL http://localhost:8081/vr/?hotreload をアクセスしてみます。しかしこれだけでは残念ながら地球は表示されません。いくつかの問題があります。
視野に入ってない
まず、上記のように位置指定を行なわずに3Dモデルを配置したとき、デフォルトではモデルは原点(0,0,0)に配置されます。ちなみに、React VRでの座標系は以下のとおりでいわゆる「右手系」と呼ばれるものです。長さの単位はメートル(1=1m)、回転角度の単位は度(°)になります。
問題は、視点のデフォルト位置が原点(0,0,0)、視線はZ軸の負の方向だということです。つまり以下の状態です。
この状態では視点が地球の中にはいっていることになり、見ることができません。視点かモデルのいずれかを移動する必要があります。ここではSceneコンポーネントを使用して、transformスタイルのtranslateで視点をz軸の正の方向に移動しておきます。
import {
AppRegistry,
asset,
View,
Scene,
} from 'react-vr';
:
export default class SpaceTour extends React.Component {
render() {
return (
<View>
<Scene style={{ transform: [{ translate: [0, 0, 10] }] }} />
<Earth />
</View>
);
}
}
この結果以下のようになります。transform, translateについては後で詳しく説明します。
これによって原点に配置された地球が視野に入ってくるので、以下のように地球が表示されました。 しかしカクカクしてて美しくないですね。
Sphereの分割数を増やす
Sphereのリファレンスを読むと、widthSegments,heightSegmentsを指定すればカクカクを直せそうです。Earth.jsを以下のように修正します。
<Sphere
widthSegments={20}
heightSegments={12}
texture={asset('2k_earth_daymap.jpg')}
{...props}
/>
カクカクしているのは直りましたが、のっぺりしています。球体感がありません。
照明がない
これは照明が無いためなので、点光源オブジェクト(PointLight)と、多少の環境光(AmbientLight)を追加します。これらもReactコンポーネントです。
[index.vr.js]
import {
AppRegistry,
AmbientLight,
PointLight,
asset,
View,
Scene,
} from 'react-vr';
:
export default class SpaceTour extends React.Component {
render() {
return (
<View>
<PointLight intensity={2.0} style={{ transform: [{ translate: [1, 1, 10] }] }} />
<AmbientLight intensity={0.1} />
:
また、地球に照明の影響を受けさせるために、Sphereの属性lit={true}を追加します。(指定しないと、照明が物体の表示に影響しなくなる。)
[src/components/Earth.js]
<Sphere
lit={true}
widthSegments={20}
heightSegments={12}
texture={asset('2k_earth_daymap.jpg')}
{...props}
/>
とりあえずこんな感じでできました。一休みして「地球か...何もかもが皆懐しい...」などとつぶやくのも良いでしょう。 ここまでのEarth.jsおよびindex.vr.js全体は以下のとおりです。
[src/components/Earth.js]
import React from 'react';
import { Sphere, asset } from 'react-vr';
const Earth = props => (
<Sphere
lit={true}
widthSegments={20}
heightSegments={12}
texture={asset('2k_earth_daymap.jpg')}
{...props}
/>
);
export default Earth;
[index.vr.js]
import React from 'react';
import {
AppRegistry,
AmbientLight,
PointLight,
asset,
View,
Scene,
} from 'react-vr';
import Earth from './src/components/Earth';
export default class SpaceTour extends React.Component {
render() {
return (
<PointLight
intensity={2.0}
style={{ transform: [{ translate: [1, 1, 10] }] }}
/>
<AmbientLight intensity={0.1} />
<Scene style={{ transform: [{ translate: [0, 0, 10] }] }} />
<View>
<Earth />
</View>
);
}
}
AppRegistry.registerComponent('SpaceTour', () => SpaceTour);
React VRコンポーネントの配置
React VRのコンポーネントの位置指定は、CSSのそれを意識したFlexboxベースでの指定と、style属性のtransformプロパティで設定するものの2種類があります。以降、それぞれについて解説します。
Flexboxベースの位置指定
Viewの直接の子コンポーネントとしてTextもしくはViewを配置したとき、CSSのFlexboxのような、矩形領域を流し込んでいくような指定が可能です。
<View style={styles.container}>
<Text style={styles.block1}>
The quick brown fox jumps over the lazy dog aaa aaa aaa aaa aaa aaa
</Text>
<Text style={styles.block2}>
The quick brown fox jumps over the lazy dog aaa aaa aaa aaa aaa aaa
</Text>
<View style={styles.block3} />
</View>
:
const styles = StyleSheet.create({
container: {
width: 8,
height: 5,
justifyContent: 'space-between',
backgroundColor: 'blue',
},
block1: { fontSize: 0.5, backgroundColor: 'green' },
block2: { fontSize: 0.5, backgroundColor: 'orange' },
block3: { width: 8, height: 1, backgroundColor: 'yellow' },
});
例えば上記のように、FlexboxのjustyContents=space-aroundで指定した1つのViewの直下に2つのTextと1つのViewを記述した場合、以下のように表示されます。
このViewの移動や回転をしない場合、原点に配置される点はViewの矩形の左上になります。
上記でわかるようにReact VRでのFlexboxは3次元に拡張しているわけではなく、3次元空間に浮かぶ「板」としてのView内での2次元配置の機能であり、CSSと同様にRowとColumnの二軸で配置していきます。
transformプロパティによる位置指定
先に示したように、Sceneによる視点の位置指定は以下のようなものでした。CSSスタイルのプロパティ「transform」を設定しています。
<Scene style={{ transform: [{ translate: [0, 0, 10] }] }} />
transformプロパティには、以下のような変換コマンドの配列を値として指定します。
{ 変換コマンド: 引数 }
変換コマンドには以下があります。
変換コマンド | 意味 | 引数 |
---|---|---|
translate | 並行移動 | [xへの移動値,y方向への移動値,z方向への移動値] |
translateX, translateY, translateZ | x,y,z軸方向の並行移動 | x,y,zへの移動値 |
scale | 拡大・縮小 | 3要素の配列の場合、[xへのスケール値,y方向へのスケール値,z方向へのスケール値]、数値の場合3方向の単一のスケール値。 |
scaleX, scaleY, scaleZ | x,y,z方向の拡大・縮小 | それぞれの軸方向のスケール値(1.0だと等倍) |
rotateX, rotateY, rotateZ | 回転 | それぞれの軸方向の回転(°単位) |
matrix | 行列による回転・移動・拡大 | 変換行列として扱われる16要素(4x4)の数値の配列。 |
transform配列が複数の変換コマンドを要素としてもつとき、末尾の要素から順に変換コマンドが適用されます。順序は結果に影響をおよぼしますので重要です。
また、コンポーネントに親子関係があるとき、子のコンポーネントの変換に先立ち、親のコンポーネントのtransformが適用されます。親を移動すれば子コンポーネントもそれに追随して移動するということです。あるいは子の観点から言えば、子コンポーネントのtransformでの位置指定は、親の位置からの相対的な指定になるということです。拡大縮小についても、親の拡大・縮小によって子コンポーネントも拡大・縮小されます。ちなみに親のtransformが子に適用されることについてSceneコンポーネントは例外で、Sceneコンポーネントのtransformスタイルは、視点の移動にのみ影響し、子コンポーネントに影響を与えません。
Flexboxベースとtransformプロパティの使い分け
基本的には3Dモデルの配置にtransformプロパティを使用し、その一部としての情報の提示やUI部品の配置にはFlexboxベースの位置指定を適宜組合わせて使っていくことになるでしょう。Flexboxベースにせよそれを保持するトップレベルのViewはtransformプロパティで位置指定を行うことになります。Flexboxベースで配置される対象としては、TextやView以外に、SphereやBoxなどの3Dオブジェクトを使用することもできますし、Flexboxベースで配置された何かをさらにtransformプロパティで動かすこともできます。
太陽と月
準備ができたので、天体をどんどん作っていきましょう。
[src/components/Moon.js]
import React from 'react';
import { Sphere, asset } from 'react-vr';
const Moon = props => (
<Sphere
lit={true}
widthSegments={20}
heightSegments={12}
texture={asset('2k_moon.jpg')}
{...props}
/>
);
export default Moon;
[src/components/Sun.js]
import React from 'react';
import { Sphere, asset } from 'react-vr';
const Sun = props => (
<Sphere
lit={true}
widthSegments={40} heightSegments={24}
texture={asset('2k_sun.jpg')}
{...props}>
</Sphere>
);
export default Sun;
ここではとりあえずサイズや位置は呼び出し側から指定させるようにするため、{...props}で右から左に受け渡すようにしています。 配置してみます。
[index.vr.js]
import React from 'react';
import {
AppRegistry,
AmbientLight,
PointLight,
asset,
View,
Scene,
} from 'react-vr';
import Earth from './src/components/Earth';
import Sun from './src/components/Sun';
import Moon from './src/components/Moon';
export default class SpaceTour extends React.Component {
render() {
return (
<View>
<PointLight
intensity={2.0}
style={{ transform: [{ translate: [1, 1, 10] }] }}
/>
<AmbientLight intensity={0.1} />
<Scene style={{ transform: [{ translate: [0, 0, 10] }] }} />
<Earth style={{ transform: [{ translate: [-8, 0, 0] }] }} />
<Sun style={{ transform: [{ translate: [0, 0, 0] }, { scale: 9 }] }} />
<Moon
style={{ transform: [{ translate: [-10, 0, 0] }, { scale: 0.5 }] }}
/>
</View>
);
}
}
AppRegistry.registerComponent('SpaceTour', () => SpaceTour);
なんとなくそれっぽい感じになりました。なお、天体の比率は実際とは異なります。 気になるのは、太陽がテクスチャを貼った球なので、照明に照らされて明暗ができているのと、地球や月が本来は輝く太陽からの光に照らされていないことです。 これを直すために、点光源PointLightを太陽の中心に配置し、かつ太陽自身のlit属性は設定しない(照明の影響を受けない)ようにします。
[src/components/Sun.js]
import React from 'react';
import { Sphere, PointLight, asset } from 'react-vr';
const Sun = props => (
<Sphere
widthSegments={40}
heightSegments={24}
texture={asset('2k_sun.jpg')}
{...props}>
<PointLight intensity={2.0} />
</Sphere>
);
export default Sun;
上記に加え、index.vr.jsのAmbientLight, PointLightを削除すると変更を加えると、以下のようになりました。
とりあえずこれで良しとしておきます。 (全体ソースコード)
ここで、Sphereコンポーネントの 子コンポーネントとしてPointLightコンポーネントを配置しているので、点光源はSunコンポーネントの中心に配置され、Sunコンポーネントを移動しても追随します。
ところで、ここで作成したモデルを別の方向から見たくなったとします。
react-devtoolsの使い方
別の方向から見てみたい場合、Sceneコンポーネントのtransformスタイルを変更するのが簡単です。前回示したとおり、「http://localhost:8081/vr/?hotreload」のように「hotreload」パラメタを付けてブラウザで閲覧すれば、ソースコードを保存した瞬間に表示に反映されます。ただ、ソースコードを変更したくない場合などはインスペクタ機能をもったツールreact-devtools(React VRに同梱)で動的に変更できるので、使用方法とあわせて説明します。
react-devtoolsの起動
別の端末で以下を実行します。
$ npm run devtools
すると、react-devtoolsアプリが起動します。これはElectronで開発されたスタンドアローンのインスペクタです。React VR専用ではなくReact(Native)汎用のものです。ちなみにブラウザ上で動作するReact VRで、たとえばChrome本体のDevtoolsのReact拡張のインスペクタ等があるのにこれを使う理由は、React VRではReactの処理がバックグラウンドスレッド(サービスワーカー)で動作し、表のスレッドではCanvasしか見えないからです。
devtoolsの接続
React VRアプリ閲覧中のブラウザのURLに、「devtools」パラメタを付与することで、React VRアプリが上記に接続しに行きます。たとえばブラウザのURL欄に「http://localhost:8081/vr/?hotreload&devtools」を入力すると以下の状態になります。
Sceneのtransform属性を変えてみます。
以下のように視点位置を変化させることができます。また、見難いですがインスペクタのDOMツリー上、マウスの場所にある要素のアウトラインがワイヤフレームで表示されます。
VRビューワでの閲覧
今回作成したVR空間は以下です。「npm run bundle」で生成したものを多少修正して配置してあります。ChromeやWebVRに対応したブラウザで閲覧し「View in VR」をクリックしてみてください。
- React VRによる太陽系デモその1: https://uehaj.github.io/rect-vr-samples/SpaceTour0/index.html (ソースコード)
次回予告
さて、これらの星たちには是非動いてほしいところです。 次回は種々のアニメーション機能をがっつりと紹介し、期待するように太陽系を回していく予定です。お楽しみに。
(執筆: 上原潤二)
参考リンク
ソフトウェア開発全般、プログラミング言語処理系、クラウドアーキテクチャ、 Reactを中心としたフロントエンド開発に興味をもっている。Groovy/Grailsの普及活動等に従事、 JGGUG(日本Grails/Groovyユーザグループ)運営委員。 「プログラミングGroovy(技術評論社)」のメイン部分を執筆、「Grails徹底入門(翔泳社)」執筆メンバー、その他多くの雑誌記事を執筆。 横浜市在住。7才長女&3才次男の子育て奮闘中。