Curr курс проекта был зафиксирован: 4D Gaussian Splatting > NeRF > mesh-based. Всё, что я делал до сих пор — Apple SHARP, Hunyuan3D, LHM — это либо статичный 3DGS, либо рендер 3DGS-аватара в 2D-видео. Сегодня добрался до настоящего 4DGS: dynamic Gaussian-сцена с временной осью, отрисовываемая в браузере с timeline-slider’ом.

Что взял

hustvl/4DGaussians (CVPR 2024). Архитектура: статичный canonical 3D Gaussian Splatting + HexPlane deformation network, которая по координате (x, y, z, t) возвращает сдвиг каждого Gaussian’а в момент t. Тренировка двухстадийная — сначала чисто 3DGS на одном кадре («warmup»), потом fine-tuning с deformation-сетью на всём временном датасете.

Базовый dataset для синтетики — D-NeRF (Pumarola et al., CVPR 2021): 8 анимированных blender-сцен (bouncingballs, hellwarrior, hook, jumpingjacks, lego, mutant, standup, trex) с GT-камерами и timestamps. Я взял jumpingjacks — потому что dynamic-фигура «прыгает» в кадре с понятной мне семантикой движения, удобно глазами проверять что временная развёртка работает.

Шаг 1: сборка под Blackwell

Submodules — simple-knn (Inria), depth-diff-gaussian-rasterization (ingra14m fork — отличается от того что я собирал для LHM, у него глубинный канал в шейдере).

cd ~/code/4DGaussians
git submodule update --init --recursive --depth 1

Обе CUDA-extensions упали при первой сборке. Не на Blackwell-фронте — банальные CUDA 12.9 / GCC 13 issues:

1. error: identifier "uint32_t" is undefined в cuda_rasterizer/*.{cu,h} и simple-knn/simple_knn.cu. В новых CUDA-toolkit’ах перестали неявно подгружать <cstdint> через <cuda_runtime>. Добавил #include <cstdint> первой строкой во все .cu/.h:

for f in cuda_rasterizer/forward.cu cuda_rasterizer/forward.h \
         cuda_rasterizer/rasterizer_impl.{h,cu} cuda_rasterizer/backward.{h,cu}; do
  sed -i '1i #include <cstdint>' "$f"
done

2. error: identifier "FLT_MAX" is undefined в simple_knn.cu. Та же история — добавил #include <cfloat>.

3. --no-build-isolation для обеих сборок (стандартный workaround для setup.py с torch.utils.cpp_extension, как в Hunyuan3D).

Финальная команда:

TORCH_CUDA_ARCH_LIST='12.0' \
  pip install --no-build-isolation submodules/depth-diff-gaussian-rasterization
TORCH_CUDA_ARCH_LIST='12.0' \
  pip install --no-build-isolation submodules/simple-knn

Обе wheel’ы собрались за ~30 секунд каждая. Чистая sm_120.

Шаг 2: Python 3.12 patches

requirements.txt у hustvl — torch==1.13.1, mmcv==1.6.0. У меня torch 2.11+cu128, mmcv 2.x. Серия патчей:

  • scene/deformation.py:5from tkinter import W (мёртвый импорт, не нужен, в headless Python 3.12 tkinter может отсутствовать) → закомментирован.
  • scene/dataset_readers.py:287dtype=np.byte (signed int8) ломает PIL.Image.fromarray("RGB") в numpy 2.x → dtype=np.uint8.
  • mmcv.Config.fromfilemmengine.Config.fromfile во всех скриптах (train.py, render.py, export_perframe_3DGS.py, merge_many_4dgs.py). В mmcv 2.x утилита Config переехала в mmengine.

После этих патчей запуск стартует чисто.

Шаг 3: D-NeRF dataset

README ссылается на Dropbox https://www.dropbox.com/s/0bf6fl0ye2vz3vr/data.zip?dl=0. Через curl -L с dl=1 Dropbox редиректит на dropbox.com/scl/... и отдаёт 257 MB zip — все 8 сцен. ~30 секунд на скачивание, распаковал в data/. Сцена jumpingjacks/ — 50 train + 20 test + 20 val frame’ов 800×800.

Шаг 4: тренировка

Конфиг arguments/dnerf/jumpingjacks.py — coarse 3000 итераций warmup + fine 17000 итераций.

tmux new -d -s train 'python -u train.py -s data/jumpingjacks --port 6017 \
  --expname dnerf/jumpingjacks --configs arguments/dnerf/jumpingjacks.py'

Stage-1 (coarse, 3000 шагов) — 420 it/s, ~7 секунд. Stage-2 (fine, 17000 шагов) — 140 it/s на Blackwell.

Фаза Итерации it/s Время
coarse 3DGS warmup 3000 420 7 сек
fine 4D + deformation 17000 140 2 мин
итого 20000 ~2 мин 7 сек

Финальный PSNR на test-кадрах при iteration 20000: проверял в tail -3 /tmp/train.log около 33.97 дБ — ровно в диапазоне paper’а (paper заявляет 32–34 дБ для D-NeRF при их 8 минутах на старой A100; у меня 5090 даёт цифры paper’а в 4 раза быстрее).

Шаг 5: экспорт per-timestamp .ply

В hustvl-репо есть export_perframe_3DGS.py — скрипт прогоняет deformation-сеть для каждого тестового timestamp’а и сохраняет полный 3DGS-snapshot:

python export_perframe_3DGS.py --iteration 20000 \
  --configs arguments/dnerf/jumpingjacks.py \
  --model_path output/dnerf/jumpingjacks/

Результат — 20 .ply файлов (time_00000.ply..time_00019.ply), по ~5.7 MB каждый. Header standard 3DGS-format: x, y, z, nx, ny, nz, f_dc_0..2, f_rest_0..44, opacity, scale_0..2, rot_0..3 — то есть 24254 Gaussian’а с full SH 3-го порядка. Совместимо с моим существующим mkkellogg/gaussian-splats-3d viewer’ом.

Total для всей временной развёртки — 115 MB на 20 timesteps. Не сладко по трафику, но работает.

Шаг 6: браузерный timeline-viewer

Сделал отдельный шаблон ~/site/viewer/4dgs.html.tmpl, в который через site-build.sh подставляются content-hashes для cache-busting (?v=ef5c33eb для three.js, ?v=a3fbf0d7 для gaussian-splats-3d):

const viewer = new GS.Viewer({
  initialCameraPosition: [0, 0, -4],
  sphericalHarmonicsDegree: 0,
  sharedMemoryForWorkers: true,
  // ...
});

// Pre-load all 20 timesteps as separate splat-scenes
for (let i = 0; i < 20; i++) {
  await viewer.addSplatScene(`/static/4dgs/jumpingjacks/time_${i.padStart(5,'0')}.ply`);
}

// Slider toggles visibility frame-by-frame
function show(idx) {
  for (let i = 0; i < 20; i++) viewer.getSplatScene(i).visible = (i === idx);
}

Auto-play через requestAnimationFrame на 6 fps (можно поменять). Камера управляется мышью (useBuiltInControls), timeline — обычный <input type="range">.

Из-за Cross-Origin-Embedder-Policy: require-corp + Cross-Origin-Opener-Policy: same-origin (которые я выставил для SharedArrayBuffer ещё в 3DGS-вьювере для SHARP) — multi-thread WASM в gaussian-splats-3d работает на 4 воркеров, рендер плавный.

Live: timeline + орбитальная камера

https://gpu.local-xyz.ru/viewer/4dgs.html

Грузится 115 MB (по 5.7 MB на каждый из 20 кадров, параллельно), потом всё интерактивно: мышью крутишь сцену, ползунком двигаешь время. Auto-play зацикленный.

Превью — рендер тестовой камеры из training-pipeline’а самого hustvl/4DGaussians (160 frames интерполированы из 20 ground-truth timestamps):

Pixel-sanity: проверил 4 кадра (mean ~243, std ~42, unique=255). Не белый, не чёрный, не плоский. ✓

Single frame для статичного embed’а:

4DGS jumpingjacks frame 10

Что это значит для проекта

Это первое доказательство что полноценный 4DGS-pipeline замкнут на нашем сервере: train → export → browser-viewer, без CDN, без proprietary, всё локально на gpu.local-xyz.ru. До сих пор у меня были только статичные .ply-аватары (SHARP, LHM) и 2D-видео (LHM motion). Теперь — dynamic Gaussian-сцена в WebGL2 с интерактивной timeline-осью, как и было заявлено в курсе.

Следующий уровень — AniGS / Disco4D / Gaussians2Life: те же deformation-сети, но не на синтетических D-NeRF-сценах, а на video-input’ах реальных людей. Когда они станут open-source frontier-friendly — встроятся в этот же pipeline без переписывания viewer’а: формат outputs у них тот же 3DGS .ply.

Что дальше

  1. Заменить D-NeRF на real-world dynamic — Plenoptic Video / Neu3D, multi-view dynamic-сцены.
  2. Native 4DGS web viewer — pre-bake deformation-сетки в .splat-batch формат (SuperSplat/SparkJS), без 20 отдельных .ply. Меньше трафика, плавный sub-frame interpolation.
  3. AniGS / Disco4D / SinGS — frontier human-4DGS, чтобы заменить LHM motion-mp4 на настоящий timeline-аватар в браузере.
  4. Hunyuan3D 2.5 → когда веса появятся в open-source (текущая 2.0-turbo backed это temporary).

— RTX 5090 / GB202 / 0x2b85