Threejs实现酷炫3D地球技术点汇总

前言

ezgif.com-gif-maker

本篇介绍一下如何用Threejs实现一个酷炫的3D地球特效,使用到的技能点如下:

  • 星空动态背景
  • 地球模型
  • 大气层光圈
  • 卫星环绕特效
  • 经纬度坐标转成3D空间坐标
  • 标注以及标注扩散光圈
  • 光柱特效
  • 飞线特效
  • geojson数据生成中国描边以及动态流光效果

正文

这里一个个介绍使用到的技术,首先先搭建初始化界面,把渲染器,相机以及基本的光照设置好。

不懂的可以参考这个页面的模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgl - mirror</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link type="text/css" rel="stylesheet" href="main.css">
<style>
html, body {
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<div id="container" style="width:100%;height:100vh;position:relative; overflow: hidden;"></div>
</div>
<script type="module">
import * as THREE from '../build/three.module.js';
import { OrbitControls } from './jsm/controls/OrbitControls.js';
let renderer, camera, scene, light, controls;
const Dom = document.querySelector( '#container' );
const width = Dom.clientWidth, height = Dom.clientHeight;
/**
* @description 初始化渲染场景
*/
function initRenderer() {
renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( width, height );
const containerDom = document.querySelector( '#container' );
containerDom.appendChild( renderer.domElement );
}
/**
* @description 初始化相机
*/
function initCamera() {
camera = new THREE.PerspectiveCamera( 45, width / height, 1, 10000 );
camera.position.set( 5, - 20, 200 );
camera.lookAt( 0, 3, 0 );
window.camera = camera;
}
/**
* @description 初始化场景
*/
function initScene() {
scene = new THREE.Scene();
scene.background = new THREE.Color( 0x020924 );
scene.fog = new THREE.Fog( 0x020924, 200, 1000 );
window.scene = scene;
}
/**
* 初始化用户交互
**/
function initControls() {
controls = new OrbitControls( camera, renderer.domElement );
controls.enableDamping = true;
controls.enableZoom = true;
controls.autoRotate = false;
controls.autoRotateSpeed = 2;
controls.enablePan = true;
}
/**
* @description 初始化光
*/
function initLight() {
const ambientLight = new THREE.AmbientLight( 0xcccccc, 1.1 );
scene.add( ambientLight );
var directionalLight = new THREE.DirectionalLight( 0xffffff, 0.2 );
directionalLight.position.set( 1, 0.1, 0 ).normalize();
var directionalLight2 = new THREE.DirectionalLight( 0xff2ffff, 0.2 );
directionalLight2.position.set( 1, 0.1, 0.1 ).normalize();
scene.add( directionalLight );
scene.add( directionalLight2 );
var hemiLight = new THREE.HemisphereLight( 0xffffff, 0x444444, 0.2 );
hemiLight.position.set( 0, 1, 0 );
scene.add( hemiLight );
var directionalLight = new THREE.DirectionalLight( 0xffffff );
directionalLight.position.set( 1, 500, - 20 );
directionalLight.castShadow = true;
directionalLight.shadow.camera.top = 18;
directionalLight.shadow.camera.bottom = - 10;
directionalLight.shadow.camera.left = - 52;
directionalLight.shadow.camera.right = 12;
scene.add(directionalLight);
}
/**
* 窗口变动
**/
function onWindowResize() {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( innerWidth, innerHeight );
renders();
}

/**
* @description 渲染
*/
function renders() {
renderer.clear();
renderer.render( scene, camera );
}

/**
* 更新
**/
function animate() {
window.requestAnimationFrame( () => {
if (controls) controls.update();
renders();
animate();
} );
}

window.onload = () => {
initRenderer();
initCamera();
initScene();
initLight();
initControls();
animate();
window.addEventListener('resize', onWindowResize, false);
};
</script>
</body>
</html>

动态星空背景介绍

image-20210621164543735

作为地球的背景,用动态星空的方式显得更加酷炫,使用原型贴图让原本方形的点模拟球形,再加上动态设置颜色以及设置旋转偏移,更好的模拟星空效果。

  • 随机生成10000个坐标点,设置不同的颜色
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const positions = [];
const colors = [];
const geometry = new THREE.BufferGeometry();
for (var i = 0; i < 10000; i ++) {
var 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 );
var 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 ) );

分别把生成的Vector3和Color放入数组,然后添加到geometry中,这样星空几何体有了。 Color里面setHSL可以设置颜色和饱和度,这里通过random来随机颜色。

  • 使用 ParticleBasicMaterial 生成材质

ParticleBasicMaterial 基础粒子材质用来搭配例子系统,这里我们可以设置粒子的大小,贴图,透明度等设置详细如下:

1
2
3
4
5
6
7
8
9
var starsMaterial = new THREE.ParticleBasicMaterial( {
map: texture,
size: 1,
transparent: true,
opacity: 1,
vertexColors: true, //true:且该几何体的colors属性有值,则该粒子会舍弃第一个属性--color,而应用该几何体的colors属性的颜色
blending: THREE.AdditiveBlending,
sizeAttenuation: true
} );
  • 使用 ParticleSystem 生成模型

这里使用ParticleSystem这个粒子系统,是为了提供性能,如果用精灵Particle动态随机生成10000个的话,帧率肯定收到影响,这里ParticleSystem的话,等于只有一个Mesh,能大大提高性能。

把上面生成的几何体geometry 以及材质ParticleBasicMaterial来生成一个ParticleSystem,如下:

1
2
3
let stars = new THREE.ParticleSystem( geometry, starsMaterial );
stars.scale.set( 300, 300, 300 );
scene.add( stars );

地球模型

地球模型比较简单,直接贴图+一个球搞定

image-20210621164709105

1
2
3
4
5
6
7
8
9
10
function initEarth() {
globeTextureLoader.load( './imgs/diqiu2/earth2.jpg', function ( texture ) {
var globeGgeometry = new THREE.SphereGeometry( radius, 100, 100 );
var globeMaterial = new THREE.MeshStandardMaterial( { map: texture } );
var globeMesh = new THREE.Mesh( globeGgeometry, globeMaterial );
group.rotation.set( 0.5, 2.9, 0.1 );
group.add( globeMesh );
scene.add( group );
} );
}

大气层光圈

大气层光圈这里也是用贴图实现,如下面这张。

image-20210621164959169

image-20210621164924403

代码如下

1
2
3
4
5
6
7
8
9
10
var texture = globeTextureLoader.load( './imgs/diqiu2/earth_aperture.png' );
var spriteMaterial = new THREE.SpriteMaterial( {
map: texture,
transparent: true,
opacity: 0.5,
depthWrite: false
} );
var sprite = new THREE.Sprite( spriteMaterial );
sprite.scale.set( radius * 3, radius * 3, 1 );
group.add( sprite );

卫星环绕特效

image-20210621165555877

这里用到一个 Mesh 和一个 Poinst 结合,分别用来实现外圈的环形和两个小卫星

光环用 PlaneGeometry 矩形平面即可,加上贴图

1
2
3
4
5
6
7
8
9
10
11
globeTextureLoader.load( './imgs/diqiu2/halo.png', function ( texture ) {
var geometry = new THREE.PlaneGeometry( 14, 14 );
var material = new THREE.MeshLambertMaterial( {
map: texture,
transparent: true,
side: THREE.DoubleSide,
depthWrite: false
} );
var mesh = new THREE.Mesh( geometry, material );
groupHalo.add( mesh );
} );

两个环绕的卫星直接使用Points即可,设置两个坐标,用来展示这2个小卫星

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
globeTextureLoader.load( './imgs/diqiu2/smallEarth.png', function ( texture ) {
var p1 = new THREE.Vector3( - 7, 0, 0 );
var p2 = new THREE.Vector3( 7, 0, 0 );
const points = [ p1,p2];
const geometry = new THREE.BufferGeometry().setFromPoints( points );
var material = new THREE.PointsMaterial( {
map: texture,
transparent: true,
side: THREE.DoubleSide,
size: 1,
depthWrite: false
} );
var earthPoints = new THREE.Points( geometry, material );
groupHalo.add( earthPoints );
} );
groupHalo.rotation.set( 1.9, 0.5, 1 );

