包阅导读总结
1. 关键词:esMap、室内场景、3D模型、停车场、可视化引擎
2. 总结:作者在短时间内需完成停车场 3D 场景演示页面开发,通过 esMap 三维可视化引擎,快速搭建场景、实现动态标注、可视化操作等功能,并介绍了拓展开发的内容。
3. 主要内容:
– 背景介绍
– 作者被分配在两天内完成停车场 3D 场景演示页面开发任务。
– 前期准备
– 需求分析,将客户需求转化为功能需求。
– 实现步骤
– 场景建模,使用 esMap 将 CAD 图纸转换成 3D 模型。
– 动态标注,通过创建专属图层实现灯具设备图标动态加载和展示。
– 可视化操作,包括单个和批量控制设备状态,以及编辑标注。
– 拓展开发
– 动态添加模型,如枪机摄像头。
– 叠加热力图,展示人流量。
– 实现路径规划和导航功能。
思维导图:
文章地址:https://mp.weixin.qq.com/s/QPuFavZoADt3kin2IHM_Cg
文章来源:mp.weixin.qq.com
作者:gyratesky
发布时间:2024/7/2 7:05
语言:中文
总字数:5060字
预计阅读时间:21分钟
评分:88分
标签:esMap,3D可视化,WebGL,室内地图,动态标注
以下为原文内容
本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com
本文为稀土掘金技术社区首发签约文章
点击关注公众号,“技术干货”及时达!
背景介绍
这两天老板们又给我整活儿了,那天上午我还在赶当前的项目任务焦头烂额时,兄弟公司的领导突然来电邀我参加一个视频会议,会议上诉说了有一个重大项目要投标之类的重要程度,紧接着果不其然地给我分配了一个任务,在2天内完成停车场的3D场景演示页面开发,具体需求如下:
实现在网页上对停车场3D模型的展示,停车场有1-3层,提供CAD图纸;
实现停车场内部的灯具展示和控制,支持单独控制某个灯具,或者批量控制指定的灯具,灯具有相应的状态;
因为还有几天时间系统就要投标演示了,所以两天时间要搞定这些内容,这咋整?面对如此“无理”的需求,一开始我本来是想拒绝的,毕竟以我对webGL粗浅的认知,不做个十天半月肯定整不出啥东西。后来转念一想我没必要从零开始啊,这需求也不新,说不定互联网上早就有工具能够满足一部分需求。
经过了几个小时的AI+脑洞摸索,我终于找到一款非常合适的工具——esMap三维可视化引擎室内地图(https://www.esmap.cn/escopemap/content/cn/sdk-intro/sdk-intro.html) ,这个平台提供了非常便捷的场景搭建服务、 在线代码演示页面、以及相对齐全的SDK文档方便开发者快速接入,最后一顿操作猛如虎,当天就把演示DEMO搭建好,并将整个操作录制成视频提交过去,大大超出了领导的预期。接下来详细跟大家讲讲这到底是怎么做到的。
前期准备
需求分析
把前面的客户需求“翻译”为功能需求,我们大概得到下面几个任务,接下来我们只要把这些任务逐个击破,就能顺利达成目标。
-
场景建模:支持将CAD图纸的.dwg文件直接转换成3D模型,并对模型进行二次编辑 -
动态标注:在模型上叠加灯具设备图标,能够根据位置数据动态加载和展示 -
可视化操作:点击每个设备图标弹出设备详情,并提供设备操作按钮(比如开灯、关灯、调光等),点击按钮后发送指令并根据指令回调更新图标状态
启动工程
-
在官网演示包页面(https://www.esmap.cn/docs/down/)中获取基础的场景演示包,演示的工程包需要放到IIS、Tomcat、NodeJs等静态文件服务器下才能运行,这里我们用Node.js搭建 -
启动服务后,通过地址直接访问esMap中的页面示例即可,我们也可以直接在这里目录里创建自己的演示页面 parkingLot.html
实现步骤
1. 场景建模
-
选择“室内三维场景 – 免费版场景 – 创建三维场景”,可以免费创建5个场景 -
在弹窗区域选择导入停车场某一层的CAD图纸,新建三维场景提供2500㎡以内三维场景免费测试使用 -
导入成功后可以看见图纸,点击AI识别可以一键自动生成模型,免费版有场景面积限制 -
通过面板 编辑场景,可以添加各种静态标注、组件、模型 -
-
点击“提交”保存场景,回到“我的室内三维场景”,鼠标悬浮到每个场景面板右下角三个点图表,即可下载和编辑场景信息,我们把场景包下载下来。 -
在官网演示包页面中获取基础的场景演示包,将我们的场景包解压放到工程data目录中。 -
在刚刚创建的parkingLot.html中编写代码 <html>
...
<divid="map-container">
</div>
<scriptsrc="../lib/config.js"></script>
<scriptsrc="../lib/esmap-3.0.min.js"></script>
<scriptsrc="../lib/jquery-2.1.4.min.js"></script>
<scripttype="text/javascript">
constesmapID='$场景目录名称';
constcontainer=document.getElementById('map-container');
constmap=newesmap.ESMap({
mode:esmap.MapMode.Building,
container:container,
mapDataSrc:'../data',//生命三维场景的目录位置
token:"$场景的唯一TOKEN"//token从官网场景信息里获取
});
map.on('mapClickNode',(event)=>{
mapCoord=event.hitCoord||null;
console.log(JSON.stringify(mapCoord))
})
</script> -
如此我们就可以快速获得1个目标3D场景了
2. 动态标注
在模型上叠加灯具设备图标,能够根据位置数据动态加载和展示。添加标注有2种方法,可以在场景编辑页面中直接插入标注,然后随场景一起导出,不过这个方法处理的标注无法进行交互,仅适合作为场景的装饰的一部分,比如“电梯、安全通道、楼梯”之类的标识
-
动态标注的实现也很简单,esMap提供了esmap.ESTextMarker ,我们只要创建一个用于放ESTextMarker的专属图层,然后根据获取到所有标注的具体位置逐个创建实例就行了。
//灯具状态图标
constICON_LIGHT_ON='/data/parkinglot1/imagelabel/light_on.png'
constICON_LIGHT_OFF='/data/parkinglot1/imagelabel/light_off.png'
constdata=[
{
"id":2,
"name":"灯具",
"coord":{
"x":12957438.625300206,
"y":4851944.339834387
}
},...
]
functionaddMarkers(data){
constbuilding=map.getBuilding();
constfloorLayer=building.getFloor(1);
//获取或创建一个用于放ESTextMarker的专属图层
letlayer=floorLayer.getOrCreateLayerByName('dynamic',esmap.ESLayerType.TEXT_MARKER);
//添加标注
data.forEach((item,index)=>{
const{coord,id,name}=item
constmarker=newesmap.ESTextMarker({
id,
name,
x:coord.x,//空间坐标x
y:coord.y,//空间坐标y
height:2.5,//离地高度
image:ICON_LIGHT_OFF,
imageSize:64,
scale:1,
fixedSize:true,//是否固定大小,默认为true,可选参数
})
layer.addMarker(marker)
//缓存标注以便控制其状态
markerList.push(marker)
})
floorLayer.addLayer(layer)
}
addMarkers(data) -
销毁动态标注,就是直接调用ESTextMarker专属图层的removeAll方法
//清空标注
functionclearMarkers(){
constbuilding=map.getBuilding();
constfloorLayer=building.getFloor(1);
floorLayer.getLayersByNames('dynamic',(layer)=>{
if(layer){
//图层方法
layer.removeAll()
//清空数据
markerList.splice(0,markerList.length)
}
})
} -
有盆友可能会好奇设备的位置数据从哪来的。如果是大批量的数据,可以通过在场景中标定原点的坐标和单位长度动态计算出来;不过因为仅仅作为演示使用数量不多,就直接人工处理了,其实是我根据建筑图纸人肉点击地图获取到的。
-
3. 可视化操作
点击每个设备图标弹出设备详情,并提供设备操作按钮,点击按钮后发送指令并根据指令回调更新图标状态。这里涉及到单灯操作和批量操作,最主要的步骤是根据ID找到刚才动态创建的ESTextMarker 实例。
-
点击单个设备查看详情,并进行设备控制操作
functionpopInfo(target){
const{ID,x,y,name}=target
constpopMarker1=newesmap.ESPopMarker({
mapCoord:{
x,
y,
//控制信息窗的空间高度
height:7,
//设置弹框位于的楼层
fnum:1,
},
//弹框的宽度
width:340,
//弹框的高度
height:200,
//弹框的内容
content:`
<divclass="pop-info">
<ul>
<li><span>ID:</span>${ID}</li>
<li><span>Name:</span>${name||'未知'}</li>
<li><span>x:</span>${x}</li>
<li><span>y:</span>${y}</li>
<li>
<buttononclick="lightOn(${ID})">开灯</button>
<buttononclick="lightOff(${ID})">关灯</button>
</li>
</ul>
</div>
`,
closeCallBack:function(){
console.log("信息窗关闭了!");
},
created:function(){},
})
} -
控制单个设备的状态切换
functionlightOn(id){
constmarker=markerList.find(v=>v.ID==id)
if(marker){
marker.image=ICON_LIGHT_ON
}
}
functionlightOff(id){
constmarker=markerList.find(v=>v.ID==id)
if(marker){
marker.image=ICON_LIGHT_OFF
}
} -
批量控制
/**
*批量开灯
*/
functionlightOnAll(){
markerList.forEach(marker=>{
marker.image=ICON_LIGHT_ON
})
}
/**
*批量关灯
*/
functionlightOffAll(){
markerList.forEach(marker=>{
marker.image=ICON_LIGHT_OFF
})
} -
4. 编辑标注
最后一个需求是支持编辑修改灯具位置,并提供保存功能。
-
这里的实现方式是提供添加和删除功能
//监听鼠标事件
map.on('mapClickNode',(event)=>{
if(event.nodeType==esmap.ESNodeType.TEXT_MARKER){
if(isRemoveNode){
removeMarker(event)
}
}
mapCoord=event.hitCoord||null;
})
//为模型填充div添加点击事件
container.onclick=function(){
if(mapCoord){
console.log(JSON.stringify(mapCoord))
}
//标注编辑
if(isEditNode==true){
createMarker({coord:mapCoord,id:newDate().getTime().toString()})
}
}
//添加点标注
functioncreateMarker(item){
constbuilding=map.getBuilding();
constfloorLayer=building.getFloor(1);
constlayer=floorLayer.getOrCreateLayerByName('dynamic',esmap.ESLayerType.TEXT_MARKER);
const{coord,id}=item
constmarker=newesmap.ESTextMarker({
id,
x:coord.x,
y:coord.y,
height:2.5,//离地高度
image:ICON_LIGHT_OFF,
imageSize:64,
scale:1,
fixedSize:true,//是否固定大小,默认为true,可选参数
})
layer.addMarker(marker)
//缓存标注
markerList.push(marker)
}
//删除点标注
functionremoveMarker(target){
constbuilding=map.getBuilding();
constfloorLayer=building.getFloor(1);
floorLayer.getLayersByNames('dynamic',(layer)=>{
constindex=markerList.findIndex(v=>v.ID==target.ID)
layer.remove(markerList[index])
markerList.splice(index,1)
})
} -
数据保存的功能实现很简单,只要把此前一直在维护的markerList保存下来就行了。
//前端保存标注数据
functionsaveMarkerData(){
constdata=markerList.map((marker,index)=>{
const{x,y}=marker
return{
"id":newDate().getTime().toString()+index,
"name":"灯具",
"coord":{x,y}
}
})
console.log(data)
localStorage.setItem('esMapLightData',JSON.stringify(data))
console.info('数据缓存成功')
} -
拓展开发
接到需求的几个小时候我这套演示录屏发给领导,果不其然得到了领导的肯定,另外趁着还有1.5天的时间,希望能够争取再优化一波。那么接下来都是附加分了,我研究了esMap的API文档和官方示例,又增加了以下几个小功能。
1.动态添加模型
在停车场场景中增加几个枪机摄像头的模型,用于检测进出人流统计,以便后续设施优化调整,这个需求也合理。
添加模型的方式与添加标注如出一辙,无非就是换了一种Marker类;这里要注意的坑就是自己导出的gltf不能直接通过代码加载到三维场景上,所有的模型必须上传,服务器加密绑定到三维场景上,这里也是个收费点 😒。附上官方的模型处理教程(https://www.esmap.cn/docs/editor-tutorial/edit-gltfmodel.html)
//为模型填充div添加点击事件
container.onclick=function(){
if(mapCoord){
console.log(JSON.stringify(mapCoord))
}
//3D模型编辑
if(isEditModel){
addModel({coord:mapCoord,id:newDate().getTime().toString()})
}
}
//添加模型
functionaddModel(item){
constbuilding=map.getBuilding();
constfloorLayer=building.getFloor(1);
varlayer=floorLayer.getOrCreateLayerByName("models",esmap.ESLayerType.MODEL3D);
const{coord,id}=item
varim=newesmap.ES3DMarker({
x:coord.x,
y:coord.y,
id,
name:"tube",
url:"/model/White-Gun-Camera.gltf",//需要与服务端绑定
size:5,//尺寸缩放
angle:260*Math.random(),//朝向角度
height:2//离地高度
});
layer.addMarker(im);
}
2.叠加热力图
有了几个摄像头定点检测统计人流量,接下来就可以做场地的人流热力图了。
热力图的实现原理就是给一个平面上分布的点添加不同的权重,根据权重大小和热点范围进行渲染,在这里只要esmap.ESHeatMap提供的热点设置方法就能达到效果,需要注意的地方是注意设置好热力图的目标楼层。
//初始化热力图
constinitHeatMap=function(){
heatmapInstance=esmap.ESHeatMap.create(map,{
bid:building.id,
radius:20,//热点半径
opacity:1,//热力图透明度
mapOpacity:0.2,//设置三维场景楼层整体透明度
backgroundColor:'#FFFFFF',//热力图背景颜色,默认白色
max:100,//热力点value的最大值
});
}
//添加随机热力图
varaddRandomHeatMap=function(){
//创建热力图对象
if(!heatmapInstance){
initHeatMap();
}
//热力图归属于楼层1
heatmapInstance.setFloorNum(1);
//清除已有的热点
heatmapInstance.clearPoints();
heatmapInstance.randomPoints(200);
//热力图应用到指定的楼层
varfloorLayer=building.getFloor(1);
floorLayer.applyHeatMap(heatmaz
3.路径规划和导航功能
停车场最大的刚需都有哪些?我认为其中少不得的一个刚需就是车主找车导航,特别是像我这种健忘和路痴车主,路径规划和导航功能尤为重要。esMap提供了一套成熟的路径自动规划功能供用户使用,实现这套功能也需要对场景进行一些编辑。
-
自动规划的路径不是凭空而生的,目前还没有这么智能,我们需要提前把场景里所有的路径都绘制出来,告诉导航模块哪些地方是可以人或车可以行走的,哪里是单行道哪里是双行道等等。打开“场景编辑-导航选项”,我们进行路径绘制
-
注意将每条线段定义为”双向,其他-人行“,这样一来终点起点位置互换都没有问题。
-
编写相关代码,路径规划这里在交互上有个设计,点击地图一次确定起点,第二次确定终点,第三次清空路径并重新确定起点。
//鼠标点击次数
letclickCount=0
//最后一次点击的坐标
letlastCoord=null
//点击容器触发路径编辑,mapCoord为mapClickNode缓存的坐标
container.onclick=function(){
if(isEditRoute){
showRoute(mapCoord);
}
};
//显示规划路径
functionshowRoute(coord){
if(!navi)return;
if(coord!=null){
//第三次点击清除路径,重现设置起点起点
if(clickCount==2){
navi.clearAll();
clickCount=0;
lastCoord=null;
}
//第一次点击添加起点
if(clickCount==0){
lastCoord=coord;
navi.setStartPoint({
x:coord.x,
y:coord.y,
fnum:1,
url:"image/start.png",
height:1,
size:64,
});
}elseif(clickCount==1){
//添加终点并画路线
//判断起点和终点是否相同
if(lastCoord.x==coord.x){
alert("起点和终点不能相同!,请重新选点");
return;
}
navi.setEndPoint({
x:coord.x,
y:coord.y,
fnum:1,
url:"image/end.png",
height:1,
size:64,
});
//画导航线
navi.getRouteResult({
drawRoute:true,
})
}
clickCount++;
}
}
//开始导航
functionstartNavi(){
if(navi.isSimulating){
//正在导航中
return;
}
navi.followAngle=true;
navi.followPosition=true;
navi.scaleAnimate=false;
navi.simulate();
}
//重置导航
functionresetNav(){
navi.stop();
navi.clearAll();
}
写在最后
以上就是我这次搞定需求所有用到的内容了,代码下载链接放到这里供大家下载交流。这些内容说难不难,我们当然都可以使用three.js+其他webGL库实现,问题是使用three.js的话可能要从砖块开始垒起了 😂 ,估计没有一周时间下不来,但是使用esMap的话我可能2-4个小时就搞定了。
esMap作为一个SaSS编辑平台,最大的优点是快速便捷搭建,最大的缺点就是有收费项目,比如免费场景数量有限,模型库收费,上传使用自定义模型也要收费,不过这些也都可以理解啦,毕竟没有谁能永远活在真空中不花钱吃饭。有时候换个角度想一想,其实我们不需要那么多技术情节,如果在某些付费平台上花费一点点钱就能够解决十万火急的问题,或者就能替代十天半个月的工作量,出了bug只要反馈给开发团队不需要自己处理,相当于雇佣了1个团队的廉价劳动力了,何乐而不为 😬。
相关链接
esMap代码在线演示(https://www.esmap.cn/onlinedev/content/editor/index/index.html#pid=29&id=54)
esMap的API文档(https://www.esmap.cn/docs/sdk3.0/esmap.ESLayer.html)
工程代码(https://codesandbox.io/p/sandbox/esmap-parkinglot-hsmhkz)