めちゃくちゃカッコいいの、作りたくない?
WebGL、興味はあるけどなかなか手を出せてないってひとは多いんじゃないでしょうか。
自分もその中の一人でした。
今回は、その前準備としてreactを用いてthree.jsに習熟することを目的とします。
具体的には以下のようなシミュレーションを作成します。
本記事は、自分用のサイトを作っている時に、キービジュアルとしてThree.jsというライブラリをReactで使用したので、そのまとめ的な記事です。
最終的には、超絶劇的にカッコいいサイトを作ることを目指します。
2020/03/30 追記
react-three-fiberのバージョンが4系に上がったため、本記事のcannonを使用する部分が動作しなくなっています。もし本記事のサンプルをそのまま使用したい場合は、react-three-fiberの3.x系を使用するようお願いします。
2021/06/26 追記
react-three-fiberのメジャーバージョンがv6まで上がっており、本記事のコードは動作しない可能性が高いです。
できるだけ新しい情報を参照している記事もご覧になることをおすすめします。
参考: Next.js / TypeScriptでThree.js(react-three-fiber)を使うまで
対象読者
本記事の対象読者として想定するのは以下のような人です。
- WebGLに興味がある人
- Three.jsを触ってみたい人
- WebGLやThree.jsは触っているが、React.jsは触ったことがない人
- React.jsで製作しているサイトをもっとかっこよくしたい人
React.jsはJavaScriptのライブラリです。よくVue.jsと宗教戦争したりしています。
SPAを簡単に製作できて、複雑なサイトを比較的整然とゴリゴリコーディングしていけるのが特徴です。
Reactを触ったことがない人向けにある程度の解説も加えていきますが、完全に解説しきることは難しいと思うので、適宜ほかの資料を参照するようにしてください。
以下のようなチュートリアル記事も多く存在しています。
初めてのReact「入門編」導入から基本まで〜TODOアプリを作ってを学ぼう!
また、現在のReactではHooksというものが使用可能です。
これを用いるとclassを使用しなくても良い場面が多くなり、可読性や保守性が向上します。
出来るだけHooksを用いたサイト製作をお勧めします。
また、Reduxを使っている方は、最近Redux Hooksが導入されたので、これを機に移行してみるといいと思います。
目標
本記事のゴールはThree.jsをReactで動かし、WebGLで創作ができるようになることです。
また、最新技術のキャッチアップも積極的にしていきます。
そのため、資料の少なめな技術(React Hooksなど)を使用することもあります。
公式ドキュメントなどが英語で用意してあることが多いので、英語を頑張って読むか、読めない方は全文をGoogle翻訳に突っ込むなどして概要だけでも掴むようにするとよいと思います。
出来るだけ解説はしていきますが、もし不明な点などあればコメント欄にて教えていただけると幸いです。
動作環境
使用する環境、ライブラリは以下です。
もしコードがうまく動かなければ、ライブラリのバージョン確認などをまずお願いします。
ちょっと多く見えますがそんなに使わないのでご安心ください。
- macOS Mojave: 10.14.6
- node: 12.10.0
- npm: 6.10.3
- yarn: 1.17.3
- create-react-app: 3.0.1
- react: 16.9.0
- react-dom: 16.9.0
- @emotion/core: 10.0.19
- cannon: 0.6.2
- three: 0.108.0
- emotion-reset: 2.0.3
- react-scripts: 3.1.1
- react-three-fiber: 3.0.0-beta.13
参考
まだまだ日本語の参考資料が少ないので、増えていってくれると嬉しいですね。
GitHub: react-three-fiber
react-three-fiber公式ドキュメント
three.js公式ドキュメント
thee.jsのチュートリアル記事(ics.media)
CANNON.jsの参考記事
react-three-fiberを使ってSVGをバラバラに動かしてみる
はじめに(みんな大嫌い環境構築編)
今回はcreate-react-appを利用して、Reactアプリケーションの雛形を作っていきます。
create-react-appはFacebookが出している、Reactアプリケーションの雛形をいい感じに整えてくれるツールです。
もしまだnode
、yarn
、create-react-app
のいずれかをインストールしていなければ、以下の記事などが大変参考になります。
MacにNode.jsをインストール
create-react-appコマンドを利用して、reactをインストールする方法
無事インストールが成功したら、以下のコマンドを好きなディレクトリで実行してください。
これでプロジェクトを作成します。
create-react-app react-three-trial
cd react-three-trial
ついでにこのプロジェクトをgitで管理しましょう。
これはあとでWebアプリとして作品を公開したくなった時にとても役立ちます。
以下のようにREADME.mdを編集して、
# react-three-trial
Reactでthree.jsを触りたい!
適当にgitにあげます。
事前にGitHubでリポジトリを作成してから以下のコマンドを実行してください。
自分のリポジトリはこんな感じになっています。
以下の[username]は自分のGitHubのアカウント名で、[repositry name]は作成したリポジトリの名前で置き換えてコマンドを実行してください。
git add -A
git commit -m "👌initial commit"
git remote add origin git@github.com:[user name]/[repositry name].git
git push -u origin master
次に必要なライブラリをインストールします。
ところで、Three.jsをReactで動かすためのライブラリには以下のようなものがあります。
このうち最も新しいのは一番下のreact-three-fiberで、他の二つのライブラリからも、react-three-fiberを使用するように、という注意書きがされています。
(ちなみにfiberというのは最近Reactに採用されたレンダリングアルゴリズムのことです。)
よって今回は、上記に加えて、最後発かつ活発に開発がされているreact-three-fiberを選択します。
注意点として、react-three-fiberは現在二つのバージョンが混じっている状態です。
現在活発に開発されているversion 3.x 系、そして以前から使われているversion 2.x 系です。
これに関しては、npmのサイトを見ていただくとわかりやすいかと思います(とってもホットですね!)
このような場合、安定性を好んで2.x系を選ぶ方も多いと思うんですが、今回は様々な機能が非常に便利になっている3.x系を用います。
もしバグを見つけたら「プルリクを送るぞ!」くらいの気持ちでやっていきましょう(今のところ深刻なバグは見当たりませんが)。
ライブラリ側に変更があった場合は、本記事のコードも修正するのでご安心ください。
では、開発者の方に感謝の気持ちを込めて早速モジュールをyarn add
していきましょう。
以下のコマンドを入力してください。
今回用いるreact-three-fiberの他に、three.js本体や諸々のライブラリを入れていきます。
yarn add three react-three-fiber@3.0.0-beta.13
次にsrc
ディレクトリの不要なファイルを消して、自分好みにしていきます。
まずsrcディレクトリの以下に示すのファイルだけ残して、他のファイルを消してください。
以下は現時点のディレクトリ構成を表しています。
.
├── App.js
├── index.js
└── serviceWorker.js
次にApp.js
をいじります。テストとして以下のようにファイルを書き換えてください。
import React from 'react';
function App() {
return <h1>test</h1>;
}
export default App;
また、src/index.js
のうち、index.css
をimportしている行を削除します。
具体的には以下のような感じになります。
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(<App />, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
以上のように変更したら、ターミナルをプロジェクトルート(アプリケーションの一番上のディレクトリ)で開き、以下のコマンドを実行してください。
yarn start
成功すると、画面が勝手に遷移するはずです。
もし自動で遷移しなければ、Google Chromeなどのブラウザを開いて検索欄にhttp://localhost:3000と打ち込んで見てください。
遷移後に、以下のような画面が出てきたら成功です!!
git add -A
git commit -m "👊いい感じに動いた!"
git push origin master
これでとりあえずReactアプリケーションを作っていく準備は整いました。
次はThree.jsを動かしていきます。
はじめてのThree.js
最初はreact-three-fiberの参考実装を動かすことを目標にやっていきます。
こちらの公式ページのサンプルを使用します。
ただ公式のサンプルコードはreactのimportなどを省いているので、ちょっとわかりにくさがあります。多分罠です。
以下のコードなら動くと思うので試してみてください。
せっかくなのでES6の記法で書いてみます。
import React, { useRef } from 'react';
import { Canvas, useFrame } from 'react-three-fiber';
const Thing = () => {
const ref = useRef();
useFrame(() => (ref.current.rotation.z += 0.01));
return (
<mesh
ref={ref}
onClick={e => console.log('click')}
onPointerOver={e => console.log('hover')}
onPointerOut={e => console.log('unhover')}
>
<planeBufferGeometry attach='geometry' args={[1, 1]} />
<meshBasicMaterial
attach='material'
color='hotpink'
opacity={0.5}
transparent
/>
</mesh>
);
};
const App = () => {
return (
<Canvas>
<Thing />
</Canvas>
);
};
export default App;
上記のように編集したのち、ファイルを保存してください。
ターミナルが先ほどyarn start
した時のままになっていれば、自動で編集内容が保存されて、以下のような画面がブラウザで表示されると思います。
このように、ピンクの正方形がくるくるしていれば成功です!!
このままだとあんまり何かをやった感じがないので、App.js
に記述した内容を少し解説していきます。
まず最初の2行です。
import React, { useRef } from 'react';
import { Canvas, useFrame } from 'react-three-fiber';
この行では、React自体のimportと、React Hooks(React v16.8からの機能です)のHookであるuseRef(公式ドキュメント)をimportしています。
以下はReactにある程度経験がある方向けの解説になります。(自分も理解が浅いので詳しい方がいれば補足をお願いできると大変嬉しいです)
useRefは関数コンポーネントに対してクラスを使わずに値を持たせるために使用されます。
useRefがクラスのstateと違うのは、その保持している値を更新した際に値の変更を通知しないという点です。
これにより、値が書き換わったとしてもDOMの再レンダリングが行われません。
今回はこの機能を、WebGLによって何かを描写する際のパラメータとして用います。これにより、DOMのレンダリングという重ための処理無しで値を変更できます。
また、2行目のCanvas、useFrameはreact-three-fiberで用意されているコンポーネントとカスタムフックです。
CanvasコンポーネントはThree.jsを使うための必要なものなので、とりあえずimportしておきましょう。
様々なプロパティをCanvasコンポーネントに渡すことが出来るので、気になる方はぜひ公式のREADMEをご覧ください。
また、useFrameは1フレームごとに引数にとった関数をトリガします(1フレームはざっと測ったところ60fps(1秒間に60回処理)がデフォルトのようでした)
要はフレームごとに何か処理をすることが出来ます。便利です。
詳しくは後ほど解説することとして次に進みます。
ざっくり抜き出してThing
コンポーネントの概形だけ見てみます。
const Thing = () => {
const ref = useRef();
useFrame(() => (ref.current.rotation.z += 0.01));
return (
...
);
}
ここではThing関数コンポーネントを宣言しています。
公式ドキュメントいわく、useRef()はmutable(変更可能)なインスタンス変数を作成する用途で使えるようです。
今回はuseFrame()に対してref.current.rotation.zを更新する関数を渡しています。
あくまでuseFrame()は関数を受け取ります。
よって、引数は関数である必要があり、実際に上記ではアロー関数を用いて、0引数の関数() => (ref.current.rotation.z += 0.01)
を渡しています。
この関数が1秒間に60回よばれる感じです。
では、上で更新しているrotation.zがどのように使われるかをみていきます。
上記に示したコードのreturn
の中に注目します。
<mesh
ref={ref}
onClick={e => console.log('click')}
onPointerOver={e => console.log('hover')}
onPointerOut={e => console.log('unhover')}
>
<planeBufferGeometry attach='geometry' args={[1, 1]} />
<meshBasicMaterial
attach='material'
color='hotpink'
opacity={0.5}
transparent
/>
</mesh>
上記はthree.jsの記法を元にコンポーネント指向でそれを書き直したような構造になっています。
先ほど定義したrefをここで渡しています。
これは、先ほども述べたように、コンポーネントの再レンダリングを防ぎつつ、値を更新してあげるための処理です。
ここの詳しい説明は今回は省略しますが、とりあえず今は
- 物体にカーソルが乗ったり外れたり、クリックされたりした時の関数を設定する
- 物体を配置する
の二つを行なっていると考えていただければ十分です。
見栄えを手の内に
それでは次に、今回作成したcanvasをコンポーネントとして使うには欠かせない、スタイル指定をやっていきます。
React用のcssモジュールには多くの種類がありますが、勢いのあるもののうち、最後発で他のモジュールの機能全部載せのemotionを使用します。
styled-components(別の CSS in JS モジュール)もいいですが、とことん新しいものを使っていきます(その方が興奮するので)。
以下でインストールします。
yarn add @emotion/core
使い方は簡単で、プリミティブなHTML要素に以下(公式のサンプルをちょっと改変)のようにcssプロパティを設定し、それにスタイルを渡してあげるだけです。
// this comment tells babel to convert jsx to calls to a function called jsx instead of React.createElement
/** @jsx jsx */
import { css, jsx } from '@emotion/core'
const color = 'white'
const theme = css`
padding: 32px;
background-color: hotpink;
font-size: 24px;
border-radius: 4px;
&:hover {
color: ${color};
}
`
render(
<div css={theme}>
Hover to change color.
</div>
)
また、このような書き方も出来ます。
これは、最初からテーマが設定された要素を生成している感じです。
import styled from '@emotion/styled'
const Button = styled.button`
padding: 32px;
background-color: hotpink;
font-size: 24px;
border-radius: 4px;
color: black;
font-weight: bold;
&:hover {
color: white;
}
`
render(<Button>This my button component.</Button>)
どちらもいい感じに使えますが、自分はスタイルの指定とコンポーネントの構造自体を分けたい派なので、前者の記法でいきます。
では早速先ほどのサンプルを、ウインドウの全画面で表示してみたいと思います。
できればGoogle Chromeの検証機能でHTML要素にclassやstyleがどうあたるかを見ながらやってみてください。
以下のようにApp.jsを編集します。
/** @jsx jsx */
import { useRef } from 'react';
import { Canvas, useFrame } from 'react-three-fiber';
import { css, jsx } from '@emotion/core';
const theme = css`
width: 100vw;
height: 100vh;
`;
const Thing = () => {
const ref = useRef();
useFrame(() => {
ref.current.rotation.z += 0.01;
});
return (
<mesh
ref={ref}
onClick={e => console.log('click')}
onPointerOver={e => console.log('hover')}
onPointerOut={e => console.log('unhover')}
>
<planeBufferGeometry attach='geometry' args={[1, 1]} />
<meshBasicMaterial
attach='material'
color='hotpink'
opacity={0.5}
transparent
/>
</mesh>
);
};
const App = () => {
return (
<div css={theme}>
<Canvas>
<Thing />
</Canvas>
</div>
);
};
export default App;
追加したのは、pragmaと呼ばれるファイル最上部の記述(おまじないだと思ってください)、それとemotionのimport、style指定用の変数themeと、Canvasをラップするためのdiv要素、およびそこに指定されたcssプロパティです。
react-three-fiberのCanvas
には直接cssプロパティを指定できないので、ラッパーを作ってスタイルを決めています。
このように編集したのち、保存してブラウザを開くとこのようになっているはずです。
うまく全画面になっていそうですね。
では、少し発展的に、ここに背景色を指定してみます。
先ほどの変数theme
を以下のように編集します。
const theme = css`
width: 100vw;
height: 100vh;
background-color: #000;
`;
これで背景色がいい感じに黒くなっているはずです。
確認してみましょう。
あれ、なんか出来てる感じがしますが、なにやら空白が左と下にある気がします(ない人もいるかもしれません)。
これはブラウザの初期スタイル的なものが原因です。
Chromeとかが勝手にスタイルを当ててくれているのですが、今回は邪魔ですね(ってかだいたい邪魔な気がします)。
あとブラウザごとにスタイルが違うってなると地獄です。やってられません。
というわけでこれを消します。
ブラウザのデフォルトCSSを消すための、reset.cssというものがあるのでこれを利用しましょう。
以下のコマンドでインストールします。
yarn add emotion-reset
今回インストールしたのはemotion用のreset.cssモジュールです。
styled-componentsにも似たモジュールでstyled-resetというものがありますね。
使い方は以下のような感じです。
どうせなので、Globalに適応したいスタイルも指定してみます。
/** @jsx jsx */
import { useRef } from 'react';
import { Canvas, useFrame } from 'react-three-fiber';
import { css, jsx, Global } from '@emotion/core';
import emotionReset from 'emotion-reset';
const globalStyles = css`
${emotionReset}
*, *::after, *::before {
box-sizing: border-box;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-smoothing: antialiased;
}
`;
const theme = css`
width: 100vw;
height: 100vh;
background-color: #000;
`;
const Thing = () => {
const ref = useRef();
useFrame(() => {
ref.current.rotation.z += 0.01;
});
return (
<mesh
ref={ref}
onClick={e => console.log('click')}
onPointerOver={e => console.log('hover')}
onPointerOut={e => console.log('unhover')}
>
<planeBufferGeometry attach='geometry' args={[1, 1]} />
<meshBasicMaterial
attach='material'
color='hotpink'
opacity={0.5}
transparent
/>
</mesh>
);
};
const App = () => {
return (
<div>
<Global styles={globalStyles} />
<div css={theme}>
<Canvas>
<Thing />
</Canvas>
</div>
</div>
);
};
export default App;
ちょっと冗長になってしまいましたが、以下のように余白のない画面が表示されれば成功です。
これで作品をいい感じにきっちり見せられるようになりました!!
整理整頓編
先ほどからのままだと、ディレクトリ構成がしっちゃかめっちゃかです。
せめてGlobalな何かと作品は分離しましょう。
srcディレクトリの配下に新しくwork.js
ファイルを作成して、src/App.js
に記述していた内容を一部src/work.js
に移譲します。
以下のようにファイルを編集してください。
/** @jsx jsx */
import { css, jsx, Global } from '@emotion/core';
import emotionReset from 'emotion-reset';
import { Work } from './work';
const globalStyles = css`
${emotionReset}
*, *::after, *::before {
box-sizing: border-box;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-smoothing: antialiased;
}
`;
const App = () => (
<div>
<Global styles={globalStyles} />
<Work />
</div>
);
export default App;
不要なモジュールのimportを削除して、Globalな指定をするのみにとどめています。
これでsrc/work.js
は以下のように作品のみに集中できます。
/** @jsx jsx */
import { useRef } from 'react';
import { Canvas, useFrame } from 'react-three-fiber';
import { css, jsx } from '@emotion/core';
const theme = css`
width: 100vw;
height: 100vh;
background-color: #000;
`;
const Thing = () => {
const ref = useRef();
useFrame(() => {
ref.current.rotation.z += 0.01;
});
return (
<mesh
ref={ref}
onClick={e => console.log('click')}
onPointerOver={e => console.log('hover')}
onPointerOut={e => console.log('unhover')}
>
<planeBufferGeometry attach='geometry' args={[1, 1]} />
<meshBasicMaterial
attach='material'
color='hotpink'
opacity={0.5}
transparent
/>
</mesh>
);
};
export const Work = () => (
<div css={theme}>
<Canvas>
<Thing />
</Canvas>
</div>
);
本来ならばもっとディレクトリを分割した方がよいのですが、今回は「やってみる」的な内容なのでしません。
ディレクトリ構成は今の所以下のような感じになっているはずです。
.
├── README.md
├── package.json
├── public
├── node_modules
├── src
│ ├── App.js
│ ├── index.js
│ ├── serviceWorker.js
│ └── work.js
└── yarn.lock
src/work.js
が増えただけですね。
以上のように変更して、もしなにも問題がなければ移行作業は成功です!
いろいろ作ってみる
では次に、src/work.js
の中身をいじって色々実験的に作っていきましょう。
とりあえず以下のようにまっさらな状態にします。
/** @jsx jsx */
import { useRef } from 'react';
import { Canvas, useFrame } from 'react-three-fiber';
import { css, jsx } from '@emotion/core';
const theme = css`
width: 100vw;
height: 100vh;
`;
const Thing = () => {
return <mesh></mesh>;
};
export const Work = () => (
<div css={theme}>
<Canvas>
<Thing />
</Canvas>
</div>
);
全画面表示はしたいので、cssの記述は残しておきましょう。
Three.jsを学ぶ
では次に、基本的なオブジェクトを作りながらReactでthree.jsを動かす方法を学んでいきます。
練習はこちらのサイトを参考にやっていくので、もしなにをやっているかわからなくなったら参考にしてみてください。
react-three-fiberのコードではありませんが、参考にはなるはずです。
ではthree.js入門の最初のコードをreact-three-fiber用に書き直してみます。
work.jsを以下の用に編集してみてください。
/** @jsx jsx */
import { useRef } from 'react';
import { Canvas, useFrame } from 'react-three-fiber';
import { css, jsx } from '@emotion/core';
const theme = css`
width: 100vw;
height: 100vh;
`;
const Thing = () => {
const ref = useRef();
useFrame(() => {
ref.current.rotation.y += 0.01;
});
return (
<mesh ref={ref}>
<boxGeometry attach='geometry' args={[400, 400, 400]} />
<meshNormalMaterial attach='material' />
</mesh>
);
};
export const Work = () => (
<div css={theme}>
<Canvas camera={{ position: [0, 0, 1000] }}>
<Thing />
</Canvas>
</div>
);
保存してブラウザで見ると、立方体がくるくう回転している感じになっていると思います。
上のように、react-three-fiberでは、three.jsのクラス名・関数名をそのまま使った(lower camel caseになっていますが)コンポーネントを組み合わせて表現を作ります。
使用できる名前はthree.jsの公式ドキュメントを参照するとよいでしょう。
元のコードを参考にさせていただくと以下のようになっていました。
//一部抜粋
function init() {
const width = 960;
const height = 540;
const renderer = new THREE.WebGLRenderer({
canvas: document.querySelector('#myCanvas')
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(width, height);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, width / height);
camera.position.set(0, 0, +1000);
const geometry = new THREE.BoxGeometry(400, 400, 400);
const material = new THREE.MeshNormalMaterial();
const box = new THREE.Mesh(geometry, material);
scene.add(box);
tick();
function tick() {
box.rotation.y += 0.01;
renderer.render(scene, camera);
requestAnimationFrame(tick);
}
}
上記は非常に手続き型的な書き方ですが、react-three-fiberの方はReactのコンポーネント指向をそのまま反映した作りになっていることがわかります。
今回はperspectiveCameraを指定していないことに注意してください。
それにしても、useFrame()
がとっても便利ですね。
明示的にコンポーネントと分離しつつ、各フレームで行われる処理を記述できます。
では次の例にいきましょう。
/** @jsx jsx */
import { useRef } from 'react';
import { Canvas, useFrame } from 'react-three-fiber';
import { css, jsx } from '@emotion/core';
const theme = css`
width: 100vw;
height: 100vh;
`;
const Thing = () => {
const ref = useRef();
useFrame(({ clock }) => {
ref.current.position.x += Math.cos(clock.getElapsedTime()) * 3;
ref.current.position.y += Math.sin(clock.getElapsedTime()) * 3;
ref.current.position.z += Math.cos(clock.getElapsedTime()) * 3;
ref.current.rotation.y += 0.01;
});
return (
<mesh ref={ref}>
<sphereGeometry attach='geometry' args={[300, 30, 30]} />
<meshStandardMaterial attach='material' color='#FF0000' />
</mesh>
);
};
export const Work = () => (
<div css={theme}>
<Canvas camera={{ position: [0, 0, 1000] }}>
<pointLight
color='#FFFFFF'
intensity={1}
position={[0, 2000, 1000]}
/>
<Thing />
</Canvas>
</div>
);
上記のコードを実際に動かすとこんな感じになります。
useFrame
に対してclockなどを渡してあげると、このように時間に応じた動きもさせることができます。
実際の作品を参考に作ってみる
色々とできるようになったので、ついに作品を作っていきます。
やってみるにしてもカッコいいやつじゃないとやる気が出ないので、react-three-fiber
の公式ページに置かれているデモのコードを参考に、物理演算の導入までいきます。
実際の作品は以下のような感じです。
カックイイ!!!!!!!
codesandbox.idにて実際のコードがみれます。
事前に確認したい方は見てみてください。
また、他のサンプルはreact-three-fiberのリポジトリから見ることが出来ます。
では、これを自分の環境で作っていきます。
わかりやすさや面白さにために、参考実装とは多少変えている点もあるので注意してください。
それでは早速やっていきましょう。
CANNONを使う
CANNON.jsはthree.jsと合わせて使うことを念頭に設計されたJavaScriptの物理演算エンジンライブラリです(カノンと読みます)。
Three.jsで作成したオブジェクトに、簡単に「衝突」などの物理的な概念を実装することができます。
上のデモのように、物体が落ちる様子を描画するには最高の手段になるでしょう。
例のごとくまずはインストールからやっていきます🌝
yarn add cannon
これだけでインストールは完了ですが、react-three-fiber
でCANNONを使うためのカスタムフックが先ほどのデモの中で公開されています(useCannon.jsというファイルです)。
カスタムフックを一から作っていくのは大変なので、CANNONを使うための前準備はこれを使用させていただきましょう。
src
ディレクトリ配下に新しくuseCannon.js
というファイルを作成して、以下の内容をコピペしてください。
import * as CANNON from 'cannon'
import React, { useState, useEffect, useContext, useRef } from 'react'
import { useRender } from 'react-three-fiber'
// Cannon-world context provider
const context = React.createContext()
export function Provider({ children }) {
// Set up physics
const [world] = useState(() => new CANNON.World())
useEffect(() => {
world.broadphase = new CANNON.NaiveBroadphase()
world.solver.iterations = 10
world.gravity.set(0, 0, -25)
}, [world])
// Run world stepper every frame
useRender(() => world.step(1 / 60))
// Distribute world via context
return <context.Provider value={world} children={children} />
}
// Custom hook to maintain a world physics body
export function useCannon({ ...props }, fn, deps = []) {
const ref = useRef()
// Get cannon world object
const world = useContext(context)
// Instanciate a physics body
const [body] = useState(() => new CANNON.Body(props))
useEffect(() => {
// Call function so the user can add shapes
fn(body)
// Add body to world on mount
world.addBody(body)
// Remove body on unmount
return () => world.removeBody(body)
}, deps)
useRender(() => {
if (ref.current) {
// Transport cannon physics into the referenced threejs object
ref.current.position.copy(body.position)
ref.current.quaternion.copy(body.quaternion)
}
})
return ref
}
デモから頂いたものですが、説明はほぼコメントで完結すると思います。
あとは実際のコードを書きながら、useCannon
の説明を加えていく形をとります。
ではまず、work.js
をまっさらにして、以下のように編集してください。
/** @jsx jsx */
import * as THREE from 'three';
import * as CANNON from 'cannon';
import React, { useEffect, useState } from 'react';
import { Canvas, useFrame } from 'react-three-fiber';
import { css, jsx } from '@emotion/core';
import { useCannon, Provider } from './useCannon';
const theme = css`
width: 100vw;
height: 100vh;
background-color: #272727;
`;
export const Work = () => {
return (
<div css={theme}></div>
);
};
とりあえず必要なものだけ入れておきました。
また、それとカッコいいので背景を暗くしてみました。
ここからさらに以下のように編集します。
具体的には、Workのreturnされているdivの中にCanvasとカメラ・ライトを加えていきます。
また、それだけでは面白くないので適当なboxを作成して追加しています。
/** @jsx jsx */
import * as THREE from 'three';
import * as CANNON from 'cannon';
import React, { useEffect, useState } from 'react';
import { Canvas, useFrame } from 'react-three-fiber';
import { css, jsx } from '@emotion/core';
import { useCannon, Provider } from './useCannon';
const theme = css`
width: 100vw;
height: 100vh;
background-color: #272727;
`;
export const Work = () => {
return (
<div css={theme}>
<Canvas camera={{ position: [0, 5, 15] }}>
<ambientLight intensity={0.5} />
<spotLight
intensity={0.6}
position={[30, 30, 50]}
angle={0.2}
penumbra={1}
castShadow
/>
<mesh castShadow receiveShadow>
<boxGeometry attach='geometry' args={[2, 2, 2]} />
<meshStandardMaterial attach='material' />
</mesh>
</Canvas>
</div>
);
};
どうでしょうか、画像のような景色になっているでしょうか。
こんなかんじになっていればひとまずOKです。
では次にBox用のコンポーネントを作ってみます。
今回はCANNONを用いて物理演算を導入するのですが、先ほど紹介したuseCannon
カスタムフックのおかげでめちゃくちゃ楽です。
みた方が早いと思うので、以下のように編集してBoxコンポーネントを作成してください。
/** @jsx jsx */
import * as THREE from 'three';
import * as CANNON from 'cannon';
import React, { useEffect, useState } from 'react';
import { Canvas, useFrame } from 'react-three-fiber';
import { css, jsx } from '@emotion/core';
import { useCannon, Provider } from './useCannon';
const theme = css`
width: 100vw;
height: 100vh;
background-color: #272727;
`;
const Box = ({ position, args }) => {
const ref = useCannon({ mass: 100000 }, body => {
body.addShape(new CANNON.Box(new CANNON.Vec3(1, 1, 1)));
body.position.set(...position);
});
return (
<mesh ref={ref} castShadow receiveShadow>
<boxGeometry attach='geometry' args={args} />
<meshStandardMaterial attach='material' />
</mesh>
);
};
export const Work = () => {
return (
<div css={theme}>
<Canvas camera={{ position: [0, 5, 15] }}>
<ambientLight intensity={0.5} />
<spotLight
intensity={0.6}
position={[30, 30, 50]}
angle={0.2}
penumbra={1}
castShadow
/>
<Provider>
<Box position={[1, 0, 1]} args={[2, 2, 2]} />
</Provider>
</Canvas>
</div>
);
};
動かすと、箱が無限に落ちていく様が見れると思います。
上記では、Boxというコンポーネントを物理世界に配置する箱一つとして完結させています。
なぜ画面奥に落下しているか、ですが、これは先ほど作成したuseCannon.jsの中のProvider
コンポーネントにて重力が設定されているからです。
Boxは初期位置と箱の大きさ(argsで表される)をpropsにとります。
argsのそれぞれの値が長方体(box)の辺の長さを決定するので、この値を変化させることで棒状のものも作成することができます。
上記をみるとBox内でbody.addShape()しています。
本来のCANNONなら物理世界にaddShape()したら、要素を削除する時にremoveしなければいけません。
しかし今回は、useCannon()
内でworld.removeBody()
をしているため、addするだけでよくなっています。
試しに箱を増やして遊んでみましょう。
の中に以下のような記述を増やしてみてください(一部抜粋)。
<Provider>
<Box position={[1, 0, 1]} args={[2, 2, 2]} />
<Box position={[1, 0, 1]} args={[1, 1, 5]} />
<Box position={[2, 1, 5]} args={[3, 3, 3]} />
<Box position={[0, 0, 6]} args={[2, 4, 2]} />
<Box position={[-1, 1, 8]} args={[2, 3, 2]} />
<Box position={[0, 2, 3]} args={[5, 5, 1]} />
<Box position={[2, -1, 13]} args={[1, 1, 10]} />
</Provider>
すると自分の環境では以下のようになります。
ちょっとgif画像の関係で落ちるスピードが早いですが、だいたいこんな感じです!
先述のuseCannonファイルの中の、Provider()
の中に以下のような記述があります。
useEffect(() => {
world.broadphase = new CANNON.NaiveBroadphase();
world.solver.iterations = 10;
world.gravity.set(0, 0, -25);
}, [world]);
この中のworld.gravity.set(0, 0, -25);
の値を変更することで、重力方向と重力加速度の大きさを制御することが可能です。
第1引数がx軸(画面右が正、左が負)、第2引数がy軸(画面上が正、下が負)、第3引数がz軸(画面手前が正、奥が負)という感じです。
world.gravity.set(0, 0, -1);
としてみると、だいぶふわっと落ちるようになったりします。
このようにworldインスタンスに値を設定してあげることで、物理世界の挙動を操作することができます。
それでは次に進みます。
今度は無限に落ちるのを防ぐために、平面を作成して配置してみます。
以下のように、Planeコンポーネントを作成して、Provideの中に追加してください。
/** @jsx jsx */
import * as THREE from 'three';
import * as CANNON from 'cannon';
import React, { useEffect, useState, useContext } from 'react';
import { Canvas, useFrame } from 'react-three-fiber';
import { css, jsx } from '@emotion/core';
import { useCannon, Provider } from './useCannon';
const theme = css`
width: 100vw;
height: 100vh;
background-color: #272727;
`;
const Plane = ({ position }) => {
const ref = useCannon({ mass: 0 }, body => {
body.addShape(new CANNON.Plane());
body.position.set(...position);
});
return (
<mesh ref={ref} receiveShadow>
<planeBufferGeometry attach='geometry' args={[1000, 1000]} />
<meshPhongMaterial attach='material' color='#272727' />
</mesh>
);
};
const Box = ({ position, args }) => {
const ref = useCannon({ mass: 100000 }, body => {
body.addShape(new CANNON.Box(new CANNON.Vec3(1, 1, 1)));
body.position.set(...position);
});
return (
<mesh ref={ref} castShadow receiveShadow>
<boxGeometry attach='geometry' args={args} />
<meshStandardMaterial attach='material' />
</mesh>
);
};
export const Work = () => {
return (
<div css={theme}>
<Canvas camera={{ position: [0, 5, 15] }}>
<ambientLight intensity={0.5} />
<spotLight
intensity={0.6}
position={[30, 30, 50]}
angle={0.2}
penumbra={1}
castShadow
/>
<Provider>
<Plane position={[0, 0, -10]} />
<Box position={[1, 0, 1]} args={[2, 2, 2]} />
<Box position={[1, 0, 1]} args={[1, 1, 5]} />
<Box position={[2, 1, 5]} args={[3, 3, 3]} />
<Box position={[0, 0, 6]} args={[2, 4, 2]} />
<Box position={[-1, 1, 8]} args={[2, 3, 2]} />
<Box position={[0, 2, 3]} args={[5, 5, 1]} />
<Box position={[2, -1, 13]} args={[1, 1, 10]} />
</Provider>
</Canvas>
</div>
);
};
上記の通りにやるとこんな感じになるはずです。
以上のように地面に衝突している感じができればほぼ完成です!
上記では、質量0の平面物体のコンポーネントを作成しています(massが質量の意味です)。
CANNONでは質量を設定していないオブジェクトは動きません。
上記で参考実装の再現は終了です!!
まとめ
かなり駆け足で進んでしまいましたが、ReactでThree.jsを動かす楽しさが少しでも伝わったでしょうか。
個人的には、このような3Dの現象の記述や表現には、Reactの宣言的(declarative)な記述がとても相性がいいように思っています。
コンポーネント指向を生かして、もっと面白いものが作れるといいなと思います。
次回はより実践的に、様々な作品を作って公開するところまでやっていきます!!
10/01(火)追記
まずは前準備ということでreact-three-fiberの参考実装をじっくり読む記事を書きました。
ReactでThree.jsとshaderを触りたいのでサンプルを読みほぐしながらreact-three-fiberを理解する回
ぜひ読んでね!