一、Vite搭建Vue,
vue3
npm create vite@latest
# 安装依赖
npm install
threejs
npm install three
创建twin文件夹

CreateTwin.js
javascript
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
export default class CreateTwin {
constructor() {
//场景
this.scene = new THREE.Scene();
//辅助观察的坐标系
this.axesHelper = new THREE.AxesHelper(1000);
this.scene.add(this.axesHelper);
//相机
const width = window.innerWidth;
const height = window.innerHeight;
this.camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 3000);
this.camera.position.set(292, 223, 185);
this.camera.lookAt(0, 0, 0);
// WebGL渲染器设置
this.renderer = new THREE.WebGLRenderer({
antialias: true, //开启优化锯齿
});
this.renderer.setPixelRatio(window.devicePixelRatio); //防止输出模糊
this.renderer.setSize(width, height);
document.body.appendChild(this.renderer.domElement);
//创建一个GLTF加载器
this.loader = new GLTFLoader();
//光源设置
// 平行光
this.directionalLight = new THREE.DirectionalLight(0xffffff, 1);
this.directionalLight.position.set(100, 60, 50);
this.scene.add(this.directionalLight);
//环境光
this.ambient = new THREE.AmbientLight(0xffffff, 0.4);
this.scene.add(this.ambient);
// 渲染循环
this.renderer.setAnimationLoop(() => {
this.renderer.render(this.scene, this.camera);
});
// 相机控件
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.target.set(0, 0, 0);
this.controls.update();
// 画布尺寸随着窗口变化
window.addEventListener("resize", () => {
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
});
}
}
twin.js
javascript
import CreateTwin from "../twin/CreateTwin";
let twin = null;
export function initTwin() {
if (twin) {
// 清理旧的实例
disposeTwin();
}
twin = new CreateTwin();
return twin;
}
export function disposeTwin() {
if (twin && twin.renderer) {
twin.renderer.dispose();
if (twin.renderer.domElement) {
twin.renderer.domElement.remove();
}
}
twin = null;
}
export default initTwin;
App.vue
javascript
<script setup>
import { onMounted, onUnmounted } from "vue";
import initTwin, { disposeTwin } from "./twin/twin";
let twin = null;
onMounted(() => {
twin = initTwin();
twin.loader.load("/工厂.glb", function (gltf) {
twin.scene.add(gltf.scene);
});
});
onUnmounted(() => {
disposeTwin();
});
</script>
<template></template>
<style scoped></style>
public放入工厂.glb

二、解析边界线(几何体顶点和Line模型)
回顾9-18.几何体顶点颜色数数据
javascript
// 引入three.js
import * as THREE from 'three';
const geometry = new THREE.BufferGeometry(); //创建一个几何体对象
const vertices = new Float32Array([
0, 0, 0, //顶点1坐标
50, 0, 0, //顶点2坐标
0, 25, 0, //顶点3坐标
]);
// 顶点位置数据
geometry.attributes.position = new THREE.BufferAttribute(vertices, 3);
//类型数组创建顶点颜色color数据
const colors = new Float32Array([
1, 0, 0, //顶点1颜色
0, 0, 1, //顶点2颜色
0, 1, 0, //顶点3颜色
]);
// 设置几何体attributes属性的颜色color属性
//3个为一组,表示一个顶点的颜色数据RGB
geometry.attributes.color = new THREE.BufferAttribute(colors, 3);
// 线模型渲染几何体顶点颜色,可以看到直线颜色渐变效果
const material = new THREE.LineBasicMaterial({
vertexColors:true,//默认false,设置为true表示使用顶点颜色渲染
});
const line = new THREE.Line(geometry, material);
export default line;

