实现3D热力图

实现思路

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

效果

效果如下:

具体代码

具体实现效果代码:

javascript 复制代码
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();
}
相关推荐
摇光93几秒前
promise
前端·面试·promise
hummhumm11 分钟前
第 12 章 - Go语言 方法
java·开发语言·javascript·后端·python·sql·golang
hummhumm12 分钟前
第 8 章 - Go语言 数组与切片
java·开发语言·javascript·python·sql·golang·database
麻花201324 分钟前
WPF学习之路,控件的只读、是否可以、是否可见属性控制
服务器·前端·学习
.54824 分钟前
提取双栏pdf的文字时 输出文件顺序混乱
前端·pdf
jyl_sh32 分钟前
WebKit(适用2024年11月份版本)
前端·浏览器·客户端·webkit
狼叔1 小时前
前端潮流KK:科技达人与多面手,如何找到自己的乐趣?-浪说回顾
前端
zhanghaisong_20151 小时前
Caused by: org.attoparser.ParseException:
前端·javascript·html·thymeleaf
Eric_见嘉1 小时前
真的能无限试(白)用(嫖)cursor 吗?
前端·visual studio code
DK七七2 小时前
多端校园圈子论坛小程序,多个学校同时代理,校园小程序分展示后台管理源码
开发语言·前端·微信小程序·小程序·php