Skip to content
View in the app

A better way to browse. Learn more.

ResHax

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.
Help us keep the site running.
Zero Tolerance for Disrespect

300 heroes, 70% of the file parsed—need help.

Featured Replies

I’ve lost count of how many attempts I’ve made, but I still haven't managed to fully restore the mesh. Is there anyone knowledgeable about this who could help with the .model/.x formats from the game *300 Heroes*? I don't want to proceed any further until I've restored the mesh; I was able to extract the model, bones, and animations from the files—they aren't encrypted, just using a different structure.

The problem: the rendered model has holes. I want to patch them, but nothing is working so far. When assembling the model at the vertex level, it looks perfect, but it tears apart when the triangles are assembled.

Last script python

157_filled_render.png

preview (1).png

preview (3).png

preview (2).png

preview.png

157_skin11.zip ezgame_to_fbx.py

  • Author

I'm reverse engineering the model format from the Chinese MOBA 300 Heroes (300英雄), newer engine.

Most of it is cracked and I can already export a rigged FBX. I'm stuck on three things

and would appreciate any pointers — full details below so you don't have to take my word for anything.

Tested on two samples: 157_skin11.model (complex character with wings and ribbons)

and 334.model (simple one, used as a control).

0x00  magic 'EG3D'
0x08  u32   main block size

0x0C  JSON metadata, length = main block size
      then raw binary buffers, referenced by offsets from the JSON

generator: "ezgame", version: "2.0", leftHandCoord: true.

Note this is not the older jumpX (.x) format that the public jumpxfbx tool handles.

This is a newer engine used by the same game — I couldn't find any existing tooling for it.

JSON meta — array of 8 elements

index

contents

meta[0]

header (generator, version, leftHandCoord)

meta[1]

root node

meta[2]

materials

meta[3]

groups → submeshes

meta[4]

bone bind palettes (inverse-bind matrices)

meta[5]

skeleton nodes

meta[6]

misc

meta[7]

animation clips

Submesh

sm = [materialIndex, vertexCount, [attrs...], [idxOffset, idxCount]]

Attributes (attr[0] = type, attr[5] = data offset):

type

what

bytes/vert

encoding

0

position

6

u16×3, dequant bias + raw*scale

1

Normals

4

NOT CRACKED ← question 1

6

Uv

4

u16×2, dequant, V-flip (v = 1-v)

5

Skin weights

8

float16×4

4

Bone indices

4

uint8×4 (index into the submesh's bind palette)

2

Tangents

8

not examined

Buffers are packed back-to-back with no padding between them.

Example for 157_skin11, submesh 0 (vertexCount = 13485):

positions 2711852 + 13485*6 = 2792762  (+2 pad, +12 scale, +12 bias)
normals   2792788 + 13485*4 = 2846728
UV        2846728 + 13485*4 = 2900668  (+8 scale, +8 bias)
weights   2900684 + 13485*8 = 3008564
bones     3008564 + 13485*4 = 3062504
tangents  3062504 + 13485*8 = 3170384  ← exactly the start of the index buffer

The last attribute lands exactly on the index buffer offset, so the layout checks out byte for byte.

Position dequantisation

scale and bias (3 floats each) sit immediately after the u16 array, with variable padding.

Don't trust attr[6] / attr[7] for this (they mean something else) — instead scan the

64 bytes following the data and take the first triple where

1e-9 < scale < 1e-2 and |bias| < 5.

Index buffer

u16, first 6 values are a header (garbage, values >= vertexCount) — skip them

then plain triples = triangle LIST

I brute-forced this: it's definitely a list. Strip, fan, planar layout (three separate

streams) and 14 different skip offsets all give roughly half the manifoldness, and the

planar interpretation is absurd (surface area 2151 vs 11). The buffer is monotonic and

vertex-cache optimised — index spread within a triangle has median 3, p90 = 12.

Skeleton

From the inverse-bind matrices in meta[4]: 64 bytes per bone, row-major → transpose →

invert = world bind matrix. For sample 157 that's 113 bones, named

Bip001 Pelvis / Spine / L Thigh … — a standard 3ds Max Biped, plus artist-added bones

Bone01, Bone39..Bone59 driving ribbons and hanging ornaments.

Zero conflicts across the 4 palettes.

What already works

The attached script runs inside Blender and does the whole pipeline:

decode → cleanup → skeleton → skinning (4 bones/vertex) → FBX export.

The result opens fine in Autodesk FBX Review with the skeleton correctly inside the mesh.

Two format-specific gotchas worth sharing:

1. strip→list conversion garbage. ~411 triangles out of 19074 (2% by count but

22% by surface area) with an edge longer than 20% of the model diagonal. They're bound

to dozens of unrelated bones — not an object, just scattered bridges. My guess is the game

converted its older jumpX assets to EG3D and the converter baked strip stitches straight

into the triangle list. Fixed by an edge-length threshold — but measure the diagonal over

the WHOLE model, not per submesh (I lost a lot of time to that: a small submesh has a

small diagonal, so its normal triangles get flagged as giants).

2. Billboard materials. A material whose name ends in _c (here 157_skin11_c) is a

camera-facing card set — the engine rotates it toward the camera every frame, so in a static

viewer its geometry looks like chaos. My control model 334.model has no _c materials,

which is exactly why it read clean from the start.

Question 1: can't crack the normal packing

Attribute type 1, exactly 4 bytes per vertex. What I tried:

interpretation

result

int8 xyz / 127

|n| median 0.815, only 14% unit length

uint8 (x-128)/127

|n| median 1.151

10:10:10:2 signed

|n| median 0.950, 14% unit

11:11:10 signed

|n| median 0.973, 16% unit

octahedral int16×2

|n| = 1.000, but agreement with face normals ~48% (i.e. random)

Octahedral gives unit length but the directions don't correlate with geometric face normals,

so either it's the wrong packing or the wrong component order. Raw bytes of the first vertices:

[  48 -127   81  -65]
[ -49 -105   52  -66]
[  89  -14 -102   59]
[-110    5   92  -76]
[-110    5   92  -76]

Question: has anyone seen this style of 4-byte normal packing? Could it be a QTangent

(a quaternion encoding normal + tangent + binormal in one go)? That would explain why reading

it directly as a normal never lines up. Note there's also a separate 8-byte tangent attribute,

which argues against QTangent — but I'd rather ask than guess.

Question 2: telling a real hole from a legitimate open edge

The mesh has open boundaries. Some are legitimate (ribbon edges, cape hem, clothing slits),

some look like genuine holes in the body (chest, shoulders, feet).

Three metrics I tried all turned out to be useless:

rendering without backface culling simply cannot show holes — the inside of the far surface

gets shaded like a normal surface, so the model always looks solid;

centroid fan-filling large boundary loops creates enormous sheets across the model;

a "closed empty region in the 2D projection" detector counts the space between ribbons

as holes (157 has lots of them → 9%, the simple 334 → 0.6%).

Meanwhile the triangle density is correct: after welding, 15318 triangles over 8036 vertices

= 1.91, where a closed manifold would be 2.0. So no triangles are missing and the decode is complete.

Question: is there a reliable automated way to distinguish "hole in the body" from

"legitimate open edge" on a game model full of alpha cards and ribbons? For now I've fallen

back on doing it by hand in Blender via Select → All by Trait → Non Manifold.

Question 3: animation transfer

meta[7] holds 17 clips: bat_idle, bat_run, bat_specialidle, bingdong, dance,

dead, relive, single_attack_attcom_1/2, single_chuansong, single_run,

single_skill_1..4, single_touzhi_1/2.

About 390 channels per clip across 139 nodes, so essentially the whole skeleton is animated.

The problem: the animations are expressed in the node coordinate frame (meta[5]), while

I build the skeleton from the mesh bind matrices. No single rigid transform maps one onto the

other — it looks like it needs a per-bone transfer.

Question: what's the correct general approach for relating a node hierarchy to a bind

skeleton in formats like this? I'm after the general method, not necessarily anything specific

to this engine.

Time sinks, in case they save someone else

Compute thresholds against the whole model diagonal, not per submesh.

Multi-object OBJ (o s0, o s1) breaks in some viewers — faces of the second object start

referencing vertices of the first. Write a single object, or de-index the OBJ.

Windows paths: "D:\157_skin11.model" without the r prefix — Python eats \157 as an

octal escape. Use forward slashes.

Attached

file

what

ezgame_to_fbx.py

Blender script: .model → rigged FBX

EG3D_WORK_REPORT-EN.md

full technical write-up, including every rejected hypothesis

157_FINAL.obj

sample decode output

Any thoughts welcome, especially on the normals and the animation transfer.

ezgame_to_fbx.py 157_FINAL.zip EG3D_WORK_REPORT_EN.zip

Create an account or sign in to comment

Account

Navigation

Search

Search

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.