Closed45

Next.js で React Three Fiber を使ってロボットアームを作る

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

このスクラップについて

このスクラップでは Next.js で Three.js を使ってロボットアームを表示できるようにするまでの過程を記録していきたい。

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

プロジェクトの作成

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

真っ白な画面にする

src/app/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
src/app/page.tsx
export default function Home() {
  return (
    <main>
      <h1>Home</h1>
    </main>
  );
}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

はじめてのコーディング

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

import { Canvas, ThreeElements, useFrame } from "@react-three/fiber";
import { useRef, useState } from "react";

function Box(props: ThreeElements["mesh"]) {
  const ref = useRef<THREE.Mesh>(null!);
  const [hovered, hover] = useState(false);
  const [clicked, click] = useState(false);

  useFrame((state, delta) => (ref.current.rotation.x += delta));

  return (
    <mesh
      {...props}
      ref={ref}
      scale={clicked ? 1.5 : 1}
      onClick={(event) => click(!clicked)}
      onPointerOver={(event) => hover(true)}
      onPointerOut={(event) => hover(false)}
    >
      <boxGeometry args={[1, 1, 1]}></boxGeometry>
      <meshStandardMaterial
        color={hovered ? "hotpink" : "orange"}
      ></meshStandardMaterial>
    </mesh>
  );
}

export default function Home() {
  return (
    <main>
      <h1>Home</h1>
      <Canvas>
        <pointLight position={[10, 10, 10]}></pointLight>
        <Box position={[-1.2, 0, 0]}></Box>
        <Box position={[1.2, 0, 0]}></Box>
      </Canvas>
    </main>
  );
}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Drei で書き替えてみる

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

import { Box } from "@react-three/drei";
import { Canvas, ThreeElements, useFrame } from "@react-three/fiber";
import { useRef, useState } from "react";

function MyBox(props: ThreeElements["mesh"]) {
  const ref = useRef<THREE.Mesh>(null!);
  const [hovered, hover] = useState(false);
  const [clicked, click] = useState(false);

  useFrame((state, delta) => (ref.current.rotation.x += delta));

  return (
    <Box
      {...props}
      ref={ref}
      args={[1, 1, 1]}
      scale={clicked ? 1.5 : 1}
      onClick={(event) => click(!clicked)}
      onPointerOver={(event) => hover(true)}
      onPointerOut={(event) => hover(false)}
      material-color={hovered ? "hotpink" : "orange"}
    ></Box>
  );
}

export default function Home() {
  return (
    <main>
      <h1>Home</h1>
      <Canvas>
        <pointLight position={[10, 10, 10]}></pointLight>
        <MyBox position={[-1.2, 0, 0]}></MyBox>
        <MyBox position={[1.2, 0, 0]}></MyBox>
      </Canvas>
    </main>
  );
}

それほど短くはならない。

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

Canvas を大きくしたい

あっているか自信がないが Canvas の style 属性を使えば良さそう。

src/app/page.tsx
export default function Home() {
  return (
    <main>
      <h1>Home</h1>
      <Canvas style={{ width: "100vw", height: "100vh" }}>
        <pointLight position={[10, 10, 10]}></pointLight>
        <MyBox position={[-1.2, 0, 0]}></MyBox>
        <MyBox position={[1.2, 0, 0]}></MyBox>
      </Canvas>
    </main>
  );
}


Canvas が大きくなった

https://github.com/pmndrs/react-three-fiber/discussions/630#discussioncomment-715526

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

OrbitControl が欲しい

なんとこれだけで良い。

src/app/page.tsx
export default function Home() {
  return (
    <main>
      <h1>Home</h1>
      <Canvas style={{ width: "100vw", height: "100vh" }}>
        <pointLight position={[10, 10, 10]}></pointLight>
        <MyBox position={[-1.2, 0, 0]}></MyBox>
        <MyBox position={[1.2, 0, 0]}></MyBox>
        <OrbitControls></OrbitControls>
      </Canvas>
    </main>
  );
}


ドラッグすることで視点を変えられる

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

