PropMotion 是做什麼的?
在你的 SwiftUI 應用程式中,讓一個 3D 物體表演。為特定、常見的工作提供生產級模式:在透明的 SceneKit 舞台上呈現一個精緻的產品演員——包含進場、出場、拋擲、陰影、觸覺回饋,以及那些耗費數日的無聲陷阱。
何時使用 SceneKit
對該框架的定位要誠實:
- SceneKit 處於維護模式。對於需要大量 3D 功能(資產管線、USD/USDZ、AR、大型世界)的新應用程式,優先選擇 RealityKit。
- SceneKit 仍然是將裝飾性 3D 演員嵌入 SwiftUI 應用程式的最快路徑:透明的
SCNView可覆蓋在任何 SwiftUI 佈局之上,幾何著色器修飾符只是一個 MSL 字串,CoreAnimation 互操作性成熟,且此處所有內容都在純 UIKit 視圖上運行,無需任何會話設定。 - 如果 3D 元素是一個帶有編排動作的英雄物體,此技能的配方可直接套用。如果是一個完整的互動世界,請先停下來考慮 RealityKit。
核心舞台配方
一個舞台包含:透明的 SCNView、一個擁有場景圖的 @MainActor 協調器、一個位於人眼高度的相機、一個三燈光系統加上一個專用陰影光、一個接觸影塊以及一個陰影捕捉器。演員透過預先烘焙的關鍵幀動畫來表演。
struct ProductStage: UIViewRepresentable {
let item: Item
// 關鍵鍵值僅在場景必須可見地改變時才需要變更。
private var stateKey: String { "\(item.id)" }
func makeCoordinator() -> Coordinator { Coordinator() }
func makeUIView(context: Context) -> SCNView {
let view = SCNView()
view.backgroundColor = .clear // 舞台覆蓋在 SwiftUI 之上
view.isOpaque = false
view.antialiasingMode = .multisampling4X
view.scene = context.coordinator.buildScene()
view.pointOfView = context.coordinator.cameraNode
context.coordinator.install(item)
context.coordinator.markState(stateKey)
// 首幀預熱,雙鑰匙點火:在關鍵路徑之外編譯著色器,
// 將演員停在幕後,並將首次進場門控在最短延遲和準備完成兩者之上。
context.coordinator.parkOffstage()
if let scene = view.scene {
view.prepare([scene]) { _ in
DispatchQueue.main.async { context.coordinator.markPipelinesWarm() }
}
}
context.coordinator.scheduleFirstEntrance()
return view
}
func updateUIView(_ view: SCNView, context: Context) {
// 狀態鍵差分:SwiftUI 重新渲染絕不能重播進場動作。
guard context.coordinator.stateKey != stateKey else { return }
context.coordinator.markState(stateKey)
context.coordinator.transition(to: item)
}
}
按照建構順序,各個部件如下:
- 透明視圖標誌(
backgroundColor = .clear,isOpaque = false),MSAA 4x。主機螢幕提供背景;場景沒有盒子。 - 協調器擁有節點層級結構,為每個動作關注點(平移、升降、偏航、旋轉)分解為一個節點,使獨立的動畫永遠不會為了一個單一變換而互相競爭。
- 相機:
projectionDirection = .horizontal,使演員大小跟隨舞台寬度;抬高位置並帶有輕微向下俯角,看起來像一個站立的觀察者。保持靜態;移動的相機看起來不自然。 - 燈光:暖色主光、冷色弱輔光、硬邊輪廓光、僅作為基礎值的環境光,再加上一個單獨的陰影投射方向光,從演員的後上方照射,使其永遠不會干擾可見的造型。
- 地面接觸:演員下方一個柔和的暗色漸層平面(接觸影塊),加上一個用於真實陰影的不可見的
.shadowOnly捕捉器平面。 - 反射材質取樣一個小型程式化環境貼圖,而非照片。保持相機後方的區域為暗色。
- 所有編排動作都是烘焙的
CAKeyframeAnimation,受到生成令牌的保護,因此任何新的節拍都能取代正在進行的工作。
包含程式碼的完整細節: references/stage-recipe.md
耗費最多時間的陷阱
| 陷阱 | 修復方法 |
|---|---|
| 當 MSAA 開啟時,延遲陰影從不渲染,且無任何控制台錯誤 | 使用 shadowMode = .forward 加上一個 .shadowOnly 捕捉器。除錯缺失的陰影時,首先給場景一個可見的灰色 lambert 地板。 |
全新 SCNView 的首幀在你的進場動畫中途編譯 Metal 管線 | 在背景執行 prepare([scene]),將演員停在幕後,延遲首次進場。永遠不要在第一幀就開始動畫。 |
fillMode = .forwards + isRemovedOnCompletion = false 會鎖定呈現結果,且移除是針對每個物件的 | 建立一個中央的 clearAnimations,從每個入口點呼叫,負責清除節點、每個子幾何體、燈光和正在進行的動作。 |
| 正面朝向的真實金屬渲染成灰色或黑色空洞 | 從正面觀看的鏡子會反射相機後方環境區域。保留已核准的塗裝基底,並添加一個帶有自身反射貼圖的薄加法層。 |
切換 castsShadow 會在一幀內彈出模糊的半影;shadowBias 和燈光的 categoryBitMask 對於前向方向光會被忽略 | 保持 castsShadow 永久開啟,並跟隨動作同步動畫化 shadowColor 的 alpha 值。 |
| 固定的預熱延遲在冷設備和模擬器上仍然會卡頓;粒子效果的首幀會編譯自己的管線,並在觸發時正好掉幀 | 雙鑰匙點火:將進場門控在最短延遲和 prepare 的完成處理器兩者之上。使用一個符合真實效果標誌的零透明度爆發來預熱粒子管線。 |
預設的 UIGraphicsImageRenderer 格式會繼承螢幕縮放比例,使每個生成的紋理像素增加 9 倍——對於即時重新渲染的紋理(例如每次按鍵的雕刻)是致命的 | 將渲染器格式的縮放比例固定為 1,並將畫布大小設定為演員在螢幕上的投影大小;將多輸入重定向合併為每次更新傳遞時的一次渲染。 |
| 從照片重現真實物體的運動會產生自信的骨架,但結果卻側向失敗——每次修復都會揭露一個新的錯誤 | 靜態照片包含姿勢,而非路徑或機制;終點姿勢擬合無法決定軌跡。對路徑進行建模:建立一個具有直接姿勢控制的校準骨架,由擁有者針對實際物體編寫關鍵幀。 |
| 關鍵幀動畫在其關鍵幀處有節奏地卡頓,且所有渲染和時間修復都無效 | 卡頓是烘焙在曲線中的:在不均勻間隔的關鍵幀上使用均勻 Catmull-Rom 基函數,會在每個節點處使速度產生階躍。使用基於距離階段的加權跨度 Hermite 切線(或自然三次樣條)進行插值,並在數值連續性檢查通過後才觸發。 |
| 從較大模型中切割出的子網格,其測量結果就好像它是整個物體,且沒有任何錯誤訊息 | 切割僅修剪了索引緩衝區;頂點緩衝區仍然保留原始模型的每個頂點。僅測量子網格索引引用的頂點。 |
賦值給 scene.lightingEnvironment 的方形圖像會被靜默忽略——零反射,每個鏡面材質渲染為黑色,無控制台輸出 | 使用公認的立方體貼圖佈局來繪製環境貼圖,最簡單的是 2:1 的球面畫布(1024x512)。只有 material.reflective 接受方形球體貼圖。 |
參考資料地圖
- references/stage-recipe.md:完整的舞台、視圖設定、協調器、節點層級結構、相機、燈光系統、接觸影塊和捕捉器、程式化環境貼圖和紋理、舞台上的衝擊粒子、舞台演員上的著色器修飾符(線性空間均勻陷阱)、保持舞台的 SwiftUI 標識(重新掛載和更新風暴陷阱)。
- references/first-frame-and-warmup.md:首次繪製時的 Metal 管線編譯卡頓及兩種解決方案,帶有延遲進場的預熱(升級為門控在 prepare 完成處理器的雙鑰匙點火),或保持場景已掛載並預熱,然後重新定位它;粒子管線預熱;使用 signpost 和設備上的卡頓追蹤來證明解決方案。
- references/shadows-and-lights.md:每個無聲的陰影陷阱、可靠的前向 + shadowOnly 組合、動畫化陰影可見性、中性的光照預算、正面朝向的金屬。
- references/baked-animation.md:為什麼烘焙的關鍵幀優於計時器、設計多階段節拍並在烘焙前編寫提示表、接縫分類(C1 vs 接觸脈衝)、設計重量、密集取樣、清理記帳、生成令牌、在動畫中途竊取節點、滾動而不滑動、沿地板路徑滾動(轉向、螢幕空間定位、除錯軌跡)、超越變換的通道(變形器權重、鏡頭值、著色器統一值)、作為創作材料的彈簧、一個屬性一個擁有者。
- references/motion-from-reference.md:重現真實物體的運動——靜止圖像和影片能告訴你什麼、不能告訴你什麼、對路徑而非機制進行建模、校準骨架(擁有者為演員擺姿勢並保存關鍵幀)、距離參數化和對手工保存姿勢進行加權跨度插值、均勻 Catmull-Rom 陷阱、數值連續性門控、無縫餘弦狀態循環及從當前階段退出、測量修剪後的子網格。
- references/physics-without-engine.md:自製固定步長積分器並烘焙到關鍵幀、牆壁和地板作為純數值、接觸驅動的觸覺回饋、一種邊緣樞軸傾倒,以及為什麼這優於用於編排場景的 SCNPhysics。
- references/gestures-and-haptics.md:無需命中測試的平移抓取、按住時的軟限制、釋放速度、衝擊觸覺、在即時手指操作下倖存的腳本化節拍(有限重試、令牌輪詢、隱藏演員手勢防護、竊取關閉持有契約)、減少動效作為一種分類法、對無形舞台的 VoiceOver 無障礙存取、與 SwiftUI 手勢共存。
- references/sequencing-stages.md:將多個舞台作為一部電影來導演——帶有節拍作為數據的主時鐘、牆壁時間漂移陷阱、在出場動作清除後進行剪輯、預先掛載下一個舞台以進行預熱、舞台比其剪輯點活得更久(殘留的煙霧)、根據使用者互動重新設定時間軸、錄製引導、逐幀驗證剪輯。
- references/modeling-actors.md:從基本幾何體構建可信的英雄物體——在目測之前的真實世界比例、用於凹陷面的環形面、管狀加環狀輪廓線、徑向圖案可讀性、每個實例的傾斜以適應凹面、開放間隙、作為幾何體的浮雕特徵(絕非繪製)、深色舞台上的緞面金屬反照率、封鎖作為檔案到達的演員。
- references/camera-choreography.md:作為表演者的相機——軌道系統、烘焙的揭示飛行、跟隨推車的焦點、帶慣性拖動軌道並返回原位、SCNFloor 反射作為接地、下照燈下的啞光舞台、用於追蹤系統的限制(以及為什麼它們永遠不應用於演員)。
- references/instancing-and-swarms.md:同時操作數十個演員——群組的一個幾何體、基於槽位的堆疊取代物理、從著陸點反向解決拋物線、翻滾混合到靜止姿勢、播種隨機性、透過排程交錯、預算說明。
- references/multi-actor-physics.md:一個烘焙模擬中的多個演員——一個時鐘上的 N 個狀態向量、成對碰撞(分離,然後在接近時交換)、凍結世界的抓取、命中測試手指選中了哪個演員、用於擠壓和觸覺的每個演員接觸列表。
審查清單
在發布舞台之前,請驗證以下項目:
-
SCNView上設定backgroundColor = .clear且isOpaque = false;舞台合成時沒有可見的邊框或接縫。 - 沒有動畫在視圖的首幀開始;
prepare已執行,且首次進場被延遲,或場景保持預熱掛載狀態。 - 陰影使用前向模式搭配
.shadowOnly捕捉器;不依賴延遲陰影、shadowBias或燈光categoryBitMask門控。 - 具有 Alpha 紋理的平面(標籤、貼花)已設定
castsShadow = false,否則它們會投射出方形陰影。 - 陰影可見性變化是透過動畫化
shadowColor的 alpha 值來實現的;castsShadow在運行時永遠不會被切換。 - 如果場景必須保真地渲染紋理顏色,則環境光加上方向光乘以入射角餘弦值,對於平坦表面應總和為中性值 (1000);沒有裁剪過的光線灰色區域。
-
lightingEnvironment(或background)上的任何影像都使用公認的立方體貼圖佈局——2:1 球面、6:1 或 1:6 條帶、或六張圖像;該處的方形圖像會被靜默忽略,且反射材質會渲染為黑色。 - 每個非同步續接(延遲進場、排程觸覺、鏈接轉換)都捕獲了一個生成令牌,並在執行前重新檢查它。
- 任何竊取正在動畫的節點的程式碼,在呼叫
removeAllAnimations之前,都會先將.presentation值快照到模型中。 - 烘焙動畫在
addAnimation之前將模型設定為最終姿勢;不依賴fillMode = .forwards而不進行清理清除。 - 多階段節拍在姿勢函數上方有一個提示表註解,該註解與階段邊界結構體匹配;階段和提示時間僅存在於該一個結構體中,絕不會作為字面值出現在取樣迴圈或排程的觸覺中。每個接縫都已分類:設計接縫為 C1,脈衝接縫僅發生在接觸事件處,且每個事件都會產生一個提示。
- 滾動物體將旋轉與距離除以半徑的結果關聯起來;沒有任何物體滑動。
- 從真實物體複製的運動是透過校準骨架(直接姿勢控制、保存的姿勢、擁有者的視角)作為關鍵幀來創作的,
…
[因翻譯需求而截斷]