我们实现标题中的效果基本分为接下来的这些功能点,如果看过“Threejs项目实战 - 省市图”,会对这些功能有一定的了解,特别是开启辉光效果和后续的飞线效果。
地球
绘制地球实体
- 加载地球贴图纹理
import earthChartletImage from './images/earth-chartlet.jpg';
let texture = new THREE.TextureLoader()
texture.load( earthChartletImage, ( texture ) => {
// 在这里会做第2步操作
});
- 通过加载好的纹理,创建一个实体,并添加到scene中
import earthChartletImage from './images/earth-chartlet.jpg';
const earthGroup = new THREE.Group();
let texture = new THREE.TextureLoader()
texture.load( earthChartletImage, ( texture ) => {
// 创建个圆形
var globeGgeometry = new THREE.SphereGeometry( 5, 100, 100 );
var globeMaterial = new THREE.MeshStandardMaterial( { map: texture, side:THREE.DoubleSide } );
var globeMesh = new THREE.Mesh( globeGgeometry, globeMaterial );
earthGroup.rotation.set(0.5, 2.9, 0.1);
earthGroup.add( globeMesh );
scene.add( earthGroup );
});
地图轮廓
绘制中国轮廓地图
- 绘制地图工具方法:createMap.js
import { util } from './util.js';
import * as THREE from 'three';
import * as d3 from 'd3-geo';
class CreateMap {
drawMap(mapUrl) {
let mapData = util.decode(mapUrl);
if (!mapData) {
console.error('mapData 数据不能是null');
return;
}
// 把经纬度转换成x,y,z 坐标
mapData.features.forEach(d => {
d.vector3 = [];
d.geometry.coordinates.forEach((coordinates, i) => {
d.vector3[i] = [];
coordinates.forEach((c, j) => {
if (c[0] instanceof Array) {
d.vector3[i][j] = [];
c.forEach(cinner => {
let cp = this.lglt2xyz(cinner);
d.vector3[i][j].push(cp);
});
} else {
let cp = this.lglt2xyz(c);
d.vector3[i].push(cp);
}
});
});
});
// 绘制地图模型
let group = new THREE.Group();
mapData.features.forEach(d => {
// if (d.properties.name !== '山东省') {
let g = new THREE.Group(); // 用于存放每个地图模块。||省份
g.data = d;
g.name = d.properties.name;
d.vector3.forEach(points => {
// 多个面
if (points[0][0] instanceof Array) {
points.forEach(p => {
let lineMesh = this.drawLine(p);
g.add(lineMesh);
});
} else {
// 单个面
let lineMesh = this.drawLine(points);
g.add(lineMesh);
}
});
group.add(g);
// }
});
// scene.add(group);
return group
}
/**
* @desc 绘制线条
* @param {} points
*/
drawLine(points) {
let positionsArry = [];
// points.forEach(d => {
// let [x, y, z] = d;
// positionsArry.push(x, y, z + 0.01)
// });
points.forEach(d => {
if (d instanceof Array) {
d.forEach(a => {
let [x, y, z] = a;
positionsArry.push(x, y, z)
})
} else {
let [x, y, z] = d;
positionsArry.push(x, y, z)
}
});
let positions = new Float32Array(positionsArry)
let lineGeo = new THREE.BufferGeometry();
lineGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3))
let material = new THREE.LineBasicMaterial({
color: 0x3BFA9E, // 0x3BFA9E 0XF19553
transparent: true,
opacity: 1,
// side: THREE.DoubleSide
});
let line = new THREE.Line(lineGeo, material);
return line;
}
/**
* @desc 经纬度转换成墨卡托投影
* @param {array} 传入经纬度
* @return array [x,y,z]
*/
lnglatToMector(lnglat) {
if (!this.projection) {
this.projection = d3
.geoMercator()
.center([108.904496, 32.668849])
.scale(80)
.rotate(Math.PI / 4)
.translate([0, 0]);
}
let [y, x] = this.projection([...lnglat]);
let z = 0;
return [x, y, z];
}
// threejs自带的经纬度转换
lglt2xyz(lnglat) {
let lng = lnglat[0];
let lat = lnglat[1];
const theta = ( 90 + lng ) * ( Math.PI / 180 );
const phi = ( 90 - lat ) * ( Math.PI / 180 );
return ( new THREE.Vector3() ).setFromSpherical( new THREE.Spherical( 5, phi, theta ) );
}
}
export const mapUtil = new CreateMap();
- 通过工具类创建中国地图轮廓
import { mapUtil } from './createMap.js';
data() {
return {
chinaMapUrl: require('./map/china.json'),
};
},
// 绘制中国地图轮廓
chinaModel = mapUtil.drawMap(this.chinaMapUrl);
earthGroup.add(chinaModel);
- 添加地图流光效果,这里我统一集成到了createMap.js中了
createMap.js
import { util } from './util.js';
import * as THREE from 'three';
import * as d3 from 'd3-geo';
import { flyLineUtil } from './createFlyLine.js';
class CreateMap {
drawMap(mapUrl) {
let mapData = util.decode(mapUrl);
if (!mapData) {
console.error('mapData 数据不能是null');
return;
}
// 把经纬度转换成x,y,z 坐标
mapData.features.forEach(d => {
d.vector3 = [];
d.geometry.coordinates.forEach((coordinates, i) => {
d.vector3[i] = [];
coordinates.forEach((c, j) => {
if (c[0] instanceof Array) {
d.vector3[i][j] = [];
c.forEach(cinner => {
let cp = this.lglt2xyz(cinner);
d.vector3[i][j].push(cp);
});
} else {
let cp = this.lglt2xyz(c);
d.vector3[i].push(cp);
}
});
});
});
// 绘制地图模型
let group = new THREE.Group();
mapData.features.forEach(d => {
// if (d.properties.name !== '山东省') {
let g = new THREE.Group(); // 用于存放每个地图模块。||省份
g.data = d;
g.name = d.properties.name;
d.vector3.forEach(points => {
// 多个面
if (points[0][0] instanceof Array) {
points.forEach(p => {
// let mesh = this.drawModel(p);
let lineMesh = this.drawLine(p);
// g.add(mesh);
g.add(lineMesh);
});
} else {
// 单个面
// let mesh = this.drawModel(points);
let lineMesh = this.drawLine(points);
// g.add(mesh);
g.add(lineMesh);
}
});
group.add(g);
// }
});
// scene.add(group);
return group
}
/**
* @desc 绘制线条
* @param {} points
*/
drawLine(points) {
let positionsArry = [];
// points.forEach(d => {
// let [x, y, z] = d;
// positionsArry.push(x, y, z + 0.01)
// });
points.forEach(d => {
if (d instanceof Array) {
d.forEach(a => {
let [x, y, z] = a;
positionsArry.push(x, y, z)
})
} else {
let [x, y, z] = d;
positionsArry.push(x, y, z)
}
});
let positions = new Float32Array(positionsArry)
let lineGeo = new THREE.BufferGeometry();
lineGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3))
let material = new THREE.LineBasicMaterial({
color: 0x3BFA9E, // 0x3BFA9E 0XF19553
transparent: true,
opacity: 1,
// side: THREE.DoubleSide
});
let line = new THREE.Line(lineGeo, material);
return line;
}
// 画流光
drawTimeLine(mapUrl) {
let mapData = util.decode(mapUrl);
if (!mapData) {
console.error('mapData 数据不能是null');
return;
}
let group = new THREE.Group();
mapData.features.forEach( elem => {
// 新建一个省份容器:用来存放省份对应的模型和轮廓线
const province = new THREE.Object3D();
const coordinates = elem.geometry.coordinates;
coordinates.forEach( multiPolygon => {
multiPolygon.forEach( polygon => {
// 这里的坐标要做2次使用:1次用来构建模型,1次用来构建轮廓线
if (polygon.length > 200) {
let v3ps = [];
for (let i = 0; i < polygon.length; i ++) {
let pos = this.lglt2xyz( polygon[i] );
v3ps.push( pos );
}
let curve = new THREE.CatmullRomCurve3( v3ps, false/*是否闭合*/ );
let color = new THREE.Vector3( 0.5999758518718452, 0.7798940272761521, 0.6181903838257632 );
let flyLine = flyLineUtil.createFlyLine( curve, {
speed: 0.15,
// color: randomVec3Color(),
color: color,
number: 2, //同时跑动的流光数量
length: 0.2, //流光线条长度
size: 2 //粗细
}, 5000 );
province.add( flyLine );
}
} );
} );
group.add( province );
} );
return group;
}
/**
* @desc 经纬度转换成墨卡托投影
* @param {array} 传入经纬度
* @return array [x,y,z]
*/
lnglatToMector(lnglat) {
if (!this.projection) {
this.projection = d3
.geoMercator()
.center([108.904496, 32.668849])
.scale(80)
.rotate(Math.PI / 4)
.translate([0, 0]);
}
let [y, x] = this.projection([...lnglat]);
let z = 0;
return [x, y, z];
}
// threejs自带的经纬度转换
lglt2xyz(lnglat) {
let lng = lnglat[0];
let lat = lnglat[1];
const theta = ( 90 + lng ) * ( Math.PI / 180 );
const phi = ( 90 - lat ) * ( Math.PI / 180 );
return ( new THREE.Vector3() ).setFromSpherical( new THREE.Spherical( 5, phi, theta ) );
}
}
export const mapUtil = new CreateMap();
createFlyLine.js
import { Line2 } from "three/examples/jsm/lines/Line2";
import { LineGeometry } from "three/examples/jsm/lines/LineGeometry";
import { LineMaterial } from "three/examples/jsm/lines/LineMaterial";
// 顶点着色器
let vertexShader = `
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;
}`;
// 分片着色器
let fragmentShader = `
#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);
}`;
let commonUniforms = {
u_time: { value: 0.0 },
};
/**
* @param curve {THREE.Curve} 路径,
* @param matSetting {Object} 材质配置项
* @param pointsNumber {Number} 点的个数 越多越细致
* */
createFlyLine (curve, matSetting, pointsNumber) {
var points = curve.getPoints( pointsNumber );
var geometry = new THREE.BufferGeometry().setFromPoints( points );
let length = points.length;
var percents = new Float32Array(length);
for (let i = 0; i < points.length; i+=1){
percents[i] = (i/length);
}
geometry.setAttribute('percent', new THREE.BufferAttribute(percents,1));
let lineMaterial = this.initLineMaterial(matSetting);
var flyLine = new THREE.Points( geometry, lineMaterial );
return flyLine
}
// 首先要写出一个使用fragmentshader生成的material并赋在点上
initLineMaterial(setting){
let number = setting ? (Number(setting.number) || 1.0) : 1.0;
let speed = setting ? (Number(setting.speed) || 1.0) : 1.0;
let length = setting ? (Number(setting.length) || 0.5) : 0.5;
let size = setting ?(Number(setting.size) || 3.0) : 3.0;
let color = setting ? setting.color || new THREE.Vector3(0,1,1) : new THREE.Vector3(0,1,1);
let singleUniforms = {
u_time: commonUniforms.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}
};
let lineMaterial = new THREE.ShaderMaterial({
uniforms: singleUniforms,
vertexShader: vertexShader,
fragmentShader: fragmentShader,
transparent: true,
//blending:THREE.AdditiveBlending,
});
return lineMaterial;
}
- 通过工具类创建中国地图轮廓流光效果
// 绘制中国轮廓流光
chinaTimeLineModel = mapUtil.drawTimeLine(this.chinaTimeLineUrl);
earthGroup.add(chinaTimeLineModel);
创建辉光图层
上面的流光效果是要开启辉光图层的,这里在“Threejs项目实战 - 省市图”专栏中有讲解,在这里会再讲一遍
将辉光效果写了一个工具类:createGlow.js
- createGlow.js:主要是处理一个创建layer层及创建通道效果
import * as THREE from 'three';
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js'
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass';
// 顶点着色器
let hgVertexShader = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`
// 分片着色器
let hgShaderMaterials = `
uniform sampler2D baseTexture;
uniform sampler2D bloomTexture;
varying vec2 vUv;
void main() {
gl_FragColor = ( texture2D( baseTexture, vUv ) + vec4( 1.0 ) * texture2D( bloomTexture, vUv ) );
}`
class createGlow {
// 创建一个 Layer,用于区分辉光物体
createLayer(num) {
const layer = new THREE.Layers()
layer.set(num)
return layer
}
// 辉光效果
createUnrealBloomPass() {
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
1.5,
0.4,
0.0
)
const params = {
bloomThreshold: 0,
bloomStrength: 1, // 辉光强度
bloomRadius: 0
}
bloomPass.threshold = params.bloomThreshold
bloomPass.strength = params.bloomStrength
bloomPass.radius = params.bloomRadius
return bloomPass
}
// ShaderPass,着色器pass,自定义程度高,需要编写OpenGL代码
// 传入bloomComposer
createShaderPass(bloomComposer) {
// 着色器材质,自定义shader渲染的材质
const shaderMaterial = new THREE.ShaderMaterial({
uniforms: {
baseTexture: { value: null },
// 辉光贴图属性设置为传入的bloomComposer,这里就说明了为什么bloomComposer不要渲染到屏幕上
bloomTexture: { value: bloomComposer.renderTarget2.texture }
},
vertexShader: hgVertexShader, // 顶点着色器
fragmentShader: hgShaderMaterials, // 片元着色器
defines: {}
})
const shaderPass = new ShaderPass(shaderMaterial, 'baseTexture')
shaderPass.needsSwap = true
return shaderPass
}
}
export const glowUtil = new createGlow();
- 使用工具类创建辉光图层:以下是具体使用方法
// 辉光图层相关变量
let composer = null;
let outlinePass = null;
let bloomLayer = null;
let bloomComposer = null;
let darkMaterial = null;
let hgMaterials = {}
let bloomIgnore = [];
// 创建辉光图层
createGlowPass() {
bloomLayer = glowUtil.createLayer(1);
// 辉光层默认样式
darkMaterial = new THREE.MeshBasicMaterial({ color: 'black' })// 跟辉光光晕有关的变量
this.createSawtooth();// 抗锯齿
}
// 抗锯齿,添加辉光层效果
createSawtooth() {
var renderPass = new RenderPass(scene, camera)
// 抗锯齿设置
var effectFXAA = new ShaderPass(FXAAShader)
effectFXAA.uniforms['resolution'].value.set(
0.6 / window.innerWidth,
0.6 / window.innerHeight
) // 渲染区域Canvas画布宽高度 不一定是全屏,也可以是区域值
effectFXAA.renderToScreen = true
// 我们封装好的 createUnrealBloomPass 函数,用来创建BloomPass(辉光效果)
const bloomPass = glowUtil.createUnrealBloomPass()
bloomComposer = new EffectComposer(renderer)
bloomComposer.renderToScreen = false // 不渲染到屏幕上
bloomComposer.addPass(renderPass)
bloomComposer.addPass(bloomPass)// 添加光晕效果
// bloomComposer.addPass(effectFXAA)// 去掉锯齿
// 创建自定义的着色器Pass,详细见下
const shaderPass = glowUtil.createShaderPass(bloomComposer)
composer = new EffectComposer(renderer)
composer.addPass(renderPass)
outlinePass = new OutlinePass(new THREE.Vector2(window.innerWidth, window.innerHeight), scene, camera)
outlinePass.edgeStrength = 5 // 包围线浓度
outlinePass.edgeGlow = 0.5 // 边缘线范围
outlinePass.edgeThickness = 2// 边缘线浓度
outlinePass.pulsePeriod = 2// 包围线闪烁评率
outlinePass.visibleEdgeColor.set('#ffffff') // 包围线颜色
outlinePass.hiddenEdgeColor.set('#190a05')// 被遮挡的边界线颜色
composer.addPass(outlinePass)
composer.addPass(shaderPass)
//composer.addPass(effectFXAA)
}
// 隐藏不需要辉光的物体
darkenNonBloomed(obj) {
if (obj instanceof THREE.Scene) { // 此处忽略Scene,否则场景背景会被影响
hgMaterials.scene = obj.background
obj.background = null
return;
}
if (
obj instanceof THREE.Sprite || // 此处忽略Sprite
bloomIgnore.includes(obj.type) || obj instanceof THREE.Line ||
(obj.isMesh && bloomLayer.test(obj.layers) === false) // 判断与辉光是否同层
) {
if(obj.name.indexOf('sd-line') !== -1)
return;
hgMaterials[obj.uuid] = obj.material
obj.material = darkMaterial
}
}
// 还原非辉光体
restoreMaterial(obj) {
if (obj instanceof THREE.Scene) {
obj.background = hgMaterials.scene
delete hgMaterials.scene
return;
}
if (hgMaterials[obj.uuid]) {
obj.material = hgMaterials[obj.uuid]
delete hgMaterials[obj.uuid]
}
}
render() {
scene.traverse(this.darkenNonBloomed) // 隐藏不需要辉光的物体
bloomComposer.render()
scene.traverse(this.restoreMaterial) // 还原
// 更新性能插件
stats.update();
TWEEN.update();
renderer.render(scene, camera);
requestAnimationFrame(this.render);
// 呼吸灯效果要放到最后渲染,要不然没效果
if (composer) {
composer.render();
}
}
这样上面的飞线就会在辉光图层下显示出发光的效果了
添加地球光晕轮廓
这里我们就通过一个贴图简单的来搞了
- 准备一个光晕贴图
- 通过材质添加到刚才的earthGroup上
import earthApertureImage from './images/earth-aperture.png';
var texture = new THREE.TextureLoader().load( earthApertureImage );
var spriteMaterial = new THREE.SpriteMaterial( {
map: texture,
transparent: true,
opacity: 0.5,
depthWrite: false
} );
var sprite = new THREE.Sprite( spriteMaterial );
sprite.scale.set( 5 * 3, 5 * 3, 1 );
earthGroup.add( sprite );
TWEEN
TWEEN实现地球视图拉近
- scene视图加载时先将摄像头视图定位在远处
createCamera() {
let element = document.getElementById("container");
camera = new THREE.PerspectiveCamera(
45,
element.clientWidth / element.clientHeight,
0.1,
1000
);
camera.position.set( 16, 16, 160 ); // 设置相机方向
// camera.position.set( 1.6, 1.6, 16 ); // 设置相机方向
camera.lookAt(new THREE.Vector3(0, 0, 0)); // 设置相机方向
scene.add(camera);
},
- 地球视图渲染完毕之后,再将camera和controls设置到实体近处,就能实现地球从远处由小变大的展示到眼前了
import { TWEEN } from "three/examples/jsm/libs/tween.module.min.js";
// 拉进摄像头
this.moveCamera(
camera.position,
controls.target,
{ x: 1.6, y: 1.6, z: 16 },
{ x: 0, y: 0.4, z: 0 },
() => {
initFlag = true
}
)
// 移动摄像机
moveCamera(oldP, oldT, newP, newT, callback) {
let tween = new TWEEN.Tween({
x1: oldP.x,
y1: oldP.y,
z1: oldP.z,
x2: oldT.x,
y2: oldT.y,
z2: oldT.z
});
tween.to(
{
x1: newP.x,
y1: newP.y,
z1: newP.z,
x2: newT.x,
y2: newT.y,
z2: newT.z
},
1500
);
// 每一帧执行函数 、这个地方就是核心了、每变一帧跟新一次页面元素
tween.onUpdate((object) => {
camera.position.set(object.x1, object.y1, object.z1);
controls.target.x = object.x2;
controls.target.y = object.y2;
controls.target.z = object.z2;
controls.update();
});
// 动画完成后的执行函数
tween.onComplete(() => {
controls.enabled = true;
callback && callback();
// this.tweenCallBack && this.tweenCallBack();
});
tween.easing(TWEEN.Easing.Cubic.InOut);
// 这个函数必须有、这个是启动函数、不加不能启动
tween.start();
}
自转动画
启动地球自转效果
- 处理视图拉近之后才执行自转效果
let initFlag = false;
// 拉进摄像头
this.moveCamera(
camera.position,
controls.target,
{ x: 1.6, y: 1.6, z: 16 },
{ x: 0, y: 0.4, z: 0 },
() => {
initFlag = true
}
);
- 通过动画不断改变y轴实现自转
render() {
// 地球自转动画
this.earthRotationAnimation();
scene.traverse(this.darkenNonBloomed) // 隐藏不需要辉光的物体
bloomComposer.render()
scene.traverse(this.restoreMaterial) // 还原
// 更新性能插件
// stats.update();
TWEEN.update();
renderer.render(scene, camera);
requestAnimationFrame(this.render);
// 呼吸灯效果要放到最后渲染,要不然没效果
if (composer) {
composer.render();
}
}
// 地球自转动画
earthRotationAnimation() {
if (initFlag) {
earthGroup.rotation.y = earthGroup.rotation.y + 0.002;
}
},
PS:项目源码及3d模型会在第一篇文章给出下载地址,或者添加wx:z13964122832备注“全球图源码”