3Dシーンを作ってすぐ公開　Three.js+Gatsby+TypeScriptによるモダンWebXRテンプレート

***この記事は[WebXR Tech Tokyo #1](https://vrtokyo.connpass.com/event/178319/)の発表で使用いたしました。***
イベントを企画いただいた運営の皆様、会場を盛り上げていただいた参加者の皆様、ありがとうございました！

# 2020/7/12追記
こちらのテンプレートがGatsby公式に追加されました。引き続きご利用いただけると幸いです。
https://www.gatsbyjs.org/starters/shunp/gatsby-three-ts-plus/

# この記事でできるようになること

Three.jsを使った3Dウェブページをモダンな技術スタックを使って簡単に開発・公開できるようになります。オリジナルテンプレートを使うことでスムーズに始められ、またGatsbyを用いることで自分好みのカスタマイズを簡単に加えることが可能です。

以下のサンプルシーンは予めテンプレートに組み込まれております。本記事での解説に加え、必要に応じてテンプレート内の実装を参考にしていただき、ご自身で3Dシーンを作成する際のお役立ていただければ幸いです。

![Coaster.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206255/3348b651-bc7e-574c-cab7-f9f709691d02.gif)
![3dobj２.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206255/0be33d76-df27-6f2c-d2de-f3d4689a842b.gif)
![obj3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206255/c7ff528b-8c89-754c-5e22-1b0fc1bd3b01.gif)


# 使用する技術スタック
:white_check_mark: Three.js
:white_check_mark: TypeScript
:white_check_mark: Gatsby
:white_check_mark: Netlify
:white_check_mark: TailwindCSS

# テンプレート

初期開発の手間を省くため、テンプレートを事前に作成しておきました。以下のコマンドを実行すると必要なモジュールが組み込まれたベースを利用することができます。

```sh
> yarn global add gatsby-cli
> gatsby new 3d-template https://github.com/shunp/gatsby-three-ts-plus
```

※Node.jsはv12系を推奨しております。バージョンによっては`gatsby new`がうまく動作しない可能性があります。

# 動作確認

```sh
> cd 3d-template
> yarn
> yarn dev
```

今回のトップにはジェットコースターの搭乗者視点によるWebXR画面を用意しました。Three.jsのサンプルページにあるものを少しカスタマイズしたものになります。

![Coaster.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206255/3348b651-bc7e-574c-cab7-f9f709691d02.gif)


# ベースシーンの解説

`src/scenes/BaseScene.tsx`にカスタム用のシーンを追加しました。最低限の必要なコンポーネントのみを記載しています。

```tsx:BaseScene.tsx
import React, { useEffect, createRef } from 'react'
import * as THREE from 'three'
import { css } from '@emotion/core'

const newScene = () => {
  const scene = new THREE.Scene()
  return scene
}

const newCamera = () => {
  const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
  camera.position.z = 400
  return camera
}

const newRenderer = (mount: React.RefObject<HTMLInputElement>) => {
  const renderer = new THREE.WebGLRenderer({ antialias: true })
  renderer.setPixelRatio(window.devicePixelRatio)
  renderer.setSize(window.innerWidth, window.innerHeight)
  renderer.autoClear = true
  if (mount.current) {
    mount.current.appendChild(renderer.domElement)
  }
  return renderer
}

const BaseScene = () => {
  const mount = createRef<HTMLInputElement>()
  useEffect(() => {
    // scene
    const scene = newScene()

    // camera
    const camera = newCamera()

    // renderer
    const renderer = newRenderer(mount)

    // mesh
    const geometry = new THREE.BoxGeometry(20, 20, 20)
    const material = new THREE.MeshNormalMaterial()
    const mesh = new THREE.Mesh(geometry, material)
    scene.add(mesh)

    // render
    const render = () => {
      renderer.render(scene, camera)
    }

    // animation
    const animate = () => {
      requestAnimationFrame(animate)
      render()
    }
    animate()
  }, [])
  return (
    <>
      <div css={css``} ref={mount} />
    </>
  )
}
export default BaseScene
```

3DシーンをWebで実装する場合に最も重要なコンポーネントとして、`Camera`, `Scene`, `Renderer`の3つがあります。`Mesh`は表示されるオブジェクトを表しており、基本的に`Geometry`と`Material`の2つから構成されてます。今回はシンプルな立方体オブジェクトのMeshが表示対象となっています。実行してコンテンツを確認してみましょう。`localhost:8000`から確認できます。

<img width="810" alt="Screen Shot 2020-06-25 at 21.54.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206255/176a8375-525b-8e82-b3cd-0aad193b0c55.png">


# カスタムシーンを追加する
上記のベースシーンをカスタマイズしてみます。カメラとオブジェクトを以下のように修正します。

```tsx:CustomScene.tsx
...

const newCamera = () => {
  const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 3000)
  camera.position.z = 4
  return camera
}
...
const BaseScene = () => {
    ...
    // mesh
    const geometry = new THREE.BoxBufferGeometry(0.75, 0.75, 0.75)
    const material = new MeshNormalMaterial()
    const mesh = new THREE.Mesh(geometry, material)
    mesh.position.x = 0
    mesh.position.y = 0
    scene.add(mesh)
    ...
```

中央に大きな立方体が1つ現れます。


<img width="809" alt="Screen Shot 2020-06-26 at 0.11.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206255/e5712089-1176-27e6-0be0-1da98b09ceda.png">

正面からだとよく分からないので、立方体だとわかるように回転を加えてみます。

```tsx:CustomScene.tsx
    ...
 
    // renderer
    const renderer = newRenderer(mount)

    // clock
    const clock = new THREE.Clock()

    // render
    const render = () => {
      const delta = clock.getDelta()

      mesh.rotation.x += delta * 0.5
      mesh.rotation.y += delta * 0.5
      renderer.render(scene, camera)
    }
    ...
```

<img width="809" alt="Screen Shot 2020-06-26 at 0.13.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206255/887ad9a8-8cfb-7d1a-3a65-3079292073b0.png">

ここまでで動きのある3Dオブジェクトを作成できましたが、少し味気ないため次節で表面に動きを付けていきます。

# GLSLの追加

WebGLで何かを描画するためには2つのシェーダが必要になります。シェーダを描くための言語GLSL(グラフィクス・ライブラリ・シェーダー言語)を利用したサンプルもThree.jsのコードにはありますが、あくまで素のHTMLを読み込むことを想定した書き方なのでそのまま利用することができません。Reactを利用する場合は以下のように記述していきます。

頂点シェーダ

```tsx:CustomScene.tsx
const vert = `
varying vec2 vUv;
void main() {
  vUv = uv;
  vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
  gl_Position = projectionMatrix * mvPosition;
}
`
```

フラグメントシェーダ

```tsx:CustomScene.tsx
const frag = `
uniform float time;
varying vec2 vUv;
void main( void ) {
  vec2 position = - 1.0 + 2.0 * vUv;
  float red = abs( sin( position.x * position.y + time / 5.0 ) );
  float green = abs( sin( position.x * position.y + time / 4.0 ) );
  float blue = abs( sin( position.x * position.y + time / 3.0 ) );
  gl_FragColor = vec4( red, green, blue, 1.0 );
}
`
```


最初に用意した`MeshNormalMaterial`を`ShaderMaterial`に置き換えます。

```tsx:CustomScene.tsx
const BaseScene = () => {
    ...
    // mesh
    const geometry = new THREE.BoxBufferGeometry(0.75, 0.75, 0.75)
    // const material = new MeshNormalMaterial()
    const uniforms = {
      time: { value: 1.0 }
    }
    const material = new THREE.ShaderMaterial({
      uniforms,
      vertexShader: vert, // 頂点シェーダ
      fragmentShader: frag // フラグメントシェーダ
    })
    const mesh = new THREE.Mesh(geometry, material)
    mesh.position.x = 0
    mesh.position.y = 0
    scene.add(mesh)
    ...
}
```

最後に、アニメーションとして動きが出るよう`render()`関数に以下を追加します。

```tsx:CustomScene.tsx
    // render
    const render = () => {
      const delta = clock.getDelta()
      uniforms.time.value += delta * 5 // 追加
      mesh.rotation.x += delta * 0.5
      mesh.rotation.y += delta * 0.5
      renderer.render(scene, camera)
    }

```

![3dobj.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206255/ce878727-359b-fb63-11ae-43519af9ed0c.gif)

表面の鮮やかな模様が時間と共に変化している動的な描画になりました。

シェーダとGLSLに関しては以下のサイトがわかりやすかったためリンクを添付します。
[WebGLのシェーダーとGLSL](https://webglfundamentals.org/webgl/lessons/ja/webgl-shaders-and-glsl.html)

ここまでのソースコードは以下のようになります。

```tsx:CustomScene.tsx
import React, { useEffect, createRef } from 'react'
import * as THREE from 'three'
import { css } from '@emotion/core'

const vert = `
varying vec2 vUv;
void main() {
  vUv = uv;
  vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
  gl_Position = projectionMatrix * mvPosition;
}
`

const frag = `
uniform float time;
varying vec2 vUv;
void main( void ) {
  vec2 position = - 1.0 + 2.0 * vUv;
  float red = abs( sin( position.x * position.y + time / 5.0 ) );
  float green = abs( sin( position.x * position.y + time / 4.0 ) );
  float blue = abs( sin( position.x * position.y + time / 3.0 ) );
  gl_FragColor = vec4( red, green, blue, 1.0 );
}
`

const newScene = () => {
  const scene = new THREE.Scene()
  return scene
}

const newCamera = () => {
  const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 3000)
  camera.position.z = 4
  return camera
}

const newRenderer = (mount: React.RefObject<HTMLInputElement>) => {
  const renderer = new THREE.WebGLRenderer({ antialias: true })
  renderer.setPixelRatio(window.devicePixelRatio)
  renderer.setSize(window.innerWidth, window.innerHeight)
  renderer.autoClear = true
  if (mount.current) {
    mount.current.appendChild(renderer.domElement)
  }
  return renderer
}

const BaseScene = () => {
  const mount = createRef<HTMLInputElement>()
  useEffect(() => {
    // scene
    const scene = newScene()

    // camera
    const camera = newCamera()

    // renderer
    const renderer = newRenderer(mount)

    // clock
    const clock = new THREE.Clock()

    // mesh
    const geometry = new THREE.BoxBufferGeometry(0.75, 0.75, 0.75)
    const uniforms = {
      time: { value: 1.0 }
    }
    const material = new THREE.ShaderMaterial({
      uniforms,
      vertexShader: vert,
      fragmentShader: frag
    })
    const mesh = new THREE.Mesh(geometry, material)
    mesh.position.x = 0
    mesh.position.y = 0
    scene.add(mesh)

    // render
    const render = () => {
      const delta = clock.getDelta()
      uniforms.time.value += delta * 5
      mesh.rotation.x += delta * 0.5
      mesh.rotation.y += delta * 0.5
      renderer.render(scene, camera)
    }

    // animation
    const animate = () => {
      requestAnimationFrame(animate)
      render()
    }
    animate()
  }, [])
  return (
    <>
      <div css={css``} ref={mount} />
    </>
  )
}
export default BaseScene
```

# Glitchの追加

Post Processing（後処理）を追加することでシーンにプラスアルファの味を加えてみます。今回は画面が割れるような振動を表現できるGlitchというフィルターを追加してみます。

Post Processingを追加するためには`EffectComposer`をインポートする必要があります。Three.jsではこのようにサンプルとして便利なライブラリを提供してくれていますので、Reactを使用する場合は次のようにインポートします。

```tsx:CustomScene.tsx
import React, { useEffect, createRef } from 'react'
import * as THREE from 'three'
import { css } from '@emotion/core'
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js' // 追加
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js' // 追加
import { GlitchPass } from 'three/examples/jsm/postprocessing/GlitchPass.js' // 追加
```

フィルターをシーンに導入するには、`EffectComposer`に`RenderPass`と`GlitchPass`を以下のように追加したあと、`animation`内で`composer`を呼び出します。

```tsx:CustomScene.tsx
    ...
    // post processing
    const composer = new EffectComposer(renderer)
    composer.addPass(new RenderPass(scene, camera))
    const glitchPass = new GlitchPass()
    composer.addPass(glitchPass)

    ...

    // animation
    const animate = () => {
      requestAnimationFrame(animate)
      render()
      composer.render()
    }
    ...
```

# テキストを重ねる

最後に3Dシーンの上にテキストを重ねてみます。シーン内にテキストを埋め込むこともできますが、今回はテキストのレイヤーを3DCanvasの上に乗せる形にします。


```tsx:CustomScene.tsx
  ...
  return (
    <>
      <div>
        <span>Three.js × Gatsby Template</span>
      </div>
      <div css={css``} ref={mount} />
    </>
  )
  ...
```

<img width="841" alt="Screen Shot 2020-06-28 at 16.03.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206255/ef9a33dc-559b-14a5-d184-d1fbfab5d0cc.png">

右上に小さく文字が表示されているため、CSSを当てて見た目を調整します。スタイルの追加には方法がいくつかありますが、今回のテンプレートにはTailwindCSSを入れているので、これを使ってデザイン調整する方法をみていきましょう。[TailwindCSSとは](https://tailwindcss.com/#what-is-tailwind)

```tsx:CustomScene.tsx
  ...
  return (
    <>
      <div className="absolute z-10 w-full h-full">
        <div className="flex justify-center mt-32">
          <span className="font-serif text-white text-4xl">Three.js × Gatsby Template</span>
        </div>
      </div>
      <div ref={mount} />
    </>
  )
  ...
```

以下のようにグリッチフィルターの上からテキストレイヤーを重ねています。Three.jsではマウスのクリックイベントやスクロールを拾うこともできますが、今回のように上からSceneを覆い隠してしまうとそれらが向こうになる場合がありますので、実現したい表現によっては注意が必要です。

![3dobj２.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206255/0be33d76-df27-6f2c-d2de-f3d4689a842b.gif)

# OBJファイルのロード

新しいシーンで3Dオブジェクトファイルの取り扱いを説明していきます。3Dモデルフォーマットにはいくつか種類がありますが、今回は`.obj`形式のファイルを`OBJLoader`を使用して取り込んで見たいと思います。

```tsx:CustomScene2.tsx
import React, { useEffect, createRef } from 'react'
import * as THREE from 'three'
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'

const MALE_OBJ_PATH = './models/male02.obj'
const FEMALE_OBJ_PATH = './models/female02.obj'
...
```

Filter同様、`OBJLoader`もThree.jsのExamplesに含まれているので、上記のようにインポートしてきます。また、取り込みたいOBJファイルをローカルにコピーしておきましょう。相対パスでファイルを指定します。

<img width="222" alt="Screen Shot 2020-06-28 at 22.54.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206255/14011738-3bd7-82aa-7397-9f03867cad91.png">

OBJファイルは`static`ディレクトリ以下におきます。ここに置くことでビルド時に`public`ディレクトリ以下にコピーが生成されて相対パスでアクセスできるようになります。

```tsx:CustomScene2.tsx
    ...
    // OBJ loader
    const loader = new OBJLoader()
    loader.load(MALE_OBJ_PATH, (object: THREE.Group) => {
      ...
    })
    loader.load(FEMALE_OBJ_PATH, (object: THREE.Group) => {
      ...
    })
```

今回は2種類のオブジェクトがロードされ、動的なポイントクラウドとしてモデルを表現しています。シーンの回転、ポイント郡の上下運動、親3DObjectと子オブジェクトの紐付けなど、他のシーンでも応用可能な実装が含まれておりますので、さらに挑戦したい方は`CustomScene2.tsx`の実装をご参照ください。`localhost:8000/custom2`を開くと以下のシーンが表示されます。

![obj3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206255/c7ff528b-8c89-754c-5e22-1b0fc1bd3b01.gif)

ここまでで3Dシーンに関する説明は以上です。同様に`page`ディレクトリ以下にファイルを作成することで新たなカスタムページを作成することができます。Three.jsはWeb上にたくさんのサンプルが落ちていますので、上記の基本をベースにサンプルを動かしつつ自身のポートフォリオを作成していってください。


# Webページの公開

## GitHubへ変更をプッシュ

最後に、ここまでの変更をWeb上で公開するため、まずは自身のGitHubリポジトリに対してソースコードをプッシュしておきましょう。

## Netlifyの設定

[Netlify](https://www.netlify.com/)を使うことで簡単にポートフォリオサイトを公開できます。[リンク](https://www.netlify.com/)からNetlifyのページに飛び、自身のページにログインをしてください。初めての方はサインアップが必要になります。右上にある「New site from Git」から新しいサイトを作成するページに飛びます。ソースコードがGitHubにある場合はGitHubボタンを選択して次に進みます。

<img width="1226" alt="Screen Shot 2020-06-25 at 17.00.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206255/e8318f55-6936-98d3-40e8-e0898fc0646c.png">

今回保存されたソースコードを含むリポジトリを選択します。ビルド設定はデフォルトで問題ありません。「Deploy site」ボタンを押するとビルドが始まり、しばらくするとサイトが公開されます。

<img width="810" alt="Screen Shot 2020-06-25 at 17.01.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206255/67d6adf9-2936-e436-24e5-7bf5ebca47d2.png">

また、Netlify公開用にランダムで割り当てられた文字列のURLではなく、独自の名前を設定することも可能です。
Setting→Domain Management→Domains→Custom domains→Options→Edit site nameから選択できます。

<img width="1242" alt="Screen Shot 2020-06-25 at 22.07.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206255/51b03c26-064e-f6db-6cbf-cdb3f1cc77b8.png">

設定したURLからページが公開されていることを確認してみましょう。


# あとがき
Three.jsの[公式サンプルページ](https://threejs.org/examples/#webgl_animation_cloth)には、様々な表現方法を用いたカタログが一覧になっています。サンプルコードとセットになっており、新しく3Dシーンを作成する際にはとても参考になるでしょう。

一方、長い歴史を持つThree.jsは、今ではあまり使われない記法で記述されたコードが多いのも現状です。
今回のテンプレートではTypeScriptやES6以降の記法を取り入れつつ、現場で使われることの多いReactをベースに作成しました。[react-three-fiber](https://github.com/react-spring/react-three-fiber)というライブラリも存在しますが、Three.jsのバージョンアップに追随できる、かつ特定のフレームに依存しないなるべく素のThree.jsを扱えるようにカスタマイズ性の高いテンプレートとして公開しています。オープンソースですのでお気軽にPR、スター等いただけると今後の活動の励みになります。

→[GitHubソースコード](https://github.com/shunp/gatsby-three-ts-plus)

また、3Dのポートフォリオジェネレータとは別に、ソーシャルメディアのポートフォリオジェネレータを別途開発中です。今後はSNSだけではなく3Dアバターなどもクリップできるようにしていく予定です。現在α版としてテストユーザを募集しておりますので、ご興味がありましたら[こちら](https://twitter.com/shunpei42ba_)までご連絡をお願いいたします。

<img width="1239" alt="Screen Shot 2020-06-28 at 22.40.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206255/698496b0-5dda-2aa9-eb4f-d7bb5e425a23.png">

お気に入りのポストを好きなSNSから、好きな数だけNoCodeでクリップできるポートフォリオサイトです。「複数のSNSを1つのURLで管理したい」「埋もれてしまった過去のポストをクリップしておきたい」場合に便利です。SNS版前略プロフィール？のようなイメージでお楽しみください。
[https://storygate.info](https://storygate.info/443502378)


最後までご一読いただきありがとうございました。