📖

React+TypeScript+R3Fのtutorial応用編1(annotations, GLTFSX, SVG)

2023/12/30に公開

Abstract

今回の参考はここ(House)。このソースをTypeScriptで実装しなおす

ポイント

  • Drei annotations
  • GLTFSXをつかう。
  • SVG描画
  • Tween.jsを使ってアニメーション

結論

今回の成果物はココ↓
https://github.com/aaaa1597/react-r3f-advanced001

前提

手順

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

めんどいから、このスケルトンコードから始める。React-Ts-Template
で、下記コマンドでフォルダ名とか整備する。

フォルダリネームとか
$ D:
$ cd .\Products\React.js\            # ご自身の適当なフォルダで。
$ rd /q /s D:\Products\React.js\react-r3f-advanced001
$ git clone https://github.com/aaaa1597/React-Ts-Template.git
$ rd /q /s React-Ts-Template/.git
$ ren React-Ts-Template react-r3f-advanced001
$ cd react-r3f-advanced001

準備

コマンドプロンプト
$ npm install --save three
$ npm install --save @types/three
$ npm install --save @react-three/fiber
$ npm install --save @react-three/drei
$ npm install --save @tweenjs/tween.js

準備2

参考はここ(House)のgithubからDLしたhouse-water-transformed.glb をプロジェクトの"react-r3f-tutorial018/public/assets/"配下にコピー。

D:\Products\React.js\react-r3f-advanced001>dir public\assets
 ドライブ D のボリューム ラベルは data です
 ボリューム シリアル番号は 277E-64C0 です

 D:\Products\React.js\react-r3f-advanced001\public\assets のディレクトリ

2023/12/30  22:02    <DIR>          .
2023/12/30  21:32    <DIR>          ..
2023/12/30  15:15         1,494,220 house-water-transformed.glb
               1 個のファイル           1,494,220 バイト
               2 個のディレクトリ  1,080,946,184,192 バイトの空き領域

npx gltfjsxコマンドを実行 -> TSXファイル生成

ここでハマった。windowsだとエラー吐いて実行できない。解決はここ
仕方ないので、ubuntuで実行した。
ubuntuで実行するときも、プロジェクト一式のpublic/assets配下に、house-water-transformed.glbをコピらないと失敗した。
原因不明。

コマンドプロンプト
$ cd ~/react-r3f-tutorial018
$ npx gltfjsx public/assets/house-water-transformed.glb --types --shadows 

で、~/配下に出力されたHouse-water-transformed.tsxを自分のプロジェクトに持ってくることで解決した。

.eslintrc.jsを修正

エラーになるので、ignoreに追加

.eslintrc.js
+        "react/no-unknown-property": ['error', { ignore: ['dispose', "rotation", "castShadow", 'receiveShadow', 'geometry', 'material'] }],

App.tsx

まず全体。

App.tsx
-import React from 'react';
+import React, { Suspense, useState, useRef } from 'react';
+import { Canvas, useFrame, useThree } from '@react-three/fiber'
+import { OrbitControls, OrbitControlsProps, Environment, useProgress, Html, Stats } from '@react-three/drei'
+import { OrbitControls as OrbitControlsImpl } from "three-stdlib"
import './App.css';
+import { Model } from './House'
+import TWEEN from '@tweenjs/tween.js'
+import annotations from './annotations.json'

+const Annotations = (props: {controls: React.MutableRefObject<OrbitControlsImpl>}) => {
+  const { camera } = useThree()
+  const [selected, setSelected] = useState(-1)
+
+  return (
+    <>{annotations.map((value, idx) => {
+        return(
+          <Html key={idx} position={[value.lookAt.x, value.lookAt.y, value.lookAt.z]}>
+            <svg height="34" width="34" transform="translate(-16 -16)" style={{ cursor: 'pointer' }}>
+              <circle cx="17" cy="17" r="16" stroke="white" strokeWidth="2" fill="rgba(0,0,0,.66)"
+                onPointerDown={() => {
+                  setSelected(idx)
+                  // change target
+                  new TWEEN.Tween(props.controls.current.target)
+                    .to({x: value.lookAt.x, y: value.lookAt.y,z: value.lookAt.z }, 1000 )
+                    .easing(TWEEN.Easing.Cubic.Out)
+                    .start()
+                  // change camera position
+                  new TWEEN.Tween(camera.position)
+                    .to({x: value.camPos.x, y: value.camPos.y, z: value.camPos.z }, 1000)
+                    .easing(TWEEN.Easing.Cubic.Out)
+                    .start()
+                }} />
+              <text x="12" y="22" fill="white" fontSize={17} fontFamily="monospace" style={{ pointerEvents: 'none' }}>
+                {idx + 1}
+              </text>
+            </svg>
+            {value.description && idx === selected &&
+              (<div id={'desc_' + idx} className="annotationDescription"
+                    dangerouslySetInnerHTML={{ __html: value.description }} />
+              )
+            }
+          </Html>
+        )
+      })
+    }</>
+  )
+}
+
+const Tween = () => {
+  useFrame(() => {
+    TWEEN.update()
+  })
+  return(<></>)
+ }

