@react-three/fiberをBlenderのglbファイルを表示する

@react-three/fiberをBlenderのglbファイルを表示する

2023/12/01
ReactThree.jsBlender

gltfjsxをインストール

gltfjsxは、Blenderモデルファイル(.glb)を、react-three-fiberで読み込むためのコンポーネントファイル(.tsx)を自動生成するツールです。
gltfjsxをインストール。

npm i -g gltfjsx

使い方は、以下の通りです。

$ npx gltfjsx [Model.glb] [options]

publicフォルダ内の、models/heart.glbを変換する場合は、以下のコマンドを実行します。
出力先はsrcフォルダ内のcomponents/Heart.tsxにします。

optionsには、-rを指定することで、画像ファイルを読み込むためのパスを指定します。
-rを指定しない場合は、画像ファイルを読み込むためのパスが、publicフォルダからの相対パスになります。

npx gltfjsx public/models/heart.glb -o src/components/Heart.tsx -r public

上記コマンドを実行すると、以下のようなコンポーネントファイルが生成されます。
コンポーネント名だけModelとなるのでHeartに変更しています。

/*
Auto-generated by: https://github.com/pmndrs/gltfjsx
Command: npx gltfjsx@6.2.15 public/models/heart.glb -o src/components/Heart.tsx -r public 
*/

import React, { useRef } from "react";
import { useGLTF } from "@react-three/drei";

export function Heart(props) {
  const { nodes, materials } = useGLTF("/models/heart.glb");
  return (
    <group {...props} dispose={null}>
      ... 中略
    </group>
  );
}
useGLTF.preload("/models/heart.glb");

後は、このコンポーネント内で呼び出すだけです。

import "./App.css";
import { Canvas } from "@react-three/fiber";
/** @jsxImportSource @emotion/react */
import { css } from "@emotion/react";
import { Heart } from "./components/Heart";
import { Environment, OrbitControls } from "@react-three/drei";

const canvas = css`
  height: 100vh !important;
  width: 100vw !important;
`;

function App() {
  return (
    <>
      <Canvas css={canvas} camera={{ position: [2, 0, 5], fov: 30 }}>
        <OrbitControls />
        <ambientLight />
        <pointLight position={[10, 10, 10]} />
        <Environment preset="sunset" background blur={0.1} /> // ちょっと暗いので、背景を夕焼けにしてみる
        <Heart scale={0.05} /> // ここで呼び出す
      </Canvas>
    </>
  );
}

export default App;

おまけ

メッシュ爆発エフェクト - React Three Fiber チュートリアルの動画をやってみた。

表示するモデルはBlenderで作成したものを上記手順でモデル化したものを使用。

Blenderでメッシュを分割する

Blenderでメッシュを分割するには、Cell Fractureを使用します。
アドオンから追加します。

Cell Fractureを追加したら、ObjectタブのQuick Explode > Cell Fractureをクリックします。

設定はとりあえずそのままでやってみて、うまくいかなければ調整します。   実行すると、メッシュが分割されます。

作成したオブジェクトをglbファイルで出力します。

gltfjsxを実行

先ほどと同じように、gltfjsxを実行します。

npx gltfjsx public/models/heart_explode.glb -o src/components/HeartExplode.tsx -r public

コンポーネントファイルが生成されます。

◎ HeartExplode.tsx

component名だけModelとなるのでHeartExplodeに変更しています。

/*
Auto-generated by: https://github.com/pmndrs/gltfjsx
Command: npx gltfjsx@6.2.15 public/models/heart.glb -o src/components/Heart.tsx -r public 
*/

import React, { FC, useRef } from "react";
import { useGLTF } from "@react-three/drei";
import * as THREE from "three";
import { useExplode } from "../utils/explode";

