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

项目源码在文末,欢迎大家下载和分享 ~~~
技术栈概览
在开始实践之前,让我们先了解本项目所使用的核心技术栈。选择合适的技术栈是项目成功的关键,每个技术都在系统中扮演着重要角色。
|------------|---------|--------|---------------------------------------------|
| 技术名称 | 版本 | 作用描述 | 选择理由 |
| Three.js | 0.170.0 | 3D渲染引擎 | 功能强大且文档完善,社区活跃度高,是Web 3D开发的首选方案 |
| Cannon-es | 0.20.0 | 物理引擎 | 轻量级JavaScript物理引擎,与Three.js配合良好,支持刚体碰撞和重力模拟 |
| TypeScript | 5.0+ | 开发语言 | 提供类型安全,降低运行时错误,提升代码可维护性 |
| Vite | 5.0+ | 构建工具 | 快速的开发服务器和热更新,显著提升开发体验 |
这套技术栈的组合经过了大量实践验证,能够高效地构建复杂的3D交互应用。Three.js负责渲染,Cannon-es处理物理模拟,TypeScript保证代码质量,Vite则提供流畅的开发体验。
第一步:搭建基础3D场景
万丈高楼平地起,任何复杂的3D应用都始于一个基础场景。在这个阶段,我们需要初始化Three.js的核心组件,包括场景、相机和渲染器。这三者构成了Three.js应用的基石,缺一不可。
场景的搭建过程需要特别注意光照系统的配置。良好的光照不仅能提升视觉效果,还能帮助用户更好地感知3D空间中的深度关系。我们使用环境光提供基础照明,配合方向光产生阴影效果,让积木在地面上投射出真实的影子,增强空间感。
TypeScript
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物理引擎。物理引擎的作用是模拟真实世界的物理规律,包括重力、碰撞、摩擦等效果。
物理世界的创建是一个精细的过程。我们需要设置合适的重力加速度,配置碰撞检测算法,并创建地面这个静态物体。物理世界中的对象分为动态和静态两类,动态对象会受到重力和碰撞影响,而静态对象则保持不动,仅参与碰撞检测。
TypeScript
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网格对象上。这个过程看似简单,但必须精确执行,否则会出现视觉与物理不匹配的问题。

第三步:实现积木系统
现在我们可以创建各种类型的积木了。积木系统的设计采用了预设模式,每种积木都有预定义的尺寸、颜色和类型。这样的设计既保证了积木的一致性,又提供了足够的灵活性。
积木的创建过程包含了几何体构建、材质设置、物理体添加三个主要步骤。几何体定义了积木的形状,材质决定了它的外观,而物理体则赋予了它碰撞和重力特性。这三者必须精确对应,才能保证积木在视觉和物理上的表现一致。
TypeScript
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;
}
}

为了提升用户体验,我们为每种积木设计了独特的视觉样式。立方体使用红色,长条积木是青色,平板积木为黄色。这种色彩编码帮助用户快速识别不同类型的积木,在搭建复杂结构时尤其有用。
第四步:开发拖拽交互系统
拖拽功能是积木搭建器的核心交互方式。用户需要能够抓取积木、移动到目标位置、然后释放。这个看似简单的交互背后,涉及射线检测、物理状态管理、拖拽平面计算等多个技术点。

射线检测是实现拖拽的基础。当用户点击屏幕时,我们从相机位置发射一条射线,检测它与场景中物体的交点。一旦检测到可拖拽物体,就进入拖拽状态。此时需要冻结物体的物理状态,否则重力会导致物体下落,影响拖拽体验。
TypeScript
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空间中平滑移动,我们创建了一个虚拟平面,积木在这个平面上移动。平面的法向量始终朝上,确保积木保持水平移动,这样的设计符合用户的直觉预期。

第五步:构建自动化建筑系统
有了基础的积木系统后,我们可以实现更高级的功能:自动搭建建筑物。这个功能通过预定义的建筑结构,自动批量创建和放置积木,极大地提升了搭建效率。
房屋搭建器采用了模块化设计思路。地基、墙壁、屋顶、装饰等每个部分都是独立的模块,通过精确的坐标计算组合在一起。这种设计不仅让代码结构清晰,也便于后续扩展更多建筑类型。
TypeScript
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场景。
界面布局经过精心设计。积木选择区域使用网格布局,每个按钮都标注了快捷键,方便用户快速操作。建筑搭建区提供一键搭建功能,用户只需点击按钮就能看到完整的建筑出现。场景控制区则包含清除、重置、重力开关等实用功能。
TypeScript
// 暴露给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交互应用的无限可能,在虚拟与现实交融的未来中,创造出更加精彩的应用体验。