1.下载threejs
npm i three
2.创建一个js文件 创建一个类
import * as THREE from 'three'
//5-1---------引入屏幕后处理
import {EffectComposer} from 'three/addons/postprocessing/EffectComposer.js' //屏幕后处理 来处理发光
export default class Base{
constructor(){
//创建场景
this.scene=new THREE.Scene()
//创建一个透视相机
this.camera=new THREE.PerspectiveCamera(
75,//角度
window.innerWidth / window.innerHeight,//宽高比
0.1,//近端距离
1000,//远端距离
)
//创建一个渲染器
this.renderer=new THREE.WebGLRenderer({antialias:true}) //抗锯齿,边缘丝滑
this.renderer.setSize(window.innerWidth,window.innerHeight) //渲染器尺寸,宽高
//使物体更加清晰
this.renderer.setPixelRatio(window.devicePixelRatio)//像素比设置为设备像素比
//挂载渲染器到子页面容器上 vue文件里id命名的容器
document.getElementById("containers").appendChild(this.renderer.domElement)
//屏幕后处理 发光
this.composer= new EffectComposer(this.renderer)
this.composer.setSize(window.innerWidth,window.innerHeight)
}
//更新的时候要把渲染器重新渲染一下
update(){
this.renderer.render(this.scene,this.camera)
}
//屏幕后处理
updateComposer(){
this.composer.render()
}
//自适应的方法,当屏幕宽高改变的时候把相机的宽高比也改变一下
resize(){
this.camera.aspect=window.innerWidth / window.innerHeight
//更新投影矩阵
this.camera.updateProjectionMatrix()
//更新渲染器尺寸
this.renderer.setSize(window.innerWidth,window.innerHeight)
}
resizeComposer(){
this.composer.setSize(window.innerWidth,window.innerHeight)
}
//添加环境光
addAmbientLight(intensity=1,color=0xffffff){ //强度,颜色
//官网拿的
const light = new THREE.AmbientLight(color,intensity ); // soft white light
this.scene.add( light );
}
}
- 创建一个vue文件,在文件里创建一个容器 ,使用id选择器,(在js文件里获取这个容器来展示场景)
<!-- -->
<template>
<div>
<div class="threeJSBox">
<div id="containers">
<div class="label" ref="label">
<span>{{ provincename }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import Base from './Base0.js'
import * as THREE from 'three'
import * as d3 from 'd3' //球形坐标转化为 二维坐标
// 2-1------加载标记点数据
import {flylineAddress,airplaneAddress,centerAddress} from './constant.js'
import { OrbitControls} from 'three/addons/controls/OrbitControls.js' //引入轨道控制器
//5-1-------使用屏幕后处理高亮
//5-2-------添加渲染通道
import {RenderPass} from 'three/addons/postprocessing/RenderPass.js'
//5-2-------添加发光通道
import {UnrealBloomPass} from 'three/addons/postprocessing/UnrealBloomPass.js'
let base,controls;
const label=ref(null)
const provincename=ref('')
onMounted(async ()=>{ //组件挂载完毕之后执行
base=new Base()
// base.camera.position.z=5 //把摄像机拉远一点测试box
base.camera.position.z=60 //把摄像机拉远一点测试box
base.camera.updateProjectionMatrix() //更新投影矩阵
base.camera.layers.enableAll();//摄像头能照到所有层级
base.addAmbientLight(0.55)
//1-1-------控制器控制轨道
controls=new OrbitControls(base.camera,base.renderer.domElement)
// //测试box
// createBox()
addPass()
//加载地图的方法
await loadMap()
update()
//把resize绑定到window
window.addEventListener('resize',resize);
//5-4---------监听鼠标移动
document.addEventListener('mousemove',onPointerMove) //鼠标移动执行
})
//射线
const raycaster = new THREE.Raycaster();
raycaster.layers.set(1); //射线层级为1
const pointer = new THREE.Vector2();
const depthMat = new THREE.MeshStandardMaterial({ //高光材质
color: 0x00ffff,
transparent: true,
blending: THREE.AdditiveBlending,
depthTest: false,
depthWrite: false,
});
function onPointerMove(){
// 5-5------将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
// 5-6-------通过摄像机和鼠标位置更新射线
raycaster.setFromCamera(pointer, base.camera);
// 5-6-------计算物体和射线的焦点
const intersects = raycaster.intersectObjects(base.scene.children);
if (intersects.length > 0) { //检测到物体个数大于0
label.value.style.top = event.clientY - label.value.clientHeight - 5 + "px";
label.value.style.left = event.clientX - label.value.clientWidth / 2 + "px";
//6------label
label.value.style.display = "block"; //显示label
provincename.value = intersects[0].object.name; //省份名字就是射线检测到的名字
let arr = Object.entries(extrudeGeos);
arr.forEach((val) => {
if (val[0] == intersects[0].object.name) { //检测到的物体和省份数组元素对此
val[1].traverse((obj) => {
obj.material = depthMat; //5-7------悬停相应省份高亮
});
} else {
val[1].traverse((obj) => {
obj.material = extrudeMats; //否则还是之前暗的挤压体
});
}
});
}else{
label.value.style.display = "none"; //不显示label
}
}
// //测试box
// function createBox(){
// const geometry = new THREE.BoxGeometry( 1, 1, 1 );
// const material = new THREE.MeshBasicMaterial( {color: 0x00ff00} );
// const cube = new THREE.Mesh( geometry, material );
// base.scene.add( cube );
// }
//5-2--------渲染 发光
function addPass(){
let renderpass=new RenderPass(base.scene,base.camera) //渲染通道把场景,相机包裹起来
base.composer.addPass(renderpass) //把通道添加到屏幕后处理
let bloompass=new UnrealBloomPass( //发光通道
new THREE.Vector2(window.innerWidth,window.innerHeight), //分辨率
0.8, //发光强度
0.1, //半径(扩散范围)
0.0 //起始亮度值
)
base.composer.addPass(bloompass) //发光通道添加到屏幕后处理里面
}
//1-3----加载地图
async function loadMap(){
const fileloader=new THREE.FileLoader()
let res=await Promise.all([
// 1-2------加载地图数据 从阿里云下载的数据
fileloader.loadAsync('/mapjson/china.json'),
fileloader.loadAsync('/mapjson/chinaedg.json')
])
if(res instanceof Array){
// console.log(JSON.parse(res[0]))
//1-4--------创建地图
createMap(res[0])
//地图流光
createMoveLight(res[1]);
}
}
//创建球形墨卡托投影
const projection = d3
.geoMercator() //地图投影方式(用于绘制地球墨卡托投影)
.center([108.5525,34.3277])//地图中心点经纬度坐标
.scale(84) //缩放
.translate([0,0]) //移动地图位置
//1-4------创建3D地图
let chinaObj=new THREE.Object3D()
const extrudeGeos = {};//存储省份
function createMap(res){
res=JSON.parse(res)
let centerCoord=null
res.features.forEach((province)=>{
let provinceObj=new THREE.Object3D()
console.log(province.geometry.type);//多边形/多个多边形
if(province.geometry.type=='MultiPolygon'){
province.geometry.coordinates.forEach((multipolygon)=>{
multipolygon.forEach((polygon)=>{
let shape=new THREE.Shape()
let arr=[]
polygon.forEach((coord,index)=>{
//corrd是球坐标,要转换成二维坐标
let [x,y]=projection(coord)
if(index==0){
shape.moveTo(x,-y) //把原点设置在解构出来的x,y
}else{
shape.lineTo(x,-y)
}
arr.push(x,-y,1) //画线
})
//1-5------绘制形状
let mesh=createPolygon(shape,arr, province)
provinceObj.add(mesh)
})
})
}else if(province.geometry.type=='Polygon'){
province.geometry.coordinates.forEach((polygon)=>{
let shape=new THREE.Shape()
let arr=[]
polygon.forEach((coord,index)=>{
let [x,y]=projection(coord)
if(index==0){
shape.moveTo(x,-y) //把原点设置在解构出来的x,y
}else{
shape.lineTo(x,-y)
}
arr.push(x,-y,1) //画线
})
//创建多边形
let mesh=createPolygon(shape,arr, province)
provinceObj.add(mesh)
})
}
// 2---2显示中心光点
if(province.properties.name==centerAddress){ //等于中心位置
centerCoord = createSprite(province,'/sprites/光圈.png',true)
}
if (province.properties.name) {
extrudeGeos[province.properties.name] = provinceObj;
}
chinaObj.add(provinceObj)
})
//中心点坐标不为空
if(centerCoord){
res.features.forEach((province)=>{
//3-1-----飞线
if(flylineAddress.indexOf(province.properties.name)!=-1){
let address=createSprite(province,'/sprites/circle.png',false)
// addressLines(centerCoord,address)
FlyLine(centerCoord,address)
}
//4-1------飞机
if(airplaneAddress.indexOf(province.properties.name)!=-1){
let address=createSprite(province,"/sprites/circle.png",false)
createSprite(province,"/sprites/圆圈.png",true,3)
// addressLines(centerCoord,address)
AirLines(centerCoord, address);
}
})
}
base.scene.add(chinaObj)
}
//挤压材质
const extrudeMats=[
new THREE.MeshStandardMaterial({ //标准材质
color:0x0000ff,
transparent:true,
opacity:0.85,
blending:THREE.AdditiveBlending,//混合模式:叠加
}),
new THREE.MeshStandardMaterial({ //标准材质
color:0x0000ff,
transparent:true,
opacity:0.35,
blending:THREE.AdditiveBlending,//混合模式:叠加
}),
]
//边缘材质
const edgMat=new THREE.LineBasicMaterial({//基础边缘材质
color:0xffffff,
// blending:THREE.AdditiveBlending, //边缘也发光会模糊
})
//1-5------绘制形状
function createPolygon(shape,arr,province){
//二维变成三维物体 使用挤压缓冲几何体
//创建一个挤压几何体
let geo=new THREE.ExtrudeGeometry(shape)
let mesh=new THREE.Mesh(geo,extrudeMats) //添加到省份的obj里
mesh.layers.set(1);
if(province.properties.name){//类似于 河北省保定市
mesh.name=province.properties.name
}
//创建画线
let buffer=new THREE.BufferGeometry()
buffer.setAttribute(
'position',
new THREE.BufferAttribute(new Float32Array(arr),3)//3代表 x,y.z
)
//创建一个线段
let line=new THREE.Line(buffer,edgMat)
chinaObj.add(line)
return mesh;
}
const textureloader=new THREE.TextureLoader() //加载图片
const animCircle=[] //接收需要动画的物体
const PointsCount=100 //曲线分成几段
const addressMat=new THREE.LineBasicMaterial({ //材质
color:0xff0000,
})
//创建两点之间的固定曲线
function addressLines(start,end){
//三维二次贝塞尔曲线
const curve=new THREE.QuadraticBezierCurve3(
new THREE.Vector3(...start), //起始点位
new THREE.Vector3( (start[0]+end[0])/2 , (start[1]+end[1])/2 , 9 ), //中间点
new THREE.Vector3(...end), //结束点位
)
const points = curve.getPoints(PointsCount) //把曲线上的点拿下来
const buffer = new THREE.BufferGeometry().setFromPoints(points) //根据点位信息来创建BufferGeometry
//生成线段
const line=new THREE.Line(buffer,addressMat)
chinaObj.add(line)
return {points,curve}
}
//创建精灵图片
function createSprite(
province, //省份信息
imageurl, //图片路径
needanim, //是否需要动画
scaleparam = 2.5, //缩放参数
opacityparam =10.0, //透明度参数
speed = 0.015 //速度
){
let [x,y] = projection(province.properties.center) //省份中心点坐标
const map=textureloader.load(imageurl)
let material=new THREE.MeshStandardMaterial({//生成基础材质
map,
transparent:true,
depthWrite:false, //深度写入
depthTest:false, //深度测试
blending: THREE.AdditiveBlending, //混合叠加
})
//创建精灵图片
let sprite=new THREE.Sprite(material)
if(needanim){
animCircle.push({
circle:sprite,
offset:0, //偏移
speed,
scaleparam,
opacityparam,
})
}
sprite.position.set(x,-y,1)
chinaObj.add(sprite)
return [x,-y,1]
}
//3-2-----飞线效果
const movePointsCount=35
const flyMat=new THREE.ShaderMaterial({
transparent:true,
vertexShader:
`
attribute float mopacity,
varying float vopacity,
void main(){
vopacity=mopacity;
//当前位置=投影矩阵*模型矩阵*传入的position
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0)
}`,
fragmentShader:
`
varying float vopacity,
void main(){
gl_fragColor=vec4(0.0,vopacity,0.0,vopacity)
}`
})
//飞线
const flylines=[]
function FlyLine(start,end){
let {points}=addressLines(start,end)
let slicepoint=points.slice(0,movePointsCount)
let linegeo=new THREE.BufferGeometry().setFromPoints(slicepoint)
let arr=[movePointsCount]
for (let i = 0; i < movePointsCount; i++) {
arr[i] = i * (1 / movePointsCount);
}
linegeo.setAttribute(
'mopacity',
new THREE.BufferAttribute(new Float32Array(arr), 1)
) //设置透明度
let line=new THREE.Line(linegeo,flyMat)
chinaObj.add(line)
flylines.push({
linegeo,
points,
speed:2,
offset:0
})
}
//4-2------飞机
const airMat = new THREE.MeshBasicMaterial({
map: textureloader.load("/sprites/飞机.png"),
transparent: true,
depthTest: false,
depthWrite: false,
side: THREE.DoubleSide,
});
const airSprites = [];
function AirLines(start, end) {
let { points, curve } = addressLines(start, end);
let sprite = new THREE.Sprite(airMat);
sprite.scale.set(3, 3, 3);
let tangent = curve.getTangent(0.5); //切线
let r = tangent.angleTo(new THREE.Vector3(0, 1, 0)); //改变飞机朝向切线
if (tangent.x < 0) sprite.rotateZ(r);
else sprite.rotateZ(-r);
// sprite.position.set(...points[points.length - 1]);
chinaObj.add(sprite);
airSprites.push({
sprite,
points,
speed: 1,
offset: 0,
});
}
//地图流光
let lightPoints = []; //存储地图边界位置数组
//流光的Geo
let lightGeo = new THREE.BufferGeometry();
let lightCount = 70; //流光长度
let lightSpeed = 10; //流光速度
let lightOffset = 0; //偏移
//材质
const lightMat = new THREE.PointsMaterial({
color: 0xffffff,
size: 0.25,
});
//流光物体
let lightpoint;
function createMoveLight(res) {
res = JSON.parse(res);
res.features.forEach((province) => {
if (province.geometry.type == "MultiPolygon") {
province.geometry.coordinates.forEach((multipolygon) => {
multipolygon.forEach((polygon) => {
polygon.forEach((coord) => {
let [x, y] = projection(coord);
lightPoints.push([x, -y, 1]);
});
});
});
}
});
lightpoint = new THREE.Points(lightGeo, lightMat);
chinaObj.add(lightpoint);
}
let lightArr = [lightCount];
//更新
function update(){
//每帧调用
requestAnimationFrame(update)
animCircle.forEach((val)=>{
val.circle.material.opacity = val.opacityparam*Math.pow((1-val.offset),2) //透明度
val.circle.scale.set( //缩放
val.scaleparam * val.offset,
val.scaleparam * val.offset,
val.scaleparam * val.offset
)
val.offset+=val.speed //偏移量+=速度
val.offset %=1 //偏移量取余等于1
})
flylines.forEach((val) => {
let slicepoint = val.points.slice(val.offset, val.offset + movePointsCount);
val.linegeo.setFromPoints(slicepoint);
val.offset += val.speed;
val.offset %= val.points.length;
});
airSprites.forEach((val) => {
val.sprite.position.set(...val.points[val.offset]);
val.offset += val.speed;
val.offset %= val.points.length;
});
for (let i = 0; i < lightCount; i++) {
lightArr[i] = lightPoints[(lightOffset + i) % lightPoints.length];
}
lightOffset += lightSpeed;
if ((lightOffset % lightPoints.length) + lightCount < lightPoints.length) {
lightOffset %= lightPoints.length;
}
lightGeo.setAttribute(
"position",
new THREE.BufferAttribute(new Float32Array(lightArr.flat(1)), 3)
);
lightpoint.geometry.attributes.position.needsUpdate = true;
base.update();
controls.update();
base.updateComposer()
}
function resize(){
//屏幕宽高比发生变化执行
base.resize()
base.resizeComposer()
}
</script>
<style lang='less' scoped>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
div{
height: 100%;
width: 100%;
.threeJSBox{
width: 100%;
height: 100%;
border: 1px solid #f00;
#containers{
width: 100%;
height: 100%;
border: 1px solid #f00;
.label{
position: absolute;
top:0;
left: 0;
background-color: rgba(0,0,0,0.5);
z-index:99;
text-align: center;
width: 100px;
height: 30px;
line-height: 30px;
padding: 0 10px;
span {
opacity: 1;
color: #ffffff;
}
}
}
}
}
</style>
4.标记点数据 放在一个js文件里
//飞线坐标
export const flylineAddress = [
"湖北省",
"甘肃省",
"内蒙古自治区",
"山东省",
"河南省",
"陕西省",
"青海省",
"辽宁省",
"山西省",
];
//飞机坐标
export const airplaneAddress = [
"新疆维吾尔自治区",
"西藏自治区",
"黑龙江省",
"广东省",
"云南省",
];
//中心点位
export const centerAddress = "北京市";