シリンダを表示させたい

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

import { Cylinder, OrbitControls } from "@react-three/drei";
import { Canvas } from "@react-three/fiber";

export default function Home() {
  return (
    <main>
      <h1>Home</h1>
      <Canvas style={{ width: "100vw", height: "100vh" }}>
        <pointLight position={[10, 10, 10]}></pointLight>
        <OrbitControls></OrbitControls>
        <Cylinder args={[1, 1, 2, 6]}></Cylinder>
      </Canvas>
    </main>
  );
}


回転時にわかりやすいように断面 6 角形にした

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

影が欲しい

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

import { Cylinder, OrbitControls } from "@react-three/drei";
import { Canvas } from "@react-three/fiber";

export default function Home() {
  return (
    <main>
      <h1>Home</h1>
      <Canvas style={{ width: "100vw", height: "100vh" }}>
        <pointLight position={[10, 10, 10]}></pointLight>
        <OrbitControls></OrbitControls>
        <Cylinder args={[1, 1, 2, 6]}>
          <meshPhongMaterial color="#f3f3f3"></meshPhongMaterial>
        </Cylinder>
      </Canvas>
    </main>
  );
}


なんだ!この影のつき方は!

おそらく法線ベクトルが断面円形を前提に設定されているのだろう。

断面円形が前提のシリンダを断面多角形として流用しようとしているからこうなった。

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

回してみる

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

import { Cylinder, OrbitControls } from "@react-three/drei";
import { Canvas, useFrame } from "@react-three/fiber";
import { useState } from "react";

export default function Home() {
  return (
    <main>
      <h1>Home</h1>
      <Canvas style={{ width: "100vw", height: "100vh" }}>
        <Scene></Scene>
      </Canvas>
    </main>
  );
}

function Scene() {
  const [rotation, setRotation] = useState(0);

  useFrame((state, delta) => setRotation(rotation + delta));

  return (
    <>
      <pointLight position={[10, 10, 10]}></pointLight>
      <OrbitControls></OrbitControls>
      <Cylinder args={[1, 1, 2, 12]} rotation={[0, rotation, 0]}>
        <meshPhongMaterial color="#f3f3f3"></meshPhongMaterial>
      </Cylinder>
    </>
  );
}


静止画ではわかりにくいが Y 軸を中心に回転している

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

円柱の上に立方体を乗っける

src/app/page.tsx(抜粋)
function Scene() {
  const [rotation, setRotation] = useState(0);

  useFrame((state, delta) => setRotation(rotation + delta));

  return (
    <>
      <pointLight position={[10, 10, 10]}></pointLight>
      <OrbitControls></OrbitControls>
      <Cylinder args={[1, 1, 2, 12]} rotation={[0, rotation, 0]}>
        <meshPhongMaterial color="#f3f3f3"></meshPhongMaterial>
      </Cylinder>
      <Box position={[0, 1.5, 0]}></Box>
    </>
  );
}


円柱は回転するが立方体は動かない

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

Group を使う

src/app/page.tsx(抜粋)
function Scene() {
  const [rotation, setRotation] = useState(0);

  useFrame((state, delta) => setRotation(rotation + delta));

  return (
    <>
      <pointLight position={[10, 10, 10]}></pointLight>
      <OrbitControls></OrbitControls>
      <group rotation={[0, rotation, 0]}>
        <Cylinder args={[1, 1, 2, 12]}>
          <meshPhongMaterial color="#f3f3f3"></meshPhongMaterial>
        </Cylinder>
        <Box position={[0, 1.5, 0]}>
          <meshPhongMaterial color="#f3f3f3"></meshPhongMaterial>
        </Box>
      </group>
    </>
  );
}


立方体も一緒に回るようになった

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

今日はここまで

あとは同じ要領で Group を入れ子にしていけばロボットアームみたいなものができそう。

3DCG 楽しいな。

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

2 軸目を増やした

