Commit Graph
253 Commits
Author SHA1 Message Date
zwitschi d17db7d050 test(review): expectations describe the current stage, judge goes 0/9 to 2/9
The 0/9 was mostly the expectations' fault, not the scene's. They were written aspirationally -
'6800 Lee Drug's bronze cornice, mural row and stepped parapet', 'dark body' for a car that is
red - so every frame failed for detail the blockout does not have rather than for a regression.
Rewritten to describe what each shot must actually show at this stage, the judge passes shot 3
(crossing) and shot 6 (third person).

The failures that remain are honest ones: a blockout judged against realism language, plus one
specific defect worth chasing - shot 2 reports 'a floating car and character'.

Screenshots/README.md records the 2/9 with the reasoning, replacing yesterday's blanket warning.
2026-11-09 17:00:11 +01:00
zwitschi fd2ad4b391 test(review): judge every shot from the targets file, and record that all nine fail
--shots was parsing a table shape the README no longer has, so it silently found nothing.
It now reads Tools/visual_targets.json, the same file the render gate uses, and reports one
line per shot with the model's verdict and reasoning.

The result is not good news and is committed as such: 0 of 9 shots judged 'yes'. Shot 7 shows
the scene itself wrong - a disassembled car floating over a blank surface with no street or
buildings in frame - and the others fail for missing detail rather than for a broken frame.
visual_loop --verify still reports 9/9, because it only measures exposure and size.

A warning now sits at the top of Screenshots/README.md: treat every image there as stale until
the scene is fixed. The cause is not yet identified - the map builder's recent changes are 15
lines of vehicle staging, which does not explain a missing street.
2026-11-09 16:55:59 +01:00
zwitschi 58095b6a33 test(vehicle): wheels are created with the physics state, and still come out zero
Two measurement errors found and corrected before drawing any conclusion:

  - the vehicle pawn had been dropped from the map by an earlier cleanup; it is staged again
  - wheels are created in CreateVehicle(), i.e. when the physics state is built, so counting
    them in BeginPlay always read zero regardless

Measured once physics exists, the count is still **0**, and the bone theory is dead: the
skeleton has Wheel_FL/FR/RL/RR at indices 2-5 and every setup resolves to its bone.

CreateWheels() runs only inside CreateVehicle() when PVehicleOutput is valid, so a zero count
means the physics state never produced a vehicle - the problem is upstream of the wheel config,
in the physics asset or the way the skeletal mesh binds it, not in the wheel setups.

Also logged at the settle check: the pawn's own downward trace reports ground at Z 152 while
the visible car's panels stand on a road of 366, so the pawn and the visible car may not be in
the same place at all - worth checking before anything else.
2026-11-09 16:53:25 +01:00
zwitschi 1da209a902 fix(placement): both heights now come from a measurement after settling
The probe was armed from BeginPlay but the delay was lost when I removed the dead block, so
it never ran. Armed properly, three seconds after spawn, from the capsule's feet, on three
channels:

  GigLine foot Visibility after settling: actor Z 456, feet 369, hit Z 366

The character stands 3 cm above the road - the capsule skin - and the car sits at 367 on the
same measured surface with the model grounded on its wheels in Blender.

Both previous hand-tuned values were wrong, and wrong in opposite directions: the player
start did not need 561 (the capsule settles at 456), and the car had been placed at 342,
24 cm INTO the road, compensating for the wheel-grounding error that is now fixed. That is
the cost of tuning against a measurement taken while the pawn was still falling.
2026-11-09 16:44:51 +01:00
zwitschi f4c1fed99b test(placement): measure the foot contact after the pawn settles, not at spawn
The earlier 'ground 366 against an actor at 561' reading was taken in BeginPlay, while the
pawn is still at its spawn height and falling - so it described the surface far below a
falling capsule, not a float. That number drove two hand-tuned Z values (561 for the player
start, 342 for the car) on the strength of a measurement taken at the wrong moment.

The probe now runs three seconds after spawn, traces from the capsule's feet rather than the
actor origin, and reports three channels with the actor and component each hits. A
multi-channel probe at spawn showed every channel hitting the same StaticMeshActor at 366,
which is consistent with the falling-pawn explanation rather than with missing sidewalk
collision.

Not yet read back: the last two game runs produced no log output at all, so the settled
numbers are unverified and recorded as such.
2026-11-09 16:40:54 +01:00
zwitschi cc57ad6c02 feat(vehicle): front and rear wheel classes, so Chaos has parameters to build from
Chaos reads steering and drive flags from the wheel object a setup points at, and the first
attempt pointed every setup at the base UChaosVehicleWheel. Dedicated front and rear classes
carry the modelled dimensions (radius 33 cm, width 22 cm), steering on the front axle, drive
on the rear, and the suspension travel - which is also how Unreal's own vehicle template is
organised.

