news 2026/7/18 13:03:06

Three.js 测量教程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Three.js 测量教程

测量 ·Line Measure· ▶ 在线运行案例

  • 案例合集:三维可视化功能案例(threehub.cn)
  • 开源仓库github地址:https://github.com/z2586300277/three-cesium-examples
  • 400个案例代码:网盘链接

你将学到什么

  • OrbitControls 相机轨道交互
  • TransformControls 模型拖拽变换
  • Raycaster 鼠标拾取与交互
  • BufferGeometry 自定义顶点/索引数据
  • requestAnimationFrame渲染循环与resize自适应

效果说明

本案例演示测量效果:支持鼠标拾取、绘制或拖拽交互;核心用到 OrbitControls、TransformControls、Raycaster。建议先打开文首在线案例查看动态画面,再对照下方源码逐步理解。

核心概念

  • OrbitControls轨道旋转缩放;开enableDamping时每帧需controls.update()
  • CubeTexture六面贴图作scene.backgroundscene.environment供 PBR 材质反射。

实现步骤

  • 搭建 Scene / Camera / Renderer 与 OrbitControls
  • rAF 循环中 update 并 render
  • 代码要点

    import * as THREE from "three";

    import { OrbitControls } from "three/addons/controls/OrbitControls.js"; import { TransformControls } from "three/addons/controls/TransformControls.js";

    const worldToScreenPosition = (pos, camera) => { // 三维坐标转换成屏幕坐标 const worldVector = new THREE.Vector3(pos.x, pos.y, pos.z); const standardVector = worldVector.project(camera); const widthHalf = window.innerWidth / 2; const heightHalf = window.innerHeight / 2; return { x: Math.round(standardVector.x * widthHalf + widthHalf), y: Math.round(-standardVector.y * heightHalf + heightHalf), z: 1, }; }

    let renderer, scene, camera, orbitControls, transformControls;

    let plane; let raycaster = new THREE.Raycaster(); let intersects; let mouse = new THREE.Vector2(); let markersGroup = new THREE.Group(); let markers = []; let points = []; let vec2Array = []; let dashLine; let line; let canDrawLine = true; let mesh; let index = 0; let distanceArray = []; let distance = 0; let totalDistance = 0; let geometry = null; let distanceDom = null; let nextPoint = null;

    init(); animate();

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

    scene = new THREE.Scene(); scene.background = new THREE.Color(0xbfe3dd); scene.add(markersGroup);

    camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 1, 100 ); camera.position.set(10, 10, 10);

    //plane let planeGeo = new THREE.PlaneGeometry(40, 40, 40); let planeMat = new THREE.MeshBasicMaterial({ color: 0x6666666 }); plane = new THREE.Mesh(planeGeo, planeMat); plane.rotation.x = -Math.PI / 2; scene.add(plane);

    // orbit orbitControls = new OrbitControls(camera, renderer.domElement); orbitControls.target.set(0, 0.5, 0); orbitControls.update(); orbitControls.enableDamping = true;

    // transform transformControls = new TransformControls(camera, renderer.domElement); transformControls.addEventListener("dragging-changed", onDragChanged); transformControls.addEventListener("mouseUp", onDragend); transformControls.addEventListener("change", onDragChanging); scene.add(transformControls); function onDragChanged(event) { orbitControls.enabled = !event.value; }

    document.body.addEventListener("mousedown", onMousedown, false); document.body.addEventListener("mousemove", onMousemove, false); window.onresize = function () { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }; } function animate() { requestAnimationFrame(animate); orbitControls.update(); renderer.render(scene, camera); } function onDragChanging(event) { if (points.length < 2) return;

    let point = mesh.position; let _idx = mesh._idx; updatePoints(points, _idx, point);

    totalDistance = updateDistanceArray(points, distanceArray); distanceDom.innerHTML = totalDistance.toFixed(2) + "m"; const positions = "array" in line.geometry.attributes.position && line.geometry.attributes.position.array;

    if (_idx === 0) { positions[0] = point.x; positions[1] = point.y; positions[2] = point.z; } else { let i = (2_idx - 1)3; positions[i] = point.x; positions[i + 1] = point.y; positions[i + 2] = point.z; positions[i + 3] = point.x; positions[i + 4] = point.y; positions[i + 5] = point.z; }

    line.geometry.attributes.position.needsUpdate = true; } function onDragend() { console.log(111); transformControls.detach(); } function onMousedown(event) { raycaster.setFromCamera(mouse, camera);

    if (canDrawLine) { // 右键点击之后停止测距 if (event.button === 2) { canDrawLine = false; } if (points.length > 0) { distanceArray.push(distance); totalDistance += distance; distance = 0 }

    intersects = raycaster.intersectObject(plane); if (intersects.length === 0) return; let point = intersects[0].point; let newPoint = new THREE.Vector3(point.x, point.y, point.z);

    // 判断是否绘制完成 if (points.length > 0 && canDrawLine === true) { points.push(newPoint, newPoint); } else { points.push(newPoint); }

    // 添加节点 let marker = new THREE.Mesh( new THREE.SphereGeometry(0.1, 30, 30), new THREE.MeshBasicMaterial({ color: 0xff5fff }) ); marker._idx = index; marker.position.set(newPoint.x, newPoint.y, newPoint.z); markersGroup.add(marker); markers.push(marker);

    // 记录场景中点位的数量 index++;

    if (!distanceDom) { distanceDom = document.createElement("span"); distanceDom.style.position = "absolute"; distanceDom.style.top = "0"; distanceDom.style.color = "red"; distanceDom.style.pointerEvents = "none"; document.body.appendChild(distanceDom); }

    if (points.length > 1) { createLine(scene, points); }

    if (!canDrawLine) { scene.remove(dashLine); return; }

    // 如果存在虚线,更新虚线位置 if (dashLine) { const positions = "array" in dashLine.geometry.attributes.position && dashLine.geometry.attributes.position.array;

    positions[0] = positions[3]; positions[1] = positions[4]; positions[2] = positions[5]; positions[3] = newPoint.x; positions[4] = newPoint.y; positions[5] = newPoint.z;

    dashLine.geometry.attributes.position.needsUpdate = true; } else { geometry = new THREE.BufferGeometry().setFromPoints([ newPoint, newPoint, ]); dashLine = new THREE.LineSegments( geometry, new THREE.LineDashedMaterial({ color: 0xff5555, transparent: true, depthTest: false, dashSize: 0.1, gapSize: 1, }) ); dashLine.frustumCulled = false; dashLine.name = "dashLine"; scene.add(dashLine); } } else { intersects = raycaster.intersectObject(markersGroup); if (intersects.length === 0) return;

    mesh = intersects[0].object; transformControls.attach(mesh); } } function updateLinePoints(line, point, arrayIndex) { const positions = "array" in line.geometry.attributes.position && line.geometry.attributes.position.array;

    positions[arrayIndex] = point.x; positions[arrayIndex + 1] = point.y; positions[arrayIndex + 2] = point.z;

    line.geometry.attributes.position.needsUpdate = true; } function onMousemove(event) { event.preventDefault(); mouse.x = (event.clientX / renderer.domElement.offsetWidth) * 2 - 1; mouse.y = -(event.clientY / renderer.domElement.offsetHeight) * 2 + 1; if (!canDrawLine) return;

    raycaster.setFromCamera(mouse, camera); intersects = raycaster.intersectObject(plane); if (!intersects.length) return;

    nextPoint = intersects[0].point; if (points.length > 0) { const pre = points[points.length - 1]; const v0 = new THREE.Vector3(pre.x, pre.y, pre.z); const v1 = new THREE.Vector3(nextPoint.x, nextPoint.y, nextPoint.z); distance = v0.distanceTo(v1); }

    if (!dashLine) return; updateLinePoints(dashLine, nextPoint, 3); } function createLine(scene, points) { if (line) scene.remove(line); geometry = new THREE.BufferGeometry().setFromPoints([...points]); line = new THREE.LineSegments( geometry, new THREE.LineBasicMaterial({ color: 0xfff000, transparent: true, depthTest: false, }) ); line.renderOrder = 10; line.name = "line"; line.onBeforeRender = () => { if (distanceDom) { let v = worldToScreenPosition(nextPoint, camera); distanceDom.style.left = v.x + 15 + "px"; distanceDom.style.top = v.y - 15 + "px"; distanceDom.innerHTML = (distance + totalDistance).toFixed(2) + "m"; } }; scene.add(line); } function updatePoints(points, index, point) { if (index === 0) { points[index] = point; } else if (index === points.length / 2) { points[points.length - 1] = point; nextPoint = points[points.length - 1]; } else { points[2 * index - 1] = point; points[2 * index] = point; } } function updateDistanceArray(points, array) { let distance = 0; for (let i = 0; i < array.length; i++) { array[i] = points[2i].distanceTo(points[2i + 1]); distance += array[i]; } return distance; } function dispose() { scene.traverse((e) => { if (e instanceof THREE.Mesh) { e.geometry.dispose(); e.material.dispose(); } }); scene.clear(); renderer.dispose(); renderer.forceContextLoss(); }

    完整源码:GitHub

    小结

    • 本文提供测量完整 Three.js 源码与在线 Demo,建议先运行案例再改 uniform/参数做二次实验
    • 更多 Three.js 实战案例见 three-cesium-examples 合集 与 GitHub 开源仓库
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/18 13:01:10

