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.

[Wii] Spore Hero 3D models (.rpm)

Featured Replies

Hello! I tried to rip models from Spore Hero, and I was able to handle the vertices and UV coordinates, but I had problems with faces: I can't find their exact data at all.
All I know about the rpm structure at the moment is that polygons have a "blocked" structure: a polygon that consists of faces(?), vertices, UV coordinates, and possibly some other information. Each such polygon has the following header:

50 48 01 96 01 34 00 31 01 46 20 11 00 00 07 00
00 00 07 20 00 00 0A C8 00 00 0E 34 00 00 00 00


All I can say about the header is that 01 34 indicates the number of faces. How can I understand this if I can't display them? Well, the game Spore has exactly the same models in a different format (.rw4/.rdx9), but not all of them. Immediately after the header, there are presumably faces, and here repeats the bytes 98 00 04 00, after "faces" following 16 bytes of 00, then vertices and UV cords.
Here is an archive with the model and the addresses of the vertices and UV cords:
(Hope this info helps to figure out how rip rpm models)
UPD: I just noticed that I used a header from another model as an example. Now the header is from the archive

CE_simple_sphere.zip

Edited by HaoMao

  • Supporter

Hello! You could try using byte face indices starting from 0x1034 in the .rpm.

(I thought of using strips with 0xFF being the terminator but the sequence FF 7F FF at 0x1088 seems to exclude that.)

Auto generated faces are too ugly:

sphere-rpm.png

Edited by shak-otay
typo

It seems as though the model data starts at 0x200 with a 0x20-byte header, which contains some offsets to the data sections relative to 0x200.  So you've got 0x720 for vertices, 0xac8 for UVs, 0xe34 for normals maybe.  The 0x31 in this header indicates the number of face sections (0x31 = 49) starting just after this header.  Each face section starts with a face type - 0x98 seems to be a tri-strip, then a short count value, followed by 4 bytes per count.  These 4 bytes are probably texture number (not sure), then it's something like vertex number, UV number, normal number.  So you have to reconstruct the buffers because they're not a direct 1:1 match.  For example, you have more UVs than vertices.

 

 

 

  • Author
13 hours ago, shak-otay said:

Hello! You could try using byte face indices starting from 0x1034 in the .rpm.

(I thought of using strips with 0xFF being the terminator but the sequence FF 7F FF at 0x1088 seems to exclude that.)

Auto generated faces are too ugly:

sphere-rpm.png

I'm... a little confused rn. How did you make this model so that it looks like... a sphere?
To make you understand what I mean, right now my sphere looks like this:
Screenshot_1.png.cf66ca7ee1e23e018fe0b46f0f6b142b.png
...I really thought that's okay for this rpm models ._.
this is what the model looks like with my template from the archive

Quote

You could try using byte face indices starting from 0x1034 in the .rpm.

I tried to do this, but it doesn't work for me because there are fewer vertices than the number of bytes that follow this address (total vertices is 0x9C, and here there is 0xFF)
 

Quote

Each face section starts with a face type - 0x98 seems to be a tri-strip, then a short count value, followed by 4 bytes per count.  These 4 bytes are probably texture number (not sure), then it's something like vertex number, UV number, normal number.  So you have to reconstruct the buffers because they're not a direct 1:1 match.  For example, you have more UVs than vertices.

If I understood you correctly, then each 0x98 should be followed by the following: faces, then vertices, UVs and normals? I tried to do this, but I only get a cloud of vertices.

Edited by HaoMao

  • Supporter
46 minutes ago, HaoMao said:

To make you understand what I mean, right now my sphere looks like this:

You'll need to use unsigned shorts for vertices here.

Quote

I tried to do this, but it doesn't work for me because there are fewer vertices than the number of bytes that follow this address (total vertices is 0x9C, and here there is 0xFF)

That's why I wrote about "terminator" FF. But not sure whether .rpm uses triangle strips. You could skip the bytes bigger than 0x9C for a test.

Or you could wait for Dave.

Edited by shak-otay

  • Author
3 minutes ago, shak-otay said:

You'll need to use unsigned shorts for vertices here.

