3
votes

My js skills could be improved to say the least! But struggling with this

I can get my model to load ok into the scene but cannot seem to get the interaction working.

It's like i need to tie in the GLTF file into the raycaster, the below code is part of it. The full Codepen link is below this code.

class PickHelper {
constructor() {
  this.raycaster = new THREE.Raycaster();
  this.pickedObject = null;
  this.pickedObjectSavedColor = 0;
}
pick(normalizedPosition, scene, camera, time) {

  if (this.pickedObject) {
    this.pickedObject.material.emissive.setHex(this.pickedObjectSavedColor);
    this.pickedObject = undefined;
  }

  this.raycaster.setFromCamera(normalizedPosition, camera);

  const intersectedObjects = this.raycaster.intersectObjects(scene.children);
  if (intersectedObjects.length) {
    this.pickedObject = intersectedObjects[0].object;
    this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
    this.pickedObject.material.emissive.setHex((time * 8) % 2 > 1 ? 0xFFFF00 : 0xFF0000);
    this.pickedObject.rotation.y += 0.1 ;

  }
}

https://codepen.io/johneemac/pen/abzqdye << FULL Code

Sorry: Cross origin issue with the gltf file on CodePen though! It won't load but you get the idea hopefully.

Super appreciate any help, thanks!

2
Your code looks roughly correct.. the only thing I can see that may confuse the issue is that you are adding a the gltf "scene" to your other "scene" and im not sure if raycasts propagate across multiple scenes... You may want to do gltf.scene.traverse( (e)=>{if(e.isMesh)mainscene.add(e)} ) and see if that changes the behavior?manthrax

2 Answers

2
votes

You have to perform the intersection test like so:

const intersectedObjects = this.raycaster.intersectObjects(scene.children, true);

Notice the second argument of intersectObjects(). It indicates that the raycaster should process the entire hierarchy of objects which is necessary in context of a loaded glTF asset.

three.js R112

2
votes

It's not clear what you're trying to do. GLTF files are collection of materials, animations, geometries, meshes, etc.. so you can't "pick" a GLTF file. You can "pick" individual elements inside. You could write some code that if something is picked, checks of the thing that was picked is one of the meshes loaded in the GLTF scene and then pick every other thing that was loaded in the GLTF scene.

In any case,

You need to give the RayCaster a list of objects to select from. In the original example that was scene.children which is just the list of Boxes added to the root of the scene. But when loading a GLTF, unless you already know the structure of the GLTF because you created the scene yourself you'll need to go find the things you want to be able to select and add them to some list that you can pass to RayCaster.intersectObjects

This code gets all the Mesh objects from the loaded GLTF file

      let pickableMeshes = [];


      // this is run after loading the gLTT

          // get a list of all the meshes in the scene
          root.traverse((node) => {
            if (node instanceof THREE.Mesh) {
              pickableMeshes.push(node);
            }
          });

Note that you could also pass true as the second argument to RayCaster.intersectObjects as in rayCaster.intersectObjects(scene.children, true). That's probably rarely what you want though as likely you have things in the scene you don't want the user to be able to select. For example if you only wanted the user to be able to select the cars then something like