从零理解 AI Skill:不只是Markdown这么简单

目录从零理解 AI Skill&#xff1a;不只是Markdown这么简单先说我最开始的误解Skill 到底是什么Prompt 和 Skill 的区别&#xff0c;不在长度Skill 和 Agent、Memory、Workflow、MCP 的关系Skill 和 MCP&#xff1a;一个管工艺&#xff0c;一个管机器Codex、Claude Code、Curso…

作者头像 李华
网站建设 2026/7/18 12:57:46

深入解析Atmosphere大气层:模块化架构下的Switch自定义固件实践

深入解析Atmosphere大气层&#xff1a;模块化架构下的Switch自定义固件实践 【免费下载链接】Atmosphere-stable 大气层整合包系统稳定版 项目地址: https://gitcode.com/gh_mirrors/at/Atmosphere-stable Atmosphere大气层作为Nintendo Switch上最成熟的自定义固件解决…

作者头像 李华
网站建设 2026/7/18 12:56:44

BMS均衡技术:被动与主动方案对比及工程实践

1. BMS均衡技术概述 电池管理系统(BMS)中的均衡技术是解决电池组单体电压差异的关键手段。在锂电池组应用中&#xff0c;由于制造工艺、使用环境等因素导致的单体电池差异会随着充放电循环不断累积&#xff0c;严重影响电池组的整体性能和寿命。 均衡技术主要分为被动均衡和主…