解析中国地图边界
CreateTwin.js,china.json文件放入public文件夹中
javascript
export default class CreateTwin {
constructor() {
//场景
this.scene = new THREE.Scene();
this.loader = new THREE.FileLoader();
this.loader.setResponseType("json");
this.mapGroup = new THREE.Group();
this.scene.add(this.mapGroup);
this.loader.load("./china.json", (data) => {
data.features.forEach((area) => {
// "Polygon":国家area有一个封闭轮廊
if (area.geometry.type === "Polygon") {
var pointArr = []; //边界线顶点坐标
area.geometry.coordinates[0].forEach((elem) => {
//z坐标设置为0.这样地图轮廓位于xoY平面上
pointArr.push(elem[0], elem[1], 0);
});
this.mapGroup.add(this.createLine(pointArr)); // 国家边界轮廓插入组对象mapGroup
//"MultiPolygon":国家area有多个封闭轮廓
} else if (area.geometry.type === "MultiPolygon") {
// 解析所有封闭轮廓边界坐标area.geometry.coordinates
area.geometry.coordinates.forEach((polygon) => {
var pointArr = []; //边界线顶点坐标
polygon[0].forEach((elem) => {
pointArr.push(elem[0], elem[1], 0);
});
this.mapGroup.add(this.createLine(pointArr)); // 国家边界轮廓插入组对象mapGroup
});
}
});
});
//辅助观察的坐标系
...
}
createLine(pointArr) {
// 创建线条几何体
const geometry = new THREE.BufferGeometry();
// 设置顶点位置
const vertices = new Float32Array(pointArr);
// 创建属性缓冲区对象
geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
// 创建线条材质
const material = new THREE.LineBasicMaterial({
color: 0x00ffff,
transparent: true,
opacity: 0.8,
});
const line = new THREE.Line(geometry, material);
return line;
}
}
javascript
// 遍历地图数据的每个区域
// "Polygon":国家area有一个封闭轮廊
if (area.geometry.type === "Polygon") {
var pointArr = []; //边界线顶点坐标
area.geometry.coordinates[0].forEach((elem) => {
//z坐标设置为0.这样地图轮廓位于xoY平面上
pointArr.push(elem[0], elem[1], 0);
});
this.mapGroup.add(this.createLine(pointArr)); // 国家边界轮廓插入组对象mapGroup
//"MultiPolygon":国家area有多个封闭轮廓
} else if (area.geometry.type === "MultiPolygon") {
// 解析所有封闭轮廓边界坐标area.geometry.coordinates
area.geometry.coordinates.forEach((polygon) => {
var pointArr = []; //边界线顶点坐标
polygon[0].forEach((elem) => {
pointArr.push(elem[0], elem[1], 0);
});
this.mapGroup.add(this.createLine(pointArr)); // 国家边界轮廓插入组对象mapGroup
});
}
简写成
javascript
if (area.geometry.type === "Polygon") {
area.geometry.coordinates = [area.geometry.coordinates];
}
this.mapGroup.add(this.createLine(area.geometry.coordinates));
但 createLine 方法需要调整,因为现在传入的是嵌套数组,GeoJSON 的坐标是 [经度, 纬度] 格式,需要转换为三维坐标 [x, y, z],createLine 方法需要补齐三维坐标
javascript
createLine(polygons) {
const group = new THREE.Group();
polygons.forEach((polygon) => {
const pointArr = polygon[0]; // 获取第一个轮廓
const vertices = [];
pointArr.forEach((elem) => {
// 将 GeoJSON 坐标 [经度, 纬度] 转换为三维坐标
vertices.push(elem[0], elem[1], 0);
});
// 创建线条几何体
const geometry = new THREE.BufferGeometry();
// 设置顶点位置
const vertexArray = new Float32Array(vertices);
// 创建属性缓冲区对象
geometry.setAttribute("position", new THREE.BufferAttribute(vertexArray, 3));
// 创建线条材质
const material = new THREE.LineBasicMaterial({
color: 0x00ffff,
transparent: true,
opacity: 0.8,
});
const line = new THREE.Line(geometry, material);
group.add(line);
});
return group;
}

计算中心点并将地图居中
javascript
const allPoints = [];
data.features.forEach((area) => {
if (area.geometry.type === "Polygon") {
area.geometry.coordinates = [area.geometry.coordinates];
}
this.mapGroup.add(this.createLine(area.geometry.coordinates));
// 收集所有坐标点用于计算中心
area.geometry.coordinates.forEach(polygon => {
polygon[0].forEach(point => {
allPoints.push(point[0], point[1]);
});
});
});
// 计算中心点
const minLon = Math.min(...allPoints.filter((_, i) => i % 2 === 0));
const maxLon = Math.max(...allPoints.filter((_, i) => i % 2 === 0));
const minLat = Math.min(...allPoints.filter((_, i) => i % 2 === 1));
const maxLat = Math.max(...allPoints.filter((_, i) => i % 2 === 1));
const centerX = (minLon + maxLon) / 2;
const centerY = (minLat + maxLat) / 2;
// 将地图移动到坐标原点
this.mapGroup.position.set(-centerX, -centerY, 0);
使用正投影相机
javascript
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
export default class CreateTwin {
constructor() {
//场景
...
//创建一个加载器
...
data.features.forEach((area) => {
...
});
this.mapGroup.position.set(-104, -28, 0);
});
//辅助观察的坐标系
...
//相机设置
const width = window.innerWidth;
const height = window.innerHeight;
const k = width / height;
const s = 20; //三维场景显示范围控制系数,系数越大,显示的范围越大
this.camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 8000);
this.camera.position.set(0, 0, 50); //相机正对地图
this.camera.lookAt(0,0,0);
// WebGL渲染器设置
...
//光源设置
// 平行光
...
//环境光
...
// 渲染循环
...
// 相机控件
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableRotate = false;
this.controls.target.set(0,0,0);
this.controls.update();
// 画布尺寸随着窗口变化
...
}
createLine(polygons) {
...
}
}

三、Shape几何体填充行政区域
javascript
this.mapGroup.add(this.createShape(area.geometry.coordinates));
javascript
createShape(pointArrs) {
const shapeArr = [];
pointArrs.forEach((polygon) => {
const pointArr = polygon[0]; // 获取第一个轮廓
const vectorArr = [];
pointArr.forEach((elem) => {
// 将 GeoJSON 坐标 [经度, 纬度] 转换为 Vector2
vectorArr.push(new THREE.Vector2(elem[0], elem[1]));
});
const shape = new THREE.Shape(vectorArr);
shapeArr.push(shape);
});
// 创建填充几何体
const geometry = new THREE.ShapeGeometry(shapeArr);
// 创建材质
const material = new THREE.MeshBasicMaterial({
color: 0x009393,
transparent: true,
opacity: 0.6,
side: THREE.DoubleSide,
});
const mesh = new THREE.Mesh(geometry, material);
return mesh;
}

