公司动态
Three.js案例实战:从环境搭建到UV映射与Vue3集成
这类 Three.js 案例合集最值得先看的不是功能列表而是能不能在普通开发环境里快速跑起来、理解核心参数和常见问题。我一般会先确认案例类型是基础几何体、材质贴图、光照交互还是更复杂的模型加载、着色器或物理效果。下面按实际落地顺序拆一遍重点放在环境准备、关键参数解释和排查思路上。1. 先确认案例类型和运行方式Three.js 案例通常有两种运行方式本地静态文件直接打开或通过本地服务器启动。如果案例涉及模型加载、纹理读取或外部资源浏览器安全策略会限制直接打开必须用服务器环境。1.1 本地静态文件直接运行纯 HTML、CSS、JS 且无外部资源加载的案例可以直接用浏览器打开index.html。这类案例适合学习基础 API比如创建场景、相机、渲染器、几何体和基础材质。!DOCTYPE html html head meta charsetutf-8 title基础立方体案例/title stylebody { margin: 0; }/style /head body script srchttps://cdnjs.cloudflare.com/ajax/libs/three.js/r185/three.min.js/script script // 基础 Three.js 场景搭建代码 const scene new THREE.Scene(); const camera new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); const renderer new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); const geometry new THREE.BoxGeometry(); const material new THREE.MeshBasicMaterial({ color: 0x00ff00 }); const cube new THREE.Mesh(geometry, material); scene.add(cube); camera.position.z 5; function animate() { requestAnimationFrame(animate); cube.rotation.x 0.01; cube.rotation.y 0.01; renderer.render(scene, camera); } animate(); /script /body /html这种案例能直接跑但功能简单适合验证环境是否正常。1.2 需要本地服务器的案例如果案例包含以下情况必须启动本地服务器加载外部模型文件.glb、.gltf、.obj读取本地纹理图片.jpg、.png使用模块化导入import maps、ES modules涉及 AJAX 请求或动态资源加载最快的方式是用 Python 或 Node.js 启动静态服务器# Python 3 python -m http.server 8000 # Python 2 python -m SimpleHTTPServer 8000 # Node.js 全局安装 http-server npm install -g http-server http-server -p 8000启动后访问http://localhost:8000查看案例。如果案例放在子目录确保路径正确。1.3 判断案例复杂度的关键点拿到案例代码时先看这几处是否有import语句需要模块化支持可能要用构建工具或特定服务器配置。是否有new THREE.TextureLoader().load()或类似加载器需要服务器环境。是否有外部模型路径检查路径是否存在网络请求是否被拦截。如果案例跑不起来先确认运行方式再排查具体问题。2. 贴图渲染和 UV 坐标的核心理解贴图如何渲染到平面或不规则表面是 Three.js 材质系统的核心。这里最容易混淆的是 UV 坐标的概念和映射关系。2.1 UV 坐标基础从平面到不规则表面UV 坐标决定了纹理图片如何映射到几何体表面。U 和 V 是二维坐标范围 [0, 1]对应纹理的宽高比例。对于规则平面PlaneGeometry默认 UV 是简单映射左下角 (0, 0)右下角 (1, 0)左上角 (0, 1)右上角 (1, 1)纹理会完整铺满整个平面。但如果平面被变形或顶点位置改变UV 保持不变纹理就会跟随表面形状拉伸。对于不规则几何体如 SphereGeometry、自定义 BufferGeometryUV 映射更复杂球体UV 沿经纬度分布极点处会有压缩圆柱体侧面 UV 沿高度和圆周展开顶底面单独映射自定义几何体需要手动计算 UV否则纹理可能错乱// 创建平面并检查默认 UV const planeGeometry new THREE.PlaneGeometry(5, 5); console.log(planeGeometry.attributes.uv.array); // 输出[0, 0, 1, 0, 0, 1, 1, 1] 等对应四个顶点的 UV // 创建球体并检查 UV const sphereGeometry new THREE.SphereGeometry(3, 32, 16); console.log(sphereGeometry.attributes.uv.array); // UV 值会更复杂沿球面分布2.2 贴图占满平面的控制方式如果希望纹理完整覆盖平面且不重复关键参数是wrapS和wrapTconst textureLoader new THREE.TextureLoader(); const texture textureLoader.load(texture.jpg); // 禁止重复拉伸到整个平面 texture.wrapS THREE.ClampToEdgeWrapping; texture.wrapT THREE.ClampToEdgeWrapping; // 或者允许重复但设置合适的重复次数 texture.wrapS THREE.RepeatWrapping; texture.wrapT THREE.RepeatWrapping; texture.repeat.set(1, 1); // 重复1次即占满整个平面 const material new THREE.MeshBasicMaterial({ map: texture });对于不规则表面纹理是否占满取决于几何体的 UV 映射。如果 UV 范围是 [0, 1]纹理就会完整映射到整个表面但可能会因表面形状而拉伸变形。2.3 自定义 UV 映射的高级处理当默认 UV 不满足需求时需要手动计算 UV 坐标。比如想要纹理在复杂表面按特定方式分布// 创建自定义几何体 const geometry new THREE.BufferGeometry(); // 顶点位置 const vertices new Float32Array([ -1, -1, 0, // 顶点0 1, -1, 0, // 顶点1 0, 1, 0 // 顶点2 ]); // 手动设置 UV 坐标 const uvs new Float32Array([ 0, 0, // 顶点0 对应纹理左下 1, 0, // 顶点1 对应纹理右下 0.5, 1 // 顶点2 对应纹理中上 ]); geometry.setAttribute(position, new THREE.BufferAttribute(vertices, 3)); geometry.setAttribute(uv, new THREE.BufferAttribute(uvs, 2));这种手动 UV 控制在不规则表面纹理映射中很常见特别是游戏模型和复杂三维扫描数据。3. 经纬度回显和球面坐标转换经纬度回显通常指将地理坐标经度、纬度转换为三维空间坐标常用于地球、地图可视化或球面数据展示。3.1 球面坐标转三维坐标公式Three.js 提供了球面坐标支持但需要理解转换原理function latLongToVector3(latitude, longitude, radius) { // 将角度转换为弧度 const phi (90 - latitude) * (Math.PI / 180); const theta (longitude 180) * (Math.PI / 180); // 球面坐标转直角坐标 const x -radius * Math.sin(phi) * Math.cos(theta); const y radius * Math.cos(phi); const z radius * Math.sin(phi) * Math.sin(theta); return new THREE.Vector3(x, y, z); } // 使用示例在北京位置北纬39.9°东经116.4°放置标记 const beijingPos latLongToVector3(39.9, 116.4, 10); const markerGeometry new THREE.SphereGeometry(0.1, 16, 16); const markerMaterial new THREE.MeshBasicMaterial({ color: 0xff0000 }); const marker new THREE.Mesh(markerGeometry, markerMaterial); marker.position.copy(beijingPos); scene.add(marker);3.2 使用 Three.js 内置球面坐标类更简洁的方式是使用Spherical类const spherical new THREE.Spherical(); spherical.radius 10; // 球体半径 // 设置经纬度注意参数顺序和单位 spherical.phi (90 - 39.9) * Math.PI / 180; // 纬度转phi极角 spherical.theta (116.4 180) * Math.PI / 180; // 经度转theta方位角 const position new THREE.Vector3(); position.setFromSpherical(spherical); marker.position.copy(position);3.3 实际应用中的注意事项经纬度回显时常见问题坐标原点Three.js 球体中心是 (0,0,0)地理坐标需要对应转换角度方向数学坐标系与地理坐标系的方向差异精度处理大量标记点时考虑性能使用 InstancedMesh 或合并几何体交互支持添加点击事件时需要将屏幕坐标转三维世界坐标// 批量添加城市标记的优化方式 const cities [ { name: 北京, lat: 39.9, lng: 116.4 }, { name: 上海, lat: 31.2, lng: 121.5 }, // ... 更多城市 ]; cities.forEach(city { const pos latLongToVector3(city.lat, city.lng, 10); // 使用实例化或简单几何体避免性能问题 });4. Vue3 Three.js GLB 集成方案现代前端项目常用 Vue3 组合式 API 集成 Three.jsGLB 格式因其二进制压缩和完整性成为首选3D模型格式。4.1 Vue3 组合式 API 集成 Three.jstemplate div refcontainer classthree-container/div /template script setup import { ref, onMounted, onUnmounted } from vue; import * as THREE from three; import { GLTFLoader } from three/examples/jsm/loaders/GLTFLoader; const container ref(null); let scene, camera, renderer, model; onMounted(() { initThree(); loadModel(); animate(); }); onUnmounted(() { // 清理资源 if (renderer) { renderer.dispose(); } }); function initThree() { // 初始化场景 scene new THREE.Scene(); scene.background new THREE.Color(0xf0f0f0); // 初始化相机 camera new THREE.PerspectiveCamera( 75, container.value.clientWidth / container.value.clientHeight, 0.1, 1000 ); camera.position.set(0, 0, 5); // 初始化渲染器 renderer new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(container.value.clientWidth, container.value.clientHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); container.value.appendChild(renderer.domElement); // 添加光源 const ambientLight new THREE.AmbientLight(0xffffff, 0.6); scene.add(ambientLight); const directionalLight new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(10, 10, 5); scene.add(directionalLight); } function loadModel() { const loader new GLTFLoader(); loader.load( /models/example.glb, (gltf) { model gltf.scene; scene.add(model); // 调整模型位置和缩放 model.scale.set(1, 1, 1); model.position.set(0, 0, 0); }, (progress) { console.log(加载进度: ${(progress.loaded / progress.total * 100)}%); }, (error) { console.error(模型加载失败:, error); } ); } function animate() { requestAnimationFrame(animate); if (model) { model.rotation.y 0.01; } renderer.render(scene, camera); } // 响应窗口大小变化 window.addEventListener(resize, () { camera.aspect container.value.clientWidth / container.value.clientHeight; camera.updateProjectionMatrix(); renderer.setSize(container.value.clientWidth, container.value.clientHeight); }); /script style scoped .three-container { width: 100%; height: 500px; } /style4.2 GLB 模型加载的优化要点GLB 加载时要注意这些实际问题模型路径和打包处理开发环境模型放在public/models/目录路径如/models/example.glb生产环境确保模型文件被正确复制到输出目录Vue CLI / Vite静态资源处理规则不同需要测试验证加载进度和错误处理// 更详细的进度监控 const manager new THREE.LoadingManager(); manager.onProgress (url, itemsLoaded, itemsTotal) { console.log(加载进度: ${itemsLoaded}/${itemsTotal}); }; manager.onError (url) { console.error(加载失败: ${url}); }; const loader new GLTFLoader(manager);模型缩放和位置适配GLB 模型可能有各种尺寸需要自动适配loader.load(/models/example.glb, (gltf) { model gltf.scene; // 计算包围盒自动缩放和居中 const box new THREE.Box3().setFromObject(model); const center box.getCenter(new THREE.Vector3()); const size box.getSize(new THREE.Vector3()); // 自动缩放以适应视图 const maxDim Math.max(size.x, size.y, size.z); const scale 5 / maxDim; // 缩放到5个单位内 model.scale.setScalar(scale); // 居中显示 model.position.sub(center.multiplyScalar(scale)); scene.add(model); });4.3 性能优化和内存管理Vue 组件销毁时必须清理 Three.js 资源onUnmounted(() { // 清理几何体、材质、纹理 scene.traverse((object) { if (object.isMesh) { object.geometry.dispose(); if (object.material) { if (Array.isArray(object.material)) { object.material.forEach(material material.dispose()); } else { object.material.dispose(); } } } }); renderer.dispose(); renderer.forceContextLoss(); });5. Water 材质和 flowDirection 属性问题排查Three.js 的水面效果通常使用 Water 类但版本差异可能导致属性不存在错误。5.1 Water 类的正确引入和使用首先确认 Water 类的引入路径// Three.js r125 的正确引入方式 import { Water } from three/examples/jsm/objects/Water.js; // 或者 const { Water } await import(three/examples/jsm/objects/Water.js); // 创建水面几何体 const waterGeometry new THREE.PlaneGeometry(100, 100); // 创建 Water 实例 const water new Water(waterGeometry, { textureWidth: 512, textureHeight: 512, waterNormals: new THREE.TextureLoader().load(/textures/waternormals.jpg, (texture) { texture.wrapS texture.wrapT THREE.RepeatWrapping; }), sunDirection: new THREE.Vector3(), sunColor: 0xffffff, waterColor: 0x001e0f, distortionScale: 3.7, fog: scene.fog ! undefined }); water.rotation.x -Math.PI / 2; scene.add(water);5.2 flowDirection 属性问题的解决方案flowDirection属性不存在通常是因为版本兼容问题。检查以下解决方案方案一使用正确的参数名较新版本中可能参数名已改变// 尝试这些替代参数 const water new Water(waterGeometry, { // ... 其他参数 flowDirection: new THREE.Vector2(1, 1), // 旧版本可能支持 direction: new THREE.Vector2(1, 1), // 新版本可能用这个 });方案二检查 Three.js 版本不同版本的 Water 类 API 可能不同// 查看当前 Three.js 版本 console.log(Three.js 版本:, THREE.REVISION); // 根据版本调整代码 if (THREE.REVISION 125) { // 使用新版本 API const water new Water(waterGeometry, { // 新版本参数 }); } else { // 使用旧版本 API const water new Water(waterGeometry, { // 旧版本参数可能包含 flowDirection }); }方案三手动实现水流方向如果 Water 类不支持 flowDirection可以手动控制// 通过更新纹理偏移模拟水流方向 function animate() { requestAnimationFrame(animate); if (water.material.uniforms) { // 根据版本调整uniform名称 const timeUniform water.material.uniforms.time; if (timeUniform) { timeUniform.value 0.01; } // 手动控制水流方向 const normalScaleUniform water.material.uniforms.normalScale; if (normalScaleUniform) { // 通过调整法线贴图缩放模拟方向 } } renderer.render(scene, camera); }5.3 水面效果的完整配置示例这里是经过测试的水面配置import { Water } from three/examples/jsm/objects/Water.js; // 加载法线贴图 const waterNormals new THREE.TextureLoader().load(/textures/waternormals.jpg); waterNormals.wrapS waterNormals.wrapT THREE.RepeatWrapping; // 创建水面 const waterGeometry new THREE.PlaneGeometry(100, 100); const water new Water(waterGeometry, { textureWidth: 512, textureHeight: 512, waterNormals: waterNormals, sunDirection: new THREE.Vector3(0, 10, 0).normalize(), sunColor: 0xffffff, waterColor: 0x001e0f, distortionScale: 3.7, fog: scene.fog ! undefined }); water.rotation.x -Math.PI / 2; water.position.y 0.1; // 稍微高于地面避免z-fighting scene.add(water); // 动画循环中更新水面 function animate() { requestAnimationFrame(animate); // 更新水面uniforms if (water.material.uniforms water.material.uniforms.time) { water.material.uniforms.time.value 0.01; } renderer.render(scene, camera); }6. 案例合集的系统化学习方法面对大量 Three.js 案例时系统化的学习方法比盲目尝试更有效。6.1 按功能模块分类学习将案例按核心功能分类基础场景场景图、相机、渲染器、坐标系几何体内置几何体、自定义 BufferGeometry、顶点操作材质贴图基础材质、物理材质、纹理映射、UV 操作光照阴影各种光源类型、阴影映射、环境光遮蔽模型加载GLTF/GLB、OBJ、FBX 格式处理动画交互补间动画、骨骼动画、物理模拟、射线检测后期处理着色器、粒子系统、后期效果每个类别选择 2-3 个典型案例深入理解而不是浅尝辄止。6.2 建立调试和排查流程遇到案例跑不通时按这个顺序排查控制台错误先看浏览器控制台的详细错误信息资源加载检查 Network 面板看模型、纹理是否成功加载Three.js 版本确认案例使用的 Three.js 版本与当前环境是否兼容运行环境确认是否需要服务器环境路径是否正确代码结构检查 import/export 语法模块加载方式逐步调试注释掉部分代码定位具体出错位置6.3 从案例到项目的过渡思路学习案例后要思考如何应用到实际项目组件化封装将 Three.js 功能封装成可复用的 Vue/React 组件性能优化大数据量时的实例化、LOD、缓存策略构建集成如何与 Webpack、Vite 等构建工具配合跨平台适配移动端触摸交互、性能限制处理团队协作代码规范、资源管理、文档维护我个人更建议先把单个案例的每个参数都理解透彻再尝试组合多个功能。Three.js 的学习曲线前期较陡但掌握核心概念后各种复杂效果都是这些基础能力的组合应用。