基于豆包MarsCode 和 Threejs 实现3D地图可视化

作者: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数据格式浅析

  • properties 中name代表当前市名称,center代表坐标位置

  • coordinates 数组中是当前市的地理坐标数组

  • type代表geometry的类型

      {
      "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绘制步骤

  1. 基于GeoJSON的点坐标数据和Three的Shape类绘制地图的2d轮廓,
  2. 使用Three的ExtrudeGeometry将2d轮廓拉伸至3d
  3. 添加地图轮廓线(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);
}

市名绘制 :

  • f.json是字体文件的json格式,本文使用的是微软雅黑
  • 可以通过 facetype.js 将ttf格式的字体文件转化为json格式

(怕麻烦的同学也可以直接求助于豆包MarsCode AI 助手)

    /**
     * 
     * @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 给我的使用感觉:

优点:

  • 初始化模版丰富,方便快速开发
  • AI交互也还可以,支持直接生成文件至项目目录,这个确实挺方便
  • 代码提示准确度也还不错

如果大家感兴趣可以点击下方链接自行体验一下,欢迎大家在评论区交流,希望可以一键三连!

相关推荐
CoderIsArt9 分钟前
基于 CMAC(神经网络)与 PID 的并行控制
人工智能·深度学习·神经网络
小屁孩大帅-杨一凡13 分钟前
python获取本地电脑的ip和mac地址
java·服务器·网络·python·tcp/ip
胜天半子_王二_王半仙18 分钟前
c++源码阅读__ThreadPool__正文阅读
开发语言·c++·开源
企业软文推广18 分钟前
奶龙IP联名异军突起:如何携手品牌营销共创双赢?
人工智能·python
gz945631 分钟前
windows下,用CMake编译qt项目,出现错误By not providing “FindQt5.cmake“...
开发语言·qt
sukalot33 分钟前
windows C#-异步编程模型(下)
开发语言·windows·c#
Yz987633 分钟前
Hive分桶超详细!!!
大数据·数据仓库·hive·hadoop·hdfs·数据库开发·big data
Francek Chen38 分钟前
【大数据技术基础 | 实验十一】Hive实验:新建Hive表
大数据·数据仓库·hive·hadoop·分布式
水w42 分钟前
Node.js windows版本 下载和安装(详细步骤)
开发语言·前端·windows·npm·node
GOTXX1 小时前
情感神经元的意外发现2
人工智能·深度学习·神经网络·机器学习·卷积神经网络