Three.js 巡检教程

巡检 · Inspection View · ▶ 在线运行案例

你将学到什么

  • OrbitControls 相机轨道交互
  • glTF/Draco 模型加载与优化
  • requestAnimationFrame 渲染循环与 resize 自适应

效果说明

本案例演示 巡检 效果:基于 WebGL 实现「巡检」可视化效果,附完整可运行源码;核心用到 OrbitControls、glTF/Draco。建议先打开文首在线案例查看动态画面,再对照下方源码逐步理解。

核心概念

  • Loader 异步加载模型;glTF 返回 gltf.scene,加载后注意 scale 与坐标系。Draco 需配置 DRACOLoader
  • OrbitControls 轨道旋转缩放;开 enableDamping 时每帧需 controls.update()

实现步骤

  • 搭建 Scene / Camera / Renderer 与 OrbitControls
  • Loader 异步加载模型/纹理资源
  • rAF 循环中 update 并 render

代码要点

复制代码
  import * as THREE from "three";

  import {GLTFLoader} from "three/examples/jsm/loaders/GLTFLoader.js";
  import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls.js'
  import {DRACOLoader} from "three/examples/jsm/loaders/DRACOLoader.js";
  import { PointerLockControls } from "three/examples/jsm/controls/PointerLockControls.js";
  import { GUI } from "three/examples/jsm/libs/lil-gui.module.min.js";
  import { animate } from "animejs";



  let scene, renderer, camera, orbitControls;



  let isRoaming = false; // 漫游模式标志



  let camera2; // 主要用于动画的相机
  const eye1 = new THREE.Object3D();



  let pointerlockControls;



  // 用于动画的位置/旋转数据对象(animejs 直接修改此对象属性)
  const itemData = { px: 0, py: 0, pz: 0, rx: 0, ry: 0, rz: 0 };



  window.addEventListener('load', () => {
      init();
      render();
      eventBinding();
  });



  // 初始化
  function init() {
      scene = new THREE.Scene();



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



  camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 2000);
      camera.position.set(100, 100, 100);



  orbitControls = new OrbitControls(camera, renderer.domElement);
      orbitControls.enableDamping = true;



  camera2 = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);
      camera2.position.set(0, 2, 0);



  const cameraHelper = new THREE.CameraHelper(camera2);
      scene.add(cameraHelper);



  // 将相机放置于 eye1 对象中,通过移动 eye1 驱动相机路径
      eye1.add(camera2);
      scene.add(eye1);



  pointerlockControls = new PointerLockControls(camera2, renderer.domElement);



  scene.add(new THREE.AmbientLight(0xffffff, 1));
      scene.add(new THREE.DirectionalLight(0xffffff, 1.5));
      scene.add(new THREE.GridHelper(200, 20));



  const gltfLoader = new GLTFLoader()
      const dracoLoader = new DRACOLoader()
      dracoLoader.setDecoderPath(FILE_HOST + 'js/three/draco/')
      gltfLoader.setDRACOLoader(dracoLoader)
      gltfLoader.load(FILE_HOST + 'models/modern_city.glb', (gltf) => {
          gltf.scene.scale.set(0.03, 0.03, 0.03);
          scene.add(gltf.scene)
      })



  const gui = new GUI();
      const btns = { startRoam: autoRun, stopAutoRun };
      gui.add(btns, 'startRoam').name('开启漫游/巡检');
      gui.add(btns, 'stopAutoRun').name('结束漫游/巡检');
      gui.add(cameraHelper, 'visible').name('相机辅助线');



  window.addEventListener('resize', onResize);
  }



  // 窗口尺寸变化响应
  function onResize() {
      const w = window.innerWidth;
      const h = window.innerHeight;
      renderer.setSize(w, h);
      camera.aspect = w / h;
      camera.updateProjectionMatrix();
      camera2.aspect = w / h;
      camera2.updateProjectionMatrix();
  }



  // 开启漫游
  function autoRun() {
      isRoaming = true;
      orbitControls.enabled = false;



  // 同步 itemData 为 eye1 当前状态,避免重复调用时跳变
      itemData.px = eye1.position.x;
      itemData.py = eye1.position.y;
      itemData.pz = eye1.position.z;
      itemData.rx = eye1.rotation.x;
      itemData.ry = eye1.rotation.y;
      itemData.rz = eye1.rotation.z;



  const a90 = -Math.PI / 2;
      const time1 = 6000; // 直线段时长
      const time2 = 6000; // 等待转弯时长
      const time3 = 1200; // 转弯时长



  // 巡检路径:沿城市街道绕行一圈(坐标根据城市模型缩放后的街道位置设定)
      animate(itemData, {
          px: [
              { to: 0,   duration: time1 },
              { to: 30,  duration: time1, delay: time3 },
              { to: 30,  duration: time1, delay: time3 },
              { to: 0,   duration: time1, delay: time3 }
          ],
          pz: [
              { to: -30, duration: time1 },
              { to: -30, duration: time1, delay: time3 },
              { to: 0,   duration: time1, delay: time3 },
              { to: 0,   duration: time1, delay: time3 }
          ],
          ry: [
              { to: a90,     duration: time3, delay: time2 },
              { to: a90 * 2, duration: time3, delay: time2 },
              { to: a90 * 3, duration: time3, delay: time2 },
              { to: a90 * 4, duration: time3, delay: time2 }
          ],
          ease: "linear",
          onUpdate: () => {
              const { px, py, pz, rx, ry, rz } = itemData;
              eye1.position.set(px, py, pz);
              eye1.rotation.set(rx, ry, rz);
          },
          onComplete: () => resetEye()
      });
  }



  // 停止漫游
  function stopAutoRun() {
      isRoaming = false;
      resetEye();
  }



  // 重置 eye1 位置及 itemData 状态
  function resetEye() {
      eye1.position.set(0, 0, 0);
      eye1.rotation.set(0, 0, 0);
      itemData.px = 0; itemData.py = 0; itemData.pz = 0;
      itemData.rx = 0; itemData.ry = 0; itemData.rz = 0;
      orbitControls.enabled = true;
  }



  // 绑定交互事件
  function eventBinding() {
      window.addEventListener("mousedown", onMouseDown);
      window.addEventListener("mouseup", onMouseRelease);
      window.addEventListener("mouseleave", onMouseRelease);
  }



  function onMouseDown() {
      if (isRoaming) {
          pointerlockControls.isLocked = true;
      }
  }



  function onMouseRelease() {
      if (isRoaming) {
          pointerlockControls.isLocked = false;
          cameraReturn();
      }
  }



  /*


    
  * 关键开发思路

  * 1. 使用 eye1 对象执行路径移动

  * 2. 利用鼠标事件对 camera2 进行方向控制

  * 3. 松开鼠标时,执行相机回归中心方向的动画

   */
  function render() {
      requestAnimationFrame(render);
      orbitControls.update();
      renderer.render(scene, isRoaming ? camera2 : camera);
  }




  `// 相机视角回归动画(使用四元数避免 rotation 动画的万向锁问题)
  function cameraReturn() {
      animate(camera2.quaternion, {
          x: 0, y: 0, z: 0, w: 1,
          ease: "linear",
          duration: 500
      });
  }
  `

完整源码:GitHub

小结