当前位置: 首页 > news >正文

从零开始XR开发:Three.js实现交互式3D积木搭建器

前言

在空间计算和XR技术快速发展的今天,越来越多的开发者开始探索3D交互应用的开发。本文将带你从零开始,逐步实现一个功能完整的3D积木搭建器。这个项目不仅能帮助你理解Three.js的基础应用,还能深入学习物理引擎集成、拖拽交互系统以及复杂3D场景的构建。

项目源码在文末,欢迎大家下载和分享 ~~~

技术栈概览

在开始实践之前,让我们先了解本项目所使用的核心技术栈。选择合适的技术栈是项目成功的关键,每个技术都在系统中扮演着重要角色。

技术名称版本作用描述选择理由
Three.js0.170.03D渲染引擎功能强大且文档完善,社区活跃度高,是Web 3D开发的首选方案
Cannon-es0.20.0物理引擎轻量级JavaScript物理引擎,与Three.js配合良好,支持刚体碰撞和重力模拟
TypeScript5.0+开发语言提供类型安全,降低运行时错误,提升代码可维护性
Vite5.0+构建工具快速的开发服务器和热更新,显著提升开发体验

这套技术栈的组合经过了大量实践验证,能够高效地构建复杂的3D交互应用。Three.js负责渲染,Cannon-es处理物理模拟,TypeScript保证代码质量,Vite则提供流畅的开发体验。

第一步:搭建基础3D场景

万丈高楼平地起,任何复杂的3D应用都始于一个基础场景。在这个阶段,我们需要初始化Three.js的核心组件,包括场景、相机和渲染器。这三者构成了Three.js应用的基石,缺一不可。

场景的搭建过程需要特别注意光照系统的配置。良好的光照不仅能提升视觉效果,还能帮助用户更好地感知3D空间中的深度关系。我们使用环境光提供基础照明,配合方向光产生阴影效果,让积木在地面上投射出真实的影子,增强空间感。

export class PhysicsWorld {public world: CANNON.World;private bodies: Map < THREE.Mesh,CANNON.Body > =new Map();constructor() {this.world = new CANNON.World({gravity: new CANNON.Vec3(0, -9.82, 0)});this.world.broadphase = new CANNON.NaiveBroadphase();this.world.solver.iterations = 10;const groundShape = new CANNON.Plane();const groundBody = new CANNON.Body({mass: 0,shape: groundShape});groundBody.quaternion.setFromEuler( - Math.PI / 2, 0, 0);this.world.addBody(groundBody);}addBox(mesh: THREE.Mesh, mass: number = 1) : CANNON.Body {const geometry = mesh.geometry as THREE.BoxGeometry;const parameters = geometry.parameters;const shape = new CANNON.Box(new CANNON.Vec3(parameters.width / 2, parameters.height / 2, parameters.depth / 2));const body = new CANNON.Body({mass: mass,shape: shape,position: new CANNON.Vec3(mesh.position.x, mesh.position.y, mesh.position.z)});this.world.addBody(body);this.bodies.set(mesh, body);return body;}
}

轨道控制器的加入让用户可以自由地观察场景。通过鼠标拖动旋转视角,滚轮缩放视图,这些交互方式已经成为3D应用的标准配置。控制器的阻尼效果能让相机移动更加平滑自然,而不是生硬的即时响应。

第二步:集成物理引擎

有了基础场景后,我们需要让积木具备真实的物理特性。这就需要集成Cannon-es物理引擎。物理引擎的作用是模拟真实世界的物理规律,包括重力、碰撞、摩擦等效果。

物理世界的创建是一个精细的过程。我们需要设置合适的重力加速度,配置碰撞检测算法,并创建地面这个静态物体。物理世界中的对象分为动态和静态两类,动态对象会受到重力和碰撞影响,而静态对象则保持不动,仅参与碰撞检测。

export class PhysicsWorld {public world: CANNON.World;private bodies: Map < THREE.Mesh,CANNON.Body > =new Map();constructor() {this.world = new CANNON.World({gravity: new CANNON.Vec3(0, -9.82, 0)});this.world.broadphase = new CANNON.NaiveBroadphase();this.world.solver.iterations = 10;const groundShape = new CANNON.Plane();const groundBody = new CANNON.Body({mass: 0,shape: groundShape});groundBody.quaternion.setFromEuler( - Math.PI / 2, 0, 0);this.world.addBody(groundBody);}addBox(mesh: THREE.Mesh, mass: number = 1) : CANNON.Body {const geometry = mesh.geometry as THREE.BoxGeometry;const parameters = geometry.parameters;const shape = new CANNON.Box(new CANNON.Vec3(parameters.width / 2, parameters.height / 2, parameters.depth / 2));const body = new CANNON.Body({mass: mass,shape: shape,position: new CANNON.Vec3(mesh.position.x, mesh.position.y, mesh.position.z)});this.world.addBody(body);this.bodies.set(mesh, body);return body;}
}

