Skip to content

Blender extension on mhp-formats and mhfu-port - #48

Merged
sp00ktober merged 13 commits into
mainfrom
blender/70
Oct 4, 2026
Merged

sp00ktober merged 13 commits into
mainfrom
blender/70

Conversation

@sp00ktober

Copy link
Copy Markdown
Contributor

The research Blender addon becomes a Blender 4.2+ extension in apps/blender (GPL-3.0-or-later), on mhp-formats and mhfu-port. For sp00ktober/MHFU-ModKit#70.

  • Extension (mhfu_blender):
    • Import: an MHFU model PAC, a built port, or an MHP3rd model with its emNNN moveset.
    • Export as an MHFU PAC, and push it into the memory stick's inject folder.
    • Animation: bones rest unrotated, so an Action's channels are the engine's. Edited clips are written back through motion.put; unedited ones stay byte for byte.
    • Geometry: groups are written in place while their topology holds and rebuilt otherwise; UV seams split.
    • The re-skin, in-game-anim and port operators are dropped: a manifest does those.
  • Packaging: uv run apps/blender/build.py stages the version and the wheels from uv.lock, then runs extension build and validate.
    • Pure-Python wheels only, so one zip serves every OS; numpy is Blender's own.
    • build.py test installs the zip into a throwaway profile and runs apps/blender/tests inside Blender. CI has no Blender, so those tests skip there.
  • mhfu-port:
    • numpy>=1.24 (Blender 4.2 to 5.0 bundle 1.24 or 1.26).
    • mhfu_port.model is the studio's loader, now shared. It stays free of mhfu, and a test enforces that.
    • motion.put is the inverse of fk.rig_clip.
    • fk.Curves now uses the engine's cubic spline with the key eases (EU 0x088630D0) instead of linear interpolation. Between keys, linear was off by up to ~430 units.
      • Built ports are byte-identical before and after.
      • verify's tear and stretch figures move a little.
      • Four studio goldens are regenerated; two of them were already stale on main.
  • mhp-formats: quantize_weights makes a vertex's weights sum to 128, as native data does. Built ports are unchanged.
  • mypy: native_parser = true, because the bpy stubs use 3.12 generics. mypy calls this option experimental.

The issue's bugs:

  1. Multi-group PMO export now goes through Pmo.to_bytes.
    2, 4, 6, 7. Geometry, skeletons, textures and vertex scale are fixed by the shared loader.
  2. Pmo.material is confirmed by render. The 284 unexplained groups were the old walker's 0x20 stride reading Khezu and Red Khezu group records as a fifth mesh.
  3. The moveset is an explicit input.
  4. The port operator is gone.

Tested:

  • pytest with game data: everything passes; studio 701 passed offscreen.
  • Inside Blender: 40 passed on 4.2.23 and 4.5.3; 39 passed and 1 skipped on 5.2.2 (the skip is the test for the pre-4.4 Action API).
  • The installed zip round-trips the Tigrex and the brute_tigrex and zinogre builds byte for byte on all three Blenders.
  • All 101 MHFU model PACs export byte-identical.
  • ruff, strict mypy and pre-commit pass.
  • Not tested: a push into a running game.

🤖 Generated with Claude Code

sp00ktober and others added 13 commits October 4, 2026 19:27
… tests

apps/blender is a Blender 4.2+ extension (mhfu_blender, GPL-3.0-or-later)
with an empty class list for the operators to come. build.py stages the
source with the dependency closure as pure wheels at uv.lock's pins
(numpy, psutil and rabbitizer left out), writes version and wheels into
the staged manifest and runs Blender's extension build and validate;
`build.py test` installs the zip into a throwaway profile and runs
apps/blender/tests inside Blender with a cached pytest. Outside Blender
those tests skip. mypy reads fake-bpy-module's 4.2 stubs through its
native parser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The inverse of fk.rig_clip. Both, and motion.split (which replaces the private
_split in motion.build), deal tracks to parts through one fk.part_joints. A part
keeps its own clip object, loop and extra tracks where the clip leaves them, so a
read-and-write round trip is byte-identical: every slot of all 101 MHFU model
PACs with an in-game anim. motion.frames and motion.filled replace slots.frames
and verify.Port.slots' own copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ender

The bpy- and Qt-free assembly of the studio's monster scene (groups, textures,
clips, from_bytes, from_path) moves to mhfu_port.model, which imports nothing
of mhfu, so a Blender extension can bundle it; a subprocess test blocks mhfu's
live chain and imports what the extension uses. The studio's Scene subclasses
Model and keeps the manifest, build id and pose.

What it needed from the mhfu chain moves to mhfu-free homes: the record map to
records.for_moveset, the donor file offsets GEO/ANIM to records, the PAC entry
numbers to model (diff.py drops its copy), clip_key to model. A donor's moveset
is an input, the emNNN file; from_path defaults it to the file two after the
model PAC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Between two keys the engine evaluates a cubic with the left key's ease_out
and the right key's ease_in as slopes in raw units per frame; fk drew a
straight line. Built ports are byte-identical; verify/stretch numbers and
the studio's posed goldens between keys move (rewritten in the container).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…read with

So a caller holding only the bytes (a .blend) can read the same model again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
File > Import > MHFU Model (and the sidebar's MHFU tab) reads an MHFU model
PAC, a built port or an MHP3rd model through mhfu_port.model: an armature
whose bones rest unrotated so pose channels are the engine's, one mesh per
PMO group with its skin, UVs, normals and textured materials, and an Action
per clip whose Bezier keys draw the engine's spline. Action -> Clip reads
an untouched Action back to its clip exactly; the armature keeps the source
bytes so the .blend alone reads the model again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
So an editor can rebuild a single group of an existing PMO.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An empty track collapses its joint's geometry; a track the part keyed comes back as rest().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… game

A group with its vertex count and faces unchanged is written in place, weights over its own
palette included; any other is rebuilt and the palette repatched for the groups after it.
Edit Mode bone moves ship, a changed armature is refused, every slot's Action goes back
through motion.put. An MHP3rd model exports as an MHFU PAC without clips. Push writes into
the memory stick's inject folder: same size in place, larger for the relocate path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
quantize_weights rounds the largest remainders up, so a vertex whose weights sum to 1.0 sums
to the unit exactly, as the games' own vertices do. quantize_vertices and the Blender
extension's in-place weight write both go through it. The brute_tigrex and zinogre builds
and every port digest are unchanged: their weights are already whole raw units.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Geometry carries each corner's UV; a vertex whose faces give it several keeps the first and is
copied for each other, which rebuilds its group instead of dropping the edit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Blender 4.2 shows about half the Tigrex's custom normals 3 to 8 degrees off the file's, so the
import records each normal as shown, and a group written in place keeps every stored normal
that has not moved from it. Byte-identical on 4.2, 4.5 and 5.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sp00ktober
sp00ktober merged commit e03deb4 into main Oct 4, 2026
5 checks passed
@sp00ktober
sp00ktober deleted the blender/70 branch October 4, 2026 20:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant