绘图工具
三维地图:Cesiumjs
建模方式:激光点云建模、航拍倾斜摄影建模、GIS建模、BIM建模、手工建模
建模工具:C4D Blender GeoBuilding ArcGIS
Cesiumjs
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="../Build/Cesium/Cesium.js"></script>
<link href="../Build/Cesium/Widgets/widgets.css" rel="stylesheet"/>
<style>
html,body{
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="Cesium"></div>
<script>
// 在 Cesium官网上 注册用户获取 token
Cesium.Ion.defaultAccessToken = '***';
// 基础图层,在线方式,从 Cesium 官方下载 瓦片
let baseLayer = Cesium.ImageryLayer.fromProviderAsync(
Cesium.IonImageryProvider.fromAssetId(3813) // 需要在 Cesium官网 Asset Depot 添加 对应图层的 权限
);
// 离线方式,自行维护一个可访问的瓦片目录
let baseLayer = Cesium.ImageryLayer.fromProviderAsync(
Cesium.TileMapServiceImageryProvider.fromUrl(
// 用模块的方式引入 Cesium 时,会有 /cesium/Assets/Textures 这个目录
Cesium.buildModuleUrl('/cesium/Assets/Textures/3813')
// 获取瓦片:用Chrome扩展程序 Save All Resources 保存用在线方式访问到的瓦片
)
);
baseLayer.gamma = 0; // 伽玛校正(对比度、亮度)
baseLayer.hue = Cesium.Math.toRadians(0); // 色调【色相】,取值范围在 0-PI,参考色调环
baseLayer.saturation = 1; // 饱和度,饱和度数值越低越(亮度高时)泛白(亮度低时)范黑
baseLayer.alpha = 1; // 透明度
baseLayer.brightness = 1; // 亮度
// 3D地图查看器
const viewer = new Cesium.Viewer('Cesium', {
baseLayerPicker: false, // 底图[卫星、地形、矢量]切换按钮
animation: false, // 左下角 时间播放控件
timeline: false, // 下方 时间轴
homeButton: false, // 右上角 主页按钮
navigationHelpButton: false, // 右上角 问号按钮
geocoder: false, // 右边上角 搜索框
fullscreenButton: false, // 右下角 全屏按钮
infoBox: false, // 点击实体时右侧出现的信息框
selectionIndicator: false, // 点击地球时鼠标处出现的指示框
contextOptions: {
webgl: {
alpha: true, // 允许透明背景
}
},
baseLayer,
});
viewer.scene.globe.show = true; // 显示地球
viewer.scene.skyBox.show = false; // 不显示星空
viewer.scene.sun.show = false; // 不显示太阳
viewer.scene.moon.show = false; // 不显示月球
viewer.scene.skyAtmosphere.show = false; // 不显示大气
viewer.scene.backgroundColor = Cesium.Color.TRANSPARENT; // 透明背景,需设置viewer.contextOptions.webgl.alpha
Cesium.GeoJsonDataSource.load("./world.json", { // 载入 GeoJson 矢量数据
fill: Cesium.Color.TRANSPARENT, // 透明填充
})
let headingPitchRange = new Cesium.HeadingPitchRange(Cesium.Math.toRadians(50), Cesium.Math.toRadians(-90), 2000);
// viewer.camera.lookAt(Cesium.Cartesian3.fromDegrees(116.39, 39.91), headingPitchRange); // 设置相机观察目标,同时设定了相机控制器的环绕点
viewer.scene.camera.setView({ // 切换相机视口
destination: Cesium.Cartesian3.fromDegrees(116.39, 39.91, 500000), // 相机经纬度和高度
orientation: { // 相机姿态
heading: Cesium.Math.toRadians(0), // 偏航角,在(相机与地心连线的法面)上的旋转,0为正北
pitch: Cesium.Math.toRadians(-100), // 俯仰角,在(相机与地心连线所在的经线平面)上选择,-90朝向地心
roll: 0 // 翻滚角
}
});
let position = Cesium.Cartesian3.fromDegrees(116.39, 39.91, 400);
viewer.entities.add({ // 添加实体
polyline: { // 线条实体
show: true,
positions: Cesium.Cartesian3.fromDegreesArray([116.39, 39.91, 116.40, 39.91]),
width: 5,
material: new Cesium.Color(0,0,1,1)
}
});
viewer.entities.add({ // 添加实体
id: 'point',
position, // 实体位置
point: { // 圆点实体
pixelSize: 100, // 圆点尺寸,为屏幕的像素尺寸,不随地图缩放和旋转
color: new Cesium.Color(0,1,0,1) // 圆点颜色
},
description: '<div>html</div>' // 被点击时右侧弹窗的内容
});
viewer.entities.add({ // 添加实体
position: Cesium.Cartesian3.fromDegrees(116.39, 39.91, 50), // 实体位置
plane: { // 矩形平面实体
plane: new Cesium.Plane(Cesium.Cartesian3.UNIT_Z, 0), // 朝向
dimensions: new Cesium.Cartesian2(400, 300),
material: Cesium.Color.RED.withAlpha(0.5), // 可以为图片
outline: true,
outlineColor: Cesium.Color.BLACK
}
});
let polygon = viewer.entities.add({ // 添加实体
id: 'polygon',
polygon: { // 多边形实体
hierarchy: Cesium.Cartesian3.fromDegreesArray([116.39, 39.91, 116.40, 39.91, 116.40, 39.90]),
material: Cesium.Color.YELLOW, // 可以为图片
extrudedHeight: 200 // 拉伸为三维物体
}
});
viewer.entities.getById("polygon"); // 获取实体
viewer.entities.remove(polygon); // 删除
viewer.entities.add({ // 添加实体
position: Cesium.Cartesian3.fromDegrees(116.39, 39.91, 150), // 实体位置
label: { // 标签实体
text: '标签',
font: '50px Helvetica',
fillColor: Cesium.Color.SKYBLUE
}
});
viewer.entities.add({ // 添加实体
position, // 实体位置
orientation: Cesium.Transforms.headingPitchRollQuaternion(position, new Cesium.HeadingPitchRoll(-90, 0, 0)), // 实体姿态
model: { // 3D模型实体
uri: './***.glb', // 载入模型
minimumPixelSize: 128, // 模型缩放时最小像素尺寸
maximumScale: 1000, // 模型缩放最大比率
show: true, // 是否显示
}
});
viewer.camera.viewBoundingSphere(new Cesium.BoundingSphere(position,20),new Cesium.HeadingPitchRange(0,0,0)); // 设置相机控制器360度环绕点
// viewer.trackedEntity = entity; // 相机控制器的环绕点
/* Cesium 坐标系 */
// WGS84弧度坐标系 new Cesium.Cartographic(经弧度, 维弧度, 高度);Cesium.Cartographic.fromDegrees(经度,维度,高度)
// 笛卡尔空间直角坐标系,原点为地心 new Cesium.Cartesian3(x,y,z);Cesium.Cartesian3.fromDegrees(经度,维度,高度)
// 屏幕坐标系 new Cesium.Cartesian2(x,y)
/* 坐标转换 */
// 弧度与角度互转:Cesium.Math.toRadians(),Cesium.Math.toDegrees()
// WGS84坐标系与笛卡尔坐标系互转: Cesium.Ellipsoid.WGS84.cartographicToCartesian(wgs84);Cesium.Ellipsoid.WGS84.cartesianToCartographic(cartesian3);Cesium.Cartographic.fromCartesian(cartesian3)
// 笛卡尔坐标系与屏幕坐标系互转:viewer.scene.pickPosition(cartesian2);viewer.scene.globe.pick(viewer.camera.getPickRay(cartesian2),viewer.scene);viewer.scene.camera.pickEllipsoid(cartesian2)
// Cesium.SceneTransforms.wgs84ToWindowCoordinates(viewer.scene,cartesian3);scene.cartesianToCanvasCoordinates(cartesian3)
// 鼠标拾取
let handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
handler.setInputAction(function (action) {
let pick = viewer.scene.pick(action.position);
if(Cesium.defined(pick)){
console.log(pick.id.id)
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
</script>
</body>
</html>
三维建筑物
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="../Build/Cesium/Cesium.js"></script>
<link href="../Build/Cesium/Widgets/widgets.css" rel="stylesheet"/>
<style>
html,body{
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="Cesium"></div>
<script>
// 在 Cesium官网上 注册用户获取 token
Cesium.Ion.defaultAccessToken = '****';
// 加载ArcGIS卫星地图栅格数据,比 Cesium 自带地图更加精细
const viewer = new Cesium.Viewer('Cesium', {
baseLayerPicker: false,
imageryProvider: new Cesium.ArcGisMapServerImageryProvider({
url: 'https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer'
}),
// 地形,需要在 Cesium官网 Asset Depot 添加 Cesium World Terrain 权限;Ctrl+鼠标滑动改变相机视角可以进入地形
terrainProvider: new Cesium.CesiumTerrainProvider({
url: Cesium.IonResource.fromAssetId(1),
requestVertexNormals: true,
requestWaterMask: true, // 水面效果
}),
});
// 添加建筑物模型,需要在 Cesium官网 Asset Depot 添加 Cesium OSM Buildings 权限
const tileset = viewer.scene.primitives.add(
new Cesium.Cesium3DTileset({
url: Cesium.IonResource.fromAssetId(96188),
})
);
// 建筑物模型样式
tileset.style = new Cesium.Cesium3DTileStyle({
color: "color('blue', 0.5)",
show: true
});
/* 加载夜晚地图,需要在 Cesium官网 Asset Depot 添加 Earth at Night 权限 */
// const viewer = new Cesium.Viewer('Cesium', {
// baseLayerPicker: false
// });
// 从 My Assets 里拷贝
// const layer = viewer.imageryLayers.addImageryProvider(
// new Cesium.IonImageryProvider({ assetId: 3812 })
// );
</script>
</body>
</html>
自转
javascript
rotate(116.39); // 北京经度
function rotate(longitude) {
viewer.scene.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(longitude, 20, 30000000), // 相机经纬度和高度
duration: 20, // 飞行时间
flyOverLongitude: longitude > 0 ? 180 : 0, // 转动时要经过的经度,从而确定转动的方向
easingFunction: Cesium.EasingFunction.LINEAR_NONE, // 均匀转动
complete() {
rotate(longitude > 0 ? longitude - 180 : 180 + longitude); // 转到背面
},
});
}
点位呼吸效果
javascript
viewer.entities.add({
id: "NewYork",
position: Cesium.Cartesian3.fromDegrees(-74.00, 40.43, 50),
billboard: {
image: './position.png',
}
});
viewer.entities.add({
id: 'NewYorkLabel',
position: Cesium.Cartesian3.fromDegrees(-74.00, 40.43, 50),
label: { // 标签
text: '纽约分公司',
font: '200 12px sans-serif', // font-weight font-size font-family
fillColor: Cesium.Color.fromCssColorString('#3D3D3D'), // 字体颜色
showBackground: true,
backgroundColor: Cesium.Color.fromCssColorString('#F0DBAF'), // 背景色,没法渐变
backgroundPadding: new Cesium.Cartesian2(10, 6),
pixelOffset: new Cesium.Cartesian2(0, -30) // 在position基础上的屏幕偏移
}
});
let NewYork = viewer.entities.getById("NewYork");
NewYork.billboard.scale = 1;
let progress = 0; // 呼吸渐变进度
let up = true; // 呼吸渐变方向
breath();
function breath() {
requestAnimationFrame(function () {
NewYork.billboard.scale = 1 + 0.1 * progress;
if(up){
if(progress >= 5){
up = false;
progress = progress - 1;
}
else {
progress = progress + 1;
}
}
else {
if(progress <= 0){
up = true;
progress = progress + 1;
}
else {
progress = progress - 1;
}
}
setTimeout(breath, 150);
});
}
地点连线(OD线 Origin-Destination Line)
html
<img id="gif" src="" style="position: absolute" />
javascript
var gif = {
name: "curve.gif", // 箭头从左侧中间点到右侧中间点
width: 1920,
height: 392,
};
// 终点位置
let toDegree = [108.947, 34.259];
let toCartesian3 = Cesium.Cartesian3.fromDegrees(toDegree[0], toDegree[1]);
let toCartesian2 = Cesium.SceneTransforms.wgs84ToWindowCoordinates(
viewer.scene,
toCartesian3
);
// 起点位置
let fromDegree = [121.506377, 31.245105];
let fromCartesian3 = Cesium.Cartesian3.fromDegrees(fromDegree[0], fromDegree[1]);
if (!isVisible(fromCartesian3)) {
fromCartesian3 = findVisibleEdge(fromDegree);
}
let fromCartesian2 =
Cesium.SceneTransforms.wgs84ToWindowCoordinates(
viewer.scene,
fromCartesian3
);
// 屏幕距离
let distance = Cesium.Cartesian2.distance(
toCartesian2,
fromCartesian2
);
/* 计算连线角度,试用 Cesium.Cartesian2.angleBetween 计算角度发现不对 */
let angle = 0;
let deltaY = toCartesian2.y - fromCartesian2.y;
if (toCartesian2.x > fromCartesian2.x) {
if (deltaY > 0) {
angle = Math.asin(deltaY / distance);
} else {
angle = -Math.asin(Math.abs(deltaY) / distance);
}
} else {
if (deltaY > 0) {
angle = Math.PI - Math.asin(deltaY / distance);
} else {
angle = Math.asin(Math.abs(deltaY) / distance) - Math.PI;
}
}
let width = distance; // 图片显示宽度
let height = (width / gif.width) * gif.height; // 图片显示高度
$("#gif")
.attr("src", gif.name)
.css("width", width + "px")
.css(
"left",
(fromCartesian2.x + toCartesian2.x) / 2 - width / 2 + "px"
)
.css(
"top",
(fromCartesian2.y + toCartesian2.y) / 2- height / 2 + "px"
)
.css("transform", `rotate(${angle}rad)`);
// 判断一个点是否可见,即是否在地球背面
function isVisible(cartesian3) {
return new Cesium.EllipsoidalOccluder(Cesium.Ellipsoid.WGS84, viewer.camera.position).isPointVisible(cartesian3);
}
// 给一个不可见的点找一个同维度的、可见的、离原点最近的点,此点在可见范围边缘上
function findVisibleEdge(fromDegree) {
let cartesian3From;
let fromLongitude = fromDegree[0];
let toLongitude = toDegree[0];
let fromEast = fromLongitude - toLongitude > 0;
let moveEast;
if (fromEast) {
moveEast = fromLongitude - toLongitude > 180;
} else {
moveEast = toLongitude - fromLongitude < 180;
}
do {
if (moveEast) {
fromLongitude = fromLongitude + 0.1;
fromLongitude = fromLongitude < 180 ? fromLongitude : fromLongitude - 360;
} else {
fromLongitude = fromLongitude - 0.1;
fromLongitude = fromLongitude > -180 ? fromLongitude : 360 + fromLongitude;
}
cartesian3From = Cesium.Cartesian3.fromDegrees(fromLongitude, fromDegree[1]);
} while (!isVisible(cartesian3From));
return cartesian3From;
},
判断一个点是否在GeoJSON内
javascript
import chinaJson from './100000.json'; // 中国区域
handler.setInputAction((action) => {
let inChina = false;
// 屏幕坐标,如果用了 autofit.js,要进行处理
position = this.autoFitPosition(action.endPosition);
// 笛卡尔坐标
let cartesian3 = viewer.scene.camera.pickEllipsoid(position);
if (cartesian3) {
// 经纬弧度坐标
let cartographic = Ellipsoid.WGS84.cartesianToCartographic(cartesian3);
chinaJson.features.forEach((feature) => {
feature.geometry.coordinates[0].forEach((polygon) => {
// 是否在区域内
if (this.isInPolygon([CesiumMath.toDegrees(cartographic.longitude), CesiumMath.toDegrees(cartographic.latitude)], polygon)) {
inChina = true;
}
});
});
}
}, ScreenSpaceEventType.MOUSE_MOVE);
isInPolygon(checkPoint, polygonPoints) {
let counter = 0;
let pointCount = polygonPoints.length;
let p1 = polygonPoints[0];
let i, xinters, p2;
for (i = 1; i <= pointCount; i++) {
p2 = polygonPoints[i % pointCount];
if (checkPoint[0] > Math.min(p1[0], p2[0]) && checkPoint[0] <= Math.max(p1[0], p2[0])) {
if (checkPoint[1] <= Math.max(p1[1], p2[1])) {
if (p1[0] !== p2[0]) {
xinters = ((checkPoint[0] - p1[0]) * (p2[1] - p1[1])) / (p2[0] - p1[0]) + p1[1];
if (p1[1] === p2[1] || checkPoint[1] <= xinters) {
counter++;
}
}
}
}
p1 = p2;
}
return counter % 2 > 0;
},
autoFitPosition(position) {
let scale = 1;
let transform = document.querySelector('body').style.transform;
if (transform) {
scale = transform.split('(')[1].split(')')[0];
scale = parseFloat(scale);
}
return new Cartesian2(position.x / scale, position.y / scale);
},
UE 像素流
package.json
javascript
"dependencies": {
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.3": "^1.0.6", // 版本与UE保持一致
"@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.3": "^1.0.5",
},
vue
javascript
<template>
<div class="ue-container" ref="ueContainer"></div>
</template>
<script>
import { Config, PixelStreaming, Flags } from '@epicgames-ps/lib-pixelstreamingfrontend-ue5.3'
import {
Application,
PixelStreamingApplicationStyle,
UIElementCreationMode
} from '@epicgames-ps/lib-pixelstreamingfrontend-ui-ue5.3'
import io from 'socket.io-client'
let ueApplication, stream
export default {
mounted() {
this.createUE()
},
methods: {
createUE() {
const PixelStreamingApplicationStyles = new PixelStreamingApplicationStyle()
PixelStreamingApplicationStyles.applyStyleSheet()
const config = new Config({ useUrlParams: true })
config.getTextSettings().at(0).value = 'ws://信令服务器地址' // 设置 SignallingServerUrl
config.setFlagEnabled(Flags.AutoConnect, true) // 自动连接
config.setFlagEnabled(Flags.AutoPlayVideo, true) // 自动播放
config.setFlagEnabled(Flags.StartVideoMuted, true) // 播放时静音,上面的自动播放才能生效
// config.setFlagEnabled(Flags.MouseInput, false) // 禁止鼠标操作
// config.setFlagEnabled(Flags.KeyboardInput, false) // 禁止键盘操作
config.setFlagEnabled(Flags.HoveringMouseMode, true) // 进入操作状态时依然显示光标
stream = new PixelStreaming(config)
ueApplication = new Application({
stream,
onColorModeChanged: (isLightMode) => PixelStreamingApplicationStyles.setColorMode(isLightMode),
fullScreenControlsConfig: {
isEnabled: false // 隐藏全屏按钮
},
settingsPanelConfig: {
visibilityButtonConfig: { creationMode: UIElementCreationMode.Disable } // 隐藏设置按钮
},
statsPanelConfig: {
visibilityButtonConfig: { creationMode: UIElementCreationMode.Disable } // 隐藏信息按钮
},
videoQpIndicatorConfig: {
disableIndicator: { disableIndicator: true } // 隐藏信号强度图标
}
})
this.$refs.ueContainer.appendChild(ueApplication.rootElement)
stream.addResponseEventListener('handle_responses', this.handleUeResponse) // 监听UE推送的消息
stream.addEventListener('videoInitialized', this.initScene) // UE初始化完成事件
},
sendMessage(data) {
stream.emitUIInteraction(data)
},
handleUeResponse(msg) {
console.log(msg)
},
async initScene() {
this.sendMessage('')
},
forbidMouse() {
// 某种情况下禁止鼠标操作,如果用 ueApplication.stream.setFlagEnabled(Flags.MouseInput, false) 会导致不能解禁
document.getElementById('videoElementParent').style.pointerEvents = 'none'
document.getElementById('streamingVideo').style.pointerEvents = 'none'
},
}
}
</script>
<style lang="less">
body {
width: 100vw;
height: 100vh;
min-height: -webkit-fill-available;
margin: 0;
#playerUI {
position: absolute;
z-index: 0;
video {
object-fit: fill;
}
}
}
.ue-container {
position: absolute;
width: 100vw;
height: 100vh;
overflow: hidden;
}
</style>
peer-stream
vue
html
<script>
import '@/utils/peer-stream.js'
let ueVideo
export default {
mounted() {
ueVideo = document.createElement('video', { is: 'peer-stream' })
ueVideo.id = 'ws://信令地址'
this.$refs.container.appendChild(ueVideo)
ueVideo.addEventListener('playing', this.initScene) // UE场景开始渲染
ueVideo.addEventListener('message', this.handleUeResponse) // 监听UE推送的消息
},
methods: {
sendUeMessage(data) {
// 发送消息
ueVideo.emitMessage(data)
},
}
}
</script>
<style lang="less">
body {
height: 100vh;
min-height: -webkit-fill-available;
margin: 0;
min-width: 1920px;
position: relative;
.video-container {
video {
position: absolute;
z-index: 0;
width: 100%;
height: 100%;
background-color: #0A1B2F;
}
}
}
</style>