经纬度转标转成3D空间坐标

上面已经实现了最基本的效果,接下来需要在地球上添加一下特效,比如标注、光柱、光圈等。

但是会存在一个问题,要定义地球上的一点,用经纬度是常用的办法,但是经纬度又不适合在Threejs上面使用,所以这里就需要先做一步转换工作,把经纬度坐标转换成xyz空间坐标。

这边直接提供两种转换方法:

  • 方法一:js方法转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
*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),
}
}
  • 方法二:threejs自带

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /**
    *lng:经度
    *lat:维度
    *radius:地球半径
    */
    lglt2xyz(lng, lat, radius) {
    const theta = (90 + lng) * (Math.PI / 180)
    const phi = (90 - lat) * (Math.PI / 180)
    return (new THREE.Vector3()).setFromSpherical(new THREE.Spherical(radius, phi, theta))
    }

标注以及标注扩散光圈

要实现标注功能很简单,直接一个平面加贴图即可

这里唯一要注意的就是,再球面上的物体,需要设置好角度,不然你会发现效果会和你预想的不一样

具体参考下面方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function createPointMesh( pos, texture ) {
var material = new THREE.MeshBasicMaterial( {
map: texture,
transparent: true, //使用背景透明的png贴图,注意开启透明计算
// side: THREE.DoubleSide, //双面可见
depthWrite: false, //禁止写入深度缓冲区数据
} );
var mesh = new THREE.Mesh( planGeometry, material );
var size = radius * 0.04;//矩形平面Mesh的尺寸
mesh.scale.set( size, size, size );//设置mesh大小
//设置mesh位置
mesh.position.set( pos.x, pos.y, pos.z );
// mesh在球面上的法线方向(球心和球面坐标构成的方向向量)
var coordVec3 = new THREE.Vector3( pos.x, pos.y, pos.z ).normalize();
// mesh默认在XOY平面上,法线方向沿着z轴new THREE.Vector3(0, 0, 1)
var meshNormal = new THREE.Vector3( 0, 0, 1 );
// 四元数属性.quaternion表示mesh的角度状态
//.setFromUnitVectors();计算两个向量之间构成的四元数值
mesh.quaternion.setFromUnitVectors( meshNormal, coordVec3 );
return mesh;
}

光圈的话,贴图用一张有渐变效果的png图

image-20210621203748125

然后也是贴在 PlaneBufferGeometry 上,和上面标注的效果实现一样,最后要在渲染函数 animate 里面动态的修改尺寸和透明度即可。

动画效果参考如下代码,WaveMeshArr是所有光圈mesh的数组集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (WaveMeshArr.length) {
WaveMeshArr.forEach( function ( mesh ) {
mesh._s += 0.007;
mesh.scale.set( mesh.size * mesh._s, mesh.size * mesh._s, mesh.size * mesh._s );
if (mesh._s <= 1.5) {
//mesh._s=1,透明度=0 mesh._s=1.5,透明度=1
mesh.material.opacity = ( mesh._s - 1 ) * 2;
} else if (mesh._s > 1.5 && mesh._s <= 2) {
//mesh._s=1.5,透明度=1 mesh._s=2,透明度=0
mesh.material.opacity = 1 - ( mesh._s - 1.5 ) * 2;
} else {
mesh._s = 1.0;
}
} );
}

光柱特效

你如果想在Three.js创建一个光柱效果,可以通过Three.js的矩形平面几何体PlaneGeometry创建一个网格模型,然后把一个背景透明的.png格式图片作为矩形网格模型的纹理贴图。

1
2
3
4
5
6
7
8
9
10
var plane = new THREE.PlaneGeometry(50,200)
var material = new THREE.MeshPhongMaterial({
//设置矩形网格模型的纹理贴图(光柱特效)
map: textureLoader.load('光柱.png'),
// 双面显示
side: THREE.DoubleSide,
// 开启透明效果,否则颜色贴图map的透明不起作用
transparent: true,
});
var mesh = new THREE.Mesh(plane, material);

