From 70fa3b5f07caf5480586d575370a4fbc40daa04a Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Fri, 13 Mar 2026 15:47:24 +0800 Subject: [PATCH] feat: support virtools camera import and export --- bbp_ng/OP_EXPORT_virtools.py | 84 ++++++++++++++++++++++++++++------ bbp_ng/OP_IMPORT_virtools.py | 52 ++++++++++++++++++++- bbp_ng/PROP_virtools_camera.py | 55 +++++++++++++++++++++- bbp_ng/UTIL_ioport_shared.py | 33 +++++++++++++ bbp_ng/UTIL_virtools_types.py | 27 ++++++++++- 5 files changed, 233 insertions(+), 18 deletions(-) diff --git a/bbp_ng/OP_EXPORT_virtools.py b/bbp_ng/OP_EXPORT_virtools.py index c6d9738..30b6303 100644 --- a/bbp_ng/OP_EXPORT_virtools.py +++ b/bbp_ng/OP_EXPORT_virtools.py @@ -1,9 +1,10 @@ import bpy, mathutils from bpy_extras.wm_utils.progress_report import ProgressReport import tempfile, os, typing +from dataclasses import dataclass from . import PROP_preferences, UTIL_ioport_shared, UTIL_naming_convention from . import UTIL_virtools_types, UTIL_functions, UTIL_file_browser, UTIL_blender_mesh, UTIL_ballance_texture -from . import PROP_virtools_group, PROP_virtools_material, PROP_virtools_mesh, PROP_virtools_texture, PROP_virtools_light +from . import PROP_virtools_group, PROP_virtools_material, PROP_virtools_mesh, PROP_virtools_texture, PROP_virtools_light, PROP_virtools_camera from .pybmap import bmap_wrapper as bmap class BBP_OT_export_virtools(bpy.types.Operator, UTIL_file_browser.ExportVirtoolsFile, UTIL_ioport_shared.ExportParams, UTIL_ioport_shared.VirtoolsParams, UTIL_ioport_shared.BallanceParams): @@ -83,10 +84,17 @@ class BBP_OT_export_virtools(bpy.types.Operator, UTIL_file_browser.ExportVirtool _TObj3dPair = tuple[bpy.types.Object, bmap.BM3dObject] _TLightPair = tuple[bpy.types.Object, bpy.types.Light, bmap.BMTargetLight] +_TCameraPair = tuple[bpy.types.Object, bpy.types.Camera, bmap.BMTargetCamera] _TMeshPair = tuple[bpy.types.Object, bpy.types.Mesh, bmap.BMMesh] _TMaterialPair = tuple[bpy.types.Material, bmap.BMMaterial] _TTexturePair = tuple[bpy.types.Image, bmap.BMTexture] +@dataclass +class _PreparedCrets: + obj3d_crets: tuple[_TObj3dPair, ...] + light_crets: tuple[_TLightPair, ...] + camera_crets: tuple[_TCameraPair, ...] + def _export_virtools( file_name_: str, encodings_: tuple[str, ...], @@ -112,17 +120,16 @@ def _export_virtools( # prepare progress reporter with ProgressReport(wm = bpy.context.window_manager) as progress: - # prepare 3dobject and light - obj3d_crets: tuple[_TObj3dPair, ...] - light_crets: tuple[_TLightPair, ...] - (obj3d_crets, light_crets) = _prepare_virtools_3dobjects(writer, progress, export_objects) + # prepare 3dobject, light and camera + prep_crets = _prepare_virtools_3dobjects(writer, progress, export_objects) # export group according to prepared 3dobject - _export_virtools_groups(writer, progress, successive_sector_, successive_sector_count_, obj3d_crets) - # export prepared light - _export_virtools_light(writer, progress, light_crets) + _export_virtools_groups(writer, progress, successive_sector_, successive_sector_count_, prep_crets.obj3d_crets) + # export prepared light and camera + _export_virtools_light(writer, progress, prep_crets.light_crets) + _export_virtools_camera(writer, progress, prep_crets.camera_crets) # export prepared 3dobject mesh_crets: tuple[_TMeshPair, ...] = _export_virtools_3dobjects( - writer, progress, obj3d_crets) + writer, progress, prep_crets.obj3d_crets) # export mesh material_crets: tuple[_TMaterialPair, ...] = _export_virtools_meshes( writer, progress, mesh_crets) @@ -140,7 +147,7 @@ def _prepare_virtools_3dobjects( writer: bmap.BMFileWriter, progress: ProgressReport, export_objects: tuple[bpy.types.Object, ...] - ) -> tuple[tuple[_TObj3dPair, ...], tuple[_TLightPair, ...]]: + ) -> _PreparedCrets: # this function only create equvalent entries in virtools engine and do not export anything # because _export_virtools_3dobjects() and _export_virtools_groups() are need use the return value of this function # @@ -153,6 +160,9 @@ def _prepare_virtools_3dobjects( # create light hashset and result light_crets: list[_TLightPair] = [] light_cret_set: set[bpy.types.Object] = set() + # create camera hashset and result + camera_crets: list[_TCameraPair] = [] + camera_cret_set: set[bpy.types.Object] = set() # start saving tr_text: str = bpy.app.translations.pgettext_rpt('Creating 3dObjects and Lights', 'BBP_OT_export_virtools/execute') progress.enter_substeps(len(export_objects), tr_text) @@ -174,8 +184,13 @@ def _prepare_virtools_3dobjects( match(obj3d.type): case 'CAMERA': # camera object - # TODO - pass + if obj3d not in camera_cret_set: + # add into set + camera_cret_set.add(obj3d) + # create virtools instance + vtcamera: bmap.BMTargetCamera = writer.create_target_camera() + # add into result list + camera_crets.append((obj3d, typing.cast(bpy.types.Camera, obj3d.data), vtcamera)) case 'LIGHT': # light object if obj3d not in light_cret_set: @@ -191,7 +206,7 @@ def _prepare_virtools_3dobjects( # leave progress and return progress.leave_substeps() - return (tuple(obj3d_crets), tuple(light_crets)) + return _PreparedCrets(tuple(obj3d_crets), tuple(light_crets), tuple(camera_crets)) def _export_virtools_groups( writer: bmap.BMFileWriter, @@ -288,6 +303,49 @@ def _export_virtools_light( # leave progress and return progress.leave_substeps() +def _export_virtools_camera( + writer: bmap.BMFileWriter, + progress: ProgressReport, + camera_crets: tuple[_TCameraPair, ...] + ) -> None: + # start saving + tr_text: str = bpy.app.translations.pgettext_rpt('Saving Cameras', 'BBP_OT_export_virtools/execute') + progress.enter_substeps(len(camera_crets), tr_text) + + for obj3d, camera, vtcamera in camera_crets: + # set name + vtcamera.set_name(obj3d.name) + + # setup 3d entity parts + # set world matrix + vtmat: UTIL_virtools_types.VxMatrix = UTIL_virtools_types.VxMatrix() + bldmat: mathutils.Matrix = UTIL_virtools_types.bldmatrix_restore_camera_obj(obj3d.matrix_world) + UTIL_virtools_types.vxmatrix_from_blender(vtmat, bldmat) + UTIL_virtools_types.vxmatrix_conv_co(vtmat) + vtcamera.set_world_matrix(vtmat) + # set visibility + vtcamera.set_visibility(not obj3d.hide_get()) + + # setup camera data + rawcamera: PROP_virtools_camera.RawVirtoolsCamera = PROP_virtools_camera.get_raw_virtools_camera(camera) + + vtcamera.set_projection_type(rawcamera.mProjectionType) + + vtcamera.set_orthographic_zoom(rawcamera.mOrthographicZoom) + + vtcamera.set_front_plane(rawcamera.mFrontPlane) + vtcamera.set_back_plane(rawcamera.mBackPlane) + vtcamera.set_fov(rawcamera.mFov) + + (w, h) = rawcamera.mAspectRatio + vtcamera.set_aspect_ratio(w, h) + + # step + progress.step() + + # leave progress and return + progress.leave_substeps() + def _export_virtools_3dobjects( writer: bmap.BMFileWriter, progress: ProgressReport, diff --git a/bbp_ng/OP_IMPORT_virtools.py b/bbp_ng/OP_IMPORT_virtools.py index d4d3905..f30df52 100644 --- a/bbp_ng/OP_IMPORT_virtools.py +++ b/bbp_ng/OP_IMPORT_virtools.py @@ -3,7 +3,7 @@ from bpy_extras.wm_utils.progress_report import ProgressReport import tempfile, os, typing from . import PROP_preferences, UTIL_ioport_shared, UTIL_naming_convention from . import UTIL_virtools_types, UTIL_functions, UTIL_file_browser, UTIL_blender_mesh, UTIL_ballance_texture -from . import PROP_virtools_group, PROP_virtools_material, PROP_virtools_mesh, PROP_virtools_texture, PROP_virtools_light, PROP_ballance_map_info +from . import PROP_virtools_group, PROP_virtools_material, PROP_virtools_mesh, PROP_virtools_texture, PROP_virtools_light, PROP_virtools_camera, 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): @@ -80,8 +80,9 @@ def _import_virtools(file_name_: str, encodings_: tuple[str, ...], resolver: UTI # import 3dobjects obj3d_cret_map: dict[bmap.BM3dObject, bpy.types.Object] = _import_virtools_3dobjects( reader, progress, resolver, mesh_cret_map) - # import light + # import light and camera _import_virtools_lights(reader, progress, resolver) + _import_virtools_cameras(reader, progress, resolver) # import groups _import_virtools_groups(reader, progress, obj3d_cret_map) @@ -423,6 +424,53 @@ def _import_virtools_lights( # leave progress progress.leave_substeps() +def _import_virtools_cameras( + reader: bmap.BMFileReader, + progress: ProgressReport, + resolver: UTIL_ioport_shared.ConflictResolver + ) -> None: + # prepare progress + tr_text: str = bpy.app.translations.pgettext_rpt('Loading Cameras', 'BBP_OT_import_virtools/execute') + progress.enter_substeps(reader.get_target_camera_count(), tr_text) + + # same creation notes like light + for vtcamera in reader.get_target_cameras(): + # create camera data block and 3d object together + (camera_3dobj, camera, init_camera) = resolver.create_camera( + UTIL_virtools_types.virtools_name_regulator(vtcamera.get_name()) + ) + + if init_camera: + # setup camera data block + rawcamera: PROP_virtools_camera.RawVirtoolsCamera = PROP_virtools_camera.RawVirtoolsCamera() + + rawcamera.mProjectionType = vtcamera.get_projection_type() + + rawcamera.mOrthographicZoom = vtcamera.get_orthographic_zoom() + + rawcamera.mFrontPlane = vtcamera.get_front_plane() + rawcamera.mBackPlane = vtcamera.get_back_plane() + rawcamera.mFov = vtcamera.get_fov() + + rawcamera.mAspectRatio = vtcamera.get_aspect_ratio() + + PROP_virtools_camera.set_raw_virtools_camera(camera, rawcamera) + PROP_virtools_camera.apply_to_blender_camera(camera) + + # setup camera associated 3d object + # add into scene + UTIL_functions.add_into_scene(camera_3dobj) + # set world matrix + vtmat: UTIL_virtools_types.VxMatrix = vtcamera.get_world_matrix() + UTIL_virtools_types.vxmatrix_conv_co(vtmat) + bldmat: mathutils.Matrix = UTIL_virtools_types.vxmatrix_to_blender(vtmat) + camera_3dobj.matrix_world = UTIL_virtools_types.bldmatrix_patch_camera_obj(bldmat) + # set visibility + camera_3dobj.hide_set(not vtcamera.get_visibility()) + + # leave progress + progress.leave_substeps() + def _import_virtools_groups( reader: bmap.BMFileReader, progress: ProgressReport, diff --git a/bbp_ng/PROP_virtools_camera.py b/bbp_ng/PROP_virtools_camera.py index 5b39512..50e761f 100644 --- a/bbp_ng/PROP_virtools_camera.py +++ b/bbp_ng/PROP_virtools_camera.py @@ -191,10 +191,39 @@ def apply_to_blender_camera(cam: bpy.types.Camera) -> None: cam.lens_unit = 'FOV' cam.angle = rawdata.mFov +def apply_to_blender_scene_resolution(cam: bpy.types.Camera) -> None: + # get raw data first + rawdata: RawVirtoolsCamera = get_raw_virtools_camera(cam) + + # fetch width and height + (w, h) = rawdata.mAspectRatio + + # compute a proper resolution from this aspect ratio + # calculate their lcm first + hw_lcm = math.lcm(w, h) + # get the first number which is greater than 1000 (1000 is a proper resolution size) + # and can be integrally divided by this lcm. + HW_MIN: int = 1000 + min_edge = ((HW_MIN // hw_lcm) + 1) * hw_lcm + # calculate the final resolution + if w < h: + # width is shorter than height, set width as min edge + width = min_edge + height = width // w * h + else: + # opposite case + height = min_edge + width = height // h * w + + # setup resolution + render_settings = bpy.context.scene.render + render_settings.resolution_x = width + render_settings.resolution_y = height + # Operators class BBP_OT_apply_virtools_camera(bpy.types.Operator): - """Apply Virtools Camera to Blender Camera.""" + """Apply Virtools Camera to Blender Camera except Resolution.""" bl_idname = "bbp.apply_virtools_camera" bl_label = "Apply to Blender Camera" bl_options = {'UNDO'} @@ -209,6 +238,22 @@ class BBP_OT_apply_virtools_camera(bpy.types.Operator): apply_to_blender_camera(cam) return {'FINISHED'} +class BBP_OT_apply_virtools_camera_resolution(bpy.types.Operator): + """Apply Virtools Camera Resolution to Blender Scene.""" + bl_idname = "bbp.apply_virtools_camera_resolution" + bl_label = "Apply to Blender Scene Resolution" + bl_options = {'UNDO'} + bl_translation_context = 'BBP_OT_apply_virtools_camera_resolution' + + @classmethod + def poll(cls, context): + return context.camera is not None + + def execute(self, context): + cam: bpy.types.Camera = context.camera + apply_to_blender_scene_resolution(cam) + return {'FINISHED'} + # Display Panel class BBP_PT_virtools_camera(bpy.types.Panel): @@ -232,9 +277,13 @@ class BBP_PT_virtools_camera(bpy.types.Panel): rawdata: RawVirtoolsCamera = get_raw_virtools_camera(cam) # draw operator - layout.operator( + row = layout.row() + row.operator( BBP_OT_apply_virtools_camera.bl_idname, text='Apply', icon='NODETREE', text_ctxt='BBP_PT_virtools_camera/draw') + row.operator( + BBP_OT_apply_virtools_camera_resolution.bl_idname, text='Apply Resolution', icon='SCENE', + text_ctxt='BBP_PT_virtools_camera/draw') # draw data layout.separator() @@ -276,6 +325,7 @@ class BBP_PT_virtools_camera(bpy.types.Panel): def register() -> None: bpy.utils.register_class(BBP_PG_virtools_camera) bpy.utils.register_class(BBP_OT_apply_virtools_camera) + bpy.utils.register_class(BBP_OT_apply_virtools_camera_resolution) bpy.utils.register_class(BBP_PT_virtools_camera) # add into camera metadata @@ -286,6 +336,7 @@ def unregister() -> None: del bpy.types.Camera.virtools_camera bpy.utils.unregister_class(BBP_PT_virtools_camera) + bpy.utils.unregister_class(BBP_OT_apply_virtools_camera_resolution) bpy.utils.unregister_class(BBP_OT_apply_virtools_camera) bpy.utils.unregister_class(BBP_PG_virtools_camera) diff --git a/bbp_ng/UTIL_ioport_shared.py b/bbp_ng/UTIL_ioport_shared.py index 0eb37c0..c902554 100644 --- a/bbp_ng/UTIL_ioport_shared.py +++ b/bbp_ng/UTIL_ioport_shared.py @@ -37,6 +37,7 @@ class ConflictResolver(): __mObjectStrategy: ConflictStrategy __mLightStrategy: ConflictStrategy + __mCameraStrategy: ConflictStrategy __mMeshStrategy: ConflictStrategy __mMaterialStrategy: ConflictStrategy __mTextureStrategy: ConflictStrategy @@ -44,11 +45,13 @@ class ConflictResolver(): def __init__(self, obj_strategy: ConflictStrategy, light_strategy: ConflictStrategy, + camera_strategy: ConflictStrategy, mesh_strategy: ConflictStrategy, mtl_strategy: ConflictStrategy, tex_strategy: ConflictStrategy): self.__mObjectStrategy = obj_strategy self.__mLightStrategy = light_strategy + self.__mCameraStrategy = camera_strategy self.__mMeshStrategy = mesh_strategy self.__mMaterialStrategy = mtl_strategy self.__mTextureStrategy = tex_strategy @@ -88,6 +91,22 @@ class ConflictResolver(): new_obj: bpy.types.Object = bpy.data.objects.new(name, new_light) return (new_obj, new_light, True) + def create_camera(self, name: str) -> tuple[bpy.types.Object, bpy.types.Camera, bool]: + """ + Create camera data block and associated 3d object. + + Same execution pattern with light creation. + """ + if self.__mCameraStrategy == ConflictStrategy.Current: + old_obj: bpy.types.Object | None = bpy.data.objects.get(name, None) + if old_obj is not None and old_obj.type == 'CAMERA': + return (old_obj, typing.cast(bpy.types.Camera, old_obj.data), False) + # create new object. + # if object or camera name is conflict, rename it directly without considering conflict strategy + new_camera: bpy.types.Camera = bpy.data.cameras.new(name) + new_obj: bpy.types.Object = bpy.data.objects.new(name, new_camera) + return (new_obj, new_camera, 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) @@ -153,6 +172,14 @@ class ImportParams(): translation_context = 'BBP/UTIL_ioport_shared.ImportParams/property' ) # type: ignore + camera_conflict_strategy: bpy.props.EnumProperty( + name = "Camera Name Conflict", + items = _g_EnumHelper_ConflictStrategy.generate_items(), + description = "Define how to process camera name conflict", + default = _g_EnumHelper_ConflictStrategy.to_selection(ConflictStrategy.Rename), + translation_context = 'BBP/UTIL_ioport_shared.ImportParams/property' + ) # type: ignore + object_conflict_strategy: bpy.props.EnumProperty( name = "Object Name Conflict", items = _g_EnumHelper_ConflictStrategy.generate_items(), @@ -173,11 +200,13 @@ class ImportParams(): grid = body.grid_flow(row_major=False, columns=2) grid.label(text='Object', icon='CUBE', text_ctxt='BBP/UTIL_ioport_shared.ImportParams/draw') grid.label(text='Light', icon='LIGHT', text_ctxt='BBP/UTIL_ioport_shared.ImportParams/draw') + grid.label(text='Camera', icon='CAMERA_DATA', text_ctxt='BBP/UTIL_ioport_shared.ImportParams/draw') grid.label(text='Mesh', icon='MESH_DATA', text_ctxt='BBP/UTIL_ioport_shared.ImportParams/draw') grid.label(text='Material', icon='MATERIAL', text_ctxt='BBP/UTIL_ioport_shared.ImportParams/draw') grid.label(text='Texture', icon='TEXTURE', text_ctxt='BBP/UTIL_ioport_shared.ImportParams/draw') grid.prop(self, 'object_conflict_strategy', text='') grid.prop(self, 'light_conflict_strategy', text='') + grid.prop(self, 'camera_conflict_strategy', text='') grid.prop(self, 'mesh_conflict_strategy', text='') grid.prop(self, 'material_conflict_strategy', text='') grid.prop(self, 'texture_conflict_strategy', text='') @@ -194,6 +223,9 @@ class ImportParams(): def general_get_light_conflict_strategy(self) -> ConflictStrategy: return _g_EnumHelper_ConflictStrategy.get_selection(self.light_conflict_strategy) + def general_get_camera_conflict_strategy(self) -> ConflictStrategy: + return _g_EnumHelper_ConflictStrategy.get_selection(self.camera_conflict_strategy) + def general_get_object_conflict_strategy(self) -> ConflictStrategy: return _g_EnumHelper_ConflictStrategy.get_selection(self.object_conflict_strategy) @@ -201,6 +233,7 @@ class ImportParams(): return ConflictResolver( self.general_get_object_conflict_strategy(), self.general_get_light_conflict_strategy(), + self.general_get_camera_conflict_strategy(), self.general_get_mesh_conflict_strategy(), self.general_get_material_conflict_strategy(), self.general_get_texture_conflict_strategy() diff --git a/bbp_ng/UTIL_virtools_types.py b/bbp_ng/UTIL_virtools_types.py index 5846d6c..27d1afb 100644 --- a/bbp_ng/UTIL_virtools_types.py +++ b/bbp_ng/UTIL_virtools_types.py @@ -79,7 +79,7 @@ def vxmatrix_to_blender(self: VxMatrix) -> mathutils.Matrix: ## Hints about Light Matrix # There is a slight difference between Virtools and Blender. # In blender, the default direction of all directional light (spot and sun) are Down (-Z). -# Hoewver, in Virtools, the default direction of all directional light (spot and directional) are Forward (+Z). +# However, in Virtools, the default direction of all directional light (spot and directional) are Forward (+Z). # # As brief view, in Blender coordinate system, you can see that we got Blender default light direction # from Virtools default light direction by rotating it around X-axis with -90 degree @@ -109,6 +109,31 @@ def bldmatrix_restore_light_obj(data: mathutils.Matrix) -> mathutils.Matrix: # so we simply right multiple it. return data @ mathutils.Matrix.Rotation(math.radians(-90), 4, 'X') +## Hints about Camera Matrix +# Just like light, camera is also different between Virtools and Blender. +# In Blender, the default camera orientation is looking at -Z and +Y up. +# Oppositely, Virtools camera is looking at +Z and +Y up. +# +# These direction is based on their own coordinate system respectively. +# Accidently this difference is same like light. +# So we can simply copy light strategy in there. + +def bldmatrix_patch_camera_obj(data: mathutils.Matrix) -> mathutils.Matrix: + """ + Add patch for camera world matrix to correct its direction. + This function is usually used when importing camera. + """ + # same operation like light matrix patch + return data @ mathutils.Matrix.Rotation(math.radians(90), 4, 'X') + +def bldmatrix_restore_camera_obj(data: mathutils.Matrix) -> mathutils.Matrix: + """ + The reverse operation of bldmatrix_patch_camera_mat(). + This function is usually used when exporting camera. + """ + # same operation like light matrix patch + return data @ mathutils.Matrix.Rotation(math.radians(-90), 4, 'X') + #endregion #region Blender EnumProperty Creation