src/app/page.tsx
function Scene() {
  const [rotation, setRotation] = useState(0);
  const material = <meshPhongMaterial color="#f3f3f3"></meshPhongMaterial>;

  // useFrame((state, delta) => setRotation(rotation + delta));

  return (
    <>
      <pointLight position={[10, 10, 10]}></pointLight>
      <OrbitControls></OrbitControls>
      <group rotation={[0, rotation - Math.PI / 4, 0]}>
        <Cylinder args={[1, 1, 2, 12]}>{material}</Cylinder>
        <Box position={[-0.5, 1.75, 0]} scale={[0.2, 1.5, 1]}>
          {material}
        </Box>
        <group rotation={[rotation - Math.PI / 4, 0, 0]} position={[0, 2, 0]}>
          <Cylinder
            args={[1, 1, 2, 12]}
            position={[-1.1, 0, 0]}
            scale={[0.5, 0.5, 0.5]}
            rotation={[0, 0, Math.PI / 2]}
          >
            {material}
          </Cylinder>
          <Box position={[0.1, 1, 0]} scale={[1, 3, 1]}>
            {material}
          </Box>
        </group>
      </group>
    </>
  );
}


良い感じに角度を付けてみた

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

3 軸目を増やした

src/app/page.tsx
function Scene() {
  const [rotation, setRotation] = useState(0);
  const material = <meshPhongMaterial color="#f3f3f3"></meshPhongMaterial>;

  // useFrame((state, delta) => setRotation(rotation + delta));

  return (
    <>
      <pointLight position={[10, 10, 10]}></pointLight>
      <OrbitControls></OrbitControls>
      <group rotation={[0, rotation - Math.PI / 4, 0]}>
        <Cylinder args={[1, 1, 2, 12]}>{material}</Cylinder>
        <Box position={[-0.5, 1.75, 0]} scale={[0.2, 1.5, 1]}>
          {material}
        </Box>
        <group rotation={[rotation - Math.PI / 4, 0, 0]} position={[0, 2, 0]}>
          <Cylinder
            args={[1, 1, 2, 12]}
            position={[-1.1, 0, 0]}
            scale={[0.5, 0.5, 0.5]}
            rotation={[0, 0, Math.PI / 2]}
          >
            {material}
          </Cylinder>
          <Box position={[0.1, 1, 0]} scale={[1, 3, 1]}>
            {material}
          </Box>
          <Box position={[-0.3, 3.2, 0]} scale={[0.2, 1.4, 1]}>
            {material}
          </Box>
          <group
            position={[0, 3.4, 0]}
            rotation={[rotation + Math.PI / 4, 0, 0]}
          >
            <Cylinder
              args={[1, 1, 2, 12]}
              position={[-0.8, 0, 0]}
              scale={[0.4, 0.4, 0.4]}
              rotation={[0, 0, Math.PI / 2]}
            >
              {material}
            </Cylinder>
            <Box position={[0.2, 0.15, 0]} scale={[0.8, 1, 1]}>
              {material}
            </Box>
            <Box position={[0.2, 1.15, -0.4]} scale={[0.8, 1, 0.2]}>
              {material}
            </Box>
          </group>
        </group>
      </group>
    </>
  );
}


だいぶロボットアームっぽくなってきた

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

4 軸めを増やした

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

import { Box, Cylinder, OrbitControls } from "@react-three/drei";
import { Canvas, useFrame } from "@react-three/fiber";
import { useState } from "react";

export default function Home() {
  return (
    <main>
      <h1>Home</h1>
      <Canvas style={{ width: "100vw", height: "100vh" }}>
        <Scene></Scene>
      </Canvas>
    </main>
  );
}

