Skip to content

サイコロを作りたい(2)吹き飛ばす

Updated: at 13:00
ロゴを立方体にしたもの

前回はスポットライトの実装について解説したので、今回はサイコロをクリックした際に吹っ飛ばす機能を実装していきます。

目次

目次を開く

使用技術

今回から主要パッケージとバージョンをちゃんと明記します(したい)。

パッケージ名バージョン
@react-three/fiber8.13.0
@react-three/drei9.66.1
@react-three/cannon6.5.2
zustand4.3.7

実装

トライアンドエラーで実装していきます。
─=≡Σ((( つ•̀ω•́)つ

下準備(物理場)

前回の時に床作りの説明をしていなかったので、床から作り始めます。どうしてライトの実装から始めてしまったんや。。。

@react-three/cannonでは、まずは物理演算を適用させる世界を宣言しなくてはなりません。

export default function MyGL() {
  const mode = useMode();

  return (
    <Suspense fallback={null}>
      <Canvas
        shadows
        gl={{ alpha: false }}
        camera={{ position: [-5, 10, 5], fov: 50 }}
      >
        <Environment preset="sunset" />
        <Lights />

        <Physics broadphase="SAP">
          <Debug color="red" scale={1.1}>
            {/* ここにサイコロや床などの物理演算を適用させたコンポーネントを配置する。 */}
          </Debug>
        </Physics>
        <OrbitControls makeDefault />
        <Stats />
      </Canvas>
    </Suspense>
  );
}

<Physics></Physics>の内側に物理演算を適用させたサイコロなどを配置します。プロパティbroadphaseでは衝突検出の前段階として働くアルゴリズムを指定できます。今回は「SAP」と呼ばれるアルゴリズムを指定しています。詳しくはcannon-es のドキュメントを確認してみてください。
また、デバッグの際はさらに<Debug></Debug>で囲むと内にあるオブジェクトにワイヤーフレームが追加されるので、認識しやすくなります。

下準備(床)

物理場ができたので床を設置します。

const Plane = (props: PlaneProps) => {
  const [ref] = usePlane(
    () => ({
      rotation: [-Math.PI / 2, 0, 0],
      ...props,
    }),
    useRef<Mesh>(null)
  );
  return (
    <group>
      <mesh ref={ref} receiveShadow>
        <planeGeometry args={[100, 100]} />
        <shadowMaterial />
      </mesh>
    </group>
  );
};

export default Plane;

@react-three/cannonのusePlaneをプレートに適用し、物理を適用します。現段階では特に変なことはしていません。物理場である<Physics></Physics>の中で宣言することを忘れないでください。

下準備(サイコロ)

サイコロを召喚します。

useGLTF.preload("/assets/model/logocube.glb");

const Model = (props: BoxProps) => {
  const { nodes, materials } = useGLTF(
    "/assets/model/logocube.glb"
  ) as GLTFResult;

  return (
    <group>
      <group position={[0, 0, 0]}>
        <mesh
          castShadow
          receiveShadow
          geometry={nodes.kuang.geometry}
          material={materials.gold}
        />
        <mesh
          castShadow
          receiveShadow
          geometry={nodes.neihong.geometry}
          material={materials.neise}
        />
      </group>
    </group>
  );
};

export default Model;

使用している3Dモデルはマテリアルを分けているため、2つメッシュが存在します。テクスチャにベイクしている場合などは1つのメッシュで済みそうですね。また3Dモデルでよくあるミスが Blenderでスケールを正規化せずにエクスポートしちゃうことです。何かとずれが生じるのでスケールは正規化しましょう。今回はサイズも1に合わせたかったので、Blender側でxyzそれぞれ1mの立方体にしています。

onClick

いよいよクリックイベントを設定します。r3fはレイキャストを設定しなくてもクリックイベントを簡単に実装できるのは神過ぎます。先に床と同じ要領で物理を適用します。

const [boxRef, boxApi] = useBox(
  () => ({
    mass: 1,
    position: [0, 10, 0],
    rotation: [
      Math.PI * Math.random(),
      Math.PI * Math.random(),
      Math.PI * Math.random(),
    ],
    args: boxSize,
    ...props,
  }),
  useRef<Group>(null!)
);

massというのは質量を表しています。基本1でいいかなと思います。
useBoxの返り値はrefapiです。refは今回の場合は<group>にくっつけます。物理オブジェクトのプロパティや状態を制御するための機能を提供するapiを使用してクリックイベントを実装していきます。

const handleClick = () => {
  const impulse: Triplet = [1, 2, 0]; // x, y, z軸方向の力
  const worldPoint: Triplet = [0, 0, 0]; // オブジェクト中心に力を適用
  boxApi.applyImpulse(impulse, worldPoint);
};

api.applyImpulse()はオブジェクト(今回はBox)に衝撃を与えることができます。第一引数に衝撃の方向ベクトル、第二引数に衝撃力が適用されるオブジェクト上の点を指定します。また、各変数をTripletで型付けしていますが、threeVector3でも大丈夫です。
では実装したサイコロをクリックしてみます。

画面外に飛ばされるサイコロ

一応は物理演算が適応されているので、今の状態でもいい感じではありますね。しかし、画面外に見切れてしまうのはよろしくないので、クリック時の衝撃が常にSceneの原点へ向くようにしてみましょう。

const onClickHandler = () => {
  const target = new Vector3(0, 2, 0); // オブジェクトが向かうべきターゲット(画面の中心)

  // オブジェクトの位置からターゲットへの方向ベクトルを計算
  const direction = target
    .clone()
    .sub(new Vector3(...boxPosition))
    .normalize();

  // 衝撃力の大きさ
  const forceMagnitude = 3.5;
  const minOffset = 4.5; // 最小のオフセット値を設定

  // 衝撃力を方向ベクトルに適用
  const impulse: Triplet = [
    forceMagnitude *
      (direction.x + (direction.x >= 0 ? minOffset : -minOffset)),
    forceMagnitude *
      (direction.y + (direction.y >= 0 ? minOffset : -minOffset)),
    forceMagnitude *
      (direction.z + (direction.z >= 0 ? minOffset : -minOffset)),
  ];

  // トルクの値をランダムに設定
  const torque: Triplet = [
    (Math.random() - 0.5) * Math.PI,
    (Math.random() - 0.5) * Math.PI,
    (Math.random() - 0.5) * Math.PI,
  ];

  const worldPoint: Triplet = [0, 0, 0];
  boxApi.applyImpulse(impulse, worldPoint);
  boxApi.applyTorque(torque);
};

簡単にいうとサイコロの位置から原点までの方向ベクトルを算出し、さらにトルクと呼ばれる回転力もランダムに決めて、サイコロに設定しています。これでサイコロが画面外に吹っ飛んでいくことはほとんどなくなったと思うのですが、この記事書いてる途中に「impulsetorque両方設定するんじゃなくて、torqueだけでよくないか?」と思って実装してみたら、torqueだけの実装の方が理想的なサイコロの動きだったので、そっちも紹介します。

const onClickHandler = () => {
  // スケーリングファクターを追加して、トルクの大きさを調整
  const torqueScalingFactor = 180;
  //
  const torqueWeight = 0.9;
  // キューブの上方向ベクトル
  const cubeUpDirection = new Vector3(0, 1, 0);

  // キューブの位置から [0, 0, 0] への方向ベクトルを計算
  const directionToOrigin = new Vector3(...boxPosition).negate().normalize();

  // トルクを計算(外積)
  const torque = cubeUpDirection
    .clone()
    .add(
      new Vector3(
        (Math.random() - 0.5) * torqueWeight,
        (Math.random() - 0.5) * torqueWeight,
        (Math.random() - 0.5) * torqueWeight
      )
    )
    .cross(directionToOrigin)
    .normalize() // トルクベクトルを正規化
    .multiplyScalar(torqueScalingFactor)
    .toArray() as Triplet;

  boxApi.applyTorque(torque);
};

コメント書いてるのでそこを読めば理解はできると思いますが、トルク計算の部分でランダムベクトルを追加しているのは、ランダム要素を入れないとサイコロが直線を行ったり来たりするだけになって見栄えが悪いからです。ランダムベクトルを加えることにより、ベクトルの向きの大筋は原点に向いてるけど、ちょっとずれている状態を演出しています。
またこの実装の場合、サイコロと床のfriction(摩擦係数)とrestitution(反発係数)をいい感じに設定してあげなければ、摩擦が大き過ぎて動かないことがあります。

// サイコロ
const [boxRef, boxApi] = useBox(
    () => ({
        mass: 1,
        material: {
            friction: 0.1, // 摩擦係数,大きいほど摩擦が強い
            restitution: 0.3, // 反発係数
        },
        ...以下略
//
const [ref] = usePlane(
    () => ({
        material: {
            friction: 0.2,
            restitution: 0.2, // 反発係数
        },
        ...以下略
サイコロがうまく画面内に収まっていい感じに転がっている図

うまく実装できました٩(๑>∀<๑)۶

終わりに

今回はサイコロにクリックイベントを付与して転がすところの実装を解説しました。ただ、現状の実装だと連続でクリックできちゃうので、その辺も制限かけないとダメですね。また、最近は@react-three/rapierに注目が集まっていて、Rust製で軽いだとか聞くので、またこのサイコロも@react-three/rapierで書き換えたいです。


Previous Post
サイコロを作りたい(3)ダークモード対応
Next Post
サイコロを作りたい(1)スポットライト