How to integrate custom 3D mouse with particle

I have created a particle system using GLSL shaders, adhering to a texture. When the mouse moves, the particles collide and scatter around, simulating a bouncing effect. They gradually come back together smoothly as the mouse passes through.

I’ve achieved something close to the desired outcome, but I’m facing an issue where the mouse layer and the particles are not aligned properly. This results in the particles overlaying on top of the mouse, causing interference. Where should I focus to fix this issue?

setMouseGeometry() {
  const hdrEquirect = new RGBELoader().load(
    "./gltf/empty_warehouse_01_2k.hdr",
    () => {
      hdrEquirect.mapping = EquirectangularReflectionMapping;
    }
  );
  const textureLoader = new TextureLoader();

  const normalMapTexture = textureLoader.load("src/normal.jpg");
  normalMapTexture.wrapS = RepeatWrapping;
  normalMapTexture.wrapT = RepeatWrapping;
  normalMapTexture.repeat.set(3, 3);

  const mouse = new Vector2();
  const plane = new Plane(new Vector3(0, 0, 1), 0);
  const raycaster = new Raycaster();

  const mouseGeometry = new SphereGeometry(17, 32, 16)
  mouseGeometry.scale(1, 1, 0.5);
  const material = new MeshPhysicalMaterial({
    color: 'salmon',
    emissive: 0x000000,
    metalness: 0,
    roughness: 0,
    ior: 1.5,
    reflectivity: 0,
    iridescence: 0,
    iridescenceIOR: 1,
    sheen: 0,
    sheenRoughness: 0,
    sheenColor: 0xffffff,
    clearcoat: 0,
    clearcoatRoughness: 0,
    opacity: 1,
    depthTest: true,
    depthWrite: false,
    transmission: 0,
    envMapIntensity: 1,
    envMap: hdrEquirect
  })
  const customCursor = new Mesh(mouseGeometry, material)

  this.scene.add(customCursor)

  window.addEventListener("mousemove", (e) => {
    mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;

    raycaster.setFromCamera(mouse, this.camera);
    raycaster.ray.intersectPlane(plane, customCursor.position);

  });
}
setParticlesGrid() {
  const geometry = new BufferGeometry();
  this.geometry = new BufferGeometry();

  const initPositions = []
  const particles = []
  const multiplier = 18
  const nbColumns = 16 * multiplier
  const nbLines = 9 * multiplier
  const rotations = [];
  const timeOffsets = [];
  const randomValuesX = [];
  const randomValuesY = [];

  this.nbColumns = nbColumns
  this.nbLines = nbLines

  for (let line = 0; line < nbLines; line++) {
    for (let column = 0; column < nbColumns; column++) {
      let x = Math.random() * nbLines - nbLines / 2;
      let y = Math.random() * nbColumns - nbColumns / 2;

      particles.push(x, y, 0);

      let initPoint = [randFloat(-300, 300), randFloat(-300, 300), randFloat(-300, 300)]
      initPositions.push(...initPoint)

      let rotation = Math.random() * Math.PI * 2;
      rotations.push(rotation);

      timeOffsets.push(Math.random());
      randomValuesX.push(Math.random());
      randomValuesY.push(Math.random());
    }
  }

  const vertices = new Float32Array(particles);
  const initPositionsFloat = new Float32Array(initPositions);
  const timeOffsetsAttribute = new Float32Array(timeOffsets);
  const randomValuesXAttribute = new Float32Array(randomValuesX);
  const randomValuesYAttribute = new Float32Array(randomValuesY);

  geometry.setAttribute('position', new BufferAttribute(vertices, 3));
  geometry.setAttribute('initPosition', new BufferAttribute(initPositionsFloat, 3));
  geometry.setAttribute('aTimeOffset', new BufferAttribute(timeOffsetsAttribute, 1));
  geometry.setAttribute('aRandomX', new BufferAttribute(randomValuesXAttribute, 1));
  geometry.setAttribute('aRandomY', new BufferAttribute(randomValuesYAttribute, 1));

  const defaultPositions = vertices.slice();
  geometry.setAttribute('defaultPosition', new BufferAttribute(defaultPositions, 3));

  geometry.center()

  const texture = LoaderManager.assets['background'].texture;
  texture.minFilter = NearestFilter;
  texture.magFilter = NearestFilter;

  this.dpr = 2
  this.uniforms = {
    uColor: { value: new Color(0xffffff) },
    uPointSize: { value: 1.0 },
    uTexture: { value: texture },
    uNbLines: { value: nbLines },
    uNbColumns: { value: nbColumns },
    uProgress: { value: 3.0 },
    uTime: { value: 0 },
    uTouch: { value: this.touch.texture },
    uScaleHeightPointSize: { value: (this.dpr * this.height) / 2 },
    uMouse: { value: new Vector2(0, 0) },
  }

  this.customMaterial = new ShaderMaterial({
    uniforms: this.uniforms,
    vertexShader,
    fragmentShader,
    transparent: true,
    depthTest: false,
    depthWrite: false,
  })
  this.mesh = new Points(geometry, this.customMaterial)
  this.mesh.renderOrder = 998;
  this.scene.add(this.mesh)
}
handleMouseMove = (e) => {
  const radius = 17;
  const baseSeparationFactor = 0.5;
  const returnFactor = 0.1;

  this.mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
  this.mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;

  this.ray.setFromCamera(this.mouse, this.camera);

  const intersects = this.ray.intersectObject(this.mesh);

  if (intersects.length) {
    const mousePosition = intersects[0].point;

    const positions = this.mesh.geometry.attributes.position.array;
    const defaultPositions = this.mesh.geometry.attributes.defaultPosition.array;

    for (let i = 0; i < positions.length; i += 3) {
      const particleX = positions[i];
      const particleY = positions[i + 1];
      const particleZ = positions[i + 2];

      const dx = mousePosition.x - particleX;
      const dy = mousePosition.y - particleY;
      const dz = mousePosition.z - particleZ;

      const distanceToMouse = Math.sqrt(dx * dx + dy * dy + dz * dz);

      const separationFactor = baseSeparationFactor * distanceToMouse;

      if (distanceToMouse < radius) {
        const forceMultiplier = 0.05;
        positions[i] -= dx * forceMultiplier * separationFactor;
        positions[i + 1] -= dy * forceMultiplier * separationFactor;
        positions[i + 2] -= dz * forceMultiplier * separationFactor;
      } else {
        positions[i] += (defaultPositions[i] - positions[i]) * returnFactor;
        positions[i + 1] += (defaultPositions[i + 1] - positions[i + 1]) * returnFactor;
        positions[i + 2] += (defaultPositions[i + 2] - positions[i + 2]) * returnFactor;
      }
    }

    this.mesh.geometry.attributes.position.needsUpdate = true;
  }
};