export const HeartExplod: FC<JSX.IntrinsicElements["group"]> = (props) => {
  const { nodes, materials } = useGLTF("/models/heart_explode.glb");
  const group = useRef<THREE.Group>(null!);

  // メッシュを爆発させる
  useExplode(group, { distance: 8 });

  return (
    <group ref={group} {...props} dispose={null}>
      <mesh
        name="origin"
        geometry={nodes.origin.geometry}
        material={materials.Red}
        rotation={[-Math.PI / 2, 0, 0]}
        scale={2311.615}
      />
      <mesh
        geometry={nodes.Heart_Full_cell.geometry}
        material={materials.Red}
        position={[-8.612, 2.336, -1.805]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell001.geometry}
        material={materials.Red}
        position={[1.552, -9.313, -1.645]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell002.geometry}
        material={materials.Red}
        position={[6.241, -1.288, -4.231]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell003.geometry}
        material={materials.Red}
        position={[0.098, 3.187, 2.946]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell004.geometry}
        material={materials.Red}
        position={[3.819, 1.303, -4.676]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell005.geometry}
        material={materials.Red}
        position={[1.715, -8.868, 1.831]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell006.geometry}
        material={materials.Red}
        position={[3.5, -6.116, 0.381]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell007.geometry}
        material={materials.Red}
        position={[-4.894, -4.775, 2.582]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell008.geometry}
        material={materials.Red}
        position={[6.668, 4.034, -1.275]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell009.geometry}
        material={materials.Red}
        position={[6.188, -2.684, 3.672]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell010.geometry}
        material={materials.Red}
        position={[0.728, -4.356, 3.925]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell011.geometry}
        material={materials.Red}
        position={[-6.128, 2.291, -3.724]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell012.geometry}
        material={materials.Red}
        position={[-8.668, 1.159, 2.468]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell013.geometry}
        material={materials.Red}
        position={[-1.655, -8.624, 1.817]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell014.geometry}
        material={materials.Red}
        position={[3.51, -3.406, 3.956]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell015.geometry}
        material={materials.Red}
        position={[3.96, 4.219, 2.375]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell016.geometry}
        material={materials.Red}
        position={[4.614, -0.366, 4.361]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell017.geometry}
        material={materials.Red}
        position={[0.314, 3.348, -2.892]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell018.geometry}
        material={materials.Red}
        position={[-0.158, 0.862, 0.017]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell019.geometry}
        material={materials.Red}
        position={[-1.53, -9.277, -1.742]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell020.geometry}
        material={materials.Red}
        position={[8.249, -1.851, -2.417]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell021.geometry}
        material={materials.Red}
        position={[-3.222, 2.048, -4.086]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell022.geometry}
        material={materials.Red}
        position={[7.817, 0.166, 3.134]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell023.geometry}
        material={materials.Red}
        position={[-2.309, -3.78, 4.027]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell024.geometry}
        material={materials.Red}
        position={[-3.221, 2.016, 4.084]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell025.geometry}
        material={materials.Red}
        position={[4.837, -5.598, 2.438]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell026.geometry}
        material={materials.Red}
        position={[-5.326, -1.08, 4.352]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell027.geometry}
        material={materials.Red}
        position={[-6.955, 3.916, -1.339]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell028.geometry}
        material={materials.Red}
        position={[-0.089, -1.339, 3.063]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell029.geometry}
        material={materials.Red}
        position={[8.837, 1.484, -0.224]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell030.geometry}
        material={materials.Red}
        position={[-4.075, 4.289, -2.481]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell031.geometry}
        material={materials.Red}
        position={[-2.054, -7.102, 4.274]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell032.geometry}
        material={materials.Red}
        position={[6.815, 3.572, 1.974]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell033.geometry}
        material={materials.Red}
        position={[-3.615, 5.223, -0.029]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell034.geometry}
        material={materials.Red}
        position={[9.541, -1.184, 0.128]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell035.geometry}
        material={materials.Red}
        position={[-0.289, -0.627, -4.222]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell036.geometry}
        material={materials.Red}
        position={[-0.095, -5.757, -1.529]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell037.geometry}
        material={materials.Red}
        position={[5.691, -5.419, -2.594]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell038.geometry}
        material={materials.Red}
        position={[4.011, 4.055, -2.373]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell039.geometry}
        material={materials.Red}
        position={[6.563, -3.829, -0.012]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell040.geometry}
        material={materials.Red}
        position={[-4.933, -0.539, -4.564]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell041.geometry}
        material={materials.Red}
        position={[-5.566, -5.506, -2.701]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell042.geometry}
        material={materials.Red}
        position={[4.789, 4.765, 0.062]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell043.geometry}
        material={materials.Red}
        position={[2.481, -5.699, -2.078]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell044.geometry}
        material={materials.Red}
        position={[6.741, 1.547, -3.647]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell045.geometry}
        material={materials.Red}
        position={[7.902, -2.329, 2.13]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell046.geometry}
        material={materials.Red}
        position={[-6.046, 2.424, 3.675]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell047.geometry}
        material={materials.Red}
        position={[2.889, -2.858, -2.747]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell048.geometry}
        material={materials.Red}
        position={[2.189, 4.581, 0]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell049.geometry}
        material={materials.Red}
        position={[-9.504, -0.321, -0.755]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell050.geometry}
        material={materials.Red}
        position={[-6.954, 3.915, 1.343]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell051.geometry}
        material={materials.Red}
        position={[-3.336, -5.917, 0.311]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell052.geometry}
        material={materials.Red}
        position={[0, -10.376, 3.217]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell053.geometry}
        material={materials.Red}
        position={[-6.627, -3.939, 0.169]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell054.geometry}
        material={materials.Red}
        position={[-2.505, -5.674, -2.098]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell055.geometry}
        material={materials.Red}
        position={[-7.51, -0.243, -3.781]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell056.geometry}
        material={materials.Red}
        position={[4.64, 2.373, 4.223]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell057.geometry}
        material={materials.Red}
        position={[-0.085, -10.567, 0.022]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell058.geometry}
        material={materials.Red}
        position={[-3.504, -3.067, -3.044]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell059.geometry}
        material={materials.Red}
        position={[-7.911, -2.432, 2.561]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell060.geometry}
        material={materials.Red}
        position={[-7.746, -2.642, -2.544]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell061.geometry}
        material={materials.Red}
        position={[-3.969, 4.319, 2.358]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell062.geometry}
        material={materials.Red}
        position={[2.287, -6.868, 4.493]}
      />
      <mesh
        geometry={nodes.Heart_Full_cell063.geometry}
        material={materials.Red}
        position={[-1.586, 5.561, 0.041]}
      />
    </group>
  );
};