function Scene() {
  const [rotationAll, setRotationAll] = useState(0);
  const [rotation, setRotation] = useState(0);
  const material = <meshPhongMaterial color="#f3f3f3"></meshPhongMaterial>;

  // useFrame((state, delta) => setRotation(rotation + delta));
  useFrame((state, delta) => setRotation(rotation + delta));

  return (
    <>
      <pointLight position={[10, 10, 10]}></pointLight>
      <OrbitControls></OrbitControls>
      <group rotation={[0, rotationAll - Math.PI / 4, 0]}>
        <Cylinder args={[1, 1, 2, 12]}>{material}</Cylinder>
        <Box position={[-0.5, 1.75, 0]} scale={[0.2, 1.5, 1]}>
          {material}
        </Box>
        <group
          rotation={[rotationAll - Math.PI / 4, 0, 0]}
          position={[0, 2, 0]}
        >
          <Cylinder
            args={[1, 1, 2, 12]}
            position={[-1.1, 0, 0]}
            scale={[0.5, 0.5, 0.5]}
            rotation={[0, 0, Math.PI / 2]}
          >
            {material}
          </Cylinder>
          <Box position={[0.1, 1, 0]} scale={[1, 3, 1]}>
            {material}
          </Box>
          <Box position={[-0.3, 3.2, 0]} scale={[0.2, 1.4, 1]}>
            {material}
          </Box>
          <group
            position={[0, 3.4, 0]}
            rotation={[rotationAll + Math.PI / 4, 0, 0]}
          >
            <Cylinder
              args={[1, 1, 2, 12]}
              position={[-0.8, 0, 0]}
              scale={[0.4, 0.4, 0.4]}
              rotation={[0, 0, Math.PI / 2]}
            >
              {material}
            </Cylinder>
            <Box position={[0.2, 0.15, 0]} scale={[0.8, 1, 1]}>
              {material}
            </Box>
            <Box position={[0.2, 1.15, -0.4]} scale={[0.8, 1, 0.2]}>
              {material}
            </Box>
            <group
              position={[0.2, 1.2, 0]}
              rotation={[0, 0, rotation + (0 * Math.PI) / 4]}
            >
              <Cylinder
                args={[1, 1, 2, 12]}
                position={[0, 0, 0.06]}
                scale={[0.36, 0.36, 0.36]}
                rotation={[Math.PI / 2, 0, 0]}
              >
                {material}
              </Cylinder>

              <Box
                position={[0, 0, -1.3]}
                scale={[0.6, 1.6, 0.6]}
                rotation={[Math.PI / 2, 0, 0]}
              >
                {material}
              </Box>
            </group>
          </group>
        </group>
      </group>
    </>
  );
}


だいぶ頭がこんがらがってきた

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

5 軸目を増やす

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

import { Box, Cylinder, OrbitControls } from "@react-three/drei";
import { Canvas, useFrame } from "@react-three/fiber";
import { useState } from "react";

export default function Home() {
  return (
    <main>
      <h1>Home</h1>
      <Canvas style={{ width: "100vw", height: "100vh" }}>
        <Scene></Scene>
      </Canvas>
    </main>
  );
}

