🏳
React + Three.jsでwaving flag
Three.jsで風にゆれる旗を作るのは割と定番のような気がしますが、
@react-three/fiber
を使ったサンプルが見つからなかったので
色々調べた結果を書こうと思います。
ゆれる旗の考え方/作り方
PlaneGeometryのvertices
(頂点)プロパティを更新すればいいということはわかります。
とはいうものの、three-fiberを使ったときにvertices
プロパティが見つかりません。
three-fiberではどうやって頂点の座標を取得して更新するか
頂点の座標情報は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;
参考リンク
Discussion