[Three.js] R3FがOffScreenに対応したらしいので使ってみる

# はじめに
Paul氏とR3Fコミュニティに感謝します。
R3F(ReactThreeFiber)はThree.jsをReactで簡単に使えるようにしたライブラリです。
R3Fのコミュニティは、毎日のように面白いプロジェクトやリソースが公開されます。

その中で、今回は先月OffscreenCanvasに対応されたということなので早速使ってみます。
Offscreenとは、WebGLでのレンダリングをメインスレッド外、つまりはWebWorkerで実行し、
メインスレッドの処理に依存せずに描画できるため、パフォーマンスの向上が狙えます。
このライブラリのすごいところは、WebWorkerを意識することなく、実装できることです。

https://github.com/pmndrs/react-three-offscreen

## サンプルの実装

```bash
# Create: ReactApp
npx create-react-app oscreen --template typescript
# install
npm install three @types/three @react-three/fiber @react-three/drei @react-three/offscreen
# 実行
npm start
```

### ディレクトリ構成
```
oscreen
└ src
   ┝ App.tsx
   ┝ Scene.tsx
   └ worker.jsx
```

### ソースコード

App.tsx
```react:App.tsx
import React, { lazy } from 'react';
import { Canvas } from "@react-three/offscreen"

const Scene = lazy(() => import("./Scene"))

const worker = new Worker(new URL("./worker.jsx", import.meta.url), { type: "module" })

function App() {
  return (
    <div style={{ height: "100vh" }}>
      <Canvas 
        dpr={[1, 1.5]}
        camera={{ position: [0, 0, 10], fov: 25 }} 
        worker={worker} 
        fallback={<Scene />}
      />
    </div>
  );
}

export default App;
```


Scene.tsx
```react:Scene.tsx
import React, { useRef, useState } from 'react'
import { useFrame } from '@react-three/fiber'
import { useGLTF, Center, ContactShadows, Environment, CameraControls } from '@react-three/drei'

const Scene = () => {
    const mesh = useRef<any>()
    const { nodes, materials } = useGLTF('/pmndrs.glb') as any;
    const [hovered, setHover] = useState(false)
    const [active, setActive] = useState(false)
    const color = hovered ? 'hotpink' : 'orange'
    useFrame((state, delta) => {
      mesh.current.rotation.x += delta / 2
      mesh.current.rotation.y += delta / 2
    })
    return (
      <>
        <Center ref={mesh}>
          <mesh
            geometry={nodes.cube.geometry}
            material={materials.base}
            material-color={color}
            scale={active ? 0.3 : 0.25}
            onClick={(e) => (e.stopPropagation(), setActive(!active))}
            onPointerOver={(e) => (e.stopPropagation(), setHover(true))}
            onPointerOut={(e) => setHover(false)}
          />
        </Center>
        <ContactShadows color={color} position={[0, -1.5, 0]} blur={3} opacity={0.75} />
        <ambientLight />
        <pointLight position={[10, 10, 5]} />
        <Environment preset="city">

        </Environment>
        <CameraControls minPolarAngle={Math.PI / 2} maxPolarAngle={Math.PI / 2} />
      </>
    )
}

export default Scene;
```

worker.jsx

```jsx:worker.jsx
import React from 'react'
import { render } from '@react-three/offscreen'
import Scene from './Scene'

render(<Scene />)
```

### 実行結果

![oscreen1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/493888/59a43fd9-512d-0147-ad9f-bdce02a683e5.gif)

ReactのStateやそのほかのdreiリソース、
fiberのuseFrameなどを利用できることを確認しました。

## その他のpmndrsのリソースが使えるかどうか

- PostProcessing/Shaderは使えるか？
- WebXRは使えるか？
これらを確認していきます

### PostProcessing/Shaderは使えるか?

Scene.tsx
```react:Scene.tsx
import React, { useRef, useState, useMemo, useEffect } from 'react'
import { useFrame, applyProps, useLoader } from '@react-three/fiber'
import { useGLTF, Center, ContactShadows, Environment, Float, OrbitControls, Lightformer, MeshReflectorMaterial } from '@react-three/drei'
import { EffectComposer, SSR, Bloom, LUT } from "@react-three/postprocessing";
import { Mesh, MeshPhysicalMaterial, Texture } from 'three';
import { LUTCubeLoader } from 'postprocessing'

const Scene = () => {
  return (
    <>
      <color attach="background" args={['#15151a']} />
      <Lambo rotation={[0, Math.PI / 1.5, 0]} scale={0.015} />
      <hemisphereLight intensity={0.5} />
      <ContactShadows resolution={1024} frames={1} position={[0, -1.16, 0]} scale={15} blur={0.5} opacity={1} far={20} />
      <mesh scale={4} position={[3, -1.161, -1.5]} rotation={[-Math.PI / 2, 0, Math.PI / 2.5]}>
        <ringGeometry args={[0.9, 1, 4, 1]} />
        <meshStandardMaterial color="red" roughness={0.75} />
      </mesh>
      <mesh scale={4} position={[-3, -1.161, -1]} rotation={[-Math.PI / 2, 0, Math.PI / 2.5]}>
        <ringGeometry args={[0.9, 1, 3, 1]} />
        <meshStandardMaterial color="gold" roughness={0.75} />
      </mesh>
      <mesh rotation={[-Math.PI/2, 0, 0]} position={[0, -1.162, 0]} >
        <planeBufferGeometry args={[32, 32]}/>
        <MeshReflectorMaterial mirror={1} resolution={1024} />
      </mesh>
      <Environment resolution={512} preset='city' frames={Infinity}>
        {/* Ceiling */}
        <Lightformer intensity={2} rotation-x={Math.PI / 2} position={[0, 4, -9]} scale={[10, 1, 1]} />
        <Lightformer intensity={2} rotation-x={Math.PI / 2} position={[0, 4, -6]} scale={[10, 1, 1]} />
        <Lightformer intensity={2} rotation-x={Math.PI / 2} position={[0, 4, -3]} scale={[10, 1, 1]} />
        <Lightformer intensity={2} rotation-x={Math.PI / 2} position={[0, 4, 0]} scale={[10, 1, 1]} />
        <Lightformer intensity={2} rotation-x={Math.PI / 2} position={[0, 4, 3]} scale={[10, 1, 1]} />
        <Lightformer intensity={2} rotation-x={Math.PI / 2} position={[0, 4, 6]} scale={[10, 1, 1]} />
        <Lightformer intensity={2} rotation-x={Math.PI / 2} position={[0, 4, 9]} scale={[10, 1, 1]} />
        {/* Sides */}
        <Lightformer intensity={2} rotation-y={Math.PI / 2} position={[-50, 2, 0]} scale={[100, 2, 1]} />
        <Lightformer intensity={2} rotation-y={-Math.PI / 2} position={[50, 2, 0]} scale={[100, 2, 1]} />
        <Lightformer rotation-y={-Math.PI / 2} position={[10, 1, 0]} scale={[20, 1, 1]} />
        {/* Accent (gold) */}
        <Float speed={5} floatIntensity={2} rotationIntensity={2}>
          <Lightformer form="ring" color="gold" intensity={1} scale={10} position={[-15, 4, -18]} target={[0, 0, 0]} />
        </Float>
        {/* Key */}
        <Lightformer form="ring" color="gold" intensity={10} scale={2} position={[10, 5, 10]} onUpdate={(self) => self.lookAt(0, 0, 0)} />
      </Environment>
      <OrbitControls enablePan={false} minPolarAngle={Math.PI / 2.2} maxPolarAngle={Math.PI / 2.2} autoRotate autoRotateSpeed={0.4} />
      <PostProcess />
    </>
  )
}

const Lambo = (props: any) => {
  const { scene, nodes, materials } = useGLTF('/lambo.glb') as any;
  useMemo(() => {
    // ⬇⬇⬇ All this is probably better fixed in Blender ...
    Object.values(nodes).forEach((node) => {
      if (node instanceof Mesh && node.isMesh) {
        // Fix glas, normals look messed up in the original, most likely deformed meshes bc of compression :/
        if (node.name.startsWith('glass')) node.geometry.computeVertexNormals()
        // Fix logo, too dark
        if (node.name === 'silver_001_BreakDiscs_0') node.material = applyProps(materials.BreakDiscs.clone(), { color: '#ddd' })
      }
    })
    // Fix windows, they have to be inset some more
    nodes['glass_003'].scale.setScalar(2.7)
    // Fix inner frame, too light
    applyProps(materials.FrameBlack, { metalness: 0.75, roughness: 0, color: 'black' })
    // Wheels, change color from chrome to black matte
    applyProps(materials.Chrome, { metalness: 1, roughness: 0, color: '#333' })
    applyProps(materials.BreakDiscs, { metalness: 0.2, roughness: 0.2, color: '#555' })
    applyProps(materials.TiresGum, { metalness: 0, roughness: 0.4, color: '#181818' })
    applyProps(materials.GreyElements, { metalness: 0, color: '#292929' })
    // Make front and tail LEDs emit light
    applyProps(materials.emitbrake, { emissiveIntensity: 3, toneMapped: false })
    applyProps(materials.LightsFrontLed, { emissiveIntensity: 3, toneMapped: false })
    // Paint, from yellow to black
    nodes.yellow_WhiteCar_0.material = new MeshPhysicalMaterial({
      roughness: 0.3,
      metalness: 0.05,
      color: '#820000',
      envMapIntensity: 0.75,
      clearcoatRoughness: 0,
      clearcoat: 1
    })
  }, [nodes, materials])
  return <primitive object={scene} {...props} />
}

const PostProcess = () => {
  const [texture, setTexture] = useState<Texture>();
  useEffect(() => {
    const loader = new LUTCubeLoader();
    loader.load('/F-6800-STD.cube', (loadedTexture: Texture) => {
      setTexture(loadedTexture);
    });
  }, []);
  let lut: JSX.Element = (<></>);
  if (texture){
    lut = <LUT lut={texture} />
  }
  return (
    <EffectComposer disableNormalPass>
      <Bloom luminanceThreshold={0.2} mipmapBlur luminanceSmoothing={0} intensity={1.45} />
      {lut}
    </EffectComposer>
  )
}

export default Scene;
```