深度冲突
line适当偏移
javascript
group.position.z += 1;

四、相机参数适配
javascript
//相机设置
const width = window.innerWidth;
const height = window.innerHeight;
const k = width / height; //canvas画布宽高比
const s = 90; //控制left, right, top, bottom范围大小
this.camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
this.camera.position.set(100, 0, 200); //相机正对地图
this.camera.lookAt(100, 0, 0);
javascript
// 相机控件
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
// this.controls = new MapControls(this.camera, this.renderer.domElement);
this.controls.target.set(100, 0, 0); //与lookAt参数保持一致
// this.controls.enableRotate = false;
this.controls.update();

包围盒
javascript
// 地图mapGroup的包围盒计算
const box3 = new THREE.Box3();
box3.expandByObject(this.mapGroup);
console.log("查看包围盒box3", box3);
// 计算包围盒长宽高尺寸
const size = new THREE.Vector3();
box3.getSize(size);
console.log("查看包围盒尺寸", size);
// 计算包围盒的几何体中心
const center = new THREE.Vector3();
box3.getCenter(center);
console.log("查看几何中心", center);

javascript
//相机设置
const width = window.innerWidth;
const height = window.innerHeight;
const k = width / height; //canvas画布宽高比
const s = 30; //控制left, right, top, bottom范围大小
this.camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
this.camera.position.set(104.29, 28.48, 200); //相机正对地图
this.camera.lookAt(104.29, 28.48, 0);
// WebGL渲染器设置
...
//光源设置
...
// 渲染循环
...
// 相机控件
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
// this.controls = new MapControls(this.camera, this.renderer.domElement);
this.controls.target.set(104.29, 28.48, 0); //与lookAt参数保持一致
// this.controls.enableRotate = false;
this.controls.update();