为了增强立体效果,可以创建两个矩形网格模型然后90度交叉即可

1
2
3
4
5
6
// 矩形网格1
var mesh1 = new THREE.Mesh(plane, material);
// 克隆网格模型mesh1,并旋转90度
var mesh2 = mesh1.clone().rotateY(Math.PI/2)
var groupMesh= new THREE.Group()
groupMesh.add(mesh1,mesh2);

最终实现效果如下

image-20210621222637050

飞线特效

飞线这里分两块介绍,一个是绘制三维三次贝赛尔曲线,另一个是飞线上模拟物体移动。

  • 飞线
    上面介绍标注的时候,就已经知道了不同位置的坐标了,这里封装了一个方法,传入2个坐标就可以生成一条贝赛尔曲线。

    目前取第一个坐标点作为飞线的起始点,比如你选择北京作为原始点,那飞线特效就是从北京飞往各个地方。

    线条的话,我这里使用Line2,因为这个支持设置线条的宽度。

    核心代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    function addLines( v0, v3 ) {
    // 夹角
    var angle = ( v0.angleTo( v3 ) * 1.8 ) / Math.PI / 0.1; // 0 ~ Math.PI
    var aLen = angle * 0.4, hLen = angle * angle * 12;
    var p0 = new THREE.Vector3( 0, 0, 0 );
    // 法线向量
    var rayLine = new THREE.Ray( p0, getVCenter( v0.clone(), v3.clone() ) );
    // 顶点坐标
    var vtop = rayLine.at( hLen / rayLine.at( 1 ).distanceTo( p0 ) );
    // 控制点坐标
    var v1 = getLenVcetor( v0.clone(), vtop, aLen );
    var v2 = getLenVcetor( v3.clone(), vtop, aLen );
    // 绘制三维三次贝赛尔曲线
    var curve = new THREE.CubicBezierCurve3( v0, v1, v2, v3 );
    var geometry = new LineGeometry();
    var points = curve.getPoints( 50 );
    var positions = [];
    var colors = [];
    var color = new THREE.Color();

    /**
    * HSL中使用渐变
    * h — hue value between 0.0 and 1.0
    * s — 饱和度 between 0.0 and 1.0
    * l — 亮度 between 0.0 and 1.0
    */
    for (var j = 0; j < points.length; j ++) {
    // color.setHSL( .31666+j*0.005,0.7, 0.7); //绿色
    color.setHSL( .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 );
    }
    geometry.setPositions( positions );
    geometry.setColors( colors );
    var matLine = new LineMaterial( {
    linewidth: 0.0006,
    vertexColors: true,
    dashed: false
    } );

    return {
    curve: curve,
    lineMesh: new Line2( geometry, matLine )
    };
    }
  • 物体移动特效

物体移动就是从飞线的起始点,飞到物体的终点。

把上面飞线生成的曲线curve,添加到数组里面。

然后循环这个数组,每个数组配套生成一个几何球体,用来当做移动的载体,再把这个球体放入一个数组。

关键来了,循环这个球体数组,把上面curve这条线段再等分100份,然后让球体坐标分别设置为上面等分100份的坐标即可,这样就可以看到球体开始循环移动了。

核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for (let i = 0; i < animateDots.length; i ++) {
const aGeo = new THREE.SphereGeometry( 0.03, 0.03, 0.03 );
const aMater = new THREE.MeshPhongMaterial( { color: '#F8D764' } );
const aMesh = new THREE.Mesh( aGeo, aMater );
aGroup.add( aMesh );
}
var vIndex = 0;
function animateLine() {
aGroup.children.forEach( ( elem, index ) => {
const v = animateDots[index][vIndex];
elem.position.set( v.x, v.y, v.z );
});
vIndex ++;
if (vIndex > 100) {
vIndex = 0;
}
setTimeout( animateLine, 20 );
}
group.add( aGroup );
animateLine();

geojson数据生成中国描边以及动态流光效果

