Three.js 中的 GLTF 模型和交互

GLTF model and interaction in Three.js

至少可以提高我的 js 技能!但为此苦苦挣扎

我可以让我的模型正常加载到场景中,但似乎无法进行交互。

好像我需要将 GLTF 文件绑定到 raycaster 中,下面的代码是其中的一部分。完整的 Codepen link 在此代码下方。

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 << 完整代码

抱歉:尽管 CodePen 上的 gltf 文件存在跨源问题!它不会加载,但你希望你明白了。

非常感谢任何帮助,谢谢!

你必须像这样执行交集测试:

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

注意 intersectObjects() 的第二个参数。它表明光线投射器应该处理整个对象层次结构,这在加载的 glTF 资产的上下文中是必需的。

three.js R112

不清楚您要做什么。 GLTF 文件是 materials、动画、几何图形、网格等的集合。因此您不能 "pick" GLTF 文件。您可以 "pick" 里面的单个元素。您可以编写一些代码,如果选择了某些东西,则检查所选择的东西是 GLTF 场景中加载的网格之一,然后选择 GLTF 场景中加载的所有其他东西。

无论如何,

您需要为 RayCaster 提供 select 的对象列表。在原始示例中,scene.children 只是添加到场景根目录的 Boxes 列表。但是当加载 GLTF 时,除非你已经知道 GLTF 的结构,因为你自己创建了场景,否则你需要去寻找你想要的东西 select 并将它们添加到你可以的列表中传递给 RayCaster.intersectObjects

此代码从加载的 GLTF 文件中获取所有 Mesh 对象

      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);
            }
          });

请注意,您也可以像 rayCaster.intersectObjects(scene.children, true) 一样将 true 作为第二个参数传递给 RayCaster.intersectObjects。这可能很少是你想要的,尽管场景中可能有你不希望用户能够 select 的东西。例如,如果您只希望用户能够 select 汽车,那么类似于

         // 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);
            }
          });

然后,您使用的 PickHelper class 正在更改每个 Box 上 material 的颜色,但这只有效,因为每个 Box 都有自己的 material。如果 Boxes 共享 materials 然后改变 material 颜色会改变所有的盒子。

加载不同的 GLTF 大多数对象共享相同的 material 因此要突出显示一个对象需要更改与该对象一起使用的 material 或选择其他方法来突出显示 select编辑的东西。

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>