Meshes push away from mouse hover

Hello,

I’m trying to replicate the effect of the threejs canvas and mouse hover in the landing page of https://sparkk.fr/en

I’ve made slight progress as you can see below but its nothing impressive or near what I want.

I tried implementing a radius of effect but that wouldn’t work so I decided to come here for help.

Here is my code, it may need full reworking:

import { Canvas, useFrame, useThree } from '@react-three/fiber';
import { OrbitControls, useGLTF, PerspectiveCamera } from '@react-three/drei';
import * as THREE from 'three';
import gsap from 'gsap';

function Model({ raycaster, mouse }) {
  const group = useRef();
  const { scene } = useGLTF('/models/st1/cube.glb');
  const originalPositions = useRef([]);
  const intersectedObjects = useRef(new Set());
  const pushDistance = 0.5;
  const maxAwayDistance = 1.6
  const { camera } = useThree();

  useEffect(() => {
    let index = 0;
    const positions = [];
    scene.traverse((child) => {
      if (child.isMesh) {
        const color = new THREE.Color(`hsl(${index * 30 % 360}, 100%, 50%)`);
        if (Array.isArray(child.material)) {
          child.material.forEach((material) => {
            material.color = color;
            material.needsUpdate = true;
          });
        } else {
          child.material.color = color;
          child.material.needsUpdate = true;
        }
        positions.push(child.position.clone());
      }
      index += 1;
    });
    originalPositions.current = positions;
  }, [scene]);

  useFrame(() => {
    if (group.current) {

      raycaster.setFromCamera(mouse, camera);

      const intersects = raycaster.intersectObjects(group.current.children, true);
      intersectedObjects.current.clear();
      intersects.forEach(intersect => {
        intersectedObjects.current.add(intersect.object);
      });

      group.current.children.forEach((child, idx) => {
        if (child.isMesh) {
          const originalPosition = originalPositions.current[idx];
          const currentDistance = child.position.distanceTo(originalPosition);

          if (intersectedObjects.current.has(child)) {
            const intersect = intersects.find((i) => i.object === child);
            if (intersect) {
              const intersectionPoint = intersect.point;
              const pushDirection = new THREE.Vector3().subVectors(child.position, intersectionPoint).normalize();
              
              if (currentDistance < maxAwayDistance) {

                gsap.to(child.position, {
                  duration: 0.5,
                  x: child.position.x + pushDirection.x * pushDistance,
                  y: child.position.y + pushDirection.y * pushDistance,
                  z: child.position.z + pushDirection.z * pushDistance,
                  ease: 'power2.out'
                });
              } else {

                gsap.to(child.position, {
                  duration: 0.5,
                  x: originalPosition.x,
                  y: originalPosition.y,
                  z: originalPosition.z,
                  ease: 'power2.out'
                });
              }
            }
          } else {
            gsap.to(child.position, {
              duration: 0.5,
              x: originalPosition.x,
              y: originalPosition.y,
              z: originalPosition.z,
              ease: 'power2.out'
            });
          }
        }
      });
    }
  });

  return <primitive object={scene} ref={group} position={[-20,-2,-20]} />;
}

const LandingScene = () => {
  const [raycaster] = useState(new THREE.Raycaster());
  const mouse = useRef(new THREE.Vector2());

  const onMouseMove = (event) => {
    mouse.current.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse.current.y = -(event.clientY / window.innerHeight) * 2 + 1;
  };

  useEffect(() => {
    window.addEventListener('mousemove', onMouseMove);
    return () => window.removeEventListener('mousemove', onMouseMove);
  }, []);

  return (
    <Canvas>
      <PerspectiveCamera makeDefault fov={75} aspect={window.innerWidth / window.innerHeight} near={0.1} far={1000} />
      <ambientLight intensity={0.5} />
      <pointLight position={[10, 10, 10]} />
      <Model raycaster={raycaster} mouse={mouse.current} />
    </Canvas>
  );
};

export default LandingScene;

If anyone can give me any aid it will be heavilt appreciated. I’m content with switching to vanilla threejs instead of fiber if that’s whats needed.

Thank you

i don’t think you need to raycast for that. you also don’t need onMouseMove. you already have that as useFrame((state) => state.pointer, this is the normalised 2d pointer that you create in your LandingScene component, but it is already there. convert that into a 3d vector. gsap imo is also overkill. something like this should be near complete (pseudo code):

import { easing } from 'maath'
...

function Model(props) {
  const { scene } = useGLTF(...)
  const cursor = new THREE.Vector3()
  const vec = new THREE.Vector3()
  const dir = new THREE.Vector3()
  useFrame(({ pointer, viewport, camera }, delta) => {
    ...
    cursor.set(pointer.x, pointer.y, 0.5).unproject(camera)
    scene.traverse((child) => {
      if (child.originalPosition === undefined) child.originalPosition = child.position.clone()
      dir.copy(cursor).sub(child.originalPosition).normalize()
      const dist = child.originalPosition.distanceTo(vec)
      easing.damp3(child.position, vec.copy(child.originalPosition).add(dir.multiplyScalar(dist / 10)), 0.2, delta)      
    })
  })
  return <primitive object={scene} {...props} />

now everything that’s near must move away in the normalised direction from pointer towards child position. how much it must move away would be dependent on the distance.

Thanks, I tried this but maybe because of the way I put the code in, everything seems wrong. Some of the meshes are not initially in the right positions and the mesh push is just based on the mouse position all over the screen not when it just hovers. Also some meshes just don’t move at all. My code looks the same as your pseudocode but without all the syntax errors that it would give. Also, where you, inside the damp3 function, passed in vec.copy() i changed originalPosition to originalPositions because originalPosition doesn’t exist and the childs position is a vector.

Is there anything else I can do to get this working?

It’s child.originalPosition. You can delete originalPositions. As I said the code is near complete, it’s pseudo code because the math has to be adjusted/fixed but it will work without anything else that’s missing.

like so https://codesandbox.io/p/sandbox/elegant-tu-wm3mt7

2 Likes

thank you so much, ive been on this fir ages

Sorry, but one more question. I’ve been trying to get it working with a custom model with objects inside. All I should need to do is replace the instances with a group then load in each mesh, correct? Then set up the references and all that. It doesn’t seem to work for me but am I looking in the right direction?

it will be similar, yes. it could be that you have to use world position or apply matrixworld. it could also be that the model has baked positions into vertices so all mesh positions would be 0/0/0 which wouldn’t work for your usecase. the last bit you have to figure out on your own, there’s lots to learn in the process.

1 Like