systems/PHOTOREAL_CAPABILITY_DIFF_2026-08-12.json
{
"audit": "photoreal capability diff - vendor feature surface vs what our runners actually pass",
"dated": "2026-08-12",
"prompted_by": "Josh: \"If it needs a different polishing path then find out what that is but I still don't think you're using all the features everything has properly.\" He was right.",
"method": "six parallel Opus lanes reading REAL vendor source signatures on disk plus live web checks; every unused feature cited to vendor file:line or doc URL; agents required to declare what they could not verify",
"audits": [
{
"tool": "TRELLIS.2 (Trellis2ImageTo3DPipeline + Trellis2TexturingPipeline, D:/assetgen/TRELLIS2_code @ upstream main)",
"verdict": "USING_MINIMALLY",
"features_UNUSED": [
"TEXTURE-STAGE DENOISING STEPS. `tex_slat_sampler_params={'steps': N}` reaches FlowEulerSampler.sample(steps=...) at D:/assetgen/TRELLIS2_code/trellis2/pipelines/samplers/flow_euler.py:86-112. Shipped value is 12 (pipeline.json tex_slat_sampler.params.steps); the official demo exposes 1-50 at app.py:388. Our MV runner routes --steps ONLY to the structure+shape stages (run_trellis2_native_pbr_mv.py:223-225), so the MATERIAL stage has literally never run at anything but 12 steps. This is the single knob most directly upstream of 'smooth plastic skin'.",
"TEXTURE-STAGE GUIDANCE. `tex_slat_sampler_params={'guidance_strength': N}` shipped at 1.0 = CFG OFF for the material stage (pipeline.json tex_slat_sampler.params.guidance_strength; classifier_free_guidance_mixin.py:8-10 short-circuits to the unconditional path when strength==1). app.py:389 offers 1.0-10.0. Our --tex-guidance flag exists (run_trellis2_native_pbr_mv.py:96) but is default None, so no run has ever turned texture CFG on. Issue #92 users propose 2.5. https://github.com/microsoft/TRELLIS.2/issues/92",
"CFG RESCALE, ALL THREE STAGES. `guidance_rescale` is implemented at trellis2/pipelines/samplers/classifier_free_guidance_mixin.py:21-30 (std-matching rescale of the x0 prediction). Shipped 0.7 / 0.5 / 0.0 (pipeline.json). app.py:378,384,390 exposes 0.0-1.0 per stage. Neither of our runners ever passes it. Raising tex rescale off 0.0 is the documented anti-oversaturation / contrast-restoration lever.",
"TIMESTEP SCHEDULE WARP. `rescale_t` per stage, applied at flow_euler.py:110-111 (`t_seq = rescale_t * t_seq / (1 + (rescale_t - 1) * t_seq)`). Shipped 5.0 / 3.0 / 3.0 (pipeline.json). app.py:379,385,391 slider 1.0-6.0. Never passed by us. It redistributes the 12 Euler steps between high-noise (layout) and low-noise (fine detail) — the direct control over whether steps get spent on pores or on silhouette.",
"1536-CUBED PIPELINE. `pipeline_type='1536_cascade'` at trellis2/pipelines/trellis2_image_to_3d.py:514 and :578-589. THIS IS THE PORE CEILING: decode_latent (:477-485) builds the PBR volume with `voxel_size = 1/resolution`, so base_color/roughness texel density is bounded by the VOXEL grid, not by texture_size. Every run this lane has made took the 1024_cascade default (run_trellis2_native_pbr_mv.py:258 records it). README times 1536 at ~60s on H100.",
"max_num_tokens IS A THRESHOLD, NOT A CAP. trellis2_image_to_3d.py:287 and :328-339: the loop computes token count and DECREMENTS hr_resolution by 128 until `num_tokens < max_num_tokens`, printing 'Due to the limited number of tokens, the resolution is reduced to N'. There is no upper bound in the code. CRITICAL: at 1024_cascade it is INERT — line 335's `or hr_resolution == 1024` breaks on the first iteration — so it only does anything at 1536_cascade, where the shipped 49152 will silently downgrade 1536 to 1408/1280/1152. Community reports 100000+ for 1536_cascade. https://deepwiki.com/visualbruno/ComfyUI-Trellis2/3.3.2-advanced-generation-with-custom-parameters",
"THE ENTIRE MESH-CONDITIONED TEXTURE PIPELINE. `Trellis2TexturingPipeline.run(mesh, image, seed, tex_slat_sampler_params, preprocess_image, resolution, texture_size)` at trellis2/pipelines/trellis2_texturing.py:374-408, with encode_shape_slat at :183-222 and its own texturing_pipeline.json weights on disk. Decouples geometry from surface: freeze the mesh you like, spend the whole budget on skin. Our run_trellis2_texture_refine.py is written and has NEVER been executed.",
"TEXTURING PIPELINE PRESERVES EXISTING UVs. trellis2_texturing.py:299-302: if the input trimesh carries `visual.uv`, it bakes into THOSE UVs instead of re-unwrapping. That means a UniRig/retopo/hand-authored UV layout survives, and it also means this is the only installed way to re-texture an EDITED mesh (graft, garment layer) without regenerating the character.",
"UV CHART QUALITY KNOBS ON to_glb. `mesh_cluster_threshold_cone_half_angle_rad`, `mesh_cluster_refine_iterations`, `mesh_cluster_global_iterations`, `mesh_cluster_smooth_strength` at o_voxel/postprocess.py:28-31, forwarded to cumesh compute_charts (site-packages/cumesh/cumesh.py:359-389). Our callers pass NONE of them and inherit to_glb's defaults, which are refine_iterations=0 / global_iterations=1 — strictly worse than cumesh's own defaults of 100 / 3.",
"DEEPER CHART CONTROLS to_glb DOESN'T EVEN FORWARD. cumesh.CuMesh.compute_charts also takes `area_penalty_weight=0.1` and `perimeter_area_ratio_weight=0.0001` (cumesh/cumesh.py:365-366) — chart-size and long-strip penalties. Reachable only by calling uv_unwrap ourselves instead of going through to_glb.",
"XATLAS PACKING OPTIONS, ENTIRELY UNSET. cumesh.CuMesh.uv_unwrap takes `xatlas_compute_charts_kwargs` and `xatlas_pack_charts_kwargs` (cumesh/cumesh.py:406-412); o_voxel.postprocess.to_glb:201-210 passes NEITHER. So xatlas runs with `padding=0`, `brute_force=False`, `texels_per_unit=0`, `block_align=False` (cumesh/xatlas.py:104-142). Zero-pixel gutters at 4K on a face is mip/bilinear bleed across seams — currently papered over by cv2.inpaint at postprocess.py:289.",
"REMESH PROJECT-BACK. `remesh_project` defaults to 0.9 in o_voxel/postprocess.py:27 and maps to `project_back` in cumesh/remeshing.py:373-383, which snaps dual-contoured vertices back onto the original surface. Our runner hardcodes 0 (run_trellis2_native_pbr_mv.py:321) — as do example.py:45 and app.py:505 — so the 0.9 default has never been tried by anyone in this lane. Also unused: `remove_inner_faces` (remeshing.py:383).",
"SAMPLE ONCE, EXTRACT MANY. `run(..., return_latent=True)` (trellis2_image_to_3d.py:498, :592-593) plus `decode_latent(shape_slat, tex_slat, resolution)` (:455-486). app.py:398 and :492 use this to split generation from GLB extraction, so one sampling run can be re-baked at several texture_size / decimation_target values — and re-decoded at a DIFFERENT resolution than it was sampled at. Neither of our runners requests the latent.",
"THE DE-LIGHTING PROOF PATH. `EnvMap` (trellis2/renderers/pbr_mesh_renderer.py:38-62, nvdiffrec IBL with mip chain), `render_utils.render_video/render_snapshot(..., envmap=...)` (trellis2/utils/render_utils.py:80, app.py:402) and `make_pbr_vis_frames` (render_utils.py:109), plus `screen_space_ambient_occlusion` (pbr_mesh_renderer.py:92-100). example.py:11-17,27 uses it. This is how you MEASURE whether lighting is baked into basecolor — relight the asset under an HDRI and compare. We have never run it; our only viewer is render_glb_turntable.py."
],
"what_we_pass": [
"C:/dev/humanity-forgotten/build/3d/bakeoff/native_tex/run_trellis2_native_pbr.py:149 — `pipe.run(img, seed=a.seed, preprocess_image=True)`. That is ALL. No sampler params, no pipeline_type, no max_num_tokens, no return_latent. Every guidance/steps/rescale value is the shipped default.",
"run_trellis2_native_pbr_mv.py:245-250 — `pipe.run_multi_image(imgs, seed, preprocess_image=True, mode, sparse_structure_sampler_params, shape_slat_sampler_params, tex_slat_sampler_params, **{pipeline_type, max_num_tokens})` via the authored trellis2_multiview.py injection.",
"run_trellis2_native_pbr_mv.py:218-222 — the ONLY sampler key we ever build is `guidance_strength` (--guidance to structure+shape, --tex-guidance to texture), and both default to None so neither is normally sent.",
"run_trellis2_native_pbr_mv.py:223-225 — `steps` is written into ss_params and sh_params ONLY. tx_params never receives steps.",
"run_trellis2_native_pbr_mv.py:316-321 — `o_voxel.postprocess.to_glb(vertices, faces, attr_volume, coords, attr_layout, voxel_size, aabb, decimation_target, texture_size, remesh, remesh_band=1, remesh_project=0, verbose=True)`.",
"run_trellis2_native_pbr_mv.py:87-88 — defaults `--texture-size 2048`, `--decimation 200000`; :89 `--remesh` off by default; :99 `--pipeline-type` default None; :102 `--max-tokens` default None; :106 `--steps` default None.",
"run_trellis2_native_pbr_mv.py:176-177 — `pipe.low_vram = False` only when --no-low-vram is passed; otherwise from_pretrained leaves it True.",
"trellis2_multiview.py:265-266 — `pipe.get_cond(images, 512)` and `pipe.get_cond(images, 1024)`; :269-271 truncates neg_cond to batch 1. :217 `max_num_tokens=49152` is our own re-declared default.",
"run_trellis2_texture_refine.py:93-94 (NEVER EXECUTED) — `pipe.run(mesh_in, img, seed, tex_slat_sampler_params={'steps': N}, resolution=512|1024, texture_size=4096)`; :76-77 correctly loads config_file='texturing_pipeline.json'.",
"run_trellis2_texture_refine.py:48 — `--resolution` argparse `choices=[512, 1024]`, which forecloses passing anything else to encode_shape_slat's free `resolution` grid_size (trellis2_texturing.py:185, 203)."
],
"misconfigurations": [
"texture_size default 2048 (run_trellis2_native_pbr_mv.py:87) vs the vendor's own example at 4096 (D:/assetgen/TRELLIS2_code/example.py:44). Nothing in o_voxel/postprocess.py:24 clamps it; app.py:532 slider tops at 4096 only as a UI choice.",
"decimation_target default 200000 (run_trellis2_native_pbr_mv.py:88) vs to_glb's default 1000000 (o_voxel/postprocess.py:23), example.py:42's 1000000, and app.py:531's 500000. We are running 5x below the vendor reference; cumesh/cumesh.py:318 confirms this is target_num_FACES (to_glb's docstring saying 'vertices' is wrong).",
"pipeline_type is never set, so every asset came from 1024_cascade (trellis2_image_to_3d.py:56 default_pipeline_type, echoed in pipeline.json). The PBR volume — and therefore all pore/freckle texel budget — is at 1024-cubed, not 1536.",
"--steps does not reach the texture sampler (run_trellis2_native_pbr_mv.py:223-225). The material stage is permanently pinned at the shipped 12 steps no matter what is passed.",
"--max-tokens is inert at the default pipeline type. trellis2_image_to_3d.py:335's `if num_tokens < max_num_tokens or hr_resolution == 1024: break` exits on iteration one when resolution==1024. The flag's own help text at run_trellis2_native_pbr_mv.py:103-105 overstates what it does unless --pipeline-type 1536_cascade is also given.",
"low_vram silently defaults TRUE: pipeline.json and texturing_pipeline.json carry no low_vram key, so from_pretrained falls back to True (trellis2_image_to_3d.py:107, trellis2_texturing.py:88) and every stage ping-pongs CPU<->GPU (:145-149, :175-179, :210-221). On a 32GB 5090 this is pure wall-clock waste; --no-low-vram exists but is opt-in.",
"to_glb's own chart defaults are below the library's. o_voxel/postprocess.py:30-31 hardcodes refine_iterations=0 and global_iterations=1 into the compute_charts call, while cumesh/cumesh.py:362-363 defaults are 100 and 3. We inherit the degraded values because we never pass mesh_cluster_* overrides.",
"run_trellis2_native_pbr.py:149 (the single-view arm that produced the baseline meshes) passes no sampler params at all, so the 'stylised CG doll' baseline was generated entirely at shipped defaults — it is not evidence about the model's ceiling."
],
"newer_alternatives": [
"Hunyuan3D 3.0 / 3.1 (3.1 since Jan 2026) — image-to-3D with automatic PBR including albedo, NORMAL, and roughness maps. TRELLIS.2 emits no normal map at all (tex decoder out_channels=6: base_color 3 + metallic 1 + roughness 1 + alpha 1, per D:/assetgen/models/TRELLIS2/ckpts/tex_dec_next_dc_f16c32_fp16.json), which is exactly the channel you need for pore/freckle micro-relief. https://learn.rundiffusion.com/ai-3d-model-generators/",
"Rodin Gen-2 / Gen-2.5 — reported as the current state of the art in SURFACE realism ('rivaling actual scans') and the most detailed geometry of the compared engines; also markets animation-ready topology. Directly relevant to the photoreal-skin goal. https://learn.rundiffusion.com/ai-3d-model-generators/",
"Tripo H3.1 and Meshy v6 — the other two engines in the current five-way production comparison; Tripo is called out as the game-dev default on speed rather than fidelity. https://learn.rundiffusion.com/ai-3d-model-generators/",
"microsoft/TRELLIS.2 upstream main is UNCHANGED versus our local D:/assetgen/TRELLIS2_code copy. I fetched raw trellis2_image_to_3d.py and trellis2_texturing.py from GitHub main and both signatures match ours byte-for-byte in argument list and defaults. There is no TRELLIS.3, and no upstream feature we are missing by not pulling. https://github.com/microsoft/TRELLIS.2",
"Multi-image conditioning is still an OPEN request upstream (issue #77, opened Jan 2026, motivated by exactly our failure — 'the back of the generated models turns out to be messy'). Our trellis2_multiview.py authored injection remains the only route, and its attach() guard at :318-321 correctly refuses if upstream ever ships one. https://github.com/microsoft/TRELLIS.2/issues/77",
"No official best-quality preset exists. Issue #92 (Jan 2026) asks the maintainers for one and proposes 1536 / texture 4096 / decimation 800000 / 50 steps all stages / guidance 8.0-8.5 shape, 2.5 texture; still awaiting maintainer response. Treat those numbers as a community starting point, not doctrine. https://github.com/microsoft/TRELLIS.2/issues/92"
],
"top_recommendation": "Stop tuning texture_size — it is not the bottleneck. The PBR attribute volume is generated at the VOXEL grid resolution (decode_latent, trellis2_image_to_3d.py:477-485, voxel_size = 1/resolution), so a 4096 map is just oversampling a 1024-cubed volume. Run the MV runner as: `--pipeline-type 1536_cascade --max-tokens 131072 --no-low-vram --texture-size 4096 --decimation 1000000`. The --max-tokens raise is MANDATORY with 1536_cascade or line 335-339 silently steps the resolution back down to 1152/1024 and prints 'Due to the limited number of tokens...' — which would look like 1536 failing when it never ran. Second, in the same change, add a --tex-steps / --tex-rescale path so tex_slat_sampler_params carries {steps: 50, guidance_strength: 2.5, guidance_rescale: 0.3, rescale_t: 3.0}; today run_trellis2_native_pbr_mv.py:223-225 makes the material stage unreachable at anything but 12 steps and CFG off. Third, finally EXECUTE run_trellis2_texture_refine.py against the best existing mesh — a whole shipped pipeline with its own weights on disk has zero runs — and validate de-lighting with render_utils.render_video(mesh, envmap=EnvMap(hdri)) + make_pbr_vis_frames, which is the only way to prove whether light is baked into basecolor rather than asserting it.",
"evidence_note": "VERIFIED BY READING SOURCE (every file:line above was opened, not recalled): trellis2/pipelines/trellis2_image_to_3d.py (595 lines, full), trellis2_texturing.py (408 lines, full), samplers/flow_euler.py, samplers/classifier_free_guidance_mixin.py, samplers/guidance_interval_mixin.py, pipelines/base.py, modules/image_feature_extractor.py, representations/mesh/base.py, renderers/pbr_mesh_renderer.py (head), example.py, example_texturing.py, app.py:340-565, README.md; D:/assetgen/models/TRELLIS2/pipeline.json and texturing_pipeline.json in full plus five ckpt jsons; the INSTALLED site-packages copies that actually execute — o_voxel/postprocess.py (331 lines, full), cumesh/cumesh.py:310-480, cumesh/xatlas.py (full), cumesh/remeshing.py:373-420; and all three of our callers in full. Upstream parity checked by fetching raw GitHub main for both pipeline files — signatures identical to local. NOT VERIFIED / EXPLICITLY FLAGGED: (1) I did not RUN anything, so every quality claim is a source-reading prediction, not a measurement — the 1536_cascade VRAM cost on a 5090 at max_num_tokens 131072 is untested and may OOM. (2) max_num_tokens has no upper bound in code; the 100000-131072 figures come from a third-party ComfyUI wrapper's docs and issue #92, not from Microsoft. (3) I could not read flex_gemm/ops/grid_sample.py (not present at the expected path), so the trilinear-only sampling mode in to_glb:265 is taken from the call site, not the implementation. (4) Passing texture_size above 4096, or texturing-pipeline resolution above 1024, is NOT validated by any vendor code path — the tex DiTs are trained at 512/1024 (slat_flow_imgshape2tex_dit_1_3B_{512,1024}) and trellis2_texturing.py:401 routes anything != 512 to the 1024 model. (5) IMPORTANT NEGATIVE FINDING: there is NO de-lighting module in TRELLIS.2 — I grepped the whole trellis2/ tree for delight/albedo/shading/irradiance and found only one unrelated comment. The PBR decomposition (base_color/metallic/roughness/alpha, layout at trellis2_image_to_3d.py:73-78) IS the de-lighting, and there is NO normal-map output at all (tex_dec out_channels=6). Micro-relief for pores cannot come from this model; it must come from a normal/height map generated elsewhere or from the 1536 voxel grid's geometry."
},
{
"tool": "MV-Adapter (multi-view generation) — vendor source /root/mv3d/MV-Adapter (WSL2), caller harness/asset_factory/mva_views.py",
"verdict": "USING_MINIMALLY",
"features_UNUSED": [
"--base_model / MVAdapterI2MVSDXLPipeline.from_pretrained(base_model): the adapter is base-model-agnostic and rides ANY diffusers-format SDXL checkpoint. Vendor arg at scripts/inference_ig2mv_sdxl.py:212-214, applied at :43. README:194 ships a PHOTOREAL example verbatim (`--base_model \"stablediffusionapi/real-dream-sdxl\" --scheduler ddpm`), README:135 dreamshaper-xl-1-0. Our mva_views.py:240 hardcodes stabilityai/stable-diffusion-xl-base-1.0 and exposes no flag. Stock SDXL base IS the smooth-plastic-skin prior; a photoreal SDXL finetune changes skin microcontrast at zero pipeline cost. THIS IS THE PHOTOREAL LEVER.",
"lora_model + lora_scale: pipe.load_lora_weights(model_, weight_name=name_) at scripts/inference_ig2mv_sdxl.py:69-71, argparse :220/:232. MULTI-LoRA with per-adapter weights via adapter_name= and pipe.set_adapters(adapter_name_list, adapter_weights=lora_scale) at scripts/inference_t2mv_sdxl.py:57-64 and :96; README:224-233 stacks 9 LoRAs with per-LoRA scales. We pass cross_attention_kwargs={\"scale\":1.0} (mva_views.py:357) — the LoRA scale knob — while NEVER loading a LoRA, so the scale multiplies nothing. Detail/skin LoRAs (README:230 lists add-detail-xl, xl_more_art-full_v1) are the direct answer to 'no pore/freckle detail'.",
"mv_scale (float, default 1.0) — pipeline __call__ param at mvadapter/pipelines/pipeline_mvadapter_i2mv_sdxl.py:304, injected into cross_attention_kwargs at :652, consumed in DecoupledMVRowColSelfAttnProcessor2_0.__call__ at mvadapter/models/attention_processor.py:447 and applied at :724-725 as `hidden_states = hidden_states + hidden_states_mv * mv_scale`. This is the multi-view self-attention blend strength. At 1.0 every view's high-frequency content is pulled toward the cross-view consensus — the exact mechanism that averages away pores and freckles. Lowering it (e.g. 0.7-0.9) trades a little cross-view consistency for per-view high-frequency survival. We never pass it.",
"control_conditioning_factor (float, default 1.0) — pipeline_mvadapter_i2mv_sdxl.py:308, used at :735 `if i < int(num_inference_steps * control_conditioning_factor): down_intrablock_additional_residuals = [...] else: None`. It DROPS the geometry/Plucker T2I-Adapter residuals after a fraction of the denoising steps, leaving the late (detail-synthesising) steps unconstrained by the G-buffer. Late steps are precisely where skin microdetail is written. We pass control_conditioning_scale=1.0 (mva_views.py:344) but never the factor, so geometry clamps all 50 steps.",
"ip_adapter_image / ip_adapter_image_embeds — pipeline_mvadapter_i2mv_sdxl.py:288-289; fully wired: checked at :491-492, embeddings computed via self.prepare_ip_adapter_image_embeds(...) at :600-605, injected as added_cond_kwargs[\"image_embeds\"] at :732-733. This is a SECOND, independent identity channel that runs alongside MV-Adapter's own reference_image path (which uses ref_scale in the decoupled attention). For a fixed face ladder (curly ash-blond, freckles, blue eyes) an IP-Adapter FaceID/PlusV2 image would lock identity across the age range without fighting the geometry control. We pass neither.",
"mvadapter_ig2mv_partial_sdxl.safetensors — a THIRD geometry-conditioned checkpoint, README:67, loaded at scripts/inference_ig2mv_partial_sdxl.py:63. Conditions on a PARTIAL reference image (only part of the object visible) instead of a full plate. Its run_pipeline also calls load_mesh(mesh_path, rescale=True, move_to_center=True, return_transform=True) at :150-156 and emits offset/scale in a _transform.json (:302-303) so generated views map back to the mesh's original space. NOT PRESENT on disk: /root/mv3d/weights_mva/huanngzh__mv-adapter holds only mvadapter_i2mv_sdxl.safetensors and mvadapter_ig2mv_sdxl.safetensors.",
"mvadapter_tg2mv_sdxl.safetensors (Text-Geometry-to-Multiview) — README:66, scripts/inference_tg2mv_sdxl.py. Geometry-conditioned generation driven by TEXT ONLY, no reference plate. Useful as a second opinion / prompt-driven skin pass on an already-approved mesh where the 2D plate is the thing baking in flat lighting. Not downloaded, never called.",
"unet_model — UNet2DConditionModel.from_pretrained(unet_model) at scripts/inference_ig2mv_sdxl.py:38-39, argparse :218; README:203-210 uses it for latent-consistency/lcm-sdxl with --scheduler lcm at 8 steps. Lets you swap the UNet independently of the VAE/text encoders. We hardcode the stock UNet (mva_views.py:240) and expose no flag.",
"SDXL micro-conditioning: original_size, target_size, crops_coords_top_left, negative_original_size, negative_target_size, negative_crops_coords_top_left — pipeline_mvadapter_i2mv_sdxl.py:294-299. These are SDXL's native resolution/crop conditioning embeddings. Setting original_size high (e.g. (2048,2048)) biases generation toward the sharp, high-resolution end of SDXL's training distribution; leaving them None makes them default to the generation size. A documented, free sharpness lever we never touch.",
"denoising_end (pipeline_mvadapter_i2mv_sdxl.py:276, implemented at :686-698) plus guidance_rescale (:293), timesteps custom schedule (:275), latents (:283), clip_skip (:300), eta (:281), callback_on_step_end (:301-302). denoising_end enables SDXL's 'Mixture of Denoisers' handoff (docstring :341-347) — stop the MV pass early and finish each view in an SDXL refiner / detail pass. None of these ten are passed by mva_views.py:348-361.",
"prompt_2 / negative_prompt_2 — pipeline_mvadapter_i2mv_sdxl.py:271 and :279. SDXL's second text encoder can be given a DIFFERENT prompt from the first. We pass a single --text default of literally \"high quality\" (mva_views.py:452) into both. For a photoreal face this is the cheapest unclaimed quality on the list.",
"scheduler choice: ShiftSNRScheduler.from_scheduler(..., scheduler_class=DDPMScheduler|LCMScheduler|None) — scripts/inference_ig2mv_sdxl.py:46-57, argparse :219. from_scheduler at mvadapter/schedulers/scheduling_shift_snr.py:57-69: when scheduler_class is None it PRESERVES the base pipeline's own scheduler class (EulerDiscrete for SDXL) and only applies the SNR shift. Our mva_views.py:243 unconditionally forces DDPMScheduler, so we can never run the vendor default, and cannot run LCM.",
"Arbitrary view counts and arbitrary camera rings. inference_i2mv_sdxl.py:198 marks --num_views \"not used\"; the real count is num_views = len(args.azimuth_deg) at :219, with --azimuth_deg nargs=\"+\" at :199-201, threaded to get_orthogonal_camera at :135-144 and to init_custom_adapter(num_views=...) at :53. The GitHub title bar reads \"[768 Resolution] [Any SDXL Model] [Various Conditions] [Arbitrary Views]\" (https://github.com/huanngzh/MV-Adapter). Our PRESETS dict (mva_views.py:143-162) freezes two 6-view sets with no CLI override — for a FACE ladder you want a denser front arc (e.g. 8-10 views clustered +/-60 deg) rather than 4 quadrants + 2 poles. Constraint to respect: attention_processor.py:562 does `height = width = math.isqrt(sequence_length)`, so latents must be SQUARE.",
"TexturePipeline (mvadapter/pipelines/pipeline_texture.py) — __call__ at :136-172 exposes uv_size=4096 (4K atlas), separate base_color_path / orm_path / normal_path + normal_strength channels producing a PBR model (TexturePipelineOutput.pbr_model_save_path, :55), poisson_reprojection, uv_inpaint_use_network, view_inpaint_include_occlusion_boundary, camera_projection_type/elevation/azimuth/distance/ortho_scale/fov_deg. ModProcessConfig at :43-50 exposes view_upscale + view_upscale_factor=2 (RealESRGAN_x2plus) and inpaint_mode in [none, uv, view] with view_inpaint_min/max_rounds (LaMa big-lama). Checkpoints per README:402-403. LICENCE CAVEAT, stated plainly: TexturePipeline.__init__ constructs NVDiffRastContextWrapper at :62, so this class is nvdiffrast-tainted and BLOCKED under your commercial-use rule — but the RealESRGAN x2 view-upscale and LaMa view-inpaint ideas are independently reimplementable in the Blender-based merge you already own."
],
"what_we_pass": [
"harness/asset_factory/mva_views.py:348-361 — the ENTIRE call: pipe(text, height=res, width=res, num_inference_steps, guidance_scale, num_images_per_prompt=n_views, generator, negative_prompt, cross_attention_kwargs={\"scale\":1.0}, reference_image, reference_conditioning_scale, control_image, control_conditioning_scale=1.0). Twelve args out of a 38-parameter signature (pipeline_mvadapter_i2mv_sdxl.py:268-312).",
"harness/asset_factory/mva_views.py:240 — base model hardcoded: P.from_pretrained(_local(\"stabilityai/stable-diffusion-xl-base-1.0\"), vae=vae). No --base_model, --unet_model or --lora_model argparse entry exists anywhere in the file (see main(), :432-461).",
"harness/asset_factory/mva_views.py:241-244 — scheduler forced: ShiftSNRScheduler.from_scheduler(pipe.scheduler, shift_mode=\"interpolated\", shift_scale=8.0, scheduler_class=DDPMScheduler), unconditionally.",
"harness/asset_factory/mva_views.py:245-257 — adapter selection: only two weight names, mvadapter_ig2mv_sdxl.safetensors / mvadapter_i2mv_sdxl.safetensors. init_custom_adapter is called with num_views + self_attn_processor only; cond_in_channels/copy_attn_weights/zero_init_module_keys (pipeline :884-887) left default.",
"harness/asset_factory/mva_views.py:143-162 — PRESETS: two frozen 6-view ortho sets (ig2mv elev [0,0,0,0,89.99,-89.99] az [0,90,180,270,180,180]; i2mv elev [0]*6 az [0,45,90,180,270,315]), distance 1.8, ortho_half 0.55. No CLI to change count, azimuths or elevations.",
"harness/asset_factory/mva_views.py:270-291 load_reference() — composites RGBA over 0.5 grey then scales the WHOLE image to 0.9 of canvas. It does NOT do the vendor's alpha-bounding-box crop.",
"harness/asset_factory/mva_views.py:452-454 — prompt defaults: --text \"high quality\", --negative \"watermark, ugly, deformed, noisy, blurry, low contrast\". No prompt_2/negative_prompt_2.",
"harness/asset_factory/mva_texture_stage.py:70 — MVA_GEN = {seed 42, steps 50, guidance 3.0, res 768, ref_scale 1.0}; BAKE_RES 1024 at :71. Invoked at :191-197 (ig2mv only, ref-scale 1.0).",
"harness/asset_factory/char_pipeline_from_ladder.py:248-250 — i2mv invoked with --res 1024 (off-distribution, see misconfigurations).",
"harness/asset_factory/mva_views.py:225-260 build_pipe() — never calls pipe.enable_vae_slicing(), which both vendor scripts do (inference_ig2mv_sdxl.py:74, inference_i2mv_sdxl.py:67)."
],
"misconfigurations": [
"--res 1024 for i2mv at harness/asset_factory/char_pipeline_from_ladder.py:250 is OFF-DISTRIBUTION. The adapter is a 768 model: training config tag is \"r768-ortho-nv6-ele0-sdxl\" with data.height/width: 768 (configs/view-guidance/mvadapter_i2mv_sdxl.yaml:2,33-34), the ig2mv config is identical (configs/geometry-guidance/mvadapter_ig2mv_sdxl.yaml:2,33-34, tag \"r768-ortho-nv6-ig2mv-dcrowcol-sdxl\"), README:11 states the headline as \"with 768 Resolution using SDXL\", every vendor entry point hardcodes height=width=768 (inference_i2mv_sdxl.py:254-255, inference_ig2mv_sdxl.py:278-279, texture_i2tex.py:34), and the GitHub repo title is literally \"[768 Resolution]\". 1024 runs (the latent stays square, required by attention_processor.py:562) but the decoupled MV attention was never trained at that token count — soft, mushy, plausible-but-wrong detail is the expected failure mode. NOTE the tension: mva_texture_stage.py:70 correctly uses 768, so the two lanes disagree with each other.",
"Reference-plate preprocessing is missing the alpha bounding-box crop. Vendor preprocess_image (scripts/inference_ig2mv_sdxl.py:91-119) computes the alpha bbox (`y, x = np.where(alpha)`, :96-98), CROPS to it, then scales the cropped SUBJECT to 0.9 of the canvas. Our load_reference (mva_views.py:283-291) skips the crop and scales the whole plate. Any margin in the plate shrinks the subject below the training framing, spending fewer pixels on the face — directly costing facial detail. The 0.5-grey composite we do get right.",
"--remove_bg / BiRefNet is never used. Vendor gates preprocess_image behind remove_bg_fn (inference_ig2mv_sdxl.py:180-184, :256-268 loading ZhengPeng7/BiRefNet). Our elif branch (mva_views.py:277) only triggers on RGBA input; an RGB plate with a background gets composited as-is, so the reference encoder is conditioned on background pixels.",
"cross_attention_kwargs={\"scale\": 1.0} at mva_views.py:357 is a no-op. That key is the LoRA scale, and we load no LoRA (no load_lora_weights call anywhere in mva_views.py). Meanwhile the two keys that DO matter for this pipeline — mv_scale and ref_scale — are set by the pipeline itself at :652-654; mv_scale silently defaults to 1.0 because we never pass it through the __call__ argument at pipeline :304.",
"scheduler_class=DDPMScheduler is forced unconditionally (mva_views.py:243). Vendor default is scheduler=None → scheduler_class=None → ShiftSNRScheduler preserves the base pipeline's own class (scheduling_shift_snr.py:68-69), i.e. EulerDiscrete for stock SDXL. We have never run the vendor's own default configuration, and cannot select LCM.",
"guidance 3.0 / steps 50 (mva_texture_stage.py:70) match the vendor defaults for ig2mv (inference_ig2mv_sdxl.py:229-230) — correct, but they were tuned for Objaverse props on stock SDXL. They are held frozen by policy comment at mva_texture_stage.py:67-70, which means no photoreal sweep has ever been run.",
"enable_vae_slicing() is never called (mva_views.py:225-260); both vendor scripts call it (inference_ig2mv_sdxl.py:74). VRAM-only, but at 1024x6 views on a 32GB card it is free headroom.",
"ControlNet is NOT available on the path we use — stating this so it is not chased. pipeline_mvadapter_i2mv_sdxl.py has ZERO controlnet references; controlnet_image / controlnet_conditioning_scale exist only in pipeline_mvadapter_t2mv_sdxl.py:159-160 (wired at :478-491, :559-566), driven by scripts/inference_scribble2mv_sdxl.py:65-67 with xinsir/controlnet-scribble-sdxl-1.0. Adding ControlNet to ig2mv would be a source change, not a flag."
],
"newer_alternatives": [
"Hunyuan3D-Paint (Hunyuan3D-2.1, open-sourced 2025-06-13 with full weights AND training code) generates PBR channels natively — albedo + roughness + metallic multi-view, not just RGB — which is the structural fix for 'lighting baked into basecolor'. MV-Adapter ig2mv only ever emits shaded RGB. https://github.com/Tencent-Hunyuan/Hunyuan3D-2.1 and https://arxiv.org/html/2506.15442v1",
"Hunyuan3D 3.5 is reported at up to 8K PBR textures and 2M+ polygons, sub-60s — a resolution class MV-Adapter's 768 cannot reach. UNVERIFIED vendor-adjacent listing, not the GitHub repo; check weights and licence before committing. https://www.3daistudio.com/Models/Hunyuan3D-3-0",
"UniTEX (CVPR 2026, open source) abandons the multi-view-then-project approach entirely: it predicts continuous volumetric Texture Functions from image+geometry via a transformer Large Texturing Model, explicitly to fix the two failures our route has — multi-view misalignment producing blurred/conflicting regions, and unseen/occluded areas. https://github.com/YixunLiang/UniTEX",
"RomanTex — 3D-aware rotary positional embedding multi-attention for texture synthesis; same family as MV-Adapter's decoupled attention but with explicit 3D-aware positional encoding across views. https://arxiv.org/pdf/2503.19011",
"MuMA — multi-channel multi-view PBR texturing with agentic post-processing; generates material channels jointly rather than RGB-then-delight. FlexPainter (https://arxiv.org/pdf/2506.02620) is a sibling, positioned directly against the MV-Adapter projection approach. https://arxiv.org/html/2503.18461v1",
"Base-model refresh (cheapest upgrade, KEEPS MV-Adapter): the adapter is base-agnostic across SDXL derivatives, so any current photoreal SDXL finetune drops in via --base_model. I did NOT verify which specific 2026 SDXL photoreal checkpoint is best or its licence — needs a HuggingFace check before you pick. No MV-Adapter port to FLUX or Qwen-Image exists as of this search; the repo is still SDXL/SD2.1 only. https://github.com/huanngzh/MV-Adapter"
],
"top_recommendation": "Stop running stock SDXL base. mva_views.py:240 hardcodes stabilityai/stable-diffusion-xl-base-1.0, and that checkpoint's prior IS the smooth-plastic-doll skin you are seeing — the adapter is faithfully reproducing what its base model knows. MV-Adapter is explicitly base-agnostic (\"Any SDXL Model\", GitHub title bar): pipe = MVAdapterI2MVSDXLPipeline.from_pretrained(base_model) at scripts/inference_ig2mv_sdxl.py:43 takes any diffusers-format SDXL derivative, and README:194 ships the photoreal recipe verbatim (--base_model \"stablediffusionapi/real-dream-sdxl\" --scheduler ddpm). Concrete change: add --base-model, --unet-model, --lora-model and --lora-scale to mva_views.py (mirroring inference_ig2mv_sdxl.py:212-220 and the multi-LoRA loader at inference_t2mv_sdxl.py:57-64/:96), point base at a photoreal SDXL finetune, and stack a skin/pore detail LoRA. In the SAME edit, three one-line fixes that all attack the same symptom: (1) drop char_pipeline_from_ladder.py:250 from --res 1024 to 768 — the adapter is a 768 model (config tag r768-ortho-nv6, README:11) and 1024 is off-distribution mush; (2) plumb mv_scale (pipeline :304) and expose it — at the default 1.0 the multi-view attention at attention_processor.py:724-725 averages exactly the high-frequency pore/freckle content you want, so sweep 0.7-1.0; (3) plumb control_conditioning_factor (pipeline :308, gate at :735) and set it ~0.6-0.8 so the geometry G-buffer releases the last ~20 steps and lets skin microdetail synthesise unclamped. Then add the vendor's alpha-bbox crop to load_reference (mva_views.py:283-291 vs inference_ig2mv_sdxl.py:96-100) so the face actually fills the training framing. Separately and in parallel: MV-Adapter emits shaded RGB only and has no PBR head at all — the \"lighting baked into basecolor\" complaint is structural to this tool, so evaluate Hunyuan3D-2.1's Paint stage (native albedo/roughness/metallic) as the delighting route.",
"evidence_note": "VERIFIED BY READING VENDOR SOURCE over WSL (wsl.exe -e cat/sed/grep against /root/mv3d/MV-Adapter): the full 38-parameter __call__ signature of pipeline_mvadapter_i2mv_sdxl.py:268-312 and its body at :600-605, :650-676, :686-698, :732-737; _init_custom_adapter at :878-937; attention_processor.py DecoupledMVRowColSelfAttnProcessor2_0 at :380-452, :516-566, :724-728 (mv_scale/ref_scale application and the math.isqrt square-latent constraint at :562); scheduling_shift_snr.py from_scheduler :57-84; loaders/custom_adapter.py in full; camera.py get_camera :152-190 and get_orthogonal_camera :194-223; geometry.py function list; all five relevant scripts/ entry points (inference_i2mv_sdxl.py, inference_ig2mv_sdxl.py, inference_ig2mv_partial_sdxl.py, inference_scribble2mv_sdxl.py, texture_i2tex.py) read end to end; pipeline_texture.py ModProcessConfig :43-50, __init__ :60-73, __call__ :136-172; both training YAMLs read in full (the 768 finding); README lines 1-60, 160-240, 330-440. Local weight inventory confirmed by ls: only i2mv + ig2mv SDXL safetensors present; partial/tg2mv/SD2.1 absent. OUR SIDE: mva_views.py read in full; mva_texture_stage.py :55-214; char_pipeline_from_ladder.py:248-250; grep across repo for call sites. NOT VERIFIED — treat as unconfirmed: (a) I ran NO configs, so all quality claims about base-model swap, mv_scale<1.0, control_conditioning_factor<1.0, prompt_2 and original_size are mechanism-grounded predictions, not measured results — they need an A/B sweep on your box. (b) I did NOT verify which specific 2026 photoreal SDXL checkpoint is best or that any is commercially licensed; real-dream-sdxl is cited only because README:194 names it. (c) Hunyuan3D 3.5's \"8K PBR / 2M poly / sub-60s\" comes from a third-party listing (3daistudio.com), NOT Tencent's repo — unconfirmed, and I did not check Hunyuan3D's licence against your commercial-use bar. (d) I did not verify SD2.1-variant behaviour, nor that ig2mv_partial/tg2mv weights still resolve at the README URLs. (e) The ControlNet-absent-from-i2mv claim is a NEGATIVE grep result (zero hits in pipeline_mvadapter_i2mv_sdxl.py vs hits at t2mv :159-160/:478-491/:559-566) — reliable, but an absence, listed precisely so nobody invents a --controlnet flag for ig2mv. (f) mvadapter/utils/mesh_utils remains nvdiffrast-tainted exactly as your module docstring documents; I confirmed TexturePipeline.__init__ instantiates NVDiffRastContextWrapper at pipeline_texture.py:62, so that class is blocked for you today — its parameters are listed as capability intelligence, not as something callable now. I did not modify any file."
},
{
"tool": "2D plate generators: Qwen-Image-Edit-2511 (stage B) + SeedVR2 (stage A), both via native ComfyUI nodes at D:/comfyui/ComfyUI",
"verdict": "USING_MINIMALLY",
"features_UNUSED": [
"image2 / image3 on TextEncodeQwenImageEditPlus — D:/comfyui/ComfyUI/comfy_extras/nodes_qwen.py:64-65 (schema), :73 (execute sig), :80-101 (loop appends a VL image AND a ref latent per slot). Three simultaneous identity references. The vendor's own 2511 template wires image1+image2+image3 into BOTH encoders (templates/image_qwen_image_edit_2511.json, nodes 149 and 151). Feed face plate + a second view + a control/pose map for consistent turnarounds.",
"TextEncodeQwenImageEdit (the NON-Plus node) — nodes_qwen.py:10-50. Its VL image budget is total=1024*1024 (:34) versus Plus's total=384*384 (:83). For a SINGLE reference that is 7.1x more identity pixels reaching the Qwen2.5-VL encoder. Plus buys slots 2-3 by paying a 7x identity-resolution tax we never asked for.",
"ReferenceLatent (display 'Set Reference Latent') — comfy_extras/nodes_edit_model.py:6-27. Appends {'reference_latents': [latent]} with NO resize and NO megapixel clamp (:26), and its own description says 'you can chain multiple to set multiple reference images'. This is the documented escape hatch from the Plus node's hard 1024x1024 ref-latent clamp (nodes_qwen.py:92-98).",
"FluxKontextMultiReferenceLatentMethod, display name 'Edit Model Reference Method' — comfy_extras/nodes_flux.py:153-165, options ['offset','index','uxo/uno','index_timestep_zero']. It IS consumed by Qwen: comfy/model_base.py:2267-2269 sets ref_latents_method, and comfy/ldm/qwen_image/model.py:464-468 branches on it. The vendor 2511 template sets index_timestep_zero on both branches. Governs how multiple refs are packed once image2/image3 are in use.",
"CFGNorm's pre_cfg boolean — comfy_extras/nodes_cfg.py:60-70 (schema, optional, default False) and :79-97 (implementation). pre_cfg=True rescales the combined noise BEFORE the CFG combine with no clamp, so it can AMPLIFY; our default path (:99-106) clamps scale to max=1.0 and can only attenuate. Directly relevant to flat/low-contrast micro-detail.",
"CFGZeroStar — comfy_extras/nodes_cfg.py:24-49, a post-CFG optimized-scale correction (CFG-Zero-star). Composable with CFGNorm; never loaded.",
"LoraLoaderModelOnly — nodes.py:740; Qwen LoRA key remapping is implemented at comfy/lora.py:319-323. Real edit-line LoRAs the vendor ships templates for: Qwen-Image-Edit-2509-Relight.safetensors (templates/image_qwen_image_edit_2509_relight.json) — a RELIGHT LoRA, which is the direct attack on 'lighting baked into basecolor' — and Qwen-Image-Edit-2511-Lightning-4steps-V1.0-bf16. models/loras contains only flores_style_v1.safetensors.",
"ModelPatchLoader + QwenImageDiffsynthControlnet — comfy_extras/nodes_model_patch.py:228-241 and :547-582. Signature: (model, model_patch, vae, image, strength FLOAT -10..10 default 1.0, optional mask MASK). Patch files named in templates/image_qwen_image_controlnet_patch.json: qwen_image_depth_diffsynth_controlnet.safetensors, qwen_image_canny_..., qwen_image_inpaint_... . D:/comfyui/ComfyUI/models/model_patches is EMPTY.",
"ControlNetLoader + ControlNetApplyAdvanced with Qwen-Image-2512-Fun-Controlnet-Union-2602.safetensors (union: canny/depth/pose) — templates/image_qwen_Image_2512_controlnet.json, source https://huggingface.co/alibaba-pai/Qwen-Image-2512-Fun-Controlnet-Union . Its own note says 'You can replace this section with other image preprocessors, such as OpenPose, or depth preprocessors.' models/controlnet is EMPTY. This is the real mechanism for pose-locked front/side/back turnarounds.",
"SeedVR2 multi-frame input. SeedVR2Preprocess accepts a 4-D IMAGE frame sequence and consumes it as ONE video — comfy_extras/nodes_seedvr.py:112-114 ('Comfy video components arrive as a 4-D IMAGE frame sequence (frames,H,W,C). SeedVR2 consumes that as one video'), padded to 4n+1 by cut_videos (:75-90). Feeding front/side/back (or all 7 ages) as one batch gives temporally-consistent detail synthesis ACROSS views. We submit one image per run.",
"SeedVR2TemporalChunk / SeedVR2TemporalMerge — nodes_seedvr.py:423-514 and :517-599. chunking_mode DynamicCombo 'auto' (predicts largest chunk that fits free VRAM, :480-490) or 'manual' with frames_per_chunk (4n+1), plus temporal_overlap crossfaded with a Hann window at merge (:416-420). This is what makes a large multi-frame SeedVR2 pass fit in 32 GB.",
"Alpha passthrough. SeedVR2PostProcessing splits and re-applies an alpha channel from the reference — nodes_seedvr.py:180-183 and :215-218 — and the vendor template feeds it via JoinImageWithAlpha. A matted plate would survive the upres. Our caller hard-converts to RGB (seedvr2_refine.py:266).",
"EmptyQwenImageLayeredLatentImage — nodes_qwen.py:109-130 (width/height/layers/batch_size, latent shape [B,16,layers+1,H/8,W/8]). Drives the Qwen-Image-Layered model (qwen_image_layered_bf16.safetensors + qwen_image_layered_vae.safetensors, templates/image_qwen_image_layered.json), which emits separated RGBA layers — a free, exact subject matte instead of a downstream background-removal guess.",
"ImageScaleToTotalPixels (comfy_extras/nodes_post_processing.py:229), ImageScaleToMaxDimension (comfy_extras/nodes_images.py:668), ResizeImageMaskNode (nodes_post_processing.py:438). The vendor Qwen control graphs scale with ImageScaleToTotalPixels at 1.68 MP and ResizeImageMaskNode at 1.6 MP — i.e. ABOVE the 1.05 MP ceiling that FluxKontextImageScale imposes on us."
],
"what_we_pass": [
"harness/asset_factory/stage_b_body_base.py:138-139 — TextEncodeQwenImageEditPlus(clip, prompt, vae, image1) only. image2 and image3 never set.",
"stage_b_body_base.py:140-141 — the NEGATIVE TextEncodeQwenImageEditPlus gets clip+prompt+vae but NO image at all, so the negative branch carries zero reference_latents while the positive carries one.",
"stage_b_body_base.py:137 — FluxKontextImageScale on the reference. This node is the only thing setting output resolution in the whole graph.",
"stage_b_body_base.py:134 — ModelSamplingAuraFlow(shift=3.1). Matches the vendor template (node default is 1.73, nodes_model_advanced.py:149); this one is correct.",
"stage_b_body_base.py:135 — CFGNorm(model, strength=1.0). pre_cfg never passed, so it silently takes the attenuate-only post-CFG path.",
"stage_b_body_base.py:145-149 — KSampler(seed, steps=40, cfg=4.0, euler, simple, denoise=1.0), latent_image from VAEEncode of the Kontext-scaled reference (node 14, :144).",
"stage_b_body_base.py:131 — UNETLoader(qwen_image_edit_2511_bf16.safetensors, weight_dtype='default'). No LoRA, no ControlNet, no ModelPatch anywhere in the graph.",
"harness/asset_factory/seedvr2_refine.py:126-156 — LoadImage -> ImageScaleBy(lanczos, scale_by=2.0) -> SeedVR2Preprocess -> VAEEncodeTiled(512/128/4096/8) -> SeedVR2Conditioning -> KSampler(steps=1, cfg=1.0, euler/simple, denoise=1.0) -> VAEDecodeTiled -> SeedVR2PostProcessing('lab') -> SaveImage.",
"seedvr2_refine.py:346 — argparse --model default is '3b' (seedvr2_3b_int8_convrot).",
"seedvr2_refine.py:266 — Image.open(path).convert('RGB'), so any alpha matte is destroyed before the model sees it."
],
"misconfigurations": [
"DEAD RESOLUTION KNOB, and it is the whole ballgame. stage_b_body_base.py:129 assigns `n = CFG[\"size\"]` (1328) and `n` is never referenced again — I grepped the file, line 129 is the only occurrence. Output resolution is therefore decided entirely by FluxKontextImageScale, which snaps to the nearest PREFERRED_KONTEXT_RESOLUTIONS entry (comfy_extras/nodes_flux.py:105-123, :145-147), all of which are ~1.05 MP. MEASURED: every plate in build/3d/ladder_pipeline/*/base/*_body_bald_*.png is exactly 672x1568. A full body at 672x1568 puts the head at roughly 220 px and the face at roughly 130 px wide. There is no pore or freckle information in that plate for TRELLIS to reconstruct.",
"WE THROW AWAY 61% OF STAGE A BEFORE THE MODEL SEES IT. MEASURED: stage-A upres output is 996x2728 (2.72 MP) at build/3d/ladder_pipeline/A14/upres/A14_face_7bsharp.png. FluxKontextImageScale lanczos-downsamples that to 672x1568 (1.05 MP) at stage_b:137, and the VAEEncode that becomes the start latent (:144) reads the DOWNSAMPLED image, not the original.",
"IDENTITY IS ENCODED AT THUMBNAIL SCALE. nodes_qwen.py:83-90: the Plus node rescales every input image to total = 384*384 = 147,456 px for the Qwen2.5-VL branch. At our 0.365 aspect that is roughly 232x635 — the face the VL encoder actually reads is about 90 px wide. The non-Plus TextEncodeQwenImageEdit uses 1024*1024 for the same branch (nodes_qwen.py:34).",
"THE FULL BODY INHERITS THE FACE PLATE'S ASPECT RATIO. nodes_flux.py:145-146 picks the resolution whose aspect is closest to the INPUT image's. Our input is a 0.365-aspect face plate, so every generated full body is forced into the extreme 672x1568 corner of the table rather than a body-appropriate frame.",
"ASYMMETRIC CFG. Our positive encoder carries a reference latent, our negative (stage_b:140-141) carries none. The vendor 2511 template feeds identical image1/2/3 into BOTH encoders (image_qwen_image_edit_2511.json nodes 149 and 151, both routed through Edit Model Reference Method). At cfg 4.0 the guidance delta is being computed against an unconditioned-on-reference negative.",
"SEEDVR2 RUNS THE SMALL WEIGHT BY DEFAULT, against the standing largest-weight law. seedvr2_refine.py:346 defaults --model to '3b' (3.46 GB int8), while seedvr2_7b_sharp_fp16.safetensors (15.35 GB, the sharp tune at full precision) is ALREADY PRESENT in D:/comfyui/ComfyUI/models/diffusion_models. The file's own comment at :83-86 says fp16-sharp 'is what this stage must use'; the default contradicts it.",
"SEEDVR2 SCALE IS HALF THE VENDOR'S. Both installed vendor image templates (utility_seedvr2_3b_int8_upscale_image.json and utility_seedvr2_7b_int8_upscale_image.json) set ResizeImageMaskNode to 'scale by multiplier' = 4. seedvr2_refine.py:347 defaults --scale to 2.0. SeedVR2 synthesizes texture at the scaled resolution, so 2x buys half the top-octave energy 4x would.",
"COLOR CORRECTION DIVERGES FROM THE VENDOR TEMPLATE, and it is the one knob that can pull synthesized detail back toward the dull source. Both vendor templates set SeedVR2PostProcessing to 'none'; seedvr2_refine.py:348 defaults to 'lab'. Separately, DIFFUSION_MODELS has a duplicate '7b-fp16' key at seedvr2_refine.py:87 and :88 — harmless (identical values) but the dict silently drops one."
],
"newer_alternatives": [
"Qwen-Image-2.0 — released 2026-02-10 per the official repo README (https://github.com/QwenLM/Qwen-Image). Unifies Qwen-Image and Qwen-Image-Edit into ONE 7B model with native 2K resolution, ~1000-token prompts, and reportedly #1 on AI Arena for both t2i and editing despite being 3x smaller (https://en.tmtpost.com/news/7874148). CAVEAT I COULD NOT RESOLVE: search results describe it as available for API testing on Alibaba Cloud BaiLian; I did NOT confirm open weights on HF, and there is no ComfyUI template for it in the installed comfyui_workflow_templates 0.11.23. Verify weight availability before planning around it.",
"Qwen-Image-2512 — released 2025-12-31, the T2I refresh with 'enhanced human realism, finer natural detail'. Already templated on this box: templates/image_qwen_Image_2512.json referencing qwen_image_2512_fp8_e4m3fn.safetensors (https://docs.comfy.org/tutorials/image/qwen/qwen-image-2512). Note the 2511 EDIT model we run is Dec 23 2025 — there is no Edit-2512; 2511 IS the current edit head of the 1.x line.",
"Qwen-Image-2512-Fun-Controlnet-Union-2602 (alibaba-pai) — a union controlnet dated 2602 i.e. Feb 2026, supporting canny/depth/pose. https://huggingface.co/alibaba-pai/Qwen-Image-2512-Fun-Controlnet-Union . Templated locally at templates/image_qwen_Image_2512_controlnet.json.",
"FLUX.2 [klein] image-edit, Apache-2.0 — six installed templates including image_flux2_klein_image_edit_9b_base.json and image_flux2_klein_9b_kv_image_edit.json. Named alongside Qwen-Image-Edit as the current recommendation for consistent characters (https://www.bentoml.com/blog/a-guide-to-open-source-image-generation-models).",
"Other image-edit models newer than our 2511 with templates ALREADY installed here (strong signal they are natively supported): image_chrono_edit_14B.json, image_longcat_image_edit.json, image_boogu_image_0_1_edit.json, Image_capybara_v0_1_image_edit.json, image_firered_image_edit1_1.json, image_mage_flow_edit_int8.json, image_joyai_image_edit.json. Each is worth a single-plate bakeoff against 2511 before more pipeline is built on 2511.",
"SeedVR2 is still the head of its class — 'SeedVR2: One-Step Video Restoration via Diffusion Adversarial Post-Training', ICLR 2026 (https://github.com/ByteDance-Seed/SeedVR). I searched specifically for a SeedVR3 and found none. Our stage A model choice is right; only its configuration is wrong."
],
"top_recommendation": "Break the 1.05 MP ceiling on the stage-B plate — it is the single mechanical cause of the plastic-doll read, and it is three line edits. Today every body plate is 672x1568 (measured) because FluxKontextImageScale (stage_b_body_base.py:137) is the only resolution authority and CFG[\"size\"]=1328 is dead code (:129, `n` never used). Replace it with ImageScaleToTotalPixels (comfy_extras/nodes_post_processing.py:229) at ~1.7 MP in a BODY aspect (e.g. 1104x1472 or 1056x1584 from Qwen's own native table in templates/image_qwen_Image_2512.json), which is what the vendor's own Qwen control graphs use (1.68 MP). Then stop feeding identity through the 384x384 keyhole: either swap TextEncodeQwenImageEditPlus for TextEncodeQwenImageEdit (nodes_qwen.py:10-50, 1024x1024 VL budget = 7.1x the identity pixels) for the single-reference case, or keep Plus and append the FULL-RESOLUTION 996x2728 plate as an unclamped reference via ReferenceLatent (nodes_edit_model.py:6-27), which has no megapixel clamp. Also mirror the vendor and feed the same images into the negative encoder (:140-141). On the SeedVR2 side, flip two defaults to match the vendor and the standing largest-weight law: --model 7b-sharp-fp16 (already on disk at 15.35 GB) and --scale 4 (both vendor image templates use 4, we use 2). Expected effect on the mesh: TRELLIS/MV-Adapter cannot reconstruct pore or freckle geometry that was never in the plate — a face rendered at ~130 px cannot produce anything but smooth plastic, and every one of these changes is upstream of that.",
"evidence_note": "VERIFIED BY READING REAL VENDOR SOURCE on this box (D:/comfyui/ComfyUI): nodes_qwen.py (Plus image1/2/3 schema at :64-65, execute at :73, the 384*384 VL clamp at :83 and the 1024*1024 ref-latent clamp at :92-98, and the non-Plus node's 1024*1024 at :34); nodes_seedvr.py (all five node schemas, the 4-D-as-video path at :112-114, chunking at :423-514, merge at :517-599, alpha at :180-183/:215-218); nodes_cfg.py (CFGNorm pre_cfg at :60-70/:79-97, CFGZeroStar at :24-49); nodes_edit_model.py (ReferenceLatent, unclamped, :6-27); nodes_flux.py (FluxKontextImageScale :126-148, the resolution table :105-123, FluxKontextMultiReferenceLatentMethod :153-165); nodes_model_advanced.py (ModelSamplingAuraFlow :145-156); nodes_model_patch.py (ModelPatchLoader :228-241, QwenImageDiffsynthControlnet :547-582); comfy/model_base.py:2247-2277 and comfy/ldm/qwen_image/model.py:459-480 proving Qwen actually consumes ref_latents_method. VERIFIED BY READING INSTALLED VENDOR TEMPLATES (comfyui_workflow_templates_json 0.1.23): I parsed the JSON including subgraph definitions and read the link graph, which is how I know the 2511 template wires image1+image2+image3 into BOTH encoders (nodes 149/151) and sets index_timestep_zero on both, and that both SeedVR2 image templates use 4x and color-correction 'none'. VERIFIED BY MEASUREMENT: PIL-read dimensions — stage A 996x2728, stage B 672x1568 — and `ls` of models/diffusion_models confirming seedvr2_7b_sharp_fp16.safetensors is present while models/model_patches and models/controlnet are empty. WEB-CHECKED: Qwen release timeline and Qwen-Image-2.0 (Feb 2026), SeedVR2 currency/no SeedVR3, current character-consistency recommendations. NOT VERIFIED, stated as such: (a) whether Qwen-Image-2.0 has OPEN WEIGHTS — search indicated BaiLian API availability and I found no HF weight repo or ComfyUI template, so treat it as API-only until checked; (b) I did NOT run anything — no graph in this report has been executed, so the resolution/quality gains are mechanical arguments from the source, not measured deltas; (c) I did not benchmark whether pre_cfg=True, color-correction 'none', or 4x SeedVR2 actually improves OUR plates — each needs a bench cell; (d) I found no explicit minimum/recommended input resolution for SeedVR2 in the node source or the HF card, so I make no claim about a shorter-edge floor; (e) I did not verify that the DiffSynth depth/canny patches or the 2512 Fun union controlnet are compatible with the EDIT 2511 model specifically — the vendor templates pair them with the base Qwen-Image / 2512 T2I models, so that pairing needs a compatibility test before it is planned on. Also flagged for the record: seedvr2_refine.py:87-88 contains a duplicate '7b-fp16' dict key."
},
{
"tool": "PHOTOREAL FINISHING PATH (de-light / PBR decomposition / micro-detail / UE5 skin) — research lane",
"verdict": "USING_MINIMALLY",
"features_UNUSED": [
"STRUCTURAL GAP, NOT A FLAG — TRELLIS.2 CANNOT EMIT A NORMAL MAP. The shipped PBR layout is exactly base_color(0:3)/metallic(3:4)/roughness(4:5)/alpha(5:6) (D:/assetgen/TRELLIS2_code/trellis2/pipelines/trellis2_image_to_3d.py:73-76), and to_glb builds a trimesh PBRMaterial with ONLY baseColorTexture + metallicRoughnessTexture (o-voxel/o_voxel/postprocess.py:296-304). Verified on disk: zero tangent-space normal maps exist anywhere in build/3d for any of the 31 quarantined GLBs; the only *_NRM* files are OBJECT-space passes cut for the de-light fit. With no normal map, geometry is the ONLY carrier of pores/freckles/wrinkles — and we then decimate it. This alone explains 'smooth plastic'. (The dataset code lists an 'emissive' attr at trellis2/datasets/sparse_voxel_pbr.py:111 that the shipped 6-channel layout drops.)",
"Trellis2TexturingPipeline.run(mesh, image, seed=42, tex_slat_sampler_params={}, preprocess_image=True, resolution=1024, texture_size=2048) — trellis2/pipelines/trellis2_texturing.py:375-384. Re-textures an EXISTING mesh, and postprocess_mesh (:299-300) REUSES that mesh's own UVs when present. This is the retopo-then-retexture door: fix geometry once, then spend the whole budget on surface, in OUR UV layout. We wrote a runner for it (build/3d/bakeoff/native_tex/run_trellis2_texture_refine.py) and there are ZERO run records for it on disk.",
"o_voxel.postprocess.to_glb(mesh_cluster_threshold_cone_half_angle_rad=np.radians(90.0), mesh_cluster_refine_iterations=0, mesh_cluster_global_iterations=1, mesh_cluster_smooth_strength=1) — postprocess.py:28-31, consumed by mesh.uv_unwrap at :201. Four UV chart-clustering knobs no caller of ours has ever touched. refine_iterations=0 is the vendor's cheapest setting; raising it and tightening the 90-degree cone raises effective TEXEL DENSITY on the face at the same texture_size, and reduces the seams that cross it.",
"Marigold Intrinsics (local, diffusers, fp16, runs in <1 s/image on far weaker cards): MarigoldIntrinsicsPipeline with prs-eth/marigold-iid-appearance-v1-1 -> albedo + ROUGHNESS + METALLICITY, and prs-eth/marigold-iid-lighting-v1-1 -> albedo + diffuse shading + non-diffuse residual under I = A*S + R. Params ensemble_size>=3, num_inference_steps, output_uncertainty=True (albedo uncertainty comes back as RGB, higher in shaded regions — a free per-texel confidence mask for the de-light). https://huggingface.co/docs/diffusers/main/en/using-diffusers/marigold_usage . This is a genuine replacement for our SH-2 fit AND the first honest roughness map we would have.",
"StableDelight — https://github.com/Stable-X/StableDelight — removes SPECULAR reflections, and the authors state explicitly it works on 3D model textures, not just photos. torch.hub loadable; sibling StableNormal (SIGGRAPH Asia 2024) at https://github.com/Stable-X/StableNormal gives sharp monocular normals. Complementary to Marigold: Marigold removes shading, StableDelight removes the highlight.",
"Adobe Substance 3D Sampler 'Image to Material (AI Powered)' — generates Normal, Height and Roughness from one image AND runs its own delighting pass internally, so the separate 'Delight (AI Powered)' filter is not needed with it. https://substance3d.adobe.com/documentation/sadoc/image-to-material-215286041.html and https://substance3d.adobe.com/documentation/sadoc/delight-ai-powered-172823952.html . Paid subscription — listed because it is the fastest known path to a normal+height from our existing basecolor, not because it is free.",
"SKAP by Texturing.XYZ (public March 2026) — https://skap.texturing.xyz/ . Takes exactly what we already have: an OBJ head + a 2K-8K diffuse OR displacement map (+ optional ID mask for non-skin), and returns 16K DISPLACEMENT, CAVITY, MULTI-CHANNEL displacement (low/mid/high frequency split across RGB), NORMAL maps for UE, plus HAEMOGLOBIN and MELANIN maps. Melanin is the freckle channel and haemoglobin is the ear/nose/knuckle flush that reads as living skin. Credit-based, ~$40 for the base displacement+cavity set, +5-15 credits for normal/haemo/melanin. Reported at https://www.cgchannel.com/2026/03/texturing-xyzs-skap-takes-your-skin-textures-to-the-next-level/ . This is the single most direct answer to 'no pore/freckle detail'.",
"MetaHuman 'From Custom Mesh' tool — https://dev.epicgames.com/documentation/metahuman/metahuman-creator-from-custom-mesh-tool-in-unreal-engine . Epic's docs name AI-GENERATED MESHES as a supported input class. Output: a fully rigged MetaHuman on standard MetaHuman topology that preserves the source mesh's proportions, in A-pose, with optional DNA export. That is retopo + UV + rig + the MetaHuman photoreal skin material in one free step under the standard UE licence (free under $1M revenue, https://www.metahuman.com/license). MetaHuman 5.7 (March 2026) added volumetric 'Estimate Joints from Mesh' and extended the conformable height range — https://www.metahuman.com/news/metahuman-5-7-brings-major-improvements-to-body-conforming-with-more-to-come . TWO NAMED RISKS FROM EPIC'S OWN PAGE, do not discover these late: 'fantasy features (pointed ears, heavy brow ridges) solve with varying quality', and oversized-head/small-body child proportions are called out as suboptimal; MetaHuman Creator still ships no child presets. Our ladder is ages 7-18 with pointed elven ears — this needs a measured test on A11 before it is adopted, not an assumption.",
"UE Subsurface Profile asset, exact parameter names we set none of — Burley Normalized: 'Surface Albedo', 'Mean Free Path Color', 'Mean Free Path Distance', 'World Unit Scale', 'Enable Burley'. Subsurface Profile: 'Scatter Radius', 'Subsurface Color', 'Falloff Color', 'Boundary Color Bleed'. Transmission: 'Extinction Scale', 'Normal Scale', 'Scattering Distribution', 'IOR', 'Transmission Tint Color'. DUAL SPECULAR: 'Roughness 0' (softer lobe), 'Roughness 1' (tighter lobe), 'Lobe Mix'. CVars r.SSS.Checkerboard, r.SSS.Scale, r.SSS.SampleSet, ShowFlag.VisualizeSSS. https://dev.epicgames.com/documentation/en-us/unreal-engine/subsurface-profile-shading-model-in-unreal-engine . Dual-lobe is the specific thing that makes skin read oily-and-alive rather than matte-and-dry.",
"Substrate is ALREADY ON in our project — r.Substrate=True at C:/dev/Humanity/Humanity/Config/DefaultEngine.ini:31, r.Substrate.ProjectGBufferFormat=0 at :33 — and Epic's Substrate overview states a Substrate Slab carries participating media and uses an assigned Subsurface Profile PER PIXEL when the slab sits at the bottom of the material topology (Subsurface Profiles are not blendable). https://dev.epicgames.com/documentation/en-us/unreal-engine/overview-of-substrate-materials-in-unreal-engine . We have the renderer for photoreal skin switched on and no skin material authored against it.",
"CHORD (Ubisoft La Forge + ETH, SIGGRAPH Asia 2025) — https://github.com/ubisoft/ubisoft-laforge-chord , paper https://arxiv.org/html/2509.09952 . Chain-of-rendering decomposition: predicts basecolor FIRST, then normal (height by integration) using an approximated irradiance map, then recovers roughness and metalness by differentiably re-rendering and minimising the difference from the input. Exactly our problem shape. LICENCE LANDMINE, verified on the repo page: 'Ubisoft Machine Learning License (Research-Only - Copyleft)' — this is the same disqualification class as nvdiffrast, so it is EVALUATION-TIER ONLY and cannot touch a shipped texture. Weights are HF-gated behind a terms click.",
"RGB<->X (Adobe, SIGGRAPH 2024) — https://zheng95z.github.io/publications/rgbx24 , https://arxiv.org/abs/2405.00666 . RGB->X estimates albedo, normal, roughness, metallicity AND lighting; X->RGB re-synthesises a realistic image from full or partial intrinsic channels. The X->RGB direction is a free round-trip VERIFIER: decompose, re-render, compare to the plate. That is the kind of positive control this factory already demands of every other stage and does not yet have for texture.",
"SuperMat (ICCV 2025) — albedo and roughness-metallic estimation on rendered images under UNKNOWN illumination, at interactive rates. https://openaccess.thecvf.com/content/ICCV2025/papers/Hong_SuperMat_Physically_Consistent_PBR_Material_Estimation_at_Interactive_Rates_ICCV_2025_paper.pdf . 'Unknown illumination' is our exact case — we do not know the light the generator hallucinated.",
"Free retopo + detail-transfer stack, no purchase: Blender QuadriFlow quad remesh (built in, https://docs.blender.org/manual/en/latest/modeling/meshes/retopology.html), then Subdivision + Shrinkwrap onto the dense generated mesh, then Multires 'Reshape' + a normal/displacement bake from the 940k-vertex RAW decoded mesh (which our records already hash at run_trellis2_native_pbr_mv.py:284-292) onto the low-poly. This is the honest fallback if MetaHuman conform loses the elven ears."
],
"what_we_pass": [
"--texture-size 2048 (default) -> to_glb(texture_size=2048): build/3d/bakeoff/native_tex/run_trellis2_native_pbr_mv.py:87 and :316-321; single-view twin run_trellis2_native_pbr.py:173",
"--decimation 200000 (default) -> to_glb(decimation_target=200000): run_trellis2_native_pbr_mv.py:88, run_trellis2_native_pbr.py:103",
"remesh_band=1, remesh_project=0 — HARDCODED, not a flag: run_trellis2_native_pbr_mv.py:321 and run_trellis2_native_pbr.py:174",
"remesh=False by default (--remesh opt-in, never used in any landed record): run_trellis2_native_pbr_mv.py:89; every record on disk reads remesh: false",
"aabb=[[-0.5,-0.5,-0.5],[0.5,0.5,0.5]], verbose=True: run_trellis2_native_pbr_mv.py:319-321",
"guidance_strength / steps / pipeline_type / max_num_tokens are exposed as flags but default to None = shipped values (7.5/7.5/1.0, 12 steps, 49152 tokens): run_trellis2_native_pbr_mv.py:91-107, :215-244",
"glb.export(out, extension_webp=True); then only BASECOLOR and METALLICROUGHNESS are pulled off mat and saved — no third map is even looked for: run_trellis2_native_pbr_mv.py:326-346",
"The runner's own HONEST_TIER string already states the finding: 'GENERATOR-NATIVE surface, unretouched. No de-light, no lookdev calibration, no cleanup' — run_trellis2_native_pbr_mv.py:360-364",
"De-light caller that EXISTS but has never been run on this asset class: C:/dev/Humanity/Humanity/Tools/lookdev/delight.py (order-2 SH fit of log-intensity vs object-space normal, GAIN_CLAMP (0.25,4.0), FLAT_FLOOR_STOPS 0.20) — zero DELIGHT records exist beside any of the 31 GLBs in build/3d/bakeoff/native_tex/QUARANTINE_EVAL_ONLY/",
"Trellis2TexturingPipeline: run_trellis2_texture_refine.py exists with --texture-size defaulting to 4096 and --resolution 1024 — and has produced zero records"
],
"misconfigurations": [
"remesh_project=0 is HARDCODED against a vendor default of 0.9. postprocess.py:27 declares remesh_project: float = 0.9 and :177 documents it as 'project_back = remesh_project, # Snaps vertices back to original surface'. Our two runners pass 0 (run_trellis2_native_pbr_mv.py:321, run_trellis2_native_pbr.py:174). The moment anyone uses --remesh, dual-contour rebuilds topology and NEVER projects back to the original surface — a systematically smoothed, slightly shrunken shell. This is a live trap sitting in the one flag that would give us clean quads.",
"decimation_target=200000 against a vendor default of 1000000 (postprocess.py:23). Because the chain emits no normal map, the mesh IS the detail. We are throwing away 80% of the only micro-detail carrier we have, before any bake exists to catch it. Our own records show raw decoded geometry at 940,424 vertices (run_trellis2_native_pbr_mv.py:28 control assertion) collapsing to ~132,689 in the exported GLB.",
"texture_size=2048 for a FULL FIGURE at 166.72 cm. That is roughly 12 texels/cm across the whole body if UVs were perfect, and the vendor's UV clustering is at its coarsest setting (refine_iterations=0), so the face gets a fraction of that. Meanwhile our own texture-refine runner already defaults to 4096 — the two runners disagree with each other and the lower one is the one we actually ran, 31 times.",
"BASECOLOR p02 = 23,24,24 sRGB, measured and written into our own records (QUARANTINE_EVAL_ONLY/RUN_NATIVE_PBR_A14V3_s42.json, maps.BASECOLOR.p02_rgb). This repo's own albedo gate enforces a 30-240 sRGB dielectric band (harness/check_lookdev_albedo.py:45-46, Tools/lookdev/albedo_validator.py). The darkest 2% of texels are BELOW the physical floor for any dielectric. That is baked shadow measured on the record, and it has never been de-lit.",
"ROUGHNESS is authored at mean 159/255 = 0.625, p02 136 = 0.53, p98 233 = 0.91 (RUN_NATIVE_PBR_A14V3_s42.json, maps.METALLICROUGHNESS G channel; MV3 arm reads 149 mean, 109-240). Human skin sits around 0.35-0.50. Everything in that map is chalk-matte, which kills every specular cue — and the specular cue is most of what 'photoreal skin' is. Metallic (B) is 0.0 everywhere, which is correct and the only calibrated thing in the map.",
"delight.py is the WRONG INSTRUMENT for this input class even if we ran it, and it says so itself: 'It is also low-frequency by construction: it cannot remove a hard cast shadow edge, and it does not claim to' (Tools/lookdev/delight.py:30-31). Generator basecolor carries hard terminators and contact shadows under the chin, in the eye sockets, under the hair. An order-2 SH fit cannot touch those. A diffusion IID model (Marigold-IID) can.",
"The de-light stage is declared MANDATORY by our own ratified standard — docs/LOOKDEV_STANDARD.md ('de-light mandatory', 'a polish claim made under uncalibrated light is not a claim') and harness/asset_factory/delta_e_lens.py:746-747 ('de-light is MANDATORY before a basecolor is albedo; a tile that cannot name its DELIGHT record REFUSES'). Zero of the 31 quarantined GLBs carry a DELIGHT record. The law exists and the stage has never run on the assets it was written for.",
"MV-Adapter's bake emits *_NRM.exr as OBJECT-space normals for the de-light fit only (delight.py:14-17, :77-96). There is no TANGENT-space normal in the chain, so nothing we produce can be plugged into a UE material normal slot without a conversion stage that does not exist."
],
"newer_alternatives": [
"MetaHuman 5.7 (March 2026) — volumetric 'Estimate Joints from Mesh', import-onto-existing-MetaHuman mesh conforming, extended viable height range. Beats hand retopo+rig for time-to-riggable by an order of magnitude, and is free under the UE licence. https://www.metahuman.com/news/metahuman-5-7-brings-major-improvements-to-body-conforming-with-more-to-come",
"SKAP (Texturing.XYZ, public March 2026) — 16K displacement/cavity/multi-channel-displacement/normal/haemoglobin/melanin generated FROM an existing diffuse or displacement map. Beats every generic 'AI skin texture' tool because it takes our actual input and returns UE-ready channels. https://skap.texturing.xyz/ , report at https://www.cgchannel.com/2026/03/texturing-xyzs-skap-takes-your-skin-textures-to-the-next-level/",
"MatPedia (arXiv 2511.16957, 2026) — joint RGB-PBR foundation model, native 1024x1024, one architecture covering text-to-material, image-to-material AND intrinsic decomposition. Beats task-specific decomposers on breadth. NO CODE RELEASE FOUND as of this check — paper-only, do not plan on it. https://arxiv.org/abs/2511.16957",
"ReasonX (Adobe Research, CVPR 2026) — https://github.com/adobe-research/ReasonX . NOT a decomposer: an MLLM 'intrinsic judge' making pairwise relative albedo/irradiance/normal/depth comparisons that is used to IMPROVE a base model, and the paper's own base model is Marigold-IID-Lighting v1.1. Reported to significantly improve Marigold where true albedo is hard to read — i.e. exactly our shadowed-face case. Dual licence (checkpoint adirik/InternVL2_5-4B-Intrinsic-Judge falls under InternVL terms) — read before shipping.",
"DANTE-W (2026) — diffuse albedo neural texturing in the wild; a prior-driven DE-SHADING module that removes baked-in shading using generative shading priors conditioned on geometry AND texture, then back-propagates the reconstruction error to refine the MESH as well. Scene-scale not character-scale today, but it is the only 2026 method found that closes the loop from de-light back onto geometry. https://arxiv.org/html/2606.30677",
"Hunyuan3D 2.1 natively outputs Base Color, Normal, Roughness, Metallic AND AO — the normal map TRELLIS.2 structurally cannot emit. It remains PERMANENTLY DISQUALIFIED on the licence this repo already read directly (Tencent Community Licence sec 1.l Territory excludes EU/UK/South Korea, sec 4.c bans use outside it, sec 1.h defines Output to include generated meshes — build/3d/PIPELINE_RESEARCH_AUG2026.md:118-127). Naming it so no future seat re-proposes it for the normal map."
],
"top_recommendation": "Stop trying to fix this in the texture and fix it in the ORDER OF OPERATIONS. The honest answer to the user's question is YES — generated meshes need retopo plus high-poly detail transfer, not better basecolors, because the chain structurally has no normal map to put detail in (trellis2_image_to_3d.py:73-76 and postprocess.py:296-304 prove TRELLIS.2 emits only basecolor and metallicRoughness, and there are zero tangent-space normals anywhere in build/3d). Run this order, and gate each step: (1) RE-GENERATE at capacity before finishing anything — to_glb(texture_size=4096, decimation_target=1000000), raise mesh_cluster_refine_iterations off 0, and delete the hardcoded remesh_project=0 in favour of the vendor's 0.9; the current 2048/200000 pair is discarding both the texel budget and the geometric detail the whole finishing path would have baked from. (2) DE-LIGHT PROPERLY with MarigoldIntrinsicsPipeline / prs-eth/marigold-iid-lighting-v1-1 (I = A*S + R) at ensemble_size>=5, output_uncertainty=True — our SH-2 delight.py cannot remove the hard cast shadows this input carries and says so at line 30; verify the result against our own 30-240 dielectric band, which the current basecolor FAILS at p02 = 23,24,24. Take roughness from marigold-iid-appearance-v1-1, because the shipped roughness (0.53-0.91) is chalk. (3) RETOPO + RIG via MetaHuman 'From Custom Mesh' — Epic names AI-generated meshes as supported input and returns rigged standard topology preserving proportions, free under the UE licence — but TEST IT ON ONE HEAD FIRST, because Epic's own page says pointed ears and child proportions 'solve with varying quality' and Creator has no child presets, which is two direct hits on an ages-7-18 elven ladder. If it loses the ears, fall back to QuadriFlow + Shrinkwrap + Multires Reshape and bake from the 940k-vertex raw decoded mesh we already hash. (4) DETAIL SYNTHESIS is the step that actually answers 'no pore/freckle detail': SKAP (skap.texturing.xyz) takes the OBJ head plus our existing 2K-8K diffuse and returns 16K displacement, cavity, multi-channel displacement, NORMAL, plus melanin (freckles) and haemoglobin (living flush) — roughly $40-55 a head, 12 heads in the ladder. (5) RE-TEXTURE the retopo'd mesh in OUR uv layout with Trellis2TexturingPipeline.run(mesh, image, texture_size=4096) — trellis2_texturing.py:375-384 reuses the mesh's own UVs at :299-300 — a pipeline we wrote a runner for and have never once executed. (6) SHADE IT: Substrate is already on (DefaultEngine.ini:31); author a Substrate Slab with a Subsurface Profile and set Dual Specular Roughness 0 / Roughness 1 / Lobe Mix plus Burley Mean Free Path Color and Distance. And keep CHORD out of the ship path entirely — its licence is 'Research-Only - Copyleft', the same class as nvdiffrast, which this repo already quarantines.",
"evidence_note": "VERIFIED BY READING VENDOR SOURCE ON THIS BOX: the TRELLIS.2 PBR attr layout with no normal channel (trellis2/pipelines/trellis2_image_to_3d.py:73-76); to_glb's full signature and defaults texture_size=2048, decimation_target=1000000, remesh_project=0.9 and the four mesh_cluster_* UV knobs (o-voxel/o_voxel/postprocess.py:14-34); the PBRMaterial construction proving only two textures are ever built (postprocess.py:296-304); Trellis2TexturingPipeline.run's signature and its reuse of existing mesh UVs (trellis2_texturing.py:287-300, :375-384). VERIFIED BY READING OUR CALLERS: run_trellis2_native_pbr_mv.py:87-89 and :316-321, run_trellis2_native_pbr.py:103 and :174 (remesh_project=0 hardcoded in both), run_trellis2_texture_refine.py, Tools/lookdev/delight.py:1-120 (SH-2 method and its self-declared low-frequency limit), harness/check_lookdev_albedo.py:44-52, docs/LOOKDEV_STANDARD.md, harness/asset_factory/delta_e_lens.py:746-747. VERIFIED BY MEASURING ARTIFACTS ON DISK: the basecolor p02 = 23,24,24 and roughness 136/159/233 figures are read out of the landed run records in QUARANTINE_EVAL_ONLY, not estimated; the absence of DELIGHT records (0 across 31 GLBs) and the absence of any tangent-space normal map in build/3d were confirmed by exhaustive search; r.Substrate=True confirmed at C:/dev/Humanity/Humanity/Config/DefaultEngine.ini:31. VERIFIED BY WEB, Aug 2026: Marigold IID checkpoint IDs and pipeline API (HF diffusers main docs); UE Subsurface Profile parameter names quoted from Epic's 5.8 doc page; MetaHuman From Custom Mesh capabilities AND its pointed-ear/child limitations quoted from Epic's own documentation; MetaHuman 5.7 body-conforming changes from metahuman.com/news; SKAP inputs/outputs/pricing from CG Channel's March 2026 report; CHORD's 'Ubisoft Machine Learning License (Research-Only - Copyleft)' read off the repo page. NOT VERIFIED — TREAT AS UNCONFIRMED: (1) MetaHuman's exact EULA position on shipping in NON-Unreal engines — metahuman.com/license says 'can be used with any engine or creative software' but a March 2026 community source disputes this for shipped products; the FAQ answers did not render, and this must be read in full before any MetaHuman-derived asset is committed to. (2) MatPedia code/weights availability — searched, no GitHub found; paper-only. (3) SKAP's licence terms for game shipping — the pricing is confirmed, the licence is not, and it must be read before purchase. (4) CHORD's VRAM/resolution requirements — not stated on the repo page. (5) I did NOT run any of these tools; every 'this would help' claim about Marigold/StableDelight/SKAP output quality on OUR plates is an inference from vendor documentation and remains unmeasured until a bake-off runs, which is the standing law this factory applies to every other stage. (6) I did not open the MV-Adapter or UniRig trees — other lanes own those."
},
{
"tool": "TRELLIS.2 (microsoft/TRELLIS.2-4B) — vendor D:/assetgen/TRELLIS2_code, weights D:/assetgen/models/TRELLIS2",
"verdict": "USING_PARTIALLY",
"features_UNUSED": [
"Trellis2TexturingPipeline.run(mesh, image, seed, tex_slat_sampler_params, resolution, texture_size) — trellis2/pipelines/trellis2_texturing.py:374-408. A SECOND shipped pipeline that re-derives PBR against an EXISTING mesh via encode_shape_slat() (L183-222), decoupling surface budget from geometry budget. Our wrapper exists at build/3d/bakeoff/native_tex/run_trellis2_texture_refine.py but has NEVER BEEN EXECUTED — zero records containing \"TEXTURE_REFINE\" exist anywhere in the repo. Its weights (shape_enc_next_dc_f16c32_fp16, texturing_pipeline.json) are on disk, loaded by nothing.",
"tex-stage `steps` is UNREACHABLE from our CLI. run_trellis2_native_pbr_mv.py:223-225 writes --steps into ss_params and sh_params ONLY; tx_params never receives it. Texture sampling is therefore permanently pinned at pipeline.json's 12, while FlowEulerCfgSampler's own default is steps=50 (trellis2/pipelines/samplers/flow_euler.py:137). Every face on the ladder was textured in 12 denoise steps.",
"guidance_rescale — samplers/classifier_free_guidance_mixin.py:11, applied L21-28 (std-matched CFG rescale). Shipped: 0.7 structure / 0.5 shape / 0.0 TEXTURE. Our runners expose no flag for it, so texture rescale is 0.0 on every run ever made.",
"guidance_interval: Tuple[float,float] — samplers/guidance_interval_mixin.py:9-13 and flow_euler.py:186. Shipped tex interval is [0.6, 0.9]; outside it guidance_strength is forced to 1. Never passed by any of our runners.",
"rescale_t: float — flow_euler.py sample() signature (timestep-schedule shaping). Shipped 5.0 structure / 3.0 shape / 3.0 texture. Zero occurrences of \"rescale_t\" anywhere in C:/dev/humanity-forgotten.",
"num_samples: int = 1 — trellis2_image_to_3d.py:491, threaded to sample_sparse_structure L542-545. Generates N distinct candidates in ONE load. Never set above 1; every ladder run pays the model-load cost for a single draw.",
"return_latent: bool = False — trellis2_image_to_3d.py:498, returns (shape_slat, tex_slat, res) at L593. Lets you re-decode or re-texture WITHOUT regenerating geometry. Zero occurrences in our repo.",
"mesh.simplify(target, verbose, options) — trellis2/representations/mesh/base.py:71-81 (CuMesh QEM with an `options` dict). The vendor's own example.py:29 calls mesh.simplify(16777216) before to_glb. We never call it; we hand raw 20M–48M-face meshes straight to to_glb's internal decimator.",
"to_glb UV-unwrap quality knobs — o-voxel/o_voxel/postprocess.py:28-31: mesh_cluster_threshold_cone_half_angle_rad, mesh_cluster_refine_iterations (DEFAULT 0 — chart refinement never runs), mesh_cluster_global_iterations, mesh_cluster_smooth_strength. Our to_glb calls (run_trellis2_native_pbr.py:169-174, _mv.py:316-321) pass none of them, so the atlas is unrefined — directly caps usable texel density for pore/freckle detail.",
"Native PBR lookdev renderer: PbrMeshRenderer (trellis2/renderers/pbr_mesh_renderer.py:190,212), EnvMap (L38-63), render_utils.render_video / render_multiview / render_snapshot / make_pbr_vis_frames (trellis2/utils/render_utils.py:80,89,100,109), plus 8 shipped HDRIs in assets/hdri/ (studio, night, forest, city, courtyard, interior, sunrise, sunset). Zero references to \"render_utils\", \"EnvMap\" or \"make_pbr_vis_frames\" in our repo. This is the vendor-native instrument for the exact complaint 'lighting baked into basecolor' — render one asset under studio.exr and night.exr and the baked light shows or it does not.",
"screen_space_ambient_occlusion (pbr_mesh_renderer.py:93), aces_tonemapping (L164), gamma_correction (L183) — shipped display-transform helpers, never used.",
"Full training / finetune stack: train.py (+ --config, --load_dir, --num_gpus, --num_nodes args L101-116), configs/gen and configs/scvae, and data_toolkit/ with encode_pbr_latent.py, encode_shape_latent.py, encode_ss_latent.py, voxelize_pbr.py, render_cond.py, dump_pbr.py. README roadmap marks 'Release training code' as DONE. Nothing in our repo touches it — this is the only route that puts real skin priors into the tex DiT rather than fighting it at inference.",
"tex_enc_next_dc_f16c32_fp16.safetensors sits in D:/assetgen/models/TRELLIS2/ckpts/ and is referenced by NEITHER pipeline.json NOR texturing_pipeline.json. It is the texture ENCODER that data_toolkit/encode_pbr_latent.py needs — a downloaded, paid-for weight doing nothing.",
"Negative conditioning is a hardcoded zeros tensor: get_cond() builds neg_cond = torch.zeros_like(cond) at trellis2_image_to_3d.py:182. The samplers accept neg_cond as a real kwarg (flow_euler.py:136, CFG mixin L11), so a genuine negative image (e.g. a plastic-doll render) is injectable to push AWAY from the CG-doll read. Never attempted."
],
"what_we_pass": [
"pipe.run_multi_image(imgs, seed, preprocess_image=True, mode, sparse_structure_sampler_params, shape_slat_sampler_params, tex_slat_sampler_params, pipeline_type, max_num_tokens) — build/3d/bakeoff/native_tex/run_trellis2_native_pbr_mv.py:245-250",
"guidance_strength (structure+shape) and tex guidance_strength, via --guidance / --tex-guidance — run_trellis2_native_pbr_mv.py:218-222. All 14 ladder records show requested_structure_and_shape: null and requested_texture: null, i.e. never exercised in production.",
"steps → ss_params + sh_params only — run_trellis2_native_pbr_mv.py:223-225 (texture stage excluded, see UNUSED #2)",
"pipeline_type='1536_cascade' and --no-low-vram — harness/asset_factory/char_pipeline_from_ladder.py:274-275",
"max_num_tokens: left at the 49152 default for every ladder run — capacity.max_num_tokens in all 14 QUARANTINE_EVAL_ONLY/A*_record.json",
"o_voxel.postprocess.to_glb(vertices, faces, attr_volume, coords, attr_layout, voxel_size, aabb, decimation_target=200000, texture_size=2048, remesh=False, remesh_band=1, remesh_project=0, verbose=True) — run_trellis2_native_pbr_mv.py:316-321",
"pipe.low_vram = False before .cuda() — run_trellis2_native_pbr_mv.py:176-178",
"Trellis2TexturingPipeline.from_pretrained(weights, config_file='texturing_pipeline.json') + run(mesh, img, seed, tex_slat_sampler_params, resolution, texture_size=4096) — run_trellis2_texture_refine.py:76-94 (WRITTEN BUT NEVER RUN)",
"pipe.run(img, seed, preprocess_image=True) — the stock single-view reference path, run_trellis2_native_pbr.py:149"
],
"misconfigurations": [
"texture_size=2048 on all 14 ladder assets (A7…A18 records) while the vendor's own example.py:44 ships texture_size=4096. We are baking the face atlas at a quarter of the pixel budget Microsoft demonstrates — and our own refine runner already defaults to 4096 (run_trellis2_texture_refine.py:49), so the lane disagrees with itself.",
"decimation_target=200000 vs vendor example.py:43 decimation_target=1000000. A14_face_haired_s42_record.json records raw_faces=48,216,416 decimated to textured_faces=188,205 — a ~256x reduction BEFORE the texture bake. Every ladder face lands in a 188k–201k band; the geometry that would carry pore-scale relief is thrown away at the export line, not by the model.",
"remesh=False on every single run (confirmed across all 29 QUARANTINE records) while example.py:45-47 ships remesh=True, remesh_band=1, remesh_project=0. Our runners expose --remesh but nothing ever set it.",
"max_num_tokens left at 49152 while requesting 1536_cascade. sample_shape_slat_cascade L328-339 SILENTLY steps hr_resolution down by 128 until the token budget fits and only prints 'the resolution is reduced to N'. On a 32GB card the ladder almost certainly never received 1536³ — and the two runs that did raise it (RUN_HEAD_A14_MV3_FULLRES, RUN_HEAD_A14_NATIVEPLATE) used 999999, proving the lane knows the fix and did not apply it to production.",
"Our recorder writes rec['pipeline_type'] = the REQUESTED type (run_trellis2_native_pbr_mv.py:258), never the achieved hr_resolution returned by sample_shape_slat_cascade. So no record on disk can tell you whether any ladder asset actually got 1536³. The capacity claim in char_pipeline_from_ladder.py:33 ('TRELLIS.2 4B, 1536_cascade') is unverifiable from our own evidence.",
"Texture CFG is effectively OFF: pipeline.json ships tex guidance_strength=1.0, and classifier_free_guidance_mixin.py:12 short-circuits to the bare conditional when strength==1. Combined with guidance_rescale=0.0 and 12 steps, the texture stage is the least-driven part of the whole pipeline — and it is the exact stage responsible for the 'smooth plastic, no freckles' complaint.",
"steps=12 shipped for all three stages against the sampler's own default of 50 (flow_euler.py:137). Only two eval runs ever raised it, and even those could not reach the texture sampler."
],
"newer_alternatives": [
"Pixal3D (TencentARC, SIGGRAPH 2026) — MIT licence, open weights on HuggingFace. Its `main` branch is explicitly 'improved implementation based on Trellis.2 backbone with better performance', released May 2026; default pipeline resolution 1536 (1024 low-VRAM), outputs geometry + PBR. Back-projects pixel features into 3D for direct pixel-to-3D correspondence (93.57% IoU on Toys4K reported). This is the strongest SWITCH candidate: same backbone we already run, MIT, no licence problem for a Steam-first solo indie. https://github.com/TencentARC/Pixal3D",
"Hunyuan3D 2.1 (Tencent) — full weights + training code + production PBR (albedo + metallic-roughness), technically excellent. LICENCE BLOCKER, face value: TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT, and 'Territory' explicitly EXCLUDES the EU, UK and South Korea. Non-commercial means it cannot ship in a Steam product. Use for evaluation only or not at all. https://github.com/Tencent-Hunyuan/Hunyuan3D-2.1 and licence discussion https://github.com/Tencent-Hunyuan/Hunyuan3D-2.1/issues/94",
"Hunyuan3D 3.0 / 3.1 Pro — 3D-DiT, 1536 geometry resolution, quad-mesh controls, rigging/animation. NOT open-weights; hosted cloud API only. The open line stops at 2.x. Not locally runnable, so out of scope for this box. https://hunyuan3d.cc/hunyuan3d-versions/",
"SAM 3D Body + SAM 3D Objects (Meta, Nov 2025; SAM 3D Body at CVPR 2026) — single-image full-body mesh on the Momentum Human Rig; MHR itself Apache-2.0, checkpoints under the SAM licence. Not a photoreal-skin tool, but a strong BODY/POSE PRIOR to constrain the age-7→18 proportion ladder that TRELLIS.2 has no anatomical knowledge of. https://github.com/facebookresearch/sam-3d-body",
"Photo3D (arXiv 2512.08535) — structure-aligned multi-view synthesis + detail enhancement, explicitly targets 'realistic appearances' as a layer ON TOP of 3D-native generators, i.e. exactly our failure mode. Claims to generalise across 3D-native paradigms. No weights released as of the arXiv page — WATCH, do not plan on it. https://arxiv.org/abs/2512.08535",
"DreamCharacter-1 (arXiv 2607.07817, July 2026) — lightweight post-adaptation on a 3D foundation backbone: geometry preference optimisation + high-res texture synthesis for occluded regions. Directly the 'production-ready character' problem. No open-weights indication on the abstract page. https://arxiv.org/abs/2607.07817"
],
"top_recommendation": "TRELLIS.2 is still the right engine — do not switch yet — but stop discarding the capacity you already paid for, in this order. (1) Run the texturing pipeline that has never once executed: run_trellis2_texture_refine.py against the ladder geometry you already accepted, at --texture-size 4096 --no-low-vram, with tex_slat_sampler_params carrying steps=50 AND guidance_strength>1 AND guidance_rescale — the last three currently cannot reach the texture stage at all (--steps writes only ss_params/sh_params at run_trellis2_native_pbr_mv.py:223-225, and pipeline.json's tex guidance_strength=1.0 makes classifier_free_guidance_mixin.py:12 short-circuit CFG entirely). This is the single largest untouched lever on 'smooth plastic skin'. (2) Fix the export line: decimation_target 200000→1000000 and texture_size 2048→4096 to match vendor example.py:43-44, and pass to_glb's mesh_cluster_refine_iterations (default 0) so the UV atlas is actually refined — right now 48M raw faces are crushed to 188k and baked into a 2048 atlas with unrefined charts, which alone forbids pore detail. (3) Raise --max-tokens off 49152 for every 1536_cascade run, because sample_shape_slat_cascade L328-339 silently steps resolution DOWN to fit and our recorder never writes the achieved resolution — the ladder's '1536_cascade' claim is currently unverifiable from our own records. (4) Prove or kill the baked-lighting diagnosis with the vendor's own PbrMeshRenderer + EnvMap over the 8 shipped HDRIs before spending another GPU-week. Then, and only then, evaluate Pixal3D's TRELLIS.2-backbone main branch (MIT) as a drop-in geometry/PBR upgrade, and treat train.py + data_toolkit finetuning as the real long-run answer to a face ladder that must look like YOUR approved 2D art rather than the model's prior.",
"evidence_note": "Vendor source read directly on disk, not recalled: trellis2/pipelines/trellis2_image_to_3d.py (full 596 lines), trellis2_texturing.py (full 409), samplers/flow_euler.py, samplers/classifier_free_guidance_mixin.py, samplers/guidance_interval_mixin.py, representations/mesh/base.py, renderers/pbr_mesh_renderer.py (symbol list), utils/render_utils.py (symbol list), example.py, example_texturing.py, README.md, train.py, and the o-voxel to_glb signature at o-voxel/o_voxel/postprocess.py:14-59. Shipped defaults read from D:/assetgen/models/TRELLIS2/pipeline.json and texturing_pipeline.json; checkpoint inventory from ckpts/ ls. Our side read in full: run_trellis2_native_pbr.py, run_trellis2_native_pbr_mv.py, run_trellis2_texture_refine.py, plus greps of char_pipeline_from_ladder.py and trellis2_multiview.py. 'Never used' claims are backed by a repo-wide grep for each symbol (render_utils, EnvMap, make_pbr_vis_frames, simplify(, return_latent, rescale_t, mesh_cluster, tex_enc, texture_size=4096 — all returned ZERO hits; guidance_rescale/guidance_interval/num_samples hit ONLY trellis2_multiview.py, and inspection shows those are docstring/passthrough mentions, not values we set). Actual production parameters were read from the 29 JSON records in build/3d/bakeoff/native_tex/QUARANTINE_EVAL_ONLY/, which is how texture_size=2048, decimation_target=200000, remesh=False, max_num_tokens=49152, and null guidance requests are asserted rather than assumed. 'Texturing pipeline never run' is evidenced by a grep for TEXTURE_REFINE across all repo JSON returning nothing. NOT VERIFIED, flagged honestly: (a) I did NOT execute anything, so the claim that raising tex steps/guidance improves perceived skin is a mechanism argument, not a measured result; (b) I did not read the bodies of PbrMeshRenderer.render or render_utils.render_video, only their signatures and the vendor example's use of them; (c) I did not confirm whether the RTX 5090 can hold 1536³ at a raised max_num_tokens — A14_face_haired already peaked at 41,539 MiB allocated, which EXCEEDS 32GB and implies host-memory spill, so raising token budget may need testing rather than assuming headroom; (d) the achieved hr_resolution of the ladder runs is genuinely unknown because our recorder does not capture it — I inferred likely downgrade from the code path at L328-339, I did not observe the printed message; (e) newer_alternatives rest on live web results from today, including third-party comparison sites for the Pixal3D IoU figure and the Hunyuan licence territory clause — the Hunyuan non-commercial + EU/UK/KR exclusion should be re-read directly in the LICENSE file before any decision, and the Pixal3D 'better performance than TRELLIS.2 backbone' phrasing is the repo's own claim, unbenchmarked by us."
},
{
"tool": "UniRig (D:/assetgen/UniRig, weights D:/assetgen/models/UniRig)",
"verdict": "NOT_USED",
"features_UNUSED": [
"SKIN-ONLY ON A TARGET SKELETON — this is the UE5 Mannequin answer. `generate_skin.sh --data_name raw_data.npz` (launch/inference/generate_skin.sh:10, forwarded run.py:59-60) makes stage 3 read the skeleton from the INPUT FILE'S OWN ARMATURE instead of the AR stage's predict_skeleton.npz. extract.py::process_armature (src/data/extract.py:278-329) harvests joints/tails/parents/NAMES/matrix_local from any FBX/GLB armature, and SkinWriter reads the names straight back out (src/system/skin.py:282). Feed it our mannequin-bound SM_CHAR_0001 FBX and UniRig predicts weights ON the mannequin bones, with UE5 bone names preserved end-to-end. README:197 documents this as the recommended 'edited-version' workflow. No skeleton prediction runs at all — the 47 locomotion clips keep riding.",
"SKELETON+SKIN TRANSFER MODE — `python -m src.inference.merge --source <rigged.fbx> --target <new_mesh.glb> --output <out>` (argparse src/inference/merge.py:424-426, transfer() src/inference/merge.py:429-454, exposed by launch/inference/merge.sh:10-12). Loads the armature AND skin from `source`, then rebinds them onto `target` by cKDTree nearest-neighbour (src/inference/merge.py:282,301-312). One approved mannequin bind can be pushed across the whole ages-7-18 ladder instead of re-solving each.",
"--add_root — creates an explicit 'Root' bone at world origin parented above the hierarchy (argparse src/inference/merge.py:423; used src/inference/merge.py:238-242 and src/data/exporter.py:234-237). UE5 requires a root at origin for root motion. merge.sh never exposes the flag, and generate_skeleton.sh:8,20 parses --add_root then NEVER FORWARDS IT (dead variable) — so it is silently False unless you call the module directly.",
"assign_cls — class-conditioning of the AR skeleton (configs/system/ar_inference_articulationxl.yaml:13; consumed src/model/unirig_ar.py:181,196; token table configs/tokenizer/tokenizer_parts_articulationxl_256.yaml:4-7). Legal values in the shipped checkpoint: `vroid`(0), `mixamo`(1, config comments it 'currently untrained, do not use it'), `articulationxl`(2). We would inherit articulationxl, which yields UNNAMED bone_0..bone_N. Setting `vroid` instead yields the named humanoid skeleton — 22 body bones + 30 finger bones (configs/skeleton/vroid.yaml:4-59) — which is a 1:1 mappable biped, not an anonymous tree. Siblings `no_cls` and `use_dir_cls` also exist (src/model/unirig_ar.py:179-180), plus run.py:57 `--cls`.",
"CUSTOM SKELETON NAME TEMPLATES — OrderConfig.parse (src/data/order.py:24-39) loads ANY yaml exposing `parts` + `parts_order`, and Order.make_names (src/data/order.py:64-77) stamps those names onto the generated bones in order. Adding configs/skeleton/ue5_mannequin.yaml (pelvis, spine_01..., clavicle_l, thigh_l, ...) and registering it under a trained cls key in configs/tokenizer/tokenizer_parts_articulationxl_256.yaml:11-14 AND configs/transform/inference_ar_transform.yaml:10-13 makes UniRig emit mannequin-NAMED bones. Honest limit: this renames/orders, it does not constrain joint positions.",
"CONSTRAINED DECODING HOOK — VocabSwitchingLogitsProcessor (src/model/unirig_ar.py:14-28) already masks every illegal token each step via TokenizerPart.next_posible_token (src/tokenizer/tokenizer_part.py:65-144), a full grammar state machine. This is the exact insertion point to hard-constrain generation to a fixed bone count/topology — the vendor built the plumbing, only the grammar is generic.",
"group_per_vertex — max bone influences per vertex. Exporter's own default is -1 = ALL bones (src/data/exporter.py:332, branch 291-292), but SkinWriter pins it to 4 (src/system/skin.py:300) and merge pins it to 4 (src/inference/merge.py:373). UE5 supports 8 and 12 influences; 4 is what makes shoulders/hips read as previz.",
"reskin() post-process, entirely hardcoded at the call site (call src/system/skin.py:250-259, signature src/system/skin.py:312-325). Exposed but never varied: `sample_method` ('mean' inverse-exp-weighted vs pinned 'median'), `nearest_samples`=7, `iter_steps`=1 (topology-aware weight-diffusion passes along mesh edges — raising this is the direct fix for blotchy weights), `alpha`, `threshold`=0.03. Note the vendor's own `# TODO: add custom post-processing here` at src/system/skin.py:234.",
"voxel_skin geodesic prior — the strongest quality lever on skinning (configs/transform/inference_skin_transform.yaml:21-32; impl VertexGroupVoxelSkin src/data/vertex_group.py:130-180, voxel_skin() src/data/vertex_group.py:434-535). Knobs: `grid`=196 with the vendor's own inline comment 'increase this for better results', `grid_query`=7, `vertex_query`=1, `grid_weight`=3.0, `alpha`=0.5, `link_dis`, `mode` 'square'|'exp' (exp is commented out at line 31), `backend` pyrender|open3d.",
"voxel_mask — exponent applied to the voxel prior when masking predicted weights: skin_pred *= voxel_skin**voxel_mask then renormalise (configs/model/unirig_skin.yaml:11 = 3.0; applied src/model/unirig_skin.py:440-441). Controls how hard geodesic reachability vetoes the network's weights — the knob against weights bleeding across the jaw/ear gap on an elven-ear head.",
"num_train_vertex — inference chunk size for the bone-point cross-attention (configs/model/unirig_skin.yaml:3 = 512, vendor comment 'increase this for faster speed at the cost of memory'; chunking loop src/model/unirig_skin.py:344-348). On a 32GB 5090 this can go far higher; README:394 says generation only needs 8GB, so ~24GB is being left idle.",
"faces_target_count — the decimation cap applied BEFORE UniRig ever sees the mesh (launch/inference/extract.sh:6 default 50000; fast_simplification.simplify at src/data/extract.py:346-347). Every downstream stage reasons about the decimated mesh; a pore-detailed head gets crushed to 50k faces unless raised.",
"Sampler settings — `method` 'origin'|'mix' (src/data/sampler.py:202-209), `num_samples` (65536 for AR, configs/transform/inference_ar_transform.yaml:2; 32768 for skin, inference_skin_transform.yaml:2) and `vertex_samples` 8192. This is the point-cloud budget the Michelangelo shape encoder sees.",
"BEST-OF-N SKELETONS — writer `repeat` + `add_num` (configs/task/quick_inference_skeleton_articulationxl_ar_256.yaml:17-18; ARWriter.on_predict_end re-enters the predict loop src/system/ar.py:275-280, numbered filenames src/system/ar.py:319-322), plus run.py:45 `--seed` (README:183 documents seed variation). Combined with the validate_rignet task's J2J/J2B/B2B metrics (src/system/ar.py:161-166, src/system/metrics.py) this is a ready-made generate-N-and-score gate — the exact 'skeleton sanity gate' our own LOCAL_MODELS_AUG2026.md:503 says UniRig requires."
],
"what_we_pass": [
"NOTHING. There is no UniRig caller anywhere in C:/dev/humanity-forgotten — no script references generate_skeleton.sh, generate_skin.sh, merge.sh, run.py --task, or D:/assetgen/UniRig (verified by repo-wide grep excluding .claude/worktrees).",
"The substitute in production is C:/dev/humanity-forgotten/build/3d/characters/bl_skin_to_mannequin.py (825 lines), a Blender bone-heat + inverse-distance-to-bone-segment autoskin.",
"bl_skin_to_mannequin.py:62 — MAX_INFL = int(argval('--max-influences', '4'))",
"bl_skin_to_mannequin.py:63 — FALLOFF = float(argval('--falloff', '4.0'))",
"bl_skin_to_mannequin.py:36-38 — invoked as: blender -b --python ... -- --rig <SKM_Manny_Simple.fbx> --refpose <HumanityChildRefPose.json> --mesh <SM_CHAR_0001_CHILD.fbx> --out-dir <dir> --report <report.json>",
"bl_skin_to_mannequin.py:25 — self-declared quality tier, verbatim: 'HONEST TIER OF THE WEIGHTS: PREVIZ-AUTOSKIN.'",
"bl_skin_to_mannequin.py:6-7 — the file's stated reason for existing is now STALE: 'PIPE_3D's ruled auto-rigger (UniRig) is not installed on this box'. It is installed. docs/CHARACTER_FACTORY_PLAYBOOK.md:1147 repeats the same stale claim ('UniRig is the ruled auto-rigger and is NOT INSTALLED').",
"Weights on disk are complete and current: skeleton/articulation-xl_quantization_256/model.ckpt, skeleton/rignet/model.ckpt, skin/articulation-xl/model.ckpt, plus data/rigxl + data/rignet (10.69GB). The HF repo VAST-AI/UniRig has not shipped a new checkpoint in ~1 year — there is no larger UniRig weight to fetch. The promised Rig-XL/VRoid full checkpoint (README:66) never shipped.",
"Two of the three downloaded checkpoints are unused by any config we would run: skeleton/rignet/model.ckpt is only referenced by configs/task/validate_rignet.yaml:4."
],
"misconfigurations": [
"NOT-OURS-YET, but these are the vendor defaults our first runner would silently inherit. faces_target_count=50000 (launch/inference/extract.sh:6) decimates the pore-detail head before UniRig sees it — the whole point of the current photoreal push.",
"group_per_vertex=4 hardcoded at src/system/skin.py:300 and src/inference/merge.py:373, while the exporter itself defaults to -1/all (src/data/exporter.py:332). 4 influences is the same ceiling our bespoke script already has — running UniRig as-shipped buys no deformation headroom.",
"voxel_skin grid=196 (configs/transform/inference_skin_transform.yaml:25) carries the vendor's own comment 'increase this for better results'. Left at 196 on a box with 128GB RAM and a 32GB GPU.",
"num_train_vertex=512 (configs/model/unirig_skin.yaml:3), vendor comment 'increase this for faster speed at the cost of memory'. README:394 says inference needs only 8GB — roughly 24GB of the 5090 sits idle.",
"TRAP: num_return_sequences (configs/system/ar_inference_articulationxl.yaml:5) is inert. UniRigAR.generate takes results[0,:] only (src/model/unirig_ar.py:164), so raising it burns compute and discards every extra candidate. Use writer.repeat (src/system/ar.py:275-280) and --seed for N candidates instead.",
"temperature=1.5 with repetition_penalty=3.0 (configs/system/ar_inference_articulationxl.yaml:10-11) on top of num_beams=15 + do_sample=True. A penalty of 3.0 is severe against a token grammar where coordinate tokens (0..255) legitimately repeat — worth an ablation before trusting skeleton output.",
"add_root is dead in the shipped launcher: generate_skeleton.sh:8 declares it and :20 parses it, but the variable is never appended to the run.py command. merge.sh never exposes it at all. UE5 root-motion needs it.",
"do_not_normalize=True is commented out at src/system/skin.py:304, so exported weights are always renormalised over the top-4 influences."
],
"newer_alternatives": [
"SkinTokens / TokenRig (VAST-AI Research, arXiv 2602.04805, Feb 2026) — the OFFICIAL successor, and UniRig's own README:12-22 now opens with a banner pointing at it. Unifies skeleton + skinning into one autoregressive sequence via FSQ-CVAE discrete skin tokens on a Qwen3-0.6B backbone with GRPO RL refinement. Reports 98-133% better skinning accuracy and 17-22% better bone prediction. MIT. Critically for us, its demo.py exposes `--use_skeleton` = 'skin a model using its existing skeleton', plus `--use_transfer` for texture/scale preservation and --top_k/--top_p/--temperature/--repetition_penalty/--num_beams/--use_postprocess. Needs 14GB VRAM (fits the 5090 with room). https://github.com/VAST-AI-Research/SkinTokens , https://huggingface.co/VAST-AI/SkinTokens , https://arxiv.org/abs/2602.04805",
"UniRig itself is FROZEN: huggingface.co/VAST-AI/UniRig shows skeleton/, skin/ and data/ all last touched ~1 year ago, and the Rig-XL/VRoid 'full checkpoints' promised at README:66 never shipped. We already hold the largest and latest UniRig weights that exist. https://huggingface.co/VAST-AI/UniRig/tree/main",
"Tripo AI (same lab lineage as UniRig, hosted/paid) is the strongest closed option per Aug-2026 roundups, with a one-click universal rig covering non-humanoids. Hosted API, not local — conflicts with the local-compute-is-free standing rule. https://www.tripo3d.ai/content/en/guide/the-best-automatic-skinning-3d-model-tools",
"AniGen: Unified S^3 Fields for Animatable 3D Asset Generation (arXiv 2604.08746, ~April 2026) surfaced in search as a newer animatable-asset method. NOT VERIFIED this session — I did not read the paper, repo, license or VRAM. Flagging as a lead only. https://arxiv.org/pdf/2604.08746",
"SkinCells: Sparse Skinning using Voronoi Cells (arXiv 2506.14714) — search-surfaced, targets sparse/compact skinning representations. NOT VERIFIED this session. https://arxiv.org/pdf/2506.14714",
"Previously scouted in our own build/3d/research/LOCAL_MODELS_AUG2026.md and NOT re-verified today: Puppeteer (Seed3D, Apache-2.0, OBJ in / FBX out), MagicArticulate (4.6GB VRAM but released weights are skeleton-only), Make-It-Animatable (MIT). RigAnything is Adobe Research License, noncommercial — DO NOT SHIP."
],
"top_recommendation": "Do NOT run UniRig's skeleton stage for the protagonist — run stage 3 ONLY, against the mannequin. Concretely: `bash launch/inference/generate_skin.sh --input <SM_CHAR_0001_CHILD_mannequin_bound.fbx> --output results/char0001_skin.fbx --data_name raw_data.npz`. Because generate_skin.sh:10 defaults data_name to raw_data.npz (overriding the task's predict_skeleton.npz), extract.py::process_armature (src/data/extract.py:278-329) harvests the UE5 Mannequin joints/tails/parents/names from the FBX and the skin model predicts weights for exactly those bones, with names carried through to export (src/system/skin.py:282). The 47 retargeted clips keep riding an untouched mannequin hierarchy. Then `python -m src.inference.merge --source results/char0001_skin.fbx --target <original_hi_poly.fbx> --output <rigged.fbx> --add_root true` to put the weights back on the undecimated mesh with a UE5 root bone. Raise faces_target_count well above 50000, lift group_per_vertex from 4 to 8 (src/system/skin.py:300), raise voxel_skin grid above 196, and raise reskin iter_steps above 1 (src/system/skin.py:250-259) — these four are the difference between UniRig output and our current PREVIZ-AUTOSKIN tier. Run SkinTokens head-to-head against it in the same harness: it is MIT, fits 14GB, and its `--use_skeleton` flag does this same job with a claimed 98-133% skinning-accuracy lead.",
"evidence_note": "VERIFIED by reading vendor source at D:/assetgen/UniRig: run.py (all CLI args), all 5 launch scripts, all 18 config yamls, src/data/{extract,order,exporter,vertex_group,sampler,tail,transform,augment,dataset,raw_data}.py, src/model/{unirig_ar,unirig_skin}.py, src/tokenizer/tokenizer_part.py, src/system/{ar,skin}.py, src/inference/merge.py, and README.md. Every file:line citation above was read directly. Weights inventoried on disk. Our side: verified by repo-wide grep of C:/dev/humanity-forgotten (excluding .claude/worktrees) that NO UniRig caller exists, and read build/3d/characters/bl_skin_to_mannequin.py. VRAM figures are the vendor's claims (README:394 = 8GB inference; README:345 = 60GB+ for skin TRAINING) — I did NOT run inference, so real 5090 VRAM and wall-clock are unmeasured. NOT VERIFIED / stated as claims, not facts: (1) whether the `vroid` cls token is actually well-trained in the shipped articulation-xl checkpoint — the config only marks `mixamo` as untrained (configs/tokenizer/...:6), which is suggestive but not proof; (2) whether skin quality on a mannequin skeleton beats bl_skin_to_mannequin.py — that needs an A/B bake, and README:197 warns skin quality collapses on a bad skeleton; (3) that a UE5-mannequin order yaml will produce correctly ORDERED name assignment — make_names assigns positionally, so a mismatch between generated bone order and template order would mislabel bones; (4) exact category coverage claims (quadrupeds/birds/serpents) — the code is genuinely template-free (bone_i fallback at src/data/order.py:76, examples/ ships giraffe/bird/tira/carrot), but I ran no creature inference; (5) AniGen and SkinCells are search-surfaced leads only, unread. Web-checked today: SkinTokens repo+arXiv fetched directly, HF VAST-AI/UniRig tree fetched (confirms no newer UniRig weights)."
}
]
}