December 20, 2025Dec 20 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 December 21, 2025Dec 21 by HaoMao
December 20, 2025Dec 20 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: Edited December 20, 2025Dec 20 by shak-otay typo
December 20, 2025Dec 20 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.
December 21, 2025Dec 21 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: 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: ...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 December 21, 2025Dec 21 by HaoMao
December 21, 2025Dec 21 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 December 21, 2025Dec 21 by shak-otay
December 21, 2025Dec 21 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.
December 21, 2025Dec 21 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 December 21, 2025Dec 21 by shak-otay
December 21, 2025Dec 21 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.
December 23, 2025Dec 23 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: 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 December 23, 2025Dec 23 by HaoMao
January 9Jan 9 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
May 5May 5 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 May 6May 6 by HaoMao
May 6May 6 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 May 6May 6 by shak-otay
May 6May 6 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
May 6May 6 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 May 6May 6 by shak-otay
May 17May 17 Author also i forgot to ask: can someone explain how i can fix this issue, when UV goes beyond the -1-1 range: This issue you can see in EM_evil_meteor_ground model. Are there any solutions to this problem?
May 17May 17 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. (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 May 17May 17 by shak-otay
May 17May 17 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!
May 17May 17 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
May 18May 18 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