Three.js - 实现一个3D地球可视化

3D地球可视化效果
Three.js - 实现一个3D地球可视化_第1张图片
3D地球的开发并不复杂,对球形物体进行贴图操作,完成球体自转和月球公转,太阳场景设置等即可

上代码

<template>
  <div class="earth_page">
    <div v-if="loadingProcess !== 100" class='loading'>
      <span class='progress'>{{loadingProcess}} %</span>
    </div>
    <div class="scene" id="viewer-container"></div>
  </div>
</template>

<script setup>
import { onBeforeUnmount, onMounted, nextTick, ref } from "vue"
import modules from "./modules/index.js";
import Animations from './utils/animations';
import * as THREE from "three";
import { TWEEN } from 'three/examples/jsm/libs/tween.module.min.js'; // tween 动画效果渲染 效果同 gsap
import { Lensflare, LensflareElement } from 'three/examples/jsm/objects/Lensflare.js';

let loadingProcess = ref(100) // loading加载数据 0 25 50 75 100
let sceneReady = false // 场景加载完毕标志,程序进行label展示,镜头拉进等效果

let viewer = null // 基础类,包含场景、相机、控制器等实例
let tiemen = null // 水面动画 函数
let allTiemen = null // 全局动画 函数

const sizes = { // 存储全局宽度 高度
  width: window.innerWidth,
  height: window.innerHeight
}

const lensflareTexture0 = 'images/lensflare0.png' // 太阳光贴图
const lensflareTexture1 = 'images/lensflare1.png' // 黑色描边贴图
const earth = new THREE.Object3D(); // 地球存储
// 地球3D层
// const earthObject = new THREE.Object3D()
// 地球半径
const globeRadius = 10

// 初始化three场景
const init = () => {
  viewer = new modules.Viewer('viewer-container') //初始化场景
  // 调整相机位置(相机位置在初始化的时候设置过一次,这里对其进行调整)
  viewer.camera.position.set(0, 600, 1600)
  // 限制controls的上下角度范围 (OrbitControls的范围)
  viewer.controls.maxPolarAngle = Math.PI / 2.1;

  // 增加灯光(初始化viewer的时候,对灯光也做了初始,这里进行灯光调整)
  let { lights } = viewer

  // 环境光会均匀的照亮场景中的所有物体。 环境光不能用来投射阴影,因为它没有方向。
  let ambientLight = lights.addAmbientLight() 
  ambientLight.setOption({color: 0xffffff, intensity: 0.8}) // 调用灯光内置方法,设置新的属性
  
  // 平行光是沿着特定方向发射的光。这种光的表现像是无限远,从它发出的光线都是平行的。常常用平行光来模拟太阳光的效果。 太阳足够远,因此我们可以认为太阳的位置是无限远,所以我们认为从太阳发出的光线也都是平行的。
  lights.addDirectionalLight([-1, 1.75, 1], { // 增加直射灯光方法 
    color: 'rgb(255,234,229)',
    // intensity: 3, // intensity属性是用来设置聚光灯的强度,默认值是1,如果设置成0那什么也看不到,该值越大,点光源看起来越亮
    // castShadow: true, // castShadow属性是用来控制光源是否产生阴影,取值为true或false
  })
  
  // 从一个点向各个方向发射的光源。一个常见的例子是模拟一个灯泡发出的光。
  const pointLight = lights.addPointLight([0, 45, -2000], { // 增加直射灯光方法 
    color: 'rgb(253,153,253)'
  })

  // 模拟太阳光效果
  const textureLoader = new THREE.TextureLoader(); // 加载texture的一个类。 内部使用ImageLoader来加载文件。
  const textureFlare0 = textureLoader.load(lensflareTexture0); // 加载太阳光 贴图
  const textureFlare1 = textureLoader.load(lensflareTexture1); // 加载黑色贴图
  // 镜头光晕
  const lensflare = new Lensflare(); // 创建一个模拟追踪着灯光的镜头光晕。 Lensflare can only be used when setting the alpha context parameter of WebGLRenderer to true.
  lensflare.addElement(new LensflareElement( textureFlare0, 600, 0, pointLight.color));
  // LensflareElement( texture : Texture, size : Float, distance : Float, color : Color )
  // texture - 用于光晕的THREE.Texture(贴图)
  // size - (可选)光晕尺寸(单位为像素)
  // distance - (可选)和光源的距离值在0到1之间(值为0时在光源的位置)
  // color - (可选)光晕的(Color)颜色
  lensflare.addElement(new LensflareElement( textureFlare1, 60, .6));
  lensflare.addElement(new LensflareElement( textureFlare1, 70, .7));
  lensflare.addElement(new LensflareElement( textureFlare1, 120, .9));
  lensflare.addElement(new LensflareElement( textureFlare1, 70, 1));
  pointLight.add(lensflare);

  // 地球
  const textLoader = new THREE.TextureLoader();
  const planet = new THREE.Mesh(new THREE.SphereGeometry(globeRadius, 64, 64), new THREE.MeshStandardMaterial({
    map: textLoader.load('images/earth_basic.jpeg'),
    normalMap: textLoader.load('images/earth_normal.jpeg'),
    roughnessMap: textLoader.load('images/earth_rough.jpeg'),
    normalScale: new THREE.Vector2(10, 10),
    metalness: .1
  }));
  planet.rotation.y = -Math.PI;
  
  // 云层
  const atmosphere = new THREE.Mesh(new THREE.SphereGeometry(globeRadius + 0.6, 64, 64), new THREE.MeshLambertMaterial({
    alphaMap: textLoader.load('images/clouds.jpeg'),
    transparent: true,
    opacity: .4,
    depthTest: true
  }))
  earth.add(planet);
  earth.add(atmosphere);
  earth.scale.set(6, 6, 6)
  earth.rotation.set(0.5, 2.9, 0.1)
  viewer.scene.add(earth);

  // 月亮
  const moon = new THREE.Mesh(new THREE.SphereGeometry(2, 32, 32), new THREE.MeshStandardMaterial({
    map: textLoader.load('images/moon_basic.jpeg'),
    normalMap: textLoader.load('images/moon_normal.jpeg'),
    roughnessMap: textLoader.load('images/moon_roughness.jpeg'),
    normalScale: new THREE.Vector2(10, 10),
    metalness: .1
  }));
  moon.position.set(-120, 0, -120);
  moon.scale.set(6, 6, 6);
  viewer.scene.add(moon);

  // Animations.animateCamera 利用tweenjs 完成的镜头切换动画工具函数,分别传入相机,控制器,相机最终位置,指向控制器位置,动作时间
  Animations.animateCamera(viewer.camera, viewer.controls, { x: 100, y: 20, z: 200 }, { x: 0, y: 0, z: 0 }, 4000, () => {
    sceneReady = true
  });

  /**
  * 创建canvas方形纹理,取代图片纹理,利用代码形式创建
  * */
  function generateSprite() {
    const canvas = document.createElement('canvas')
    canvas.width = 16
    canvas.height = 16

    const context = canvas.getContext('2d')
    // 创建颜色渐变
    const gradient = context.createRadialGradient(
      canvas.width / 2,
      canvas.height / 2,
      0,
      canvas.width / 2,
      canvas.height / 2,
      canvas.width / 2
    )
    gradient.addColorStop(0, 'rgba(255,255,255,1)')
    gradient.addColorStop(0.2, 'rgba(0,255,255,1)')
    gradient.addColorStop(0.4, 'rgba(0,0,64,1)')
    gradient.addColorStop(1, 'rgba(0,0,0,1)')

    // 绘制方形
    context.fillStyle = gradient
    context.fillRect(0, 0, canvas.width, canvas.height)
    // 转为纹理
    const texture = new THREE.Texture(canvas)
    texture.needsUpdate = true
    return texture
  }

  const positions = []
  const colors = []
  // 创建 几何体
  const geometry = new THREE.SphereGeometry()
  for (let i = 0; i < 10000; i++) {
    let vertex = new THREE.Vector3()
    vertex.x = Math.random() * 2 - 1
    vertex.y = Math.random() * 2 - 1
    vertex.z = Math.random() * 2 - 1
    positions.push(vertex.x, vertex.y, vertex.z)
    // const color = new THREE.Color();
    // color.setHSL( Math.random() * 0.2 + 0.5, 0.55, Math.random() * 0.25 + 0.55 );
    // colors.push( color.r, color.g, color.b );
  }
  // 对几何体 设置 坐标 和 颜色
  geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
  // geometry.setAttribute( 'color', new THREE.Float32BufferAttribute( colors, 3 ) );
  // 默认球体
  geometry.computeBoundingSphere()

  // ------------- 1 ----------
  // 星星资源图片
  // PointsMaterial 点基础材质
  const starsMaterial = new THREE.PointsMaterial({
    map: generateSprite(),
    size: 1,
    transparent: true,
    opacity: 1,
    //true:且该几何体的colors属性有值,则该粒子会舍弃第一个属性--color,而应用该几何体的colors属性的颜色
    // vertexColors: true,
    blending: THREE.AdditiveBlending,
    sizeAttenuation: true
  })
  // 粒子系统 网格
  let stars = new THREE.Points(geometry, starsMaterial)
  stars.scale.set(600, 600, 600)
  // viewer.scene.add(stars)

  const groupHalo = new THREE.Group();
  // 地球光圈
  // const geometryCircle = new THREE.PlaneGeometry( 200, 200 );
  // const materialCircle = new THREE.MeshLambertMaterial( {
  //   map: textureFlare0, 
  //   transparent: true,
  //   side: THREE.DoubleSide, 
  //   depthWrite: false
  // } );
  // const meshCircle = new THREE.Mesh( geometryCircle, materialCircle );
  // groupHalo.add( meshCircle );

  // 转动的球
  // const p1 = new THREE.Vector3( -200, 0, 0 );
  // const p2 = new THREE.Vector3( 200, 0, 0 );
  // const points = [p1,p2];
  // const geometryPoint = new THREE.BufferGeometry().setFromPoints( points );
  // const materialPoint = new THREE.PointsMaterial({
  //   map: textureFlare1,
  //   transparent: true,
  //   side: THREE.DoubleSide, 
  //   size: 1, 
  //   depthWrite: false
  // });
  // const earthPoints = new THREE.Points( geometryPoint, materialPoint );
  // groupHalo.add( earthPoints );
	// groupHalo.rotation.set( 1.9, 0.5, 1 );
  // viewer.scene.add(groupHalo)

  // 经纬度转标转成3D空间坐标
  /** js方法转换
  *lng:经度
  *lat:维度
  *radius:地球半径
  */
  // function lglt2xyz(lng, lat, radius) {
  //   const phi = (180 + lng) * (Math.PI / 180)
  //   const theta = (90 - lat) * (Math.PI / 180)
  //   return {
  //     x: -radius * Math.sin(theta) * Math.cos(phi),
  //     y: radius * Math.cos(theta),
  //     z: radius * Math.sin(theta) * Math.sin(phi),
  //   }
  // }

  /**
   * 经维度 转换坐标
   * THREE.Spherical 球类坐标
   * lng:经度
   * lat:维度
   * radius:地球半径
   */
   function lglt2xyz(lng, lat, radius) {
    // 以z轴正方向为起点的水平方向弧度值
    const theta = (90 + lng) * (Math.PI / 180)
    // 以y轴正方向为起点的垂直方向弧度值
    const phi = (90 - lat) * (Math.PI / 180)
    return new THREE.Vector3().setFromSpherical(new THREE.Spherical(radius, phi, theta))
  }

  // 移动 队列
  const moveArr = []
  function renders() {
    moveArr.forEach(function (mesh) {
      mesh._s += 0.01
      let tankPosition = new THREE.Vector3()
      tankPosition = mesh.curve.getPointAt(mesh._s % 1)
      mesh.position.set(tankPosition.x, tankPosition.y, tankPosition.z)
    })
  }

  /**
  * 绘制 目标点
  * */
  function spotCircle(spot) {
    // 圆
    const geometry1 = new THREE.CircleGeometry(0.12, 100)
    const material1 = new THREE.MeshBasicMaterial({ color: 0x4caf50, side: THREE.DoubleSide })
    const circle = new THREE.Mesh(geometry1, material1)
    circle.position.set(spot[0], spot[1], spot[2])
    // mesh在球面上的法线方向(球心和球面坐标构成的方向向量)
    var coordVec3 = new THREE.Vector3(spot[0], spot[1], spot[2]).normalize()
    // mesh默认在XOY平面上,法线方向沿着z轴new THREE.Vector3(0, 0, 1)
    var meshNormal = new THREE.Vector3(0, 0, 1)
    // 四元数属性.quaternion表示mesh的角度状态
    //.setFromUnitVectors();计算两个向量之间构成的四元数值
    circle.quaternion.setFromUnitVectors(meshNormal, coordVec3)
    earth.add(circle)

    // 圆环
    const geometry2 = new THREE.RingGeometry(0.03, 0.04, 100)
    // transparent 设置 true 开启透明
    const material2 = new THREE.MeshBasicMaterial({ color: 0xff0000, side: THREE.DoubleSide, transparent: true })
    const circleY = new THREE.Mesh(geometry2, material2)
    circleY.position.set(spot[0], spot[1], spot[2])

    // 指向圆心
    circleY.lookAt(new THREE.Vector3(0, 0, 0))
    earth.add(circleY)
    // 加入动画队列
    // bigByOpacityArr.push(circleY)
  }

  // 在线上移动的物体就简单了。根据三维三次贝塞尔曲线得到的点,绘制一个几何体。把点缓存下来,加入移动队列进行动画。
  /**
   * 线上移动物体
   * */
   function moveSpot(curve) {
    // 线上的移动物体
    const aGeo = new THREE.SphereGeometry(0.06, 64, 64)
    const aMater = new THREE.MeshPhongMaterial({ color: 0x9c27b0, side: THREE.DoubleSide })
    const aMesh = new THREE.Mesh(aGeo, aMater)
    // 保存曲线实例
    aMesh.curve = curve
    aMesh._s = 0

    moveArr.push(aMesh)
    earth.add(aMesh)
  }

  // 绘制飞线

  // 在3D中飞线,都是曲线且都是在球外部进行连接的。所以我们需要使用三维三次贝塞尔曲线。
  // 先获取要连线的两个坐标。计算出两点的夹角,根据夹角计算偏移。计算出放大后的终点位置。以这两个值计算出三维三次贝塞尔曲线的中间点。
  // 这是在网上随便找的算法,想优化的可以自己计算或者继续在网上找。
  // 然后就是根据三维三次贝塞尔曲线创建线几何体,加入地球场景中。
  /**
   * 绘制 两个目标点并连线
   * */
  function lineConnect(posStart, posEnd) {
    const v0 = lglt2xyz(posStart[0], posStart[1], globeRadius)
    const v3 = lglt2xyz(posEnd[0], posEnd[1], globeRadius)

    // angleTo() 计算向量的夹角
    const angle = v0.angleTo(v3)
    let vtop = v0.clone().add(v3)
    // multiplyScalar 将该向量与所传入的 标量进行相乘
    vtop = vtop.normalize().multiplyScalar(globeRadius)

    let n
    if (angle <= 1) {
      n = (globeRadius / 5) * angle
    } else if (angle > 1 && angle < 2) {
      n = (globeRadius / 5) * Math.pow(angle, 2)
    } else {
      n = (globeRadius / 5) * Math.pow(angle, 1.5)
    }

    const v1 = v0
      .clone()
      .add(vtop)
      .normalize()
      .multiplyScalar(globeRadius + n)
    const v2 = v3
      .clone()
      .add(vtop)
      .normalize()
      .multiplyScalar(globeRadius + n)
    // 三维三次贝塞尔曲线(v0起点,v1第一个控制点,v2第二个控制点,v3终点)
    const curve = new THREE.CubicBezierCurve3(v0, v1, v2, v3)

    // 绘制 目标位置
    spotCircle([v0.x, v0.y, v0.z])
    spotCircle([v3.x, v3.y, v3.z])
    moveSpot(curve)

    const lineGeometry = new THREE.BufferGeometry()
    // 获取曲线 上的50个点
    var points = curve.getPoints(50)
    var positions = []
    var colors = []
    var color = new THREE.Color()

    // 给每个顶点设置演示 实现渐变
    for (var j = 0; j < points.length; j++) {
      color.setHSL(0.81666 + j, 0.88, 0.715 + j * 0.0025) // 粉色
      colors.push(color.r, color.g, color.b)
      positions.push(points[j].x, points[j].y, points[j].z)
    }
    // 放入顶点 和 设置顶点颜色
    lineGeometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3, true))
    lineGeometry.setAttribute('color', new THREE.BufferAttribute(new Float32Array(colors), 3, true))

    const material = new THREE.LineBasicMaterial({ vertexColors: THREE.VertexColors, side: THREE.DoubleSide })
    const line = new THREE.Line(lineGeometry, material)

    earth.add(line)
  }

  /**
  * 画图
  * */
  function drawChart() {
      lineConnect([-58.48, 36], [116.4, 39.91])
      lineConnect([-58.48, 36], [121.564136, 25.071558])
      lineConnect([-58.48, 36], [104.896185, 11.598253])
      lineConnect([-58.48, 36], [130.376441, -16.480708])
      lineConnect([-58.48, 36], [-71.940328, -13.5304])
      lineConnect([-58.48, 36], [-3.715707, 40.432926])
      lineConnect([-58.48, 36], [-78.940328, -23.5304])
      lineConnect([-58.48, 36], [-31.715707, 30.432926])
      lineConnect([-58.48, 36], [-34.940328, -73.5304])
      lineConnect([-58.48, 36], [-28.715707, 20.432926])
      lineConnect([-58.48, 36], [-51.940328, -83.5304])
      lineConnect([-58.48, 36], [-39.715707, 10.432926])
  }
  drawChart()
  // 全局动画逻辑
  const clock = new THREE.Clock();
  allTiemen = {
    fun: ({earth, moon}) => {
      renders();
      TWEEN && TWEEN.update();
      const elapsedTime = clock.getElapsedTime()
      earth && (earth.rotation.y += 0.002)
      atmosphere && (atmosphere.rotation.y += 0.004)
      atmosphere && (atmosphere.rotation.x += 0.002)
      // 公转
      moon && (moon.position.x = Math.sin(elapsedTime * .5) * -120);
      moon && (moon.position.z = Math.cos(elapsedTime * .5) * -120);
    },
    content: {earth, moon}
  }
  viewer.addAnimate(allTiemen)
}

onBeforeUnmount(()=>{
  window.removeEventListener('resize', () => {
    viewer._undateDom()
  })
})

onMounted(()=>{
  init()
  // 监听页面大小变动,自适应页面, 第一次直接触发执行
  window.addEventListener('resize', () => {
    viewer._undateDom()
  })
  // 初次页面变动执行不成功,主动延迟执行一次
  nextTick(()=>{
    viewer._undateDom()
  })
})  
</script>

<style lang="scss">
.earth_page {
  height: 100vh;
  overflow: hidden;
  .scene {
    height: 100vh;
    width: 100%;
    overflow: hidden;
  }
}
</style>

更多详细代码请关注公众号索取(备注:公众号):
在这里插入图片描述

你可能感兴趣的:(javascript,3d,开发语言)