Add files via upload

This commit is contained in:
Currency
2025-04-13 13:50:29 +02:00
committed by GitHub
parent e9556afd60
commit a194c5093e

654
xml_map_importer.py Normal file
View File

@@ -0,0 +1,654 @@
"""
XML Map Importer for Blender 4.4
Imports XML map files with prop libraries.
"""
bl_info = {
"name": "Tanki XML Map Importer",
"author": "MapMakers Conglomerate",
"version": (1, 0),
"blender": (4, 4, 0),
"location": "File > Import > XML Map (.xml)",
"description": "Import tanki XML maps ",
"warning": "You need to have the 3DS addonn enabled to use this.",
"doc_url": "",
"category": "Import-Export",
}
import bpy
import os
import xml.etree.ElementTree as ET
import math
import tempfile
import mathutils
import time
import threading
from collections import defaultdict
from bpy.props import StringProperty, CollectionProperty, BoolProperty, EnumProperty, IntProperty
from bpy_extras.io_utils import ImportHelper
from bpy.types import Operator, Panel, PropertyGroup, AddonPreferences
# preferences
class XMLMapImporterPreferences(AddonPreferences):
bl_idname = __name__
prop_libs_directory: StringProperty(
name="Prop Libraries Directory",
subtype='DIR_PATH',
description="Directory containing prop libraries"
)
threads: IntProperty(
name="Import Threads",
description="Number of threads to use for parallel importing (0 = auto)",
default=0,
min=0,
max=32
)
batch_size: IntProperty(
name="Batch Size",
description="Number of props to process in each batch",
default=50,
min=10,
max=1000
)
def draw(self, context):
layout = self.layout
layout.label(text="XML Map Importer Settings")
layout.prop(self, "prop_libs_directory")
layout.prop(self, "threads")
layout.prop(self, "batch_size")
class PropLibraryItem(PropertyGroup):
name: StringProperty(name="Name")
path: StringProperty(name="Path")
loaded: BoolProperty(name="Loaded", default=False)
# Main importer
class IMPORT_OT_xml_map(Operator, ImportHelper):
"""Import an XML map file with associated prop libraries"""
bl_idname = "import_scene.xml_map"
bl_label = "Import XML Map"
bl_options = {'PRESET', 'UNDO'}
filename_ext = ".xml"
filter_glob: StringProperty(
default="*.xml",
options={'HIDDEN'},
)
import_textures: BoolProperty(
name="Import Textures",
description="Import and assign textures to objects",
default=True,
)
create_collections: BoolProperty(
name="Create Collection",
description="Create a collection for the map",
default=True,
)
scale_factor: bpy.props.FloatProperty(
name="Scale Factor",
description="Scale factor for imported objects",
default=0.01,
)
axis_forward: EnumProperty(
name="Forward Axis",
items=(
('X', "X", ""),
('Y', "Y", ""),
('Z', "Z", ""),
('-X', "-X", ""),
('-Y', "-Y", ""),
('-Z', "-Z", ""),
),
default='Y',
)
axis_up: EnumProperty(
name="Up Axis",
items=(
('X', "X", ""),
('Y', "Y", ""),
('Z', "Z", ""),
('-X', "-X", ""),
('-Y', "-Y", ""),
('-Z', "-Z", ""),
),
default='Z',
)
rotation_mode: EnumProperty(
name="Rotation",
items=(
('DEGREES', "Degrees", "Rotations in XML are in degrees"),
('RADIANS', "Radians", "Rotations in XML are in radians"),
),
default='RADIANS',
)
use_caching: BoolProperty(
name="Cache Props",
description="Cache prop meshes to speed up loading of repeated props",
default=True,
)
def execute(self, context):
prefs = context.preferences.addons[__name__].preferences
prop_libs_dir = prefs.prop_libs_directory
if not os.path.isdir(prop_libs_dir):
self.report({'ERROR'}, f"Prop libraries directory not found: {prop_libs_dir}")
return {'CANCELLED'}
start_time = time.time()
result = self.import_xml_map(context, prop_libs_dir)
end_time = time.time()
if result == {'FINISHED'}:
self.report({'INFO'}, f"Map imported in {end_time - start_time:.2f} seconds")
return result
def import_xml_map(self, context, prop_libs_dir):
# parse the XML
try:
tree = ET.parse(self.filepath)
root = tree.getroot()
except Exception as e:
self.report({'ERROR'}, f"Error parsing XML file: {e}")
return {'CANCELLED'}
if root.tag != 'map':
self.report({'ERROR'}, "Not a valid map file")
return {'CANCELLED'}
map_name = os.path.splitext(os.path.basename(self.filepath))[0]
map_collection = None
if self.create_collections:
map_collection = bpy.data.collections.new(map_name)
bpy.context.scene.collection.children.link(map_collection)
# Load the prop libs - this could be slow
self.report({'INFO'}, "Loading prop libraries...")
prop_libraries = self.load_prop_libraries(prop_libs_dir)
if self.use_caching:
self._mesh_cache = {}
self._material_cache = {}
static_geometry = root.find('static-geometry')
if static_geometry is not None:
self.import_static_geometry(context, static_geometry, prop_libraries, map_collection)
if self.use_caching:
self._mesh_cache = {}
self._material_cache = {}
self.report({'INFO'}, f"XML Map imported successfully: {map_name}")
return {'FINISHED'}
def load_prop_libraries(self, prop_libs_dir):
"""Load prop libraries from the directory"""
prop_libraries = {}
lib_dirs = [os.path.join(prop_libs_dir, item) for item in os.listdir(prop_libs_dir)
if os.path.isdir(os.path.join(prop_libs_dir, item))]
# Load each library
for lib_dir in lib_dirs:
lib_xml_path = os.path.join(lib_dir, "library.xml")
if os.path.exists(lib_xml_path):
try:
lib_tree = ET.parse(lib_xml_path)
lib_root = lib_tree.getroot()
lib_name = lib_root.get('name')
if lib_name:
prop_libraries[lib_name] = {
'path': lib_dir,
'xml': lib_root,
'props': {}
}
props_dict = {}
for prop_group in lib_root.findall('.//prop-group'):
group_name = prop_group.get('name')
for prop in prop_group.findall('.//prop'):
prop_name = prop.get('name')
prop_key = f"{group_name}/{prop_name}"
props_dict[prop_key] = prop
prop_libraries[lib_name]['props'] = props_dict
except Exception as e:
print(f"Error loading prop library {lib_dir}: {e}")
return prop_libraries
def import_static_geometry(self, context, static_geometry, prop_libraries, parent_collection):
"""Import all the props from static geometry"""
target_collection = parent_collection or context.scene.collection
props_to_import = []
for prop_elem in static_geometry.findall('.//prop'):
library_name = prop_elem.get('library-name')
group_name = prop_elem.get('group-name')
prop_name = prop_elem.get('name')
position_elem = prop_elem.find('.//position')
if position_elem is None:
continue
x = float(position_elem.find('x').text)
y = float(position_elem.find('y').text)
z = float(position_elem.find('z').text)
rotation_elem = prop_elem.find('.//rotation')
rot_z = 0.0
if rotation_elem is not None and rotation_elem.find('z') is not None:
rot_z = float(rotation_elem.find('z').text)
if self.rotation_mode == 'DEGREES':
rot_z = math.radians(rot_z)
texture_name_elem = prop_elem.find('.//texture-name')
texture_name = ""
if texture_name_elem is not None and texture_name_elem.text:
texture_name = texture_name_elem.text
props_to_import.append({
'library_name': library_name,
'group_name': group_name,
'prop_name': prop_name,
'position': (x, y, z),
'rotation': rot_z,
'texture_name': texture_name
})
prefs = context.preferences.addons[__name__].preferences
batch_size = prefs.batch_size
total_props = len(props_to_import)
self.report({'INFO'}, f"Importing {total_props} props...")
props_by_type = defaultdict(list)
for prop_info in props_to_import:
prop_key = f"{prop_info['library_name']}/{prop_info['group_name']}/{prop_info['prop_name']}"
props_by_type[prop_key].append(prop_info)
imported_count = 0
for prop_type, props in props_by_type.items():
if props:
prop_info = props[0]
self.import_prop(context, prop_info['library_name'], prop_info['group_name'],
prop_info['prop_name'], prop_info['position'], prop_info['rotation'],
prop_info['texture_name'], prop_libraries, target_collection)
imported_count += 1
for prop_info in props[1:]:
self.import_prop(context, prop_info['library_name'], prop_info['group_name'],
prop_info['prop_name'], prop_info['position'], prop_info['rotation'],
prop_info['texture_name'], prop_libraries, target_collection)
imported_count += 1
if imported_count % 50 == 0:
self.report({'INFO'}, f"Imported {imported_count}/{total_props} props...")
self.report({'INFO'}, f"Finished importing {imported_count} props")
def import_prop(self, context, library_name, group_name, prop_name,
position, rotation, texture_name, prop_libraries, parent_collection):
"""Import a single prop"""
if library_name not in prop_libraries:
return None
library = prop_libraries[library_name]
prop_key = f"{group_name}/{prop_name}"
if prop_key not in library['props']:
return None
prop_def = library['props'][prop_key]
mesh_elem = prop_def.find('.//mesh')
if mesh_elem is None:
return None
mesh_file = mesh_elem.get('file')
mesh_path = os.path.join(library['path'], mesh_file)
if not os.path.exists(mesh_path):
return None
cache_key = f"{library_name}_{group_name}_{prop_name}"
mesh_data = None
material = None
if self.use_caching and cache_key in self._mesh_cache:
mesh_data = self._mesh_cache[cache_key]
if texture_name and texture_name in self._material_cache:
material = self._material_cache[texture_name]
else:
mesh_data = self.import_mesh_data(context, mesh_path, library_name, prop_name)
if self.use_caching and mesh_data:
self._mesh_cache[cache_key] = mesh_data
if mesh_data:
object_name = f"{library_name}_{prop_name}"
prop_obj = bpy.data.objects.new(object_name, mesh_data)
parent_collection.objects.link(prop_obj)
scaled_position = [p * self.scale_factor for p in position]
if self.axis_up == 'Z':
final_position = (scaled_position[0], scaled_position[1], scaled_position[2])
else:
final_position = (scaled_position[0], scaled_position[2], scaled_position[1])
prop_obj.location = final_position
rot_matrix = mathutils.Matrix.Rotation(rotation, 4, 'Z')
if self.axis_up == 'Z':
prop_obj.rotation_euler = rot_matrix.to_euler('XYZ')
else:
conversion_matrix = mathutils.Matrix.Rotation(math.pi/2.0, 4, 'X')
final_rotation = conversion_matrix @ rot_matrix @ conversion_matrix.inverted()
prop_obj.rotation_euler = final_rotation.to_euler()
prop_obj.scale = (self.scale_factor, self.scale_factor, self.scale_factor)
if self.import_textures and texture_name:
if material:
if prop_obj.data.materials:
prop_obj.data.materials[0] = material
else:
prop_obj.data.materials.append(material)
else:
material = self.create_material(texture_name, mesh_elem, library)
if material:
if prop_obj.data.materials:
prop_obj.data.materials[0] = material
else:
prop_obj.data.materials.append(material)
if self.use_caching:
self._material_cache[texture_name] = material
return prop_obj
return None
def import_mesh_data(self, context, mesh_path, library_name, prop_name):
"""Import mesh from 3DS file"""
pre_import_objects = set(bpy.data.objects)
temp_collection = bpy.data.collections.new("__temp_import")
bpy.context.scene.collection.children.link(temp_collection)
try:
original_active_collection = context.view_layer.active_layer_collection
temp_layer_collection = context.view_layer.layer_collection.children[temp_collection.name]
context.view_layer.active_layer_collection = temp_layer_collection
bpy.ops.import_scene.max3ds(filepath=mesh_path)
context.view_layer.active_layer_collection = original_active_collection
imported_objects = [obj for obj in bpy.data.objects if obj not in pre_import_objects]
mesh_objects = [obj for obj in imported_objects if obj.type == 'MESH']
if not mesh_objects:
bpy.data.collections.remove(temp_collection)
return None
meshes_with_materials = [obj for obj in mesh_objects if
obj.data.materials and
len(obj.data.materials) > 0 and
any(mat is not None for mat in obj.data.materials)]
best_mesh = None
if meshes_with_materials:
meshes_with_materials.sort(key=lambda obj: len(obj.data.vertices), reverse=True)
best_mesh = meshes_with_materials[0].data
best_mesh = best_mesh.copy()
for obj in imported_objects:
bpy.data.objects.remove(obj, do_unlink=True)
bpy.data.collections.remove(temp_collection)
return best_mesh
except Exception as e:
if temp_collection and temp_collection.name in bpy.data.collections:
bpy.data.collections.remove(temp_collection)
print(f"Error importing mesh {mesh_path}: {e}")
return None
def create_material(self, texture_name, mesh_elem, library):
"""Create a new material with texture"""
texture_elem = mesh_elem.find(f'.//texture[@name="{texture_name}"]')
if texture_elem is None:
return None
diffuse_map = texture_elem.get('diffuse-map')
if not diffuse_map:
return None
texture_path = os.path.join(library['path'], diffuse_map)
if not os.path.exists(texture_path):
return None
img = None
if texture_path in bpy.data.images:
img = bpy.data.images[texture_path]
else:
img = bpy.data.images.load(texture_path)
mat_name = f"{texture_name}_material"
mat = bpy.data.materials.new(name=mat_name)
mat.use_nodes = True
nodes = mat.node_tree.nodes
links = mat.node_tree.links
bsdf = nodes.get('Principled BSDF')
if bsdf is None:
bsdf = nodes.new('ShaderNodeBsdfPrincipled')
tex_node = nodes.new('ShaderNodeTexImage')
tex_node.image = img
links.new(tex_node.outputs['Color'], bsdf.inputs['Base Color'])
return mat
# ui for import options
def draw(self, context):
layout = self.layout
box = layout.box()
box.label(text="Import Options")
box.prop(self, "import_textures")
box.prop(self, "create_collections")
box.prop(self, "scale_factor")
box.prop(self, "use_caching")
box = layout.box()
box.label(text="Coordinate System")
row = box.row()
row.prop(self, "axis_forward")
row.prop(self, "axis_up")
box = layout.box()
box.label(text="Rotation Settings")
box.prop(self, "rotation_mode")
class VIEW3D_PT_xml_map_libraries(Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'XML Map'
bl_label = "XML Map Libraries"
def draw(self, context):
layout = self.layout
scene = context.scene
prefs = context.preferences.addons[__name__].preferences
row = layout.row()
row.label(text="Prop Libraries Directory:")
row = layout.row()
row.prop(prefs, "prop_libs_directory", text="")
row = layout.row()
row.prop(prefs, "threads")
row = layout.row()
row.prop(prefs, "batch_size")
row = layout.row()
row.operator("xml_map.refresh_libraries", text="Refresh Libraries")
class XML_MAP_OT_refresh_libraries(Operator):
"""Refresh the list of prop libraries"""
bl_idname = "xml_map.refresh_libraries"
bl_label = "Refresh Prop Libraries"
def execute(self, context):
prefs = context.preferences.addons[__name__].preferences
prop_libs_dir = prefs.prop_libs_directory
if not os.path.isdir(prop_libs_dir):
self.report({'ERROR'}, f"Prop libraries directory not found: {prop_libs_dir}")
return {'CANCELLED'}
self.report({'INFO'}, f"Prop libraries refreshed from: {prop_libs_dir}")
return {'FINISHED'}
# registration
classes = (
XMLMapImporterPreferences,
PropLibraryItem,
IMPORT_OT_xml_map,
VIEW3D_PT_xml_map_libraries,
XML_MAP_OT_refresh_libraries,
)
def menu_func_import(self, context):
self.layout.operator(IMPORT_OT_xml_map.bl_idname, text="XML Map (.xml)")
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
def unregister():
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
if __name__ == "__main__":
register()