巡检 ·Inspection View· ▶ 在线运行案例
- 案例合集:三维可视化功能案例(threehub.cn)
- 开源仓库github地址:https://github.com/z2586300277/three-cesium-examples
- 400个案例代码:网盘链接
你将学到什么
- 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. 松开鼠标时,执行相机回归中心方向的动画
// 相机视角回归动画(使用四元数避免 rotation 动画的万向锁问题) function cameraReturn() { animate(camera2.quaternion, { x: 0, y: 0, z: 0, w: 1, ease: "linear", duration: 500 }); }完整源码:GitHub
小结
- 本文提供巡检完整 Three.js 源码与在线 Demo,建议先运行案例再改 uniform/参数做二次实验
- 更多 Three.js 实战案例见 three-cesium-examples 合集 与 GitHub 开源仓库