Compiles clean. Not yet verified in a run: the drive test did not log the vehicle this time,
so whether the wheel count changes from 0 is still unproven and recorded as such rather than
claimed.
2026-11-09 16:32:25 +01:00
zwitschi 98eae6f3e9 docs(architecture): make section pages canonical 2026-11-09 16:30:29 +01:00
zwitschi 15cc6cbdd0 fix(vehicle): the glazing reads as glass
The windows material was metallic 1.0 with roughness 0.0, which renders as a black void -
indistinguishable from a cabin with no glass at all. It is now tinted translucent: opacity
0.85, roughness 0.08, dark blue-grey base. The windscreen and side glass read as glass.

Opacity 0.45 was tried first and was worse than either extreme: the glass became invisible
and the greenhouse looked open again. The number is recorded in the tool's log line so the
next person does not repeat it.

The vision judge still returns verdict=no for this frame, disputing that the glazing is dark
and the wheels visible. The image shows both. It is an advisory helper - AGENTS.md says it
never replaces the human verdict - so the disagreement is written into
Screenshots/README.md rather than being resolved by iterating against the model.
2026-11-09 16:21:41 +01:00
zwitschi bb12ce8c33 fix(vehicle): the car is red in the engine, not black
Root cause was in the template, not the import path: 'car paint' is a legacy mix-shader
graph. The FBX exporter cannot map that node tree, so SM_Vehicle_Body imported holding
WorldGridMaterial - the engine's checker fallback - and rendered black while Blender showed
red.

Two changes:
  - Tools/vehicle_from_template.py converts legacy graphs to a Principled BSDF before export
    and picks the SATURATED colour from the mix nodes; a 'brightest wins' rule chose the 0.8
    grey diffuse beside the 0.49 red and produced a grey car.
  - Tools/ue_vehicle_materials.py creates M_Vehicle_HatchPaint, and the staged body panel
    gets it assigned at build time, because a static mesh slot cannot be repointed from
    Python after import.

Judge on the new capture: verdict=yes confidence=0.9 - 'a red hatch car with a red body and
dark wheels parked on a road'. The comparison table in Screenshots/README.md now shows both
columns agreeing on colour.

Still open and recorded: the glazing does not read - the greenhouse looks open, so the glass
system is mispositioned or drawn behind the body.
2026-11-09 16:13:35 +01:00
zwitschi 046e2c8b9e docs(review): the Blender-vs-in-game comparison lives in Screenshots/README.md
Merged COMPARISON.md into the review README as a generated section between
<!-- blender-comparison:begin/end --> markers, so there is one place to look and one place
to sign off. Tools/build_comparison_md.py rewrites only that block - the hand-written
sections around it are untouched - and formats the file with prettier afterwards, because a
generated table is never byte-identical to prettier's output and the repo gates on
--check.

Run it after every capture or Blender render; it is the routine that keeps the page current
with new images rather than a one-off export.
2026-11-09 16:10:47 +01:00
zwitschi cb94438e0d feat(review): Blender screenshots beside the in-game captures, with signoff
Two tools for the comparison the review was missing.

- Tools/blender_screenshots.py renders out of a .blend: one image per mesh object, and
  four turntable angles of the whole scene, framed from the scene's own bounds so adding
  geometry never silently crops a shot. Output lands in Screenshots/blender/, beside the
  in-game captures.
- Tools/build_comparison_md.py writes Screenshots/COMPARISON.md: a table with the in-game
  capture and its Blender render side by side per row, then a signoff table with one row
  per in-game screen. Unmatched shots show an em dash rather than disappearing, because a
  missing Blender render is information too.

The first run already earns its keep: the Blender render of the owner's hot hatch is red
with clear glazing, while the in-game capture of the same asset is near-black, so the
material colour is being lost on import - visible in one glance in the new page.
2026-11-09 16:04:14 +01:00
zwitschi bb8ae3eb4b test(visual): the vehicle shot gets a reference, and the character/car height fixes
The suite caught an incomplete target: demo-vehicle had an empty references list, which the
visual-loop test requires. It now points at Art/blender/vehicle/hatch-reference.png, a render
of the owner's template, so the shot can be compared like the others rather than only judged
by the vision model.
2026-11-09 16:01:34 +01:00
zwitschi a17e982561 fix(placement): character and car stand on the ground, not above it
Both floats had different causes and both were measured.

Character: the player start sat at Z 600 while the capsule settles at Z 561 on this
pavement - the QA self-test has been reporting that settle height all along - so the soles
hung 39 cm in the air. The start now uses the measured 561.

Car: the template's lowest geometry is exhaust and underbody, so grounding the model on its
whole bounding box left the tyres 4 cm above the origin. It is grounded on its WHEELS now.
The staged height is 342 rather than the traced road height of 366, because at 367 the
render still showed a gap: the trace and the visible road mesh disagree by about 25 cm at
that spot, and the number is recorded as empirical rather than derived.

Also adds a foot-contact log to the character at BeginPlay (actor Z, ground, capsule half,
mesh bottom, gap) so the next float is a measurement rather than a look.
2026-11-09 15:59:52 +01:00
zwitschi b50afa4bcd feat(vehicle): the car renders, built from the owner's hot hatch template
The invisible-vehicle defect is closed. Shot 9 now shows the car instead of an empty road.

Tools/vehicle_from_template.py converts Art/blender/vehicle/hot hatch.blend into UE-ready
assets, following two skills:

  vehicle-artist - systems split (body 113k tris, trim 10.5k, glass 20.5k, lights 18.6k,
  wheels 47.8k), one UCX_ hull 1.45 x 3.94 x 0.76 m, two decimated LODs at 39.7k and 13.6k.
  unreal-export - scale normalised to a documented 4.05 m hot hatch before export (the
  template measures 7.26 m, factor 0.5579), then verified after import: body bounds
  1.51 x 4.02 x 1.22 m against the 4.05 m target.

Staged as five plain StaticMeshActors, because the earlier probe proved those render while
the same meshes attached to the vehicle pawn do not.

Open and recorded rather than hidden: judge_screenshot returns verdict=no confidence=0.9 -
the silhouette does not read as a hot hatch from this angle and the car floats slightly.
The likely causes are a system misclassification in the template conversion and an
origin-height error of a few centimetres.
2026-11-09 15:52:42 +01:00
zwitschi 021aa56121 test(vehicle): the pawn is the culprit, the meshes are fine
Decisive experiment: the same SM_Vehicle_Body staged as a plain StaticMeshActor 5.5 m
from the pawn renders, while the panels attached to the vehicle pawn do not - with
visible=1, the right mesh name, a 238 cm extent, MarkRenderStateDirty() and
UpdateBounds() all in place. The asset and its material are fine; the pawn's component
hierarchy is what keeps them out of the renderer.

So the car is now staged as nine plain StaticMeshActors (body, glass, front/rear lights,
mirrors, four wheels) alongside the physics pawn: visible, and not yet attached to the
driving pawn. A visible car that does not drive yet beats an invisible one that does.

Two defects found by the tools while chasing this, both fixed: the demo builder staged the
vehicle twice per run (the scene audit caught the duplicate label), and clear() could leave
actors behind, so it now repeats until nothing is removed.
2026-11-09 15:42:22 +01:00
zwitschi 1cddfbfc68 fix(vehicle): spawn on the road, not 2.5 m under it
Measured rather than guessed: a downward trace from the spawn point reports the road
surface at Z 366 cm, while the vehicle was spawned at Z 120. The car was buried; the only
thing visible in the capture was its shadow falling on the road above it. Spawn is now
Z 470, the camera aims at 430, and it settles at Z 470.

Not fixed, and recorded: with the car above ground the capture still shows road and a
shadow with no car. Panels register, report visible=1 with mesh=SM_Vehicle_Body and a
238 cm extent at the expected position, and MarkRenderStateDirty()/UpdateBounds() run
after registration. The shadow proves a collider is there; the visible geometry is not.

Screenshots/README.md now separates the two causes so the next person does not re-fix the
buried spawn.
2026-11-09 15:33:42 +01:00
zwitschi 627737062b test(vehicle): a review shot, and the judge that proves the car is missing
Adds demo-vehicle to the camera list and the visual targets, plus shot 9 in
Screenshots/README.md - marked FAIL, because that is what it shows.

The vehicle is staged and runs: spawned at (-12950,-900,120), 9 visual panels attached,
panel 0 visible with a 238 cm extent, settled at Z 119 cm. The capture shows an empty road
with a soft shadow. Two checks disagree in a useful way: visual_loop --verify PASSES the
frame (it only tests size, blankness and tonal range, so a well-exposed empty road passes),
while judge_screenshot returns verdict=no confidence=1.0.

Strongest clue, recorded in TODO.md: a downward trace from the vehicle finds no ground at
all (ground at Z -9999), so the car sits over a surface without collision or inside
geometry - the shadow and the panels are not in the same place.

