この記事はWebXR Tech Tokyo #1の発表で使用いたしました。
イベントを企画いただいた運営の皆様、会場を盛り上げていただいた参加者の皆様、ありがとうございました!
2020/7/12追記
こちらのテンプレートがGatsby公式に追加されました。引き続きご利用いただけると幸いです。
https://www.gatsbyjs.org/starters/shunp/gatsby-three-ts-plus/
この記事でできるようになること
Three.jsを使った3Dウェブページをモダンな技術スタックを使って簡単に開発・公開できるようになります。オリジナルテンプレートを使うことでスムーズに始められ、またGatsbyを用いることで自分好みのカスタマイズを簡単に加えることが可能です。
以下のサンプルシーンは予めテンプレートに組み込まれております。本記事での解説に加え、必要に応じてテンプレート内の実装を参考にしていただき、ご自身で3Dシーンを作成する際のお役立ていただければ幸いです。
使用する技術スタック
Three.js
TypeScript
Gatsby
Netlify
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のサンプルページにあるものを少しカスタマイズしたものになります。
ベースシーンの解説
src/scenes/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
から確認できます。
カスタムシーンを追加する
上記のベースシーンをカスタマイズしてみます。カメラとオブジェクトを以下のように修正します。
...
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つ現れます。
正面からだとよく分からないので、立方体だとわかるように回転を加えてみます。
...
// 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)
}
...
ここまでで動きのある3Dオブジェクトを作成できましたが、少し味気ないため次節で表面に動きを付けていきます。
GLSLの追加
WebGLで何かを描画するためには2つのシェーダが必要になります。シェーダを描くための言語GLSL(グラフィクス・ライブラリ・シェーダー言語)を利用したサンプルもThree.jsのコードにはありますが、あくまで素のHTMLを読み込むことを想定した書き方なのでそのまま利用することができません。Reactを利用する場合は以下のように記述していきます。
頂点シェーダ
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 );
}
`
最初に用意したMeshNormalMaterial
をShaderMaterial
に置き換えます。
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()
関数に以下を追加します。
// 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)
}
表面の鮮やかな模様が時間と共に変化している動的な描画になりました。
シェーダとGLSLに関しては以下のサイトがわかりやすかったためリンクを添付します。
WebGLのシェーダーとGLSL
ここまでのソースコードは以下のようになります。
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を使用する場合は次のようにインポートします。
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
を呼び出します。
...
// 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の上に乗せる形にします。
...
return (
<>
<div>
<span>Three.js × Gatsby Template</span>
</div>
<div css={css``} ref={mount} />
</>
)
...
右上に小さく文字が表示されているため、CSSを当てて見た目を調整します。スタイルの追加には方法がいくつかありますが、今回のテンプレートにはTailwindCSSを入れているので、これを使ってデザイン調整する方法をみていきましょう。TailwindCSSとは
...
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を覆い隠してしまうとそれらが向こうになる場合がありますので、実現したい表現によっては注意が必要です。
OBJファイルのロード
新しいシーンで3Dオブジェクトファイルの取り扱いを説明していきます。3Dモデルフォーマットにはいくつか種類がありますが、今回は.obj
形式のファイルをOBJLoader
を使用して取り込んで見たいと思います。
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ファイルをローカルにコピーしておきましょう。相対パスでファイルを指定します。
OBJファイルはstatic
ディレクトリ以下におきます。ここに置くことでビルド時にpublic
ディレクトリ以下にコピーが生成されて相対パスでアクセスできるようになります。
...
// 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
を開くと以下のシーンが表示されます。
ここまでで3Dシーンに関する説明は以上です。同様にpage
ディレクトリ以下にファイルを作成することで新たなカスタムページを作成することができます。Three.jsはWeb上にたくさんのサンプルが落ちていますので、上記の基本をベースにサンプルを動かしつつ自身のポートフォリオを作成していってください。
Webページの公開
GitHubへ変更をプッシュ
最後に、ここまでの変更をWeb上で公開するため、まずは自身のGitHubリポジトリに対してソースコードをプッシュしておきましょう。
Netlifyの設定
Netlifyを使うことで簡単にポートフォリオサイトを公開できます。リンクからNetlifyのページに飛び、自身のページにログインをしてください。初めての方はサインアップが必要になります。右上にある「New site from Git」から新しいサイトを作成するページに飛びます。ソースコードがGitHubにある場合はGitHubボタンを選択して次に進みます。
今回保存されたソースコードを含むリポジトリを選択します。ビルド設定はデフォルトで問題ありません。「Deploy site」ボタンを押するとビルドが始まり、しばらくするとサイトが公開されます。
また、Netlify公開用にランダムで割り当てられた文字列のURLではなく、独自の名前を設定することも可能です。
Setting→Domain Management→Domains→Custom domains→Options→Edit site nameから選択できます。
設定したURLからページが公開されていることを確認してみましょう。
あとがき
Three.jsの公式サンプルページには、様々な表現方法を用いたカタログが一覧になっています。サンプルコードとセットになっており、新しく3Dシーンを作成する際にはとても参考になるでしょう。
一方、長い歴史を持つThree.jsは、今ではあまり使われない記法で記述されたコードが多いのも現状です。
今回のテンプレートではTypeScriptやES6以降の記法を取り入れつつ、現場で使われることの多いReactをベースに作成しました。react-three-fiberというライブラリも存在しますが、Three.jsのバージョンアップに追随できる、かつ特定のフレームに依存しないなるべく素のThree.jsを扱えるようにカスタマイズ性の高いテンプレートとして公開しています。オープンソースですのでお気軽にPR、スター等いただけると今後の活動の励みになります。
また、3Dのポートフォリオジェネレータとは別に、ソーシャルメディアのポートフォリオジェネレータを別途開発中です。今後はSNSだけではなく3Dアバターなどもクリップできるようにしていく予定です。現在α版としてテストユーザを募集しておりますので、ご興味がありましたらこちらまでご連絡をお願いいたします。
お気に入りのポストを好きなSNSから、好きな数だけNoCodeでクリップできるポートフォリオサイトです。「複数のSNSを1つのURLで管理したい」「埋もれてしまった過去のポストをクリップしておきたい」場合に便利です。SNS版前略プロフィール?のようなイメージでお楽しみください。
https://storygate.info
最後までご一読いただきありがとうございました。