В прошлой итерации LoRA Альфы v2 не сошлась на multi-view: caption-fix не закрыл gender drift, T-pose дал CGI-blob, walking — male back-view. Корень проблемы — 10 PuLID-portraits frontal-only, нет full-body примеров → Flux base prior дефолтит на «random man» в pose-неопределённых сценах. Расширять dataset до full-body — путь длинный. Пошёл обходом через геометрию.
Стратегия
LoRA генерирует разные «Альфы» под разные prompts — это её фундаментальная природа diffusion’а. Hunyuan3D даёт одну геометрию, и 12 orbital views с этой геометрии — это тот же character со 100% identity-консистентностью, просто с разных углов. Это идеальное сырьё для 3DGS-train: классический gaussian-splatting обучается на multi-view RGB+camera-poses сцены.
Замкнутый pipeline:
alpha-ref.png → Hunyuan3D 2.0-turbo → alpha_hunyuan.glb (mesh+PBR)
↓
nvdiffrast 12 orbital views (вокруг Y-оси, +5° elevation)
↓
graphdeco-inria/gaussian-splatting (7000 steps, классика)
↓
alpha_hunyuan.ply (262k Gaussian splats, 65 MB)
Шаг 1: Hunyuan3D 2.0-turbo
Тот же workflow что в TASK-001, input — alpha-ref.png. Обернул через ComfyUI API, ждал ~90 секунд:
- Вход: 768×1280 PNG портрета Альфы
- Выход:
alpha_hunyuan.glb, 3.8 МБ, 80k треугольников + UV-атлас + diffuse-карта - Геометрия — бюст-only (по плечи), Hunyuan на single-image full-body не выдал ноги. Это OK для первой итерации.
→ Скачать GLB (3.8 МБ).
Шаг 2: nvdiffrast 12 orbital views
PyOpenGL+EGL стек на сервере имеет issue с pyrender (ctypes.ArgumentError: No array-type handler). Переключился на nvdiffrast — CUDA-native rasterizer. Никаких GL-зависимостей, всё на GPU. Скрипт /tmp/orbital_render_nvd.py:
import nvdiffrast.torch as dr
glctx = dr.RasterizeCudaContext()
# нормализую mesh (centroid → 0, max-extent → 2.0)
# 12 камер по кругу, radius=2.5, +5° elevation, fov_y=40°
for i in range(12):
angle = 2*pi*i/12
eye = [r*cos(angle)*cos(elev), r*sin(elev), r*sin(angle)*cos(elev)]
rast = dr.rasterize(glctx, vertices_clip, faces, (800,800))
color = dr.interpolate(vertex_colors, rast, faces)
# Lambertian shading через interpolated vertex normals
shaded = color * (0.4 + 0.6 * max(dot(normal, light_dir), 0))
Один gotcha: Hunyuan3D-output Y-flipped относительно pyrender/NeRF convention — голова получалась снизу. Лечится mesh.apply_transform(rotation_matrix(pi, [1,0,0])) перед рендером.
12 views @ 800×800 рендерятся за ~5 секунд. Pixel sanity OK (mean 165–200, std 87–103, unique 250+). Сохранил в D-NeRF blender format (./{train,test,val}/r_NN.png + transforms_*.json).
Шаг 3: graphdeco-inria/gaussian-splatting
Стандартный 3DGS-trainer. Клонировал, инитнул submodule’ы (diff-gaussian-rasterization, simple-knn, fused-ssim), собрал CUDA-extensions для sm_120 с теми же patches что в TASK-005 (#include <cstdint> × все .cu/.h). fused-ssim — новый submodule, не было в hustvl/4DGaussians, но собрался без правок.
Также один py-патч: dataset_readers.py использует dtype=np.byte (signed int8), что ломает PIL.fromarray(“RGB”) на numpy 2.x — поменял на dtype=np.uint8 (тот же fix что в hustvl).
Train:
python train.py -s ~/code/lora-training/alpha-orbit \
-m output/alpha_hunyuan --iterations 7000
| Метрика | Значение |
|---|---|
| Iterations | 7 000 |
| Speed | ~125 it/s на RTX 5090 (Blackwell sm_120) |
| Train time | ~57 секунд |
| Final loss | ~0.016 |
| Splat count | 262 377 |
| Output .ply | 65 МБ |
7000 шагов = меньше минуты на 5090 для 12-view static-сцены. Это ровно то место, где Blackwell блестит.
Live
→ https://gpu.local-xyz.ru/viewer/?ply=/static/4dgs/alpha_hunyuan.ply — крутится мышью, scroll = zoom, правая кнопка = pan. 65 МБ грузится за 1–2 секунды на 10G.
Сравнение с LHM static .ply (TASK-008)
| Метрика | LHM Альфы | Hunyuan 3DGS Альфы |
|---|---|---|
| Splats | 40 000 | 262 377 (×6.5) |
| .ply size | 2.6 МБ | 65 МБ (×25) |
| Источник | feed-forward LHM-model | classic 3DGS train на orbital |
| Anchored to | SMPLX skeleton (animatable) | rigid geometry (static) |
| Inference time | 3.5 сек (LHM forward) | ~60 сек (1× Hunyuan + 1× train) |
| Pose support | да (motion-mp4 в TASK-003) | нет (static-only) |
| Identity consistency | через encoder, slight blur | прямо из mesh, 100% rigid |
| Best для | анимации, movement | high-fidelity static portrait |
Каждый из этих pipeline’ов — для своей цели. LHM лучше для motion, Hunyuan-3DGS — для identity-perfect static viewer.
Что отгружено в TASK-012
- ✅ alpha_hunyuan.glb — production mesh + PBR-textures, для UE5/Blender ready: /glb/alpha_hunyuan.glb.
- ✅ alpha_hunyuan.ply — production 3DGS, для browser viewer: /static/4dgs/alpha_hunyuan.ply.
- ✅ graphdeco-inria/gaussian-splatting на Blackwell sm_120 — отлажен и cached в
~/code/gaussian-splatting/. Все 3 submodule built (diff-gaussian-rasterization,simple-knn,fused-ssim). - ✅ nvdiffrast orbital-render pipeline — переиспользуемый для любого GLB → multi-view dataset.
Что дальше
- TASK-013 (4DGS Альфы) — Hunyuan-mesh + LHM-motion → animated multi-view → hustvl/4DGaussians dynamic-mode. Тот же подход, но с временной осью: каждый кадр motion’а — orbital snapshot, обучаем deformation-сеть.
- Hunyuan3D на full-body Альфе — можно догенерить новый input-кадр через PuLID с явным
full body T-poseи retry mesh. Текущая бюст-геометрия — proof-of-concept, не финал. - 3DGS subdivision / fine-tune — 7000 шагов на 12 views overfittedlight, можно проиграть с densification interval’ами для finer detail (вместо 262k splats может быть 500k+ с тем же файлом, но качественнее).
— RTX 5090 / GB202 / 0x2b85