+const Loader = () => {
+  const { progress } = useProgress()
+  return <Html center>{progress} % loaded</Html>
+}

-function App() {
+const App = () => {
+  const ref = useRef<OrbitControlsImpl>(null!)
+
  return (
-    <div className="App">
-      hello world!!
+    <div style={{ width: "75vw", height: "75vh" }}>
+     <Canvas camera={{ position: [8, 2, 12] }}>
+       <OrbitControls ref={ref} target={[8, 2, 3]} />
+       <Suspense fallback={<Loader />}>
+         <Environment preset="forest" background blur={0.75} />
+         <Model />
+         <Annotations controls={ref} />
+         <Tween />
+       </Suspense>
+       <Stats />
+     </Canvas>
    </div>
  );
}

export default App;

npx gltfjsxで自動生成した分に、透過設定追加

House.tsx
export function Model(props: JSX.IntrinsicElements['group']) {
  const { nodes, materials } = useGLTF('assets/house-water-transformed.glb') as GLTFResult

+  materials.ground_1.transparent = true
+  materials.ground_1.opacity = 0.2
+  materials.ground_1.depthWrite = false
+  materials.wall_1_2.transparent = true
+  materials.wall_1_2.opacity = 0.2
+  materials.wall_1_2.depthWrite = false
+  materials.room_58_344.transparent = true
+  materials.room_58_344.opacity = 0.2
+  materials.room_58_344.depthWrite = false
+  materials.grey.transparent = true
+  materials.grey.opacity = 0.2
+  materials.grey.depthWrite = false
+  materials.flltgrey.transparent = true
+  materials.flltgrey.opacity = 0.2
+  materials.flltgrey.depthWrite = false
+  materials.flltgrey_sweethome3d_window_pane_420.transparent = true
+  materials.flltgrey_sweethome3d_window_pane_420.opacity = 0.2
+  materials.flltgrey_sweethome3d_window_pane_420.depthWrite = false
+  materials['default'].depthWrite = true
+  materials['default'].opacity = 0.2
+  materials['default'].depthWrite = false
+  materials.Glass.transparent = true
+  materials.Glass.opacity = 0.2
+  materials.Glass.depthWrite = false
+  materials.flltgrey_sweethome3d_window_pane_420.transparent = true
+  materials.flltgrey_sweethome3d_window_pane_420.opacity = 0.2
+  materials.flltgrey_sweethome3d_window_pane_420.depthWrite = false
+  materials.white_Fenetre_480.transparent = true
+  materials.white_Fenetre_480.opacity = 0.2
+  materials.white_Fenetre_480.depthWrite = false
+  materials.white_13_526.transparent = true
+  materials.white_13_526.opacity = 0.2
+  materials.white_13_526.depthWrite = false
+  materials.wall_1_2.transparent = true
+  materials.wall_1_2.opacity = 0.2
+  materials.wall_1_2.depthWrite = false
+  materials.glassblutint.transparent = true
+  materials.glassblutint.opacity = 0.2
+  materials.glassblutint.depthWrite = false
+  materials.Aluminium_652.transparent = true
+  materials.Aluminium_652.opacity = 0.2
+  materials.Aluminium_652.depthWrite = false
+  materials.GLASS.transparent = true
+  materials.GLASS.opacity = 0.2
+  materials.GLASS.depthWrite = false
+  materials.Glass_sweethome3d_window_mirror_985.transparent = true
+  materials.Glass_sweethome3d_window_mirror_985.opacity = 0.2
+  materials.Glass_sweethome3d_window_mirror_985.depthWrite = false

で、実行。


出来た!!

ハマりポイント

javascript → typescriptに変更するのに、結構ハマったので備忘録かねて。

  • 子要素つけろって怒られた!! 解決方法はココ

  • 型違いで、怒られた!! 解決方法はココ

  • propsを受け取る関数コンポーネントが分からんかった。
    ポイント1. 関数コンポーネントに引数を追加するときは、Propsの型定義をする。
    ポイント2. 関数コンポーネントを渡すときは、バラした状態になる。
    ポイント3. useRefの型は、React.MutableRefObject<なんちゃら>。

App.tsx
import { OrbitControls as OrbitControlsImpl } from "three-stdlib"

//                   ↓ポイント1        ↓ポイント3
const Annotations = (props: {controls: React.MutableRefObject<OrbitControlsImpl>}) => {
  return (
    <>
    </>
  )
}

  const ref = useRef<OrbitControlsImpl>(null!)
  <Annotations controls={ref} />
//             ↑ポイント2
  • windows11だと、npx gltfjsxコマンドが失敗する。
    windowsだとエラー吐いて実行できない。解決はここ

Discussion