function Scene() {
  const [rotationAll, setRotationAll] = useState(0);
  const [rotation, setRotation] = useState(0);
  const material = <meshPhongMaterial color="#f3f3f3"></meshPhongMaterial>;

  // useFrame((state, delta) => setRotation(rotation + delta));
  useFrame((state, delta) => setRotation(rotation + delta));

  return (
    <>
      <pointLight position={[10, 10, 10]}></pointLight>
      <OrbitControls></OrbitControls>
      <group rotation={[0, rotationAll - Math.PI / 4, 0]}>
        <Cylinder args={[1, 1, 2, 12]}>{material}</Cylinder>
        <Box position={[-0.5, 1.75, 0]} scale={[0.2, 1.5, 1]}>
          {material}
        </Box>
        <group
          rotation={[rotationAll - Math.PI / 4, 0, 0]}
          position={[0, 2, 0]}
        >
          <Cylinder
            args={[1, 1, 2, 12]}
            position={[-1.1, 0, 0]}
            scale={[0.5, 0.5, 0.5]}
            rotation={[0, 0, Math.PI / 2]}
          >
            {material}
          </Cylinder>
          <Box position={[0.1, 1, 0]} scale={[1, 3, 1]}>
            {material}
          </Box>
          <Box position={[-0.3, 3.2, 0]} scale={[0.2, 1.4, 1]}>
            {material}
          </Box>
          <group
            position={[0, 3.4, 0]}
            rotation={[rotationAll + Math.PI / 4, 0, 0]}
          >
            <Cylinder
              args={[1, 1, 2, 12]}
              position={[-0.8, 0, 0]}
              scale={[0.4, 0.4, 0.4]}
              rotation={[0, 0, Math.PI / 2]}
            >
              {material}
            </Cylinder>
            <Box position={[0.2, 0.15, 0]} scale={[0.8, 1, 1]}>
              {material}
            </Box>
            <Box position={[0.2, 1.15, -0.4]} scale={[0.8, 1, 0.2]}>
              {material}
            </Box>
            <group
              position={[0.2, 1.2, 0]}
              rotation={[0, 0, rotationAll + (1 * Math.PI) / 4]}
            >
              <Cylinder
                args={[1, 1, 2, 12]}
                position={[0, 0, 0.06]}
                scale={[0.36, 0.36, 0.36]}
                rotation={[Math.PI / 2, 0, 0]}
              >
                {material}
              </Cylinder>

              <Box
                position={[0, 0, -1.3]}
                scale={[0.6, 1.6, 0.6]}
                rotation={[Math.PI / 2, 0, 0]}
              >
                {material}
              </Box>
              <Box
                position={[-0.25, 0, -1.3 - 1.6 / 2 - 0.8 / 2]}
                scale={[0.1, 0.8, 0.6]}
                rotation={[Math.PI / 2, 0, 0]}
              >
                {material}
              </Box>
              <group
                position={[0, 0, -2.6]}
                rotation={[rotationAll + (-1 * Math.PI) / 4, 0, 0]}
              >
                <Cylinder
                  args={[1, 1, 2, 12]}
                  position={[-0.54, 0, 0]}
                  scale={[0.24, 0.24, 0.24]}
                  rotation={[0, 0, Math.PI / 2]}
                >
                  {material}
                </Cylinder>
                <Box
                  position={[0, 0, -0.5]}
                  scale={[0.4, 1.4, 0.4]}
                  rotation={[Math.PI / 2, 0, 0]}
                >
                  {material}
                </Box>
              </group>
            </group>
          </group>
        </group>
      </group>
    </>
  );
}


もう何が何だかよくわからない

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

アニメーションを設ける

