From 7c3d4ffb0794bc65fc15312e4d1eed8e893c2cae Mon Sep 17 00:00:00 2001 From: Bryce Date: Thu, 18 Dec 2025 22:31:54 -0800 Subject: [PATCH] adds openpose --- blender_compass.py | 174 +++++++++++++++++++++++++++------------------ 1 file changed, 105 insertions(+), 69 deletions(-) diff --git a/blender_compass.py b/blender_compass.py index 27ead50..44f2cf6 100644 --- a/blender_compass.py +++ b/blender_compass.py @@ -1,10 +1,10 @@ bl_info = { "name": "Compass Render", # Main display name "author": "Bryce", # Your name/credit - "version": (1, 1, 0), # Version number + "version": (1, 2, 0), # Version number (bumped) "blender": (4, 3, 0), # Minimum Blender version "location": "Properties > Render Properties > Multi-Angle Renderer", - "description": "Multi-angle animation renderer with Line Art support", + "description": "Multi-angle animation renderer with Line Art + optional OpenPose layer output", "warning": "Beta version - use with caution", # Optional warning "doc_url": "https://your-documentation.com", # Optional docs link "category": "Render", # Category in add-ons list @@ -18,13 +18,35 @@ from bpy.props import StringProperty, FloatProperty, BoolProperty, EnumProperty from bpy.types import Panel, Operator +def find_openpose_view_layer(scene: bpy.types.Scene): + """ + Tries to find an OpenPose View Layer by common names. + Customize this list if your pipeline uses a specific layer name. + """ + candidates = { + "OpenPose", + "OpenPose_Layer", + "OpenPoseLayer", + "OPENPOSE", + "openpose", + "openpose_layer", + } + for vl in scene.view_layers: + if vl.name in candidates: + return vl + return None + + class RENDER_OT_setup_compositor(Operator): - """Setup compositor for multi-pass rendering with Line Art""" + """Setup compositor for multi-pass rendering with Line Art (and optional OpenPose)""" bl_idname = "render.setup_compositor" bl_label = "Setup Multi-Pass Compositor" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): + lineart_view_layer = None + openpose_view_layer = None + scene = context.scene props = scene.multi_angle_props @@ -47,7 +69,6 @@ class RENDER_OT_setup_compositor(Operator): render_layers.location = (0, 0) render_layers.layer = "ViewLayer" - # Create file output nodes for each pass # Main image output image_output = nodes.new(type='CompositorNodeOutputFile') image_output.location = (600, 300) @@ -79,7 +100,6 @@ class RENDER_OT_setup_compositor(Operator): lineart_mix_node = None if props.use_lineart: - # Create Line Art grease pencil output lineart_output = nodes.new(type='CompositorNodeOutputFile') lineart_output.location = (600, -100) lineart_output.label = "LineArt Output" @@ -91,9 +111,7 @@ class RENDER_OT_setup_compositor(Operator): # Find or create Grease Pencil Line Art object gp_obj = self.ensure_lineart_object(context) - # Create viewer node for Line Art (Grease Pencil objects render through different path) # We'll use a separate view layer for Line Art - lineart_view_layer = None for vl in scene.view_layers: if vl.name == "LineArt_Layer": lineart_view_layer = vl @@ -130,13 +148,33 @@ class RENDER_OT_setup_compositor(Operator): else: links.new(render_layers.outputs['Image'], image_output.inputs['image']) - # Depth with improved mapping + + # Optional OpenPose layer support + openpose_output = None + openpose_view_layer = find_openpose_view_layer(scene) + if openpose_view_layer is not None: + openpose_output = nodes.new(type='CompositorNodeOutputFile') + openpose_output.location = (600, -300) + openpose_output.label = "OpenPose Output" + openpose_output.base_path = "" + openpose_output.file_slots.clear() + openpose_output.file_slots.new("openpose") + openpose_output.format.file_format = 'PNG' + + openpose_render_layers = nodes.new(type='CompositorNodeRLayers') + openpose_render_layers.location = (0, -400) + openpose_render_layers.layer = openpose_view_layer.name + + # Links + links.new(render_layers.outputs['Depth'], depth_map_range.inputs['Value']) links.new(depth_map_range.outputs['Value'], depth_output.inputs['depth']) - # Line Art output if lineart_output and 'lineart_render_layers' in locals(): links.new(lineart_render_layers.outputs['Image'], lineart_output.inputs['lineart']) + + if openpose_output and 'openpose_render_layers' in locals(): + links.new(openpose_render_layers.outputs['Image'], openpose_output.inputs['openpose']) # Store node references for later use scene['multiangle_image_output'] = image_output.name @@ -145,8 +183,18 @@ class RENDER_OT_setup_compositor(Operator): if lineart_output: scene['multiangle_lineart_output'] = lineart_output.name + + if openpose_output: + scene['multiangle_openpose_output'] = openpose_output.name self.report({'INFO'}, "Multi-pass compositor setup complete") + if openpose_view_layer is not None: + self.report({'INFO'}, f"OpenPose layer detected: '{openpose_view_layer.name}' (output enabled)") + else: + # Ensure stale key doesn't linger if user removed the layer + if 'multiangle_openpose_output' in scene: + del scene['multiangle_openpose_output'] + self.report({'INFO'}, "OpenPose layer not found (output not enabled)") return {'FINISHED'} def ensure_lineart_object(self, context): @@ -161,14 +209,11 @@ class RENDER_OT_setup_compositor(Operator): break if not gp_obj: - # Create new Grease Pencil object for Line Art - # Try multiple methods for different Blender versions gp_data = None - self.report({'INFO'}, 'HERE') + # Method 1: Try Grease Pencil v3 (Blender 4.3+) if hasattr(bpy.data, 'grease_pencils_v3'): try: - self.report({'INFO'}, 'grease pencil v3') gp_data = bpy.data.grease_pencils_v3.new("LineArt_GP") except: pass @@ -176,7 +221,6 @@ class RENDER_OT_setup_compositor(Operator): # Method 2: Try legacy grease pencils collection if not gp_data and hasattr(bpy.data, 'grease_pencils'): try: - self.report({'INFO'}, 'grease pencil GP') gp_data = bpy.data.grease_pencils.new("LineArt_GP") except: pass @@ -184,18 +228,15 @@ class RENDER_OT_setup_compositor(Operator): # Method 3: Try operator methods if not gp_data: try: - # Try modern operator name bpy.ops.object.grease_pencil_add(type='EMPTY') - gp_obj = context.active_objecthas + gp_obj = context.active_object # FIXED typo gp_obj.name = "LineArt_Object" except: try: - # Try legacy operator name bpy.ops.object.gpencil_add(type='EMPTY') gp_obj = context.active_object gp_obj.name = "LineArt_Object" except: - # Create empty as fallback and warn user bpy.ops.object.empty_add(type='PLAIN_AXES') gp_obj = context.active_object gp_obj.name = "LineArt_Object_EMPTY" @@ -223,7 +264,6 @@ class RENDER_OT_setup_compositor(Operator): scene.collection.objects.unlink(gp_obj) lineart_collection.objects.link(gp_obj) - self.report({'INFO'}, f"HERE {gp_obj.type}") # Only proceed with modifier setup if we have a proper Grease Pencil object if not gp_obj or gp_obj.type != 'GREASEPENCIL': return gp_obj @@ -282,6 +322,14 @@ class RENDER_OT_setup_compositor(Operator): mat.grease_pencil.show_fill = False gp_obj.data.materials.append(mat) + + # Configure the Line Art modifier + if lineart_mod: + lineart_mod.source_type = 'SCENE' + lineart_mod.use_contour = True + lineart_mod.use_crease = True + lineart_mod.target_layer = gp_layer.name + lineart_mod.target_material = gp_obj.data.materials[0] return gp_obj @@ -307,24 +355,23 @@ class RENDER_OT_multi_angle(Operator): 'multiangle_depth_output' in scene) use_lineart = props.use_lineart and 'multiangle_lineart_output' in scene + use_openpose = use_multipass and ('multiangle_openpose_output' in scene) and (find_openpose_view_layer(scene) is not None) # Find camera and its parent camera = None camera_parent = None - # Look for active camera first if scene.camera: camera = scene.camera if camera.parent: camera_parent = camera.parent - # If no active camera or no parent, try to find a parented camera if not camera or not camera_parent: for obj in scene.objects: if obj.type == 'CAMERA' and obj.parent: camera = obj camera_parent = obj.parent - scene.camera = camera # Set as active camera + scene.camera = camera break if not camera: @@ -335,33 +382,27 @@ class RENDER_OT_multi_angle(Operator): self.report({'ERROR'}, "Camera must be parented to an empty object") return {'CANCELLED'} - # Store original rotation original_rotation = camera_parent.rotation_euler.copy() - - # Store original active view layer original_view_layer = context.view_layer.name - # Define 8 angles (in degrees) and their directory names angles_and_dirs = [ - (0, "s"), # South - (45, "sw"), # Southwest - (90, "w"), # West - (135, "nw"), # Northwest - (180, "n"), # North - (225, "ne"), # Northeast - (270, "e"), # East - (315, "se") # Southeast + (0, "s"), + (45, "sw"), + (90, "w"), + (135, "nw"), + (180, "n"), + (225, "ne"), + (270, "e"), + (315, "se") ] - # Store original output path original_filepath = scene.render.filepath base_path = bpy.path.abspath(original_filepath) - # Create main animation directory if it doesn't exist if base_path: base_dir = os.path.dirname(base_path) else: - base_dir = bpy.path.abspath("//") # Use blend file directory + base_dir = bpy.path.abspath("//") animation_dir = os.path.join(base_dir, animation_name) @@ -382,12 +423,16 @@ class RENDER_OT_multi_angle(Operator): } if use_lineart: output_nodes['lineart'] = scene.node_tree.nodes[scene['multiangle_lineart_output']] + if use_openpose: + output_nodes['openpose'] = scene.node_tree.nodes[scene['multiangle_openpose_output']] if 'multiangle_depth_map_range' in scene: depth_map_node = scene.node_tree.nodes[scene['multiangle_depth_map_range']] except KeyError: self.report({'WARNING'}, "Compositor nodes not found. Run 'Setup Multi-Pass Compositor' first.") use_multipass = False + use_openpose = False + use_lineart = False # Update depth mapping range if specified if depth_map_node and props.depth_range > 0: @@ -400,34 +445,32 @@ class RENDER_OT_multi_angle(Operator): render_type = "multi-pass " if use_multipass else "" if use_lineart: render_type += "with Line Art " + if use_openpose: + render_type += "with OpenPose " self.report({'INFO'}, f"Starting multi-angle {render_type}render: {len(angles_and_dirs)} angles") - # Render from each angle for angle_deg, direction in angles_and_dirs: - # Create subdirectory for this angle angle_dir = os.path.join(animation_dir, direction) try: os.makedirs(angle_dir, exist_ok=True) if use_multipass: - # Create subdirectories for each pass os.makedirs(os.path.join(angle_dir, "image"), exist_ok=True) os.makedirs(os.path.join(angle_dir, "depth"), exist_ok=True) if use_lineart: os.makedirs(os.path.join(angle_dir, "lineart"), exist_ok=True) + if use_openpose: + os.makedirs(os.path.join(angle_dir, "openpose"), exist_ok=True) except OSError as e: self.report({'ERROR'}, f"Could not create directory {angle_dir}: {e}") continue - # Set rotation (convert degrees to radians) angle_rad = mathutils.Matrix.Rotation(math.radians(angle_deg), 4, 'Z') camera_parent.rotation_euler = angle_rad.to_euler() - # Update scene bpy.context.view_layer.update() if use_multipass: - # Set up compositor output paths for each pass base_filename = f"{animation_name}_{direction}_" output_nodes['image'].base_path = os.path.join(angle_dir, "image") + os.sep @@ -439,16 +482,16 @@ class RENDER_OT_multi_angle(Operator): if use_lineart: output_nodes['lineart'].base_path = os.path.join(angle_dir, "lineart") + os.sep output_nodes['lineart'].file_slots[0].path = base_filename + + if use_openpose: + output_nodes['openpose'].base_path = os.path.join(angle_dir, "openpose") + os.sep + output_nodes['openpose'].file_slots[0].path = base_filename - # Don't set scene.render.filepath as compositor handles output scene.render.filepath = "" else: - # Set output path for regular rendering scene.render.filepath = os.path.join(angle_dir, f"{animation_name}_{direction}_") self.report({'INFO'}, f"Rendering {direction} angle ({angle_deg}°)") - - # Render animation bpy.ops.render.render(animation=True) current_render += total_frames @@ -528,7 +571,6 @@ class RENDER_PT_multi_angle_panel(Panel): scene = context.scene props = scene.multi_angle_props - # Instructions box = layout.box() box.label(text="Setup Requirements:", icon='INFO') box.label(text="• Camera must be parented to an Empty") @@ -537,27 +579,24 @@ class RENDER_PT_multi_angle_panel(Panel): layout.separator() - # Compositor setup section compositor_box = layout.box() compositor_box.label(text="Multi-Pass Setup:", icon='NODETREE') - # Check if compositor is set up - use_multipass = (scene.use_nodes and - 'multiangle_image_output' in scene) - + use_multipass = (scene.use_nodes and 'multiangle_image_output' in scene) use_lineart = props.use_lineart and 'multiangle_lineart_output' in scene + use_openpose = use_multipass and ('multiangle_openpose_output' in scene) and (find_openpose_view_layer(scene) is not None) if use_multipass: compositor_box.label(text="✓ Multi-pass compositor ready", icon='CHECKMARK') passes = ["Image", "Depth"] if use_lineart: passes.append("Line Art") - row = compositor_box.row() - row.label(text=f"Renders: {', '.join(passes)}") + if use_openpose: + passes.append("OpenPose") + compositor_box.label(text=f"Renders: {', '.join(passes)}") else: compositor_box.label(text="⚠ Multi-pass not configured", icon='ERROR') - # Line Art settings layout.separator() lineart_box = layout.box() lineart_box.label(text="Line Art Settings:", icon='GREASEPENCIL') @@ -573,17 +612,13 @@ class RENDER_PT_multi_angle_panel(Panel): row = compositor_box.row() row.scale_y = 1.5 - op = row.operator("render.setup_compositor", text="Setup Multi-Pass Compositor", icon='NODETREE') + row.operator("render.setup_compositor", text="Setup Multi-Pass Compositor", icon='NODETREE') layout.separator() - # Animation name input layout.prop(props, "animation_name") - - # Depth range setting layout.prop(props, "depth_range") - # Display current settings col = layout.column(align=True) col.label(text="Current Settings:", icon='SETTINGS') @@ -602,19 +637,19 @@ class RENDER_PT_multi_angle_panel(Panel): total_frames = frame_end - frame_start + 1 col.label(text=f"Frames: {frame_start}-{frame_end} ({total_frames} total)") - # Calculate total renders - pass_count = 1 # Always have image + pass_count = 1 if use_multipass: - pass_count = 2 # Image + Depth + pass_count = 2 if use_lineart: - pass_count = 3 # Image + Depth + Line Art + pass_count += 1 + if use_openpose: + pass_count += 1 total_renders = total_frames * 8 * pass_count col.label(text=f"Total renders: {total_frames * 8} × {pass_count} passes = {total_renders}") layout.separator() - # Render button row = layout.row() row.scale_y = 2.0 button_text = "Render 8 Angles" @@ -622,13 +657,14 @@ class RENDER_PT_multi_angle_panel(Panel): button_text += " (Multi-Pass" if use_lineart: button_text += " + Line Art" + if use_openpose: + button_text += " + OpenPose" button_text += ")" else: button_text += " (Image Only)" row.operator("render.multi_angle", text=button_text, icon='RENDER_ANIMATION') - # Output structure info layout.separator() box = layout.box() box.label(text="Output Structure:", icon='FILE_FOLDER') @@ -638,6 +674,8 @@ class RENDER_PT_multi_angle_panel(Panel): passes_text = "image/, depth/" if use_lineart: passes_text += ", lineart/" + if use_openpose: + passes_text += ", openpose/" box.label(text=f" ├── s/{passes_text}") box.label(text=f" ├── sw/{passes_text}") box.label(text=f" └── ... (8 angles × {pass_count} passes)") @@ -647,12 +685,10 @@ class RENDER_PT_multi_angle_panel(Panel): box.label(text=" ├── sw/ (image files)") box.label(text=" └── ... (8 angles)") - # Angles info layout.separator() box = layout.box() box.label(text="Render Angles:", icon='CAMERA_DATA') - # Create two columns for the angles split = box.split(factor=0.5) col1 = split.column() col2 = split.column()