Tomb Raider Forums  

Go Back   Tomb Raider Forums > Tomb Raider Level Editor and Modding > Tomb Raider Modding

Reply
 
Thread Tools
Old 16-02-24, 12:00   #1
MuruCoder
Member
 
MuruCoder's Avatar
 
Joined: Jan 2018
Posts: 1,417
Default 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.
  • Main Layout
  • Shaders (later when we have better understanding)
  • Joints & Animation (later when we have better understanding)

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.
  • (Various information is in GitHub Readme)
  • Installing Blender Addons
    • Edit > Preferences > Add-ons > Install
    • Deactivate when updating
  • Addon Preferences
    • Texture Converter, Game & Custom PNG paths
  • Importing TRMs
  • Exporting TRMs
    • Remember to backup files
    • Select the object(s) to be exported

(Many information below needs updating. I intend to get back to these over time.)
  • Materials/Textures and UVMaps
    • Have at least 1 material and all vertices assigned to some material
    • Number + Underscore rule for naming Materials
    • Object to be exported should have only 1 UVMap
    • Merging objects must share the same name for the UVMap
    • UV coordinates must be within texture bounds
  • Rigging/Skeletons/Armatures/Bones/Joints/Vertex Groups
    • Vertex Group orders are important
    • Maximum 3 groups assigned per vertex
    • Weights must be Normalized
    • Have same Vertex Group setup when merging objects (names & orders)

Last edited by MuruCoder; 16-04-24 at 14:24.
MuruCoder is offline   Reply With Quote
Old 17-02-24, 00:52   #2
tavschke
Member
 
tavschke's Avatar
 
Joined: Feb 2024
Posts: 4
Default

im interested in this aswell I hope we get a file importer soon too cheers
tavschke is offline   Reply With Quote
Old 17-02-24, 03:33   #3
nusretsirin
Member
 
nusretsirin's Avatar
 
Joined: Dec 2012
Posts: 33
Default

I'm also interested in a TRM editor or converter. I want to remaster my Jungle themed level ��
nusretsirin is offline   Reply With Quote
Old 17-02-24, 20:51   #4
MuruCoder
Member
 
MuruCoder's Avatar
 
Joined: Jan 2018
Posts: 1,417
Default

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.
MuruCoder is offline   Reply With Quote
Old 18-02-24, 00:50   #5
viggs234
Member
 
Joined: Aug 2022
Posts: 1
Default

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.
viggs234 is offline   Reply With Quote
Old 19-02-24, 00:14   #6
MuruCoder
Member
 
MuruCoder's Avatar
 
Joined: Jan 2018
Posts: 1,417
Default

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
MuruCoder is offline   Reply With Quote
Old 19-02-24, 05:22   #7
tavschke
Member
 
tavschke's Avatar
 
Joined: Feb 2024
Posts: 4
Default

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
tavschke is offline   Reply With Quote
Old 19-02-24, 14:47   #8
MuruCoder
Member
 
MuruCoder's Avatar
 
Joined: Jan 2018
Posts: 1,417
Default 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
There're DUMMY.TRM files. They contain the marker then 20 bytes of zeros for num_shaders, num_textures, num_unknown1, num_indices and num_vertices.

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
}
I used to call these "geometry" but so far "shader" seems the best term for this. It has 3 indices offset & length pairs pointing into the indices array. Both should be multiples of 3 I assume. It offers different rendering for different polygon groups of the object. I did many tests but can't figure out exactly how type & unknown values affect rendering. Shininess (like catsuit or wetsuit has) seems to depend on this.

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
}
Normal components seem in the range 1-253. Values 0, 254 & 255 might be for representing -+Infinity & NaN values or simply unused. Subtracting 127 from the components and then normalising seems to work. Pseudocode:
Code:
vec = Vector(x-127, y-127, z-127)
vec.normalize()
Texture byte is pointing into the texture IDs list above, but starts from 1 not the usual 0. Vertices of the same face/polygon seem to point to the same texture, naturally.

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.
MuruCoder is offline   Reply With Quote
Old 19-02-24, 15:03   #9
MuruCoder
Member
 
MuruCoder's Avatar
 
Joined: Jan 2018
Posts: 1,417
Default 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.

Click image for larger version

Name:	trm_texture_hexedit.png
Views:	499
Size:	7.1 KB
ID:	4393

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.
MuruCoder is offline   Reply With Quote
Old 19-02-24, 15:33   #10
MuruCoder
Member
 
MuruCoder's Avatar
 
Joined: Jan 2018
Posts: 1,417
Default 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
And the script:

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.
MuruCoder is offline   Reply With Quote
Reply

Bookmarks

Thread Tools

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is On
HTML code is Off



All times are GMT. The time now is 12:32.


Powered by vBulletin® Version 3.8.11
Copyright ©2000 - 2024, vBulletin Solutions Inc.
Tomb Raider Forums is not owned or operated by CDE Entertainment Ltd.
Lara Croft and Tomb Raider are trademarks of CDE Entertainment Ltd.