src/app/page.tsx
function Scene() {
  const [currentTime, setRotationAllT] = useState(0);
  const [rotation, setRotation] = useState(0);
  const material = <meshPhongMaterial color="#f3f3f3"></meshPhongMaterial>;

  useFrame((state, delta) => setRotationAllT(currentTime + delta));
  useFrame((state, delta) => setRotation(rotation + delta));

  const interval = 4000;
  const triangleWave = Math.abs(
    (((currentTime * 1000) % interval) * 2) / interval - 1.0
  );

  const maxRotationDegree = Math.PI / 4;
  const degreeToRotate = triangleWave * maxRotationDegree;

  return (
    <>
      <pointLight position={[10, 10, 10]}></pointLight>
      <OrbitControls></OrbitControls>
      <group rotation={[0, degreeToRotate - Math.PI / 4, 0]}>
        <Cylinder args={[1, 1, 2, 12]}>{material}</Cylinder>
        <Box position={[-0.5, 1.75, 0]} scale={[0.2, 1.5, 1]}>
          {material}
        </Box>
        <group
          rotation={[degreeToRotate - Math.PI / 4, 0, 0]}
          position={[0, 2, 0]}
        >
          <Cylinder
            args={[1, 1, 2, 12]}
            position={[-1.1, 0, 0]}
            scale={[0.5, 0.5, 0.5]}
            rotation={[0, 0, Math.PI / 2]}
          >
            {material}
          </Cylinder>
          <Box position={[0.1, 1, 0]} scale={[1, 3, 1]}>
            {material}
          </Box>
          <Box position={[-0.3, 3.2, 0]} scale={[0.2, 1.4, 1]}>
            {material}
          </Box>
          <group
            position={[0, 3.4, 0]}
            rotation={[degreeToRotate + Math.PI / 4, 0, 0]}
          >
            <Cylinder
              args={[1, 1, 2, 12]}
              position={[-0.8, 0, 0]}
              scale={[0.4, 0.4, 0.4]}
              rotation={[0, 0, Math.PI / 2]}
            >
              {material}
            </Cylinder>
            <Box position={[0.2, 0.15, 0]} scale={[0.8, 1, 1]}>
              {material}
            </Box>
            <Box position={[0.2, 1.15, -0.4]} scale={[0.8, 1, 0.2]}>
              {material}
            </Box>
            <group
              position={[0.2, 1.2, 0]}
              rotation={[0, 0, degreeToRotate + (1 * Math.PI) / 4]}
            >
              <Cylinder
                args={[1, 1, 2, 12]}
                position={[0, 0, 0.06]}
                scale={[0.36, 0.36, 0.36]}
                rotation={[Math.PI / 2, 0, 0]}
              >
                {material}
              </Cylinder>

              <Box
                position={[0, 0, -1.3]}
                scale={[0.6, 1.6, 0.6]}
                rotation={[Math.PI / 2, 0, 0]}
              >
                {material}
              </Box>
              <Box
                position={[-0.25, 0, -1.3 - 1.6 / 2 - 0.8 / 2]}
                scale={[0.1, 0.8, 0.6]}
                rotation={[Math.PI / 2, 0, 0]}
              >
                {material}
              </Box>
              <group
                position={[0, 0, -2.6]}
                rotation={[degreeToRotate + (-1 * Math.PI) / 4, 0, 0]}
              >
                <Cylinder
                  args={[1, 1, 2, 12]}
                  position={[-0.54, 0, 0]}
                  scale={[0.24, 0.24, 0.24]}
                  rotation={[0, 0, Math.PI / 2]}
                >
                  {material}
                </Cylinder>
                <Box
                  position={[0, 0, -0.5]}
                  scale={[0.4, 1.4, 0.4]}
                  rotation={[Math.PI / 2, 0, 0]}
                >
                  {material}
                </Box>
              </group>
            </group>
          </group>
        </group>
      </group>
    </>
  );
}


やっぱり動くと面白いな

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

今日はここまで

次回はまとめてクローズしよう。

いや、その前にスライダーなどでインタラクティブに動かせるようにすると面白いかも。

やってみよう。

円柱や直方体を STL にすればだいぶん見栄えがよくなりそうだ。

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

初期位置の変更

Scene にカメラを加える。

src/app/page.tsx(抜粋)
      <PerspectiveCamera
        makeDefault
        fov={75}
        near={0.1}
        far={1000}
        position={[0, 5, 15]}
      ></PerspectiveCamera>
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

スライダーの追加

コマンド
npm install jotai
src/app/page.tsx
"use client";

import {
  Box,
  Cylinder,
  OrbitControls,
  PerspectiveCamera,
} from "@react-three/drei";
import { Canvas, useFrame } from "@react-three/fiber";
import { atom, useAtom, useAtomValue } from "jotai";
import { FC, useState } from "react";

const axisDegreesAtom = atom([-45, -45, 45, 45, -45]);
const axisRadiansAtom = atom((get) =>
  get(axisDegreesAtom).map((degree) => (degree * Math.PI) / 180)
);

export default function Home() {
  return (
    <main>
      <h1>Robot Arm V2</h1>
      <form>
        {[...Array(5)].map((_, i) => (
          <AxisDegreeSlider index={i} key={i}></AxisDegreeSlider>
        ))}
      </form>
      <Canvas style={{ width: "100vw", height: "100vh" }}>
        <Scene></Scene>
      </Canvas>
    </main>
  );
}

