From 44d3b1fc998da5e050ffa4657ca8f62cffe18621 Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Fri, 20 Mar 2026 13:49:00 +0800 Subject: [PATCH] feat: finish virtools camera work - add operator for applying camera aspect ratio to blender scene. - add camera aspect ratio preset in virtools camera panel. - update game camera operators. use virtools camera instead of directly modifying camera properties. --- bbp_ng/OP_OBJECT_game_view.py | 129 ++++++++++++++------------------- bbp_ng/PROP_virtools_camera.py | 97 +++++++++++++++++++++++-- bbp_ng/__init__.py | 1 - 3 files changed, 143 insertions(+), 84 deletions(-) diff --git a/bbp_ng/OP_OBJECT_game_view.py b/bbp_ng/OP_OBJECT_game_view.py index 872e2f5..626ed9f 100644 --- a/bbp_ng/OP_OBJECT_game_view.py +++ b/bbp_ng/OP_OBJECT_game_view.py @@ -1,34 +1,15 @@ import bpy, mathutils import typing, enum, math -from . import UTIL_functions +from . import UTIL_functions, PROP_virtools_camera -# TODO: -# This file should have fully refactor after we finish Virtools Camera import and export, -# because this module is highly rely on it. Current implementation is a compromise. -# There is a list of things to be done: -# - Remove BBP_OT_game_resolution operator, because Virtools Camera will have similar function in panel. -# - Update BBP_OT_game_cameraoperator with Virtools Camera. - -#region Game Resolution +#region Enum Defines class ResolutionKind(enum.IntEnum): Normal = enum.auto() - Extended = enum.auto() - Widescreen = enum.auto() - Panoramic = enum.auto() - - def to_resolution(self) -> tuple[int, int]: - match self: - case ResolutionKind.Normal: return (1024, 768) - case ResolutionKind.Extended: return (1280, 720) - case ResolutionKind.Widescreen: return (1400, 600) - case ResolutionKind.Panoramic: return (2000, 700) - + WideScreen = enum.auto() _g_ResolutionKindDesc: dict[ResolutionKind, tuple[str, str]] = { - ResolutionKind.Normal: ("Normal", "Aspect ratio: 4:3."), - ResolutionKind.Extended: ("Extended", "Aspect ratio: 16:9."), - ResolutionKind.Widescreen: ("Widescreen", "Aspect ratio: 7:3."), - ResolutionKind.Panoramic: ("Panoramic", "Aspect ratio: 20:7."), + ResolutionKind.Normal: ("Normal", "Vanilla Ballance Resolution"), + ResolutionKind.WideScreen: ("Wide Screen", "Ballance Resolution with Wide Screen Fix"), } _g_EnumHelper_ResolutionKind = UTIL_functions.EnumPropHelper( ResolutionKind, @@ -39,45 +20,6 @@ _g_EnumHelper_ResolutionKind = UTIL_functions.EnumPropHelper( lambda _: "" ) -class BBP_OT_game_resolution(bpy.types.Operator): - """Set Blender render resolution to Ballance game""" - bl_idname = "bbp.game_resolution" - bl_label = "Game Resolution" - bl_options = {'REGISTER', 'UNDO'} - bl_translation_context = 'BBP_OT_game_resolution' - - resolution_kind: bpy.props.EnumProperty( - name = "Resolution Kind", - description = "The type of preset resolution.", - items = _g_EnumHelper_ResolutionKind.generate_items(), - default = _g_EnumHelper_ResolutionKind.to_selection(ResolutionKind.Normal), - translation_context = 'BBP_OT_game_resolution/property' - ) # type: ignore - - def invoke(self, context, event): - return self.execute(context) - - def draw(self, context): - layout = self.layout - layout.use_property_split = True - layout.prop(self, 'resolution_kind') - - def execute(self, context): - # fetch resolution - resolution_kind = _g_EnumHelper_ResolutionKind.get_selection(self.resolution_kind) - resolution = resolution_kind.to_resolution() - # setup resolution - render_settings = bpy.context.scene.render - render_settings.resolution_x = resolution[0] - render_settings.resolution_y = resolution[1] - return {'FINISHED'} - -#endregion - -#region Game Camera - -#region Enum Defines - class TargetKind(enum.IntEnum): Cursor = enum.auto() ActiveObject = enum.auto() @@ -281,6 +223,21 @@ class BBP_OT_game_camera(bpy.types.Operator): translation_context = 'BBP_OT_game_camera/property' ) # type: ignore + + modify_resolution: bpy.props.BoolProperty( + name = 'Modify Resolution', + description = 'Whether modify the resolution of camera.', + default = False, + translation_context = 'BBP_OT_game_camera/property' + ) # type: ignore + resolution_kind: bpy.props.EnumProperty( + name = "Resolution Kind", + description = "The type of preset resolution.", + items = _g_EnumHelper_ResolutionKind.generate_items(), + default = _g_EnumHelper_ResolutionKind.to_selection(ResolutionKind.Normal), + translation_context = 'BBP_OT_game_camera/property' + ) # type: ignore + @classmethod def poll(cls, context): # find camera object @@ -333,6 +290,12 @@ class BBP_OT_game_camera(bpy.types.Operator): layout.label(text='Perspective', text_ctxt='BBP_OT_game_camera/draw') layout.row().prop(self, 'perspective_kind', expand=True) + # Show resolution kind + layout.separator() + layout.prop(self, 'modify_resolution', text='Resolution', text_ctxt='BBP_OT_game_camera/draw') + if self.modify_resolution: + layout.row().prop(self, 'resolution_kind', expand=True) + def execute(self, context): # fetch angle angle: float @@ -347,11 +310,12 @@ class BBP_OT_game_camera(bpy.types.Operator): camera_obj = typing.cast(bpy.types.Object, _find_camera_obj()) target_kind = _g_EnumHelper_TargetKind.get_selection(self.target_kind) perspective_kind = _g_EnumHelper_PerspectiveKind.get_selection(self.perspective_kind) + resolution_kind = _g_EnumHelper_ResolutionKind.get_selection(self.resolution_kind) # setup its transform and properties glob_trans = _fetch_glob_translation(camera_obj, target_kind) _setup_camera_transform(camera_obj, angle, perspective_kind, glob_trans) - _setup_camera_properties(camera_obj) + _setup_camera_properties(camera_obj, resolution_kind) # return return {'FINISHED'} @@ -451,24 +415,39 @@ def _setup_camera_transform(camobj: bpy.types.Object, angle: float, perspective: glob_trans_mat = mathutils.Matrix.Translation(glob_trans) camobj.matrix_world = glob_trans_mat @ trans_mat @ rot_mat -def _setup_camera_properties(camobj: bpy.types.Object) -> None: - # fetch camera +def _setup_camera_properties(camobj: bpy.types.Object, resolution_kind: ResolutionKind | None) -> None: + # fetch camera and its raw data camera = typing.cast(bpy.types.Camera, camobj.data) + rawdata = PROP_virtools_camera.get_raw_virtools_camera(camera) # set clipping - camera.clip_start = 4 - camera.clip_end = 1200 - # set FOV - camera.lens_unit = 'FOV' - camera.angle = math.radians(58) - -#endregion + rawdata.mFrontPlane = 4 + rawdata.mBackPlane = 1200 + # set FOV and aspect ratio according to presented resolution kind + if resolution_kind is not None: + match resolution_kind: + case ResolutionKind.Normal: + rawdata.mFov = math.radians(58) + rawdata.mAspectRatio = (4, 3) + case ResolutionKind.WideScreen: + # prepare input arguments + aspect_ratio = (16, 9) + fov = math.radians(58) + # FOV correction reference: + # https://github.com/doyaGu/BallanceModLoaderPlus/blob/c4ab4386fd834af69a960c156fca97237b2fd4c5/src/RenderHook.cpp#L46 + aspect = aspect_ratio[0] / aspect_ratio[1] + rawdata.mFov = math.atan2(math.tan(fov * 0.5) * 0.75 * aspect, 1.0) * 2.0 + rawdata.mAspectRatio = aspect_ratio + + # rewrite it back + PROP_virtools_camera.set_raw_virtools_camera(camera, rawdata) + # and apply it into camera and blender scene + PROP_virtools_camera.apply_to_blender_camera(camera) + PROP_virtools_camera.apply_to_blender_scene_resolution(camera) def register() -> None: - bpy.utils.register_class(BBP_OT_game_resolution) bpy.utils.register_class(BBP_OT_game_camera) def unregister() -> None: bpy.utils.unregister_class(BBP_OT_game_camera) - bpy.utils.unregister_class(BBP_OT_game_resolution) diff --git a/bbp_ng/PROP_virtools_camera.py b/bbp_ng/PROP_virtools_camera.py index 50e761f..b99f35c 100644 --- a/bbp_ng/PROP_virtools_camera.py +++ b/bbp_ng/PROP_virtools_camera.py @@ -1,9 +1,7 @@ import bpy -import typing, math +import typing, math, enum from . import UTIL_functions, UTIL_virtools_types -# Raw Data - class RawVirtoolsCamera(): # Class Member @@ -54,10 +52,12 @@ class RawVirtoolsCamera(): h = max(1, h) self.mAspectRatio = (w, h) -# Blender Property Group +#region Blender Enum Prop Helper _g_Helper_CK_CAMERA_PROJECTION = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.CK_CAMERA_PROJECTION) +#endregion + class BBP_PG_virtools_camera(bpy.types.PropertyGroup): projection_type: bpy.props.EnumProperty( name = "Type", @@ -134,7 +134,7 @@ class BBP_PG_virtools_camera(bpy.types.PropertyGroup): translation_context = 'BBP_PG_virtools_camera/property' ) # type: ignore -# Getter Setter and Applyer +#region Getter Setter and Applyer def get_virtools_camera(cam: bpy.types.Camera) -> BBP_PG_virtools_camera: return cam.virtools_camera @@ -220,7 +220,50 @@ def apply_to_blender_scene_resolution(cam: bpy.types.Camera) -> None: render_settings.resolution_x = width render_settings.resolution_y = height -# Operators +#endregion + +#region Aspect Ratio Preset + +class AspectRatioPresetType(enum.IntEnum): + Normal = enum.auto() + Extended = enum.auto() + Widescreen = enum.auto() + Panoramic = enum.auto() + + def to_aspect_ratio(self) -> tuple[int, int]: + match self: + case AspectRatioPresetType.Normal: return (4, 3) + case AspectRatioPresetType.Extended: return (16, 9) + case AspectRatioPresetType.Widescreen: return (7, 3) + case AspectRatioPresetType.Panoramic: return (20, 7) + +_g_AspectRatioPresetTypeDesc: dict[AspectRatioPresetType, tuple[str, str]] = { + AspectRatioPresetType.Normal: ("Normal", "Aspect ratio: 4:3."), + AspectRatioPresetType.Extended: ("Extended", "Aspect ratio: 16:9."), + AspectRatioPresetType.Widescreen: ("Widescreen", "Aspect ratio: 7:3."), + AspectRatioPresetType.Panoramic: ("Panoramic", "Aspect ratio: 20:7."), +} + +_g_Helper_AspectRatioPresetType = UTIL_functions.EnumPropHelper( + AspectRatioPresetType, + lambda x: str(x.value), + lambda x: AspectRatioPresetType(int(x)), + lambda x: _g_AspectRatioPresetTypeDesc[x][0], + lambda x: _g_AspectRatioPresetTypeDesc[x][1], + lambda _: "" +) + +def preset_virtools_camera_aspect_ratio(cam: bpy.types.Camera, preset_type: AspectRatioPresetType) -> None: + # get raw data from it + rawdata = get_raw_virtools_camera(cam) + # modify its aspect ratio + rawdata.mAspectRatio = preset_type.to_aspect_ratio() + # rewrite it. + set_raw_virtools_camera(cam, rawdata) + +#endregion + +#region Operators class BBP_OT_apply_virtools_camera(bpy.types.Operator): """Apply Virtools Camera to Blender Camera except Resolution.""" @@ -254,7 +297,41 @@ class BBP_OT_apply_virtools_camera_resolution(bpy.types.Operator): apply_to_blender_scene_resolution(cam) return {'FINISHED'} -# Display Panel +class BBP_OT_preset_virtools_camera_aspect_ratio(bpy.types.Operator): + """Preset Virtools Camera Aspect Ratio with Virtools Presets.""" + bl_idname = "bbp.preset_virtools_camera_aspect_ratio" + bl_label = "Preset Virtools Camera Aspect Ratio" + bl_options = {'UNDO'} + bl_translation_context = 'BBP_OT_preset_virtools_camera_aspect_ratio' + + preset_type: bpy.props.EnumProperty( + name = "Preset", + description = "The preset which you want to apply.", + items = _g_Helper_AspectRatioPresetType.generate_items(), + translation_context = 'BBP_OT_preset_virtools_camera_aspect_ratio/property' + ) # type: ignore + + @classmethod + def poll(cls, context): + return context.camera is not None + + def invoke(self, context, event): + wm = context.window_manager + return wm.invoke_props_dialog(self) + + def draw(self, context): + self.layout.prop(self, "preset_type") + + def execute(self, context): + # get essential value + cam: bpy.types.Camera = context.camera + expected_preset: AspectRatioPresetType = _g_Helper_AspectRatioPresetType.get_selection(self.preset_type) + + # apply preset to material + preset_virtools_camera_aspect_ratio(cam, expected_preset) + return {'FINISHED'} + +#endregion class BBP_PT_virtools_camera(bpy.types.Panel): """Show Virtools Camera Properties""" @@ -314,7 +391,9 @@ class BBP_PT_virtools_camera(bpy.types.Panel): # aspect ratio layout.separator() - layout.label(text='Aspect Ratio', text_ctxt='BBP_PT_virtools_camera/draw') + row = layout.row() + row.label(text='Aspect Ratio', text_ctxt='BBP_PT_virtools_camera/draw') + row.operator(BBP_OT_preset_virtools_camera_aspect_ratio.bl_idname, text='', icon = "PRESET") sublayout = layout.row() sublayout.use_property_split = False sublayout.prop(props, 'aspect_ratio_w', text = '', expand = True) @@ -326,6 +405,7 @@ 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_OT_preset_virtools_camera_aspect_ratio) bpy.utils.register_class(BBP_PT_virtools_camera) # add into camera metadata @@ -336,6 +416,7 @@ def unregister() -> None: del bpy.types.Camera.virtools_camera bpy.utils.unregister_class(BBP_PT_virtools_camera) + bpy.utils.unregister_class(BBP_OT_preset_virtools_camera_aspect_ratio) 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/__init__.py b/bbp_ng/__init__.py index a95fbdf..4c921f1 100644 --- a/bbp_ng/__init__.py +++ b/bbp_ng/__init__.py @@ -179,7 +179,6 @@ class BBP_MT_View3DMenu(bpy.types.Menu): layout.operator(OP_OBJECT_legacy_align.BBP_OT_legacy_align.bl_idname) layout.separator() layout.label(text='Camera', icon='CAMERA_DATA', text_ctxt='BBP_MT_View3DMenu/draw') - layout.operator(OP_OBJECT_game_view.BBP_OT_game_resolution.bl_idname) layout.operator(OP_OBJECT_game_view.BBP_OT_game_camera.bl_idname) layout.separator() layout.label(text='Select', icon='SELECT_SET', text_ctxt='BBP_MT_View3DMenu/draw')