Search code examples
three.jsvirtual-realityraycasting

Raycast an object to enbable a mouse click event with Three.js


I'm adding objects to a scene for each entry in a database. I had a cube appearing in the scene for entry, but when I tried to add raycasting to click on objects it doesnt work, the objects dont appear and the console reads "Expression unavailable".. I got parts of the code from the three.js website for Raycasting so not sure what I'm doing wrong.

Here is the JS code:

var renderer, scene, container, camera;
var geometry, material;
var controls, group;

var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();


init()

function onMouseMove( event ) {

    // calculate mouse position in normalized device coordinates
    // (-1 to +1) for both components

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

}

function init() {
    // init renderer
    renderer = new THREE.WebGLRenderer( { antialias: true } );
    renderer.setPixelRatio( window.devicePixelRatio );
    renderer.setSize( window.innerWidth, window.innerHeight );
    // document.body.appendChild( renderer.domElement );

    container = document.getElementById('container');
    container.appendChild( renderer.domElement );

    // init scene
    scene = new THREE.Scene();
    scene.background = new THREE.Color( 0xffffff );

    group = new THREE.Group();
    scene.add( group )

    //fetch data from database and add object for each entry
    getData()
    async function getData() {
        var response = await fetch('/api/indexvr');
        var data = await response.json();
        console.log(data) 

        for (var i=0; i<data.length; i++) {
            cube = new THREE.Mesh( geometry, material );
            cube.position.x = i;
            scene.add(cube);
            //group.add(data)
        }
    }

    // init camera
    camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 1, 1000 );
    camera.position.set( 15, 15, 15 ); //camera.position.set( 5, 0, 10 );
    camera.lookAt( scene.position );
    // controls = new OrbitControls( camera, renderer.domElement );
    // controls.enableRotate = true;
}

function render() {

    // update the picking ray with the camera and mouse position
    raycaster.setFromCamera( mouse, camera );

    // calculate objects intersecting the picking ray
    var intersects = raycaster.intersectObjects( scene.children );

    for ( var i = 0; i < intersects.length; i++ ) {

        intersects[ i ].object.material.color.set( 0xff0000 );

    }

    renderer.render( scene, camera );

}

window.addEventListener( 'mousemove', onMouseMove, false );

window.requestAnimationFrame(render);

The HTML just has a div called "container" and this tag:

<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/108/three.js"></script>

It doesn't produce any error it only says this in the console: enter image description here

So it is fetching the data but can't render the scene

var renderer, scene, container, camera;
var geometry, material;
var controls, group;

var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();


init()

function onMouseMove(event) {

  // calculate mouse position in normalized device coordinates
  // (-1 to +1) for both components

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

}

function init() {
  // init renderer
  renderer = new THREE.WebGLRenderer({
    antialias: true
  });
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(window.innerWidth, window.innerHeight);
  // document.body.appendChild( renderer.domElement );

  container = document.getElementById('container');
  container.appendChild(renderer.domElement);

  // init scene
  scene = new THREE.Scene();
  scene.background = new THREE.Color(0xffffff);


  group = new THREE.Group();
  scene.add(group)

  //fetch data from database and add object for each entry
  getData()
  async function getData() {
    /**
     * @author TheJim01
     * Replacing DB call with fake data to make it work here.
     * Nancy: Please feel free to add appropriate data.
     */
    // var response = await fetch('/api/indexvr');
    // var data = await response.json();
    var data = [{}, {}, {}, {}, {}]
    console.log(data)

    for (var i = 0; i < data.length; i++) {
      cube = new THREE.Mesh(geometry, material);
      cube.position.x = i;
      scene.add(cube);
      //group.add(data)
    }
  }

  // init camera
  camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 1000);
  camera.position.set(15, 15, 15); //camera.position.set( 5, 0, 10 );
  camera.lookAt(scene.position);
  // controls = new OrbitControls( camera, renderer.domElement );
  // controls.enableRotate = true;
}

function render() {

  // update the picking ray with the camera and mouse position
  raycaster.setFromCamera(mouse, camera);

  // calculate objects intersecting the picking ray
  var intersects = raycaster.intersectObjects(scene.children);

  for (var i = 0; i < intersects.length; i++) {

    intersects[i].object.material.color.set(0xff0000);

  }

  renderer.render(scene, camera);

}

