trellis.2. unified 3d workflows

This commit is contained in:
Aggtaa
2026-09-24 13:34:06 +03:00
parent a47d23603c
commit 8db94e5c31
11 changed files with 690 additions and 177 deletions
+129 -3
View File
@@ -57,15 +57,141 @@ export function modelGraph(o: {
'10': { class_type: 'DecimateMesh', inputs: { mesh: ['9', 0], target_face_count: o.faces, placement_mode: 'midpoint' } },
'11': { class_type: 'MeshSmoothNormals', inputs: { mesh: ['10', 0], crease_angle: 180.0 } },
'12': { class_type: 'SaveGLB', inputs: { mesh: ['11', 0], filename_prefix: o.prefix } },
'13': { class_type: 'RotateMesh', inputs: { mesh: ['11', 0], mode: 'euler_xyz', 'mode.angle_x': 0, 'mode.angle_y': 35, 'mode.angle_z': 0 } },
'14': { class_type: 'RenderMesh', inputs: { mesh: ['13', 0], mode: 'solid', width: 512, height: 512, background: '#E7E5E4' } },
'15': { class_type: 'SaveImage', inputs: { images: ['14', 0], filename_prefix: o.prefix + '_preview' } },
...meshPreview(['11', 0], o.prefix, 13),
};
}
/** PNG-превью меша без текстур в ракурсе 3/4 → <prefix>_preview_…; узлы id, id+1, id+2. */
function meshPreview(mesh: Link, prefix: string, id: number): Graph {
return {
[id]: { class_type: 'RotateMesh', inputs: { mesh, mode: 'euler_xyz', 'mode.angle_x': 0, 'mode.angle_y': 35, 'mode.angle_z': 0 } },
[id + 1]: { class_type: 'RenderMesh', inputs: { mesh: [String(id), 0], mode: 'solid', width: 512, height: 512, background: '#E7E5E4' } },
[id + 2]: { class_type: 'SaveImage', inputs: { images: [String(id + 1), 0], filename_prefix: prefix + '_preview' } },
};
}
/** Ракурсы рендеров текстурированной модели TRELLIS.2, градусы поворота вокруг вертикали. */
export const TEXTURE_VIEWS = [0, 90, 180, 270];
/**
* Общая часть TRELLIS.2 (встроенные ноды ComfyUI, по шаблону 3d_pixal3d_trellis2_image_to_model): по картинке —
* плотный меш формы (нода 22) и цвет-воксели (нода 25).
*/
function trellisNodes(o: { image: string; seed: number }): Graph {
return {
// картинка → маска → кадр (TRELLIS.2: pad_factor 1.0) → условие DINOv3
'1': { class_type: 'LoadImage', inputs: { image: o.image } },
'2': { class_type: 'LoadBackgroundRemovalModel', inputs: { bg_removal_name: 'BiRefNet-general.safetensors' } },
'3': { class_type: 'RemoveBackground', inputs: { bg_removal_model: ['2', 0], image: ['1', 0] } },
'4': { class_type: 'ImageCropToMask', inputs: {
images: ['1', 0], masks: ['3', 0], width: 1024, height: 1024, pad_factor: 1.0, grow_mask: 0, background: '#000000' } },
'5': { class_type: 'CLIPVisionLoader', inputs: { clip_name: 'dino_v3_L_naf_fp32.safetensors' } },
'6': { class_type: 'Trellis2Conditioning', inputs: { clip_vision_model: ['5', 0], image: ['4', 0] } },
'7': { class_type: 'UNETLoader', inputs: { unet_name: 'trellis_2_int8_convrot.safetensors', weight_dtype: 'default' } },
'8': { class_type: 'VAELoader', inputs: { vae_name: 'trellis_2_shape_vae_bf16.safetensors' } },
'9': { class_type: 'VAELoader', inputs: { vae_name: 'trellis_2_texture_vae_bf16.safetensors' } },
// структура (разреженные воксели)
'10': { class_type: 'CFGOverride', inputs: { model: ['7', 0], cfg: 1, start_percent: 0.667, end_percent: 1 } },
'11': { class_type: 'RescaleCFG', inputs: { model: ['10', 0], multiplier: 0.7 } },
'12': { class_type: 'ModelSamplingSD3', inputs: { model: ['11', 0], shift: 5 } },
'13': { class_type: 'EmptyTrellis2LatentStructure', inputs: { batch_size: 1 } },
'14': { class_type: 'KSampler', inputs: { model: ['12', 0], positive: ['6', 0], negative: ['6', 1], latent_image: ['13', 0],
seed: o.seed, steps: 12, cfg: 7.5, sampler_name: 'euler', scheduler: 'normal', denoise: 1 } },
'15': { class_type: 'VaeDecodeStructureTrellis2', inputs: { samples: ['14', 0], vae: ['8', 0], resolution: '32' } },
// форма 512 → 1536
'16': { class_type: 'Trellis2ShapeStage', inputs: { positive: ['6', 0], negative: ['6', 1], voxel: ['15', 0] } },
'17': { class_type: 'CFGOverride', inputs: { model: ['7', 0], cfg: 1, start_percent: 0.769, end_percent: 1 } },
'18': { class_type: 'RescaleCFG', inputs: { model: ['17', 0], multiplier: 0.5 } },
'19': { class_type: 'KSampler', inputs: { model: ['18', 0], positive: ['16', 0], negative: ['16', 1], latent_image: ['16', 2],
seed: o.seed, steps: 20, cfg: 7.5, sampler_name: 'euler', scheduler: 'normal', denoise: 1 } },
'20': { class_type: 'Trellis2UpsampleStage', inputs: { positive: ['16', 0], negative: ['16', 1], shape_latent: ['19', 0], vae: ['8', 0],
target_resolution: 1536 } },
'21': { class_type: 'KSampler', inputs: { model: ['18', 0], positive: ['20', 0], negative: ['20', 1], latent_image: ['20', 2],
seed: o.seed, steps: 12, cfg: 7.5, sampler_name: 'euler', scheduler: 'simple', denoise: 1 } },
'22': { class_type: 'VaeDecodeShapeTrellis', inputs: { samples: ['21', 0], vae: ['8', 0] } },
// цвет-воксели
'23': { class_type: 'Trellis2TextureStage', inputs: { positive: ['20', 0], negative: ['20', 1], shape_latent: ['21', 0] } },
'24': { class_type: 'KSampler', inputs: { model: ['7', 0], positive: ['23', 0], negative: ['23', 1], latent_image: ['23', 2],
seed: o.seed + 1, steps: 12, cfg: 1, sampler_name: 'euler', scheduler: 'normal', denoise: 1 } },
'25': { class_type: 'VaeDecodeTextureTrellis', inputs: { samples: ['24', 0], vae: ['9', 0], shape_subdivides: ['22', 1] } },
};
}
/**
* Текстурированная модель (узел textured) и её файлы: GLB, base color, развёртка (узел unwrapped — меш с UV).
* Файлы: <prefix>_NNNNN_.glb, <prefix>_basecolor_…, <prefix>_uv_….
*/
function saveTextured(g: Graph, prefix: string, unwrapped: string, baseColor: Link, textured: Link) {
g['32'] = { class_type: 'MeshSmoothNormals', inputs: { mesh: textured, crease_angle: 180 } };
g['33'] = { class_type: 'SaveGLB', inputs: { mesh: ['32', 0], filename_prefix: prefix } };
g['34'] = { class_type: 'SaveImage', inputs: { images: baseColor, filename_prefix: prefix + '_basecolor' } };
g['35'] = { class_type: 'RenderUVAtlas', inputs: { mesh: [unwrapped, 0], resolution: 1024 } };
g['36'] = { class_type: 'SaveImage', inputs: { images: ['35', 0], filename_prefix: prefix + '_uv' } };
return g;
}
/**
* Модель TRELLIS.2 целиком: своя форма по картинке → ремеш и упрощение до o.faces треугольников (это и модель
* без текстур, <prefix>_mesh_…glb + превью) → UV-развёртка → текстуры из цвет-вокселей: base color и, если o.pbr,
* metallic и roughness → текстурированный GLB, развёртка и рендеры с 4 сторон (<prefix>_view<угол>_…).
*/
export function trellisGraph(o: {
image: string; prefix: string; seed: number; faces: number; textureSize: number; pbr: boolean;
}): Graph {
const g = trellisNodes(o);
g['26'] = { class_type: 'RemeshMesh', inputs: { mesh: ['22', 0], resolution: 768, sign_mode: 'udf', 'sign_mode.qef': false,
'sign_mode.drop_inverted_components': false, 'sign_mode.drop_enclosed_components': false, band: 1, project_back: 0,
fix_poles: false, smooth_iters: 20, drop_small_components: 0.01, precluster_max_verts: 20000000 } };
g['27'] = { class_type: 'DecimateMesh', inputs: { mesh: ['26', 0], target_face_count: o.faces, placement_mode: 'midpoint' } };
// модель без текстур
g['38'] = { class_type: 'MeshSmoothNormals', inputs: { mesh: ['27', 0], crease_angle: 180 } };
g['39'] = { class_type: 'SaveGLB', inputs: { mesh: ['38', 0], filename_prefix: o.prefix + '_mesh' } };
Object.assign(g, meshPreview(['38', 0], o.prefix, 60));
// текстуры: каждый тексель проецируется на плотную поверхность TRELLIS.2 и берёт цвет из её цвет-вокселей
g['28'] = { class_type: 'UnwrapMesh', inputs: { mesh: ['27', 0], segmenter: 'pec', resolution: o.textureSize, padding: 1, weld_distance: 0.0002 } };
g['29'] = { class_type: 'BakeTextureFromVoxel', inputs: { mesh: ['28', 0], voxel_colors: ['25', 0], texture_size: o.textureSize,
reference_mesh: ['22', 0] } };
g['30'] = { class_type: 'ApplyTextureToMesh', inputs: {
mesh: ['28', 0], base_color: ['29', 0], ...(o.pbr ? { metallic: ['29', 1], roughness: ['29', 2] } : {}) } };
saveTextured(g, o.prefix, '28', ['29', 0], ['30', 0]);
if (o.pbr) {
g['52'] = { class_type: 'SaveImage', inputs: { images: ['29', 1], filename_prefix: o.prefix + '_metallic' } };
g['53'] = { class_type: 'SaveImage', inputs: { images: ['29', 2], filename_prefix: o.prefix + '_roughness' } };
}
TEXTURE_VIEWS.forEach((angle, i) => {
const r = String(40 + i * 3), v = String(41 + i * 3), s = String(42 + i * 3);
g[r] = { class_type: 'RotateMesh', inputs: { mesh: ['32', 0], mode: 'euler_xyz', 'mode.angle_x': 0, 'mode.angle_y': angle, 'mode.angle_z': 0 } };
g[v] = { class_type: 'RenderMesh', inputs: { mesh: [r, 0], mode: 'texture', width: 512, height: 512, background: '#E7E5E4' } };
g[s] = { class_type: 'SaveImage', inputs: { images: [v, 0], filename_prefix: `${o.prefix}_view${angle}` } };
});
return g;
}
/**
* Текстура Hunyuan3D-2 Paint (turbo) для модели проекта — нода custom_nodes/img2glb_hunyuan_paint:
* Delight снимает свет с очищенной картинки, мультивью-модель рисует 6 видов (<prefix>_mv_…) по нормалям и
* позициям меша (o.mesh — путь в output) после UV-развёртки, виды запекаются в текстуру. Геометрия не меняется.
*/
export function paintGraph(o: { image: string; mesh: string; prefix: string; seed: number; textureSize: number; delight: boolean }): Graph {
const g: Graph = {
'1': { class_type: 'LoadImage', inputs: { image: o.image } },
// маска объекта: фон картинки заменяется белым, как в оригинальном пайплайне (там картинка RGBA)
'2': { class_type: 'LoadBackgroundRemovalModel', inputs: { bg_removal_name: 'BiRefNet-general.safetensors' } },
'3': { class_type: 'RemoveBackground', inputs: { bg_removal_model: ['2', 0], image: ['1', 0] } },
'26': { class_type: 'Img2GlbLoadOutputMesh', inputs: { path: o.mesh } },
'28': { class_type: 'UnwrapMesh', inputs: { mesh: ['26', 0], segmenter: 'pec', resolution: o.textureSize, padding: 1, weld_distance: 0.0002 } },
'29': { class_type: 'Img2GlbHunyuanPaint', inputs: { mesh: ['28', 0], image: ['1', 0], mask: ['3', 0],
texture_size: o.textureSize, seed: o.seed, delight: o.delight } },
'30': { class_type: 'ApplyTextureToMesh', inputs: { mesh: ['28', 0], base_color: ['29', 0] } },
'37': { class_type: 'SaveImage', inputs: { images: ['29', 1], filename_prefix: o.prefix + '_mv' } },
};
return saveTextured(g, o.prefix, '28', ['29', 0], ['30', 0]);
}
/** Что за задача в очереди — по нодам графа (работает и для задач, запущенных прямо в ComfyUI). */
export function kindOf(graph: unknown): JobKind {
const types = Object.values((graph ?? {}) as Graph).map(n => n?.class_type);
if (types.includes('Img2GlbHunyuanPaint')) return 'текстура';
if (types.includes('SaveGLB')) return '3D-модель';
if (types.includes('RemoveBackground')) return 'очистка фона';
if (types.includes('KSampler') && types.includes('CheckpointLoaderSimple')) return 'генерация';