功能解析
我们一般提到拆楼,其实就是单独的显示某一层的模型,为了实现这个功能,我们在数据层面就得先准备好。这里我们建议把每一层的模型都单独发布,让图层以楼层为结构,后续在实现我们的功能就会非常简单。
注意:我们需要提前在Explorer中导入这些分层模型数据。
我这里也给大家准备了一份测试数据,就是图片上的数据,另外也把本次文章的代码共享给大家,大家可以下载学习一下。
实现拆楼一般有两种方式:显隐拆楼与位移拆楼,咱们一一来解决一下。
显隐拆楼
其实就是显示当前的楼层,然后把在这层之上的模型全部隐藏,比如我显示5楼,我就把5楼以上的模型全部隐藏,其实就实现了单独查看楼层的功能。
实现逻辑
实现逻辑很简单,这里先简单列举一下:
- 获取图层树信息,让图层名称与id绑定。(涉及对象 infotree)
- 点击楼层事件:
- 创建两个数组,分别存放隐藏图层id和显示图层id。(比当前楼层高就隐藏,低就显示)
- 执行api,让图层进行显隐藏。(涉及对象 tileLayer)
- 重置事件:把图层全部设置为显示
实现步骤
1.前置:图层名称与id绑定
我们知道,使用飞渡api的时候,传参都是用的id,但图层的id普遍都是随机生成的,没有实际的概念。所以我们一般会用图层名称去绑定id,一是让代码的可读性更高,二是方便后续维护。
这里我们先要创建一个数组,存储对应的图层名称,这里的图层名称要和Explorer工程的名称对应,并且要从低到高排列,方便后续判断。再创建一个对象,用于存储名称与id的对应关系,方便接口调用。
javascript
const buildNameList = ['L1-4', 'L5', 'L6', 'L7', 'L8', 'L9', 'L10', 'L11', 'L12', 'L13'] // 拆楼建筑名称数组
const buildIdObj = {} // 拆楼建筑对象,保存id
在场景初始化后,为了清除上个用户执行的操作,需要先用fdapi.reset()
重置一下视频流。再通过fdapi.infoTree.get()
获取图层树信息,获取的信息大概是长这样的,这里列举了拆楼相关的对象。
json
{
"infotree":[
{"iD":"5D6728F44BEAA533268D6EA04ADC368F","index":119,"parentIndex":118,"name":"L1-4","visiblity":true,"type":"EPT_Scene"},{"iD":"70EEA6304541BFB27E6B618878888B40","index":120,"parentIndex":118,"name":"L5","visiblity":true,"type":"EPT_Scene"},{"iD":"261F27DE488D97B44A02269B1822B71D","index":121,"parentIndex":118,"name":"L6","visiblity":true,"type":"EPT_Scene"},{"iD":"6F5F764C4E3C1D5449744D9682BD5AD3","index":122,"parentIndex":118,"name":"L7","visiblity":true,"type":"EPT_Scene"},{"iD":"19290B504B590F6D0C10BC9338C56FA9","index":123,"parentIndex":118,"name":"L8","visiblity":true,"type":"EPT_Scene"},{"iD":"071393574557CA6CE69885A50902E490","index":124,"parentIndex":118,"name":"L9","visiblity":true,"type":"EPT_Scene"},{"iD":"69D4CB194720B94227E0AEABF37BB186","index":125,"parentIndex":118,"name":"L10","visiblity":true,"type":"EPT_Scene"},{"iD":"D3B74FE946F47778DD3D649093F8344E","index":126,"parentIndex":118,"name":"L11","visiblity":true,"type":"EPT_Scene"},{"iD":"FA364521477CAFC950D2B0A8BC68C1B3","index":127,"parentIndex":118,"name":"L12","visiblity":true,"type":"EPT_Scene"},{"iD":"BD8B0B704427DBAB94779FB6075C6D5F","index":128,"parentIndex":118,"name":"L13","visiblity":true,"type":"EPT_Scene"}]
}
所以这里我们获取回调后的infotree
对象,做一个循环,把图层名称与id放入到一个对象中,做对应关系,方便后续调用。下面就是初始化后执行的代码。
javascript
const __DigitalTwinPlayer = new DigitalTwinPlayer(HostConfig.Player, {
domId: 'player',
apiOptions: {
//事件监听回调函数
onReady: async () => {
// 重置视频流
await fdapi.reset(2 | 4)
// 获取图层树信息,让图层名称与id绑定
const { infotree } = await fdapi.infoTree.get()
infotree.forEach(item => {
if (buildNameList.includes(item.name)) {
buildIdObj[item.name] = item.iD
}
})
}
}
})
2.楼层点击事件
首先创建两个数组,分别存放隐藏图层id和显示图层id。
点击楼层后,把当前楼层的名称获取到。然后循环图层名称数组,默认都push
到显示id数组中,当循环名称与点击名称一致时,后面的id就push
到隐藏数组中。
最后分别通过__g.tileLayer.show()
与__g.tileLayer.hide()
,去显示隐藏对应的图层即可。
javascript
// 选择楼层
const chooseFloor = name => {
const showList = [],
hideList = []
// 高于选择楼层就要隐藏,这边做个判断
let isHide = false
buildNameList.forEach(itemName => {
isHide ? hideList.push(buildIdObj[itemName]) : showList.push(buildIdObj[itemName])
if (itemName == name) isHide = true
})
// 操作模型
__g.tileLayer.show(showList)
__g.tileLayer.hide(hideList)
}
3.重置模型事件
这个方法比较简单,就是获取所有图层的id,然后都显示出来,这里就不再赘述。
javascript
// 重置模型
const reset = () => {
const idList = []
for (let key in buildIdObj) idList.push(buildIdObj[key])
__g.tileLayer.show(idList)
}
把这些实现完后,咱们的显隐拆楼就完成了。
实现代码
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>页面初始化</title>
<link rel="stylesheet" href="./lib/styles.css" />
<!-- 按需替换dts ac.min.js路径 -->
<script type="text/javascript" src="./lib/aircity/ac_conf.js"></script>
<script type="text/javascript" src="./lib/aircity/ac.min.js"></script>
</head>
<body>
<div id="player"></div>
<div class="btn" onclick="reset()">重置模型</div>
<div class="build-list">
<div class="build-item" onclick="chooseFloor('L13')">L13</div>
<div class="build-item" onclick="chooseFloor('L12')">L12</div>
<div class="build-item" onclick="chooseFloor('L11')">L11</div>
<div class="build-item" onclick="chooseFloor('L10')">L10</div>
<div class="build-item" onclick="chooseFloor('L9')">L9</div>
<div class="build-item" onclick="chooseFloor('L8')">L8</div>
<div class="build-item" onclick="chooseFloor('L7')">L7</div>
<div class="build-item" onclick="chooseFloor('L6')">L6</div>
<div class="build-item" onclick="chooseFloor('L5')">L5</div>
<div class="build-item" onclick="chooseFloor('L1-4')">L1-4</div>
</div>
<script type="text/javascript">
// 拆楼建筑名称数组
const buildNameList = ['L1-4', 'L5', 'L6', 'L7', 'L8', 'L9', 'L10', 'L11', 'L12', 'L13']
// 拆楼建筑对象,保存id
const buildIdObj = {}
/**
* 初始化场景
*/
const OnLoad = () => {
const __DigitalTwinPlayer = new DigitalTwinPlayer(HostConfig.Player, {
domId: 'player',
apiOptions: {
//事件监听回调函数
onReady: async () => {
// 重置视频流
await fdapi.reset(2 | 4)
// 获取图层树信息,让图层名称与id绑定
const { infotree } = await fdapi.infoTree.get()
infotree.forEach(item => {
if (buildNameList.includes(item.name)) {
buildIdObj[item.name] = item.iD
}
})
}
}
})
}
// 选择楼层
const chooseFloor = name => {
const showList = [],
hideList = []
// 高于选择楼层就要隐藏,这边做个判断
let isHide = false
buildNameList.forEach(itemName => {
isHide ? hideList.push(buildIdObj[itemName]) : showList.push(buildIdObj[itemName])
if (itemName == name) isHide = true
})
// 操作模型
__g.tileLayer.show(showList)
__g.tileLayer.hide(hideList)
}
// 重置模型
const reset = async () => {
const idList = []
for (let key in buildIdObj) idList.push(buildIdObj[key])
__g.tileLayer.show(idList)
}
/**
* 自适应分辨率
*/
const onResize = async () => {
let playerView = document.getElementById('player')
playerView.style.height = window.innerHeight + 'px'
playerView.style.width = window.innerWidth + 'px'
}
// 页面加载调用
window.addEventListener('load', OnLoad, true)
// 页面窗口变换调用
window.addEventListener('resize', onResize, true)
</script>
</body>
</html>
位移拆楼
实现位移拆楼有两个阶段
-
模型开启:把模型设置成开启状态,先把所有楼层都往上位移,并且都保留间距。
-
抽拉楼层:点击楼层后把对应的楼层单独位移抽拉出来。
实现步骤
位移的实现逻辑会比显隐的方式复杂一些,这里也先简单列举一下:
- 获取图层树信息,绑定图层对应信息。(涉及对象 infotree)
- 让图层名称与id绑定。
- 记录模型的初始位置,方便后续重置模型。
- 记录模型的开启位置,计算楼层开启后每个图层对应的z值做保存,方便后续开启模型。
- 楼层位移事件:封装一个方法,让模型可以从起点位移到终点,并且位移速度需要可控。
- 模型开启:把每个模型都往上做位移,从初始位置位移到开启位置。
- 点击楼层事件:
- 计算抽拉位置:计算模型位移后的点位,通过方向、距离两个参数计算。
- 如果当前模型没有其他楼层展开,则直接把点击图层位移到抽拉位置。
- 如果当前模型有其他楼层展开,则还需要把之前的图层收回。(收回就是回到开启位置)
- 如果点击的和当前展开的是同一层,则直接收回当前图层。
- 重置事件:把图层全部设置为初始位置
从以上步骤我们可以分析出需要的全局参数
- 状态参数
- 建筑开启状态-Boolean:用于记录建筑是否开启,如果没开启就不能点击楼层和重置模型。
- 当前建筑展开的楼层-String:在点击楼层事件中做判断使用。
- 模型当前是否在移动-Boolean:如果模型还在移动,就禁止其他事件,防止交互阻塞。
- 效果参数
- 建筑抬升高度-Number:用于计算模型的开启位置。
- 动画帧数-Number:位移的动画需要多少帧完成,dts都是异步操作,直接用速度会把握不准,所以使用帧来代替速度。
- 楼层位移距离-Number:用于计算楼层的抽拉位置。
- 楼层位移角度-Number:用于计算楼层的抽拉位置。
具体方法
1.前置:定义全局参数,绑定图层信息
和前面一致,需要定义一个数组存储图层名称。再创建一个对象,用于存储每个图层的必要信息。再定义上述提到的全局参数。
javascript
const buildNameList = ['L1-4', 'L5', 'L6', 'L7', 'L8', 'L9', 'L10', 'L11', 'L12', 'L13']
const buildInfo = {}
// 状态参数
let openState = false, // 建筑开启状态
openFloor = '', // 建筑开启的楼层
moveState = false // 是否正在移动(动画)
// 效果参数
const fps = 8, // 动画需要多少帧完成
risingHeight = 5, // 建筑抬升高度
distance = 20, // 拆楼距离
angle = 270 // 拆楼角度,0是x轴即正东方向,逆时针计算
执行场景初始化,并在初始化完成后,获取图层树信息。保存三个信息:id、初始位置、开启位置。
id可以直接获取,初始位置则需要通过fdapi.tileLayer.get()
获取图层详细信息,我们拿一个图层举例,获取的信息大致如下:
json
{
"data":[
{
"id": "5D6728F44BEAA533268D6EA04ADC368F",
"groupId": "",
"userData": "",
"dbTabId": "AB1EE06C468C21FA1C46578838AEDB6F",
"fileName": "H:/work/客户成功-培训中心/专题视频/01二次开发实现拆楼/3dt/L1-4.3dt",
"supportAttach": "true",
"minVisibleHeight": -10000000,
"maxVisibleHeight": 10000000,
"bFlattenSupported": 0,
"bbox": [493286.53,2492013.53, -3.521393,493336.769375,2491968.09,15.45922],
"rotation": [ 0,0,0],
"scale": [1, 1,1],
"location": [493309.21,2491988.39,2.3]
}
]
}
可以看到location
就是初始位置的坐标,但开启位置需要通过全局参数risingHeight
稍微计算一下,从下往上,每个模型的位移值都要再加上risingHeight
的高度值,最终实现的代码如下:
javascript
const __DigitalTwinPlayer = new DigitalTwinPlayer(HostConfig.Player, {
domId: 'player',
apiOptions: {
//事件监听回调函数
onReady: async () => {
fdapi.reset(2 | 4)
// 获取图层树信息
const { infotree } = await fdapi.infoTree.get()
// 把图层id存放到一个数组中,方便查询属性
const idList = []
infotree.forEach(item => {
if (buildNameList.includes(item.name)) {
idList.push(item.iD)
}
})
// 查询图层属性
const { data } = await fdapi.tileLayer.get(idList)
// 存储每个图层对应的属性
data.forEach((item, index) => {
buildInfo[buildNameList[index]] = {
id: item.id,
location: item.location,
openLocation: [item.location[0], item.location[1], item.location[2] + (index + 1) * risingHeight]
}
})
}
}
})
2.楼层位移事件
这里我们需要封装一个通用方法,传参是图层id和终点位置,我们能直接把对应id的模型,从当前的位置位移到终点位置,这里我们用到的方法就是fdapi.tileLayer.setLocation()
,通过这个方法我们可以进行图层位移,但只能瞬间移动,不能做到平滑移动。
这里我们可以引用一个方法,通过两个点的坐标以及分段数,计算两个点之间的的点位坐标,如下所示:
javascript
// 传参示例
getLineSegmentPoint([[0,0,20],[100,0,20]],20);
// 线段分点
function getLineSegmentPoint(lineSegment, interval) {
try {
if (interval && lineSegment && lineSegment.length === 2) {
const point1 = lineSegment[0]
const point2 = lineSegment[1]
const a = point2[1] - point1[1]
const b = point2[0] - point1[0]
const c = point2[2] - point1[2]
const o = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2) + Math.pow(c, 2))
const n = o / interval
const p = []
for (let i = 1; i <= interval; i++) {
const x = (b / o) * (n * i) + point1[0]
const y = (a / o) * (n * i) + point1[1]
const z = (c / o) * (n * i) + point1[2]
p.push([x, y, z])
}
return p
} else {
console.error('线段取点失败', lineSegment, interval)
}
} catch (error) {
console.error('线段取点失败', error)
}
}
有了这个方法我们就在两个点直接有了非常多的点位,可以通过fdapi.tileLayer.setLocation()
多次移动,做到平滑位移了。我们平常用其他系统大多数都是同步逻辑,在dts中api操作都是异步的,所以这里移动不需要写定时器,直接循环调用就可以做到逐帧调用代码了,具体代码如下:
javascript
const fps = 20 // 全局参数
// 传参示例
setLocation({id:'123456',location:[100,0,20]});
// 模型位移,从模型当前位置位移到指定位置
const setLocation = param => {
// 设置移动状态为true
moveState = true
// 获取模型当前位置-通过tileLayer.get 获取模型信息
const { data } = await fdapi.tileLayer.get(param.id)
const startLocation = data[0].location
const endLocation = param.location
const pathPoints = getLineSegmentPoint([startLocation, endLocation], fps)
// 每一帧根据路径点移动一次模型
for (let f = 0; f <= fps - 1; f++) {
fdapi.tileLayer.setLocation(param.id, pathPoints[f])
// 最后一帧,把移动状态设置为false
if (f == fps - 1) moveState = false
}
}
单个模型的位移完成了,但我们有些情况不是单个图层位移,例如设置开启状态的时候,我们需要同时移动多个图层。这时候我们需要改造一下我们的方法,让他的输入参改为数组,每一帧循环调用fdapi.tileLayer.setLocation()
方法去设置位移。但由于DTS的api是异步操作,所以会导致移动的时候会一步步执行,很显然不是我们想要的效果。 我们想要的效果是让这几个图层,在上升的时候,是同步进行上升的。这时候我们可以用到
fdapi.tileLayer
的updateBegin()
与updateEnd()
方法。
在开始修改之前调用updateBegin()
,然后可以多次调用setXXX()
方法,最后调用updateEnd()
提交修改更新数据。这样可以让两个方法中间的位移操作变成同步。改造后的代码如下:
javascript
// 模型位移,从模型当前位置位移到指定位置
const setLocation = async arr => {
// 设置移动状态为true
moveState = true
// 获取模型当前位置-通过tileLayer.get 获取模型信息
const idList = []
for (let index in arr) idList.push(arr[index].id)
const { data } = await fdapi.tileLayer.get(idList)
// 获取根据起点和终点,计算路径点
for (let index in arr) {
const startLocation = data[index].location
const endLocation = arr[index].location
arr[index].pathPoints = getLineSegmentPoint([startLocation, endLocation], fps)
}
// 每一帧根据路径点移动一次模型
for (let f = 0; f <= fps - 1; f++) {
fdapi.tileLayer.updateBegin()
for (let index in arr) fdapi.tileLayer.setLocation(arr[index].id, arr[index].pathPoints[f], null)
await fdapi.tileLayer.updateEnd()
if (f == fps - 1) moveState = false
}
}
3.模型开启事件
模型开启就是把所有楼层从初始位置设置到开始位置,两个位置坐标在初始化的时候就已经存储好了。我们通过循环的方式直接批量的调用前面的楼层位移事件即可。
javascript
// 开始拆楼
const begin = () => {
if (openState) {
console.log('模型已经开启,可点击楼层进行拆楼')
return
}
// 构造setLocationList id,对应结束的位置
const setLocationList = []
for (let key in buildInfo) {
setLocationList.push({
id: buildInfo[key].id,
location: buildInfo[key].openLocation
})
}
setLocation(setLocationList)
// 打开状态设置为开启
openState = true
}
4.点击楼层事件
在实现楼层位移之前,我们需要通过全局参数的方向和距离来计算位移的终点位置,这里也是用了一个方法来计算,具体如下:
javascript
//根据角度和距离计算点 传参:点位坐标[0,0,0],角度,距离
function calculatePoint(point, angle, distance) {
const radians = angle * (Math.PI / 180) // 将角度转换为弧度
const x1 = point[0] + distance * Math.cos(radians)
const y1 = point[1] + distance * Math.sin(radians)
return point[2] ? [x1, y1, point[2]] : [x1, y1]
}
有了这个方法后,我们只需要通过计算终点点位,再执行楼层位移事件即可,但我们还得分析一下一些特殊情况:
当楼层是开启状态时,点击楼层就会把对应的楼层抽拉出来,这时有下列几种情况
- 如果拆楼状态没开启,则直接返回。
- 如果模型仍在位移,则直接返回。
- 如果点击的是顶层,则直接返回(顶层已经可见,不需要抽拉)。
- 如果当前模型没有其他楼层展开,则直接把点击图层位移到抽拉位置。
- 如果当前模型有其他楼层展开,则还需要把之前的图层收回。(回到开启位置即可)
- 如果点击的和当前展开的是同一层,则直接收回当前图层。
所以我们需要判断不同的条件,执行一些不同的操作,最终的实现如下:
javascript
// 点击某一层
const chooseFloor = name => {
if (!openState) {
console.log('请先开启拆楼')
return
}
if (moveState) {
console.log(='模型位移中,请等模型位移结束再操作')
return
}
if (name === buildNameList[buildNameList.length - 1]) {
console.log( '顶层不拆')
return
}
const setLocationList = []
// 如果已经有打开的楼层,则要把当前楼层回收
if (openFloor) {
setLocationList.push({ id: [buildInfo[openFloor].id], location: buildInfo[openFloor].openLocation })
// 同一层则直接重置当前楼层
if (openFloor == name) {
setLocation(setLocationList)
openFloor = ''
return
}
}
// 需要位移的楼层:通过起点、角度、距离 计算终点位置
const startLocation = JSON.parse(JSON.stringify(buildInfo[name].openLocation))
const endLocation = calculatePoint(startLocation, angle, distance)
setLocationList.push({ id: [buildInfo[name].id], location: endLocation })
setLocation(setLocationList)
openFloor = name // 保存当前楼层信息
}
5.重置模型事件
这个方法比较简单,就是获取所有图层的id,然后都设置成初始位置,再把状态参数都设置为默认,这里就不再赘述。
javascript
// 重置模型
const reset = () => {
if (!openState) {
console.log('模型不需要重置')
return
}
fdapi.tileLayer.updateBegin()
for (let key in buildInfo) {
fdapi.tileLayer.setLocation(buildInfo[key].id, buildInfo[key].location)
}
fdapi.tileLayer.updateEnd()
openState = false
openFloor = ''
}
把这些实现完后,咱们的位移拆楼就完成了。
实现代码
javascript
const buildNameList = ['L1-4', 'L5', 'L6', 'L7', 'L8', 'L9', 'L10', 'L11', 'L12', 'L13']
const buildInfo = {}
// 状态参数
let openState = false, // 建筑开启状态
openFloor = '', // 建筑开启的楼层
moveState = false // 是否正在移动(动画)
// 效果参数
const fps = 20, // 动画需要多少帧完成
risingHeight = 5, // 建筑抬升高度
distance = 20, // 拆楼距离
angle = 270 // 拆楼角度,0是x轴即正东方向,逆时针计算
/**
* 初始化场景
*/
const OnLoad = () => {
const __DigitalTwinPlayer = new DigitalTwinPlayer(HostConfig.Player, {
domId: 'player',
apiOptions: {
//事件监听回调函数
onReady: async () => {
fdapi.reset(2 | 4)
// 获取图层树信息
const { infotree } = await fdapi.infoTree.get()
// 把图层id存放到一个数组中,方便查询属性
const idList = []
infotree.forEach(item => {
if (buildNameList.includes(item.name)) {
idList.push(item.iD)
}
})
// 查询图层属性
const { data } = await fdapi.tileLayer.get(idList)
console.log(data)
// 存储每个图层对应的属性
data.forEach((item, index) => {
buildInfo[buildNameList[index]] = {
id: item.id,
location: item.location,
openLocation: [item.location[0], item.location[1], item.location[2] + (index + 1) * risingHeight]
}
})
}
}
})
}
// 开始拆楼
const begin = () => {
if (openState) {
console.log('模型已经开启,可点击楼层进行拆楼')
return
}
// 构造setLocationList id,对应结束的位置
const setLocationList = []
for (let key in buildInfo) {
setLocationList.push({
id: buildInfo[key].id,
location: buildInfo[key].openLocation
})
}
setLocation(setLocationList)
// 打开状态设置为开启
openState = true
}
// 点击某一层
const chooseFloor = name => {
if (!openState) {
console.log('\x1b[33m%s\x1b[0m', '请先开启拆楼')
return
}
if (moveState) {
console.log('\x1b[33m%s\x1b[0m', '模型位移中,请等模型位移结束再操作')
return
}
if (name === buildNameList[buildNameList.length - 1]) {
console.log('\x1b[33m%s\x1b[0m', '顶层不拆')
return
}
const setLocationList = []
// 如果已经有打开的楼层,则要把当前楼层回收
if (openFloor) {
setLocationList.push({ id: [buildInfo[openFloor].id], location: buildInfo[openFloor].openLocation })
// 同一层则直接重置当前楼层
if (openFloor == name) {
setLocation(setLocationList)
openFloor = ''
return
}
}
// 需要位移的楼层:通过起点、角度、距离 计算终点位置
const startLocation = JSON.parse(JSON.stringify(buildInfo[name].openLocation))
const endLocation = calculatePoint(startLocation, angle, distance)
setLocationList.push({ id: [buildInfo[name].id], location: endLocation })
setLocation(setLocationList)
openFloor = name // 保存当前楼层信息
}
// 模型位移,从模型当前位置位移到指定位置
const setLocation = async arr => {
// 设置移动状态为true
moveState = true
// 获取模型当前位置-通过tileLayer.get 获取模型信息
const idList = []
for (let index in arr) idList.push(arr[index].id)
const { data } = await fdapi.tileLayer.get(idList)
// 获取根据起点和终点,计算路径点
for (let index in arr) {
const startLocation = data[index].location
const endLocation = arr[index].location
arr[index].pathPoints = getLineSegmentPoint([startLocation, endLocation], fps)
}
// 每一帧根据路径点移动一次模型
for (let f = 0; f <= fps - 1; f++) {
fdapi.tileLayer.updateBegin()
for (let index in arr) fdapi.tileLayer.setLocation(arr[index].id, arr[index].pathPoints[f], null)
await fdapi.tileLayer.updateEnd()
if (f == fps - 1) moveState = false
}
}
// 重置模型
const reset = () => {
if (!openState) {
console.log('\x1b[33m%s\x1b[0m', '模型不需要重置')
return
}
fdapi.tileLayer.updateBegin()
for (let key in buildInfo) {
fdapi.tileLayer.setLocation(buildInfo[key].id, buildInfo[key].location)
}
fdapi.tileLayer.updateEnd()
openState = false
openFloor = ''
}
/**
* 根据角度和距离计算点
*/
function calculatePoint(point, angle, distance) {
const radians = angle * (Math.PI / 180) // 将角度转换为弧度
const x1 = point[0] + distance * Math.cos(radians)
const y1 = point[1] + distance * Math.sin(radians)
return point[2] ? [x1, y1, point[2]] : [x1, y1]
}
// 线段分点
function getLineSegmentPoint(lineSegment, interval) {
try {
if (interval && lineSegment && lineSegment.length === 2) {
const point1 = lineSegment[0]
const point2 = lineSegment[1]
const a = point2[1] - point1[1]
const b = point2[0] - point1[0]
const c = point2[2] - point1[2]
const o = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2) + Math.pow(c, 2))
const n = o / interval
const p = []
for (let i = 1; i <= interval; i++) {
const x = (b / o) * (n * i) + point1[0]
const y = (a / o) * (n * i) + point1[1]
const z = (c / o) * (n * i) + point1[2]
p.push([x, y, z])
}
return p
} else {
console.error('线段取点失败', lineSegment, interval)
}
} catch (error) {
console.error('线段取点失败', error)
}
}
进阶:tick位移拆楼
使用上面的的位移方法,在移动帧设置的较大时,会有明显的卡顿现象,这是因为与视频流的交互是异步的。为了解决这种动画调用的问题,DTS有一个tick
方法,可以把需要更新的代码传入到实例中,通过同步的方式直接运行,这样就可以避免掉帧卡顿。
tick介绍
用户经常需要使用定时器(setInterval)或者循环来执行一些非常频繁的接口调用,以实现一些自定义的动画效果,但是这些接口调用是通过JS API实现的, 因为Javascript是单线程执行的,太频繁的调用会影响浏览器的响应,另外Cloud的鼠标、键盘交互事件也是通过JS发送到后台的,频繁的接口调用会影响交互事件的传输,造成交互卡顿。 而且如果传输的数据量比较大时,也会占用不少带宽。
为了解决这个问题,实现更高效的API调用,tick实现了在C++每帧渲染的时候执行JS脚本以实现用户自定义的动画效果,这种实现比前端API调用有以下优点:
- 高效,直接本机通过C++调用JS
- 不影响鼠标、键盘交互,以及其他接口的调用
- 不占用网络带宽
tick的使用
tick的具体使用方法和流程这里不再赘述,想知道更多tick相关可以查询官方的开发文档。
DTS API Tutorial: API_FrameTick (freedo3d.com)
我们实现拆楼,其实只需要3步
- 准备一个tick页面
- 在Cloud文件资源进行添加tick页面
- 客户端代码里执行
fdapi.registerTick()
与fdapi.removeTck()
,分别用于执行tick与移除tick。
具体方法
1.tick页面编写
在tick页面中,我们要把前面的 楼层位移事件 方法实现出来,接受客户端传来的id、终点位置、帧数,完成位移。
我们需要在tick页面初始化后,获取到起点位置,并通过 线段分点 的方法来根据帧数获取间隔点位。最后在tick()
方法内逐帧位移,也就是执行fdapi.tileLayer.setLocation()
改变模型位置。
在执行完最后帧后,要使用fdapi.removeTick()
销毁tick页面,并通过FdExternal.postEvent('tick end')
回调一个结束事件给到客户端,让客户端可以修改模型的移动状态,从而控制整个程序正常运行。
ps:FdExternal类封装了进程内JS与C++的互操作功能,详情可以看用户手册
html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>TICK</title>
<!-- 按需替换路径 -->
<script type="text/javascript" src="http://127.0.0.1:8090/libac"></script>
<style type="text/css"></style>
</head>
<body></body>
</html>
<script>
let i = 0
let urlParam = window.location.search.slice(1)
let urlParamArr = urlParam.split('&')
let dataStr = urlParamArr.find(item => item.includes('data'))
const arr = JSON.parse(decodeURI(dataStr.split('=')[1]))
let fpsUrlArr = urlParamArr.find(item => item.includes('fps'))
const fps = fpsUrlArr ? JSON.parse(decodeURI(fpsUrlArr.split('=')[1])) : 20
const StartEndObj = {}
let state = true
window.onload = async function () {
if (!arr) return
new DigitalTwinAPI()
const idList = []
for (let index in arr) idList.push(arr[index].id)
const { data } = await fdapi.tileLayer.get(idList)
const tickArr = []
for (let index in arr) {
const startLocation = data[index].location
const endLocation = arr[index].location
const line = getLineSegmentPoint([startLocation, endLocation], fps)
StartEndObj[data[index].id] = line
}
}
function clientCalled(str) {
document.getElementById('r3').innerText = str
}
function tick(frame) {
if (!arr) return
if (!state) return
fdapi.tileLayer.updateBegin()
for (let key in StartEndObj) fdapi.tileLayer.setLocation(key, StartEndObj[key][i], null)
fdapi.tileLayer.updateEnd()
if (i >= fps - 1) {
ue.internal.postevent('tick end')
i = 0
state = false
fdapi.removeTick()
}
i++
}
// 线段分点
function getLineSegmentPoint(lineSegment, interval) {
try {
if (interval && lineSegment && lineSegment.length === 2) {
const point1 = lineSegment[0]
const point2 = lineSegment[1]
const a = point2[1] - point1[1]
const b = point2[0] - point1[0]
const c = point2[2] - point1[2]
const o = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2) + Math.pow(c, 2))
const n = o / interval
const p = []
for (let i = 1; i <= interval; i++) {
const x = (b / o) * (n * i) + point1[0]
const y = (a / o) * (n * i) + point1[1]
const z = (c / o) * (n * i) + point1[2]
p.push([x, y, z])
}
return p
} else {
console.error('线段取点失败', lineSegment, interval)
}
} catch (error) {
console.error('线段取点失败', error)
}
}
function tick_next(o, frame) {}
</script>
2.Cloud文件资源添加
这一步比较简单,打开Cloud 的文件资源模块,打开对应的数据目录,把编写好的tick页面存放到目录下,进行刷新显示,我们就可以在右侧的资源路径复制tick页面的路径。
3.客户端页面改写
客户端我们只需要修正三个位置
-
setLocation()
改为执行tick页面javascript// 模型位移-从当前位置位移到目标位置 const setLocation = async arr => { // 设置移动状态为true moveState = true const tickUrl = `@path:tick/moveTo.html?data=${JSON.stringify(arr)}&fps=${fps}&.html` await fdapi.registerTick(tickUrl) }
-
在
DigitalTwinPlayer
的apiOptions.onEvent
接受回调信息,把移动状态设置为falsejavascriptapiOptions: { onEvent: e => { if (e.Data === 'tick end') moveState = false } }
-
重置模型时销毁tick
javascript
// 重置模型
const reset = async () => {
await fdapi.removeTick() // 加多一行
}
把这些实现完后,咱们的tick位移拆楼就完成了。
实现代码
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>tick位移拆楼</title>
<link rel="stylesheet" href="./lib/styles.css" />
<script type="text/javascript" src="./lib/aircity/ac_conf.js"></script>
<script type="text/javascript" src="./lib/aircity/ac.min.js"></script>
</head>
<body>
<div id="player"></div>
<div class="btn" onclick="begin()">开始拆楼</div>
<div class="btn" onclick="reset()" style="right: 220px">结束拆楼</div>
<div class="build-list">
<div class="build-item" onclick="chooseFloor('L13')">L13</div>
<div class="build-item" onclick="chooseFloor('L12')">L12</div>
<div class="build-item" onclick="chooseFloor('L11')">L11</div>
<div class="build-item" onclick="chooseFloor('L10')">L10</div>
<div class="build-item" onclick="chooseFloor('L9')">L9</div>
<div class="build-item" onclick="chooseFloor('L8')">L8</div>
<div class="build-item" onclick="chooseFloor('L7')">L7</div>
<div class="build-item" onclick="chooseFloor('L6')">L6</div>
<div class="build-item" onclick="chooseFloor('L5')">L5</div>
<div class="build-item" onclick="chooseFloor('L1-4')">L1-4</div>
</div>
<script type="text/javascript">
// 拆楼建筑名称数组
const buildNameList = ['L1-4', 'L5', 'L6', 'L7', 'L8', 'L9', 'L10', 'L11', 'L12', 'L13']
// 拆楼建筑对象,保存id与原始坐标信息
const buildInfo = {}
let openState = false, // 建筑开启状态
openFloor = '', // 当前打开楼层
moveState = false // 移动状态
// 效果参数
const fps = 20, // 动画需要多少帧完成
risingHeight = 5, // 建筑抬升高度
distance = 20, // 拆楼距离
angle = 270 // 拆楼角度,0是x轴即正东方向,逆时针计算
/**
* 初始化场景
*/
const OnLoad = () => {
const __DigitalTwinPlayer = new DigitalTwinPlayer(HostConfig.Player, {
domId: 'player',
apiOptions: {
onEvent: e => {
if (e.Data === 'tick end') moveState = false
},
//事件监听回调函数
onReady: async () => {
// 重置视频流
await fdapi.reset(2 | 4)
await fdapi.removeTick()
// 获取图层树信息
const { infotree } = await fdapi.infoTree.get()
// 把图层id存放到一个数组中,方便查询属性
const idList = []
infotree.forEach(item => {
if (buildNameList.includes(item.name)) {
idList.push(item.iD)
}
})
// 查询图层属性
const { data } = await fdapi.tileLayer.get(idList)
// 存储每个图层对应的属性
data.forEach((item, index) => {
buildInfo[buildNameList[index]] = {
id: item.id,
location: item.location,
openLocation: [item.location[0], item.location[1], item.location[2] + (index + 1) * risingHeight]
}
})
}
}
})
}
// 开始拆楼
const begin = () => {
if (openState) {
console.log('模型已经开启,可点击楼层进行拆楼')
return
}
// 构造setLocationList id,对应结束的位置
const setLocationList = []
for (let key in buildInfo) {
setLocationList.push({
id: buildInfo[key].id,
location: buildInfo[key].openLocation
})
}
setLocation(setLocationList)
// 打开状态设置为开启
openState = true
}
// 点击某一层
const chooseFloor = name => {
if (!openState) {
console.log('\x1b[33m%s\x1b[0m', '请先开启拆楼')
return
}
if (moveState) {
console.log('\x1b[33m%s\x1b[0m', '模型位移中,请等模型位移结束再操作')
return
}
if (name === buildNameList[buildNameList.length - 1]) {
console.log('\x1b[33m%s\x1b[0m', '顶层不拆')
return
}
const setLocationList = []
// 如果已经有打开的楼层,则要把当前楼层回收
if (openFloor) {
setLocationList.push({ id: [buildInfo[openFloor].id], location: buildInfo[openFloor].openLocation })
// 同一层则直接重置当前楼层
if (openFloor == name) {
setLocation(setLocationList)
openFloor = ''
return
}
}
// 需要位移的楼层:通过起点、角度、距离 计算终点位置
const startLocation = JSON.parse(JSON.stringify(buildInfo[name].openLocation))
const endLocation = calculatePoint(startLocation, angle, distance)
setLocationList.push({ id: [buildInfo[name].id], location: endLocation })
setLocation(setLocationList)
openFloor = name // 保存当前楼层信息
}
// 模型位移-从当前位置位移到目标位置
const setLocation = async arr => {
// 设置移动状态为true
moveState = true
const tickUrl = `@path:tick/moveTo.html?data=${JSON.stringify(arr)}&fps=${fps}&.html`
await fdapi.registerTick(tickUrl)
}
// 重置模型
const reset = async () => {
if (!openState) {
console.log('\x1b[33m%s\x1b[0m', '模型不需要重置')
return
}
await fdapi.removeTick()
fdapi.tileLayer.updateBegin()
for (let key in buildInfo) {
fdapi.tileLayer.setLocation(buildInfo[key].id, buildInfo[key].location)
}
fdapi.tileLayer.updateEnd()
openState = false
openFloor = ''
}
// 根据角度和距离计算点
function calculatePoint(point, angle, distance) {
var radians = angle * (Math.PI / 180) // 将角度转换为弧度
var x1 = point[0] + distance * Math.cos(radians)
var y1 = point[1] + distance * Math.sin(radians)
return point[2] ? [x1, y1, point[2]] : [x1, y1]
}
/**
* 自适应分辨率
*/
const onResize = async () => {
let playerView = document.getElementById('player')
playerView.style.height = window.innerHeight + 'px'
playerView.style.width = window.innerWidth + 'px'
}
// 页面加载调用
window.addEventListener('load', OnLoad, true)
// 页面窗口变换调用
window.addEventListener('resize', onResize, true)
</script>
</body>
</html>