May 1, 20251 yr Localization I'm trying to reverse engineer an old EA Bright Light game called Create. Inside the .big files, there were .ea3 and .ea3high files, which I know are uncompressed 3D models by the file structure. What I want to do is find the lengths/offsets of the mesh data so I can convert the files to .obj without having to guess where the data chunks start. The .ea3high files are largely irrelevant since all they seem to do is add a couple of vectors to each vertex (probably tangent data and whatnot). I did managed to figure out the stride of the main mesh data in some smaller .ea3 files.. The vertex data always begins with a small header that starts with PH and ends with 01 00 00 00... The offsets/lengths I'm looking for are likely in those header lines somewhere, but I can't seem to find a value that matches up consistently. Once the vertex, normal, and UV data ends, it switches to face data in short TStrip form. There are sometimes multiple meshes in one file as well, with each section prefaced by similar PH headers. I've attached the small mesh I successfully extracted, along with some screenshots from Model Researcher and a couple of larger meshes. I mainly just want to find a consistent way to calculate where the face data starts. SampleMesh.7z OtherMeshes.7z Edited May 2, 20251 yr by jmancoder
May 2, 20251 yr Supporter 7 hours ago, jmancoder said: I mainly just want to find a consistent way to calculate where the face data starts. Hi, what's the base for your calculation? End of the vertex header, start of vertex block? Maybe I don't understand the problem - usually you'd search for 000001000200 here to find the start of the face index blocks. Edited May 2, 20251 yr by shak-otay
May 2, 20251 yr Author Localization 17 hours ago, shak-otay said: usually you'd search for 000001000200 here to find the start of the face index blocks I did manage to make a Python script to extract most of the face and vertex data by just looking for a sequence like that. I feel like there's something off about a few of the .ea3 files though. No matter what I try, the faces are heavily distorted on meshes like sp_the_ferriswheel_v0.ea3, even when I try to find the offsets manually in Model Researcher and hex2obj. I think they have a slightly different file structure. I was thinking if I could interpret the PH header that appears before each submesh, it would be easier to see what I'm reading wrong. I've attached a couple more meshes that didn't convert well. MoreMeshes.7z
May 3, 20251 yr Supporter 8 hours ago, jmancoder said: I was thinking if I could interpret the PH header that appears before each submesh,
May 3, 20251 yr Supporter Solution End of guess work guys...😉 Hope you are familiar with 010 HEX Editor Templates... You can easily rewrite it to the Noesis form. Or maybe I can... Found only 32 / 36 stride type in samples you provide. There may be more I guess. Template parse only 32 / 36 stride. //------------------------------------------------ //--- 010 Editor v14.0 Binary Template // // File: // Authors: // Version: // Purpose: // Category: // File Mask: // ID Bytes: // History: //------------------------------------------------ LittleEndian();OutputPaneClear(); local uint32 i,j,k,l,m,n; char Sign[4]; uint32 TotalFileSize; uint32 MatrixOffset; uint32 MeshDataBaseOffset; FSkip(16); float Matrix4x4[16]; float BBox[8]; uint32 Unknown_0, UnknownOffset, MaterialIndex, MaterialIndexOffset, TextureCount, MaterialInfoOffset, Unknown_1, Unknown_2, BoneMatrixOffset, Unknown_3, Unknown_4, Unknown_5, Unknown_6, Unknown_7, RootOffset, Unknown_8; FSeek(MaterialIndexOffset); struct { struct { uint32 MaterialPropOffset; ubyte TextureMapCount,Unknown_1,Unknown_2,Unknown_3; ubyte Unknown_4,Unknown_5; uint16 Unknown_6; uint32 Unknown_7; }MaterialInfo[MaterialIndex]<optimize=false>; struct { for (i=0; i < MaterialIndex; i++) { FSeek(MaterialInfo[i].MaterialPropOffset); struct { ubyte TextureIndex; ubyte Unknown_0,Unknown_1,Unknown_2; float Unknown_3; }MaterialProp; } }MaterialProperties; FSeek(MaterialInfoOffset); uint32 TextureNameOffset; FSeek(TextureNameOffset); struct { string TextureName; }Texture[TextureCount]<optimize=false>; }MaterialTable; FSeek(UnknownOffset); struct { float Unknown_0[8]; uint32 Unknown_1[4]; float Unknown_2[8]; }UnknownData; struct { uint32 MeshIndex; uint32 Unknown_16,MeshInfoOffset; FSeek(MeshInfoOffset); struct { float BBox[8]; uint32 ShapeInfoOffset, Unknown_0, UnknownOffset; ubyte MaterialIndex, Unknown_1, Unknown_2, Unknown_3; }MeshInfo[MeshIndex]<optimize=false>; }MeshTable; if (BoneMatrixOffset != 0) { FSeek(BoneMatrixOffset); struct { uint32 BoneCount; uint32 BoneNameInfoOffset, BoneParentOffset, Unknown_2, Unknown_3, BoneIndexOffset, BoneIndex, Unknown_6; struct { float Mat00,Mat01,Mat02,Mat03, Mat10,Mat11,Mat12,Mat13, Mat20,Mat21,Mat22,Mat23, Mat30,Mat31,Mat32,Mat33; }BonePosition[BoneCount]<optimize=false>; FSeek(BoneNameInfoOffset); struct { uint32 BoneNameOffset; local uint32 cPos=FTell(); FSeek(BoneNameOffset); string BoneName; FSeek(cPos); }BoneNames[BoneCount]<optimize=false>; FSeek(BoneParentOffset); ubyte BoneParent[BoneCount]; FSeek(RootOffset); char RootName[8]; ubyte Count; ubyte RootIndex[Count]; }BoneTable; } for (i=0; i < MeshTable.MeshIndex; i++) { FSeek(MeshTable.MeshInfo[i].ShapeInfoOffset); struct { char Sign[2]; ubyte Unknown_0; ubyte Unknown_1; uint16 Unknown_2; uint16 Unknown_3; uint32 Unknown_4; uint32 MeshDataSize; uint32 VertexOffset; uint32 IndexOffset; uint32 Unknown_5; uint16 Unknown_6; uint16 Unknown_7; uint16 Unknown_8; uint16 IndexCount; ubyte Stride; ubyte Unknown_9; uint16 VertexCount; FSkip(24); struct { float VPosX,VPosY,VPosZ; float VNPosX,VNPosY,VNPosZ; float UVPosX,UVPosY; if (Stride == 36) float Unknown; }VertexBuffer[VertexCount]<optimize=false>; FSeek(MeshDataBaseOffset + IndexOffset); struct { uint16 Index; }Indices[IndexCount]; }Mesh; }
May 3, 20251 yr Supporter Using Make_H2O for H2O creation: Make_H2O-EA3.zip Edited May 3, 20251 yr by shak-otay
May 6, 20251 yr Supporter O.K, here's Noesis script. Not sure how to assign material + texture to it. from inc_noesis import * import noesis import rapi import os def registerNoesisTypes(): handle = noesis.register("Bright Light", ".ea3") noesis.setHandlerTypeCheck(handle, noepyCheckType) noesis.setHandlerLoadModel(handle, noepyLoadModel) noesis.logPopup() return 1 def noepyCheckType(data): bs = NoeBitStream(data) if len(data) < 20: return 0 return 1 def noepyLoadModel(data, mdlList): bs = NoeBitStream(data) baseName = rapi.getExtensionlessName(rapi.getLocalFileName(rapi.getInputName())) ctx = rapi.rpgCreateContext() Underline = "_" # Header Start bs.read(4) # Sign TotalFileSize = bs.readUInt() MatrixOffset = bs.readUInt() MeshDataBaseOffset = bs.readUInt() # Base Offset bs.read(16) Matrix4x4 = bs.read(64) BBox = bs.read(32) Unknown_0 = bs.readUInt() UnknownOffset = bs.readUInt() MaterialIndex = bs.readUInt() MaterialIndexOffset = bs.readUInt() TextureCount = bs.readUInt() MaterialInfoOffset = bs.readUInt() Unknown_1 = bs.readUInt() Unknown_2 = bs.readUInt() BoneMatrixOffset = bs.readUInt() Unknown_3 = bs.readUInt() Unknown_4 = bs.readUInt() Unknown_5 = bs.readUInt() Unknown_6 = bs.readUInt() Unknown_7 = bs.readUInt() RootOffset = bs.readUInt() Unknown_8 = bs.readUInt() # Header End MaterialPropOffsetList = [] TextureIndexList = [] TextureNameList = [] ShapeInfoOffsetList = [] MaterialIndexList = [] bs.seek(MaterialIndexOffset, NOESEEK_ABS) for i in range(0, MaterialIndex): MaterialPropOffsetList.append(bs.readUInt()) TextureMapCount = bs.readUByte() bs.read(11) for i in range(0, MaterialIndex): MaterialPropOffset = MaterialPropOffsetList[i] bs.seek(MaterialPropOffset, NOESEEK_ABS) TextureIndexList.append(bs.readUByte()) bs.read(7) bs.seek(MaterialInfoOffset, NOESEEK_ABS) TextureNameOffset = bs.readUInt() bs.seek(TextureNameOffset, NOESEEK_ABS) for j in range(0, TextureCount): TextureNameList.append(bs.readString()) TextureName = TextureNameList[j] bs.seek(UnknownOffset, NOESEEK_ABS) bs.read(80) MeshIndex = bs.readUInt() bs.read(4) MeshInfoOffset = bs.readUInt() bs.seek(MeshInfoOffset, NOESEEK_ABS) for k in range(0, MeshIndex): BBox = bs.read(32) ShapeInfoOffsetList.append(bs.readUInt()) Unknown_0 = bs.readUInt() UnknownOffset = bs.readUInt() MaterialIndexList.append(bs.readUByte()) bs.read(3) for k in range(0, MeshIndex): ShapeIndexDigfmt = "{:04d}".format(k) ShapeInfoOffset = ShapeInfoOffsetList[k] MaterialIndex = MaterialIndexList[k] bs.seek(ShapeInfoOffset, NOESEEK_ABS) bs.read(20) IndexOffset = bs.readUInt() + MeshDataBaseOffset bs.read(10) IndexCount = bs.readUShort() Stride = bs.readUByte() Unknown_9 = bs.readUByte() VertexCount = bs.readUShort() bs.read(24) VertexBuffer = bs.readBytes(VertexCount * Stride) bs.seek(IndexOffset, NOESEEK_ABS) IndexBuffer = bs.readBytes(IndexCount * 2) rapi.rpgBindPositionBufferOfs(VertexBuffer, noesis.RPGEODATA_FLOAT, Stride, 0) rapi.rpgBindUV1BufferOfs(VertexBuffer, noesis.RPGEODATA_FLOAT, Stride, 24) rapi.rpgBindNormalBufferOfs(VertexBuffer, noesis.RPGEODATA_FLOAT, Stride, 12) rapi.rpgSetName(baseName + Underline + ShapeIndexDigfmt) rapi.rpgSetMaterial(TextureNameList[TextureIndexList[MaterialIndexList[k]]]) rapi.rpgCommitTriangles(IndexBuffer, noesis.RPGEODATA_USHORT, IndexCount, noesis.RPGEO_TRIANGLE_STRIP) mdl = rapi.rpgConstructModel() mdlList.append(mdl) return 1 Edited May 8, 20251 yr by h3x3r
May 6, 20251 yr Supporter There's more sub meshes than texture names so you need to trick around with this. (Test only, not a proper solution:) since I don't have the textures I created a fake one (ma_the_paperpattern01_cm.png) and assigned it like so: ... texCnt = 0 for j in range(0, TextureCount): TextureNameList.append(bs.readString()) TextureName = TextureNameList[j] print(j, TextureName) texCnt += 1 ... for k in range(0, MeshIndex): ... rapi.rpgCommitTriangles(IndexBuffer, noesis.RPGEODATA_USHORT, IndexCount, noesis.RPGEO_TRIANGLE_STRIP) if k < texCnt: rapi.rpgSetMaterial(TextureNameList[k]) Edited May 6, 20251 yr by shak-otay
May 6, 20251 yr Supporter Yeah I know. Thing is each mesh has a info about material index, and material index has a info about texture index.
May 6, 20251 yr Supporter Why not use rapi.rpgSetMaterial(TextureNameList[MaterialIndexList[k]]) ? edit: without the if k < texCnt: of course Edited May 6, 20251 yr by shak-otay
May 7, 20251 yr Localization On 5/6/2025 at 4:30 PM, shak-otay said: Why not use rapi.rpgSetMaterial(TextureNameList[MaterialIndexList[k]]) ? edit: without the if k < texCnt: of course Isn't the correct usage the following? rapi.rpgSetMaterial(TextureNameList[TextureIndexList[MaterialIndexList[k]]]); See the sp_the_ferriswheel_v0.ea3 file!
May 7, 20251 yr Localization On 5/1/2025 at 11:39 PM, jmancoder said: I'm trying to reverse engineer an old EA Bright Light game called Create. Inside the .big files, there were .ea3 and .ea3high files, which I know are uncompressed 3D models by the file structure. Could you tell me what the file extension of the texture files is? .dds, .jpg, .png?
May 7, 20251 yr Author Localization 34 minutes ago, Karpati said: Could you tell me what the file extension of the texture files is? .dds, .jpg, .png? .fsh, actually. I was using tools for Sim City 4 modding to convert them to .png textures. Textures.7z
May 8, 20251 yr Supporter On 5/7/2025 at 7:22 PM, Karpati said: rapi.rpgSetMaterial(TextureNameList[TextureIndexList[MaterialIndexList[k]]]); Thank you both guys! This did the trick. Updated script. EDiT: made a simple Noesis script for *.fsh. from inc_noesis import * import noesis import rapi import os def registerNoesisTypes(): handle = noesis.register("Bright Light", ".fsh") noesis.setHandlerTypeCheck(handle, noepyCheckType) noesis.setHandlerLoadRGBA(handle, noepyLoadRGBA) noesis.logPopup() return 1 def noepyCheckType(data): bs = NoeBitStream(data) if len(data) < 20: return 0 return 1 def noepyLoadRGBA(data, texList): bs = NoeBitStream(data) baseName = rapi.getExtensionlessName(rapi.getLocalFileName(rapi.getInputName())) bs.read(4) FileSize = bs.readUInt() bs.read(24) PixelFormat = bs.readUByte() bs.read(3) TextureWidth = bs.readUShort() TextureHeight = bs.readUShort() bs.read(8) if PixelFormat == 123: TextureBuffer = TextureWidth * TextureHeight # 8Bit print("Pixel Format > 8-Bit Palette") elif PixelFormat == 96: TextureBuffer = TextureWidth * TextureHeight //2 # DXT1 print("Pixel Format > DXT1") data = bs.readBytes(TextureBuffer) if PixelFormat == 123: bs.seek(FileSize - 1072, NOESEEK_ABS) bs.read(8) PaletteSize = bs.readUInt() bs.read(4) PaletteBuffer = bs.read(PaletteSize * 4) if PixelFormat == 123: data = rapi.imageDecodeRawPal(data, PaletteBuffer, TextureWidth, TextureHeight, 8, "b8 g8 r8 a8") texFmt = noesis.NOESISTEX_RGBA32 elif PixelFormat == 96: texFmt = noesis.NOESISTEX_DXT1 texList.append(NoeTexture(rapi.getInputName(), TextureWidth, TextureHeight, data, texFmt)) return 1 @jmancoder can you please provide these textures? tx_the_rollercoastercarriage_v0_cm tx_the_rollercoastermetal_cm lm_the_rollercoastercurved tx_the_rc_scaffold_cm tx_the_coasterrails_cm ma_the_paperpattern01_cm Edited May 9, 20251 yr by h3x3r
Monday at 06:29 AM4 days Author Localization Sorry to necro this thread, but I could use some advice on how to read per-vertex bone indices and weights for this mesh format. I have attached some more sample files (original extension was ea3high). I've also attached what I have so far for a Blender add-on. I haven't finished the node reading part yet, but it works for now. I renamed a decent number of the functions related to the DX9 mesh pipeline in this Binary Ninja project: https://drive.google.com/file/d/1XaiGOYLxQEaST5UTFfVaT3ZTwB3qxvaD/view?usp=sharing. I don't know if the rules permit uploading game executables, but either way, here are some function offsets of interest in CreateGame.exe: 0x4a6210 - Initial parsing and offset->pointer conversion stage of FPM models. 0x524030 - Generates the vertex declaration from an FVF-like bitmask. The field I'm stuck on is used as a D3DDECLUSAGE_BLENDWEIGHT of type D3DDECLTYPE_SHORT4, though it is likely manipulated in the vertex shader. 0x4d80e0 - Where the actual bone matrices are read and multiplied. Reversing this further would probably be my next step since the bone hierarchy is currently wrong. 0x5282e0 - Appears to be where the primitives are drawn, vertex shaders are set, etc. Sample Meshes.7z fpm_importer.zip
Create an account or sign in to comment