巡检 · 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. 松开鼠标时,执行相机回归中心方向的动画
*/
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
小结
- 本文提供 巡检 完整 Three.js 源码与在线 Demo,建议先运行案例再改 uniform/参数做二次实验
- 更多 Three.js 实战案例见 three-cesium-examples 合集 与 GitHub 开源仓库