Well, that's what I've done since the beginning of the topic... this is odd, I don't have this problem with other model formats.

  • Supporter

Your pictures shows use of signed shorts, imho. (characteristic splitting of the mesh into 4 separated parts)

using unsigned shorts
# 0x920: verts= 156
v 134.273438 160.218750 119.273438
v 137.363281 160.218750 105.683594
v 145.921875 150.296875 124.898438
v 150.296875 150.296875 105.683594
v 154.863281 137.363281 129.218750
...

signed shorts
# 0x920: verts= 156
v -121.726562 -95.781250 119.273438
v -118.636719 -95.781250 105.683594
v -110.078125 -105.703125 124.898438
v -105.703125 -105.703125 105.683594
...

(The actual values depend on the factor (or divisor) used for the conversion.)

Edited by shak-otay

  • Author

oh i see now! But yes, I'll wait for Dave for now, because I have absolutely no ideas and understanding of how to continue.

  • Author

So. I tried to deleted all the bytes consisting of 98 00 NN (where NN is a different number of bytes whose meaning I didn't understand) and makes faces as TStrip format and Byte type and 3 padding. And it gave the result:
Screenshot_3.png.c573984f5429299180d87f728cee6367.png
It's not perfect, but the result is still good.
the count of bytes 0x9800NN is equal to the number 0x31 (49), so I think this bytes could be the beginning of each face sections (as Dave mentioned earlier).
Also 0x0196 bytes almost equivalent to the number of all faces in the models, but I could be wrong.

Edited by HaoMao

  • 3 weeks later...
  • Localization

I know this is Spore Hero but i recently looked into 2 games EA published (Monopoly 2008 & Hasbro Family Game Night) and they also use .rpm files too (same header "RPM7" and has visible texture and bone names split by 00)

Here's some samples of those 2 games: rpm_samples.zip

  • 3 months later...
  • Author

I figured out how to read the sections, and now most of the models from Spore Hero are readable without problems! But now I want to read the materials, textures, skeleton and animation of the model, and I want to find out how this can be implemented?
Here is the code and presumably the rpm structure:
code:
 

Quote

 

#Noesis Python model import+export test module, imports/exports some data from/to a made-up format

from inc_noesis import *

import noesis

import math

#rapi methods should only be used during handler callbacks
import rapi

#registerNoesisTypes is called by Noesis to allow the script to register formats.
#Do not implement this function in script files unless you want them to be dedicated format modules!
def registerNoesisTypes():
   handle = noesis.register("RPM Model", ".rpm")
   noesis.setHandlerTypeCheck(handle, noepyCheckType)
   noesis.setHandlerLoadModel(handle, noepyLoadModel)
   noesis.logPopup()
   return 1

NOEPY_HEADER = "RPM"

#check if it's this type based on the data
def noepyCheckType(data):
   bs = NoeBitStream(data)
   if len(data) < 4:
      return 0
   if bs.readBytes(3).decode("ASCII").rstrip("\0") != NOEPY_HEADER:
      return 0
   return 1      

'''
def NormalizeAndAddVertices(bs, verts):
    x = (bs.readUShort() + 32768) / 65535.0
    y = (bs.readUShort() + 32768) / 65535.0
    z = (bs.readUShort() + 32768) / 65535.0
    if x == 0 and y == 0 and z == 0: return 0
    verts.append(NoeVec3(( x - 0.9, y - 0.9, z - 0.9 )))
    return 1

def NormalizeAndAddUVs(bs, UVs):
    uv = [bs.readUShort(), bs.readUShort()]
    for i in range(len(uv)):
        uv = (uv + 32768) / 65535.0
        if uv > 1: uv = uv - 1
        elif uv < -1: uv = uv + 1
        if uv[0] == 0 and uv[1] == 0: return 0  
    UVs.append(NoeVec3((uv[0] * 16.0 - 8.0, ((1.0 - uv[1]) * 16.0) - 7.0, 0)))
    return 1
'''

#uses to cut extra bytes to get correct offset
def FindLength(bs, offset):
   back = bs.tell()
   num = 0
   for i in range(offset - 1, 0, -1):
       bs.seek(back + i)
       if bs.readUByte() != 0: break
       num +=1
   bs.seek(back)
   return num

def NormalizeAndAddVertices(bs, verts, isFloat):
    if isFloat == False:
        x = bs.readUShort() / 139264.0
        y = bs.readUShort() / 139264.0
        z = bs.readUShort() / 139264.0
    else:
        x = bs.readFloat() / 139264.0
        y = bs.readFloat() / 139264.0
        z = bs.readFloat() / 139264.0
    verts.append(NoeVec3(( x, y, z )))
    return 1

def NormalizeAndAddUVs(bs, UVs):
    uv = [bs.readUShort(), bs.readUShort()]
    for i in range(len(uv)):
        uv = uv / 4096.0
        #need smh better than this
        #if uv > 16: uv = uv - 16
        #elif uv < -16: uv = uv + 16
    UVs.append(NoeVec3((uv[0], 1.0 - uv[1], 0)))
    return 1

def PolygonReading(bs, model, polygon, modelSize, booleans):
   #print('========== TOTAL ==========')
   print(hex(bs.tell()))
   numPrimitives = bs.readUShort(); #print('idxNum', numPrimitives)
   numFaces = bs.readUShort(); #print('numFaces', numFaces)
   numGroups = bs.readUShort(); #print('sections', numGroups)
   if numFaces == 0 or numGroups == 0: return 0
   '''
   unk1 = bs.readUByte(); #print('flag?', hex(unk1), unk1)
   unk2 = bs.readUByte(); #print('unk2', hex(unk2), unk2)
   unk3 = bs.readUByte(); #print('unk3', hex(unk3), unk3)
   unk4 = bs.readUByte(); #print('unk4', hex(unk4), unk4)
   '''
   bs.readUInt()
   sectionsLenght = bs.readUInt(); #print('sections lenght', hex(sectionsLenght))
   offsets = [bs.readUInt(), bs.readUInt(), bs.readUInt(), bs.readUInt()]
   total = [0, 0]
   for i, offset in enumerate(offsets):
       if offset > modelSize: return 0
       if i < 2 and offset != 0:
           nextOffset = (polygon[1]- FindLength(bs, polygon[1] - bs.tell()) - (offset + polygon[0]))
           for x in range(i + 1, 4):
               if offsets[x] != 0:
                   nextOffset = (offsets[x] - offset)
                   break
           if i == 0: total = math.ceil(nextOffset / (6 + 6 * booleans[1]))
           else: total = math.ceil(nextOffset / 4)
   
   #print('========== OFFSETS')
   #print('Vertices offset:', hex(offsets[0]))
   #print('UVs offset:', hex(offsets[1]))
   #print('unknown offset:', hex(offsets[2]))
   #print('unknown offset:', hex(offsets[3]))
   #print('Vertices total:', total[0])
   #print('UV total:', total[1])
   
   groupLength = (sectionsLenght - (numGroups * 3) - FindLength(bs, sectionsLenght))
   groupLength = math.ceil(groupLength / numPrimitives)
   #print('group length:', groupLength)
   
   move = 0
   if offsets[0] != 0 and total[0] != 0:
       if total[0] > 256:
           #print('vertices more than 256: indx should be UShort()', total[0])
           move += 2
       else:
           #print('vertices less than 256: indx should be UByte()', total[0])
           move += 1
   else:
       move += 6 + 6 * booleans[1]
       
   if offsets[1] != 0 and total[1] != 0:
       if total[1] > 256:
           #print('UV more than 256: indx should be UShort()', total[1])
           move += 2
       else:
           #print('UV less than 256: indx should be UByte()', total[1])
           move += 1
   else:
       move += 4
       
   if booleans[0]:  #hasBones
       move += 1
   #print('move', move)
   triangles = []
   uvTriangles = []
   verts = []
   UVs = []
   if groupLength != 0:
    for group in range(numGroups):
        if bs.readUByte() == 0x98:
            length = bs.readUShort()
            idxVerts = []
            idxUVs = []
            vertsPrim = []
            UVsPrim = []
            
            for idx in range(length):
                if booleans[0]: bs.readUByte()  #hasBones
                # vert indices
                if offsets[0] != 0:
                    if total[0] > 256: idxVerts.append(bs.readUShort())
                    else: idxVerts.append(bs.readUByte())
                else: NormalizeAndAddVertices(bs, vertsPrim, booleans[1])
                    
                if groupLength - move > 0:
                    bs.readBytes(groupLength - move)
                    
                # UV indices
                if offsets[1] != 0:
                    if total[1] > 256: idxUVs.append(bs.readUShort())
                    else: idxUVs.append(bs.readUByte())
                else: NormalizeAndAddUVs(bs, UVsPrim)
                
            if len(vertsPrim) > 0:
                vert_map = {(v[0], v[1], v[2]): i for i, v in enumerate(verts)}
                for prim_vert in vertsPrim:
                    key = (prim_vert[0], prim_vert[1], prim_vert[2])
        
                    if key in vert_map:
                        new_idx = vert_map[key]
                    else:
                        new_idx = len(verts)
                        verts.append(prim_vert)
                        vert_map[key] = new_idx
                        
                    idxVerts.append(new_idx)
                            
            if len(UVsPrim) > 0:
                uv_map = {(v[0], v[1]): i for i, v in enumerate(UVs)}
                for prim_uv in UVsPrim:
                    uv_key = (prim_uv[0], prim_uv[1])
                    
                    if uv_key in uv_map:
                        new_idx = uv_map[uv_key]
                    else:
                        new_idx = len(UVs)
                        UVs.append(prim_uv)
                        uv_map[uv_key] = new_idx
                        
                    idxUVs.append(new_idx)
            
            for l in range(length - 2):
                v0, v1, v2 = idxVerts[l], idxVerts[l + 1], idxVerts[l + 2]
                uv0, uv1, uv2 = idxUVs[l], idxUVs[l + 1], idxUVs[l + 2]
                if l % 2 == 0:
                    triangles.extend([v0, v1, v2])
                    uvTriangles.extend([uv0, uv1, uv2])
                else:
                    triangles.extend([v1, v0, v2])
                    uvTriangles.extend([uv1, uv0, uv2])
   
   #print('triangles', len(triangles))
   #print('uvTri', len(uvTriangles))
      
   if total[0] != 0 and offsets[0] != 0:
    bs.seek(offsets[0] + polygon[0])
    for vert in range(total[0]):
        NormalizeAndAddVertices(bs, verts, booleans[1])

   if total[1] != 0 and offsets[1] != 0:
    bs.seek(offsets[1] + polygon[0])
    for vert in range(total[1]):
        NormalizeAndAddUVs(bs, UVs)
        
   newVerts = []
   newUVs = []
   newTriangles = []

   if len(verts) != 0 and len(UVs) != 0:
    pairMap = {}
   
    for i in range(len(triangles)):
        vertIdx = triangles
        uvIdx = uvTriangles
        
        if vertIdx >= len(verts):
            print('vert indice out of range:', vertIdx)
            return 0
        if uvIdx >= len(UVs):
            print('uv indice out of range:', uvIdx)
            return 0
    
        key = (vertIdx, uvIdx)
    
        if key not in pairMap:
            pairMap[key] = len(newVerts)
            newVerts.append(verts[vertIdx])
            newUVs.append(UVs[uvIdx])
    
        newTriangles.append(pairMap[key])

   #print('newVerts', len(newVerts))
   #print('newTriangles', len(newTriangles))
   #print('newUVs', len(newUVs))
   mesh = NoeMesh(newTriangles, newVerts)
   #mesh = NoeMesh([], verts)
   mesh.setUVs(newUVs)
   if mesh != 0:
    model.append(mesh)
   return 1

#load the model
def noepyLoadModel(data, mdlList):
   bs = NoeBitStream(data, 1)
   RPMMagic = bs.readUInt()
   modelSize = bs.readUInt()
   magicLength = bs.readUInt() #always 0x20
   firstOffset = bs.readUInt(); #print('first submesh', hex(firstOffset))
   bs.seek(0x50)
   vector = bs.readUInt() + bs.readUInt() + bs.readUInt()
   #print(vector)
   '''
   bs.readBytes(0x10)
   listt = []
   while bs.tell() < 0x80:
       x = bs.readUInt();
       y = bs.readUInt();
       z = bs.readUInt();
       w = bs.readUInt();
       #print(float(x)/ 10**len(str(x)), float(y)/ 10**len(str(y)), float(z)/ 10**len(str(z)), float(w)/ 10**len(str(w)))
       listt.append(NoeVec4((x, y, z, z)))
   '''
   bs.seek(0x80)
   #materials
   numMat = bs.readUInt(); #print('total materials?:', numMat)
   matOffset = bs.readUInt(); #print('materials offset?:', hex(matOffset))
   matType = bs.readUInt(); #print('materials type?:', hex(matType))
   matUnk2 = bs.readUInt(); #print('materials UNK2?:', hex(matUnk2))
   #textures
   numTex = bs.readUInt(); #print('total textures:', numTex)
   texOffset = bs.readUInt(); #print('textures offset:', hex(texOffset))
   texType = bs.readUInt(); #print('textures type?:', hex(texType))
   texUnk2 = bs.readUInt(); #print('textures UNK2:', hex(texUnk2))
   #bones
   sktOffset = bs.readUInt(); #print('bones offset:', hex(sktOffset))
   sktUNK = bs.readUInt(); #print('bones UNK:', hex(sktUNK))
   sktFlag1 = bs.readUInt(); #print('bones flag?:', hex(sktFlag1))
   sktFlag2 = bs.readUInt(); #print('bones flag? (2):', hex(sktFlag2))
   
   bs.seek(firstOffset - 2)
   submeshesOffsets = []
   while bs.tell() < modelSize:
       b = bs.readUShort()
       if b == 0x5048 and (bs.tell() - 2) % 16 == 0:
           bs.seek(bs.tell() + 0xE)
           offsets = [bs.readUInt(), bs.readUInt(), bs.readUInt(), bs.readUInt()]
           invalid = False
           for i in range(4):
               if offsets != 0 and offsets + firstOffset >= modelSize:
                   invalid = True
           if invalid == False:
               #print('header:', hex(bs.tell() - 32))
               submeshesOffsets.append(bs.tell() - 30)
   #print('total submeshes:', len(submeshesOffsets))
   
   model = []
   for i in range(len(submeshesOffsets)):
       bs.seek(submeshesOffsets)
       if i + 1 < len(submeshesOffsets):
           nextOffset = submeshesOffsets[i + 1]
       else:
           nextOffset = modelSize
       PolygonReading(bs, model, [firstOffset, nextOffset], modelSize, [sktOffset != 0, vector == 0])
   mdlList.append(NoeModel(model))
   return 1
   

 

structure:

Quote

 

ce_simple_sphere.rpm

Vertices - usigned_short type
UVs - unsigned_short type? or signed_short
Faces - Byte type and TStrip format
???
??? — uses 4 bytes (2 unsigned_short or uint/float?)

52 50 4D 37 (RPM7) 0x0 - model header
00 00 12 20 0x4 - file size
00 00 00 20 0x8 - model header length
00 00 02 00 0xE - polygon offset
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

0x30-0x70 — bounding box?
0x80 - materials header
00 00 00 01 - total materials
00 00 00 F0 - materials offset
00 00 00 01 - materials type
00 00 00 C4 - extra data offset?

0x90 - textures header
00 00 00 01 - total textures
00 00 00 DC - textures offset
00 00 00 00 - textures type
00 00 00 00 - RGBA offset

0xA0 - skeleton header
00 00 01 90 - skeleton offset
00 00 00 00 - ???
00 00 00 04 - ???
00 00 00 01 - ???

00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

00 00 00 00 00 00 00 D4 01 - ???
00 0E - blend mode?
01 - ???
00 00 - ???
FF FF - flag?

mesh header struct (0x20)
50 48 0x0 - mesh header
01 96 0x2 - total primitives
01 34 0x4 - total faces
00 31 0x6 - total submeshes
01 0x7 - flag?
46 0x8 - ???
20 0x9 - ???
11 0xA - ???
00 00 07 00 0xC - submeshes length
00 00 07 20 0x10 - Vertices offset + mesh offset
00 00 0A C8 0x14 - UVs offset + mesh offset
00 00 0E 34 0x18 - ??? + mesh offset (weights?)
00 00 00 00 0x1C - ??? + mesh offset

if vertices and/or UV offsets are null, then they will be instead indices inside of primitives

 


I know the headers for materials, textures and skeleton, but the animation of the model is contained in a separate BNK_swapped file (there is also RCB_swapped in the folder with it, but I do not know what it is responsible for), and it seems that there are no tools for them to reading/unpacking. Is there anything you can recommend to work with this?
I will pin the archive with a couple of examples of rpm models: two without a skeleton and two with a skeleton, as well as with a different number of textures.

rpm models.zip

Edited by HaoMao

  • Supporter

Thanks for your code but it doesn't work for me.

Quote

File "...\plugins\python\fmt_Wii_rpm-HanMao.py", line 126, in PolygonReading
    if offsets[0] != 0 and total[0] != 0:
TypeError: 'int' object is not subscriptable

edit: could you append your code as a file, please?

Edited by shak-otay

  • Author
1 hour ago, shak-otay said:

Thanks for your code but it doesn't work for me.

edit: could you append your code as a file, please?

Yes, sure! There a file:

SporeHeroRPM.py

  • Supporter

Thanks!

As I thought, there's a problem when quoting python code instead of using code tags (< / >), the [ i ] disappear:

File:
uv[ i ] = (uv[ i ] + 32768) / 65535.0

quoted in forum:
uv = (uv + 32768) / 65535.0

Edited by shak-otay

  • 2 weeks later...
  • Author

also i forgot to ask: can someone explain how i can fix this issue, when UV goes beyond the -1-1 range:image.png.982a181e42c3f4cd47884b76cfb53f06.png
This issue you can see in EM_evil_meteor_ground model. Are there any solutions to this problem?

  • Supporter

The problematic lines seem to be these, for example:

vt  15.975342 0.257568 0.000000
vt  15.985840 0.340820 0.000000

That's not hard to handle:
 

if uv[0] > 15.0:

    uv[0] /= 16.0

but since your code looks complicated

UVs.append(NoeVec3((uv[0] * 16.0 - 8.0, ((1.0 - uv[1]) * 16.0) - 7.0, 0)))

you'll need to trick around.:classic_biggrin:

(btw, didn't test the patch code, maybe uv[0]= uv[0] / 16.0 is required)

edit: in another sub mesh there's

vt  0.519287 -14.911865 0.000000

So you'll need to act accordingly.

Edited by shak-otay

  • Author
2 hours ago, shak-otay said:

The problematic lines seem to be these, for example:

vt  15.975342 0.257568 0.000000
vt  15.985840 0.340820 0.000000

That's not hard to handle:
 

if uv[0] > 15.0:

    uv[0] /= 16.0

Thank you for help, this is help me solved the problem! 
 

Quote

but since your code looks complicated

UVs.append(NoeVec3((uv[0] * 16.0 - 8.0, ((1.0 - uv[1]) * 16.0) - 7.0, 0)))

you'll need to trick around.

I think you looked on my old code that was commented out. Current version looks like this:
 

def NormalizeAndAddUVs(bs, UVs):
    uv = [bs.readUShort(), bs.readUShort()]
    for i in range(len(uv)):
        uv[i] = uv[i] / 4096.0
        if uv[i] > 15.0 or uv[i] < -15.0:  uv[i] /= 16.0
    UVs.append(NoeVec3((uv[0], 1.0 - uv[1], 0)))
    return 1
Quote

edit: in another sub mesh there's

vt  0.519287 -14.911865 0.000000

Huh, i didn't find it when i export models again... maybe your patch fixed that too!

  • Author

Btw, can you advice me something how to work with materials and textures in this format? I wrote it before, but i still have no clue how works with that. If you need it, i can write here the binary offset of materials and textures, which contains some info

  • Author
15 hours ago, shak-otay said:

Dunno about the materials but gsh tex was treated here.

Darn it... but thanks for the link!

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.