В настоящее время я занимаюсь разработкой некоторых карт помещений, двумерных и трехмерных, а источники данных в основном в формате geojson.
Уже есть относительно зрелые фреймворки и решения для рисования карт на основе geojson.
Но сегодня нам еще предстоит просто реализовать отображение 3D-данных в Threejs.
кодовый адрес адрес предварительного просмотра
В основном реализует 2 функции
- Отображение 3D-карты
- Информационный дисплей POI
сбор информации
Сначала нам нуженОбщие данные китайских провинций
В этих данных нужные нам поля:
-
properties.name
Дисплей для информации о POI -
properties.centroid
Позиционирование для информации о POI -
geometry.coordinates
Используется для создания наших 3D-моделей
Построение 3D-окружения
Уведомление
- Ниже приведен только основной код
- Зеленая линия — это ось Y.
- Красная линия - это ось x
- Синяя линия — это ось Z.
mounted () {
// 初始化3D环境
this.initEnvironment()
// 构建光照系统
this.buildLightSystem()
// 构建辅助系统
this.buildAuxSystem()
},
methods: {
// 初始化3D环境
initEnvironment () {
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0xf0f0f0)
// 建一个空对象存放对象
this.map = new THREE.Object3D()
// 设置相机参数
this.setCamera();
// 初始化
this.renderer = new THREE.WebGLRenderer({
alpha: true,
canvas: document.querySelector('canvas')
})
this.renderer.setPixelRatio(window.devicePixelRatio)
this.renderer.setSize(window.innerWidth, window.innerHeight - 10)
document.addEventListener('mousemove', this.onDocumentMouseMove, false)
window.addEventListener('resize', this.onWindowResize, false)
},
setCamera () {
this.camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 10000);
this.camera.position.set(0, -70, 90);
this.camera.lookAt(0, 0, 0);
},
// 构建辅助系统: 网格和坐标
buildAuxSystem () {
let axisHelper = new THREE.AxesHelper(2000)
this.scene.add(axisHelper)
let gridHelper = new THREE.GridHelper(600, 60)
this.scene.add(gridHelper)
let controls = new THREE.OrbitControls(this.camera, this.renderer.domElement)
controls.enableDamping = true
controls.dampingFactor = 0.25
controls.rotateSpeed = 0.35
},
// 光照系统
buildLightSystem () {
let directionalLight = new THREE.DirectionalLight(0xffffff, 1.1);
directionalLight.position.set(300, 1000, 500);
directionalLight.target.position.set(0, 0, 0);
directionalLight.castShadow = true;
let d = 300;
const fov = 45 //拍摄距离 视野角值越大,场景中的物体越小
const near = 1 //相机离视体积最近的距离
const far = 1000//相机离视体积最远的距离
const aspect = window.innerWidth / window.innerHeight; //纵横比
directionalLight.shadow.camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
directionalLight.shadow.bias = 0.0001;
directionalLight.shadow.mapSize.width = directionalLight.shadow.mapSize.height = 1024;
this.scene.add(directionalLight)
let light = new THREE.AmbientLight(0xffffff, 0.6)
this.scene.add(light)
},
// 根据浏览器窗口变化动态更新尺寸
onWindowResize () {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
},
onDocumentMouseMove (event) {
event.preventDefault();
}
}
нарисовать модель карты
анализ данных
Далее нам нужно отрисовать карту согласно геометрии.координаты
"geometry": {
"type": "MultiPolygon",
"coordinates": [
[
[
[
117.210024,
40.082262
],
[
117.105315,
40.074479
],
[
117.105315,
40.074479
],
...
]
]
]
}
Преобразование координат
Наши координатные данные - это координаты широты и долготы, нам нужно преобразовать их в координаты плоскости используется здесьd3-geoМетод преобразования координат
граненый рисунок
Обратите внимание, что тип здесь — MultiPolygon, а наши координатные точки вложены в многоуровневый массив.
Поскольку в наших данных
Некоторые очертания провинций закрыты
Некоторые провинции состоят из нескольких частей.
Код
Наша модель разделена на две части
- Основная часть: мы используем THREE.Shape() + THREE.ExtrudeGeometry() для реализации
- Контурная часть: мы используем THREE.Line() для достижения
initMap () {
// d3-geo转化坐标
const projection = d3geo.geoMercator().center([104.0, 37.5]).scale(80).translate([0, 0]);
// 遍历省份构建模型
chinaJson.features.forEach(elem => {
// 新建一个省份容器:用来存放省份对应的模型和轮廓线
const province = new THREE.Object3D()
const coordinates = elem.geometry.coordinates
coordinates.forEach(multiPolygon => {
multiPolygon.forEach(polygon => {
// 这里的坐标要做2次使用:1次用来构建模型,1次用来构建轮廓线
const shape = new THREE.Shape()
const lineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff })
const linGeometry = new THREE.Geometry()
for (let i = 0; i < polygon.length; i++) {
const [x, y] = projection(polygon[i])
if (i === 0) {
shape.moveTo(x, -y)
}
shape.lineTo(x, -y);
linGeometry.vertices.push(new THREE.Vector3(x, -y, 4.01))
}
const extrudeSettings = {
depth: 4,
bevelEnabled: false
};
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings)
const material = new THREE.MeshBasicMaterial({ color: '#d13a34', transparent: true, opacity: 0.6 })
const mesh = new THREE.Mesh(geometry, material)
const line = new THREE.Line(linGeometry, lineMaterial)
province.add(mesh)
province.add(line)
})
})
// 将geojson的properties放到模型中,后面会用到
province.properties = elem.properties
if (elem.properties.centroid) {
const [x, y] = projection(elem.properties.centroid)
province.properties._centroid = [x, y]
}
this.map.add(province)
})
this.scene.add(this.map)
}
Эффект после внедрения такой
Информационный дисплей POI
Если речь идет о разработке внутренних карт, нам обычно нужно отобразить некоторую информацию о модуле, такую как имя, значок и так далее. Здесь мы просто отображаем название провинции.
Мой подход:
-
Получите координаты центральной точки каждого модуля провинции и преобразуйте их в экранные координаты.
-
Создайте новый холст и нарисуйте на холсте название провинции по координатам
-
Решить проблему столкновения координат
Код
showName () {
const width = window.innerWidth
const height = window.innerHeight
let canvas = document.querySelector('#name')
if (!canvas) return
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
// 新建一个离屏canvas
const offCanvas = document.createElement('canvas')
offCanvas.width = width
offCanvas.height = height
const ctxOffCanvas = canvas.getContext('2d');
// 设置canvas字体样式
ctxOffCanvas.font = '16.5px Arial';
ctxOffCanvas.strokeStyle = '#FFFFFF';
ctxOffCanvas.fillStyle = '#000000';
// texts用来存储显示的名称,重叠的部分就不会放到里面
const texts = [];
/**
* 遍历省份数据,有2个核心功能
* 1. 将3维坐标转化成2维坐标
* 2. 后面遍历到的数据,要和前面的数据做碰撞对比,重叠的就不绘制
* */
this.map.children.forEach((elem, index) => {
if (!elem.properties._centroid) return
// 找到中心点
const y = -elem.properties._centroid[1]
const x = elem.properties._centroid[0]
const z = 4
// 转化为二维坐标
const vector = new THREE.Vector3(x, y, z)
const position = vector.project(this.camera)
// 构建文本的基本属性:名称,left, top, width, height -> 碰撞对比需要这些坐标数据
const name = elem.properties.name
const left = (vector.x + 1) / 2 * width
const top = -(vector.y - 1) / 2 * height
const text = {
name,
left,
top,
width: ctxOffCanvas.measureText(name).width,
height: 16.5
}
// 碰撞对比
let show = true
for (let i = 0; i < texts.length; i++) {
if (
(text.left + text.width) < texts[i].left ||
(text.top + text.height) < texts[i].top ||
(texts[i].left + texts[i].width) < text.left ||
(texts[i].top + texts[i].height) < text.top
) {
show = true
} else {
show = false
break
}
}
if (show) {
texts.push(text)
ctxOffCanvas.strokeText(name, left, top)
ctxOffCanvas.fillText(name, left, top)
}
})
// 离屏canvas绘制到canvas中
ctx.drawImage(offCanvas, 0, 0)
}
Обратите внимание: поскольку наш холст сложен на холсте threejs только для отображения, нам нужно добавить стиль pointer-events: none;
Спасибо за прочтение