React-Three-Fiber 마우스를 따라다니는 인스턴스

김찬진·2024년 8월 12일
0

Three

목록 보기
8/8

lusion 에서 THREE.JS로 마우스를 따라다니는 인스턴스를 구현한것이 있어서 이것에 영감을 받아 만들었습니다. 여기에 THREE.js, React-Three-Fiber, React-Three-Drei 가 사용되었습니다.

소스 코드

function MouseFollower({ vec = new THREE.Vector3(), instanceCount = 90 })

먼저 입력값을 받는 파라미터는 x,y,z 위치를 결정하는 vector3 와 인스턴스의 개수입니다.

  const swarmTextures = {
    basecolor: useLoader(THREE.TextureLoader, '/textures/swarm/Glass_Window_003_basecolor.jpg'),
    normalMap: useLoader(THREE.TextureLoader, '/textures/swarm/Glass_Window_003_normal.jpg'),
    heightMap: useLoader(THREE.TextureLoader, '/textures/swarm/Glass_Window_003_height.png'),
    roughnessMap: useLoader(THREE.TextureLoader, '/textures/swarm/Glass_Window_003_roughness.jpg'),
    aoMap: useLoader(THREE.TextureLoader, '/textures/swarm/Glass_Window_003_ambientOcclusion.jpg'),
    metallicMap: useLoader(THREE.TextureLoader, '/textures/swarm/Glass_Window_003_metallic.jpg'),
  };

그다음 마우스 주변에 texture를 로드하는데, texture가 없어도 상관은 없습니다.

  const { MouseFollowerHandler } = useStore();

  const MouseGroup = useRef();
  const light = useRef();
  const lightSphereRef = useRef();

  const { camera, viewport } = useThree((state) => ({ camera: state.camera, viewport: state.viewport }));

  const mouseInstances = useRef([]);
  const lensFlareMaterialRef = useRef();

마우스에 여러가지 Ref 값들을 선언합니다. 순서대로

  • MouseFollowerHandler : Zustand 라이브러리를 통해 위의 인스턴스를 작동시킬지 여부입니다. 리덕스와 기능은 같지만 Zustand와 React-Three-Drei를 같은 회사에서 만든것이라 그런지 서로 호환이 좋다고 소개되어있어서 이것을 사용했습니다. 이 부분은 특정 페이지에서 인스턴스를 작동시키는게 목적이라 처음부터 위의 기능을 사용하는것이라면 굳이 사용하지 않아도 되는 것들입니다.

  • MouseGroup : 맨 처음 gif 파일을 보면, 마우스 주변을 따라다니는 인스턴스와, 마우스 위치에서 고정되어있는 Geometry, 마우스 위치에 고정된 Light 3가지로 구성되어있는데 이것을 묶어주는 GroupRef입니다.

  • light : 마우스 위치에 고정된 Light Ref입니다.

  • lightSphereRef : 마우스 위치에 고정된 Geometry Ref 입니다.

  • camera, viewport : 전체화면의 크기를 계산하기 위한 THREE.js stdlib 입니다.

  • mouseInstances : 마우스를 따라다니는 인스턴스 Ref 입니다.

  • lensFlareMaterialRef : 초기에는 lightSphereRef 에 Custom Material을 적용시키려고 Ref 값을 주었으나, EffectComposer 사용으로 변경했고 지금은 사용되지 않습니다.

  const initialPositions = useMemo(
    () =>
      Array.from({ length: instanceCount }, () => ({
        position: new THREE.Vector3((Math.random() - 0.5) * 2, (Math.random() - 0.5) * 2, (Math.random() - 0.5) * 2),
        velocity: new THREE.Vector3(
          (Math.random() - 0.5) * 0.04,
          (Math.random() - 0.5) * 0.04,
          (Math.random() - 0.5) * 0.04
        ),
      })),
    [instanceCount]
  );

초기 인스턴스들의 위치값들입니다. 위치와 속도값은 순서대로 x,y,z 에 할당됩니다.

  useEffect(() => {
    if (MouseFollowerHandler) {
      mouseInstances.current.forEach((instance) => {
        gsap.to(instance.scale, {
          x: 1,
          y: 1,
          z: 1,
          duration: 1,
          ease: 'power2.out',
        });
      });
      gsap.to(lightSphereRef.current.scale, {
        x: 1,
        y: 1,
        z: 1,
        duration: 1,
        ease: 'power2.out',
      });
    } else {
      mouseInstances.current.forEach((instance) => {
        gsap.to(instance.scale, {
          x: 0,
          y: 0,
          z: 0,
          duration: 1,
          ease: 'power2.out',
        });
      });
      gsap.to(lightSphereRef.current.scale, {
        x: 0,
        y: 0,
        z: 0,
        duration: 1,
        ease: 'power2.out',
      });
    }
  }, [MouseFollowerHandler]);

