From cb893b770a8605635b5bf70d5db48a482a794664 Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Fri, 3 Jan 2025 09:36:32 +0800 Subject: [PATCH] feat: add light object support in importing virtools file. - fix all `def poll(self)` to `def poll(cls)` to let it fit class method name convention. - fix wrong progress counter when importing virtools file. - add light support when importing virtools file. - add corresponding conflict strategy and resolver for light. --- bbp_ng/OP_EXPORT_bmfile.py | 2 +- bbp_ng/OP_EXPORT_virtools.py | 2 +- bbp_ng/OP_IMPORT_bmfile.py | 2 +- bbp_ng/OP_IMPORT_virtools.py | 83 +++++++++++++++++++++++++++++------ bbp_ng/OP_UV_rail_uv.py | 2 +- bbp_ng/PROP_virtools_group.py | 6 +-- bbp_ng/UTIL_functions.py | 10 +++-- bbp_ng/UTIL_ioport_shared.py | 50 +++++++++++++++++++-- 8 files changed, 130 insertions(+), 27 deletions(-) diff --git a/bbp_ng/OP_EXPORT_bmfile.py b/bbp_ng/OP_EXPORT_bmfile.py index e3d1f03..3d0413d 100644 --- a/bbp_ng/OP_EXPORT_bmfile.py +++ b/bbp_ng/OP_EXPORT_bmfile.py @@ -8,7 +8,7 @@ class BBP_OT_export_bmfile(bpy.types.Operator, UTIL_file_browser.ExportBmxFile, bl_options = {'PRESET'} @classmethod - def poll(self, context): + def poll(cls, context): return PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder() def execute(self, context): diff --git a/bbp_ng/OP_EXPORT_virtools.py b/bbp_ng/OP_EXPORT_virtools.py index b56d1cf..04cd9db 100644 --- a/bbp_ng/OP_EXPORT_virtools.py +++ b/bbp_ng/OP_EXPORT_virtools.py @@ -13,7 +13,7 @@ class BBP_OT_export_virtools(bpy.types.Operator, UTIL_file_browser.ExportVirtool bl_options = {'PRESET'} @classmethod - def poll(self, context): + def poll(cls, context): return ( PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder() and bmap.is_bmap_available()) diff --git a/bbp_ng/OP_IMPORT_bmfile.py b/bbp_ng/OP_IMPORT_bmfile.py index b658fc9..8c579ae 100644 --- a/bbp_ng/OP_IMPORT_bmfile.py +++ b/bbp_ng/OP_IMPORT_bmfile.py @@ -8,7 +8,7 @@ class BBP_OT_import_bmfile(bpy.types.Operator, UTIL_file_browser.ImportBmxFile, bl_options = {'PRESET', 'UNDO'} @classmethod - def poll(self, context): + def poll(cls, context): return PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder() def execute(self, context): diff --git a/bbp_ng/OP_IMPORT_virtools.py b/bbp_ng/OP_IMPORT_virtools.py index 0a56671..06f7b7d 100644 --- a/bbp_ng/OP_IMPORT_virtools.py +++ b/bbp_ng/OP_IMPORT_virtools.py @@ -3,7 +3,7 @@ import tempfile, os, typing from . import PROP_preferences, UTIL_ioport_shared from . import UTIL_virtools_types, UTIL_functions, UTIL_file_browser, UTIL_blender_mesh, UTIL_ballance_texture, UTIL_naming_convension -from . import PROP_virtools_group, PROP_virtools_material, PROP_virtools_mesh, PROP_virtools_texture, PROP_ballance_map_info +from . import PROP_virtools_group, PROP_virtools_material, PROP_virtools_mesh, PROP_virtools_texture, PROP_virtools_light, PROP_ballance_map_info from .PyBMap import bmap_wrapper as bmap class BBP_OT_import_virtools(bpy.types.Operator, UTIL_file_browser.ImportVirtoolsFile, UTIL_ioport_shared.ImportParams, UTIL_ioport_shared.VirtoolsParams, UTIL_ioport_shared.BallanceParams): @@ -13,7 +13,7 @@ class BBP_OT_import_virtools(bpy.types.Operator, UTIL_file_browser.ImportVirtool bl_options = {'PRESET', 'UNDO'} @classmethod - def poll(self, context): + def poll(cls, context): return ( PROP_preferences.get_raw_preferences().has_valid_blc_tex_folder() and bmap.is_bmap_available()) @@ -59,6 +59,8 @@ def _import_virtools(file_name_: str, encodings_: tuple[str], resolver: UTIL_iop # import 3dobjects obj3d_cret_map: dict[bmap.BM3dObject, bpy.types.Object] = _import_virtools_3dobjects( reader, progress, resolver, mesh_cret_map) + # import light + _import_virtools_lights(reader, progress, resolver) # import groups _import_virtools_groups(reader, progress, obj3d_cret_map) @@ -200,7 +202,7 @@ def _import_virtools_meshes( ) -> dict[bmap.BMMesh, bpy.types.Mesh]: # create map and prepare progress mesh_cret_map: dict[bmap.BMMesh, bpy.types.Mesh] = {} - progress.enter_substeps(reader.get_material_count(), "Loading Meshes") + progress.enter_substeps(reader.get_mesh_count(), "Loading Meshes") for vtmesh in reader.get_meshs(): # create mesh @@ -296,11 +298,7 @@ def _import_virtools_3dobjects( ) -> dict[bmap.BM3dObject, bpy.types.Object]: # create map and prepare progress obj3d_cret_map: dict[bmap.BM3dObject, bpy.types.Object] = {} - progress.enter_substeps(reader.get_material_count(), "Loading 3dObjects") - - # get some essential blender data - blender_view_layer = bpy.context.view_layer - blender_collection = blender_view_layer.active_layer_collection.collection + progress.enter_substeps(reader.get_3dobject_count(), "Loading 3dObjects") for vt3dobj in reader.get_3dobjects(): # get virtools binding mesh data first @@ -314,8 +312,8 @@ def _import_virtools_3dobjects( # setup if necessary if init_obj3d: - # link to collection - blender_collection.objects.link(obj3d) + # add into scene + UTIL_functions.add_into_scene(obj3d) # set world matrix vtmat: UTIL_virtools_types.VxMatrix = vt3dobj.get_world_matrix() @@ -338,6 +336,61 @@ def _import_virtools_3dobjects( progress.leave_substeps() return obj3d_cret_map +def _import_virtools_lights( + reader: bmap.BMFileReader, + progress: ProgressReport, + resolver: UTIL_ioport_shared.ConflictResolver + ) -> None: + # prepare progress + progress.enter_substeps(reader.get_target_light_count(), "Loading Lights") + + # please note light is slightly different between virtools and blender. + # in virtools, light is the sub class of 3d entity. + # it means that virtools use class inheritance to implement light. + # however, in blender, light is the data property of object. + # comparing with normal mesh object, it just replace the data property of object to a light. + # so in blender, light is implemented as a data struct attached to object. + # thus we can reuse light data for multiple objects but virtools can not. + # in virtools, every light are individual objects. + for vtlight in reader.get_target_lights(): + # create light data block and 3d object together + (light_3dobj, light, init_light) = resolver.create_light( + UTIL_virtools_types.virtools_name_regulator(vtlight.get_name()) + ) + + if init_light: + # setup light data block + rawlight: PROP_virtools_light.RawVirtoolsLight = PROP_virtools_light.RawVirtoolsLight() + rawlight.mType = vtlight.get_type() + rawlight.mColor = vtlight.get_color() + + rawlight.mConstantAttenuation = vtlight.get_constant_attenuation() + rawlight.mLinearAttenuation = vtlight.get_linear_attenuation() + rawlight.mQuadraticAttenuation = vtlight.get_quadratic_attenuation() + + rawlight.mRange = vtlight.get_range() + + rawlight.mHotSpot = vtlight.get_hot_spot() + rawlight.mFalloff = vtlight.get_falloff() + rawlight.mFalloffShape = vtlight.get_falloff_shape() + + PROP_virtools_light.set_raw_virtools_light(light, rawlight) + PROP_virtools_light.apply_to_blender_light(light) + + # setup light associated 3d object + # add into scene + UTIL_functions.add_into_scene(light_3dobj) + # set world matrix + # TODO: fix light direction + vtmat: UTIL_virtools_types.VxMatrix = vtlight.get_world_matrix() + UTIL_virtools_types.vxmatrix_conv_co(vtmat) + light_3dobj.matrix_world = UTIL_virtools_types.vxmatrix_to_blender(vtmat) + # set visibility + light_3dobj.hide_set(not vtlight.get_visibility()) + + # leave progress + progress.leave_substeps() + def _import_virtools_groups( reader: bmap.BMFileReader, progress: ProgressReport, @@ -350,7 +403,7 @@ def _import_virtools_groups( sector_count: int = 1 # prepare progress - progress.enter_substeps(reader.get_material_count(), "Loading Groups") + progress.enter_substeps(reader.get_group_count(), "Loading Groups") for vtgroup in reader.get_groups(): # if this group do not have name, skip it @@ -365,7 +418,7 @@ def _import_virtools_groups( # creating map for item in vtgroup.get_objects(): # get or create set - objgroups: set[str] = reverse_map.get(item, None) + objgroups: set[str] | None = reverse_map.get(item, None) if objgroups is None: objgroups = set() reverse_map[item] = objgroups @@ -385,7 +438,7 @@ def _import_virtools_groups( progress.leave_substeps() # now we can assign 3dobject group data by reverse map - progress.enter_substeps(reader.get_material_count(), "Applying Groups") + progress.enter_substeps(len(reverse_map), "Applying Groups") for mapk, mapv in reverse_map.items(): # check object assoc_obj = obj3d_cret_map.get(mapk, None) @@ -396,6 +449,10 @@ def _import_virtools_groups( gpoper.clear_groups() gpoper.add_groups(mapv) + # step + progress.step() + + # leave progress progress.leave_substeps() diff --git a/bbp_ng/OP_UV_rail_uv.py b/bbp_ng/OP_UV_rail_uv.py index 809cd92..b5eac56 100644 --- a/bbp_ng/OP_UV_rail_uv.py +++ b/bbp_ng/OP_UV_rail_uv.py @@ -10,7 +10,7 @@ class BBP_OT_rail_uv(bpy.types.Operator): bl_options = {'UNDO'} @classmethod - def poll(self, context): + def poll(cls, context): return _check_rail_target() def invoke(self, context, event): diff --git a/bbp_ng/PROP_virtools_group.py b/bbp_ng/PROP_virtools_group.py index e782abf..dbc3e74 100644 --- a/bbp_ng/PROP_virtools_group.py +++ b/bbp_ng/PROP_virtools_group.py @@ -297,7 +297,7 @@ class BBP_OT_add_virtools_group(bpy.types.Operator, SharedGroupNameInputProperti bl_options = {'UNDO'} @classmethod - def poll(self, context: bpy.types.Context): + def poll(cls, context: bpy.types.Context): return context.object is not None def invoke(self, context, event): @@ -324,7 +324,7 @@ class BBP_OT_rm_virtools_group(bpy.types.Operator): # Then pass it to helper. @classmethod - def poll(self, context: bpy.types.Context): + def poll(cls, context: bpy.types.Context): if context.object is None: return False @@ -351,7 +351,7 @@ class BBP_OT_clear_virtools_groups(bpy.types.Operator): bl_options = {'UNDO'} @classmethod - def poll(self, context: bpy.types.Context): + def poll(cls, context: bpy.types.Context): return context.object is not None def invoke(self, context, event): diff --git a/bbp_ng/UTIL_functions.py b/bbp_ng/UTIL_functions.py index 0dbcc57..cac581c 100644 --- a/bbp_ng/UTIL_functions.py +++ b/bbp_ng/UTIL_functions.py @@ -52,6 +52,11 @@ def draw(self, context: bpy.types.Context): bpy.context.window_manager.popup_menu(draw, title = title, icon = icon) +def add_into_scene(obj: bpy.types.Object): + view_layer = bpy.context.view_layer + collection = view_layer.active_layer_collection.collection + collection.objects.link(obj) + def move_to_cursor(obj: bpy.types.Object): # use obj.matrix_world to move, not obj.location because this bug: # https://blender.stackexchange.com/questions/27667/incorrect-matrix-world-after-transformation @@ -62,10 +67,7 @@ def move_to_cursor(obj: bpy.types.Object): obj.matrix_world = obj.matrix_world @ mathutils.Matrix.Translation(bpy.context.scene.cursor.location - obj.location) def add_into_scene_and_move_to_cursor(obj: bpy.types.Object): - view_layer = bpy.context.view_layer - collection = view_layer.active_layer_collection.collection - collection.objects.link(obj) - + add_into_scene(obj) move_to_cursor(obj) def select_certain_objects(objs: tuple[bpy.types.Object, ...]) -> None: diff --git a/bbp_ng/UTIL_ioport_shared.py b/bbp_ng/UTIL_ioport_shared.py index 7bc31ae..1b6cae1 100644 --- a/bbp_ng/UTIL_ioport_shared.py +++ b/bbp_ng/UTIL_ioport_shared.py @@ -67,20 +67,31 @@ class ConflictResolver(): """ __mObjectStrategy: ConflictStrategy + __mLightStrategy: ConflictStrategy __mMeshStrategy: ConflictStrategy __mMaterialStrategy: ConflictStrategy __mTextureStrategy: ConflictStrategy - def __init__(self, obj_strategy: ConflictStrategy, mesh_strategy: ConflictStrategy, mtl_strategy: ConflictStrategy, tex_strategy: ConflictStrategy): + def __init__(self, + obj_strategy: ConflictStrategy, + light_strategy: ConflictStrategy, + mesh_strategy: ConflictStrategy, + mtl_strategy: ConflictStrategy, + tex_strategy: ConflictStrategy): self.__mObjectStrategy = obj_strategy + self.__mLightStrategy = light_strategy self.__mMeshStrategy = mesh_strategy self.__mMaterialStrategy = mtl_strategy self.__mTextureStrategy = tex_strategy - def create_object(self, name: str, data: bpy.types.Mesh) -> tuple[bpy.types.Object, bool]: + def create_object(self, name: str, data: bpy.types.Mesh | None) -> tuple[bpy.types.Object, bool]: """ Create object according to conflict strategy. - `data` will only be applied when creating new object (no existing instance or strategy order rename) + `data` will only be applied when creating new object (no existing instance or strategy order rename). + + Please note this function is only used to create mesh 3d object. + If you want to create light object, please use other functions provided by this class. + The 3d object and data block of light is created together. """ if self.__mObjectStrategy == ConflictStrategy.Current: old: bpy.types.Object | None = bpy.data.objects.get(name, None) @@ -88,6 +99,26 @@ def create_object(self, name: str, data: bpy.types.Mesh) -> tuple[bpy.types.Obje return (old, False) return (bpy.data.objects.new(name, data), True) + def create_light(self, name: str) -> tuple[bpy.types.Object, bpy.types.Light, bool]: + """ + Create light data block and associated 3d object. + + If conflict strategy is "Current", we try fetch 3d object with given name first, + then check whether it is light. + If no given name object or this object is not light, we create a new one, + otherwise return old one. + """ + if self.__mLightStrategy == ConflictStrategy.Current: + old_obj: bpy.types.Object | None = bpy.data.objects.get(name, None) + if old_obj is not None and old_obj.type == 'LIGHT': + return (old_obj, typing.cast(bpy.types.Light, old_obj.data), False) + # create new object. + # if object or light name is conflict, rename it directly without considering conflict strategy. + # create light with default point light type + new_light: bpy.types.Light = bpy.data.lights.new(name, 'POINT') + new_obj: bpy.types.Object = bpy.data.objects.new(name, new_light) + return (new_obj, new_light, True) + def create_mesh(self, name: str) -> tuple[bpy.types.Mesh, bool]: if self.__mMeshStrategy == ConflictStrategy.Current: old: bpy.types.Mesh | None = bpy.data.meshes.get(name, None) @@ -144,6 +175,13 @@ class ImportParams(): default = _g_EnumHelper_ConflictStrategy.to_selection(ConflictStrategy.Rename), ) # type: ignore + light_conflict_strategy: bpy.props.EnumProperty( + name = "Light Name Conflict", + items = _g_EnumHelper_ConflictStrategy.generate_items(), + description = "Define how to process light name conflict", + default = _g_EnumHelper_ConflictStrategy.to_selection(ConflictStrategy.Rename), + ) # type: ignore + object_conflict_strategy: bpy.props.EnumProperty( name = "Object Name Conflict", items = _g_EnumHelper_ConflictStrategy.generate_items(), @@ -160,6 +198,8 @@ def draw_import_params(self, layout: bpy.types.UILayout) -> None: body.label(text = 'Object Name Conflict') body.prop(self, 'object_conflict_strategy', text = '') + body.label(text = 'Light Name Conflict') + body.prop(self, 'light_conflict_strategy', text = '') body.label(text = 'Mesh Name Conflict') body.prop(self, 'mesh_conflict_strategy', text = '') body.label(text = 'Material Name Conflict') @@ -176,12 +216,16 @@ def general_get_material_conflict_strategy(self) -> ConflictStrategy: def general_get_mesh_conflict_strategy(self) -> ConflictStrategy: return _g_EnumHelper_ConflictStrategy.get_selection(self.mesh_conflict_strategy) + def general_get_light_conflict_strategy(self) -> ConflictStrategy: + return _g_EnumHelper_ConflictStrategy.get_selection(self.light_conflict_strategy) + def general_get_object_conflict_strategy(self) -> ConflictStrategy: return _g_EnumHelper_ConflictStrategy.get_selection(self.object_conflict_strategy) def general_get_conflict_resolver(self) -> ConflictResolver: return ConflictResolver( self.general_get_object_conflict_strategy(), + self.general_get_light_conflict_strategy(), self.general_get_mesh_conflict_strategy(), self.general_get_material_conflict_strategy(), self.general_get_texture_conflict_strategy()