物理引擎与渲染引擎的同步是关键环节。每一帧渲染前,我们都需要更新物理世界的状态,然后将物理体的位置和旋转同步到对应的Three.js网格对象上。这个过程看似简单,但必须精确执行,否则会出现视觉与物理不匹配的问题。

第三步:实现积木系统

现在我们可以创建各种类型的积木了。积木系统的设计采用了预设模式,每种积木都有预定义的尺寸、颜色和类型。这样的设计既保证了积木的一致性,又提供了足够的灵活性。

积木的创建过程包含了几何体构建、材质设置、物理体添加三个主要步骤。几何体定义了积木的形状,材质决定了它的外观,而物理体则赋予了它碰撞和重力特性。这三者必须精确对应,才能保证积木在视觉和物理上的表现一致。

export class BlockSystem {public static readonly BLOCK_PRESETS: BlockDefinition[] = [{type: BlockType.CUBE,size: new THREE.Vector3(1, 1, 1),color: 0xff6b6b,name: '标准立方体'},{type: BlockType.LONG_BLOCK,size: new THREE.Vector3(2, 0.5, 0.5),color: 0x4ecdc4,name: '长条积木'},{type: BlockType.FLAT_BLOCK,size: new THREE.Vector3(2, 0.25, 1),color: 0xf9ca24,name: '平板积木'},{type: BlockType.CYLINDER,size: new THREE.Vector3(0.5, 1, 0.5),color: 0x45b7d1,name: '圆柱积木'},{type: BlockType.SPHERE,size: new THREE.Vector3(0.5, 0.5, 0.5),color: 0xff9ff3,name: '球体积木'}];createBlock(definition: BlockDefinition, position: THREE.Vector3) : THREE.Mesh {let geometry: THREE.BufferGeometry;switch (definition.type) {case BlockType.CUBE:geometry = new THREE.BoxGeometry(definition.size.x, definition.size.y, definition.size.z);break;case BlockType.CYLINDER:geometry = new THREE.CylinderGeometry(definition.size.x, definition.size.x, definition.size.y, 32);break;case BlockType.SPHERE:geometry = new THREE.SphereGeometry(definition.size.x, 32, 32);break;default:geometry = new THREE.BoxGeometry(1, 1, 1);}const material = new THREE.MeshStandardMaterial({color: definition.color,roughness: 0.7,metalness: 0.3});const mesh = new THREE.Mesh(geometry, material);mesh.position.copy(position);mesh.castShadow = true;mesh.receiveShadow = true;this.scene.add(mesh);this.physics.addBox(mesh, 1);return mesh;}
}

为了提升用户体验,我们为每种积木设计了独特的视觉样式。立方体使用红色,长条积木是青色,平板积木为黄色。这种色彩编码帮助用户快速识别不同类型的积木,在搭建复杂结构时尤其有用。

第四步:开发拖拽交互系统

拖拽功能是积木搭建器的核心交互方式。用户需要能够抓取积木、移动到目标位置、然后释放。这个看似简单的交互背后,涉及射线检测、物理状态管理、拖拽平面计算等多个技术点。

射线检测是实现拖拽的基础。当用户点击屏幕时,我们从相机位置发射一条射线,检测它与场景中物体的交点。一旦检测到可拖拽物体,就进入拖拽状态。此时需要冻结物体的物理状态,否则重力会导致物体下落,影响拖拽体验。

export class DragControls {private onMouseDown(event: MouseEvent) : void {this.updateMousePosition(event);this.raycaster.setFromCamera(this.mouse, this.camera);const intersects = this.raycaster.intersectObjects(this.draggableObjects);if (intersects.length > 0) {const object = intersects[0].object as THREE.Mesh;this.selectedObject = object;this.isDragging = true;this.physics.freezeBody(object);const normal = new THREE.Vector3(0, 1, 0);this.dragPlane.setFromNormalAndCoplanarPoint(normal, intersects[0].point);this.offset.copy(intersects[0].point).sub(object.position);this.highlightObject(object, true);}}private onMouseMove(event: MouseEvent) : void {if (this.isDragging && this.selectedObject) {this.raycaster.setFromCamera(this.mouse, this.camera);const intersection = new THREE.Vector3();this.raycaster.ray.intersectPlane(this.dragPlane, intersection);if (intersection) {this.selectedObject.position.copy(intersection.sub(this.offset));const body = this.physics.getBody(this.selectedObject);if (body) {body.position.copy(this.selectedObject.position as any);}}}}
}