useGLTF.preload("/models/heart_explode.glb");

◎ utils/explode.ts

各メッシュを爆発(移動)させるための処理を記述します。

import { useEffect } from "react";
import { useScroll } from "@react-three/drei";
import * as THREE from "three";
import { useFrame } from "@react-three/fiber";

export const useExplode = (
  group: React.MutableRefObject<THREE.Group>,
  { distance = 3, enableRotation = true }
) => {
  useEffect(() => {
    const groupWorldPosition = new THREE.Vector3();
    group.current.getWorldPosition(groupWorldPosition);

    group.current?.children.forEach((mesh) => {
      mesh.originalPosition = mesh.position.clone();
      // オブジェクトが完全に中心にない場合、そのメッシュのワールド座標が必用になる為
      const meshWorldPosition = new THREE.Vector3();
      // TODO ここが謎だが、実行しないとmeshの座標がワールド座標にならない??
      mesh.getWorldPosition(meshWorldPosition);

      mesh.directionVector = meshWorldPosition
        .clone()
        .sub(groupWorldPosition)
        .normalize();

      mesh.originalRotation = mesh.rotation.clone();
      mesh.targetRotation = new THREE.Euler(
        Math.random() * Math.PI,
        Math.random() * Math.PI,
        Math.random() * Math.PI
      );

      mesh.targetPosition = mesh.originalPosition
        .clone()
        .add(mesh.directionVector.clone().multiplyScalar(distance));
    });
  }, [distance]);

  // スクロール量を取得
  const scrollData = useScroll();

  useFrame(() => {
    group.current?.children.forEach((mesh) => {
      // スクロール量0の場合(爆発してない)オリジナル画像を表示する
      if (scrollData.offset < 0.0001) {
        if (mesh.name === "origin") {
          mesh.visible = true;
        } else {
          mesh.visible = false;
        }
      } else {
        if (mesh.name === "origin") {
          mesh.visible = false;
        } else {
          mesh.visible = true;
        }
      }

      /**
       * lerp
       * 2つの値の間を滑らかに補間する
       * 0〜1の間の値を入れると、その間の値を返す
       * 例: lerp(0, 10, 0.5) => 5
       * 例: lerp(0, 10, 0.2) => 2
       */

      mesh.position.x = THREE.MathUtils.lerp(
        mesh.originalPosition.x,
        mesh.targetPosition.x,
        scrollData.offset // 0〜1が入る
      );

      mesh.position.y = THREE.MathUtils.lerp(
        mesh.originalPosition.y,
        mesh.targetPosition.y,
        scrollData.offset
      );

      mesh.position.z = THREE.MathUtils.lerp(
        mesh.originalPosition.z,
        mesh.targetPosition.z,
        scrollData.offset
      );

      if (enableRotation) {
        mesh.rotation.x = THREE.MathUtils.lerp(
          mesh.originalRotation.x,
          mesh.targetRotation.x,
          scrollData.offset
        );

        mesh.rotation.y = THREE.MathUtils.lerp(
          mesh.originalRotation.y,
          mesh.targetRotation.y,
          scrollData.offset
        );

        mesh.rotation.z = THREE.MathUtils.lerp(
          mesh.originalRotation.z,
          mesh.targetRotation.z,
          scrollData.offset
        );
      }
    });
  });
};

◎ App.tsx

ScrollControlsを使用して、スクロールできるようにしますが、オブジェクトはスクロールさせないので、<Scroll></Scroll>では囲みません。

特に必用ないですがFloatを使用して、オブジェクトを浮遊させます。

import "./App.css";
import { Canvas } from "@react-three/fiber";
/** @jsxImportSource @emotion/react */
import { css } from "@emotion/react";
import { Heart } from "./components/Heart";
import {
  Environment,
  Float,
  OrbitControls,
  ScrollControls,
} from "@react-three/drei";

const canvas = css`
  height: 100vh !important;
  width: 100vw !important;
`;

function App() {
  return (
    <>
      <Canvas css={canvas} camera={{ position: [2, 0, 5], fov: 30 }}>
        <OrbitControls enableZoom={false} />
        <ambientLight />
        <pointLight position={[10, 10, 10]} />
        <ScrollControls pages={4}>
          <Float floatIntensity={2} speed={3}>
            <Heart scale={0.05} />
          </Float>
        </ScrollControls>
        <Environment preset="sunset" background blur={0.5} />
      </Canvas>
    </>
  );
}

export default App;

こんな感じになります。

関連記事

なにかお手伝いできることがあればご連絡ください。

お問い合わせはこちらから

※Googleフォームが表示されます