16-02-24, 12:00 | #1 |
Member
Joined: Jan 2018
Posts: 1,417
|
TRM File Format (TR I-III Remastered)
Every 3D object including Lara outfits seem to be of this format, except for level data of course.
FORMAT INFORMATION - Incomplete but enough for most meshes.
HEX EDITOR TIPS - Some basic modifications you can do yourself. BLENDER SCRIPTS - For Blender 4.0. Feel free to modify these scripts to suit your needs.
BLENDER TIPS - Some hints for making your life easier with these scripts.
(Many information below needs updating. I intend to get back to these over time.)
Last edited by MuruCoder; 16-04-24 at 14:24. |
17-02-24, 00:52 | #2 |
Member
Joined: Feb 2024
Posts: 4
|
im interested in this aswell I hope we get a file importer soon too cheers
|
17-02-24, 03:33 | #3 |
Member
Joined: Dec 2012
Posts: 33
|
I'm also interested in a TRM editor or converter. I want to remaster my Jungle themed level
|
17-02-24, 20:51 | #4 |
Member
Joined: Jan 2018
Posts: 1,417
|
Some updates.
It seems like the first group of unknowns are 44 bytes per orange unknown. W in UVs I said redundant might not be so redundant. That value seems to affect face visibility somehow. |
18-02-24, 00:50 | #5 |
Member
Joined: Aug 2022
Posts: 1
|
Hey. Xentax may have shutdown, but there is a smaller forum called "reshax.com". If you share your findings there I'm sure others can help. It might give a bigger reach.
|
19-02-24, 00:14 | #6 |
Member
Joined: Jan 2018
Posts: 1,417
|
Maybe later, I think I have the info to modify most meshes now.
So apparently that W isn't what I thought. What they did is have 3 joints. Their weights go in between U and V. Basically the last 8 bytes are each 1 byte and go like: Joint1, Joint2, Joint3, U, Weight1, Weight2, Weight3, V |
19-02-24, 05:22 | #7 |
Member
Joined: Feb 2024
Posts: 4
|
the post has got a bit of traction maybe its time to teach us your ways or something youve achieved mesh editing i dont think anyone else has yet
|
19-02-24, 14:47 | #8 |
Member
Joined: Jan 2018
Posts: 1,417
|
Format Information
(There're still many unknown parts and some terms are guesswork!)
TRM files start with a marker then contain shader, texture, animation or pose(?), indice & vertex data. Values are Little Endian and there's zero padding for 4 byte alignment when necessary. MAIN LAYOUT: Code:
uint32 trm2 // "TRM\x02" format marker uint32 num_shaders Shader shaders[num_shaders] // 44 bytes each uint32 num_textures uint16 textures[num_textures] // DDS texture IDs // zero padding for 4 byte alignment uint32 num_unknown1 // animations or poses count(?) { // exists if num_unknown1 > 0 Unknown1 unknown1[num_unknown1] // 48 bytes each uint32 num_unknown2 Unknown2 unknown2[num_unknown2] // 8 bytes each uint32 num_unknown3 Unknown3 unknown3[num_unknown3] // 4 bytes each uint16 num_unknown4 // uint16 because num_unknown5 is sometimes non-zero and messes up the calculation below uint16 num_unknown5 Unknown4 unknown4[num_unknown3 * num_unknown4] // 48 bytes each } uint32 num_indices uint32 num_vertices uint16 indices[num_indices] // zero padding for 4 byte alignment Vertex vertices[num_vertices] // 24 bytes each SHADER STRUCTURE: Code:
Shader { // 44 bytes uint32 type // or flags(?) values seen: 0, 2, 3, 6, 12, 14, 18, 19 uint32 unknown1 // these unknowns sometimes look like floats uint32 unknown2 // could be color values or multipliers uint32 unknown3 uint32 unknown4 uint32 indice_offset1 // faces to be drawn regularly(?) uint32 indice_length1 uint32 indice_offset2 // faces to be drawn with special shading(?) uint32 indice_length2 uint32 indice_offset3 // faces to be drawn with special shading(?) adds fixed transparency uint32 indice_length3 } TEXTURE IDs: Textures are easy. Notice most textures are in TEX folders as DDS files and have numbers as names. Here's a list of uint16s that correspond to those filenames. UINT16 gives us 65536 possibilities, game uses about 9000. This makes one consider using higher numbers for modding purposes but they don't seem to work. Also there're unused texture numbers below 9000. Some modders tried using them instead but reported having issues. The safest way is just replacing existing textures, whichever textures the TRM you're modding is using, replace those. (Don't forget zero padding after this list for 4 byte alignment) ANIMATION DATA: This depends on the first num_unknown1 value. If it is zero there's no more of this data. If it's non-zero, I managed to figure out some other count values to be able to skip over this data for now. INDICES & VERTICES: Finally, what makes up the 3D geometries. First we have indices and vertices counts together. Once you got those, indices are easy. Just uint16 for each indice. 3 of them make 1 triangle (or polygon, or face). (Don't forget zero padding after this list for 4 byte alignment) VERTEX STRUCTURE: Code:
Vertex { // 24 bytes float x, y, z // the usual x, y, z coordinates uint8 normal[3] // each x, y, z component 1 byte uint8 texture // order in textures list, starting from 1 uint8 joints[3] // bone IDs (skeleton data itself is not in TRM files) uint8 u // horizontal UV uint8 weights[3] // weights of above bones uint8 v // vertical UV, might appear upside down } Code:
vec = Vector(x-127, y-127, z-127) vec.normalize() Up to 3 joint IDs per vertex. Each byte is an ID, use 0 for default. Yes, U component of UV data comes before weights of the joints. I convert U to float simply by dividing it with 255. 3 weights corresponding to the joint IDs above. These 3 weights should total to 255 or "FF" hexadecimal, i.e. normalised. Otherwise object has weird appearance like parts showing in front or behind. If there's only 1 joint assigned, the first is 255 then other 2 weights are zero. Finally V component of UV data. Again divide it by 255 to convert to float. This so far needs to be flipped with (1.0 - float) because UVs end up upside down. Last edited by MuruCoder; 16-04-24 at 14:30. |
19-02-24, 15:03 | #9 |
Member
Joined: Jan 2018
Posts: 1,417
|
Texture ID Modification
This is assuming you know what are and how to use Hex Editors.
Based on format information you can manually edit the textures a TRM file is using. These 2 byte numbers seem to correspond to the DDS file names. Texture info usually starts at offset 52 (0x34) but refer to the format when there're more than 1 shaders in a particular TRM. So if you have multiple mods you wish to keep but unable because they are using the same texture slot, you can easily make one of them use another texture slot. Here's an example TRM contents in HexEditor. 54524d02 - TRM format marker 02000000 - Number of Shaders, in this case 2 88 Bytes - 44 bytes per Shader so in this case skip 88 bytes 07000000 - Number of Texture IDs, in this case 7 TexIDs - Texture IDs, 2 bytes each Last edited by MuruCoder; 24-03-24 at 20:38. |
19-02-24, 15:33 | #10 |
Member
Joined: Jan 2018
Posts: 1,417
|
Blender Import Script v0.1
(Old Version) v0.1 - Check First Post Instead
Below is the import script for those who're familiar with Blender. Save into "trm_import.py" file. Load & run this in your Scripting tab. Coded this in Blender 4.0, may not work on older versions. Select a TRM file and hopefully you'll have your mesh. Incomplete but sufficient for many TRM files. The mesh is big so zoom out. Increasing view distance (Clip End) helps. Materials are randomly coloured. The number in their names is important (corresponds to the DDS file names) but you can change what's after underscore. You can add/remove materials, just remember the name has to start with texture ID followed by underscore. Export script will use this. This script won't find and convert DDS files. You can manually convert DDS files into something Blender can open, assign them to the materials and use them while working. There's no skeleton/armature info but vertex groups and weight data is there. Lara outfit files seem to have 15 joints. You can manually create an armature if you wish to test how well mesh joints bend. You can save & reuse that armature in the future. The names of vertex groups isn't important but their orders are. The order goes like this. It's similar to classic engine hierarchy. Code:
0 Hip 1 Thigh left 2 Calf left 3 Foot left 4 Thigh right 5 Calf right 6 Foot right 7 Torso 8 Arm right 9 Elbow right 10 Hand right 11 Arm left 12 Elbow left 13 Hand left 14 Head trm_import.py Code:
import bpy import struct import string import math import random def read_some_data(context, filepath, use_some_setting): f = open(filepath, 'rb') # TRM\x02 marker if struct.unpack('>I', f.read(4))[0] != 0x54524d02: ShowError("Not a TRM file!") return {'FINISHED'} # ! THIS IS A GUESS AND NEEDS LOOKING INTO num_geometries = struct.unpack('<I', f.read(4))[0] print("Geometries: ", num_geometries) # ! SKIP UNKNOWN DATA, ASSUMING 44 BYTES PER GEOMETRY f.read(num_geometries * 44) # MATERIALS num_materials = struct.unpack('<I', f.read(4))[0] print("Materials: ", num_materials) materials = struct.unpack("<%sH" % num_materials, f.read(num_materials * 2)) # BYTE ALIGN while f.tell() % 4: f.read(1) # ANOTHER UNKNOWN, COULD BE SHAPEKEY OR ANIMATION num_unknown = struct.unpack('<I', f.read(4))[0] if num_unknown != 0: ShowError("Unknown data, don't know how to process or skip!") return {'FINISHED'} # INDICE & VERTICE COUNTS num_indices = struct.unpack('<I', f.read(4))[0] num_vertices = struct.unpack('<I', f.read(4))[0] print("Indices: ", num_indices, "Vertices: ", num_vertices) # READ INDICES indices = struct.unpack("<%sH" % num_indices, f.read(num_indices * 2)) # BYTE ALIGN while f.tell() % 4: f.read(1) # READ VERTICES vertices = [] highest_joint = 0 for n in range(num_vertices): vertex = struct.unpack("<fff12B", f.read(24)) vertices.append(vertex) highest_joint = max(vertex[7], vertex[8], vertex[9], highest_joint) print("Highest Joint: ", highest_joint) f.close() # CREATE OBJECT trm_mesh = bpy.data.meshes.new('TRM_Mesh') trm = bpy.data.objects.new('TRM', trm_mesh) trm_vertices = [] trm_edges = [] trm_faces = [] for n in range(num_vertices): v = vertices[n] trm_vertices.append([v[0], v[1], v[2]]) for n in range(0, num_indices, 3): trm_faces.append([indices[n], indices[n+2], indices[n+1]]) trm_mesh.from_pydata(trm_vertices, trm_edges, trm_faces) trm_mesh.update() # CREATE MATERIALS WITH RANDOM COLOR for n in range(num_materials): mat = bpy.data.materials.new(name=str(materials[n])+"_Material") mat.diffuse_color = (random.random(), random.random(), random.random(), 1) trm.data.materials.append(mat) # ASSIGN MATERIALS for n in range(len(trm_mesh.polygons)): p = trm_mesh.polygons[n] p.material_index = vertices[p.vertices[1]][6] - 1 # CREATE UV DATA trm_mesh.uv_layers.new() uvs = trm_mesh.uv_layers.active for p in trm_mesh.polygons: for i in p.loop_indices: v = trm_mesh.loops[i].vertex_index uvs.data[i].uv = (vertices[v][10] / 255, (255 - vertices[v][14]) / 255) # CREATE & ASSIGN VERTEX GROUPS for n in range(highest_joint + 1): trm.vertex_groups.new(name="J"+str(n)+"_joint") for n in range(num_vertices): g = trm.vertex_groups v = vertices[n] if v[11] > 0: g[v[7]].add([n], v[11]/255, 'ADD') if v[12] > 0: g[v[8]].add([n], v[12]/255, 'ADD') if v[13] > 0: g[v[9]].add([n], v[13]/255, 'ADD') # CREATE NORMALS trm_normals = [] for n in range(num_vertices): v = vertices[n] trm_normals.append(((v[3]-128)/127, (v[4]-128)/127, (v[5]-128)/127)) trm_mesh.use_auto_smooth = True trm_mesh.normals_split_custom_set_from_vertices(trm_normals) trm_mesh.update() # ORIENTATION FOR BLENDER trm.rotation_euler[0] = math.radians(90) trm.rotation_euler[1] = math.radians(180) # DONE bpy.context.collection.objects.link(trm) return {'FINISHED'} def ShowError(message = ''): def draw(self, context): self.layout.label(text = message) bpy.context.window_manager.popup_menu(draw, title="ERROR!", icon='ERROR') # ImportHelper is a helper class, defines filename and # invoke() function which calls the file selector. from bpy_extras.io_utils import ImportHelper from bpy.props import StringProperty, BoolProperty, EnumProperty from bpy.types import Operator class ImportSomeData(Operator, ImportHelper): """This appears in the tooltip of the operator and in the generated docs""" bl_idname = "import_test.some_data" # important since its how bpy.ops.import_test.some_data is constructed bl_label = "Import TRM Data" filename_ext = ".TRM" filter_glob: StringProperty( default="*.TRM", options={'HIDDEN'}, maxlen=255, ) use_setting: BoolProperty( name="Example Boolean", description="Example Tooltip", default=True, ) type: EnumProperty( name="Example Enum", description="Choose between two items", items=( ('OPT_A', "First Option", "Description one"), ('OPT_B', "Second Option", "Description two"), ), default='OPT_A', ) def execute(self, context): return read_some_data(context, self.filepath, self.use_setting) # Only needed if you want to add into a dynamic menu. def menu_func_import(self, context): self.layout.operator(ImportSomeData.bl_idname, text="TRM Import Operator") # Register and add to the "file selector" menu (required to use F3 search "TRM Import Operator" for quick access). def register(): bpy.utils.register_class(ImportSomeData) bpy.types.TOPBAR_MT_file_import.append(menu_func_import) def unregister(): bpy.utils.unregister_class(ImportSomeData) bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) if __name__ == "__main__": register() bpy.ops.import_test.some_data('INVOKE_DEFAULT') Last edited by MuruCoder; 20-02-24 at 10:22. |
Bookmarks |
Thread Tools | |
|
|