0%

Three 绘制3D地球

使用Three来绘制一个3D的地球,而且需要地球可以自转。

准备

需要安装并导入Three.js。

需要地球的贴图,一个是纹理图,一个是凹凸贴图,凹凸贴图的文理可以让黑色和白色值映射到与灯相关的感知深度,形成不影响对象的几何形状,这样可以让对象更有立体感。

照样,我采用的是angular框架,所以只关注当前组件里面的代码。

页面框架

html结构:

1
2
3
<div id="world">
<canvas id="canvas" #canvas></canvas>
</div>

canvas作为渲染的主体。

css样式:

1
2
3
4
5
6
7
8
9
10
11
#world {
width: 100%;
height: 100%;
background: url(../../../../../assets/img/plane/starry-deep-outer-space-galaxy.jpg) center center;
background-size: 100%;
overflow: hidden;
#canvas {
width: 100%;
height: 100%;
}
}

为了更好的表示太空,我给视窗添加了背景图片。

js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import THREE from '../three.js';
@ViewChild('canvas')
private canvasRef: ElementRef;
private get canvas(): HTMLCanvasElement {
return this.canvasRef.nativeElement;
}

renderer: THREE.RenderElement;

// 初始化
init() {
}
// 渲染输出
public render() {
this.renderer.render(this.scene, this.camera);
window.requestAnimationFrame(this.render.bind(this));
this.animate();
}
// 动画状态变更
animate() {
}

ok,准备工作做好了,目前视窗里面看到的只是一个背景图片之外啥都看不到:
earth-1

构建场景

构建基本的3D场景,那我们需要一个相机camera,一个场景scene

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
scene: THREE.Scene;
camera: THREE.PerspectiveCamera;

init(){
//...
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(45, this.canvas.clientWidth / this.canvas.clientHeight, 0.1, 1000);

this.renderer = new THREE.WebGLRenderer({
canvas: this.canvas,
antialias: true,
alpha: true,
});
this.renderer.setSize(this.canvas.clientWidth, this.canvas.clientHeight);
this.renderer.shadowMap.enabled = true;
this.renderer.render(this.scene, this.camera);
//
this.camera.position.set(-20, 30, 40);
this.camera.lookAt(0, 0, 0);
this.render();
}

我们添加了场景对象,添加了照相机对象,然后把照相机放在了位置(-20, 30, 40)处,并让照相机的焦点汇聚在点(0, 0, 0)处。并且设置渲染器:

  • canvas: this.canvas 渲染器绘制输出的目标为canvas画布
  • antialias: true 是否抗锯齿?默认为false,这里打开抗锯齿。
  • alpha: true 画布是否包含透明度缓冲区?默认值为false,这里打开透明度,这样可以让我们设置的背景可见。

运行上面的代码后,发现还是没什么变化,为了方便观察,我们加上坐标轴:

1
2
3
4
5
6
7
8
9
init() {
//...
axex();
this.render();
}
axes() {
const axes = new THREE.AxesHelper(10);
this.scene.add(axes);
}

如果看到视窗中心出现了坐标轴,那么我们目前的场景是初始化成功了的,可以进行下一步了。

添加光源

我们创建地球是一个具有凹凸文理的地球,所以是需要使用感光材料来创建的,我们需要先添加光源。

光源需要两个,一个是环境光ambiLight,均匀照亮场景中的物体,没有方向,可以充当漫反射的光源。另一个是有方向的点光源DirectionalLight,从一个点方向发射的平行光,,可以来充当太阳。

具体代码:

1
2
3
4
5
6
7
8
9
10
public addLight() {
// 均匀照亮场景中的物体,没有方向
const ambiLight = new THREE.AmbientLight(0x111111);
this.scene.add(ambiLight);
// 点光源
const spotLight = new THREE.DirectionalLight(0xffffff);
spotLight.position.set(-20, 30, 40);
spotLight.intensity = 1.5;
this.scene.add(spotLight);
}

环境光不需要太强,我们设置的比较暗;我们把“太阳“放置在了摄像机的位置,然后给了一个光的强度intensity

构建地球

首先,我们需要加载两个贴图:

1
2
const planetTexture  = new THREE.TextureLoader().load( '/assets/img/plane/Earth.png' );
const bumpTexture = new THREE.TextureLoader().load( '/assets/img/plane/EarthNormal.png' );

然后创建球体的材质:

1
2
3
4
const planetMaterial = new THREE.MeshPhongMaterial({
map: planetTexture,
bumpMap: bumpTexture,
});

MeshPhongMaterial创建的材料具有感光性,灯光射过来的时候才可以看见,否则就是一团黑。

在创建一个带网格的简单材质:

1
2
3
const wireMaterial = new THREE.MeshBasicMaterial({
wireframe: true,
});

这个可以充当经线和纬线。

创建地球的几何体:

1
const planetGeom = new THREE.SphereGeometry(20, 40, 40);

创建了一个半径为20的球体,水平分段数和垂直分段数为40.

通过几何体和材质来

1
const planet = THREE.SceneUtils.createMultiMaterialObject(planetGeom, [planetMaterial, wireMaterial]);

运行时发现报了错:

1
THREE.SceneUtils has been moved to /examples/js/utils/SceneUtils.js

🤔这意思是SceneUtils被挪出去了啊。。。。

/node_modules/three/examples/js/utils/SceneUtils.js里查看发现是有这个文件的,但是没法子引入,还好里面方法不是很长,那我直接拷过来用好了:

1
2
3
4
5
6
7
createMultiMaterialObject( geometry, materials ) {
const group = new THREE.Group();
for ( let i = 0, l = materials.length; i < l; i ++ ) {
group.add( new THREE.Mesh( geometry, materials[ i ] ) );
}
return group;
}

然后创建地球的代码变成了这样:

1
2
const planet = this.createMultiMaterialObject(planetGeom, [planetMaterial, wireMaterial]);
this.scene.add(planet);

最后运行代码,可以发现我们成功的创建了地球🌍~
earth-2
这里为什么使用这个SceneUtils里面的createMultiMaterialObject方法来创建球体呢?我们可以尝试一下使用常见的THREE.Mesh来创建:

1
const planet = new THREE.Mesh(planetGeom, planetMaterial);

发现也是可以正常创建的。之所以用这个SceneUtils,是为了包含材质中定义的每种材质的新网格。也就是将同一几何体不同材质创建的网格添加到一个group中,和为一个网格定义多重材质的做法不一样,这种方式对同时需要材质和线框的对象非常有用。在当前粒子里面,我们可以创建一个真实的地球,并画出了经纬线。

添加路径控制

目前我们的地球是静止不动的,我们需要给它加上自转,并且可以通过鼠标来控制。

我们需要使用轨道控制THREE.OrbitControls来辅助我们,轨道控制允许摄像机围绕目标进行轨道运行。

这个OrbitControls也是被分离出去了,在/examples/js/controls/OrbitControls.js中,我们需要包含进来,这个库的内容有点多,不可能直接拷进来,我们需要对THREE的导入进行改造:

1
2
3
4
import * as THREE from 'three'; // build/three.js from node_module/three
window.THREE = THREE;
require('three/examples/js/controls/OrbitControls.js');
export default THREE;

创建了一个three.js文件,先引入three,然后加载扩展库,最后全部导出,这样我们在程序中就可以使用:

1
2
3
4
5
6
7
8
9
orbitControls: THREE.OrbitControls;
clock: THREE.Clock;
addControl() {
this.orbitControls = new THREE.OrbitControls(this.camera);
this.orbitControls.enableDamping = true;
this.orbitControls.autoRotate = true;
this.orbitControls.autoRotateSpeed = 1;
this.clock = new THREE.Clock();
}

轨道控制接受一个摄像机对象,然后控制摄像机的旋转、平移。我们设置了:

  • enableDamping 是否使用阻尼惯性,默认为false。
  • autoRotate 是否开启自动旋转,默认为false
  • autoRotateSpeed 自动旋转的速度。默认值为2.0

其他参数参见文档

这里我们使用了THREE.Clock(),这个用来跟踪时间对象。地球是不断自转的,但是我们可以按下鼠标左键自由控制,当鼠标方开始,要根据当前的位置继续进行自转,而当前的位置更新就需要从这个clock里面进行读取:

1
2
3
4
animate() {
const delta = this.clock.getDelta();
this.orbitControls.update(delta);
}

现在运行可以看到,地球会自转,然后鼠标左键可以拖动旋转,鼠标右键可以平移,键盘的上下左右也可以控制~


通过这个练习可以综合的对光源、材质、物体以及路径控制有个大体的概念,下一步可以考虑使用d3-geo来创建可以定点的地球。

点击查看源代码
点击查看demo

码字辛苦,打赏个咖啡☕️可好?💘