Jump to content

Need advice for finding data offsets in .ea3 model files from an old EA Bright Light game


Go to solution Solved by h3x3r,

Recommended Posts

Posted (edited)

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.

Screenshot (243).png

Screenshot (242).png

SampleMesh.7z OtherMeshes.7z

Edited by jmancoder
  • jmancoder changed the title to Need advice for finding data offsets in .ea3 model files from an old EA Bright Light game
  • Engineers
Posted (edited)
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.

EA-BL-Create.png

train_mesh2.png

Edited by shak-otay
Posted
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

  • Engineers
  • Solution
Posted

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;
}

 

  • Like 1
  • Engineers
Posted (edited)

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 by h3x3r
  • Like 1
  • Thanks 1
  • Engineers
Posted (edited)

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])

 

curved_fakeTex.png

Edited by shak-otay
Posted
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!

  • Thanks 1
Posted
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?

  • Engineers
Posted (edited)
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 by h3x3r

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...