🏳

React + Three.jsでwaving flag

2021/12/16に公開

Three.jsで風にゆれる旗を作るのは割と定番のような気がしますが、
@react-three/fiberを使ったサンプルが見つからなかったので
色々調べた結果を書こうと思います。

ゆれる旗の考え方/作り方

https://www.youtube.com/watch?v=Su1n2Uuf38E

PlaneGeometryのvertices(頂点)プロパティを更新すればいいということはわかります。
とはいうものの、three-fiberを使ったときにverticesプロパティが見つかりません。

three-fiberではどうやって頂点の座標を取得して更新するか

https://github.com/pmndrs/react-three-fiber/discussions/968#discussioncomment-317165

頂点の座標情報はpositionプロパティに入ってるようです。Three.jsのサンプルコードではVector3になっていますが、ここでは全て配列になっています。

書いてみたコード

githubで例示されているコードでは以下のように座標を取得すればいいことがわかります。

//x
position.array[index * 3]
//y
position.array[index * 3 + 1]
//z
position.array[index * 3 + 2]

以下のコードでは@react-three/cannonは必須ではありませんが、usePlaneフックが便利なので使ってしまいました。

全てのコード

import { TextureLoader } from "three/src/loaders/TextureLoader";
import { Canvas, useFrame, useLoader, useThree } from "@react-three/fiber";
import { Physics, usePlane, Debug } from "@react-three/cannon";
import CameraControls from "camera-controls";
import * as THREE from "three";
import { useMemo } from "react";
CameraControls.install({ THREE });

const TeamFlag = ({img_url}) => {
  const Flag = () => {
    const map = useLoader(TextureLoader, 
      img_url
    );
    const [mesh] = usePlane(() => ({
      rotation: [-0.1, 0, 0],
      position: [0, 0, 0],
      type: "Static",
      mass: 100,
      args: [3, 3, 15, 9],
    }));
    useFrame((state) => {
      const t = state.clock.getElapsedTime();
      function* enumerate(count) {
        let i = 0;
        while (i < count) yield i++;
      }
      const { geometry } = mesh.current;
      const { position, normal, uv } = geometry.attributes;
      for (const index of enumerate(position.count)) {
        const waveX1 = 0.5 * Math.sin(position.array[index * 3] * 2 + t*4);
        const waveX2 = 0.25 * Math.sin(position.array[index * 3] * 3 + t *2);
        const waveY1 =
          0.25 * Math.sin(position.array[index * 3 + 1] * 3 + t / 5);
        const multi = (position.array[index * 3] + 2) / 5;

        //z
        position.array[index * 3 + 2] = (waveX1 + waveX2 + waveY1) * multi;
      }
      position.needsUpdate = true;
      geometry.computeVertexNormals();
    });
    return (
      <mesh ref={mesh}>
        <planeGeometry args={[3, 3, 15, 9]}/>
        <meshBasicMaterial map={map} opacity={1} />
      </mesh>
    );
  };

  function Controls({
    zoom,
    focus,
    pos = new THREE.Vector3(),
    look = new THREE.Vector3(),
  }) {
    const camera = useThree((state) => state.camera);
    const gl = useThree((state) => state.gl);
    const controls = useMemo(
      () => new CameraControls(camera, gl.domElement),
      []
    );
    return useFrame((state, delta) => {
      state.camera.position.lerp(pos, 0.5);
      state.camera.updateProjectionMatrix();
      controls.setLookAt(
        state.camera.position.x,
        state.camera.position.y,
        state.camera.position.z,
        look.x,
        look.y,
        look.z,
        true
      );
      return controls.update(delta);
    });
  }

  return (
    <div>
      <Canvas
        style={{
            width:80,
            height:80
        }}
      >
        <Physics>
          <Flag />
        </Physics>
        <Controls
          zoom={false}
          focus={{}}
          pos={new THREE.Vector3(0, 0, 2.2)}
          look={new THREE.Vector3(0, 0, 0)}
        />
      </Canvas>
    </div>
  );
};

export default TeamFlag;

参考リンク

https://github.com/nikpundik/react-three-fiber-cloth-example

Discussion