五、渲染省份或城市地图轮廓
javascript
this.loader.load("./江苏省.json", (data) => {}


重新设置相机设置

javascript
//相机设置
const width = window.innerWidth;
const height = window.innerHeight;
const k = width / height; //canvas画布宽高比
const s = 3; //控制left, right, top, bottom范围大小
this.camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
// this.camera.position.set(104.29, 28.48, 200); //相机正对地图
// this.camera.lookAt(104.29, 28.48, 0);
this.camera.position.set(119.16, 32.94, 200); //相机正对地图
this.camera.lookAt(119.16, 32.94, 0);

六、拉伸行政区轮廓
src\twin\ExtrudeMesh.js
javascript
import * as THREE from "three";
export function ExtrudeMesh(pointArrs, height) {
const shapeArr = [];
pointArrs.forEach((polygon) => {
const pointArr = polygon[0]; // 获取第一个轮廓
const vectorArr = [];
pointArr.forEach((elem) => {
// 将 GeoJSON 坐标 [经度, 纬度] 转换为 Vector2
vectorArr.push(new THREE.Vector2(elem[0], elem[1]));
});
const shape = new THREE.Shape(vectorArr);
shapeArr.push(shape);
});
// 创建填充几何体
// const geometry = new THREE.ShapeGeometry(shapeArr);
const geometry = new THREE.ExtrudeGeometry(shapeArr, {
depth: height,
bevelEnabled: true,
});
// 创建材质
const material = new THREE.MeshBasicMaterial({
color: 0x009393,
transparent: true,
opacity: 0.6,
side: THREE.DoubleSide,
});
const mesh = new THREE.Mesh(geometry, material);
return mesh;
}
替换src\twin\shapeMesh.js
javascript
const height = 1;
// this.mapGroup.add(createShape(area.geometry.coordinates));
this.mapGroup.add(ExtrudeMesh(area.geometry.coordinates, height));

八、经纬度转墨卡托坐标
src\twin\lonLat2Mercator.js
javascript
export function lonLat2Mercator(E, N) {
const x = (E * 20037508.34) / 180;
let y = Math.log(Math.tan(((90 + N) * Math.PI) / 360)) / (Math.PI / 180);
y = (y * 20037508.34) / 180;
return {
x: x,
y: y,
};
}
ExtrudeMesh.js或shapeMesh.js
javascript
// 将 GeoJSON 坐标 [经度, 纬度] 转换为 Vector2
const coord = lonLat2Mercator(elem[0], elem[1]);
vectorArr.push(new THREE.Vector2(coord.x, coord.y));
// vectorArr.push(new THREE.Vector2(elem[0], elem[1]));

包围盒发生改变

javascript
//相机设置
const width = window.innerWidth;
const height = window.innerHeight;
const k = width / height; //canvas画布宽高比
// const s = 30; //控制left, right, top, bottom范围大小
const mapSize = 7087871; //包围盒方向y表示地图大小
const s = mapSize / 2;
// this.camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
this.camera = new THREE.OrthographicCamera(
-s * k,
s * k,
s,
-s,
1,
mapSize * 10
);
// this.camera.position.set(104.29, 28.48, 200); //相机正对地图
// this.camera.position.set(104.29, 28.48 - 100, 200); //相机倾斜地图
this.camera.position.set(11626571.24, 4305445.99, mapSize * 5);
// this.camera.lookAt(104.29, 28.48, 0);
this.camera.lookAt(11626571.24, 4305445.99, 0);
javascript
// 相机控件
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
// this.controls = new MapControls(this.camera, this.renderer.domElement);
// this.controls.target.set(104.29, 28.48, 0); //与lookAt参数保持一致
this.controls.target.set(11626571.24, 4305445.99, 0); //与lookAt参数保持一致
// this.controls.enableRotate = false;
this.controls.update();
范围小,变化不大

line

需要把参数设大
javascript
const height = 100000;
// this.mapGroup.add(createShape(area.geometry.coordinates));
this.mapGroup.add(ExtrudeMesh(area.geometry.coordinates, height));
javascript
lineGroup.position.z += 100000.2;

九、热点---波动光圈
准备一张光圈贴图
javascript
//矩形平面网格模型设置背景透明的png贴图
const geometry = new THREE.PlaneGeometry(100, 100); //默认在XoY平面上
const textureLoader = new THREE.TextureLoader(); // TextureLoader创建一个纹理加载器对象
const material = new THREE.MeshBasicMaterial({
color: 0x00ffff, //设置光圈颜色
map: textureLoader.load("./光圈贴图.png"),
transparent: true, //使用背景透明的png贴图,注意开启透明计算//
side: THREE.DoubleSide, //双面可见
});
this.mesh = new THREE.Mesh(geometry, material);
// mesh.rotateX(-Math.PI / 2);//旋转到xoz平面scene.add(mesh);
this.mesh.position.z = 0.5;
this.scene.add(this.mesh);

javascript
// 渲染循环
this.renderer.setAnimationLoop(() => {
// 光圈缩放动画:1~2.5倍之间变化
this._s += 0.01;
this.mesh.scale.set(this._s, this._s, 1);
// 缩放2.5对应0,缩放1.0对应1
this.mesh.material.opacity = 1 - (this._s - 1) / 1.5;
if (this._s > 2.5) this._s = 1;
this.renderer.render(this.scene, this.camera);
});

requestAnimationFrame 是浏览器原生的动画循环 API
javascript
function animate() {
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
animate();
renderer.setAnimationLoop 是 Three.js 提供的封装方法,底层也是基于 requestAnimationFrame
javascript
renderer.setAnimationLoop(() => {
renderer.render(scene, camera);
});
主要区别:
- WebXR 兼容 :
setAnimationLoop会自动处理 VR/AR 设备的渲染帧,requestAnimationFrame需要手动处理 - 简洁性 :
setAnimationLoop不需要递归调用,更简洁 - 普通项目 :两者效果相同,
requestAnimationFrame更常见
淡入淡出
javascript
this._s = 2.5;
// 渲染循环
this.renderer.setAnimationLoop(() => {
// 光圈缩放动画:1~2.5倍之间变化
// this._s += 0.01;
// this.mesh.scale.set(this._s, this._s, this._s);
// // 缩放2.5对应0,缩放1.0对应1
// this.mesh.material.opacity = 1 - (this._s - 1) / 1.5;
// if (this._s > 2.5) this._s = 1;
// 淡入淡出
this._s += 0.01;
this.mesh.scale.set(this._s, this._s, this._s);
if (this._s <= 1.3) {
this.mesh.material.opacity = (this._s - 1.0) * 3.3;
} else if (this._s > 1.3 && this._s <= 2.5) {
this.mesh.material.opacity = 1 - (this._s - 1) / 1.5;
} else {
this._s = 1.0;
}
this.renderer.render(this.scene, this.camera);
});

javascript
this.loader.load("./china.json", (data) => {
data.features.forEach((area) => {
if (area.geometry.type === "Polygon") {
area.geometry.coordinates = [area.geometry.coordinates];
}
this.mapGroup.add(this.createLine(area.geometry.coordinates));
this.mapGroup.add(this.createShape(area.geometry.coordinates));
// 标注省份行政中心光圈
if (area.properties && area.properties.center) {
const pos = area.properties.center;
const size = Math.random() * 3 + 2;
const geometry = new THREE.PlaneGeometry(size, size);
const material = new THREE.MeshBasicMaterial({
color: 0x00ffff,
map: new THREE.TextureLoader().load("./光圈贴图.png"),
transparent: true,
side: THREE.DoubleSide,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(pos[0], pos[1], 0.5);
mesh._s = size;
this.cityPointGroup.add(mesh);
this.meshes.push(mesh);
}
});
});
javascript
// 渲染循环
this.renderer.setAnimationLoop(() => {
// 遍历所有省份光圈,执行缩放动画
this.meshes.forEach((mesh) => {
mesh._s += 0.01;
mesh.scale.set(mesh._s, mesh._s, mesh._s);
if (mesh._s <= 1.3) {
mesh.material.opacity = (mesh._s - 1.0) * 3.3;
} else if (mesh._s > 1.3 && mesh._s <= 2.5) {
mesh.material.opacity = 1 - (mesh._s - 1) / 1.5;
} else {
mesh._s = 1.0;
}
});
this.renderer.render(this.scene, this.camera);
});

十、热点---旋转棱锥
src\twin\ConeMesh.js
javascript
import * as THREE from "three";
export function createConeMesh(center) {
const pos = center;
const height = 4;
const redius = 1.5;
const geometry = new THREE.ConeGeometry(redius, height, 4);
geometry.rotateX(-Math.PI / 2);
geometry.translate(0, 0, height / 2);
const material = new THREE.MeshLambertMaterial({
color: 0x00ffff,
transparent: true,
opacity: 0.8,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(pos[0], pos[1] - 1, 5.5);
return mesh;
}
javascript
// 省份棱锥组
this.coneGroup = new THREE.Group();
this.mapGroup.add(this.coneGroup);
// 标注省份行政中心棱锥
if (area.properties && area.properties.center) {
const mesh1 = createConeMesh(area.properties.center);
this.coneGroup.add(mesh1);
this.meshes1.push(mesh1);
}
javascript
// 棱锥缩放动画参数 - 先初始化为空数组
this.meshes1 = [];
// 渲染循环
this.renderer.setAnimationLoop(() => {
// 遍历所有省份光圈,执行缩放动画
this.meshes.forEach((mesh) => {
mesh._s += 0.01;
mesh.scale.set(mesh._s, mesh._s, mesh._s);
if (mesh._s <= 1.3) {
mesh.material.opacity = (mesh._s - 1.0) * 3.3;
} else if (mesh._s > 1.3 && mesh._s <= 2.5) {
mesh.material.opacity = 1 - (mesh._s - 1) / 1.5;
} else {
mesh._s = 1.0;
}
});
this.meshes1.forEach((mesh1) => {
mesh1.rotateZ(0.01);
});
this.renderer.render(this.scene, this.camera);
// console.log(this.controls.target);
});

十一、HTML元素作为标签
javascript
// 引入CSS3渲染器CSS3DRenderer
import { CSS3DRenderer } from "three/addons/renderers/CSS3DRenderer.js";
// 引入CSS3模型对象CSS3DObject
import { CSS3DObject } from 'three/addons/renderers/CSS3DRenderer.js';
标签
javascript
var div = document.createElement("div");
div.innerHTML = "xxxx";
div.style.padding = "5px 10px";
div.style.color = "#fff";
div.style.fontSize = "16px";
div.style.position = "absolute";
div.style.backgroundColor = "rgba(25,25,25,0.5)";
div.style.borderRadius = "5px";
javascript
//div元素包装为css模型对象css3Dobject,并插入场景中
var label = new CSS3DObject(div);
label.position.copy(); //定位css3Dobject模型对象
label.scale.set(0.5, 0.5, 0.5);
label.position.y += 20;
this.scene.add(label); //css3模型标签插入到场景中
javascript
// 创建一个CSS3渲染器CSS3DRenderer
const css3Renderer = new CSS3DRenderer();
css3Renderer.setSize(width, height);
// HTML标签<div id="tag"></div>外面父元素叠加到canvas画布上且重合
css3Renderer.domElement.style.position = "absolute";
css3Renderer.domElement.style.top = "0px";
//设置.pointerEvents=none,解决HTML元素标签对threejs canvas画布鼠标事件的遮挡
css3Renderer.domElement.style.pointerEvents = "none";
document.body.appendChild(css3Renderer.domElement);
渲染循环
javascript
css3Renderer.render(this.scene, this.camera);

十二、鼠标单击拾取选中一个行政区
javascript
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
// 引入渲染器通道RenderPass
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
// 引入OutlinePass通道
import { OutlinePass } from "three/addons/postprocessing/OutlinePass.js";
javascript
// 标记鼠标拾取到的mesh
let chooseMesh = null;
// 选中网格模型变为半透明效果
const choose = (event) => {
// 如果之前有选中的mesh,恢复原来透明度
if (chooseMesh) {
chooseMesh.material.color.set(0x009393);
}
const Sx = event.clientX; // 鼠标单击位置横坐标
const Sy = event.clientY; // 鼠标单击位置纵坐标
// 屏幕坐标转WebGL标准设备坐标
const x = (Sx / window.innerWidth) * 2 - 1; // WebGL标准设备横坐标
const y = -(Sy / window.innerHeight) * 2 + 1; // WebGL标准设备纵坐标
// 创建一个射线投射器Raycaster
const raycaster = new THREE.Raycaster();
// 通过鼠标单击位置标准设备坐标和相机参数计算射线投射器Raycaster的射线属性.ray
raycaster.setFromCamera(new THREE.Vector2(x, y), this.camera);
// 返回.intersectObjects()参数中射线选中的网格模型对象
// 未选中对象返回空数组[],选中一个数组1个元素,选中两个数组两个元素
const intersects = raycaster.intersectObjects(this.mapGroup.children);
console.log("射线器返回的对象", intersects);
// intersects.length大于0说明,说明选中了模型
if (intersects.length > 0) {
intersects[0].object.material.color.set(0x00ffff);
chooseMesh = intersects[0].object;
}
};
// 监听窗口鼠标单击事件
this.renderer.domElement.addEventListener("click", choose);
javascript
// 创建后处理对象EffectComposer,WebGL渲染器作为参数
const composer = new EffectComposer(this.renderer);
//创建一个渲染器通道,场景和相机作为参数
const renderPass = new RenderPass(this.scene, this.camera);
composer.addPass(renderPass);
// OutlinePass第一个参数v2的尺寸和canvas画布保持一致
const v2 = new THREE.Vector2(window.innerWidth, window.innerHeight);
const outlinePass = new OutlinePass(v2, this.scene, this.camera);
outlinePass.visibleEdgeColor.set(0x00ffff);
outlinePass.edgeThickness = 4;
outlinePass.edgeStrength = 6;
// 渲染循环
composer.render();

这样即使 Line 在 ExtrudeMesh 上层,射线检测也会跳过它,直接选中底层的 ExtrudeMesh。不需要修改 createLine 或 Line 对象本身的设置。
javascript
// intersects.length大于0说明,说明选中了模型
if (intersects.length > 0) {
// 只选中Mesh类型(ExtrudeMesh),过滤掉Line、光圈、棱锥等
const selectedMesh = intersects.find((intersect) => {
return intersect.object.type === 'Mesh' &&
intersect.object.geometry.type === 'ExtrudeGeometry';
});
if (selectedMesh) {
selectedMesh.object.material.color.set(0x00ffff);
chooseMesh = selectedMesh.object;
}
}

十三、中国地图颜色深浅可视化
javascript
// 加载年末常住人口数据
this.loader.load("./年末常住人口(万人).json", (data2) => {
// 每个省份的名字作为属性,属性值是国家对应人口
var popObj = {};
// 人口最高对应红色,人口最低对应白色
var color1 = new THREE.Color(0xffffff);
var color2 = new THREE.Color(0xff0000);
var popMax = 12780; // 设置一个基准值,以最高的广东人口为准
data2.features.forEach((obj) => {
var pop = obj.value; // 当前省份人口
popObj[obj.name] = pop; // 每个省份的名字作为属性,属性值是国家对应人口
});
console.log("人口数据对象", popObj);
// this.loader.load("./江苏省.json", (data) => {
this.loader.load("./china.json", (data) => {
data.features.forEach((area) => {
if (area.geometry.type === "Polygon") {
area.geometry.coordinates = [area.geometry.coordinates];
}
this.mapGroup.add(createLine(area.geometry.coordinates));
// 根据人口数据设置省份颜色
const provinceName = area.properties.name;
const height = 1;
var mesh = ExtrudeMesh(area.geometry.coordinates, height);
// 如果有对应的人口数据,设置颜色
if (popObj[provinceName]) {
var pop = popObj[provinceName];
// 计算颜色插值:人口越多越红
var colorRatio = pop / popMax;
var color = new THREE.Color().lerpColors(color1, color2, colorRatio);
mesh.material.color = color;
}
this.mapGroup.add(mesh);
// 标注省份行政中心光圈
if (area.properties && area.properties.center) {
const mesh = createCityPointMesh(area.properties.center);
this.cityPointGroup.add(mesh);
this.meshes.push(mesh);
}
// 标注省份行政中心棱锥
if (area.properties && area.properties.center) {
const mesh1 = createConeMesh(area.properties.center);
this.coneGroup.add(mesh1);
this.meshes1.push(mesh1);
}
});
// 地图mapGroup的包围盒计算
const box3 = new THREE.Box3();
box3.expandByObject(this.mapGroup);
console.log("查看包围盒box3", box3);
// 计算包围盒长宽高尺寸
const size = new THREE.Vector3();
box3.getSize(size);
console.log("查看包围盒尺寸", size);
// 计算包围盒的几何体中心
const center = new THREE.Vector3();
box3.getCenter(center);
console.log("查看几何中心", center);
});
});
年末常住人口(万人).json

每个省份根据人口数据有不同的颜色,恢复时应该恢复到原来的颜色,而不是固定的 0x009393
javascript
let originalColor = null; // 保存选中mesh的原始颜色
// 选中网格模型变为半透明效果
const choose = (event) => {
// 如果之前有选中的mesh,恢复原来颜色
if (chooseMesh && originalColor) {
chooseMesh.material.color.copy(originalColor);
}
...
// intersects.length大于0说明,说明选中了模型
if (intersects.length > 0) {
// 只选中Mesh类型(ExtrudeMesh),过滤掉Line、光圈、棱锥等
const selectedMesh = intersects.find((intersect) => {
return (
intersect.object.type === "Mesh" &&
intersect.object.geometry.type === "ExtrudeGeometry"
);
});
if (selectedMesh) {
// 保存原始颜色
originalColor = selectedMesh.object.material.color.clone();
// 设置选中颜色
selectedMesh.object.material.color.set(0x00ffff);
chooseMesh = selectedMesh.object;
}
}
}

鼠标射线拾取+div信息弹窗
src\twin\CreateTwin.js
javascript
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { MapControls } from "three/examples/jsm/controls/MapControls.js";
// 引入CSS3渲染器CSS3DRenderer
import { CSS3DRenderer } from "three/addons/renderers/CSS3DRenderer.js";
// 引入CSS3模型对象CSS3DObject
import { CSS3DObject } from "three/addons/renderers/CSS3DRenderer.js";
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
// 引入渲染器通道RenderPass
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
// 引入OutlinePass通道
import { OutlinePass } from "three/addons/postprocessing/OutlinePass.js";
import { createLine } from "./line.js";
import { ExtrudeMesh } from "./ExtrudeMesh.js";
import { createCityPointMesh } from "./cityPointMesh.js";
import { createConeMesh } from "./ConeMesh.js";
export default class CreateTwin {
constructor() {
//场景
this.scene = new THREE.Scene();
//创建一个加载器
this.loader = new THREE.FileLoader();
this.loader.setResponseType("json");
this.mapGroup = new THREE.Group();
this.scene.add(this.mapGroup);
// 省份光圈组
this.cityPointGroup = new THREE.Group();
this.mapGroup.add(this.cityPointGroup);
// 省份棱锥组
this.coneGroup = new THREE.Group();
this.mapGroup.add(this.coneGroup);
// 光圈缩放动画参数 - 先初始化为空数组
this.meshes = [];
// 棱锥缩放动画参数 - 先初始化为空数组
this.meshes1 = [];
// 加载年末常住人口数据
this.loader.load("./年末常住人口(万人).json", (data2) => {
// 每个省份的名字作为属性,属性值是国家对应人口
var popObj = {};
// 人口最高对应红色,人口最低对应白色
var color1 = new THREE.Color(0xffffff);
var color2 = new THREE.Color(0xff0000);
var popMax = 12780; // 设置一个基准值,以最高的广东人口为准
data2.features.forEach((obj) => {
var pop = obj.value; // 当前省份人口
popObj[obj.name] = pop; // 每个省份的名字作为属性,属性值是国家对应人口
});
// console.log("人口数据对象", popObj)
this.loader.load("./china.json", (data) => {
data.features.forEach((area) => {
if (area.geometry.type === "Polygon") {
area.geometry.coordinates = [area.geometry.coordinates];
}
this.mapGroup.add(createLine(area.geometry.coordinates));
// 根据人口数据设置省份颜色
const provinceName = area.properties.name;
const height = 1;
var mesh = ExtrudeMesh(area.geometry.coordinates, height);
// mesh自定义一个pop属性,用于标签设置
mesh.pop = popObj[provinceName] || 0;
// mesh经纬度用于控制标签位置
mesh.center = area.properties.center;
// 如果有对应的人口数据,设置颜色
if (popObj[provinceName]) {
var pop = popObj[provinceName];
// 计算颜色插值:人口越多越红
var colorRatio = pop / popMax;
var color = new THREE.Color().lerpColors(
color1,
color2,
colorRatio
);
mesh.material.color = color;
mesh.color = color; // 记录下自身的颜色,以便选中改变mesh颜色的时候,不选中状态再改变回来
}
this.mapGroup.add(mesh);
// 标注省份行政中心光圈
if (area.properties && area.properties.center) {
const mesh = createCityPointMesh(area.properties.center);
this.cityPointGroup.add(mesh);
this.meshes.push(mesh);
}
// 标注省份行政中心棱锥
if (area.properties && area.properties.center) {
const mesh1 = createConeMesh(area.properties.center);
this.coneGroup.add(mesh1);
this.meshes1.push(mesh1);
}
});
// 地图mapGroup的包围盒计算
const box3 = new THREE.Box3();
box3.expandByObject(this.mapGroup);
console.log("查看包围盒box3", box3);
// 计算包围盒长宽高尺寸
const size = new THREE.Vector3();
box3.getSize(size);
console.log("查看包围盒尺寸", size);
// 计算包围盒的几何体中心
const center = new THREE.Vector3();
box3.getCenter(center);
console.log("查看几何中心", center);
});
});
// 辅助观察的坐标系
this.axesHelper = new THREE.AxesHelper(300);
this.scene.add(this.axesHelper);
//相机设置
const width = window.innerWidth;
const height = window.innerHeight;
const k = width / height; //canvas画布宽高比
const s = 30; //控制left, right, top, bottom范围大小
this.camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
this.camera.position.set(104.29, 28.48 - 40, 200); //相机倾斜地图
this.camera.lookAt(104.29, 28.48, 0);
// WebGL渲染器设置
this.renderer = new THREE.WebGLRenderer({
antialias: true, //开启优化锯齿
});
this.renderer.setPixelRatio(window.devicePixelRatio); //防止输出模糊
this.renderer.setSize(width, height);
document.body.appendChild(this.renderer.domElement);
// 创建一个CSS3渲染器CSS3DRenderer
const css3Renderer = new CSS3DRenderer();
css3Renderer.setSize(width, height);
// HTML标签<div id="tag"></div>外面父元素叠加到canvas画布上且重合
css3Renderer.domElement.style.position = "absolute";
css3Renderer.domElement.style.top = "0px";
//设置.pointerEvents=none,解决HTML元素标签对threejs canvas画布鼠标事件的遮挡
css3Renderer.domElement.style.pointerEvents = "none";
document.body.appendChild(css3Renderer.domElement);
// 创建标签div元素
var div = document.createElement("div");
div.style.padding = "1px 2px";
div.style.color = "#fff";
div.style.fontSize = "2px";
div.style.position = "absolute";
div.style.backgroundColor = "rgba(25,25,25,0.5)";
div.style.borderRadius = "5px";
div.style.visibility = "hidden"; // 初始隐藏标签
//div元素包装为css模型对象css3Dobject,并插入场景中
var label = new CSS3DObject(div);
this.scene.add(label); //css3模型标签插入到场景中
// 标记鼠标拾取到的mesh
let chooseMesh = null;
let originalColor = null; // 保存选中mesh的原始颜色
// 选中网格模型变为半透明效果
const choose = (event) => {
// 如果之前有选中的mesh,恢复原来颜色
if (chooseMesh && originalColor) {
chooseMesh.material.color.copy(originalColor);
label.element.style.visibility = "hidden";
} else {
label.element.style.visibility = "hidden"; // 没有选中mesh,隐藏标签
}
const Sx = event.clientX; // 鼠标单击位置横坐标
const Sy = event.clientY; // 鼠标单击位置纵坐标
// 屏幕坐标转WebGL标准设备坐标
const x = (Sx / window.innerWidth) * 2 - 1; // WebGL标准设备横坐标
const y = -(Sy / window.innerHeight) * 2 + 1; // WebGL标准设备纵坐标
// 创建一个射线投射器Raycaster
const raycaster = new THREE.Raycaster();
// 通过鼠标单击位置标准设备坐标和相机参数计算射线投射器Raycaster的射线属性.ray
raycaster.setFromCamera(new THREE.Vector2(x, y), this.camera);
// 返回.intersectObjects()参数中射线选中的网格模型对象
// 未选中对象返回空数组[],选中一个数组1个元素,选中两个数组两个元素
const intersects = raycaster.intersectObjects(this.mapGroup.children);
console.log("射线器返回的对象", intersects);
// intersects.length大于0说明,说明选中了模型
if (intersects.length > 0) {
// 只选中Mesh类型(ExtrudeMesh),过滤掉Line、光圈、棱锥等
const selectedMesh = intersects.find((intersect) => {
return (
intersect.object.type === "Mesh" &&
intersect.object.geometry.type === "ExtrudeGeometry"
);
});
if (selectedMesh) {
// 保存原始颜色
originalColor = selectedMesh.object.material.color.clone();
// 设置选中颜色
selectedMesh.object.material.color.set(0x00ffff);
chooseMesh = selectedMesh.object;
// 显示标签并设置内容
label.element.style.visibility = "visible";
label.element.innerHTML = `省份:${chooseMesh.pop || 0}万人`;
// 设置标签位置到mesh的center位置
if (chooseMesh.center) {
label.position.set(chooseMesh.center[0], chooseMesh.center[1], 0);
} else {
// 如果没有center,使用射线检测的点
const point = selectedMesh.point;
label.position.copy(point);
}
label.position.y += 2; // 标签在mesh上方
label.scale.set(0.5, 0.5, 0.5);
}
}
};
// 监听窗口鼠标单击事件
this.renderer.domElement.addEventListener("click", choose);
// 创建后处理对象EffectComposer,WebGL渲染器作为参数
const composer = new EffectComposer(this.renderer);
//创建一个渲染器通道,场景和相机作为参数
const renderPass = new RenderPass(this.scene, this.camera);
composer.addPass(renderPass);
// OutlinePass第一个参数v2的尺寸和canvas画布保持一致
const v2 = new THREE.Vector2(window.innerWidth, window.innerHeight);
const outlinePass = new OutlinePass(v2, this.scene, this.camera);
outlinePass.visibleEdgeColor.set(0x00ffff);
outlinePass.edgeThickness = 4;
outlinePass.edgeStrength = 6;
//光源设置
// 平行光1
this.directionalLight = new THREE.DirectionalLight(0xffffff, 0.6);
this.directionalLight.position.set(0, 0, 50);
this.scene.add(this.directionalLight);
// 平行光2
this.directionalLight1 = new THREE.DirectionalLight(0xffffff, 0.6);
this.directionalLight1.position.set(0, 0, -50);
this.scene.add(this.directionalLight1);
//环境光
this.ambient = new THREE.AmbientLight(0xffffff, 0.6);
this.scene.add(this.ambient);
// 渲染循环
this.renderer.setAnimationLoop(() => {
// 遍历所有省份光圈,执行缩放动画
this.meshes.forEach((mesh) => {
mesh._s += 0.01;
mesh.scale.set(mesh._s, mesh._s, mesh._s);
if (mesh._s <= 1.3) {
mesh.material.opacity = (mesh._s - 1.0) * 3.3;
} else if (mesh._s > 1.3 && mesh._s <= 2.5) {
mesh.material.opacity = 1 - (mesh._s - 1) / 1.5;
} else {
mesh._s = 1.0;
}
});
this.meshes1.forEach((mesh1) => {
mesh1.rotateZ(0.01);
});
composer.render();
css3Renderer.render(this.scene, this.camera);
this.renderer.render(this.scene, this.camera);
// console.log(this.controls.target);
});
// 相机控件
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
// this.controls = new MapControls(this.camera, this.renderer.domElement);
this.controls.target.set(104.29, 28.48, 0); //与lookAt参数保持一致
// this.controls.enableRotate = false;
this.controls.update();
// 画布尺寸随着窗口变化
window.addEventListener("resize", () => {
css3Renderer.setSize(window.innerWidth, window.innerHeight);
c;
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
});
}
}

十四、行政区域拉伸不同高度可视化数据
javascript
// 高度:行政区轮廓拉伸高度,和人口大小正相关
var height = popObj[provinceName] / 5000; // 拉伸高度
var mesh = ExtrudeMesh(area.geometry.coordinates, height);

飞线
mergeBufferGeometries 合并多个几何体
相机控件MapControls
左键移动