トマシープが学ぶ

Unity/VR/AR/デザイン好きのミーハー 記事内容は自分用のメモです

React/r3f実装いろいろ【状態管理、スクロールエリア、ネームプレート、ARHitTest、GltfをS3からロードなど】

状態管理

zustandがいいらしい

qiita.com

zustand-demo.pmnd.rs

ChatGPTに言われるがままにしたので使い方が会ってるのかよくわからないが、とりあえず動いた。

store.tsというのを作って、そこでストアを作成する

// store.ts
import create from 'zustand';

type State = {
  name: string;
  setName: (name: string) => void;
};

export const useStore = create<State>*1;

変数をセットしたいコンポーネントでストアに保存

import { useStore } from './store';

// ...

const setName = useStore((state) => state.setName);

// ...

return (
  <Input onChange={(e) => setName(e.target.value)} />
  {/* ... */}
);

そして取得

import { useStore } from './store';

// ...

const name = useStore((state) => state.name);

// ...

<NamePlate name={name} /* ... */ />

スクロールエリア

画面内の一部をスクロールエリアにしたい。

はてなブログのブログ記事画面のように。

今はスクロールを一番下にしても真ん中の要素がフッター部分の下に行って見えない。

sectionなどで囲って、styleでoverflow: "auto", maxHeight: "500px"とすればよかった。

簡単。

r3fでネームプレート

r3fでアバターのネームプレートを表示する。

3Dのテキストだけだと見ずらいから、影やアウトラインをつけるか、背景にピル型の矩形をつけたい。

ピル型の背景をつけているサンプルがdreiにあった!

https://codesandbox.io/p/sandbox/the-three-graces-0n9it?file=%2Fsrc%2FApp.js%3A6%2C1-6%2C41

htmlを使っている。でもhtmlタグはそのままだと画面に張り付く。でもサンプルだと張り付いてないな・・・

 

transformって書くだけでbillboardにならなくなった。

  const pillStyle: CSSProperties = {
    backgroundColor: "rgba(0, 0, 0, 0.5)", // 半透明のグレー
    borderRadius: "50px", // ピル型
    padding: "8px 20px",
    display: "inline-block",
    color: "white",
    transform: "scale(0.2)",
  };
      <Html
        position={[position.x, position.y, position.z]}
        style={pillStyle}
        transform
        //occlude="blending"
        // geometry={
        //   /** The geometry is optional, it allows you to use any shape.
        //    *  By default it would be a plane. We need round edges here ...
        //    */
        //   <roundedPlaneGeometry args={[1.66, 0.47, 0.24]} />
        // }
      >
        {name}
      </Html>

geometryでroudedPlaneGeometry使わなくても別にピルにできる。

ちなみにgeometryはmaathという別のライブラリを使っている。使おうとしたが <roundedPlaneGeometry>はエラーが出た。

github.com

 

AR/VRモードに入るとhtmlで書いた要素は表示されなかった・・・

 

しょうがないのでhtml使わないことにした。Unityと同じ感じで、ネームプレート用の画像を用意して、それをplaneのマテリアルに設定する。

これでできた!透過画像は非同期に読み込まないと透過されなかった。

 

LookAt

ネームプレートをカメラに向けてlookat

Copilot様の言う通りですんなりできた。空間にカメラ置いてなくても問題なかった

import { useFrame, useThree } from "@react-three/fiber";

// このコンポーネント内で
const YourComponent = () => {
  const { camera } = useThree();

  // refを作成してオブジェクトにアクセスします
  const ref = useRef<THREE.Mesh>(null);

  useFrame(() => {
    // オブジェクトが存在し、カメラが存在する場合のみ実行します
    if (ref.current && camera) {
      ref.current.lookAt(camera.position);
    }
  });

  return (
    <mesh ref={ref}>
      {/* 他のプロパティや子要素 */}
    </mesh>
  );
};

 

あと複数の要素をまとめるのはgroupというタグを使うとできた。

その際、refはMeshからGroup属性に変えないといけなかった。

const ref = useRef<THREE.Group>(null);

VRMにアニメーション

pixivのサンプルだとmixamoのfbxを入れるサンプルはある

pixiv.github.io

github.com

ググってもmixamoのファイル入れる記事が多いのはこのせいなのか。

そしてvrmとvrm表示とanimation読み込みとstatus変化が別のコンポーネントにあるせいでどうやって自分のコードで読み込めばいいかわからない;;

シンプルな記事が欲しい

複雑すぎて記事では書けないが、なんだかんだできた。

余白

r3fを表示しているページで、Canvasの周りにちょっとだけ余白があって気になった。

  useEffect(() => {
    document.body.style.margin = '0';
  }, );

これを書いたらなくなった。

ant designの上書き

ant designのDrawerを使ったら、余白が結構大きく設定されてた。もう少し小さくしたい。

      <Drawer
        bodyStyle={{ padding: 0 }}

としたら上書き設定できた。でも非推奨とは出る。でもビルドしても動いてるしいいやろ。

cssを使った上書きの方法

github.com

フォントを変える

昔やった方法でCSSにフォント書いても適用されないと思ってたら、Next.jsのテンプレート特有の問題だった!もーー

layout.tsxにあるimport { Inter } from "next/font/google";を変えたらいい.

よくわからないけどnext.jsがいい感じにフォントを扱ってくれるらしい。

zenn.dev

これで一部変わったが、AntDesignのパーツのフォントはほとんど変わらない。

この記事+copilotでなんとかなった。

stackoverflow.com

global.cssなどでフォントを使えるようにしておいて

body {
  font-family: "Zen Maru Gothic", serif;
  background-color: rgb(249, 249, 249);
  font-weight: 400;
  font-style: normal;
}

layout.tsxでConfigProviderを使って設定したらうまくいった!

next/font/googleを使う方法はよくわからなかった

ARHitTest

平面検出してそこにARをだしたい。

reactではない元のWebXRAPIのサンプルでHitTestを動かせる。タップしたところにお花が咲く

immersive-web.github.io

webxr-samples/hit-test.html at main · immersive-web/webxr-samples · GitHub

react-xrにもサンプルがあった

github.com

このデモで↑のコードと同じような動きをするが、ボックスが平面に沿って動くだけで、よくあるタップしたらそこにモデル表示とか、平面を検出してくださいUIとかは入ってない。

https://5iff9.csb.app/

こちらのサンプルが、タップしたところにモデルが表示されて固定される!

github.com

デモ

r3f-fiber-draft.vercel.app

mapに配列としてタップしたときの位置を渡している?

        models.map(({ position, id }) => {
          return <Model key={id} position={position} />;
        })}

タップはレティクル自体をタップする方式だった。

        <Interactive onSelect={placeModel}>
          <mesh ref={reticleRef} rotation-x={-Math.PI / 2}>
            <ringGeometry args={[0.1, 0.25, 32]} />
            <meshStandardMaterial color={"white"} />
          </mesh>
        </Interactive>

あまり一般的ではない気がする。GoogleのネイティブのARは平面検知したらすぐにその場に出た。それがシンプルでいいかも!

あと平面検出してくださいUIは自分で出さないといけないのか・・・

平面検知したかどうかってどうやってとるのだろう?

useHitTest((hitMatrix: Matrix4, hit: XRHitTestResult) => {
  if (hitMatrix && hit) {
    // ヒットテストが成功した
  } else {
    // ヒットテストが失敗した
  }
});

でうまくいかない

ホームに追加したときのアイコン

Webページをスマホのホーム画面に追加したときのアイコンはファビコンが使われるわけじゃないらしい

<link rel="apple-touch-icon" href="画像パス" />

www.weblab.co.jp

でもnext.jsで画像読み込みってimageタグ使わないとうまくできない・・・

r3fでマテリアルにテクスチャ読み込むときもうまくできなかった。

imageタグを使わない読み込み方を教えてほしい

 

ファビコンと同じく、名前を設定してappフォルダの下に置けばいいらしい

nextjs.org

これでiOSでは表示されたけど、Androidは表示されない・・・

iconというのも設定した。

絶対パスじゃないといけないとかほかの記事に書いてたけどnext.jsでもそうなのかな

zenn.dev

この質問も同じ状況だ。そして解決していない・・・

https://www.reddit.com/r/nextjs/comments/1alwj4w/add_home_screen_icon_on_android_devices/?rdt=34862

GLTFファイルのS3からの読み込み

ローカルのgltfファイル読み込みはVRMファイル読み込みのスクリプトをほんの少し変えたらできた。

      loader.register((parser) => {
        return new VRMLoaderPlugin(parser)
      })

を消すだけ。

VRMファイル読み込みはこちらの記事を参照していた

synamon.hatenablog.com

ローカルの読み込みはできたが、S3に置いたものを読み込めない。copilot的には  loader.load(のあとのURLを"model/filename"みたいなローカルのパスから、"https://S3のURL"に置き換えるだけでいいはずなのに・・・

こちらの記事に書いてあるようにgltfを置いてあるS3のパケットの設定でCross-Origin Resource Sharing (CORS)を有効にしたら表示できた。

stackoverflow.com

書く中身はChatGPTに聞いた

[ { "AllowedHeaders": [ "*" ], "AllowedMethods": [ "GET" ], "AllowedOrigins": [ "*" ], "ExposeHeaders": , "MaxAgeSeconds": 3000 } ]

*1:set) => ({

  name: 'デフォルトの名前',
  setName: (name: string) => set({ name }),
}