Search code examples
three.jsraycastingpoints

THREE.js Raycasting Points


I'm trying to implement raycasting for the Points object.

The problem is that the raycaster selection doesn't match the pointer position.

I took as reference these 2 examples from three: webgl_interactive_raycasting_points webgl_interactive_points but i can't still figure out what i am doing wrong.

here is my code pen: https://codepen.io/simone-tasca/pen/YzapWMN

let scene = new THREE.Scene()
const near = 0.1
const far = 5000
const fov = 30
const aspect = window.innerWidth / window.innerHeight

const camera = new THREE.PerspectiveCamera(fov, aspect, near, far)
camera.position.set(-1.25, 0.8, -1.9)

let ambientLight = new THREE.AmbientLight('white', 1)
scene.add(ambientLight)

// POINTS CONTAINER ==========================================================
const worldCenter = [-1.07, 0, -6.85]
const rotationCorrection = [0.781, 4.305, 0.28]

const worldMap = new THREE.Object3D()
worldMap.position.set(...worldCenter)
worldMap.rotation.set(...rotationCorrection)
scene.add(worldMap)

// THREE.Points =============================================================
const PARTICLE_SIZE = 0.1
let particles, raycaster, INTERSECTED, pointer

let vertices = []
let names = []
let sizes = []

prepareData(sampleData).forEach(coords => {
  vertices.push(...coords)
  sizes.push(PARTICLE_SIZE)
})

const geometry = new THREE.BufferGeometry()
geometry.attributes.position = new THREE.Float32BufferAttribute(vertices, 3)
geometry.attributes.size = new THREE.Float32BufferAttribute(sizes, 1)

let material = new THREE.PointsMaterial({
  color: 0xffffff,
  transparent: true,
  depthTest: true,
  depthWrite: false
})
material.onBeforeCompile = shader => {
  shader.vertexShader =
    shader.vertexShader.replace('uniform float size;', 'attribute float size;')
}

particles = new THREE.Points(geometry, material)
worldMap.add(particles)

// RAYCASTER =============================================================
raycaster = new THREE.Raycaster()
pointer = new THREE.Vector2(99999, 99999)

document.addEventListener('pointermove', (event) => {
  pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
  pointer.y = - (event.clientY / window.innerHeight) * 2 + 1;
})
document.addEventListener('pointerout', () => pointer.set(99999, 99999))

// RENDERING ==================================================================
let canvas = document.querySelector('#c')
const renderer = new THREE.WebGLRenderer({
  canvas: canvas, alpha: false
})

function resizeRendererToDisplaySize(renderer) {
  const width = canvas.clientWidth
  const height = canvas.clientHeight
  const needResize = canvas.width !== width || canvas.height !== height

  if (needResize) renderer.setSize(width, height, false)

  return needResize
}

function render(time) {
  if (resizeRendererToDisplaySize(renderer)) {
    const canvas = renderer.domElement
    camera.aspect = canvas.clientWidth / canvas.clientHeight
    camera.updateProjectionMatrix()
  }

  const geometry = particles.geometry
  const attributes = geometry.attributes

  raycaster.setFromCamera(pointer, camera)
  
  let intersects = raycaster.intersectObject(particles)
  
  if (intersects.length > 0) {
    if (INTERSECTED != intersects[0].index) {
      console.log(intersects[0].index)
      attributes.size.array[INTERSECTED] = PARTICLE_SIZE
      INTERSECTED = intersects[0].index
      attributes.size.array[INTERSECTED] = PARTICLE_SIZE * 3
      attributes.size.needsUpdate = true
    }
  } else if (INTERSECTED !== null) {
    attributes.size.array[INTERSECTED] = PARTICLE_SIZE
    attributes.size.needsUpdate = true
    INTERSECTED = null
  }

  renderer.render(scene, camera)
  requestAnimationFrame(render)
}

render()

Solution

  • You're very close. The only thing missing is to declare how "wide" you want your raycaster to be. Add this line after initiating the raycaster:

    raycaster = new THREE.Raycaster()
    raycaster.params.Points.threshold = 0.05;
    

    The threshold is by default 1 unit wide. Think of this as painting with a very broad brush, the first particle you'll hit may not be the closest to your mouse pointer. So when you get intersects[0].index, it's going to be the first particle you hit with that broad ray (closest to the camera), not the closest one to your mouse. If you declare a narrower threshold, your ray will be more precise and you'll get more accurate results.

    https://threejs.org/docs/#api/en/core/Raycaster.params