根据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();
}
相关推荐
羊小猪~~1 分钟前
前端入门一之CSS知识详解
前端·javascript·css·vscode·前端框架·html·javas
ReBeX7 分钟前
【GeoJSON在线编辑平台】(1)创建地图+要素绘制+折点编辑+拖拽移动
前端·javascript·gis·openlayers·webgis
阿宝分享技术12 分钟前
从xss到任意文件读取
前端·xss
今天也想MK代码16 分钟前
在Swift开发中简化应用程序发布与权限管理的解决方案——SparkleEasy
前端·javascript·chrome·macos·electron·swiftui
宝子向前冲27 分钟前
纯前端生成PDF(jsPDF)并下载保存或上传到OSS
前端·pdf·html2canvas·oss·jspdf
V+zmm1013433 分钟前
社区养老服务小程序ssm+论文源码调试讲解
java·服务器·前端·javascript·小程序·毕业设计·1024程序员节
卡布叻_星星38 分钟前
同一个页面击穿element样式后,会影响同样组件的使用
前端·vue.js·elementui
猫猫村晨总1 小时前
涉及到行合并的el-table表格导出功能实现
前端·vue.js·element plus
VillanelleS1 小时前
Vue2进阶之Vue3高级用法
前端·javascript·vue.js
天农学子1 小时前
EasyUI弹出框行编辑,通过下拉框实现内容联动
前端·javascript·easyui