Closed42

three.jsを色々触ってみる

Taisei.MTaisei.M

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

Taisei.MTaisei.M

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

Taisei.MTaisei.M

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

Taisei.MTaisei.M

3Dオブジェクトは、GeometryとMaterialによって構成される

  • Geometry
    • 点と面の情報を含んでいて、オブジェクトの形状を示す
  • Material
    • Geometryに対して色をつける要素

GeometryにMaterialを適用したものをMeshといい、これはSceneに追加して自由に操作することができる3Dオブジェクト

Taisei.MTaisei.M

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>
Taisei.MTaisei.M

英語ドキュメントだと以下のコードで実装されていて、アニメーションの追加を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>
Taisei.MTaisei.M

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は光源がないと見えないため光源を追加
  • アニメーションを水平方法のみへ変更
Taisei.MTaisei.M

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

Taisei.MTaisei.M

公式ドキュメントにはGLTFLoaderのcdnが書いてなかったので共有
↓これです

https://cdn.jsdelivr.net/npm/three/examples/jsm/three/addons/loaders/GLTFLoader.js

Taisei.MTaisei.M

コードはこちら

    <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>
Taisei.MTaisei.M

公式ドキュメントのチュートリアルから変更している箇所は2つのみ

  1. 4.の部分でcdnからimportしたGLTFLoaderを使って3Dモデルを読み込む
  2. 3Dモデルを照らす光源を追加

表示結果

Taisei.MTaisei.M

Reactアプリケーションで3Dオブジェクトを表示する

reactでThree.jsを扱う場合はReact Three Fiberを使うと良さそう
https://github.com/pmndrs/react-three-fiber

Taisei.MTaisei.M

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

Taisei.MTaisei.M

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ではそれぞれキャメルケースで定義してあげるってだけなのでめちゃくちゃ簡単

Taisei.MTaisei.M

これを書くことは、

// 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()

簡単に定義できて超いいね

Taisei.MTaisei.M

各classのconstructorに対しては、argsというpropsで値を渡せる

//three.js
new THREE.BoxGeometry(2, 2, 2)

// react-three-fiber
<boxGeometry args={[2, 2, 2]} />
Taisei.MTaisei.M

ドキュメントに沿って進めてブラウザ上でBoxオブジェクトの描画ができたけど、インタラクティブに操作ができない

あと、Blenderなどで作成した3Dモデルの読み込みについてはどうするんだろう

Taisei.MTaisei.M

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

https://r3f.docs.pmnd.rs/api/canvas#defaults

Taisei.MTaisei.M

こんな風に書くとりんごの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;
Taisei.MTaisei.M

JSX Componentsとしてモデルを読み込む例も挙げられてる
https://r3f.docs.pmnd.rs/tutorials/loading-models#loading-gltf-models-as-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}って型ついてないぞ

Taisei.MTaisei.M

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

Taisei.MTaisei.M

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

Taisei.MTaisei.M

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

Taisei.MTaisei.M

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

use-cannonはcannon-esを理解できればすぐ扱えるようになりそうなイメージなので、ひとまずcannon-esで物理演算をやってみる

Taisei.MTaisei.M

cannon-esで3Dオブジェクトに物理演算を適用する際のイメージとしては、

cannon-es上で物理演算用の見えないオブジェクトを作成し、Three.js側のオブジェクトをcannon-esのオブジェクトの位置や動きに同期させて表示する

って感じ

Taisei.MTaisei.M

完成したコードに説明を加えたのがこんな感じ

    <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>
Taisei.MTaisei.M

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

このスクラップは2024/10/02にクローズされました