const AxisDegreeSlider: FC<{ index: number }> = ({ index }) => {
  const [axisDegrees, setAxisDegrees] = useAtom(axisDegreesAtom);

  return (
    <div className="flex items-center">
      <label htmlFor={`axis${index}`} className="mr-3">
        Axis {index}
      </label>
      <input
        name={`axis${index}`}
        id={`axis${index}`}
        type="range"
        min={-180}
        max={180}
        step={1}
        value={axisDegrees[index]}
        onChange={(event) =>
          setAxisDegrees((axisDegrees) => [
            ...axisDegrees.slice(0, index),
            parseInt(event.target.value, 10),
            ...axisDegrees.slice(index + 1),
          ])
        }
      />
    </div>
  );
};

function Scene() {
  const axisRadians = useAtomValue(axisRadiansAtom);
  const [currentTime, setCurrentTime] = useState(0);
  const material = <meshPhongMaterial color="#f3f3f3"></meshPhongMaterial>;

  useFrame((state, delta) => setCurrentTime(currentTime + delta));

  const interval = 4000;
  const triangleWave = Math.abs(
    (((currentTime * 1000) % interval) * 2) / interval - 1.0
  );

  const maxRotationDegree = Math.PI / 4;
  const degreeToRotate = triangleWave * maxRotationDegree;

  return (
    <>
      <pointLight position={[10, 10, 10]}></pointLight>
      <OrbitControls></OrbitControls>
      <PerspectiveCamera
        makeDefault
        fov={75}
        near={0.1}
        far={1000}
        position={[0, 5, 15]}
      ></PerspectiveCamera>
      <group rotation={[0, axisRadians[0], 0]}>
        <Cylinder args={[1, 1, 2, 12]}>{material}</Cylinder>
        <Box position={[-0.5, 1.75, 0]} scale={[0.2, 1.5, 1]}>
          {material}
        </Box>
        <group rotation={[axisRadians[1], 0, 0]} position={[0, 2, 0]}>
          <Cylinder
            args={[1, 1, 2, 12]}
            position={[-1.1, 0, 0]}
            scale={[0.5, 0.5, 0.5]}
            rotation={[0, 0, Math.PI / 2]}
          >
            {material}
          </Cylinder>
          <Box position={[0.1, 1, 0]} scale={[1, 3, 1]}>
            {material}
          </Box>
          <Box position={[-0.3, 3.2, 0]} scale={[0.2, 1.4, 1]}>
            {material}
          </Box>
          <group position={[0, 3.4, 0]} rotation={[axisRadians[2], 0, 0]}>
            <Cylinder
              args={[1, 1, 2, 12]}
              position={[-0.8, 0, 0]}
              scale={[0.4, 0.4, 0.4]}
              rotation={[0, 0, Math.PI / 2]}
            >
              {material}
            </Cylinder>
            <Box position={[0.2, 0.15, 0]} scale={[0.8, 1, 1]}>
              {material}
            </Box>
            <Box position={[0.2, 1.15, -0.4]} scale={[0.8, 1, 0.2]}>
              {material}
            </Box>
            <group position={[0.2, 1.2, 0]} rotation={[0, 0, axisRadians[3]]}>
              <Cylinder
                args={[1, 1, 2, 12]}
                position={[0, 0, 0.06]}
                scale={[0.36, 0.36, 0.36]}
                rotation={[Math.PI / 2, 0, 0]}
              >
                {material}
              </Cylinder>

              <Box
                position={[0, 0, -1.3]}
                scale={[0.6, 1.6, 0.6]}
                rotation={[Math.PI / 2, 0, 0]}
              >
                {material}
              </Box>
              <Box
                position={[-0.25, 0, -1.3 - 1.6 / 2 - 0.8 / 2]}
                scale={[0.1, 0.8, 0.6]}
                rotation={[Math.PI / 2, 0, 0]}
              >
                {material}
              </Box>
              <group position={[0, 0, -2.6]} rotation={[axisRadians[4], 0, 0]}>
                <Cylinder
                  args={[1, 1, 2, 12]}
                  position={[-0.54, 0, 0]}
                  scale={[0.24, 0.24, 0.24]}
                  rotation={[0, 0, Math.PI / 2]}
                >
                  {material}
                </Cylinder>
                <Box
                  position={[0, 0, -0.5]}
                  scale={[0.4, 1.4, 0.4]}
                  rotation={[Math.PI / 2, 0, 0]}
                >
                  {material}
                </Box>
              </group>
            </group>
          </group>
        </group>
      </group>
    </>
  );
}


