作者:Sword99
前言
本人前端吗喽一枚, 自从 21 年毕业后就来到了杭州工作,在杭州也待了 3 年多了,节假日偶尔也会去杭州省内其他城市旅游,偶尔刷掘金看到豆包MarsCode,刚好来试用体验一下并结合 Threejs 实现了浙江省内旅游景点的 3D 可视化展示(文章末尾会放源码地址)。
项目预览:
一. 项目初始化
使用 html/css/js 模版
项目初始化详情(默认安装了 vite),点击顶部运行按钮或使用命令行npm run start即可启动项目
安装项目依赖, package.json 概览
{ "name": "web-test", "version": "1.0.0", "description": "", "scripts": { "start": "vite --host --port=8000" }, "devDependencies": { "vite": "^5.2.12", "vite-plugin-full-reload": "^1.1.0" }, "dependencies": { "d3": "^7.9.0", "three": "^0.169.0" }}
复制代码
二. 代码实现
1. threejs 初始化配置
初始化场景,限制一下 control 的旋转角度,别的较为基础,没啥好说的。
const renderer = new THREE.WebGLRenderer({ antialias: true, canvas: document.querySelector('#container'), }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(window.devicePixelRatio);
const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 500, ); const initYDistance = 370; camera.position.set(0, initYDistance, 250); camera.lookAt(0, 0, 250);
const controls = new OrbitControls( camera, renderer.domElement, ); controls.maxDistance = initYDistance; controls.minDistance = initYDistance; controls.minPolarAngle = Math.PI * 0.05; controls.maxPolarAngle = Math.PI * 0.48; controls.update();
// 场景 const scene = new THREE.Scene(); scene.background = new THREE.Color(0xf0f0f0);
const helper = new THREE.GridHelper(2500, 100); scene.add(helper);
const color = 0xffffff; const intensity = 1; // 环境光 const light = new THREE.AmbientLight(color, intensity); // 加入场景 scene.add(light);
复制代码
2. GeoJSON 数据的获取
此时我想试试豆包MarsCode 的实力,就点击右侧 AI 样式的按钮,打开对话框,询问豆包 MarsCode。
出乎我意料的是豆包MarsCode 在准确回答的同时还支持保存文件至当前项目目录。
3. GeoJSON 数据的解析
Threejs 中通常使用 FileLoader 解析 json 格式的数据
const loader = new THREE.FileLoader(); loader.load('/zhejiang.json', (data) => { const jsondata = JSON.parse(data); resolveData(jsondata);});
复制代码
Geojson 数据格式浅析
{ "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": { "adcode": 330100, "name": "杭州市", "center": [120.153576, 30.287459], ....... }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [120.721941, 30.286334], [120.710868, 30.297542], ....... ] ] ] } }, ...... ]}
复制代码
4. 3d 地图绘制
地图的 3d 绘制步骤
基于 GeoJSON 的点坐标数据和 Three 的 Shape 类绘制地图的 2d 轮廓,
使用 Three 的 ExtrudeGeometry 将 2d 轮廓拉伸至 3d
添加地图轮廓线(BufferGeometry)以及各个市的名称(TextGeometry)
具体实现代码
地图轮廓绘制 :
/** * 立体几何图形绘制 * @param polygon 多边形点数组 * @param color 材质颜色 * */const drawExtrudeMesh = (polygon, color, projection) => { const shape = new THREE.Shape(); polygon.forEach((row, i) => { const [x, y] = projection(row); if (i === 0) { shape.moveTo(x, -y); } shape.lineTo(x, -y); });
const extrudeGeometry = new THREE.ExtrudeGeometry(shape, { depth: 10, bevelEnabled: false, }); const extrudeMeshMaterial = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.9, }); return new THREE.Mesh(extrudeGeometry, extrudeMeshMaterial);};
复制代码
轮廓线绘制 :
/** * 轮廓线图形绘制 * @param polygon 多边形点数组 * @param color 材质颜色 * */const lineDraw = (polygon, color, projection) => { const lineGeometry = new THREE.BufferGeometry(); const pointsArray = new Array(); polygon.forEach((row) => { const [x, y] = projection(row); pointsArray.push(new THREE.Vector3(x, -y, 9)); }); lineGeometry.setFromPoints(pointsArray); const lineMaterial = new THREE.LineBasicMaterial({ color: color, }); return new THREE.Line(lineGeometry, lineMaterial);}
复制代码
市名绘制 :
/** * * @param {中心点坐标} centerPosition * @param {中心点名称} centerName * @returns */ const drawFont = (centerPosition, centerName) => { return new Promise((resolve, reject) => { const loader = new FontLoader(); loader.load('/f.json', (font) => { if (!font) { reject('Font loading failed.'); return; } const textGeometry = new TextGeometry(centerName, { font: font, size: 0.2, depth: 0.1, bevelEnabled: false, });
const textMaterial = new THREE.MeshBasicMaterial({ color: '#fff', });
const textMesh = new THREE.Mesh(textGeometry, textMaterial); const [x, y] = centerPosition; textMesh.position.set(x, -y, 10); resolve(textMesh); }, undefined, (error) => { reject('Font loading error: ' + error); }); }) }
复制代码
5. 交互事件的添加
给各个市添加点击事件,在 ThreeJs 中常用的方式是通过射线来检测当前鼠标是否在某一个 mesh 上。
坐标归一化: 将 window.click 事件中 event 对象获取的位置参数转化为 three 中归一化坐标
/** * 获取鼠标在three.js 中归一化坐标 * */const setPickPosition = (event) => { let pickPosition = { x: 0, y: 0 }; pickPosition.x = (event.clientX / renderer.domElement.width) * 2 - 1; pickPosition.y = (event.clientY / renderer.domElement.height) * -2 + 1; return pickPosition;}
复制代码
射线检测:
let lastPick = null; // 上一次点击的mesh let lastPickColor = "" // 上一次点击mesh的颜色 // 鼠标点击事件 const onRay = (event) => { let pickPosition = setPickPosition(event); const raycaster = new THREE.Raycaster(); raycaster.setFromCamera(pickPosition, camera); // 计算物体和射线的交点 const intersects = raycaster.intersectObjects([map], true); const intersectExtudeMesh = intersects.find((item) => { return item.object.geometry.type === "ExtrudeGeometry" }) // 数组大于0 表示有相交对象 if (intersectExtudeMesh) { if (lastPick && lastPickColor) { if ( lastPick.object.properties !== intersectExtudeMesh.object.properties ) { lastPick.object.material.color.set(lastPickColor); lastPick = intersectExtudeMesh; lastPickColor = JSON.parse(JSON.stringify(intersectExtudeMesh.object.material.color)); intersectExtudeMesh.object.material.color.set('#c699aa'); } else { lastPick.object.material.color.set(lastPickColor); lastPick = null; lastPickColor = ""; setToolTip('') return } } else { lastPick = intersectExtudeMesh; lastPickColor = JSON.parse(JSON.stringify(intersectExtudeMesh.object.material.color)); intersectExtudeMesh.object.material.color.set('#c699aa'); } setToolTip(intersectExtudeMesh.object.properties) } else { if (lastPick && lastPickColor) { // 复原 if (lastPick.object.properties) { lastPick.object.material.color.set(lastPickColor); lastPick = null; } } setToolTip('') } }
复制代码
根据点击的 mesh 展示相应的信息
/** * 景点信息展示 * @param {市名} proviceName */const setToolTip = (proviceName) => { const tooltip = document.getElementById('tooltip') if (proviceName) { tooltip.style.display = 'block' generateDom(tooltip,proviceName,travelData.find((item) => item.city === proviceName)?.attractions) } else { tooltip.style.display = 'none' }}
复制代码
绑定 click 事件
// 监听鼠标click事件window.addEventListener('click', onRay)
复制代码
三. 项目提交至仓库
豆包MarsCode 支持代码上传到 github,配置好认证信息就可以提交啦!
四. 结语
就这个项目而言,豆包MarsCode 给我的使用感觉:
优点:
如果大家感兴趣可以点击下方链接自行体验一下,欢迎大家在评论区交流,希望可以一键三连!
豆包体验地址: marscode
代码仓库地址: github
评论