이 부분역시 MouseFollowerHandler 와 마찬가지로 특정 페이지에서 인스턴스들을 on/off 시키는 기능입니다. 처음부터 사용하면 위의 함수는 사용하지 않습니다.

  useFrame((state) => {
    const { pointer } = state;
    // 페이지 수가 늘어나면 그에맞춰서 추가
    const x = (pointer.x * viewport.width) / 6;
    const y = (pointer.y * viewport.height) / 6;

    MouseGroup.current.position.lerp(vec.set(x, y, 0), 0.05);
    light.current.position.lerp(vec.set(x, y, 1), 0.05);

    mouseInstances.current.forEach((instance, i) => {
      const { position, velocity } = initialPositions[i];

      // Add velocity to position (simulate movement)
      position.add(velocity);

      // Reverse velocity if position goes out of bounds
      if (position.x > 1.2 || position.x < -1.2) velocity.x = -velocity.x;
      if (position.y > 1.2 || position.y < -1.2) velocity.y = -velocity.y;
      if (position.z > 1.2 || position.z < -1.2) velocity.z = -velocity.z;

      // Calculate target position relative to the group considering mouse pointer
      const targetPosition = position.clone().add(vec.set(x / (Math.PI * 15), y / (Math.PI * 15), 0));

      // Smoothly move instance towards the target position using lerp
      instance.position.lerp(targetPosition, 0.1);
    });
  });

실질적으로 인스턴스와 빛, 마우스의 geometry가 움직이는 함수입니다.

    const x = (pointer.x * viewport.width) / 6;
    const y = (pointer.y * viewport.height) / 6;

이 부분은 react-three-drei의 ScrollContoller를 사용하여 화면을 확장시켰기때문에 그에 맞춰 화면을 분할한거고 만약 단일 페이지에서 실행시킨다면 아래와 같이 사용하셔야합니다.

    const x = (pointer.x * viewport.width);
    const y = (pointer.y * viewport.height);
      if (position.x > 1.2 || position.x < -1.2) velocity.x = -velocity.x;
      if (position.y > 1.2 || position.y < -1.2) velocity.y = -velocity.y;
      if (position.z > 1.2 || position.z < -1.2) velocity.z = -velocity.z;

이 부분은 인스턴스가 너무 멀리 떨어지지않도록 특정 position을 초과하면 다시 돌아오게끔 설정하였습니다.

그 외에 거의 모든 부분에 lerp을 넣어서 보간하였는데 다음과 같은 공식을 사용합니다.

Lerp(a,b,α)=a(1α)+bαLerp(a,b,\alpha) = a \cdot(1-\alpha)+b\cdot \alpha
return (
    <>
      <group ref={MouseGroup}>
        <Sphere args={[0.025, 32, 32]} ref={lightSphereRef} scale={0}>
          <meshStandardMaterial color="white" roughness={1} ref={lensFlareMaterialRef} />
        </Sphere>
        <pointLight ref={light} position={[0, 0, 1]} intensity={5} color={'white'} />
        <spotLight ref={light} position={[0, 0, 1]} angle={0.15} penumbra={3} />

        {Array.from({ length: instanceCount }).map((_, i) => (
          <instancedMesh ref={(ref) => (mouseInstances.current[i] = ref)} key={i} args={[null, null, 1]}>
            <dodecahedronGeometry args={[0.05, 0]} />
            <meshStandardMaterial
              map={swarmTextures.basecolor}
              normalMap={swarmTextures.normalMap}
              roughnessMap={swarmTextures.roughnessMap}
              metalnessMap={swarmTextures.metallicMap}
              aoMap={swarmTextures.aoMap}
              aoMapIntensity={0.1}
              color={'white'}
              roughness={0.2}
              metalness={0.4}
            />
          </instancedMesh>
        ))}
      </group>
      <EffectComposer>
        <SelectiveBloom
          lights={[light]}
          selection={[lightSphereRef]}
          intensity={1.0}
          luminanceThreshold={0.9}
          luminanceSmoothing={0.025}
        />
      </EffectComposer>
    </>
  );
}

마지막 return 부분에 할당된 geometry, material, instance 들을 설정해주면 완성입니다.

react-three-drei의 Instance 가 지원되는데 react-three-fiber의 instancedMesh 를 사용한 이유는, Instance를 사용하면 퍼포먼스가 크게 떨어지고 메모리 누수가 발생합니다. 웬만한 대량 인스턴스를 처리할때에는 instanceMesh를 사용하는것을 추천합니다.

profile
AI/Web 개발자

0개의 댓글