拖拽平面的概念很重要。为了让积木在3D空间中平滑移动,我们创建了一个虚拟平面,积木在这个平面上移动。平面的法向量始终朝上,确保积木保持水平移动,这样的设计符合用户的直觉预期。

第五步:构建自动化建筑系统

有了基础的积木系统后,我们可以实现更高级的功能:自动搭建建筑物。这个功能通过预定义的建筑结构,自动批量创建和放置积木,极大地提升了搭建效率。

房屋搭建器采用了模块化设计思路。地基、墙壁、屋顶、装饰等每个部分都是独立的模块,通过精确的坐标计算组合在一起。这种设计不仅让代码结构清晰,也便于后续扩展更多建筑类型。

export class HouseBuilder {buildSmallHouse(centerX: number = 0, centerZ: number = 0) : THREE.Mesh[] {const blocks: THREE.Mesh[] = [];const baseY = 0.125;// 搭建地基const foundation = [{x: centerX - 1,y: baseY,z: centerZ - 1},{x: centerX,y: baseY,z: centerZ - 1},{x: centerX + 1,y: baseY,z: centerZ - 1},// ... 更多地基位置];foundation.forEach(pos = >{const block = this.blockSystem.createBlock(BlockSystem.BLOCK_PRESETS.find(p = >p.type === BlockType.FLAT_BLOCK) ! , new THREE.Vector3(pos.x, pos.y, pos.z));this.dragControls.addDraggable(block);this.physics.freezeBody(block);blocks.push(block);});// 搭建墙壁const wallHeight = baseY + 0.75;const walls = [{x: centerX - 1,y: wallHeight,z: centerZ - 1},{x: centerX + 1,y: wallHeight,z: centerZ - 1},// ... 更多墙壁位置];walls.forEach(pos = >{const block = this.blockSystem.createBlock(BlockSystem.BLOCK_PRESETS.find(p = >p.type === BlockType.CUBE) ! , new THREE.Vector3(pos.x, pos.y, pos.z));this.dragControls.addDraggable(block);this.physics.freezeBody(block);blocks.push(block);});return blocks;}
}

建筑物的物理状态管理需要特别处理。我们使用freezeBody方法将建筑物的所有积木设置为静态,这样它们就不会受到重力影响而坍塌。但同时保留了拖拽功能,用户依然可以调整建筑物的位置或单个积木的摆放。

一个完整的小房子包含了地基、墙壁、屋顶、装饰和烟囱等多个部分。地基使用平板积木铺设,墙壁用立方体堆叠,屋顶再次使用平板覆盖,烟囱则用圆柱积木表现。这种分层搭建的思路,既符合真实建筑的构造方式,也让代码逻辑清晰易懂。

第六步:打造用户界面

完善的用户界面能显著提升应用的易用性。我们设计了一个侧边栏控制面板,提供积木选择、建筑搭建、场景控制等功能。界面采用半透明黑色背景,既保持美观又不遮挡3D场景。

界面布局经过精心设计。积木选择区域使用网格布局,每个按钮都标注了快捷键,方便用户快速操作。建筑搭建区提供一键搭建功能,用户只需点击按钮就能看到完整的建筑出现。场景控制区则包含清除、重置、重力开关等实用功能。

// 暴露给UI的全局函数
(window as any).buildHouse = () => {
houseBuilder.buildSmallHouse(0, 0);
updateStats();
}; (window as any).buildHouseWithGarden = () = >{houseBuilder.buildHouseWithGarden(0, 0);updateStats();
}; (window as any).toggleGravity = () = >{const gravity = physics.world.gravity;if (gravity.y === 0) {physics.world.gravity.set(0, -9.82, 0);} else {physics.world.gravity.set(0, 0, 0);}updateStats();
};
function updateStats() {const blockCountEl = document.getElementById('blockCount');const gravityStatusEl = document.getElementById('gravityStatus');if (blockCountEl) {blockCountEl.textContent = blockSystem.getBlockCount().toString();}if (gravityStatusEl) {const gravity = physics.world.gravity;gravityStatusEl.textContent = gravity.y === 0 ? '关闭': '开启';}
}

实时统计功能让用户随时了解场景状态。统计面板显示当前积木数量和重力开关状态,这些信息会在用户操作后自动更新。通过这些反馈,用户能更好地掌控整个搭建过程。

技术亮点与优化

整个项目的实现过程中,有几个值得特别说明的技术亮点。首先是物理引擎与渲染引擎的同步机制。我们在每一帧动画循环中,先更新物理世界状态,再将物理体的变换同步到Three.js对象,最后执行渲染。这个顺序保证了物理模拟的准确性和视觉的流畅性。

性能优化方面,我们采用了多项措施。阴影贴图分辨率设置为2048×2048,在质量和性能之间取得平衡。物理引擎的迭代次数设为10,既保证了碰撞检测的精度,又不会造成过大的计算负担。拖拽时冻结物理体的策略,避免了不必要的物理计算,提升了交互响应速度。

优化项目优化方案效果说明
渲染性能PCF软阴影 + 合理贴图分辨率在保证视觉效果前提下降低GPU负担
物理计算拖拽时冻结物理体减少不必要的碰撞检测和物理模拟
内存管理及时释放不用的几何体和材质避免内存泄漏,保持应用长时间稳定运行
交互响应事件节流和防抖减少事件处理频率,提升交互流畅度

代码架构的设计遵循了单一职责原则。物理世界管理、积木系统、拖拽控制、建筑搭建等功能都封装在独立的类中,各司其职。这种模块化设计不仅让代码易于理解和维护,也为后续功能扩展提供了良好的基础。

源码地址

GithHub: https://github.com/Damon-Liu-code/3D-building-block-simulator
InsCode: https://inscode.csdn.net/@weixin_41793160/3D-building-block-simulator
在线运行: https://3d-simulator.inscode.cc/

总结与展望

通过这个项目的实践,我们从零开始构建了一个功能完整的3D积木搭建器。从基础的3D场景搭建,到物理引擎集成,再到复杂的拖拽交互和自动化建筑系统,每一步都凝聚了对3D应用开发的深入理解。

对于有志于XR应用开发的开发者,这个项目提供了一个很好的学习起点。掌握了这些基础技术后,可以进一步探索更高级的主题,如WebXR API的使用、AR内容的开发、或是在Rokid等专业XR平台上构建应用。空间计算的时代已经到来,现在正是投身这个领域的最佳时机。让我们一起探索3D交互应用的无限可能,在虚拟与现实交融的未来中,创造出更加精彩的应用体验。

http://www.dtcms.com/a/452948.html

相关文章:

