From 870ee552cd419aa97eb746df2a68515cf0bf3a7b Mon Sep 17 00:00:00 2001 From: MCDeedle Date: Sat, 24 May 2025 22:17:07 +1000 Subject: [PATCH 1/6] #36. Added a bounding_box class to handle repeated bounding box operations. --- bms_blender_plugin/common/bounding_box.py | 53 +++++++++++++++++++ .../exporter/export_bounding_boxes.py | 18 +++++++ 2 files changed, 71 insertions(+) create mode 100644 bms_blender_plugin/common/bounding_box.py create mode 100644 bms_blender_plugin/exporter/export_bounding_boxes.py diff --git a/bms_blender_plugin/common/bounding_box.py b/bms_blender_plugin/common/bounding_box.py new file mode 100644 index 0000000..c698ce3 --- /dev/null +++ b/bms_blender_plugin/common/bounding_box.py @@ -0,0 +1,53 @@ +from enum import Enum +from mathutils import Vector +from bms_blender_plugin.common.coordinates import vector_to_bms_coords + +class BoundingBox: + """Represents a bounding box. Each instance is a single bounding box. Calculates core attributes on init.""" + + def __init__(self,obj): + self.blender_coords = self.get_blender_vertices(obj) + self.bms_coords = self.transform_blender_vertices(self.blender_coords) + self.min_bms_vertex = self.get_min_vertex(self.bms_coords) + self.max_bms_vertex = self.get_max_vertex(self.bms_coords) + + def get_blender_vertices(self, obj): + """Returns the vertices of the object in the world coordinate system""" + vertices = [obj.matrix_world.inverted() @ vertex.co for vertex in obj.data.vertices] + return vertices + + def transform_blender_vertices(self, blender_vertices): + """Transforms the vertices of the object to the BMS coordinate system""" + transformed_vertices = [vector_to_bms_coords(vertex) for vertex in blender_vertices] + return transformed_vertices + + def get_min_vertex(self, bms_coords): + """ Assuming a simple cube primitive, the min vertex will always be the one with the smallest sum of coordinates.""" + min_vector = bms_coords[0] + for vector in bms_coords[1:]: + if sum(min_vector) > sum(vector): + min_vector = vector + return min_vector + + def get_max_vertex(self, bms_coords): + """ Assuming a simple cube primitive, the max vertex will always be the one with the largest sum of coordinates.""" + max_vector = bms_coords[0] + for vector in bms_coords[1:]: + if sum(max_vector) < sum(vector): + max_vector = vector + return max_vector + + def bbox_to_txtpb_format(self): + """Returns a string representation of the bounding box in the correct format for .txtpb files.""" + return (f'bounding_box {{\n' + f' min {{\n' + f' x: {round(self.min_bms_vertex.x, 1)}\n' + f' y: {round(self.min_bms_vertex.y, 1)}\n' + f' z: {round(self.min_bms_vertex.z, 1)}\n' + f' }}\n' + f' max {{\n' + f' x: {round(self.max_bms_vertex.x, 1)}\n' + f' y: {round(self.max_bms_vertex.y, 1)}\n' + f' z: {round(self.max_bms_vertex.z, 1)}\n' + f' }}\n' + f'}}\n') diff --git a/bms_blender_plugin/exporter/export_bounding_boxes.py b/bms_blender_plugin/exporter/export_bounding_boxes.py new file mode 100644 index 0000000..6b49597 --- /dev/null +++ b/bms_blender_plugin/exporter/export_bounding_boxes.py @@ -0,0 +1,18 @@ +import operator +import os + +def export_bounding_boxes(bounding_boxes, file_directory): + """ + Exports bounding box data as required for >.txtpb file. + Output will still need to be written to >.txtpb by users, but this at least gets them the + information in the correct format. + """ + bounding_boxes_filepath = os.path.join(file_directory, "bounding_boxes.txtpb") + + with open(bounding_boxes_filepath, "w") as bounding_box_file: + bounding_box_file.write("# The following data needs to be manually added to the required .txtpb file\n") + + for bounding_box in bounding_boxes: + bounding_box_file.write(bounding_box.bbox_to_txtpb_format()) + + print(f"bounding_boxes.txtpb: {bounding_boxes_filepath}") From b26ebfc0a8bb6205b87926a7e9f90228cd2c48da Mon Sep 17 00:00:00 2001 From: MCDeedle Date: Sat, 24 May 2025 22:18:28 +1000 Subject: [PATCH 2/6] #36. Refactored hotspot transformation as this appears to be common to the required bbox transformation. --- bms_blender_plugin/common/coordinates.py | 15 +++++++++++---- bms_blender_plugin/common/hotspot.py | 9 ++------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/bms_blender_plugin/common/coordinates.py b/bms_blender_plugin/common/coordinates.py index 4231772..2dc5120 100644 --- a/bms_blender_plugin/common/coordinates.py +++ b/bms_blender_plugin/common/coordinates.py @@ -3,10 +3,13 @@ # Define the matrices here BMS_SPACE_MATRIX = Matrix(((1, 0, 0, 0), (0, 0, 1, 0), (0, 1, 0, 0), (0, 0, 0, 1))) BMS_SPACE_MATRIX_INV = BMS_SPACE_MATRIX.inverted_safe() -""" Hotpots use an x=rearwards, y=left, z=up coordinate system, whereas blender uses an x=right, y=forward, z=up +""" +Two different vector object types (bbox and hotspots) use an x=rearwards, y=left, z=up coordinate system, whereas blender uses an x=right, y=forward, z=up coordinate system. The following transformation matrix will output a vector [-blender_y, -blender_x, blender_z] which -from our blender vector definitions is [-forward, -right, up] = [rearwards, left, up]""" -BMS_HOTSPOT_MATRIX = Matrix(((0, -1, 0), (-1, 0, 0), (0, 0, 1))) +from our blender vector definitions is [-forward, -right, up] = [rearwards, left, up] +""" +BMS_VECTOR_TRANSFORM_MATRIX = Matrix(((0, -1, 0), (-1, 0, 0), (0, 0, 1))) + def to_bms_coords(data, space_mat=BMS_SPACE_MATRIX, space_mat_inv=BMS_SPACE_MATRIX_INV): """Transforms from Blender space to BMS space (-Z forward, Y up).""" @@ -32,4 +35,8 @@ def to_bms_coords(data, space_mat=BMS_SPACE_MATRIX, space_mat_inv=BMS_SPACE_MATR return data[0], 1 - data[1] # unknown else: - raise NotImplementedError("Unknown data type encountered.") \ No newline at end of file + raise NotImplementedError("Unknown data type encountered.") + +def vector_to_bms_coords(data): + # Transforms specific vectors correctly. Use cases so far are bounding boxes and hotspots. + return data @ BMS_VECTOR_TRANSFORM_MATRIX \ No newline at end of file diff --git a/bms_blender_plugin/common/hotspot.py b/bms_blender_plugin/common/hotspot.py index 0a07fc8..a9bbd9e 100644 --- a/bms_blender_plugin/common/hotspot.py +++ b/bms_blender_plugin/common/hotspot.py @@ -1,6 +1,6 @@ from enum import Enum from mathutils import Vector -from bms_blender_plugin.common.coordinates import BMS_HOTSPOT_MATRIX +from bms_blender_plugin.common.coordinates import vector_to_bms_coords class MouseButton(int, Enum): @@ -43,12 +43,7 @@ def __init__( self.mouse_button = mouse_button self.button_type = button_type self.blender_coords = Vector((self.blend_x, self.blend_y, self.blend_z)) - self.bms_coords = self.to_bms_hotspot_coords(self.blender_coords) - - def to_bms_hotspot_coords(self, data, transformation=BMS_HOTSPOT_MATRIX): - """Transforms applied elsewhere do not seem to be compatible for hotspots (maybe other vectors as well?) - Without robust testing the safest way to fix this is within the hotspot class.""" - return data @ transformation + self.bms_coords = vector_to_bms_coords(self.blender_coords) def __str__(self): """"Format according to 3dButtons.dat. From 1ec32d7a61c2bdab2537a42dc777f842fc8e99b4 Mon Sep 17 00:00:00 2001 From: MCDeedle Date: Sat, 24 May 2025 22:19:48 +1000 Subject: [PATCH 3/6] #36. Refactored bounding box implementation in bml_output and parent.dat. Added some inline explaination of Dimension numbers to parent.dat file output. --- bms_blender_plugin/exporter/bml_output.py | 56 +++++++++++-------- .../exporter/export_parent_dat.py | 14 ++--- 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/bms_blender_plugin/exporter/bml_output.py b/bms_blender_plugin/exporter/bml_output.py index 00bf563..54721ee 100644 --- a/bms_blender_plugin/exporter/bml_output.py +++ b/bms_blender_plugin/exporter/bml_output.py @@ -10,12 +10,14 @@ get_bml_type, ) from bms_blender_plugin.exporter.export_hotspots import export_hotspots +from bms_blender_plugin.common.bounding_box import BoundingBox from bms_blender_plugin.exporter.export_lods import ( export_lods, ) from bms_blender_plugin.exporter.export_materials import ( export_materials, ) from bms_blender_plugin.exporter.export_parent_dat import get_slots, export_parent_dat +from bms_blender_plugin.exporter.export_bounding_boxes import export_bounding_boxes def export_bml(context, lods, file_directory, file_prefix, export_settings: ExportSettings): @@ -42,28 +44,28 @@ def export_bml(context, lods, file_directory, file_prefix, export_settings: Expo print(f"unit scaling factor: {scale_factor}") - bounding_box_1_coords = Vector((0, 0, 0)) - bounding_box_2_coords = Vector((0, 0, 0)) - - # try to find the bounding box - we take the first one we get - for obj in context.scene.objects: - if get_bml_type(obj) == BlenderNodeType.BBOX: - max_x, min_x, max_y, min_y, max_z, min_z = (0,)*6 - # find the min and max coordinates of the 8 vertices - for corner in obj.data.vertices: - max_x = max(corner.co.x, max_x) - min_x = min(corner.co.x, min_x) - max_y = max(corner.co.y, max_y) - min_y = min(corner.co.y, min_y) - max_z = max(corner.co.z, max_z) - min_z = min(corner.co.z, min_z) - - bounding_box_1_coords = Vector((max_x, max_y, max_z)) - bounding_box_2_coords = Vector((min_x, min_y, min_z)) - - bounding_box_1_coords *= scale_factor - bounding_box_2_coords *= scale_factor - break + """ + Bounding Box handling. + A complex model can have more than one bounding box, for example an Aircraft Model usually has fuselage, + wing, rudder and radar hitboxes defined by separate bounding boxes. + As far as I know we don't care about identifying each particular bounding box with an identity key. + Here we will iterate over all bounding boxes and construct the min and max vertex positions + We will then take the first BBox and extract it's coordinates for the Parent.dat file. + The remaining hitboxes can be exported to a separate file, but we need to do further work to integrate these into the + AC.txtpb files. + """ + + # Gather all bounding boxes into an array of bounding_box objects. + BBox_Array = [BoundingBox(obj) for obj in context.scene.objects if get_bml_type(obj) == BlenderNodeType.BBOX] + """ + Get the first bounding box min and max coordinates. A bit arbitrary at this point, + but in instances of a single bbox, no harm and I suspect that these will be in named order. + Scale appropriately + """ + bounding_box_1_min_coords = BBox_Array[0].min_bms_vertex + bounding_box_1_max_coords = BBox_Array[0].max_bms_vertex + bounding_box_1_min_coords *= scale_factor + bounding_box_1_max_coords *= scale_factor # we have to call export_lods even if we don't want to export models # because it will return materials and hotspots @@ -94,8 +96,8 @@ def export_bml(context, lods, file_directory, file_prefix, export_settings: Expo context, file_directory, file_prefix, - bounding_box_1_coords, - bounding_box_2_coords, + bounding_box_1_min_coords, + bounding_box_1_max_coords, scale_factor, number_of_texture_sets, get_slots(context.scene), @@ -105,6 +107,11 @@ def export_bml(context, lods, file_directory, file_prefix, export_settings: Expo if export_settings.export_hotspots: export_hotspots(all_hotspots, file_directory) + # If there is more than one bounding box defined, output all to a file. + if len(BBox_Array) > 1: + export_bounding_boxes(BBox_Array, file_directory) + + elapsed = datetime.datetime.now() - start_time elapsed_minutes = divmod(elapsed.total_seconds(), 60) @@ -113,5 +120,6 @@ def export_bml(context, lods, file_directory, file_prefix, export_settings: Expo f"{math.trunc(elapsed_minutes[0])}m {round(elapsed_minutes[1],2)}s" ) + print(success_message) return success_message, all_exported_bmls diff --git a/bms_blender_plugin/exporter/export_parent_dat.py b/bms_blender_plugin/exporter/export_parent_dat.py index 39b3a93..c793ddd 100644 --- a/bms_blender_plugin/exporter/export_parent_dat.py +++ b/bms_blender_plugin/exporter/export_parent_dat.py @@ -59,8 +59,8 @@ def export_parent_dat( context, file_directory: str, file_prefix: str, - bounding_box_1_coords, - bounding_box_2_coords, + bounding_box_1_min_coords, + bounding_box_1_max_coords, scale_factor, number_of_texture_sets, slot_list, @@ -69,9 +69,6 @@ def export_parent_dat( """Exports the Parent.dat as a file""" parent_dat_filepath = os.path.join(file_directory, "Parent.dat") - bounding_box_1_coords = to_bms_coords(bounding_box_1_coords) - bounding_box_2_coords = to_bms_coords(bounding_box_2_coords) - # note: the "Switches" and "Dofs" config in the Parent.dat don't actually amount to the number of DOFs/switches but # to the highest dof_number in the model highest_switch_number, highest_dof_number = get_highest_switch_and_dof_number( @@ -86,10 +83,11 @@ def export_parent_dat( with open(parent_dat_filepath, "w") as parent_dat_file: str_output = ( + f"# Dimensions = bounding_sphere_radius bbox_min_x bbox_max_x bbox_min_y bbox_max_y bbox_min_z bbox_max_z\n" f"Dimensions = {round(bounding_sphere_radius, 6)} " - f"{round(bounding_box_1_coords.x, 6):.6f} {round(bounding_box_2_coords.x, 6):.6f} " - f"{round(bounding_box_1_coords.y, 6):.6f} {round(bounding_box_2_coords.y, 6):.6f} " - f"{round(bounding_box_1_coords.z, 6):.6f} {round(bounding_box_2_coords.z, 6):.6f}\n" + f"{round(bounding_box_1_min_coords.x, 6):.6f} {round(bounding_box_1_max_coords.x, 6):.6f} " + f"{round(bounding_box_1_min_coords.y, 6):.6f} {round(bounding_box_1_max_coords.y, 6):.6f} " + f"{round(bounding_box_1_min_coords.z, 6):.6f} {round(bounding_box_1_max_coords.z, 6):.6f}\n" f"TextureSets = {number_of_texture_sets}\n" f"Switches = {highest_switch_number}\n" f"Dofs = {highest_dof_number}\n" From 7759295af06b7d7d6aa92a148b28972ecdee1fb0 Mon Sep 17 00:00:00 2001 From: MCDeedle Date: Sat, 24 May 2025 22:37:32 +1000 Subject: [PATCH 4/6] #36. Fixed comment indicator type --- bms_blender_plugin/exporter/export_parent_dat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bms_blender_plugin/exporter/export_parent_dat.py b/bms_blender_plugin/exporter/export_parent_dat.py index c793ddd..323f113 100644 --- a/bms_blender_plugin/exporter/export_parent_dat.py +++ b/bms_blender_plugin/exporter/export_parent_dat.py @@ -83,7 +83,7 @@ def export_parent_dat( with open(parent_dat_filepath, "w") as parent_dat_file: str_output = ( - f"# Dimensions = bounding_sphere_radius bbox_min_x bbox_max_x bbox_min_y bbox_max_y bbox_min_z bbox_max_z\n" + f"// Dimensions = bounding_sphere_radius bbox_min_x bbox_max_x bbox_min_y bbox_max_y bbox_min_z bbox_max_z\n" f"Dimensions = {round(bounding_sphere_radius, 6)} " f"{round(bounding_box_1_min_coords.x, 6):.6f} {round(bounding_box_1_max_coords.x, 6):.6f} " f"{round(bounding_box_1_min_coords.y, 6):.6f} {round(bounding_box_1_max_coords.y, 6):.6f} " From 8443e1e8a8736fee58c9850410d93016e08e52a5 Mon Sep 17 00:00:00 2001 From: MCDeedle Date: Sat, 24 May 2025 22:59:56 +1000 Subject: [PATCH 5/6] #36. Updated Bounding Box Manual entry to reflect changes. --- docs/Manual/MANUAL.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/Manual/MANUAL.md b/docs/Manual/MANUAL.md index 9e3e410..f786336 100644 --- a/docs/Manual/MANUAL.md +++ b/docs/Manual/MANUAL.md @@ -373,16 +373,16 @@ The data definition of racks is located in the ```BmsRack.dat``` (not part of th Falcon uses bounding box dimensions in order to set the objects hitbox. In order to create a bounding box, Go to the "Add" panel, then "Falcon BMS", then "Bounding Box" (alternatively, you can use the BMS Tools Panel). A bounding box will be added to the scene and automatically fitted to the geometry of the currently active collection. The bounding box can be moved and scaled. -The bounding box is only required once per scene. +Multiple bounding boxes can be added to each scene, but only one is required. ![Bounding Box](Bounding_Box.png) > ⚠️ The bounding box rotation is constrained since Falcon only implements bounding boxes aligned with X/Y/Z Axis. *Do not* force a rotation on the bounding box. When applying a bounding box to your model, think about how the in-game hitbox should be. i.e. for an aircraft hangar the bounding box should only be the roof, since planes have the option to taxi into them. -The bounding box will be exported into the ```Parent.dat``` file, and will populate the "dimensions" line. -> ℹ️ BMS aircraft can have multiple additional bounding boxes. Those are defined in the \.dat (ACDATA). Their export is not yet implemented. +The first bounding box will be exported into the ```Parent.dat``` file, and will populate the "dimensions" line. If more than one bounding box exists in the model, bounding box data will also be exported to bounding_boxes.txtpb +This additional data will still need to be integrated into the \Data\Sim\Acdata\.txtpb file by the user, but should be formatted by the plugin to make this process easier. From fe4e82b824ec436dff420f912b845466f8db655b Mon Sep 17 00:00:00 2001 From: MCDeedle Date: Sun, 25 May 2025 08:08:06 +1000 Subject: [PATCH 6/6] #36. Removed unused imports. --- bms_blender_plugin/common/bounding_box.py | 2 -- bms_blender_plugin/exporter/bml_output.py | 1 - bms_blender_plugin/exporter/export_bounding_boxes.py | 1 - 3 files changed, 4 deletions(-) diff --git a/bms_blender_plugin/common/bounding_box.py b/bms_blender_plugin/common/bounding_box.py index c698ce3..995a6c3 100644 --- a/bms_blender_plugin/common/bounding_box.py +++ b/bms_blender_plugin/common/bounding_box.py @@ -1,5 +1,3 @@ -from enum import Enum -from mathutils import Vector from bms_blender_plugin.common.coordinates import vector_to_bms_coords class BoundingBox: diff --git a/bms_blender_plugin/exporter/bml_output.py b/bms_blender_plugin/exporter/bml_output.py index 54721ee..6009127 100644 --- a/bms_blender_plugin/exporter/bml_output.py +++ b/bms_blender_plugin/exporter/bml_output.py @@ -2,7 +2,6 @@ import math import bpy -from mathutils import Vector from bms_blender_plugin.common.blender_types import BlenderNodeType, LodItem from bms_blender_plugin.common.export_settings import ExportSettings diff --git a/bms_blender_plugin/exporter/export_bounding_boxes.py b/bms_blender_plugin/exporter/export_bounding_boxes.py index 6b49597..e7a7e07 100644 --- a/bms_blender_plugin/exporter/export_bounding_boxes.py +++ b/bms_blender_plugin/exporter/export_bounding_boxes.py @@ -1,4 +1,3 @@ -import operator import os def export_bounding_boxes(bounding_boxes, file_directory):