根据JSON绘制3D地区

效果如下:

需要掌握的知识点:

  • threejs 的做色器(实现流动光波)
  • threejs 的后期处理(实现bloom泛光效果)
  • threejs 的CatmullRomCurve3 曲线创建(实现地区边缘)
  • threejs 的Shape和ExtrudeGeometry(实现3d区块地图)
  • 第三方库 d3-geo (使用这个库将金纬度转化为平面坐标)

具体实现代码如下:

javascript 复制代码
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
import * as d3geo from 'd3-geo'
import guangzhouJSON from '../assets/json/guangzhou.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 bloomOptions = {
        threshold: 0,// 亮度阈值
        strength: 0.5,// 光晕强度
        radius: 0.5,// 光晕半径
        exposure: 0.5// 曝光
    }

    // 相机控制器配置
    const cameraControl = {
        autoCamera: true,// 是否自动旋转
        height: 10,// 相机高度
        width: 0.5,// 相机宽度
        depth: 1,// 相机深度
        cameraPosX: 10,// 相机位置x
        cameraPosY: 181,// 相机位置y
        cameraPosZ: 116,// 相机位置z
        autoRotate: false,// 是否自动旋转
        rotateSpeed: 2000// 旋转速度
    }

    let geoFun;// 地理投影函数
    let map = null; // 地图
    let guangzhouData = [];// 地图数据
    const group = new THREE.Group();
    const composer = new EffectComposer(renderer);// 效果合成器
    let time = 1;// 时间
    const edgeMaterial = new THREE.ShaderMaterial({
        uniforms: {
            time: { value: 0.0 },// 当前时间
            len: { value: 0.05 },// 粒子在时间轴上活动的长度
            size: { value: 0.02 },// 粒子的大小
            color1: { value: new THREE.Color('#FFFFFF') },// 定义边框色
            color2: { value: new THREE.Color('yellow') }// 定义移动色
        },
        // 顶点着色器
        vertexShader: `
        uniform float time;
        uniform float size;
        uniform float len;
        uniform vec3 color1;
        uniform vec3 color2;
        varying vec3 vColor; 
        void main() {
            vColor = color1;// 设置初始颜色
            vec3 newPosition = position;// 新的位置初始化为当前顶点的位置
            float d = uv.x - time;// 当前纹理坐标的x值与时间值的差值
            if(abs(d) < len) {
                // 粒子接近时间轴
                newPosition = newPosition + normal * size;// 根据法线方向和粒子大小调整位置,模拟粒子在表面上的移动
                vColor = color2;// 设置颜色为移动色
            }
            gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
        }`,
        // 片段着色器
        fragmentShader: `
        varying vec3 vColor; 
        void main() {
            gl_FragColor =vec4(vColor, 1.0);
        }`,
    })

    // 初始化地理投影
    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 processData = () => {
        guangzhouData = guangzhouJSON.features[0].geometry.coordinates[0][0];
        // 数据 从 经纬度-> 像素坐标-> 三维坐标
        guangzhouData = guangzhouData.map(item => {
            const two = latlng2px(item);
            const three = new THREE.Vector3(two[0], 0, two[1]);
            return three;
        })
    }

    // 创建地图块
    const createMap = () => {
        const shape = new THREE.Shape();
        shape.moveTo(guangzhouData[0].x, guangzhouData[0].z);// 移动到第一个点
        for (let i = 1; i < guangzhouData.length; i++) {
            shape.lineTo(guangzhouData[i].x, guangzhouData[i].z);// 连线
        }
        shape.lineTo(guangzhouData[0].x, guangzhouData[0].z);// 闭合
        const geometry = new THREE.ExtrudeGeometry(shape, {
            depth: 0.2,// 区块厚度
            bevelEnabled: false// 是否使用倒角
        });

        const img = new URL('../assets/images/tex.png', import.meta.url).href;
        const text = new THREE.TextureLoader().load(img);
        text.wrapS = THREE.RepeatWrapping;// 水平方向
        text.wrapT = THREE.RepeatWrapping;// 垂直方向
        const material = new THREE.MeshBasicMaterial({
            map: text,
            color: new THREE.Color('#00FFFF')
        })
        map = new THREE.Mesh(geometry, material);
        map.rotateX(Math.PI / 2);// 旋转90度
        group.add(map);
    }

    // 创建边缘线
    const createEdge = () => {
        // 创建曲线(数据, 是否闭合, 曲线类型, 张力)
        const curve = new THREE.CatmullRomCurve3(guangzhouData, true, 'catmullrom', 0);
        // 创建管状几何体(曲线, 管道半径, 管道分段, 管道圆周分段, 是否闭合)
        const geometry = new THREE.TubeGeometry(curve, Math.round(guangzhouData.length * 0.5), 0.01, 8, true);
        // const edgeMaterial = new THREE.MeshBasicMaterial({ color: 'white' });// 如果只需要白色边缘线,可以直接使用这个材质
        const mesh = new THREE.Mesh(geometry, edgeMaterial);
        group.add(mesh);
    }

    // 设置模型的中心点
    const setModeCenter = (object, viewControl) => {
        // 如果对象不存在,则返回   
        if (!object) {
            return;
        }
        if (object.updateMatrixWorld) {
            object.updateMatrixWorld();// 更新模型矩阵
        }
        // 获得包围盒得min和max
        let box = new THREE.Box3().setFromObject(object);

        let objSize;// 获取包围盒的尺寸
        try {
            objSize = box.getSize();
        } catch (error) {
            objSize = new THREE.Vector3(
                Math.abs(box.max.x - box.min.x),
                Math.abs(box.max.y - box.min.y),
                Math.abs(box.max.z - box.min.z)
            );
        }

        // 返回包围盒的中心点
        const center = box.getCenter(new THREE.Vector3());
        object.position.x += object.position.x - center.x;
        object.position.y += object.position.y - center.y;
        object.position.z += object.position.z - center.z;

        let width = objSize.x;
        let height = objSize.y;
        let depth = objSize.z;

        // 设置相机位置
        let centroid = new THREE.Vector3().copy(objSize);
        centroid.multiplyScalar(0.5);
        if (viewControl.autoCamera) {
            camera.position.x =
                centroid.x * (viewControl.centerX || 0) + width * (viewControl.width || 0);
            camera.position.y =
                centroid.y * (viewControl.centerY || 0) + height * (viewControl.height || 0);
            camera.position.z =
                centroid.z * (viewControl.centerZ || 0) + depth * (viewControl.depth || 0);
        } else {
            camera.position.set(
                viewControl.cameraPosX || 0,
                viewControl.cameraPosY || 0,
                viewControl.cameraPosZ || 0
            );
        }
        camera.lookAt(0, 0, 0);
    }

    // 添加Bloom发光效果
    const addBloom = () => {
        // 创建一个渲染器通道
        const renderPass = new RenderPass(scene, camera, null, new THREE.Color('rgba(0,0,255,0.0)'), 0);
        composer.addPass(renderPass);// 添加渲染器通道

        // 创建一个Bloom发光效果通道 (泛光所覆盖的场景大小, 泛光强度, 泛光模糊度, 泛光衰减)
        const bloomPass = new UnrealBloomPass(new THREE.Vector2(innerWidth, innerHeight), 1.5, 0.4, 0.85);
        bloomPass.threshold = bloomOptions.threshold;// 发光阈值
        bloomPass.strength = bloomOptions.strength;// 发光强度
        bloomPass.radius = bloomOptions.radius;// 发光模糊度
        bloomPass.copyUniforms.opacity.value = 1;// 发光衰减
        composer.addPass(bloomPass);// 添加发光效果通道

        const outputPass = new OutputPass(); // 创建一个输出通道
        composer.addPass(outputPass);// 添加输出通道
    }

    const animationRender = () => {
        renderer.setViewport(0, 0, innerWidth, innerHeight);// 设置渲染器视口大小
        renderer.autoClear = false;// 关闭自动清除
        renderer.clear();// 清除渲染器
        map.visible = false;
        composer.render();// 渲染后期效果
        renderer.clearDepth();// 清除深度缓存
        map.visible = true;
        renderer.render(scene, camera);// 渲染场景
    }

    // 初始化
    const init = () => {
        initGeo(180)// 初始化地理投影
        processData()// 处理地图数据
        createMap()// 创建地图块
        createEdge()// 创建边缘线
        scene.add(group);// 添加到场景中
        setModeCenter(group, cameraControl)// 设置模型的中心点
        addBloom()// 添加Bloom发光效果
    }
    init();

    /* ------------------------------动画函数--------------------------------- */
    const animation = () => {
        if (edgeMaterial) {
            if (time >= 1.0) {
                time = 0.0;
            }
            time += 0.005;
            edgeMaterial.uniforms.time.value = time;
        }
        controls.update();// 如果不调用,就会很卡
        animationRender()
        // renderer.render(scene, camera);
        requestAnimationFrame(animation);
    }
    animation();
}
相关推荐
荔枝味啊~10 分钟前
相机位姿估计
人工智能·计算机视觉·3d
Boilermaker199241 分钟前
【Java EE】SpringIoC
前端·数据库·spring
中微子1 小时前
JavaScript 防抖与节流:从原理到实践的完整指南
前端·javascript
天天向上10241 小时前
Vue 配置打包后可编辑的变量
前端·javascript·vue.js
芬兰y1 小时前
VUE 带有搜索功能的穿梭框(简单demo)
前端·javascript·vue.js
好果不榨汁1 小时前
qiankun 路由选择不同模式如何书写不同的配置
前端·vue.js
小蜜蜂dry2 小时前
Fetch 笔记
前端·javascript
拾光拾趣录2 小时前
列表分页中的快速翻页竞态问题
前端·javascript
小old弟2 小时前
vue3,你看setup设计详解,也是个人才
前端
Lefan2 小时前
一文了解什么是Dart
前端·flutter·dart