模型导航 ·Model Nav· ▶ 在线运行案例
- 案例合集:三维可视化功能案例(threehub.cn)
- 开源仓库github地址:https://github.com/z2586300277/three-cesium-examples
- 400个案例代码:网盘链接
你将学到什么
- OrbitControls 相机轨道交互
- glTF/Draco 模型加载与优化
- GSAP 时间轴与补间动画
requestAnimationFrame渲染循环与resize自适应
效果说明
本案例演示模型导航效果:基于 WebGL 实现「模型导航」可视化效果,附完整可运行源码;核心用到 OrbitControls、glTF/Draco、GSAP。建议先打开文首在线案例查看动态画面,再对照下方源码逐步理解。
核心概念
- Scene / Camera / WebGLRenderer构成最小渲染闭环;大场景可开
logarithmicDepthBuffer缓解 Z-fighting。 - OrbitControls提供轨道旋转/缩放;开启
enableDamping后需在 animate 中controls.update()。 - 阅读下方完整源码时,建议从
init/load/animate三条主线入手,再深入 shader 与工具函数。
实现步骤
- 搭建灯光与环境(如有)
- requestAnimationFrame 循环 update + render
代码要点
// 模型导航 从0,0,0自动寻路至56,0,0 改变坐标即可重新计算(version:0.1:导航错误等未处理)import * as THREE from "three"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js"; const box = document.getElementById("box");
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 75, box.clientWidth / box.clientHeight, 0.1, 1000 );
camera.position.set(20, 50, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, logarithmicDepthBuffer: true, });
renderer.setSize(box.clientWidth, box.clientHeight);
scene.add(new THREE.AxesHelper(10)) scene.add(new THREE.GridHelper(100, 100)) box.appendChild(renderer.domElement);
new OrbitControls(camera, renderer.domElement);
window.onresize = () => { renderer.setSize(box.clientWidth, box.clientHeight);
camera.aspect = box.clientWidth / box.clientHeight; camera.updateProjectionMatrix(); };
animate();
function animate() { requestAnimationFrame(animate); TWEEN.update() renderer.render(scene, camera); }
scene.add(new THREE.AmbientLight(0xfff, 4)); // 加载模型 gltf/ glb draco解码器 const loader = new GLTFLoader();
loader.setDRACOLoader( new DRACOLoader().setDecoderPath( FILE_HOST + "js/three/draco/" ) );
import { init } from "@recast-navigation/core"; import * as TRR from "@recast-navigation/three"; let path; const add_nav_mesh = async () => { console.log(init); await init(); loader.load( FILE_HOST + "files/model/navmesh02.glb", (gltf) => { scene.add(gltf.scene); let navMesh = TRR.threeToSoloNavMesh(gltf.scene.children); console.log(navMesh); // navmesh helper // const helper = new TRR.NavMeshHelper(navMesh); // scene.add(helper); gltf.scene.traverse((mesh) => { if (mesh.isMesh) { mesh.material.side = THREE.DoubleSide; } }); let pathResult = compute_route(navMesh.navMesh); // 实例化路径 path = pathResult.path draw_path(pathResult.path); } ); }; import { NavMeshQuery } from "@recast-navigation/core"; import { Vector3 } from "three"; const compute_route = (navMesh) => { const navMeshQuery = new NavMeshQuery(navMesh); const startPosition = new Vector3(0, 0, 0); const endPosition = new Vector3(56, 0, 0); let route = navMeshQuery.computePath(startPosition, endPosition); return route; }; import * as Path from "three.path"; import { StaticDrawUsage, Mesh, MeshPhongMaterial, DoubleSide } from "three"; const draw_path = (path) => { const points = new Path.PathPointList(); points.set(path, 0.1, 10, new Vector3(0, 1, 0)); const geo = new Path.PathGeometry( { pathPointList: points, options: { width: 0.1, // default is 0.1 arrow: true, // default is true progress: 1, // default is 1 side: "both", // "left"/"right"/"both", default is "both" }, usage: StaticDrawUsage, // geometry usage }, false ); var material = new MeshPhongMaterial({ color: 0x58dede, depthWrite: true, transparent: true, opacity: 0.9, side: DoubleSide, }); var mesh = new Mesh(geo, material); scene.add(mesh); through_path(path) }; const computeDistance = (vec1, vec2) => { return Math.sqrt( Math.pow(vec1.x - vec2.x, 2) + Math.pow(vec1.y - vec2.y, 2) + Math.pow(vec1.z - vec2.z, 2) ); }; // 运动路径示意 import * as TWEEN from "@tweenjs/tween.js"; const add_box = () => { const geometry = new THREE.BoxGeometry(1, 1, 1); //几何体 const material = new THREE.MeshBasicMaterial({ color: 0xff0000 }); //材质 const mesh = new THREE.Mesh(geometry, material); //网格模型 // mesh.position.set(0, 10, 0); //网格模型位置 scene.add(mesh); //场景添加网格模型 return mesh; }; let i = 0; const through_path = () => { const distance = computeDistance(path[i], path[i + 1]); const tween = new TWEEN.Tween(path[i]); let box = add_box(); const time = Math.round(distance / 2); tween .to(path[i + 1], time * 1000) .onUpdate((item) => { box.position.copy(item); }) .onComplete(() => { if (i + 2 < path.length) { i = i + 1; through_path(); } }); tween.start(); }; await add_nav_mesh();完整源码:GitHub
小结
- 本文提供模型导航完整 Three.js 源码与在线 Demo,建议先运行案例再改 uniform/参数做二次实验
- 更多 Three.js 实战案例见 three-cesium-examples 合集 与 GitHub 开源仓库