Open5

react-three/xrを使って、next.jsでVRアプリを作ってみた。#v0 #MetaQuest3

Riku OgawaRiku Ogawa

まずは、コード自動生成ができるAIツール「v0」を使って世界を作成。

とりあえず、簡単に50個のキューブと物理演算を追加してみました。

プロンプト履歴はこちら👇
https://v0.dev/chat/218ERJZqd2L

Riku OgawaRiku Ogawa

https://zenn.dev/tatsuyasusukida/scraps/d85f30e7acb460
こちらのサイトを参考に簡単にコードを作ってみようとしたが、どうやら最新のreact-three/xrライブラリは書き方が少し変わっているらしい。VRButtonが非推奨となっており、動かなかった。

https://github.com/pmndrs/xr
公式のドキュメントを見てみると、サンプルコードが載っていた。

import { Canvas } from '@react-three/fiber'
import { XR, createXRStore } from '@react-three/xr'
import { useState } from 'react'

const store = createXRStore()

export function App() {
  const [red, setRed] = useState(false)
  return (
    <>
      <button onClick={() => store.enterAR()}>Enter AR</button>
      <Canvas>
        <XR store={store}>
          <mesh pointerEventsType={{ deny: 'grab' }} onClick={() => setRed(!red)} position={[0, 1, -1]}>
            <boxGeometry />
            <meshBasicMaterial color={red ? 'red' : 'blue'} />
          </mesh>
        </XR>
      </Canvas>
    </>
  )
}

必要な作業としては、

  • const store = createXRStore()を追加
  • <button onClick={() => store.enterVR()}>Enter VR</button>を追加
    • 今回はVRなのでAR→VRに変更
  • <XR>タグでメッシュを囲う

これだけで良いっぽい。

Riku OgawaRiku Ogawa

create-next-appでnextアプリを作成後、page.jsを下のように書き換える。
※型定義がめんどくさいのでtypescript使わずにjsで実装している

// app/page.js
"use client";

import { Canvas } from "@react-three/fiber";
import { XR, createXRStore } from "@react-three/xr";
import Scene from "../components/scene"

export default function Home() {
  const store = createXRStore({});
  return (
    <main className="container mx-auto">
      <h1 className="mt-4 mb-4 text-4xl">React Three XR</h1>
      <button onClick={() => store.enterVR()}>Enter VR</button>
      <Canvas
        shadows
        className="aspect-video border border-gray-300"
        camera={{ position: [0, 20, 20], fov: 50 }}
      >
        <XR store={store} >
          <Scene />
        </XR>
      </Canvas>
    </main>
  );
}

v0で作ったコードを張り付け👇

今回は物体を掴む物理演算を行いたかったので、その部分は手動で修正を加えています。
react-three/xr公式リポジトリに載っていたexamples内で使っているコードをそのまま持ってきている。
https://github.com/pmndrs/xr/blob/main/examples/rag-doll/src/helpers/Drag.js

// components/scene.js
"use client";

import { Suspense, useEffect, useRef, useState } from "react";
import { Physics, usePlane, useBox } from "@react-three/cannon";
import {
  AccumulativeShadows,
  RandomizedLight,
} from "@react-three/drei";
import { Cursor } from "./Drag"
import { useDragConstraint } from "./Drag";

function Box({ color }) {
  const [ref] = useBox(() => ({
    mass: 1,
    position: [
      (Math.random() - 0.5) * 0.5,
      (Math.random() + 5) * 0.5,
      (Math.random() - 0.5) * 0.5,
    ],
    rotation: [Math.random(), Math.random(), Math.random()],
    args: [0.1, 0.1, 0.1],
  }));
  const bind = useDragConstraint(ref);

  return (
    <mesh ref={ref} {...bind } castShadow>
      <boxGeometry args={[0.1, 0.1, 0.1]} />
      <meshStandardMaterial color={color} roughness={0.5} metalness={0.5} />
    </mesh>
  );
}

function Plane() {
  const [ref] = usePlane(() => ({ rotation: [-Math.PI / 2, 0, 0], position: [0, 1, 0] }));
  return (
    <mesh ref={ref} receiveShadow>
      <planeGeometry args={[5, 5]} />
      <meshStandardMaterial color="#f0f0f0" roughness={0.4} metalness={0.3} />
    </mesh>
  );
}

function Lights() {
  return (
    <>
      <ambientLight intensity={0.8} />
      <directionalLight
        position={[10, 20, 10]}
        intensity={1.5}
        castShadow
        shadow-mapSize-width={4096}
        shadow-mapSize-height={4096}
        shadow-camera-left={-25}
        shadow-camera-right={25}
        shadow-camera-top={25}
        shadow-camera-bottom={-25}
        shadow-camera-near={0.1}
        shadow-camera-far={100}
      />
      <directionalLight
        position={[-10, 20, -10]}
        intensity={1}
        castShadow
        shadow-mapSize-width={4096}
        shadow-mapSize-height={4096}
        shadow-camera-left={-25}
        shadow-camera-right={25}
        shadow-camera-top={25}
        shadow-camera-bottom={-25}
        shadow-camera-near={0.1}
        shadow-camera-far={100}
      />
      <pointLight position={[0, 20, 0]} intensity={0.5} />
    </>
  );
}

function Shadows() {
  return (
    <AccumulativeShadows
      temporal
      frames={100}
      color="#316d39"
      colorBlend={0.5}
      opacity={1}
      scale={20}
      position={[0, -0.01, 0]}
    >
      <RandomizedLight
        amount={8}
        radius={10}
        ambient={0.5}
        position={[5, 5, -10]}
        bias={0.001}
      />
    </AccumulativeShadows>
  );
}

export default function Scene() {
  const [boxes, setBoxes] = useState([]);

  useEffect(() => {
    const colors = [
      "#ff6b6b",
      "#4ecdc4",
      "#45aaf2",
      "#fed330",
      "#fd9644",
      "#a55eea",
    ];
    setBoxes(
      Array.from({ length: 50 }, (_, i) => (
        <Box
          key={i}
          color={colors[Math.floor(Math.random() * colors.length)]}
        />
      ))
    );
  }, []);

  return (
    <>
      <color attach="background" args={["#f0f0f0"]} />
      <Lights />
      <Shadows />
      <Suspense>
        <Physics>
          <Cursor />
          <Plane />
          {boxes}
        </Physics>
      </Suspense>
    </>
  );
}

フォルダ構成

Riku OgawaRiku Ogawa
npm run dev --experimental-https

を実行してhttpsでローカルサーバーを起動します。

httpだと、MetaQuest3からアクセスしたときにWebXR not supportedと表示されてしまう👇
https://github.com/pmndrs/xr/discussions/327

こんな感じで表示される👇(めちゃくちゃちっさいのはVR用にサイズ調整しているせいです)

EnterVRを押して起動!
PCからアクセスすると、VRの動きをエミュレーションできる!