当前位置: 首页 > article >正文

Vue3实现地球上加载柱体

最终效果为上图。

实现该技术,需要一些技术,我分别罗列一下:

  • canvas:需要使用canvas根据json来绘制地球,不懂的可以看这篇canvas绘制地球 
  • threejs:需要会使用threejs,这里并没有使用shader,不需要制作复杂的东西。
  • Vue3:这个可选。不会也能实现。

需要使用的插件:

  • @surbowl/world-geo-json-zh :这个第三方包是简体中文 Geo JSON 世界地图,带有国家(地区)的 ISO 3166 代码、中文简称与全称。含中国南海海域十段线。

  • three :这个我就不用说了。

然后下面是具体实现的代码:

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import * as TWEEN from 'three/examples/jsm/libs/tween.module.js';
import worldJSON from '@surbowl/world-geo-json-zh'
import earthquakeJSON from '../assets/json/earthquake.json'

export default (domId) => {
    /* ------------------------------初始化三件套--------------------------------- */
    const dom = document.getElementById(domId);
    const { innerHeight, innerWidth } = window

    const scene = new THREE.Scene();

    const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 1, 2000);
    camera.position.set(0, 0, 10);
    camera.lookAt(scene.position);

    const renderer = new THREE.WebGLRenderer({
        antialias: true,// 抗锯齿
        alpha: false,// 透明度
        powerPreference: 'high-performance',// 性能
        logarithmicDepthBuffer: true,// 深度缓冲
    })
    // renderer.setClearColor(0x000000, 0);// 设置背景色
    // renderer.clear();// 清除渲染器
    renderer.shadowMap.enabled = true;// 开启阴影
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;// 阴影类型
    renderer.outputEncoding = THREE.sRGBEncoding;// 输出编码
    renderer.toneMapping = THREE.ACESFilmicToneMapping;// 色调映射
    renderer.toneMappingExposure = 1;// 色调映射曝光
    renderer.physicallyCorrectLights = true;// 物理正确灯光
    renderer.setPixelRatio(devicePixelRatio);// 设置像素比
    renderer.setSize(innerWidth, innerHeight);// 设置渲染器大小
    dom.appendChild(renderer.domElement);

    // 重置大小
    window.addEventListener('resize', () => {
        const { innerHeight, innerWidth } = window
        camera.aspect = innerWidth / innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(innerWidth, innerHeight);
    })

    /* ------------------------------初始化工具--------------------------------- */
    const controls = new OrbitControls(camera, renderer.domElement) // 相机轨道控制器
    controls.enableDamping = true // 是否开启阻尼
    controls.dampingFactor = 0.05// 阻尼系数
    controls.panSpeed = -1// 平移速度

    // const axesHelper = new THREE.AxesHelper(10);
    // scene.add(axesHelper);

    /* ------------------------------正题--------------------------------- */
    // 地图配置
    const mapOptions = {
        sphere: null, // 球体
        bg: 'rgb(10 ,20 ,28)',// 背景色
        borderColor: 'rgb(10 ,20 ,28)',// 边框颜色
        blurColor: '#000000',// 模糊颜色
        borderWidth: 1,// 边框宽度
        blurWidth: 5,// 模糊宽度
        fillColor: 'rgb(26, 35, 44)',// 填充颜色
        barHueStart: 0.7,// 柱体颜色起始值
        barHueEnd: 0.2,// 柱体颜色结束值
        barLightStart: 0.1,// 柱体亮度起始值
        barLightEnd: 1.0// 柱体亮度结束值
    }

    // 相机位置
    const cameraPos = {
        x: 0.27000767404584447,
        y: 1.0782003329514755,
        z: 3.8134631736522793
    }

    // 相机控制器位置
    const controlPos = {
        x: 0,
        y: 0,
        z: 0
    }

    // 柱状图信息
    const barInfo = {
        barMin: 0.01,
        barMax: 0.5,
        currentBarH: 0.01,// 柱体高度
        min: Number.MAX_SAFE_INTEGER,
        max: Number.MIN_SAFE_INTEGER,
        range: 0,
        mesh: null,// 柱体
        lonHelper: null, // 经度辅助线
        latHelper: null, // 纬度辅助线
        positionHelper: null,
        originHelper: null,
    }

    // 用于绑定整个地球的容器
    const objGroup = new THREE.Group();
    scene.add(objGroup);

    // 绘制地图
    const drawRegion = (ctx, c, geoInfo) => {
        ctx.beginPath();
        c.forEach((item, i) => {
            let pos = [(item[0] + 180) * 10, (-item[1] + 90) * 10];
            if (i == 0) {
                ctx.moveTo(pos[0], pos[1]);
            } else {
                ctx.lineTo(pos[0], pos[1]);
            }
        });
        ctx.closePath();
        ctx.fill();
        ctx.stroke();
    }

    // 创建地球
    const createMap = () => {
        const canvas = document.createElement('canvas');
        canvas.width = 3600;
        canvas.height = 1800;
        const ctx = canvas.getContext('2d');
        ctx.fillStyle = mapOptions.bg;
        ctx.rect(0, 0, canvas.width, canvas.height);
        ctx.fill();

        // 设置地图样式
        ctx.strokeStyle = mapOptions.borderColor;
        ctx.lineWidth = mapOptions.borderWidth;
        ctx.fillStyle = mapOptions.fillColor;
        if (mapOptions.blurWidth) {
            ctx.shadowColor = mapOptions.blurColor;
            ctx.shadowBlur = mapOptions.blurWidth;
        }

        // 遍历数据
        worldJSON.features.forEach(c1 => {
            // 判断是否为多边形
            if (c1.geometry.type == 'MultiPolygon') {
                c1.geometry.coordinates.forEach(c2 => {
                    c2.forEach(c3 => {
                        drawRegion(ctx, c3)
                    })
                })
            }
            else {
                c1.geometry.coordinates.forEach(c2 => {
                    drawRegion(ctx, c2)
                })
            }
        })

        const map = new THREE.CanvasTexture(canvas);// 创建纹理贴图
        map.wrapS = THREE.RepeatWrapping;// 水平方向重复
        map.wrapT = THREE.RepeatWrapping;// 垂直方向重复
        const geometry = new THREE.SphereGeometry(1, 32, 32);// 创建球体几何体
        const material = new THREE.MeshBasicMaterial({
            map: map,// 纹理贴图
            transparent: true// 透明
        });
        mapOptions.sphere = new THREE.Mesh(geometry, material);
        mapOptions.sphere.visible = false;// 隐藏地球
        objGroup.add(mapOptions.sphere);// 添加到场景中
    }

    // 创建柱体
    const createBar = (info, index) => {
        const amount = (info.mag - barInfo.min) / barInfo.range;// 根据值计算比例
        const hue = THREE.MathUtils.lerp(mapOptions.barHueStart, mapOptions.barHueEnd, amount);// 根据值计算颜色
        const saturation = 1;// 饱和度
        const lightness = THREE.MathUtils.lerp(mapOptions.barLightStart, mapOptions.barLightEnd, amount);// 根据值计算亮度
        const color = new THREE.Color();
        color.setHSL(hue, saturation, lightness);// 设置颜色
        barInfo.mesh.setColorAt(index, color);// 设置颜色
        barInfo.lonHelper.rotation.y = THREE.MathUtils.degToRad(info.lon) + Math.PI * 0.5;
        barInfo.latHelper.rotation.x = THREE.MathUtils.degToRad(-info.lat);
        barInfo.positionHelper.updateWorldMatrix(true, false);
        let h = THREE.MathUtils.lerp(0.01, 0.5, amount);
        barInfo.positionHelper.scale.set(0.01, 0.01, h <= barInfo.currentBarH ? h : barInfo.currentBarH);
        barInfo.originHelper.updateWorldMatrix(true, false);
        barInfo.mesh.setMatrixAt(index, barInfo.originHelper.matrixWorld);
    }

    // 创建柱体群
    const createBars = (list) => {
        list.forEach((info, index) => {
            createBar(info, index)
        })
        barInfo.mesh.instanceColor.needsUpdate = true;
        barInfo.mesh.instanceMatrix.needsUpdate = true;
    }

    // 创建全部柱体
    const createAllBars = () => {
        // 辅助对象
        barInfo.lonHelper = new THREE.Object3D();// 经度辅助对象
        scene.add(barInfo.lonHelper);
        barInfo.latHelper = new THREE.Object3D();// 纬度辅助对象
        barInfo.lonHelper.add(barInfo.latHelper);
        barInfo.positionHelper = new THREE.Object3D();// 位置辅助对象
        barInfo.positionHelper.position.z = 1;
        barInfo.latHelper.add(barInfo.positionHelper);
        barInfo.originHelper = new THREE.Object3D();// 原点
        barInfo.originHelper.position.z = 0.5;
        barInfo.positionHelper.add(barInfo.originHelper);

        const boxGeometry = new THREE.BoxGeometry(1, 1, 1);
        const boxMaterial = new THREE.MeshBasicMaterial({ color: '#FFFFFF' });
        earthquakeJSON.forEach(c1 => {
            if (barInfo.min > c1.mag) {
                barInfo.min = c1.mag;
            }
            if (barInfo.max < c1.mag) {
                barInfo.max = c1.mag;
            }
        })
        barInfo.range = barInfo.max - barInfo.min;
        barInfo.mesh = new THREE.InstancedMesh(boxGeometry, boxMaterial, earthquakeJSON.length);
        objGroup.add(barInfo.mesh);
        createBars(earthquakeJSON);
        objGroup.scale.set(0.1, 0.1, 0.1)
        mapOptions.sphere.visible = true;// 显示地球
    }

    // 播放动画
    const play = () => {
        const orgCamera = camera.position;
        const orgControl = controls.target;
        const tween = new TWEEN.Tween({
            scale: 0.1,
            rotate: 0,
            cameraX: orgCamera.x,
            cameraY: orgCamera.y,
            cameraZ: orgCamera.z,
            controlsX: orgControl.x,
            controlsY: orgControl.y,
            controlsZ: orgControl.z
        })
            .to({
                scale: 1,
                rotate: Math.PI,
                cameraX: cameraPos.x,
                cameraY: cameraPos.y,
                cameraZ: cameraPos.z,
                controlsX: controlPos.x,
                controlsY: controlPos.y,
                controlsZ: controlPos.z
            }, 2000)
            .easing(TWEEN.Easing.Quadratic.Out)
            .onUpdate((obj) => {
                objGroup.scale.set(obj.scale, obj.scale, obj.scale);
                objGroup.rotation.y = obj.rotate;
                camera.position.set(obj.cameraX, obj.cameraY, obj.cameraZ);
                controls.target.set(obj.controlsX, obj.controlsY, obj.controlsZ);
            })
            .chain(
                new TWEEN.Tween({ h: barInfo.barMin })
                    .to({ h: barInfo.barMax }, 2000)
                    .easing(TWEEN.Easing.Quadratic.Out)
                    .onUpdate((obj) => {
                        barInfo.currentBarH = obj.h;
                        createBars(earthquakeJSON);
                    })
            )
            .start();
        TWEEN.add(tween);
    }

    // 初始化
    const init = () => {
        camera.near = 0.1;// 相机最近距离
        camera.updateProjectionMatrix();// 更新相机投影矩阵
        createMap();
        createAllBars();
        play();
    }
    init();

    /* ------------------------------动画函数--------------------------------- */
    const animation = () => {
        TWEEN.update();
        renderer.render(scene, camera);
        controls.update();
        requestAnimationFrame(animation);
    }
    animation();
}

