three.jsを色々触ってみる

Three.jsのドキュメントはコチラ
日本語にも対応していて、creating a scene
のセクションに沿って進めれば簡単なオブジェクトの描画ができる

Three.jsで3Dオブジェクトを表示

公式ドキュメントが充実してるから基本その通りに進めればいい

3Dオブジェクトの表示に必要な要素はこの3つ
- scene
- camera
- renderer

Three.jsのドキュメントのサイドメニューを見ると、いろんなcameraやrendererが用意されていることがわかる
これらを使ってどのような方法で3Dオブジェクトを表示するのかを選択できるイメージ

3Dオブジェクトは、GeometryとMaterialによって構成される
- Geometry
- 点と面の情報を含んでいて、オブジェクトの形状を示す
- Material
- Geometryに対して色をつける要素
GeometryにMaterialを適用したものをMeshといい、これはSceneに追加して自由に操作することができる3Dオブジェクト

Creating a scene
のサンプルコードにコメントを追加したものが以下
...
<script type="module">
import * as THREE from "https://cdn.jsdelivr.net/npm/three/build/three.module.js";
// 1. Scene
const scene = new THREE.Scene();
// 2. Camera
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 5;
// 3. Renderer
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 4. 立方体の3Dオブジェクトを定義、sceneに追加
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// 5. `Scene` のレンダリング
function animate() {
requestAnimationFrame(animate);
// キューブの回転アニメーションを追加 ( 60fps )
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
}
animate();
</script>

英語ドキュメントだと以下のコードで実装されていて、アニメーションの追加をrenderer.setAnimationLoop
で設定してる
個人的にはこっちのほうがすっきりして好き
コード
...
<script type="module">
import * as THREE from "https://cdn.jsdelivr.net/npm/three/build/three.module.js";
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setAnimationLoop(animate);
document.body.appendChild(renderer.domElement);
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
camera.position.z = 5;
function animate() {
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
}
</script>

GeometryやMaterialはいろんなものがあって、たとえばコードをこのように変更してみると別のオブジェクトを異なる質感で描画できる
<script type="module">
import * as THREE from "https://cdn.jsdelivr.net/npm/three/build/three.module.js";
// 1. Scene
const scene = new THREE.Scene();
// 2. Camera
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 5;
// 3. Renderer
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 4. 立方体の3Dオブジェクトを定義、sceneに追加
const geometry = new THREE.ConeGeometry(1, 2, 10);
const material = new THREE.MeshPhongMaterial({ color: 0xffff00 });
const cone = new THREE.Mesh(geometry, material);
scene.add(cone);
// MeshPhongMaterialは暗いと見えないため光源を追加
const light = new THREE.DirectionalLight(0xffffff);
light.position.set(0, 1, 1).normalize();
scene.add(light);
// 5. `Scene` のレンダリング
function animate() {
requestAnimationFrame(animate);
// コーンを水平方向へ回転するアニメーション
cone.rotation.y += 0.01;
renderer.render(scene, camera);
}
animate();
</script>
変更箇所
- Geometry, Materialを別のものへ更新
- MeshPhongMaterialは光源がないと見えないため光源を追加
- アニメーションを水平方法のみへ変更

表示結果

次に、Blenderなどで作成した3Dモデルを読み込んで表示してみる

Loading 3D models
セクションを参照

公式ドキュメントにはGLTFLoaderのcdnが書いてなかったので共有
↓これです
https://cdn.jsdelivr.net/npm/three/examples/jsm/three/addons/loaders/GLTFLoader.js

この動画を参考にBlenderでりんごを作ってみた

これをThree.jsでブラウザ上に描画する