作者头像 李华
网站建设 2026/7/18 12:55:44

UF2固件格式终极指南:3分钟掌握跨平台烧录技术

UF2固件格式终极指南&#xff1a;3分钟掌握跨平台烧录技术 【免费下载链接】uf2 UF2 file format specification 项目地址: https://gitcode.com/gh_mirrors/uf/uf2 UF2&#xff08;USB Flashing Format&#xff09;是微软开发的一种革命性固件烧录格式&#xff0c;专为…

作者头像 李华
网站建设 2026/7/18 12:53:23

从零构建具身智能机器人:基于LeRobot与ACT模型的模仿学习全流程实践

1. 项目概述&#xff1a;从零到一&#xff0c;让机器人“学会”执行任务 如果你对机器人、人工智能&#xff0c;尤其是最近火热的“具身智能”感兴趣&#xff0c;但又觉得动辄几十上百万的工业机械臂和复杂的算法框架让人望而却步&#xff0c;那么LeRobot这个项目可能就是为你量…

作者头像 李华
网站建设 2026/7/18 12:52:22

基于EasyWeChat与ChatterBot搭建微信公众号智能客服系统

1. 项目概述与背景 最近在运营公众号时发现一个痛点&#xff1a;用户发送消息后往往需要等待人工回复&#xff0c;而夜间或节假日时段根本无法及时响应。这让我开始思考如何用技术手段解决这个问题。经过调研&#xff0c;发现结合EasyWeChat和ChatterBot可以快速搭建一个智能回…

作者头像 李华