简介
本篇介绍一下如何用Three.js实现一个简单的3D地球。前面已经把基础讲解了,现在开始实现一些小示例。
开始绘制
基础环境搭建
- 先把Three.js的基础模块,渲染器、相机、场景、灯光、纹理加载器等创建好。这些都是开发一个项目必备的基础模块,不了解的可以去看前面的文章。本文主要介绍如何实现3D地球可视化这些基础就默认你会了。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>学习</title>
</head>
<body>
<canvas id="c3d" class="c2d" width="1200" height="800"></canvas>
<script type="module">
import * as THREE from './file/three.js-dev/build/three.module.js'
import { OrbitControls } from './file/three.js-dev/examples/jsm/controls/OrbitControls.js'
const Dom = document.querySelector('#c3d')
const width = Dom.clientWidth
const height = Dom.clientHeight
// 纹理加载器
const loader = new THREE.TextureLoader()
// 渲染器
let renderer
// 相机
let camera
// 场景
let scene
// 灯光
let light
// 相机控制
let controls
/**
* 初始化渲染器
* */
function initRenderer() {
// antialias: true, alpha: true 抗锯齿设置
renderer = new THREE.WebGLRenderer({ canvas: Dom, antialias: true, alpha: true })
// window.devicePixelRatio 设备像素比
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(width, height)
}
/**
* 初始化相机
*/
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
}
/**
* 初始化场景
*/
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
}
/**
* 初始化光
*/
function initLight() {
// 环境光
const ambientLight = new THREE.AmbientLight(0xcccccc, 1.1)
scene.add(ambientLight)
// 平行光
let directionalLight = new THREE.DirectionalLight(0xffffff, 0.2)
directionalLight.position.set(1, 0.1, 0).normalize()
// 平行光2
let directionalLight2 = new THREE.DirectionalLight(0xff2ffff, 0.2)
directionalLight2.position.set(1, 0.1, 0.1).normalize()
scene.add(directionalLight)
scene.add(directionalLight2)
// 半球光
let hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.2)
hemiLight.position.set(0, 1, 0)
scene.add(hemiLight)
// 平行光3
let directionalLight3 = new THREE.DirectionalLight(0xffffff)
directionalLight3.position.set(1, 500, -20)
// 开启阴影
directionalLight3.castShadow = true
// 设置光边界
directionalLight3.shadow.camera.top = 18
directionalLight3.shadow.camera.bottom = -10
directionalLight3.shadow.camera.left = -52
directionalLight3.shadow.camera.right = 12
scene.add(directionalLight3)
}
/**
* 渲染函数
* */
function renders(time) {
time *= 0.003
renderer.clear()
renderer.render(scene, camera)
}
/**
* 动画渲染函数
*/
function animate() {
window.requestAnimationFrame((time) => {
if (controls) controls.update()
renders(time)
animate()
})
}
window.onload = () => {
// 初始化
initRenderer()
initCamera()
initScene()
initLight()
initControls()
// 渲染
animate()
}
</script>
</body>
</html>
- 因为需要使用纹理加载器,就需要启动服务。这是在根目录执行(需要安装node)。
npx http-server
- 就会在根目录启动一个服务。
创建星空背景
- 既然是地图的背景,用动态星空的方式更加合理。
- 使用图片贴图,让图片中的图形模拟球形,然后动态设置颜色、旋转偏移等,更好的模拟星空效果。
- 这里使用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
}
- 随机生成10000个坐标。
- 创建一个几何体,把这些坐标转换为
Vector3
坐标,保存在position
中。 - 使用
ParticleBasicMaterial
材质,来控制每个坐标要展示的图案(纹理)、大小、透明度等。 - 使用
ParticleSystem
网格,在这些坐标上绘制要展示的粒子。 - 最后加入场景中。
/**
* 背景绘制
* */
function bg() {
const positions = []
const colors = []
// 创建 几何体
const geometry = new THREE.BufferGeometry()
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)
}
// 对几何体 设置 坐标 和 颜色
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
// 默认球体
geometry.computeBoundingSphere()
// ------------- 1 ----------
// 星星资源图片
// ParticleBasicMaterial 点基础材质
var starsMaterial = new THREE.ParticleBasicMaterial({
map: generateSprite(),
size: 2,
transparent: true,
opacity: 1,
//true:且该几何体的colors属性有值,则该粒子会舍弃第一个属性--color,而应用该几何体的colors属性的颜色
// vertexColors: true,
blending: THREE.AdditiveBlending,
sizeAttenuation: true
})
// 粒子系统 网格
let stars = new THREE.ParticleSystem(geometry, starsMaterial)
stars.scale.set(300, 300, 300)
scene.add(stars)
}
- 调用绘制方法
window.onload = () => {
...
// 绘制
bg()
...
}
创建地球和月球
- 我们知道月球要围绕地球转,地球需要自转。通过计算的方式去修改坐标非常的复杂,这是使用
Object3D
创建内部场景的方式来实现旋转。 - 修改渲染函数,添加旋转动画队列。
// 旋转队列
const rotateSlowArr = []
/**
* 渲染函数
* */
function renders(time) {
time *= 0.003
// 3D对象 旋转
// _y 初始坐标 _s 旋转速度
rotateSlowArr.forEach((obj) => {
obj.rotation.y = obj._y + time * obj._s
})
renderer.clear()
renderer.render(scene, camera)
}
- 创建3个内部场景,用来控制地球和月球单独旋转。只需要设置旋转场景,在场景中固定位置的几何体,就会按场景的原点进行旋转。
- 通过
sphereGeometry
创建球形几何体。 - 通过
loader.load
加载地球和月球纹理图,设置给材质。 - 创建网格加入对应的内部场景。
- 设置场景的旋转参数,加入旋转队列中。
// 地球,月亮 3D层
const landOrbitObject = new THREE.Object3D()
// 地球3D层
const earthObject = new THREE.Object3D()
// 月亮3D层
const moonObject = new THREE.Object3D()
// 地球半径
const globeRadius = 5
/**
* 球相关加载
* */
function earth() {
const radius = globeRadius
const widthSegments = 100
const heightSegments = 100
const sphereGeometry = new THREE.SphereGeometry(radius, widthSegments, heightSegments)
// 地球
const earthTexture = loader.load('./img/3.jpg')
const earthMaterial = new THREE.MeshStandardMaterial({
map: earthTexture
})
const earthMesh = new THREE.Mesh(sphereGeometry, earthMaterial)
// 月球
const moonTexture = loader.load('./img/2.jpg')
const moonMaterial = new THREE.MeshPhongMaterial({ map: moonTexture })
const moonMesh = new THREE.Mesh(sphereGeometry, moonMaterial)
moonMesh.scale.set(0.1, 0.1, 0.1)
moonMesh.position.x = 10
moonObject.add(moonMesh)
// 加入动画队列
moonObject._y = 0
moonObject._s = 1
rotateSlowArr.push(moonObject)
// 地球加入 地球3D层
earthObject.add(earthMesh)
earthObject.rotation.set(0.5, 2.9, 0.1)
earthObject._y = 2.0
earthObject._s = 0.1
// 加入动画队列
rotateSlowArr.push(earthObject)
// 加入 地球3D层
landOrbitObject.add(earthObject)
// 加入 月亮3D层
landOrbitObject.add(moonObject)
scene.add(landOrbitObject)
}
...
// 绘制
earth()
...
绘制目标点
- 想要在球形几何体上绘图,我们就需要知道要绘制的坐标。创建公用函数,经维度转换坐标。
/**
* 经维度 转换坐标
* 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 bigByOpacityArr = []
function renders(time) {
time *= 0.003
bigByOpacityArr.forEach(function (mesh) {
// 目标 圆环放大 并 透明
mesh._s += 0.01
mesh.scale.set(1 * mesh._s, 1 * mesh._s, 1 * mesh._s)
if (mesh._s <= 2) {
mesh.material.opacity = 2 - mesh._s
} else {
mesh._s = 1
}
})
}
- 在对应坐标创建几何体,指向圆心。把需要动画的网格加入动画队列。最后加入地球内部内部场景,这些几何体也需要和地球几何体一起旋转。
/**
* 绘制 目标点
* */
function spotCircle(spot) {
// 圆
const geometry1 = new THREE.CircleGeometry(0.02, 100)
const material1 = new THREE.MeshBasicMaterial({ color: 0xff0000, 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)
earthObject.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))
earthObject.add(circleY)
// 加入动画队列
bigByOpacityArr.push(circleY)
}
/**
* 画图
* */
function drawChart() {
let markPos = lglt2xyz(106.553091, 29.57337, 5)
// 目标点
spotCircle([markPos.x, markPos.y, markPos.z])
}
绘制飞线
- 在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.addAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3, true))
lineGeometry.addAttribute('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)
earthObject.add(line)
}
- 在线上移动的物体就简单了。根据三维三次贝塞尔曲线得到的点,绘制一个几何体。把点缓存下来,加入移动队列进行动画。
/**
* 线上移动物体
* */
function moveSpot(curve) {
// 线上的移动物体
const aGeo = new THREE.SphereGeometry(0.04, 0.04, 0.04)
const aMater = new THREE.MeshPhongMaterial({ color: 0xff0000, side: THREE.DoubleSide })
const aMesh = new THREE.Mesh(aGeo, aMater)
// 保存曲线实例
aMesh.curve = curve
aMesh._s = 0
moveArr.push(aMesh)
earthObject.add(aMesh)
}
- 移动动画,获取当前时间在
curve
上的点位置,修改移动物体的坐标。
// 移动 队列
const moveArr = []
function renders(time) {
time *= 0.003
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 drawChart() {
let markPos = lglt2xyz(106.553091, 29.57337, 5)
// 目标点
spotCircle([markPos.x, markPos.y, markPos.z])
let markPos2 = lglt2xyz(106.553091, 33.57337, 5)
// 目标点
spotCircle([markPos2.x, markPos2.y, markPos2.z])
let markPos3 = lglt2xyz(111.553091, 29.57337, 5)
// 目标点
spotCircle([markPos3.x, markPos3.y, markPos3.z])
lineConnect([121.48, 31.23], [116.4, 39.91])
lineConnect([121.48, 31.23], [121.564136, 25.071558])
lineConnect([121.48, 31.23], [104.896185, 11.598253])
lineConnect([121.48, 31.23], [130.376441, -16.480708])
lineConnect([121.48, 31.23], [-71.940328, -13.5304])
lineConnect([121.48, 31.23], [-3.715707, 40.432926])
}
边界炫光
- 根据geojson数据,使用线几何体创建边界
- 使用粒子动画在边界上隐藏显示,实现炫光效果。
- 详细介绍 实现地图边界炫光路径效果
/**
* 定义 着色器
**/
const vertexShader = `
attribute float aOpacity;
uniform float uSize;
varying float vOpacity;
void main(){
gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.0);
gl_PointSize = uSize;
vOpacity=aOpacity;
}
`
const fragmentShader = `
varying float vOpacity;
uniform vec3 uColor;
float invert(float n){
return 1.-n;
}
void main(){
if(vOpacity <=0.2){
discard;
}
vec2 uv=vec2(gl_PointCoord.x,invert(gl_PointCoord.y));
vec2 cUv=2.*uv-1.;
vec4 color=vec4(1./length(cUv));
color*=vOpacity;
color.rgb*=uColor;
gl_FragColor=color;
}
`
/**
* 边界炫光路径
* */
function dazzleLight() {
const loader = new THREE.FileLoader()
loader.load('./file/100000.json', (data) => {
const jsondata = JSON.parse(data)
// 中国边界
const feature = jsondata.features[0]
const province = new THREE.Object3D()
province.properties = feature.properties.name
// 点数据
const coordinates = feature.geometry.coordinates
coordinates.forEach((coordinate) => {
// coordinate 多边形数据
coordinate.forEach((rows) => {
// 绘制线
const line = lineDraw(rows, 0xaa381e)
province.add(line)
})
})
// 添加地图边界
earthObject.add(province)
// 拉平 为一维数组
const positions = new Float32Array(lines.flat(1))
// 设置顶点
geometryLz.setAttribute('position', new THREE.BufferAttribute(positions, 3))
// 设置 粒子透明度为 0
opacitys = new Float32Array(positions.length).map(() => 0)
geometryLz.setAttribute('aOpacity', new THREE.BufferAttribute(opacitys, 1))
geometryLz.currentPos = 0
// 炫光移动速度
geometryLz.pointSpeed = 20
// 控制 颜色和粒子大小
const params = {
pointSize: 2.0,
pointColor: '#4ec0e9'
}
// 创建着色器材质
const material = new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShader,
transparent: true, // 设置透明
uniforms: {
uSize: {
value: params.pointSize
},
uColor: {
value: new THREE.Color(params.pointColor)
}
}
})
const points = new THREE.Points(geometryLz, material)
earthObject.add(points)
})
}
/**
* 边框 图形绘制
* @param polygon 多边形 点数组
* @param color 材质颜色
* */
let indexBol = true
function lineDraw(polygon, color) {
const lineGeometry = new THREE.BufferGeometry()
const pointsArray = new Array()
polygon.forEach((row) => {
// 转换坐标
const xyz = lglt2xyz(row[0], row[1], globeRadius)
// 创建三维点
pointsArray.push(xyz)
if (indexBol) {
// 为了好看 这里只要内陆边界
lines.push([xyz.x, xyz.y, xyz.z])
}
})
indexBol = false
// 放入多个点
lineGeometry.setFromPoints(pointsArray)
const lineMaterial = new THREE.LineBasicMaterial({
color: color
})
return new THREE.Line(lineGeometry, lineMaterial)
}