Three.js与Cannon.js实现魔方茶艺3D交互应用开发指南

📅 2026/7/16 2:18:22
Three.js与Cannon.js实现魔方茶艺3D交互应用开发指南
1. 魔方与茶文化的技术融合从物理模拟到交互体验在技术开发领域将传统益智玩具与现代交互设计相结合往往能创造出令人耳目一新的用户体验。魔方作为经典的立体拼图玩具其机械结构和旋转逻辑本身就蕴含着丰富的算法思想而茶道文化中的拧、泡等动作则为我们提供了自然直观的交互隐喻。本文将带领大家实现一个融合魔方旋转与茶艺动作的交互式应用涵盖3D建模、物理引擎、手势识别等关键技术点。这个项目特别适合有一定前端基础、希望进阶学习WebGL和交互设计的开发者。通过学习你将掌握Three.js的3D渲染技术、Cannon.js物理引擎的集成方法以及如何将传统文化元素转化为数字交互体验。无论是用于教育展示、娱乐应用还是文化传播这套技术方案都具有很高的实用价值。2. 技术选型与环境准备2.1 核心技术栈说明本项目采用现代前端技术栈主要基于以下框架和库Three.js负责3D场景的渲染和动画控制提供强大的WebGL封装Cannon.js物理引擎模拟魔方的重力、碰撞和旋转物理效果Hammer.js手势识别库处理触摸屏上的旋转、拧动等操作GSAP动画库用于创建流畅的过渡效果和交互反馈2.2 开发环境配置首先确保你的开发环境满足以下要求# 创建项目目录结构 mkdir magic-cube-tea-app cd magic-cube-tea-app # 初始化npm项目 npm init -y # 安装核心依赖 npm install three cannon-es hammerjs gsap npm install --save-dev webpack webpack-cli webpack-dev-server项目基础目录结构如下src/ ├── js/ │ ├── core/ # 核心模块 │ │ ├── SceneManager.js │ │ ├── CubeManager.js │ │ └── PhysicsManager.js │ ├── utils/ # 工具函数 │ │ ├── GestureHandler.js │ │ └── AnimationHelper.js │ └── main.js # 入口文件 ├── css/ │ └── style.css # 样式文件 ├── assets/ # 资源文件 │ ├── textures/ # 纹理贴图 │ └── models/ # 3D模型 └── index.html # 主页面2.3 基础HTML结构搭建创建主页面文件设置基本的3D渲染容器!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title魔方茶艺交互体验/title link relstylesheet hrefcss/style.css /head body div idcontainer div idcanvas-container/div div idui-controls button idshake-btn摇一摇魔方/button button idtwist-btn拧一拧茶壶/button div idgesture-hint手势操作提示区域/div /div /div script srcjs/main.js/script /body /html3. 3D场景搭建与魔方建模3.1 Three.js场景初始化创建场景管理器负责3D环境的初始化和渲染循环// js/core/SceneManager.js import * as THREE from three; export class SceneManager { constructor(containerId) { this.container document.getElementById(containerId); this.scene new THREE.Scene(); this.camera new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); this.renderer new THREE.WebGLRenderer({ antialias: true }); this.init(); } init() { // 设置渲染器参数 this.renderer.setSize(window.innerWidth, window.innerHeight); this.renderer.setClearColor(0xf0f0f0); this.renderer.shadowMap.enabled true; this.renderer.shadowMap.type THREE.PCFSoftShadowMap; // 将画布添加到容器 this.container.appendChild(this.renderer.domElement); // 设置相机位置 this.camera.position.set(0, 5, 10); this.camera.lookAt(0, 0, 0); // 添加基础灯光 this.setupLighting(); // 开始渲染循环 this.animate(); } setupLighting() { // 环境光 const ambientLight new THREE.AmbientLight(0xffffff, 0.6); this.scene.add(ambientLight); // 方向光 const directionalLight new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(10, 20, 15); directionalLight.castShadow true; directionalLight.shadow.mapSize.width 2048; directionalLight.shadow.mapSize.height 2048; this.scene.add(directionalLight); } animate() { requestAnimationFrame(() this.animate()); this.renderer.render(this.scene, this.camera); } // 响应窗口大小变化 onWindowResize() { this.camera.aspect window.innerWidth / window.innerHeight; this.camera.updateProjectionMatrix(); this.renderer.setSize(window.innerWidth, window.innerHeight); } }3.2 魔方模型创建实现魔方管理器负责生成可交互的3D魔方// js/core/CubeManager.js import * as THREE from three; export class CubeManager { constructor(scene) { this.scene scene; this.cubeGroup new THREE.Group(); this.cubies []; // 存储所有小方块 this.cubeSize 3; // 3x3x3魔方 this.createCube(); } createCube() { const cubeletSize 1; const gap 0.05; // 方块间间隙 // 定义六个面的颜色 const colors { front: 0xff0000, // 红 back: 0xff8800, // 橙 top: 0xffffff, // 白 bottom: 0xffff00, // 黄 left: 0x00ff00, // 绿 right: 0x0000ff // 蓝 }; // 创建3x3x3魔方 for (let x -1; x 1; x) { for (let y -1; y 1; y) { for (let z -1; z 1; z) { // 跳过中心块不可见 if (x 0 y 0 z 0) continue; const cubelet this.createCubelet(x, y, z, cubeletSize, gap, colors); this.cubies.push(cubelet); this.cubeGroup.add(cubelet); } } } this.scene.add(this.cubeGroup); } createCubelet(x, y, z, size, gap, colors) { const group new THREE.Group(); const geometry new THREE.BoxGeometry(size - gap, size - gap, size - gap); // 创建六个面但只显示外表面 const materials []; const positions [ { normal: new THREE.Vector3(0, 0, 1), color: colors.front }, // 前 { normal: new THREE.Vector3(0, 0, -1), color: colors.back }, // 后 { normal: new THREE.Vector3(0, 1, 0), color: colors.top }, // 上 { normal: new THREE.Vector3(0, -1, 0), color: colors.bottom }, // 下 { normal: new THREE.Vector3(-1, 0, 0), color: colors.left }, // 左 { normal: new THREE.Vector3(1, 0, 0), color: colors.right } // 右 ]; positions.forEach(pos { const material new THREE.MeshLambertMaterial({ color: this.shouldShowFace(x, y, z, pos.normal) ? pos.color : 0x444444 }); materials.push(material); }); const cube new THREE.Mesh(geometry, materials); cube.position.set(x * size, y * size, z * size); cube.userData { originalPosition: { x, y, z } }; group.add(cube); return group; } shouldShowFace(x, y, z, normal) { // 判断是否为外表面 return (x -1 normal.x -1) || (x 1 normal.x 1) || (y -1 normal.y -1) || (y 1 normal.y 1) || (z -1 normal.z -1) || (z 1 normal.z 1); } // 魔方旋转方法 rotateLayer(layer, axis, clockwise true) { const rotationAxis new THREE.Vector3(); switch (axis) { case x: rotationAxis.set(1, 0, 0); break; case y: rotationAxis.set(0, 1, 0); break; case z: rotationAxis.set(0, 0, 1); break; } const angle clockwise ? Math.PI / 2 : -Math.PI / 2; const affectedCubies this.getCubiesInLayer(layer, axis); affectedCubies.forEach(cubie { cubie.rotateOnWorldAxis(rotationAxis, angle); }); } getCubiesInLayer(layer, axis) { return this.cubies.filter(cubie { const pos cubie.children[0].userData.originalPosition; switch (axis) { case x: return pos.x layer; case y: return pos.y layer; case z: return pos.z layer; } }); } }4. 物理引擎集成与交互实现4.1 Cannon.js物理系统设置集成物理引擎实现真实的物理效果// js/core/PhysicsManager.js import * as CANNON from cannon-es; export class PhysicsManager { constructor() { this.world new CANNON.World(); this.world.gravity.set(0, -9.82, 0); this.world.broadphase new CANNON.NaiveBroadphase(); this.world.solver.iterations 10; this.bodies new Map(); // 存储物理体与3D对象的映射 this.setupGround(); } setupGround() { // 创建地面 const groundShape new CANNON.Plane(); const groundBody new CANNON.Body({ mass: 0 }); groundBody.addShape(groundShape); groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2); this.world.addBody(groundBody); } addPhysicsBody(mesh, mass 1) { const shape new CANNON.Box(new CANNON.Vec3( mesh.geometry.parameters.width / 2, mesh.geometry.parameters.height / 2, mesh.geometry.parameters.depth / 2 )); const body new CANNON.Body({ mass }); body.addShape(shape); body.position.copy(mesh.position); body.quaternion.copy(mesh.quaternion); this.world.addBody(body); this.bodies.set(mesh, body); return body; } update() { this.world.step(1/60); // 同步物理体与3D对象的位置 this.bodies.forEach((body, mesh) { mesh.position.copy(body.position); mesh.quaternion.copy(body.quaternion); }); } // 施加力模拟摇动效果 applyShakeForce(intensity 10) { this.bodies.forEach((body, mesh) { if (body.mass 0) { const force new CANNON.Vec3( (Math.random() - 0.5) * intensity, Math.random() * intensity, (Math.random() - 0.5) * intensity ); body.applyForce(force, body.position); } }); } }4.2 手势识别与交互控制实现触摸屏手势识别支持拧动和摇动操作// js/utils/GestureHandler.js import Hammer from hammerjs; export class GestureHandler { constructor(element, cubeManager, physicsManager) { this.cubeManager cubeManager; this.physicsManager physicsManager; this.isRotating false; this.setupGestures(element); } setupGestures(element) { const mc new Hammer.Manager(element); // 旋转手势 const rotate new Hammer.Rotate(); rotate.set({ enable: true }); mc.add(rotate); // 拖动手势 const pan new Hammer.Pan(); pan.set({ direction: Hammer.DIRECTION_ALL }); mc.add(pan); // 缩放手势 const pinch new Hammer.Pinch(); pinch.set({ enable: true }); mc.add(pinch); mc.on(rotatestart, (e) this.onRotateStart(e)); mc.on(rotate, (e) this.onRotate(e)); mc.on(rotateend, (e) this.onRotateEnd(e)); mc.on(panstart, (e) this.onPanStart(e)); mc.on(pan, (e) this.onPan(e)); mc.on(panend, (e) this.onPanEnd(e)); // 摇动手势检测通过加速度传感器 if (window.DeviceMotionEvent) { window.addEventListener(devicemotion, (e) this.handleShake(e)); } } onRotateStart(e) { this.isRotating true; this.startRotation e.rotation; } onRotate(e) { if (!this.isRotating) return; const deltaRotation e.rotation - this.startRotation; const rotationSpeed deltaRotation * 0.01; // 根据旋转方向拧动魔方层 if (Math.abs(deltaRotation) 30) { const layer Math.floor(Math.random() * 3) - 1; // -1, 0, 1 const axis [x, y, z][Math.floor(Math.random() * 3)]; const clockwise deltaRotation 0; this.cubeManager.rotateLayer(layer, axis, clockwise); this.startRotation e.rotation; } } onRotateEnd(e) { this.isRotating false; } handleShake(e) { const acceleration e.accelerationIncludingGravity; if (!acceleration) return; const shakeThreshold 15; const totalAcceleration Math.abs(acceleration.x) Math.abs(acceleration.y) Math.abs(acceleration.z); if (totalAcceleration shakeThreshold) { this.physicsManager.applyShakeForce(totalAcceleration / 2); } } // 茶壶拧动特效 twistTeapot(direction) { // 模拟茶壶拧动动画 const twistAnimation { duration: 0.5, rotation: direction clockwise ? Math.PI / 4 : -Math.PI / 4, onComplete: () this.resetTwist() }; // 这里可以添加茶壶模型的旋转动画 console.log(茶壶${direction clockwise ? 顺时针 : 逆时针}拧动); } resetTwist() { // 复位茶壶位置 console.log(茶壶复位); } }5. 茶艺元素集成与动画效果5.1 茶壶模型创建与动画创建茶壶3D模型并实现拧动动画// js/core/TeapotManager.js import * as THREE from three; import { GLTFLoader } from three/examples/jsm/loaders/GLTFLoader.js; export class TeapotManager { constructor(scene) { this.scene scene; this.teapot null; this.isTwisting false; this.loadTeapotModel(); } loadTeapotModel() { const loader new GLTFLoader(); // 可以使用在线模型或本地模型 loader.load(assets/models/teapot.glb, (gltf) { this.teapot gltf.scene; this.teapot.scale.set(0.5, 0.5, 0.5); this.teapot.position.set(3, 1, 0); // 设置茶壶材质和阴影 this.teapot.traverse((child) { if (child.isMesh) { child.castShadow true; child.receiveShadow true; } }); this.scene.add(this.teapot); }, undefined, (error) { console.error(茶壶模型加载失败:, error); this.createFallbackTeapot(); }); } createFallbackTeapot() { // 备用方案使用基本几何体创建茶壶 const teapotGroup new THREE.Group(); // 壶身 const bodyGeometry new THREE.CylinderGeometry(1, 0.8, 1.5, 32); const bodyMaterial new THREE.MeshPhongMaterial({ color: 0x8B4513 }); const body new THREE.Mesh(bodyGeometry, bodyMaterial); body.castShadow true; // 壶嘴 const spoutGeometry new THREE.ConeGeometry(0.1, 0.8, 8); const spout new THREE.Mesh(spoutGeometry, bodyMaterial); spout.position.set(0.7, 0.3, 0); spout.rotation.z -Math.PI / 6; // 壶把 const handleGeometry new THREE.TorusGeometry(0.4, 0.05, 8, 32); const handle new THREE.Mesh(handleGeometry, bodyMaterial); handle.position.set(-0.6, 0.5, 0); handle.rotation.z Math.PI / 2; teapotGroup.add(body); teapotGroup.add(spout); teapotGroup.add(handle); teapotGroup.position.set(3, 1, 0); this.teapot teapotGroup; this.scene.add(this.teapot); } // 茶壶拧动动画 twist(clockwise true) { if (this.isTwisting || !this.teapot) return; this.isTwisting true; const twistAngle clockwise ? Math.PI / 3 : -Math.PI / 3; const originalRotation this.teapot.rotation.y; // 使用GSAP创建平滑动画 const twistTween { rotation: originalRotation twistAngle, duration: 0.3, ease: power2.inOut, onComplete: () { setTimeout(() this.untwist(originalRotation), 200); } }; // 实际项目中这里使用GSAP this.animateTwist(twistTween); } animateTwist(tween) { const startTime Date.now(); const animate () { const elapsed Date.now() - startTime; const progress Math.min(elapsed / (tween.duration * 1000), 1); if (progress 1) { this.teapot.rotation.y this.easeInOut(progress) * tween.rotation; requestAnimationFrame(animate); } else { this.teapot.rotation.y tween.rotation; tween.onComplete tween.onComplete(); } }; animate(); } easeInOut(t) { return t 0.5 ? 2 * t * t : -1 (4 - 2 * t) * t; } untwist(originalRotation) { const untwistTween { rotation: originalRotation, duration: 0.2, ease: power2.out, onComplete: () { this.isTwisting false; } }; this.animateTwist(untwistTween); } }5.2 粒子效果与视觉反馈添加茶水和互动粒子效果// js/effects/ParticleSystem.js import * as THREE from three; export class ParticleSystem { constructor(scene) { this.scene scene; this.particles []; this.particleGroup new THREE.Group(); this.scene.add(this.particleGroup); } createTeaSteam(position) { const particleCount 20; const geometry new THREE.BufferGeometry(); const positions new Float32Array(particleCount * 3); const sizes new Float32Array(particleCount); for (let i 0; i particleCount; i) { positions[i * 3] position.x (Math.random() - 0.5) * 0.5; positions[i * 3 1] position.y Math.random() * 2; positions[i * 3 2] position.z (Math.random() - 0.5) * 0.5; sizes[i] Math.random() * 0.5 0.1; } geometry.setAttribute(position, new THREE.BufferAttribute(positions, 3)); geometry.setAttribute(size, new THREE.BufferAttribute(sizes, 1)); const material new THREE.PointsMaterial({ color: 0xffffff, size: 0.1, transparent: true, opacity: 0.6 }); const particles new THREE.Points(geometry, material); this.particleGroup.add(particles); this.particles.push({ mesh: particles, startTime: Date.now(), duration: 3000 // 3秒 }); } update() { const currentTime Date.now(); for (let i this.particles.length - 1; i 0; i--) { const particle this.particles[i]; const elapsed currentTime - particle.startTime; const progress elapsed / particle.duration; if (progress 1) { this.particleGroup.remove(particle.mesh); this.particles.splice(i, 1); } else { // 更新粒子位置和透明度 particle.mesh.position.y 0.01; particle.mesh.material.opacity 0.6 * (1 - progress); } } } createMagicSparkles(position, count 50) { // 创建魔方旋转时的魔法火花效果 const geometry new THREE.BufferGeometry(); const positions new Float32Array(count * 3); const colors new Float32Array(count * 3); for (let i 0; i count; i) { positions[i * 3] position.x (Math.random() - 0.5) * 2; positions[i * 3 1] position.y (Math.random() - 0.5) * 2; positions[i * 3 2] position.z (Math.random() - 0.5) * 2; // 随机颜色 colors[i * 3] Math.random(); colors[i * 3 1] Math.random(); colors[i * 3 2] Math.random(); } geometry.setAttribute(position, new THREE.BufferAttribute(positions, 3)); geometry.setAttribute(color, new THREE.BufferAttribute(colors, 3)); const material new THREE.PointsMaterial({ size: 0.1, vertexColors: true, transparent: true, opacity: 0.8 }); const sparkles new THREE.Points(geometry, material); this.particleGroup.add(sparkles); this.particles.push({ mesh: sparkles, startTime: currentTime, duration: 2000 }); } }6. 主程序集成与用户体验优化6.1 应用入口文件整合将各个模块整合到主程序中// js/main.js import { SceneManager } from ./core/SceneManager.js; import { CubeManager } from ./core/CubeManager.js; import { TeapotManager } from ./core/TeapotManager.js; import { PhysicsManager } from ./core/PhysicsManager.js; import { GestureHandler } from ./utils/GestureHandler.js; import { ParticleSystem } from ./effects/ParticleSystem.js; class MagicCubeTeaApp { constructor() { this.init(); } init() { // 初始化场景 this.sceneManager new SceneManager(canvas-container); this.physicsManager new PhysicsManager(); this.particleSystem new ParticleSystem(this.sceneManager.scene); // 创建魔方和茶壶 this.cubeManager new CubeManager(this.sceneManager.scene); this.teapotManager new TeapotManager(this.sceneManager.scene); // 设置手势交互 this.gestureHandler new GestureHandler( this.sceneManager.renderer.domElement, this.cubeManager, this.physicsManager ); // 绑定UI事件 this.setupUIEvents(); // 启动主循环 this.animate(); // 响应窗口大小变化 window.addEventListener(resize, () this.onWindowResize()); } setupUIEvents() { document.getElementById(shake-btn).addEventListener(click, () { this.shakeCube(); }); document.getElementById(twist-btn).addEventListener(click, () { this.twistTeapot(); }); // 键盘快捷键 document.addEventListener(keydown, (e) { switch(e.code) { case Space: this.shakeCube(); break; case KeyT: this.twistTeapot(); break; } }); } shakeCube() { this.physicsManager.applyShakeForce(15); this.particleSystem.createMagicSparkles( this.cubeManager.cubeGroup.position ); } twistTeapot() { this.teapotManager.twist(Math.random() 0.5); this.particleSystem.createTeaSteam( this.teapotManager.teapot.position ); } animate() { requestAnimationFrame(() this.animate()); // 更新物理世界 this.physicsManager.update(); // 更新粒子系统 this.particleSystem.update(); // 更新渲染 this.sceneManager.renderer.render( this.sceneManager.scene, this.sceneManager.camera ); } onWindowResize() { this.sceneManager.onWindowResize(); } } // 启动应用 new MagicCubeTeaApp();6.2 响应式样式设计添加CSS样式确保在不同设备上都有良好的显示效果/* css/style.css */ body { margin: 0; padding: 0; font-family: Microsoft YaHei, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); overflow: hidden; } #container { position: relative; width: 100vw; height: 100vh; } #canvas-container { width: 100%; height: 100%; } #ui-controls { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); display: flex; gap: 20px; z-index: 100; } button { padding: 12px 24px; background: rgba(255, 255, 255, 0.9); border: none; border-radius: 25px; font-size: 16px; font-weight: bold; color: #333; cursor: pointer; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); transition: all 0.3s ease; } button:hover { background: rgba(255, 255, 255, 1); transform: translateY(-2px); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); } button:active { transform: translateY(0); } #gesture-hint { background: rgba(0, 0, 0, 0.7); color: white; padding: 10px 20px; border-radius: 20px; font-size: 14px; } /* 移动端适配 */ media (max-width: 768px) { #ui-controls { flex-direction: column; align-items: center; } button { padding: 15px 30px; font-size: 18px; } #gesture-hint { font-size: 12px; text-align: center; } } /* 加载动画 */ .loading { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; font-size: 24px; z-index: 1000; }7. 性能优化与最佳实践7.1 3D渲染性能优化针对WebGL渲染进行性能调优// js/utils/PerformanceOptimizer.js export class PerformanceOptimizer { static optimizeRenderer(renderer) { // 设置合理的像素比例 renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // 启用纹理压缩如果支持 if (renderer.extensions.get(WEBGL_compressed_texture)) { // 配置纹理压缩格式 } } static optimizeScene(scene) { // 合并几何体减少draw call this.mergeGeometries(scene); // 设置适当的LOD细节层次 this.setupLOD(scene); } static mergeGeometries(scene) { // 将相同材质的几何体合并 const geometryGroups new Map(); scene.traverse((object) { if (object.isMesh) { const key object.material.uuid; if (!geometryGroups.has(key)) { geometryGroups.set(key, []); } geometryGroups.get(key).push(object); } }); geometryGroups.forEach((meshes, materialId) { if (meshes.length 1) { this.mergeMeshGroup(meshes); } }); } static setupLOD(scene) { // 为远距离对象设置低细节模型 scene.traverse((object) { if (object.isMesh object.geometry) { // 根据对象大小和重要性设置LOD const boundingBox new THREE.Box3().setFromObject(object); const size boundingBox.getSize(new THREE.Vector3()); const maxDim Math.max(size.x, size.y, size.z); if (maxDim 1) { // 小对象使用较低细节 object.geometry this.simplifyGeometry(object.geometry, 0.5); } } }); } }7.2 内存管理与资源清理确保应用不会内存泄漏// js/utils/MemoryManager.js export class MemoryManager { constructor() { this.resources new Set(); } trackResource(resource) { this.resources.add(resource); return resource; } disposeResource(resource) { if (resource.dispose) { resource.dispose(); } if (resource.geometry) { resource.geometry.dispose(); } if (resource.material) { if (Array.isArray(resource.material)) { resource.material.forEach(material material.dispose()); } else { resource.material.dispose(); } } if (resource.texture) { resource.texture.dispose(); } this.resources.delete(resource); } cleanup() { this.resources.forEach(resource this.disposeResource(resource)); this.resources.clear(); } // 监控内存使用 monitorMemory() { if (performance.memory) { const usedMB performance.memory.usedJSHeapSize / 1048576; const limitMB performance.memory.jsHeapSizeLimit / 1048576; if (usedMB limitMB * 0.8) { console.warn(内存使用过高建议清理资源); this.cleanupUnusedResources(); } } } cleanupUnusedResources() { // 清理长时间未使用的资源 const now Date.now(); this.resources.forEach(resource { if (now - resource.lastUsed 30000) { // 30秒未使用 this.disposeResource(resource); } }); } }8. 常见问题与解决方案8.1 性能问题排查问题现象可能原因解决方案页面卡顿帧率低1. 图形复杂度太高2. 物理计算过多3. 内存泄漏1. 减少多边形数量2. 优化物理参数3. 使用性能分析工具移动端发热严重1. 连续渲染未优化2. 计算密集型操作1. 添加渲染暂停机制2. 降低渲染质量加载时间过长1. 模型文件太大2. 资源未压缩1. 使用GLTF压缩2. 实现渐进式加载8