10 Commits
Author SHA1 Message Date
zwitschi 101db3f2a4 chore: prepare repository for Gitea publication 2026-11-07 11:08:04 +01:00
zwitschi f4420de09c docs: record the Enhanced Input python traps and the gameplay status 2026-11-07 10:05:25 +01:00
zwitschi a0de0a6626 feat: first-person player with Enhanced Input
AGigLineCharacter: 1.75 m capsule, 1.60 m eye camera, 4 m/s walk, mouse look
scaled in degrees per second (no double DeltaTime), jump. Loaded ahead of a
Blueprint so the demo is playable with no assets wired by hand.

Tools/ue_create_input_assets.py builds /Game/Input headlessly: five 1D actions
and an 11-mapping context. One action per direction instead of per-key modifiers,
because modifier objects do not survive the save path here: a mapping's modifier
array serialises None, and dimensioning a 2D move with per-key axis masks would
need exactly those modifiers. The cost is four action assets rather than two.

Also adds a fourth review camera at eye height and documents it; its approval is
pending.
2026-11-07 10:05:13 +01:00
zwitschi b3815499e0 docs: embed screenshots with markdown image syntax in review README 2026-11-07 09:54:28 +01:00
zwitschi bbb3e147b3 tools: visual-review screenshot pipeline
- AGigLineShotActor (dev-only): walks a fixed camera list in -game mode,
  captures via FScreenshotRequest, quits. Test content only.
- ue_build_review_map.py builds /Game/Test/ReviewMap from current content
  (gray floor, key light, sky, MPFB character, 1 m scale cubes).
- render_screenshots.sh: build map -> run game -> collect PNGs into
  gitignored Screenshots/.
- Screenshots/README.md (committed) describes each capture with approval
  state; AGENTS.md gains the visual-review instructions.
2026-11-07 09:52:10 +01:00
zwitschi 774d810dab docs: retitle the roadmap status block to the current time 2026-11-07 09:31:03 +01:00
zwitschi f9f4f887dd docs: bring setup and toolchain in line with a finished bootstrap
Initial setup is complete: setup.md opens with a per-section status table and
drops the stale blockers, toolchain.md records that every pin has been exercised
on real work (engine compile + package, Blender/MPFB/Blosm assets, PROJ origin),
and MPFB's load-and-export check moves from pending to verified. Records the one
unexercised path — a fresh checkout — as a v0.1.0 release check.
2026-11-07 09:30:47 +01:00
zwitschi 1c6b956435 bootstrap: finish project bootstrap — scale test, MPFB character, build docs
- Static-mesh scale test passes: 1 m = 100.00 cm, origin preserved, axis
  mapping east→+X / north→−Y / up→+Z (named presets ue_metric_v1 both sides).
- MPFB 2.0.18 acceptance passes: generated human + standard 137-bone rig
  with weights + v2 skin baked to 1024² diffuse; imports as SkeletalMesh
  with Skeleton, Material, and Texture2D in /Game/Test/Character.
- MPFB headless requires XDG_CONFIG_HOME at the Flatpak config tree;
  documented in Art/blender/README.md, probe in Tools/addon_compat_probe.py.
- Docs/build.md completed: fresh-checkout build, packaging, headless checks.
- Docs/setup.md reconciled with reality; ROADMAP/bootstrap closed out.
2026-11-07 09:29:11 +01:00
zwitschi 01939de9a0 docs: add the memory convention and seed MEMORY.md
AGENTS.md now states that planned work is checkbox items in ROADMAP.md marked
open, in progress, or done, and that discoveries, gotchas, notes, and bugs go to
MEMORY.md for coordination between collaborators. MEMORY.md is seeded with what
this build-up actually cost: the Unreal read-only-home crash, Blender/Blosm
headless workarounds and the spherical projection, the tiled-TIFF DEM trap, the
reference licensing and throttling rules, and the LFS silent-blob trap.

Also stages the concurrently created Unreal project files, and stops per-project
Build/ output (FileOpenOrder logs) from entering the repository.
2026-11-07 09:08:58 +01:00
zwitschi ab3adf5f7c docs: reconcile the ROADMAP status block with the engine state 2026-11-07 08:59:15 +01:00
60 changed files with 1983 additions and 75 deletions

No files matched your search

