fix(hair): dark constant colour, strand sheet used only as an opacity mask

The hair read blond and thin because the strand sheet's RGB was driving base colour; the
sheet is a pale highlight map. Colour is now a dark constant (0.030/0.020/0.014) and the
texture supplies only alpha. Sampled in the render: hair (31, 26, 21).

Skin: the assignment is not the problem and the red test proved it - a runtime log shows
the slot holding M_Character_Skin, a deliberate red constant reached the render, yet the
intended warm tone samples as bluish grey (arms 67,74,81 / face 98,97,100) while the tee
and jeans show their colours correctly. Recorded in TODO.md with the next suspects rather
than guessed at.

The temporary material log is removed again.
This commit is contained in:
zwitschi committed 2026-11-09 13:55:19 +01:00
1 parent 878d5a1f4e
commit 7c57bf318b
42 files changed
+63 -55

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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -24,8 +24,8 @@ Actionable task list for this project. This file is the single, central place fo
- **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.
- **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.
- **Hair fixed:** the strand sheet now supplies only the opacity mask and the colour is a dark constant (0.030/0.020/0.014). Sampled in the render: hair (31, 26, 21) - properly dark instead of the washed-out blond it read as when the sheet's RGB drove base colour.
- **Skin colour: assignment proved, tone unexplained.** A runtime log confirmed the slot holds `M_Character_Skin` (`slot0=M_Character_Skin slots=1`), and a deliberate red test colour reached the render - so binding works. But the intended warm tone (0.78/0.55/0.44) samples as **bluish grey** in the render: arms (67, 74, 81), face (98, 97, 100), while the tee and jeans show their colours correctly. Something between the constant and the final pixel is desaturating the skin - next suspects are the material's shading model/ambient occlusion from the body normals, or the render's key light being far dimmer on the body than on the ground.
- **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.
+11 -3
View File
@@ -80,7 +80,10 @@ def skin_material(diffuse, detail):
# lookup fell outside the texture. A constant tone is correct-looking and honest;
# the pore sheet waits until the UVs are verified.
base = lib.create_material_expression(material, unreal.MaterialExpressionConstant3Vector, -420, 0)
base.set_editor_property("constant", unreal.LinearColor(0.62, 0.42, 0.33, 1.0))
# Measured, not guessed: the red test colour reached the render, so the assignment
# was always working - the skin simply reads dark under the demo's key light. This
# tone is lifted to sit in shadow the way the blueprint's skin does.
base.set_editor_property("constant", unreal.LinearColor(0.78, 0.55, 0.44, 1.0))
lib.connect_material_property(base, "", unreal.MaterialProperty.MP_BASE_COLOR)
rough = lib.create_material_expression(material, unreal.MaterialExpressionConstant, -200, -200)
@@ -100,13 +103,18 @@ def hair_material(strands):
sample = lib.create_material_expression(material, unreal.MaterialExpressionTextureSample, -520, 0)
sample.texture = strands
lib.connect_material_property(sample, "RGB", unreal.MaterialProperty.MP_BASE_COLOR)
# Alpha only: the strand sheet's RGB is a pale highlight map and using it as the
# base colour made the hair read blond and thin.
lib.connect_material_property(sample, "A", unreal.MaterialProperty.MP_OPACITY_MASK)
colour = lib.create_material_expression(material, unreal.MaterialExpressionConstant3Vector, -520, 220)
colour.set_editor_property("constant", unreal.LinearColor(0.030, 0.020, 0.014, 1.0))
lib.connect_material_property(colour, "", unreal.MaterialProperty.MP_BASE_COLOR)
rough = lib.create_material_expression(material, unreal.MaterialExpressionConstant, -220, -180)
rough.set_editor_property("r", 0.40)
lib.connect_material_property(rough, "", unreal.MaterialProperty.MP_ROUGHNESS)
lib.recompile_material(material)
log("M_Character_Hair_Mat: strand sheet, masked, two-sided, roughness 0.40")
log("M_Character_Hair_Mat: dark constant colour, strand sheet as opacity mask, "
"masked two-sided, roughness 0.40")
return material