diff --git a/__init__.py b/__init__.py index 6d2335b..91151c4 100644 --- a/__init__.py +++ b/__init__.py @@ -24,7 +24,7 @@ "author" : "Marek Zajac", "description" : "Import and export asset files for DICE's Refractor 2 engine", "blender" : (4, 1, 1), - "version" : (0, 7, 6), + "version" : (0, 7, 7), "location": "File -> Import/Export -> BF2", "warning" : "", "doc_url": "https://github.com/marekzajac97/bf2-blender/blob/main/docs/README.md", diff --git a/core/mesh.py b/core/mesh.py index d089b1c..c45879c 100644 --- a/core/mesh.py +++ b/core/mesh.py @@ -102,7 +102,8 @@ def _export_mesh(mesh_obj, mesh_file, mesh_type, **kwargs): class MeshImporter: def __init__(self, context, mesh_file, mesh_type='', reload=False, - texture_path='', geom_to_ske=None, merge_materials=True, reporter=DEFAULT_REPORTER): + texture_path='', geom_to_ske=None, merge_materials=True, + reporter=DEFAULT_REPORTER): self.context = context self.is_vegitation = 'vegitation' in mesh_file.lower() # yeah this is legit how BF2 detects it lmao if mesh_type: @@ -425,19 +426,25 @@ def _cleanup_old_materials(self): bpy.data.materials.remove(material, do_unlink=True) def _get_unique_material_index(self, other_bf2_mat): - merge_materials = self.merge_materials + material_index = lod_index = geom_index = -1 bone_count = 0 - if merge_materials and isinstance(self.bf2_mesh, BF2SkinnedMesh): - for geom in self.bf2_mesh.geoms: - for lod in geom.lods: - try: - material_index = lod.materials.index(other_bf2_mat) - rig = lod.rigs[material_index] - bone_count = len(rig.bones) - except ValueError: - pass - if merge_materials: + if self.merge_materials: + if isinstance(self.bf2_mesh, BF2SkinnedMesh): + for geom_idx, geom in enumerate(self.bf2_mesh.geoms): + if material_index != -1: + break + for lod_idx, lod in enumerate(geom.lods): + try: + material_index = lod.materials.index(other_bf2_mat) + rig = lod.rigs[material_index] + bone_count = len(rig.bones) + geom_index = geom_idx + lod_index = lod_idx + break + except ValueError: + pass + for mat_idx, mat_data in enumerate(self.mesh_materials): bf2_mat = mat_data['bf2_mat'] if type(bf2_mat) != type(other_bf2_mat): @@ -458,14 +465,20 @@ def _get_unique_material_index(self, other_bf2_mat): if (isinstance(bf2_mat, MaterialWithTransparency) and bf2_mat.alpha_mode != other_bf2_mat.alpha_mode): continue - if mat_data['bone_count'] + bone_count > MAX_BONE_LIMIT: - self.reporter.warning("Cannot merge material, bone limit has been reached") + + material_bone_count = mat_data['geom_to_bone_count'].setdefault(geom_index, 0) + if material_bone_count + bone_count > MAX_BONE_LIMIT: + self.reporter.warning(f"Geom{geom_index} Lod{lod_index}: cannot merge material {material_index}, bone limit has been reached") break - mat_data['bone_count'] += bone_count + mat_data['geom_to_bone_count'][geom_index] += bone_count + + # self.reporter.warning(f"Geom{geom_index} Lod{lod_index}: material {material_index} has been merged") return mat_idx mat_idx = len(self.mesh_materials) - mat_data = {'bf2_mat': other_bf2_mat, 'bone_count': bone_count} + geom_to_bone_count = dict() + geom_to_bone_count[geom_index] = bone_count + mat_data = {'bf2_mat': other_bf2_mat, 'geom_to_bone_count': geom_to_bone_count} self.mesh_materials.append(mat_data) return mat_idx @@ -908,7 +921,9 @@ def _can_merge_vert(self, this, other, uv_count): return False for uv_chan in range(uv_count): uv_attr = f'texcoord{uv_chan}' - if getattr(this, uv_attr) != getattr(other, uv_attr): + this_uv = getattr(this, uv_attr) + other_uv = getattr(other, uv_attr) + if any([abs(this_uv[i] - other_uv[i]) > 0.0001 for i in (0, 1)]): return False return True diff --git a/core/object_template.py b/core/object_template.py index 73a861e..31eda21 100644 --- a/core/object_template.py +++ b/core/object_template.py @@ -31,7 +31,8 @@ def __init__(self, part_id, obj) -> None: self.matrix_local = obj.matrix_local.copy() self.children = [] -def import_object(context, con_filepath, import_collmesh=False, import_rig=('AUTO', None), reload=False, reporter=DEFAULT_REPORTER, **kwargs): +def import_object(context, con_filepath, import_collmesh=False, import_rig=('AUTO', None), + reload=False, weld_verts=False, reporter=DEFAULT_REPORTER, **kwargs): BF2Engine().shutdown() # clear previous state obj_template_manager = BF2Engine().get_manager(ObjectTemplate) geom_template_manager = BF2Engine().get_manager(GeometryTemplate) @@ -47,18 +48,27 @@ def import_object(context, con_filepath, import_collmesh=False, import_rig=('AUT if root_template is None: root_template = object_template else: - raise ImportException(f"{con_filepath}: found multiple root objects: {root_template}, {object_template}, which one to import?") + raise ImportException(f"{con_filepath}: found multiple root objects: {root_template.name}, {object_template.name}, which one to import?") if root_template is None: raise ImportException(f"{con_filepath}: root object not found!") if not root_template.geom: ImportException(f"The imported object '{root_template.name}' has no geometry set") - - geometry_template = geom_template_manager.templates[root_template.geom.lower()] + + _verify_template(root_template) + + geometry_template_name = root_template.geom.lower() + if geometry_template_name not in geom_template_manager.templates: + raise ImportException(f"Geometry '{collmesh_template_name}' is not defined") + + geometry_template = geom_template_manager.templates[geometry_template_name] collmesh_template = None if root_template.collmesh: - collmesh_template = col_template_manager.templates[root_template.collmesh.lower()] + collmesh_template_name = root_template.collmesh.lower() + if collmesh_template_name not in col_template_manager.templates: + raise ImportException(f"Collision mesh '{collmesh_template_name}' is not defined") + collmesh_template = col_template_manager.templates[collmesh_template_name] con_dir = os.path.dirname(con_filepath) geometry_type = geometry_template.geometry_type @@ -90,6 +100,11 @@ def import_object(context, con_filepath, import_collmesh=False, import_rig=('AUT geom_parts = {'mesh1': lod_obj} # XXX hack else: geom_parts = _split_mesh_by_vertex_groups(context, lod_obj) + + if weld_verts: + for geom_part_obj in geom_parts.values(): + _weld_verts(geom_part_obj) + new_lod = _apply_mesh_data_to_lod(context, root_template, geom_parts, coll_parts, geom_idx, lod_idx) new_lod.parent = geom_obj _fix_unassigned_parts(geom_obj, new_lod) @@ -376,7 +391,7 @@ def _strip_prefix(s): for char_idx, _ in enumerate(s): if s[char_idx:].startswith('__'): return s[char_idx+2:] - return ExportException(f"'{s}' has no GxLx__ prefix!") + raise ExportException(f"'{s}' has no GxLx__ prefix!") def _object_hierarchy_has_any_meshes(obj, parent_bones): if obj.data and isinstance(obj.data, Mesh) and len(obj.data.vertices): @@ -838,6 +853,17 @@ def _triangulate(obj): bpy.ops.object.mode_set(mode='OBJECT') obj.hide_set(hide) +def _weld_verts(obj): + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + bpy.ops.object.select_all(action='DESELECT') + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_mode(type='VERT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.remove_doubles(threshold=0.0001) + bpy.ops.object.mode_set(mode='OBJECT') + def _get_nr_of_animted_uvs(mesh_geoms): matrix_set = set() for geom_obj in mesh_geoms: @@ -919,4 +945,14 @@ def put_rig_safe(geom_idx, ske_name): geom_to_ske[geom_idx] = rigs[ske_name] else: raise ImportException(f'Unhandled import_rig_mode {import_rig_mode}') - return geom_to_ske \ No newline at end of file + return geom_to_ske + +def _verify_template(root_obj_template): + part_id_to_obj_template = dict() + def _check_geom_part_unique(obj_template): + if obj_template.geom_part in part_id_to_obj_template: + raise ImportException(f"'{obj_template.name}' has the same ObjectTemplate.geometryPart index as '{part_id_to_obj_template[obj_template.geom_part].name}'") + part_id_to_obj_template[obj_template.geom_part] = obj_template + for child in obj_template.children: + _check_geom_part_unique(child.template) + _check_geom_part_unique(root_obj_template) diff --git a/docs/README.md b/docs/README.md index 68935c0..e139291 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,18 +9,18 @@ After installation, set up your `BF2 mod directory` (`Edit -> Preferences -> Add - TIP: If you plan to modify the imported animation, you can delete redundant keyframes using [Decimate](https://docs.blender.org/manual/en/latest/editors/graph_editor/fcurves/editing.html#decimate) option - When exporting, you can select/deselect bones for export in the export menu (matters for 3P animations, depending on whether you're making soldier or weapon animations, a different bone set needs to be selected). -# ObjectTemplate vs Mesh import/export -There are two ways of importing BF2 meshes. One is to use `Import -> BF2` menu to directly import a specific mesh file type e.g. `StaticMesh`. This however will only import the "raw" mesh data according to its internal file structure lacking information about objectTemplate's metadata such as geometry part names, their position/rotation, hierarchy, collision material names etc. This is fine for simple meshes or when you intend just to make small tweaks to the mesh, but generally if you don't have a good reason to use those options, **don't** use them. A preferable way to import a mesh is the `Import -> BF2 -> ObjecTemplate (.con)` option, which parses the objectTemplate definition and import the visible geometry of the object (optionally also its collision mesh), split all mesh parts into sub-meshes, reposition them, recreate their hierarchy as well as rename collision mesh materials. For re-exporting, always use the corresponding option from the `Export -> BF2` menu. +# Import/export of ObjectTemplate vs (Bundled/Skinned/Static/Collision)Mesh +There are two ways of importing BF2 meshes. One is to use `Import -> BF2` menu to directly import a specific mesh file type e.g. `StaticMesh`. This however will only import the "raw" mesh data according to its internal file structure lacking information about objectTemplate's metadata such as geometry part names, their position/rotation, hierarchy, collision material names etc. This is fine for simple meshes or when you intend just to make small tweaks to the mesh, but generally if you don't have a good reason to use those options, **don't** use them. A preferable way to import a mesh is the `Import -> BF2 -> ObjecTemplate (.con)` option, which parses the objectTemplate definition and imports the visible geometry of the object (optionally also its collision mesh), split all mesh parts into sub-meshes, reposition them, recreate their hierarchy as well as rename collision mesh materials. For re-exporting, always use the corresponding option from the `Export -> BF2` menu. -# Object exporting -- `Export -> BF2 -> ObjecTemplate (.con)` shall be used for exporting objects (like 3ds Max exporter, it spits out a `.con` file + visible mesh + collision mesh into `Meshes` sub-directory). Export option will only be available when you have an object active in the viewport. Before reading any further, I highly advise you to import any existing BF2 mesh first (`Import -> BF2 -> ObjecTemplate (.con)`), and look at how everything is set up to make below steps easier to follow. +# ObjectTemplate exporting +- `Export -> BF2 -> ObjecTemplate (.con)` shall be used for exporting objects created from scratch (like 3ds Max exporter, it spits out a `.con` file + visible mesh + collision mesh into `Meshes` sub-directory). Export option will only be available when you have an object active in the viewport. Before reading any further, I highly advise you to import any existing BF2 mesh first (`Import -> BF2 -> ObjecTemplate (.con)`), and look at how everything is set up to make below steps easier to follow. ## The object hierarchy - The active object needs to be the root of the hierarchy. The root needs to be prefixed with the geometry type: `StaticMesh`, `BundledMesh` or `SkinnedMesh`, followed by an underscore and the name of the root ObjectTemplate. Each child of the root object must be an empty object that corresponds to Geom (prefixed with `G__`). A static may also contain an empty child object which defines its anchor point (prefixed with `ANCHOR__`). -- Geom represents the same mesh in a different perspective or state e.g. for Soldiers/Weapons/Vehicles Geom0 and Geom1 refer to 1P and 3P meshes respectively. Statics and Vehicles may also have an extra Geom for the destroyed/wreck variant. +- Each Geom usually represents the same object viewed from a different perspective or in a different state e.g. for Soldiers/Weapons/Vehicles Geom0 and Geom1 refer to 1P and 3P meshes respectively. Statics and Vehicles may also have an extra Geom for the destroyed/wreck variant. - Each child of the Geom object must be an object that corresponds to Lod (Level of detail) (prefixed with `GL__`) containing mesh data. Each Lod should be a simplified version of the previous one. There must be at least one Lod. -- Each Lod may contain multiple child objects that will be exported as separate ObjectTemplates using different geometry parts, each Lod must contain the same hierarchy of them. StaticMeshes or SkinnedMeshes usually don't have any parts, so Lod will be just a single object, but for BundledMeshes you might want to have multiple geometry parts (e.g. "hull" as root and a "turret" and "motor" as its child objects). Those objects cannot be empty, each one must contain mesh data to export properly! However, if you just want them to export as invisible but separate logical objects (e.g. the `Engine` ObjectTemplate of the vehicle) you can delete all geometry (verts/faces) from the mesh object. -- Each object in the hierarchy should have its corresponding BF2 ObjectTemplate type set (e.g. `Bundle`, `PlayerControlObject` etc). You will find this property in the `Object Properties` tab, it defaults to `SimpleObject`. However, you may want some meshes to export as separate geometry parts but at the same time don't export as separate ObjectTemplates (e.g. an animatable magazine that is a part of `GenericFirearm`) in such case simply leave this property empty. +- Each Lod may contain multiple child objects that will be exported as separate ObjectTemplates using different geometry parts, each Lod must contain the same hierarchy of them. StaticMeshes or SkinnedMeshes usually don't have any parts, so Lod will be just a single object, but for BundledMeshes you might want to have multiple geometry parts (e.g. "hull" as root and a "turret" and "motor" as its child objects). Those objects cannot be empty, each one must contain mesh data to export properly! However, the mesh data itself may have no geometry (verts & faces deleted), which is useful for exporting things as invisible but separate logical objects (e.g. the `Engine` ObjectTemplate of the vehicle). +- Each object in the hierarchy should have its corresponding BF2 ObjectTemplate type set (e.g. `Bundle`, `PlayerControlObject` etc). You will find this property in the `Object Properties` tab, it defaults to `SimpleObject`. It can be left empty when an object is intended to be exported as a separate geometry part but not as a separate ObjectTemplate (e.g. an animatable weapon part of the handheld `GenericFirearm`). ## Materials and UVs - Each material that is assigned to any visible mesh must be set up for export. To setup BF2 material go to `Material Properties`, you should see `BF2 Material` panel there. Enable `Is BF2 Material` and choose appropriate settings: `Alpha Mode`, `Shader` and `Technique` (BundledMesh/SkinnedMesh only) as well as desired texture maps to load. @@ -35,10 +35,10 @@ There are two ways of importing BF2 meshes. One is to use `Import -> BF2` menu t ## Collision meshes - Each object may contain collision mesh data. To add it, you need to create an empty child object that is prefixed with `NONVIS__`. This new object should have a maximum of 4 child objects (suffixed with `_COL`) containing collision mesh data, each corresponding to a specific collision type: Projectile = COL0, Vehicle = COL1, Soldier = COL2, AI (navmesh) = COL3. Collision meshes should only be added under object's Lod0 hierchies. -- Each COL can have an arbitrary number of materials assigned, no special material settings are required, the material mapping will be dumped to the `.con` file. +- Each COL can have an arbitrary number of materials assigned, no special material settings are required, object's material index-to-name mapping will be saved inside the `.con` file. ## Tank tracks skinning (BundledMesh) -The term "skinning" is probably an exaggeration in the context of BundledMeshes because there is no weighting involved. Essentially, it comes down to just "moving" some of the vertices from one object (geometry part) to another so that individual vertices that make up a face get split among different parts and those can be affected by in-game physics differently which causes some faces to stretch and deform. This technique is most commonly used for setting up tank tracks by splitting them up and "linking" the pieces to wheel objects. No modelling software allows to do this while keeping the faces intact so to achieve this in Blender create a new [Vertex Group](https://docs.blender.org/manual/en/latest/modeling/meshes/properties/vertex_groups/index.html) named **exactly** the same as the child object that the vertices are supposed to be transferred to and add them to the group. Make sure that a single vertex is assigned to **exactly one** vertex group, or you will get an export error. +The term "skinning" is probably an exaggeration in the context of BundledMeshes because there is no weighting involved. Essentially, it comes down to just "moving" some of the vertices from one object (geometry part) to another so that individual vertices that make up a face get split among different parts. These parts can be affected by in-game physics differently which causes some faces to stretch and deform. This technique is most commonly used for setting up tank tracks by splitting them up and "linking" the pieces to wheel objects. No modelling software allows to do this while keeping the faces intact so to achieve this in Blender create a new [Vertex Group](https://docs.blender.org/manual/en/latest/modeling/meshes/properties/vertex_groups/index.html) named **exactly** the same as the child object that the vertices are supposed to be transferred to and add them to the group. Make sure that a single vertex is assigned to **exactly one** vertex group, or you will get an export error. ## Animated UVs (BundledMesh) To set up animated UVs go to `Edit Mode`, select specific parts (vertices/faces) of your mesh that should use UV animation and assign them to proper sets using `Mesh -> BF2` menu, choosing Left/Right Tracks/Wheels Translation/Rotation. You can also select vertices/faces currently assigned to those sets using `Select -> BF2` menu. Vertices assinged to "wheel rotation" set will additionaly require setting up the center point of UV rotation for each wheel individually. Select all vertices, and position the 2D cursor to the wheel center in the UV Editing view, then select `Mesh -> BF2 -> Set Animated UV Rotation Center`. Repeat the process for all wheels. diff --git a/operators/import_export/ops_mesh.py b/operators/import_export/ops_mesh.py index 5fb3505..4679c51 100644 --- a/operators/import_export/ops_mesh.py +++ b/operators/import_export/ops_mesh.py @@ -61,6 +61,9 @@ def draw(self, context): col.prop(self, "lod") col.enabled = self.only_selected_lod + col = layout.column() + col.prop(self, "merge_materials") + def invoke(self, context, _event): try: # suggest to load only single LOD whe skeleton got imported previoulsy @@ -89,7 +92,7 @@ def execute(self, context): self.__class__.IMPORT_FUNC(context, self.filepath, texture_path=mod_path, - merge_materials=self.merge_materials + merge_materials=self.merge_materials, **kwargs) except Exception as e: self.report({"ERROR"}, traceback.format_exc()) diff --git a/operators/import_export/ops_object_template.py b/operators/import_export/ops_object_template.py index c287277..058c9af 100644 --- a/operators/import_export/ops_object_template.py +++ b/operators/import_export/ops_object_template.py @@ -62,6 +62,12 @@ class IMPORT_OT_bf2_object(bpy.types.Operator, ImportHelper): default=True ) # type: ignore + weld_verts: BoolProperty( + name="Weld Vertices", + description="Welds vertices based on their proximity. Export process splits some of the vertices as their per-face attribute values (normals, tangents, UVs etc) differ", + default=False + ) # type: ignore + skeletons_to_link : CollectionProperty(type=SkeletonsToLinkCollection) # type: ignore instance=None @@ -86,6 +92,12 @@ def draw(self, context): row.operator(IMPORT_OT_bf2_object_skeleton_add.bl_idname, text='', icon='ADD') row.operator(IMPORT_OT_bf2_object_skeleton_remove.bl_idname, text='', icon='REMOVE') + col = layout.column() + col.prop(self, "merge_materials") + + col = layout.column() + col.prop(self, "weld_verts") + def execute(self, context): mod_path = context.preferences.addons[PLUGIN_NAME].preferences.mod_directory @@ -101,6 +113,7 @@ def execute(self, context): import_rig=(self.import_rig_mode, geom_to_ske), texture_path=mod_path, merge_materials=self.merge_materials, + weld_verts=self.weld_verts, reporter=Reporter(self.report)) except ImportException as e: self.report({"ERROR"}, str(e)) diff --git a/operators/ops_material_props.py b/operators/ops_material_props.py index 21e4177..30882fa 100644 --- a/operators/ops_material_props.py +++ b/operators/ops_material_props.py @@ -77,17 +77,19 @@ def draw(self, context): col = self.layout.column() col.prop(material, "is_bf2_material") + enabled = material.is_bf2_material + col = self.layout.column() col.prop(material, "bf2_shader") - col.enabled = material.is_bf2_material + col.enabled = enabled col = self.layout.column() col.prop(material, "bf2_technique") - col.enabled = material.is_bf2_material and material.bf2_shader != 'STATICMESH' + col.enabled = enabled and material.bf2_shader != 'STATICMESH' col = self.layout.column() col.prop(material, "is_bf2_vegitation") - col.enabled = material.is_bf2_material and material.bf2_shader == 'STATICMESH' + col.enabled = enabled and material.bf2_shader == 'STATICMESH' row = self.layout.row(align=True) row.label(text="Alpha Mode") @@ -101,7 +103,7 @@ def draw(self, context): elif identifier == 'ALPHA_BLEND': item_layout.enabled = material.bf2_shader == 'BUNDLEDMESH' - row.enabled = material.is_bf2_material + row.enabled = enabled col = self.layout.column() if material.bf2_shader in ('BUNDLEDMESH', 'SKINNEDMESH'): @@ -114,26 +116,27 @@ def draw(self, context): col = self.layout.column() col.prop(material, "texture_slot_0", text="Base") + col.enabled = enabled col = self.layout.column() col.prop(material, "texture_slot_1", text="Detail") - col.enabled = is_staticmesh_map_allowed(material, "Detail") + col.enabled = enabled and is_staticmesh_map_allowed(material, "Detail") col = self.layout.column() col.prop(material, "texture_slot_2", text="Dirt") - col.enabled = not is_vegitation and is_staticmesh_map_allowed(material, "Dirt") + col.enabled = enabled and not is_vegitation and is_staticmesh_map_allowed(material, "Dirt") col = self.layout.column() col.prop(material, "texture_slot_3", text="Crack") - col.enabled = not is_vegitation and is_staticmesh_map_allowed(material, "Crack") + col.enabled = enabled and not is_vegitation and is_staticmesh_map_allowed(material, "Crack") col = self.layout.column() col.prop(material, "texture_slot_4", text="Detail Normal") - col.enabled = is_staticmesh_map_allowed(material, "NDetail") + col.enabled = enabled and is_staticmesh_map_allowed(material, "NDetail") col = self.layout.column() col.prop(material, "texture_slot_5", text="Crack Normal") - col.enabled = not is_vegitation and is_staticmesh_map_allowed(material, "NCrack") + col.enabled = enabled and not is_vegitation and is_staticmesh_map_allowed(material, "NCrack") mod_path = context.preferences.addons[PLUGIN_NAME].preferences.mod_directory if not mod_path: