What is Three.js?
ブラウザ上に3Dオブジェクトを表示することができるJavaScriptの3Dライブラリです。
紹介されているサイトはどれもめちゃくちゃかっこいいので、みてみるのおすすめです!
今回はsketchfabという3DモデルをダウンロードできるサイトからglTF形式の3Dアニメーションモデルを画面に描画してみました。
⬇︎デプロイ先
⬇︎githubURL
next.jsのプロジェクトを作成
npx create-next-app
✔ Would you like to use TypeScript with this project? … Yes
✔ Would you like to use ESLint with this project? … Yes
✔ Would you like to use Tailwind CSS with this project? … Yes
✔ Would you like to use `src/` directory with this project? … Yes
✔ Use App Router (recommended)? … No
✔ Would you like to customize the default import alias? … No
今回はApp Routerは使用していません。
npm run devでサーバーを起動するとページが表示されます。
必要なライブラリをインストール
npm install three three-stdlib
npm install react-three/drei --save
npm install three @react-three/fiber --save
npm install --save-dev @types/three
型定義もinstallし、今回はreact-threeを使用するので、それもinstallします。
3D Modelをインストール
3D Modelはsketchfabからダウンロードしました。
フォーマットはglTFを選択しましょう。
表示されているモデルはアイコンをみるとダウンロード可能か、有料か、アニメーションがあるのかを判断できます。
今回はハンマーヘッドシャークの素材をダウンロードしました。
ディレクトリ構成
- 📂 project-root
- 📂 .next/ (ビルド後に生成される)
- 📂 node_modules/ (依存関係のパッケージ)
- 📂 public/ (静的ファイル)
- 🖼️ favicon.ico
- 🖼️ vercel.svg
- ここにglTFファイルのzipを解凍したものを入れる
- img
- water.jpegを置く
- 📂 src/ (ソースファイル)
- 📂 components/
- 📄 GeoOcean.tsx
- 📂 pages/
- 📄 _app.js
- 📄 _document.js
- 📄 index.js
- 📂 styles/
- 📄 globals.css
- 📂 components/
- 📄 .gitignore
- 📄 next.config.js (オプション)
- 📄 package.json
- 📄 README.md
publicに3Dモデルとテクスチャのjpegを入れます。
テクスチャに関しては後述します。
表示ページを作成
import Image from 'next/image'
import { Inter } from 'next/font/google'
import * as THREE from 'three'
import { useEffect } from 'react'
import GeoOcean from '@/components/GeoOcean'
const inter = Inter({ subsets: ['latin'] })
export default function Home() {
// console.log(THREE)
return (
<main
className={`flex min-h-screen flex-col items-center justify-between p-24 ${inter.className}`}
>
<div>
<GeoOcean />
</div>
</main>
)
}
GeoOceanコンポーネントに実際の描画の処理を書きます
3Dモデル表示コンポーネント
必要なライブラリをimport
import * as THREE from 'three'
import React, { Suspense, useRef, useMemo, useEffect } from 'react'
import { Canvas, extend, useThree, useLoader, useFrame, Object3DNode } from '@react-three/fiber'
import { OrbitControls, Sky } from '@react-three/drei'
import { GLTFLoader, Water } from 'three-stdlib'
import { AnimationMixer } from 'three';
型定義や型の拡張を行う
type ModelProps = {
url: string;
};
extend({ Water })
declare global {
namespace JSX {
interface IntrinsicElements {
water: Object3DNode<Water, typeof Water>
}
}
}
今回オブジェクトだけでなく、海や太陽の背景も表示しました。
その際にwaterを表示するためにJSXの型の拡張が必要でした。
海を表示する関数定義
water.jpegはここから取ってきてました
function Ocean() {
const ref = useRef<any>();
const gl = useThree((state) => state.gl)
const waterNormals = useLoader(THREE.TextureLoader, '/img/water.jpeg')
waterNormals.wrapS = waterNormals.wrapT = THREE.RepeatWrapping
const geom = useMemo(() => new THREE.PlaneGeometry(10000, 10000), [])
const config = useMemo(
() => ({
textureWidth: 512,
textureHeight: 512,
waterNormals,
sunDirection: new THREE.Vector3(),
sunColor: 0xffffff,
waterColor: 0x001e0f,
distortionScale: 3.7,
fog: false
// format: gl.encoding
}),
[waterNormals]
)
useFrame((state, delta) => (ref.current.material.uniforms.time.value += delta))
return <water ref={ref} args={[geom, config]} rotation-x={-Math.PI / 2} />
}
format: gl.encodingはなぜかエラーになるので、コメントアウトしてます。
有識者がいればここの仕様を教えてください。
モデルを表示する関数を定義
function Model({ url }: ModelProps) {
const ref = useRef<any>()
const mixerRef = useRef<AnimationMixer | null>(null);
useFrame((state, delta) => {
// ref.current.position.y = 10 + Math.sin(state.clock.elapsedTime) * 20
ref.current.position.y = 10
// ref.current.rotation.y += delta
ref.current.rotation.x = -0.2
ref.current.rotation.y = 6.2
ref.current.rotation.z = 0
if (mixerRef.current) {
mixerRef.current.update(delta);
}
})
const { scene, animations } = useLoader(GLTFLoader, url)
useEffect(() => {
if (animations && animations.length > 0) {
mixerRef.current = new AnimationMixer(ref.current);
animations.forEach((clip: THREE.AnimationClip) => {
mixerRef.current?.clipAction(clip).play();
});
}
return () => {
if (mixerRef.current) {
mixerRef.current.stopAllAction();
}
};
}, [animations])
return (
<primitive ref={ref} object={scene} dispose={null} scale={[8, 8, 8]} position={[0, 0, 0]} />
)
}
useLoader(GLTFLoader, url)
これでモデルを読み込んでいます。
AnimationMixerを使用して3Dモデルを動かしています。
ここでモデルを表示してます。
3Dモデルをそのまま表示すると小さすぎたのでscaleを8倍にしてます。
また、positionを設定することでオブジェクトがカメラの中心にくるようになってます。
コンポーネントを表示する部分
export default function GeoOcean() {
return (
// positionはカメラの位置
<Canvas camera={{ position: [-20, 5, 20], fov: 55, near: 1, far: 20000 }} style={{width: '1000px', height: '1000px'}} gl={{ antialias: true, alpha: false }}>
<pointLight position={[100, 100, 100]} />
<pointLight position={[-100, -100, -100]} />
<ambientLight intensity={0.5} />
<Suspense fallback={null}>
<Ocean />
<Model url="/3d-model/model_great_hammerhead_shark/scene.gltf" />
</Suspense>
<Sky sunPosition={[500, 150, -1000]} turbidity={0.1} />
<OrbitControls target={new THREE.Vector3(0, 8, 0)} />
</Canvas>
)
}
- カメラの位置をコントロールできる。マウスでカメラの位置を変更したり、zoom操作も可能です。
今後
今後このアプリはサメ図鑑としてさらにサメの種類を増やしていきます。
ここを親のコンポーネントからurlを渡せるようにして、いろんなサメを表示できるようにしていきます。
参考