From 684567777f0bad3e222d5282bf4d3444c309d9d1 Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Fri, 6 Mar 2026 16:10:05 +0800 Subject: [PATCH] feat: add virtools camera support --- bbp_ng/PROP_virtools_camera.py | 292 +++++++++++++++++++++++++++++++++ bbp_ng/PROP_virtools_light.py | 3 +- bbp_ng/UTIL_virtools_types.py | 4 +- bbp_ng/__init__.py | 4 +- 4 files changed, 298 insertions(+), 5 deletions(-) create mode 100644 bbp_ng/PROP_virtools_camera.py diff --git a/bbp_ng/PROP_virtools_camera.py b/bbp_ng/PROP_virtools_camera.py new file mode 100644 index 0000000..5b39512 --- /dev/null +++ b/bbp_ng/PROP_virtools_camera.py @@ -0,0 +1,292 @@ +import bpy +import typing, math +from . import UTIL_functions, UTIL_virtools_types + +# Raw Data + +class RawVirtoolsCamera(): + # Class Member + + mProjectionType: UTIL_virtools_types.CK_CAMERA_PROJECTION + + mOrthographicZoom: float + + mFrontPlane: float + mBackPlane: float + mFov: float + + mAspectRatio: tuple[int, int] + + # Class member default value + + cDefaultProjectionType: typing.ClassVar[UTIL_virtools_types.CK_CAMERA_PROJECTION] = UTIL_virtools_types.CK_CAMERA_PROJECTION.CK_PERSPECTIVEPROJECTION + + cDefaultOrthographicZoom: typing.ClassVar[float] = 1.0 + + cDefaultFrontPlane: typing.ClassVar[float] = 1.0 + cDefaultBackPlane: typing.ClassVar[float] = 4000.0 + cDefaultFov: typing.ClassVar[float] = 0.5 + + cDefaultAspectRatio: typing.ClassVar[tuple[int, int]] = (4, 3) + + def __init__(self, **kwargs): + # assign default value for each component + self.mProjectionType = kwargs.get("mProjectionType", RawVirtoolsCamera.cDefaultProjectionType) + + self.mOrthographicZoom = kwargs.get("mOrthographicZoom", RawVirtoolsCamera.cDefaultOrthographicZoom) + + self.mFrontPlane = kwargs.get("mFrontPlane", RawVirtoolsCamera.cDefaultFrontPlane) + self.mBackPlane = kwargs.get("mBackPlane", RawVirtoolsCamera.cDefaultBackPlane) + self.mFov = kwargs.get("mFov", RawVirtoolsCamera.cDefaultFov) + + self.mAspectRatio = kwargs.get("mAspectRatio", RawVirtoolsCamera.cDefaultAspectRatio) + + def regulate(self) -> None: + # everything should be positive + self.mOrthographicZoom = max(0.0, self.mOrthographicZoom) + self.mFrontPlane = max(0.0, self.mFrontPlane) + self.mBackPlane = max(0.0, self.mBackPlane) + self.mFov = max(0.0, self.mFov) + + # aspect ratio should be positive and at least 1 + (w, h) = self.mAspectRatio + w = max(1, w) + h = max(1, h) + self.mAspectRatio = (w, h) + +# Blender Property Group + +_g_Helper_CK_CAMERA_PROJECTION = UTIL_virtools_types.EnumPropHelper(UTIL_virtools_types.CK_CAMERA_PROJECTION) + +class BBP_PG_virtools_camera(bpy.types.PropertyGroup): + projection_type: bpy.props.EnumProperty( + name = "Type", + description = "The type of this camera.", + items = _g_Helper_CK_CAMERA_PROJECTION.generate_items(), + default = _g_Helper_CK_CAMERA_PROJECTION.to_selection(RawVirtoolsCamera.cDefaultProjectionType), + translation_context = 'BBP_PG_virtools_camera/property' + ) # type: ignore + + orthographic_zoom: bpy.props.FloatProperty( + name = "Orthographic Zoom", + description = "Defines the orthographic zoom.", + min = 0.0, + soft_min = 0.0, + soft_max = 0.5, + step = 5, + default = RawVirtoolsCamera.cDefaultOrthographicZoom, + translation_context = 'BBP_PG_virtools_camera/property' + ) # type: ignore + + front_plane: bpy.props.FloatProperty( + name = "Front Plane", + description = "Defines the front plane.", + min = 0.0, + soft_min = 0.0, + soft_max = 5000.0, + step = 100, + default = RawVirtoolsCamera.cDefaultFrontPlane, + translation_context = 'BBP_PG_virtools_camera/property' + ) # type: ignore + + back_plane: bpy.props.FloatProperty( + name = "Back Plane", + description = "Defines the back plane.", + min = 0.0, + soft_min = 0.0, + soft_max = 5000.0, + step = 100, + default = RawVirtoolsCamera.cDefaultBackPlane, + translation_context = 'BBP_PG_virtools_camera/property' + ) # type: ignore + + fov: bpy.props.FloatProperty( + name = "Field of View", + description = "Defines the field of view.", + subtype = 'ANGLE', + min = 0.0, + max = math.radians(180.0), + step = 100, + precision = 100, + default = RawVirtoolsCamera.cDefaultFov, + translation_context = 'BBP_PG_virtools_camera/property' + ) # type: ignore + + aspect_ratio_w: bpy.props.IntProperty( + name = "Aspect Ratio Width", + description = "Defines the width of aspect ratio.", + min = 1, + soft_min = 1, + soft_max = 40, + step = 1, + default = RawVirtoolsCamera.cDefaultAspectRatio[0], + translation_context = 'BBP_PG_virtools_camera/property' + ) # type: ignore + + aspect_ratio_h: bpy.props.IntProperty( + name = "Aspect Ratio Height", + description = "Defines the height of aspect ratio.", + min = 1, + soft_min = 1, + soft_max = 40, + step = 1, + default = RawVirtoolsCamera.cDefaultAspectRatio[1], + translation_context = 'BBP_PG_virtools_camera/property' + ) # type: ignore + +# Getter Setter and Applyer + +def get_virtools_camera(cam: bpy.types.Camera) -> BBP_PG_virtools_camera: + return cam.virtools_camera + +def get_raw_virtools_camera(cam: bpy.types.Camera) -> RawVirtoolsCamera: + props: BBP_PG_virtools_camera = get_virtools_camera(cam) + rawdata: RawVirtoolsCamera = RawVirtoolsCamera() + + rawdata.mProjectionType = _g_Helper_CK_CAMERA_PROJECTION.get_selection(props.projection_type) + + rawdata.mOrthographicZoom = props.orthographic_zoom + + rawdata.mFrontPlane = props.front_plane + rawdata.mBackPlane = props.back_plane + rawdata.mFov = props.fov + + rawdata.mAspectRatio = (props.aspect_ratio_w, props.aspect_ratio_h) + + rawdata.regulate() + return rawdata + +def set_raw_virtools_camera(cam: bpy.types.Camera, rawdata: RawVirtoolsCamera) -> None: + props: BBP_PG_virtools_camera = get_virtools_camera(cam) + + props.projection_type = _g_Helper_CK_CAMERA_PROJECTION.to_selection(rawdata.mProjectionType) + + props.orthographic_zoom = rawdata.mOrthographicZoom + + props.front_plane = rawdata.mFrontPlane + props.back_plane = rawdata.mBackPlane + props.fov = rawdata.mFov + + (props.aspect_ratio_w, props.aspect_ratio_h) = rawdata.mAspectRatio + +def apply_to_blender_camera(cam: bpy.types.Camera) -> None: + # get raw data first + rawdata: RawVirtoolsCamera = get_raw_virtools_camera(cam) + + # set camera type + match(rawdata.mProjectionType): + case UTIL_virtools_types.CK_CAMERA_PROJECTION.CK_PERSPECTIVEPROJECTION: + cam.type = 'PERSP' + case UTIL_virtools_types.CK_CAMERA_PROJECTION.CK_ORTHOGRAPHICPROJECTION: + cam.type = 'ORTHO' + + # set orthographic zoom + cam.ortho_scale = rawdata.mOrthographicZoom + + # front and back plane + cam.clip_start = rawdata.mFrontPlane + cam.clip_end = rawdata.mBackPlane + + # fov + cam.lens_unit = 'FOV' + cam.angle = rawdata.mFov + +# Operators + +class BBP_OT_apply_virtools_camera(bpy.types.Operator): + """Apply Virtools Camera to Blender Camera.""" + bl_idname = "bbp.apply_virtools_camera" + bl_label = "Apply to Blender Camera" + bl_options = {'UNDO'} + bl_translation_context = 'BBP_OT_apply_virtools_camera' + + @classmethod + def poll(cls, context): + return context.camera is not None + + def execute(self, context): + cam: bpy.types.Camera = context.camera + apply_to_blender_camera(cam) + return {'FINISHED'} + +# Display Panel + +class BBP_PT_virtools_camera(bpy.types.Panel): + """Show Virtools Camera Properties""" + bl_label = "Virtools Camera" + bl_idname = "BBP_PT_virtools_camera" + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = "data" # idk why blender use `data` as the camera tab same as mesh. + bl_translation_context = 'BBP_PT_virtools_camera' + + @classmethod + def poll(cls, context): + return context.camera is not None + + def draw(self, context): + # get layout and target + layout = self.layout + cam: bpy.types.Camera = context.camera + props: BBP_PG_virtools_camera = get_virtools_camera(cam) + rawdata: RawVirtoolsCamera = get_raw_virtools_camera(cam) + + # draw operator + layout.operator( + BBP_OT_apply_virtools_camera.bl_idname, text='Apply', icon='NODETREE', + text_ctxt='BBP_PT_virtools_camera/draw') + + # draw data + layout.separator() + # show camera type first + layout.prop(props, 'projection_type') + # all camera has front and back plane + layout.label(text='Clipping', text_ctxt='BBP_PT_virtools_camera/draw') + sublayout = layout.column() + sublayout.use_property_split = True + sublayout.prop(props, 'front_plane') + sublayout.prop(props, 'back_plane') + + # only perspective camera has fov setting + if rawdata.mProjectionType == UTIL_virtools_types.CK_CAMERA_PROJECTION.CK_PERSPECTIVEPROJECTION: + layout.separator() + layout.label(text='Perspective Parameters', text_ctxt='BBP_PT_virtools_camera/draw') + sublayout = layout.column() + sublayout.use_property_split = True + sublayout.prop(props, 'fov') + + # only orthographic camera has orthographic zoom setting + if rawdata.mProjectionType == UTIL_virtools_types.CK_CAMERA_PROJECTION.CK_ORTHOGRAPHICPROJECTION: + layout.separator() + layout.label(text='Orthographic Parameters', text_ctxt='BBP_PT_virtools_camera/draw') + sublayout = layout.column() + sublayout.use_property_split = True + sublayout.prop(props, 'orthographic_zoom') + + # aspect ratio + layout.separator() + layout.label(text='Aspect Ratio', text_ctxt='BBP_PT_virtools_camera/draw') + sublayout = layout.row() + sublayout.use_property_split = False + sublayout.prop(props, 'aspect_ratio_w', text = '', expand = True) + sublayout.prop(props, 'aspect_ratio_h', text = '', expand = True) + +# Register + +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_PT_virtools_camera) + + # add into camera metadata + bpy.types.Camera.virtools_camera = bpy.props.PointerProperty(type = BBP_PG_virtools_camera) + +def unregister() -> None: + # remove from metadata + del bpy.types.Camera.virtools_camera + + bpy.utils.unregister_class(BBP_PT_virtools_camera) + bpy.utils.unregister_class(BBP_OT_apply_virtools_camera) + bpy.utils.unregister_class(BBP_PG_virtools_camera) + + diff --git a/bbp_ng/PROP_virtools_light.py b/bbp_ng/PROP_virtools_light.py index 12bf7e5..c0b6e5f 100644 --- a/bbp_ng/PROP_virtools_light.py +++ b/bbp_ng/PROP_virtools_light.py @@ -1,5 +1,4 @@ -import bpy, mathutils -from bpy.types import Context +import bpy import typing, math from . import UTIL_functions, UTIL_virtools_types diff --git a/bbp_ng/UTIL_virtools_types.py b/bbp_ng/UTIL_virtools_types.py index 15ffdc0..5846d6c 100644 --- a/bbp_ng/UTIL_virtools_types.py +++ b/bbp_ng/UTIL_virtools_types.py @@ -235,8 +235,8 @@ _g_Docstring: dict[type, dict[int, EnumDocstring]] = { VXMESH_LITMODE.VX_LITMESH.value: EnumDocstring("Lit", "Lighting is done by renderer using normals and face material information. "), }, CK_CAMERA_PROJECTION: { - CK_CAMERA_PROJECTION.CK_PERSPECTIVEPROJECTION.value: EnumDocstring("Perspectiveprojection", ""), - CK_CAMERA_PROJECTION.CK_ORTHOGRAPHICPROJECTION.value: EnumDocstring("Orthographicprojection", ""), + CK_CAMERA_PROJECTION.CK_PERSPECTIVEPROJECTION.value: EnumDocstring("Perspective projection", ""), + CK_CAMERA_PROJECTION.CK_ORTHOGRAPHICPROJECTION.value: EnumDocstring("Orthographic projection", ""), } } diff --git a/bbp_ng/__init__.py b/bbp_ng/__init__.py index 901fb99..a95fbdf 100644 --- a/bbp_ng/__init__.py +++ b/bbp_ng/__init__.py @@ -17,7 +17,7 @@ UTIL_icons_manager.register() # then load other modules from . import UTIL_translation -from . import PROP_preferences, PROP_ptrprop_resolver, PROP_virtools_material, PROP_virtools_texture, PROP_virtools_mesh, PROP_virtools_light, PROP_virtools_group +from . import PROP_preferences, PROP_ptrprop_resolver, PROP_virtools_material, PROP_virtools_texture, PROP_virtools_mesh, PROP_virtools_light, PROP_virtools_camera, PROP_virtools_group from . import PROP_ballance_element, PROP_bme_material, PROP_ballance_map_info from . import OP_IMPORT_bmfile, OP_EXPORT_bmfile, OP_IMPORT_virtools, OP_EXPORT_virtools from . import OP_UV_flatten_uv, OP_UV_rail_uv @@ -327,6 +327,7 @@ def register() -> None: PROP_virtools_texture.register() PROP_virtools_mesh.register() PROP_virtools_light.register() + PROP_virtools_camera.register() PROP_virtools_group.register() PROP_ballance_element.register() PROP_bme_material.register() @@ -397,6 +398,7 @@ def unregister() -> None: PROP_bme_material.unregister() PROP_ballance_element.unregister() PROP_virtools_group.unregister() + PROP_virtools_camera.unregister() PROP_virtools_light.unregister() PROP_virtools_mesh.unregister() PROP_virtools_texture.unregister()