DAZAI CHEN
← 回到論文

Tech Dev

空間記憶檔案的系統架構與實作筆記。

最後更新:2026年3月29日

系統架構

整個系統分成三層:先把空間「捕捉」下來,把素材「處理」成 3DGS 模型,最後在 Quest 3 上獨立執行的 VR runtime 把這些空間 render 出來。

三層系統架構:捕捉、處理、執行環境
三層架構:拍攝(素材)→ 處理(3DGS 訓練)→ 執行(Quest 3 上的 VR 體驗)。

整個架構裡最關鍵的決定,是把邏輯從 Blueprint 移出來、用 TypeScript 寫。Blueprint 只負責偵測手勢、處理硬體事件、跟引擎打交道;至於「接下來會發生什麼」這類邏輯(相簿排版、動畫、轉場、引導流程的狀態),全部寫在 TypeScript 裡,透過 PuerTS 跟引擎溝通。這個切法是整個專案最關鍵的生產力提升:TypeScript 改完馬上重新跑,引擎不用重編。


捕捉流程

捕捉流程:素材到對位到 3DGS 到 Unreal
原始素材經過對位與 3DGS 訓練,最終進入 Unreal Engine。

素材來源沒有統一規格,有什麼就用什麼:能再回去的地方用 Insta360 X5 拍 360 影片,隨手記憶用 iPhone,已經回不去的地方就用單張舊照片。素材先用 ffmpeg 抽 frame,丟進 COLMAP 或 RealityScan 對位,然後在 PostShot 或 Luma AI 訓練成 3DGS。對只剩一張照片的舊記憶,我用 Sharp(Mescheder et al., 2025) 從單張影像生成 3DGS 場景。每個訓練好的場景輸出成 .ply,可以直接丟進 Unreal。


執行環境

整個體驗是 Quest 3 standalone,不需要接電腦。主要組成:

  • 3DGS renderSplatRenderer,我自己寫的 UE5 開源插件。支援靜態 .ply 跟動態 .gsd(4DGS)序列,用 compute shader 跑,RTX 4080 上 200 萬 Gaussian 可以維持 100+ FPS。
  • 手勢追蹤 — Meta Interaction SDK(ISDK)。捏指選卡片,左手抬起手掌朝臉就召喚出選單。
  • 燈光 — UE5 的 Directional / Sphere / Spotlight。每個空間獨立調光,目標是「被記得的樣子」,而不是相機原本捕捉的當下。
  • 空間音效 — UE5 Audio + 環境音 bed + ElevenLabs 生成的旁白。
  • 場景串流 — UE5 World Partition。每個空間是一個獨立 level,選了卡片才載入。
  • UI / 相簿 — UMG Widget,用 ISDK 的 ray-interactable 在 3D 空間裡可互動。
  • 邏輯層 — TypeScript 透過 PuerTS。相簿的排版、動畫、淡入淡出、串流時序、引導流程的狀態,全部在這裡。

互動流程

四個段落:引導、瀏覽相簿、進入空間、空間記憶
四個段落:引導影片、瀏覽相簿、選空間進入、站在被光線、聲音、旁白包圍的空間記憶裡。

完整的觀者旅程在 作品主頁。下面是驅動每個段落的實作筆記。


實作筆記

下面這部分是邊做邊記的工程筆記,技術細節跟程式碼為主,偏向開發參考用。完整版(包含原本所有 BP era 的細節)保留在 英文版 的 Implementation reference 區塊。

相簿(TS_WidgetCarousel via PuerTS)

整個相簿的邏輯在 TS_WidgetCarousel.ts。Blueprint 端只負責收手勢輸入,TypeScript 端負責所有的排版、動畫、選卡、轉場。

  • 三個分類(Travel / Personal / Motion),上下滑切換。
  • 卡片排成弧形(最多 18 張,3 ring × 6 cards),左右滑捲動。
  • 用 ISDK 的 IsdkInteractableWidgetComponent,可以原生支援 ray + pinch。
  • 兩手同時捏指可以 zoom in/out,調整相簿到自己舒服的距離。
  • 開相簿時 3DGS 場景自動降亮度(透過 SetBrightness 控制 SplatRenderer actor),讓卡片看得清楚。

引導(TS_Onboarding via PuerTS)

四個段落(welcome / palm / swipe / farewell),每段有對應的影片教學跟 ElevenLabs 配的旁白。

  • 在每一個段落之間 fade 切換,每段持續 6–9 秒。
  • SnapToHMD() 讓 widget 永遠跟著頭盔,距離鏡頭固定 X=120、Z=-30。
  • ElevenLabs 配音用 ffmpeg 切段,每段透過 SpawnSound2D 播放。
  • Skip button 隨時可按,按了會 fade out 當下的音檔,跳過剩下的引導,直接打開相簿。

一些 PuerTS 上踩過的坑

  • 在 constructor 裡初始化的 property 會被 UE 清掉,所有初始化要放 ReceiveBeginPlay()
  • 改完 TS 一定要 npx tsc,沒有 hot reload。
  • BP widget 的變數要用 (widget as any).VarName 才存取得到。
  • WidgetTree.RootWidget 在 spawn 完之後不會立刻有,要等大概 30 frame。
  • SetBrushFromTexture 餵 MediaTexture 會 crash,要改用 brush.ResourceObject = mediaTexture
  • SpawnSound2D 會回傳 AudioComponent 可以控制;PlaySound2D 是 fire-and-forget。
  • TArray out 參數要 import { $ref, $unref } from 'puerts'

3DGS 跟 translucent material 的衝突

3DGS splat 跟 translucent material 在 render order 上會打架,把 translucent plane 放進 3DGS 場景裡會被 splat 蓋掉看不到。

修法:在 plane component 上設 Translucency Sort Priority,數字越大越上層。試過 DitheredTemporalAA 但會出現點狀 pattern,最後還是 translucent + sort priority 最乾淨。

把 2D 媒體放進 3DGS 的設計準則

3DGS 場景本身已經很碎、很有顆粒感,邊邊也不平整。如果直接放矩形照片進去,硬邊會跟周圍的有機質感打架,看起來很怪。

實際 work 的做法:用去背的照片。透明背景讓內容(一份食物、窗外的風景)融進 splat 環境裡,沒有矩形邊緣破壞錯覺。設計準則:放進 3DGS 場景的東西要乾淨、無邊框,3DGS 本身已經提供所有質感,插入的媒體只要呈現主體本身就好。


早期探索:把媒體嵌進空間

在做 carousel 之前,最早的版本是把照片跟影片直接嵌在一個被捕捉的空間裡(布魯克林公寓),讓記憶從它們發生的位置「浮現」出來。後來放棄這個方向,改成多個空間 + 相簿的結構。但這個探索留下了不少實作筆記(BP_Clip、Material:M_Clip、放置工作流程等),完整版保留在 英文版的 Implementation reference 裡。


最後更新:2026-03-29