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