コードはこちら
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
// 1. Scene
const scene = new THREE.Scene();
// 2. Camera
const camera = new THREE.PerspectiveCamera(
25,
window.innerWidth / window.innerHeight,
1,
1000
);
camera.position.set(1, 3, 5);
camera.lookAt(0, 0, 0);
// 3. Renderer
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 光源を追加
const light = new THREE.DirectionalLight("#ffffff", 2);
const ambient = new THREE.AmbientLight("#85b2cd");
light.position.set(10, 10, 0).normalize();
scene.add(light);
scene.add(ambient);
// 4. 3Dモデルの読み込み
let model;
const loader = new GLTFLoader();
loader.load(
"model/apple.glb",
(gltf) => {
model = gltf.scene;
model.scale.set(1, 1, 1);
scene.add(model);
},
undefined,
(error) => {
console.log("読み込みに失敗しました 😢");
}
);
// りんごを水平方向へ回転するアニメーション
const animate = () => {
requestAnimationFrame(animate);
if (model) model.rotation.y += 0.01;
renderer.render(scene, camera);
};
animate();
</script>

公式ドキュメントのチュートリアルから変更している箇所は2つのみ
-
4.
の部分でcdnからimportしたGLTFLoaderを使って3Dモデルを読み込む - 3Dモデルを照らす光源を追加
表示結果

Reactアプリケーションで3Dオブジェクトを表示する
reactでThree.jsを扱う場合はReact Three Fiberを使うと良さそう

Your first scene
セクションが用意されているから読み進める

The general rule is that Fiber components are available under the camel-case version of their name in three.js.
といっても、Three.jsのgeometryやmaterialはすべて使えるし、JSXではそれぞれキャメルケースで定義してあげるってだけなのでめちゃくちゃ簡単

これを書くことは、
// react-three-fiber
<Canvas>
<mesh>
<boxGeometry />
<meshStandardMaterial />
</mesh>
</Canvas>
これと同義
// three.js
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
const renderer = new THREE.WebGLRenderer()
renderer.setSize(width, height)
document.querySelector('#canvas-container').appendChild(renderer.domElement)
const mesh = new THREE.Mesh()
mesh.geometry = new THREE.BoxGeometry()
mesh.material = new THREE.MeshStandardMaterial()
scene.add(mesh)
function animate() {
requestAnimationFrame(animate)
renderer.render(scene, camera)
}
animate()
簡単に定義できて超いいね

各classのconstructorに対しては、args
というpropsで値を渡せる
//three.js
new THREE.BoxGeometry(2, 2, 2)
// react-three-fiber
<boxGeometry args={[2, 2, 2]} />

ドキュメントに沿って進めてブラウザ上でBoxオブジェクトの描画ができたけど、インタラクティブに操作ができない
あと、Blenderなどで作成した3Dモデルの読み込みについてはどうするんだろう

Getting Started
セクションにEco systemがずらりと紹介されていた
ぱっと見た感じ、3Dモデルの読み込みは@react-three/gltfjsx
を別途インストールする必要がありそうだな

オブジェクトをインタラクティブに操作したい場合は、@react-three/drei
からOrbitControls
をインストールして使う

react-three-fiberのCanvas
は、デフォルトでscene内部に以下が定義されるよう
- THREE.Perspective camera
- THREE.Orthographic cam if orthographic is true
- THREE.PCFSoftShadowMap if shadows is true
- THREE.Scene (into which all the JSX is rendered) and a THREE.Raycaster

こんな風に書くとりんごの3Dモデルを読み込んで表示できる
import { OrbitControls } from '@react-three/drei';
import { Canvas, useLoader, useThree } from '@react-three/fiber';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import './App.css';
import { useEffect } from 'react';
// useThree フックを使ってcameraオブジェクトにアクセスし、lookAt を変更
const CameraController = () => {
const { camera } = useThree();
useEffect(() => {
camera.lookAt(0, 0, 0);
}, [camera]);
return null;
};
// 3Dオブジェクト(アップル)の読み込み
const Apple = () => {
const apple = useLoader(GLTFLoader, '/apple.glb');
apple.scene.position.set(0, 0, 0);
return <primitive object={apple.scene} />;
};
const App = () => {
return (
<div style={{ width: '100vw', height: '100vh' }}>
<Canvas camera={{ position: [1, 3, 5], fov: 25 }}>
<CameraController />
<directionalLight intensity={2} position={[10, 10, 10]} />
<ambientLight color={'#85b2cd'} />
<Apple />
<OrbitControls
makeDefault
autoRotate
autoRotateSpeed={0.1}
minPolarAngle={0}
maxPolarAngle={Math.PI / 2}
/>
</Canvas>
</div>
);
};
export default App;

