🚀

第3回 React+TypeScriptなWebアプリで、QRコードをARしてみた。(カメラ編)

2024/02/12に公開

<- [第2回]React+TypeScriptなWebアプリで、QRコードをARしてみた。(QRコード読み込み編)
[第4回]React+TypeScriptなWebアプリで、QRコードをARしてみた。(検出補助線を引く編) ->


Abstract

React+TypescriptのWebアプリで、ARを実装してみた。第3回。
今回は、3Dキャラクタの背景にカメラ映像を表示させる。
3Dキャラクタは前回使ったモデルを使用する。

結論

今回の成果物はココ↓
https://github.com/aaaa1597/ReactTs-QrAr003

前提

手順

1.プロジェクト生成 -> VSCodeで開く

このテンプレートコードから始める。ReactTs-QrAr002

git cloneとか、フォルダリネームとか
$ D:
$ cd .\Products\React.js\            # ご自身の適当なフォルダに読み替えてね。
$ rd /q /s D:\Products\React.js\ReactTs-QrAr002
$ git clone https://github.com/aaaa1597/ReactTs-QrAr002.git
$ rd /q /s "ReactTs-QrAr002/.git"
$ ren ReactTs-QrAr002 ReactTs-QrAr003
$ cd ReactTs-QrAr003

準備

コマンドプロンプト
$ npm install

実行してみる

コマンドプロンプト
$ npm start


ここまでで、一旦動作するのが確認できる。

カメラ起動を実装

コマンドプロンプト
$ npm install react-zxing

App.css

透過設定する。

App.tscss
#root {
- background: #000000;
+ background: #00000000;
}

App.tsx

App.tsx
+import { useZxing } from "react-zxing";
+import ResultPoint from '@zxing/library/esm/core/ResultPoint';

useZxingとResultPointを使う設定。

App.tsx
+  const { ref } = useZxing({

Zxingを使う時のお約束。

App.tsx
+    constraints: {
+      audio: false,
+      video: {
+        facingMode: 'environment',
+        width: { min: 1024, ideal: 1920, max: 1920 },
+        height: { min: 576, ideal: 1080, max: 1080 },
+      },
+    },

カメラサイズを、"出来れば1920x1080になりますように"ってお願いしている。

App.tsx
+    onError(ret) {
+      console.log('onError::ret=', ret);
+    },
+    onDecodeError(ret) {
+      console.log('onDecodeError::ret=', ret);
+    },
+    onDecodeResult(result) {
+      console.log('onDecodeResult::result=', result);
+      if(result.getResultPoints().length <= 0) return;
+
+//        setResult(result.getText());
+
+      const points: ResultPoint[] = result.getResultPoints()
+      console.log( 'ref.current?.offsetLeft=', ref.current?.offsetLeft)
+      console.log(points.length, " -----[0]: ", points[0].getX(), " ,", points[0].getY(),)
+      console.log(points.length, " -----[1]: ", points[1].getX(), " ,", points[1].getY(),)
+      console.log(points.length, " -----[2]: ", points[2].getX(), " ,", points[2].getY(),)
+      console.log(points.length, " -----[3]: ", points[3].getX(), " ,", points[3].getY(),)
+    },
+  });

エラーハンドラと検知ハンドラ。カメラ起動しただけの素の状態だとonDecodeError()が動く。onDecodeErrorは引数に例外が渡される。

App.tsx
+  /* Videoサイズ変更に合わせてCanvasサイズを変更する */
+  useEffect(() => {
+    if(!ref.current) return;
+    props.setSize({width: ref.current.videoWidth, height: ref.current.videoHeight});
+  }, [ref.current?.videoWidth, ref.current?.videoHeight]);
+
+  console.log("ref.current?.videoxxx=(", ref.current?.videoWidth, ",", ref.current?.videoHeight, ")" );
+
+  return (
+    <video ref={ref} />
+  );
+};

Videoのサイズが確定した時点で、Canvasサイズを設定する処理。
ここのconsole.logの値は、undefined -> 0 -> 1920(1080)と変化する。

全体。

App.tsx
import React, { useEffect, Suspense, useRef, useState, useMemo } from 'react';
import './App.css';
import { Canvas, useLoader, useFrame } from '@react-three/fiber'
import * as THREE from 'three'
import { OrbitControls, useFBX } from '@react-three/drei'
import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader";
+import { useZxing } from "react-zxing";
+import ResultPoint from '@zxing/library/esm/core/ResultPoint';

const FBXModel = (props:{setActionName: React.Dispatch<React.SetStateAction<string>>}) => {
  /* FBXモデル読込み */
  const fbx = useLoader(FBXLoader, "assets/Ch09_nonPBR.fbx");
  /* AnimationClip(s)読込み */
  const animCrips: THREE.AnimationClip[][] = []
  animCrips[0] = useFBX('./assets/BreakdanceEnding2.fbx').animations
  animCrips[1] = useFBX('./assets/BreakdanceUprockVar1.fbx').animations
  animCrips[2] = useFBX('./assets/HipHopDancing.fbx').animations
  animCrips[3] = useFBX('./assets/NorthernSoulSpin.fbx').animations
  animCrips[4] = useFBX('./assets/SwingDancing.fbx').animations
  animCrips[5] = useFBX('./assets/BreakdanceEnding1.fbx').animations
  const animNames = ['BreakdanceEnding2', 'BreakdanceUprockVar1', 'HipHopDancing', 'NorthernSoulSpin', 'SwingDancing', 'BreakdanceEnding1']
  /* 変数定義 */
  const mixer = useRef<THREE.AnimationMixer>();
  const [ animIdx, setAnimIdx ] = useState<number>(0);
  const animActions = useMemo(() => [] as THREE.AnimationAction[], [])

  /* 初期化 */
  useEffect(() => {
    fbx.scale.multiplyScalar(0.02)
    mixer.current = new THREE.AnimationMixer(fbx)
    animCrips.forEach((val: THREE.AnimationClip[], idx: number) => {
      if(!mixer.current) return;
      animActions[idx] = mixer.current.clipAction(val[0])
    })
    new Promise(() => setTimeout(() => {0}, 1000)).then(()=>animActions[0].play())
  }, [])

  /* モーション切替え処理 */
  useEffect(() => {
    const act: THREE.AnimationAction = animActions[animIdx]
    act?.reset().fadeIn(0.3).play()
    props.setActionName(animNames[animIdx] + ' : ' + animIdx)
    return () => {
      act?.fadeOut(0.3)
    }
  }, [animIdx])

  /* FPS処理 */
  useFrame((state, delta) => {
    if(mixer.current)
      mixer.current.update(delta);
    const durationtime: number= animActions[animIdx].getClip().duration
    const currenttime: number = animActions[animIdx].time
    if(currenttime/durationtime > 0.9/*90%を超えたら次のモーションへ*/) {
      const index: number = (animIdx+1) % (animCrips.length)
      setAnimIdx( index )
    }
  });

  return (
    <primitive object={fbx} position={[1, -1, 1]} />
  )
}

+const ZxingQRCodeReader = (props:{setSize: React.Dispatch<React.SetStateAction<React.CSSProperties>>}) => {
+  const { ref } = useZxing({
+    constraints: {
+      audio: false,
+      video: {
+        facingMode: 'environment',
+        width: { min: 1024, ideal: 1920, max: 1920 },
+        height: { min: 576, ideal: 1080, max: 1080 },
+      },
+    },
+    onError(ret) {
+      console.log('onError::ret=', ret);
+    },
+    onDecodeError(ret) {
+      console.log('onDecodeError::ret=', ret);
+    },
+    onDecodeResult(result) {
+      console.log('onDecodeResult::result=', result);
+      if(result.getResultPoints().length <= 0) return;
+
+//        setResult(result.getText());
+
+      const points: ResultPoint[] = result.getResultPoints()
+      console.log( 'ref.current?.offsetLeft=', ref.current?.offsetLeft)
+      console.log(points.length, " -----[0]: ", points[0].getX(), " ,", points[0].getY(),)
+      console.log(points.length, " -----[1]: ", points[1].getX(), " ,", points[1].getY(),)
+      console.log(points.length, " -----[2]: ", points[2].getX(), " ,", points[2].getY(),)
+      console.log(points.length, " -----[3]: ", points[3].getX(), " ,", points[3].getY(),)
+    },
+  });
+
+  /* Videoサイズ変更に合わせてCanvasサイズを変更する */
+  useEffect(() => {
+    if(!ref.current) return;
+    props.setSize({width: ref.current.videoWidth, height: ref.current.videoHeight});
+  }, [ref.current?.videoWidth, ref.current?.videoHeight]);
+
+  console.log("ref.current?.videoxxx=(", ref.current?.videoWidth, ",", ref.current?.videoHeight, ")" );
+
+  return (
+    <video ref={ref} />
+  );
+};

const App = () => {
  const [actionName, setActionName] = useState<string>('aaabbb');
  const [size, setSize] = useState<React.CSSProperties>({width: "300px", height: "200px"});
  return (
-   <div style={{ width: "100vw", height: "75vh" }}>
+   <div>
+     <ZxingQRCodeReader setSize={setSize}/>
-     <Canvas camera={{ position: [3, 1, 3] }}>
+     <Canvas camera={{ position: [3, 1, 3] }} style={{ position: "absolute", left: "0px",  top: "0px", width: `${size.width}px`,  height: `${size.height}px`,}}>
        <ambientLight intensity={2} />
        <pointLight position={[40, 40, 40]} />
        <Suspense fallback={null}>
          <FBXModel setActionName={setActionName}/>
        </Suspense>
        <OrbitControls />
-       <Environment preset="forest" background />
        <axesHelper args={[5]} />
        <gridHelper />
      </Canvas>
-     <div id="summry">{actionName}</div>
+     <div id="summry" style={{background: "rgba(255, 192, 192, 0.7)"}}>{actionName}</div>
    </div>
  );
}

export default App;

で、実行。


出来た!! 背景にはカメラから取得した画像が表示されている。


<- [第2回]React+TypeScriptなWebアプリで、QRコードをARしてみた。(QRコード読み込み編)
[第4回]React+TypeScriptなWebアプリで、QRコードをARしてみた。(検出補助線を引く編) ->

Discussion