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

实现3D热力图

实现思路

  1. 首先是需要用canvas绘制一个2D的热力图,如果你还不会,请看json绘制热力图。
  2. 使用Threejs中的canvas贴图,将贴图贴在PlaneGeometry平面上。
  3. 使用着色器材质,更具json中的数据让平面模型 拔地而起。
  4. 使用Threejs内置的TWEEN,加上一个动画。

效果 

效果如下:

具体代码 

具体实现效果代码:

import * as THREE from 'three';
import * as TWEEN from 'three/examples/jsm/libs/tween.module.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import * as d3geo from 'd3-geo'
import trafficJSON from '../assets/json/traffic.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(-25, 288, 342);
    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);

    /* ------------------------------正题--------------------------------- */
    let geoFun;// 地理投影函数
    let info = {
        max: Number.MIN_SAFE_INTEGER,
        min: Number.MAX_SAFE_INTEGER,
        maxlng: Number.MIN_SAFE_INTEGER,
        minlng: Number.MAX_SAFE_INTEGER,
        maxlat: Number.MIN_SAFE_INTEGER,
        minlat: Number.MAX_SAFE_INTEGER,
        data: []
    };

    // 初始化地理投影
    const initGeo = (size) => {
        geoFun = d3geo.geoMercator().scale(size || 100)
    }

    // 经纬度转像素坐标
    const latlng2px = (pos) => {
        if (pos[0] >= -180 && pos[0] <= 180 && pos[1] >= -90 && pos[1] <= 90) {
            return geoFun(pos);
        }
        return pos;
    };

    // 创建颜色
    const createColors = (option) => {
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        canvas.width = 256;
        canvas.height = 1;
        const grad = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
        for (let k in option.colors) {
            grad.addColorStop(k, option.colors[k]);
        }
        ctx.fillStyle = grad;
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        return ctx.getImageData(0, 0, canvas.width, 1).data;
    }

    // 绘制圆
    const drawCircle = (ctx, option, item) => {
        let { lng, lat, value } = item;
        let x = lng - option.minlng + option.radius;
        let y = lat - option.minlat + option.radius;
        const grad = ctx.createRadialGradient(x, y, 0, x, y, option.radius);
        grad.addColorStop(0.0, 'rgba(0,0,0,1)');
        grad.addColorStop(1.0, 'rgba(0,0,0,0)');
        ctx.fillStyle = grad;
        ctx.beginPath();
        ctx.arc(x, y, option.radius, 0, 2 * Math.PI);
        ctx.closePath();
        ctx.globalAlpha = (value - option.min) / option.size;
        ctx.fill();
    }

    // 创建热力图
    const createHeatmap = (option) => {
        const canvas = document.createElement('canvas');
        canvas.width = option.width;
        canvas.height = option.height;
        const ctx = canvas.getContext('2d');
        option.size = option.max - option.min;
        option.data.forEach((item) => {
            drawCircle(ctx, option, item);
        });
        const colorData = createColors(option);
        const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
        for (let i = 3; i < imageData.data.length; i = i + 4) {
            let opacity = imageData.data[i];
            let offset = opacity * 4;
            //red
            imageData.data[i - 3] = colorData[offset];
            //green
            imageData.data[i - 2] = colorData[offset + 1];
            //blue
            imageData.data[i - 1] = colorData[offset + 2];
        }
        ctx.putImageData(imageData, 0, 0);
        return { canvas, option };
    }

    // 初始化
    const init = () => {
        initGeo(1000)
        // 处理数据
        trafficJSON.features.forEach((item) => {
            let pos = latlng2px(item.geometry.coordinates);// 经纬度转像素坐标
            let newitem = {
                lng: pos[0],
                lat: pos[1],
                value: item.properties.avg
            }
            info.max = Math.max(newitem.value, info.max);
            info.maxlng = Math.max(newitem.lng, info.maxlng);
            info.maxlat = Math.max(newitem.lat, info.maxlat);
            info.min = Math.min(newitem.value, info.min);
            info.minlng = Math.min(newitem.lng, info.minlng);
            info.minlat = Math.min(newitem.lat, info.minlat);
            info.data.push(newitem);
        })
        info.size = info.max - info.min;
        info.sizelng = info.maxlng - info.minlng;
        info.sizelat = info.maxlat - info.minlat;
        const radius = 50;
        const { canvas, option } = createHeatmap({
            width: info.sizelng + radius * 2,
            height: info.sizelng + radius * 2,
            colors: {
                0.1: '#2A85B8',
                0.2: '#16B0A9',
                0.3: '#29CF6F',
                0.4: '#5CE182',
                0.5: '#7DF675',
                0.6: '#FFF100',
                0.7: '#FAA53F',
                1: '#D04343'
            },
            radius,
            ...info
        })
        const map = new THREE.CanvasTexture(canvas);
        map.wrapS = THREE.RepeatWrapping;
        map.wrapT = THREE.RepeatWrapping;
        // 创建平面
        const geometry = new THREE.PlaneGeometry(
            option.width * 0.5,
            option.height * 0.5,
            500,
            500
        );
        const material = new THREE.ShaderMaterial({
            transparent: true,
            side: THREE.DoubleSide,
            uniforms: {
                map: { value: map },
                uHeight: { value: 100 },
                uOpacity: { value: 2.0 }
            },
            vertexShader: `
            uniform sampler2D map;
            uniform float uHeight;
            varying vec2 v_texcoord;
            void main(void)
            {
                v_texcoord = uv;
                float h=texture2D(map, v_texcoord).a*uHeight;
                gl_Position = projectionMatrix * modelViewMatrix * vec4( position.x,position.y,h, 1.0 );
            }`,
            fragmentShader: `
            precision mediump float;
            uniform sampler2D map;
            uniform float uOpacity;
            varying vec2 v_texcoord;
            void main (void)
            {
                vec4 color= texture2D(map, v_texcoord);
                float a=color.a*uOpacity;
                gl_FragColor.rgb =color.rgb;
                gl_FragColor.a=a>1.0?1.0:a;
            }`
        });
        const plane = new THREE.Mesh(geometry, material);
        plane.rotateX(-Math.PI * 0.5);
        scene.add(plane);
        const tween = new TWEEN
            .Tween({ v: 0 })
            .to({ v: 100 }, 2000)
            .onUpdate(obj => {
                material.uniforms.uHeight.value = obj.v;
            })
            .easing(TWEEN.Easing.Quadratic.InOut)
            .start();
        TWEEN.add(tween);
    }
    init();


    /* ------------------------------动画函数--------------------------------- */
    const animation = () => {
        TWEEN.update();
        controls.update();// 如果不调用,就会很卡
        renderer.render(scene, camera);
        requestAnimationFrame(animation);
    }
    animation();
}


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