Diagnostics added along the way: mesh/visibility/bounds at BeginPlay, per-panel world
positions and extents, and the ground trace. The visual panels were also unhidden from the
physics mesh: SetVisibility(false, ...) hid them whether or not propagation was requested.
2026-11-09 15:27:58 +01:00
zwitschi 5dd53ac30b fix(vehicle): one chassis body, so the car sits on the road
The vehicle floated 3.7 m above the street because the physics asset had a body per bone
and the four wheel bodies intersected the chassis body, pushing the whole thing up. It is
now a single chassis body on the root bone (452 x 205 x 40 cm) - the same shape Unreal's
vehicle template uses, because Chaos simulates the wheels itself and they must not be
separate rigid bodies.

Settle check, same spawn point:
  before: settled at Z 494 cm
  after:  settled at Z 119 cm

Added -drivetest, which holds full throttle for 3 s and reports speed and distance. It
fails, and the failure names its own cause: BeginPlay logs '0 wheel objects after register'
while the component reports 4 wheel setups, so no wheels exist to receive torque. Recorded
in TODO.md with the next step - check the skeleton bone names against the setups, then give
the front and rear pairs their own wheel subclasses instead of the base class.
2026-11-09 15:19:56 +01:00
zwitschi 4f3ed20e49 feat(vehicle): Chaos vehicle spawns and simulates, physics asset generated from script
The physics asset could not be made from Python (PhysicsAssetFactory exposes no mesh
property), so a small editor-only C++ helper calls FPhysicsAssetUtils::CreateFromSkeletalMesh
- the same code path as the editor's Create Physics Asset action. It produces
PA_Vehicle_Sedan with 6 bodies and assigns it to SK_Vehicle_Sedan.

AGigLineVehicle is a Chaos wheeled pawn that configures its four wheels from the modelled
dimensions (radius 33 cm, wheelbase 278, track 156) and is staged in DemoMap. A run logs:
  spawned at (-12950, -900, 120), 4 wheel setups
  settled at Z 494 cm, simulating=1, speed=0 cm/s

That last line is the defect and it is not hidden: the vehicle rests 3.7 m above the road,
so the generated physics bodies do not match the mesh. Recorded in TODO.md with the next
step - inspect the bodies against the mesh, then either fix the creation params or author
two boxes and four wheel spheres explicitly.
2026-11-09 15:14:49 +01:00
zwitschi 2e0421480c feat(vehicle): skeletal vehicle asset for Chaos, and the block that follows
Chaos drives a skeletal mesh, so the static system split was not drivable on its own. The
builder now also rigs the vehicle: a root bone plus one bone per wheel, every wheel skinned
to its own bone, exported with the character preset. SK_Vehicle_Sedan imports with
SK_Vehicle_Sedan_Skeleton (508 triangles).

Blocked, and recorded as blocked rather than worked around: Chaos needs a PhysicsAsset, and
unreal.PhysicsAssetFactory in 5.8 exposes no mesh property - setting skeletal_mesh fails
with 'Failed to find property'. Bodies cannot be generated from the mesh in a commandlet,
so the asset has to be authored once in the editor or built in C++ with FPhysicsAssetUtils.

Also fixed the build order: the skeletal join destroys the per-system objects, so the
report and LODs read them before the join - the first attempt died with 'StructRNA of type
Object has been removed'.
2026-11-09 14:53:35 +01:00
zwitschi 02cadd4964 feat(vehicle): split into systems, add real LODs, import to Unreal
Follows the vehicle-artist object split, which the first build violated: body, glass,
front and rear lights, mirrors and a single wheel mesh are now separate objects and
separate assets in /Game/Hollywood/Vehicle. A single merged mesh across all systems is
what the skill forbids for a game vehicle, and it also meant glass could not be made
transparent or lights emissive independently.

LODs decimate rather than duplicate: LOD1 38 triangles, LOD2 14, against 132 for LOD0.
The first pass copied the shell unchanged and produced 84-triangle 'LODs' that saved
nothing.

Imports verified with bounds: body 4.76 x 1.76 x 1.28 m, wheel 0.22 x 0.66 x 0.66 m,
which match the modelled dimensions.

Open, recorded: LOD group wiring needs StaticMeshEditorSubsystem.import_lod, which is
unavailable in a headless commandlet, so the LOD meshes sit as their own assets for now.
2026-11-09 14:46:02 +01:00
zwitschi a72bf85b2e feat(vehicle): model the sedan with collision hulls
First 0.3.0 asset. Tools/vehicle_build.py builds a 4.72 x 1.80 x 1.44 m sedan - 2.78 m
wheelbase, 1.62 m track, 0.33 m wheels - as a three-tier silhouette: sill slab, bonnet
and boot shoulders, cabin greenhouse, plus glazing, lamps, bumpers and mirrors. 252
triangles.

Collision is four UCX_-prefixed convex hulls (body, cabin, two bumpers) per the
collision-proxy convention, hidden from render so the preview reads as the car rather
than the colliders.

Two corrections from the preview, both visible in the saved image: the first build was
one tall slab plus a floating cabin and read as a box truck, and the wheels were buried
inside a full-width sill - the sill is now 1.56 m so the wheels stand proud.

Reports every dimension to JSON so the Chaos vehicle config can read the wheelbase, track
and radius instead of restating them.
2026-11-09 14:43:20 +01:00
zwitschi 73b5bd6cd1 Merge pull request #37: v0.2.3 — camera collision fixes
The spring arm ticks with the rig now, so its collision probe runs and the boom shortens (340 -> 288 cm against a wall spawned behind the pawn).
2026-11-09 14:41:49 +01:00
zwitschi 82cafc2f29 fix(camera): prove the boom shortens, and record how to repeat it
With the spring arm activated the collision now works: a 2 m wall spawned 4 m behind the
pawn shortens the boom from 340 cm to 288 cm - 12 cm clear of the wall, matching the probe
size. -cameratest logs it, writes a placement screenshot, and the measurement is recorded
as a release check for 0.2.3.

Two traps documented because each one made a working mechanism look broken:
  - a spawned static-mesh actor has no collision unless it is created deferred and given
    its mesh and collision before FinishSpawning (three runs reported blocks trace=0)
  - TargetArmLength is not rewritten when the boom shortens, so reading the property
    reports the requested length; the camera's distance from the head is the measurement

Writes the 0.2.3 changelog entry and prunes the section from TODO.md.
v0.2.3
2026-11-09 14:40:18 +01:00
zwitschi 3e4dbd1014 fix(camera): activate the spring arm with the third-person rig
Real defect, found by reading the code rather than by the harness: the spring arm was
created with SetAutoActivate(false), and ToggleCamera only activated the camera. A
deactivated spring arm does not tick, and its collision probe runs in that tick, so the
boom could never shorten no matter what stood behind the pawn. ToggleCamera and BeginPlay
now activate the arm alongside the camera.

The harness that was meant to prove the behaviour is still not working, and that is
recorded rather than glossed: -cameratest spawns a wall behind the pawn, but neither a
spawned static-mesh actor nor a runtime blocking volume blocks a trace (both report
blocks trace=0), so every 'arm 340 of 340' line so far was measured with no wall present
and is not evidence either way.
2026-11-09 14:37:03 +01:00
zwitschi a6701aa3d1 test(camera): -cameratest harness for the spring-arm collision, result inconclusive
0.2.3 is about camera clipping, and nothing had ever exercised the spring arm's
collision test. This adds a harness: find the nearest wall from the spawn, stand the pawn
70 cm off it facing away, wait for the boom, then report the arm length, whether the boom
ends inside geometry, and a back-trace from camera to head.

Inconclusive, and the setup is the suspect rather than the engine: every run reports the
arm at its resting 340 cm, but BOTH traces come back clear, so nothing was between the
pawn and the boom at all. The placement is wrong, not necessarily the collision.

Three wrong turns recorded in TODO.md because each cost a run: the probe accepted the
floor as a wall, then faced the pawn into the wall by negating the impact normal (which
points away from it), then set actor rotation only - which a possessed pawn discards in
favour of control rotation.

Next: look at the placement screenshot before trusting the measurement.
2026-11-09 14:33:57 +01:00
zwitschi d8b44f1e57 Merge pull request #36: v0.2.2 — the character pass
Clothing, hair cards, character blueprints and render-vs-blueprint comparison, raised sun, walk cycle starting at maximum stride. All eight review shots approved.
2026-11-09 14:26:22 +01:00
zwitschi 6c4e0701fb release: v0.2.2 — the character pass
Writes the 0.2.2 entry and prunes the section from TODO.md. The entry records that this
ships as a patch by the owner's decision even though it adds asset categories, which the
policy otherwise puts in a minor release - so the numbering is not mistaken for scope.

Contents: clothing as three separate skinned meshes, hair cards oriented from the scalp
normal, ComfyUI character blueprints and the render-vs-blueprint comparison, procedural
textures, materials built from scratch, the raised sun, and the walk cycle starting at
maximum stride.

