├── ops ├── old │ ├── __init__.py │ ├── draw_utils │ │ ├── __init__.py │ │ ├── bl_ui_label.py │ │ ├── bezier.py │ │ ├── bl_ui_drag_panel.py │ │ ├── bl_ui_draw_op.py │ │ ├── bl_ui_widget.py │ │ ├── bl_ui_button.py │ │ ├── shader.py │ │ └── bl_ui_slider.py │ └── utils.py ├── motion │ ├── __init__.py │ ├── bake.py │ ├── add.py │ └── op_motion_cam.py ├── switch_camera.py ├── __init__.py ├── preview_camera.py ├── add_camera.py ├── snapshot.py └── adjust_camera_lens.py ├── gizmos ├── motion │ ├── __init__.py │ ├── curve.py │ └── gz_custom.py ├── public_gizmo.py ├── __init__.py ├── motion_move.py ├── button_2d.py └── preview_camera.py ├── debug.py ├── .gitignore ├── res ├── nodes │ └── process.blend ├── asset │ ├── CamerHelper.blend │ └── CamerHelperWithGizmo.blend ├── custom_shape │ ├── gz_shape.blend │ └── __init__.py ├── __init__.py └── translate │ ├── zh_CN.py │ └── __init__.py ├── ui ├── __init__.py ├── menu.py └── camera.py ├── blender_manifest.toml ├── register_module.py ├── update.py ├── __init__.py ├── utils ├── asset.py ├── gpu.py ├── color.py ├── area.py ├── __init__.py ├── gizmo.py └── property.py ├── README.md ├── preferences ├── ensure_asset_library_exists.py └── __init__.py └── camera_thumbnails.py /ops/old/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gizmos/motion/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /debug.py: -------------------------------------------------------------------------------- 1 | DEBUG_PREVIEW_CAMERA = False 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.blend1 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | .idea 7 | -------------------------------------------------------------------------------- /res/nodes/process.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AIGODLIKE/CameraHelper/HEAD/res/nodes/process.blend -------------------------------------------------------------------------------- /res/asset/CamerHelper.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AIGODLIKE/CameraHelper/HEAD/res/asset/CamerHelper.blend -------------------------------------------------------------------------------- /res/custom_shape/gz_shape.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AIGODLIKE/CameraHelper/HEAD/res/custom_shape/gz_shape.blend -------------------------------------------------------------------------------- /res/asset/CamerHelperWithGizmo.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AIGODLIKE/CameraHelper/HEAD/res/asset/CamerHelperWithGizmo.blend -------------------------------------------------------------------------------- /gizmos/public_gizmo.py: -------------------------------------------------------------------------------- 1 | 2 | class PublicGizmo: 3 | bl_space_type = 'VIEW_3D' 4 | bl_region_type = 'WINDOW' 5 | bl_options = {'PERSISTENT', 'SCALE', 'SHOW_MODAL_ALL'} -------------------------------------------------------------------------------- /res/__init__.py: -------------------------------------------------------------------------------- 1 | from . import translate 2 | 3 | 4 | def register(): 5 | translate.register() 6 | 7 | 8 | def unregister(): 9 | translate.unregister() 10 | -------------------------------------------------------------------------------- /ui/__init__.py: -------------------------------------------------------------------------------- 1 | from . import camera,menu 2 | 3 | def register(): 4 | camera.register() 5 | menu.register() 6 | 7 | 8 | def unregister(): 9 | camera.unregister() 10 | menu.unregister() 11 | -------------------------------------------------------------------------------- /ops/motion/__init__.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from .add import CAMHP_PT_add_motion_cams 3 | 4 | register_class, unregister_class = bpy.utils.register_classes_factory( 5 | ( 6 | CAMHP_PT_add_motion_cams, 7 | ) 8 | ) 9 | 10 | 11 | def register(): 12 | register_class() 13 | 14 | 15 | def unregister(): 16 | unregister_class() 17 | -------------------------------------------------------------------------------- /blender_manifest.toml: -------------------------------------------------------------------------------- 1 | name = "Camera Helper" 2 | id = "CameraHepler" 3 | maintainer = "ACGGIT, Atticus, 小萌新" 4 | version = "1.4.2" 5 | tagline = "Camera Helper with amazing tools" 6 | blender_version_min = "4.2.0" 7 | type = "add-on" 8 | schema_version = "1.0.0" 9 | tags = ["Render", "User Interface", "Camera"] 10 | license = ["SPDX:GPL-3.0-or-later"] 11 | -------------------------------------------------------------------------------- /register_module.py: -------------------------------------------------------------------------------- 1 | from . import ops, preferences, res, gizmos, ui, update 2 | 3 | module_list = [ 4 | ui, 5 | res, 6 | ops, 7 | update, 8 | gizmos, 9 | preferences, 10 | ] 11 | 12 | 13 | def register(): 14 | for mod in module_list: 15 | mod.register() 16 | 17 | 18 | def unregister(): 19 | for mod in module_list: 20 | mod.unregister() 21 | -------------------------------------------------------------------------------- /update.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.app.handlers import persistent 3 | 4 | 5 | @persistent 6 | def depsgraph_update_post(scene, depsgraph): 7 | from .ops.preview_camera import CameraThumbnails 8 | CameraThumbnails.update() 9 | 10 | 11 | def register(): 12 | bpy.app.handlers.depsgraph_update_post.append(depsgraph_update_post) 13 | 14 | 15 | def unregister(): 16 | bpy.app.handlers.depsgraph_update_post.remove(depsgraph_update_post) 17 | -------------------------------------------------------------------------------- /ops/switch_camera.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | class SwitchCamera(bpy.types.Operator): 5 | bl_idname = 'camhp.switch_camera' 6 | bl_label = 'Switch Camera' 7 | 8 | @classmethod 9 | def poll(cls, context): 10 | camera = getattr(context, "camera", None) 11 | return camera and camera.type == "CAMERA" 12 | 13 | def invoke(self, context, event): 14 | if camera := getattr(context, "camera", None): 15 | context.space_data.camera = camera 16 | return {"FINISHED"} 17 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "Camera Helper", 3 | "author": "ACGGIT, Atticus, 小萌新", 4 | "blender": (4, 2, 0), 5 | "version": (1, 4, 2), 6 | "category": "辣椒出品", 7 | "support": "COMMUNITY", 8 | "doc_url": "", 9 | "tracker_url": "", 10 | "description": "", 11 | "location": "3D视图右侧控件栏/进入相机视图", 12 | } 13 | 14 | from . import register_module 15 | 16 | 17 | def register(): 18 | register_module.register() 19 | 20 | 21 | def unregister(): 22 | register_module.unregister() 23 | -------------------------------------------------------------------------------- /utils/asset.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from pathlib import Path 3 | 4 | 5 | class AssetDir(Enum): 6 | ASSET_BLEND = 'CamerHelper.blend' 7 | ASSET_BLEND_WITH_GIZMO = 'CamerHelperWithGizmo.blend' 8 | 9 | 10 | def get_asset_dir(subpath=None): 11 | """custom dir""" 12 | preset_dir = Path(__file__).parent.parent.joinpath("res", "asset") 13 | 14 | print("get_asset_dir", preset_dir) 15 | if subpath in [item.value for item in AssetDir]: 16 | return preset_dir.joinpath(subpath) 17 | 18 | return preset_dir 19 | -------------------------------------------------------------------------------- /utils/gpu.py: -------------------------------------------------------------------------------- 1 | import gpu 2 | from gpu_extras.batch import batch_for_shader 3 | 4 | 5 | def draw_box(x1, x2, y1, y2, color=[0, 0, 0, 0.5]): 6 | indices = ((0, 1, 2), (2, 1, 3)) 7 | 8 | vertices = ((x1, y1), (x2, y1), (x1, y2), (x2, y2)) 9 | # draw area 10 | shader = gpu.shader.from_builtin('UNIFORM_COLOR') 11 | batch = batch_for_shader(shader, 12 | 'TRIS', {"pos": vertices}, 13 | indices=indices) 14 | shader.bind() 15 | shader.uniform_float("color", color) 16 | batch.draw(shader) 17 | -------------------------------------------------------------------------------- /gizmos/__init__.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from .button_2d import Button2DGizmos 4 | from .motion_move import MotionCameraAdjustGizmo, MotionCameraAdjustGizmos 5 | from .preview_camera import PreviewCameraGizmos, PreviewCameraAreaGizmo 6 | 7 | classes = ( 8 | Button2DGizmos, 9 | 10 | PreviewCameraAreaGizmo, 11 | PreviewCameraGizmos, 12 | 13 | # MotionCameraAdjustGizmo, 14 | # MotionCameraAdjustGizmos, 15 | ) 16 | 17 | register_class, unregister_class = bpy.utils.register_classes_factory(classes) 18 | 19 | 20 | def register(): 21 | register_class() 22 | 23 | 24 | def unregister(): 25 | unregister_class() 26 | -------------------------------------------------------------------------------- /ops/__init__.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from .add_camera import AddCamera 4 | from .adjust_camera_lens import AdjustCameraLens 5 | from .preview_camera import PreviewCamera 6 | from .snapshot import Snapshot 7 | from .switch_camera import SwitchCamera 8 | from . import motion 9 | register_class, unregister_class = bpy.utils.register_classes_factory(( 10 | AddCamera, 11 | PreviewCamera, 12 | AdjustCameraLens, 13 | 14 | Snapshot, 15 | SwitchCamera, 16 | )) 17 | 18 | 19 | def register(): 20 | register_class() 21 | motion.register() 22 | 23 | def unregister(): 24 | unregister_class() 25 | motion.unregister() 26 | -------------------------------------------------------------------------------- /ops/old/draw_utils/__init__.py: -------------------------------------------------------------------------------- 1 | import gpu 2 | # import bgl 3 | from contextlib import contextmanager 4 | 5 | @contextmanager 6 | def wrap_gpu_state(): 7 | gpu.state.blend_set('ALPHA') 8 | yield 9 | gpu.state.blend_set('NONE') 10 | 11 | @contextmanager 12 | def wrap_bgl_restore(width): 13 | # bgl.glEnable(bgl.GL_BLEND) 14 | # bgl.glEnable(bgl.GL_LINE_SMOOTH) 15 | # bgl.glEnable(bgl.GL_DEPTH_TEST) 16 | # bgl.glLineWidth(width) 17 | # bgl.glPointSize(8) 18 | ori_blend = gpu.state.blend_get() 19 | gpu.state.blend_set('ALPHA') 20 | gpu.state.line_width_set(width) 21 | gpu.state.point_size_set(8) 22 | 23 | yield # do the work 24 | # restore opengl defaults 25 | # bgl.glDisable(bgl.GL_BLEND) 26 | # bgl.glDisable(bgl.GL_LINE_SMOOTH) 27 | # bgl.glEnable(bgl.GL_DEPTH_TEST) 28 | # bgl.glLineWidth(1) 29 | # bgl.glPointSize(5) 30 | gpu.state.blend_set(ori_blend) 31 | gpu.state.line_width_set(1) 32 | gpu.state.point_size_set(5) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Camera Helper 2 | 3 | This is an addon for Blender that can easily create motion camera animation. 4 | 5 | ## Version 6 | 7 | v1.5.0: for Blender 5.0 higher (developing) 8 | 9 | v1.4.2: for Blender 4.2 higher 10 | 11 | ## Usage 12 | 13 | 1. Create few cameras to set the path of motion camera, you can create by yourself, its just a convient way 14 | 15 | ![GIF 2024-8-27 16-56-30](https://github.com/user-attachments/assets/ba16cdda-3ca2-4c11-91bd-7639b3491189) 16 | 17 | 2. Hold Ctrl, Left-Click the camera button to enter the curve create mode, select cameras in order, then Right-Click to 18 | exit this mode 19 | 20 | ![GIF 2024-8-27 16-57-09](https://github.com/user-attachments/assets/8c99727a-5533-4255-a221-e34e52584c5d) 21 | 22 | 3. Then you can modify the animation in Modifier Attribute menu with Geometry nodes 23 | 24 | ![GIF 2024-8-27 17-01-46](https://github.com/user-attachments/assets/16d6af5f-c6e9-4f71-9a1d-67672a2708fd) 25 | 26 | ## Notice 27 | 28 | - This addon is based on Geometry Nodes, some nodes may will be delete in future editions \(like Transfer Attribute\) 29 | -------------------------------------------------------------------------------- /utils/color.py: -------------------------------------------------------------------------------- 1 | from math import fabs 2 | 3 | import numpy as np 4 | 5 | """ 6 | version 1.0.0 7 | """ 8 | 9 | def clamp(a, b, c): 10 | if a < b: 11 | return b 12 | elif a > c: 13 | return c 14 | return a 15 | 16 | 17 | def hsv_to_rgb(h, s, v): 18 | nr = fabs(h * 6.0 - 3.0) - 1.0 19 | ng = 2.0 - fabs(h * 6.0 - 2.0) 20 | nb = 2.0 - fabs(h * 6.0 - 4.0) 21 | 22 | nr = clamp(nr, 0.0, 1.0) 23 | nb = clamp(nb, 0.0, 1.0) 24 | ng = clamp(ng, 0.0, 1.0) 25 | 26 | r_r = ((nr - 1.0) * s + 1.0) * v 27 | r_g = ((ng - 1.0) * s + 1.0) * v 28 | r_b = ((nb - 1.0) * s + 1.0) * v 29 | 30 | return ( 31 | r_r, 32 | r_g, 33 | r_b, 34 | ) 35 | 36 | 37 | def linear_to_srgb(c_linear): 38 | # 对每个颜色分量进行伽马校正 39 | c_srgb = np.where(c_linear <= 0.0031308, 12.92 * c_linear, 1.055 * (c_linear ** (1 / 2.4)) - 0.055) 40 | return c_srgb # 假设image是一个linear RGB图像 41 | 42 | 43 | def srgb_to_linear(c_srgb): 44 | # 对每个颜色分量进行逆伽马校正 45 | c_linear = np.where(c_srgb <= 0.04045, c_srgb / 12.92, ((c_srgb + 0.055) / 1.055) ** 2.4) 46 | return c_linear 47 | -------------------------------------------------------------------------------- /res/translate/zh_CN.py: -------------------------------------------------------------------------------- 1 | data = { 2 | "Add View Camera\nCtrl Left Click: Add Motion Camera": "添加视角相机\nCtrl+左键点击: 添加运动摄像机", 3 | "Use Cursor to Adjust Camera Lens": "以游标为基准调准相机焦距", 4 | "Switch Camera": "切换相机", 5 | "Motion Camera": "动态相机", 6 | "Add Motion Camera": "添加动态相机", 7 | "Left Click->Camera Name to Add Source": "左键->相机名字以添加源", 8 | "Right Click->End Add Mode": "右键->结束添加模式", 9 | "ESC->Cancel": "ESC->取消", 10 | "Sync to Selected": "同步到选中项", 11 | "Draw Modal": "模态绘制", 12 | "Motion Curve": "运动曲线", 13 | "Sub Camera": "子级相机", 14 | "There is no camera as child of this object": "此对象子级中没有相机物体", 15 | "Bake Motion Camera": "烘焙运动相机", 16 | "Pin Selected Camera": "固定选中相机", 17 | "Max Width": "最大宽度", 18 | "Max Height": "最大高度", 19 | "Camera Thumbnails": "相机缩略图", 20 | "Camera Thumbnails\nLeft Click: Enable\nCtrl: Pin Selected Camera\nCtrl Shift Click: Send to Viewer": "相机缩略图\n左键点击: 启用\nCtrl: 固定选中相机\nCtrl Shift 左键:生成缩略图", 21 | "Camera Helper": "相机助手", 22 | "Camera Settings": "相机设置", 23 | "Snapshot": "快照", 24 | "Not find camera": "未找到相机", 25 | "Please select a camera":"请选择一个相机" 26 | } 27 | -------------------------------------------------------------------------------- /ops/preview_camera.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from ..camera_thumbnails import CameraThumbnails 4 | from ..utils import get_operator_bl_idname, get_camera 5 | 6 | 7 | class PreviewCamera(bpy.types.Operator): 8 | """Camera Thumbnails\nLeft Click: Enable\nCtrl: Pin Selected Camera\nCtrl Shift Click: Send to Viewer""" 9 | bl_idname = get_operator_bl_idname("preview_camera") 10 | bl_label = "Preview Camera" 11 | 12 | check_data = {} 13 | camera_data = {} 14 | 15 | @classmethod 16 | def poll(cls, context): 17 | return context.space_data.type == "VIEW_3D" 18 | 19 | def invoke(self, context, event): 20 | camera = get_camera(context) 21 | if camera is None and not CameraThumbnails.check_is_draw(context): 22 | self.report({'ERROR'}, "Please select a camera") 23 | return {'CANCELLED'} 24 | 25 | print("camera", camera) 26 | if event.shift and event.ctrl: 27 | bpy.ops.camhp.pv_snap_shot("INVOKE_DEFAULT") 28 | elif event.ctrl: 29 | CameraThumbnails.pin_selected_camera(context, camera) 30 | else: 31 | CameraThumbnails.switch_preview(context, camera) 32 | context.area.tag_redraw() 33 | return {"FINISHED"} 34 | -------------------------------------------------------------------------------- /res/translate/__init__.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | class TranslationHelper: 5 | def __init__(self, name: str, data: dict, lang='zh_CN'): 6 | self.name = name 7 | self.translations_dict = dict() 8 | 9 | for src, src_trans in data.items(): 10 | key = ("Operator", src) 11 | self.translations_dict.setdefault(lang, {})[key] = src_trans 12 | key = ("*", src) 13 | self.translations_dict.setdefault(lang, {})[key] = src_trans 14 | 15 | def register(self): 16 | try: 17 | bpy.app.translations.register(self.name, self.translations_dict) 18 | except(ValueError): 19 | pass 20 | 21 | def unregister(self): 22 | bpy.app.translations.unregister(self.name) 23 | 24 | 25 | # Set 26 | ############ 27 | from . import zh_CN 28 | 29 | camhp_zh_CN = TranslationHelper('camhp_zh_CN', zh_CN.data) 30 | camhp_zh_HANS = TranslationHelper('camhp_zh_HANS', zh_CN.data, lang='zh_HANS') 31 | 32 | 33 | def register(): 34 | if bpy.app.version < (4, 0, 0): 35 | camhp_zh_CN.register() 36 | else: 37 | camhp_zh_CN.register() 38 | camhp_zh_HANS.register() 39 | 40 | 41 | def unregister(): 42 | if bpy.app.version < (4, 0, 0): 43 | camhp_zh_CN.unregister() 44 | else: 45 | camhp_zh_CN.register() 46 | camhp_zh_HANS.unregister() 47 | -------------------------------------------------------------------------------- /utils/area.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from mathutils import Vector 3 | 4 | 5 | def area_offset(context) -> Vector: 6 | """ 7 | {'FOOTER', 8 | 'ASSET_SHELF_HEADER', 9 | 'NAVIGATION_BAR', 10 | 'UI', 11 | 'TOOL_HEADER', 12 | 'ASSET_SHELF', 13 | 'HEADER', 14 | 'CHANNELS', 'HUD', 'TOOLS', 'EXECUTE', 'WINDOW'} 15 | """ 16 | x = y = 0 17 | area = context.area 18 | if area is None: 19 | return Vector((x, y)) 20 | for region in area.regions: 21 | if region.type == "TOOLS": 22 | x += region.width 23 | elif region.type == "HEADER": 24 | y += region.height 25 | elif region.type == "TOOL_HEADER": 26 | y += region.height 27 | return Vector((x, y)) 28 | 29 | 30 | def get_area_max_parent(area: bpy.types.Area): 31 | """如果当前area是最大化的 32 | 则反回未最大化之前的area""" 33 | screen = bpy.context.screen 34 | if screen.show_fullscreen: 35 | if bpy.context.screen.name.endswith("-nonnormal"): # 当前屏幕为最大化时,获取最大化之前的屏幕 36 | name = screen.name.replace("-nonnormal", "") 37 | screen = bpy.data.screens.get(name, None) 38 | if screen: 39 | for i in screen.areas: 40 | if i.type == "EMPTY": 41 | return i 42 | return area 43 | -------------------------------------------------------------------------------- /ops/add_camera.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from ..utils import get_operator_bl_idname 4 | 5 | 6 | class AddCamera(bpy.types.Operator): 7 | """Add View Camera\nCtrl Left Click: Add Motion Camera""" 8 | bl_idname = get_operator_bl_idname("add_camera") 9 | bl_label = 'Add View Camera' 10 | bl_options = {'REGISTER', 'UNDO'} 11 | 12 | @classmethod 13 | def poll(cls, context): 14 | return context.area and context.area.type == 'VIEW_3D' 15 | 16 | def invoke(self, context, event): 17 | if event.ctrl: 18 | bpy.ops.camhp.add_motion_cams('INVOKE_DEFAULT') 19 | bpy.ops.ed.undo_push() 20 | return {'FINISHED'} 21 | else: 22 | return self.execute(context) 23 | 24 | def execute(self, context): 25 | # 创建相机 26 | cam_data = bpy.data.cameras.new(name='Camera') 27 | cam = bpy.data.objects.new('Camera', cam_data) 28 | context.collection.objects.link(cam) 29 | # 设置 30 | cam.data.show_name = True 31 | # 进入视图 32 | context.scene.camera = cam 33 | context.view_layer.objects.active = cam 34 | try: 35 | bpy.ops.view3d.camera_to_view("INVOKE_DEFAULT") 36 | except: 37 | pass 38 | 39 | area = context.area 40 | space = [i for i in area.spaces if i.type == "VIEW_3D"][0] 41 | r3d = space.region_3d 42 | r3d.view_camera_zoom = 0 43 | 44 | context.region.tag_redraw() 45 | 46 | return {"FINISHED"} 47 | -------------------------------------------------------------------------------- /preferences/ensure_asset_library_exists.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from ..utils import get_asset_dir 3 | 4 | 5 | def ensure_asset_library_exists(lock, passedSleepTime): 6 | import time 7 | import bpy 8 | 9 | # check if not background mode 10 | if bpy.app.background: return 11 | 12 | while not hasattr(bpy.context, 'preferences') or not hasattr(bpy.context,'view_layer'): 13 | time.sleep(0.5) 14 | 15 | libraries = bpy.context.preferences.filepaths.asset_libraries 16 | asset_dir = str(get_asset_dir()) 17 | 18 | if 'CameraHelper' not in libraries: 19 | print('CameraHelper not in libraries') 20 | bpy.ops.preferences.asset_library_add() 21 | library = libraries[-1] 22 | library.name = 'CameraHelper' 23 | library.path = asset_dir 24 | bpy.ops.wm.save_userpref() 25 | 26 | for library in libraries: 27 | if library.name == "CameraHelper" and library.path != asset_dir: 28 | print('CameraHelper path not correct') 29 | library.path = asset_dir 30 | bpy.ops.wm.save_userpref() 31 | break 32 | 33 | 34 | def register(): 35 | preferences.register() 36 | data_keymap.register() 37 | 38 | lock = threading.Lock() 39 | lock_holder = threading.Thread(target=ensure_asset_library_exists, args=(lock, 5), name='Init_Preferences') 40 | lock_holder.daemon = True 41 | lock_holder.start() 42 | 43 | 44 | def unregister(): 45 | preferences.unregister() 46 | data_keymap.unregister() 47 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from .. import __package__ as base_name 4 | 5 | 6 | def get_pref(): 7 | """get preferences of this addon""" 8 | return bpy.context.preferences.addons[base_name].preferences 9 | 10 | 11 | def get_operator_bl_idname(suffix: str) -> str: 12 | return f"camera_helper.{suffix}" 13 | 14 | 15 | def get_menu_bl_idname(suffix: str) -> str: 16 | return f"CAMERA_HELPER_MT_{suffix}" 17 | 18 | 19 | def get_panel_bl_idname(suffix: str) -> str: 20 | return f"CAMERA_HELPER_PT_{suffix}" 21 | 22 | 23 | def get_camera(context) -> "None | bpy.types.Camera": 24 | """ 25 | for obj in context.selected_objects: 26 | if obj.type == "CAMERA": 27 | return obj 28 | for obj in context.scene.objects: 29 | if obj.type == "CAMERA": 30 | return obj 31 | if context.scene.camera: 32 | return context.scene.camera 33 | """ 34 | if hasattr(context, "object") and context.object and context.object.type == "CAMERA": 35 | return context.object 36 | return None 37 | 38 | 39 | def get_camera_preview_size(context): 40 | pref = get_pref() 41 | max_height = pref.camera_thumb.max_width 42 | max_width = pref.camera_thumb.max_height 43 | height = max_height 44 | ratio = context.scene.render.resolution_x / context.scene.render.resolution_y 45 | width = int(height * ratio) 46 | if width > max_width: 47 | width = max_width 48 | height = int(width / ratio) 49 | return width, height 50 | -------------------------------------------------------------------------------- /res/custom_shape/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import bmesh 4 | import bpy 5 | import numpy as np 6 | 7 | def load_shape_geo_obj(obj_name='ROTATE'): 8 | """ 加载一个几何形状的模型,用于绘制几何形状的控件 """ 9 | gz_shape_path = Path(__file__).parent.joinpath('gz_shape.blend') 10 | with bpy.data.libraries.load(str(gz_shape_path)) as (data_from, data_to): 11 | data_to.objects = [obj_name] 12 | return data_to.objects[0] 13 | 14 | 15 | def create_geo_shape(obj=None, shape_type='TRIS', scale=1): 16 | """ 创建一个几何形状,默认创造球体 17 | """ 18 | if obj: 19 | tmp_mesh = obj.data 20 | else: 21 | tmp_mesh = bpy.data.meshes.new('tmp') 22 | bm = bmesh.new() 23 | bmesh.ops.create_uvsphere(bm, u_segments=16, v_segments=8, radius=scale / 5, calc_uvs=True) 24 | bm.to_mesh(tmp_mesh) 25 | bm.free() 26 | 27 | mesh = tmp_mesh 28 | vertices = np.zeros((len(mesh.vertices), 3), 'f') 29 | mesh.vertices.foreach_get("co", vertices.ravel()) 30 | mesh.calc_loop_triangles() 31 | 32 | if shape_type == 'LINES': 33 | edges = np.zeros((len(mesh.edges), 2), 'i') 34 | mesh.edges.foreach_get("vertices", edges.ravel()) 35 | custom_shape_verts = vertices[edges].reshape(-1, 3) 36 | else: 37 | tris = np.zeros((len(mesh.loop_triangles), 3), 'i') 38 | mesh.loop_triangles.foreach_get("vertices", tris.ravel()) 39 | custom_shape_verts = vertices[tris].reshape(-1, 3) 40 | 41 | bpy.data.meshes.remove(mesh) 42 | 43 | return custom_shape_verts 44 | -------------------------------------------------------------------------------- /ops/old/draw_utils/bl_ui_label.py: -------------------------------------------------------------------------------- 1 | from . bl_ui_widget import BL_UI_Widget 2 | from .shader import wrap_blf_size 3 | import blf 4 | 5 | class BL_UI_Label(BL_UI_Widget): 6 | 7 | def __init__(self, x, y, width, height): 8 | super().__init__(x, y, width, height) 9 | 10 | self._text_color = (1.0, 1.0, 1.0, 1.0) 11 | self._text = "Label" 12 | self._text_size = 16 13 | 14 | @property 15 | def text_color(self): 16 | return self._text_color 17 | 18 | @text_color.setter 19 | def text_color(self, value): 20 | self._text_color = value 21 | 22 | @property 23 | def text(self): 24 | return self._text 25 | 26 | @text.setter 27 | def text(self, value): 28 | self._text = value 29 | 30 | @property 31 | def text_size(self): 32 | return self._text_size 33 | 34 | @text_size.setter 35 | def text_size(self, value): 36 | self._text_size = value 37 | 38 | def is_in_rect(self, x, y): 39 | return False 40 | 41 | def draw(self): 42 | if not self.visible: 43 | return 44 | 45 | area_height = self.get_area_height() 46 | 47 | wrap_blf_size(0,self._text_size) 48 | size = blf.dimensions(0, self._text) 49 | 50 | textpos_y = area_height - self.y_screen - self.height 51 | blf.position(0, self.x_screen, textpos_y, 0) 52 | 53 | r, g, b, a = self._text_color 54 | 55 | blf.color(0, r, g, b, a) 56 | 57 | blf.draw(0, self._text) -------------------------------------------------------------------------------- /utils/gizmo.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | def offset_2d_gizmo(context, gizmo, offset_step): 5 | # ui scale 6 | ui_scale = bpy.context.preferences.system.dpi * (1 / 72) 7 | region = context.region 8 | 9 | step = 30 * ui_scale 10 | icon_scale = (80 * 0.35) / 2 # 14 11 | # 从屏幕右侧起 12 | start_x = region.width - (icon_scale * ui_scale + step) / 2 13 | start_y = region.height 14 | 15 | # 检查是否启用区域重叠,若启用则加上宽度以符合侧面板移动 16 | if context.preferences.system.use_region_overlap: 17 | for region in context.area.regions: 18 | if region.type == 'UI': 19 | start_x -= region.width 20 | elif region.type == 'HEADER': 21 | start_y -= region.height 22 | 23 | # 检查是否开启坐标轴 24 | if context.preferences.view.mini_axis_type == 'MINIMAL': 25 | size = context.preferences.view.mini_axis_size * ui_scale * 2 # 获取实际尺寸 此尺寸需要乘2 26 | start_y -= size + step * 2 # 27 | elif context.preferences.view.mini_axis_type == 'GIZMO': 28 | size = context.preferences.view.gizmo_size_navigate_v3d * ui_scale * 1.2 # 获取实际尺寸 此尺寸需要乘1.2 29 | start_y -= size + step * 2 # 30 | elif context.preferences.view.mini_axis_type == 'NONE': 31 | start_y -= step * 2 32 | 33 | # 检查是否开启默认控件 34 | if context.preferences.view.show_navigate_ui: 35 | start_y -= (icon_scale * ui_scale + step) * 3 36 | else: 37 | start_y -= step * 2 * ui_scale 38 | 39 | gizmo.matrix_basis[0][3] = start_x 40 | gizmo.matrix_basis[1][3] = start_y - step * offset_step 41 | gizmo.scale_basis = icon_scale 42 | -------------------------------------------------------------------------------- /ops/old/draw_utils/bezier.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from mathutils.geometry import interpolate_bezier 3 | 4 | class CubicBezier(object): 5 | def __init__(self, points): 6 | self.points = np.array(points).astype(np.float32) 7 | 8 | def at(self, t): 9 | pt = 1 * (1 - t) ** 3 * self.points[0] 10 | pt += 3 * t ** 1 * (1 - t) ** 2 * self.points[1] 11 | pt += 3 * t ** 2 * (1 - t) ** 1 * self.points[2] 12 | pt += 1 * t ** 3 * self.points[3] 13 | return pt 14 | 15 | def split(self, t): 16 | p1, p2, p3, p4 = self.points 17 | 18 | p12 = (p2 - p1) * t + p1 19 | p23 = (p3 - p2) * t + p2 20 | p34 = (p4 - p3) * t + p3 21 | p123 = (p23 - p12) * t + p12 22 | p234 = (p34 - p23) * t + p23 23 | p1234 = (p234 - p123) * t + p123 24 | 25 | return [p1, p12, p123, p1234, p234, p34, p4] 26 | 27 | 28 | def beziers_from_spline(spline, mat): 29 | spline_beziers = [] 30 | 31 | pt_count = len(spline.bezier_points) 32 | for i in range(pt_count if spline.use_cyclic_u else pt_count - 1): 33 | bezier_points = [mat @ spline.bezier_points[i].co, 34 | mat @ spline.bezier_points[i].handle_right, 35 | mat @ spline.bezier_points[i - pt_count + 1].handle_left, 36 | mat @ spline.bezier_points[i - pt_count + 1].co 37 | ] 38 | 39 | spline_beziers.append(CubicBezier(bezier_points)) 40 | return spline_beziers 41 | 42 | 43 | def sample_spline_split(spline_beziers, samples=12): 44 | """对曲线点进行采样,返回采样点""" 45 | pts = [] 46 | i = 0 47 | while i < 1: 48 | i += 1 / samples 49 | split = spline_beziers.split(i) 50 | pts.append(split[3]) # 2,4 handle left/right, 3 middle 51 | 52 | return pts 53 | -------------------------------------------------------------------------------- /ops/old/draw_utils/bl_ui_drag_panel.py: -------------------------------------------------------------------------------- 1 | from . bl_ui_widget import BL_UI_Widget 2 | 3 | class BL_UI_Drag_Panel(BL_UI_Widget): 4 | 5 | def __init__(self, x, y, width, height): 6 | super().__init__(x,y, width, height) 7 | self.drag_offset_x = 0 8 | self.drag_offset_y = 0 9 | self.is_drag = False 10 | self.widgets = [] 11 | 12 | def set_location(self, x, y): 13 | super().set_location(x,y) 14 | self.layout_widgets() 15 | 16 | def add_widget(self, widget): 17 | self.widgets.append(widget) 18 | 19 | def add_widgets(self, widgets): 20 | self.widgets = widgets 21 | self.layout_widgets() 22 | 23 | def layout_widgets(self): 24 | for widget in self.widgets: 25 | widget.update(self.x_screen + widget.x, self.y_screen + widget.y) 26 | 27 | def update(self, x, y): 28 | super().update(x - self.drag_offset_x, y + self.drag_offset_y) 29 | 30 | def child_widget_focused(self, x, y): 31 | for widget in self.widgets: 32 | if widget.is_in_rect(x, y): 33 | return True 34 | return False 35 | 36 | def mouse_down(self, x, y): 37 | if self.child_widget_focused(x, y): 38 | return False 39 | 40 | if self.is_in_rect(x,y): 41 | height = self.get_area_height() 42 | self.is_drag = True 43 | self.drag_offset_x = x - self.x_screen 44 | self.drag_offset_y = y - (height - self.y_screen) 45 | return True 46 | 47 | return False 48 | 49 | def mouse_move(self, x, y): 50 | if self.is_drag: 51 | height = self.get_area_height() 52 | self.update(x, height - y) 53 | self.layout_widgets() 54 | 55 | def mouse_up(self, x, y): 56 | self.is_drag = False 57 | self.drag_offset_x = 0 58 | self.drag_offset_y = 0 -------------------------------------------------------------------------------- /ui/menu.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | class CAMERA_HELPER_MT_Switch_Camera(bpy.types.Menu): 5 | bl_label = "Switch Camera" 6 | bl_space_type = "VIEW_3D" 7 | bl_region_type = "WINDOW" 8 | 9 | def draw(self, context): 10 | from ..ops.switch_camera import SwitchCamera 11 | layout = self.layout 12 | layout.operator_context = "INVOKE_DEFAULT" 13 | for i, obj in enumerate(bpy.data.objects): 14 | if obj.type != "CAMERA": 15 | continue 16 | name = obj.name 17 | if obj is context.scene.camera: 18 | icon = "VIEW_CAMERA" 19 | else: 20 | icon = "CAMERA_DATA" 21 | layout.context_pointer_set("camera", obj) 22 | layout.operator(SwitchCamera.bl_idname, text=name, icon=icon, translate=False) 23 | 24 | 25 | class CAMHP_MT_popup_cam_settings(bpy.types.Menu): 26 | """Properties""" 27 | bl_label = "Camera Settings" 28 | bl_space_type = "VIEW_3D" 29 | bl_region_type = "WINDOW" 30 | 31 | def draw(self, context): 32 | layout = self.layout 33 | camera_obj = context.space_data.camera 34 | if camera_obj: 35 | layout.context_pointer_set("camera", camera_obj.data) 36 | layout.popover("DATA_PT_camera") 37 | layout.popover("DATA_PT_lens") 38 | layout.popover("DATA_PT_camera_display") 39 | 40 | layout.separator() 41 | if context.engine == "CYCLES": 42 | layout.popover("CYCLES_CAMERA_PT_dof") 43 | else: 44 | layout.popover("DATA_PT_camera_dof") 45 | layout.separator() 46 | layout.popover("DATA_PT_camera_safe_areas") 47 | 48 | else: 49 | layout.label(text="No Camera Selected") 50 | 51 | 52 | def register(): 53 | bpy.utils.register_class(CAMERA_HELPER_MT_Switch_Camera) 54 | bpy.utils.register_class(CAMHP_MT_popup_cam_settings) 55 | 56 | 57 | def unregister(): 58 | bpy.utils.unregister_class(CAMERA_HELPER_MT_Switch_Camera) 59 | bpy.utils.unregister_class(CAMHP_MT_popup_cam_settings) 60 | -------------------------------------------------------------------------------- /ops/snapshot.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import numpy as np 3 | 4 | from ..utils import get_camera 5 | from ..utils.color import linear_to_srgb 6 | 7 | 8 | def save_texture_to_image(context, camera, texture): 9 | camera_name = camera.name 10 | 11 | x = context.scene.render.resolution_x 12 | y = context.scene.render.resolution_y 13 | key = f"Snap_Shot_{camera_name}" 14 | if key in bpy.data.images: 15 | img = bpy.data.images[key] 16 | bpy.data.images.remove(img) 17 | img = bpy.data.images.new(name=key, 18 | width=x, 19 | height=y) 20 | try: 21 | data = np.asarray(texture.read(), dtype=np.uint8) 22 | image_float = data.astype(np.float32) / 255.0 23 | image_float = image_float.transpose((2, 1, 0)).ravel() 24 | img.pixels.foreach_set(linear_to_srgb(image_float)) 25 | except TypeError as e: 26 | print(e) 27 | 28 | bpy.ops.render.view_show('INVOKE_DEFAULT') 29 | for area in context.screen.areas: 30 | if area.type == 'IMAGE_EDITOR': 31 | for space in area.spaces: 32 | if space.type == 'IMAGE_EDITOR': 33 | space.image = img 34 | break 35 | return key 36 | 37 | class Snapshot(bpy.types.Operator): 38 | """Snap Shot""" 39 | bl_idname = "camhp.pv_snap_shot" 40 | bl_label = "Snapshot" 41 | 42 | def invoke(self, context, event): 43 | from ..camera_thumbnails import CameraThumbnails 44 | area = context.area 45 | camera = None 46 | 47 | CameraThumbnails.update() 48 | if camera_data := CameraThumbnails.get_camera_data(area): 49 | if camera_name := camera_data.get("camera_name", None): 50 | camera = context.scene.objects.get(camera_name, None) 51 | if camera is None: 52 | camera = get_camera(context) 53 | 54 | if camera: 55 | space = context.space_data 56 | ori_show_overlay = space.overlay.show_overlays 57 | space.overlay.show_overlays = False 58 | 59 | texture = CameraThumbnails.update_camera_texture(context, camera, True) 60 | save_texture_to_image(context, camera, texture) 61 | space.overlay.show_overlays = ori_show_overlay 62 | 63 | self.report({'INFO'}, 'Snapshot') 64 | return {'FINISHED'} 65 | self.report({'ERROR'}, 'Not find camera') 66 | return {'CANCELLED'} 67 | -------------------------------------------------------------------------------- /gizmos/motion_move.py: -------------------------------------------------------------------------------- 1 | import blf 2 | import bpy 3 | import gpu.matrix 4 | from mathutils import Vector 5 | 6 | from .public_gizmo import PublicGizmo 7 | 8 | 9 | class MotionCameraAdjustGizmo(bpy.types.Gizmo): 10 | bl_idname = "MOTION_CAMERA_ADJUST_GT_gizmo" 11 | bl_options = {"PERSISTENT", "SCALE", "SHOW_MODAL_ALL", "UNDO", "GRAB_CURSOR"} 12 | 13 | def invoke(self, context, event): 14 | print("invoke") 15 | return {"RUNNING_MODAL"} 16 | 17 | def modal(self, context, event, tweak): 18 | print(self.bl_idname, "modal", event.type, event.value) 19 | if event.type == "LEFTMOUSE": 20 | return {"FINISHED"} 21 | elif event.type in {"RIGHTMOUSE", "ESC"}: 22 | return {"CANCELLED"} 23 | mouse = Vector((event.mouse_region_x, event.mouse_region_y)) 24 | diff = self.start_mouse - mouse 25 | self.offset_after = offset = self.start_offset + Vector((-diff.x, diff.y)) 26 | context.area.tag_redraw() 27 | return {"RUNNING_MODAL"} 28 | 29 | def exit(self, context, cancel): 30 | print("exit", cancel) 31 | 32 | def draw(self, context): 33 | with gpu.matrix.push_pop(): 34 | obj = context.object 35 | 36 | depsgraph = context.evaluated_depsgraph_get() 37 | evaluated_obj = context.object.evaluated_get(depsgraph) 38 | 39 | gpu.state.depth_mask_set(False) 40 | blf.draw(0, f"aaa {evaluated_obj.name} {len(evaluated_obj.data.vertices)}") 41 | 42 | # def test_select(self, context, mouse_pos): 43 | # return False 44 | 45 | def refresh(self, context): 46 | print(self.bl_idname, "refresh") 47 | 48 | 49 | class MotionCameraAdjustGizmos(bpy.types.GizmoGroup, PublicGizmo): 50 | bl_idname = "MOTION_CAMERA_ADJUST_GT_gizmos" 51 | bl_label = "Motion Camera Adjust Gizmos" 52 | 53 | @classmethod 54 | def poll(cls, context): 55 | obj = context.object 56 | return ( 57 | obj and 58 | obj.type == "MESH" and 59 | obj.modifiers.active and 60 | obj.modifiers.active.type == "NODES" and 61 | obj.modifiers.active.name == "MotionCamera" 62 | ) 63 | 64 | def setup(self, context): 65 | print("setup") 66 | gz = self.preview_camera = self.gizmos.new(MotionCameraAdjustGizmo.bl_idname) 67 | gz.use_draw_modal = True 68 | 69 | def refresh(self, context): 70 | context.area.tag_redraw() 71 | 72 | def draw_prepare(self, context): 73 | ... 74 | -------------------------------------------------------------------------------- /ops/old/draw_utils/bl_ui_draw_op.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from bpy.types import Operator 4 | from .shader import wrap_blf_size 5 | 6 | class BL_UI_OT_draw_operator(Operator): 7 | bl_idname = "object.bl_ui_ot_draw_operator" 8 | bl_label = "bl ui widgets operator" 9 | bl_description = "Operator for bl ui widgets" 10 | bl_options = {'REGISTER'} 11 | 12 | def __init__(self, *args, **kwargs): 13 | super().__init__(*args, **kwargs) 14 | self.draw_handle = None 15 | self.draw_event = None 16 | self._finished = False 17 | 18 | self.widgets = [] 19 | 20 | def init_widgets(self, context, widgets): 21 | self.widgets = widgets 22 | for widget in self.widgets: 23 | widget.init(context) 24 | 25 | def on_invoke(self, context, event): 26 | pass 27 | 28 | def on_finish(self, context): 29 | self._finished = True 30 | 31 | def invoke(self, context, event): 32 | 33 | self.on_invoke(context, event) 34 | 35 | args = (self, context) 36 | 37 | self.register_handlers(args, context) 38 | 39 | context.window_manager.modal_handler_add(self) 40 | return {"RUNNING_MODAL"} 41 | 42 | def register_handlers(self, args, context): 43 | self.draw_handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback_px, args, "WINDOW", "POST_PIXEL") 44 | self.draw_event = context.window_manager.event_timer_add(0.005, window=context.window) 45 | 46 | def unregister_handlers(self, context): 47 | 48 | context.window_manager.event_timer_remove(self.draw_event) 49 | 50 | bpy.types.SpaceView3D.draw_handler_remove(self.draw_handle, "WINDOW") 51 | 52 | self.draw_handle = None 53 | self.draw_event = None 54 | 55 | def handle_widget_events(self, event): 56 | result = False 57 | for widget in self.widgets: 58 | if widget.handle_event(event): 59 | result = True 60 | return result 61 | 62 | def modal(self, context, event): 63 | 64 | if self._finished: 65 | return {'FINISHED'} 66 | 67 | if context.area: 68 | context.area.tag_redraw() 69 | 70 | if self.handle_widget_events(event): 71 | return {'RUNNING_MODAL'} 72 | 73 | if event.type in {"RIGHTMOUSE"} and event.value == 'PRESS': 74 | self.finish() 75 | 76 | if event.type in {"ESC"}: 77 | return {'CANCELLED'} 78 | 79 | elif event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}: 80 | return {"PASS_THROUGH"} 81 | 82 | return {'RUNNING_MODAL'} 83 | 84 | def finish(self): 85 | self.unregister_handlers(bpy.context) 86 | self.on_finish(bpy.context) 87 | 88 | # Draw handler to paint onto the screen 89 | def draw_callback_px(self, op, context): 90 | for widget in self.widgets: 91 | widget.draw() 92 | -------------------------------------------------------------------------------- /gizmos/button_2d.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from .public_gizmo import PublicGizmo 4 | from ..camera_thumbnails import CameraThumbnails 5 | from ..utils.gizmo import offset_2d_gizmo 6 | 7 | 8 | class Gizmos: 9 | def create_gizmo(self, name) -> bpy.types.Gizmo: 10 | gz = self.gizmos.new(name) 11 | gz.icon = 'VIEW_PERSPECTIVE' 12 | gz.draw_options = {'BACKDROP', 'OUTLINE'} 13 | gz.use_tooltip = True 14 | gz.use_draw_modal = True 15 | gz.alpha = .8 16 | gz.color = 0.08, 0.08, 0.08 17 | gz.color_highlight = 0.28, 0.28, 0.28 18 | gz.alpha_highlight = 0.8 19 | 20 | gz.scale_basis = (80 * 0.35) / 2 # Same as buttons defined in C 21 | return gz 22 | 23 | def create_adjust_camera(self, context): 24 | # 调整焦距控件 25 | from ..ops.adjust_camera_lens import AdjustCameraLens 26 | gz = self.create_gizmo("GIZMO_GT_button_2d") 27 | gz.icon = 'VIEW_PERSPECTIVE' 28 | gz.target_set_operator(AdjustCameraLens.bl_idname) 29 | self.gz_move = gz 30 | 31 | def create_camera_settings(self, context): 32 | # 相机设置 33 | gz = self.create_gizmo("GIZMO_GT_button_2d") 34 | gz.icon = 'PROPERTIES' 35 | props = gz.target_set_operator("wm.call_menu") 36 | props.name = "CAMHP_MT_popup_cam_settings" 37 | self.gz_setttings = gz 38 | 39 | def create_add_camera(self, context): 40 | from ..ops.add_camera import AddCamera 41 | gz = self.create_gizmo("GIZMO_GT_button_2d") 42 | gz.icon = 'ADD' 43 | gz.target_set_operator(AddCamera.bl_idname) 44 | self.gz_add_cam = gz 45 | 46 | def create_camera_preview(self, context): 47 | from ..ops.preview_camera import PreviewCamera 48 | gz = self.create_gizmo("GIZMO_GT_button_2d") 49 | gz.use_event_handle_all = True 50 | gz.icon = 'IMAGE_PLANE' 51 | gz.target_set_operator(PreviewCamera.bl_idname) 52 | self.gz_cam_pv = gz 53 | 54 | def create_camera_switch(self, context): 55 | from ..ui.menu import CAMERA_HELPER_MT_Switch_Camera 56 | gz = self.create_gizmo("GIZMO_GT_button_2d") 57 | gz.use_event_handle_all = True 58 | gz.icon = 'VIEW_CAMERA' 59 | ops = gz.target_set_operator("wm.call_menu") 60 | ops.name = CAMERA_HELPER_MT_Switch_Camera.__name__ 61 | self.gz_cam_switch = gz 62 | 63 | 64 | class Button2DGizmos(bpy.types.GizmoGroup, Gizmos, PublicGizmo): 65 | bl_idname = "Button_UI_2D_gizmos" 66 | 67 | bl_label = "2D Button Gizmos" 68 | 69 | def setup(self, context): 70 | self.create_add_camera(context) 71 | self.create_camera_preview(context) 72 | self.create_camera_settings(context) 73 | self.create_adjust_camera(context) 74 | self.create_camera_switch(context) 75 | 76 | def draw_prepare(self, context): 77 | for i, gz in enumerate(self.gizmos): 78 | gz.hide = True 79 | 80 | region_3d = context.space_data.region_3d 81 | view_perspective = region_3d.view_perspective 82 | 83 | # if view_perspective == "PERSP": # 透视 84 | # ... 85 | # elif view_perspective == "ORTHO": # 正交 86 | # ... 87 | gizmos = self.gizmos 88 | if view_perspective == "CAMERA": # 相机 89 | gizmos = [ 90 | self.gz_move, 91 | self.gz_setttings, 92 | self.gz_cam_switch, 93 | self.gz_cam_pv, 94 | ] 95 | else: 96 | gizmos = [ 97 | self.gz_add_cam, 98 | self.gz_cam_pv, 99 | ] 100 | 101 | for i, gz in enumerate(gizmos): 102 | offset_2d_gizmo(context, gz, i) 103 | gz.hide = False 104 | 105 | context.area.tag_redraw() 106 | self.refresh(context) 107 | 108 | def refresh(self, context): 109 | CameraThumbnails.update_2d_button_color(context, self.gz_cam_pv) 110 | context.area.tag_redraw() 111 | -------------------------------------------------------------------------------- /preferences/__init__.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import rna_keymap_ui 3 | from .. import __package__ as base_package 4 | from bpy.props import EnumProperty, StringProperty, FloatProperty, IntProperty, BoolProperty, PointerProperty, \ 5 | FloatVectorProperty 6 | 7 | 8 | class GizmoMotionCamera(bpy.types.PropertyGroup): 9 | loop: BoolProperty(name="Loop", default=False) 10 | 11 | color: FloatVectorProperty(name='Color', subtype='COLOR_GAMMA', size=4, 12 | default=(0.8, 0.0, 0.0, 0.6)) 13 | color_highlight: FloatVectorProperty(name='Active Highlight', subtype='COLOR_GAMMA', size=4, 14 | default=(1, 0.0, 0.0, 0.8)) 15 | scale_basis: FloatProperty(name='Scale', default=1, min=0.2) 16 | 17 | 18 | class GizmoMotionSource(bpy.types.PropertyGroup): 19 | color: FloatVectorProperty(name='Color', subtype='COLOR_GAMMA', size=4, 20 | default=(0.0, 0.6, 0.8, 0.6)) 21 | color_highlight: FloatVectorProperty(name='Active Highlight', subtype='COLOR_GAMMA', size=4, 22 | default=(0.0, 0.8, 1.0, 0.8)) 23 | scale_basis: FloatProperty(name='Scale', default=0.75, min=0.1) 24 | 25 | 26 | class DrawMotionCurve(bpy.types.PropertyGroup): 27 | color: FloatVectorProperty(name='Color', subtype='COLOR_GAMMA', size=4, default=(0.8, 0, 0, 0.5)) 28 | width: IntProperty(name='Width', default=3, min=1, soft_max=5) 29 | 30 | 31 | class CameraThumb(bpy.types.PropertyGroup): 32 | max_width: IntProperty(name='Max Width', default=400, min=50, soft_max=800) 33 | max_height: IntProperty(name='Max Height', default=300, min=50, soft_max=600) 34 | 35 | position: EnumProperty(name='Position', items=[ 36 | ('TOP_LEFT', 'Top Left', ''), 37 | ('TOP_RIGHT', 'Top Right', ''), 38 | ('BOTTOM_LEFT', 'Bottom Left', ''), 39 | ('BOTTOM_RIGHT', 'Bottom Right', ''), 40 | ], default='TOP_LEFT') 41 | 42 | 43 | class CAMHP_Preference(bpy.types.AddonPreferences): 44 | bl_idname = base_package 45 | 46 | 47 | gz_motion_camera: PointerProperty(type=GizmoMotionCamera) 48 | gz_motion_source: PointerProperty(type=GizmoMotionSource) 49 | 50 | draw_motion_curve: PointerProperty(type=DrawMotionCurve) 51 | 52 | camera_thumb: PointerProperty(type=CameraThumb) 53 | 54 | def draw(self, context): 55 | layout = self.layout 56 | self.draw_settings(context, layout) 57 | 58 | def draw_settings(self, context, layout): 59 | col = layout.column() 60 | col.use_property_split = True 61 | 62 | box = col.box().column(align=True) 63 | box.label(text='Motion Camera', icon='GIZMO') 64 | box.prop(self.gz_motion_camera, 'loop') 65 | box.prop(self.gz_motion_camera, 'color') 66 | box.prop(self.gz_motion_camera, 'color_highlight') 67 | box.prop(self.gz_motion_camera, 'scale_basis') 68 | 69 | box = col.box().column(align=True) 70 | box.label(text='Source', icon='GIZMO') 71 | box.prop(self.gz_motion_source, 'color', text='Color') 72 | box.prop(self.gz_motion_source, 'color_highlight') 73 | box.prop(self.gz_motion_source, 'scale_basis') 74 | 75 | box = col.box().column(align=True) 76 | box.label(text='Motion Curve', icon='CURVE_DATA') 77 | box.prop(self.draw_motion_curve, 'color') 78 | box.prop(self.draw_motion_curve, 'width', slider=True) 79 | 80 | box = col.box().column(align=True) 81 | box.label(text='Camera Thumbnails', icon='CAMERA_DATA') 82 | box.prop(self.camera_thumb, 'max_width', slider=True) 83 | box.prop(self.camera_thumb, 'max_height', slider=True) 84 | row = box.row(align=True) 85 | row.prop(self.camera_thumb, 'position', expand=True) 86 | 87 | 88 | def register(): 89 | bpy.utils.register_class(GizmoMotionCamera) 90 | bpy.utils.register_class(GizmoMotionSource) 91 | bpy.utils.register_class(DrawMotionCurve) 92 | bpy.utils.register_class(CameraThumb) 93 | bpy.utils.register_class(CAMHP_Preference) 94 | 95 | 96 | def unregister(): 97 | bpy.utils.unregister_class(CAMHP_Preference) 98 | bpy.utils.unregister_class(GizmoMotionCamera) 99 | bpy.utils.unregister_class(CameraThumb) 100 | bpy.utils.unregister_class(GizmoMotionSource) 101 | bpy.utils.unregister_class(DrawMotionCurve) 102 | -------------------------------------------------------------------------------- /gizmos/preview_camera.py: -------------------------------------------------------------------------------- 1 | import blf 2 | import bpy 3 | import gpu.matrix 4 | from gpu_extras.presets import draw_texture_2d 5 | from mathutils import Vector 6 | 7 | from .public_gizmo import PublicGizmo 8 | from ..debug import DEBUG_PREVIEW_CAMERA 9 | from ..camera_thumbnails import CameraThumbnails 10 | from ..utils.area import area_offset 11 | from ..utils.gpu import draw_box 12 | 13 | 14 | class PreviewCameraAreaGizmo(bpy.types.Gizmo): 15 | bl_idname = "PREVIEW_CAMERA_GT_gizmo" 16 | bl_options = {"PERSISTENT", "SCALE", "SHOW_MODAL_ALL", "UNDO", "GRAB_CURSOR"} 17 | 18 | draw_points = None 19 | is_hover = None 20 | start_offset = None 21 | start_mouse = None 22 | offset_after = None 23 | 24 | def invoke(self, context, event): 25 | self.start_offset = CameraThumbnails.get_camera_data(context.area)["offset"] 26 | self.start_mouse = Vector((event.mouse_region_x, event.mouse_region_y)) 27 | if DEBUG_PREVIEW_CAMERA: 28 | print("invoke") 29 | return {"RUNNING_MODAL"} 30 | 31 | def modal(self, context, event, tweak): 32 | if DEBUG_PREVIEW_CAMERA: 33 | print(self.bl_idname, "modal", event.type, event.value, CameraThumbnails.get_camera_data(context.area)) 34 | if event.type == "LEFTMOUSE": 35 | return {"FINISHED"} 36 | elif event.type in {"RIGHTMOUSE", "ESC"}: 37 | return {"CANCELLED"} 38 | mouse = Vector((event.mouse_region_x, event.mouse_region_y)) 39 | diff = self.start_mouse - mouse 40 | self.offset_after = offset = self.start_offset + Vector((-diff.x, diff.y)) 41 | CameraThumbnails.get_camera_data(context.area)["offset"] = offset 42 | context.area.tag_redraw() 43 | return {"RUNNING_MODAL"} 44 | 45 | def exit(self, context, cancel): 46 | if DEBUG_PREVIEW_CAMERA: 47 | print("exit", cancel) 48 | if cancel: 49 | CameraThumbnails.get_camera_data(context.area)["offset"] = self.start_offset 50 | else: 51 | CameraThumbnails.get_camera_data(context.area)["offset"] = self.offset_after 52 | 53 | def draw(self, context): 54 | """ 55 | 从左上角开始绘制 56 | """ 57 | from ..utils import get_camera_preview_size 58 | w, h = get_camera_preview_size(context) 59 | with gpu.matrix.push_pop(): 60 | gpu.state.depth_mask_set(False) 61 | data = CameraThumbnails.get_camera_data(context.area) 62 | offset = data["offset"] 63 | x, y = area_offset(context) + offset 64 | y = context.area.height - y 65 | y -= h 66 | gpu.matrix.translate((x, y)) 67 | 68 | color = (.1, .1, .1, 0) if self.is_hover else (.2, .2, .2, .5) 69 | border = 5 70 | draw_box(-border, w + border, -border, h + border, color) 71 | 72 | if texture := CameraThumbnails.texture_data.get(data["camera_name"], None): 73 | draw_texture_2d(texture, (0, 0), w, h) 74 | # DEBUG 75 | if DEBUG_PREVIEW_CAMERA: 76 | blf.position(0, 0, 0, 1) 77 | for text in ( 78 | f"Preview Camera {self.is_hover}", 79 | hash(context.area), 80 | texture, 81 | f"{self.draw_points}", 82 | str(data) 83 | ): 84 | gpu.matrix.translate((0, -15)) 85 | blf.draw(0, str(text)) 86 | self.draw_points = (x, y), (x + w, y + h) 87 | 88 | def test_select(self, context, mouse_pos): 89 | if self.draw_points is None: 90 | return -1 91 | x, y = mouse_pos 92 | (x1, y1), (x2, y2) = self.draw_points 93 | x_ok = x1 < x < x2 94 | y_ok = y1 < y < y2 95 | is_hover = 0 if x_ok and y_ok else -1 96 | self.is_hover = is_hover == 0 97 | return is_hover 98 | 99 | def refresh(self, context): 100 | if DEBUG_PREVIEW_CAMERA: 101 | print(self.bl_idname, "refresh") 102 | 103 | 104 | class PreviewCameraGizmos(bpy.types.GizmoGroup, PublicGizmo): 105 | bl_idname = "Preview_Camera_UI_gizmos" 106 | bl_label = "Preview Camera Gizmos" 107 | 108 | @classmethod 109 | def poll(cls, context): 110 | return CameraThumbnails.check_is_draw(context) 111 | 112 | def setup(self, context): 113 | gz = self.preview_camera = self.gizmos.new(PreviewCameraAreaGizmo.bl_idname) 114 | gz.use_draw_modal = True 115 | 116 | def refresh(self, context): 117 | context.area.tag_redraw() 118 | 119 | def draw_prepare(self, context): 120 | ... 121 | -------------------------------------------------------------------------------- /ops/old/draw_utils/bl_ui_widget.py: -------------------------------------------------------------------------------- 1 | import gpu 2 | import bpy 3 | 4 | from gpu_extras.batch import batch_for_shader 5 | from . import wrap_gpu_state 6 | from .shader import get_shader 7 | 8 | class BL_UI_Widget: 9 | 10 | def __init__(self, x, y, width, height): 11 | self.x = x 12 | self.y = y 13 | self.x_screen = x 14 | self.y_screen = y 15 | self.width = width 16 | self.height = height 17 | self._bg_color = (0.8, 0.8, 0.8, 1.0) 18 | self._tag = None 19 | self.context = None 20 | self.__inrect = False 21 | self._mouse_down = False 22 | self._is_visible = True 23 | 24 | def set_location(self, x, y): 25 | self.x = x 26 | self.y = y 27 | self.x_screen = x 28 | self.y_screen = y 29 | self.update(x,y) 30 | 31 | @property 32 | def bg_color(self): 33 | return self._bg_color 34 | 35 | @bg_color.setter 36 | def bg_color(self, value): 37 | self._bg_color = value 38 | 39 | @property 40 | def visible(self): 41 | return self._is_visible 42 | 43 | @visible.setter 44 | def visible(self, value): 45 | self._is_visible = value 46 | 47 | @property 48 | def tag(self): 49 | return self._tag 50 | 51 | @tag.setter 52 | def tag(self, value): 53 | self._tag = value 54 | 55 | def draw(self): 56 | if not self.visible: 57 | return 58 | 59 | self.shader.bind() 60 | self.shader.uniform_float("color", self._bg_color) 61 | 62 | with wrap_gpu_state(): 63 | self.batch_panel.draw(self.shader) 64 | 65 | def init(self, context): 66 | self.context = context 67 | self.update(self.x, self.y) 68 | 69 | def update(self, x, y): 70 | 71 | area_height = self.get_area_height() 72 | 73 | self.x_screen = x 74 | self.y_screen = area_height - y 75 | 76 | indices = ((0, 1, 2), (0, 2, 3)) 77 | 78 | y_screen_flip = area_height - self.y_screen 79 | 80 | # bottom left, top left, top right, bottom right 81 | vertices = ( 82 | (self.x_screen, y_screen_flip), 83 | (self.x_screen, y_screen_flip - self.height), 84 | (self.x_screen + self.width, y_screen_flip - self.height), 85 | (self.x_screen + self.width, y_screen_flip)) 86 | 87 | self.shader = get_shader(type='2d') 88 | self.batch_panel = batch_for_shader(self.shader, 'TRIS', {"pos" : vertices}, indices=indices) 89 | 90 | 91 | def handle_event(self, event): 92 | x = event.mouse_region_x 93 | y = event.mouse_region_y 94 | 95 | if(event.type == 'LEFTMOUSE'): 96 | # print(event.type,event.value ) 97 | if(event.value == 'PRESS'): 98 | self._mouse_down = True 99 | return self.mouse_down(x, y) 100 | else: 101 | self._mouse_down = False 102 | self.mouse_up(x, y) 103 | 104 | 105 | elif(event.type == 'MOUSEMOVE'): 106 | self.mouse_move(x, y) 107 | 108 | inrect = self.is_in_rect(x, y) 109 | 110 | # we enter the rect 111 | if not self.__inrect and inrect: 112 | self.__inrect = True 113 | self.mouse_enter(event, x, y) 114 | 115 | # we are leaving the rect 116 | elif self.__inrect and not inrect: 117 | self.__inrect = False 118 | self.mouse_exit(event, x, y) 119 | 120 | return False 121 | 122 | elif event.value == 'PRESS' and (event.ascii != '' or event.type in self.get_input_keys()): 123 | return self.text_input(event) 124 | 125 | return False 126 | 127 | def get_input_keys(self) : 128 | return [] 129 | 130 | def get_area_height(self): 131 | return bpy.context.area.height 132 | 133 | def is_in_rect(self, x, y): 134 | area_height = self.get_area_height() 135 | 136 | widget_y = area_height - self.y_screen 137 | if ( 138 | (self.x_screen <= x <= (self.x_screen + self.width)) and 139 | (widget_y >= y >= (widget_y - self.height)) 140 | ): 141 | return True 142 | 143 | return False 144 | 145 | def text_input(self, event): 146 | return False 147 | 148 | def mouse_down(self, x, y): 149 | return self.is_in_rect(x,y) 150 | 151 | def mouse_up(self, x, y): 152 | pass 153 | 154 | def set_mouse_enter(self, mouse_enter_func): 155 | self.mouse_enter_func = mouse_enter_func 156 | 157 | def call_mouse_enter(self): 158 | try: 159 | if self.mouse_enter_func: 160 | self.mouse_enter_func(self) 161 | except: 162 | pass 163 | 164 | def mouse_enter(self, event, x, y): 165 | self.call_mouse_enter() 166 | 167 | def set_mouse_exit(self, mouse_exit_func): 168 | self.mouse_exit_func = mouse_exit_func 169 | 170 | def call_mouse_exit(self): 171 | try: 172 | if self.mouse_exit_func: 173 | self.mouse_exit_func(self) 174 | except: 175 | pass 176 | 177 | def mouse_exit(self, event, x, y): 178 | self.call_mouse_exit() 179 | 180 | def mouse_move(self, x, y): 181 | pass -------------------------------------------------------------------------------- /ops/motion/bake.py: -------------------------------------------------------------------------------- 1 | 2 | # bake motion camera 3 | class CAMHP_OT_bake_motion_cam(bpy.types.Operator): 4 | bl_idname = "camhp.bake_motion_cam" 5 | bl_label = "Bake Motion Camera" 6 | 7 | frame_start: IntProperty(name="Start Frame", default=1) 8 | frame_end: IntProperty(name="End Frame", default=100) 9 | frame_step: IntProperty(name="Frame Step", default=1) 10 | 11 | # bake 12 | cam = None 13 | ob = None 14 | timer = None 15 | 16 | def modal(self, context, event): 17 | if event.type == 'TIMER': 18 | ob = self.cam_bake 19 | affect = self.affect 20 | cam = self.cam 21 | 22 | if self.frame > self.frame_end: 23 | context.window_manager.event_timer_remove(self.timer) 24 | return {'FINISHED'} 25 | else: 26 | self.frame += self.frame_step 27 | 28 | context.scene.frame_set(self.frame) 29 | matrix = context.object.matrix_world.copy() 30 | print('frame', self.frame, matrix) 31 | # 位置 32 | loc = matrix.to_translation() 33 | ob.location = loc 34 | ob.keyframe_insert('location') 35 | 36 | if affect.use_euler: 37 | if self.euler_prev is None: 38 | euler = matrix.to_euler(context.object.rotation_mode) 39 | else: 40 | euler = matrix.to_euler(context.object.rotation_mode, self.euler_prev) 41 | self.euler_prev = euler.copy() 42 | 43 | ob.rotation_euler = self.euler_prev 44 | ob.keyframe_insert('rotation_euler') 45 | 46 | if not cam: return {'PASS_THROUGH'} 47 | # 相机数值 48 | if affect.use_lens: 49 | ob.data.lens = G_PROPS['lens'] 50 | ob.data.keyframe_insert('lens') 51 | 52 | if affect.use_focus_distance: 53 | ob.data.dof.focus_distance = G_PROPS['focal'] 54 | ob.data.dof.keyframe_insert('focus_distance') 55 | 56 | if affect.use_aperture_fstop: 57 | ob.data.dof.aperture_fstop = G_PROPS['fstop'] 58 | ob.data.dof.keyframe_insert('aperture_fstop') 59 | 60 | # 自定义属性 61 | # for item in affect.custom_props: 62 | # if item.data_path == '': continue 63 | # 64 | # tg_obj = ob 65 | # from_obj = context.object 66 | # 67 | # src_obj, src_attr = parse_data_path(tg_obj.data, item.data_path) 68 | # _from_obj, from_attr = parse_data_path(from_obj.data, item.data_path) 69 | # if from_attr is None or src_attr is None or src_obj is None: continue 70 | # 71 | # from_value = getattr(_from_obj, from_attr) 72 | # 73 | # try: 74 | # if isinstance(from_value, float): 75 | # setattr(src_obj, src_attr, from_value) 76 | # elif isinstance(from_value, mathutils.Vector): 77 | # setattr(src_obj, src_attr, from_value) 78 | # elif isinstance(from_value, mathutils.Matrix): 79 | # setattr(src_obj, src_attr, from_value) 80 | # elif isinstance(from_value, bool): 81 | # setattr(src_obj, src_attr, from_value) 82 | # elif isinstance(from_value, bpy.types.Object): 83 | # setattr(src_obj, src_attr, from_value) 84 | # 85 | # if hasattr(src_obj, 'keyframe_insert'): 86 | # kf = getattr(src_obj, 'keyframe_insert') 87 | # kf(src_attr) 88 | # 89 | # except Exception as e: 90 | # print(e) 91 | 92 | return {'PASS_THROUGH'} 93 | 94 | def invoke(self, context, event): 95 | # print('invoke') 96 | wm = context.window_manager 97 | m_cam = context.object.motion_cam 98 | affect = m_cam.affect 99 | 100 | if context.object.type == 'CAMERA': 101 | cam = context.object 102 | elif (affect.use_sub_camera and 103 | affect.sub_camera and 104 | affect.sub_camera.type == 'CAMERA'): 105 | 106 | cam = affect.sub_camera 107 | else: 108 | cam = None 109 | 110 | if cam is None: 111 | self.report({'ERROR'}, "无相机") 112 | return {'CANCELLED'} 113 | 114 | if m_cam.id_data.animation_data is None: 115 | self.report({'ERROR'}, "无动画") 116 | return {'CANCELLED'} 117 | 118 | action = m_cam.id_data.animation_data.action 119 | 120 | if action is None: 121 | self.report({'ERROR'}, "无动作") 122 | return {'CANCELLED'} 123 | 124 | self.frame_start = int(action.frame_range[0]) 125 | self.frame_end = int(action.frame_range[1]) 126 | 127 | name = cam.name + '_bake' 128 | data_name = cam.data.name + '_bake' 129 | # print(name, data_name) 130 | cam_data = bpy.data.cameras.new(name=data_name) 131 | ob = bpy.data.objects.new(name, cam_data) 132 | # ob.constraints.clear() 133 | # ob.location = 0, 0, 0 134 | 135 | context.collection.objects.link(ob) 136 | 137 | context.scene.frame_set(self.frame_start) 138 | 139 | self.frame = self.frame_start 140 | self.cam = cam 141 | self.cam_bake = ob 142 | self.affect = affect 143 | self.euler_prev = None 144 | # print('invoke end') 145 | self.timer = wm.event_timer_add(0.01, window=context.window) 146 | context.window_manager.modal_handler_add(self) 147 | return {"RUNNING_MODAL"} 148 | # return wm.invoke_props_dialog(self) 149 | 150 | -------------------------------------------------------------------------------- /ui/camera.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | class pop_cam_panel(bpy.types.Panel): 5 | """Properties""" 6 | bl_space_type = 'VIEW_3D' 7 | bl_region_type = 'HEADER' 8 | 9 | 10 | class CAMHP_PT_pop_cam_comp_panel(pop_cam_panel): 11 | bl_label = "Composition Guides" 12 | bl_idname = 'CAMHP_PT_pop_cam_comp_panel' 13 | 14 | def draw(self, context): 15 | layout = self.layout 16 | layout.use_property_split = True 17 | layout.use_property_decorate = False 18 | 19 | cam = context.scene.camera.data 20 | 21 | layout.prop(cam, "show_composition_thirds") 22 | 23 | col = layout.column(heading="Center", align=True) 24 | col.prop(cam, "show_composition_center") 25 | col.prop(cam, "show_composition_center_diagonal", text="Diagonal") 26 | 27 | col = layout.column(heading="Golden", align=True) 28 | col.prop(cam, "show_composition_golden", text="Ratio") 29 | col.prop(cam, "show_composition_golden_tria_a", text="Triangle A") 30 | col.prop(cam, "show_composition_golden_tria_b", text="Triangle B") 31 | 32 | col = layout.column(heading="Harmony", align=True) 33 | col.prop(cam, "show_composition_harmony_tri_a", text="Triangle A") 34 | col.prop(cam, "show_composition_harmony_tri_b", text="Triangle B") 35 | 36 | 37 | class CAMHP_PT_pop_cam_dof(pop_cam_panel): 38 | bl_label = "Depth of Field" 39 | bl_idname = 'CAMHP_PT_pop_cam_dof' 40 | 41 | def draw(self, context): 42 | layout = self.layout 43 | layout.use_property_split = True 44 | layout.use_property_decorate = False 45 | 46 | cam = context.scene.camera.data 47 | 48 | dof = cam.dof 49 | layout.prop(dof, "use_dof") 50 | layout.active = dof.use_dof 51 | 52 | col = layout.column() 53 | col.prop(dof, "focus_object", text="Focus on Object") 54 | sub = col.column() 55 | sub.active = (dof.focus_object is None) 56 | sub.prop(dof, "focus_distance", text="Focus Distance") 57 | 58 | flow = layout.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=False) 59 | 60 | col = flow.column() 61 | col.prop(dof, "aperture_fstop") 62 | 63 | col = flow.column() 64 | col.prop(dof, "aperture_blades") 65 | col.prop(dof, "aperture_rotation") 66 | col.prop(dof, "aperture_ratio") 67 | 68 | 69 | class CAMHP_PT_pop_cam_lens(pop_cam_panel): 70 | bl_label = "Lens" 71 | bl_idname = 'CAMHP_PT_pop_cam_lens' 72 | 73 | def draw(self, context): 74 | layout = self.layout 75 | layout.use_property_split = True 76 | layout.use_property_decorate = False 77 | 78 | cam = context.scene.camera.data 79 | 80 | layout.prop(cam, "type") 81 | 82 | col = layout.column() 83 | col.separator() 84 | 85 | if cam.type == 'PERSP': 86 | if cam.lens_unit == 'MILLIMETERS': 87 | col.prop(cam, "lens") 88 | elif cam.lens_unit == 'FOV': 89 | col.prop(cam, "angle") 90 | col.prop(cam, "lens_unit") 91 | 92 | elif cam.type == 'ORTHO': 93 | col.prop(cam, "ortho_scale") 94 | 95 | elif cam.type == 'PANO': 96 | engine = context.engine 97 | if engine == 'CYCLES': 98 | ccam = cam.cycles 99 | col.prop(ccam, "panorama_type") 100 | if ccam.panorama_type == 'FISHEYE_EQUIDISTANT': 101 | col.prop(ccam, "fisheye_fov") 102 | elif ccam.panorama_type == 'FISHEYE_EQUISOLID': 103 | col.prop(ccam, "fisheye_lens", text="Lens") 104 | col.prop(ccam, "fisheye_fov") 105 | elif ccam.panorama_type == 'EQUIRECTANGULAR': 106 | sub = col.column(align=True) 107 | sub.prop(ccam, "latitude_min", text="Latitude Min") 108 | sub.prop(ccam, "latitude_max", text="Max") 109 | sub = col.column(align=True) 110 | sub.prop(ccam, "longitude_min", text="Longitude Min") 111 | sub.prop(ccam, "longitude_max", text="Max") 112 | elif ccam.panorama_type == 'FISHEYE_LENS_POLYNOMIAL': 113 | col.prop(ccam, "fisheye_fov") 114 | col.prop(ccam, "fisheye_polynomial_k0", text="K0") 115 | col.prop(ccam, "fisheye_polynomial_k1", text="K1") 116 | col.prop(ccam, "fisheye_polynomial_k2", text="K2") 117 | col.prop(ccam, "fisheye_polynomial_k3", text="K3") 118 | col.prop(ccam, "fisheye_polynomial_k4", text="K4") 119 | 120 | elif engine in {'BLENDER_RENDER', 'BLENDER_EEVEE', 'BLENDER_WORKBENCH'}: 121 | if cam.lens_unit == 'MILLIMETERS': 122 | col.prop(cam, "lens") 123 | elif cam.lens_unit == 'FOV': 124 | col.prop(cam, "angle") 125 | col.prop(cam, "lens_unit") 126 | 127 | col = layout.column() 128 | col.separator() 129 | 130 | sub = col.column(align=True) 131 | sub.prop(cam, "shift_x", text="Shift X") 132 | sub.prop(cam, "shift_y", text="Y") 133 | 134 | col.separator() 135 | sub = col.column(align=True) 136 | sub.prop(cam, "clip_start", text="Clip Start") 137 | sub.prop(cam, "clip_end", text="End") 138 | 139 | 140 | 141 | def register(): 142 | bpy.utils.register_class(CAMHP_PT_pop_cam_comp_panel) 143 | bpy.utils.register_class(CAMHP_PT_pop_cam_dof) 144 | bpy.utils.register_class(CAMHP_PT_pop_cam_lens) 145 | 146 | 147 | def unregister(): 148 | bpy.utils.unregister_class(CAMHP_PT_pop_cam_comp_panel) 149 | bpy.utils.unregister_class(CAMHP_PT_pop_cam_dof) 150 | bpy.utils.unregister_class(CAMHP_PT_pop_cam_lens) 151 | -------------------------------------------------------------------------------- /ops/adjust_camera_lens.py: -------------------------------------------------------------------------------- 1 | from math import tan, radians 2 | 3 | import blf 4 | import bpy 5 | from mathutils import Vector 6 | 7 | from ..utils import get_operator_bl_idname 8 | 9 | 10 | 11 | def wrap_blf_size(font_id: int, size): 12 | if bpy.app.version >= (4, 0, 0): 13 | blf.size(font_id, size) 14 | else: 15 | blf.size(font_id, size, 72) 16 | 17 | 18 | def view3d_camera_border(scene: bpy.types.Scene, region: bpy.types.Region, rv3d: bpy.types.RegionView3D) -> list[ 19 | Vector]: 20 | obj = scene.camera 21 | cam = obj.data 22 | 23 | frame = cam.view_frame(scene=scene) 24 | 25 | # move from object-space into world-space 26 | frame = [obj.matrix_world @ v for v in frame] 27 | 28 | # move into pixelspace 29 | from bpy_extras.view3d_utils import location_3d_to_region_2d 30 | frame_px = [location_3d_to_region_2d(region, rv3d, v) for v in frame] 31 | return frame_px 32 | 33 | 34 | def draw_lens_callback(self, context): 35 | font_id = 0 36 | 37 | area = context.area 38 | r3d = area.spaces[0].region_3d 39 | 40 | frame_px = view3d_camera_border(context.scene, area, r3d) 41 | 42 | px = frame_px[1] # bottom right 43 | x = px[0] 44 | y = px[1] 45 | ui_scale = bpy.context.preferences.system.dpi * 1 / 72 46 | wrap_blf_size(font_id, 30 * ui_scale) 47 | text_width, text_height = blf.dimensions(font_id, f"{int(context.scene.camera.data.lens)} mm") 48 | x = x - text_width - 10 * ui_scale 49 | y = y + int(text_height) - 10 * ui_scale 50 | 51 | blf.position(font_id, x, y, 0) 52 | blf.color(font_id, 1, 1, 1, 0.5) 53 | blf.draw(font_id, f"{int(context.scene.camera.data.lens)} mm") 54 | 55 | 56 | class Cam: 57 | """ 58 | 相机实用类 59 | """ 60 | 61 | def __init__(self, cam): 62 | self.cam = cam 63 | self.startLocation = cam.location.copy() 64 | self.startAngle = cam.data.angle 65 | 66 | def restore(self): 67 | self.cam.location = self.startLocation.copy() 68 | self.cam.data.angle = self.startAngle 69 | 70 | def limit_angle_range(self, value): 71 | max_view_radian = 3.0 # 172d 72 | min_view_radian = 0.007 # 0.367d 73 | self.cam.data.angle = max(min(self.cam.data.angle + value, max_view_radian), min_view_radian) 74 | 75 | def get_angle(self) -> float: 76 | return self.cam.data.angle 77 | 78 | def offsetLocation(self, localCorrectionVector): 79 | self.cam.location = self.cam.location + (localCorrectionVector @ self.cam.matrix_world.inverted()) 80 | 81 | def get_local_point(self, point) -> Vector: 82 | return self.cam.matrix_world.inverted() @ point 83 | 84 | 85 | class AdjustCameraLens(bpy.types.Operator): 86 | """Use Cursor to Adjust Camera Lens""" 87 | bl_idname = get_operator_bl_idname("adjust_camera_lens") 88 | bl_label = "Adjust Camera Lens" 89 | bl_options = {'GRAB_CURSOR', 'BLOCKING', 'UNDO'} 90 | 91 | _handle = None 92 | mouse_pos = None 93 | 94 | @classmethod 95 | def poll(cls, context): 96 | return context.area.type == 'VIEW_3D' and context.scene.camera is not None 97 | 98 | def append_handles(self): 99 | self.cursor_set = True 100 | bpy.context.window.cursor_modal_set('MOVE_X') 101 | self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_lens_callback, (self, bpy.context), 'WINDOW', 102 | 'POST_PIXEL') 103 | bpy.context.window_manager.modal_handler_add(self) 104 | 105 | def remove_handles(self): 106 | bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') 107 | bpy.context.window.cursor_modal_restore() 108 | 109 | def adjust_lens(self, angleOffset): 110 | # limit angle range 111 | self.camera.limit_angle_range(angleOffset) 112 | current_angle = self.camera.get_angle() 113 | 114 | scale = self.fovTan / tan(current_angle * 0.5) 115 | 116 | correctionOffset = self.startLocalCursorZ * (1.0 - scale) 117 | 118 | self.camera.offsetLocation(Vector((0.0, 0.0, correctionOffset))) 119 | 120 | def modal(self, context, event): 121 | self.mouse_pos = event.mouse_region_x, event.mouse_region_y 122 | 123 | if event.type == 'MOUSEMOVE': 124 | self.mouseDX = self.mouseDX - event.mouse_x 125 | # shift 减速 126 | multiplier = 0.01 if event.shift else 0.1 127 | 128 | self.startLocalCursorZ = self.camera.get_local_point(self.cursorLocation)[2] 129 | self.fovTan = tan(self.camera.get_angle() * 0.5) 130 | 131 | offset = self.mouseDX 132 | self.adjust_lens(radians(-offset * multiplier)) 133 | # 重置 134 | self.mouseDX = event.mouse_x 135 | 136 | elif event.type == 'LEFTMOUSE': 137 | self.remove_handles() 138 | self.report({"INFO"}, f"{context.scene.camera.name}: {int(context.scene.camera.data.lens)}mm") 139 | return {'FINISHED'} 140 | 141 | elif event.type in {'RIGHTMOUSE', 'ESC'}: 142 | self.camera.restore() 143 | self.remove_handles() 144 | return {'CANCELLED'} 145 | 146 | return {'RUNNING_MODAL'} 147 | 148 | def invoke(self, context, event): 149 | self.mouse_pos = [0, 0] 150 | # 确认游标在视野内 151 | cam = context.scene.camera 152 | localCursor = cam.matrix_world.inverted() @ context.scene.cursor.location 153 | if localCursor[2] > 0: 154 | self.report({'WARNING'}, 'Place the 3D cursor in your view') 155 | return {'CANCELLED'} 156 | # 初始化参数 157 | self.camera = Cam(cam) 158 | self.mouseDX = event.mouse_x 159 | self.cursorLocation = context.scene.cursor.location 160 | # add handles 161 | self.append_handles() 162 | return {'RUNNING_MODAL'} 163 | -------------------------------------------------------------------------------- /ops/old/draw_utils/bl_ui_button.py: -------------------------------------------------------------------------------- 1 | from .bl_ui_widget import BL_UI_Widget 2 | 3 | import gpu 4 | import blf 5 | # import bgl 6 | import bpy 7 | from gpu_extras.batch import batch_for_shader 8 | from . import wrap_gpu_state 9 | from .shader import wrap_blf_size 10 | 11 | class BL_UI_Button(BL_UI_Widget): 12 | 13 | def __init__(self, x, y, width, height): 14 | super().__init__(x, y, width, height) 15 | self._text_color = (1.0, 1.0, 1.0, 1.0) 16 | self._hover_bg_color = (0.5, 0.5, 0.5, 1.0) 17 | self._select_bg_color = (0.7, 0.7, 0.7, 1.0) 18 | 19 | self._text = "Button" 20 | self._text_size = 16 21 | self._textpos = (x, y) 22 | 23 | self.__state = 0 24 | self.__image = None 25 | self.__image_size = (24, 24) 26 | self.__image_position = (4, 2) 27 | 28 | @property 29 | def text_color(self): 30 | return self._text_color 31 | 32 | @text_color.setter 33 | def text_color(self, value): 34 | self._text_color = value 35 | 36 | @property 37 | def text(self): 38 | return self._text 39 | 40 | @text.setter 41 | def text(self, value): 42 | self._text = value 43 | 44 | @property 45 | def text_size(self): 46 | return self._text_size 47 | 48 | @text_size.setter 49 | def text_size(self, value): 50 | self._text_size = value 51 | 52 | @property 53 | def hover_bg_color(self): 54 | return self._hover_bg_color 55 | 56 | @hover_bg_color.setter 57 | def hover_bg_color(self, value): 58 | self._hover_bg_color = value 59 | 60 | @property 61 | def select_bg_color(self): 62 | return self._select_bg_color 63 | 64 | @select_bg_color.setter 65 | def select_bg_color(self, value): 66 | self._select_bg_color = value 67 | 68 | def set_image_size(self, imgage_size): 69 | self.__image_size = imgage_size 70 | 71 | def set_image_position(self, image_position): 72 | self.__image_position = image_position 73 | 74 | def set_image(self, rel_filepath): 75 | try: 76 | self.__image = bpy.data.images.load(rel_filepath, check_existing=True) 77 | self.__image.gl_load() 78 | except: 79 | pass 80 | 81 | def update(self, x, y): 82 | super().update(x, y) 83 | area_height = self.get_area_height() 84 | self._textpos = [x, area_height - y] 85 | 86 | def draw(self): 87 | if not self.visible: 88 | return 89 | 90 | area_height = self.get_area_height() 91 | 92 | self.shader.bind() 93 | 94 | self.set_colors() 95 | 96 | with wrap_gpu_state(): 97 | 98 | self.batch_panel.draw(self.shader) 99 | 100 | self.draw_image() 101 | 102 | 103 | # Draw text 104 | self.draw_text(area_height) 105 | 106 | def set_colors(self): 107 | color = self._bg_color 108 | text_color = self._text_color 109 | 110 | # pressed 111 | if self.__state == 1: 112 | color = self._select_bg_color 113 | 114 | # hover 115 | elif self.__state == 2: 116 | color = self._hover_bg_color 117 | 118 | self.shader.uniform_float("color", color) 119 | 120 | def draw_text(self, area_height): 121 | wrap_blf_size(0,self._text_size) 122 | size = blf.dimensions(0, self._text) 123 | 124 | textpos_y = area_height - self._textpos[1] - (self.height + size[1]) / 2.0 125 | blf.position(0, self._textpos[0] + (self.width - size[0]) / 2.0, textpos_y + 1, 0) 126 | 127 | r, g, b, a = self._text_color 128 | blf.color(0, r, g, b, a) 129 | 130 | blf.draw(0, self._text) 131 | 132 | def draw_image(self): 133 | if self.__image is None: 134 | return 135 | try: 136 | off_x = self.over_scale(self._image_position[0]) 137 | off_y = self.over_scale(self._image_position[1]) 138 | 139 | sx = self.over_scale(self._image_size[0]) 140 | sy = self.over_scale(self._image_size[1]) 141 | 142 | x_screen = self.over_scale(self.x_screen) 143 | y_screen = self.over_scale(self.y_screen) 144 | 145 | texture = gpu.texture.from_image(self._image) 146 | 147 | # Bottom left, top left, top right, bottom right 148 | vertices = ((x_screen + off_x, y_screen - off_y), 149 | (x_screen + off_x, y_screen - sy - off_y), 150 | (x_screen + off_x + sx, y_screen - sy - off_y), 151 | (x_screen + off_x + sx, y_screen - off_y)) 152 | 153 | self.shader_img = gpu.shader.from_builtin('2D_IMAGE') 154 | self.batch_img = batch_for_shader(self.shader_img, 155 | 'TRI_FAN', {"pos": vertices, 156 | "texCoord": ((0, 1), (0, 0), (1, 0), (1, 1)), }, 157 | ) 158 | # import bgl 159 | # bgl.glActiveTexture(bgl.GL_TEXTURE0) 160 | # bgl.glBindTexture(bgl.GL_TEXTURE_2D, self._image.bindcode) 161 | 162 | self.shader_img.bind() 163 | # self.shader_img.uniform_int("image", 0) 164 | self.shader_img.uniform_sampler("image", texture) 165 | self.batch_img.draw(self.shader_img) 166 | except Exception as e: 167 | pass 168 | 169 | return False 170 | 171 | def set_mouse_down(self, mouse_down_func, obj): 172 | self.mouse_down_func_obj = obj 173 | self.mouse_down_func = mouse_down_func 174 | 175 | def mouse_down(self, x, y): 176 | if self.is_in_rect(x, y): 177 | self.__state = 1 178 | try: 179 | self.mouse_down_func(self.mouse_down_func_obj) 180 | except: 181 | pass 182 | 183 | return True 184 | 185 | return False 186 | 187 | def mouse_move(self, x, y): 188 | if self.is_in_rect(x, y): 189 | if (self.__state != 1): 190 | # hover state 191 | self.__state = 2 192 | else: 193 | self.__state = 0 194 | 195 | def mouse_up(self, x, y): 196 | if self.is_in_rect(x, y): 197 | self.__state = 2 198 | else: 199 | self.__state = 0 200 | -------------------------------------------------------------------------------- /utils/property.py: -------------------------------------------------------------------------------- 1 | # version = 1.0.0 2 | import json 3 | 4 | exclude_items = {"rna_type", "bl_idname", "srna"} # 排除项 5 | 6 | from collections.abc import Iterable 7 | 8 | import bpy 9 | from mathutils import Euler, Vector, Matrix, Color 10 | 11 | 12 | def __set_collection_data__(prop, data): 13 | """设置集合值 14 | 15 | Args: 16 | prop (_type_): _description_ 17 | data (_type_): _description_ 18 | """ 19 | for i in data: 20 | pro = prop.add() 21 | set_property(pro, data[i]) 22 | 23 | 24 | def __set_color_ramp__(prop, data): 25 | """ 26 | bpy.types.ColorRamp 27 | TODO 28 | """ 29 | ... 30 | # for i in prop.values(): 31 | # prop.remove(i) 32 | 33 | 34 | def __set_prop__(prop, path, value): 35 | """设置单个属性""" 36 | pr = getattr(prop, path, None) 37 | if pr is not None or path in prop.bl_rna.properties: 38 | pro = prop.bl_rna.properties[path] 39 | typ = pro.type 40 | try: 41 | if typ == "POINTER": 42 | set_property(pr, value) 43 | elif typ == "COLLECTION": 44 | if type(prop) == bpy.types.ColorRamp: 45 | # print(prop, type(prop), pr, value, typ, path) 46 | __set_color_ramp__(pr, value) 47 | else: 48 | __set_collection_data__(pr, value) 49 | elif typ == "ENUM" and pro.is_enum_flag: 50 | # 可多选枚举 51 | setattr(prop, path, set(value)) 52 | else: 53 | setattr(prop, path, value) 54 | except Exception as e: 55 | print("ERROR", typ, pro, value, e) 56 | import traceback 57 | traceback.print_stack() 58 | traceback.print_exc() 59 | 60 | 61 | def set_property(prop, data: dict): 62 | for k, item in data.items(): 63 | pr = getattr(prop, k, None) 64 | if pr is not None or k in prop.bl_rna.properties: 65 | __set_prop__(prop, k, item) 66 | 67 | 68 | def set_property_to_kmi_properties(properties: "bpy.types.KeyMapItem.properties", props) -> None: 69 | """注入operator property 70 | 在绘制项时需要使用此方法 71 | set operator property 72 | self.operator_property: 73 | """ 74 | 75 | def _for_set_prop(prop, pro, pr): 76 | for index, j in enumerate(pr): 77 | try: 78 | getattr(prop, pro)[index] = j 79 | except Exception as e: 80 | print(e.args) 81 | 82 | for pro in props: 83 | pr = props[pro] 84 | if hasattr(properties, pro): 85 | if pr is tuple: 86 | # 阵列参数 87 | _for_set_prop(properties, pro, pr) 88 | else: 89 | try: 90 | setattr(properties, pro, props[pro]) 91 | except Exception as e: 92 | print(e.args) 93 | 94 | 95 | def __collection_data__(prop, exclude=(), reversal=False, only_set=False) -> dict: 96 | """获取输入集合属性的内容 97 | 98 | Args: 99 | prop (_type_): _description_ 100 | 101 | Returns: 102 | :param prop: 103 | :param reversal: 104 | :param exclude: 105 | """ 106 | data = {} 107 | for index, value in enumerate(prop): 108 | if value not in exclude_items: 109 | data[index] = get_property(value, exclude, reversal, only_set) 110 | return data 111 | 112 | 113 | def get_property(prop, exclude=(), reversal=False, only_set=False) -> dict: 114 | """ 115 | 获取输入的属性内容 116 | 可多选枚举(ENUM FLAG)将转换为列表 list(用于json写入,json 没有 set类型) 117 | 集合信息将转换为字典 index当索引保存 dict 118 | 119 | Args: 120 | prop (bl_property): 输入blender的属性内容 121 | exclude (tuple): 排除内容 122 | reversal (bool): 反转排除内容,如果为True,则只搜索exclude 123 | only_set (bool): 仅被设置的属性 124 | Returns: 125 | dict: 反回字典式数据, 126 | """ 127 | data = {} 128 | bl_rna = prop.bl_rna 129 | for pr in bl_rna.properties: 130 | try: 131 | identifier = pr.identifier 132 | is_ok = (identifier in exclude) if reversal else (identifier not in exclude) 133 | 134 | is_exclude = identifier not in exclude_items 135 | if is_exclude and is_ok: 136 | typ = pr.type 137 | 138 | pro = getattr(prop, identifier, None) 139 | if pro is None: 140 | continue 141 | 142 | is_array = False 143 | if typ == "POINTER": 144 | pro = get_property(pro, exclude, reversal, only_set) 145 | elif typ == "COLLECTION": 146 | pro = __collection_data__(pro, exclude, reversal, only_set) 147 | elif typ == "ENUM" and pr.is_enum_flag: 148 | # 可多选枚举 149 | pro = list(pro) 150 | elif typ == "FLOAT" and pr.subtype == "COLOR" and type(pro) != float: 151 | # color 152 | pro = pro[:] 153 | is_array = True 154 | elif isinstance(pro, (Euler, Vector, bpy.types.bpy_prop_array, Color)): 155 | pro = pro[:] 156 | is_array = True 157 | elif isinstance(pro, Matrix): 158 | res = () 159 | for j in pro: 160 | res += (*tuple(j[:]),) 161 | is_array = True 162 | pro = res 163 | 164 | # 将浮点数设置位数 165 | if isinstance(pro, Iterable) and type(pro) not in (str, dict): 166 | pro = [round(i, 2) if type(i) == float else i for i in pro][:] 167 | if isinstance(pro, float): 168 | pro = round(pro, 2) 169 | 170 | if only_set: # and typ not in ("POINTER", "COLLECTION") #去掉默认值,只有被修改了的值列出 171 | default_value = getattr(pr, "default", None) 172 | if is_array: 173 | default_value = list(getattr(pr, "default_array", None)) 174 | 175 | if default_value == pro: 176 | continue 177 | if type(pro) in (set, dict, list, tuple) and len(pro) == 0: # 去掉空的数据 178 | continue 179 | # print(identifier, default_value, pro, "\t", default_value == pro, type(pro)) 180 | data[identifier] = pro 181 | except Exception as e: 182 | print(prop, pr) 183 | print(e.args) 184 | import traceback 185 | traceback.print_exc() 186 | return data 187 | 188 | 189 | def get_kmi_property(kmi): 190 | return dict( 191 | get_property( 192 | kmi, 193 | exclude=( 194 | "name", "id", "show_expanded", "properties", "idname", "map_type", "active", "propvalue", 195 | "shift_ui", "ctrl_ui", "alt_ui", "oskey_ui", "is_user_modified", "is_user_defined" 196 | ) 197 | ) 198 | ) 199 | 200 | 201 | def get_property_enum_items(cls, prop_name) -> list: 202 | res = [] 203 | for item in cls.properties[prop_name].enum_items: 204 | res.append((item.identifier, item.name, item.description)) 205 | return res 206 | -------------------------------------------------------------------------------- /gizmos/motion/curve.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class CAMHP_UI_motion_curve_gz(GizmoGroupBase, GizmoGroup): 4 | bl_idname = "CAMHP_UI_motion_curve_gz" 5 | bl_label = "Camera Motion Curve" 6 | bl_options = {'3D', 'PERSISTENT'} 7 | 8 | _move_gz = dict() 9 | _rotate_gz = dict() 10 | _gz_axis = dict() 11 | 12 | cam_list = list() 13 | 14 | @classmethod 15 | def poll(cls, context): 16 | ob = context.object 17 | view = context.space_data 18 | if ( 19 | ob and 20 | ob.type in {'CAMERA', 'EMPTY'} and 21 | view.region_3d.view_perspective != 'CAMERA' and 22 | not view.region_quadviews 23 | ): 24 | return True 25 | else: 26 | return False 27 | 28 | def draw_prepare(self, context): 29 | super().draw_prepare(context) 30 | self.refresh(context) 31 | 32 | def setup(self, context): 33 | self._move_gz = dict() 34 | self._rotate_gz = dict() 35 | self.gz_motion_cam = None 36 | 37 | self.cam_list = [item.camera for item in context.object.motion_cam.list] 38 | 39 | self.add_motion_cam_gz(context) 40 | self.draw_prepare(context) 41 | print(self._move_gz) 42 | 43 | def add_motion_cam_gz(self, context): 44 | if self.gz_motion_cam is None: 45 | gz = self.gizmos.new("CAMHP_GT_custom_move_1d") 46 | gz.target_set_prop('offset', context.object.motion_cam, 'offset_factor') 47 | 48 | gz._camera = context.object 49 | gz.use_tooltip = True 50 | gz.use_event_handle_all = True 51 | 52 | # 设置gizmo的偏好 53 | pref_gz = get_pref().gz_motion_camera 54 | 55 | gz.alpha = pref_gz.color[3] 56 | gz.color = pref_gz.color[:3] 57 | gz.color_highlight = pref_gz.color_highlight[:3] 58 | gz.alpha_highlight = pref_gz.color_highlight[3] 59 | 60 | gz.use_draw_modal = True 61 | gz.use_draw_scale = False 62 | 63 | self.gz_motion_cam = gz 64 | try: 65 | for gz in self._move_gz.keys(): 66 | self.gizmos.remove(gz) 67 | 68 | for gz in self._rotate_gz.keys(): 69 | self.gizmos.remove(gz) 70 | except ReferenceError: # new file open 71 | pass 72 | 73 | self._move_gz = dict() 74 | self._rotate_gz = dict() 75 | self._gz_axis = dict() 76 | 77 | for index, item in enumerate(context.object.motion_cam.list): 78 | item = context.object.motion_cam.list[index] 79 | 80 | self.add_move_gz(index, item) 81 | print('Add gizmos') 82 | # TODO 移除gizmo以避免崩溃。 Blender报错:EXCEPTION_ACCESS_VIOLATION,联系官方处理中 83 | # TODO 已经解决,似乎是python的锅 https://projects.blender.org/blender/blender/issues/109111#issuecomment-963329 84 | self.add_rotate_gz(item, 'X') 85 | self.add_rotate_gz(item, 'Y') 86 | self.add_rotate_gz(item, 'Z') 87 | 88 | self.correct_rotate_gz_euler() 89 | 90 | def correct_rotate_gz_euler(self): 91 | for gz, axis in self._gz_axis.items(): 92 | if axis == 'X': 93 | rotate = Euler((math.radians(90), math.radians(-180), math.radians(-90)), 'XYZ') # 奇怪的数值 94 | 95 | elif axis == 'Y': 96 | rotate = Euler((math.radians(-90), 0, 0), 'XYZ') 97 | 98 | else: 99 | rotate = Euler((0, 0, math.radians(90)), 'XYZ') 100 | 101 | cam = self._rotate_gz[gz] 102 | # print('correct gizmos') 103 | rotate.rotate(cam.matrix_world.to_euler('XYZ')) 104 | gz.matrix_basis = rotate.to_matrix().to_4x4() 105 | gz.matrix_basis.translation = cam.matrix_world.translation 106 | 107 | def add_rotate_gz(self, item, axis='Z'): 108 | # rotate gizmos 109 | # gizmos = self.gizmos.new("GIZMO_GT_dial_3d") 110 | gz = self.gizmos.new("CAMHP_GT_custom_rotate_1d") 111 | 112 | prop = gz.target_set_operator(CAMHP_OT_rotate_object.bl_idname) 113 | prop.obj_name = item.camera.name 114 | prop.axis = axis 115 | 116 | gz.use_tooltip = True 117 | gz.use_event_handle_all = True 118 | 119 | gz.use_draw_modal = True 120 | gz.use_draw_scale = False 121 | 122 | # red, green, blue for X Y Z axis 123 | gz.alpha = 0.5 124 | gz.alpha_highlight = 1 125 | 126 | ui = bpy.context.preferences.themes[0].user_interface 127 | 128 | axis_x = ui.axis_x[:3] 129 | axis_y = ui.axis_y[:3] 130 | axis_z = ui.axis_z[:3] 131 | 132 | if axis == 'X': 133 | gz.color = axis_x 134 | elif axis == 'Y': 135 | gz.color = axis_y 136 | elif axis == 'Z': 137 | gz.color = axis_z 138 | 139 | gz.color_highlight = (1, 1, 1) 140 | 141 | self._rotate_gz[gz] = item.camera 142 | self._gz_axis[gz] = axis 143 | 144 | def add_move_gz(self, index, item): 145 | # move gizmos 146 | gz = self.gizmos.new("CAMHP_GT_custom_move_3d") 147 | gz._index = index 148 | gz._camera = item.camera 149 | 150 | gz.target_set_prop('offset', item.camera, 'location') 151 | 152 | gz.use_tooltip = True 153 | gz.use_event_handle_all = True 154 | 155 | pref_gz = get_pref().gz_motion_source 156 | gz.alpha = pref_gz.color[3] 157 | gz.color = pref_gz.color[:3] 158 | gz.color_highlight = pref_gz.color_highlight[:3] 159 | gz.alpha_highlight = pref_gz.color_highlight[3] 160 | 161 | gz.use_draw_modal = True 162 | gz.use_draw_scale = False 163 | 164 | self._move_gz[gz] = item.camera 165 | 166 | def refresh(self, context): 167 | # print("CamHp::refresh") 168 | update_gz = False 169 | # 添加相机时候自动添加gizmo 170 | cam_list = [item.camera for item in context.object.motion_cam.list] 171 | if self.cam_list != cam_list: 172 | self.cam_list = cam_list 173 | update_gz = True 174 | 175 | # 切换物体移除gizmo 176 | if len(context.object.motion_cam.list) == 0: 177 | if self.gz_motion_cam: 178 | self.gizmos.remove(self.gz_motion_cam) 179 | self.gz_motion_cam = None 180 | 181 | for gz in self._move_gz.keys(): 182 | self.gizmos.remove(gz) 183 | 184 | for gz in self._rotate_gz.keys(): 185 | # print('remove gizmos') 186 | self.gizmos.remove(gz) 187 | 188 | self._move_gz = dict() 189 | self._rotate_gz = dict() 190 | 191 | elif self.gz_motion_cam is None or update_gz: 192 | self.add_motion_cam_gz(context) 193 | 194 | # 矫正位置 move gizmos 195 | if self.gz_motion_cam: 196 | self.gz_motion_cam.matrix_basis = context.object.matrix_world.normalized() 197 | z = Vector((0, 0, 1)) 198 | norm = z 199 | norm.rotate(context.object.matrix_world.to_euler('XYZ')) 200 | self.gz_motion_cam.matrix_basis.translation -= norm * context.object.motion_cam.offset_factor # 修复偏移 201 | self.gz_motion_cam.matrix_basis.translation += z # 向z移动 202 | 203 | # 矫正位置 rotate gizmos 204 | if self.gz_motion_cam: 205 | self.correct_rotate_gz_euler() 206 | 207 | context.area.tag_redraw() 208 | 209 | -------------------------------------------------------------------------------- /camera_thumbnails.py: -------------------------------------------------------------------------------- 1 | import time 2 | from contextlib import contextmanager 3 | 4 | import bpy 5 | import gpu 6 | from mathutils import Vector 7 | 8 | from .debug import DEBUG_PREVIEW_CAMERA 9 | from .utils import get_camera 10 | from .utils.area import get_area_max_parent 11 | 12 | 13 | @contextmanager 14 | def camera_context(context): 15 | """Tips: 打开了渲染窗口会出现screen不匹配无法刷新""" 16 | if context.screen: 17 | for area in context.screen.areas: 18 | if area.type == "VIEW_3D": 19 | for region in area.regions: 20 | if region.type == "WINDOW": 21 | for space in area.spaces: 22 | if space.type == "VIEW_3D": 23 | try: 24 | with context.temp_override( 25 | area=area, 26 | region=region, 27 | space_data=space, 28 | ): 29 | ori_show_overlay = space.overlay.show_overlays 30 | space.overlay.show_overlays = False 31 | yield True 32 | space.overlay.show_overlays = ori_show_overlay 33 | return 34 | except TypeError: 35 | ... 36 | yield False 37 | 38 | 39 | class CameraThumbnails: 40 | """Camera Thumbnails\nLeft Click: Enable\nCtrl: Pin Selected Camera\nCtrl Shift Click: Send to Viewer""" 41 | 42 | camera_data = { 43 | # area:{ 44 | # camera_name:str, 45 | # offset:Vector 46 | # pin:bool, 47 | # enabled:bool, 48 | # } 49 | } 50 | 51 | texture_data = { 52 | # camera_name:gpu.types.GPUTexture 53 | } 54 | 55 | @classmethod 56 | def pin_selected_camera(cls, context, camera: bpy.types.Camera): 57 | data = cls.camera_data 58 | area_hash = hash(get_area_max_parent(context.area)) 59 | if area_hash not in data: 60 | cls.switch_preview(context, camera) 61 | 62 | data[area_hash]["enabled"] = True 63 | data[area_hash]["pin"] = data[area_hash]["pin"] ^ True 64 | cls.update() 65 | 66 | @classmethod 67 | def switch_preview(cls, context, camera: bpy.types.Camera): 68 | data = cls.camera_data 69 | 70 | area_hash = hash(get_area_max_parent(context.area)) 71 | camera_name = "camera_name" if camera is None else camera.name 72 | 73 | if area_hash in data: 74 | data[area_hash]["enabled"] = data[area_hash]["enabled"] ^ True 75 | else: 76 | data[area_hash] = { 77 | "camera_name": camera_name, 78 | "offset": Vector((0, 0)), 79 | "pin": False, 80 | "enabled": True, 81 | } 82 | cls.update() 83 | 84 | @classmethod 85 | def update(cls): 86 | from .debug import DEBUG_PREVIEW_CAMERA 87 | start_time = time.time() 88 | context = bpy.context 89 | camera = get_camera(context) 90 | 91 | if camera is not None: 92 | for key, value in cls.camera_data.items(): # 切换预览相机 93 | pin = value.get("pin", False) 94 | 95 | if DEBUG_PREVIEW_CAMERA: 96 | print("pin\t", pin, "\t", value["camera_name"], key, value, ) 97 | if pin is False: 98 | value["camera_name"] = camera.name 99 | cls.camera_data[key] = value 100 | 101 | update_completion_list = [] 102 | with camera_context(context) as is_update: 103 | if is_update: 104 | if DEBUG_PREVIEW_CAMERA: 105 | print(is_update, "cls.camera_data") 106 | for camera_data in cls.camera_data.values(): # 更新相机纹理 107 | camera_name = camera_data["camera_name"] 108 | camera = context.scene.objects.get(camera_name, None) 109 | enabled = camera_data.get("enabled", False) 110 | repetition_detection = camera_name not in update_completion_list 111 | if camera and enabled and repetition_detection: 112 | cls.update_camera_texture(context, camera) 113 | update_completion_list.append(camera_name) 114 | 115 | if DEBUG_PREVIEW_CAMERA: 116 | print(f"update {time.time() - start_time}s\t", update_completion_list) 117 | print("\n") 118 | 119 | @classmethod 120 | def update_camera_texture(cls, context, camera, use_resolution=False) -> gpu.types.GPUTexture: 121 | from .utils import get_camera_preview_size 122 | 123 | is_update = True 124 | scene = context.scene 125 | name = camera.name 126 | 127 | if use_resolution: 128 | render = context.scene.render 129 | w, h = render.resolution_x, render.resolution_y 130 | else: 131 | w, h = get_camera_preview_size(context) 132 | 133 | texture = None 134 | do_color_management = (bpy.app.version >= (5, 0, 0)) 135 | if is_update: 136 | if name not in cls.camera_data: 137 | offscreen = gpu.types.GPUOffScreen(w, h) 138 | else: 139 | offscreen = cls.camera_data[name] 140 | view_matrix = camera.matrix_world.inverted() 141 | projection_matrix = camera.calc_matrix_camera( 142 | context.evaluated_depsgraph_get(), 143 | x=w, 144 | y=h, 145 | ) 146 | offscreen.draw_view3d( 147 | scene, 148 | context.view_layer, 149 | context.space_data, 150 | context.region, 151 | view_matrix, 152 | projection_matrix, 153 | do_color_management=do_color_management, 154 | draw_background=True, 155 | ) 156 | if DEBUG_PREVIEW_CAMERA: 157 | print("update_camera_texture", camera.name) 158 | texture = cls.texture_data[name] = offscreen.texture_color 159 | return texture 160 | 161 | @classmethod 162 | def check_is_draw(cls, context): 163 | area_hash = hash(get_area_max_parent(context.area)) 164 | if area_hash in cls.camera_data: 165 | return cls.camera_data[area_hash]["enabled"] 166 | return False 167 | 168 | @classmethod 169 | def check_is_pin(cls, context): 170 | area_hash = hash(context.area) 171 | return area_hash in cls.camera_data and cls.camera_data[area_hash].get("pin", False) 172 | 173 | @classmethod 174 | def update_2d_button_color(cls, context, gizmo): 175 | if cls.check_is_draw(context): 176 | gizmo.color = 0.08, 0.6, 0.08 177 | gizmo.color_highlight = 0.28, 0.8, 0.28 178 | if cls.check_is_pin(context): 179 | gizmo.color = 0.8, 0.2, 0.2 180 | gizmo.color_highlight = 1, 0.2, 0.2 181 | else: 182 | gizmo.color = 0.08, 0.08, 0.08 183 | gizmo.color_highlight = 0.28, 0.28, 0.28 184 | 185 | @classmethod 186 | def get_camera_data(cls, area): 187 | area_hash = hash(get_area_max_parent(area)) 188 | return CameraThumbnails.camera_data.get(area_hash, None) 189 | -------------------------------------------------------------------------------- /ops/motion/add.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.app.translations import pgettext_iface as tip_ 3 | from bpy_extras.view3d_utils import location_3d_to_region_2d 4 | 5 | from ..old.draw_utils.bl_ui_button import BL_UI_Button 6 | from ..old.draw_utils.bl_ui_drag_panel import BL_UI_Drag_Panel 7 | from ..old.draw_utils.bl_ui_draw_op import BL_UI_OT_draw_operator 8 | from ..old.draw_utils.bl_ui_label import BL_UI_Label 9 | from ...utils.asset import AssetDir, get_asset_dir 10 | 11 | 12 | def get_obj_2d_loc(obj, context): 13 | r3d = context.space_data.region_3d 14 | loc = location_3d_to_region_2d(context.region, r3d, obj.matrix_world.translation) 15 | return loc 16 | 17 | 18 | def load_asset(name: str, asset_type: str, filepath: str) -> bpy.types.NodeTree | bpy.types.Object: 19 | """load asset into current scene from giving asset type""" 20 | if asset_type == 'objects': 21 | attr = 'objects' 22 | elif asset_type == 'node_groups': 23 | attr = 'node_groups' 24 | else: 25 | raise ValueError('asset_type not support') 26 | 27 | # reuse existing data 28 | data_lib = getattr(bpy.data, attr) 29 | if name in data_lib and asset_type in {'node_groups'}: 30 | return data_lib[name] 31 | 32 | with bpy.data.libraries.load(filepath, link=False) as (data_from, data_to): 33 | src = getattr(data_from, attr) 34 | res = [x for x in src if x == name] 35 | if not res: 36 | raise ValueError(f'No {name} found in {filepath}') 37 | setattr(data_to, attr, res) 38 | # clear asset mark 39 | obj = getattr(data_to, attr)[0] 40 | obj.asset_clear() 41 | return obj 42 | 43 | 44 | class CAMHP_PT_add_motion_cams(BL_UI_OT_draw_operator, bpy.types.Operator): 45 | bl_idname = 'camhp.add_motion_cams' 46 | bl_label = 'Add Motion Camera' 47 | bl_options = {'UNDO_GROUPED', 'INTERNAL', 'BLOCKING', 'GRAB_CURSOR'} 48 | 49 | buttons = list() 50 | controller = None 51 | 52 | def __init__(self, *args, **kwargs): 53 | super().__init__(*args, **kwargs) 54 | self.buttons.clear() 55 | 56 | # 面板提示 57 | self.panel = BL_UI_Drag_Panel(100, 300, 300, 290) 58 | self.panel.bg_color = 0.2, 0.2, 0.2, 0.9 59 | 60 | self.label_tip1 = BL_UI_Label(20, 120, 40, 50) 61 | self.label_tip1.text = tip_('Left Click->Camera Name to Add Source') 62 | self.label_tip1.text_size = 22 63 | 64 | self.label_tip2 = BL_UI_Label(20, 95, 40, 50) 65 | self.label_tip2.text = tip_('Right Click->End Add Mode') 66 | self.label_tip2.text_size = 22 67 | 68 | self.label_tip3 = BL_UI_Label(20, 70, 40, 50) 69 | self.label_tip3.text = tip_('ESC->Cancel') 70 | self.label_tip3.text_size = 22 71 | 72 | # 为每个相机添加一个按钮 73 | for obj in bpy.context.view_layer.objects: 74 | if obj.type != 'CAMERA': continue 75 | 76 | loc = get_obj_2d_loc(obj, bpy.context) 77 | if loc is None: continue # 相机处于较远的位置 78 | x, y = loc 79 | 80 | btn = BL_UI_Button(x, y, 120, 30) 81 | btn.bg_color = (0.1, 0.1, 0.1, 0.8) 82 | btn.hover_bg_color = (0.6, 0.6, 0.6, 0.8) 83 | btn.text = obj.name 84 | # button1.set_image("//img/scale_24.png") 85 | # self.button1.set_image_size((24,24)) 86 | # button1.set_image_position((4, 2)) 87 | btn.set_mouse_down(self.add_motion_cam, obj) 88 | setattr(btn, 'bind_obj', obj.name) 89 | 90 | self.buttons.append(btn) 91 | 92 | def cancel(self, context): 93 | if getattr(self, 'new_cam', None) is not None: 94 | bpy.data.objects.remove(self.new_cam) 95 | if getattr(self, 'cam', None) is not None: 96 | bpy.data.objects.remove(self.controller) 97 | return {'CANCELLED'} 98 | 99 | def on_invoke(self, context, event): 100 | widgets_panel = [self.label_tip1, self.label_tip2, self.label_tip3] 101 | widgets = [self.panel] 102 | widgets += widgets_panel 103 | 104 | self.init_widgets(context, widgets_panel + self.buttons) 105 | 106 | self.panel.add_widgets(widgets_panel) 107 | 108 | # Open the panel at the mouse location 109 | self.panel.set_location(context.area.width * 0.8, 110 | context.area.height * 1) 111 | 112 | # 创建相机 113 | cam_data = bpy.data.cameras.new(name='Camera') 114 | cam_obj = bpy.data.objects.new('Camera', cam_data) 115 | 116 | if bpy.app.version >= (4, 3, 0): 117 | asset_motioncam = get_asset_dir(AssetDir.ASSET_BLEND_WITH_GIZMO.value) 118 | else: 119 | asset_motioncam = get_asset_dir(AssetDir.ASSET_BLEND.value) 120 | controller_obj = load_asset(name='Controller', asset_type='objects', filepath=str(asset_motioncam)) 121 | asset_motion_src = load_asset(name='MotionCameraSource', asset_type='node_groups', 122 | filepath=str(asset_motioncam)) 123 | asset_motion_gen = load_asset(name='MotionCamera', asset_type='node_groups', 124 | filepath=str(asset_motioncam)) 125 | 126 | context.collection.objects.link(cam_obj) 127 | context.collection.objects.link(controller_obj) 128 | # 设置 129 | cam_obj.data.show_name = True 130 | cam_obj.name = 'Motion Camera' 131 | # deselect all 132 | bpy.ops.object.select_all(action='DESELECT') 133 | context.view_layer.objects.active = controller_obj 134 | controller_obj.select_set(True) 135 | cam_obj.select_set(True) 136 | # toggle edit mode 137 | bpy.ops.object.mode_set(mode='EDIT') 138 | bpy.ops.mesh.select_all(action='SELECT') 139 | bpy.ops.object.vertex_parent_set() 140 | bpy.ops.object.mode_set(mode='OBJECT') 141 | 142 | self.controller = controller_obj 143 | self.new_cam = cam_obj 144 | self.ng_motion_src = asset_motion_src 145 | self.ng_motion_gen = asset_motion_gen 146 | 147 | def add_motion_cam(self, obj): 148 | bpy.context.view_layer.objects.active = self.controller 149 | self.controller.select_set(True) 150 | 151 | if 'MotionCamera' not in self.controller.modifiers: 152 | mod_gen = self.controller.modifiers.new(type='NODES', name='MotionCamera') 153 | mod_gen.node_group = self.ng_motion_gen 154 | mod_gen.show_group_selector = False 155 | 156 | mod = self.controller.modifiers.new(type='NODES', name='MotionCameraSource') 157 | mod.node_group = self.ng_motion_src 158 | mod.show_group_selector = False 159 | 160 | # set value 161 | mod['Socket_2'] = obj 162 | mod.show_viewport = False 163 | mod.show_viewport = True 164 | # move to bottom 165 | for i, m in enumerate(self.controller.modifiers): 166 | if m.name == 'MotionCamera': 167 | self.controller.modifiers.move(i, len(self.controller.modifiers) - 1) 168 | self.controller.modifiers.active = m 169 | break 170 | bpy.context.area.tag_redraw() 171 | 172 | def modal(self, context, event): 173 | 174 | for i, btn in enumerate(self.buttons): 175 | if not hasattr(btn, 'bind_obj'): continue 176 | 177 | obj_name = getattr(btn, 'bind_obj') 178 | obj = bpy.data.objects.get(obj_name) 179 | 180 | if obj is None: continue 181 | x, y = get_obj_2d_loc(obj, context) 182 | btn.update(x, y) 183 | context.area.tag_redraw() 184 | 185 | return super().modal(context, event) 186 | -------------------------------------------------------------------------------- /ops/old/utils.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | from pathlib import Path 4 | from typing import Callable, Union 5 | from mathutils import Vector 6 | 7 | # 用于处理曲线的几何节点组 ----------------------------------------------------- 8 | C_GET_CURVE_ATTR = 'get_curve_attr' 9 | C_GET_CURVE_EVAL_POS = 'get_curve_eval_pos' 10 | 11 | 12 | # ----------------------------------------------------------------------------- 13 | 14 | 15 | def meas_time(func: Callable) -> Callable: 16 | """计时装饰器 17 | 18 | :param func: 19 | :return: func 20 | """ 21 | 22 | def wrapper(*args, **kwargs): 23 | import time 24 | t = time.time() 25 | func(*args, **kwargs) 26 | end = time.time() 27 | # print(f'函数 "{func.__name__}" 花费 {(end - t) * 1000}ms') 28 | 29 | return wrapper 30 | 31 | 32 | def get_geo_node_file(filename: str = 'process.blend') -> Path: 33 | """几何节点组的文件路径 34 | 35 | :param filename: 36 | :return: 37 | """ 38 | return Path(__file__).parent.joinpath('nodes', filename) 39 | 40 | 41 | def get_mesh_obj_coords(context: bpy.types.Context, obj: bpy.types.Object, 42 | deps: Union[bpy.types.Depsgraph, None] = None) -> list[ 43 | Vector]: 44 | """获取mesh对象的位置 45 | 46 | :param obj:bpy.types.Object 47 | :return:list 48 | """ 49 | depsg_eval = deps if deps else context.evaluated_depsgraph_get() # deps 由外部传入,防止冻结 50 | 51 | obj_eval = obj.evaluated_get(depsg_eval) 52 | return [v.co for v in obj_eval.data.vertices] 53 | 54 | 55 | def get_mesh_obj_attrs(context: bpy.types.Context, obj: bpy.types.Object, 56 | deps: Union[bpy.types.Depsgraph, None] = None) -> dict[str, list[Union[float, Vector]]]: 57 | """获取mesh对象的属性值 58 | 59 | :param obj:bpy.types.Object 60 | :param attr: 61 | :return:list 62 | """ 63 | attr_dict = dict() 64 | 65 | depsg_eval = deps if deps else context.evaluated_depsgraph_get() # deps 由外部传入,防止冻结 66 | depsg_eval.update() 67 | obj_eval = obj.evaluated_get(depsg_eval) 68 | 69 | for name, attr in obj_eval.data.attributes.items(): 70 | attr_data = attr.data 71 | try: 72 | attr_dict[name] = [v.value for v in attr_data.values()] 73 | except: 74 | attr_dict[name] = [v.vector for v in attr_data.values()] 75 | 76 | # print(attr_dict) 77 | 78 | return attr_dict 79 | 80 | 81 | def view3d_find() -> Union[tuple[bpy.types.Region, bpy.types.RegionView3D], tuple[None, None]]: 82 | # returns first 3d view, normally we get from context 83 | for area in bpy.context.window.screen.areas: 84 | if area.type == 'VIEW_3D': 85 | v3d = area.spaces[0] 86 | rv3d = v3d.region_3d 87 | for region in area.regions: 88 | if region.type == 'WINDOW': 89 | return region, rv3d 90 | return None, None 91 | 92 | 93 | 94 | # 以下所有方法都会出发depsgraph更新,无法用于实时动画set/get 95 | ############################################################################### 96 | 97 | def get_geo_node_group(filename: str = 'process.blend', node_group: str = C_GET_CURVE_ATTR) -> bpy.types.NodeTree: 98 | """获取几何节点组 99 | 100 | :param filename: 101 | :param node_group:name of node group 102 | :return: bpy.types.NodeGroup 103 | """ 104 | fp = get_geo_node_file(filename) 105 | if node_group in bpy.data.node_groups: # 版本控制:将会重新曲线按钮用于更新节点版本 106 | return bpy.data.node_groups[node_group] 107 | 108 | with bpy.data.libraries.load(str(fp), link=False) as (data_from, data_to): 109 | data_to.node_groups = [node_group] 110 | 111 | ng = data_to.node_groups[0] 112 | 113 | return ng 114 | 115 | 116 | def create_tool_collection(name: str = 'CameraHelper') -> bpy.types.Collection: 117 | if name not in bpy.data.collections: 118 | coll = bpy.data.collections.new(name) 119 | bpy.context.scene.collection.children.link(coll) 120 | coll = bpy.data.collections['CameraHelper'] 121 | coll.hide_viewport = False 122 | coll.hide_render = False 123 | 124 | return coll 125 | 126 | 127 | def gen_bezier_curve_from_points(coords: list[Vector], curve_name: str, resolution_u: int = 12, 128 | close_spline: bool = False, type: str = 'SMOOTH') -> bpy.types.Object: 129 | """根据点集生成贝塞尔曲线 130 | 131 | :param coords:list of tuple(x, y, z) 132 | :param curve_name: 曲线物体/数据名字 133 | :param resolution_u: 曲线分割精度 134 | :param close_spline: 是否闭合曲线 135 | :return:曲线物体 bpy.types.Object 136 | """ 137 | # 清理 138 | if curve_name in bpy.data.objects: 139 | bpy.data.objects.remove(bpy.data.objects[curve_name]) 140 | 141 | if curve_name in bpy.data.curves: 142 | bpy.data.curves.remove(bpy.data.curves[curve_name]) 143 | 144 | # 创建曲线 145 | curve_data = bpy.data.curves.new(curve_name, type='CURVE') 146 | curve_data.dimensions = '3D' 147 | curve_data.resolution_u = resolution_u 148 | # 创建样条 149 | # 创建点 150 | if type == 'SMOOTH': 151 | spline = curve_data.splines.new('BEZIER') 152 | spline.bezier_points.add(len(coords) - 1) 153 | else: 154 | spline = curve_data.splines.new('POLY') 155 | spline.points.add(len(coords) - 1) 156 | # 设置点 157 | for i, coord in enumerate(coords): 158 | x, y, z = coord 159 | if type == 'SMOOTH': 160 | spline.bezier_points[i].handle_right_type = 'AUTO' 161 | spline.bezier_points[i].handle_left_type = 'AUTO' 162 | spline.bezier_points[i].co = (x, y, z) 163 | spline.bezier_points[i].handle_left = (x, y, z) 164 | spline.bezier_points[i].handle_right = (x, y, z) 165 | else: 166 | spline.points[i].co = (x, y, z, 1) 167 | 168 | # 闭合,或为可选项 169 | spline.use_cyclic_u = close_spline 170 | 171 | if type == 'SMOOTH': 172 | # 取消端点影响a 173 | def map_handle_to_co(pt): 174 | pt.handle_right_type = 'FREE' 175 | pt.handle_left_type = 'FREE' 176 | pt.handle_left = pt.co 177 | pt.handle_right = pt.co 178 | 179 | map_handle_to_co(spline.bezier_points[0]) 180 | map_handle_to_co(spline.bezier_points[-1]) 181 | 182 | # 创建物体 183 | curve_obj = bpy.data.objects.new(curve_name, curve_data) 184 | # 链接到场景(否则物体将不会更新) 185 | # coll = bpy.context.collection 186 | coll = create_tool_collection() 187 | coll.objects.link(curve_obj) 188 | 189 | return curve_obj 190 | 191 | 192 | def set_obj_geo_mod(obj: bpy.types.Object, name: str = 'Geo Node', 193 | node_group: Union[bpy.types.NodeTree, None] = None) -> bpy.types.Modifier: 194 | """添加几何模型修饰器 195 | 196 | :param obj:bpy.types.Object 197 | """ 198 | mod = obj.modifiers.new(type='NODES', name=name) 199 | if mod.node_group: 200 | bpy.data.node_groups.remove(mod.node_group) 201 | mod.node_group = node_group 202 | 203 | return mod 204 | 205 | 206 | def gen_curve_sample_obj(curve_obj: bpy.types.Object, postfix: str = '_sample', 207 | node_group: str = C_GET_CURVE_ATTR) -> bpy.types.Object: 208 | """根据曲线物体生属性采样用物体 209 | 210 | :param curve_obj: 211 | :return: 212 | """ 213 | tmp_name = curve_obj.name + postfix 214 | 215 | if tmp_name in bpy.data.objects: 216 | bpy.data.objects.remove(bpy.data.objects[tmp_name]) 217 | if tmp_name in bpy.data.meshes: 218 | bpy.data.meshes.remove(bpy.data.meshes[tmp_name]) 219 | 220 | # 创建mesh 221 | tmp_mesh = bpy.data.meshes.new(tmp_name) 222 | bm = bmesh.new() 223 | bmesh.ops.create_uvsphere(bm, u_segments=2, v_segments=1, radius=0.5, calc_uvs=True) 224 | bm.to_mesh(tmp_mesh) 225 | bm.free() 226 | # 创建物体并使用节点修改器 227 | tmp_obj = bpy.data.objects.new(tmp_name, tmp_mesh) 228 | 229 | ng = get_geo_node_group(node_group=node_group) 230 | mod = set_obj_geo_mod(tmp_obj, node_group=ng) 231 | # 设置输入物体 232 | mod["Input_2"] = curve_obj 233 | 234 | # coll = bpy.context.collection 235 | coll = create_tool_collection() 236 | coll.objects.link(tmp_obj) 237 | 238 | return tmp_obj 239 | 240 | 241 | ############################################################################################## 242 | 243 | # 集成方法 --------------------------------------------------------------------------------------------------------- 244 | 245 | def gen_sample_attr_obj(curve_obj): 246 | return gen_curve_sample_obj(curve_obj, postfix='_attr', node_group=C_GET_CURVE_ATTR) 247 | 248 | 249 | def gen_sample_mesh_obj(curve_obj): 250 | return gen_curve_sample_obj(curve_obj, postfix='_mesh', node_group=C_GET_CURVE_EVAL_POS) 251 | 252 | # -------------------------------------------------------------------------------------------------------------------- 253 | -------------------------------------------------------------------------------- /ops/old/draw_utils/shader.py: -------------------------------------------------------------------------------- 1 | # import bgl 2 | import blf 3 | import bpy 4 | import gpu 5 | from gpu_extras.batch import batch_for_shader 6 | from gpu_extras.presets import draw_texture_2d 7 | 8 | WIDTH = 512 9 | HEIGHT = 256 10 | PADDING = 20 11 | 12 | indices = ((0, 1, 2), (2, 1, 3)) 13 | 14 | 15 | def ui_scale(): 16 | # return bpy.context.preferences.system.dpi * bpy.context.preferences.system.pixel_size / 72 17 | # since blender 4.0, the bpy.context.preferences.system.pixel_size will jump from 1~2 while the ui scale tweak from 1.18~1.19 18 | return bpy.context.preferences.system.dpi * 1 / 72 19 | 20 | 21 | def get_shader(type='3d'): 22 | if bpy.app.version < (4, 0, 0): 23 | shader_3d = gpu.shader.from_builtin('3D_UNIFORM_COLOR') 24 | shader_2d = gpu.shader.from_builtin('2D_UNIFORM_COLOR') 25 | shader_debug = gpu.shader.from_builtin('2D_UNIFORM_COLOR') 26 | shader_tex = gpu.shader.from_builtin('2D_IMAGE') 27 | else: 28 | shader_3d = gpu.shader.from_builtin('UNIFORM_COLOR') 29 | shader_2d = gpu.shader.from_builtin('UNIFORM_COLOR') 30 | shader_debug = gpu.shader.from_builtin('UNIFORM_COLOR') 31 | shader_tex = gpu.shader.from_builtin('IMAGE') 32 | 33 | if type == '3d': 34 | return shader_3d 35 | elif type == '2d': 36 | return shader_2d 37 | elif type == 'debug': 38 | return shader_debug 39 | elif type == 'tex': 40 | return shader_tex 41 | 42 | 43 | def wrap_blf_size(font_id: int, size): 44 | if bpy.app.version >= (4, 0, 0): 45 | blf.size(font_id, size) 46 | else: 47 | blf.size(font_id, size, 72) 48 | 49 | 50 | def get_style_font_size() -> int: 51 | style = bpy.context.preferences.ui_styles[0] 52 | if widget_label := getattr(style, "widget_label", None): 53 | return int(widget_label.points * ui_scale()) 54 | elif widget := getattr(style, "widget", None): 55 | return int(widget.points) 56 | return 10 57 | 58 | 59 | def get_start_point(thumb_width, thumb_height, index=0): 60 | # const 61 | padding = int(10 * ui_scale()) 62 | font_size = int(get_style_font_size()) 63 | text_height = int(font_size * 3) # 2 line text and padding 64 | stats_height = int(font_size * 6 + padding * 5) # 5 line text and padding 65 | # get area 66 | area = bpy.context.area 67 | show_text = area.spaces[0].overlay.show_text 68 | show_stats = area.spaces[0].overlay.show_stats 69 | 70 | position = 'TOP_LEFT' 71 | 72 | if position == 'TOP_LEFT': 73 | right = False 74 | top = True 75 | elif position == 'TOP_RIGHT': 76 | right = True 77 | top = True 78 | elif position == 'BOTTOM_LEFT': 79 | right = False 80 | top = False 81 | else: 82 | right = True 83 | top = False 84 | 85 | ui_width = toolbar_width = header_height = tool_header_height = asset_shelf_width = 0 86 | if bpy.context.preferences.system.use_region_overlap: 87 | for region in area.regions: 88 | if region.type == 'UI': 89 | ui_width = region.width 90 | elif region.type == 'TOOLS': 91 | toolbar_width = region.width 92 | elif region.type == 'HEADER': 93 | header_height = region.height 94 | elif region.type == 'TOOL_HEADER': 95 | tool_header_height = region.height 96 | elif region.type == 'ASSET_shelf': 97 | asset_shelf_width = region.width 98 | 99 | header_height += tool_header_height 100 | if show_text: 101 | header_height += text_height 102 | if show_stats: 103 | header_height += stats_height 104 | 105 | if right: 106 | w = area.width - ui_width - thumb_width - padding 107 | else: 108 | w = padding + toolbar_width 109 | 110 | if top: 111 | h = area.height - header_height - thumb_height - padding 112 | else: 113 | h = padding 114 | 115 | if index != 0: 116 | h = h - index * (thumb_height + padding) 117 | 118 | return (w, h) 119 | 120 | 121 | class CameraThumb: 122 | border_width = 5 123 | 124 | def __init__(self, context, deps): 125 | self.context = context 126 | self.deps = deps 127 | self.offscreen = gpu.types.GPUOffScreen(WIDTH, HEIGHT) 128 | self.cam = None 129 | self.buffer = None 130 | self.snapshot = context.window_manager.camhp_snap_shot_image 131 | 132 | self.max_width = get_pref().camera_thumb.max_width 133 | self.max_height = get_pref().camera_thumb.max_height 134 | 135 | self.update_cam(context) 136 | self.update_resolution(context) 137 | 138 | def __call__(self, context): 139 | self.draw(context) 140 | 141 | def draw(self, context): 142 | if context.window_manager.camhp_pv.enable: 143 | self.draw_border(context) 144 | self.draw_camera_thumb(context) 145 | 146 | def update_resolution(self, context): 147 | max_height = get_pref().camera_thumb.max_width 148 | max_width = get_pref().camera_thumb.max_height 149 | self.height = max_height 150 | self.ratio = context.scene.render.resolution_x / context.scene.render.resolution_y 151 | self.width = int(self.height * self.ratio) 152 | if self.width > max_width: 153 | self.width = max_width 154 | self.height = int(self.width / self.ratio) 155 | 156 | def update_cam(self, context): 157 | try: 158 | if context.window_manager.camhp_pv.pin: 159 | cam = context.window_manager.camhp_pv.pin_cam 160 | else: 161 | cam = context.object 162 | 163 | self.cam = cam 164 | except ReferenceError: 165 | # delete class 166 | context.window_manager.camhp_pv.pin = False 167 | 168 | def draw_camera_thumb(self, context): 169 | try: 170 | self.update_cam(context) 171 | self.update_resolution(context) 172 | 173 | show_overlay = False 174 | scene = context.scene 175 | # matrix 176 | view_matrix = self.cam.matrix_world.inverted() 177 | projection_matrix = self.cam.calc_matrix_camera(self.deps, x=self.width, y=self.height) 178 | # set space data 179 | ori_show_overlay = context.space_data.overlay.show_overlays 180 | context.space_data.overlay.show_overlays = show_overlay 181 | 182 | # color management, if version is >= 5.0.0, the screen color space will be linear by default 183 | do_color_management = (bpy.app.version >= (5, 0, 0)) 184 | self.offscreen.draw_view3d( 185 | scene, 186 | context.view_layer, 187 | context.space_data, 188 | context.region, 189 | view_matrix, 190 | projection_matrix, 191 | do_color_management=do_color_management) 192 | gpu.state.depth_mask_set(False) 193 | context.space_data.overlay.show_overlays = ori_show_overlay 194 | start = get_start_point(self.width + self.border_width, self.height + self.border_width) 195 | draw_texture_2d(self.offscreen.texture_color, start, self.width, self.height) 196 | 197 | framebuffer = gpu.state.active_framebuffer_get() 198 | buffer = framebuffer.read_color(*start, self.width, self.height, 4, 0, 'FLOAT') 199 | buffer.dimensions = self.width * self.height * 4 200 | self.buffer = buffer 201 | 202 | # restore 203 | context.space_data.overlay.show_overlays = ori_show_overlay 204 | except Exception as e: 205 | print(e) 206 | 207 | def draw_border(self, context): 208 | border_color = (0.5, 0.5, 0.5, 1) 209 | 210 | def get_verts(x, y, w, h, t): return ( 211 | (x - t, y - t), (x + w + t, y - t), (x - t, y + h + t), (x + w + t, y + h + t)) 212 | 213 | indices = ((0, 1, 2), (2, 1, 3)) 214 | 215 | # bgl.glEnable(bgl.GL_BLEND) 216 | # bgl.glEnable(bgl.GL_LINE_SMOOTH) 217 | # bgl.glEnable(bgl.GL_DEPTH_TEST) 218 | 219 | shader_2d = get_shader('2d') 220 | shader_2d.bind() 221 | start = get_start_point(self.width + self.border_width, self.height + self.border_width) 222 | # shadow 223 | shader_2d.uniform_float('color', (0.15, 0.15, 0.15, 0.15)) 224 | batch = batch_for_shader( 225 | shader_2d, 'TRIS', 226 | { 227 | "pos": get_verts(*start, self.width, self.height, 5) 228 | }, 229 | indices=indices) 230 | batch.draw(shader_2d) 231 | 232 | # border 233 | shader_2d.uniform_float('color', border_color) 234 | border_batch = batch_for_shader( 235 | shader_2d, 'TRIS', 236 | { 237 | "pos": get_verts(*start, self.width, self.height, 1) 238 | }, 239 | indices=indices) 240 | border_batch.draw(shader_2d) 241 | 242 | # bgl.glDisable(bgl.GL_BLEND) 243 | # bgl.glDisable(bgl.GL_LINE_SMOOTH) 244 | # bgl.glEnable(bgl.GL_DEPTH_TEST) 245 | -------------------------------------------------------------------------------- /ops/old/draw_utils/bl_ui_slider.py: -------------------------------------------------------------------------------- 1 | from .bl_ui_widget import * 2 | from .shader import get_shader,wrap_blf_size 3 | import blf 4 | 5 | 6 | class BL_UI_Slider(BL_UI_Widget): 7 | 8 | def __init__(self, x, y, width, height): 9 | super().__init__(x, y, width, height) 10 | self._text_color = (1.0, 1.0, 1.0, 1.0) 11 | self._color = (0.5, 0.5, 0.7, 1.0) 12 | self._hover_color = (0.5, 0.5, 0.8, 1.0) 13 | self._select_color = (0.7, 0.7, 0.7, 1.0) 14 | self._bg_color = (0.8, 0.8, 0.8, 0.6) 15 | 16 | self._min = 0 17 | self._max = 100 18 | 19 | self.x_screen = x 20 | self.y_screen = y 21 | 22 | self._text_size = 14 23 | self._decimals = 2 24 | 25 | self._show_min_max = True 26 | 27 | self.__state = 0 28 | self.__is_drag = False 29 | self.__slider_pos = 0 30 | self.__slider_value = round(0, self._decimals) 31 | self.__slider_width = 5 32 | self.__slider_height = 13 33 | self.__slider_offset_y = 3 34 | 35 | @property 36 | def text_color(self): 37 | return self._text_color 38 | 39 | @text_color.setter 40 | def text_color(self, value): 41 | self._text_color = value 42 | 43 | @property 44 | def text_size(self): 45 | return self._text_size 46 | 47 | @text_size.setter 48 | def text_size(self, value): 49 | self._text_size = value 50 | 51 | @property 52 | def color(self): 53 | return self._color 54 | 55 | @color.setter 56 | def color(self, value): 57 | self._color = value 58 | 59 | @property 60 | def hover_color(self): 61 | return self._hover_color 62 | 63 | @hover_color.setter 64 | def hover_color(self, value): 65 | self._hover_color = value 66 | 67 | @property 68 | def select_color(self): 69 | return self._select_color 70 | 71 | @select_color.setter 72 | def select_color(self, value): 73 | self._select_color = value 74 | 75 | @property 76 | def min(self): 77 | return self._min 78 | 79 | @min.setter 80 | def min(self, value): 81 | self._min = value 82 | 83 | @property 84 | def max(self): 85 | return self._max 86 | 87 | @max.setter 88 | def max(self, value): 89 | self._max = value 90 | 91 | @property 92 | def decimals(self): 93 | return self._decimals 94 | 95 | @decimals.setter 96 | def decimals(self, value): 97 | self._decimals = value 98 | 99 | @property 100 | def show_min_max(self): 101 | return self._show_min_max 102 | 103 | @show_min_max.setter 104 | def show_min_max(self, value): 105 | self._show_min_max = value 106 | 107 | def draw(self): 108 | if not self.visible: 109 | return 110 | 111 | area_height = self.get_area_height() 112 | 113 | self.shader.bind() 114 | 115 | color = self._color 116 | text_color = self._text_color 117 | 118 | # pressed 119 | if self.__state == 1: 120 | color = self._select_color 121 | 122 | # hover 123 | elif self.__state == 2: 124 | color = self._hover_color 125 | 126 | # Draw background 127 | self.shader.uniform_float("color", self._bg_color) 128 | # bgl.glEnable(bgl.GL_BLEND) 129 | gpu.state.blend_set('ALPHA') 130 | self.batch_bg.draw(self.shader) 131 | 132 | # Draw slider 133 | self.shader.uniform_float("color", color) 134 | 135 | self.batch_slider.draw(self.shader) 136 | # bgl.glDisable(bgl.GL_BLEND) 137 | gpu.state.blend_set('None') 138 | 139 | 140 | # Draw value text 141 | wrap_blf_size(0,self._text_size) 142 | 143 | sValue = str(round(self.__slider_value, self._decimals)) 144 | size = blf.dimensions(0, sValue) 145 | 146 | blf.position(0, self.__slider_pos + 1 + self.x_screen - size[0] / 2.0, 147 | area_height - self.y_screen + self.__slider_offset_y, 0) 148 | 149 | blf.draw(0, sValue) 150 | 151 | # Draw min and max 152 | if self._show_min_max: 153 | sMin = str(round(self._min, self._decimals)) 154 | 155 | size = blf.dimensions(0, sMin) 156 | 157 | blf.position(0, self.x_screen - size[0] / 2.0, 158 | area_height - self.height - self.y_screen, 0) 159 | blf.draw(0, sMin) 160 | 161 | sMax = str(round(self._max, self._decimals)) 162 | 163 | size = blf.dimensions(0, sMax) 164 | 165 | r, g, b, a = self._text_color 166 | blf.color(0, r, g, b, a) 167 | 168 | blf.position(0, self.x_screen + self.width - size[0] / 2.0, 169 | area_height - self.height - self.y_screen, 0) 170 | blf.draw(0, sMax) 171 | 172 | def update_slider(self): 173 | # Slider triangles 174 | # 175 | # 0 176 | # 1 /\ 2 177 | # | | 178 | # 3---- 4 179 | 180 | # batch for slider 181 | area_height = self.get_area_height() 182 | 183 | h = self.__slider_height 184 | w = self.__slider_width 185 | pos_y = area_height - self.y_screen - self.height / 2.0 + self.__slider_height / 2.0 + self.__slider_offset_y 186 | pos_x = self.x_screen + self.__slider_pos 187 | 188 | indices = ((0, 1, 2), (1, 2, 3), (3, 2, 4)) 189 | 190 | vertices = ( 191 | (pos_x, pos_y), 192 | (pos_x - w, pos_y - w), 193 | (pos_x + w, pos_y - w), 194 | (pos_x - w, pos_y - h), 195 | (pos_x + w, pos_y - h) 196 | ) 197 | 198 | self.shader = get_shader(type = '2d') 199 | self.batch_slider = batch_for_shader(self.shader, 'TRIS', 200 | {"pos": vertices}, indices=indices) 201 | 202 | def update(self, x, y): 203 | 204 | area_height = self.get_area_height() 205 | 206 | # Min Max 207 | # |---------V--------------| 208 | 209 | self.x_screen = x 210 | self.y_screen = y 211 | 212 | self.update_slider() 213 | 214 | # batch for background 215 | pos_y = area_height - self.y_screen - self.height / 2.0 216 | pos_x = self.x_screen 217 | 218 | indices = ((0, 1, 2), (0, 2, 3)) 219 | 220 | # bottom left, top left, top right, bottom right 221 | vertices = ( 222 | (pos_x, pos_y), 223 | (pos_x, pos_y + 4), 224 | (pos_x + self.width, pos_y + 4), 225 | (pos_x + self.width, pos_y) 226 | ) 227 | 228 | self.batch_bg = batch_for_shader(self.shader, 'TRIS', {"pos": vertices}, indices=indices) 229 | 230 | def set_value_change(self, value_change_func): 231 | self.value_change_func = value_change_func 232 | 233 | def is_in_rect(self, x, y): 234 | area_height = self.get_area_height() 235 | slider_y = area_height - self.y_screen - self.height / 2.0 + self.__slider_height / 2.0 + self.__slider_offset_y 236 | 237 | if ( 238 | (self.x_screen + self.__slider_pos - self.__slider_width <= x <= 239 | (self.x_screen + self.__slider_pos + self.__slider_width)) and 240 | (slider_y >= y >= slider_y - self.__slider_height) 241 | ): 242 | return True 243 | 244 | return False 245 | 246 | def __value_to_pos(self, value): 247 | return self.width * (value - self._min) / (self._max - self._min) 248 | 249 | def __pos_to_value(self, pos): 250 | return self._min + round(((self._max - self._min) * self.__slider_pos / self.width), self._decimals) 251 | 252 | def get_value(self): 253 | return self.__slider_value 254 | 255 | def set_value(self, value): 256 | if value < self._min: 257 | value = self._min 258 | if value > self._max: 259 | value = self._max 260 | 261 | if value != self.__slider_value: 262 | self.__slider_value = round(value, self._decimals) 263 | 264 | try: 265 | self.value_change_func(self, self.__slider_value) 266 | except: 267 | pass 268 | 269 | self.__slider_pos = self.__value_to_pos(self.__slider_value) 270 | 271 | if self.context is not None: 272 | self.update_slider() 273 | 274 | def __set_slider_pos(self, x): 275 | if x <= self.x_screen: 276 | self.__slider_pos = 0 277 | elif x >= self.x_screen + self.width: 278 | self.__slider_pos = self.width 279 | else: 280 | self.__slider_pos = x - self.x_screen 281 | 282 | newValue = self.__pos_to_value(self.__slider_pos) 283 | 284 | if newValue != self.__slider_value: 285 | self.__slider_value = newValue 286 | 287 | try: 288 | self.value_change_func(self, self.__slider_value) 289 | except: 290 | pass 291 | 292 | def mouse_down(self, x, y): 293 | if self.is_in_rect(x, y): 294 | self.__state = 1 295 | self.__is_drag = True 296 | 297 | return True 298 | 299 | return False 300 | 301 | def mouse_move(self, x, y): 302 | if self.is_in_rect(x, y): 303 | if (self.__state != 1): 304 | # hover state 305 | self.__state = 2 306 | else: 307 | self.__state = 0 308 | 309 | if self.__is_drag: 310 | self.__set_slider_pos(x) 311 | self.update(self.x_screen, self.y_screen) 312 | 313 | def mouse_up(self, x, y): 314 | self.__state = 0 315 | self.__is_drag = False 316 | -------------------------------------------------------------------------------- /gizmos/motion/gz_custom.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | import bpy 4 | from bpy_extras import view3d_utils 5 | from mathutils import Vector 6 | 7 | from ops.old.draw_utils import wrap_bgl_restore 8 | from ops.motion.op_motion_cam import get_obj_2d_loc 9 | 10 | @dataclass 11 | class GizmoInfo_2D: 12 | name: str 13 | type: str 14 | icon: str 15 | draw_options: set[str] 16 | 17 | alpha: float 18 | color: list[float] 19 | alpha_highlight: float 20 | color_highlight: list[float] 21 | 22 | scale_basis: float = (80 * 0.35) / 2 23 | use_tooltip: bool = True 24 | 25 | 26 | class GizmoBase3D(bpy.types.Gizmo): 27 | # The id must be "offset" 28 | bl_target_properties = ( 29 | {"id": "offset", "type": 'FLOAT', "array_length": 1}, 30 | ) 31 | __slots__ = ( 32 | # Automatically add attributes for properties defined in the GizmoInfo above 33 | "draw_options", 34 | "draw_style", 35 | "gizmo_name", 36 | 37 | # Extra attributes used for intersection 38 | "custom_shape", 39 | "init_mouse", 40 | "init_value", 41 | "_index", 42 | "_camera", 43 | ) 44 | 45 | def setup(self): 46 | if not hasattr(self, "custom_shape"): 47 | self.custom_shape = self.new_custom_shape('LINES', create_geo_shape()) 48 | 49 | def draw(self, context): 50 | self._update_offset_matrix() 51 | self.draw_custom_shape(self.custom_shape) 52 | 53 | def draw_select(self, context, select_id): 54 | if self.custom_shape is None: 55 | self.custom_shape = create_geo_shape() 56 | 57 | self._update_offset_matrix() 58 | with wrap_bgl_restore(self.line_width): 59 | self.draw_custom_shape(self.custom_shape, select_id=select_id) 60 | 61 | def mouse_ray(self, context, event): 62 | """获取鼠标射线""" 63 | region = context.region 64 | rv3d = context.region_data 65 | coord = event.mouse_region_x, event.mouse_region_y 66 | ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, coord) 67 | ray_direction = view3d_utils.region_2d_to_vector_3d(region, rv3d, coord) 68 | return ray_origin, ray_direction 69 | 70 | def exit(self, context, cancel): 71 | context.area.header_text_set(None) 72 | if cancel: 73 | self.target_set_value("offset", self.init_value) 74 | 75 | def invoke(self, context, event): 76 | self.init_mouse = self._projected_value(context, event) 77 | self.init_value = self.target_get_value("offset") 78 | return {'RUNNING_MODAL'} 79 | 80 | 81 | class CAMHP_OT_insert_keyframe(bpy.types.Operator): 82 | bl_idname = 'camhp.insert_keyframe' 83 | bl_label = 'Insert Keyframe' 84 | 85 | def execute(self, context): 86 | context.object.motion_cam.keyframe_insert('offset_factor') 87 | 88 | for area in context.window.screen.areas: 89 | for region in area.regions: 90 | region.tag_redraw() 91 | 92 | return {'FINISHED'} 93 | 94 | 95 | class CAMHP_GT_custom_move_1d(GizmoBase3D, Gizmo): 96 | bl_idname = "CAMHP_GT_custom_move_1d" 97 | # The id must be "offset" 98 | bl_target_properties = ( 99 | {"id": "offset", "type": 'FLOAT', "array_length": 1}, 100 | ) 101 | 102 | def setup(self): 103 | if not hasattr(self, "custom_shape"): 104 | pref_gz = get_pref().gz_motion_camera 105 | shape_obj = load_shape_geo_obj('SLIDE') 106 | 107 | self.custom_shape = self.new_custom_shape('TRIS', 108 | create_geo_shape(obj=shape_obj, scale=pref_gz.scale_basis)) 109 | try: 110 | bpy.data.objects.remove(shape_obj) 111 | except: 112 | pass 113 | 114 | def _update_offset_matrix(self): 115 | # offset behind the light 116 | self.matrix_offset.col[3][2] = self.target_get_value("offset") 117 | 118 | def _projected_value(self, context, event): 119 | return event.mouse_x 120 | 121 | def modal(self, context, event, tweak): 122 | mouse = self._projected_value(context, event) 123 | delta = (mouse - self.init_mouse) 124 | if 'SNAP' in tweak: 125 | delta = round(delta) 126 | if 'PRECISE' in tweak: 127 | delta /= 10.0 128 | 129 | value = self.init_value - delta / 1000 130 | 131 | start_cam = context.object.motion_cam.list[0] 132 | end_cam = context.object.motion_cam.list[1] 133 | 134 | # 比较开始结束相机的屏幕位置以决定是否反向 135 | if start_cam.camera and end_cam.camera: 136 | start_x, start_y = get_obj_2d_loc(start_cam.camera, context) 137 | end_x, end_y = get_obj_2d_loc(end_cam.camera, context) 138 | 139 | if end_x > start_x: 140 | value = self.init_value + delta / 1000 141 | 142 | # loop 143 | if get_pref().gz_motion_camera.loop: 144 | if value > 1: 145 | value = abs(1 - value) 146 | elif value < 0: 147 | value = abs(value + 1) 148 | else: 149 | if value > 1: 150 | value = 1 151 | elif value < 0: 152 | value = 0 153 | 154 | self.target_set_value("offset", value) 155 | context.area.header_text_set(f"Move: {value:.4f}") 156 | return {'RUNNING_MODAL'} 157 | 158 | def invoke(self, context, event): 159 | self.init_mouse = self._projected_value(context, event) 160 | self.init_value = self.target_get_value("offset") 161 | 162 | if event.ctrl: 163 | def pop_up(cls, context): 164 | layout = cls.layout 165 | d = self._camera.data 166 | layout.popover(panel='CAMHP_PT_MotionCamPanel') 167 | 168 | context.window_manager.popup_menu(pop_up, title=f'{self._camera.name}', icon='CAMERA_DATA') 169 | # bpy.ops.wm.call_panel(name='CAMHP_PT_MotionCamPanel', keep_open=True) 170 | return {'INTERFACE'} 171 | 172 | return super().invoke(context, event) 173 | 174 | 175 | # 创建3d gizmos 176 | class CAMHP_GT_custom_move_3d(GizmoBase3D, Gizmo): 177 | bl_idname = "CAMHP_GT_custom_move_3d" 178 | # The id must be "offset" 179 | bl_target_properties = ( 180 | {"id": "offset", "type": 'FLOAT', "array_length": 3}, 181 | ) 182 | 183 | def _update_offset_matrix(self): 184 | try: 185 | x, y, z = self.target_get_value("offset") 186 | self.matrix_offset.col[3][0] = x 187 | self.matrix_offset.col[3][1] = y 188 | self.matrix_offset.col[3][2] = z 189 | except ValueError: 190 | pass 191 | 192 | def setup(self): 193 | if not hasattr(self, "custom_shape"): 194 | pref_gz = get_pref().gz_motion_source 195 | shape_obj = load_shape_geo_obj('MOVE') 196 | self.custom_shape = self.new_custom_shape('TRIS', 197 | create_geo_shape(obj=shape_obj, scale=pref_gz.scale_basis)) 198 | 199 | try: 200 | bpy.data.objects.remove(shape_obj) 201 | except: 202 | pass 203 | 204 | def _projected_value(self, context, event): 205 | """用于光线投射 206 | 207 | :param context: 208 | :param event: 209 | :return: offset Vector 210 | """ 211 | ray_origin, ray_direction = self.mouse_ray(context, event) 212 | mat = self.matrix_basis.inverted() 213 | ray_origin = mat @ ray_origin 214 | ray_direction = mat.to_3x3() @ ray_direction 215 | 216 | dist = ray_origin.magnitude 217 | 218 | offset = ray_origin + dist * ray_direction 219 | return offset 220 | 221 | def invoke(self, context, event): 222 | if event.ctrl: 223 | def pop_up(cls, context): 224 | layout = cls.layout 225 | d = self._camera.data 226 | layout.prop(d, 'lens') 227 | layout.prop(d.dof, 'focus_distance') 228 | layout.prop(d.dof, 'aperture_fstop') 229 | # layout.separator() 230 | # ob = self._camera 231 | # col = layout.column() 232 | # col.prop(ob, "rotation_euler", text="Rotation") 233 | # layout.popover(panel='CAMHP_PT_MotionCamPanel') 234 | 235 | context.window_manager.popup_menu(pop_up, title=f'{self._camera.name}', icon='CAMERA_DATA') 236 | 237 | return {'INTERFACE'} 238 | 239 | self.init_mouse = self._projected_value(context, event) 240 | self.init_value = Vector(self.target_get_value("offset")) 241 | 242 | return {'RUNNING_MODAL'} 243 | 244 | def modal(self, context, event, tweak): 245 | mouse = self._projected_value(context, event) 246 | delta = (mouse - self.init_mouse) 247 | if 'SNAP' in tweak: 248 | delta = Vector([round(d) for d in delta]) 249 | if 'PRECISE' in tweak: 250 | delta /= 10.0 251 | value = self.init_value + delta 252 | self.target_set_value("offset", value) 253 | context.area.header_text_set(f"{self._camera.name}: {value[0]:.4f}, {value[1]:.4f}, {value[2]:.4f}") 254 | return {'RUNNING_MODAL'} 255 | 256 | def set_curve_pos(self): 257 | # print(tweak) 258 | # widget驱动曲线点 259 | # if context.object.motion_cam.path: 260 | # spline = context.object.motion_cam.path.data.splines[0] 261 | # spline.bezier_points[self._index].co = value 262 | # spline.bezier_points[self._index].handle_left = value 263 | # spline.bezier_points[self._index].handle_right = value 264 | 265 | # context.area.header_text_set(f"{self.gizmo_name}: {value[0]:.4f}, {value[1]:.4f}, {value[2]:.4f}") 266 | pass 267 | 268 | 269 | class CAMHP_GT_custom_rotate_1d(Gizmo): 270 | bl_idname = "CAMHP_GT_custom_rotate_1d" 271 | 272 | def draw(self, context): 273 | self.draw_custom_shape(self.custom_shape) 274 | 275 | def draw_select(self, context, select_id): 276 | self.draw_custom_shape(self.custom_shape, select_id=select_id) 277 | 278 | def setup(self): 279 | if not hasattr(self, "custom_shape"): 280 | pref_gz = get_pref().gz_motion_source 281 | shape_obj = load_shape_geo_obj('ROTATE') 282 | self.custom_shape = self.new_custom_shape('TRIS', 283 | create_geo_shape(obj=shape_obj, scale=pref_gz.scale_basis * 3)) 284 | 285 | try: 286 | bpy.data.objects.remove(shape_obj) 287 | except: 288 | pass 289 | 290 | 291 | class CAMHP_OT_rotate_object(bpy.types.Operator): 292 | bl_idname = 'camhp.rotate_object' 293 | bl_label = 'Rotate Object' 294 | # bl_options = {'REGISTER', 'UNDO'} 295 | 296 | obj_name: bpy.props.StringProperty(name='Object Name') 297 | axis: bpy.props.EnumProperty(items=[('X', 'X', 'X'), ('Y', 'Y', 'Y'), ('Z', 'Z', 'Z')]) 298 | 299 | def append_handles(self): 300 | self.cursor_set = True 301 | bpy.context.window.cursor_modal_set('MOVE_X') 302 | bpy.context.window_manager.modal_handler_add(self) 303 | 304 | def remove_handles(self): 305 | bpy.context.window.cursor_modal_restore() 306 | 307 | def modal(self, context, event): 308 | if event.type == 'MOUSEMOVE': 309 | self.mouseDX = self.mouseDX - event.mouse_x 310 | # shift 减速 311 | multiplier = 0.005 if event.shift else 0.01 312 | offset = multiplier * self.mouseDX 313 | 314 | # 校正 315 | loc_x, loc_y = get_obj_2d_loc(self.obj, context) 316 | if self.startX > loc_x and self.axis != 'Z': 317 | offset *= -1 318 | 319 | rotate_mode = {'Z': 'ZYX', 'X': 'XYZ', 'Y': 'YXZ'}[self.axis] 320 | 321 | # 设置旋转矩阵(缩放为负数时失效) 322 | rot = self.obj.rotation_euler.to_matrix().to_euler(rotate_mode) 323 | axis = self.axis.lower() 324 | setattr(rot, axis, getattr(rot, axis) + offset) 325 | self.obj.rotation_euler = rot.to_matrix().to_euler(self.obj.rotation_mode) 326 | 327 | # 重置 328 | self.mouseDX = event.mouse_x 329 | 330 | elif event.type == 'LEFTMOUSE': 331 | self.remove_handles() 332 | return {'FINISHED'} 333 | 334 | elif event.type in {'RIGHTMOUSE', 'ESC'}: 335 | return {'CANCELLED'} 336 | 337 | return {'RUNNING_MODAL'} 338 | 339 | def invoke(self, context, event): 340 | self.obj = bpy.data.objects[self.obj_name] 341 | 342 | self.mouseDX = event.mouse_x 343 | self.startX = event.mouse_x 344 | 345 | # add handles 346 | self.append_handles() 347 | 348 | return {"RUNNING_MODAL"} 349 | -------------------------------------------------------------------------------- /ops/motion/op_motion_cam.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import CollectionProperty, PointerProperty, FloatProperty, IntProperty, StringProperty, BoolProperty, \ 3 | EnumProperty 4 | from bpy.types import PropertyGroup, Operator, Panel, UIList 5 | 6 | from ops.old.utils import gen_bezier_curve_from_points, gen_sample_attr_obj, gen_sample_mesh_obj 7 | from ops.old.utils import meas_time, get_mesh_obj_attrs 8 | 9 | C_ATTR_FAC = 'factor' 10 | C_ATTR_LENGTH = 'length' 11 | G_STATE_UPDATE = False # 用于保护曲线更新的状态 12 | # 绕过blender更新bug 13 | G_PROPS = {} 14 | 15 | 16 | def parse_data_path(src_obj, scr_data_path): 17 | """解析来自用户的data_path 18 | 19 | :param src_obj: 20 | :param scr_data_path: 21 | :return: 22 | """ 23 | 24 | def get_obj_and_attr(obj, data_path): 25 | path = data_path.split('.') 26 | if len(path) == 1 and hasattr(obj, path[0]): 27 | return obj, path[0] 28 | else: 29 | if path[0] == '': 30 | return obj, '' 31 | if not hasattr(obj, path[0]): 32 | return obj, None 33 | 34 | back_obj = getattr(obj, path[0]) 35 | back_path = '.'.join(path[1:]) 36 | 37 | return get_obj_and_attr(back_obj, back_path) 38 | 39 | return get_obj_and_attr(src_obj, scr_data_path) 40 | 41 | 42 | def get_active_motion_item(obj): 43 | """ 44 | 45 | :param obj: bpy.types.Object 46 | :return: bpy.types.Object.motion_cam.list.item / NONE 47 | """ 48 | if len(obj.motion_cam.list) > 0: 49 | return obj.motion_cam.list[obj.motion_cam.list_index] 50 | 51 | 52 | def mix_value(val1, val2, fac): 53 | return (1 - fac) * val1 + fac * val2 54 | 55 | 56 | def get_interpolate_euler(from_obj, to_obj, fac): 57 | from_quart = from_obj.matrix_world.to_quaternion().copy() 58 | to_quart = to_obj.matrix_world.to_quaternion().copy() 59 | return from_quart.slerp(to_quart, fac).to_euler() 60 | 61 | 62 | def get_interpolate_lens(from_obj, to_obj, fac): 63 | G_PROPS['lens'] = mix_value(from_obj.data.lens, to_obj.data.lens, fac) 64 | return G_PROPS['lens'] 65 | 66 | 67 | def get_interpolate_fstop(from_obj, to_obj, fac): 68 | G_PROPS['fstop'] = mix_value(from_obj.data.dof.aperture_fstop, to_obj.data.dof.aperture_fstop, fac) 69 | return G_PROPS['fstop'] 70 | 71 | 72 | def get_interpolate_focal(from_obj, to_obj, fac): 73 | def get_focus_dis(cam): 74 | dis = cam.data.dof.focus_distance 75 | obj = cam.data.dof.focus_object 76 | if obj: 77 | get_loc = lambda ob: ob.matrix_world.translation 78 | dis = get_loc(obj).dist(get_loc(cam)) 79 | 80 | return dis 81 | 82 | G_PROPS['focal'] = mix_value(get_focus_dis(from_obj), get_focus_dis(to_obj), fac) 83 | return G_PROPS['focal'] 84 | 85 | 86 | def interpolate_cam(tg_obj, from_obj, to_obj, fac): 87 | """用于切换相机的插值 88 | 89 | :param tg_obj: 90 | :param from_obj: 91 | :param to_obj: 92 | :param fac: 93 | :return: 94 | """ 95 | affect = tg_obj.motion_cam.affect 96 | use_euler = affect.use_euler 97 | 98 | use_sub_camera = affect.use_sub_camera 99 | 100 | use_lens = affect.use_lens 101 | use_aperture_fstop = affect.use_aperture_fstop 102 | use_focus_distance = affect.use_focus_distance 103 | 104 | # 限定变化, 位置变化由曲线约束 105 | if use_euler: 106 | tg_obj.rotation_euler = get_interpolate_euler(from_obj, to_obj, fac) 107 | 108 | # 子级为相机 109 | if use_sub_camera and affect.sub_camera and affect.sub_camera.type == 'CAMERA': 110 | cam = affect.sub_camera 111 | elif tg_obj.type == 'CAMERA': 112 | cam = tg_obj 113 | else: 114 | cam = None 115 | 116 | # print(cam) 117 | if cam is None: return 118 | 119 | # 相机变化 120 | if from_obj.type == to_obj.type == 'CAMERA': 121 | # is_sub_camera 122 | if use_lens: 123 | cam.data.lens = get_interpolate_lens(from_obj, to_obj, fac) 124 | if use_aperture_fstop: 125 | cam.data.dof.aperture_fstop = get_interpolate_fstop(from_obj, to_obj, fac) 126 | if use_focus_distance: 127 | cam.data.dof.focus_distance = get_interpolate_focal(from_obj, to_obj, fac) 128 | 129 | # # 自定义 130 | # for item in affect.custom_props: 131 | # if item.data_path == '': continue 132 | # 133 | # src_obj, src_attr = parse_data_path(cam.data, item.data_path) 134 | # _from_obj, from_attr = parse_data_path(from_obj.data, item.data_path) 135 | # _to_obj, to_attr = parse_data_path(to_obj.data, item.data_path) 136 | # if from_attr is None or to_attr is None or src_attr is None: continue 137 | # 138 | # from_value = getattr(_from_obj, from_attr) 139 | # to_value = getattr(_to_obj, to_attr) 140 | # 141 | # try: 142 | # if isinstance(from_value, float): 143 | # setattr(src_obj, src_attr, mix_value(from_value, to_value, fac)) 144 | # elif isinstance(from_value, mathutils.Vector): 145 | # setattr(src_obj, src_attr, from_value.copy().lerp(to_value, fac)) 146 | # elif isinstance(from_value, mathutils.Matrix): 147 | # setattr(src_obj, src_attr, from_value.copy().lerp(to_value, fac)) 148 | # elif isinstance(from_value, bool): 149 | # setattr(src_obj, src_attr, from_value) 150 | # elif isinstance(from_value, bpy.types.Object): 151 | # setattr(src_obj, src_attr, from_value) 152 | # except Exception as e: 153 | # print(e) 154 | 155 | 156 | def gen_cam_path(self, context): 157 | """生成相机路径曲线 158 | 159 | :param self:` 160 | :param contexnt: m 161 | :return: 162 | """ 163 | 164 | @meas_time 165 | def process(): 166 | obj = self.id_data 167 | cam_list = list() 168 | for item in obj.motion_cam.list: 169 | if item.camera is not None: 170 | cam_list.append(item.camera) 171 | 172 | if len(cam_list) < 2: return 173 | 174 | cam_pts = [cam.matrix_world.translation for cam in cam_list] 175 | path = gen_bezier_curve_from_points(coords=cam_pts, 176 | type=obj.motion_cam.path_type, 177 | curve_name=obj.name + '-MotionPath', 178 | resolution_u=12) 179 | 180 | # 生成hook修改器(用于接受动画曲线的输入) 181 | path.modifiers.clear() 182 | for i, cam in enumerate(cam_list): 183 | # print(cam.name) 184 | hm = path.modifiers.new( 185 | name=f"Hook_{i}", 186 | type='HOOK', 187 | ) 188 | if i == 0: # bug,使用强制更新 189 | hm = path.modifiers.new( 190 | name=f"Hook_{i}", 191 | type='HOOK', 192 | ) 193 | if obj.motion_cam.path_type == 'SMOOTH': 194 | hm.vertex_indices_set([i * 3, i * 3 + 1, i * 3 + 2]) # 跳过手柄点 195 | else: 196 | hm.vertex_indices_set([i]) 197 | hm.object = cam 198 | 199 | # 生成用于采样/绘制的网格数据 200 | path_attr = gen_sample_attr_obj(path) 201 | path_mesh = gen_sample_mesh_obj(path) 202 | # 设置 203 | obj.motion_cam.path = path 204 | obj.motion_cam.path_attr = path_attr 205 | obj.motion_cam.path_mesh = path_mesh 206 | 207 | # 约束 208 | if 'Motion Camera' in obj.constraints: 209 | const = obj.constraints['Motion Camera'] 210 | obj.constraints.remove(const) 211 | 212 | const = obj.constraints.new('FOLLOW_PATH') 213 | const.name = 'Motion Camera' 214 | 215 | const.use_fixed_location = True 216 | const.target = path 217 | 218 | try: 219 | const.driver_remove('offset_factor') 220 | except: 221 | pass 222 | 223 | d = const.driver_add('offset_factor') 224 | d.driver.type = 'AVERAGE' 225 | 226 | var1 = d.driver.variables.new() 227 | var1.targets[0].id = obj 228 | var1.targets[0].data_path = 'motion_cam.offset_factor' 229 | 230 | # update for driver 231 | path.data.update_tag() 232 | 233 | # hide 234 | coll = bpy.context.collection 235 | # coll.objects.unlink(path) 236 | # coll.objects.unlink(path_attr) 237 | # coll.objects.unlink(path_mesh) 238 | 239 | global G_STATE_UPDATE 240 | 241 | G_STATE_UPDATE = True 242 | process() 243 | G_STATE_UPDATE = False 244 | 245 | 246 | # 偏移factor的get/set------------------------------------------------------------------- 247 | 248 | def get_offset_factor(self): 249 | return self.get('offset_factor', 0.0) 250 | 251 | 252 | def _update_cam(self, context): 253 | if G_STATE_UPDATE: return 254 | update_cam(self.id_data, self.id_data.motion_cam.offset_factor) 255 | 256 | 257 | def update_cam(obj, val): 258 | if 'Motion Camera' not in obj.constraints: 259 | return 260 | if obj.motion_cam.affect.enable is False: 261 | return 262 | 263 | # 防止移动帧时的无限触发更新 264 | if hasattr(bpy.context, 'active_operator'): 265 | if bpy.context.active_operator == getattr(getattr(bpy.ops, 'transform'), 'transform'): return 266 | 267 | path = obj.motion_cam.path 268 | path_attr = obj.motion_cam.path_attr 269 | path_mesh = obj.motion_cam.path_mesh 270 | 271 | if path is None or path_attr is None or path_mesh is None: 272 | return 273 | 274 | attr_values_dict = get_mesh_obj_attrs(bpy.context, path_attr) 275 | attr_fac = attr_values_dict.get(C_ATTR_FAC) 276 | attr_length = attr_values_dict.get(C_ATTR_LENGTH) 277 | 278 | if not attr_fac or not attr_length: return 279 | 280 | for i, item in enumerate(obj.motion_cam.list): 281 | item_next = obj.motion_cam.list[i + 1] if i < len(obj.motion_cam.list) - 1 else None 282 | item_pre = obj.motion_cam.list[i - 1] if i > 0 else None 283 | 284 | fac = attr_fac[i] 285 | 286 | if item_next: # 有下一点时,用本点和下一点比较,若value存在当前区间,则在当前相机重进行转换 287 | from_obj = item.camera 288 | to_obj = item_next.camera 289 | next_fac = attr_fac[i + 1] 290 | 291 | if fac <= val < next_fac: 292 | true_fac = (val - fac) / (next_fac - fac) 293 | 294 | interpolate_cam(obj, from_obj, to_obj, true_fac) 295 | break 296 | else: # 不存在下一点时,与上一点进行比较 297 | if item_pre is None: continue 298 | 299 | from_obj = item_pre.camera 300 | to_obj = item.camera 301 | pre_fac = attr_fac[i - 1] 302 | 303 | if pre_fac <= val < fac: 304 | true_fac = (val - pre_fac) / (fac - pre_fac) 305 | else: 306 | true_fac = 1 307 | 308 | interpolate_cam(obj, from_obj, to_obj, true_fac) 309 | break 310 | 311 | 312 | def set_offset_factor(self, value): 313 | # 限制或循环val 314 | val = max(min(value, 1), 0) # 循环 315 | self['offset_factor'] = val 316 | 317 | obj = self.id_data 318 | 319 | global G_STATE_UPDATE 320 | if G_STATE_UPDATE is True: return 321 | 322 | G_STATE_UPDATE = True 323 | 324 | # obj.constraints['Motion Camera'].offset_factor = value 325 | update_cam(obj, val) 326 | 327 | G_STATE_UPDATE = False 328 | 329 | 330 | def update_driver(self, context): 331 | obj = self.id_data 332 | if 'Motion Camera' not in obj.constraints: 333 | return 334 | cons = obj.constraints['Motion Camera'] 335 | cons.enabled = obj.motion_cam.affect.enable 336 | 337 | 338 | # -------------------------------------------------------------------------------- 339 | 340 | # Properties -------------------------------------------------------------- 341 | ############################################################################### 342 | 343 | class MotionCamItemProps(PropertyGroup): 344 | camera: PointerProperty(name='Camera', type=bpy.types.Object, 345 | poll=lambda self, obj: obj.type == 'CAMERA' and obj != self, 346 | update=gen_cam_path) 347 | 348 | 349 | class MotionCamAffectCustomProp(PropertyGroup): 350 | data_path: StringProperty(name='Data Path', default='') 351 | 352 | 353 | class MotionCamAffect(PropertyGroup): 354 | enable: BoolProperty(name='Enable', default=True, update=update_driver, options={'HIDDEN'}, ) 355 | 356 | use_euler: BoolProperty(name='Rotation', default=True, options={'HIDDEN'}) 357 | # search for sub camera 358 | use_sub_camera: BoolProperty(name='Sub Camera', default=False, options={'HIDDEN'}) 359 | sub_camera: PointerProperty(type=bpy.types.Object) 360 | 361 | use_lens: BoolProperty(name='Focal Length', default=True, options={'HIDDEN'}) 362 | use_focus_distance: BoolProperty(name='Focus Distance', default=True, options={'HIDDEN'}) 363 | use_aperture_fstop: BoolProperty(name='F-Stop', default=True, options={'HIDDEN'}) 364 | 365 | custom_props: CollectionProperty(type=MotionCamAffectCustomProp) 366 | 367 | 368 | class MotionCamListProp(PropertyGroup): 369 | # UI 370 | ui: EnumProperty(items=[('CONTROL', 'Set', ''), ('AFFECT', 'Affect', '')], options={'HIDDEN'}) 371 | # 相机列表 372 | list: CollectionProperty(name='List', type=MotionCamItemProps) 373 | list_index: IntProperty(name='List', min=0, default=0, update=gen_cam_path) 374 | 375 | # 路径 376 | path: PointerProperty(type=bpy.types.Object) 377 | path_attr: PointerProperty(type=bpy.types.Object) # 用于实时采样属性 378 | path_mesh: PointerProperty(type=bpy.types.Object) # 用于实时采样位置,绘制 379 | path_type: EnumProperty(name='Type', items=[('LINEAR', 'Linear', ''), ('SMOOTH', 'Smooth', '')], 380 | default='SMOOTH', 381 | options={'HIDDEN'}, 382 | update=gen_cam_path) 383 | 384 | # 偏移 用于混合相机其他参数 385 | offset_factor: FloatProperty(name='Offset Factor', min=0, max=1, 386 | description='Offset Factor', 387 | set=set_offset_factor, 388 | get=get_offset_factor) 389 | 390 | # 影响 391 | affect: PointerProperty(type=MotionCamAffect) 392 | 393 | 394 | ############################################################################### 395 | 396 | 397 | # List/Operators --------------------------------------------------------------- 398 | ############################################################################### 399 | 400 | class CAMHP_OT_affect_add_custom_prop(Operator): 401 | bl_idname = 'camhp.affect_add_custom_prop' 402 | bl_label = 'Add Custom Prop' 403 | bl_description = 'Add Custom Prop' 404 | bl_options = {'REGISTER', 'UNDO'} 405 | 406 | def execute(self, context): 407 | obj = context.object 408 | m_cam = obj.motion_cam 409 | affect = m_cam.affect 410 | 411 | item = affect.custom_props.add() 412 | return {'FINISHED'} 413 | 414 | 415 | class CAMHP_OT_affect_remove_custom_prop(Operator): 416 | bl_idname = 'camhp.affect_remove_custom_prop' 417 | bl_label = 'Remove' 418 | bl_description = 'Remove Custom Prop' 419 | bl_options = {'REGISTER', 'UNDO'} 420 | 421 | index: IntProperty() 422 | 423 | def execute(self, context): 424 | obj = context.object 425 | m_cam = obj.motion_cam 426 | affect = m_cam.affect 427 | 428 | affect.custom_props.remove(self.index) 429 | return {'FINISHED'} 430 | 431 | 432 | class CAMHP_UL_CameraList(UIList): 433 | 434 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): 435 | row = layout.row(align=True) 436 | row.label(text='', icon='CAMERA_DATA') 437 | layout.prop(item, 'camera', text='', emboss=True) 438 | 439 | 440 | class ListAction: 441 | """Add / Remove / Copy current config""" 442 | bl_options = {'INTERNAL', 'UNDO'} 443 | 444 | index: IntProperty() 445 | action = None 446 | 447 | def execute(self, context): 448 | m_cam = context.object.motion_cam 449 | 450 | if self.action == 'ADD': 451 | new_item = m_cam.list.add() 452 | new_item.name = f'Motion{len(m_cam.list)}' 453 | new_item.influence = 0.5 454 | # correct index 455 | old_index = m_cam.list_index 456 | new_index = len(m_cam.list) - 1 457 | m_cam.list_index = new_index 458 | 459 | for i in range(old_index, new_index - 1): 460 | bpy.ops.camhp.motion_list_up() 461 | 462 | elif self.action == 'REMOVE': 463 | m_cam.list.remove(self.index) 464 | m_cam.list_index = self.index - 1 if self.index != 0 else 0 465 | 466 | elif self.action == 'COPY': 467 | src_item = m_cam.list[self.index] 468 | 469 | new_item = m_cam.list.add() 470 | 471 | for key in src_item.__annotations__.keys(): 472 | value = getattr(src_item, key) 473 | setattr(new_item, key, value) 474 | 475 | old_index = m_cam.list_index 476 | new_index = len(m_cam.list) - 1 477 | m_cam.list_index = len(m_cam.list) - 1 478 | 479 | for i in range(old_index, new_index - 1): 480 | bpy.ops.camhp.motion_list_up() 481 | 482 | return {'FINISHED'} 483 | 484 | 485 | class ListMove: 486 | bl_options = {'INTERNAL', 'UNDO'} 487 | 488 | index: IntProperty() 489 | action = None 490 | 491 | def execute(self, context): 492 | m_cam = context.object.motion_cam 493 | 494 | my_list = m_cam.list 495 | index = m_cam.list_index 496 | neighbor = index + (-1 if self.action == 'UP' else 1) 497 | my_list.move(neighbor, index) 498 | self.move_index(context) 499 | 500 | return {'FINISHED'} 501 | 502 | def move_index(self, context): 503 | m_cam = context.object.motion_cam 504 | index = m_cam.list_index 505 | new_index = index + (-1 if self.action == 'UP' else 1) 506 | m_cam.list_index = max(0, min(new_index, len(m_cam.list) - 1)) 507 | 508 | 509 | class CAMHP_OT_motion_list_add(ListAction, Operator): 510 | """""" 511 | bl_idname = 'camhp.motion_list_add' 512 | bl_label = 'Add' 513 | 514 | action = 'ADD' 515 | 516 | 517 | class CAMHP_OT_motion_list_remove(ListAction, Operator): 518 | """""" 519 | bl_idname = 'camhp.motion_list_remove' 520 | bl_label = 'Remove' 521 | 522 | action = 'REMOVE' 523 | 524 | 525 | class CAMHP_OT_copy_motion_cam(ListAction, Operator): 526 | """""" 527 | bl_idname = 'camhp.motion_list_copy' 528 | bl_label = 'Copy' 529 | 530 | action = 'COPY' 531 | 532 | 533 | class CAMHP_OT_move_up_motion_cam(ListMove, Operator): 534 | """""" 535 | bl_idname = 'camhp.motion_list_up' 536 | bl_label = 'Move Up' 537 | 538 | action = 'UP' 539 | 540 | 541 | class CAMHP_OT_move_down_motion_cam(ListMove, Operator): 542 | """""" 543 | bl_idname = 'camhp.motion_list_down' 544 | bl_label = 'Move Down' 545 | 546 | action = 'DOWN' 547 | 548 | 549 | # Operator for the list of cameras ------------------------------------------- 550 | ############################################################################### 551 | 552 | from ops.old.draw_utils.bl_ui_draw_op import BL_UI_OT_draw_operator 553 | from ops.old.draw_utils.bl_ui_button import BL_UI_Button 554 | from ops.old.draw_utils.bl_ui_drag_panel import BL_UI_Drag_Panel 555 | from ops.old.draw_utils.bl_ui_label import BL_UI_Label 556 | from ...utils.asset import AssetDir, get_asset_dir 557 | 558 | from bpy_extras.view3d_utils import location_3d_to_region_2d 559 | from bpy.app.translations import pgettext_iface as tip_ 560 | 561 | 562 | def get_obj_2d_loc(obj, context): 563 | r3d = context.space_data.region_3d 564 | loc = location_3d_to_region_2d(context.region, r3d, obj.matrix_world.translation) 565 | return loc 566 | 567 | 568 | def load_asset(name: str, asset_type: str, filepath: str) -> bpy.types.NodeTree | bpy.types.Object: 569 | """load asset into current scene from giving asset type""" 570 | if asset_type == 'objects': 571 | attr = 'objects' 572 | elif asset_type == 'node_groups': 573 | attr = 'node_groups' 574 | else: 575 | raise ValueError('asset_type not support') 576 | 577 | # reuse existing data 578 | data_lib = getattr(bpy.data, attr) 579 | if name in data_lib and asset_type in {'node_groups'}: 580 | return data_lib[name] 581 | 582 | with bpy.data.libraries.load(filepath, link=False) as (data_from, data_to): 583 | src = getattr(data_from, attr) 584 | res = [x for x in src if x == name] 585 | if not res: 586 | raise ValueError(f'No {name} found in {filepath}') 587 | setattr(data_to, attr, res) 588 | # clear asset mark 589 | obj = getattr(data_to, attr)[0] 590 | obj.asset_clear() 591 | return obj 592 | 593 | 594 | ############################################################################### 595 | 596 | 597 | # UI ------------------------------------------- 598 | ############################################################################### 599 | 600 | class CAMHP_PT_MotionCamPanel(Panel): 601 | bl_label = 'Motion Camera' 602 | bl_space_type = 'PROPERTIES' 603 | bl_idname = 'CAMHP_PT_MotionCamPanel' 604 | bl_context = "object" 605 | bl_region_type = 'WINDOW' 606 | 607 | @classmethod 608 | def poll(self, context): 609 | return context.object and context.object.type in {'EMPTY', 'CAMERA'} 610 | 611 | def draw(self, context): 612 | layout = self.layout 613 | 614 | # layout.use_property_decorate = False 615 | row = layout.row(align=True) 616 | row.prop(context.object.motion_cam, 'ui', expand=True) 617 | 618 | if context.object.motion_cam.ui == 'CONTROL': 619 | self.draw_control(context, layout) 620 | elif context.object.motion_cam.ui == 'AFFECT': 621 | self.draw_setttings(context, layout) 622 | 623 | def draw_control(self, context, layout): 624 | layout.label(text=context.object.name, icon=context.object.type + '_DATA') 625 | 626 | layout.prop(context.object.motion_cam, 'path_type') 627 | layout.prop(context.object.motion_cam, 'offset_factor', slider=True) 628 | 629 | # 视口k帧 630 | if context.area.type == 'VIEW_3D': 631 | layout.operator('camhp.insert_keyframe') 632 | 633 | layout.label(text='Source') 634 | box = layout.box() 635 | row = box.row(align=True) 636 | 637 | col = row.column(align=0) 638 | 639 | col.template_list( 640 | "CAMHP_UL_CameraList", "The list", 641 | context.object.motion_cam, "list", 642 | context.object.motion_cam, "list_index", ) 643 | 644 | col_btn = row.column(align=1) 645 | 646 | col_btn.operator('camhp.motion_list_add', text='', icon='ADD') 647 | 648 | d = col_btn.operator('camhp.motion_list_remove', text='', icon='REMOVE') 649 | d.index = context.object.motion_cam.list_index 650 | 651 | col_btn.separator() 652 | 653 | col_btn.operator('camhp.motion_list_up', text='', icon='TRIA_UP') 654 | col_btn.operator('camhp.motion_list_down', text='', icon='TRIA_DOWN') 655 | 656 | col_btn.separator() 657 | 658 | c = col_btn.operator('camhp.motion_list_copy', text='', icon='DUPLICATE') 659 | c.index = context.object.motion_cam.list_index 660 | 661 | layout.operator('camhp.bake_motion_cam') 662 | 663 | def draw_setttings(self, context, layout): 664 | 665 | col = layout.column() 666 | col.use_property_split = True 667 | col.active = context.object.motion_cam.affect.enable 668 | 669 | affect = context.object.motion_cam.affect 670 | 671 | col.prop(affect, 'enable') 672 | 673 | col.separator() 674 | 675 | col.prop(affect, 'use_euler') 676 | 677 | col.prop(affect, 'use_sub_camera') 678 | col.prop(affect, 'sub_camera', text='') 679 | 680 | sub = col.column() 681 | sub.active = False 682 | if context.object.type != 'CAMERA': 683 | if affect.sub_camera is None or affect.sub_camera.type != 'CAMERA': 684 | sub.active = affect.use_sub_camera 685 | warn = sub.column() 686 | warn.alert = True 687 | warn.label(text='There is no camera as child of this object') 688 | else: 689 | sub.active = affect.use_sub_camera 690 | else: 691 | sub.active = True 692 | 693 | sub.prop(affect, 'use_lens') 694 | 695 | sub.separator() 696 | 697 | box = sub.box().column(align=True) 698 | box.label(text='Depth of Field') 699 | box.prop(affect, 'use_focus_distance') 700 | box.prop(affect, 'use_aperture_fstop') 701 | 702 | box = sub.box().column(align=True) 703 | box.label(text='Custom Properties') 704 | box.separator() 705 | 706 | for i, item in enumerate(affect.custom_props): 707 | if i == 0: 708 | box.label(text='Data Path') 709 | 710 | row = box.row() 711 | row.use_property_split = False 712 | # 检测是否有效 713 | src_obj, src_attr = parse_data_path(context.object.data, item.data_path) 714 | if src_attr is None: 715 | row.alert = True 716 | row.label(text='Invalid') 717 | 718 | row.prop(item, 'data_path', text='') 719 | 720 | row.operator('camhp.affect_remove_custom_prop', text='', icon='X', emboss=False).index = i 721 | box.separator(factor=0.5) 722 | 723 | box.separator(factor=0.5) 724 | box.operator('camhp.affect_add_custom_prop', text='Add', icon='ADD') 725 | 726 | 727 | def draw_context(self, context): 728 | if context.object and context.object.type in {'CAMERA', 'EMPTY'}: 729 | layout = self.layout 730 | layout.separator() 731 | op = layout.operator('wm.call_panel', text='Motion Camera') 732 | op.name = 'CAMHP_PT_MotionCamPanel' 733 | op.keep_open = True 734 | layout.separator() 735 | 736 | 737 | def draw_add_context(self, context): 738 | if context.object.type == "CAMERA": 739 | layout = self.layout 740 | layout.operator_context = 'INVOKE_DEFAULT' 741 | layout.operator('camhp.add_motion_cams') 742 | 743 | 744 | ############################################################################### 745 | 746 | def register(): 747 | bpy.utils.register_class(MotionCamItemProps) 748 | bpy.utils.register_class(MotionCamAffectCustomProp) 749 | bpy.utils.register_class(MotionCamAffect) 750 | bpy.utils.register_class(MotionCamListProp) 751 | bpy.types.Object.motion_cam = PointerProperty(type=MotionCamListProp) 752 | # list action 753 | bpy.utils.register_class(CAMHP_OT_affect_add_custom_prop) 754 | bpy.utils.register_class(CAMHP_OT_affect_remove_custom_prop) 755 | 756 | bpy.utils.register_class(CAMHP_OT_motion_list_add) 757 | bpy.utils.register_class(CAMHP_OT_motion_list_remove) 758 | bpy.utils.register_class(CAMHP_OT_copy_motion_cam) 759 | bpy.utils.register_class(CAMHP_OT_move_up_motion_cam) 760 | bpy.utils.register_class(CAMHP_OT_move_down_motion_cam) 761 | 762 | # UI 763 | bpy.utils.register_class(CAMHP_UL_CameraList) 764 | # bpy.utils.register_class(CAMHP_PT_MotionCamPanel) 765 | 766 | bpy.utils.register_class(CAMHP_PT_add_motion_cams) 767 | bpy.utils.register_class(CAMHP_OT_bake_motion_cam) 768 | 769 | # bpy.types.VIEW3D_MT_object_context_menu.append(draw_context) 770 | # bpy.types.VIEW3D_MT_object_context_menu.append(draw_add_context) 771 | 772 | 773 | def unregister(): 774 | del bpy.types.Object.motion_cam 775 | bpy.utils.unregister_class(MotionCamListProp) 776 | bpy.utils.unregister_class(MotionCamAffect) 777 | bpy.utils.unregister_class(MotionCamAffectCustomProp) 778 | bpy.utils.unregister_class(MotionCamItemProps) 779 | # List 780 | bpy.utils.unregister_class(CAMHP_OT_affect_add_custom_prop) 781 | bpy.utils.unregister_class(CAMHP_OT_affect_remove_custom_prop) 782 | 783 | bpy.utils.unregister_class(CAMHP_OT_motion_list_add) 784 | bpy.utils.unregister_class(CAMHP_OT_motion_list_remove) 785 | bpy.utils.unregister_class(CAMHP_OT_copy_motion_cam) 786 | bpy.utils.unregister_class(CAMHP_OT_move_up_motion_cam) 787 | bpy.utils.unregister_class(CAMHP_OT_move_down_motion_cam) 788 | 789 | # UI 790 | bpy.utils.unregister_class(CAMHP_UL_CameraList) 791 | # bpy.utils.unregister_class(CAMHP_PT_MotionCamPanel) 792 | 793 | bpy.utils.unregister_class(CAMHP_PT_add_motion_cams) 794 | bpy.utils.unregister_class(CAMHP_OT_bake_motion_cam) 795 | # bpy.types.VIEW3D_MT_object_context_menu.remove(draw_context) 796 | # bpy.types.VIEW3D_MT_object_context_menu.remove(draw_add_context) 797 | --------------------------------------------------------------------------------