Help us understand the problem. What is going on with this article?

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

この記事はWebXR Tech Tokyo #1の発表で使用いたしました。
イベントを企画いただいた運営の皆様、会場を盛り上げていただいた参加者の皆様、ありがとうございました!

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

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

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

Coaster.gif
3dobj2.gif
obj3.gif

使用する技術スタック

:white_check_mark: Three.js
:white_check_mark: TypeScript
:white_check_mark: Gatsby
:white_check_mark: Netlify
:white_check_mark: TailwindCSS

テンプレート

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

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

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

動作確認

> cd 3d-template
> yarn
> yarn dev

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

Coaster.gif

ベースシーンの解説

src/scenes/BaseScene.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は表示されるオブジェクトを表しており、基本的にGeometryMaterialの2つから構成されてます。今回はシンプルな立方体オブジェクトのMeshが表示対象となっています。実行してコンテンツを確認してみましょう。localhost:8000から確認できます。

Screen Shot 2020-06-25 at 21.54.57.png

カスタムシーンを追加する

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

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つ現れます。

Screen Shot 2020-06-26 at 0.11.43.png

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

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

Screen Shot 2020-06-26 at 0.13.58.png

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

GLSLの追加

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

頂点シェーダ

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

フラグメントシェーダ

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

最初に用意したMeshNormalMaterialShaderMaterialに置き換えます。

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()関数に以下を追加します。

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

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

シェーダとGLSLに関しては以下のサイトがわかりやすかったためリンクを添付します。
WebGLのシェーダーとGLSL

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

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を使用する場合は次のようにインポートします。

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' // 追加

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

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の上に乗せる形にします。

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

Screen Shot 2020-06-28 at 16.03.35.png

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

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を覆い隠してしまうとそれらが向こうになる場合がありますので、実現したい表現によっては注意が必要です。

3dobj2.gif

OBJファイルのロード

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

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ファイルをローカルにコピーしておきましょう。相対パスでファイルを指定します。

Screen Shot 2020-06-28 at 22.54.50.png

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

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

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

Webページの公開

GitHubへ変更をプッシュ

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

Netlifyの設定

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

Screen Shot 2020-06-25 at 17.00.35.png

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

Screen Shot 2020-06-25 at 17.01.34.png

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

Screen Shot 2020-06-25 at 22.07.10.png

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

あとがき

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

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

GitHubソースコード

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

Screen Shot 2020-06-28 at 22.40.06.png

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

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

shunp
2018年Amazonが選ぶベストアーキテクチャに金融業界から日本初選出。
https://storygate.info/443502378
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした