diff --git a/terrain_mesh_extractor/ExtractTerrainMeshes.py b/terrain_mesh_extractor/ExtractTerrainMeshes.py new file mode 100644 index 0000000..0c71337 --- /dev/null +++ b/terrain_mesh_extractor/ExtractTerrainMeshes.py @@ -0,0 +1,88 @@ +from unitypackff.asset import Asset +from unitypackff.environment import UnityEnvironment +import bpy +import bmesh +import os + +dongpath = r'C:\Users\gents\AppData\LocalLow\Unity\Web Player\Cache\Fusionfall' +env = UnityEnvironment(base_path=dongpath) +outpath = r'C:\Users\gents\3D Objects\FFTerrainMeshes' + +def rip_terrain_mesh(f, outpath): + dong = Asset.from_file(f, environment=env) + + for k, v in dong.objects.items(): + if v.type == 'TerrainData': + terrainData = dong.objects[k].read() + terrain_width = terrainData['m_Heightmap']['m_Width'] - 1 + terrain_height = terrainData['m_Heightmap']['m_Height'] - 1 + + # create the terrain + bpy.ops.mesh.primitive_grid_add(x_subdivisions=terrain_width, y_subdivisions=terrain_height, size=40, enter_editmode=True, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1)) + context = bpy.context + grid = context.edit_object + + # apply triangulate modifier + mod = grid.modifiers.new("Triangulate", 'TRIANGULATE') + mod.quad_method = 'FIXED' # triangle orientation + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.modifier_apply(modifier="Triangulate") + + bpy.ops.object.mode_set(mode='EDIT') + bm = bmesh.from_edit_mesh(context.edit_object.data) + bm.verts.ensure_lookup_table() + for index, height in enumerate(terrainData['m_Heightmap']['m_Heights']): + height = height / terrainData['m_Heightmap']['m_Scale']['y'] + bm.verts[index].co.z = height + + indices = [] + shift_amt = abs(bm.verts[0].co.x - bm.verts[1].co.x) + # gather m_Shifts positions + for shift in terrainData['m_Heightmap']['m_Shifts']: + shift_index = shift['y'] + shift['x'] * 129 + indices.append(shift_index) + v = bm.verts[shift_index] + flags = shift['flags'] # bits: +X -X +Y -Y + if flags & 0b1000: # +X + v.co.x += shift_amt + if flags & 0b0100: # -X + v.co.x -= shift_amt + if flags & 0b0010: # +Y + v.co.y += shift_amt + if flags & 0b0001: # -Y + v.co.y -= shift_amt + + # flip to correct orientation + bpy.ops.object.mode_set(mode="OBJECT") + bpy.ops.object.select_all(action='SELECT') + bpy.ops.transform.mirror(orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', constraint_axis=(False, True, False)) + outfile = f"{k}.fbx" + bpy.ops.export_scene.fbx(filepath=os.path.join(outpath, outfile)) + + # select modified vertices + #bpy.ops.object.mode_set(mode="EDIT") + #bm = bmesh.from_edit_mesh(context.edit_object.data) + #bm.verts.ensure_lookup_table() + #for v in bm.verts: + # v.select = False + #for shift_index in indices: + # v = bm.verts[shift_index] + # v.select = True + + # clear the scene + bpy.ops.object.mode_set(mode="OBJECT") + bpy.ops.object.select_all(action='SELECT') + bpy.ops.object.delete() + +dongs = os.listdir(dongpath) +for dongname in dongs: + if not dongname.endswith("resourceFile"): + continue + assets = os.listdir(os.path.join(dongpath, dongname)) + for assetname in assets: + if not assetname.startswith("CustomAssetBundle"): + continue + with open(os.path.join(dongpath, dongname, assetname), "rb") as f: + outdir = os.path.join(outpath, dongname, assetname) + os.makedirs(outdir, exist_ok=True) + rip_terrain_mesh(f, outdir) \ No newline at end of file diff --git a/terrain_mesh_extractor/README.md b/terrain_mesh_extractor/README.md new file mode 100644 index 0000000..7476f92 --- /dev/null +++ b/terrain_mesh_extractor/README.md @@ -0,0 +1,5 @@ +# Terrain Mesh Extractor +Blender + UPFF script to import terrain data as a mesh into Blender, then apply the shifts property to applicable vertices. +- Exports as FBX +- The fbx filenames are the index of the TerrainData object within the asset file +- Folders for asset bundles that had no terrain objects will be empty