commit eff6d7cb682ab98267534179887117371159c0b1 Author: 小煜 <2082529121@qq.com> Date: Fri Feb 13 10:03:47 2026 +0800 feat: 添加 ComfyProjector Blender 插件初始版本 - 新增插件核心文件 __init__.py,包含插件信息和注册逻辑 - 添加工具模块 utils.py,实现 AOV 烘焙和网格视图生成功能 - 包含资产文件:Materials.blend 和 geonodesuvpack.blend - 添加 .gitignore 文件以忽略 Python 缓存目录 - 插件提供 3D 视图面板,包含两个操作按钮 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba0430d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__/ \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..f2d2fc7 --- /dev/null +++ b/__init__.py @@ -0,0 +1,34 @@ +bl_info = { + "name" : "ComfyProjector", + "author" : "Blender Procedural", + "description" : "Projector for ComfyUI", + "blender" : (5, 0, 0), + "version" : (1, 0, 0), + "location" : "", + "warning" : "", + "doc_url": "", + "tracker_url": "", + "category" : "3D View" +} + +import bpy # noqa: E402 +from .utils import classes # noqa: E402 + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + bpy.types.Scene.sna_activeobject = bpy.props.PointerProperty(type=bpy.types.Object) + bpy.types.Scene.sna_bakecam = bpy.props.PointerProperty(type=bpy.types.Object) + bpy.types.Scene.sna_activeobject_name = bpy.props.StringProperty() + + +def unregister(): + if hasattr(bpy.types.Scene, "sna_activeobject_name"): + del bpy.types.Scene.sna_activeobject_name + if hasattr(bpy.types.Scene, "sna_bakecam"): + del bpy.types.Scene.sna_bakecam + if hasattr(bpy.types.Scene, "sna_activeobject"): + del bpy.types.Scene.sna_activeobject + for cls in reversed(classes): + bpy.utils.unregister_class(cls) diff --git a/assets/Materials.blend b/assets/Materials.blend new file mode 100644 index 0000000..0b634b2 Binary files /dev/null and b/assets/Materials.blend differ diff --git a/assets/geonodesuvpack.blend b/assets/geonodesuvpack.blend new file mode 100644 index 0000000..2463121 Binary files /dev/null and b/assets/geonodesuvpack.blend differ diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..5bbec8e --- /dev/null +++ b/utils.py @@ -0,0 +1,175 @@ +import bpy +import bpy.utils.previews +from mathutils import Vector +import os +def property_exists(prop_path, glob, loc): + try: + eval(prop_path, glob, loc) + return True + except: # noqa: E722 + return False + +class Comfy_OT_Aovbake_Ad980(bpy.types.Operator): + bl_idname = "comfyui.aovbake_ad980" + bl_label = "aovbake" + bl_description = "" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, _context): + if bpy.app.version >= (3, 0, 0) and True: + cls.poll_message_set( + "Please save your Blender file before running the script." + ) + return not ("" == bpy.data.filepath) + + def execute(self, _context): + if "" == bpy.data.filepath: + self.report( + {"ERROR"}, + message="Please save your Blender file before running the script.", + ) + else: + if bpy.context.view_layer.objects.active.type == "MESH": + bpy.data.scenes["Scene"].render.film_transparent = True + bpy.data.scenes["Scene"].render.engine = "CYCLES" + bpy.data.scenes["Scene"].view_layers["ViewLayer"].use_pass_z = True + bpy.data.scenes["Scene"].cycles.samples = 16 + bpy.data.scenes["Scene"].render.resolution_x = 1024 + bpy.data.scenes["Scene"].render.resolution_y = 1024 + bpy.data.scenes["Scene"].view_layers["ViewLayer"].use_pass_normal = True + bpy.context.scene.sna_activeobject = ( + bpy.context.view_layer.objects.active + ) + bpy.data.scenes["Scene"].use_nodes = True + import math + + def show_message_box(message="", title="Message", icon="INFO"): + """Shows a pop-up message in Blender.""" + + def draw(self, context): + self.layout.label(text=message) + + bpy.context.window_manager.popup_menu(draw, title=title, icon=icon) + + def create_camera_for_object(obj): + if obj.type != "MESH": + show_message_box("Selected object must be a mesh.", "Error") + return None + bpy.ops.object.camera_add() + cam = bpy.context.object + cam.name = "ObjectCamera" + bpy.context.scene.camera = cam + bpy.context.scene.sna_bakecam = bpy.context.scene.camera + bbox_corners = [ + obj.matrix_world @ Vector(corner) for corner in obj.bound_box + ] + min_x = min(corner.x for corner in bbox_corners) + max_x = max(corner.x for corner in bbox_corners) + min_y = min(corner.y for corner in bbox_corners) + max_y = max(corner.y for corner in bbox_corners) + min_z = min(corner.z for corner in bbox_corners) + max_z = max(corner.z for corner in bbox_corners) + center = ( + Vector((min_x + max_x, min_y + max_y, min_z + max_z)) / 2 + ).to_3d() + size = max(max_x - min_x, max_y - min_y, max_z - min_z) * 1.5 + cam.location = center + Vector((0, -size, 0)) + cam.data.type = "ORTHO" + cam.data.ortho_scale = size + cam.rotation_euler = (math.radians(90), 0, 0) + bpy.context.scene.sna_activeobject.select_set(state=True) + bpy.ops.view3d.camera_to_view_selected() + return cam + + def main(): + obj = bpy.context.active_object + if obj is None: + show_message_box("No active object selected.", "Error") + return + create_camera_for_object(obj) + + main() + bpy.context.scene.sna_activeobject.select_set( + state=True, + ) + bpy.context.view_layer.objects.active = ( + bpy.context.scene.sna_activeobject + ) + bpy.data.scenes["Scene"].use_nodes = False + return {"FINISHED"} + + def invoke(self, context, _event): + return self.execute(context) + + +class Comfy_OT_Mvgen_B6229(bpy.types.Operator): + bl_idname = "comfyui.mvgen_b6229" + bl_label = "mvgen" + bl_description = "" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, _context): + if bpy.app.version >= (3, 0, 0) and True: + cls.poll_message_set( + "Please save your Blender file before running the script." + ) + return not ("" == bpy.data.filepath) + + def execute(self, _context): + if "" == bpy.data.filepath: + self.report( + {"ERROR"}, + message="Please save your Blender file before running the script.", + ) + else: + if (None is not bpy.context.view_layer.objects.active): + if bpy.context.view_layer.objects.active.type == 'MESH': + bpy.context.scene.sna_activeobject = bpy.context.view_layer.objects.active + bpy.context.scene.sna_activeobject_name = bpy.context.scene.sna_activeobject.name + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + if property_exists("bpy.data.node_groups['UV_New']", globals(), locals()): + pass + else: + before_data = list(bpy.data.node_groups) + bpy.ops.wm.append(directory=os.path.join(os.path.dirname(__file__), 'assets', 'geonodesuvpack.blend') + r'\NodeTree', filename='UV_New', link=False) + new_data = list(filter(lambda d: d not in before_data, list(bpy.data.node_groups))) + appended_4ED0B = None if not new_data else new_data[0] + bpy.context.view_layer.objects.active = bpy.context.scene.sna_activeobject + bpy.context.scene.sna_activeobject.select_set(state=True, ) + modifier_5D814 = bpy.context.scene.sna_activeobject.modifiers.new(name='MV_Nodes', type='NODES', ) + node_group = bpy.data.node_groups.get('UV_New') + if node_group is None: + self.report({'ERROR'}, message='Missing node group: UV_New') + return {"CANCELLED"} + bpy.context.scene.sna_activeobject.modifiers['MV_Nodes'].node_group = node_group + bpy.ops.object.convert(target='MESH', keep_original=True) + bpy.data.objects[bpy.context.scene.sna_activeobject_name].hide_render = True + bpy.ops.comfyui.aovbake_ad980('INVOKE_DEFAULT', ) + bpy.data.objects[bpy.context.scene.sna_activeobject_name].modifiers.clear() + bpy.context.view_layer.objects[bpy.context.scene.sna_activeobject_name].hide_set(state=True, ) + return {"FINISHED"} + + def invoke(self, context, _event): + return self.execute(context) + + +class Comfy_PT_ComfyProjectorPanel(bpy.types.Panel): + bl_label = "ComfyProjector" + bl_idname = "Comfy_PT_comfyprojector" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "ComfyProjector" + + def draw(self, _context): + layout = self.layout + layout.operator("comfyui.aovbake_ad980", text="AOV Bake") + layout.operator("comfyui.mvgen_b6229", text="mvgen") + + +classes = ( + Comfy_OT_Aovbake_Ad980, + Comfy_OT_Mvgen_B6229, + Comfy_PT_ComfyProjectorPanel, +)