Next.js / React で React Three Cannon を使って物理演算を行う
このスクラップについて
このスクラップでは React Three Cannon を使って Web ページ上で基本的な物理演算を行い、演算結果を React Three Fiber を使って 3D 表示するまでの過程を記録していく。
npm パッケージと GitHub リポジトリ
React Three Cannon
React Three Fiber
ワークスペースの作成
前のロボットアームから初めても良いがごちゃごちゃになりそうなので新たにワークスペースを作る。
npx create-next-app \
--typescript \
--tailwind \
--eslint \
--app \
--src-dir \
--import-alias "@/*" \
--use-npm \
hello-physics
npm パッケージのインストール
npm install three @types/three @react-three/fiber @react-three/drei @react-three/cannon
箱の表示
@tailwind base;
@tailwind components;
@tailwind utilities;
import "./globals.css";
export const metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
"use client";
import { Box, OrbitControls, PerspectiveCamera } from "@react-three/drei";
import { Canvas } from "@react-three/fiber";
import Image from "next/image";
export default function Home() {
const defaultMaterial = <meshPhongMaterial></meshPhongMaterial>;
return (
<main className="container mx-auto">
<h1 className="mt-4 mb-4 text-4xl">Hello Physics</h1>
<Canvas style={{ width: "50%", height: "50vh" }}>
<OrbitControls></OrbitControls>
<PerspectiveCamera
makeDefault
position={[0, 0, 10]}
></PerspectiveCamera>
<ambientLight intensity={0.1}></ambientLight>
<pointLight position={[10, 10, 10]}></pointLight>
<Box position={[-1.2, 0, 0]}>{defaultMaterial}</Box>
<Box position={[1.2, 0, 0]}>{defaultMaterial}</Box>
</Canvas>
</main>
);
}
5/24 (水) はここまで
なんか画像を表示するだけで終わってしまった。
ここまでの作業時間は 1 時間。
生の Three.js と cannon-es を使った方が良いのではというもやもやを抱えながら作業している。
もしかしたらこのままクローズするかも知れない。
せめてあと 30 分くらいやって Getting Started くらいは終わらしてからクローズしよう。
クローズするのはもったいないかも
Fiber + cannon-es を組み合わせるというやり方も面白そうなのでその方向性で続けてみようかな。
5/25 (木) はここから
今日は Fiber + cannon-es で箱を空中から地面に落とすまでを目標にしたい。
React Three Cannon が合わなかった理由
Getting Started でコード自体は動作するが型エラーが出たのでこれでは先が思いやられると感じた。
function World() {
const defaultMaterial = <meshPhongMaterial></meshPhongMaterial>;
const [ref, api] = useBox(() => ({ mass: 1 }));
return (
<>
<Box ref={ref} position={[-1.2, 0, 0]}>
{defaultMaterial}
</Box>
<Box position={[1.2, 0, 0]}>{defaultMaterial}</Box>
</>
);
}
エラーメッセージ
Type 'RefObject<Object3D<Event>>' is not assignable to type 'Ref<Mesh<BufferGeometry<NormalBufferAttributes>, Material | Material[]>> | undefined'.
Type 'RefObject<Object3D<Event>>' is not assignable to type 'RefObject<Mesh<BufferGeometry<NormalBufferAttributes>, Material | Material[]>>'.
Type 'Object3D<Event>' is missing the following properties from type 'Mesh<BufferGeometry<NormalBufferAttributes>, Material | Material[]>': isMesh, geometry, material, updateMorphTargets, getVertexPosition
パッケージの更新
npm uninstall @react-three/cannon
npm install cannon-es
5/25 (木) はここまで
無為に 1 時間が流れた気がする、昨日からの累計では 2 時間か。
とりあえず useRef() を使った方が良いかも知れないと感じた。
やっぱり cannon-es を使うのは大変かも知れない
Three.js + cannon-es か React Three Fiber + React Three Cannon の組み合わせでどちらか統一した方が良いかも知れない。
5/25 (木) あと少しだけ
隙間時間ができたので 30 分だけやろう。
公式 Examples では型エラーは出なかった
何が違うんだろう?
おかえり@react-three/cannon
npm install @react-three/cannon
ソースコードの復帰
"use client";
import { Physics, useBox } from "@react-three/cannon";
import { Box, OrbitControls, PerspectiveCamera } from "@react-three/drei";
import { Canvas } from "@react-three/fiber";
export default function Home() {
return (
<main className="container mx-auto">
<h1 className="mt-4 mb-4 text-4xl">Hello Physics</h1>
<Canvas style={{ width: "50%", height: "50vh" }}>
<OrbitControls></OrbitControls>
<PerspectiveCamera
makeDefault
position={[0, 0, 10]}
></PerspectiveCamera>
<ambientLight intensity={0.1}></ambientLight>
<pointLight position={[10, 10, 10]}></pointLight>
<Physics>
<World></World>
</Physics>
</Canvas>
</main>
);
}
function World() {
const defaultMaterial = <meshPhongMaterial></meshPhongMaterial>;
const [ref, api] = useBox(() => ({ mass: 1 }));
return (
<>
<Box ref={ref} position={[-1.2, 0, 0]}>
{defaultMaterial}
</Box>
<Box position={[1.2, 0, 0]}>{defaultMaterial}</Box>
</>
);
}
エラーメッセージ
Type 'RefObject<Object3D<Event>>' is not assignable to type 'Ref<Mesh<BufferGeometry<NormalBufferAttributes>, Material | Material[]>> | undefined'.
Type 'RefObject<Object3D<Event>>' is not assignable to type 'RefObject<Mesh<BufferGeometry<NormalBufferAttributes>, Material | Material[]>>'.
Type 'Object3D<Event>' is missing the following properties from type 'Mesh<BufferGeometry<NormalBufferAttributes>, Material | Material[]>': isMesh, geometry, material, updateMorphTargets, getVertexPosition
tsconfig.json の比較
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
{
"compilerOptions": {
"allowJs": true,
"allowSyntheticDefaultImports": true,
"baseUrl": "./src",
"declaration": true,
"emitDeclarationOnly": true,
"esModuleInterop": true,
"incremental": true,
"jsx": "react-jsx",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"module": "esnext",
"moduleResolution": "node",
"noImplicitThis": false,
"outDir": "dist",
"pretty": true,
"removeComments": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "esnext",
"types": ["vite/client"]
},
"exclude": ["node_modules"],
"include": ["./src", "vite.config.ts"]
}
エラーメッセージが消えた!
function World() {
const defaultMaterial = <meshPhongMaterial></meshPhongMaterial>;
const [ref, api] = useBox(
() => ({ mass: 1, position: [-1.2, 0, 0] }),
useRef<Mesh>(null) // ここがポイントです。
);
return (
<>
<Box ref={ref}>{defaultMaterial}</Box>
<Box position={[1.2, 0, 0]}>{defaultMaterial}</Box>
</>
);
}
下記が参考になった。
5/25 (木) おかわりはここまで
追加で 30 分くらい続けたので累計では 2.5 時間になった。
5/25 (木) おかわり 2 回目
"use client";
import { Physics, useBox, usePlane } from "@react-three/cannon";
import {
Box,
OrbitControls,
PerspectiveCamera,
Plane,
} from "@react-three/drei";
import { Canvas } from "@react-three/fiber";
import { useRef } from "react";
import { Mesh } from "three";
export default function Home() {
return (
<main className="container mx-auto">
<h1 className="mt-4 mb-4 text-4xl">Hello Physics</h1>
<Canvas style={{ width: "50%", height: "50vh" }}>
<OrbitControls></OrbitControls>
<PerspectiveCamera
makeDefault
position={[0, 10, 10]}
></PerspectiveCamera>
<ambientLight intensity={0.1}></ambientLight>
<pointLight position={[10, 10, 10]}></pointLight>
<Physics>
<World></World>
</Physics>
</Canvas>
</main>
);
}
function World() {
const defaultMaterial = <meshPhongMaterial></meshPhongMaterial>;
const [boxRef] = useBox(
() => ({ args: [1, 1, 1], mass: 1, position: [0, 5, 0] }),
useRef<Mesh>(null)
);
const planeSize = 10;
const planeThickness = 0.01;
const [planeRef, api] = useBox(
() => ({
args: [planeSize, planeThickness, planeSize],
position: [0, -5, 0],
type: "Static",
}),
useRef<Mesh>(null)
);
return (
<>
<Box args={[1, 1, 1]} ref={boxRef}>
{defaultMaterial}
</Box>
<Box args={[planeSize, planeThickness, planeSize]} ref={planeRef}>
{defaultMaterial}
</Box>
</>
);
}
ようやく箱が地面に落下していった
ref を使っても位置と回転しか反映されないので Scale は手動で合わせる必要がある。
30 分くらい使ったので今日だけで 2 時間、合計で 3 時間か。
明日以降
公式 Examples の写経をしていこう。
5/26 (金) はここから
Monday Morning の公式 Example を写経する。
はじめの一歩
"use client";
import { Physics } from "@react-three/cannon";
import { Canvas } from "@react-three/fiber";
export default function Home() {
return (
<main className="container mx-auto">
<h1 className="mt-4 mb-4 text-4xl">Hello Physics</h1>
<Canvas
camera={{ far: 100, near: 1, position: [-25, 20, 25], zoom: 25 }}
orthographic
shadows
style={{ cursor: "none", width: "70%", height: "70vh" }}
>
<color attach="background" args={["#171720"]}></color>
<fog attach="fog" args={["#171720", 20, 70]}></fog>
<ambientLight intensity={0.2}></ambientLight>
<pointLight
position={[-10, -10, -10]}
color="red"
intensity={1.5}
></pointLight>
<Physics
iterations={15}
gravity={[0, -200, 0]}
allowSleep={false}
></Physics>
</Canvas>
</main>
);
}
今のところ黒い画面が表示されるだけ
はじめに Ragdoll に手を出さない方が良い
// function Ragdoll() {
// const mouth = useRef<Mesh>(null);
// const eyes = useRef<Group>(null);
// useFrame(({ clock }) => {
// if (!eyes.current || !mouth.current) {
// return;
// }
// eyes.current.position.y = Math.sin(clock.getElapsedTime() * 1) * 0.06;
// mouth.current.scale.y = (1 + Math.sin(clock.getElapsedTime())) * 1.5;
// });
// return <></>;
// }
// type BoxProps = Omit<MeshProps, "args"> &
// Pick<BoxBufferGeometryProps, "args"> &
// Pick<MeshStandardMaterialProps, "color" | "opacity" | "transparent">;
// type BodyPartProps = BoxProps & {
// config?: ConeTwistConstraintOpts;
// name: string;
// render?: ReactNode;
// };
// function BodyPart({
// children,
// config = {},
// name,
// render,
// ...props
// }: BodyPartProps): JSX.Element {
// return <></>;
// }
Plane の追加
function Plane(props: PlaneProps) {
const [ref] = usePlane(() => ({ ...props }), useRef<Mesh>(null));
return (
<mesh ref={ref} receiveShadow>
<planeBufferGeometry args={[1000, 1000]}></planeBufferGeometry>
<meshStandardMaterial color="#171720"></meshStandardMaterial>
</mesh>
);
}
次に <Physics>...<Physics>
に下記を追加する。
<Plane position={[0, -5, 0]} rotation={[-Math.PI / 2, 0, 0]}></Plane>
color を変えるなどすると見えるようになる
椅子の背もたれができた
type BoxProps = Omit<MeshProps, "args"> &
Pick<BoxBufferGeometryProps, "args"> &
Pick<MeshStandardMaterialProps, "color" | "opacity" | "transparent">;
// eslint-disable-next-line react/display-name
const Box = forwardRef<Mesh, BoxProps>(
(
{
args = [1, 1, 1],
children,
color = "white",
opacity = 1,
transparent = false,
...props
},
ref
) => {
return (
<mesh castShadow receiveShadow ref={ref} {...props}>
<boxBufferGeometry args={args}></boxBufferGeometry>
<meshStandardMaterial
color={color}
opacity={opacity}
transparent={transparent}
></meshStandardMaterial>
</mesh>
);
}
);
function Chair() {
const [ref] = useCompoundBody(
() => ({
mass: 1,
position: [-6, 0, 0],
shapes: [
{ args: [1.5, 1.5, 0.25], mass: 1, position: [0, 0, 0], type: "Box" },
{
args: [1.5, 0.25, 1.5],
mass: 1,
position: [0, -1.75, 1.25],
type: "Box",
},
],
type: "Dynamic",
}),
useRef<Group>(null)
);
// const bind = useDragConstraint(ref);
return (
<group ref={ref}>
<Box position={[0, 0, 0]} scale={[3, 3, 0.5]}></Box>
</group>
);
}
背もたれが落下した後の様子
5/26 (金) はここまで
TypeScript の面でもめちゃくちゃ学ぶことが多い。
使いこなせたら Web 3D アプリが作れそうで夢がある。
途中で挫折しなくて良かった。
今日の学習時間は 1 時間なので累計では 4 時間。
5/26 (金) おかわり
function Chair() {
const [ref] = useCompoundBody(
() => ({
mass: 1,
position: [-6, 0, 0],
shapes: [
{ args: [1.5, 1.5, 0.25], mass: 1, position: [0, 0, 0], type: "Box" },
{
args: [1.5, 0.25, 1.5],
mass: 1,
position: [0, -1.75, 1.25],
type: "Box",
},
{
args: [0.25, 1.5, 0.25],
mass: 10,
position: [5 + -6.25, -3.5, 0],
type: "Box",
},
{
args: [0.25, 1.5, 0.25],
mass: 10,
position: [5 + -3.75, -3.5, 0],
type: "Box",
},
{
args: [0.25, 1.5, 0.25],
mass: 10,
position: [5 + -6.25, -3.5, 2.5],
type: "Box",
},
{
args: [0.25, 1.5, 0.25],
mass: 10,
position: [5 + -3.75, -3.5, 2.5],
type: "Box",
},
],
type: "Dynamic",
}),
useRef<Group>(null)
);
// const bind = useDragConstraint(ref);
return (
<group ref={ref}>
<Box position={[0, 0, 0]} scale={[3, 3, 0.5]}></Box>
<Box position={[0, -1.75, 1.25]} scale={[3, 0.5, 3]}></Box>
<Box position={[5 + -6.25, -3.5, 0]} scale={[0.5, 3, 0.5]}></Box>
<Box position={[5 + -3.75, -3.5, 0]} scale={[0.5, 3, 0.5]}></Box>
<Box position={[5 + -6.25, -3.5, 2.5]} scale={[0.5, 3, 0.5]}></Box>
<Box position={[5 + -3.75, -3.5, 2.5]} scale={[0.5, 3, 0.5]}></Box>
</group>
);
}
椅子が完成した
テーブルの追加
function Table() {
const [seat] = useBox(
() => ({
args: [2.5, 0.25, 2.5],
position: [9, -0.8, 0],
type: "Static",
}),
useRef<Mesh>(null)
);
const [leg1] = useBox(
() => ({
args: [0.25, 2, 0.25],
position: [7.2, -3, 1.8],
type: "Static",
}),
useRef<Mesh>(null)
);
const [leg2] = useBox(
() => ({
args: [0.25, 2, 0.25],
position: [10.8, -3, 1.8],
type: "Static",
}),
useRef<Mesh>(null)
);
const [leg3] = useBox(
() => ({
args: [0.25, 2, 0.25],
position: [7.2, -3, -1.8],
type: "Static",
}),
useRef<Mesh>(null)
);
const [leg4] = useBox(
() => ({
args: [0.25, 2, 0.25],
position: [10.8, -3, -1.8],
type: "Static",
}),
useRef<Mesh>(null)
);
return (
<>
<Box scale={[5, 0.5, 5]} ref={seat}></Box>
<Box scale={[0.5, 4, 0.5]} ref={leg1}></Box>
<Box scale={[0.5, 4, 0.5]} ref={leg2}></Box>
<Box scale={[0.5, 4, 0.5]} ref={leg3}></Box>
<Box scale={[0.5, 4, 0.5]} ref={leg4}></Box>
</>
);
}
机が完成した
ランプの追加
const Lamp = () => {
const light = useRef<SpotLight>(null);
const [fixed] = useSphere(
() => ({
args: [1],
position: [0, 5, 0],
type: "Static",
}),
useRef<Mesh>(null)
);
const [lamp] = useBox(
() => ({
angularDamping: 0.99,
args: [1, 0, 5],
linearDamping: 0.9,
mass: 1,
position: [0, 16, 0],
}),
useRef<Mesh>(null)
);
usePointToPointConstraint(fixed, lamp, {
pivotA: [0, 0, 0],
pivotB: [0, 2, 0],
});
// const bind = useDragConstraint(lamp);
return (
<>
<mesh ref={lamp}>
<coneBufferGeometry
attach="geometry"
args={[2, 2.5, 32]}
></coneBufferGeometry>
<meshStandardMaterial attach="material"></meshStandardMaterial>
<pointLight intensity={10} distance={5}></pointLight>
<spotLight
ref={light}
position={[0, 20, 0]}
angle={0.4}
penumbra={1}
intensity={0.6}
castShadow
></spotLight>
</mesh>
</>
);
};
angularDamping を 1 未満に変更する必要がある
5/26 (金) おかわりはここまで
追加 1 時間なので累計では 5 時間。
5/29 (月) はここから
実際の開発に着手したいが学ぶことが多そうなので引き続き写経を続けていく。
カーソルの表示
const Cursor = () => {
const [ref, api] = useSphere(
() => ({
args: [0.5],
position: [0, 0, 10000],
type: "Static",
}),
cursor
);
useFrame(({ mouse, viewport: { height, width } }) => {
const x = mouse.x * width;
const y = (mouse.y * height) / 1.9 + -x / 3.5;
api.position.set(x / 1.4, y, 0);
});
return (
<mesh ref={ref}>
<sphereBufferGeometry args={[0.5, 32, 32]}></sphereBufferGeometry>
<meshBasicMaterial
fog={false}
depthTest={false}
transparent
opacity={0.5}
></meshBasicMaterial>
</mesh>
);
};
カーソルが表示された
制約の追加
function useDragConstraint(child: RefObject<Object3D>) {
const [, , api] = usePointToPointConstraint(cursor, child, {
pivotA: [0, 0, 0],
pivotB: [0, 0, 0],
});
useEffect(() => void api.disable(), []);
const onPointerDown = useCallback((e: ThreeEvent<PointerEvent>) => {
e.stopPropagation();
//@ts-expect-error Investigate proper types here.
e.target.setPointerCapture(e.pointerId);
api.enable();
}, []);
const onPointerUp = useCallback(() => api.disable(), []);
return { onPointerDown, onPointerUp };
}
ランプや椅子がドラッグ&ドロップできるようになった
React Error
VSCode で警告が表示されたので無効にした。
/* eslint-disable react-hooks/exhaustive-deps */
あとはマグカップとラグドール
ちょっと飽きてきたので HingeMotor をやってみよう。
6/1 (木) はここから
前回の「ここまで」を書き忘れたけど多分 1 時間稼働で累計 6 時間になったのではないかと思う。
今日は HingeMotor をやろうと思ったけどこの辺りで実務のトライアルをやってみて学びが足りなかったら再び写経をしようと思う。
新しいワークスペースの作成
# cd ~/workspace
npx create-next-app \
--typescript \
--tailwind \
--eslint \
--app \
--src-dir \
--import-alias "@/*" \
--use-npm \
robot-arm-physics
npm install three @types/three @react-three/fiber @react-three/drei @react-three/cannon
ずっと気になっていたこと
ambientLight とかどうしてインポートしているのに使えるんだろうと気になっていがこの辺りが関係しているらしい。
declare global {
namespace JSX {
interface IntrinsicElements extends ThreeElements {
}
}
}
ここまで 1 時間
Hello World レベルのサンプルを作るのに 1 時間もかかってしまった、累計では 7 時間。
"use client";
import { Physics, useBox, usePlane } from "@react-three/cannon";
import {
Box,
OrbitControls,
PerspectiveCamera,
Plane,
} from "@react-three/drei";
import { Canvas } from "@react-three/fiber";
import { FC, useRef } from "react";
import { Mesh } from "three";
export default function Home() {
return (
<main className="mx-auto container">
<h1 className="text-4xl mt-4 mb-4">Robot arm physics</h1>
<Canvas
style={{
width: "70%",
height: "70vh",
}}
>
<Physics>
<OrbitControls></OrbitControls>
<ambientLight intensity={0.2}></ambientLight>
<pointLight position={[5, 10, 5]} intensity={0.6}></pointLight>
<PerspectiveCamera
makeDefault
position={[5, 5, 5]}
></PerspectiveCamera>
<MyBox mass={1} position={[0, 0.5, 0]} scale={[1, 1, 1]}></MyBox>
<MyBox mass={1} position={[-0.5, 3, 0]} scale={[1, 1, 1]}></MyBox>
<MyPlane scale={[10, 10, 10]} position={[0, 0, 0]}></MyPlane>
</Physics>
</Canvas>
</main>
);
}
const MyBox: FC<{
mass: number;
position: [number, number, number];
scale: [number, number, number];
}> = ({ mass, position, scale }) => {
const [ref] = useBox(
() => ({
args: scale,
position,
mass,
}),
useRef<Mesh>(null)
);
return (
<Box ref={ref} scale={scale}>
<meshStandardMaterial></meshStandardMaterial>
</Box>
);
};
const MyPlane: FC<{
position: [number, number, number];
scale: [number, number, number];
}> = ({ position, scale }) => {
const [ref] = usePlane(
() => ({
rotation: [-Math.PI / 2, 0, 0],
args: scale,
}),
useRef<Mesh>(null)
);
return (
<Plane ref={ref} scale={scale}>
<meshStandardMaterial></meshStandardMaterial>
</Plane>
);
};
Three と Connon のスケールは同じで良かったっけ?
たった 2 日やらなかっただけで忘れまくっている。
Static と Kinematic の違いって何だ?
Static(訳:静的)
A static body does not move during simulation and behaves as if it has infinite mass. Static bodies can be moved manually by setting the position of the body. The velocity of a static body is always zero. Static bodies do not collide with other static or kinematic bodies.
Static な物体はシミュレーションの間は動かず、無限の質量を持っているように振る舞います。位置を設定することで手動で動かすことができます。速度は常にゼロです。Static なボディは他の Static / Kinematic なボディと接触しません。
Kinematic(訳:運動学的)
A kinematic body moves under simulation according to its velocity. They do not respond to forces. They can be moved manually, but normally a kinematic body is moved by setting its velocity. A kinematic body behaves as if it has infinite mass. Kinematic bodies do not collide with other static or kinematic bodies.
Kinematic なボディはシミュレーションの間に速度に従って動きます。力には反応しません。手動で動かすことができますが、通常は速度を設定することで動かします。無限の質量を持っているように振る舞います。Kinematic なボディは他の Static / Kinematic なボディと接触しません。
どちらを使えば良い?
位置をコントールしたい場合は Static が良さそう。
MyBox & MyPlane のリファクタリング
type MyBoxProps = BoxProps;
const MyBox: FC<MyBoxProps> = ({ args, ...rest }) => {
const [ref] = useBox(
() => ({
args,
...rest,
}),
useRef<Mesh>(null)
);
return (
<Box ref={ref} scale={args}>
<meshStandardMaterial></meshStandardMaterial>
</Box>
);
};
type MyPlaneProps = PlaneProps & Pick<MeshProps, "scale">;
const MyPlane: FC<MyPlaneProps> = ({ scale, ...rest }) => {
const [ref] = usePlane(
() => ({
...rest,
}),
useRef<Mesh>(null)
);
return (
<Plane ref={ref} scale={scale}>
<meshStandardMaterial></meshStandardMaterial>
</Plane>
);
};
Static で Dynamic を押す
"use client";
import {
BoxProps,
Physics,
PlaneProps,
Triplet,
useBox,
usePlane,
} from "@react-three/cannon";
import {
Box,
OrbitControls,
PerspectiveCamera,
Plane,
} from "@react-three/drei";
import { Canvas, MeshProps, useFrame } from "@react-three/fiber";
import { FC, useRef } from "react";
import { Mesh, Vector3 } from "three";
export default function Home() {
return (
<main className="mx-auto container">
<h1 className="text-4xl mt-4 mb-4">Robot arm physics</h1>
<Canvas
style={{
width: "70%",
height: "70vh",
}}
>
<Physics>
<OrbitControls></OrbitControls>
<ambientLight intensity={0.2}></ambientLight>
<pointLight position={[5, 10, 5]} intensity={0.6}></pointLight>
<PerspectiveCamera
makeDefault
position={[5, 5, 5]}
></PerspectiveCamera>
<MyStaticBox
position={[1, 0.5, 1]}
args={[1, 1, 1]}
type="Static"
></MyStaticBox>
<MyBox mass={1} position={[-1, 0.5, -1]} args={[1, 1, 1]}></MyBox>
<MyPlane
rotation={[-Math.PI / 2, 0, 0]}
scale={[10, 10, 10]}
position={[0, 0, 0]}
></MyPlane>
</Physics>
</Canvas>
</main>
);
}
type MyStaticBoxProps = Omit<BoxProps, "args" | "position" | "type"> & {
args: Exclude<BoxProps["args"], undefined>;
position: Exclude<BoxProps["position"], undefined>;
type: "Static";
};
const MyStaticBox: FC<MyStaticBoxProps> = ({ args, position, ...rest }) => {
const [ref, api] = useBox(
() => ({
args,
position,
...rest,
}),
useRef<Mesh>(null)
);
useFrame(({ clock }) => {
const t = clock.getElapsedTime();
const [xx, yy, zz] = position;
const length = Math.sqrt(xx * xx + zz * zz);
const x = length * Math.sin(t);
const y = yy;
const z = length * Math.cos(t);
api.position.set(x, y, z);
});
return (
<Box ref={ref} scale={args}>
<meshStandardMaterial></meshStandardMaterial>
</Box>
);
};
type MyBoxProps = BoxProps;
const MyBox: FC<MyBoxProps> = ({ args, ...rest }) => {
const [ref] = useBox(
() => ({
args,
...rest,
}),
useRef<Mesh>(null)
);
return (
<Box ref={ref} scale={args}>
<meshStandardMaterial></meshStandardMaterial>
</Box>
);
};
type MyPlaneProps = PlaneProps & Pick<MeshProps, "scale">;
const MyPlane: FC<MyPlaneProps> = ({ scale, ...rest }) => {
const [ref] = usePlane(
() => ({
...rest,
}),
useRef<Mesh>(null)
);
return (
<Plane ref={ref} scale={scale}>
<meshStandardMaterial></meshStandardMaterial>
</Plane>
);
};
静止画ではわかりにくいが MyBox が MyStaticBox に押される
ちなみに MyBox と MyStaticBox の座標が重なっていると MyBox が吹っ飛んでいくのでシュール。
6/1 (木) はここまで
2 時間学んだので累計で 8 時間になった。
React Three Fiber で表示するだけだったら group で自動計算してもらえたが Cannon を使う場合は自分で頑張って計算する必要があるかも知れない。
平行移動と回転だけとはいえなかなか大変そうだ。
あと 2 つの Static で挟めば把持(grasp)ってできるのだろうか。
なんか吹っ飛んでいきそうな予感がする。
6/2 (金) はここから
今日は簡単なロボットを作って箱を A 地点から B 地点へ移動させるゲームを作り始めてみよう。
きっと今日では終わらないだろう。
まずは物理シミュレーションなしで作る
"use client";
import { Box, Cylinder, OrbitControls, Plane } from "@react-three/drei";
import { Canvas } from "@react-three/fiber";
export default function Home() {
return (
<main className="container mx-auto">
<h1 className="text-4xl mt-4 mb-4">Robot arm game</h1>
<Canvas
style={{
width: "70%",
height: "70vh",
}}
camera={{
position: [50, 50, 50],
}}
>
<pointLight position={[100, 200, 100]} intensity={0.8}></pointLight>
<ambientLight intensity={0.1}></ambientLight>
<OrbitControls></OrbitControls>
<group>
<Cylinder args={[10, 10, 20, 12]} position={[0, 10, 0]}>
<meshStandardMaterial></meshStandardMaterial>
</Cylinder>
<Box args={[20, 4, 10]} position={[15, 11, 0]}>
<meshStandardMaterial></meshStandardMaterial>
</Box>
</group>
<Plane args={[100, 100]} rotation={[-Math.PI / 2, 0, 0]}>
<meshStandardMaterial color={"#888"}></meshStandardMaterial>
</Plane>
</Canvas>
</main>
);
}
この子をくるくる回してみようと思います
動かせるようにした
"use client";
import { Box, Cylinder, OrbitControls, Plane } from "@react-three/drei";
import { Canvas } from "@react-three/fiber";
import { ChangeEvent, useState } from "react";
export default function Home() {
const [controller, setController] = useState({
positionX: 0,
positionZ: 0,
axis0: 0,
});
const onChange = (event: ChangeEvent<HTMLInputElement>) =>
setController({
...controller,
[event.target.name]: parseInt(event.target.value, 10),
});
const rangeInput = (name: string, value: number) => (
<div className="mb-2 flex items-center">
<label htmlFor={name} className="mr-2">
{name}
</label>
<input
type="range"
id={name}
name={name}
max={80}
min={-80}
value={value}
onChange={onChange}
/>
</div>
);
return (
<main className="container mx-auto">
<h1 className="text-4xl mt-4 mb-4">Robot arm game</h1>
<form className="mb-4">
{rangeInput("positionX", controller.positionX)}
{rangeInput("positionZ", controller.positionZ)}
{rangeInput("axis0", controller.axis0)}
</form>
<Canvas
style={{
width: "70%",
height: "70vh",
}}
camera={{
position: [50, 50, 50],
}}
>
<pointLight position={[100, 200, 100]} intensity={0.8}></pointLight>
<ambientLight intensity={0.1}></ambientLight>
<OrbitControls></OrbitControls>
<group
position={[controller.positionX, 0, controller.positionZ]}
rotation={[0, (controller.axis0 * Math.PI) / 180, 0]}
>
<Cylinder args={[10, 10, 20, 12]} position={[0, 10, 0]}>
<meshStandardMaterial></meshStandardMaterial>
</Cylinder>
<Box args={[20, 4, 10]} position={[15, 11, 0]}>
<meshStandardMaterial></meshStandardMaterial>
</Box>
</group>
<Plane args={[200, 200]} rotation={[-Math.PI / 2, 0, 0]}>
<meshStandardMaterial color={"#888"}></meshStandardMaterial>
</Plane>
</Canvas>
</main>
);
}
くるくる回るようになりました
ここまでで 1 時間経ってしまった。
午後からもう 1 時間再開しよう。
6/2 (金) PM 再開
group を使わないで動くようにしてみよう。
試行錯誤の末できた
"use client";
import { Box, Cylinder, OrbitControls, Plane } from "@react-three/drei";
import { Canvas } from "@react-three/fiber";
import { ChangeEvent, useState } from "react";
import { Matrix4 } from "three";
export default function Home() {
const [controller, setController] = useState({
positionX: 0,
positionZ: 0,
axis0: 0,
});
const onChange = (event: ChangeEvent<HTMLInputElement>) =>
setController({
...controller,
[event.target.name]: parseInt(event.target.value, 10),
});
const rangeInput = (name: string, value: number, max: number) => (
<div className="mb-2 flex items-center">
<label htmlFor={name} className="mr-2">
{name}
</label>
<input
type="range"
id={name}
name={name}
max={max}
min={-max}
value={value}
onChange={onChange}
/>
</div>
);
const matrixController = new Matrix4()
.makeTranslation(controller.positionX, 0, controller.positionZ)
.multiply(new Matrix4().makeRotationY((controller.axis0 * Math.PI) / 180));
const matrixCylinder = new Matrix4().makeTranslation(0, 10, 0);
const matrixBox = new Matrix4().makeTranslation(15, 11, 0);
return (
<main className="container mx-auto">
<h1 className="text-4xl mt-4 mb-4">Robot arm game</h1>
<form className="mb-4">
{rangeInput("positionX", controller.positionX, 80)}
{rangeInput("positionZ", controller.positionZ, 80)}
{rangeInput("axis0", controller.axis0, 180)}
</form>
<Canvas
style={{
width: "70%",
height: "70vh",
}}
camera={{
position: [50, 50, 50],
}}
>
<pointLight position={[100, 200, 100]} intensity={0.8}></pointLight>
<ambientLight intensity={0.1}></ambientLight>
<OrbitControls></OrbitControls>
<Cylinder
args={[10, 10, 20, 12]}
matrixAutoUpdate={false}
matrix={matrixController.clone().multiply(matrixCylinder)}
>
<meshStandardMaterial></meshStandardMaterial>
</Cylinder>
<Box
args={[20, 4, 10]}
matrixAutoUpdate={false}
matrix={matrixController.clone().multiply(matrixBox)}
>
<meshStandardMaterial></meshStandardMaterial>
</Box>
<Plane args={[200, 200]} rotation={[-Math.PI / 2, 0, 0]}>
<meshStandardMaterial color={"#888"}></meshStandardMaterial>
</Plane>
</Canvas>
</main>
);
}
group のありがたみがよくわかりました
6/2 (金) はここまで
2 時間学んだので累計で 10 時間になった。
次回はどうにかして位置と回転の情報を取り出して Cannon の側と同期すればやりたいことができる気がする。
これができたら次は把持を試してみたい。
6/5 (月) はここから
今日こそ箱を A 地点から B 地点へ移動させるゲームを完成させたい。
Position と Quaternion を使う
Matrix4 で指定していた部分を Position と Quaternion に切り替える。
const matrixController = new Matrix4()
.makeTranslation(controller.positionX, 0, controller.positionZ)
.multiply(new Matrix4().makeRotationY((controller.axis0 * Math.PI) / 180));
const matrixLocalCylinder = new Matrix4().makeTranslation(0, 10, 0);
const matrixLocalBox = new Matrix4().makeTranslation(15, 11, 0);
const matrixCylinder = matrixController.clone().multiply(matrixLocalCylinder);
const matrixBox = matrixController.clone().multiply(matrixLocalBox);
const positionCylinder = new Vector3().setFromMatrixPosition(matrixCylinder);
const positionBox = new Vector3().setFromMatrixPosition(matrixBox);
const quaternionCylinder = new Quaternion().setFromRotationMatrix(
matrixCylinder
);
const quaternionBox = new Quaternion().setFromRotationMatrix(matrixBox);
表示されるものが変わらなければ OK
decompose() を使うこともできる
const matrixController = new Matrix4()
.makeTranslation(controller.positionX, 0, controller.positionZ)
.multiply(new Matrix4().makeRotationY((controller.axis0 * Math.PI) / 180));
const matrixLocalCylinder = new Matrix4().makeTranslation(0, 10, 0);
const matrixLocalBox = new Matrix4().makeTranslation(15, 11, 0);
const matrixCylinder = matrixController.clone().multiply(matrixLocalCylinder);
const matrixBox = matrixController.clone().multiply(matrixLocalBox);
const positionCylinder = new Vector3();
const positionBox = new Vector3();
const quaternionCylinder = new Quaternion();
const quaternionBox = new Quaternion();
matrixCylinder.decompose(positionCylinder, quaternionCylinder, new Vector3());
matrixBox.decompose(positionBox, quaternionBox, new Vector3());
物理演算付きシリンダーを作る
type MyCylinderProps = Omit<MeshProps, "args" | "position" | "quaternion"> & {
args: CylinderBufferGeometryProps["args"];
position: Vector3;
quaternion: Quaternion;
};
const MyCylinder: FC<MyCylinderProps> = ({
args,
position,
quaternion,
children,
...rest
}) => {
const [ref, api] = useCylinder(
() => ({
args,
position: [position.x, position.y, position.z],
quaternion: [quaternion.x, quaternion.y, quaternion.z, quaternion.w],
type: "Static",
}),
useRef<Mesh>(null)
);
useEffect(() => {
api.position.copy(position);
}, [api.position, position]);
useEffect(() => {
api.quaternion.copy(quaternion);
}, [api.quaternion, quaternion]);
return (
<Cylinder ref={ref} args={args} {...rest}>
{children}
</Cylinder>
);
};
物理演算付きの動く箱を作る
type MyDynamicBoxProps = Omit<MeshProps, "args" | "position"> & {
args: Triplet;
position: Triplet;
mass: number;
};
const MyDynamicBox: FC<MyDynamicBoxProps> = ({
args,
position,
mass,
children,
...rest
}) => {
const [ref, api] = useBox(
() => ({
args,
position,
mass,
}),
useRef<Mesh>(null)
);
return (
<Box ref={ref} args={args}>
{children}
</Box>
);
};
物理演算付きの地面を作る
type MyPlaneProps = Omit<MeshProps, "args" | "rotation"> & {
args: PlaneGeometryProps["args"];
rotation: Triplet;
};
const MyPlane: FC<MyPlaneProps> = ({ args, rotation, children }) => {
const [ref] = usePlane(
() => ({
rotation,
}),
useRef<Mesh>(null)
);
return (
<Plane ref={ref} args={args}>
{children}
</Plane>
);
};
なんやかんやありまして
ロボットを操作して箱を動かせるようになりました。
"use client";
import {
CylinderProps,
Physics,
Triplet,
useBox,
useCylinder,
usePlane,
} from "@react-three/cannon";
import { Box, Cylinder, OrbitControls, Plane } from "@react-three/drei";
import {
BoxBufferGeometryProps,
BoxGeometryProps,
Canvas,
CylinderBufferGeometryProps,
CylinderGeometryProps,
MeshProps,
PlaneGeometryProps,
useFrame,
} from "@react-three/fiber";
import { ChangeEvent, FC, useEffect, useRef, useState } from "react";
import { Euler, Matrix4, Mesh, Quaternion, Vector3 } from "three";
export default function Home() {
const [controller, setController] = useState({
positionX: 0,
positionZ: 0,
axis0: 0,
});
const onChange = (event: ChangeEvent<HTMLInputElement>) =>
setController({
...controller,
[event.target.name]: parseInt(event.target.value, 10),
});
const rangeInput = (name: string, value: number, max: number) => (
<div className="mb-2 flex items-center">
<label htmlFor={name} className="mr-2">
{name}
</label>
<input
type="range"
id={name}
name={name}
max={max}
min={-max}
value={value}
onChange={onChange}
/>
</div>
);
const matrixController = new Matrix4()
.makeTranslation(controller.positionX, 0, controller.positionZ)
.multiply(new Matrix4().makeRotationY((controller.axis0 * Math.PI) / 180));
const matrixLocalCylinder = new Matrix4().makeTranslation(0, 10, 0);
const matrixLocalBox = new Matrix4().makeTranslation(15, 11, 0);
const matrixCylinder = matrixController.clone().multiply(matrixLocalCylinder);
const matrixBox = matrixController.clone().multiply(matrixLocalBox);
const positionCylinder = new Vector3();
const positionBox = new Vector3();
const quaternionCylinder = new Quaternion();
const quaternionBox = new Quaternion();
matrixCylinder.decompose(positionCylinder, quaternionCylinder, new Vector3());
matrixBox.decompose(positionBox, quaternionBox, new Vector3());
return (
<main className="container mx-auto">
<h1 className="text-4xl mt-4 mb-4">Robot arm game</h1>
<form className="mb-4">
{rangeInput("positionX", controller.positionX, 80)}
{rangeInput("positionZ", controller.positionZ, 80)}
{rangeInput("axis0", controller.axis0, 180)}
</form>
<Canvas
style={{
width: "70%",
height: "70vh",
}}
camera={{
position: [50, 50, 50],
}}
>
<Physics gravity={[0, -98.1, 0]}>
<pointLight position={[100, 200, 100]} intensity={0.8}></pointLight>
<ambientLight intensity={0.1}></ambientLight>
<OrbitControls></OrbitControls>
<MyPlane args={[200, 200]} rotation={[-Math.PI / 2, 0, 0]}>
<meshStandardMaterial color={"#888"}></meshStandardMaterial>
</MyPlane>
<MyCylinder
args={[10, 10, 20, 12]}
position={positionCylinder}
rotation={new Euler().setFromQuaternion(quaternionCylinder)}
>
<meshStandardMaterial></meshStandardMaterial>
</MyCylinder>
<MyBox
args={[20, 4, 10]}
position={positionBox}
rotation={new Euler().setFromQuaternion(quaternionBox)}
>
<meshStandardMaterial></meshStandardMaterial>
</MyBox>
<MyDynamicBox args={[30, 30, 30]} position={[0, 50, 0]} mass={1}>
<meshStandardMaterial></meshStandardMaterial>
</MyDynamicBox>
</Physics>
</Canvas>
</main>
);
}
type MyDynamicBoxProps = Omit<MeshProps, "args" | "position"> & {
args: Triplet;
position: Triplet;
mass: number;
};
const MyDynamicBox: FC<MyDynamicBoxProps> = ({
args,
position,
mass,
children,
...rest
}) => {
const [ref, api] = useBox(
() => ({
args,
position,
mass,
}),
useRef<Mesh>(null)
);
return (
<Box ref={ref} args={args}>
{children}
</Box>
);
};
type MyPlaneProps = Omit<MeshProps, "args" | "rotation"> & {
args: PlaneGeometryProps["args"];
rotation: Triplet;
};
const MyPlane: FC<MyPlaneProps> = ({ args, rotation, children }) => {
const [ref] = usePlane(
() => ({
rotation,
}),
useRef<Mesh>(null)
);
return (
<Plane ref={ref} args={args}>
{children}
</Plane>
);
};
type MyCylinderProps = Omit<MeshProps, "args" | "position" | "rotation"> & {
args: CylinderBufferGeometryProps["args"];
position: Vector3;
rotation: Euler;
};
const MyCylinder: FC<MyCylinderProps> = ({
args,
position,
rotation,
children,
...rest
}) => {
const [ref, api] = useCylinder(
() => ({
args,
position: [position.x, position.y, position.z],
rotation: [rotation.x, rotation.y, rotation.z],
type: "Static",
}),
useRef<Mesh>(null)
);
useEffect(() => {
api.position.copy(position);
}, [api.position, position]);
useEffect(() => {
api.rotation.copy(rotation);
}, [api.rotation, rotation]);
return (
<Cylinder ref={ref} args={args} {...rest}>
{children}
</Cylinder>
);
};
type MyBoxProps = Omit<MeshProps, "args" | "position" | "rotation"> & {
args: Triplet;
position: Vector3;
rotation: Euler;
};
const MyBox: FC<MyBoxProps> = ({
args,
position,
rotation,
children,
...rest
}) => {
const [ref, api] = useBox(
() => ({
args,
position: [position.x, position.y, position.z],
rotation: [rotation.x, rotation.y, rotation.z],
type: "Static",
}),
useRef<Mesh>(null)
);
useEffect(() => {
api.position.copy(position);
}, [api.position, position]);
useEffect(() => {
api.rotation.copy(rotation);
}, [api.rotation, rotation]);
return (
<Box ref={ref} args={args} {...rest}>
{children}
</Box>
);
};
動画の方がわかりやすいが動画を撮影する気力がなくなってしまった
6/14 (水) はここから
9 日ぶりの更新となってしまった。
前回は「ここまで」投稿を忘れたが確か 2 時間稼働だったので累計で今は 12 時間になっているはず。
とりあえず Static を使って物理空間に関与できるようになったものの、もう少し良いやり方があるのではないかと思っている。
今日は公式 Example の中からロボットに関連する例をピックアップして写経していこうと思う。
https://cannon.pmnd.rs/#/demo/HingeMotor
ソースコードは下記の通り、300 行程度とちょうど良い。
ワークスペースの作成
# cd ~/workspace
npx create-next-app \
--typescript \
--tailwind \
--eslint \
--app \
--src-dir \
--import-alias "@/*" \
--use-npm \
hinge-motor
npm install three @types/three @react-three/fiber @react-three/drei @react-three/cannon
cd hinge-motor
npm run dev
まっさらにする
"use client";
export default function Home() {
return <main></main>;
}
import "./globals.css";
export const metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
@tailwind base;
@tailwind components;
@tailwind utilities;
はじめの一歩
"use client";
import { OrbitControls } from "@react-three/drei";
import { Canvas } from "@react-three/fiber";
export default function Home() {
return (
<main className="container mx-auto">
<h1 className="text-4xl mt-4 mb-4">Hinge Motor</h1>
<Canvas
shadows
gl={{ alpha: false }}
style={{
width: "70%",
height: "70vh",
}}
>
<OrbitControls></OrbitControls>
<Scene></Scene>
</Canvas>
</main>
);
}
function Scene() {
return <></>;
}
カメラをロボットに向ける
const v = new Vector3();
function Scene() {
const cameraRef = useRef<Cam>(null);
const robotRef = useRef<Mesh>(null);
// カメラがロボットの方を向くようにしています。
useFrame(() => {
if (!cameraRef.current || !robotRef.current) {
return;
}
robotRef.current.getWorldPosition(v);
cameraRef.current.lookAt(v);
});
return <></>;
}
こうすれば良いのか、やはり写経は勉強になる。
ところで v にも useRef 使っても良い気がするがなぜグローバルにしているんだろう。
カメラやライトの準備
function Scene() {
const cameraRef = useRef<Cam>(null);
const robotRef = useRef<Mesh>(null);
// カメラがロボットの方を向くようにしています。
useFrame(() => {
if (!cameraRef.current || !robotRef.current) {
return;
}
robotRef.current.getWorldPosition(v);
cameraRef.current.lookAt(v);
});
return (
<Suspense fallback={null}>
<PerspectiveCamera
ref={cameraRef}
makeDefault
position={[-40, 10, 20]}
></PerspectiveCamera>
<hemisphereLight intensity={0.35}></hemisphereLight>
<spotLight
position={[20, 30, 10]}
angle={Math.PI / 5}
penumbra={1}
intensity={1}
distance={180}
castShadow
shadow-mapSize-width={256}
shadow-mapSize-height={256}
></spotLight>
<color attach="background" args={["#f6d186"]}></color>
<Physics iterations={80} gravity={[0, -40, 0]}>
</Physics>
</Suspense>
);
}
背景に色がついた
地面ができた
const GROUP_GROUND = 2 ** 0;
const GROUP_BODY = 2 ** 1;
type OurPlaneProps = Pick<PlaneBufferGeometryProps, "args"> &
Pick<PlaneProps, "position" | "rotation">;
function Plane({ args, ...props }: OurPlaneProps) {
const [ref] = usePlane(
() => ({
collisionFilterGroup: GROUP_GROUND,
type: "Static",
...props,
}),
useRef<Group>(null)
);
return (
<group ref={ref}>
<mesh>
<planeBufferGeometry args={args}></planeBufferGeometry>
<meshBasicMaterial color="#ffb385"></meshBasicMaterial>
</mesh>
<mesh receiveShadow>
<planeBufferGeometry args={args}></planeBufferGeometry>
<shadowMaterial color="lightsalmon"></shadowMaterial>
</mesh>
</group>
);
}
<Physics iterations={80} gravity={[0, -40, 0]}>
<Plane
args={[120, 120]}
position={[-20, -5, 0]}
rotation={[-Math.PI / 2, 0, 0]}
></Plane>
</Physics>
薄い茶色の地面ができた
collisionFilterGroup とは何なのだろう?下記のデモが参考になりそう。
ここまでで約 1 時間経過した。
Collision Filter のソースコード
ConstraintPart の追加
type BoxShapeProps = Pick<
MeshStandardMaterialProps,
"color" | "opacity" | "transparent"
> &
Pick<BoxProps, "args">;
const BoxShape = forwardRef<Mesh, PropsWithChildren<BoxShapeProps>>(
(
{
args = [1, 1, 1],
children,
color = "white",
opacity = 1,
transparent = false,
...props
},
ref
) => (
<mesh receiveShadow castShadow ref={ref} {...props}>
<boxBufferGeometry args={args}></boxBufferGeometry>
<meshStandardMaterial
color={color}
transparent={transparent}
opacity={opacity}
></meshStandardMaterial>
</mesh>
)
);
const ref = createRef<Object3D>();
const context = createContext<
[bodyRef: RefObject<Object3D>, props: BoxShapeProps]
>([ref, {}]);
function normalizeSize([px = 0, py = 0, pz = 0]): (scale: Triplet) => Triplet {
return ([ox = 1, oy = 1, oz = 1]) => [px * ox, py * oy, pz * oz];
}
type ConstraintProps = {
config?: HingeConstraintOpts;
enableMotor: boolean;
motorSpeed?: number;
parentPivot?: Triplet;
pivot?: Triplet;
} & BoxProps &
BoxShapeProps;
const ConstraintPart = forwardRef<Mesh, PropsWithChildren<ConstraintProps>>(
(
{
config = {},
enableMotor,
motorSpeed = 7,
color,
children,
pivot = [0, 0, 0],
parentPivot = [0, 0, 0],
...props
},
ref
) => {
const parent = useContext(context);
const normParentPivot =
parent && parent[1].args
? normalizeSize(parent[1].args)
: () => undefined;
const normPivot = props.args ? normalizeSize(props.args) : () => undefined;
const [bodyRef] = useBox(
() => ({
collisionFilterGroup: GROUP_BODY,
collisionFilterMask: GROUP_GROUND,
linearDamping: 0.4,
mass: 1,
...props,
}),
ref
);
const [, , hingeApi] = useHingeConstraint(bodyRef, parent[0], {
axisA: [0, 0, 1],
axisB: [0, 0, 1],
collideConnected: false,
pivotA: normPivot(pivot),
pivotB: normParentPivot(parentPivot),
...config,
});
useEffect(() => {
if (enableMotor) {
hingeApi.enableMotor();
} else {
hingeApi.disableMotor();
}
}, [enableMotor]);
useEffect(() => {
hingeApi.setMotorSpeed(motorSpeed);
}, [motorSpeed]);
return (
<context.Provider value={[bodyRef, props]}>
<BoxShape ref={bodyRef} {...props} color={color}></BoxShape>
{children}
</context.Provider>
);
}
);
何をやっているかはよくわからない。
障害物の追加
function Obstacles() {
return (
<>
<ConstraintPart
collisionFilterGroup={GROUP_GROUND}
collisionFilterMask={GROUP_BODY | GROUP_GROUND}
mass={4}
args={[-30, -0.4, 30]}
position={[-45, -4, 0]}
rotation={[0, Math.PI / -4, 0]}
color={"#ffb385"}
></ConstraintPart>
<ConstraintPart
collisionFilterGroup={GROUP_GROUND}
collisionFilterMask={GROUP_BODY | GROUP_GROUND}
mass={4}
args={[-15, -0.5, 15]}
position={[-50, -2, 0]}
rotation={[0, Math.PI / -1.25, 0]}
color={"#dc9c76"}
></ConstraintPart>
<ConstraintPart
collisionFilterGroup={GROUP_GROUND}
collisionFilterMask={GROUP_BODY | GROUP_GROUND}
mass={4}
args={[-10, -0.5, 10]}
position={[-45, 0, -5]}
rotation={[0, Math.PI / 3, 0]}
color={"#c58e6e"}
></ConstraintPart>
</>
);
}
<Physics iterations={80} gravity={[0, -40, 0]}>
<Plane
args={[120, 120]}
position={[-20, -5, 0]}
rotation={[-Math.PI / 2, 0, 0]}
></Plane>
<Obstacles></Obstacles>
</Physics>
3 枚の板のようなものが追加された
衝突について考える
衝突は次の条件が満たされた場合に発生するようだ。
(bodyA.collisionFilterGroup & bodyB.collisionFilterMask) && (bodyB.collisionFilterGroup & bodyA.collisionFilterMask)
地面の collisionFilterGroup は GROUP_GROUND、collisionFilterMask は未設定だと -1 = 0x1111...1111 になるようだ。
一方、障害物の collisionFilterGroup は GROUP_GROUND、collisionFilterMask は GROUP_BODY | GROUP_GROUND に設定されている。
したがって障害物は地面と衝突判定が行われる。
また、障害物同士でも当たり判定が行われることになる。
collisionFilterGroup を所属するグループ、collisionFilterMask を衝突対象のグループと考えるとわかりやすい。
6/14 (水) はここまで
2 時間学んだので累計で 14 時間となった。
あと 2 時間くらいで写経を終わらせたいところだ。
6/15 (木) はここから
昨日に引き続き写経を行う。
AM は 1 時間くらいサクっとやろう。
ロボットの追加
type LegsProps = {
bodyDepth?: number;
delay?: number;
} & Pick<ConstraintProps, "motorSpeed">;
const Legs = forwardRef<Mesh, LegsProps>(
({ bodyDepth = 0, delay = 0, motorSpeed = 7 }, bodyRef) => {
const horizontalRef = useRef<Mesh>(null);
const frontLegRef = useRef<Mesh>(null);
const frontUpperLegRef = useRef<Mesh>(null);
const backLegRef = useRef<Mesh>(null);
const partDepth = 0.3;
const bodyWidth = 10;
const bodyHeight = 2;
const legLength = 6;
const size3 = normalizeSize([1, 3, partDepth]);
const size5 = normalizeSize([1, 5, partDepth]);
const size10 = normalizeSize([1, 10, partDepth]);
useHingeConstraint(frontUpperLegRef, frontLegRef, {
axisA: [0, 0, 1],
axisB: [0, 0, 1],
collideConnected: false,
pivotA: size3([0, 0.5, 0.5]),
pivotB: size5([0, 0.5, -0.5]),
});
useHingeConstraint(backLegRef, horizontalRef, {
axisA: [0, 0, 1],
axisB: [0, 0, 1],
collideConnected: false,
pivotA: size3([0, 0.5, 0.5]),
pivotB: size5([0, 0.5, -0.5]),
});
const [isWalking, setIsWalking] = useState(false);
useEffect(() => {
const t = setTimeout(() => setIsWalking(true), delay);
return () => clearTimeout(t);
}, []);
return (
<group>
<ConstraintPart
ref={bodyRef}
mass={1}
args={[
bodyHeight,
bodyWidth,
bodyDepth ? bodyDepth + partDepth * 3 : 0,
]}
rotation={[0, 0, Math.PI / 2]}
position={[0, 0, bodyDepth]}
transparent={!bodyDepth}
opacity={Number(!!bodyDepth)}
>
<ConstraintPart
ref={frontUpperLegRef}
args={[1, 3, partDepth]}
position={[-2, 0.5, bodyDepth]}
rotation={[0, 0, Math.PI / 3]}
pivot={[0, -0.5, -0.5]}
parentPivot={[0, 0.2, 0.5]}
color={"#85ffb3"}
></ConstraintPart>
<ConstraintPart
enableMotor={isWalking}
motorSpeed={motorSpeed}
args={[0.5, 1, partDepth]}
position={[bodyWidth * -0.5, -1.5 / 2, bodyDepth]}
parentPivot={[0, 0.5, 0.5]}
pivot={[0, 0.5, -0.5]}
color={"black"}
>
<ConstraintPart
ref={frontLegRef}
args={[1, legLength, partDepth]}
position={[bodyWidth * -0.5, -1, bodyDepth]}
rotation={[0, 0, Math.PI / -6]}
parentPivot={[0, -0.5, 0.5]}
pivot={[0, 0, -0.5]}
color={"#85b3ff"}
>
<ConstraintPart
ref={horizontalRef}
parentPivot={[0, 0, 0.5]}
pivot={[0, -0.5, -0.5]}
args={[1, bodyWidth, partDepth]}
position={[0, 0, bodyDepth]}
color={"#ff85b3"}
rotation={[0, 0, Math.PI / -2.5]}
></ConstraintPart>
</ConstraintPart>
</ConstraintPart>
<ConstraintPart
ref={backLegRef}
args={[1, legLength, partDepth]}
pivot={[0, 0, -1]}
parentPivot={[-0.0, -0.5, 0.5]}
position={[bodyWidth * 0.5, 0, bodyDepth]}
rotation={[0, 0, Math.PI / 4]}
color={"#85b3ff"}
></ConstraintPart>
</ConstraintPart>
</group>
);
}
);
const Robot = forwardRef<Mesh>((_, legsLeftRef) => {
const [motorSpeed, setMotorSpeed] = useState(7);
const legsRightRef = useRef<Mesh>(null);
useLockConstraint(legsRightRef, legsLeftRef, {});
return (
<group
onPointerDown={() => setMotorSpeed(2)}
onPointerUp={() => setMotorSpeed(7)}
>
<Legs
ref={legsLeftRef}
delay={1000}
bodyDepth={3}
motorSpeed={motorSpeed}
></Legs>
<Legs
ref={legsRightRef}
delay={1000}
bodyDepth={3}
motorSpeed={motorSpeed}
></Legs>
</group>
);
});
<Physics iterations={80} gravity={[0, -40, 0]}>
<Plane
args={[120, 120]}
position={[-20, -5, 0]}
rotation={[-Math.PI / 2, 0, 0]}
></Plane>
<Obstacles></Obstacles>
<Robot ref={robotRef}></Robot>
</Physics>
レッグが片方しかない
レッグを両方にする
設定ミスだった。
const Robot = forwardRef<Mesh>((_, legsLeftRef) => {
const [motorSpeed, setMotorSpeed] = useState(7);
const legsRightRef = useRef<Mesh>(null);
useLockConstraint(legsRightRef, legsLeftRef, {});
return (
<group
onPointerDown={() => setMotorSpeed(2)}
onPointerUp={() => setMotorSpeed(7)}
>
<Legs
ref={legsLeftRef}
delay={1000}
bodyDepth={3}
motorSpeed={motorSpeed}
></Legs>
<Legs ref={legsRightRef} motorSpeed={motorSpeed}></Legs>
</group>
);
});
何やら動きがおかしい
ヒンジ制約の修正
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 3b14e61..1334958 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -315,8 +315,8 @@ const Legs = forwardRef<Mesh, LegsProps>(
axisA: [0, 0, 1],
axisB: [0, 0, 1],
collideConnected: false,
- pivotA: size3([0, 0.5, 0.5]),
- pivotB: size5([0, 0.5, -0.5]),
+ pivotA: size5([0, 0.5, 0.5]),
+ pivotB: size10([0, 0.5, -0.5]),
});
const [isWalking, setIsWalking] = useState(false);
ようやく完成した
ここまで 1 時間
合計では 15 時間となった。
あと 1 時間くらいかけてコードから新しいテクニックを学び取ろう。
6/21 (水) はここから
前から 1 週間近く経過してしまった。
前回予定した通り、あと 1 時間かけてコードから新しいテクニックを学び取ろう。
LockConstraint
LockConstraint をコメントアウトするとシュールな感じになる
PointToPointContraint との違いがよくわからない。
Legs
片方は中央の箱付きでもう片方は中央の箱なしになっている。
どういう仕組みになっているんだろうと気になったので調べてみたところ、片方の bodyDepth が 0 になっていることがわかった。
bodyDepth がゼロなのに物体として扱われるのかが気になったが、ゼロの場合はものすごく薄い板のような扱いになるようだ。
今更だけど
奥の方の黒い部品が物理法則を無視したところに付いてることに気づいた。
自分の間違いかと思ったが公式もそうだったので意図的なものだろう。
確かに反転ささせるのとか面倒そう。
コメントを付けてみた
const Legs = forwardRef<Mesh, LegsProps>(
({ bodyDepth = 0, delay = 0, motorSpeed = 7 }, bodyRef) => {
const horizontalRef = useRef<Mesh>(null);
const frontLegRef = useRef<Mesh>(null);
const frontUpperLegRef = useRef<Mesh>(null);
const backLegRef = useRef<Mesh>(null);
const partDepth = 0.3;
const bodyWidth = 10;
const bodyHeight = 2;
const legLength = 6;
const size3 = normalizeSize([1, 3, partDepth]);
const size5 = normalizeSize([1, 5, partDepth]);
const size10 = normalizeSize([1, 10, partDepth]);
// ボディに接続された緑色の板を青色の前足を接続する。
useHingeConstraint(frontUpperLegRef, frontLegRef, {
axisA: [0, 0, 1],
axisB: [0, 0, 1],
collideConnected: false,
pivotA: size3([0, 0.5, 0.3]),
pivotB: size5([0, 0.5, -0.5]),
});
// 青色の後ろ足と水平のピンク色の板を接続する。
useHingeConstraint(backLegRef, horizontalRef, {
axisA: [0, 0, 1],
axisB: [0, 0, 1],
collideConnected: false,
pivotA: size5([0, 0.5, 0.5]),
pivotB: size10([0, 0.5, -0.5]),
});
const [isWalking, setIsWalking] = useState(false);
useEffect(() => {
const t = setTimeout(() => setIsWalking(true), delay);
return () => clearTimeout(t);
}, []);
return (
<group>
{/* 中央の白い箱 */}
<ConstraintPart
ref={bodyRef}
mass={1}
args={[
bodyHeight,
bodyWidth,
bodyDepth ? bodyDepth + partDepth * 3 : 0,
]}
rotation={[0, 0, Math.PI / 2]}
position={[0, 0, bodyDepth]}
transparent={!bodyDepth}
opacity={Number(!!bodyDepth)}
>
{/* 前足とボディに固定された緑色の板 */}
<ConstraintPart
ref={frontUpperLegRef}
args={[1, 3, partDepth]}
position={[-2, 0.5, bodyDepth]}
rotation={[0, 0, Math.PI / 3]}
pivot={[0, -0.5, -0.5]}
parentPivot={[0, 0.2, 0.5]}
color={"#85ffb3"}
></ConstraintPart>
{/* 回転している黒色の板 */}
<ConstraintPart
enableMotor={isWalking}
motorSpeed={motorSpeed}
args={[0.5, 1, partDepth]}
position={[bodyWidth * -0.5, -1.5 / 2, bodyDepth]}
parentPivot={[0, 0.5, 0.5]}
pivot={[0, 0.5, -0.5]}
color={"black"}
>
{/* 青色の前足 */}
<ConstraintPart
ref={frontLegRef}
args={[1, legLength, partDepth]}
position={[bodyWidth * -0.5, -1, bodyDepth]}
rotation={[0, 0, Math.PI / -6]}
parentPivot={[0, -0.5, 0.5]}
pivot={[0, 0, -0.5]}
color={"#85b3ff"}
>
{/* 前足と後ろ足をつなぐ水平のピンク色の板 */}
<ConstraintPart
ref={horizontalRef}
parentPivot={[0, 0, 0.5]}
pivot={[0, -0.5, -0.5]}
args={[1, bodyWidth, partDepth]}
position={[0, 0, bodyDepth]}
color={"#ff85b3"}
rotation={[0, 0, Math.PI / -2.5]}
></ConstraintPart>
</ConstraintPart>
</ConstraintPart>
<ConstraintPart
ref={backLegRef}
args={[1, legLength, partDepth]}
pivot={[0, 0, -1]}
parentPivot={[-0.0, -0.5, 0.5]}
position={[bodyWidth * 0.5, 0, bodyDepth]}
rotation={[0, 0, Math.PI / 4]}
color={"#85b3ff"}
></ConstraintPart>
</ConstraintPart>
</group>
);
}
);
6/21 (水) はここまで
合計 16 時間、この辺りでクローズしよう。
ヒンジ制約は面白いが回転角の指定はできなさそうなので最初にやったように Static を使う方向でまずはやってみるか。
学びが多く充実した時間だった。