Verified: 8/8 shots, all approved; mean deltaE 31.9 -> 21.8 against the blueprint; 95
tool tests. Known limitations listed rather than omitted - garments are shells over body
faces, the baked diffuse cannot be used because the mesh UVs do not map to it, and the
character has no face detail.
v0.2.2
2026-11-09 14:26:13 +01:00
zwitschi 0e7a6e898b docs(review): owner approved shots 6, 7 and 8 — all eight shots approved
Ticks both the shot sections and their status rows, so the table and the sections agree:
5 previously approved, these 3 approved 2026-11-09 in their clothed, haired, relit state.
2026-11-09 14:25:35 +01:00
zwitschi 2f6d06f897 docs(review): restore the status rows for shots 7 and 8
The previous documentation commit dropped both rows from the status table while editing
the section text - the table then listed six shots against eight embeds. Restored, both
marked verified and pending approval.
2026-11-09 14:24:02 +01:00
zwitschi 5de4183456 docs(review): shots 6, 7 and 8 reflect the clothed, haired character
The appearance changed in 0.2.2 and the README still described an unclothed body with
splayed fingers, which is a defect in its own right: a shot whose README section is
stale is not a review.

Shots 6, 7 and 8 now describe the clothing, the dark card hair and the raised sun, and
their approvals are reset to pending because the appearance changed. 8/8 shots verify.
2026-11-09 14:23:47 +01:00
zwitschi bea6aa3e05 light: raise the sun so the character leaves building shadow (needs approval)
The skin was never grey. A red test colour reached the render and the slot was confirmed
holding M_Character_Skin, so the material and the assignment were both fine - the body
was simply standing in building shadow and lit almost entirely by the blue skylight
(intensity 2.6). Warm skin under blue fill reads blue-grey.

Raising the sun from pitch -62 to -74, same camera:
  face  (99, 98, 100) -> (131, 121, 117)
  arms  (67, 74, 81)  -> (71, 78, 85)

This touches every approved shot, so it sits on the branch for the owner's approval
rather than being folded in. Also records that the Unreal commandlet silently drops
unknown switches, which is why a --unlit-test flag never took effect.
2026-11-09 14:20:07 +01:00
zwitschi 7c57bf318b 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.
2026-11-09 13:55:19 +01:00
zwitschi 878d5a1f4e 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.
2026-11-09 13:50:21 +01:00
zwitschi 86e9c5b693 feat(clothing): tee, jeans and shoes as separate skinned meshes
Follows the character-artist clothing workflow literally: duplicate the body's own
faces for the garment base, solidify for cloth thickness, and keep each piece a
separate mesh. Attached as leader-pose components beside the hair, so no second rig.

- Tools/character_clothing_build.py builds three garments from the body mesh, trimmed
  geometrically (height band plus a width limit) instead of by dominant bone: the bone
  filter left holes wherever a face's strongest bone was outside the allowed list.
- Tools/ue_character_clothing_import.py imports them as CharacterTee / CharacterJeans /
  CharacterShoes, each arriving with its own single material slot.
- The pawn attaches all three, hidden with the body in first person.

Three traps recorded in MEMORY.md, each of which cost a build: MPFB weights its helper
geometry to groups that dominate every vertex, Blender refuses to apply a modifier to a
mesh with shape keys, and object-mode polygon selection is discarded on entering edit
mode - the delete wiped the whole garment.

Known defects, in TODO.md: the garments have coverage holes at shoulders, hips and
knees, and the hair reads light and wispy since its material was repointed to the
masked strand sheet.
2026-11-09 13:41:39 +01:00
zwitschi 85a8437cbc feat(character): ComfyUI blueprints and a render-vs-blueprint evaluation
Two tools that turn character work into something measurable.

- Tools/comfyui_character_blueprint.py generates a four-view sheet (front, side, back,
  head) plus a contact sheet from one description, with fixed seeds and a JSON record
  of prompts and prompt ids. First sheet: 'GigLine walker', lean pedestrian in a navy
  tee, jeans and sneakers.
- Tools/character_match.py compares a rendered character against a sheet: figure
  aspect, fill and coverage deltas, mean CIE76 colour distance, dominant-palette
  distance, and a side-by-side montage. The deltas stay separate on purpose - a single
  similarity score between a photoreal sheet and a low-detail body would be a number
  with no meaning.
- comfyui_generate.py grew wait_for_image() so both tools share the polling path
  instead of duplicating it.
- 13 unit tests for the colour maths, masks and palette; 95 tool tests pass.

First real result, recorded rather than smoothed over: aspect delta 1.24, fill delta
0.12, mean deltaE 31.9, palette deltaE 25.4. The montage shows the reason - the model
is a nude body and the blueprint wears a tee and jeans, so clothing dominates every
metric. Figure segmentation is explicit (--render-box): auto-detection picked backdrop
blobs on every attempt, and a background plate is the proper fix.
2026-11-09 13:36:28 +01:00
zwitschi 1fc5835327 fix(hair): cards follow the scalp normal, three overlapping rows
The first card build anchored every card on a fixed outward vector, so the silhouette
was a spiked crown. Each card is now oriented from the scalp normal at its anchor: the
length runs along the downward tangent, the width across it, and a small tip lift gives
the cut volume. Three overlapping rows (7/9/10) replace the single ring; overlap is
what stops cards reading as separate spikes.

26 cards, 401 verts, 224 quads, 468 tris, still all quads on one head bone.

Recorded as still open: the skin renders darker than the baked diffuse suggests, and
the generated pore and strand textures remain unwired because the material-editing
tool segfaults in the headless commandlet.
2026-11-09 13:25:40 +01:00
zwitschi 39f07b0f5f feat(hair): hair cards as their own skinned mesh, attached as a second component
Follows the character-artist skill on two points it is explicit about: hair for a
game character is a card system, and swappable parts stay separate meshes.

- Tools/character_hair_build.py builds SM_Char_Hair: a scalp cap plus 18 cards in
  three rings, quads only, every vertex weighted 1.0 to the head bone, UV-mapped for
  the strand sheet from Tools/generate_character_textures.py. 321 verts / 404 tris.
- Tools/ue_character_hair_import.py imports it as its own skeletal mesh, which is the
  point: joined into the body, Unreal collapses the second material slot and the hair
  renders with the skin material.
- The pawn carries it as a second USkeletalMeshComponent on SetLeaderPoseComponent, so
  it follows the head bone with no second rig and hides with the body in first person.

Still wrong and recorded: the cards point outward, so the silhouette reads as a spiked
crown rather than a short cut. The next pass orients them tangentially along the
skull with a second overlapping row. Tools/ue_character_materials.py also segfaults in
the headless commandlet when editing a material graph, so the generated textures are
not wired yet and the hair keeps its Blender base colour.
2026-11-09 13:18:40 +01:00
zwitschi 72d00bdfbd memory: ComfyUI API shape, SD's failure on flat swatches, the material-slot collapse 2026-11-09 13:10:57 +01:00
zwitschi 54c579ab1a feat(tex): ComfyUI tool, procedural character textures, hair cap behind a flag
Tools/comfyui_generate.py drives the local ComfyUI instance for generated textures
and writes a sidecar with model, prompt, seed and workflow, so a generated asset can
be regenerated or rejected instead of being an unexplained PNG. Models list and
generation both verified against 127.0.0.1:8188.

Stable Diffusion is not usable for flat skin swatches, and that is worth recording:
three attempts produced an anatomical close-up twice and a leather-scale surface once.
Skin detail is generated procedurally instead - tileable octave noise for pores, plus
a strand sheet for hair - in Tools/generate_character_textures.py, with sidecars.

Hair geometry exists (cap conforming to the head, weighted to the head bone, second
material slot in Blender) but Unreal's import collapses the second material slot, so
the cap renders with the skin material and reads as a bald skull. It is behind
--hair and off; the next step is a separate skeletal mesh attached as a second
component.
2026-11-09 13:09:39 +01:00
zwitschi 89a9c59747 Merge pull request #35: walk cycle starts at maximum stride
The stride shot regression was the clip's phase, not the capture harness: a sine starts and crosses zero at the two frames where the legs pass together.
2026-11-09 13:05:51 +01:00
zwitschi 4b9ad22108 fix(walk): the cycle starts at maximum stride, so the capture shows one
The stride shot regressed twice for two different reasons, and only the second was
the clip. The walk cycle was authored on a sine, which is zero at both the start and
the middle of the cycle - exactly the two frames where the legs pass together - so a
deterministic capture at any phase near those frames read as a still. The probe
proved it: frames 0 and 15 carried identical hip rotations while 7 and 22 were the
stride.

The cycle now starts at maximum stride (cosine). Length, amplitudes, keys and loop
are unchanged; only the phase moved. demo-third-person-walk is back as shot 8 and
shows a genuine stride with the curled hands from 0.2.1.

8/8 shots verify.
2026-11-09 12:55:53 +01:00
zwitschi 0d57b718cf Merge pull request #34: 0.2.0 + 0.2.1 — third-person body, animation, rig fixes
Third-person body with animated idle/walk, review harness for a deterministic stride capture, F3 debug overlay, and the 0.2.1 rig fixes (curled fingers, derived arm angle).
2026-11-09 12:53:28 +01:00
zwitschi e9f31f271e release: v0.2.1 — rig fixes changelog entry, TODO pruning
Writes the 0.2.1 entry (fingers, derived arm angle, base keys) and removes the
section from TODO.md. The stride-capture regression is listed under known
limitations rather than omitted: the shot is retired, not shipped.
v0.2.1
2026-11-09 12:52:01 +01:00
zwitschi 03af8146a6 fix(rig): curl the fingers, derive the arm angle
Two defects from the 0.2.0 review, both fixed and both measured.

