696 lines
27 KiB
Python
696 lines
27 KiB
Python
bl_info = {
|
||
"name": "Compass Render", # Main display name
|
||
"author": "Bryce", # Your name/credit
|
||
"version": (1, 1, 0), # Version number
|
||
"blender": (4, 3, 0), # Minimum Blender version
|
||
"location": "Properties > Render Properties > Multi-Angle Renderer",
|
||
"description": "Multi-angle animation renderer with Line Art support",
|
||
"warning": "Beta version - use with caution", # Optional warning
|
||
"doc_url": "https://your-documentation.com", # Optional docs link
|
||
"category": "Render", # Category in add-ons list
|
||
}
|
||
|
||
import bpy
|
||
import os
|
||
import math
|
||
import mathutils
|
||
from bpy.props import StringProperty, FloatProperty, BoolProperty, EnumProperty
|
||
from bpy.types import Panel, Operator
|
||
|
||
|
||
class RENDER_OT_setup_compositor(Operator):
|
||
"""Setup compositor for multi-pass rendering with Line Art"""
|
||
bl_idname = "render.setup_compositor"
|
||
bl_label = "Setup Multi-Pass Compositor"
|
||
bl_options = {'REGISTER', 'UNDO'}
|
||
|
||
def execute(self, context):
|
||
scene = context.scene
|
||
props = scene.multi_angle_props
|
||
|
||
# Enable compositor
|
||
scene.use_nodes = True
|
||
scene.render.use_compositing = True
|
||
|
||
# Clear existing nodes
|
||
scene.node_tree.nodes.clear()
|
||
|
||
# Enable required render passes
|
||
scene.view_layers[0].use_pass_z = True # Depth
|
||
|
||
# Create compositor nodes
|
||
nodes = scene.node_tree.nodes
|
||
links = scene.node_tree.links
|
||
|
||
# Create input node for main render
|
||
render_layers = nodes.new(type='CompositorNodeRLayers')
|
||
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)
|
||
image_output.label = "Image Output"
|
||
image_output.base_path = "" # Will be set during render
|
||
image_output.file_slots.clear()
|
||
image_output.file_slots.new("image")
|
||
image_output.format.file_format = 'PNG'
|
||
|
||
# Depth output with improved mapping
|
||
depth_map_range = nodes.new(type='CompositorNodeMapRange')
|
||
depth_map_range.location = (200, 100)
|
||
depth_map_range.inputs['From Min'].default_value = 0.0
|
||
depth_map_range.inputs['From Max'].default_value = 10.0
|
||
depth_map_range.inputs['To Min'].default_value = 0.0
|
||
depth_map_range.inputs['To Max'].default_value = 1.0
|
||
depth_map_range.use_clamp = True
|
||
|
||
depth_output = nodes.new(type='CompositorNodeOutputFile')
|
||
depth_output.location = (600, 100)
|
||
depth_output.label = "Depth Output"
|
||
depth_output.base_path = ""
|
||
depth_output.file_slots.clear()
|
||
depth_output.file_slots.new("depth")
|
||
depth_output.format.file_format = 'PNG'
|
||
|
||
# Line Art setup if enabled
|
||
lineart_output = None
|
||
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"
|
||
lineart_output.base_path = ""
|
||
lineart_output.file_slots.clear()
|
||
lineart_output.file_slots.new("lineart")
|
||
lineart_output.format.file_format = 'PNG'
|
||
|
||
# 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
|
||
break
|
||
|
||
if not lineart_view_layer:
|
||
lineart_view_layer = scene.view_layers.new("LineArt_Layer")
|
||
# Configure LineArt layer to only show line art
|
||
for collection in lineart_view_layer.layer_collection.children:
|
||
if "LineArt" not in collection.name:
|
||
collection.holdout = True
|
||
|
||
# Line Art render layer node
|
||
lineart_render_layers = nodes.new(type='CompositorNodeRLayers')
|
||
lineart_render_layers.location = (0, -200)
|
||
lineart_render_layers.layer = "LineArt_Layer"
|
||
|
||
# Mix node to remove Line Art from main image if needed
|
||
if props.separate_lineart:
|
||
lineart_mix_node = nodes.new(type='CompositorNodeMixRGB')
|
||
lineart_mix_node.location = (400, 300)
|
||
lineart_mix_node.blend_type = 'SUBTRACT'
|
||
lineart_mix_node.inputs['Fac'].default_value = 1.0
|
||
|
||
# Connect nodes
|
||
# Main image
|
||
if lineart_mix_node and props.separate_lineart:
|
||
# Remove Line Art from main image
|
||
links.new(render_layers.outputs['Image'], lineart_mix_node.inputs['Image1'])
|
||
self.report({'INFO'}, f"Rendering {lineart_mix_node.inputs}")
|
||
if 'lineart_render_layers' in locals():
|
||
links.new(lineart_render_layers.outputs['Image'], lineart_mix_node.inputs['Image2'])
|
||
links.new(lineart_mix_node.outputs['Color'], image_output.inputs['image'])
|
||
else:
|
||
links.new(render_layers.outputs['Image'], image_output.inputs['image'])
|
||
|
||
# Depth with improved mapping
|
||
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'])
|
||
|
||
# Store node references for later use
|
||
scene['multiangle_image_output'] = image_output.name
|
||
scene['multiangle_depth_output'] = depth_output.name
|
||
scene['multiangle_depth_map_range'] = depth_map_range.name
|
||
|
||
if lineart_output:
|
||
scene['multiangle_lineart_output'] = lineart_output.name
|
||
|
||
self.report({'INFO'}, "Multi-pass compositor setup complete")
|
||
return {'FINISHED'}
|
||
|
||
def ensure_lineart_object(self, context):
|
||
"""Create or get Line Art Grease Pencil object with robust version handling"""
|
||
scene = context.scene
|
||
|
||
# Look for existing Line Art object
|
||
gp_obj = None
|
||
for obj in scene.objects:
|
||
if obj.type == 'GREASEPENCIL' and 'LineArt' in obj.name:
|
||
gp_obj = obj
|
||
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
|
||
|
||
# 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
|
||
|
||
# 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.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"
|
||
self.report({'WARNING'}, "Could not create Grease Pencil object. Created empty instead.")
|
||
return gp_obj
|
||
|
||
# If we have gp_data, create the object
|
||
if gp_data:
|
||
gp_obj = bpy.data.objects.new("LineArt_Object", gp_data)
|
||
bpy.context.scene.collection.objects.link(gp_obj)
|
||
|
||
# Create Line Art collection if it doesn't exist
|
||
lineart_collection = None
|
||
for collection in bpy.data.collections:
|
||
if collection.name == "LineArt":
|
||
lineart_collection = collection
|
||
break
|
||
|
||
if not lineart_collection:
|
||
lineart_collection = bpy.data.collections.new("LineArt")
|
||
scene.collection.children.link(lineart_collection)
|
||
|
||
# Move object to Line Art collection
|
||
if gp_obj and gp_obj.name in scene.collection.objects:
|
||
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
|
||
|
||
# Ensure Line Art modifier exists
|
||
lineart_mod = None
|
||
|
||
# Check if object has grease_pencil_modifiers (new GP3) or modifiers (legacy)
|
||
modifiers_collection = gp_obj.modifiers
|
||
valid_types = ['LINEART']
|
||
|
||
for mod in modifiers_collection:
|
||
if mod.type in valid_types:
|
||
lineart_mod = mod
|
||
break
|
||
|
||
if not lineart_mod:
|
||
# Add Line Art modifier with proper error handling
|
||
try:
|
||
# Try new Grease Pencil 3 modifier first
|
||
lineart_mod = gp_obj.modifiers.new(name="LineArt", type='LINEART')
|
||
except (AttributeError, TypeError):
|
||
try:
|
||
# Try legacy GP modifier
|
||
lineart_mod = gp_obj.modifiers.new(name="LineArt", type='GP_LINEART')
|
||
except (AttributeError, TypeError):
|
||
self.report({'ERROR'}, "Could not create Line Art modifier. Unsupported Blender version.")
|
||
return gp_obj
|
||
|
||
# Configure Line Art modifier
|
||
if lineart_mod:
|
||
lineart_mod.source_type = 'SCENE'
|
||
lineart_mod.use_contour = True
|
||
lineart_mod.use_crease = True
|
||
lineart_mod.thickness = 3
|
||
|
||
# Set performance settings if available
|
||
if hasattr(lineart_mod, 'use_back_face_culling'):
|
||
lineart_mod.use_back_face_culling = True
|
||
|
||
# Set crease threshold if available
|
||
props = scene.multi_angle_props
|
||
if hasattr(lineart_mod, 'crease_threshold'):
|
||
lineart_mod.crease_threshold = math.radians(props.lineart_crease_angle)
|
||
|
||
# Ensure material exists
|
||
if not gp_obj.data.materials:
|
||
mat = bpy.data.materials.new(name="LineArt_Material")
|
||
bpy.data.materials.create_gpencil_data(mat)
|
||
#mat.is_grease_pencil = True
|
||
|
||
# Set material properties with error handling for different GP versions
|
||
if hasattr(mat, 'grease_pencil'):
|
||
mat.grease_pencil.color = (0, 0, 0, 1) # Black lines
|
||
mat.grease_pencil.show_stroke = True
|
||
mat.grease_pencil.show_fill = False
|
||
|
||
gp_obj.data.materials.append(mat)
|
||
|
||
return gp_obj
|
||
|
||
|
||
class RENDER_OT_multi_angle(Operator):
|
||
"""Render animation from multiple angles"""
|
||
bl_idname = "render.multi_angle"
|
||
bl_label = "Render Multi-Angle Animation"
|
||
bl_options = {'REGISTER', 'UNDO'}
|
||
|
||
def execute(self, context):
|
||
scene = context.scene
|
||
props = scene.multi_angle_props
|
||
animation_name = props.animation_name
|
||
|
||
if not animation_name:
|
||
self.report({'ERROR'}, "Please enter an animation name")
|
||
return {'CANCELLED'}
|
||
|
||
# Check if compositor is set up for multi-pass
|
||
use_multipass = (scene.use_nodes and
|
||
'multiangle_image_output' in scene and
|
||
'multiangle_depth_output' in scene)
|
||
|
||
use_lineart = props.use_lineart and 'multiangle_lineart_output' in scene
|
||
|
||
# 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
|
||
break
|
||
|
||
if not camera:
|
||
self.report({'ERROR'}, "No camera found in the scene")
|
||
return {'CANCELLED'}
|
||
|
||
if not camera_parent:
|
||
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
|
||
]
|
||
|
||
# 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
|
||
|
||
animation_dir = os.path.join(base_dir, animation_name)
|
||
|
||
try:
|
||
os.makedirs(animation_dir, exist_ok=True)
|
||
except OSError as e:
|
||
self.report({'ERROR'}, f"Could not create directory: {e}")
|
||
return {'CANCELLED'}
|
||
|
||
# Get compositor output nodes if multi-pass is enabled
|
||
output_nodes = {}
|
||
depth_map_node = None
|
||
if use_multipass:
|
||
try:
|
||
output_nodes = {
|
||
'image': scene.node_tree.nodes[scene['multiangle_image_output']],
|
||
'depth': scene.node_tree.nodes[scene['multiangle_depth_output']]
|
||
}
|
||
if use_lineart:
|
||
output_nodes['lineart'] = scene.node_tree.nodes[scene['multiangle_lineart_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
|
||
|
||
# Update depth mapping range if specified
|
||
if depth_map_node and props.depth_range > 0:
|
||
depth_map_node.inputs['From Max'].default_value = props.depth_range
|
||
|
||
total_frames = scene.frame_end - scene.frame_start + 1
|
||
total_renders = len(angles_and_dirs) * total_frames
|
||
current_render = 0
|
||
|
||
render_type = "multi-pass " if use_multipass else ""
|
||
if use_lineart:
|
||
render_type += "with Line Art "
|
||
|
||
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)
|
||
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
|
||
output_nodes['image'].file_slots[0].path = base_filename
|
||
|
||
output_nodes['depth'].base_path = os.path.join(angle_dir, "depth") + os.sep
|
||
output_nodes['depth'].file_slots[0].path = base_filename
|
||
|
||
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
|
||
|
||
# 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
|
||
progress = (current_render / total_renders) * 100
|
||
self.report({'INFO'}, f"Progress: {progress:.1f}% - Completed {direction}")
|
||
|
||
# Restore original settings
|
||
camera_parent.rotation_euler = original_rotation
|
||
scene.render.filepath = original_filepath
|
||
context.window.view_layer = scene.view_layers[original_view_layer]
|
||
|
||
# Reset compositor output paths if multi-pass was used
|
||
if use_multipass:
|
||
for node in output_nodes.values():
|
||
node.base_path = ""
|
||
|
||
bpy.context.view_layer.update()
|
||
|
||
self.report({'INFO'}, f"Multi-angle {render_type}render complete! Files saved in: {animation_dir}")
|
||
return {'FINISHED'}
|
||
|
||
|
||
class MultiAngleProperties(bpy.types.PropertyGroup):
|
||
animation_name: StringProperty(
|
||
name="Animation Name",
|
||
description="Name for the animation (will create folder with this name)",
|
||
default="my_animation"
|
||
)
|
||
|
||
depth_range: FloatProperty(
|
||
name="Depth Range",
|
||
description="Maximum depth value for depth mapping (adjust for better contrast)",
|
||
default=10.0,
|
||
min=0.1,
|
||
max=1000.0
|
||
)
|
||
|
||
use_lineart: BoolProperty(
|
||
name="Enable Line Art",
|
||
description="Include Line Art pass in rendering",
|
||
default=False
|
||
)
|
||
|
||
separate_lineart: BoolProperty(
|
||
name="Separate Line Art",
|
||
description="Remove Line Art from main image pass",
|
||
default=True
|
||
)
|
||
|
||
lineart_thickness: FloatProperty(
|
||
name="Line Thickness",
|
||
description="Thickness of Line Art strokes",
|
||
default=3.0,
|
||
min=0.1,
|
||
max=20.0
|
||
)
|
||
|
||
lineart_crease_angle: FloatProperty(
|
||
name="Crease Angle",
|
||
description="Angle threshold for crease lines (degrees)",
|
||
default=60.0,
|
||
min=0.0,
|
||
max=180.0
|
||
)
|
||
|
||
|
||
class RENDER_PT_multi_angle_panel(Panel):
|
||
"""Multi-Angle Renderer Panel"""
|
||
bl_label = "Multi-Angle Renderer"
|
||
bl_idname = "RENDER_PT_multi_angle"
|
||
bl_space_type = 'PROPERTIES'
|
||
bl_region_type = 'WINDOW'
|
||
bl_context = "render"
|
||
|
||
def draw(self, context):
|
||
layout = self.layout
|
||
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")
|
||
box.label(text="• Empty should be at scene center")
|
||
box.label(text="• Set your frame range before rendering")
|
||
|
||
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_lineart = props.use_lineart and 'multiangle_lineart_output' in scene
|
||
|
||
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)}")
|
||
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')
|
||
|
||
row = lineart_box.row()
|
||
row.prop(props, "use_lineart")
|
||
|
||
if props.use_lineart:
|
||
col = lineart_box.column(align=True)
|
||
col.prop(props, "separate_lineart")
|
||
col.prop(props, "lineart_thickness")
|
||
col.prop(props, "lineart_crease_angle")
|
||
|
||
row = compositor_box.row()
|
||
row.scale_y = 1.5
|
||
op = 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')
|
||
|
||
if context.scene.camera:
|
||
camera = context.scene.camera
|
||
if camera.parent:
|
||
col.label(text=f"Camera: {camera.name}")
|
||
col.label(text=f"Parent: {camera.parent.name}")
|
||
else:
|
||
col.label(text="⚠ Camera has no parent!", icon='ERROR')
|
||
else:
|
||
col.label(text="⚠ No active camera!", icon='ERROR')
|
||
|
||
frame_start = context.scene.frame_start
|
||
frame_end = context.scene.frame_end
|
||
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
|
||
if use_multipass:
|
||
pass_count = 2 # Image + Depth
|
||
if use_lineart:
|
||
pass_count = 3 # Image + Depth + Line Art
|
||
|
||
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"
|
||
if use_multipass:
|
||
button_text += " (Multi-Pass"
|
||
if use_lineart:
|
||
button_text += " + Line Art"
|
||
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')
|
||
|
||
if use_multipass:
|
||
box.label(text="animation_name/")
|
||
passes_text = "image/, depth/"
|
||
if use_lineart:
|
||
passes_text += ", lineart/"
|
||
box.label(text=f" ├── s/{passes_text}")
|
||
box.label(text=f" ├── sw/{passes_text}")
|
||
box.label(text=f" └── ... (8 angles × {pass_count} passes)")
|
||
else:
|
||
box.label(text="animation_name/")
|
||
box.label(text=" ├── s/ (image files)")
|
||
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()
|
||
|
||
angles_info = [
|
||
("S (0°)", "South"),
|
||
("SW (45°)", "Southwest"),
|
||
("W (90°)", "West"),
|
||
("NW (135°)", "Northwest"),
|
||
("N (180°)", "North"),
|
||
("NE (225°)", "Northeast"),
|
||
("E (270°)", "East"),
|
||
("SE (315°)", "Southeast")
|
||
]
|
||
|
||
for i, (angle, desc) in enumerate(angles_info):
|
||
if i < 4:
|
||
col1.label(text=f"{angle}")
|
||
else:
|
||
col2.label(text=f"{angle}")
|
||
|
||
|
||
def register():
|
||
bpy.utils.register_class(MultiAngleProperties)
|
||
bpy.utils.register_class(RENDER_OT_setup_compositor)
|
||
bpy.utils.register_class(RENDER_OT_multi_angle)
|
||
bpy.utils.register_class(RENDER_PT_multi_angle_panel)
|
||
bpy.types.Scene.multi_angle_props = bpy.props.PointerProperty(type=MultiAngleProperties)
|
||
|
||
|
||
def unregister():
|
||
bpy.utils.unregister_class(RENDER_PT_multi_angle_panel)
|
||
bpy.utils.unregister_class(RENDER_OT_multi_angle)
|
||
bpy.utils.unregister_class(RENDER_OT_setup_compositor)
|
||
bpy.utils.unregister_class(MultiAngleProperties)
|
||
del bpy.types.Scene.multi_angle_props
|
||
|
||
|
||
if __name__ == "__main__":
|
||
register()
|