three.js创建3D地球

three.js创建3D地球

1 three-globe

three-globe‌是一个基于‌Three.js‌的开源项目,用于创建三维地球数据可视化。它通过‌WebGL‌技术,使用户能够在浏览器中展示地球上的各种数据,如国家边界、城市位置、温度分布等‌。使用three-globe开源项目可以快速的构建一个3D地球

2 使用示例

1 使用以下版本
"three": "^0.171.0",
"three-globe": "^2.35.3",
npm install three@^0.171.0 three-globe@^2.35.3

2 index.vue

<template>
    <div id="canvasEarth"></div>
    <div id="readerEarth"></div>
</template>

<script setup lang="js">
import * as THREE from "three"
import ThreeGlobe from 'three-globe'
//卫星模型
import {createSatellite} from '@/views/earth/satellite.ts';
//3D地球
import {createGlobe} from '@/views/earth/globe.ts';
//棱锥
import {createCone} from '@/views/earth/cone.ts';
//中国描边
import {createMapStroke} from '@/views/earth/cityLine.ts';
//3D地球模型对象
const Globe = new ThreeGlobe();
//场景对象
const scene = new THREE.Scene();
//设置地球材质透明度
var material=Globe.globeMaterial();
material.transparent = true;  // 启用透明度
material.opacity = 0.8;       // 设置透明度为 0.8
// 设置双面渲染,以避免穿透效果
material.side = THREE.DoubleSide;  // 双面渲染
// 设置 alpha 测试,这样可以避免透明度小于某个值时,材质仍然被渲染
material.alphaTest = 0.1;  // 透明度低于 0.1 时,材质不被渲染
scene.add(Globe);
//渲染地球
createGlobe(Globe,scene);
//渲染卫星
createSatellite( Globe);
//渲染棱锥
createCone( Globe);
//中国描边
 createMapStroke( Globe)


</script>
<style>
canvas { display: block; }

#canvasEarth {
    position: relative;
    width: 100%;
    height: 100%;
    top: -10%;
}
#readerEarth {
    position: absolute;
    top: 0;
    left: 0;
    pointer-events: none; /* Prevent it from blocking interactions */
}
.label-country{
    position: relative;
    left: 30px;
}
</style>

3 satellite.ts 需要一个卫星的3d建模

import { ColladaLoader } from 'three/addons/loaders/ColladaLoader.js';
import * as THREE from "three";
function createSatellite(Globe: any){
    const loader = new ColladaLoader();

    loader.load('/3D/model.dae', function (collada) {
        const satellite = collada.scene;
        satellite.scale.set(0.02,0.02,0.02);
        satellite.position.set(-70,70,70);

        Globe.add(satellite)
        var angle = 0;
        const radius = 130; // 轨道半径
        const path = new THREE.CurvePath();
        const curve = new THREE.EllipseCurve(0, 0, radius, radius, 0, Math.PI * 2);
        path.add(curve);
        (function animate() {
            if (satellite) {
                angle += 0.001; // 每一帧增加角度
                const position = path.getPointAt((angle % 1)); // 获取路径上的点
                satellite.position.set(position.x, position.y, 0); // 更新卫星位置
            }
            requestAnimationFrame(animate);
        })();
    });
}
export {createSatellite};

4 globe.ts

import marble from "@/assets/img/earth-night.jpg";
import * as THREE from "three";
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
import {CSS2DRenderer} from 'three/addons/renderers/CSS2DRenderer.js';
import {setWordData} from './data.ts';

function createGlobe(Globe: any, scene: THREE.Scene) {

    //设置3D地球 线,光点,光圈,文字
    setWordData(Globe);
    Globe.globeImageUrl(marble)
        .showAtmosphere(true) // 是否显示围绕地球的明亮光晕,代表大气层
        .atmosphereColor('rgba(94,165,196,0.24)') // 大气颜色
        .atmosphereAltitude(0.4) // 表示大气最大高
        .arcAltitudeAutoScale(0.6)
        .arcCurveResolution(128)
        .arcColor('color') // 线条颜色的 Arc 对象访问器函数或属性
        .arcDashLength('dashLength') // 用于表示圆弧中虚线段的长度
        .arcDashGap('dashGap') // 用于表示短划线段之间的间隙长度
        .arcDashInitialGap('dashInitialGap') // 初始间隙长度
        .arcStroke('stroke')
        .arcDashAnimateTime('dashAnimateTime') // 用于对整行长度从起点到终点置的运动进行动画处理。
        .arcsTransitionDuration(0); // 过渡持续时间
    // 环境光
    Globe.add(new THREE.AmbientLight(0xffffff, 10));
    // 设置相机
    const camera = new THREE.PerspectiveCamera();

    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    camera.position.z = 450;
    //渲染器
    const renderer = new THREE.WebGLRenderer({
        alpha: true, antialias: true
    });
    renderer.setSize(window.innerWidth, window.innerHeight);
    const canvasContainer = document.getElementById('canvasEarth') as HTMLElement;
    canvasContainer.appendChild(renderer.domElement);
    var css2DRenderer = new CSS2DRenderer();
    css2DRenderer.setSize(window.innerWidth, window.innerHeight);
    css2DRenderer.domElement.style.position = 'absolute';  // 保持绝对定位
    css2DRenderer.domElement.style.top = '0px';
    css2DRenderer.domElement.style.zIndex = '1'; // 确保它不遮挡 WebGL 渲染器
    css2DRenderer.domElement.style.pointerEvents = 'none'; // 禁止捕获鼠标事件
    canvasContainer.appendChild(css2DRenderer.domElement);


    //  轨道偏移器
    const tbControls = new OrbitControls(camera, renderer.domElement);
    tbControls.enableZoom = false;
    tbControls.update();

    (function animate() {
        requestAnimationFrame(animate);
        Globe.rotateY(0.001);
        tbControls.update();
        renderer.render(scene, camera);
        css2DRenderer.render(scene, camera);
    })();
    // 监听窗口尺寸变化
    window.addEventListener('resize', () => {
        // 更新渲染器大小
        renderer.setSize(window.innerWidth, window.innerHeight);
        css2DRenderer.setSize(window.innerWidth, window.innerHeight);
        // 更新摄像机的纵横比
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();  // 更新投影矩阵
    });

}

export {createGlobe}

5 data.ts

import {getRandomItems} from "@/views/util/randomItems.ts";
import mygeo from "@/assets/json/mygeo.json";
import * as THREE from "three";
import aperture from "@/assets/img/aperture.png";
import pointJPG from "@/assets/img/point.png";
import {createTag} from "@/views/earth/tag.ts";
const apertureGroup = new THREE.Group();
const pointGroup = new THREE.Group()
let labelObject = new THREE.Group()
let arcsData: any[] = [];
let arcsDataCopy: any[] = [];
const mashNormal = new THREE.Vector3(0, 0, 1);
function setWordData(Globe:any){
    setInterval(async () => {
        arcsData = [];
        arcsDataCopy = [];
        pointGroup.clear();
        apertureGroup.clear();
        labelObject.clear();
        // const elements = document.querySelectorAll(".label-country");
        // elements.forEach(element => {
        //     element.remove()
        // });
        const coordItem = getRandomItems(mygeo, 10);
        coordItem.forEach((arc: any) => {
            arcsData.push({
                startLat: arc.coordinate[1],
                startLng: arc.coordinate[0],
                endLat: 28.45892581576906,
                endLng: 116.539649563166506,
                size: Math.random() * 1,
                dashLength: 1,
                stroke: 0.5,
                dashAnimateTime: 1,
                dashInitialGap: 1,
                dashGap: 0,
                r: 1,
                country: arc.country,
                op: "-",
                city: arc.city,
                color: 'rgba(16,220,228,0.5)'
            })
        })
        arcsData.push({
            startLat: 28.45892581576906,
            startLng: 116.539649563166506,
            endLat: 28.45892581576906,
            endLng: 116.539649563166506,
            size: Math.random() * 1,
            dashLength: 1,
            stroke: 0.5,
            dashAnimateTime: 1,
            dashInitialGap: 1,
            dashGap: 0,
            r: 1.15,
            country: '中国',
            op: "-",
            city: '南昌',
            color: '#5fd0da'
        })
        arcsDataCopy = arcsData
        arcsData.forEach(arc => {
            arcsDataCopy.push({
                startLat: arc.startLat,
                startLng: arc.startLng,
                endLat: 28.45892581576906,
                endLng: 116.539649563166506,
                size: Math.random() * 1,
                dashLength: 0.02,
                stroke: 1.5,
                dashAnimateTime: Math.floor(Math.random() * (2500 - 1500 + 1)) + 1500,
                dashInitialGap: 1.3,
                dashGap: 1,
                color: 'rgba(243,158,0,0.8)'
            })
            // 添加光圈
            const geometry = new THREE.PlaneGeometry(12, 12);
            const texture = new THREE.TextureLoader().load(aperture);
            const apertureMaterial = new THREE.MeshBasicMaterial({
                color: 0xffffff,
                map: texture,
                transparent: true,
                aoMapIntensity: 0,
                side: THREE.DoubleSide
            })
            const apertureMesh = new THREE.Mesh(geometry, apertureMaterial)
            const apertureCoords = Globe.getCoords(arc.startLat, arc.startLng);
            const apertureVector3 = new THREE.Vector3(apertureCoords.x, apertureCoords.y, apertureCoords.z).normalize()
            apertureMesh.position.set(apertureCoords.x * arc.r, apertureCoords.y * arc.r, apertureCoords.z * arc.r);
            apertureMesh.quaternion.setFromUnitVectors(mashNormal, apertureVector3)
            apertureGroup.add(apertureMesh);

            //添加光点
            const pointGeometry = new THREE.PlaneGeometry(4, 4);
            const pointTexture = new THREE.TextureLoader().load(pointJPG);
            const pointMaterial = new THREE.MeshBasicMaterial({
                color: 0xd2cccc,
                map: pointTexture,
                transparent: true
            })
            const pointMesh = new THREE.Mesh(pointGeometry, pointMaterial)
            pointMesh.position.set(apertureCoords.x * 1.01, apertureCoords.y * 1.01, apertureCoords.z * 1.01);
            pointMesh.quaternion.setFromUnitVectors(mashNormal, apertureVector3)
            pointGroup.add(pointMesh);
            var _s = Math.random();
            (function aperturAnimate() {
                _s += 0.01;
                apertureMesh.scale.set(_s, _s, _s)
                if (_s <= 1.7) {
                    apertureMesh.material.opacity = (_s - 1.0) / (1.7 - 1.0)
                } else if (_s > 1.7 && _s <= 2.5) {
                    apertureMesh.material.opacity = 1 - (_s - 1.7) / (2.5 - 1.7)
                } else {
                    _s = 1.0
                }
                requestAnimationFrame(aperturAnimate);
            })();
        })

        labelObject = createTag(Globe, arcsData);
        Globe.add(labelObject)
        Globe.add(apertureGroup)
        Globe.add(pointGroup)
        Globe.arcsData(arcsDataCopy) // 弧映射图层中列表
    }, 8000)
}
export {setWordData}

6 tag.ts

import { CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
import * as THREE from "three";

function createTag(Globe: any,arcsData:any){
    const labelGroup = new THREE.Group()

    arcsData.forEach((country: any)=>{
        const Gt = Globe.getCoords(country.startLat, country.startLng);
        var label = document.createElement('div');
        label.className = 'label-country';
        label.textContent = country.country;
        label.style.color = 'rgb(237,236,236)';
        label.style.fontSize = '0.8vw';
        label.style.opacity = '0.7';
        label.style.backgroundColor = 'rgba(42,142,196,0.53)';
        label.style.padding = '0.3vw 0.6vw';
        label.style.borderRadius = '0.2vw';
        label.style.pointerEvents ='none';//避免HTML标签遮挡三维场景的鼠标事件//·设置HTML元素标签在three.js世界坐标中位置
        var labelObject = new CSS2DObject(label);
        labelObject.position.set(Gt.x,Gt.y,Gt.z);
        labelGroup.add(labelObject)
    })

    return labelGroup;
}
export {createTag}

7 cone.ts

import * as THREE from "three"


function createCone(Globe: any){
    const Gt = Globe.getCoords(28.45892581576906, 116.539649563166506);
    const Vector3 = new THREE.Vector3(Gt.x, Gt.y, Gt.z).normalize()
    const mashNormal = new THREE.Vector3(0, 0, 1);
    // 设置棱锥
    const height = 15
    const coneGeometry = new THREE.ConeGeometry(4, height, 4);
    coneGeometry.rotateX(-Math.PI / 2)
    coneGeometry.translate(0, 0, height / 2)
    const coneMaterial = new THREE.MeshLambertMaterial({color: 0x10dce4});
    const coneMesh = new THREE.Mesh(coneGeometry, coneMaterial);
    const coneMesh2 = coneMesh.clone()
    coneMesh2.scale.z = 0.5
    coneMesh2.position.z = height * (1 + coneMesh2.scale.z)
    coneMesh2.rotateX(Math.PI)
    coneMesh.add(coneMesh2)
    coneMesh.position.set(Gt.x, Gt.y, Gt.z);
    coneMesh.quaternion.setFromUnitVectors(mashNormal, Vector3)
    Globe.add(coneMesh);
    var _r = 1.06;
    (function animate() {
        _r -= 0.001;
        if (_r <= 1.0) {
            _r = 1.06;
        }
        coneMesh.rotateZ(0.05);
        coneMesh.position.set(Gt.x * _r, Gt.y * _r, Gt.z * _r);
        requestAnimationFrame(animate);
    })();
}
export {createCone}

8 cityLine.ts

import * as THREE from "three"
import chinaInfoJson from '../china/json/china.json'



export const createMapStroke = (Globe:any) => {
    const cityStroke = new THREE.Object3D();
    cityStroke.name = "cityStroke";

    const lineMaterial = new  THREE.LineBasicMaterial({
        color: 0xb9995b,
        opacity: 0.5,    // 初始透明度
        transparent: true,  // 透明度控制
        side: THREE.DoubleSide
    });

    chinaInfoJson.features.forEach((elem: any) => {
        const provinceLine = new  THREE.Group();
        provinceLine.name = elem.properties.name;
        const coordinates = elem.geometry.coordinates;
        coordinates.forEach((multiPolygon: any) => {
            multiPolygon.forEach((polygon: any) => {
                const line = createCityLine(polygon, lineMaterial,Globe);
                provinceLine.add(line);
            });
        });
        cityStroke.add(provinceLine);
    })
    Globe.add(cityStroke);
}

/**
 * 球面画线
 * @param polygon
 * @param lineMaterial
 */
export const createCityLine = (polygon: any, lineMaterial:  THREE.LineBasicMaterial,Globe:any) => {
    const positions = [];
    const linGeometry = new  THREE.BufferGeometry();
    for (let i = 0; i < polygon.length; i++) {
        let pos = Globe.getCoords( polygon[i][1], polygon[i][0]);
        positions.push(pos.x, pos.y, pos.z);
    }
    linGeometry.setAttribute(
        "position",
        new THREE.Float32BufferAttribute(positions, 3)
    );

    return new THREE.Line(linGeometry, lineMaterial)
}

9 randomItems.ts 随机获取 number 条数据

function getRandomItems(arr:any, count:number) {
    const shuffled = arr.slice(); // 复制数组
    let i = arr.length, randomIndex, temp;

    // 洗牌算法(Fisher-Yates shuffle)
    while (i--) {
        randomIndex = Math.floor(Math.random() * (i + 1)); // 生成随机索引
        temp = shuffled[i];
        shuffled[i] = shuffled[randomIndex];
        shuffled[randomIndex] = temp;
    }

    // 返回前 count 条数据
    return shuffled.slice(0, count);
}
export {getRandomItems}

静态资源

GeoJSON 格式的全球,全国地理数据,这个可以去网上找

地球贴图
在这里插入图片描述
圆环
在这里插入图片描述

在这里插入图片描述
mygeo.json

[
  {
    "country": "科特迪瓦",
    "city": "阿比让",
    "coordinate": [
      4.016666666666667,
      5.316666666666666
    ]
  },
  ......  //这里填写更多的地区坐标信息
    {
    "country": "瑞士",
    "city": "苏黎世",
    "coordinate": [
      8.533333333333333,
      47.36666666666667
    ]
  }
]

在这里插入图片描述

### 创建带有描边效果的3D地球模型 为了实现带有描边效果的3D地球模型,可以采用两种主要方法之一:一种是在几何体周围创建一个稍微放大并应用纯色材质的副本作为轮廓;另一种则是利用着色器来模拟描边效果。这里提供基于第二种方案的一个简单实例。 #### 方法一:使用两个同心球体模拟描边 这种方法涉及创建两个SphereGeometry对象,其中较大的那个用于呈现描边部分。通过调整内外层的颜色和透明度设置,可以获得良好的视觉效果。 ```javascript // 导入three.js库 import * from 'three'; function createEarthWithOutline(radius, outlineThickness) { const scene = new THREE.Scene(); // 主地球 let earthMaterial = new THREE.MeshPhongMaterial({ color: 0x2b6cb0 }); let earthGeometry = new THREE.SphereBufferGeometry(radius, 32, 32); let earthMesh = new THREE.Mesh(earthGeometry, earthMaterial); // 描边(外部更大的半径) let outlineRadius = radius + outlineThickness; let outlineMaterial = new THREE.MeshBasicMaterial({ color: 0x000000, side: THREE.BackSide }); let outlineGeometry = new THREE.SphereBufferGeometry(outlineRadius, 32, 32); let outlineMesh = new THREE.Mesh(outlineGeometry, outlineMaterial); // 将两者加入场景中 scene.add(outlineMesh); // 先添加外框以确保渲染顺序正确 scene.add(earthMesh); return {scene, earthMesh}; } ``` 上述代码片段展示了如何构建一个基础版本的带描边地球[^1]。请注意,在实际应用场景下可能还需要考虑光照条件等因素的影响,并相应地调整材料属性。 #### 方法二:运用ShaderMaterial实现实时描边 对于更复杂的交互需求或是追求更高的效率来说,直接操作GPU可能是更好的选择。这可以通过编写自定义GLSL着色器并在Three.js使用`THREE.ShaderMaterial`类来达成目的。 下面是一个简化版的例子: ```javascript const vertexShaderSource = ` varying vec3 vNormal; void main() { vNormal = normalize(normalMatrix * normal); gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0 ); }`; const fragmentShaderSource = ` uniform float uEdgeWidth; // 边缘宽度参数 uniform sampler2D textureSampler; // 纹理采样器 varying vec3 vNormal; float edgeFactor(vec3 n){ return smoothstep(-uEdgeWidth, uEdgeWidth, dot(n,-vec3(0.,0.,1.))); } void main(){ vec4 texColor = texture2D(textureSampler, uv); if(texColor.a<0.5) discard; float factor=edgeFactor(vNormal)*texColor.r*0.8+0.2*texColor.g; gl_FragColor=vec4(mix(vec3(0.),vec3(1.),factor),1.); }`; let shaderMaterial = new THREE.ShaderMaterial({ uniforms:{ "uEdgeWidth":{value:.01}, "textureSampler":{ value:new THREE.TextureLoader().load('path/to/your/image.jpg')} }, vertexShader:vertexShaderSource, fragmentShader:fragmentShaderSource }); let geometry=new THREE.SphereBufferGeometry(1,32,32); let mesh=new THREE.Mesh(geometry,shaderMaterial); scene.add(mesh); ``` 这段脚本实现了更加灵活可控的效果,允许动态调节边缘厚度和其他特性[^2]。不过需要注意的是,这种方式通常会增加一定的编程复杂性和硬件资源消耗。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值