- Fingers splayed because the bind is an A-pose hand. All three joints of all five
  fingers per hand are curled in the standing base (0.28/0.34/0.22 rad), with the
  curl axis probed on the rig first. The front shot now shows relaxed hands.
- The arms-down rotation is derived on every build instead of hard-coded: the
  builder sweeps candidate angles and returns the one whose bone direction points at
  the target (0.60 rad, direction dot 1.000 this build). The old 0.55 was a
  hand-swept constant.
- Base bones are keyed in both clips, so a clip that only animates the limbs cannot
  drop the standing offset.

Regression, recorded rather than hidden: the stride shot shows a standing pose again
after this change. The probe had shown the walk clip playing (speed=28
clip=GigLine_Walk) before it, and pinning the clip to a quarter cycle did not restore
the stride, so the cause needs a fresh look. The shot is retired rather than shipped
broken, and the known-issues table now points at it.
2026-11-09 12:48:40 +01:00
zwitschi 30e4d65743 todo: claim 0.2.1 rig fixes — splayed fingers, derived standing pose 2026-11-09 12:43:25 +01:00
zwitschi bc2e9c3b8e feat(hud): F3 debug overlay — FPS, frame time, worst frame, state
A top-left readout for performance work: smoothed FPS and frame time, the worst
frame in a rolling two-second window, the active view, player speed, a frame
counter and the player's position in metres from the project origin.

- F3 toggles it, through the console variable gigline.Debug so a packaged build can
  be checked without a rebuild (IA_ToggleDebug).
- On automatically for -perf and -qaselftest, so a performance capture never depends
  on pressing a key first; off in the built demo, because a player should not see an
  FPS counter over Hollywood Boulevard.
- The first two seconds are a warm-up window excluded from the worst-frame figure.
  Without it the readout showed "worst 400 ms" for a healthy run: the startup shader
  and streaming hitches dominated the rolling maximum.

Measured cost on the self-test route at 1440p: 2.64 to 2.77 ms average, about 5 %,
still 6x the floor. The release-check number stays the overlay-off run; the
overlay-on series is committed beside it as
Docs/perf/0.2.0-walk-frametimes-overlay.csv.

Docs: controls (F3 row, overlay section, switch table), release checks (cost row),
architecture building block view (HUD and input list).
2026-11-09 12:41:01 +01:00
zwitschi 12272d6b96 release: v0.2.0 — third-person body, animation, stride capture
Writes the 0.2.0 changelog entry with the measured numbers, removes the completed
section from TODO.md (its goal stays in ROADMAP.md) and updates the roadmap status.

Release checks: 8/8 shots verify with 6, 7 and 8 approved; 1440p at 2.64 ms average
(379 fps); -qaselftest passes walk, reset, pause, transect, wall and facade probes;
82 tool tests pass. Known limitations are listed rather than hidden: splayed fingers,
a swept base rotation and unproven camera collision, all marked for later patches.
v0.2.0
2026-11-09 12:30:49 +01:00
zwitschi 56e104f5f3 todo: close the 0.2.0 animation check, leaving only the tag
Both clips play and advance (idle proved by the single-node instance's asset time,
walk by shot 8), the body carries its skin material and the QA self-test passes.
The two cosmetic defects — splayed fingers and a base rotation that was swept rather
than derived — stay open as 0.2.2 items and are named in the check.
2026-11-09 12:30:00 +01:00
zwitschi 8e880ed09d perf: 0.2.0 re-measured at 1440p — 379 fps average, 6.3x the floor
-qaselftest at 2560x1440, TSR on, VSync off, no frame generation: 759 frames,
avg 2.64 ms (379 fps), p50 2.54, p95 3.39, p99 3.76, max 4.05 ms. The body, skin
material and animation add about 2 % over 0.1.5's 2.61 ms.

The same run passed every QA check: walk, reset 0 cm from spawn, pause/unpause,
transect 1456 samples with 0 holes, wall probe 0/35, facade probe 0/9.

Series committed as Docs/perf/0.2.0-walk-frametimes.csv and the result recorded in
the release-checks table next to the 0.1.0 and 0.1.5 numbers.
2026-11-09 12:29:46 +01:00