Open33

物理エンジン Rapier を試してみる

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

経緯

同じく物理エンジンである Cannon.js を TypeScript + React で使えるようにした @react-three/cannon をこれまで使っていた。

https://www.npmjs.com/package/@react-three/cannon

Three.js を React で使えるようにした React Three Fiber の GitHub ページを見ていたら Ecosystem のセクションに @react-three/rapier を見つけた。

https://github.com/pmndrs/react-three-fiber#ecosystem

https://www.npmjs.com/package/@react-three/rapier

@react-three/cannon と @react-three/rapier のどっちが使いやすかが気になったのでこのスクラップを作成した次第。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

プロジェクト作成

コマンド
npx create-next-app \
  --typescript \
  --tailwind \
  --eslint \
  --app \
  --src-dir \
  --import-alias "@/*" \
  --use-npm \
  hello-rapier
cd hello-rapier
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

不要コードの削除

src/app/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
src/app/layout.tsx
import "./globals.css";
import type { Metadata } from "next";
import { Inter } from "next/font/google";

export const metadata: Metadata = {
  title: "Hello Rapier",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

はじめの一歩

src/app/page.tsx
"use client";

import { Canvas } from "@react-three/fiber";
import { Physics } from "@react-three/rapier";

export default function Home() {
  return (
    <main className="container mx-auto">
      <h1 className="mt-4 mb-4 text-2xl">Hello Rapier</h1>
      <Canvas className="aspect-square">
        <Physics></Physics>
      </Canvas>
    </main>
  );
}

まだ動かない。

Physics は Suspense で囲む必要があるようだが敢えてこのままにしておく。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

drei インストール

先ほど忘れてしまったが @react-three/drei をインストールしておこう。

コマンド
npm install @react-three/drei

@react-three/fiber をインストールするときはセットでインストールしておいた方が良さげ。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

トーラスノットの描画

src/app/page.tsx
"use client";

import { Environment, TorusKnot } from "@react-three/drei";
import { Canvas } from "@react-three/fiber";
import { Physics } from "@react-three/rapier";

export default function Home() {
  return (
    <main className="container mx-auto">
      <h1 className="mt-4 mb-4 text-2xl">Hello Rapier</h1>
      <Canvas className="aspect-video">
        <Environment preset="studio"></Environment>
        <TorusKnot>
          <meshStandardMaterial color="#ccc"></meshStandardMaterial>
        </TorusKnot>
        <Physics></Physics>
      </Canvas>
    </main>
  );
}


Environment なんて便利なものを初めて知った

https://github.com/pmndrs/drei#environment

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

RigidBody 導入

Three.js のメッシュを RigidBody(剛体)で囲むことで物理シミュレーションの対象にできるようだ。


トーラスノットが自由落下した後なので何も写っていないように見える

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

床を作る

src/app/page.tsx
"use client";

import { Environment, TorusKnot } from "@react-three/drei";
import { Canvas } from "@react-three/fiber";
import { CuboidCollider, Physics, RigidBody } from "@react-three/rapier";

export default function Home() {
  return (
    <main className="container mx-auto">
      <h1 className="mt-4 mb-4 text-2xl">Hello Rapier</h1>
      <Canvas className="aspect-video">
        <Environment preset="studio"></Environment>
        <Physics>
          <RigidBody position={[-3, 2, 0]}>
            <TorusKnot scale={0.5}>
              <meshStandardMaterial color="#ccc"></meshStandardMaterial>
            </TorusKnot>
          </RigidBody>
          {/* 床 */}
          <CuboidCollider
            position={[0, -2.5, 0]}
            args={[10, 1, 10]}
          ></CuboidCollider>
        </Physics>
      </Canvas>
    </main>
  );
}


落下して床で止まる、バウンドはしない

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Hull コライダー

src/app/page.tsx
"use client";

import { Environment, TorusKnot } from "@react-three/drei";
import { Canvas } from "@react-three/fiber";
import { CuboidCollider, Physics, RigidBody } from "@react-three/rapier";

export default function Home() {
  return (
    <main className="container mx-auto">
      <h1 className="mt-4 mb-4 text-2xl">Hello Rapier</h1>
      <Canvas className="aspect-video">
        <Environment preset="studio"></Environment>
        <Physics debug>
          {/* Hull コライダー */}
          <RigidBody colliders="hull" position={[-3, 2, 0]}>
            <TorusKnot scale={0.5}>
              <meshStandardMaterial color="#ccc"></meshStandardMaterial>
            </TorusKnot>
          </RigidBody>
          {/* 床 */}
          <CuboidCollider
            position={[0, -2.5, 0]}
            args={[10, 1, 10]}
          ></CuboidCollider>
        </Physics>
      </Canvas>
    </main>
  );
}


良い感じに包まれている

hull とは convex hull のことで凸包(とつほう)のことらしい。

https://ja.wikipedia.org/wiki/凸包

今さらだけど OrbitControls が無いと不便だな。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Ball コライダー

src/app/page.tsx
"use client";

import { Environment, OrbitControls, TorusKnot } from "@react-three/drei";
import { Canvas } from "@react-three/fiber";
import { CuboidCollider, Physics, RigidBody } from "@react-three/rapier";

export default function Home() {
  return (
    <main className="container mx-auto">
      <h1 className="mt-4 mb-4 text-2xl">Hello Rapier</h1>
      <Canvas className="aspect-video">
        <Environment preset="studio"></Environment>
        <Physics debug>
          {/* Hull コライダー */}
          <RigidBody colliders="hull" position={[-3, 2, 0]}>
            <TorusKnot scale={0.5}>
              <meshStandardMaterial color="#ccc"></meshStandardMaterial>
            </TorusKnot>
          </RigidBody>

          {/* Ball コライダー */}
          <RigidBody colliders="ball" position={[-1, 2, 0]}>
            <TorusKnot scale={0.5}>
              <meshStandardMaterial color="#ccc"></meshStandardMaterial>
            </TorusKnot>
          </RigidBody>

          {/* 床 */}
          <CuboidCollider
            position={[0, -2.5, 0]}
            args={[10, 1, 10]}
          ></CuboidCollider>

          <OrbitControls></OrbitControls>
        </Physics>
      </Canvas>
    </main>
  );
}


Ball はかなり直感的だ

ついでに OrbitControls を追加した。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Cuboid コライダー

src/app/page.tsx
"use client";

import { Environment, OrbitControls, TorusKnot } from "@react-three/drei";
import { Canvas } from "@react-three/fiber";
import { CuboidCollider, Physics, RigidBody } from "@react-three/rapier";

export default function Home() {
  return (
    <main className="container mx-auto">
      <h1 className="mt-4 mb-4 text-2xl">Hello Rapier</h1>
      <Canvas className="aspect-video">
        <Environment preset="studio"></Environment>
        <Physics debug>
          {/* Hull コライダー */}
          <RigidBody colliders="hull" position={[-3, 2, 0]}>
            <TorusKnot scale={0.5}>
              <meshStandardMaterial color="#ccc"></meshStandardMaterial>
            </TorusKnot>
          </RigidBody>

          {/* Ball コライダー */}
          <RigidBody colliders="ball" position={[-1, 2, 0]}>
            <TorusKnot scale={0.5}>
              <meshStandardMaterial color="#ccc"></meshStandardMaterial>
            </TorusKnot>
          </RigidBody>

          {/* Cuboid コライダー */}
          <RigidBody colliders="cuboid" position={[1, 2, 0]}>
            <TorusKnot scale={0.5}>
              <meshStandardMaterial color="#ccc"></meshStandardMaterial>
            </TorusKnot>
          </RigidBody>

          {/* 床 */}
          <CuboidCollider
            position={[0, -2.5, 0]}
            args={[10, 1, 10]}
          ></CuboidCollider>

          <OrbitControls></OrbitControls>
        </Physics>
      </Canvas>
    </main>
  );
}


Cuboid もわかりやすい

cuboid とは直方体のことらしい。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Trimesh コライダー

src/app/page.tsx
"use client";

import { Environment, OrbitControls, TorusKnot } from "@react-three/drei";
import { Canvas } from "@react-three/fiber";
import { CuboidCollider, Physics, RigidBody } from "@react-three/rapier";

export default function Home() {
  return (
    <main className="container mx-auto">
      <h1 className="mt-4 mb-4 text-2xl">Hello Rapier</h1>
      <Canvas className="aspect-video">
        <Environment preset="studio"></Environment>
        <Physics debug>
          {/* Hull コライダー */}
          <RigidBody colliders="hull" position={[-3, 2, 0]}>
            <TorusKnot scale={0.5}>
              <meshStandardMaterial color="#ccc"></meshStandardMaterial>
            </TorusKnot>
          </RigidBody>

          {/* Ball コライダー */}
          <RigidBody colliders="ball" position={[-1, 2, 0]}>
            <TorusKnot scale={0.5}>
              <meshStandardMaterial color="#ccc"></meshStandardMaterial>
            </TorusKnot>
          </RigidBody>

          {/* Cuboid コライダー */}
          <RigidBody colliders="cuboid" position={[1, 2, 0]}>
            <TorusKnot scale={0.5}>
              <meshStandardMaterial color="#ccc"></meshStandardMaterial>
            </TorusKnot>
          </RigidBody>

          {/* Trimesh コライダー */}
          <RigidBody colliders="trimesh" position={[3, 2, 0]}>
            <TorusKnot scale={0.5}>
              <meshStandardMaterial color="#ccc"></meshStandardMaterial>
            </TorusKnot>
          </RigidBody>

          {/* 床 */}
          <CuboidCollider
            position={[0, -2.5, 0]}
            args={[10, 1, 10]}
          ></CuboidCollider>

          <OrbitControls></OrbitControls>
        </Physics>
      </Canvas>
    </main>
  );
}


Trimesh コライダーはなんというかパーフェクトな感じがする

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

ContactShadows 追加

https://github.com/pmndrs/drei#contactshadows

src/app/page.tsx
"use client";

import {
  ContactShadows,
  Environment,
  OrbitControls,
  TorusKnot,
} from "@react-three/drei";
import { Canvas } from "@react-three/fiber";
import { CuboidCollider, Physics, RigidBody } from "@react-three/rapier";

export default function Home() {
  return (
    <main className="container mx-auto">
      <h1 className="mt-4 mb-4 text-2xl">Hello Rapier</h1>
      <Canvas className="aspect-video">
        <Environment preset="studio"></Environment>
        <Physics debug>
          {/* Hull コライダー */}
          <RigidBody colliders="hull" position={[-3, 2, 0]}>
            <TorusKnot scale={0.5}>
              <meshStandardMaterial color="#ccc"></meshStandardMaterial>
            </TorusKnot>
          </RigidBody>

          {/* Ball コライダー */}
          <RigidBody colliders="ball" position={[-1, 2, 0]}>
            <TorusKnot scale={0.5}>
              <meshStandardMaterial color="#ccc"></meshStandardMaterial>
            </TorusKnot>
          </RigidBody>

          {/* Cuboid コライダー */}
          <RigidBody colliders="cuboid" position={[1, 2, 0]}>
            <TorusKnot scale={0.5}>
              <meshStandardMaterial color="#ccc"></meshStandardMaterial>
            </TorusKnot>
          </RigidBody>

          {/* Trimesh コライダー */}
          <RigidBody colliders="trimesh" position={[3, 2, 0]}>
            <TorusKnot scale={0.5}>
              <meshStandardMaterial color="#ccc"></meshStandardMaterial>
            </TorusKnot>
          </RigidBody>

          {/* 床 */}
          <CuboidCollider
            position={[0, -2.5, 0]}
            args={[10, 1, 10]}
          ></CuboidCollider>

          <ContactShadows
            scale={20}
            blur={0.4}
            opacity={0.2}
            position={[-0, -1.5, 0]}
          ></ContactShadows>

          <OrbitControls></OrbitControls>
        </Physics>
      </Canvas>
    </main>
  );
}


はじめて使ったけどこんなに簡単に影を作れるのがすごい

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

トーラス

演習としてトーラス(ドーナッツ)を Trimesh コライダーで作ってみる。

src/app/page.tsx
"use client";

import {
  ContactShadows,
  Environment,
  OrbitControls,
  Torus,
  TorusKnot,
} from "@react-three/drei";
import { Canvas } from "@react-three/fiber";
import { CuboidCollider, Physics, RigidBody } from "@react-three/rapier";

export default function Home() {
  return (
    <main className="container mx-auto">
      <h1 className="mt-4 mb-4 text-2xl">Hello Rapier</h1>
      <Canvas className="aspect-video" shadows>
        <Environment preset="studio"></Environment>
        <Physics debug>
          {/* トーラス */}
          <RigidBody
            colliders="trimesh"
            position={[0, 2, 0]}
            rotation={[Math.PI / 2, 0, 0]}
          >
            <Torus scale={0.5}>
              <meshStandardMaterial color="#ccc"></meshStandardMaterial>
            </Torus>
          </RigidBody>

          {/* 床 */}
          <CuboidCollider
            position={[0, -2.5, 0]}
            args={[10, 1, 10]}
          ></CuboidCollider>

          <ContactShadows
            scale={20}
            blur={0.4}
            opacity={0.2}
            position={[-0, -1.5, 0]}
          ></ContactShadows>

          <OrbitControls></OrbitControls>
        </Physics>
      </Canvas>
    </main>
  );
}


良い感じにドーナッツが出てきた

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

自動コライダー機能を無効にする

コード例
const Scene = () => (
  <Physics colliders={false}>
    {/* Use an automatic CuboidCollider for all meshes inside this RigidBody */}
    <RigidBody colliders="cuboid">
      <Box />
    </RigidBody>

    {/* Use an automatic BallCollider for all meshes inside this RigidBody */}
    <RigidBody position={[0, 10, 0]} colliders="ball">
      <Sphere />
    </RigidBody>
  </Physics>
);
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

8/10 (木) はここから

ちょっと日が空いてしまった、8 日ぶりの更新となる。

今日は 1 時間くらいまた「次のサンプル」を試してみよう。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

コーディング

src/app/compound-colliders/page.tsx
"use client";

import {
  ContactShadows,
  Environment,
  OrbitControls,
  TorusKnot,
} from "@react-three/drei";
import { Canvas } from "@react-three/fiber";
import {
  BallCollider,
  ConeCollider,
  CuboidCollider,
  Physics,
  RigidBody,
  RoundCuboidCollider,
} from "@react-three/rapier";

export default function CompoundColliders() {
  return (
    <main className="container mx-auto">
      <h1 className="text-2xl mt-4 mb-4">Compound Colliders</h1>
      <Canvas className="aspect-video">
        <Environment preset="studio"> </Environment>
        <ContactShadows
          scale={20}
          blur={0.4}
          opacity={0.2}
          position={[0, -1.5, 0]}
        ></ContactShadows>
        <OrbitControls></OrbitControls>
        <Physics debug>
          <RigidBody colliders="hull" position={[0, 2, -2]}>
            <TorusKnot>
              <meshPhysicalMaterial></meshPhysicalMaterial>
            </TorusKnot>
            <ConeCollider args={[1, 1]} position={[1, 1, 1]}></ConeCollider>
            <RoundCuboidCollider
              args={[1, 1, 1, 0.1]}
              position={[-1, -1, -1]}
            ></RoundCuboidCollider>
            <BallCollider args={[1]} position={[0, 1, 2]}></BallCollider>
          </RigidBody>

          {/* 床 */}
          <CuboidCollider
            position={[0, -2.5, 0]}
            args={[10, 1, 10]}
          ></CuboidCollider>
        </Physics>
      </Canvas>
    </main>
  );
}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

コーディングはできたけど

何も起こらない。

正確にいうと最初の一瞬だけ何かが起きてそれから何も起こらない。

src/app/instanced-meshes/page.tsx
"use client";

import {
  ContactShadows,
  Environment,
  OrbitControls,
  TorusKnot,
} from "@react-three/drei";
import { Canvas } from "@react-three/fiber";
import {
  BallCollider,
  ConeCollider,
  CuboidCollider,
  InstancedRigidBodies,
  InstancedRigidBodyProps,
  Physics,
  RapierRigidBody,
  RigidBody,
  RoundCuboidCollider,
} from "@react-three/rapier";
import { count } from "console";
import { useEffect, useMemo, useRef } from "react";

export default function InstancedMeshes() {
  const COUNT = 101;

  const rigidBodies = useRef<RapierRigidBody[]>(null);

  useEffect(() => {
    if (!rigidBodies.current) {
      return;
    }

    rigidBodies.current[40].applyImpulse({ x: 0, y: 10, z: 0 }, true);
    const rigidBody100 = rigidBodies.current.at(100);

    if (rigidBody100) {
      rigidBody100.applyImpulse({ x: 0, y: 10, z: 0 }, true);
    }

    rigidBodies.current.forEach((api) => {
      api.applyImpulse({ x: 0, y: 10, z: 0 }, true);
    });
  });

  const instances = useMemo(() => {
    const instances: InstancedRigidBodyProps[] = [];

    for (let i = 0; i < COUNT; i++) {
      instances.push({
        key: "instance_" + Math.random(),
        position: [Math.random() * 10, Math.random() * 10, Math.random() * 10],
        rotation: [Math.random(), Math.random(), Math.random()],
      });
    }

    return instances;
  }, []);

  return (
    <main className="container mx-auto">
      <h1 className="text-2xl mt-4 mb-4">Instanced Meshes</h1>
      <Canvas className="aspect-video">
        <Environment preset="studio"> </Environment>
        <ContactShadows
          scale={20}
          blur={0.4}
          opacity={0.2}
          position={[0, -1.5, 0]}
        ></ContactShadows>
        <OrbitControls></OrbitControls>
        <Physics debug>
          <InstancedRigidBodies
            ref={rigidBodies}
            instances={instances}
            colliders="ball"
          >
            <instancedMesh
              args={[undefined, undefined, COUNT]}
              count={COUNT}
            ></instancedMesh>
          </InstancedRigidBodies>
        </Physics>
      </Canvas>
    </main>
  );
}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

boxGeometry を中に入れたら表示された。

src/app/instanced-meshes/page.tsx(抜粋)
            <instancedMesh args={[undefined, undefined, COUNT]} count={COUNT}>
              <boxGeometry args={[0.5, 0.5, 0.5]}></boxGeometry>
            </instancedMesh>