物理エンジン Rapier を試してみる
このスクラップについて
この記事では Rust 製の物理エンジンである Rapier を試してみる。
日本語での読み方
Rapier は「レイピア」と読めば良いのだろうか?
経緯
同じく物理エンジンである Cannon.js を TypeScript + React で使えるようにした @react-three/cannon をこれまで使っていた。
Three.js を React で使えるようにした React Three Fiber の GitHub ページを見ていたら Ecosystem のセクションに @react-three/rapier を見つけた。
@react-three/cannon と @react-three/rapier のどっちが使いやすかが気になったのでこのスクラップを作成した次第。
Rust だけど
WebAssembly のおかげで JavaScript / TypeScript で使えるようになっている。
プロジェクト作成
npx create-next-app \
--typescript \
--tailwind \
--eslint \
--app \
--src-dir \
--import-alias "@/*" \
--use-npm \
hello-rapier
cd hello-rapier
npm パッケージのインストール
npm install @react-three/fiber @react-three/rapier
不要コードの削除
@tailwind base;
@tailwind components;
@tailwind utilities;
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>
);
}
はじめの一歩
"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 で囲む必要があるようだが敢えてこのままにしておく。
drei インストール
先ほど忘れてしまったが @react-three/drei をインストールしておこう。
npm install @react-three/drei
@react-three/fiber をインストールするときはセットでインストールしておいた方が良さげ。
トーラスノットの描画
"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 なんて便利なものを初めて知った
RigidBody 導入
Three.js のメッシュを RigidBody(剛体)で囲むことで物理シミュレーションの対象にできるようだ。
トーラスノットが自由落下した後なので何も写っていないように見える
床を作る
"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>
);
}
落下して床で止まる、バウンドはしない
デバッグ
Physics コンポーネントに debug タグをつけると補助線が表示されて便利。
7/31 (月) はここまで
次回も引き続きサンプルを勉強していこう。
ここまで 1 時間くらい。
8/2 (水) はここから
今日は引き続きサンプルを勉強していこう。
Hull コライダー
"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 のことで凸包(とつほう)のことらしい。
今さらだけど OrbitControls が無いと不便だな。
Ball コライダー
"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 を追加した。
Cuboid コライダー
"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 とは直方体のことらしい。
Trimesh コライダー
"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 コライダーはなんというかパーフェクトな感じがする
ContactShadows 追加
"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>
);
}
はじめて使ったけどこんなに簡単に影を作れるのがすごい
トーラス
演習としてトーラス(ドーナッツ)を Trimesh コライダーで作ってみる。
"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>
);
}
良い感じにドーナッツが出てきた
自動コライダー機能を無効にする
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>
);
次のサンプル
こちらを試してみよう。
8/10 (木) はここから
ちょっと日が空いてしまった、8 日ぶりの更新となる。
今日は 1 時間くらいまた「次のサンプル」を試してみよう。
ファイル作成
mkdir src/app/compound-colliders
touch 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>
);
}
スクーリンショット
直方体・円錐・球などがくっついている
Instanced Meshes を試してみる
ファイル作成
mkdir src/app/instanced-meshes
touch 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>
);
}
そもそも Instanced Mesh とは
同じ形だが変換行列が異なるオブジェクトを大量に描画する時に利用できるようだ。
boxGeometry を中に入れたら表示された。
<instancedMesh args={[undefined, undefined, COUNT]} count={COUNT}>
<boxGeometry args={[0.5, 0.5, 0.5]}></boxGeometry>
</instancedMesh>
8/10 (木) はここまで
次はフォースをかけてみよう。