资讯动态

OpenMontage 中的 Three.js 交互开发指南:Raycaster、相机控制与对象选择实战

发布时间:2026/9/10 11:57:17 来源:尧图企业网站定制
OpenMontage 中的 Three.js 交互开发指南Raycaster、相机控制与对象选择实战【免费下载链接】OpenMontageWorlds first open-source, agentic video production system. 12 production pipelines, 100 tools, 700 agent skill and production-knowledge files. Turn your AI coding assistant into a full video production studio.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMontage导读本指南系统讲解 Three.js 交互层的核心技术基于 Raycaster 的射线拾取点击检测、悬停高亮、框选、六种常用相机控制Orbit / Fly / FirstPerson / PointerLock / Trackball / Map、TransformControls 与 DragControls 操作器以及世界坐标与屏幕坐标的互转。在 OpenMontage 项目中这套交互能力直接服务于threejs_world工具产出的可编辑 3D 世界工作区tools/graphics/threejs_world.py也适用于任何需要鼠标/触摸输入、点击检测、相机漫游或交互式 3D 体验的前端场景。读完本指南你将能独立实现一套点击选中 悬停反馈 相机操控 对象拖拽的完整 3D 交互系统。快速上手一个可运行的交互骨架所有 Three.js 交互的起点都是一样的创建渲染器、场景与相机然后在动画循环中更新控制器并渲染。下面这段代码同时集成了轨道控制与鼠标点击拾取是后续各节的最小可运行骨架import * as THREE from three; import { OrbitControls } from three/addons/controls/OrbitControls.js; // Camera controls const controls new OrbitControls(camera, renderer.domElement); controls.enableDamping true; // Raycasting for click detection const raycaster new THREE.Raycaster(); const mouse new THREE.Vector2(); function onClick(event) { mouse.x (event.clientX / window.innerWidth) * 2 - 1; mouse.y -(event.clientY / window.innerHeight) * 2 1; raycaster.setFromCamera(mouse, camera); const intersects raycaster.intersectObjects(scene.children); if (intersects.length 0) { console.log(Clicked:, intersects[0].object); } } window.addEventListener(click, onClick);这段代码的要点在于两点鼠标坐标归一化clientX/clientY是浏览器窗口像素坐标必须转换成 NDC归一化设备坐标范围 [-1, 1]后才能交给 Raycaster其中 Y 轴方向相反屏幕向下为正NDC 向上为正射线与场景求交intersectObjects(scene.children)返回按距离排序的相交结果intersects[0]即最近命中对象。Raycaster一切拾取的地基基本用法与返回结构Raycaster 支持两种射线构建方式setFromCamera(mousePosition, camera)从相机出发进行鼠标拾取set(origin, direction)从任意原点沿指定方向投射direction 必须是归一化向量const raycaster new THREE.Raycaster(); // From camera (mouse picking) raycaster.setFromCamera(mousePosition, camera); // From any origin and direction raycaster.set(origin, direction); // origin: Vector3, direction: normalized Vector3 // Get intersections const intersects raycaster.intersectObjects(objects, recursive); // intersects array contains: // { // distance: number, // Distance from ray origin // point: Vector3, // Intersection point in world coords // face: Face3, // Intersected face // faceIndex: number, // Face index // object: Object3D, // Intersected object // uv: Vector2, // UV coordinates at intersection // uv1: Vector2, // Second UV channel // normal: Vector3, // Interpolated face normal // instanceId: number // For InstancedMesh // }每个相交条目都包含丰富信息distance用于排序或距离判断point是命中的世界坐标object是命中对象uv可用来做纹理采样而instanceId专门用于InstancedMesh——这在 OpenMontage 的程序化世界中非常关键见下文与仓库 Three.js 工具链的结合。鼠标坐标转换全窗口与指定画布默认公式假设事件监听绑定在window上全窗口归一化const mouse new THREE.Vector2(); function updateMouse(event) { // For full window mouse.x (event.clientX / window.innerWidth) * 2 - 1; mouse.y -(event.clientY / window.innerHeight) * 2 1; }但当页面中存在多个画布或画布未铺满窗口时必须用getBoundingClientRect()将坐标限定到具体 canvas 的本地坐标系否则拾取会整体偏移// For specific canvas element function updateMouseCanvas(event, canvas) { const rect canvas.getBoundingClientRect(); mouse.x ((event.clientX - rect.left) / rect.width) * 2 - 1; mouse.y -((event.clientY - rect.top) / rect.height) * 2 1; }这也是文档事件处理最佳实践一节中InteractionManager.updateMouse的推荐做法——始终把事件绑定在renderer.domElement上并使用其边界矩形换算。触摸支持移动端的处理与鼠标完全同构只需从event.touches[0]取第一个触点function onTouchStart(event) { event.preventDefault(); if (event.touches.length 1) { const touch event.touches[0]; mouse.x (touch.clientX / window.innerWidth) * 2 - 1; mouse.y -(touch.clientY / window.innerHeight) * 2 1; raycaster.setFromCamera(mouse, camera); const intersects raycaster.intersectObjects(clickableObjects); if (intersects.length 0) { handleSelection(intersects[0]); } } } renderer.domElement.addEventListener(touchstart, onTouchStart);注意三点event.preventDefault()防止浏览器默认滚动/缩放行为只处理单指touches.length 1以区分拖拽与点击事件绑定在renderer.domElement上而非 window避免与页面其他手势冲突。Raycaster 选项近远裁剪、线/点精度与层过滤const raycaster new THREE.Raycaster(); // Near/far clipping (default: 0, Infinity) raycaster.near 0; raycaster.far 100; // Line/Points precision raycaster.params.Line.threshold 0.1; raycaster.params.Points.threshold 0.1; // Layers (only intersect objects on specific layers) raycaster.layers.set(1);near/far限制有效命中距离区间适合剔除远距离误选params.Line.threshold/params.Points.threshold对Line和Points这类无面几何体设置命中容差像素/世界单位没有它射线几乎无法点中一根线条layersRaycaster 与 Object3D 各有一个 32 位 Layer 掩码只有双方层匹配的对象才会参与求交是场景规模变大后最优雅的过滤手段。高效投射限制对象、分层、节流// Only check specific objects const clickables [mesh1, mesh2, mesh3]; const intersects raycaster.intersectObjects(clickables, false); // Use layers for filtering mesh1.layers.set(1); // Clickable layer raycaster.layers.set(1); // Throttle raycast for hover effects let lastRaycast 0; function onMouseMove(event) { const now Date.now(); if (now - lastRaycast 50) return; // 20fps max lastRaycast now; // Raycast here }三个层次的优化由浅入深首先只对可点击清单求交并关闭递归recursive: false其次用 Layers 隔离可点击层最后对高频的mousemove悬停检测做 50ms 节流把每帧射线投射次数压到每秒最多 20 次避免在复杂场景中拖垮帧率。相机控制六种控制器选型与配置OrbitControls最常用的轨道/检视控制器围绕目标点旋转、缩放、平移适合模型检视、编辑器和世界浏览import { OrbitControls } from three/addons/controls/OrbitControls.js; const controls new OrbitControls(camera, renderer.domElement); // Damping (smooth movement) controls.enableDamping true; controls.dampingFactor 0.05; // Rotation limits controls.minPolarAngle 0; // Top controls.maxPolarAngle Math.PI / 2; // Horizon controls.minAzimuthAngle -Math.PI / 4; // Left controls.maxAzimuthAngle Math.PI / 4; // Right // Zoom limits controls.minDistance 2; controls.maxDistance 50; // Enable/disable features controls.enableRotate true; controls.enableZoom true; controls.enablePan true; // Auto-rotate controls.autoRotate true; controls.autoRotateSpeed 2.0; // Target (orbit point) controls.target.set(0, 1, 0); // Update in animation loop function animate() { controls.update(); // Required for damping and auto-rotate renderer.render(scene, camera); }关键参数速查minPolarAngle/maxPolarAngle限制俯仰角0为顶部视角Math.PI/2为地平线视角minAzimuthAngle/maxAzimuthAngle限制水平方位角范围minDistance/maxDistance限制缩放距离enableDampingdampingFactor开启惯性阻尼但必须在动画循环中调用controls.update()否则阻尼不生效autoRotateautoRotateSpeed自动旋转适合展台/预览场景同样依赖update()。FlyControls自由飞行import { FlyControls } from three/addons/controls/FlyControls.js; const controls new FlyControls(camera, renderer.domElement); controls.movementSpeed 10; controls.rollSpeed Math.PI / 24; controls.dragToLook true; // Update with delta function animate() { controls.update(clock.getDelta()); renderer.render(scene, camera); }适合第一人称探索或无人机航拍式的自由相机。update()必须传入帧间隔delta单位秒否则运动速度与帧率耦合。FirstPersonControls第一人称射击视角import { FirstPersonControls } from three/addons/controls/FirstPersonControls.js; const controls new FirstPersonControls(camera, renderer.domElement); controls.movementSpeed 10; controls.lookSpeed 0.1; controls.lookVertical true; controls.constrainVertical true; controls.verticalMin Math.PI / 4; controls.verticalMax (Math.PI * 3) / 4; function animate() { controls.update(clock.getDelta()); }lookSpeed控制鼠标转头的灵敏度verticalMin/Max限定垂直视野范围constrainVertical决定是否强制执行该限制。PointerLockControls指针锁定 键盘漫游适合游戏式第一人称漫游点击画布锁定鼠标指针隐藏光标并持续捕获移动配合 WASD 键控制位移import { PointerLockControls } from three/addons/controls/PointerLockControls.js; const controls new PointerLockControls(camera, document.body); // Lock pointer on click document.addEventListener(click, () { controls.lock(); }); controls.addEventListener(lock, () { console.log(Pointer locked); }); controls.addEventListener(unlock, () { console.log(Pointer unlocked); }); // Movement const velocity new THREE.Vector3(); const direction new THREE.Vector3(); const moveForward false; const moveBackward false; document.addEventListener(keydown, (event) { switch (event.code) { case KeyW: moveForward true; break; case KeyS: moveBackward true; break; } }); function animate() { if (controls.isLocked) { direction.z Number(moveForward) - Number(moveBackward); direction.normalize(); velocity.z - direction.z * 0.1; velocity.z * 0.9; // Friction controls.moveForward(-velocity.z); } }示例用velocity乘以摩擦系数0.9实现平滑加减速——速度会指数衰减趋近目标手感比硬编码位移更自然。注意只有controls.isLocked为真时才允许移动避免指针未锁定时误操作。TrackballControls轨迹球import { TrackballControls } from three/addons/controls/TrackballControls.js; const controls new TrackballControls(camera, renderer.domElement); controls.rotateSpeed 2.0; controls.zoomSpeed 1.2; controls.panSpeed 0.8; controls.staticMoving true; function animate() { controls.update(); }与 OrbitControls 的差别TrackballControls 的旋转不受极角限制可以任意翻滚包括越过天顶且没有固定的上方向staticMoving为true时禁用惯性。MapControls2D 地图风格import { MapControls } from three/addons/controls/MapControls.js; const controls new MapControls(camera, renderer.domElement); controls.enableDamping true; controls.dampingFactor 0.05; controls.screenSpacePanning false; controls.maxPolarAngle Math.PI / 2;MapControls 是 OrbitControls 的地图化变体相机保持近似俯视禁止翻转到底部maxPolarAngle Math.PI/2平移类似地图拖拽。screenSpacePanning为false时平移沿世界地平面进行。选型建议检视/编辑场景用 OrbitControls第一人称漫游用 PointerLockControls 或 FirstPersonControls自由镜头动画用 FlyControls俯视地图用 MapControls。在 OpenMontage 的threejs_world工作区中blockout阶段的自由视角检视即可用 OrbitControls 搭配 Layers 实现点选区域 → 查看地标的交互闭环。TransformControls平移/旋转/缩放操作器操作器Gizmo是 3D 编辑器的基础——选中对象后出现三轴手柄拖拽即编辑变换import { TransformControls } from three/addons/controls/TransformControls.js; const transformControls new TransformControls(camera, renderer.domElement); scene.add(transformControls); // Attach to object transformControls.attach(selectedMesh); // Switch modes transformControls.setMode(translate); // translate, rotate, scale // Change space transformControls.setSpace(local); // local, world // Size transformControls.setSize(1); // Events transformControls.addEventListener(dragging-changed, (event) { // Disable orbit controls while dragging orbitControls.enabled !event.value; }); transformControls.addEventListener(change, () { renderer.render(scene, camera); }); // Keyboard shortcuts window.addEventListener(keydown, (event) { switch (event.key) { case g: transformControls.setMode(translate); break; case r: transformControls.setMode(rotate); break; case s: transformControls.setMode(scale); break; case Escape: transformControls.detach(); break; } });实现要点TransformControls 必须scene.add()进场景才会显示手柄attach(object)绑定目标detach()解绑Escape 快捷键的语义关键事件是dragging-changed拖拽手柄期间必须临时禁用 OrbitControlsorbitControls.enabled !event.value否则两个控制器会互相打架用 Blender 式快捷键G / R / S切换模式、Escape退出贴近美术人员的操作直觉。DragControls直接拖拽对象DragControls 让对象可以被鼠标抓住沿任意平面移动import { DragControls } from three/addons/controls/DragControls.js; const draggableObjects [mesh1, mesh2, mesh3]; const dragControls new DragControls( draggableObjects, camera, renderer.domElement, ); dragControls.addEventListener(dragstart, (event) { orbitControls.enabled false; event.object.material.emissive.set(0xaaaaaa); }); dragControls.addEventListener(drag, (event) { // Constrain to ground plane event.object.position.y 0; }); dragControls.addEventListener(dragend, (event) { orbitControls.enabled true; event.object.material.emissive.set(0x000000); });三个事件的典型职责dragstart禁用轨道控制并给出视觉反馈发光高亮drag中执行约束逻辑示例将对象强制钉在地平面y 0可替换为射线-平面求交实现任意平面拖拽dragend恢复轨道控制并清除高亮。选择系统点击选中、框选与悬停点击选中Click to Select点击选中 点击空白取消是最基础的选择模式const raycaster new THREE.Raycaster(); const mouse new THREE.Vector2(); let selectedObject null; function onMouseDown(event) { mouse.x (event.clientX / window.innerWidth) * 2 - 1; mouse.y -(event.clientY / window.innerHeight) * 2 1; raycaster.setFromCamera(mouse, camera); const intersects raycaster.intersectObjects(selectableObjects); // Deselect previous if (selectedObject) { selectedObject.material.emissive.set(0x000000); } // Select new if (intersects.length 0) { selectedObject intersects[0].object; selectedObject.material.emissive.set(0x444444); } else { selectedObject null; } }用mousedown而非click可以更早响应也便于与 OrbitControls 的拖拽区分拖拽结束后不触发误选。选中态用material.emissive增亮表达注意命中对象需为Mesh有material或做空值防护。框选Box Selection用SelectionBoxSelectionHelper实现按住拖出一个矩形框松手选中框内所有对象import { SelectionBox } from three/addons/interactive/SelectionBox.js; import { SelectionHelper } from three/addons/interactive/SelectionHelper.js; const selectionBox new SelectionBox(camera, scene); const selectionHelper new SelectionHelper(renderer, selectBox); // CSS class document.addEventListener(pointerdown, (event) { selectionBox.startPoint.set( (event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window.innerHeight) * 2 1, 0.5, ); }); document.addEventListener(pointermove, (event) { if (selectionHelper.isDown) { selectionBox.endPoint.set( (event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window.innerHeight) * 2 1, 0.5, ); } }); document.addEventListener(pointerup, (event) { selectionBox.endPoint.set( (event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window.innerHeight) * 2 1, 0.5, ); const selected selectionBox.select(); console.log(Selected objects:, selected); });原理SelectionBox用相机视锥与屏幕矩形求交集SelectionHelper负责在 DOM 上绘制半透明选区CSS 类selectBox需自行提供样式。Z 分量固定 0.5NDC 深度中点代表屏幕矩形投影到视锥中间深度。悬停效果Hover Effects悬停反馈的关键是不丢失对象的原始颜色——用userData缓存初始色移出时恢复const raycaster new THREE.Raycaster(); const mouse new THREE.Vector2(); let hoveredObject null; function onMouseMove(event) { mouse.x (event.clientX / window.innerWidth) * 2 - 1; mouse.y -(event.clientY / window.innerHeight) * 2 1; raycaster.setFromCamera(mouse, camera); const intersects raycaster.intersectObjects(hoverableObjects); // Reset previous hover if (hoveredObject) { hoveredObject.material.color.set(hoveredObject.userData.originalColor); document.body.style.cursor default; } // Apply new hover if (intersects.length 0) { hoveredObject intersects[0].object; if (!hoveredObject.userData.originalColor) { hoveredObject.userData.originalColor hoveredObject.material.color.getHex(); } hoveredObject.material.color.set(0xff6600); document.body.style.cursor pointer; } else { hoveredObject null; } } window.addEventListener(mousemove, onMouseMove);结合高效投射一节的节流技巧悬停检测的性能问题即可解决。把originalColor存在userData上而非全局变量是与点击选中共用对象时的稳健做法。键盘输入状态表驱动用按下置位、抬起复位的状态表收集按键再在更新循环中统一消费可避免keydown重复触发导致的累加错误const keys {}; document.addEventListener(keydown, (event) { keys[event.code] true; }); document.addEventListener(keyup, (event) { keys[event.code] false; }); function update() { const speed 0.1; if (keys[KeyW]) player.position.z - speed; if (keys[KeyS]) player.position.z speed; if (keys[KeyA]) player.position.x - speed; if (keys[KeyD]) player.position.x speed; if (keys[Space]) player.position.y speed; if (keys[ShiftLeft]) player.position.y - speed; }推荐使用event.code物理键位如KeyW、Space、ShiftLeft而非event.key随键盘布局变化的字符这样在 QWERTY/中文输入法等布局下行为一致。该模式同样适用于上文中 PointerLockControls 的 WASD 漫游。世界坐标与屏幕坐标互转世界 → 屏幕把 HTML 元素钉在 3D 对象上用Vector3.project(camera)把世界坐标投影到 NDC再换算成像素坐标function worldToScreen(position, camera) { const vector position.clone(); vector.project(camera); return { x: ((vector.x 1) / 2) * window.innerWidth, y: (-(vector.y - 1) / 2) * window.innerHeight, }; } // Position HTML element over 3D object const screenPos worldToScreen(mesh.position, camera); element.style.left screenPos.x px; element.style.top screenPos.y px;典型用途把 DOM 标签名称牌、工具提示、状态条钉在 3D 物体上。注意必须在每次相机移动后重新计算且物体在相机后方时vector.z 1应隐藏标签。屏幕 → 世界在指定深度反投影function screenToWorld(screenX, screenY, camera, targetZ 0) { const vector new THREE.Vector3( (screenX / window.innerWidth) * 2 - 1, -(screenY / window.innerHeight) * 2 1, 0.5, ); vector.unproject(camera); const dir vector.sub(camera.position).normalize(); const distance (targetZ - camera.position.z) / dir.z; return camera.position.clone().add(dir.multiplyScalar(distance)); }先unproject得到一条射线方向再求解射线与水平面z targetZ的交点。射线-平面相交通用落地/放置算法相比固定深度反投影射线-平面相交更通用——点击屏幕即可把对象精确放到任意平面上function getRayPlaneIntersection(mouse, camera, plane) { const raycaster new THREE.Raycaster(); raycaster.setFromCamera(mouse, camera); const intersection new THREE.Vector3(); raycaster.ray.intersectPlane(plane, intersection); return intersection; } // Ground plane const groundPlane new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); const worldPos getRayPlaneIntersection(mouse, camera, groundPlane);THREE.Plane(normal, constant)中法向量(0, 1, 0)、常量0即 y0 的地平面。这也是 DragControls 中把对象约束在指定平面上拖动的底层实现。事件处理最佳实践封装一个 InteractionManager把拾取逻辑收敛为一个可复用的管理器是生产代码的正确姿势——它统一了事件绑定、坐标换算、命中分发与资源释放class InteractionManager { constructor(camera, renderer, scene) { this.camera camera; this.renderer renderer; this.scene scene; this.raycaster new THREE.Raycaster(); this.mouse new THREE.Vector2(); this.clickables []; this.bindEvents(); } bindEvents() { const canvas this.renderer.domElement; canvas.addEventListener(click, (e) this.onClick(e)); canvas.addEventListener(mousemove, (e) this.onMouseMove(e)); canvas.addEventListener(touchstart, (e) this.onTouchStart(e)); } updateMouse(event) { const rect this.renderer.domElement.getBoundingClientRect(); this.mouse.x ((event.clientX - rect.left) / rect.width) * 2 - 1; this.mouse.y -((event.clientY - rect.top) / rect.height) * 2 1; } getIntersects() { this.raycaster.setFromCamera(this.mouse, this.camera); return this.raycaster.intersectObjects(this.clickables, true); } onClick(event) { this.updateMouse(event); const intersects this.getIntersects(); if (intersects.length 0) { const object intersects[0].object; if (object.userData.onClick) { object.userData.onClick(intersects[0]); } } } addClickable(object, callback) { this.clickables.push(object); object.userData.onClick callback; } dispose() { // Remove event listeners } } // Usage const interaction new InteractionManager(camera, renderer, scene); interaction.addClickable(mesh, (intersect) { console.log(Clicked at:, intersect.point); });设计亮点声明式 APIaddClickable(object, callback)把可点对象注册与回调绑定合二为一回调存于userData.onClick业务代码无需关心事件流单一坐标来源所有换算都走updateMouse基于 canvas 边界矩形鼠标、触摸天然统一递归求交intersectObjects(this.clickables, true)对组/嵌套模型同样生效可释放性预留dispose()用于移除监听防止单页应用中多次实例化造成监听器泄漏生产代码应在此处removeEventListener并解绑各控制器事件。性能优化建议限制射线投射次数对mousemove处理器做节流前文 50ms 方案把悬停检测压到 20fps 上限使用 Layers 过滤把射线目标与非目标分到不同层Raycaster 只对目标层求交使用简化碰撞网格对精细模型用不可见的低面数包围盒代替原几何体参与射线检测按需禁用控制器controls.enabled false在不需交互如播放过场动画时彻底关闭控制与相关监听批量更新把多个交互检查悬停、选中、拖拽合并到同一帧统一执行避免重复setFromCamera。简化碰撞网格的经典写法// Use simpler geometry for raycasting const complexMesh loadedModel; const collisionMesh new THREE.Mesh( new THREE.BoxGeometry(1, 1, 1), new THREE.MeshBasicMaterial({ visible: false }), ); collisionMesh.userData.target complexMesh; clickables.push(collisionMesh);命中collisionMesh后通过collisionMesh.userData.target拿到真正的复杂模型。不可见材质visible: false让代理网格只参与求交不参与渲染。与 OpenMontage Three.js 工具链的结合本技能在 OpenMontage 中与 3D 世界生成链路直接衔接。仓库中的threejs_world工具tools/graphics/threejs_world.py会物化出一个可编辑的 Three.js 工作区index.htmlworld.cssworld-runtime.js其运行时模板位于 tools/graphics/templates/threejs_world/world-runtime.js。交互技术在该链路中的落点包括InstancedMesh 与instanceId运行时用THREE.InstancedMesh批量散射岩石、晶体与树木world-runtime.js当一个实例可能承载数千棵树的绘制时intersects[0].instanceId正是区分你点到了哪一棵树的唯一凭据场景图与拾取目标运行时把地形、环境、地标分别归入terrainGroup/environmentGroup/landmarkGroupworld-runtime.js对地标组做intersectObjects(landmarkGroup.children, true)即可实现点击地标弹出区域信息的交互且天然避开了地形和水面相机路径与检视模式工作区支持cinematic/semantic/wireframe三种渲染模式与blockout/production两档质量等级tools/graphics/threejs_world.pyblockout阶段配合 OrbitControls 自由检视semantic模式则适合做区域拾取的可视化调试输入契约工具的完整输入参数render_mode、quality_tier、duration_seconds、width/height等由 schemas/tools/threejs_world.schema.json 声明交互层若需要把运行时视角序列化回世界规范可参考其中camera_path的结构tests/tools/test_threejs_world.py 中给出了完整的世界规格示例运行时钩子工作区加载完成后会暴露window.__worldGraph { scene, camera, terrainGroup, environmentGroup, landmarkGroup, instanceStats }world-runtime.js外部脚本例如基于本技能实现的交互管理器可以直接挂载射线拾取与 TransformControls为世界工作区加入点选 → 编辑 → 回写能力。技能体系中本threejs-interaction与threejs-fundamentals相机与场景搭建、threejs-animation交互动画、threejs-shaders视觉反馈效果等主题互为补充参见 skills/INDEX.md 的 3D 技能清单可配合 skills/creative/3d-world-generation.md 一起构成完整的 3D 世界制作与交互闭环。小结一套生产可用的 Three.js 交互系统由四层构成射线拾取Raycaster NDC 坐标换算 Layers/节流优化决定点中了什么相机控制Orbit/Fly/FirstPerson/PointerLock/Trackball/Map决定怎么观察操作器TransformControls/DragControls决定怎么编辑坐标互转与事件封装worldToScreen、射线-平面求交、InteractionManager把交互结果反馈给业务。在此基础上通过userData挂载回调、dragging-changed协调多控制器、dispose()释放资源即可稳定支撑编辑器、漫游器、展台预览等真实场景——这也正是 OpenMontage 可编辑 3D 世界工作区所需的交互底座。【免费下载链接】OpenMontageWorlds first open-source, agentic video production system. 12 production pipelines, 100 tools, 700 agent skill and production-knowledge files. Turn your AI coding assistant into a full video production studio.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMontage创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价