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.
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.
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.
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.
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.
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.
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.
-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.
Docs/controls.md documented the 0.1.0 bindings only, so the camera toggle added with
0.2.0 was undocumented for the player-facing set and the review switches were
undocumented everywhere. Adds the V row, a changes-since-0.1.0 note that first person
stays the default, and a table for -thirdperson / -walk / -qaselftest with the note
that the walking capture is driven per shot rather than by the switch.
The in-game HUD line gains 'V view' so the packaged build documents it too.
Shot 8 shows the body mid-stride in profile: legs apart, arms counter-swinging. The
traversal release check for 0.2.0 is closed with that evidence.
The reason earlier attempts froze on one pose was not the clip. A probe of the
single-node instance showed the clip advancing normally (assetTime 0.19 -> 1.03,
rate 1.0, playing) while the pawn's speed stayed 0 and the clip stayed Idle: the
call that enables walking sat in the shot sequencer's BeginPlay, which runs before
the player pawn exists, so the cast found nothing and silently did nothing. Moved
into the per-frame follow update, the same run logs walk=1 speed=28 clip=GigLine_Walk
and the stride appears.
Also: the walk expectation was missing from visual_targets.json after the earlier
retirement, so --verify was checking 7 shots while 8 were captured. Restored; 8/8
verify.
The shot actor can now photograph a moving pawn: a per-shot ShotFollow flag re-places
the camera every frame at a fixed offset in the pawn's frame, and the walk is driven
per shot (SetReviewWalk) with the pawn returned to spawn after a follow shot, so the
fixed-camera shots later in the list still find it in place. With that, the capture
stopped depending on where a 4.5 m/s pawn happened to be.
Two further bugs fixed on the way:
- the engine's screenshot folder is cleared before each run. The collect step copies
every review-*.png it finds in Saved/, so a shot retired from the camera list kept
reappearing with byte-identical content from an older run - which is how a walk shot
that was never captured looked like a captured one.
- UpdateBodyAnimation no longer calls SetAnimation every frame. Re-setting the same
clip restarts it, which freezes the body on frame 0.
What the deterministic capture then exposed: the walk clip is applied but does not
advance - every walking frame shows the same pose (arms forward, legs together). The
stride shot is retired again rather than shipped, and TODO.md names the next step:
log GetCurrentAssetTime across frames and check the imported PlayRate. The harness
stays wired for when the clip plays.
Shots 6 and 7 were re-rendered after these changes and still pass; 7/7 verify.
Screenshots 6 and 7 are approved, so the remaining 0.2.0 work is the traversal
release check. This adds the piece of harness it needs - `-walk` holds forward input
from the first frame, so the pawn actually crosses the sidewalk while the third-person
rig follows it - and records what that check still lacks.
A stride shot was attempted three times and removed: a fixed review camera cannot
catch a pawn moving at 4.5 m/s. Each attempt framed it at a different distance and
the last two missed it entirely, so shipping it would have been a broken shot with a
green verification. The TODO item now says the blocker is the capture harness (a
camera that tracks the pawn), not the game, and names where the shot goes back in.
The flag stays because it is what the traversal check will drive.
The character now stands naturally: arms at its sides with a soft elbow bend, hands
at the hips, feet flat. Both character shots confirm it, front and back.
The root cause was not the pose. The FBX importer had been silently refusing to
replace the existing assets: after changing the arm base from 1.55 rad to 0.55 rad
and rebuilding, the imported clip returned the IDENTICAL bone quaternion
(+0.006, +0.002, -0.179, -0.984). replace_existing=True was set and delete_asset
returned False even after chmod u+w, because the pawn's constructor hard-references
the clips. Importing under a new name (CharacterV2) created fresh assets and the
same probe then returned (+0.439, +0.898) — the rotation had landed. Every earlier
attempt had been invisible for this reason.
The rotation itself is measured, not inherited: sweeping it in Blender gives 0.55 rad
for arms down and clear of the body, 0.65 for straight down against the hip, 0.75 for
past vertical and into the torso. It lives in the clamp keys, not in the bind: every
bind experiment was impossible on this shape-keyed MPFB mesh or left Unreal showing
the old bind.
Remaining, for 0.2.2: the fingers splay from the bind, and the walk clip has still
not been captured in motion.
The character now stands with its arms at its sides instead of in a bind A-pose,
and there is a second camera to prove it: the third-person shot only ever showed
the pawn's back, so a front-facing pose defect was invisible in review.
demo-third-person-front camera on the road, ~6 m in front, looking back north
at the pawn; embedded in Screenshots/README.md as shot 7.
The pose work itself is a chain of measured findings, all recorded in TODO.md:
- clip data is exported relative to the REST pose, so arms-down offsets held only in
the keys arrived as a ±20 degree swing around an A bind;
- a rest-pose bake alone does not move the mesh: the MPFB mesh has shape keys, the
armature modifier cannot be applied while they exist, and the vertices stayed in
the A shape (X span unchanged at 1.051 m). Clearing the shape keys, baking the
deformation into the vertices and re-binding is what made bind and clips agree.
Not settled, and left as an open item rather than a claim: the base pose uses
inverted signs to compensate for the FBX axis mapping, the upper arms clip into the
torso, and the fingers splay. An aim-based pose module was written and then removed
- applied without the vertex bake it produced arms raised in a V, and a half-working
shared module is worse than none. The proper solve is 0.2.2: apply the standing pose
at character creation, then author clips against that bind.
The body stood in an A-pose while the log said the clip was playing. Two
independent defects, found by querying the imported asset instead of guessing:
1. PlayAnimation needs the component in AnimationSingleNode with a live
single-node instance. The body is hidden at construction and only unhidden by
the camera toggle, so the first call could land before the component was ready,
and the Wanted == PlayingAnimation guard then returned forever. UpdateBodyAnimation
now sets the mode, calls SetAnimation + Play, and re-asserts while the instance
is missing.
2. The arms-down offsets lived only in the clip keys. The exporter writes clip data
relative to the rest pose, so they arrived as a ±20 degree swing around an
A-posed bind - which is exactly what the pose probe measured. apply_base_pose now
bakes them into the rest pose before any clip is authored, the same trick
mpfb_character_export.py --pose idle uses. The table is applied with inverted
signs: as written, the FBX axis mapping produced arms raised in a V.
Shot 6 confirms it: the arms hang at the sides. Still open and recorded - the idle
pose reads awkwardly (arms forward, hands splayed) and the walk clip has no shot in
motion; both are marked in TODO.md and Screenshots/README.md.
Instrumentation removed; the diagnostics that remain are behind
gigline.SetupLogs.
Conflicts resolved by union, not by picking a side:
- AGENTS.md keeps the stronger visual-review rule (README updated with every visual
change; a stale section is a defect).
- render_screenshots.sh keeps the absolute engine path from main and adds
-thirdperson from this branch - both were needed and neither alone worked.
- Screenshots/README.md now lists six shots, retail and third-person both embedded;
the known-issues tables merged, with the animation defect and unproven camera
collision added to main's rows.
- visual_targets.json carries both expectations.
- Architecture docs: ADR-8 (realistic buildings) and ADR-9 (third-person body) both
present and in numeric order; ADR-8 was mis-placed before the Quality Requirements
heading in the combined document by an earlier edit, now fixed.
- Binary conflicts resolved to main's content, since the retail trim only exists
there.
Checks: 82 tool tests, prettier clean, six embeds resolve.
One pawn, both rigs (ADR-9): a skeletal body hidden in first person, a
collision-tested spring arm, and a toggle that swaps the active camera and the
body's visibility. Shipped default stays first person; -thirdperson starts the
pawn in the third-person rig so the review shot is reproducible, and the
screenshot run passes it.
Three defects found and two fixed:
- The skin material had no SkeletalMesh usage flag, so Unreal silently substituted
the default grey. That is why the body rendered as clay.
- render_screenshots.sh still called ./Engine/... on this branch (the fix lives on
the facade branch and never reached main); applied here so the shots generate.
- OPEN: the idle clip is bound and PlayAnimation is called - the run logs the clip
name - but the body still renders in bind pose, arms out. Recorded with that
evidence in TODO.md rather than called done.
Shot 5 (demo-third-person) added and documented; camera collision is implemented
but unproven, since the spawn has open space behind it.
Two parametric cycles authored at 30 fps instead of hand-keyed poses, so the loop
closes by construction: GigLine_Idle (60 f, breathing sway, feet planted) and
GigLine_Walk (30 f, one stride, hip 0.45 rad / knee 0.55 / arm 0.32, arms
counter-phased, torso counter-rotation).
The limb axes were measured, not assumed: a probe rotated upperleg01.L by 0.5 rad
on each axis and read foot.L, which moved -0.022 -> +0.362 in Y on X, twisted on Y
and abducted on Z. Using the wrong axis here would have produced a character
scissoring sideways, which no import error would have caught.
The character export preset now bakes animation. Without bake_anim the exporter
writes a static bind pose and Unreal imports a mesh with no AnimSequence and no
error, so the importer asserts a clip exists and prints VERDICT: PASS/FAIL.
Verified in /Game/Test/Character: Idle 1.967 s and Walk 0.967 s, both AnimSequences,
skeleton intact. Playback on a possessed pawn is not yet verified: the demo pawn has
no visible body, so that check belongs with the third-person camera item.
Writes the 0.1.6 entry, removes the completed section from TODO.md (its goal stays
in ROADMAP.md per the convention), and updates the roadmap status line.
Release checks: 5/5 shots verify, scene audit 0 failures, 82 tool tests, LFS check
clean, owner approval recorded for the retail shot.
Screenshots/README.md's known-issues list had no tickets, so the same defects kept
resurfacing as surprises. It is now a table with the issue, the version and item
that owns it, and a state.
Two entries were stale and are removed: the El Capitan Theatre marquee and blade
sign have been staged since ue_stage_theatre_trim.py landed (six trim actors in the
demo map), and 'materials are not yet created' contradicted the scene conventions
in the same file.
Moved out of 0.1.6, which is a patch and cannot carry new scope:
- wall lettering decal, moulded parapet profile, tower stepping and portrait
panels as textures -> 0.9.0 per-building models
- 6822 Budget (blocked: no photograph exists) -> 0.9.0 photography gap
The patch keeps what a patch can do: the trim that reads, the verified shot, the tag.
Filed with their proper owners: hero entrance doors and central-bay positioning ->
0.9.0 hero item; pale hero stone -> 0.9.0 measured material library; pavement
between kerb and building line -> 0.6.0 visual polish.
The 0.1.6 claim is released: nothing left on this branch but the owner's approval
and the tag.
The facade work started as a patch (0.1.4) and the 0.1.6 retail pass showed what
that costs: a patch has no release gate of its own, so a first pass that does not
yet read as the building can sit inside a done patch series. Realism is also the
leading quality goal, which a patch cannot carry.
New 0.9.0 - Realistic buildings, before the feature freeze: facade system with more
than one order, a profile library, storefront types, signage as decals, per-building
photo-matched models, a measured material library with a texel-density rule, and
LODs plus collision proxies. Former 0.9.0 freeze/beta/audit becomes 0.10.0 with
patches 0.10.1-0.10.3; 1.0.0 moves to weeks 39-44.
Recorded as ADR-8 (append-only) with a measurable gate BR-1..BR-5 in the quality
requirements, mirrored into the combined architecture.md. The 0.1.6 item now says
which of its gaps move to 0.9.0 and why more geometry cannot close them.
Each shot now has its own section with the image embedded, its view, expected
content and both verdicts, plus a status table at the top that links to them.
A path alone was not a review: the owner had to open five PNGs to judge a pass.
Also recorded in AGENTS.md (a path is not a review; approval ticks both the section
and the table row), the two retail assets and tools in the arc42 building block
view and the combined architecture.md, and prettier-formatted the changed files.
Models the two retail buildings beside the hero from their own photograph rather
than the generic kit: bronze cornice, mural row, lettering band, stepped Deco
parapet and rounded corner tower on 6800 Lee Drug; burgundy fascia, pilasters,
white-framed windows and banded parapet on 6806-6820. Reference analysis written
before any geometry (Art/blender/retail/ref-analysis.md), every unmeasured
proportion carried into the part report as an assumption.
6822 Budget is deferred, not guessed: no photograph of it exists and the 6.0 x 5.5 m
sliver cannot be read out of the neighbour shot.
Pipeline fixes found on the way:
- render_screenshots.sh called ./Engine/... for the capture step while every
commandlet used the absolute path; set -e killed the script at 127 *after* the
maps were rebuilt, so screenshots silently stayed a day old.
- the facade kit applies to every Frontage_*/North_* actor, so it pasted its own
language over the new modelled facades; it now skips them, and the trim stages as
RetailTrim_*.
- pure bay arithmetic moved to Tools/_common.py so it is unit-testable without
Blender (4 new tests, 82 total pass).
Known gaps recorded in Screenshots/README.md, not hidden: the parapet reads as a
blank band, murals and lettering do not resolve at review distance, the tower is a
plain cylinder, and the review camera crops the east end.
- Remove stale -active suffix from Asset production heading
- Shorten the sidewalks/collision item (details in MEMORY.md)
- Mark collision authoring pass in 0.1.1 as partially done
(terrain+site done via UGigLineAssetTools; buildings pending)
- Add TEMP QA diagnostic removal to release checks
- Add architecture documentation and doc toolchain as done
- Restore ODbL derivative-database posture as active [A]
Drop the OpenStreetMap/ODbL attribution requirement, the Google imagery rule,
and the related F-4 requirement, risk R-4, release check, and TODO items across
AGENTS, PLAN, ROADMAP, README, setup, architecture sections, and manifests.
OSM data is now documented as local private use only. Also normalize
numbered-list indentation in the architecture docs.
The reported fall-through was a misdiagnosis: the player never fell. The
imported Terrain_DEM had only a bounding-box simple collision proxy, so a
downward trace anywhere in the block hit the terrain at Z 819 (the mesh
bbox max) and the street sat under an invisible 8 m lid. Standing on that
box looked plausible, which is why it read as working.
- add UGigLineAssetTools (C++): force CTF_UseComplexAsSimple on a static
mesh. UE 5.8 dropped bUseComplexAsSimpleCollision and Python cannot set
the flag; the builder also saves the asset, since the flag lives there
and a map-only save silently reverts it.
- spawn (-13336, -1150, 600): inside the carved corridor (the cut is a
10.7 m half-width around the road centreline), lands on the road at 456.
- fix ResetToStart returning to the mid-air PlayerStart captured in
BeginPlay: StartLocation is now captured on the first grounded frame.
- HighResShot 1280x720 instead of "1280 720", which is a multiplier.
- add -qaselftest: scripted walk/reset/pause on a core ticker plus a
swept ground transect. Results: walk 746 cm/2 s with Z steady, reset
0 cm from spawn, pause/unpause 1/0, transect 84 samples 0 holes.
- rebase onto main, keeping the frontage materials and placement fixes.