window.addEventListener('mousemove', onMouseMove, false);

window.requestAnimationFrame(render);
<script src="//threejs.org/build/three.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/108/three.js"></script>
<script src="https://unpkg.com/spritejs/dist/spritejs.min.js"></script>

<div id="container"></div>


Solution

  • There are several things wrong that I can see. Whether that's because you omitted sections of your code, I can't say.

    First, the code you provided does not define geometry, nor a material. You implied you're drawing a cube for each DB result, so I'll make an assumption and use BoxBufferGeometry. You also don't have any lights defined, so I'll just use MeshBasicMaterial which doesn't need lights.

    With those out of the way, it looks like you're halfway to setting up a render loop using window.requestAnimationFrame, but you still only call render once, even though your DB fetching is asynchronous. In other words, the render might occur before you even get a response from your DB, so you would see nothing. I've added some boilerplate code to set up a render loop, similar to how three.js does in its examples.

    Interestingly enough, that was all it took. The raycaster started working, and I was able to console log the results out. I did get some false-positives when the scene first started rendering, but that's because there hadn't been any mouse input yet, so it was raycasting from the middle of the screen (where the first cube exists).

    Normally, you wouldn't want to raycast for every frame, but I understand the VR situation might be different (darn fidgety humans).

    Finally, one last change I made was to give each cube its own material (well, clones of the original). This was necessary to ensure you could raycast against each one individually.

    // Need to create geometry and material
    var geometry = new THREE.BoxBufferGeometry(0.5, 0.5, 0.5);
    var material = new THREE.MeshBasicMaterial({
      color: "green"
    });
    
    var renderer, scene, container, camera;
    var controls, group;
    
    var raycaster = new THREE.Raycaster();
    var mouse = new THREE.Vector2();
    
    init()
    
    function onMouseMove(event) {
    
      // calculate mouse position in normalized device coordinates
      // (-1 to +1) for both components
    
      mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
      mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
    
    }
    
    function init() {
      // init renderer
      renderer = new THREE.WebGLRenderer({
        antialias: true
      });
      renderer.setPixelRatio(window.devicePixelRatio);
      renderer.setSize(window.innerWidth, window.innerHeight);
      // document.body.appendChild( renderer.domElement );
    
      container = document.getElementById('container');
      container.appendChild(renderer.domElement);
    
      // init scene
      scene = new THREE.Scene();
      scene.background = new THREE.Color(0xffffff);
    
    
      group = new THREE.Group();
      scene.add(group)
    
      //fetch data from database and add object for each entry
      getData()
      async function getData() {
        /**
         * @author TheJim01
         * Replacing DB call with fake data to make it work here.
         * Nancy: Please feel free to add appropriate data.
         */
        // var response = await fetch('/api/indexvr');
        // var data = await response.json();
        var data = [{}, {}, {}, {}, {}]
        //console.log(data)
    
        for (var i = 0; i < data.length; i++) {
          cube = new THREE.Mesh(geometry, material.clone());
          cube.position.x = i;
          scene.add(cube);
          //group.add(data)
        }
      }
    
      // init camera
      camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 1000);
      camera.position.set(15, 15, 15); //camera.position.set( 5, 0, 10 );
      camera.lookAt(scene.position);
      // controls = new OrbitControls( camera, renderer.domElement );
      // controls.enableRotate = true;
    }
    
    function render() {
    
      // update the picking ray with the camera and mouse position
      raycaster.setFromCamera(mouse, camera);
    
      // calculate objects intersecting the picking ray
      var intersects = raycaster.intersectObjects(scene.children);
      if (intersects.length > 0) {
        console.log(intersects);
      }
    
      for (var i = 0; i < intersects.length; i++) {
    
      intersects[i].object.material.color.set(0xff0000);
      
    
      }
    
      renderer.render(scene, camera);
    
    }
    
    window.addEventListener('mousemove', onMouseMove, false);
    
    // Here's the bbasic render loop implementation
    function animate() {
      requestAnimationFrame(animate);
      render();
    }
    animate();
    html,
    body {
      width: 100%;
      height: 100%;
      margin: 0;
      padding: 0;
      overflow: hidden;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/108/three.min.js"></script>
    
    <div id="container"></div>