+13
View File
@@ -39,13 +39,26 @@ Thumbs.db
# Credentials and environment — never committed
.env
.env.*
*.env
# Local, private reference material — never committed (see Docs/reference-notes-hollywood.md)
/Art/reference/
/Docs/data-inventory.md
/Docs/environment.md
/Docs/reference-notes-hollywood.md
/.agents/skills/blender-skills/
# Blender/Blosm working caches
/Art/blender/.cache/
# Scratch files
.tmp-*
# Unreal per-project build artefacts (FileOpenOrder logs and the like)
Build/
# Rendered screenshots for visual review — generated locally, never committed.
# The review README stays tracked; the PNGs do not.
/Screenshots/*
!/Screenshots/README.md
+39
View File
@@ -39,6 +39,7 @@ The initial release is **`0.1.0`**: a walkable exterior demo around **6840 Holly
- Track `.uasset`, `.umap`, `.blend`, and large source binaries with LFS.
- Ignore `Binaries/`, `Intermediate/`, `Saved/`, and Derived Data Cache.
- Never commit Unreal Engine source, credentials, or non-redistributable assets.
- Planned remote repository: `https://git.allucanget.biz/zwitschi/gig-line.git`. Do not assume it is configured or available.
## Release Policy
@@ -77,11 +78,49 @@ Always use a consistent scale test, named export presets, and repeatable import
- Keep restricted assets out of the public repository; prefer redistributable replacements.
- Review Epic distribution requirements before publishing builds.
## Visual review (screenshots)
Any change that affects the game's appearance — meshes, materials, lighting, levels, character, camera, post-processing — must be rendered for human review:
1. Run `Tools/render_screenshots.sh` (rebuilds `Game/GigLine/Content/Test/ReviewMap.umap` from current content, runs the game on it, captures the fixed camera list, copies the PNGs into `Screenshots/`).
2. Update `Screenshots/README.md`: one row per screenshot with what it shows and an approval state; describe any new shots or scene changes in the same change.
3. The PNGs are **never committed** — `.gitignore` keeps only `Screenshots/README.md`. Review happens on disk.
4. Wait for the user's approval before treating a visual change as done; record approval by ticking the table in `Screenshots/README.md`.
New content that needs a different camera angle gets a new entry in the shot list in `Tools/ue_build_review_map.py`.
## Roadmap
The living roadmap with per-version task checklists is in **[ROADMAP.md](ROADMAP.md)**.
- `ROADMAP.md` is the single, central TODO and progress list for all agents. Put planned work and actionable follow-ups there rather than creating parallel task lists.
- When working on a level-three (`###`) section, append ` - active - <sessionId>` to its heading, using your current session ID. Remove the suffix when you stop working on that section. Mark checklist items currently being worked on `- [-]` or `- [~]`; update the marks as their status changes.
- Planned work is listed as checkbox items in `ROADMAP.md`, marked **open** (`- [ ]`), **in progress/active** (`- [-]` or `- [~]`), or **done** (`- [x]`). Keep the marks accurate.
- Existing in-progress marks can describe unfinished work, not necessarily a live session; use the active heading and assignment details to identify current ownership.
- Each version is a `##` heading; task groups are `###` headings with `- [ ]` steps (`[-]` in progress, `[x]` done).
- When **all** steps in a version section are done: write a short descriptive entry to `CHANGELOG.md`, then remove the version section from `ROADMAP.md` to keep it concise.
- Finish each milestone's release checks **before** expanding scope. Cut map size and simulation complexity before compromising the core walking/driving experience.
- Define the release checks in `Docs/release-checks.md`. If a check fails, do not tag the release; open a patch task in `ROADMAP.md` and report the failing check in `CHANGELOG.md`.
## Parallel agent workflow
- Designate one coordinator to serialize assignments, updates to the shared roadmap, and integration into `main`. Agree on task and file or asset ownership before work starts; avoid assigning the same section or binary asset to multiple agents at once.
- The coordinator records the session ID, branch name, and owned files or assets alongside the active roadmap section and items on the shared `main` coordination copy. Publish and synchronize this state through the remote when agents use different machines. Branch-local changes are not visible to other worktrees until integrated; the roadmap is not an atomic lock.
- For parallel implementation, use a separate branch and Git worktree per agent, based on the agreed `main` revision. Do not switch branches in a directory another agent is using. Create branches, commits, pushes, and merges only when authorized; never overwrite another agent's uncommitted work.
- Keep ignored local references, generated files, build outputs, and Unreal editor processes in mind: worktrees do not automatically share ignored files, and shared output paths or concurrent editors can collide. Arrange required inputs and isolated outputs before running tools.
- Blender and Unreal binary assets cannot be meaningfully merged like text. Give each asset one owner; use Git LFS locks when supported by the remote, but do not assume locking is available.
- When work is ready, report the branch, changes, checks, unresolved issues, and relevant memory findings to the coordinator. Review and integrate branches into `main` one at a time, reconcile roadmap and memory changes, and update ownership and status. Follow the visual review and user-approval requirements before marking visual work done.
## Memory
**[MEMORY.md](MEMORY.md)** is shared session memory for agents and collaborators. Write to it whenever you:
- discover a fact about the environment, a tool, or a data source that cost time to establish;
- hit a gotcha, trap, or surprising failure mode, and what actually caused it;
- find or fix a bug, and how it was found so it can be found again;
- make a decision that does not fit a `ROADMAP.md` checkbox, such as a workaround, an assumption, or a rejected approach and why.
Keep entries short and dated, with the evidence: the command, the log line, or the file path. Anything that belongs in a formal document (setup steps, tool versions, licensing) goes in `Docs/` as well — memory is for coordination, not a replacement for the record. Do not let `MEMORY.md` become a second roadmap: planned work stays in `ROADMAP.md`.
Memory edits on an agent's branch are not shared live. Communicate important findings to the coordinator during work and reconcile them into the shared `main` copy when integrating.
+16 -1
View File
@@ -4,7 +4,7 @@ Editable `.blend` files. Everything here is regenerable or hand-editable, and ev
## Modelling skills
`.agents/skills/blender-skills/` holds 94 Blender skill packs (MIT, v1.3.0) — `blender-modeler`, `hard-surface`, `uv-workflow`, `collision-proxy`, `archviz`, and so on. They are instructions for the agent, not add-ons, and nothing in the pipeline depends on them. Provenance in `THIRD_PARTY_NOTICES.md`.
Optional local Blender skill packs are agent instructions, not add-ons; nothing in the pipeline depends on them. They are not distributed with the repository.
## Blockout
@@ -19,6 +19,7 @@ B=/var/lib/flatpak/app/org.blender.Blender/x86_64/stable/active/files
C="$HOME/.var/app/org.blender.Blender/config/blender/5.2"
LD_LIBRARY_PATH="$B/blender/lib" \
XDG_CONFIG_HOME="$HOME/.var/app/org.blender.Blender/config" \
BLENDER_USER_SCRIPTS="$C/scripts" \
"$B/blender/blender" -b --factory-startup \
--python Tools/blosm_blockout.py -- \
@@ -143,3 +144,17 @@ The extract is complete for the block but not for every relation inside its bbox
- Not cleaned, not subdivided into façade modules, not UV-mapped, not detailed.
- Materials are placeholders from Blosm's simple mode. Realistic mode needs the paid material pack and was not run.
- Terrain, kerbs, and crossing ramps are not in it: those come from the DEM cross-sections in `Geo/verification/2026-11-06-cross-sections.md`.
## Scale test and the Unreal frame (2026-11-07)
`Art/blender/scaletest/scale-test-2026-11-07.blend` — three 1 m cubes at origin, +10 m east, +10 m north, plus one red test material. Regenerate with `Tools/scale_test_export.py`; import with `Tools/ue_scale_test_import.py`. Both are the named-preset templates for production assets.
Export preset **`ue_metric_v1`** (Blender FBX): `apply_unit_scale`, `apply_scale_options='FBX_SCALE_NONE'`, `axis_forward='-Y'`, `axis_up='Z'`, `use_space_transform`, no bake-space-transform. Import preset **`ue_metric_v1`** (UE `FbxImportUI`): static mesh, import materials, uniform scale 1.0, no unit conversion, no force-front-X.
Measured result in Unreal centimetres: 1 m cube → exactly 100.00 cm; origin preserved. Axis mapping **east → +X, north → −Y, up → +Z**. The north mirror is inherent to the right-handed Blender → left-handed UE conversion; the FBX importer fixes triangle winding, so geometry is not visually mirrored. Compass conversions in game code must use north = −Y.
## Add-on compatibility (re-checked 2026-11-07)
`Tools/addon_compat_probe.py` loads both pinned add-ons headless and runs an MPFB smoke test. Result on Blender 5.2.0 LTS: Blosm 2.7.28 imports; MPFB 2.0.18 enables and `mpfb.create_human` generates a default human (19 158 verts, UVs, no materials, no armature — skeleton and baked materials are separate steps).
**Required invocation change:** every direct-binary headless run must now set `XDG_CONFIG_HOME="$HOME/.var/app/org.blender.Blender/config"`. Without it Blender resolves the extensions dir to `~/.config/blender/5.2/extensions`, where nothing is installed, and `bl_ext.blender_org.mpfb` is not discovered. Blosm is unaffected (it lives in `scripts/addons`, found via `BLENDER_USER_SCRIPTS`), but set the variable in all runs for consistency.
Binary file not shown.
Binary file not shown.
+62 -4
View File
@@ -1,6 +1,6 @@
# Build
Fresh-checkout build procedure. To be completed once the pinned engine is installed and a blank project has been packaged — see [setup.md](setup.md). Until then this file records only the constraints that are already known.
Fresh-checkout build procedure. Completed 2026-11-07 after the pinned engine was installed and a blank `Game/GigLine` build was packaged and launch-tested.
## Constraints
@@ -8,14 +8,72 @@ Fresh-checkout build procedure. To be completed once the pinned engine is instal
- Build output (`Binaries/`, `Intermediate/`, `Saved/`, `DerivedDataCache/`) is never committed.
- Large content is LFS-tracked, so a fresh clone needs `git lfs install && git lfs pull` before any build.
## Procedure (placeholder)
## Engine
Pinned install: **Unreal Engine 5.8.4** (CL 58902052, promoted build) at
`/run/media/system/DATA/Engines/UE_5.8.4`. The engine bundles **no** Linux clang; the system clang (23.1.4, Fedora 45) is the game-code toolchain. Game targets must use `BuildSettingsVersion.V7` to share the installed editor's build environment.
## Fresh-checkout build
```bash
git clone <remote> gig-line && cd gig-line
git lfs install && git lfs pull
# engine path, project generation, build, and packaging commands — fill in after setup
Tools/check-lfs.sh # must pass before any content build
# Compile the project's editor target (first build also proves the toolchain)
cd /run/media/system/DATA/Engines/UE_5.8.4
Engine/Build/BatchFiles/Linux/Build.sh GigLineEditor Linux Development \
-project="$PWD/../../Documents/06-games/gig-line/Game/GigLine/GigLine.uproject" \
-WaitMutex
# Expected tail: "Result: Succeeded"; module binary lands in
# Game/GigLine/Binaries/Linux/libUnrealEditor-GigLine.so
# Open in the editor (GUI, Vulkan)
./Engine/Binaries/Linux/UnrealEditor \
/run/media/system/DATA/Documents/06-games/gig-line/Game/GigLine/GigLine.uproject
```
Adapt the `-project=` path to the actual checkout location.
## Package a Linux build
```bash
cd /run/media/system/DATA/Engines/UE_5.8.4
Engine/Build/BatchFiles/RunUAT.sh BuildCookRun \
-project="/run/media/system/DATA/Documents/06-games/gig-line/Game/GigLine/GigLine.uproject" \
-noP4 -platform=Linux -clientconfig=Development \
-cook -allmaps -build -stage -pak \
-archive -archivedirectory="/run/media/system/DATA/Builds/GigLine" \
-unattended
# Expected: "BUILD SUCCESSFUL" (~1 min for the blank project; archive ~1.1 GiB)
# Launch test: /run/media/system/DATA/Builds/GigLine/Linux/GigLine.sh
```
The archive directory is deliberately **outside** the repository; never archive into the checkout.
## Headless content checks
Both content-acceptance tests run headless and are the template for all asset import verification:
```bash
# Static-mesh scale test (Blender export -> Unreal import -> measure)
# Blender: Tools/scale_test_export.py, Unreal: Tools/ue_scale_test_import.py
# MPFB character (generate -> rig -> bake -> export -> skeletal import)
# Blender: Tools/mpfb_character_export.py, Unreal: Tools/ue_character_import.py
# Unreal side of any test:
./Engine/Binaries/Linux/UnrealEditor-Cmd \
/run/media/system/DATA/Documents/06-games/gig-line/Game/GigLine/GigLine.uproject \
-run=pythonscript -script=<repo>/Tools/<script>.py -nullrhi -unattended -nopause -stdout
# Note: 5.8 swallows Python print() in the commandlet; scripts write result files to /tmp.
```
## Known engine quirks (2026-11-07)
- One handled `PSOPrecache` ensure fires at packaged-build exit only; not blocking.
- Interchange 5.8 does not create material/texture assets for skeletal FBX imports — `Tools/ue_character_import.py` finishes that chain explicitly.
- Android Turnkey status errors and datarouter DNS warnings in logs are benign.
## Packaged Linux build
Record here: the packaging command, output path, size, and the controls document path. The packaged build must launch and quit cleanly on the development workstation before `v0.1.0` is tagged.
- 2026-11-07: blank `Game/GigLine` Development build packaged via the command above; 1.1 GiB archive at `/run/media/system/DATA/Builds/GigLine/Linux`; `GigLine.sh` launched on the desktop session, Vulkan swapchain created, 759 frames rendered across a 60 s smoke test. Controls document to be written with the first playable (`0.1.0` Gameplay section).
+6 -6
View File
@@ -4,15 +4,17 @@ Licensing is tracked in four separate layers. Do not merge them; each has differ
| Layer | What it covers | Status |
| --- | --- | --- |
| Project code | `Game/Source`, `Tools/`, `Geo/` scripts | **Open decision** — proposal: MIT. Choose before the first public commit. |
| Project art | Blender sources, baked textures, exported meshes | Project-owned, shipped under the same terms as the code unless a texture says otherwise. |
| Project code | Project-owned code in `Game/Source`, `Tools/`, and `Geo/` scripts | [CC0 1.0](../LICENSE), chosen by the project owner. |
| Project art | Blender sources, baked textures, exported meshes | Project-owned art is [CC0 1.0](../LICENSE); third-party contributions retain their own terms. See the asset notices. |
| Engine | Unreal Engine | Proprietary / source-available. Not redistributable as engine source. Distribution follows Epic's terms. |
| Geographic data | OSM extract, elevation data | ODbL and the elevation provider's own licence. Attribution and share-alike review required before distributing a derived database. |
Project-owned documentation is also dedicated under [CC0 1.0](../LICENSE). Third-party material retains its own terms.
## Rules
1. An asset whose licence cannot be verified does not get committed. Record it as unresolved in [THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md) and replace it with a redistributable asset before the release it would ship in.
2. Reference photography is dev-only material and is **never committed**. It lives in the gitignored `Art/reference/` tree, is used for internal, private development only, and is not shipped or baked into shipped textures. What the images are evidence for is recorded in [reference-notes-hollywood.md](reference-notes-hollywood.md); the images stay outside version control.
2. Reference photography is dev-only material and is **never committed**. It lives in the gitignored `Art/reference/` tree, is used for internal, private development only, and is not shipped or baked into shipped textures. Local reference notes are also excluded from version control.
3. OSM is layout data only. Do not treat it as authoritative for façades, heights, or pavement detail, and do not import it as a texture or panorama source.
4. Never use extracted Google imagery, Street View, or geometry. No exception.
5. Attribution: "© OpenStreetMap contributors" with a link to <https://www.openstreetmap.org/copyright> appears in the shipped build's UI (a `0.1.0` release check) and in the notices.
@@ -20,11 +22,9 @@ Licensing is tracked in four separate layers. Do not merge them; each has differ
## Open decisions
- **Code licence.** MIT is proposed: short, compatible with the assets' separate terms, and no copyleft spill onto art. Decide before publishing the repository.
- **Documentation licence.** Default to the code licence unless a reason appears to separate them.
- **ODbL posture.** Decide whether distributed builds contain a derived geographic database (a modified OSM extract) or only rendered geometry. The answer changes the share-alike obligations for the shipped product. Record the decision here and in the release notes.
- **Reference photography retention.** Decided 2026-11-06: reference images are **not** committed. Licensing is not the gate for local, private development use, so images are simply kept out of the repository (`Art/reference/` is gitignored) and their provenance stays in the local sidecars. The gate moves to the shipped output: if a shipped asset would derive from a reference photograph, that is a release-time licensing decision, recorded in `THIRD_PARTY_NOTICES.md` before the asset ships.
## Provenance sources
Primary record: [THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md) for anything committed or shipped, and [reference-notes-hollywood.md](reference-notes-hollywood.md) for what the local-only reference images are evidence of. Per-file provenance for the local sets lives in `Art/reference/hollywood/SIDECAR.md` and in each folder's `sidecar.json` — local files, not committed.
Primary record: [THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md) for anything committed or shipped. Evidence notes and per-file provenance for local reference images remain private and untracked.
+36 -22
View File
@@ -8,12 +8,26 @@ Ordered procedure for bringing the workstation and the project to a buildable st
Anything marked **human step** needs credentials, licence acceptance, or a decision that cannot be automated. Never store Epic credentials, tokens, or licence keys in the repository.
## Status — 2026-11-07
**Initial setup is complete.** Sections 1 to 4 are done and verified; the project builds, packages, and imports Blender assets with the pinned toolchain. What remains in section 3 is one optional geo environment.
| Section | State |
| --- | --- |
| 1. System | done — Bazzite `45.20261105`, driver `615.78.08`, Vulkan verified, host build tools layered |
| 2. Engine | done — UE `5.8.4` at `/run/media/system/DATA/Engines/UE_5.8.4`, editor launches with Vulkan on the 5070 Ti |
| 3. Tooling pins | done except the optional geo venv; Blender 5.2, MPFB 2.0.18, Blosm 2.7.28, QGIS 4.2.3 all verified |
| 4. Project bootstrap | done — C++ project, packaged blank build, static-mesh scale test, MPFB character import |
| 5. Repository | procedure documented; a fresh checkout has not yet been exercised end to end |
Next: `0.1.0` content work (façade, block frontage, gameplay). Current board is [ROADMAP.md](../ROADMAP.md).
## 1. System
- `[x]` Update Bazzite with its supported mechanism.
`ujust update` (or `rpm-ostree upgrade`) followed by a reboot. Deployment `45.20261105` is booted and default; `44.20261026` retained as rollback.
- `[x]` Verify the NVIDIA driver and Vulkan from the desktop session.
`nvidia-smi` names the RTX 5070 Ti on driver `615.78.08` and the Wayland desktop renders through it. Vulkan enumerates it as GPU0: discrete, proprietary driver `615.78.08`, API `1.4.351`, conformance `1.4.3.3`. The integrated RADV GPU is GPU1 — watch that the engine does not select it. Evidence in [environment.md](environment.md).
`nvidia-smi` names the RTX 5070 Ti on driver `615.78.08` and the Wayland desktop renders through it. Vulkan enumerates it as GPU0: discrete, proprietary driver `615.78.08`, API `1.4.351`, conformance `1.4.3.3`. The integrated RADV GPU is GPU1 — watch that the engine does not select it.
- `[x]` Install `git-lfs`, `cmake`, `ninja-build`, `clang` as an ostree layer, reboot, then `git lfs install`.
Done: `git-lfs 3.7.1`, `clang 23.1.4`, `cmake 4.3.0`, `ninja 1.13.2`; lfs filter active, `Tools/check-lfs.sh` passes.
@@ -21,16 +35,15 @@ Anything marked **human step** needs credentials, licence acceptance, or a decis
- `[x]` **Human step.** Create an Epic account, accept the Unreal license.
Done on 2026-11-06 with the account email recorded locally; no credentials stored in the repository.
- `[-]` **Human step.** Acquire one pinned engine build. Two routes:
- `[x]` **Human step.** Acquire one pinned engine build. Two routes:
- **Launcher build**: install the Linux Epic Games Launcher, download the pinned engine version, install to the DATA volume, and disable auto-update so the pin holds.
- **Source build**: clone the pinned `release` branch from the Epic Games GitHub organisation, then `./Setup.sh`, `./GenerateProjectFiles.sh`, `make -j16`. Budget 120–200 GB and a multi-hour first build on 16 threads.
**5.8.4** downloaded to `/run/media/system/DATA/Downloads/Linux_Unreal_Engine_5.8.4.zip`: 39 819 212 076 bytes (37.08 GiB), ZIP central directory present with 299 959 entries, `Engine/Build/Build.version` reports **5.8.4**, changelist 58902052, `++UE5+Release-5.8`. Extracting to `/run/media/system/DATA/Engines/UE_5.8.4`.
The earlier 5.2.0 archive (19.59 GiB, CL 25360045) remains on disk as a fallback only.
**5.8.4** downloaded to `/run/media/system/DATA/Downloads/Linux_Unreal_Engine_5.8.4.zip`: 39 819 212 076 bytes (37.08 GiB), ZIP central directory present with 299 959 entries, `Engine/Build/Build.version` reports **5.8.4**, changelist 58902052, `++UE5+Release-5.8`.
- `[x]` Verify the archive end to end before extracting. `unzip -tq` on 2026-11-06: "No errors detected", exit 0.
- `[ ]` Extract to the DATA volume and record the install path. Plan: `mkdir -p /run/media/system/DATA/Engines/UE_5.2 && cd $_ && unzip -q <zip>`; engine root becomes `/run/media/system/DATA/Engines/UE_5.2/Engine`. Roughly 55–60 GB extracted, so keep it off the repository volume tree.
- `[ ]` Resolve the engine version: 5.2.0 is what is on disk, 5.8.x was the planned pin. Record the decision and its reasons.
- `[ ]` Record the exact engine version and build route in [toolchain.md](toolchain.md).
- `[ ]` Validate that the engine editor opens and a blank project runs on Bazzite. Unreal does not support this distribution; record any workarounds.
- `[x]` Extract to the DATA volume and record the install path. Done 2026-11-07: `/run/media/system/DATA/Engines/UE_5.8.4`, 73 GiB on disk, extraction verified by full headless editor start.
- `[x]` Resolve the engine version: **5.8.4 pinned** (see the decision below). The 5.2.0 archive (19.59 GiB, CL 25360045) remains on disk as a fallback only and should be deleted once the pin is trusted.
- `[x]` Record the exact engine version and build route in [toolchain.md](toolchain.md). Done: 5.8.4, Epic download route, CL 58902052.
- `[x]` Validate that the engine editor opens and a blank project runs on Bazzite. Done 2026-11-07: headless `-NullRHI` smoke test **and** full editor GUI launch with Vulkan RHI on the RTX 5070 Ti (loader 1.4.350, `VULKAN_SM5`, map check 0 errors). No distribution workarounds were needed; only benign log noise (Android Turnkey status, telemetry DNS failures).
### Engine version decision
@@ -41,24 +54,24 @@ The archive on disk is **Unreal Engine 5.2.0**, released May 2023, three and a h
- No engine fixes from 5.3 through 5.8, and no compatibility with content or plugins authored for newer versions.
- Bazzite is not a supported Unreal distribution in any version, so this risk compounds rather than introduces.
**Decision taken 2026-11-06, archive delivered 2026-11-07:** the current stable release is **5.8.4**, and that is what will be pinned. The 5.2.0 archive stays only until 5.8.4 launches on Bazzite, then gets deleted; two engines on disk invite accidental mixing. The 5.2.0 costs listed above are why the newer build was preferred.
**Decision taken 2026-11-06, archive delivered 2026-11-07, extracted and validated 2026-11-07:** the current stable release is **5.8.4**, and that is the pin. The 5.2.0 archive stays only until the 5.8.4 pin is trusted through asset production, then gets deleted; two engines on disk invite accidental mixing. The 5.2.0 costs listed above are why the newer build was preferred. Note: this 5.8.4 build ships **no** bundled Linux clang — the system clang (23.1.4) is the game-code toolchain, accepted by the first C++ compile and by the packaged build.
## 3. Tooling pins
- `[x]` Blender 5.2 LTS (Flatpak) and MPFB 2.0.18 recorded.
- `[x]` Install QGIS and record its version. QGIS **4.2.3**, Flatpak `stable`, runtime `org.kde.Platform 6.11`, bundled PROJ `9.9.0`. The stale `lts` 3.28.9 install is still beside it and should be uninstalled.
- `[x]` Install QGIS and record its version. QGIS **4.2.3**, Flatpak `stable`, runtime `org.kde.Platform 6.11`, bundled PROJ `9.9.0`. The stale `lts` 3.28.9 install was uninstalled 2026-11-07; only `stable` remains.
- `[x]` Install Blosm in Blender, record its version, and confirm it loads on Blender 5.2. Blosm **2.7.28** imports the block extract headless in `3Dsimple` mode; the exact command and the two required headless workarounds are in `Art/blender/README.md`. No second Blender version was needed.
- `[ ]` Confirm the geo tool Python environment (venv with `pyproj`, `GDAL`, `shapely` from the QGIS or distrobox environment) and record it.
- `[ ]` **Optional.** Confirm the geo tool Python environment (venv with `pyproj`, `GDAL`, `shapely`). Not needed so far: every geo script in `Geo/scripts/` is standard-library only, and PROJ 9.9.0 is invoked from the QGIS Flatpak (`LD_LIBRARY_PATH` trick in `Geo/manifests/local-origin.md`). Add the venv only when a step genuinely needs `pyproj` or GDAL in-process.
## 4. Project bootstrap
- `[ ]` Create the C++ Unreal project at `Game/GigLine/GigLine.uproject` with Enhanced Input enabled and Blueprint gameplay enabled. Commit `Config/`, `Source/`, and `GigLine.uproject`; leave `Binaries/`, `Intermediate/`, `Saved/`, and the DDC out of the repository — `.gitignore` already covers them.
- `[ ]` Point the local Derived Data Cache at the DATA volume. Confirm the setting name and section against the pinned engine's documentation rather than copying a config snippet from another version.
- `[ ]` Enable Git LFS integration in the editor before importing content, so large assets are routed to LFS on import rather than on a later re-add.
- `[ ]` Package and launch a blank Linux build from the command line, so the packaging path is proven before any content exists.
- `[ ]` Export one static mesh from Blender with a known metric size and import it into Unreal. Verify the 100× scale conversion (1 m → 100 uu), the pivot, normals, and material assignment.
- `[ ]` Export one MPFB character with baked materials and a deform skeleton and verify it imports with the expected skeleton hierarchy and scale.
- `[ ]` Commit the baseline and write the fresh-checkout build procedure into [build.md](build.md).
- `[x]` Create the C++ Unreal project at `Game/GigLine/GigLine.uproject` with Enhanced Input enabled and Blueprint gameplay enabled. Created 2026-11-07: primary game module + default game mode, Enhanced Input plugin and input classes in `Config/DefaultInput.ini`. `Config/`, `Source/`, and `GigLine.uproject` are committed; `Binaries/`, `Intermediate/`, `Saved/`, and the DDC stay out via `.gitignore`.
- `[x]` Point the local Derived Data Cache at the DATA volume. The engine lives on the DATA volume and installed builds default `LocalDDC` under the engine root, so the DDC is already off the repository SSD tree. Revisit only if a project-local DDC is later configured.
- `[x]` Enable Git LFS integration in the editor before importing content, so large assets are routed to LFS on import rather than on a later re-add. Satisfied by repo-level `.gitattributes` routing: content is added via `git add` (CLI or editor), and the attributes send `.uasset`/`.umap`/`.blend` to LFS regardless of which tool wrote them. `Tools/check-lfs.sh` is the guard.
- `[x]` Package and launch a blank Linux build from the command line, so the packaging path is proven before any content exists. Done 2026-11-07: UAT `BuildCookRun` succeeded in 56 s; 1.1 GiB archive at `/run/media/system/DATA/Builds/GigLine`; `GigLine.sh` rendered 759 frames across a 60 s smoke test. Full procedure in [build.md](build.md).
- `[x]` Export one static mesh from Blender with a known metric size and import it into Unreal. Verified 2026-11-07 with the three-cube calibration scene: 1 m → exactly 100.00 cm, origin preserved, material imported; axis mapping east → +X, north → −Y, up → +Z. Presets and scripts in [build.md](build.md) and `Art/blender/README.md`.
- `[x]` Export one MPFB character with baked materials and a deform skeleton and verify it imports with the expected skeleton hierarchy and scale. Done 2026-11-07: MPFB 2.0.18 generated human (19 158 verts) + standard rig (137 bones, weights) + v2 skin baked to a 1024² diffuse; imported into Unreal as a `SkeletalMesh` with a 138-bone `Skeleton`, a `Material` carrying the baked `Texture2D`, in `/Game/Test/Character`.
- `[x]` Commit the baseline and write the fresh-checkout build procedure into [build.md](build.md). Done 2026-11-07.
## 5. Repository
@@ -74,12 +87,13 @@ Without `git lfs install`, adding an LFS-matched file does not error — it comm
Sizes: the engine lives outside the repository (pinned install path recorded in `toolchain.md`). The repository holds the Unreal project, Blender sources, geo sources, and tooling.
**Fresh checkout not yet tested end to end.** The build procedure in [build.md](build.md) was produced from a working tree, not from a clean clone. Exercise it before `v0.1.0` is tagged — it is a release check on the board.
## Known blockers
| Blocker | Owner | Needed for |
| --- | --- | --- |
| Epic account, license acceptance, engine download | human | All Unreal work |
| QGIS pin: replace the stale `3.28.9` flatpak | human | Geo pipeline |
| Blosm install and Blender 5.2 compatibility check | human | OSM blockouts |
| Delete the fallback 5.2.0 engine archive (`/run/media/system/DATA/Downloads/Linux_Unreal_Engine_5.2.0.zip`) once the 5.8.4 pin is trusted through asset production | agent | Disk hygiene (19.6 GiB), pin safety |
| Fresh-checkout build exercised against the documented procedure | agent | `v0.1.0` release check |
Closed: Bazzite update and reboot, driver and Vulkan verification, `git-lfs`/`clang`/`cmake`/`ninja` layering.
Closed 2026-11-07: Epic account and engine download, QGIS `lts` removal, Blosm and MPFB on Blender 5.2 (headless invocation fix in `Art/blender/README.md`), engine validation on Bazzite, full project bootstrap. Earlier closed: Bazzite update and reboot, driver and Vulkan verification, `git-lfs`/`clang`/`cmake`/`ninja` layering.
+8 -6
View File
@@ -4,16 +4,18 @@ Pinned versions for this project. Record the exact build string a tool reports,
Audited 2026-11-06, updated 2026-11-07 on the development workstation.
**State:** initial setup is complete. Every pinned tool below has been exercised on real work — the engine compiled game code and packaged a build, Blender/MPFB/Blosm produced committed assets, and QGIS's PROJ produced the project origin. The only unexercised path is a fresh checkout, tracked as a `v0.1.0` release check.
## Pins
| Component | Pinned | Source | Status |
| --- | --- | --- | --- |
| Bazzite | `45.20261105` (Kinoite, `bazzite-dx-nvidia`), kernel `7.2.15-ogc1.1.fc45` | `ujust update` / `rpm-ostree upgrade` | Booted and default. `44.20261026` retained as rollback. |
| NVIDIA driver | `615.78.08`, open kernel module | Bazzite image | Verified: desktop session renders through the RTX 5070 Ti; Vulkan GPU0 is the RTX 5070 Ti on driver `615.78.08`, conformance `1.4.3.3`. |
| Unreal Engine | `5.8.4` — CL 58902052, `++UE5+Release-5.8`, promoted build | Epic download | Fully extracted to `/run/media/system/DATA/Engines/UE_5.8.4` (73 GiB on disk); headless smoke test passed 2026-11-07 (`UnrealEditor-Cmd -nullrhi -run=BlankProgram`, clean startup and shutdown). Pin is 5.8.4; formal validation completes when the editor GUI launches on Bazzite. The verified 5.2.0 archive stays as fallback until then. |
| Unreal Linux toolchain | system `clang 23.1.4` (Fedora 45) | `rpm-ostree` layer | This 5.8.4 build ships **no** bundled Linux clang (no `clang++` anywhere in the engine tree, verified 2026-11-07); UBT compiles game code with the system toolchain. Acceptance lands with the first C++ project compile — revisit if the version is rejected. |
| Unreal Engine | `5.8.4` — CL 58902052, `++UE5+Release-5.8`, promoted build | Epic download | Fully extracted to `/run/media/system/DATA/Engines/UE_5.8.4` (73 GiB on disk). **Validated 2026-11-07:** headless `-NullRHI` smoke test and full editor GUI launch with Vulkan RHI on the RTX 5070 Ti (loader 1.4.350, `VULKAN_SM5`, map check 0 errors). Pin is final. The verified 5.2.0 archive stays as fallback. |
| Unreal Linux toolchain | system `clang 23.1.4` (Fedora 45) | `rpm-ostree` layer | This 5.8.4 build ships **no** bundled Linux clang (no `clang++` anywhere in the engine tree, verified 2026-11-07); UBT compiles game code with the system toolchain. **Accepted 2026-11-07:** game-module compile and link (`GigLineEditor`, `libUnrealEditor-GigLine.so`) and a packaged Development build. Game targets use `BuildSettingsVersion.V7` + `EngineIncludeOrderVersion.Latest` (`Source/GigLine.Target.cs`, `Source/GigLineEditor.Target.cs`) to share the installed editor's build environment. |
| Blender | `5.2.0` LTS (build 2026-07-14) | Flatpak `org.blender.Blender` | Installed. Run headless from the app tree with `LD_LIBRARY_PATH` + `BLENDER_USER_SCRIPTS`; `flatpak run` is unavailable to the agent shell. |
| MPFB | `2.0.18` (extension `mpfb`, declares Blender >= 4.2) | Blender extensions platform | Installed. Load-and-export check on 5.2 still pending. |
| MPFB | `2.0.18` (extension `bl_ext.blender_org.mpfb`, `blender_version_min 4.2.0`) | Blender extensions platform | Installed. **Fully verified 2026-11-07:** generates a default human (19 158 verts, UVs), standard 137-bone rig with weights, and a baked 1024² skin; the character imports into Unreal as a `SkeletalMesh` with Skeleton, Material, and Texture. Headless runs must set `XDG_CONFIG_HOME=$HOME/.var/app/org.blender.Blender/config` or the extension is not discovered (`Art/blender/README.md`). |
| Blosm | `2.7.28` (legacy add-on, `bl_info.blender = (4, 5, 0)`, manifest min 4.2) | blosm.com | Installed and importing on Blender 5.2 in `3Dsimple` mode. Two headless workarounds are required; see `Art/blender/README.md`. Paid material asset pack lives in `~/Downloads/blosm-assets/` and is not committed. |
| QGIS | `4.2.3`, Flatpak `stable`, runtime `org.kde.Platform 6.11`; bundled PROJ `9.9.0` | flathub | Installed 2026-11-06. Stale `lts` branch (`3.28.9`, EOL Qt5 runtime) uninstalled 2026-11-07; only `stable` remains. |
| Git | `2.56.0` | Bazzite image | Installed. |
@@ -31,12 +33,12 @@ Unreal Engine 5.9 is in preview and Unreal Engine 6 has been announced as of Nov
| --- | --- |
| Package | blender-skills `1.3.0`, upstream commit `8f778d2`, MIT |
| Source | <https://github.com/arjun988/blender-skills> |
| Location | `.agents/skills/blender-skills/` — 94 skills plus a shared `references/` directory, 269 Markdown files, 1.2 MB |
| Location | Local only: `.agents/skills/blender-skills/` — not distributed with the repository |
| Installed | 2026-11-06, copied from a local checkout of `.claude/skills/` without edits |
These are instructions and reference notes for agents, not code that runs in a build. Four of them assume a **Blender MCP server**; this project drives Blender headlessly through `bpy` (`Tools/*.py`) instead, so MCP is optional and nothing depends on it.
To update: copy `.claude/skills/` from a newer upstream checkout over the existing directory, then record the new version and commit hash in this table. Skills are read on demand, so the harness catalog may need a reload before they appear as first-class entries.
To update: copy `.claude/skills/` from a newer upstream checkout over the local directory, then record the new version and commit hash in this table. Skills are read on demand, so the harness catalog may need a reload before they appear as first-class entries.
## Pin rules
@@ -48,6 +50,6 @@ To update: copy `.claude/skills/` from a newer upstream checkout over the existi
## Open compatibility risks
1. **Blosm on Blender 5.2 — worked around, not resolved.** `3Dsimple` import works headless (see the blockout). Two headless-only defects are patched around in the script rather than in the add-on: an asset-directory check that fires before `mode` is honoured, and a background-mode import path that uses a broken relative import when the default level table is empty. `3Drealistic` mode has not been exercised; it loads the paid material pack and previously failed inside the realistic renderer setup. If realistic materials are wanted, test that mode separately before depending on it.
2. **MPFB 2.0.18 on Blender 5.2.** The extension declares Blender >= 4.2 but predates 5.x. The acceptance test is the `0.1.0` requirement to export one MPFB character with baked materials and a deform skeleton and import it into Unreal.
2. **MPFB 2.0.18 on Blender 5.2.** The extension declares Blender >= 4.2 but predates 5.x. Generation verified 2026-11-07: `mpfb.create_human` works headless once `XDG_CONFIG_HOME` points at the Flatpak config tree. Remaining acceptance test is the `0.1.0` requirement to export one MPFB character with baked materials and a deform skeleton and import it into Unreal.
3. **QGIS 4.x recipes.** Only `stable` (4.2.3) is installed now; the old `lts` was removed 2026-11-07. QGIS 4.x uses Qt6, so any processing recipe copied from a 3.x tutorial needs re-checking before it is trusted.
4. **Driver and Vulkan validation on Bazzite.** Not an officially supported Unreal distribution. The engine install itself is proven (full init headlessly); only the Vulkan RHI on the RTX 5070 Ti is unproven, and one desktop-terminal run settles it.
+22
View File
@@ -0,0 +1,22 @@
[/Script/EngineSettings.GameMapsSettings]
GlobalDefaultGameMode="/Script/GigLine.GigLineGameModeBase"
[/Script/Engine.RendererSettings]
; Leave defaults for 0.1.0; quality preset is documented at release time.
[URL]
GameName=GigLine
[/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings]
bEnablePlugin=False
bAllowNetworkConnection=False
bIncludeInShipping=False
bAllowExternalStartInShipping=False
bCompileAFSProject=False
bUseCompression=False
bLogFiles=False
bReportStats=False
ConnectionType=USBOnly
bUseManualIPAddress=False
ManualIPAddress=
+9
View File
@@ -0,0 +1,9 @@
[/Script/EngineSettings.GeneralProjectSettings]
ProjectName=Gig Line
CompanyName=
CopyrightNotice=
[/Script/UnrealEd.ProjectPackagingSettings]
BuildConfiguration=PPBC_Development
FullRebuild=False
UsePakFile=True
+5
View File
@@ -0,0 +1,5 @@
[/Script/Engine.InputSettings]
DefaultPlayerInputClass=/Script/EnhancedInput.EnhancedPlayerInput
DefaultInputComponentClass=/Script/EnhancedInput.EnhancedInputComponent
bEnableLegacyInputScales=False
bEnableDefaultMapping=1
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.
+19
View File
@@ -0,0 +1,19 @@
{
"FileVersion": 3,
"EngineAssociation": "5.8",
"Category": "",
"Description": "Linux-first walking/driving demo around 6840 Hollywood Blvd. See repository AGENTS.md and ROADMAP.md.",
"Modules": [
{
"Name": "GigLine",
"Type": "Runtime",
"LoadingPhase": "Default"
}
],
"Plugins": [
{
"Name": "EnhancedInput",
"Enabled": true
}
]
}
+13
View File
@@ -0,0 +1,13 @@
using UnrealBuildTool;
using System.Collections.Generic;
public class GigLineTarget : TargetRules
{
public GigLineTarget(TargetInfo Target) : base(Target)
{
Type = TargetType.Game;
DefaultBuildSettings = BuildSettingsVersion.V7;
IncludeOrderVersion = EngineIncludeOrderVersion.Latest;
ExtraModuleNames.Add("GigLine");
}
}
@@ -0,0 +1,18 @@
using UnrealBuildTool;
public class GigLine : ModuleRules
{
public GigLine(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[]
{
"Core",
"CoreUObject",
"Engine",
"InputCore",
"EnhancedInput"
});
}
}
+4
View File
@@ -0,0 +1,4 @@
#include "GigLine.h"
#include "Modules/ModuleManager.h"
IMPLEMENT_PRIMARY_GAME_MODULE(FDefaultGameModuleImpl, GigLine, "GigLine");
+3
View File
@@ -0,0 +1,3 @@
#pragma once
#include "CoreMinimal.h"
@@ -0,0 +1,210 @@
#include "GigLineCharacter.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/PlayerController.h"
#include "InputAction.h"
#include "InputMappingContext.h"
AGigLineCharacter::AGigLineCharacter()
{
PrimaryActorTick.bCanEverTick = false;
// 1.75 m tall, 0.42 m wide, human scale in Unreal units (1 m = 100 uu).
GetCapsuleComponent()->InitCapsuleSize(42.0f, 87.5f);
// Move relative to where the player is looking, not where the actor points.
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = true;
bUseControllerRotationRoll = false;
UCharacterMovementComponent* Movement = GetCharacterMovement();
Movement->bOrientRotationToMovement = false;
Movement->MaxWalkSpeed = WalkSpeedMetresPerSecond * 100.0f;
Movement->JumpZVelocity = 420.0f;
Movement->AirControl = 0.2f;
Movement->BrakingDecelerationWalking = 2048.0f;
Movement->MaxStepHeight = 45.0f;
FirstPersonCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FirstPersonCamera"));
FirstPersonCamera->SetupAttachment(GetCapsuleComponent());
FirstPersonCamera->SetRelativeLocation(FVector(0.0f, 0.0f, 72.5f)); // eye height 1.60 m
FirstPersonCamera->bUsePawnControlRotation = true;
// An eye-level mesh is not needed for 0.1.0; the body is a capsule with a camera.
GetMesh()->SetHiddenInGame(true);
GetMesh()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
// Input assets are created by Tools/ue_create_input_assets.py. They are loaded in
// BeginPlay rather than the constructor: the constructor runs during CDO
// construction where asset loads are unreliable, and a missing asset should be
// a loud warning, not a silent no-op.
}
void AGigLineCharacter::LoadInputAssets()
{
auto LoadSoft = [](const TCHAR* Path) -> TObjectPtr<UObject>
{
return LoadObject<UObject>(nullptr, Path);
};
MappingContext = Cast<UInputMappingContext>(LoadSoft(TEXT("/Game/Input/IMC_Default.IMC_Default")));
MoveAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_Move.IA_Move")));
StrafeAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_Strafe.IA_Strafe")));
MoveBackAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_MoveBack.IA_MoveBack")));
StrafeLeftAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_StrafeLeft.IA_StrafeLeft")));
LookAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_Look.IA_Look")));
JumpAction = Cast<UInputAction>(LoadSoft(TEXT("/Game/Input/IA_Jump.IA_Jump")));
if (!MappingContext || !MoveAction || !MoveBackAction || !StrafeAction || !StrafeLeftAction
|| !LookAction)
{
UE_LOG(LogTemp, Warning,
TEXT("GigLine: input assets missing (context=%s move=%s look=%s). ")
TEXT("Run Tools/ue_create_input_assets.py to build /Game/Input."),
MappingContext ? TEXT("ok") : TEXT("missing"),
(MoveAction && MoveBackAction && StrafeAction && StrafeLeftAction) ? TEXT("ok")
: TEXT("missing"),
LookAction ? TEXT("ok") : TEXT("missing"));
}
}
void AGigLineCharacter::BeginPlay()
{
Super::BeginPlay();
LoadInputAssets();
// Push the mapping context for this local player.
if (MappingContext)
{
if (const APlayerController* PC = Cast<APlayerController>(GetController()))
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem =
ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer()))
{
Subsystem->AddMappingContext(MappingContext, 0);
}
}
}
}
void AGigLineCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
UEnhancedInputComponent* Input = Cast<UEnhancedInputComponent>(PlayerInputComponent);
if (!Input)
{
UE_LOG(LogTemp, Error, TEXT("GigLine: player input component is not Enhanced Input"));
return;
}
// One action per direction keeps the sign explicit: W and S differ only by
// which handler runs, not by a modifier in the asset.
if (MoveAction)
{
Input->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AGigLineCharacter::MoveForward);
}
if (MoveBackAction)
{
Input->BindAction(MoveBackAction, ETriggerEvent::Triggered, this, &AGigLineCharacter::MoveBack);
}
if (StrafeAction)
{
Input->BindAction(StrafeAction, ETriggerEvent::Triggered, this, &AGigLineCharacter::MoveStrafe);
}
if (StrafeLeftAction)
{
Input->BindAction(StrafeLeftAction, ETriggerEvent::Triggered, this, &AGigLineCharacter::MoveLeft);
}
if (LookAction)
{
Input->BindAction(LookAction, ETriggerEvent::Triggered, this, &AGigLineCharacter::Look);
}
if (JumpAction)
{
Input->BindAction(JumpAction, ETriggerEvent::Started, this, &AGigLineCharacter::StartJump);
Input->BindAction(JumpAction, ETriggerEvent::Completed, this, &AGigLineCharacter::StopJump);
}
}
namespace
{
/** One-dimensional action value; S and A arrive positive and are negated here. */
float SignedAxis(const FInputActionValue& Value, bool bNegativeKey)
{
float Axis = 0.0f;
switch (Value.GetValueType())
{
case EInputActionValueType::Boolean:
Axis = Value.Get<bool>() ? 1.0f : 0.0f;
break;
case EInputActionValueType::Axis1D:
Axis = Value.Get<float>();
break;
default:
Axis = Value.Get<FVector2D>().Y;
break;
}
return bNegativeKey ? -Axis : Axis;
}
}
void AGigLineCharacter::MoveForward(const FInputActionValue& Value)
{
ApplyMove(FMath::Abs(SignedAxis(Value, false)), 0.0f);
}
void AGigLineCharacter::MoveBack(const FInputActionValue& Value)
{
ApplyMove(-FMath::Abs(SignedAxis(Value, false)), 0.0f);
}
void AGigLineCharacter::MoveStrafe(const FInputActionValue& Value)
{
ApplyMove(0.0f, FMath::Abs(SignedAxis(Value, false)));
}
void AGigLineCharacter::MoveLeft(const FInputActionValue& Value)
{
ApplyMove(0.0f, -FMath::Abs(SignedAxis(Value, false)));
}
void AGigLineCharacter::ApplyMove(float Forward, float Strafe)
{
const AController* Controller = GetController();
if (!Controller || (FMath::IsNearlyZero(Forward) && FMath::IsNearlyZero(Strafe)))
{
return;
}
// Forward/right on the controller yaw keeps the capsule upright on slopes.
const FRotator YawRotation(0.0f, Controller->GetControlRotation().Yaw, 0.0f);
const FVector ForwardDir = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
const FVector RightDir = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
AddMovementInput(ForwardDir, Forward);
AddMovementInput(RightDir, Strafe);
}
void AGigLineCharacter::Look(const FInputActionValue& Value)
{
// Enhanced Input applies the mapping's scalar modifier, which already scales by
// DeltaTime; scaling again by DeltaTime would make the mouse frame-rate dependent.
const FVector2D Axis = Value.Get<FVector2D>();
AddControllerYawInput(Axis.X * LookSensitivity);
AddControllerPitchInput(Axis.Y * LookSensitivity * (bInvertLookY ? -1.0f : 1.0f));
}
void AGigLineCharacter::StartJump()
{
Jump();
}
void AGigLineCharacter::StopJump()
{
StopJumping();
}
@@ -0,0 +1,86 @@
// First-person player character for the 0.1.0 walkable Hollywood block.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "InputActionValue.h"
#include "GigLineCharacter.generated.h"
class UCameraComponent;
class UInputAction;
class UInputMappingContext;
/**
* Walking first-person character.
*
* Movement is C++ and network-agnostic in the shapes it uses (Enhanced Input ->
* AddMovementInput), so a Blueprint subclass can retune values without touching
* code. Defaults are human-scale: 1.75 m capsule, 1.6 m eye height, 4 m/s walk.
*/
UCLASS()
class GIGLINE_API AGigLineCharacter : public ACharacter
{
GENERATED_BODY()
public:
AGigLineCharacter();
virtual void BeginPlay() override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
protected:
/** Loads the Enhanced Input assets from /Game/Input; called from BeginPlay. */
void LoadInputAssets();
/** Moves along the control yaw by the given forward/strafe amounts. */
void ApplyMove(float Forward, float Strafe);
/** Handles input actions to movement; assigned in C++ so the project runs with no assets. */
/** Forward/back and strafe are separate 1D actions, summed into one move. */
void MoveForward(const FInputActionValue& Value);
void MoveStrafe(const FInputActionValue& Value);
void MoveBack(const FInputActionValue& Value);
void MoveLeft(const FInputActionValue& Value);
void Look(const FInputActionValue& Value);
void StartJump();
void StopJump();
/** Mapping context for the gamepad and keyboard. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputMappingContext> MappingContext;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> MoveAction;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> MoveBackAction;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> StrafeAction;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> StrafeLeftAction;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> LookAction;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
TObjectPtr<UInputAction> JumpAction;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
TObjectPtr<UCameraComponent> FirstPersonCamera;
/** Degrees per second per unit of mouse input; defaults to Unreal's usual 45. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera")
float LookSensitivity = 45.0f;
/** Invert the pitch axis, for players who prefer it. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera")
bool bInvertLookY = false;
/** Walk speed in metres per second; converted to Unreal units on first use. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float WalkSpeedMetresPerSecond = 4.0f;
};
@@ -0,0 +1,8 @@
#include "GigLineGameModeBase.h"
#include "GigLineCharacter.h"
AGigLineGameModeBase::AGigLineGameModeBase()
{
DefaultPawnClass = AGigLineCharacter::StaticClass();
}
@@ -0,0 +1,21 @@
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "GigLineGameModeBase.generated.h"
/**
* Default game mode for the 0.1.0 walkable Hollywood block demo.
*
* Sets the first-person character as the default pawn and spawns it at the start
* point, so the demo runs with no Blueprint wiring. Gameplay tuning that belongs to
* designers still lives in Blueprint subclasses on top of this.
*/
UCLASS()
class GIGLINE_API AGigLineGameModeBase : public AGameModeBase
{
GENERATED_BODY()
public:
AGigLineGameModeBase();
};
@@ -0,0 +1,78 @@
#include "GigLineShotActor.h"
#include "Camera/CameraActor.h"
#include "Engine/World.h"
#include "GameFramework/PlayerController.h"
#include "UnrealClient.h"
AGigLineShotActor::AGigLineShotActor()
{
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = true;
}
void AGigLineShotActor::BeginPlay()
{
Super::BeginPlay();
NextShot();
}
void AGigLineShotActor::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
if (bWaitingForSettle)
{
SettleCountdown -= DeltaSeconds;
if (SettleCountdown <= 0.f)
{
bWaitingForSettle = false;
const FString FileName = FString::Printf(TEXT("review-%02d-%s"), CurrentShot,
CurrentShot < ShotNames.Num() ? *ShotNames[CurrentShot] : *FString::FromInt(CurrentShot));
// Resolution is the game window's; the runner passes -ResX/-ResY.
FScreenshotRequest::RequestScreenshot(FileName, false, false);
bWaitingForScreenshot = true;
}
return;
}
if (bWaitingForScreenshot && !FScreenshotRequest::IsScreenshotRequested())
{
bWaitingForScreenshot = false;
UE_LOG(LogTemp, Log, TEXT("GigLineShotActor: captured %s"), *FScreenshotRequest::GetFilename());
++CurrentShot;
NextShot();
}
}
void AGigLineShotActor::NextShot()
{
if (!PlayerController)
{
PlayerController = GetWorld()->GetFirstPlayerController();
}
if (CurrentShot >= ShotLocations.Num() || !PlayerController)
{
UE_LOG(LogTemp, Log, TEXT("GigLineShotActor: done (%d shots)"), CurrentShot);
if (bQuitWhenDone)
{
PlayerController->ConsoleCommand(TEXT("quit"));
}
return;
}
if (CurrentCamera)
{
CurrentCamera->Destroy();
}
FActorSpawnParameters Params;
Params.ObjectFlags |= RF_Transient;
CurrentCamera = GetWorld()->SpawnActor<ACameraActor>(
ACameraActor::StaticClass(), ShotLocations[CurrentShot], ShotRotations[CurrentShot], Params);
PlayerController->SetViewTarget(CurrentCamera);
SettleCountdown = SettleSeconds;
bWaitingForSettle = true;
}
@@ -0,0 +1,60 @@
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "GigLineShotActor.generated.h"
/**
* Dev-only helper for the visual-review screenshot pipeline.
*
* Placed in the review map by Tools/ue_build_review_map.py. In -game mode it
* walks a fixed list of camera setups, sets the view target to a spawned
* CameraActor for each, requests a screenshot via FScreenshotRequest, and
* quits the game when all shots are written. Lives in test content only;
* the shipped game never references this map.
*/
UCLASS()
class GIGLINE_API AGigLineShotActor : public AActor
{
GENERATED_BODY()
public:
AGigLineShotActor();
virtual void BeginPlay() override;
virtual void Tick(float DeltaSeconds) override;
/** World-space locations for each shot camera. */
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TArray<FVector> ShotLocations;
/** Rotations for each shot camera (same index as ShotLocations). */
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TArray<FRotator> ShotRotations;
/** File name fragment for each shot (same index as ShotLocations). */
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TArray<FString> ShotNames;
/** Seconds to let the view target settle before the capture. Resolution comes from the runner's -ResX/-ResY. */
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float SettleSeconds = 0.35f;
/** Quit the game after the last shot (disable to keep the game open). */
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool bQuitWhenDone = true;
private:
void NextShot();
UPROPERTY()
TObjectPtr<class APlayerController> PlayerController;
UPROPERTY()
TObjectPtr<class ACameraActor> CurrentCamera;
int32 CurrentShot = 0;
float SettleCountdown = 0.f;
bool bWaitingForSettle = false;
bool bWaitingForScreenshot = false;
};
@@ -0,0 +1,13 @@
using UnrealBuildTool;
using System.Collections.Generic;
public class GigLineEditorTarget : TargetRules
{
public GigLineEditorTarget(TargetInfo Target) : base(Target)
{
Type = TargetType.Editor;
DefaultBuildSettings = BuildSettingsVersion.V7;
IncludeOrderVersion = EngineIncludeOrderVersion.Latest;
ExtraModuleNames.Add("GigLine");
}
}
+2
View File
@@ -4,6 +4,8 @@ Geographic sources, manifests, and the scripts that turn them into Blender impor
OSM is layout data only. It is not authoritative for façade detail, height, or pavement, and it is never sampled as a texture or panorama source.
The OSM source files are © OpenStreetMap contributors, available under the [Open Database License 1.0](https://opendatacommons.org/licenses/odbl/1-0/); see [OpenStreetMap attribution](https://www.openstreetmap.org/copyright). Public use of a derivative database can require sharing that database or the changes needed to recreate it under ODbL. Review the applicable obligations for each distributed product.
## Layout
| Path | Contents |
@@ -86,9 +86,9 @@ Excluded and why: `6780` (Bank of America) and everything east of it is east of
## Still unverified
- **Elevation.** No DEM checked. The choice between using a separately licensed DEM and skipping it depends on measured height variation across the 279 m block.
- **Façade width — resolved.** 25.75 m, from the traced north edge, agreeing with the six-column, 4.96 m-bay arithmetic and with the photo framing. The sibling notes' 29.6 m is dropped for lack of any derivation; reasoning in [`../../Docs/reference-notes-hollywood.md`](../../Docs/reference-notes-hollywood.md).
- **Façade width — resolved.** 25.75 m, from the traced north edge, agreeing with the six-column, 4.96 m-bay arithmetic and with the photo framing. The sibling notes' 29.6 m is dropped for lack of any derivation; detailed reference notes are local only.
- **Projected origin value.** The CRS and origin point are fixed in [`../manifests/local-origin.md`](../manifests/local-origin.md); the EPSG:26911 easting/northing still needs computing with PROJ or QGIS.
Measured since this pass: street profile, carriageway and sidewalk widths, and the designated crossing — see [`2026-11-06-street-profile.md`](2026-11-06-street-profile.md). North-side reference photography was fetched on 2026-11-06 (local only); see [`../../Docs/reference-notes-hollywood.md`](../../Docs/reference-notes-hollywood.md).
Measured since this pass: street profile, carriageway and sidewalk widths, and the designated crossing — see [`2026-11-06-street-profile.md`](2026-11-06-street-profile.md). North-side reference photography was fetched on 2026-11-06 and remains local only.
Next: the Blender blockout can start — extract, footprints, bounds, CRS, origin, elevation, street profile, and façade width are all settled.
+9
View File
@@ -0,0 +1,9 @@
Project-owned code, art, and documentation in this repository are dedicated to
the public domain under the Creative Commons CC0 1.0 Universal Public Domain
Dedication.
SPDX-License-Identifier: CC0-1.0
Full legal text: <https://creativecommons.org/publicdomain/zero/1.0/legalcode>
Third-party material and geographic data have separate terms; see
Docs/licensing.md and THIRD_PARTY_NOTICES.md.
+96
View File
@@ -0,0 +1,96 @@
# MEMORY.md
Shared working memory between sessions and collaborators. Short, dated entries with evidence: the command, the log line, or the file path.
Scope: facts and traps that cost time to establish. Planned work lives in [ROADMAP.md](ROADMAP.md); formal setup, pins, and licensing live in [Docs/](Docs/).
---
## 2026-11-07 — Publication audit
- MPFB 2.0.18's installed `blender_manifest.toml` identifies its code as GPL-3.0-or-later; its versioned `LICENSE.md` explicitly puts bundled mesh, texture, and rig assets under CC0 1.0. The earlier CC0 claim for the add-on code was wrong.
- Blender inspection found no packed images or linked libraries in the tracked scenes. The character scene references local MPFB textures and a temporary baked texture, so those paths are not portable to a fresh checkout.
- The 2026-11-07 publication rewrite removed the approved private documents and bundled skills from all reachable refs; local copies remain ignored. The original history backup at `/tmp/gig-line-private-history-backup-42a29fe2` contains those private files and must not be pushed; `/tmp` may be cleared by the system.
- `git ls-remote --refs origin` succeeded on 2026-11-07 with no advertised refs; `git ls-remote --exit-code origin HEAD` returned 2 because no HEAD was advertised. This does not verify LFS upload support.
- Gitleaks 8.28.0 found an Android File Server token in historical `Game/GigLine/Config/DefaultEngine.ini`. The token was removed from reachable history and the unused server disabled; the old private backup still contains it and must not be published.
## 2026-11-07 — Unreal Engine 5.8.4
- **Extracted** to `/run/media/system/DATA/Engines/UE_5.8.4` (73 GiB, `unzip` exit 0). CL `58902052`, `++UE5+Release-5.8`.
- **Gotcha: the editor needs a writable `~/.config/Epic`.** In the agent shell it crashed with signal 11 twice and looked like a broken install. The log named it:
`create dir('/home/zwitschi/.config/Epic/') failed: errno=30 (Read-only file system)` →
`Fatal error: Failed to create Intermediate directory '…/.config/Epic/UnrealEngine/5.8/Intermediate/'`.
Rerunning with `HOME=/tmp/uehome XDG_CONFIG_HOME=/tmp/uehome/.config` reached `LogInit: Display: Engine is initialized.`
- **Vulkan RHI is still unproven.** The successful run used `-NullRHI`. Untested: `UnrealEditor -vulkan` from a desktop terminal.
- A packaged game does not need `~/.config/Epic`, so this failure does not follow the build.
## 2026-11-07 — Agent shell limits
- The agent shell has a **read-only home** and read-only `/run`: `sudo` is blocked (`no new privileges`), `flatpak run` and `podman` cannot create instances, `/dev/nvidia*` is absent so `nvidia-smi` always fails there. None of this is a broken machine; verify GPU things from a normal terminal.
- Anything outside the session workspace (`/run/media/system/DATA/Engines`, `/tmp`) needs the wider sandbox with an approval prompt.
## 2026-11-07 — Reference photography
- **Licensing is not a gate for local development.** Reference images live in the gitignored `Art/reference/` and are never committed. What each set establishes is written into `Docs/reference-notes-hollywood.md`.
- **Commons text search runs dry; categories do not.** `Tools/commons_fetch.py --category "Category:Hollywood Masonic Temple"` surfaced 15 files a search never showed, including 4–5k px façades.
- **`upload.wikimedia.org` throttles hard.** Batches of six or fewer with `--delay 3`; a throttled file is skipped and recorded rather than killing the run (HTTP 429).
- **Unqualified queries return junk.** `Hollywood Boulevard shops` returned a Münster street, `Hollywood Masonic Temple entrance` a Seattle card index. Check the hit list before accepting a set.
- **OpenRouter web search is expensive.** One `gpt-5.2` + `openrouter:web_search` call ran 17 searches and cost **$0.38**; two of its eight URLs were Commons files already held. Use it for a known gap, not a sweep. `Tools/page_image_fetch.py` turns results into files by reading `og:image`.
- **The inherited "eight stone steps, 12 m wide" is wrong.** Photographs show three portals, each with its own short flight. Do not copy that number into geometry.
## 2026-11-06 — Blender and Blosm, headless
- **`flatpak run` does not work here.** Call the binary inside the app tree:
`B=/var/lib/flatpak/app/org.blender.Blender/x86_64/stable/active/files`, then `LD_LIBRARY_PATH="$B/blender/lib" "$B/blender/blender" -b …` with `BLENDER_USER_SCRIPTS="$HOME/.var/app/org.blender.Blender/config/blender/5.2/scripts"`.
- **Blosm needs two headless workarounds**, both already in `Tools/blosm_blockout.py`:
1. it refuses to import until an asset directory is set, and the check fires before the mode is honoured;
2. in background mode the default level table is empty, and the add-on then takes a path with a broken relative import (`from gui import addDefaultLevels`). Seeding one entry avoids it.
- **Blosm projects on a sphere, not an ellipsoid.** Its frame is about 0.4 % oversized and rotated by meridian convergence — roughly 2 m at the theatre. Fixed with a six-parameter affine into EPSG:26911 (0.0 cm residual).
- **Transform roots only.** 256 of 275 objects are children; transforming both parent and child applies the correction twice and reads as a 2 m placement error.
- **Blosm quietly fetches from Overpass** for relation members missing from the extract. `loadMissingMembers` is off by default in the tool so imports stay offline and reproducible.
## 2026-11-06 — Geo data traps
- **The 3DEP export is a tiled TIFF, not strips.** A strip reader silently scrambles rows and produces fake 0 m / 127 m bands. `Geo/scripts/dem_stats.py` handles tiles.
- **Sample walk routes through PROJ.** Placing samples with the OSM spherical approximation put them metres off the UTM meshes and reported 91 phantom holes. `/tmp` is not shared between tool calls here either — regenerate instead of saving intermediate files there.
- **3DEP cannot resolve kerbs.** Sharpest break near the kerb line is 0.09–0.12 m/m, a gutter transition, not a 0.15 m upstand. Kerb height stays a stated assumption.
- **Building interiors are not ground.** Points inside the hero footprint returned 114.5–117.1 m — structure, not terrain. Excluded from the relief figure.
- Two measurements independently agree on the carriageway: crossing length 20.9 m and gutter spacing −10 / +10…12 m.
## 2026-11-06 — Repository traps
- **Without `git lfs install` the lfs filter is inactive and `git add` does not warn** — it stores the file as a plain blob. `Tools/check-lfs.sh` is the guard.
- **Never commit reference images or `.env`.** `.gitignore` covers `/Art/reference/`, `.env`, `*.env`. When images slipped in early, history was rewritten to remove them.
- The project origin is Hollywood × Highland: `E 376 515.389 / N 3 774 224.083` in EPSG:26911. Blender X east, Y north, Z up, metres, Z measured from the origin ground (NAVD88 115.911 m). The whole block sits at negative X.
## 2026-11-07 — Unreal pipeline findings
- **UE 5.8 game targets must use `BuildSettingsVersion.V7`** to share the installed editor's build environment; V5 is rejected with "modifies the values of properties … not allowed".
- **`UnrealEditor-Cmd -run=pythonscript` swallows Python `print()`** with `-stdout` in 5.8 — write results to a file instead (see `Tools/ue_scale_test_import.py`).
- **`unreal.Box` has `min`/`max`, not `origin`/`get_extent()`** in 5.8 Python; compute centre/size from the corners.
- **FBX Blender→UE axis mapping is east→+X, north→−Y, up→+Z** with preset `ue_metric_v1`; the Y mirror is inherent (RH→LH). Compass math in game code: north = −Y. Verified with the three-cube scale test.
- Headless asset import works fine with `-nullrhi`; `unreal.AssetImportTask` + `FbxImportUI` is the pattern (Interchange pipeline under the hood).
- **Flatpak Blender headless needs `XDG_CONFIG_HOME=$HOME/.var/app/org.blender.Blender/config`** for extensions: `bpy.utils.user_resource('EXTENSIONS')` otherwise resolves to `~/.config/blender/5.2/extensions` (empty) and `bl_ext.blender_org.mpfb` is undiscoverable. Blosm unaffected (scripts/addons via `BLENDER_USER_SCRIPTS`). Found via `Tools/addon_compat_probe.py`; documented in `Art/blender/README.md`.
- **UE 5.8 Interchange skips material/texture assets for skeletal FBX imports** (static imports do get materials). Finish the chain in Python: import texture, build Material via `MaterialEditingLibrary`, assign with `sm.set_editor_property('materials', [SkeletalMaterial])` — the struct field is `material_interface`, not `material`, and `SkeletalMesh.set_material()` does not exist. See `Tools/ue_character_import.py`.
- **Blender 5.2 FBX exporter removed `armature_nla`** (passes for 4.x recipes fail). Also: a baked image is only referenced by the FBX exporter when `image.filepath` (not just `filepath_raw`) is set and the exporter can map the node — MPFB's v2 skin node-group tree can't be mapped, so a flat export material is required.
## 2026-11-07 — Collaboration and scope
- **Two sessions edit this repo at once.** The other session created the Unreal project and the blank package while this one was documenting. Check `git log` and `git status` before committing, and never `git add -A` blindly: it once swept generated `Game/GigLine/Build/**` logs into a commit. Stages the source files deliberately.
- **`Build/` under a project is generated**, not source — `FileOpenOrder/*.log` and packaging output. `.gitignore` now covers it alongside `Binaries/`, `Intermediate/`, `Saved/`, `DerivedDataCache/`.
- **Scope boundary for `0.1.0`:** do not tag `v0.1.0` without an explicit go-ahead from the owner. It is a minor release and carries the walking/driving demo promise; the board's release checks decide when it is ready, not the pace of work.
## 2026-11-07 — Screenshot pipeline
- **`-executePythonScript` + editor is unreliable for captures**: the editor quits right after a *successful* deferred script (async screenshot requests never render), and after `load_map` in a commandlet every `spawn_actor` call returns None — build scenes in the default world instead.
- **Robust in-game capture recipe**: commandlet builds a map (`ue_build_review_map.py`), then the game runs in `-game` mode and a tiny C++ actor (`AGigLineShotActor`) walks camera setups via `SetViewTarget` + `FScreenshotRequest::RequestScreenshot`. In 5.8 `FScreenshotRequest` has no resolution argument (window size via `-ResX/-ResY`) and no `IsFinished()` — poll `!IsScreenshotRequested()`. Lives in `UnrealClient.h`, not `Misc/ScreenshotManager.h`.
- **Commandlet-only quirks**: bulk render-target readback (`read_render_target_raw`, `export_render_target`) silently returns empty in commandlets; single-pixel `read_render_target_uv` works. Game targets must use `BuildSettingsVersion.V7`.
## 2026-11-07 — Enhanced Input from Python (UE 5.8)
- **`unreal.InputCoreTypes` does not exist** in the 5.8 Python bindings, and `unreal.Key` takes no constructor argument. Build a key with `k = unreal.Key(); k.import_text("SpaceBar")` — the export text is the key name (`MouseX`, `Gamepad_Left2D`).
- **Factories are `unreal.InputAction_Factory` and `unreal.InputMappingContext_Factory`**, not `InputActionFactory`. `create_asset(..., factory=None)` also works.
- **Do not delete an asset and recreate it in the same commandlet run.** `create_asset` returns `None` for a path deleted earlier in that process. Load the existing asset and overwrite it instead (`Tools/ue_create_input_assets.py: fresh()`).
- **Modifiers on `EnhancedActionKeyMapping` do not survive saving.** Assigning `InputModifierNegate` to the mapping's `modifiers` reads back correctly in memory, even after a `gc.collect()`, but the saved asset reads `mods=[NoneType]`. Keep-alive references, explicit flag setting, and in-place saves all failed to change it. Workaround: bind one action per direction (W and S as separate actions) so no modifier is needed.
- **A read-only packed `.umap` silently blocks `save_map`.** `EditorLoadingAndSavingUtils.save_map` returned `False` with no error until the file's mode was fixed (`chmod u+w`). Symptom: a rebuilt review map keeps three cameras after a fourth is added.
- `-run=pythonscript` still swallows `print()`, so write results to a file; `/tmp` is visible to the commandlets even though it is not shared between agent tool calls.
+3 -3
View File
@@ -4,7 +4,7 @@ A Linux-first Unreal Engine game about walking and driving through a real city.
## Status
Pre-production. Workstation setup and repository scaffolding are in progress; no engine, content, or geographic data is committed yet. Current state is tracked in [ROADMAP.md](ROADMAP.md), and the per-task setup state in [Docs/setup.md](Docs/setup.md).
Pre-production. The Unreal project, prototype content, and geographic data are tracked; the engine itself is not. Current state is tracked in [ROADMAP.md](ROADMAP.md), and the per-task setup state in [Docs/setup.md](Docs/setup.md).
## Layout
@@ -31,12 +31,12 @@ The engine is installed outside the repository and pinned in [Docs/toolchain.md]
- [ROADMAP.md](ROADMAP.md) — living task list, source of truth for progress
- [PLAN.md](PLAN.md) — scope, stack, release policy, timeline
- [AGENTS.md](AGENTS.md) — contributor and agent rules
- [MEMORY.md](MEMORY.md) — shared session memory: discoveries, gotchas, bugs
- [Docs/toolchain.md](Docs/toolchain.md) — pinned versions
- [Docs/environment.md](Docs/environment.md) — workstation audit
- [Docs/release-checks.md](Docs/release-checks.md) — release gate definitions
- [Docs/licensing.md](Docs/licensing.md) and [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) — licensing
- [CHANGELOG.md](CHANGELOG.md) — release history
## Licensing
Code licence is an open decision (MIT proposed). Engine, art, and geographic data are licensed separately; see [Docs/licensing.md](Docs/licensing.md). OSM-derived output carries "© OpenStreetMap contributors".
Project-owned code, art, and documentation are dedicated under [CC0 1.0](LICENSE). The engine, third-party assets, and geographic data have separate terms; see [Docs/licensing.md](Docs/licensing.md). OSM-derived output carries "© OpenStreetMap contributors".
+30 -17
View File
@@ -2,42 +2,45 @@
Living roadmap for this project. This file is the single source of truth for current progress.
## Status at a glance — 2026-11-07 08:50
## Status at a glance — 2026-11-07 09:45
Workstation and geo pipeline are done and verified. The art pipeline has a first blockout, terrain, and street in metres, in one shared coordinate frame. The Unreal step has not started: the 5.8.4 archive is verified and extracting, so every engine-facing item below is still open.
Workstation, geo pipeline, and engine validation are done. The art pipeline has a first blockout, terrain, and street in metres, in one shared coordinate frame. **Project bootstrap is complete**: `Game/GigLine` builds, packages, and both content-acceptance tests (static-mesh scale, MPFB skeletal character) pass. Remaining `0.1.0` work is geo detailing, asset production, gameplay, and release checks.
| Area | State |
| --- | --- |
| Workstation | Bazzite `45.20261105`, driver `615.78.08`, Vulkan good, `git-lfs`/`clang`/`cmake`/`ninja` installed |
| Tooling | Blender 5.2 + MPFB 2.0.18 + Blosm 2.7.28 working headless; QGIS 4.2.3 (stale `lts` removed 2026-11-07); PROJ 9.9.0; Unreal 5.8.4 extracted, headless smoke test passed, GUI launch pending |
| Tooling | Blender 5.2 + MPFB 2.0.18 + Blosm 2.7.28 working headless; QGIS 4.2.3; PROJ 9.9.0; Unreal 5.8.4 installed |
| Geo | extract, address + footprints + bounds, CRS + origin, 3DEP DEM, street profile, cross-sections — all verified and committed |
| Engine | 5.8.4 installed and validated: headless + Vulkan GUI editor launch pass on the RTX 5070 Ti |
| Blender | blockout (87 meshes), terrain, street (road/kerbs/sidewalks), site assembly — all in one frame, walk-checked |
| Reference | 72 images local, uncommitted; hero entrance located; inherited eight-step claim overturned |
| Unreal | engine extracted and headless-verified; editor GUI not launched yet |
| Unreal project | bootstrap complete: C++ project builds, blank Linux build packaged + launch-tested, scale test and MPFB skeletal character accepted |
**Open decisions:** code licence (MIT proposed); whether shipped builds carry a derived OSM database (ODbL posture).
**Next up:** model the hero façade and block frontage from the reference set, then first-person traversal on the site terrain. **Release gate:** `v0.1.0` is tagged only on the owner's explicit go-ahead once the release checks pass — not automatically when the checkboxes close.
**Open decision:** whether shipped builds carry a derived OSM database (ODbL posture).
## 0.1.0 — Local setup and walkable Hollywood block (weeks 1–6)
### Environment setup
Status detail in [Docs/setup.md](Docs/setup.md); workstation audit in [Docs/environment.md](Docs/environment.md); pins in [Docs/toolchain.md](Docs/toolchain.md).
Status detail in [Docs/setup.md](Docs/setup.md); pins in [Docs/toolchain.md](Docs/toolchain.md). The workstation audit is local only.
- [x] Update Bazzite using its supported mechanism; verify NVIDIA driver and Vulkan operation.
- [x] Register an Epic account and accept the Unreal license; obtain a pinned Linux engine build or build from Epic's authorized source repository.
- [-] Validate Unreal runs on Bazzite before asset production (not an officially supported distribution). 5.8.4 fully extracted (73 GiB, CL 58902052, promoted build); headless smoke test passed 2026-11-07 (`UnrealEditor-Cmd -nullrhi -run=BlankProgram`, clean startup and shutdown). Editor GUI launch on Vulkan still pending.
- [-] Install the engine-required compiler/SDK, Git, Git LFS, and an editor; use Distrobox for isolated CLI dependencies where useful. clang 23.1.4 / cmake 4.3.0 / ninja 1.13.2 installed; verified 2026-11-07 that this engine build bundles no Linux clang, so the system compiler is the toolchain — final proof lands with the first C++ project compile.
- [-] Pin Blender, MPFB, Blosm, and QGIS versions; confirm add-on compatibility; record in `docs/toolchain.md`. Done: Blender 5.2.0 LTS, MPFB 2.0.18, Blosm 2.7.28 (imports headless on 5.2 in `3Dsimple`; two headless workarounds documented), QGIS 4.2.3 with PROJ 9.9.0, stale QGIS `lts` 3.28.9 uninstalled 2026-11-07, Unreal 5.8.4 recorded as pin in `Docs/toolchain.md`. Left: finalize the Unreal pin once the editor GUI launches.
- [x] Validate Unreal runs on Bazzite before asset production (not an officially supported distribution). 2026-11-07: headless `-NullRHI` smoke test **and** full editor GUI launch with the Vulkan RHI passed — device `NVIDIA GeForce RTX 5070 Ti`, Vulkan loader 1.4.350, `VULKAN_SM5`, map check 0 errors. Only benign log noise (Android Turnkey status, telemetry DNS failures).
- [x] Install the engine-required compiler/SDK, Git, Git LFS, and an editor; use Distrobox for isolated CLI dependencies where useful. clang 23.1.4 / cmake 4.3.0 / ninja 1.13.2 installed; this engine build bundles no Linux clang, so the system compiler is the toolchain. **Accepted 2026-11-07:** the first C++ game-module compile and link (`libUnrealEditor-GigLine.so`) succeeded with system clang.
- [x] Pin Blender, MPFB, Blosm, and QGIS versions; confirm add-on compatibility; record in `docs/toolchain.md`. Done: Blender 5.2.0 LTS, MPFB 2.0.18, Blosm 2.7.28, QGIS 4.2.3 with PROJ 9.9.0 (stale `lts` removed 2026-11-07), Unreal 5.8.4 finalized as pin after the editor GUI launch.
- [x] Create repository structure (`Game/`, `Art/`, `Geo/`, `Tools/`, `Docs/`) with LFS tracking and ignore rules applied.
- [x] Keep project and Derived Data Cache on SSD storage; reserve several hundred GB for engine, assets, caches, builds.
### Project bootstrap
- [ ] Create a C++ Unreal project with Blueprint gameplay iteration and Enhanced Input.
- [ ] Package and launch a blank Linux build.
- [ ] Export one static mesh from Blender and import into Unreal; verify scale and materials.
- [ ] Export one MPFB character with baked materials and deform skeleton; verify import.
- [ ] Document initial setup in `Docs/`: engine install, project checkout, fresh-checkout build procedure.
- [x] Create a C++ Unreal project with Blueprint gameplay iteration and Enhanced Input. Created 2026-11-07 at `Game/GigLine/GigLine.uproject` (primary game module + default game mode, Enhanced Input plugin + input classes in `Config/DefaultInput.ini`); `GigLineEditor` target compiles clean with system clang (UBT: Result Succeeded).
- [x] Package and launch a blank Linux build. 2026-11-07: UAT `BuildCookRun` (cook + build + stage + pak, Development config) succeeded in 56 s; 1.1 GiB archive at `/run/media/system/DATA/Builds/GigLine` (outside the repo, uncommitted). `GigLine.sh` launched on the desktop session, created a Vulkan swapchain, and rendered 759 frames across a 60 s test. Known minor: one handled `PSOPrecache` ensure fires at exit only; not blocking, watch it in `0.1.x`.
- [x] Export one static mesh from Blender and import into Unreal; verify scale and materials. 2026-11-07: calibration scene (three 1 m cubes at origin / +10 m east / +10 m north) exported from Blender with preset `ue_metric_v1` (`Tools/scale_test_export.py`) and imported headless via `Tools/ue_scale_test_import.py`. Measured in UE: cubes exactly 100.00 cm, origin preserved, material imported. Axis mapping: **east → +X, north → −Y, up → +Z** (the Y mirror is inherent to the right-handed→left-handed conversion; importer handles winding).
- [x] Export one MPFB character with baked materials and deform skeleton; verify import. 2026-11-07: MPFB 2.0.18 generated human (19 158 verts, 1.66 m tall) + standard rig with weights (137 bones) + v2 skin baked to a 1024² diffuse texture (`Tools/mpfb_character_export.py`, export preset `ue_character_v1`). Imported headless (`Tools/ue_character_import.py`) into `/Game/Test/Character`: `SkeletalMesh` + 138-bone `Skeleton` + `Material` with the baked `Texture2D` assigned to slot 0. Quirks recorded: 5.8 Interchange skips materials for skeletal FBX imports (finished in script); Blender 5.2 dropped the FBX `armature_nla` option; the baked image needs `image.filepath` set for FBX to reference it.
- [x] Document initial setup in `Docs/`: engine install, project checkout, fresh-checkout build procedure. 2026-11-07: `Docs/setup.md` reconciled with reality (all engine/bootstrap items marked done with evidence); `Docs/build.md` completed with the fresh-checkout build, packaging, and headless-content-check procedures plus known engine quirks.
### Geographic data
@@ -47,11 +50,11 @@ Status detail in [Docs/setup.md](Docs/setup.md); workstation audit in [Docs/envi
- [x] In QGIS, set a suitable local projected CRS (e.g. EPSG:26911) and record a local origin. EPSG:26911, origin and projected easting/northing are recorded in `Geo/manifests/local-origin.md` with PROJ 9.9.0 control points; QGIS 4.2.3 is installed for the DEM reprojection step.
- [x] Verify the address, building footprints, and demo boundaries for 6840 Hollywood Blvd against source data.
- [-] Import OSM blockouts through Blosm; clean and detail them in Blender. Imported 2026-11-06 (`Art/blender/blockout/blockout-2026-11-06.blend`, 86 meshes, scale 1:1 against OSM, re-framed into EPSG:26911 with a 0.0 cm affine residual); cleaning and detailing not started.
- [ ] Preserve geographic coordinates in metadata; define explicit meter→centimeter conversion to local Unreal coordinates. The frame is defined (`Geo/manifests/local-origin.md`: EPSG:26911, origin Hollywood × Highland, X east / Y north / Z up, ×100 on export) and the Blender meshes already use it; the export preset and the asserted ×100 are not built yet.
- [-] Preserve geographic coordinates in metadata; define explicit meter→centimeter conversion to local Unreal coordinates. The frame is defined (`Geo/manifests/local-origin.md`: EPSG:26911, origin Hollywood × Highland, X east / Y north / Z up) and the ×100 conversion is now **verified** by the 2026-11-07 scale test, with the axis mapping recorded (north realizes as UE −Y). Left: build the production export preset for the actual site meshes using these settings.
### Asset production
- [ ] Establish a consistent scale test, named export presets, and repeatable import settings.
- [x] Establish a consistent scale test, named export presets, and repeatable import settings. Scale test = the three-cube calibration scene (`Art/blender/scaletest/`, regenerable via `Tools/scale_test_export.py`); named export preset `ue_metric_v1` (FBX settings in that script); named import preset `ue_metric_v1` (`FbxImportUI` settings in `Tools/ue_scale_test_import.py`). Both scripts are the templates for production asset export/import.
- [-] Collect reference photography for manual façade work. Local only, never committed: `Art/reference/` is gitignored. **72 images in eleven folders**: hero façades to 4k, the El Capitan Theatre exterior series, premiere-night staging, Ovation Hollywood, the north side, and the Lee Drug neighbour. Gaps largely closed by the Flickr/web-search pass: the hero entrance is now documented (three portals with individual short flights — the inherited "eight steps, 12 m wide" is overturned), and the 6806–6822 retail run appears in an oblique 2013 frame. Still missing: a clean isolated frame of the door recess and a night frame of 6840 itself.
- [x] Download reference photography for the two north-side buildings inside the 50 m buffer — TCL Chinese Theatre (OSM way 424105908, 6925 Hollywood Blvd) and Madame Tussaud's (way 424105889, 6933 Hollywood Blvd). Done 2026-11-06 with `Tools/commons_fetch.py` against the Wikimedia Commons API: eleven frames in the local `Art/reference/north-chinese/` and `Art/reference/north-tussauds/`, provenance in each `sidecar.json`, nothing committed.
- [ ] Model hero façade for the El Capitan Entertainment Centre site.
@@ -62,12 +65,22 @@ Status detail in [Docs/setup.md](Docs/setup.md); workstation audit in [Docs/envi
### Gameplay
- [ ] First-person movement, mouse look, and Enhanced Input bindings.
- [-] First-person movement, mouse look, and Enhanced Input bindings. Implemented 2026-11-07: `AGigLineCharacter` (C++, 1.75 m capsule, 1.60 m eye camera, 4 m/s walk, mouse look in deg/s, jump) as the default pawn, with five actions and an 11-entry context in `/Game/Input` built by `Tools/ue_create_input_assets.py`. **Not yet confirmed interactively** — launch PIE on `Test/ReviewMap` and walk. Screenshot review pending (`Screenshots/README.md`).
- [ ] Collision, reset, and pause functionality.
- [ ] Walkable sidewalks and a designated street crossing. Crossing chosen: mid-block signalised crossing, OSM way 385648319, at 130 m along the block (`Geo/verification/2026-11-06-street-profile.md`).
- [ ] Fixed daytime lighting and basic ambience.
- [ ] Geographic attribution display ("© OpenStreetMap contributors" + license link).
### Repository publication - active - 42a29fe2-651a-4483-be7f-6d9040c53053
- [x] Remove confirmed private documents and bundled skills from publishable history while preserving local copies; validate all refs and LFS objects. Rewritten 2026-11-07; original history is in a separate private local backup.
- [x] Scan all reachable Git history with Gitleaks 8.28.0. Removed an Android File Server security token from history and disabled the unused server; the rescan found no leaks.
- [x] Review Unreal asset provenance via the generation/import scripts and inspect all 18 reachable Unreal asset versions for recognizable private-reference paths. No such paths were found; this is not a full binary-content audit.
- [x] Review OSM source distribution notices: original data retains attribution and licence metadata, and the repository links directly to ODbL 1.0.
- [ ] Review ODbL obligations for each shipped derivative database or produced work before public release.
- [-] Validate Gitea LFS upload support during the first authorized push. `origin` was reachable on 2026-11-07 and advertised no refs; LFS upload remains untested.
- [-] Commit and review the publication preparation changes before pushing.
### Release checks
- [ ] Player can traverse both sides of the demo without falling through geometry. Blender pre-check passes: 0 raycast holes along both sidewalks and the designated crossing; worst seam step 15.8 cm at the sidewalk's outer edge (`Art/blender/README.md`). Engine traversal still to be tested.
+91
View File
@@ -0,0 +1,91 @@
# Screenshots — visual review
Rendered in-game captures of the current project content, generated by
[`Tools/render_screenshots.sh`](../Tools/render_screenshots.sh). **The PNGs in
this folder are never committed** (`.gitignore` excludes everything here
except this README) — they exist on disk so a human can review the game's
appearance after every change that could affect it. The `![...]` embeds below
resolve locally next to this file.
## How to regenerate
```bash
Tools/render_screenshots.sh
```
This rebuilds `Game/GigLine/Content/Test/ReviewMap.umap` from the current
content (floor, key light, sky, MPFB character, the three 1 m scale cubes),
runs the game on it in `-game` mode, lets `AGigLineShotActor` walk its fixed
camera list and capture, and copies the PNGs here. Takes about a minute.
Each screenshot is embedded below with its description and approval state.
After reviewing, tick the approval checkbox so agents know whether the look
is accepted or needs work.
## review-00-overview.png — generated 2026-11-07, approval: ☐ pending
![Wide overview from the south-west: character at origin, three 1 m calibration cubes in a row at 4 m north, gray floor, key light and sky](review-00-overview.png)
Wide shot from the south-west, elevated. The review scene as a whole:
character at origin, three 1 m calibration cubes in a row at 4 m north, gray
floor, one key light plus fill sky.
## review-01-character.png — generated 2026-11-07, approval: ☐ pending
![Medium eye-height shot of the MPFB-generated character from the south, A-pose with baked skin material](review-01-character.png)
Medium shot, eye height, from the south. The MPFB-generated character with
its baked skin material and A-pose bind.
## review-02-scale-cubes.png — generated 2026-11-07, approval: ☐ pending
![Close three-quarter view of the 1 m scale-test cube row on the gray floor](review-02-scale-cubes.png)
Close three-quarter view of the cube row. Each cube is exactly 1 m — use it
to judge scale against the character.
## review-03-first-person.png — generated 2026-11-07, approval: ☐ pending
![Third-person shot from a player's eye line looking across the review floor: the MPFB character at origin (spawn point), one red cube behind it, sky and horizon behind](review-03-first-person.png)
New shot for the first-person player work. Camera is at 165 cm — the player's
eye height — looking from behind and slightly south-east of the spawn point.
It shows where the player lands (the character stands at origin, which is also
the default spawn) and how the capsule/camera relate to the 1 m cubes.
**What changed in the game since the last review:** the project now has a
first-person `AGigLineCharacter` (1.75 m capsule, 1.60 m eye camera, 4 m/s
walk, Enhanced Input with WASD + mouse look + jump) wired as the default pawn
by `AGigLineGameModeBase`. Nothing visible from these cameras should differ:
the review map spawns its own pawn-free staging, so this shot is the reference
for the new pawn's position and eye height, not of the pawn itself.
**Not shown, needs a human check:** walking, mouse look and jump can only be
judged interactively. Launch the editor, press Play on `Test/ReviewMap`, and
confirm W/A/S/D move in the expected directions, the mouse looks around with no
acceleration, and Space jumps once per press.
## What to look for
- **Character**: proportions, bind pose, skin material readability.
- **Scale cubes**: each is exactly 1 m — use them to sanity-check anything
whose size looks off.
- **Lighting**: one warm key light and fill sky; this is the temporary review
rig, not the `0.1.0` daytime look.
## Known cosmetic issues in the review scene
- The gray floor material renders with a speckled pattern instead of flat
gray — suspect the `Constant3Vector` wiring in `Tools/ue_build_review_map.py`.
Cosmetic only; the floor exists so shadows and scale read correctly.
- The horizon shows a black band below the sky atmosphere (no distant ground
plane). Expected in the empty review scene.
## Conventions
- `review-NN-name.png` names are produced by `AGigLineShotActor`
(`Game/GigLine/Source/GigLine/GigLineShotActor.h`); the camera list lives in
`Tools/ue_build_review_map.py`.
- Add or adjust shots there when new content needs review, then update this
README's table in the same change.
- Agents: see the *Visual review* section in [`AGENTS.md`](../AGENTS.md).
+8 -12
View File
@@ -10,7 +10,7 @@ Status values: **verified** (licence confirmed and permits our use), **pending**
| --- | --- | --- | --- |
| Unreal Engine | 5.8.x (pin) | Epic Games EULA, proprietary | verified — not redistributable; distribution per Epic terms |
| Blender | 5.2 LTS | GPL-2.0-or-later | verified — tool only, no GPL code linked into a shipped build |
| MPFB | 2.0.18 | CC0 (base mesh and add-on assets) | verified — confirm per-asset at character production |
| MPFB | 2.0.18 | GPL-3.0-or-later (add-on code); CC0 1.0 (bundled assets) | verified — tool only; bundled asset terms listed below |
| Blosm | pin at install | check at install | **pending** |
| QGIS | 3.44.15 LTR | GPL-2.0-or-later | **pending** — tool only |
| Git LFS | 3.x | MIT | **pending** |
@@ -23,7 +23,7 @@ Status values: **verified** (licence confirmed and permits our use), **pending**
| OSM building ways 424024147, 424024148 | OpenStreetMap API v0.6, `Geo/raw/*.json` | ODbL 1.0 | verified |
| Elevation data — USGS 3DEP 1 m DEM | `elevation.nationalmap.gov` 3DEP ImageServer export, `Geo/raw/dem-3dep-1m-26911-2026-11-06.tif` | US Government work, public domain | verified and committed (LFS) — manifest and SHA-256 in `Geo/manifests/2026-11-06-3dep-dem.md` |
Attribution string for any OSM-derived output: "© OpenStreetMap contributors", <https://www.openstreetmap.org/copyright>.
Attribution string for any OSM-derived output: "© OpenStreetMap contributors", <https://www.openstreetmap.org/copyright>. The data is available under the [Open Database License 1.0](https://opendatacommons.org/licenses/odbl/1-0/).
## Reference photography (local only, not committed, not shipped)
@@ -35,7 +35,7 @@ Reference photographs are **not** part of this repository. They live in the giti
| `Art/reference/north-chinese/` | Fetched 2026-11-06 with `Tools/commons_fetch.py` from the Wikimedia Commons API | `sidecar.json` in the folder |
| `Art/reference/north-tussauds/` | Same | `sidecar.json` in the folder |
What the images are used for, what they are evidence for, and the block's recorded measurements live in [Docs/reference-notes-hollywood.md](Docs/reference-notes-hollywood.md). That document is committed; the images are not.
Reference notes and images are local only and excluded from version control.
Rules that still apply, because they are about shipped output rather than local working material:
@@ -43,14 +43,10 @@ Rules that still apply, because they are about shipped output rather than local
- Venue lettering, signage artwork, and trademarks are separate rights from the photograph. Copying a façade's proportions is not the same as reproducing its signage.
- A reference image whose provenance cannot be found locally must not be used at all.
## Agent tooling (development only, not shipped)
| Component | Version | Licence | Status |
| --- | --- | --- | --- |
| blender-skills | `1.3.0`, upstream commit `8f778d2` | MIT, © 2026 blender-skills contributors (<https://github.com/arjun988/blender-skills>) | verified — instruction and reference material for agents, committed under `.agents/skills/blender-skills/`, never built into a build or shipped |
Copied 2026-11-06 from a local checkout at `~/.agents/skills/blender-skills/.claude/skills/`, preserving upstream layout and filenames. No content was edited.
## Assets
No shipped 3D, texture, or audio assets exist yet. Add a row per asset category with author, source, licence, and redistribution status before the first content commit.
| Component | Source | Licence | Status |
| --- | --- | --- | --- |
| Character mesh, standard rig, and baked skin | MPFB 2.0.18 bundled base mesh, rig, and textures; `Tools/mpfb_character_export.py` | CC0 1.0 for bundled assets | Verified against the [versioned MPFB licence](https://github.com/makehumancommunity/mpfb2/blob/v2.0.18/LICENSE.md); imported character assets and Blender source are tracked. |
| OSM blockout | OSM extract and Blosm-generated geometry | ODbL 1.0 for source data | Source attribution above; review ODbL obligations for distributed derived databases. |
| Terrain and street prototype | USGS 3DEP DEM, OSM data, project scripts | Public domain USGS data; ODbL 1.0 for OSM data | Source manifests in `Geo/manifests/`; review ODbL obligations for distributed derived databases. |
+1 -1
View File
@@ -12,6 +12,7 @@ Repeatable import/export and processing helpers. Anything that was done twice by
## Helpers
- `render_screenshots.sh` — visual-review pipeline: rebuilds `/Game/Test/ReviewMap` from current content (`ue_build_review_map.py`), runs the game on it in `-game` mode, lets `AGigLineShotActor` (`Game/GigLine/Source/GigLine/`) capture the fixed camera list, and copies the PNGs into the gitignored `Screenshots/`. See `Screenshots/README.md`.
- `check-lfs.sh` — repository guard: fails if `git-lfs` or the `lfs` filter is missing, or if an LFS-tracked file was committed as a plain blob.
- `site_assembly.py` — assembles terrain + street + blockout into one file and raycasts the walking routes for holes and steps.
- `street_mesh.py` — road, kerbs, and sidewalks on the DEM, in the project frame; kerbs flush at the three crossings.
@@ -22,6 +23,5 @@ Repeatable import/export and processing helpers. Anything that was done twice by
## Planned helpers
- Blender export preset runner (named presets, metric scale assertion)
- Blender→Unreal scale test (export a known-size cube, verify 100× conversion)
- OSM extract fetch and manifest writer (records date, bounds, checksum)
- QGIS processing steps as batch scripts, so the geo cleanup is reproducible rather than GUI-only
+64
View File
@@ -0,0 +1,64 @@
# Headless compatibility probe for Blosm and MPFB on the pinned Blender 5.2.
#
# blender -b --factory-startup --python Tools/addon_compat_probe.py
#
# Checks: Blender version, Blosm add-on loads + version, MPFB extension
# enables + version, MPFB can create a default human headless, resulting
# object inventory (mesh/armature/materials/uvs).
import bpy
import sys
REPORT = "/tmp/addon-compat-report.txt"
lines = []
def log(msg):
lines.append(str(msg))
bl = bpy.app.version_string
log(f"Blender: {bl}")
# --- Blosm (legacy add-on, scripts/addons) ---
try:
import blosm # noqa
info = getattr(blosm, "bl_info", {})
log(f"blosm: import OK, version {info.get('version', '?')}, blender {info.get('blender', '?')}")
except Exception as exc: # noqa
log(f"blosm: IMPORT FAILED: {exc!r}")
# --- MPFB (extension, extensions/blender_org/mpfb) ---
try:
import addon_utils
bpy.ops.preferences.addon_enable(module="bl_ext.blender_org.mpfb")
enabled = "bl_ext.blender_org.mpfb" in bpy.context.preferences.addons
version = "?"
try:
import bl_ext.blender_org.mpfb as mpfb_mod
version = getattr(mpfb_mod, "MPFB_VERSION", "?")
except Exception:
pass
log(f"mpfb: enable OK (registered={enabled}), version {version}")
except Exception as exc: # noqa
log(f"mpfb: ENABLE FAILED: {exc!r}")
# --- MPFB create-human smoke test ---
try:
bpy.ops.mpfb.create_human("EXEC_DEFAULT")
meshes = [o for o in bpy.data.objects if o.type == "MESH"]
armatures = [o for o in bpy.data.objects if o.type == "ARMATURE"]
log(f"mpfb.create_human: OK — {len(bpy.data.objects)} objects, "
f"{len(meshes)} meshes, {len(armatures)} armatures")
for o in meshes:
uv = bool(o.data.uv_layers)
mats = [m.name for m in o.data.materials]
log(f" mesh {o.name}: {len(o.data.vertices)} verts, uv={uv}, materials={mats}")
for o in armatures:
log(f" armature {o.name}: {len(o.data.bones)} bones")
except Exception as exc: # noqa
log(f"mpfb.create_human: FAILED: {exc!r}")
with open(REPORT, "w") as f:
f.write("\n".join(lines) + "\n")
print("\n".join(lines))
+1 -1
View File
@@ -22,7 +22,7 @@ fi
if ! command -v git-lfs >/dev/null 2>&1; then
echo "ERROR: git-lfs is not installed. LFS-tracked files would be stored as ordinary blobs." >&2
echo " Install git-lfs, run 'git lfs install', then re-check. See Docs/environment.md." >&2
echo " Install git-lfs, run 'git lfs install', then re-check. See Docs/setup.md." >&2
fail=1
fi
+148
View File
@@ -0,0 +1,148 @@
# MPFB character export for the Blender -> Unreal pipeline (0.1.0 acceptance).
#
# Generates a default human, adds the standard deform rig with weights,
# materializes the v2 skin, bakes its diffuse to a texture, and exports
# FBX with the named export preset "ue_character_v1".
#
# blender -b --factory-startup --python Tools/mpfb_character_export.py -- \
# --out /tmp/gigline/character.fbx \
# --save-blend Art/blender/character/character-<date>.blend
import argparse
import os
import sys
import bpy
REPORT = "/tmp/mpfb-export-report.txt"
lines = []
def log(msg):
lines.append(str(msg))
def enable_mpfb():
bpy.ops.preferences.addon_enable(module="bl_ext.blender_org.mpfb")
assert "bl_ext.blender_org.mpfb" in bpy.context.preferences.addons, "MPFB did not enable"
def build_character():
for obj in list(bpy.data.objects):
bpy.data.objects.remove(obj, do_unlink=True)
bpy.ops.mpfb.create_human("EXEC_DEFAULT")
human = bpy.context.active_object
log(f"create_human: {human.name}, {len(human.data.vertices)} verts, dims={[round(d, 3) for d in human.dimensions]}")
bpy.ops.mpfb.add_standard_rig("EXEC_DEFAULT")
armatures = [o for o in bpy.data.objects if o.type == "ARMATURE"]
log(f"add_standard_rig: {len(armatures)} armature(s), bones={[len(a.data.bones) for a in armatures]}")
bpy.ops.object.select_all(action="DESELECT")
human.select_set(True)
bpy.context.view_layer.objects.active = human
bpy.ops.mpfb.create_v2_skin("EXEC_DEFAULT")
mats = [m.name for m in human.data.materials]
log(f"create_v2_skin: materials={mats}")
return human
def bake_diffuse(human, size=1024):
mat = human.data.materials[0]
nodes, links = mat.node_tree.nodes, mat.node_tree.links
img = bpy.data.images.new("T_Skin_Diffuse", size, size)
tex_node = nodes.new("ShaderNodeTexImage")
tex_node.image = img
nodes.active = tex_node
tex_node.select = True
bsdf = next((n for n in nodes if n.type == "BSDF_PRINCIPLED"), None)
bsdf_input = bsdf.inputs["Base Color"] if bsdf else None
if bsdf is not None:
for link in list(links):
if link.to_node == bsdf and link.to_socket == bsdf_input:
links.remove(link)
links.new(tex_node.outputs["Color"], bsdf_input)
scene = bpy.context.scene
scene.render.engine = "CYCLES"
scene.cycles.device = "CPU"
scene.cycles.samples = 1
scene.render.bake.use_pass_direct = False
scene.render.bake.use_pass_indirect = False
scene.render.bake.use_pass_color = True
scene.render.bake.margin = 8
bpy.ops.object.select_all(action="DESELECT")
human.select_set(True)
bpy.context.view_layer.objects.active = human
bpy.ops.object.bake(type="DIFFUSE", pass_filter={"COLOR"}, use_clear=True)
img.filepath = "/tmp/gigline/T_Skin_Diffuse.png"
img.filepath_raw = img.filepath
img.file_format = "PNG"
img.save()
log(f"bake: baked {size}x{size} diffuse -> {img.filepath} (filepath now set for FBX)")
# MPFB's v2 skin is a node-group tree the FBX exporter cannot map to a
# texture. Build a flat export material carrying the baked diffuse so the
# FBX (and therefore Unreal) gets a real texture.
export_mat = bpy.data.materials.new("M_Character_Export")
export_mat.use_nodes = True
nt = export_mat.node_tree
nt.nodes.clear()
out = nt.nodes.new("ShaderNodeOutputMaterial")
bsdf = nt.nodes.new("ShaderNodeBsdfPrincipled")
tex = nt.nodes.new("ShaderNodeTexImage")
tex.image = img
nt.links.new(tex.outputs["Color"], bsdf.inputs["Base Color"])
nt.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])
human.data.materials.clear()
human.data.materials.append(export_mat)
log("export material: M_Character_Export with baked texture assigned")
def export_fbx(human, path):
os.makedirs(os.path.dirname(path), exist_ok=True)
# Named export preset "ue_character_v1" — change only via a new preset name.
bpy.ops.export_scene.fbx(
filepath=path,
use_selection=False,
object_types={"ARMATURE", "MESH"},
apply_unit_scale=True,
apply_scale_options="FBX_SCALE_NONE",
axis_forward="-Y",
axis_up="Z",
use_space_transform=True,
use_mesh_modifiers=True,
add_leaf_bones=False,
path_mode="COPY",
embed_textures=False,
mesh_smooth_type="OFF",
bake_space_transform=False,
)
log(f"export: FBX preset ue_character_v1 -> {path}")
def main():
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
parser = argparse.ArgumentParser()
parser.add_argument("--out", default="/tmp/gigline/character.fbx")
parser.add_argument("--save-blend", default=None)
args = parser.parse_args(argv)
enable_mpfb()
human = build_character()
bake_diffuse(human)
if args.save_blend:
os.makedirs(os.path.dirname(args.save_blend), exist_ok=True)
bpy.ops.wm.save_as_mainfile(filepath=args.save_blend)
log(f"save: {args.save_blend}")
export_fbx(human, args.out)
with open(REPORT, "w") as f:
f.write("\n".join(lines) + "\n")
main()
+136
View File
@@ -0,0 +1,136 @@
# UE-side screenshot renderer for visual review (commandlet, real RHI).
#
# Builds a small review scene in the default world (floor, sky, key light,
# MPFB character, the three scale-test cubes) and renders three fixed camera
# angles synchronously via SceneCapture2D render targets, writing PNGs
# directly into <repo>/Screenshots/ (gitignored).
#
# Invoked by Tools/render_screenshots.sh (UnrealEditor-Cmd -run=pythonscript).
# NOTE: no -nullrhi — rendering needs the real Vulkan RHI. In 5.8 the
# commandlet swallows print(); results go to /tmp/ue-screenshots-result.txt.
# Do NOT load_map here: after a map load the commandlet's spawn context dies
# and every spawn_actor call returns None.
import os
import unreal
RESULT = "/tmp/ue-screenshots-result.txt"
lines = []
def log(msg):
lines.append(str(msg))
REPO = os.path.abspath(os.path.join(unreal.Paths.project_dir(), "..", ".."))
OUT_DIR = os.path.join(REPO, "Screenshots")
WORLD = unreal.EditorLevelLibrary.get_editor_world()
def spawn(cls, loc, label=None, scale=None, rot=None):
a = unreal.EditorLevelLibrary.spawn_actor_from_class(cls, loc)
if a is None:
log(f"spawn {cls.__name__} returned None")
return None
if rot is not None:
a.set_actor_rotation(rot, False)
if scale is not None:
a.set_actor_scale3d(scale)
if label:
a.set_actor_label(label)
return a
def spawn_mesh(mesh, loc, label=None, scale=None):
a = unreal.EditorLevelLibrary.spawn_actor_from_object(mesh, loc)
if a is None:
log(f"spawn mesh {label} returned None")
return None
if scale is not None:
a.set_actor_scale3d(scale)
if label:
a.set_actor_label(label)
return a
# --- stage content -----------------------------------------------------------
try:
floor_mesh = unreal.EditorAssetLibrary.load_asset("/Game/Test/ScaleTest/ScaleTest_A")
if floor_mesh:
spawn_mesh(floor_mesh, unreal.Vector(450, 450, -50.0), "ReviewFloor", scale=(45.0, 45.0, 0.1))
log("staged: floor (45 m plane from ScaleTest_A)")
dl = spawn(unreal.DirectionalLight, unreal.Vector(0, 0, 900), "RenderKeyLight",
rot=unreal.Rotator(pitch=-38.0, yaw=35.0, roll=0.0))
if dl:
log("staged: DirectionalLight")
if spawn(unreal.SkyLight, unreal.Vector(0, 0, 1200), "RenderFillSky"):
log("staged: SkyLight")
if spawn(unreal.SkyAtmosphere, unreal.Vector(0, 0, 0), "RenderSky"):
log("staged: SkyAtmosphere")
char_mesh = unreal.EditorAssetLibrary.load_asset("/Game/Test/Character/character")
if char_mesh:
if spawn_mesh(char_mesh, unreal.Vector(0, 0, 0), "ReviewCharacter"):
log("staged: MPFB character at origin")
else:
log("MISSING: /Game/Test/Character/character")
for name, x, y in (("ScaleTest_A", 0.0, 0.0), ("ScaleTest_B", 400.0, 0.0), ("ScaleTest_C", 0.0, 400.0)):
m = unreal.EditorAssetLibrary.load_asset(f"/Game/Test/ScaleTest/{name}")
if m:
spawn_mesh(m, unreal.Vector(x, y, 50.0), f"Cube_{name}")
log(f"staged: {name} at ({x}, {y}, 50)")
except Exception as exc: # noqa
log(f"staging failed: {exc!r}")
# --- render ------------------------------------------------------------------
def render_shot(name, loc, target, fov=70.0):
rt = unreal.RenderingLibrary.create_render_target2d(
WORLD, 1920, 1080, unreal.TextureRenderTargetFormat.RTF_RGBA8_SRGB,
unreal.LinearColor(0.0, 0.0, 0.0, 1.0))
cap = spawn(unreal.SceneCapture2D, loc)
if cap is None:
log(f"{name}: no capture actor")
return
cap.set_actor_rotation(unreal.MathLibrary.find_look_at_rotation(loc, target), False)
comp = cap.get_component_by_class(unreal.SceneCaptureComponent2D)
comp.set_editor_property("texture_target", rt)
comp.set_editor_property("capture_source", unreal.SceneCaptureSource.SCS_FINAL_COLOR_LDR)
comp.set_editor_property("fov_angle", fov)
comp.set_editor_property("capture_every_frame", False)
comp.set_editor_property("capture_on_movement", False)
try:
pps = comp.get_editor_property("post_process_settings")
pps.set_editor_property("auto_exposure_method", unreal.AutomaticExposureMethod.AEM_MANUAL)
pps.set_editor_property("auto_exposure_bias", 1.0)
comp.set_editor_property("post_process_settings", pps)
except Exception as exc: # noqa
log(f"{name}: exposure control skipped ({exc!r})")
comp.capture_scene()
os.makedirs(OUT_DIR, exist_ok=True)
ok = unreal.RenderingLibrary.export_render_target(WORLD, rt, OUT_DIR, name)
out = os.path.join(OUT_DIR, f"{name}.png")
size = os.path.getsize(out) if os.path.exists(out) else 0
log(f"rendered {name}: export ok={ok}, {size} bytes")
unreal.EditorLevelLibrary.destroy_actor(cap)
SHOTS = [
("01-overview", unreal.Vector(900, -1100, 500), unreal.Vector(200, 100, 80), 70.0),
("02-character", unreal.Vector(180, -260, 160), unreal.Vector(0, 0, 85), 55.0),
("03-scale-cubes", unreal.Vector(550, -700, 150), unreal.Vector(200, 200, 50), 60.0),
]
for shot_name, loc, target, fov in SHOTS:
try:
render_shot(shot_name, loc, target, fov)
except Exception as exc: # noqa
log(f"shot {shot_name} failed: {exc!r}")
with open(RESULT, "w") as f:
f.write("\n".join(lines) + "\n")
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# Render in-game screenshots for visual review into <repo>/Screenshots/ (gitignored).
#
# Usage: Tools/render_screenshots.sh
# Rebuilds /Game/Test/ReviewMap from current content, runs the game on it
# (the GigLineShotActor walks three cameras, captures, quits), then copies
# the PNGs into Screenshots/.
set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENGINE="${UE_ENGINE_DIR:-/run/media/system/DATA/Engines/UE_5.8.4}"
PROJ="$REPO/Game/GigLine/GigLine.uproject"
echo "== 1/3 build review map =="
cd "$ENGINE"
./Engine/Binaries/Linux/UnrealEditor-Cmd "$PROJ" \
-run=pythonscript -script="$REPO/Tools/ue_build_review_map.py" \
-nullrhi -unattended -nopause -stdout
cat /tmp/ue-review-map-result.txt
echo "== 2/3 run game on review map =="
./Engine/Binaries/Linux/UnrealEditor-Cmd "$PROJ" /Game/Test/ReviewMap \
-game -ResX=1920 -ResY=1080 -nopause -nosplash -stdout
echo "== 3/3 collect screenshots =="
mkdir -p "$REPO/Screenshots"
rm -f "$REPO"/Screenshots/review-*.png
found=0
for src in \
"$REPO/Game/GigLine/Saved/Screenshots"/*/review-*.png \
"$REPO/Game/GigLine/Saved/Screenshots"/*/*/review-*.png \
"$HOME/.config/Epic/GigLine/Saved/Screenshots"/*/review-*.png; do
[ -e "$src" ] || continue
cp -f "$src" "$REPO/Screenshots/"
found=1
done
[ "$found" = "1" ] || { echo "ERROR: no review-*.png found"; exit 1; }
ls -la "$REPO/Screenshots"
+81
View File
@@ -0,0 +1,81 @@
# Scale-test scene builder and FBX exporter for the Blender -> Unreal pipeline.
#
# Builds three 1 m cubes in the project geographic frame (EPSG:26911, origin
# Hollywood x Highland, X east / Y north / Z up):
# ScaleTest_A centred at (0, 0, 0.5) - origin + 1 m size reference
# ScaleTest_B centred at (10, 0, 0.5) - +10 m east probe
# ScaleTest_C centred at (0, 10, 0.5) - +10 m north probe
# and exports them as one FBX with the named export preset "ue_metric_v1".
#
# Run headless (see Art/blender/README.md for the Flatpak invocation):
# blender -b --factory-startup --python Tools/scale_test_export.py -- \
# --out /tmp/gigline/scaletest.fbx --save-blend Art/blender/scaletest/scale-test-<date>.blend
import argparse
import os
import sys
import bpy
def build_scene():
scene = bpy.context.scene
for obj in list(bpy.data.objects):
bpy.data.objects.remove(obj, do_unlink=True)
mat = bpy.data.materials.new("M_ScaleTest_Red")
mat.use_nodes = True
bsdf = mat.node_tree.nodes.get("Principled BSDF")
bsdf.inputs["Base Color"].default_value = (0.8, 0.05, 0.05, 1.0)
specs = {
"ScaleTest_A": (0.0, 0.0, 0.5),
"ScaleTest_B": (10.0, 0.0, 0.5),
"ScaleTest_C": (0.0, 10.0, 0.5),
}
for name, loc in specs.items():
bpy.ops.mesh.primitive_cube_add(size=1.0, location=loc)
obj = bpy.context.active_object
obj.name = name
obj.data.materials.append(mat)
print(f"[scale_test] created {name} at {loc}")
def export_fbx(path):
os.makedirs(os.path.dirname(path), exist_ok=True)
# Named export preset "ue_metric_v1" - change only via a new preset name.
bpy.ops.export_scene.fbx(
filepath=path,
use_selection=False,
object_types={"MESH"},
apply_unit_scale=True,
apply_scale_options="FBX_SCALE_NONE",
axis_forward="-Y",
axis_up="Z",
use_space_transform=True,
use_mesh_modifiers=True,
mesh_smooth_type="OFF",
path_mode="AUTO",
bake_space_transform=False,
)
print(f"[scale_test] exported FBX preset ue_metric_v1 -> {path}")
def main():
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
parser = argparse.ArgumentParser()
parser.add_argument("--out", default="/tmp/gigline/scaletest.fbx")
parser.add_argument("--save-blend", default=None)
args = parser.parse_args(argv)
build_scene()
if args.save_blend:
os.makedirs(os.path.dirname(args.save_blend), exist_ok=True)
bpy.ops.wm.save_as_mainfile(filepath=args.save_blend)
print(f"[scale_test] saved {args.save_blend}")
export_fbx(args.out)
main()
+134
View File
@@ -0,0 +1,134 @@
# Builds /Game/Test/ReviewMap — the visual-review scene — and saves it as a
# map asset. Run before the screenshot pass (Tools/render_screenshots.sh does
# this automatically).
#
# Scene: metric floor, key light, fill sky, sky atmosphere, MPFB character,
# the three 1 m scale cubes, and an AGigLineShotActor with the camera list.
#
# UnrealEditor-Cmd <uproject> -run=pythonscript -script=<repo>/Tools/ue_build_review_map.py \
# -nullrhi -unattended -nopause -stdout
#
# Do NOT load_map here: after a map load the commandlet's spawn context dies
# (every spawn_actor call returns None). Everything is spawned into the
# default world and saved as a new map asset.
import os
import unreal
RESULT = "/tmp/ue-review-map-result.txt"
MAP_PATH = "/Game/Test/ReviewMap"
lines = []
def log(msg):
lines.append(str(msg))
def spawn(cls, loc, label=None, scale=None, rot=None):
a = unreal.EditorLevelLibrary.spawn_actor_from_class(cls, loc)
if a is None:
log(f"spawn {cls.__name__} returned None")
return None
if rot is not None:
a.set_actor_rotation(rot, False)
if scale is not None:
a.set_actor_scale3d(scale)
if label:
a.set_actor_label(label)
return a
# --- scene -------------------------------------------------------------------
try:
# Neutral gray floor material (the scale-test mesh ships red)
floor_mat = None
if not unreal.EditorAssetLibrary.does_asset_exist("/Game/Test/M_ReviewFloor"):
floor_mat = unreal.AssetToolsHelpers.get_asset_tools().create_asset(
"M_ReviewFloor", "/Game/Test", unreal.Material, unreal.MaterialFactoryNew())
n = unreal.MaterialEditingLibrary.create_material_expression(
floor_mat, unreal.MaterialExpressionConstant3Vector)
n.set_editor_property("constant", unreal.LinearColor(0.35, 0.35, 0.35, 1.0))
unreal.MaterialEditingLibrary.connect_material_property(
n, "", unreal.MaterialProperty.MP_BASE_COLOR)
unreal.MaterialEditingLibrary.recompile_material(floor_mat)
unreal.EditorAssetLibrary.save_loaded_asset(floor_mat)
else:
floor_mat = unreal.EditorAssetLibrary.load_asset("/Game/Test/M_ReviewFloor")
floor_mesh = unreal.EditorAssetLibrary.load_asset("/Game/Test/ScaleTest/ScaleTest_A")
if floor_mesh and floor_mat:
floor = spawn(unreal.StaticMeshActor, unreal.Vector(450, 450, -50.0), "ReviewFloor",
scale=(45.0, 45.0, 0.1))
if floor:
floor.static_mesh_component.set_static_mesh(floor_mesh)
floor.static_mesh_component.set_material(0, floor_mat)
log("staged: floor (45 m plane, gray)")
else:
log("floor spawn failed")
if spawn(unreal.DirectionalLight, unreal.Vector(0, 0, 900), "RenderKeyLight",
rot=unreal.Rotator(pitch=-38.0, yaw=35.0, roll=0.0)):
log("staged: DirectionalLight")
if spawn(unreal.SkyLight, unreal.Vector(0, 0, 1200), "RenderFillSky"):
log("staged: SkyLight")
if spawn(unreal.SkyAtmosphere, unreal.Vector(0, 0, 0), "RenderSky"):
log("staged: SkyAtmosphere")
char_mesh = unreal.EditorAssetLibrary.load_asset("/Game/Test/Character/character")
if char_mesh:
ch = spawn(unreal.SkeletalMeshActor, unreal.Vector(0, 0, 0), "ReviewCharacter",
rot=unreal.Rotator(pitch=0.0, yaw=180.0, roll=0.0))
if ch:
ch.skeletal_mesh_component.set_skinned_asset_and_update(char_mesh)
log("staged: MPFB character at origin, facing -Y")
else:
log("MISSING: /Game/Test/Character/character")
for name, x in (("ScaleTest_A", -400.0), ("ScaleTest_B", 0.0), ("ScaleTest_C", 400.0)):
m = unreal.EditorAssetLibrary.load_asset(f"/Game/Test/ScaleTest/{name}")
if m:
c = spawn(unreal.StaticMeshActor, unreal.Vector(x, 600.0, 50.0), f"Cube_{name}")
if c:
c.static_mesh_component.set_static_mesh(m)
log(f"staged: {name} at ({x}, 600, 50)")
shots = spawn(unreal.GigLineShotActor, unreal.Vector(0, 0, 300), "ShotSequencer")
if shots:
shots.set_editor_property("shot_locations", [
unreal.Vector(900, -1100, 500),
unreal.Vector(200, -300, 160),
unreal.Vector(200, 100, 200),
# first-person eye height, just south-east of the character: this is the
# view a player has at spawn, capsule and camera stack in frame
unreal.Vector(120, -260, 165),
])
shots.set_editor_property("shot_rotations", [
unreal.MathLibrary.find_look_at_rotation(
unreal.Vector(900, -1100, 500), unreal.Vector(100, 200, 80)),
unreal.MathLibrary.find_look_at_rotation(
unreal.Vector(200, -300, 160), unreal.Vector(0, 0, 85)),
unreal.MathLibrary.find_look_at_rotation(
unreal.Vector(200, 100, 200), unreal.Vector(0, 600, 50)),
unreal.MathLibrary.find_look_at_rotation(
unreal.Vector(120, -260, 165), unreal.Vector(-200, 600, 90)),
])
shots.set_editor_property("shot_names",
["overview", "character", "scale-cubes", "first-person"])
shots.set_editor_property("quit_when_done", True)
log("staged: ShotSequencer with 4 cameras")
except Exception as exc: # noqa
log(f"staging failed: {exc!r}")
# --- save --------------------------------------------------------------------
try:
world = unreal.EditorLevelLibrary.get_editor_world()
saved = unreal.EditorLoadingAndSavingUtils.save_map(world, MAP_PATH)
log(f"save_map {MAP_PATH}: {saved}")
except Exception as exc: # noqa
log(f"save_map failed: {exc!r}")
with open(RESULT, "w") as f:
f.write("\n".join(lines) + "\n")
+117
View File
@@ -0,0 +1,117 @@
# Unreal-side half of the MPFB character acceptance: imports the skeletal FBX
# with the named import preset "ue_character_v1" and verifies the result.
#
# UnrealEditor-Cmd <uproject> -run=pythonscript -script=<repo>/Tools/ue_character_import.py \
# -nullrhi -unattended -nopause -stdout
import unreal
FBX = "/tmp/gigline/character.fbx"
DEST = "/Game/Test/Character"
REPORT = "/tmp/ue-character-result.txt"
lines = []
def log(msg):
lines.append(str(msg))
def import_character():
if unreal.EditorAssetLibrary.does_directory_exist(DEST):
unreal.EditorAssetLibrary.delete_directory(DEST)
task = unreal.AssetImportTask()
task.filename = FBX
task.destination_path = DEST
task.automated = True
task.save = True
task.replace_existing = True
ui = unreal.FbxImportUI()
ui.import_mesh = True
ui.import_materials = True
ui.import_textures = True
ui.import_as_skeletal = True
ui.import_animations = False
smd = ui.skeletal_mesh_import_data
smd.import_uniform_scale = 1.0
smd.convert_scene_unit = False
smd.force_front_x_axis = False
smd.normal_import_method = unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS
task.options = ui
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True)
def import_texture_and_material():
# Interchange 5.8 does not create material/texture assets for skeletal
# FBX imports; finish the baked-material chain explicitly.
task = unreal.AssetImportTask()
task.filename = "/tmp/gigline/T_Skin_Diffuse.png"
task.destination_path = DEST
task.automated = True
task.save = True
task.replace_existing = True
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
tex = unreal.EditorAssetLibrary.load_asset(f"{DEST}/T_Skin_Diffuse")
log(f"texture: {tex.get_class().get_name() if tex else 'MISSING'}")
mat_path = f"{DEST}/M_Character_Export"
if unreal.EditorAssetLibrary.does_asset_exist(mat_path):
unreal.EditorAssetLibrary.delete_asset(mat_path)
mat = unreal.AssetToolsHelpers.get_asset_tools().create_asset(
"M_Character_Export", DEST, unreal.Material, unreal.MaterialFactoryNew())
tex_expr = unreal.MaterialEditingLibrary.create_material_expression(
mat, unreal.MaterialExpressionTextureSample)
tex_expr.set_editor_property("texture", tex)
unreal.MaterialEditingLibrary.connect_material_property(
tex_expr, "RGB", unreal.MaterialProperty.MP_BASE_COLOR)
unreal.MaterialEditingLibrary.recompile_material(mat)
sm = unreal.EditorAssetLibrary.load_asset(f"{DEST}/character")
entry = unreal.SkeletalMaterial()
entry.set_editor_property("material_interface", mat)
entry.set_editor_property("material_slot_name", "M_Character_Export")
sm.set_editor_property("materials", [entry])
unreal.EditorAssetLibrary.save_loaded_asset(sm)
log("material assigned to slot 0 (via SkeletalMaterial.material_interface)")
unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True)
def verify():
sm = unreal.EditorAssetLibrary.load_asset(f"{DEST}/character")
if sm is None:
log(f"MISSING skeletal asset {DEST}/character; assets: {unreal.EditorAssetLibrary.list_assets(DEST)}")
return
log(f"class: {sm.get_class().get_name()}")
try:
skel = sm.get_editor_property("skeleton")
tree = skel.get_editor_property("bone_tree")
log(f"bones: {len(tree)}")
except Exception as exc: # noqa
log(f"bone count unavailable: {exc!r}")
try:
mats = sm.get_editor_property("materials")
log(f"material slots: {len(mats)} -> {[m.get_editor_property('material_slot_name') for m in mats]}")
except Exception as exc: # noqa
log(f"material slots unavailable: {exc!r}")
try:
bounds = sm.get_editor_property("bounds")
o, e = bounds.origin, bounds.box_extent
log(f"bounds cm: size=({e.x * 2:.1f}, {e.y * 2:.1f}, {e.z * 2:.1f}) origin=({o.x:.1f}, {o.y:.1f}, {o.z:.1f})")
except Exception as exc: # noqa
log(f"bounds unavailable: {exc!r}")
log(f"assets: {unreal.EditorAssetLibrary.list_assets(DEST)}")
# materials actually imported as assets?
for a in unreal.EditorAssetLibrary.list_assets(DEST):
obj = unreal.EditorAssetLibrary.load_asset(a)
log(f" {a} : {obj.get_class().get_name()}")
import_character()
import_texture_and_material()
verify()
with open(REPORT, "w") as f:
f.write("\n".join(lines) + "\n")
+156
View File
@@ -0,0 +1,156 @@
# Creates the Enhanced Input assets the first-person character loads:
# /Game/Input/IA_Move Axis2D, WASD + left stick
# /Game/Input/IA_Look Axis2D, mouse XY + right stick
# /Game/Input/IA_Jump axis1d, Space / gamepad face button
# /Game/Input/IMC_Default InputMappingContext holding the three mappings
#
# Run headless (imports/dirty packages are saved before exit):
# <engine>/Engine/Binaries/Linux/UnrealEditor-Cmd <uproject> -run=pythonscript \
# -script=<repo>/Tools/ue_create_input_assets.py -nullrhi -unattended -nopause \
# -stdout -FullStdOutLogOutput
#
# The result goes to /tmp/gigline/input-assets.txt because 5.8's pythonscript
# commandlet swallows print() output with -stdout.
#
# Mapping choices:
# - Look stays unit-free (no scalar modifier): the character applies its own
# degrees-per-second, and a modifier here would double-scale.
# - Move is a 2D axis: W = +Y, S = -Y, D = +X, A = -X via Negate on the Y axis.
# For keyboard keys the Negate modifier's Y axis maps to the axis's Y.
import unreal
ASSET_DIR = "/Game/Input"
_KEEP_ALIVE = []
def negate_y_modifier():
"""A negate-by-Y modifier, built with its Y flag set and X/Z cleared.
Returns the object plus its falsy reference; constructing the default
modifier gives all axes true, so each flag is set explicitly. The caller owns
keep-alive.
"""
neg = unreal.InputModifierNegate()
neg.set_editor_property("x", False)
neg.set_editor_property("y", True)
neg.set_editor_property("z", False)
return neg
RESULT = "/tmp/gigline/input-assets.txt"
lines = []
def log(msg):
lines.append(str(msg))
unreal.log(str(msg))
def fresh(name, cls, factory):
"""Load the asset if it exists, otherwise create it.
Deleting then recreating in the same commandlet run fails: create_asset
returns None for a path deleted earlier in the process. Overwriting in place
is also what keeps the assets' identities stable across reruns.
"""
path = f"{ASSET_DIR}/{name}"
existing = unreal.EditorAssetLibrary.load_asset(path)
if existing is not None:
return existing
return unreal.AssetToolsHelpers.get_asset_tools().create_asset(
name, ASSET_DIR, cls, factory())
def save(asset):
unreal.EditorAssetLibrary.save_loaded_asset(asset, only_if_is_dirty=False)
def make_key(name):
"""Build an FKey by name.
`unreal.InputCoreTypes` is not exposed to Python in 5.8 and `unreal.Key` does
not accept a constructor argument, but the struct's `import_text` parses the
key's export text, which is just its name.
"""
key = unreal.Key()
key.import_text(name)
return key
def build():
if not unreal.EditorAssetLibrary.does_directory_exist(ASSET_DIR):
unreal.EditorAssetLibrary.make_directory(ASSET_DIR)
# --- actions ---------------------------------------------------------
# Move keys are one-dimensional: W/S carry forward/back on the value's single
# component and D/A carry strafe, combined by the character. A 2D value would
# need per-key axis modifiers, and modifiers do not survive the save path here.
ia_move = fresh("IA_Move", unreal.InputAction, unreal.InputAction_Factory)
ia_move.set_editor_property("value_type", unreal.InputActionValueType.AXIS1D)
save(ia_move)
ia_look = fresh("IA_Look", unreal.InputAction, unreal.InputAction_Factory)
ia_look.set_editor_property("value_type", unreal.InputActionValueType.AXIS2D)
save(ia_look)
ia_jump = fresh("IA_Jump", unreal.InputAction, unreal.InputAction_Factory)
ia_jump.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
save(ia_jump)
# --- mapping context -------------------------------------------------
imc = fresh("IMC_Default", unreal.InputMappingContext, unreal.InputMappingContext_Factory)
# Nine entries, one per key. No modifiers: the character negates S and A in
# C++ instead, because a modifier object attached to a mapping struct
# serialises as None on save in this build (the struct's object array does not
# keep the modifier alive across the save path). One less moving part, and the
# negation is visible where it is used.
# Two actions feed the one-dimensional move: IA_Move for forward/back, and a
# second action for strafe, both reporting on the value's single component.
ia_strafe = fresh("IA_Strafe", unreal.InputAction, unreal.InputAction_Factory)
ia_strafe.set_editor_property("value_type", unreal.InputActionValueType.AXIS1D)
save(ia_strafe)
ia_back = fresh("IA_MoveBack", unreal.InputAction, unreal.InputAction_Factory)
ia_back.set_editor_property("value_type", unreal.InputActionValueType.AXIS1D)
save(ia_back)
ia_left = fresh("IA_StrafeLeft", unreal.InputAction, unreal.InputAction_Factory)
ia_left.set_editor_property("value_type", unreal.InputActionValueType.AXIS1D)
save(ia_left)
mapping_table = [
(ia_move, "W"), (ia_back, "S"), (ia_strafe, "D"), (ia_left, "A"),
(ia_move, "Gamepad_LeftY"), (ia_strafe, "Gamepad_LeftX"),
(ia_look, "MouseX"), (ia_look, "MouseY"), (ia_look, "Gamepad_Right2D"),
(ia_jump, "SpaceBar"), (ia_jump, "Gamepad_FaceButton_Bottom"),
]
mappings = []
for action, key_name in mapping_table:
try:
entry = unreal.EnhancedActionKeyMapping()
entry.set_editor_property("action", action)
entry.set_editor_property("key", make_key(key_name))
mappings.append(entry)
except Exception as exc: # noqa: BLE001
log(f"skip {action.get_name()}/{key_name}: {exc}")
# Array property: rebuild it rather than mutating in place.
imc.set_editor_property("mappings", mappings)
save(imc)
log(f"IA_Move={ia_move.get_path_name()} IA_Look={ia_look.get_path_name()} "
f"IA_Jump={ia_jump.get_path_name()}")
log(f"IMC_Default={imc.get_path_name()} mappings={len(mappings)}")
log("assets=" + ",".join(unreal.EditorAssetLibrary.list_assets(ASSET_DIR)))
try:
build()
except Exception as exc: # noqa: BLE001 - surface the failure in the result file
import traceback
log(f"FAILED: {type(exc).__name__}: {exc}")
log(traceback.format_exc())
with open(RESULT, "w") as handle:
handle.write("\n".join(lines))
+60
View File
@@ -0,0 +1,60 @@
# Unreal-side half of the scale test: imports the Blender scale-test FBX with
# the named import preset "ue_metric_v1" and measures the result.
#
# Run headless:
# <engine>/Engine/Binaries/Linux/UnrealEditor-Cmd <uproject> -run=pythonscript \
# -script=<repo>/Tools/ue_scale_test_import.py -nullrhi -unattended -nopause -stdout
import unreal
FBX = "/tmp/gigline/scaletest.fbx"
DEST = "/Game/Test/ScaleTest"
def import_fbx():
task = unreal.AssetImportTask()
task.filename = FBX
task.destination_path = DEST
task.automated = True
task.save = True
task.replace_existing = True
ui = unreal.FbxImportUI()
ui.import_mesh = True
ui.import_materials = True
ui.import_textures = False
ui.import_as_skeletal = False
smd = ui.static_mesh_import_data
smd.import_uniform_scale = 1.0
smd.combine_meshes = False
smd.convert_scene_unit = False
smd.force_front_x_axis = False
smd.normal_import_method = unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS
task.options = ui
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True)
def measure():
# Named import preset "ue_metric_v1" measures in Unreal centimetres.
lines = ["---- measured local-space bounds (cm) ----"]
for name in ("ScaleTest_A", "ScaleTest_B", "ScaleTest_C"):
path = f"{DEST}/{name}"
asset = unreal.EditorAssetLibrary.load_asset(path)
if asset is None:
lines.append(f"MISSING asset {path}")
continue
box = asset.get_bounding_box()
mn, mx = box.min, box.max
lines.append(
f"{name}: origin=({(mn.x + mx.x) / 2:.2f}, {(mn.y + mx.y) / 2:.2f}, {(mn.z + mx.z) / 2:.2f}) "
f"size=({mx.x - mn.x:.2f}, {mx.y - mn.y:.2f}, {mx.z - mn.z:.2f})"
)
lines.append(f"assets: {unreal.EditorAssetLibrary.list_assets(DEST)}")
with open("/tmp/ue-scale-test-result.txt", "w") as f:
f.write("\n".join(lines))
import_fbx()
measure()