  • 如何解决网站只收录首页的一些办法wordpress多站点内容聚合
  • 个人备忘录的设计与实现
  • 删除cad无关线条 的ppo 随手记
  • Python AI编程在微创手术通过数据分析改善恢复的路径分析(下)
  • 深度学习之神经网络1(Neural Network)
  • pycharm下创建flask项目,配置端口问题
  • 计算机科学中的核心思想与理论
  • SpringCloud,vue3应用使用AlibabaCloudToolkit自动化部署到远程服务器上的docker
  • 如何从RSSI和SNR 判断现场的LoRaWAN的信号质量?
  • 【万字解读】品牌SEO实战指南:7步打造AI时代的搜索权威
  • 网站短期就业培训班开发公司总经理管理方案
  • GitHub 热榜项目 - 日榜(2025-10-07)
  • TDengine 比较函数 NULLIF 用户手册
  • SSM面试题学习
  • 网站建设练手项目我是做装修什么网站可以
  • Effective Python 第41条:考虑用mix-in类来表示可组合的功能
  • STM32独立看门狗IWDG与窗口看门狗WWDG知识梳理笔记
  • HTML-CSS-JS-入门学习笔记
  • 基于 MacOS 的Rokid 开发本地环境搭建指南
  • 以前的计算集群:Beowulf集群(贝奥武夫集群)
  • 软件开发中前端页面、后台管理页面、后端、数据中台的关系与开发流程
  • 政务微网站建设方案wordpress在线版本
  • TypeScript 循环
  • 【征文计划】JSAR实战:从零开始的空间小程序开发之旅
  • 用A4打印机1:1打印A3试卷(A3 pdf切割)
  • 知识体系_大数据框架环境搭建_虚拟机环境准备
  • 个人网站设计的参考文献网站建设制作解决方案
  • 《什么是Redis?》
  • soular入门到实战(4) - 如何通过工作台聚合TikLab所有工具链
  • 解决GitHub大文件推送错误:彻底清理PDB文件并配置.gitignore