diff --git a/PHOTOMODE.md b/PHOTOMODE.md new file mode 100644 index 00000000..f6930ef1 --- /dev/null +++ b/PHOTOMODE.md @@ -0,0 +1,157 @@ +# Photo Mode ? proposal and research notes + +**Status: shelved 2026-08-09. Nothing implemented. This is a design record so the research does +not have to be repeated.** + +Goal: produce clean head-on renders of every 'Mech, on a solid background with no UI, to use as +source art for the MFD / Radar / print damage paper dolls. + +Every source reference below was verified against the tree on 2026-08-09. + +## Why this is cheap: the capture already exists + +The game already renders the mech alone, on a solid background, at stepped rotations, and reads +it back out of the frame buffer. It is the **MechView** feature, and it ships enabled behind a +switch. Photo mode is that same code writing files instead of a texture. + +`-help` documents it (`MW4Application.cpp` around line 919): + +```text +-mechview <1|2> Show the rotating mech view. 1 = on the radar +-mv <1|2> screen, 2 = on the main screen. Default: off. +``` + +What it does today: on entering MechLab it bakes a **25-frame turntable** of the current mech into +a 2048x2048 atlas of 360x360 cells (5x5, so 14.4 degrees per step), then flips through the cells +to animate a rotating mech on the pod's radar monitor or on the main screen. It is pre-rendered +because drawing live 3D onto a *secondary* DirectDraw device every frame is not practical ? the +same constraint behind all the MFD work. + +Authored by "jcem", same era as the other CTCL cabinet features. + +## The machinery, with verified locations + +| Piece | Where | Notes | +|---|---|---| +| Per-frame render hook | `CoreTech/Libraries/GameOS/WinMain.cpp:1215` | `if (g_pfnCTCL_AfterBeginScene) { (*g_pfnCTCL_AfterBeginScene)(); }` ? called right after `BeginScene` | +| Hook pointer | `WinMain.cpp:43` | `void (__stdcall *g_pfnCTCL_AfterBeginScene)() = NULL;` ? free unless CTCL installs it | +| The bake (our template) | `mw4/Code/MW4/MW4Shell.cpp:14857` | `CTCL_AfterBeginScene()` ? the whole capture loop | +| The playback | `MW4Shell.cpp:14936` | `CTCL_UpdateMechView()` ? swaps in after the bake | +| Hook install | `MW4Shell.cpp:1705` | `g_pfnCTCL_AfterBeginScene = CTCL_AfterBeginScene;` | +| Exclusivity assert | `MW4Shell.cpp:1623` | `gosASSERT(!g_pfnCTCL_AfterBeginScene);` ? only one owner allowed | +| Solid-colour clear | `CoreTech/Libraries/GameOS/3DRasterizer.cpp:1044` | `gos_SetupViewport(bool FillZ, float ZBuffer, bool FillBG, DWORD BGColor, ...)` ? background colour is already a parameter; CTCL passes `0x00000000` | +| Mech-only draw | `mw4/Code/MW4/MechLab.cpp:1598` | `MechLab::UpdateDisplay()` is just `s_MechCamera->DrawScene(true)` ? **no UI is drawn** | +| Rotation | `MechLab.cpp:257` `Execute()` | `MechLab.cpp:285` is the `yaw += 0.02f` spin; `MechLab.cpp:275` gates an absolute-yaw branch on `g_pfnCTCL_AfterBeginScene == CTCL_AfterBeginScene` | +| Chassis count | `MechLab.cpp:1185` | `MechLab::GetChassisCount()` | +| Chassis swap | `MechLab.cpp:1199` | `MechLab::SetMech(int mech_id, const char *mech_name, const char *path)` | +| MechLab construction | `MW4Shell.cpp:4693` | `MechLab::Instance = new MechLab;` | +| Camera | `MechLab.cpp:175` | `SetPerspective(1.0f, 1100.0f, Stuff::Pi_Over_3)`, full viewport | +| Atlas dimensions | `CoreTech/Libraries/GameOS/render.cpp:1443` | `m_ptMechViewDim` 2048x2048, `m_ptMechViewSize` 360x360 | +| Mode global | `CoreTech/Libraries/GameOS/VideoCard.cpp:64` | `g_nMechViewType` ? 0 off, 1 radar screen, 2 main screen | +| Switch parsing | `MW4Application.cpp:1575` | `-mechview ` / `-mv ` | + +The capture loop, abridged from `CTCL_AfterBeginScene`: + +```cpp +for (int qq = 0; qq < nCount; qq++) { + gos_SetupViewport(1, 1.0, 1, 0x00000000, 0.0, 0.0, 1.0, 1.0); // clear to solid colour + MechLab::Instance->Execute(); // step to an absolute yaw + MechLab::Instance->UpdateDisplay(); // draw ONLY the mech + BackBufferSurface->Lock(&rcSource, &desc2, DDLOCK_..., NULL); // read back a centre crop + /* memcpy into the atlas cell */ + BackBufferSurface->Unlock(&rcSource); +} +``` + +Also noted: `MechView_Before()` / `MechView_After()` at `MechLab.cpp:97` and `:102` toggle +`ClearBackBufferBeforeDraw`. They are **defined and never called** anywhere in the tree ? dead +helpers. They are an alternative route to the same clear if the viewport approach proves awkward. + +## Chosen design + +**A fully automatic batch mode. No hotkeys, no new UI, no interaction.** + +```text +MW4.exe -photoall +``` + +Boots, writes `photos\.bmp` for all 65 chassis, exits. Optional later: +`-photoall ` for an n-step turntable per chassis instead of a single head-on frame. + +### Rejected: a new MechLab tab + +MechLab tabs are defined in `Content/ShellScripts/MechBay/*.script` **plus** code callbacks +(`ML_CallbackHandler`, the `MLDataSetup*` IDs). A new tab means script authoring, new callback +IDs and layout work ? and the tab's own UI would be on screen during capture, which is the exact +thing being avoided. It would also ship to players who have no use for it. + +### Rejected: interactive hotkey as the primary interface + +A `-photo` switch plus F9/F10 in MechLab is feasible ? `gos_GetKeyStatus()` is usable from shell +code (see `MW4Shell.cpp:11678`) ? but it needs one manual session per chassis. Worth adding later +as a supplement for reshooting a single mech; not the main path. + +### Getting into MechLab without navigating menus + +Two routes: + +1. **Drive the shell.** Add a `PhotoShellStart` to the existing `shellStart` enum; there is + precedent (`InstantActionShellStart`, `CampaignShellStart`, `CampaignLoadShellStart`). Fits the + existing design but requires understanding the shell state machine. +2. **Bypass the shell.** Construct `MechLab` directly the way `MW4Shell.cpp:4693` does, loop, + capture, exit. A batch art tool needs no shell screen at all, so this is smaller, more + predictable, and cannot get stuck on a dialog. Same shape as the editor's headless `-report`. + +**Recommendation: route 2.** + +## Implementation plan + +| # | File | Work | +|---|---|---| +| 1 | `MW4Application.cpp` (~1575, beside `-mechview`) | parse `-photoall` (+ optional step count); set globals | +| 2 | `MechLab.cpp:275` | widen the absolute-yaw condition so photo mode also sets an exact angle instead of the `+= 0.02f` spin | +| 3 | new `Photo_AfterBeginScene()` | modelled on `CTCL_AfterBeginScene`; explicit angles rather than the 5x5 grid maths, configurable crop, writes files | +| 4 | install into `g_pfnCTCL_AfterBeginScene` | assignment only; `WinMain.cpp:1215` already calls it | +| 5 | **BMP writer** | **the only genuinely new code, ~40 lines.** R5G6B5 or X8R8G8B8 to 24bpp BMP. Nothing in the tree writes images ? verified, there is no screenshot facility anywhere | +| 6 | `-help` | document the switch | + +## Open questions ? need a real run to settle + +1. **Framing per chassis.** The camera is fixed (`SetPerspective(1.0f, 1100.0f, Pi_Over_3)`, full + viewport) with the mech at the scene root. A Flea and an Atlas will not fill the frame equally. + Probably acceptable since each mech gets its own 512 sheet, but if consistent scale is wanted, + derive a per-mech camera distance from the mech's bounding box. +2. **Which yaw is head-on.** `yaw = 0` gives the model's native orientation; whether that is front, + back or side has to be eyeballed once and then hard-coded. + +## Constraints + +- **Run windowed.** Fullscreen is `bitdepth=16` (R5G6B5). Windowed sets `bitDepth = DesktopBpp` + = 32. Bigger window gives a bigger capture, up to the engine's 4:3 maximum of 1600x1200 + (`ImageHlp.cpp` asserts the supported mode list). 1200px tall beats the 1024 the original art + pipeline started from. +- **Must not clash with `-mechview` / CTCL.** `MW4Shell.cpp:1623` asserts the hook is unowned; + both cannot install it. Photo mode should refuse to arm when MechView or CTCL is active. +- Requires a Windows VC6 rebuild to test; none of this is verifiable on Linux. + +## Possible phase 2: per-component capture + +The mech is an `ElementRenderer` element with sub-elements per body part. If per-part visibility +is reachable, photo mode could capture **each zone separately** and hand over the exploded pieces +directly ? turning the paper-doll pipeline from "capture, then cut up by hand in an image editor" +into "capture, done". Not investigated. This is the difference between a convenience and a genuine +step change, so it is worth checking the element hierarchy before finalising the design. + +## Why this was wanted + +From the MFD/Radar mapping work on 2026-08-09 (see `MFD-RADAR-MAPPINGS.md`): + +- **The Urbanmech has no damage art at all.** Its external MFD, radar and cockpit-atlas bitmaps + are byte-identical copies of the Uller's; only its mechlab portrait is genuine, and that is a + cropped three-quarter view unusable as a paper-doll source. It is the only unintentional + placeholder in the roster (Behemoth II sharing Behemoth's art is deliberate). Fixing it needs + new source renders ? exactly what photo mode would produce. +- **Several chassis have crude MFD sheets.** `thor`, `loki`, `bushwacker`, `solitaire`, `chimera`, + `cougar` all sit in the 0.85?0.90 fit band with loose boxes. The Atlas was regenerated from its + Radar art (0.795 -> 0.956), but that trick only works where good Radar art exists. Photo mode + would remove that dependency.