相关文章:

  • python装饰器的使用以及私有化
  • STM32嵌入式闹钟系统设计与实现
  • BERT配置详解1:构建强大的自然语言处理模型
  • 【Vue】Vue3.0(二十一)Vue 3.0中 的$event使用示例
  • 计算机网络易混淆知识点串记
  • 动态规划 —— 子数组系列-最大子数组和
  • VPN相关学习笔记
  • 企业级工位管理:Spring Boot实践
  • wget命令之Tomcat(三)
  • JVM垃圾回收详解二(重点)
  • 【数据结构】线性表——链表
  • 区块链技术在电子政务中的应用
  • 5 分钟内最多允许用户尝试登录3次,如果错误次数超过限制,需要对该用户进行锁定。如何实现?
  • 《Django 5 By Example》阅读笔记:p1-p16
  • Spark 的容错机制:保障数据处理的稳定性与高效性
  • 「IDE」集成开发环境专栏目录大纲
  • 【c++篇】:掌握list--实用的基本使用方法与模拟实现思路
  • 练习LabVIEW第四十二题
  • [CKS] 关闭API凭据自动挂载
  • 深入浅出rust内存对齐
  • 【机器学习】机器学习中用到的高等数学知识-1.线性代数 (Linear Algebra)
  • 初始JavaEE篇 —— 文件操作与IO
  • Tomcat漏洞利用工具-TomcatScanPro(二)
  • vue3 基于element-plus进行的一个可拖动改变导航与内容区域大小的简单方法
  • web——sqliabs靶场——第三关
  • Redis的使用场景及简单优化