🚀
第3回 React+TypeScriptなWebアプリで、QRコードをARしてみた。(カメラ編)
<- [第2回]React+TypeScriptなWebアプリで、QRコードをARしてみた。(QRコード読み込み編)
[第4回]React+TypeScriptなWebアプリで、QRコードをARしてみた。(検出補助線を引く編) ->
Abstract
React+TypescriptのWebアプリで、ARを実装してみた。第3回。
今回は、3Dキャラクタの背景にカメラ映像を表示させる。
3Dキャラクタは前回使ったモデルを使用する。
結論
今回の成果物はココ↓
前提
- React+Typescriptの開発環境は構築済 [環境構築]WindowsにVSCode+React+TypeScriptの開発環境を構築してみた。
- 前回プロジェクトから始める。ReactTs-QrAr002
- Webカメラは準備しておく。
手順
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