### 実行結果

![oscreen2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/493888/4cb6332c-051a-f3d8-c3c2-265dfcddffd5.gif)

ちゃんと使えそうでした。

### WebXRは使えるか

```react:Scene.tsx
import React, { useRef, useState, useMemo, useEffect } from 'react'
import { useFrame, applyProps, useLoader } from '@react-three/fiber'
import { useGLTF, useFont, Text3D, ContactShadows, Environment, Float, OrbitControls, Lightformer, MeshReflectorMaterial } from '@react-three/drei'
import { EffectComposer, Bloom, LUT } from "@react-three/postprocessing";
import { Mesh, MeshPhysicalMaterial, Texture } from 'three';
import { LUTCubeLoader } from 'postprocessing';
import { XR, startSession } from "@react-three/xr";


const Scene = () => {
  const font = useFont("/MPLUS.json");
  const fontdata = font.data;
  const startXR = () => {
    startSession("immersive-vr", undefined);
  }
  return (
    <XR>
      <Text3D font={fontdata} rotation={[0, Math.PI / 1.5, 0]} position={[-1.25, 3, 3.5]} onClick={startXR}>
        XRで実行
      </Text3D>
      <color attach="background" args={['#15151a']} />
      <Lambo rotation={[0, Math.PI / 1.5, 0]} scale={0.015} />
      <hemisphereLight intensity={0.5} />
      <ContactShadows resolution={1024} frames={1} position={[0, -1.16, 0]} scale={15} blur={0.5} opacity={1} far={20} />
      <mesh scale={4} position={[3, -1.161, -1.5]} rotation={[-Math.PI / 2, 0, Math.PI / 2.5]}>
        <ringGeometry args={[0.9, 1, 4, 1]} />
        <meshStandardMaterial color="red" roughness={0.75} />
      </mesh>
      <mesh scale={4} position={[-3, -1.161, -1]} rotation={[-Math.PI / 2, 0, Math.PI / 2.5]}>
        <ringGeometry args={[0.9, 1, 3, 1]} />
        <meshStandardMaterial color="gold" roughness={0.75} />
      </mesh>
      <mesh rotation={[-Math.PI/2, 0, 0]} position={[0, -1.162, 0]} >
        <planeBufferGeometry args={[32, 32]}/>
        <MeshReflectorMaterial mirror={1} resolution={1024} />
      </mesh>
      <Environment resolution={512} preset='city' frames={Infinity}>
        <Lightformer intensity={2} rotation-x={Math.PI / 2} position={[0, 4, -9]} scale={[10, 1, 1]} />
        <Lightformer intensity={2} rotation-x={Math.PI / 2} position={[0, 4, -6]} scale={[10, 1, 1]} />
        <Lightformer intensity={2} rotation-x={Math.PI / 2} position={[0, 4, -3]} scale={[10, 1, 1]} />
        <Lightformer intensity={2} rotation-x={Math.PI / 2} position={[0, 4, 0]} scale={[10, 1, 1]} />
        <Lightformer intensity={2} rotation-x={Math.PI / 2} position={[0, 4, 3]} scale={[10, 1, 1]} />
        <Lightformer intensity={2} rotation-x={Math.PI / 2} position={[0, 4, 6]} scale={[10, 1, 1]} />
        <Lightformer intensity={2} rotation-x={Math.PI / 2} position={[0, 4, 9]} scale={[10, 1, 1]} />
        <Lightformer intensity={2} rotation-y={Math.PI / 2} position={[-50, 2, 0]} scale={[100, 2, 1]} />
        <Lightformer intensity={2} rotation-y={-Math.PI / 2} position={[50, 2, 0]} scale={[100, 2, 1]} />
        <Lightformer rotation-y={-Math.PI / 2} position={[10, 1, 0]} scale={[20, 1, 1]} />
        <Float speed={5} floatIntensity={2} rotationIntensity={2}>
          <Lightformer form="ring" color="gold" intensity={1} scale={10} position={[-15, 4, -18]} target={[0, 0, 0]} />
        </Float>
        <Lightformer form="ring" color="gold" intensity={10} scale={2} position={[10, 5, 10]} onUpdate={(self) => self.lookAt(0, 0, 0)} />
      </Environment>
      <OrbitControls enablePan={false} minPolarAngle={Math.PI / 2.2} maxPolarAngle={Math.PI / 2.2} autoRotate autoRotateSpeed={0.4} />
      <PostProcess />
    </XR>
  )
}

const Lambo = (props: any) => {
  const { scene, nodes, materials } = useGLTF('/lambo.glb') as any;
  useMemo(() => {
    // ⬇⬇⬇ All this is probably better fixed in Blender ...
    Object.values(nodes).forEach((node) => {
      if (node instanceof Mesh && node.isMesh) {
        // Fix glas, normals look messed up in the original, most likely deformed meshes bc of compression :/
        if (node.name.startsWith('glass')) node.geometry.computeVertexNormals()
        // Fix logo, too dark
        if (node.name === 'silver_001_BreakDiscs_0') node.material = applyProps(materials.BreakDiscs.clone(), { color: '#ddd' })
      }
    })
    // Fix windows, they have to be inset some more
    nodes['glass_003'].scale.setScalar(2.7)
    // Fix inner frame, too light
    applyProps(materials.FrameBlack, { metalness: 0.75, roughness: 0, color: 'black' })
    // Wheels, change color from chrome to black matte
    applyProps(materials.Chrome, { metalness: 1, roughness: 0, color: '#333' })
    applyProps(materials.BreakDiscs, { metalness: 0.2, roughness: 0.2, color: '#555' })
    applyProps(materials.TiresGum, { metalness: 0, roughness: 0.4, color: '#181818' })
    applyProps(materials.GreyElements, { metalness: 0, color: '#292929' })
    // Make front and tail LEDs emit light
    applyProps(materials.emitbrake, { emissiveIntensity: 3, toneMapped: false })
    applyProps(materials.LightsFrontLed, { emissiveIntensity: 3, toneMapped: false })
    // Paint, from yellow to black
    nodes.yellow_WhiteCar_0.material = new MeshPhysicalMaterial({
      roughness: 0.3,
      metalness: 0.05,
      color: '#820000',
      envMapIntensity: 0.75,
      clearcoatRoughness: 0,
      clearcoat: 1
    })
  }, [nodes, materials])
  return <primitive object={scene} {...props} />
}

const PostProcess = () => {
  const [texture, setTexture] = useState<Texture>();
  useEffect(() => {
    const loader = new LUTCubeLoader();
    loader.load('/F-6800-STD.cube', (loadedTexture: Texture) => {
      setTexture(loadedTexture);
    });
  }, []);
  let lut: JSX.Element = (<></>);
  if (texture){
    lut = <LUT lut={texture} />
  }
  return (
    <EffectComposer disableNormalPass>
      <Bloom luminanceThreshold={0.2} mipmapBlur luminanceSmoothing={0} intensity={1.45} />
      {lut}
    </EffectComposer>
  )
}

export default Scene;
```

重くて表示はできないようですが、XRも実行可能のようです。

![画像1.png](https://qiita-image-store.s3.ap-northeast-
1.amazonaws.com/0/493888/2844b8f4-3c0d-8f98-cecb-852a544b74e2.png)

# 最後にパフォーマンス比較
動くBoxをさらに５００個おいて比較しました。
通常：30~45FPS
OScreen：40~50FPS

単純な比較のみでも効果があった。
この上に通常のReactのHTMLレンダリング等でプロジェクトを作りこむと、この差は大きくなるんだと思います。


![oscreen4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/493888/12624f04-e655-9628-ab7e-c691b8fc6564.gif)

比較ソースコード

```react:App.tsx(Normal)
import React, { lazy } from 'react';
// import { Canvas } from "@react-three/offscreen"
import { Canvas } from "@react-three/fiber";

const Scene = lazy(() => import("./Scene"))

const worker = new Worker(new URL("./worker.jsx", import.meta.url), { type: "module" })

function App() {
  return (
    <div style={{ height: "100vh" }}>
      {/* <Canvas 
        gl={{ logarithmicDepthBuffer: true, antialias: false }}
        dpr={[1, 1.5]}
        camera={{ position: [0, 0, -16], fov: 25 }} 
        worker={worker} 
        fallback={<Scene />}
      /> */}
      <Canvas
        gl={{ logarithmicDepthBuffer: true, antialias: false }}
        dpr={[1, 1.5]}
        camera={{ position: [0, 0, -16], fov: 25 }} 
      >
        <Scene/>
      </Canvas>
    </div>
  );
}
```

```react:App.tsx(OffScreen)
import React, { lazy } from 'react';
import { Canvas } from "@react-three/offscreen"
// import { Canvas } from "@react-three/fiber";

const Scene = lazy(() => import("./Scene"))

const worker = new Worker(new URL("./worker.jsx", import.meta.url), { type: "module" })

function App() {
  return (
    <div style={{ height: "100vh" }}>
      <Canvas 
        gl={{ logarithmicDepthBuffer: true, antialias: false }}
        dpr={[1, 1.5]}
        camera={{ position: [0, 0, -16], fov: 25 }} 
        worker={worker} 
        fallback={<Scene />}
      />
      {/* <Canvas
        gl={{ logarithmicDepthBuffer: true, antialias: false }}
        dpr={[1, 1.5]}
        camera={{ position: [0, 0, -16], fov: 25 }} 
      >
        <Scene/>
      </Canvas> */}
    </div>
  );
}

export default App;

```

```react:Scene.tsx
import React, { useRef, useState, useMemo, useEffect, Suspense } from 'react'
import { useFrame, applyProps, useLoader } from '@react-three/fiber'
import { useGLTF, useFont, Text3D, ContactShadows, Environment, Float, OrbitControls, Lightformer, MeshReflectorMaterial } from '@react-three/drei'
import { EffectComposer, Bloom, LUT } from "@react-three/postprocessing";
import { Color, Mesh, MeshPhysicalMaterial, Texture, Vector3 } from 'three';
import { LUTCubeLoader } from 'postprocessing';
import { XR, startSession } from "@react-three/xr";
import { Perf } from "r3f-perf";

const Scene = () => {
  const font = useFont("/MPLUS.json");
  const fontdata = font.data;
  const startXR = () => {
    startSession("immersive-vr", undefined);
  }
  return (
    <XR>
      <Text3D font={fontdata} rotation={[0, Math.PI / 1.5, 0]} position={[0, 2.5, 3.5]} onClick={startXR}>
        ピッカピカ
      </Text3D>
      <Text3D font={fontdata} size={0.45} rotation={[0, Math.PI / 1.5, 0]} position={[0, 1.5, 1.75]}>
        R3Fは楽しい
      </Text3D>
      <color attach="background" args={['#15151a']} />
      <Lambo rotation={[0, Math.PI / 1.5, 0]} scale={0.015} />
      <hemisphereLight intensity={0.5} />
      <ContactShadows resolution={1024} frames={1} position={[0, -1.16, 0]} scale={15} blur={0.5} opacity={1} far={20} />
      <mesh scale={4} position={[3, -1.161, -1.5]} rotation={[-Math.PI / 2, 0, Math.PI / 2.5]}>
        <ringGeometry args={[0.9, 1, 4, 1]} />
        <meshStandardMaterial color="red" roughness={0.75} />
      </mesh>
      <mesh scale={4} position={[-3, -1.161, -1]} rotation={[-Math.PI / 2, 0, Math.PI / 2.5]}>
        <ringGeometry args={[0.9, 1, 3, 1]} />
        <meshStandardMaterial color="gold" roughness={0.75} />
      </mesh>
      <mesh rotation={[-Math.PI/2, 0, 0]} position={[0, -1.162, 0]} >
        <planeBufferGeometry args={[32, 32]}/>
        <MeshReflectorMaterial mirror={1} resolution={1024} />
      </mesh>
      <Environment resolution={512} preset='city' frames={Infinity}>
        <Lightformer intensity={2} rotation-x={Math.PI / 2} position={[0, 4, -9]} scale={[10, 1, 1]} />
        <Lightformer intensity={2} rotation-x={Math.PI / 2} position={[0, 4, -6]} scale={[10, 1, 1]} />
        <Lightformer intensity={2} rotation-x={Math.PI / 2} position={[0, 4, -3]} scale={[10, 1, 1]} />
        <Lightformer intensity={2} rotation-x={Math.PI / 2} position={[0, 4, 0]} scale={[10, 1, 1]} />
        <Lightformer intensity={2} rotation-x={Math.PI / 2} position={[0, 4, 3]} scale={[10, 1, 1]} />
        <Lightformer intensity={2} rotation-x={Math.PI / 2} position={[0, 4, 6]} scale={[10, 1, 1]} />
        <Lightformer intensity={2} rotation-x={Math.PI / 2} position={[0, 4, 9]} scale={[10, 1, 1]} />
        <Lightformer intensity={2} rotation-y={Math.PI / 2} position={[-50, 2, 0]} scale={[100, 2, 1]} />
        <Lightformer intensity={2} rotation-y={-Math.PI / 2} position={[50, 2, 0]} scale={[100, 2, 1]} />
        <Lightformer rotation-y={-Math.PI / 2} position={[10, 1, 0]} scale={[20, 1, 1]} />
        <Float speed={5} floatIntensity={2} rotationIntensity={2}>
          <Lightformer form="ring" color="gold" intensity={1} scale={10} position={[-15, 4, -18]} target={[0, 0, 0]} />
        </Float>
        <Lightformer form="ring" color="gold" intensity={10} scale={2} position={[10, 5, 10]} onUpdate={(self) => self.lookAt(0, 0, 0)} />
      </Environment>
      <OrbitControls enablePan={false} minPolarAngle={Math.PI / 2.2} maxPolarAngle={Math.PI / 2.2} autoRotate autoRotateSpeed={0.4} />
      <PostProcess />
      {[...Array(1000)].map((_, i) => (
        <AnimationBox key={i} />
      ))}
      <Perf position={"bottom-right"} />
    </XR>
  )
}

const Lambo = (props: any) => {
  const { scene, nodes, materials } = useGLTF('/lambo.glb') as any;
  useMemo(() => {
    // ⬇⬇⬇ All this is probably better fixed in Blender ...
    Object.values(nodes).forEach((node) => {
      if (node instanceof Mesh && node.isMesh) {
        // Fix glas, normals look messed up in the original, most likely deformed meshes bc of compression :/
        if (node.name.startsWith('glass')) node.geometry.computeVertexNormals()
        // Fix logo, too dark
        if (node.name === 'silver_001_BreakDiscs_0') node.material = applyProps(materials.BreakDiscs.clone(), { color: '#ddd' })
      }
    })
    // Fix windows, they have to be inset some more
    nodes['glass_003'].scale.setScalar(2.7)
    // Fix inner frame, too light
    applyProps(materials.FrameBlack, { metalness: 0.75, roughness: 0, color: 'black' })
    // Wheels, change color from chrome to black matte
    applyProps(materials.Chrome, { metalness: 1, roughness: 0, color: '#333' })
    applyProps(materials.BreakDiscs, { metalness: 0.2, roughness: 0.2, color: '#555' })
    applyProps(materials.TiresGum, { metalness: 0, roughness: 0.4, color: '#181818' })
    applyProps(materials.GreyElements, { metalness: 0, color: '#292929' })
    // Make front and tail LEDs emit light
    applyProps(materials.emitbrake, { emissiveIntensity: 3, toneMapped: false })
    applyProps(materials.LightsFrontLed, { emissiveIntensity: 3, toneMapped: false })
    // Paint, from yellow to black
    nodes.yellow_WhiteCar_0.material = new MeshPhysicalMaterial({
      roughness: 0.3,
      metalness: 0.05,
      color: '#820000',
      envMapIntensity: 0.75,
      clearcoatRoughness: 0,
      clearcoat: 1
    })
  }, [nodes, materials])
  return <primitive object={scene} {...props} />
}

const AnimationBox = () => {
  const ref = useRef<any>();
  const [speed] = useState(Math.random() * 0.05 + 0.01);

  const p = new Vector3(
    Math.random() * 30 - 15,
    Math.random() * 3,
    Math.random() * 30 - 15
  );
  
  const size = 0.25;
  const reverse = Math.random() > 0.5? true: false;
  const color = useMemo(() => {
    return new Color().setHSL(Math.random(), 1.0, 0.5);
  }, []);

  useFrame((state) => {
    if (ref.current) {
      if (reverse){
        ref.current.position.y -= Math.sin(state.clock.getElapsedTime()) * speed;  
      }
      else{
        ref.current.position.y += Math.sin(state.clock.getElapsedTime()) * speed;
      }
    }
  });

  return (
    <mesh ref={ref} position={p} castShadow receiveShadow>
      <boxBufferGeometry attach="geometry" args={[size, size, size]} />
      <meshStandardMaterial attach="material" color={color} />
    </mesh>
  );
}

const PostProcess = () => {
  const [texture, setTexture] = useState<Texture>();
  useEffect(() => {
    const loader = new LUTCubeLoader();
    loader.load('/F-6800-STD.cube', (loadedTexture: Texture) => {
      setTexture(loadedTexture);
    });
  }, []);
  let lut: JSX.Element = (<></>);
  if (texture){
    lut = <LUT lut={texture} />
  }
  return (
    <EffectComposer disableNormalPass>
      <Bloom luminanceThreshold={0.2} mipmapBlur luminanceSmoothing={0} intensity={1.45} />
      {lut}
    </EffectComposer>
  )
}

export default Scene;
```

# 最後に
R3Fの海外コミュニティでは毎日のように効率化リソースが公開され、Web3などの新しいプロジェクトがでてきて毎日楽しいです。
次はWebGPUが実装されるんでしょうか。楽しみです。

おわり。