3Dモデルの表示方法はここに書いてある

JSX Componentsとしてモデルを読み込む例も挙げられてる
/*
Auto-generated by: https://github.com/pmndrs/gltfjsx
*/
import React, { useRef } from 'react'
import { useGLTF } from '@react-three/drei'
export default function Model(props) {
const groupRef = useRef()
const { nodes, materials } = useGLTF('/Poimandres.gltf')
return (
<group ref={groupRef} {...props} dispose={null}>
<mesh castShadow receiveShadow geometry={nodes.Curve007_1.geometry} material={materials['Material.001']} />
<mesh castShadow receiveShadow geometry={nodes.Curve007_2.geometry} material={materials['Material.002']} />
</group>
)
}
useGLTF.preload('/Poimandres.gltf')
geometry={nodes.Curve007_1.geometry}
って型ついてないぞ

↑ ここらへんはJSX Componentsとして定義する機会があったときに調査する

ひとまずreact-three-fiberで3Dモデルの描画までできた

3Dオブジェクトに対して物理演算をやってみる

以下はreact-three-fiberのドキュメントに記載されていた、物理演算ライブラリ

今回は圧倒的にスター数が多い、use-cannonを使用する

use-cannonは、cannon-esをフックとして提供してくれているライブラリだけど、どちらもドキュメントが若干ややこい 🫠

今回は圧倒的にスター数が多い、use-cannonを使用する
use-cannonはcannon-esを理解できればすぐ扱えるようになりそうなイメージなので、ひとまずcannon-esで物理演算をやってみる

cannon-esで3Dオブジェクトに物理演算を適用する際のイメージとしては、
「cannon-es上で物理演算用の見えないオブジェクトを作成し、Three.js側のオブジェクトをcannon-esのオブジェクトの位置や動きに同期させて表示する」
って感じ

これを踏まえて、公式ドキュメントのGetting Started
を読み進めてみるとだいぶわかりやすい

完成したコードに説明を加えたのがこんな感じ
<script type="module">
import * as THREE from "three";
import * as CANNON from "cannon";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
// 1. Scene
const scene = new THREE.Scene();
// 2. Camera
const camera = new THREE.PerspectiveCamera(
100,
window.innerWidth / window.innerHeight,
1,
2000
);
camera.position.set(0, 3, -5);
camera.lookAt(0, 0, 0);
// 3. Renderer
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Controller
const controls = new OrbitControls(camera, renderer.domElement);
// 物理エンジンを含む世界を作成
const world = new CANNON.World({
gravity: new CANNON.Vec3(0, -9.81, 0),
});
// Cannon.jsのオブジェクトを作成
const radius = 1;
const sphereBody = new CANNON.Body({
mass: 5,
shape: new CANNON.Sphere(radius),
});
sphereBody.position.set(0, 10, 0);
world.addBody(sphereBody);
const groundBody = new CANNON.Body({
type: CANNON.Body.STATIC,
shape: new CANNON.Plane(),
});
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);
world.addBody(groundBody);
// Three.jsのオブジェクトを作成
const geometry = new THREE.SphereGeometry(radius);
const material = new THREE.MeshNormalMaterial();
const sphereMesh = new THREE.Mesh(geometry, material);
scene.add(sphereMesh);
const animate = () => {
requestAnimationFrame(animate);
controls.update();
world.fixedStep();
// Cannon.jsのオブジェクトの位置と姿勢をThree.jsのオブジェクトと同期
sphereMesh.position.copy(sphereBody.position);
sphereMesh.quaternion.copy(sphereBody.quaternion);
renderer.render(scene, camera);
};
animate();
</script>

表示結果は、虹色の球体が上から落ちてくる感じ

3Dオブジェクトの重さや他のオブジェクトと衝突したときの挙動などを色々指定できる
ここらへんはこの動画見るとひと通り掴めるはず

Three.jsの基本はこれで完全に理解した :done: