なにかお手伝いできることがあればご連絡ください。
※Googleフォームが表示されます
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でメッシュを分割するには、Cell Fracture
を使用します。
アドオンから追加します。
Cell Fracture
を追加したら、Object
タブのQuick Explode > Cell Fracture
をクリックします。
設定はとりあえずそのままでやってみて、うまくいかなければ調整します。 実行すると、メッシュが分割されます。
作成したオブジェクトをglbファイルで出力します。
先ほどと同じように、gltfjsxを実行します。
npx gltfjsx public/models/heart_explode.glb -o src/components/HeartExplode.tsx -r public
コンポーネントファイルが生成されます。
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");
各メッシュを爆発(移動)させるための処理を記述します。
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
);
}
});
});
};
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フォームが表示されます