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:5—from tkinter import W(мёртвый импорт, не нужен, в headless Python 3.12 tkinter может отсутствовать) → закомментирован.scene/dataset_readers.py:287—dtype=np.byte(signed int8) ломаетPIL.Image.fromarray("RGB")в numpy 2.x →dtype=np.uint8.mmcv.Config.fromfile→mmengine.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-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.
Что дальше
- Заменить D-NeRF на real-world dynamic — Plenoptic Video / Neu3D, multi-view dynamic-сцены.
- Native 4DGS web viewer — pre-bake deformation-сетки в
.splat-batch формат (SuperSplat/SparkJS), без 20 отдельных .ply. Меньше трафика, плавный sub-frame interpolation. - AniGS / Disco4D / SinGS — frontier human-4DGS, чтобы заменить LHM motion-mp4 на настоящий timeline-аватар в браузере.
- Hunyuan3D 2.5 → когда веса появятся в open-source (текущая 2.0-turbo backed это temporary).
— RTX 5090 / GB202 / 0x2b85