3D地球可視化效果
3D地球的開發(fā)并不復(fù)雜,對(duì)球形物體進(jìn)行貼圖操作,完成球體自轉(zhuǎn)和月球公轉(zhuǎn),太陽場景設(shè)置等即可
上代碼文章來源:http://www.zghlxwxcb.cn/news/detail-757002.html
<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 動(dòng)畫效果渲染 效果同 gsap
import { Lensflare, LensflareElement } from 'three/examples/jsm/objects/Lensflare.js';
let loadingProcess = ref(100) // loading加載數(shù)據(jù) 0 25 50 75 100
let sceneReady = false // 場景加載完畢標(biāo)志,程序進(jìn)行l(wèi)abel展示,鏡頭拉進(jìn)等效果
let viewer = null // 基礎(chǔ)類,包含場景、相機(jī)、控制器等實(shí)例
let tiemen = null // 水面動(dòng)畫 函數(shù)
let allTiemen = null // 全局動(dòng)畫 函數(shù)
const sizes = { // 存儲(chǔ)全局寬度 高度
width: window.innerWidth,
height: window.innerHeight
}
const lensflareTexture0 = 'images/lensflare0.png' // 太陽光貼圖
const lensflareTexture1 = 'images/lensflare1.png' // 黑色描邊貼圖
const earth = new THREE.Object3D(); // 地球存儲(chǔ)
// 地球3D層
// const earthObject = new THREE.Object3D()
// 地球半徑
const globeRadius = 10
// 初始化three場景
const init = () => {
viewer = new modules.Viewer('viewer-container') //初始化場景
// 調(diào)整相機(jī)位置(相機(jī)位置在初始化的時(shí)候設(shè)置過一次,這里對(duì)其進(jìn)行調(diào)整)
viewer.camera.position.set(0, 600, 1600)
// 限制controls的上下角度范圍 (OrbitControls的范圍)
viewer.controls.maxPolarAngle = Math.PI / 2.1;
// 增加燈光(初始化viewer的時(shí)候,對(duì)燈光也做了初始,這里進(jìn)行燈光調(diào)整)
let { lights } = viewer
// 環(huán)境光會(huì)均勻的照亮場景中的所有物體。 環(huán)境光不能用來投射陰影,因?yàn)樗鼪]有方向。
let ambientLight = lights.addAmbientLight()
ambientLight.setOption({color: 0xffffff, intensity: 0.8}) // 調(diào)用燈光內(nèi)置方法,設(shè)置新的屬性
// 平行光是沿著特定方向發(fā)射的光。這種光的表現(xiàn)像是無限遠(yuǎn),從它發(fā)出的光線都是平行的。常常用平行光來模擬太陽光的效果。 太陽足夠遠(yuǎn),因此我們可以認(rèn)為太陽的位置是無限遠(yuǎn),所以我們認(rèn)為從太陽發(fā)出的光線也都是平行的。
lights.addDirectionalLight([-1, 1.75, 1], { // 增加直射燈光方法
color: 'rgb(255,234,229)',
// intensity: 3, // intensity屬性是用來設(shè)置聚光燈的強(qiáng)度,默認(rèn)值是1,如果設(shè)置成0那什么也看不到,該值越大,點(diǎn)光源看起來越亮
// castShadow: true, // castShadow屬性是用來控制光源是否產(chǎn)生陰影,取值為true或false
})
// 從一個(gè)點(diǎn)向各個(gè)方向發(fā)射的光源。一個(gè)常見的例子是模擬一個(gè)燈泡發(fā)出的光。
const pointLight = lights.addPointLight([0, 45, -2000], { // 增加直射燈光方法
color: 'rgb(253,153,253)'
})
// 模擬太陽光效果
const textureLoader = new THREE.TextureLoader(); // 加載texture的一個(gè)類。 內(nèi)部使用ImageLoader來加載文件。
const textureFlare0 = textureLoader.load(lensflareTexture0); // 加載太陽光 貼圖
const textureFlare1 = textureLoader.load(lensflareTexture1); // 加載黑色貼圖
// 鏡頭光暈
const lensflare = new Lensflare(); // 創(chuàng)建一個(gè)模擬追蹤著燈光的鏡頭光暈。 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時(shí)在光源的位置)
// 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 完成的鏡頭切換動(dòng)畫工具函數(shù),分別傳入相機(jī),控制器,相機(jī)最終位置,指向控制器位置,動(dòng)作時(shí)間
Animations.animateCamera(viewer.camera, viewer.controls, { x: 100, y: 20, z: 200 }, { x: 0, y: 0, z: 0 }, 4000, () => {
sceneReady = true
});
/**
* 創(chuàng)建canvas方形紋理,取代圖片紋理,利用代碼形式創(chuàng)建
* */
function generateSprite() {
const canvas = document.createElement('canvas')
canvas.width = 16
canvas.height = 16
const context = canvas.getContext('2d')
// 創(chuàng)建顏色漸變
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)
// 轉(zhuǎn)為紋理
const texture = new THREE.Texture(canvas)
texture.needsUpdate = true
return texture
}
const positions = []
const colors = []
// 創(chuàng)建 幾何體
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 );
}
// 對(duì)幾何體 設(shè)置 坐標(biāo) 和 顏色
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
// geometry.setAttribute( 'color', new THREE.Float32BufferAttribute( colors, 3 ) );
// 默認(rèn)球體
geometry.computeBoundingSphere()
// ------------- 1 ----------
// 星星資源圖片
// PointsMaterial 點(diǎn)基礎(chǔ)材質(zhì)
const starsMaterial = new THREE.PointsMaterial({
map: generateSprite(),
size: 1,
transparent: true,
opacity: 1,
//true:且該幾何體的colors屬性有值,則該粒子會(huì)舍棄第一個(gè)屬性--color,而應(yīng)用該幾何體的colors屬性的顏色
// vertexColors: true,
blending: THREE.AdditiveBlending,
sizeAttenuation: true
})
// 粒子系統(tǒng) 網(wǎng)格
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 );
// 轉(zhuǎn)動(dòng)的球
// 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)
// 經(jīng)緯度轉(zhuǎn)標(biāo)轉(zhuǎn)成3D空間坐標(biāo)
/** js方法轉(zhuǎn)換
*lng:經(jīng)度
*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),
// }
// }
/**
* 經(jīng)維度 轉(zhuǎn)換坐標(biāo)
* THREE.Spherical 球類坐標(biāo)
* lng:經(jīng)度
* lat:維度
* radius:地球半徑
*/
function lglt2xyz(lng, lat, radius) {
// 以z軸正方向?yàn)槠瘘c(diǎn)的水平方向弧度值
const theta = (90 + lng) * (Math.PI / 180)
// 以y軸正方向?yàn)槠瘘c(diǎn)的垂直方向弧度值
const phi = (90 - lat) * (Math.PI / 180)
return new THREE.Vector3().setFromSpherical(new THREE.Spherical(radius, phi, theta))
}
// 移動(dòng) 隊(duì)列
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)
})
}
/**
* 繪制 目標(biāo)點(diǎn)
* */
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在球面上的法線方向(球心和球面坐標(biāo)構(gòu)成的方向向量)
var coordVec3 = new THREE.Vector3(spot[0], spot[1], spot[2]).normalize()
// mesh默認(rèn)在XOY平面上,法線方向沿著z軸new THREE.Vector3(0, 0, 1)
var meshNormal = new THREE.Vector3(0, 0, 1)
// 四元數(shù)屬性.quaternion表示mesh的角度狀態(tài)
//.setFromUnitVectors();計(jì)算兩個(gè)向量之間構(gòu)成的四元數(shù)值
circle.quaternion.setFromUnitVectors(meshNormal, coordVec3)
earth.add(circle)
// 圓環(huán)
const geometry2 = new THREE.RingGeometry(0.03, 0.04, 100)
// transparent 設(shè)置 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)
// 加入動(dòng)畫隊(duì)列
// bigByOpacityArr.push(circleY)
}
// 在線上移動(dòng)的物體就簡單了。根據(jù)三維三次貝塞爾曲線得到的點(diǎn),繪制一個(gè)幾何體。把點(diǎn)緩存下來,加入移動(dòng)隊(duì)列進(jìn)行動(dòng)畫。
/**
* 線上移動(dòng)物體
* */
function moveSpot(curve) {
// 線上的移動(dòng)物體
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)
// 保存曲線實(shí)例
aMesh.curve = curve
aMesh._s = 0
moveArr.push(aMesh)
earth.add(aMesh)
}
// 繪制飛線
// 在3D中飛線,都是曲線且都是在球外部進(jìn)行連接的。所以我們需要使用三維三次貝塞爾曲線。
// 先獲取要連線的兩個(gè)坐標(biāo)。計(jì)算出兩點(diǎn)的夾角,根據(jù)夾角計(jì)算偏移。計(jì)算出放大后的終點(diǎn)位置。以這兩個(gè)值計(jì)算出三維三次貝塞爾曲線的中間點(diǎn)。
// 這是在網(wǎng)上隨便找的算法,想優(yōu)化的可以自己計(jì)算或者繼續(xù)在網(wǎng)上找。
// 然后就是根據(jù)三維三次貝塞爾曲線創(chuàng)建線幾何體,加入地球場景中。
/**
* 繪制 兩個(gè)目標(biāo)點(diǎn)并連線
* */
function lineConnect(posStart, posEnd) {
const v0 = lglt2xyz(posStart[0], posStart[1], globeRadius)
const v3 = lglt2xyz(posEnd[0], posEnd[1], globeRadius)
// angleTo() 計(jì)算向量的夾角
const angle = v0.angleTo(v3)
let vtop = v0.clone().add(v3)
// multiplyScalar 將該向量與所傳入的 標(biāo)量進(jìn)行相乘
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起點(diǎn),v1第一個(gè)控制點(diǎn),v2第二個(gè)控制點(diǎn),v3終點(diǎn))
const curve = new THREE.CubicBezierCurve3(v0, v1, v2, v3)
// 繪制 目標(biāo)位置
spotCircle([v0.x, v0.y, v0.z])
spotCircle([v3.x, v3.y, v3.z])
moveSpot(curve)
const lineGeometry = new THREE.BufferGeometry()
// 獲取曲線 上的50個(gè)點(diǎn)
var points = curve.getPoints(50)
var positions = []
var colors = []
var color = new THREE.Color()
// 給每個(gè)頂點(diǎn)設(shè)置演示 實(shí)現(xiàn)漸變
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)
}
// 放入頂點(diǎn) 和 設(shè)置頂點(diǎn)顏色
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()
// 全局動(dòng)畫邏輯
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)
// 公轉(zhuǎn)
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()
// 監(jiān)聽頁面大小變動(dòng),自適應(yīng)頁面, 第一次直接觸發(fā)執(zhí)行
window.addEventListener('resize', () => {
viewer._undateDom()
})
// 初次頁面變動(dòng)執(zhí)行不成功,主動(dòng)延遲執(zhí)行一次
nextTick(()=>{
viewer._undateDom()
})
})
</script>
<style lang="scss">
.earth_page {
height: 100vh;
overflow: hidden;
.scene {
height: 100vh;
width: 100%;
overflow: hidden;
}
}
</style>
更多詳細(xì)代碼請(qǐng)關(guān)注公眾號(hào)索取(備注:公眾號(hào)):文章來源地址http://www.zghlxwxcb.cn/news/detail-757002.html
到了這里,關(guān)于Three.js - 實(shí)現(xiàn)一個(gè)3D地球可視化的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!