If I were you, I’d first try to figure out the reason for overlapping.

  • it could be just a conflict between 3D reality and 2D expectation
  • it could be some bug in the calculations of repelling/returning forces
  • something else

The 2D/3D conflict happens if the particles are in 3D, they are outside the sphere, but visually they are inside (see the left snapshot). To fix this, force all particles to be always on the plane of the sphere.

If the problem is with the calculations, the best is to debug it. For this, make just one particle so that is easy to debug what values are calculated.

It’s hard. I can’t put it on. orbitcontrols so that it cannot be rotated. I’m not sure that why the OrbitControls not working. It don’t have any error

setControls() {
    this.controls = new OrbitControls(this.camera, this.renderer.domElement);
    this.controls.enableDamping = true;
  }
  setScene() {
    this.scene = new Scene()
    
  }

  setCamera() {
    const aspectRatio = this.width / this.height
    const fieldOfView = 60
    const nearPlane = 0.1
    const farPlane = 10000

    this.camera = new PerspectiveCamera(fieldOfView, aspectRatio, nearPlane, farPlane)
    this.camera.position.y = 0
    this.camera.position.x = 0
    this.camera.position.z = 250
    this.camera.lookAt(0, 0, 0)

    this.scene.add(this.camera)
  }

Ok, I discovered that mouseGeometry not moving properly It also moves in the z axis. I’m looking for a way to fix it.