現(xiàn):從節(jié)點(diǎn)遍歷到動(dòng)畫完整指南)
去年接了個(gè)房產(chǎn)數(shù)字化項(xiàng)目客戶提的需求很簡(jiǎn)單一棟樓的3DTiles模型要能像抽屜一樣點(diǎn)某層就“抽”出來(lái)露出里面的戶型結(jié)構(gòu)。聽起來(lái)不難真正動(dòng)手才發(fā)現(xiàn)Cesium里做分層分戶抽屜牽涉到數(shù)據(jù)組織方式、glTF節(jié)點(diǎn)訪問(wèn)、矩陣變換、動(dòng)畫插值這一整套鏈路任何一個(gè)環(huán)節(jié)沒(méi)想清楚都會(huì)卡殼。這篇文章把我完整的實(shí)現(xiàn)思路、核心代碼、以及調(diào)試中踩過(guò)的坑全部整理出來(lái)給準(zhǔn)備做同類功能的同學(xué)一條可行的捷徑。1. 先想清楚再做兩條技術(shù)路線怎么選1.1 分層分戶抽屜到底在解決什么問(wèn)題先說(shuō)場(chǎng)景。這類功能最常見于房產(chǎn)售樓系統(tǒng)、樓宇招商平臺(tái)、物業(yè)管理后臺(tái)和應(yīng)急疏散演練。整棟樓以Cesium 3DTiles格式加載到三維地球后用戶需要按樓層查看甚至按戶型查看。傳統(tǒng)做法是“切剖”用剖切面把樓體切開但這種方式對(duì)操作要求高普通用戶根本玩不轉(zhuǎn)。抽屜效果把“剖切”變成“抽拉”——樓層從樓體中水平滑出內(nèi)部戶型自然暴露交互直覺(jué)非常清晰。本質(zhì)上是解決三維模型的信息層級(jí)展示問(wèn)題樓棟、樓層、戶三元層級(jí)用戶需要自由下鉆又不能丟失整體空間位置關(guān)系。1.2 方案A模型預(yù)處理拆分把BIM模型Revit、SketchUp、Blender都行按樓層拆分分別導(dǎo)出切片成多個(gè)3DTiles每個(gè)樓層是一個(gè)獨(dú)立的tileset.json。前端維護(hù)一個(gè)樓層數(shù)組點(diǎn)擊樓層時(shí)設(shè)置對(duì)應(yīng)tileset的偏移量實(shí)現(xiàn)抽屜效果。優(yōu)點(diǎn)很突出每個(gè)樓層獨(dú)立加載按需流式加載內(nèi)存占用小可以做精確的樓層權(quán)限控制比如某層只有特定用戶能看修改某層模型只需重新生成該層的3DTiles不用全量重出抽屜動(dòng)畫簡(jiǎn)單只需要改tileset.modelMatrix缺點(diǎn)同樣明顯建模階段就要把樓層拆開如果模型已經(jīng)是一個(gè)整體拆分工作量大樓層之間的“縫”容易在視覺(jué)上接不上后續(xù)新增樓層要重新走一遍生產(chǎn)管線。1.3 方案B單模型加節(jié)點(diǎn)變換模型保持原始完整結(jié)構(gòu)導(dǎo)出一個(gè)3DTiles在前端加載后通過(guò)訪問(wèn)glTF內(nèi)部節(jié)點(diǎn)對(duì)單個(gè)樓層節(jié)點(diǎn)做矩陣變換。Cesium 3DTiles內(nèi)部的b3dm、glTF是有節(jié)點(diǎn)樹結(jié)構(gòu)的BIM模型如果命名規(guī)范樓層和戶型都能按名稱找到。優(yōu)點(diǎn)原始模型不拆避免接縫問(wèn)題和數(shù)據(jù)重復(fù)運(yùn)行時(shí)可動(dòng)態(tài)控制任意粒度的節(jié)點(diǎn)樓層、戶型、墻體都能動(dòng)適合做交互式拆解抽屜只是其中一種動(dòng)畫形式缺點(diǎn)依賴模型內(nèi)部命名規(guī)范命名混亂時(shí)定位節(jié)點(diǎn)非常痛苦前期開發(fā)調(diào)試成本高3DTiles節(jié)點(diǎn)樹不是直觀可見的DOM樹數(shù)據(jù)量大的樓棟一次性加載壓力大我做項(xiàng)目時(shí)最終選了方案B為主、方案A為輔的混合策略樓體整體加載但在建模階段就要求Revit導(dǎo)出的模型按樓層命名了節(jié)點(diǎn)。如果你的項(xiàng)目模型還沒(méi)建強(qiáng)烈建議在建模階段就做節(jié)點(diǎn)命名規(guī)范后面省的時(shí)間遠(yuǎn)大于前期溝通成本。對(duì)比項(xiàng)方案A預(yù)處理拆層方案B單模型節(jié)點(diǎn)變換模型改動(dòng)量大需要按樓層拆模導(dǎo)出小只需規(guī)范命名加載策略每層獨(dú)立流式整棟加載動(dòng)畫控制控制tileset整體位移控制內(nèi)部節(jié)點(diǎn)位移權(quán)限控制天然支持按層加載需要額外邏輯開發(fā)調(diào)試成本低較高適合場(chǎng)景超大體量樓宇、需權(quán)限隔離中等體量、交互拆解為主2. 數(shù)據(jù)側(cè)準(zhǔn)備確保節(jié)點(diǎn)能拿到、能識(shí)別2.1 控制好3DTiles的生產(chǎn)參數(shù)模型導(dǎo)出3DTiles時(shí)有幾個(gè)參數(shù)直接影響你能不能做分層分戶。不要設(shè)置clampToGround: true。一旦貼地放置所有樓層會(huì)被壓扁到地表高度信息丟失分層無(wú)從談起。保留模型原始地理坐標(biāo)和高度生成后檢查tileset的boundingSphere是否覆蓋了樓體的完整高度范圍。不要過(guò)度壓縮幾何。有些轉(zhuǎn)換工具默認(rèn)開Draco壓縮壓縮后節(jié)點(diǎn)層級(jí)和名稱可能丟失。我用的生產(chǎn)鏈路是Revit導(dǎo)出FBX - gltf-pipeline轉(zhuǎn)glTF - 3D Tiles工具切片。FBX轉(zhuǎn)glTF時(shí)保留節(jié)點(diǎn)層級(jí)之后切片到3DTiles時(shí)節(jié)點(diǎn)名仍然能保留。網(wǎng)上很多人在問(wèn)“shp轉(zhuǎn)3dtiles”“fbx轉(zhuǎn)3dtiles”“cesium模型可以直接加載su嗎”——如果你是從GIS數(shù)據(jù)轉(zhuǎn)通常沒(méi)有內(nèi)部節(jié)點(diǎn)概念如果是建筑模型su、fbx、rvt節(jié)點(diǎn)名是保留的。傾斜攝影模型基本沒(méi)有語(yǔ)義化節(jié)點(diǎn)直接放棄方案B先用單體化處理或手工重建簡(jiǎn)化模型。2.2 給樓層和戶型的命名約定這是整個(gè)項(xiàng)目的隱性關(guān)鍵。Demo模型隨便起名無(wú)所謂生產(chǎn)項(xiàng)目必須約定樓層節(jié)點(diǎn)統(tǒng)一前綴floor_1、floor_2、floor_3戶型節(jié)點(diǎn)統(tǒng)一前綴room_1_11層1號(hào)房、room_1_2公共區(qū)域lobby_1代表1層大堂遍歷節(jié)點(diǎn)時(shí)通過(guò)名稱解析比通過(guò)位置計(jì)算穩(wěn)定得多。有一版我在沒(méi)有命名規(guī)范的情況下只能靠節(jié)點(diǎn)包圍盒的高度反推樓層一旦模型有地下層、夾層、設(shè)備層反推邏輯寫到你懷疑人生。2.3 沒(méi)有規(guī)范命名的兜底方案老模型實(shí)在改不了也不是完全沒(méi)救??梢园垂?jié)點(diǎn)包圍盒中心點(diǎn)的Z坐標(biāo)來(lái)分層function getFloorIndexFromNode(node, baseHeight, floorHeight) { const boundingSphere node.boundingVolume?.boundingSphere; if (!boundingSphere) return null; const centerHeight boundingSphere.center.z; const index Math.round((centerHeight - baseHeight) / floorHeight); return index; }baseHeight取樓棟最低點(diǎn)floorHeight取標(biāo)準(zhǔn)層高。注意地下層需要另做映射比如地下室層高4.5米、地上標(biāo)準(zhǔn)層高3米混在一起計(jì)算會(huì)錯(cuò)位。這種方案適合應(yīng)急驗(yàn)證生產(chǎn)環(huán)境我不推薦。3. 抽屜效果的本質(zhì)兩套“移動(dòng)”邏輯3.1 抽屜是純平移不是旋轉(zhuǎn)也不是縮放很多人第一次做拆解動(dòng)畫會(huì)想到旋轉(zhuǎn)展開——像門一樣打開。但抽屜效果的本質(zhì)是純平移把目標(biāo)樓層沿某個(gè)水平方向平移到樓體輪廓外同時(shí)保證平移方向可預(yù)測(cè)、可逆。平移的距離單位是米。Cesium的笛卡爾坐標(biāo)Cartesian3單位是米但注意你操作的是地心坐標(biāo)系下的絕對(duì)坐標(biāo)還是模型局部坐標(biāo)。平移樓層時(shí)正確做法是在模型局部坐標(biāo)系里做偏移再通過(guò)模型矩陣轉(zhuǎn)換到世界坐標(biāo)。3.2 平移作用在哪個(gè)層級(jí)這是容易混淆的地方平移整個(gè)tileset通過(guò)tileset.modelMatrix做但這樣整棟樓一起動(dòng)不是分層分戶平移某個(gè)樓層對(duì)應(yīng)的glTF節(jié)點(diǎn)推薦方案平移戶型子節(jié)點(diǎn)需要拿到樓層節(jié)點(diǎn)下的子節(jié)點(diǎn)逐個(gè)處理在Cesium的3DTiles內(nèi)部每個(gè)tile的content可能是b3dm、pnts等格式。b3dm內(nèi)部包含一個(gè)glTF模型這個(gè)模型的節(jié)點(diǎn)樹才是我們操作的對(duì)象。舊版本Cesium可以通過(guò)tileset._root._content._model._getNodeByName(name)拿節(jié)點(diǎn)不同版本內(nèi)部結(jié)構(gòu)有差異這一點(diǎn)項(xiàng)目里一定要鎖定Cesium版本。3.3 動(dòng)畫插值決定手感抽屜不能“瞬移”要能看到抽拉過(guò)程。Cesium沒(méi)有內(nèi)置補(bǔ)間動(dòng)畫器我采用viewer.clock.onTick監(jiān)聽每一幀根據(jù)時(shí)間差計(jì)算插值系數(shù)再用矩陣運(yùn)算設(shè)置節(jié)點(diǎn)的位移量。插值系數(shù)用簡(jiǎn)單的線性插值就能看但視覺(jué)偏生硬。稍微好一點(diǎn)用緩動(dòng)函數(shù)function easeInOutCubic(t) { return t 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t 2, 3) / 2; }實(shí)測(cè)下來(lái)抽屜動(dòng)畫用easeInOutCubic或easeOutQuad觀眾視覺(jué)感受最舒服拉開的前半段稍快快到位時(shí)剎住很像真實(shí)抽屜的慣性感。4. 可復(fù)現(xiàn)的完整代碼加載3DTiles并實(shí)現(xiàn)分層分戶抽屜4.1 場(chǎng)景初始化和3DTiles加載先初始化Viewer關(guān)閉不相關(guān)的默認(rèn)控件加載tileset時(shí)關(guān)閉clampToGround并把maximumScreenSpaceError調(diào)大減少渲染壓力。const viewer new Cesium.Viewer(cesiumContainer, { animation: false, timeline: false, terrainProvider: Cesium.createWorldTerrainAsync() }); // 背景和光照的調(diào)整讓模型內(nèi)部結(jié)構(gòu)看得更清楚 viewer.scene.globe.baseColor Cesium.Color.fromCssColorString(#2b2b2b); viewer.scene.light new Cesium.DirectionalLight({ direction: new Cesium.Cartesian3(-0.5, -0.5, -1), intensity: 0.8 }); const tileset await Cesium.Cesium3DTileset.fromUrl(/data/building/tileset.json); tileset.maximumScreenSpaceError 16; viewer.scene.primitives.add(tileset); await viewer.zoomTo(tileset, new Cesium.HeadingPitchRange(0, -0.6, 300));這里zoomTo不只是起飛視角它會(huì)讓相機(jī)定位到tileset包圍球適合觀看的位置。注意createWorldTerrainAsync是異步的如果不需要地形可以去掉直接Cesium.Cartesian3.fromDegrees設(shè)置相機(jī)位置。4.2 等3DTiles加載完成再遍歷節(jié)點(diǎn)這是新手最容易踩的坑3DTiles是流式加載的馬上遍歷tileset.root時(shí)內(nèi)部幾何可能還沒(méi)到位。需要等待加載完成。await Cesium.when(tileset.readyPromise, async () { // 確保所有待加載的Tile都渲染完成 const allLoaded () tileset.root.contentReady || tileset.root.contentAvailable; // 簡(jiǎn)單穩(wěn)妥做法均勻延時(shí)輪詢檢查 await waitForTilesLoad(tileset); setupFloorNodes(tileset); });waitForTilesLoad是一個(gè)輪詢函數(shù)檢查tileset._root._content是否存在且content._model已創(chuàng)建。注意_root開頭的方法都是Cesium內(nèi)部API新版本可能變化生產(chǎn)代碼要鎖定版本并包一層Try-Catch。4.3 遍歷glTF節(jié)點(diǎn)收集樓層拿到模型對(duì)象后按命名約定遍歷節(jié)點(diǎn)。我封裝了一個(gè)按名稱前綴匹配節(jié)點(diǎn)的函數(shù)function findNodesByPrefix(model, prefix) { const matched []; model._getNodeNames?.()?.forEach((name) { if (name.startsWith(prefix)) { matched.push({ name, node: model._getNodeByName(name) }); } }); return matched; }有些Cesium版本沒(méi)有_getNodeNames就手動(dòng)遞歸遍歷model._rootNode的childrenfunction traverseGltfNode(node, callback) { if (!node) return; callback(node); const children node._children || node.children; if (children) { children.forEach(child traverseGltfNode(child, callback)); } }遍歷時(shí)注意glTF節(jié)點(diǎn)的訪問(wèn)要用內(nèi)部屬性和3DTiles的tile節(jié)點(diǎn)是兩套體系。收集后保存每個(gè)節(jié)點(diǎn)的初始矩陣抽屜動(dòng)畫結(jié)束后要能恢復(fù)。4.4 核心樓層抽屜動(dòng)畫控制構(gòu)建一個(gè)狀態(tài)機(jī)保存每個(gè)樓層的節(jié)點(diǎn)、初始矩陣、當(dāng)前偏移量、動(dòng)畫狀態(tài)const floorState {}; function setupFloorNodes(tileset) { const model tileset.root.content.model || tileset._root._content._model; if (!model) return; const floorPrefix floor_; const roomPrefix room_; // 遍歷模型節(jié)點(diǎn)找到所有樓層 traverseGltfNode(model._rootNode, (node) { if (node.name node.name.startsWith(floorPrefix)) { floorState[node.name] { node: node, originMatrix: node.matrix.clone(), offsetX: 0, isOpen: false, animation: null }; } }); }有了樓層節(jié)點(diǎn)做抽屜動(dòng)畫就是修改node.matrix。注意不能直接改節(jié)點(diǎn)的position屬性因?yàn)間lTF節(jié)點(diǎn)的position是局部坐標(biāo)而我們需要的是相對(duì)模型的局部平移。正確做法是初始矩陣乘以平移矩陣。function setFloorOffset(floorName, offsetX, offsetY, offsetZ) { const state floorState[floorName]; if (!state) return; const translationMatrix Cesium.Matrix4.fromTranslation( new Cesium.Cartesian3(offsetX, offsetY, offsetZ) ); const newMatrix Cesium.Matrix4.multiply( state.originMatrix, translationMatrix, new Cesium.Matrix4() ); state.node.matrix newMatrix; state.offsetX offsetX; }抽屜效果的位移方向我選擇沿X軸建筑開間方向向外平移。不同的模型在建模軟件里朝向可能不同上線前要確認(rèn)平移方向?qū)τ脩趔w驗(yàn)友好。如果模型不是正北朝向用世界坐標(biāo)偏移會(huì)出現(xiàn)斜向抽拉最好在局部坐標(biāo)系做ENU變換。4.5 抽屜動(dòng)畫插值實(shí)現(xiàn)定義抽屜拉開角度/距離然后用時(shí)間差驅(qū)動(dòng)插值。這里我用viewer.clock.onTick做幀監(jiān)聽function animateFloorDrawer(floorName, targetDistance, duration 1.0) { const state floorState[floorName]; if (!state) return; const startTime Cesium.JulianDate.now(); const startOffset state.offsetX; const targetOffset targetDistance; if (state.animation) { viewer.clock.onTick.removeEventListener(state.animation); } const update (clock) { const currentTime clock.currentTime; const ratio Math.min( Cesium.JulianDate.secondsDifference(currentTime, startTime) / duration, 1.0 ); const eased easeInOutCubic(ratio); const newOffset startOffset (targetOffset - startOffset) * eased; setFloorOffset(floorName, newOffset, 0, 0); if (ratio 1.0) { viewer.clock.onTick.removeEventListener(update); state.animation null; state.isOpen targetOffset ! 0; } }; state.animation update; viewer.clock.onTick.addEventListener(update); }這里有一個(gè)很重要的點(diǎn)Cesium的onTick事件在所有渲染幀都會(huì)觸發(fā)如果多個(gè)樓層同時(shí)動(dòng)畫會(huì)有多個(gè)閉包同時(shí)存在注意清理監(jiān)聽器避免內(nèi)存泄漏。我在state.animation里保存了當(dāng)前動(dòng)畫函數(shù)新動(dòng)畫啟動(dòng)時(shí)先移除舊的。4.6 實(shí)現(xiàn)整棟樓的“全拉出”和“全收回”實(shí)際業(yè)務(wù)中用戶不止是單層抽拉還有“一鍵全部拉開”的需求。做法就是遍歷所有樓層先統(tǒng)一計(jì)算每個(gè)樓層抽屜的展開方向和距離避免層與層重疊然后逐層做動(dòng)畫錯(cuò)開啟動(dòng)時(shí)間形成順序推拉的節(jié)奏function openAllDrawers(distance 30, staggerDelay 120) { const floorNames Object.keys(floorState); floorNames.forEach((name, index) { setTimeout(() { animateFloorDrawer(name, distance, 0.8); }, index * staggerDelay); }); }4.7 點(diǎn)擊樓層實(shí)現(xiàn)交互最簡(jiǎn)單實(shí)用的方式不是直接拾取3DTiles節(jié)點(diǎn)而是給每個(gè)樓層添加一個(gè)透明拾取盒子。因?yàn)?DTiles內(nèi)部的glTF節(jié)點(diǎn)拾取在不同的Cesium版本表現(xiàn)不穩(wěn)定透明盒子方案穩(wěn)定且易于控制還可以加點(diǎn)擊反饋和描邊。function createFloorPickBox(floorName, centerPosition, dimensions) { const entity viewer.entities.add({ name: pick_${floorName}, position: Cesium.Cartesian3.fromDegrees(centerPosition.lng, centerPosition.lat, centerPosition.height), box: { dimensions: new Cesium.Cartesian3(dimensions.x, dimensions.y, dimensions.z), material: Cesium.Color.WHITE.withAlpha(0), outline: false }, properties: { floorName: floorName } }); return entity; } const handler new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); handler.setInputAction((movement) { const picked viewer.scene.pick(movement.position); if (picked?.id?.properties?.floorName) { const floorName picked.id.properties.floorName.getValue(); toggleFloorDrawer(floorName); } }, Cesium.ScreenSpaceEventType.LEFT_CLICK);toggleFloorDrawer里根據(jù)isOpen狀態(tài)決定是拉出還是收回。點(diǎn)擊盒子所在樓層后如果已經(jīng)展開就收回否則就展開。4.8 相機(jī)跟隨與定位抽屜拉出后相機(jī)要能自動(dòng)聚焦到被拉出的樓層。Cesium提供了viewer.flyTo來(lái)定位到實(shí)體但3DTiles內(nèi)部的節(jié)點(diǎn)沒(méi)有直接對(duì)外的Entity。需要手動(dòng)計(jì)算節(jié)點(diǎn)包圍盒的中心function flyToFloor(floorName, offset 50) { const state floorState[floorName]; if (!state) return; // 節(jié)點(diǎn)本身是glTF節(jié)點(diǎn)拿不到transform后的世界位置 // 用節(jié)點(diǎn)包圍盒中心 當(dāng)前tileset的modelMatrix換算 const tilesetPosition tileset.boundingSphere.center; const worldPosition Cesium.Matrix4.multiplyByPoint( tileset.modelMatrix, tilesetPosition, new Cesium.Cartesian3() ); viewer.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees( Cesium.Math.toDegrees(worldPosition.longitude), Cesium.Math.toDegrees(worldPosition.latitude), worldPosition.height offset ), orientation: { heading: 0, pitch: Cesium.Math.toRadians(-45), roll: 0 }, duration: 0.8 }); }5. 實(shí)際項(xiàng)目里容易踩的六個(gè)坑5.1 allTilesLoaded永遠(yuǎn)不觸發(fā)很多教程都告訴你tileset.allTilesLoaded事件可以判斷加載完成但這個(gè)事件在相機(jī)視角不斷變化、Tile不斷被替換時(shí)會(huì)反復(fù)觸發(fā)甚至不觸發(fā)。穩(wěn)妥做法是不等這個(gè)事件而是用輪詢檢查tileset.root._content是否存在并已有_model配合一個(gè)超時(shí)上限。我在項(xiàng)目中用了50次輪詢間隔100毫秒5秒內(nèi)還沒(méi)拿到模型就報(bào)錯(cuò)提示。5.2 修改了matrix但不生效或閃跳修改glTF節(jié)點(diǎn)node.matrix在大多數(shù)Cesium版本下一幀生效但如果修改的是_content里尚未加載完成的node操作會(huì)被后續(xù)的tile加載覆蓋。解決辦法確認(rèn)在拿到_model之后再獲取節(jié)點(diǎn)并保存初始矩陣動(dòng)畫期間避免攝像頭大幅視角切換觸發(fā)新的Tile細(xì)化。如果遇到修正了matrix又跳回原位檢查是否修改的是node.transform而不是node.matrix。兩者區(qū)別node.transform是glTF規(guī)范的標(biāo)準(zhǔn)屬性為Matrix4node.matrix是Cesium對(duì)節(jié)點(diǎn)變換的內(nèi)部存儲(chǔ)舊版本Cesium中操作的是node.matrix新版里node.transform更穩(wěn)定。5.3 抽屜拉開后樓層之間穿模這是最影響觀感的問(wèn)題。單純平移只能拉開抽屜但相鄰樓層之間的公共墻、管線還是會(huì)穿幫。兩個(gè)針對(duì)手段平移距離必須大于被拉出樓層的最大輪廓尺寸讓樓層完全脫離樓體輪廓剪裁平面配合在樓體側(cè)面設(shè)置一個(gè)或多個(gè)ClippingPlane把當(dāng)前樓體內(nèi)部被遮擋的部分“切掉”const plane new Cesium.ClippingPlane( new Cesium.Cartesian3(0, 1, 0), // 法線方向 clipDistance );注意ClippingPlane是無(wú)限平面要配準(zhǔn)確的法線和距離否則會(huì)把樓體外部也切了。我實(shí)測(cè)的結(jié)論動(dòng)畫時(shí)長(zhǎng)在0.8秒時(shí)剪裁平面跟隨抽屜距離線性移動(dòng)視覺(jué)上幾乎看不到穿模。5.4 平移距離的米制換算問(wèn)題抽屜拉開距離不能拍腦袋定要根據(jù)模型的實(shí)際寬度算。比如樓體沿X軸方向?qū)挾葹?2米那么拉出至少35米。用tileset.boundingSphere.radius * 2可以粗略估算樓體寬度更精確的做法是獲取樓層節(jié)點(diǎn)的包圍盒尺寸const boundingSphere Cesium.BoundingSphere.fromBoundingSphere( // 部分版本節(jié)點(diǎn)有boundingVolume state.node.boundingVolume?.boundingSphere );如果拿不到node級(jí)包圍盒就用tileset的整體包圍盒乘以縮放系數(shù)。開發(fā)階段在UI上加一個(gè)distance滑動(dòng)條動(dòng)態(tài)調(diào)整是最好的調(diào)參方式。5.5 傾斜攝影模型根本做不了節(jié)點(diǎn)抽屜很多網(wǎng)友搜“cesium傾斜攝影”進(jìn)來(lái)想給傾斜攝影模型做分層分戶。這里必須潑冷水傾斜攝影的3DTiles是密集Mesh沒(méi)有語(yǔ)義化的樓層節(jié)點(diǎn)node名全是隨機(jī)字符串。要做分層分戶只能用單體化工具把傾斜模型按建筑底面切割或者重建簡(jiǎn)模BIM手工建模/自動(dòng)建?;蛘邔?duì)OSGB數(shù)據(jù)進(jìn)行壓平和分類傾斜攝影適合全貌查看、量測(cè)不適合做精細(xì)分層。我把這個(gè)寫在文章里希望做選型時(shí)少走彎路。5.6 Cesium版本差異是最大的隱藏成本1.x階段Cesium迭代非???DTiles內(nèi)部實(shí)現(xiàn)頻繁調(diào)整。項(xiàng)目里用某版本寫好的_root._content._model._getNodeByName過(guò)兩個(gè)季度升級(jí)Cesium后可能完全失效。項(xiàng)目經(jīng)驗(yàn)鎖定Cesium版本升級(jí)做專項(xiàng)回歸。每次升級(jí)先跑節(jié)點(diǎn)遍歷自測(cè)再跑抽屜動(dòng)畫最后檢查相機(jī)拾取。6. 從抽屜到成品我的落地功能清單和建議擴(kuò)展6.1 基礎(chǔ)功能清單最終交付給客戶時(shí)做的分層分戶抽屜功能包含以下模塊模塊實(shí)現(xiàn)方式優(yōu)先級(jí)樓層抽屜點(diǎn)擊樓層水平拉出P0一鍵全展開/全收回全部樓層順序拉出P0樓層定位拉出后相機(jī)跟隨P1戶型高亮拉出后對(duì)應(yīng)戶型變色P1信息面板點(diǎn)擊戶型顯示面積/戶主P1剖切聯(lián)動(dòng)樓層展開時(shí)自動(dòng)剖切遮擋P26.2 結(jié)合單體化做戶型信息聯(lián)動(dòng)傾斜攝影做不了抽屜但是正射場(chǎng)景結(jié)合“單體化”可以做戶型點(diǎn)擊信息聯(lián)動(dòng)。所謂單體化本質(zhì)上是對(duì)3DTiles的Feature增加屬性讓前端通過(guò)Cesium3DTileFeature讀取batchId對(duì)應(yīng)的屬性值然后彈窗顯示。這個(gè)和抽屜效果可以混搭樓體用BIM做抽屜周邊環(huán)境用傾斜攝影做單體化房地產(chǎn)項(xiàng)目經(jīng)常這么組合。6.3 配合動(dòng)態(tài)光照和熱力圖做營(yíng)銷效果抽屜效果做出來(lái)后客戶往往會(huì)追加兩類效果一是動(dòng)態(tài)光照模擬一天內(nèi)太陽(yáng)光移動(dòng)照射到拉出的樓層和戶型售樓處大屏上效果拔群。實(shí)現(xiàn)上用viewer.scene.light的direction隨時(shí)間變化即可注意在太陽(yáng)轉(zhuǎn)至建筑背面時(shí)室內(nèi)光線過(guò)暗需要加環(huán)境光保底。二是熱力圖按樓層/戶型展示認(rèn)購(gòu)率、空置率??梢栽诘孛姣B加一個(gè)熱力圖圖層也可以給已售戶型做顏色覆蓋。數(shù)據(jù)量小時(shí)用Entity的Polygon拾取數(shù)據(jù)量大時(shí)用GeoJSON加載再樣式映射。6.4 鷹眼和聯(lián)動(dòng)定位詳情頁(yè)和總覽頁(yè)之間需要“鷹眼”地圖做定位反饋。Cesium實(shí)現(xiàn)鷹眼最簡(jiǎn)單方式是用一個(gè)右下角的小Cesium.Viewer跟隨主相機(jī)同步視角或者用2D地圖如Leaflet做同步定位。我項(xiàng)目里用的是第二個(gè)Viewer同步的方式代碼量少且可以支持3D聯(lián)動(dòng)視角。6.5 穩(wěn)定性和老舊設(shè)備降級(jí)抽屜動(dòng)畫底層依賴每幀矩陣計(jì)算老舊電腦或?yàn)g覽器如果不支持WebGL 2.0大概率白屏卡頓。建議做能力檢測(cè)const canvas document.createElement(canvas); const gl canvas.getContext(webgl2) || canvas.getContext(webgl); if (!gl) { // 提示用戶升級(jí)瀏覽器或使用支持WebGL的設(shè)備 }同時(shí)把Cesium的scene.debugShowFramesPerSecond暴露給內(nèi)部測(cè)試目標(biāo)設(shè)備上幀率低于25fps時(shí)降低maximumScreenSpaceError并減少同時(shí)展開的樓層數(shù)。我自己在這個(gè)項(xiàng)目里最后沉淀下來(lái)的體會(huì)是分層分戶抽屜談不上高深算法但非??简?yàn)工程串聯(lián)能力。數(shù)據(jù)側(cè)命名規(guī)范能省一半前端時(shí)間模型側(cè)層級(jí)結(jié)構(gòu)決定你能做多細(xì)的節(jié)點(diǎn)控制而前端側(cè)把矩陣變換、動(dòng)畫插值和拾取交互想清楚就成功了大半。如果你正準(zhǔn)備做這個(gè)功能建議先拿一個(gè)命名規(guī)范的測(cè)試模型跑通節(jié)點(diǎn)遍歷和矩陣動(dòng)畫這一小段再鋪開到完整業(yè)務(wù)不要一上來(lái)就調(diào)大模型。記住先把一個(gè)樓層拉出來(lái)再談?wù)麠潣恰?