Jump to content
Irup

.TDF Terrain format

Recommended Posts

Irup

I could not find any documentation for it, so I took Xir's suggestion and I've mapped out the basics of the format, but most of it is still a mystery to me. Here's a testing script with my findings for anyone curious, or wanting to do more research. If anyone has any findings that could go into an editor, please post them.

 

Overview:

The terrain is divided into 1024 (32x32) tiles.

There are 4 levels of detail, and each tile's surface consists of a grid of 17x17, 9x9, 5x5 and 3x3 vertices, with each grid popping in and out of view depending on how far you are from it. They can be edited separately.

Each vertex consists of only a height, and 4 bytes, which determine things like lighting and texture blending.

from struct import unpack

infilepath =\
r'C:\Program Files (x86)\LEGO Media\LEGO Racers 2\game data\EDITOR GEN\TERRAIN\SANDY ISLAND\TERRDATA.TDF backup'
outfilepath =\
r'C:\Program Files (x86)\LEGO Media\LEGO Racers 2\game data\EDITOR GEN\TERRAIN\SANDY ISLAND\TERRDATA.TDF'


HEADER_BYTESPER = 0x4
HEADER_OFFSET   = 0x0
HEADER_SIZE     = 0x20

# 0 1 2 3  4  5  6  7
# ffffffff bb bb bb bb
# |        |  |  |  |
# height   |  |  |  texture blending?
#          |  |  light level?????
#          |  flags
#          unknown?????????????
#flags:
# 0x01 0b00000001 = pass-through (all LOD meshes have this checked)
# 0x02 0b00000010
# 0x04 0b00000100
# 0x08 0b00001000
# 0x10 0b00010000
# 0x20 0b00100000
# 0x40 0b01000000
# 0x80 0b10000000
VERTEX_LODS = 4
VERTEX_BYTESPER = 0x8
VERTEX_OFFSET   = (0x20, 0x242020, 0x2e4020, 0x316020)
VERTEX_SIZE     = (0x242000, 0xa2000, 0x32000, 0x12000) # [VERTEX_BYTESPER * lod**2 * WORLD_TILE_LINE**2 for lod in TILE_VERTEX_LINE]
VERTEX_LOD_BYTESPER = [x // 32**2 for x in VERTEX_SIZE]
# unknown
#UNKDA0_BYTESPER = 0xF8     # UNKDA0_SIZE / WORLD_TILE_TOTAL
UNKDA0_OFFSET   = 0x328020 #
UNKDA0_SIZE     = 0x3E000  #
# unknown
UNKDA1_BYTESPER = 0x120    # UNKDA1_SIZE / WORLD_TILE_TOTAL
UNKDA1_OFFSET   = 0x366020 #
UNKDA1_SIZE     = 0x48000  #
# all integers, offsets into tiles from UNKDA1_OFFSET? the numbers are divisible by UNKDA1_BYTESPER
UNKDA2_BYTESPER = 0x4      # UNKDA2_SIZE / WORLD_TILE_TOTAL 
UNKDA2_OFFSET   = 0x3AE020 #
UNKDA2_SIZE     = 0x1000   #
# all floats
UNKDA3_BYTESPER = 0xC8     # UNKDA3_SIZE / WORLD_TILE_TOTAL
UNKDA3_OFFSET   = 0x3AF020 #
UNKDA3_SIZE     = 0x32000  #

TILE_VERTEX_LINE    = (17, 9, 5, 3)
TILE_LENGTH         = [x**2 * VERTEX_BYTESPER    for x in TILE_VERTEX_LINE]
WORLD_TILE_LINE     = 32
WORLD_VERTEX_TOTAL  = [x**2 * WORLD_TILE_LINE**2 for x in TILE_VERTEX_LINE]

DISTANCE = 0.2

def blender_import(): #imports 300000 vertices, but doesn't work too well
	import bpy, bmesh
	f = open(infilepath, 'rb')
	T = TILE_VERTEX_LINE[0]
	terrain_bmesh = bmesh.new()
	for v in range(WORLD_VERTEX_TOTAL[0]):
		terrain_bmesh.verts.new(( #alien code, not safe for human brains
			DISTANCE * ((v % T) + T * ((v // (T**2)) % WORLD_TILE_LINE)),
			DISTANCE * (((v // T) % T) + T * (v // (T**2 * WORLD_TILE_LINE))),
			unpack('f', f.read(4))[0],
		))
		f.read(4)
	terrain_mesh = bpy.data.meshes.new('terraintest mesh')
	terrain_bmesh.to_mesh(terrain_mesh)
	terrain_bmesh.free()
	terrain_object = bpy.data.objects.new('terraintest obj', terrain_mesh)
	bpy.context.scene.objects.link(terrain_object)

def action():
	def showheader():
		f = open(infilepath, 'rb')
		f.seek(HEADER_OFFSET)
		signature = f.read(4).decode('ansi')
		print('Signature:',signature)
		header = [f.read(4) for num in range((HEADER_SIZE-4) // 4)]
		labels = (
			'Unknown 0 ',
			'Unknown 1 ',
			'Unknown 2 ',
			'Add height',
			'Unknown 3 ',
			'Unknown 4 ',
			'Unknown 5 ',
		)
		for label, number in zip(labels, header):
			print('%s: %-10i %-11i %-8x' % (
					label,
					unpack('I', number)[0],
					unpack('i', number)[0],
					unpack('I', number)[0]
				),
				unpack('f', number)[0]
			)
		f.close()
	def findpassthroughvert():
		f = open(infilepath, 'rb')
		o = open(outfilepath, 'wb')
		f.seek(HEADER_OFFSET)
		o.write(f.read(HEADER_SIZE))
		for lod in range(VERTEX_LODS):
			for tile in range(32**2):
				passthroughvertex = False
				for vertex in range(TILE_VERTEX_LINE[lod]**2):
						co = f.read(4)
						unk, flags, light, texblend = f.read(4)
						if not lod and flags & 1: passthroughvertex = (f.tell()-8, flags)
						o.write(co + bytes((0, 0 if passthroughvertex else flags, light, texblend)))
				if passthroughvertex: print('Found pass-through vertex in tile %i, flags: %2x offset: %x' % (tile, passthroughvertex[1], passthroughvertex[0]))
		o.write(f.read())
		f.close()
		o.close()
	def clearflagseveryfifthtile():
		f = open(infilepath, 'rb')
		o = open(outfilepath, 'wb')
		f.seek(HEADER_OFFSET)
		o.write(f.read(HEADER_SIZE))
		for lod in range(VERTEX_LODS):
			for tile in range(32**2):
				if not tile % 5:
					for vertex in range(TILE_VERTEX_LINE[lod]**2):
						co = f.read(4)
						unk, flags, light, texblend = f.read(4)
						o.write(co + bytes((unk, 0, light, texblend)))
				else:
					o.write(f.read(TILE_LENGTH[lod]))
		f.close()
		o.close()
	def makeunkda1homogenous(every = None, copytile = 32*4+16): #can make tiles that crash the game if crossed, tiles that flicker textures, the results are not very predictable
		f = open(infilepath, 'rb')
		o = open(outfilepath, 'wb')
		f.seek(HEADER_OFFSET)
		o.write(f.read(HEADER_SIZE + sum(VERTEX_SIZE) + UNKDA0_SIZE))
		
		f.seek(UNKDA1_OFFSET + UNKDA1_BYTESPER*copytile)
		tilesample = f.read(UNKDA1_BYTESPER)
		
		for tile in range(32**2):
			if every != None and not tile % every:
				o.write(tilesample)
			else:
				f.seek(UNKDA1_OFFSET + UNKDA1_BYTESPER*tile)
				o.write(f.read(UNKDA1_BYTESPER))
			
		f.seek(UNKDA2_OFFSET)
		o.write(f.read())
		f.close()
		o.close()
	makeunkda1homogenous(every = 5)
	input('Finished')
	
action()

 

Here I set all the vertex heights in every fifth tile to zero:

gGl49Dp.png

 

Here's the very broken, but almost recognizable blender import:

rOEMqec.png

 

This weird thing happened by making every tile info structure (presumably, UNKDA1) homogenous to tile info 144. I've updated the script to be nicer and have more functions. All the invisible ground was solid.

pQf7Rke.png

Share this post


Link to post
Share on other sites
Mysteli

Ah welp, you've got more on it than I have from my several times poking at it, bravo! x)

 

Quote

Here I set all the vertex heights in every fifth tile to zero:

Ooh - if it ain't too much, think you can quickly whip up a flat map? I tried setting all bytes to 0 once, but I got these spiked up walls on the edges of the tiles for some reason.

 

You know, I noticed something, sometimes the terrain can have cutouts, I wonder how that is done? If you find an answer, let me know.

Share this post


Link to post
Share on other sites
Irup

I don't know where the tile borders are stored, but they're not in the vertex chunk, known as VERTEX_OFFSET in the script. Perhaps they're in UNKDA1 or UNKDA3, or even the TDF's accompanying OCCLUSION DATA file, which I think determines what chunks are rendered depending on where you are, as a rendering optimization. That would need to be explored as well to make a future flatgrass map less annoying.

 

The hole, like your screenshot shows, is fillable by setting the vertex flags to 0:

jFOLXfn.png

Share this post


Link to post
Share on other sites
Mysteli

Thanks, but I was actually wondering the opposite. What do the bytes/value look like that make it non-filled?

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.


  • Recently Browsing   0 members

    No registered users viewing this page.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.