这块其实可以单独拿出来介绍,根据geojson数据生成地图用的,不过复杂的是用来做拉升,生成几何体模型用的,这边只是简单的给地图画线条。

另外就是用到的一个线条流光的shader特效,这里分开来介绍。

geojson数据可以去这个网站下载:https://datav.aliyun.com/tools/atlas/index.html

  • 根据geojson数据画线条
    上面已经有过划线的经验了,所以这块做起来也很方便,只要就是2个地方要注意
    1:加载读取geojson数据,循环后把经纬度转化成空间xyz坐标

    2:根据这些坐标,使用Line生成线条即可。

    核心代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    function initMap( chinaJson ) {
    // 遍历省份构建模型
    chinaJson.features.forEach( elem => {
    // 新建一个省份容器:用来存放省份对应的模型和轮廓线
    const province = new THREE.Object3D();
    const coordinates = elem.geometry.coordinates;
    coordinates.forEach( multiPolygon => {
    multiPolygon.forEach( polygon => {
    const lineMaterial = new THREE.LineBasicMaterial( { color: 0XF19553 } ); //0x3BFA9E
    const positions = [];
    const linGeometry = new THREE.BufferGeometry();
    for (let i = 0; i < polygon.length; i ++) {
    var pos = lglt2xyz( polygon[i][0], polygon[i][1] );
    positions.push( pos.x, pos.y, pos.z );
    }
    linGeometry.setAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) );
    const line = new THREE.Line( linGeometry, lineMaterial );
    province.add( line );
    } );
    } );
    map.add( province );
    } );
    group.add( map );
    }
  • 根据geojson设置流光效果
    画完中国的描边后,我们还可以让中国的外边框轮廓有动态流光效果,这里还是需要下载中国外边框的geojson数据,用上面那个地址即可,把包含子区域的选项去掉打钩就行。

这里和上面生成的方法不一样,上面最终生成的是Line,而这里最终生成的是Points,然后通过shader处理生成跑动的流光效果。

核心shader写法如下

uniforms:

1
2
3
4
5
6
7
8
const singleUniforms = {
u_time: uniforms2.u_time,
number: { type: 'f', value: number },
speed: { type: 'f', value: speed },
length: { type: 'f', value: length },
size: { type: 'f', value: size },
color: { type: 'v3', value: color }
};

顶点着色器和片元着色器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<script id="vertexShader2" type="x-shader/x-vertex">
varying vec2 vUv;
attribute float percent;
uniform float u_time;
uniform float number;
uniform float speed;
uniform float length;
varying float opacity;
uniform float size;
void main()
{
vUv = uv;
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
float l = clamp(1.0-length,0.0,1.0);
gl_PointSize = clamp(fract(percent*number + l - u_time*number*speed)-l ,0.0,1.) * size * (1./length);
opacity = gl_PointSize/size;
gl_Position = projectionMatrix * mvPosition;
}
</script>
<script id="fragmentShader2" type="x-shader/x-vertex">
#ifdef GL_ES
precision mediump float;
#endif
varying float opacity;
uniform vec3 color;
void main(){
if(opacity <=0.2){
discard;
}
gl_FragColor = vec4(color,1.0);
}
</script>

效果如下:

ezgif.com-gif-maker (1)

总结

以上就是这篇3D地球的技术拆解,把每个技术点都掌握了,后期就能配合开发出更多的特效了。

有需要源码的童鞋,可以淘宝搜一下 threejs 3d地球,目前特价19.9元~ 一顿饭钱

店铺名叫 嘟来嘟往小店

项目基于官方r129项目开发,没有额外封装,小白都能看得懂哈。

文章目录
  1. 1. 前言
  2. 2. 正文
    1. 2.1. 动态星空背景介绍
    2. 2.2. 地球模型
    3. 2.3. 大气层光圈
    4. 2.4. 卫星环绕特效
    5. 2.5. 经纬度转标转成3D空间坐标
    6. 2.6. 标注以及标注扩散光圈
    7. 2.7. 光柱特效
    8. 2.8. 飞线特效
    9. 2.9. geojson数据生成中国描边以及动态流光效果
  3. 3. 总结