3D地球可视化效果
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>
更多详细代码请关注公众号索取(备注:公众号):