fix(clothing): close the coverage holes, and bind materials without crashing

Solidify thickness went from 1 cm to 2 cm and the shell is fattened 8 mm along its own
normals afterwards. That closed the shoulders, hips and knees where the body was
showing through the cloth.

Evaluation against the blueprint, same shot and box before and after:
  mean deltaE     31.9 -> 21.8
  palette deltaE  25.4 -> 19.7
  aspect delta     1.24 -> 1.22

Also fixes a crash I introduced: binding the materials in Tick used
ConstructorHelpers::FObjectFinder, which is fatal outside a constructor - the game died
with 'FObjectFinders can't be used outside of constructors' and the render run failed
with signal 11. The binding now lives in BeginPlay and uses LoadObject.

Not fixed, and recorded: the arms and face still render neutral grey, so
M_Character_Skin itself is suspect rather than the assignment.
This commit is contained in:
zwitschi committed 2026-11-09 13:50:21 +01:00
1 parent 86e9c5b693
commit 878d5a1f4e
43 files changed
+95 -58

No files matched your search

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -311,6 +311,27 @@ void AGigLineCharacter::BeginPlay()
// `-walk` holds forward input from the first frame. The screenshot run uses it
// with `-thirdperson` so a capture can show the walk clip mid-stride: an idle
// shot cannot prove the animation set, and a still cannot show a stride.
// Bind the character materials here rather than in the constructor: the slot
// assignment made there never reached the render. LoadObject, not FObjectFinder,
// because FObjectFinder may only be used inside a constructor - putting one in
// Tick was a fatal "FObjectFinders can't be used outside of constructors".
if (GetMesh())
{
if (UMaterialInterface* Skin = LoadObject<UMaterialInterface>(
nullptr, TEXT("/Game/Test/Character/M_Character_Skin.M_Character_Skin")))
{
GetMesh()->SetMaterial(0, Skin);
}
}
if (HairMesh)
{
if (UMaterialInterface* HairMat = LoadObject<UMaterialInterface>(
nullptr, TEXT("/Game/Test/Character/M_Character_Hair_Mat.M_Character_Hair_Mat")))
{
HairMesh->SetMaterial(0, HairMat);
}
}
bWalkForever = FParse::Param(FCommandLine::Get(), TEXT("walk"));
// `-perf` starts the overlay on: the performance capture and any manual frame
+2
View File
@@ -508,3 +508,5 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO.
- **MPFB vertex groups are polluted by helper geometry.** The body mesh weights its helper cubes to groups named `Left`, `Right`, `HelperGeometry` and `JointCubes` at 1.0, so a "strongest group" lookup returns a helper for most vertices - the first clothing build selected **zero** faces. Filter helper names out before asking which bone a vertex belongs to.
- **Blender refuses to apply a modifier to a mesh with shape keys.** The body carries ten; a garment copy must clear them (`shape_key_remove(all=True)`) before solidify can be applied.
- **Edit-mode selection reverts to vertex selection.** Setting `polygon.select` in object mode and then entering edit mode re-derives selection from the vertices, which were all still selected - deleting "the unselected faces" wiped the entire garment. Filter with `bmesh.ops.delete(context="FACES")` instead.
- **`ConstructorHelpers::FObjectFinder` outside a constructor is a fatal error**, not a warning: the game dies in `Tick` with "FObjectFinders can't be used outside of constructors". Use `LoadObject<T>(nullptr, Path)` for runtime loads and keep `FObjectFinder` to the constructor.
- **A 1 cm solidify shell lets the body poke through.** Garments built by duplicating body faces needed 2 cm of solidify plus an 8 mm fatten along their own normals before the cloth read as continuous at shoulders, hips and knees.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3 -2
View File
@@ -23,8 +23,9 @@ Actionable task list for this project. This file is the single, central place fo
- **Skin still needs work.** `M_Character_Skin` (constant tone 0.62/0.42/0.33, roughness 0.58) is built and the pawn calls `SetMaterial(0, ...)`, but the body still renders neutral grey in review shots, so the assignment is not taking. The baked `T_Character_Diffuse` cannot be used either: sampling it renders the body black, so the MPFB mesh's UVs do not map onto that bake. Next: verify the slot index after `SetSkeletalMesh`, or assign in `BeginPlay` instead of the constructor.
- **Character blueprint and evaluation tools exist.** `Tools/comfyui_character_blueprint.py` generates a four-view sheet (front/side/back/head) from one description with fixed seeds; `Tools/character_match.py` compares a render against it and reports aspect, fill, coverage, mean CIE76 and palette distance plus a side-by-side montage. First result against `Screenshots/demo/review-demo-third-person-front.png`: **aspect delta 1.24, fill delta 0.12, mean ΔE 31.9, palette ΔE 25.4** — and the montage shows why: the model is a **nude body**, the blueprint is clothed (navy tee, jeans, sneakers).
- **Clothing exists now** (`Tools/character_clothing_build.py` + `Tools/ue_character_clothing_import.py`): tee, jeans and shoes as three separate skinned meshes, built from the body's own faces per the character-artist workflow (duplicate faces, solidify for thickness, one mesh per piece), imported as `CharacterTee` / `CharacterJeans` / `CharacterShoes` and attached as leader-pose components. The navy tee, dark jeans and off-white shoes read correctly at review distance.
- **The garments have coverage holes.** Grey skin shows through at the shoulders, hips and knees. Solidify offset (1 cm) is not enough where the body is convex, and trimming by x-width removes faces the body then shows through. Next: raise the offset, or grow the garment along its own normals after the solidify.
- **Hair reads light and wispy** since its material instance was repointed to `M_Character_Hair_Mat` (masked strand sheet): the alpha is cutting the cards into separate hairs and the base colour is being lifted by the texture. Either darken the texture or drive colour from a constant and use the sheet only as an opacity mask.
- **Garment coverage fixed 2026-11-09.** Solidify thickness raised to 2 cm plus a normal fatten pass of 8 mm closed the shoulders, hips and knees; the tee, jeans and shoes now read as continuous cloth. Evaluation against the blueprint improved from **mean ΔE 31.9 → 21.8** and **palette ΔE 25.4 → 19.7**; the montage is `Art/characters/blueprints/match-front.png`.
- **Hair reads light and wispy** since its material instance was repointed to `M_Character_Hair_Mat` (masked strand sheet): the alpha is cutting the cards into separate hairs and the base colour is lifted by the texture. Either darken the texture or use the sheet only as an opacity mask and drive colour from a constant.
- **Skin still renders neutral grey.** The constructor assignment never reached the render, and moving it to `BeginPlay` with `LoadObject` required fixing a fatal: `FObjectFinder` may only be used inside a constructor, so the first attempt crashed the game in `Tick`. The binding now runs in `BeginPlay`, but the arms and face are still grey in the render, so `M_Character_Skin` itself is suspect — possibly the material has no compiled base-colour expression reaching the pin.
- **Figure segmentation is explicit, not automatic.** Auto-detection failed on the review shots: the backdrop (sky band, pale wall, dark pavement) is larger and more saturated than the subject, so every size-based rule picked a backdrop blob. `--render-box` takes the figure box as fractions of the frame. A background plate rendered without the character would make this automatic.
- `Tools/ue_character_materials.py` (wiring the generated textures into the skin and hair materials) **segfaults in the headless commandlet** when it edits the material graph. Not wired; the hair keeps the dark base colour it carries from Blender. Needs either an editor-session run or a material asset built from scratch instead of edited in place.
+17 -4
View File
@@ -38,9 +38,9 @@ GARMENTS = (
"upperarm01.L", "upperarm01.R", "breast.L", "breast.R",
"neck01", "neck02", "pelvis.L", "pelvis.R"],
"z_min": 0.90, "z_max": 1.44,
"x_max": 0.235, # trims the bare arms out of the tee
"x_max": 0.255, # trims the bare arms out of the tee
"use_bones": False,
"thickness": 0.009,
"thickness": 0.020,
"colour": (0.020, 0.032, 0.075, 1.0), # navy cotton
"roughness": 0.78,
},
@@ -51,9 +51,9 @@ GARMENTS = (
"upperleg02.L", "upperleg02.R", "lowerleg01.L", "lowerleg01.R",
"lowerleg02.L", "lowerleg02.R", "spine01"],
"z_min": 0.06, "z_max": 1.06,
"x_max": 0.30,
"x_max": 0.32,
"use_bones": False,
"thickness": 0.010,
"thickness": 0.020,
"colour": (0.032, 0.055, 0.105, 1.0), # dark denim
"roughness": 0.62,
},
@@ -154,6 +154,19 @@ def build_garment(body, arm, spec, out_dir):
solidify.offset = 1.0
bpy.ops.object.modifier_apply(modifier=solidify.name)
# Push the shell out along its own normals. Solidify alone left the body visible
# through the garment at every convex patch - shoulders, hips, knees.
import bmesh
bm = bmesh.new()
bm.from_mesh(mesh)
bm.normal_update()
for vertex in bm.verts:
vertex.co += vertex.normal * spec.get("fatten", 0.008)
bm.to_mesh(mesh)
bm.free()
mesh.update()
log(f"{spec['asset']}: fattened by {spec.get('fatten', 0.008):.3f} m")
material = bpy.data.materials.get(f"M_Char_{spec['name']}") \
or bpy.data.materials.new(f"M_Char_{spec['name']}")
material.use_nodes = True