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

前言

在空间计算和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交互应用的无限可能,在虚拟与现实交融的未来中,创造出更加精彩的应用体验。

相关推荐
掘金安东尼2 小时前
前端周刊434期(2025年9月29日–10月5日)
前端·javascript·面试
掘金安东尼3 小时前
前端周刊433期(2025年9月22日–9月28日)
前端·javascript·github
井柏然3 小时前
为什么打 npm 包时要将 Vue/React 进行 external 处理?
javascript·vite·前端工程化
江城开朗的豌豆3 小时前
uni-app弹层遮罩难题?看我如何见招拆招!
前端·javascript·微信小程序
江城开朗的豌豆3 小时前
小程序生命周期漫游指南:从诞生到落幕的完整旅程
前端·javascript·微信小程序
江城开朗的豌豆3 小时前
跨平台开发实战:我的小程序双端(iOS、安卓)开发指南
前端·javascript·微信小程序
艾小码4 小时前
前端路由的秘密:手写一个迷你路由,看懂Hash和History的较量
前端·javascript
千码君201610 小时前
React Native:快速熟悉react 语法和企业级开发
javascript·react native·react.js·vite·hook
-dzk-11 小时前
【3DGS复现】Autodl服务器复现3DGS《简单快速》《一次成功》《新手练习复现必备》
运维·服务器·python·计算机视觉·3d·三维重建·三维