         // get a list of all the meshes in the scene who's names start with "car"
          root.traverse((node) => {
            if (node instanceof THREE.Mesh && (/^car/i).test(node.name)) {
              pickableMeshes.push(node);
            }
          });

Then, PickHelper class you used was changing the color of the material on each Box but that only works because each Box has its own material. If the Boxes shared materials then changing the material color would change all the boxes.

Loading a different GLTF most the objects shared the same material so to be able to highlight one requires changing the material used with that object or choosing some other method to highlight the selected thing.

function main() {
  const canvas = document.querySelector('#c');
  const renderer = new THREE.WebGLRenderer({canvas});

  const fov = 60;
  const aspect = 2;  // the canvas default
  const near = 0.1;
  const far = 200;
  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
  camera.position.z = 30;

  const scene = new THREE.Scene();
  scene.background = new THREE.Color('white');

  // put the camera on a pole (parent it to an object)
  // so we can spin the pole to move the camera around the scene
  const cameraPole = new THREE.Object3D();
  scene.add(cameraPole);
  cameraPole.add(camera);

  {
    const color = 0xFFFFFF;
    const intensity = 1;
    const light = new THREE.DirectionalLight(color, intensity);
    light.position.set(-1, 2, 4);
    camera.add(light);
  }

  function frameArea(sizeToFitOnScreen, boxSize, boxCenter, camera) {
    const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
    const halfFovY = THREE.Math.degToRad(camera.fov * .5);
    const distance = halfSizeToFitOnScreen / Math.tan(halfFovY);
    // compute a unit vector that points in the direction the camera is now
    // in the xz plane from the center of the box
    const direction = (new THREE.Vector3())
        .subVectors(camera.position, boxCenter)
        .multiply(new THREE.Vector3(1, 0, 1))
        .normalize();

    // move the camera to a position distance units way from the center
    // in whatever direction the camera was from the center already
    camera.position.copy(direction.multiplyScalar(distance).add(boxCenter));

    // pick some near and far values for the frustum that
    // will contain the box.
    camera.near = boxSize / 100;
    camera.far = boxSize * 100;

    camera.updateProjectionMatrix();

    // point the camera to look at the center of the box
    camera.lookAt(boxCenter.x, boxCenter.y, boxCenter.z);
  }

  let pickableMeshes = [];

  {
    const gltfLoader = new THREE.GLTFLoader();
    gltfLoader.load('https://threejsfundamentals.org/threejs/resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf', (gltf) => {
      const root = gltf.scene;
      scene.add(root);

      // compute the box that contains all the stuff
      // from root and below
      const box = new THREE.Box3().setFromObject(root);

      const boxSize = box.getSize(new THREE.Vector3()).length();
      const boxCenter = box.getCenter(new THREE.Vector3());

      // set the camera to frame the box
      frameArea(boxSize * 0.7, boxSize, boxCenter, camera);
      
      // get a list of all the meshes in the scen
      root.traverse((node) => {
        if (node instanceof THREE.Mesh) {
          pickableMeshes.push(node);
        }
      });
    });
  }

  function resizeRendererToDisplaySize(renderer) {
    const canvas = renderer.domElement;
    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;
  }

  class PickHelper {
    constructor() {
      this.raycaster = new THREE.Raycaster();
      this.pickedObject = null;
      this.pickedObjectSavedMaterial = null;
      this.selectMaterial = new THREE.MeshBasicMaterial();
      this.infoElem = document.querySelector('#info');
    }
    pick(normalizedPosition, scene, camera, time) {
      // restore the color if there is a picked object
      if (this.pickedObject) {
        this.pickedObject.material = this.pickedObjectSavedMaterial;
        this.pickedObject = undefined;
        this.infoElem.textContent = '';
      }

      // cast a ray through the frustum
      this.raycaster.setFromCamera(normalizedPosition, camera);
      // get the list of objects the ray intersected
      const intersectedObjects = this.raycaster.intersectObjects(pickableMeshes);
      if (intersectedObjects.length) {
        // pick the first object. It's the closest one
        this.pickedObject = intersectedObjects[0].object;
        // save its color
        this.pickedObjectSavedMaterial = this.pickedObject.material;
        this.pickedObject.material = this.selectMaterial;
        // flash select material color to flashing red/yellow
        this.selectMaterial.color.setHex((time * 8) % 2 > 1 ? 0xFFFF00 : 0xFF0000);
        this.infoElem.textContent = this.pickedObject.name;
      }
    }
  }

  const pickPosition = {x: 0, y: 0};
  const pickHelper = new PickHelper();
  clearPickPosition();

  function render(time) {
    time *= 0.001;  // convert to seconds;

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

    cameraPole.rotation.y = time * .1;

    pickHelper.pick(pickPosition, scene, camera, time);

    renderer.render(scene, camera);

    requestAnimationFrame(render);
  }
  requestAnimationFrame(render);

  function getCanvasRelativePosition(event) {
    const rect = canvas.getBoundingClientRect();
    return {
      x: event.clientX - rect.left,
      y: event.clientY - rect.top,
    };
  }

  function setPickPosition(event) {
    const pos = getCanvasRelativePosition(event);
    pickPosition.x = (pos.x / canvas.clientWidth ) *  2 - 1;
    pickPosition.y = (pos.y / canvas.clientHeight) * -2 + 1;  // note we flip Y
  }

  function clearPickPosition() {
    // unlike the mouse which always has a position
    // if the user stops touching the screen we want
    // to stop picking. For now we just pick a value
    // unlikely to pick something
    pickPosition.x = -100000;
    pickPosition.y = -100000;
  }
  window.addEventListener('mousemove', setPickPosition);
  window.addEventListener('mouseout', clearPickPosition);
  window.addEventListener('mouseleave', clearPickPosition);

  window.addEventListener('touchstart', (event) => {
    // prevent the window from scrolling
    event.preventDefault();
    setPickPosition(event.touches[0]);
  }, {passive: false});

  window.addEventListener('touchmove', (event) => {
    setPickPosition(event.touches[0]);
  });

  window.addEventListener('touchend', clearPickPosition);
}

main();
body { margin: 0; }
#c { width: 100vw; height: 100vh; display: block; }
#info { position: absolute; left: 0; top: 0; background: black; color: white; padding: 0.5em; font-family: monospace; }
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r112/build/three.min.js"></script>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r112/examples/js/loaders/GLTFLoader.js"></script>

<canvas id="c"></canvas>
<div id="info"></div>