スライダーに連動するようになった

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

ローカルでビルドできる?

コマンド
npm run build
コンソール出力
> next build

- info Creating an optimized production build  
- info Compiled successfully
- info Linting and checking validity of types  
- info Collecting page data  
- info Generating static pages (4/4)
- info Finalizing page optimization  

Route (app)                                Size     First Load JS
┌ ○ /                                      207 kB          284 kB
└ ○ /favicon.ico                           0 B                0 B
+ First Load JS shared by all              76.9 kB
  ├ chunks/139-002b31d82a1d4f5d.js         24.5 kB
  ├ chunks/2443530c-7eefc34b15e3bd81.js    50.5 kB
  ├ chunks/main-app-9f2f089e5a0bf896.js    214 B
  └ chunks/webpack-64d95c37b5463075.js     1.68 kB

Route (pages)                              Size     First Load JS
─ ○ /404                                   178 B            86 kB
+ First Load JS shared by all              85.8 kB
  ├ chunks/main-ec7b1d67e0a9d0ca.js        83.9 kB
  ├ chunks/pages/_app-c544d6df833bfd4a.js  192 B
  └ chunks/webpack-64d95c37b5463075.js     1.68 kB

○  (Static)  automatically rendered as static HTML (uses no initial props)

ビルドは成功した。

特に何も設定しなくてもいけるかもしれない。

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

Cloudflare の設定


Cloudflare にログインして Workers & Pages の作成ページへ移動する

CLI もあるようだが数日前にリリースされたばかりのようだ。

ちょっと怖いけどせっかくだから使ってみようかな。

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

やはり GUI から

npm create cloudflare@latest を実行するとプロジェクトが作成されそうになり、違うなと思ったので素直に Web GUI を使ってデプロイする。

まずは「Git に接続」ボタンを押してリポジトリを選ぶ。


フレームワークプリセットを「Next.js」とする

下記の記事によると App Router にも対応しているようだ。

https://zenn.dev/microcms/articles/1b4331eca6e512

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

エラーメッセージ

当然成功したと思ったらまさかのエラーメッセージが表示された。

Error: Could not access built-in node modules, please make sure that your Cloudflare Pages project has the 'nodejs_compat' compatibility flag set.

どうやら nodejs_compat フラグが必要のようだ。

先に紹介した Zenn 記事がとても参考になる。

https://zenn.dev/microcms/articles/1b4331eca6e512?redirected=1

現時点だと static export でも十分な気がするがせっかくなのでこのまま進めてみよう。

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

振り返り

  • @react-three/fiber を使うと React / Next.js で宣言的に Three.js が使える。
  • @react-three/drei には便利なヘルパーがたくさんあるので自分で作らずまずは探してみる。
  • マウスで操作できるようにするには OrbitControls を使う。
  • 円柱が 6 角形など少ない状態でライティングすると法線ベクトルの関係で影が期待通りにはつかない。
  • X, Y, Z軸を中心に回転させるには group コンポーネントで囲んで rotation 属性を指定する。
  • group が X 軸を中心に回転するなら group の position 属性の X 成分は 0 になる、Y と Z も同様。
  • 初期位置を変更するには PerspectiveCamera を使う。
  • Cloudflare で Next.js(非 static export)アプリとして動かすには nodejs_compat フラグ指定が必要。
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

おわりに

@react-three/fiber はとても便利な一方で検索した時の情報量が生の Three.js より少ないので少し込み入ったことをする時には調べるのに時間がかかりそうだと感じた。

型定義についても自動補完優先であまり人間が読むようにはなっていないので型定義を見て使用可能なオプションを理解するというのも難しそうだ。

できれば @react-three/fiber でこのまま続けたいが挫折したら生の Three.js に戻ってくるかも知れない。

このスクラップは2023/05/24にクローズされました