在引入的地方有使用到一个文件 earthquake.json我也一块放在资源里面了。

最后在vue的onMounted生命周期里面调用就好了。


http://www.kler.cn/a/373174.html

相关文章:

  • 基于Matlab 火焰识别技术
  • scala 权限
  • 【网络面试篇】TCP与UDP类
  • postman的脚本设置接口关联
  • 学习threejs,使用粒子实现下雪特效
  • 智能文档处理平台:免费体验智能化医疗信息提取
  • 如何将 Excel 数据转换为 SQL 脚本:从入门到实战
  • C# 将批量图片转为PDF文件
  • ts:模块导入、导出的简单使用(export、import)
  • 【Vue3】第二篇
  • 2024年“炫转青春”山东省飞盘联赛盛大开赛——临沭县青少年飞盘运动迅速升温
  • 文本分段Chunking综述-RAG
  • 解决:如何在opencv中得到与matlab立体标定一样的矫正图?(python版opencv)
  • 【无人机设计与控制】红嘴蓝鹊优化器RBMO求解无人机路径规划MATLAB
  • R变量索引 - 什么时候使用 @或$
  • webrtc agc2实现原理
  • 高通 Android 12 首次安装去掉下拉弹窗
  • 书生实战营第四期-第三关 Git+InternStudio
  • MATLAB人脸考勤系统
  • ROS2入门学习——ROS1与ROS2区别
  • Unity XR Interaction Toolkit 开发教程(2):导入 SDK【3.0 以上版本】
  • 前缀和算法 | 计算分矩阵的和
  • 安卓开发之数据库的创建与删除
  • Ajax:跨域 JSONP
  • 面向对象的需求分析方法
  • etcd多实例配置