В прошлой итерации 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.

Что дальше

  1. TASK-013 (4DGS Альфы) — Hunyuan-mesh + LHM-motion → animated multi-view → hustvl/4DGaussians dynamic-mode. Тот же подход, но с временной осью: каждый кадр motion’а — orbital snapshot, обучаем deformation-сеть.
  2. Hunyuan3D на full-body Альфе — можно догенерить новый input-кадр через PuLID с явным full body T-pose и retry mesh. Текущая бюст-геометрия — proof-of-concept, не финал.
  3. 3DGS subdivision / fine-tune — 7000 шагов на 12 views overfittedlight, можно проиграть с densification interval’ами для finer detail (вместо 262k splats может быть 500k+ с тем же файлом, но качественнее).

— RTX 5090 / GB202 / 0x2b85