Skip to content

Instantly share code, notes, and snippets.

@Packsod
Last active July 15, 2025 10:37
Show Gist options
  • Save Packsod/47b346cc3dd602d97f8120860950c3d4 to your computer and use it in GitHub Desktop.
Save Packsod/47b346cc3dd602d97f8120860950c3d4 to your computer and use it in GitHub Desktop.
Cam_CamP_backup & restore
import bpy
class RenderSelectedCamPOperator(bpy.types.Operator):
bl_idname = "scene.render_selected_camp"
bl_label = "Render Selected CamP"
bl_options = {'REGISTER'}
render_CamP: bpy.props.IntProperty(name="Select CamP ind(1~24)", description="Specify a CamP_sub to render controlnet images", default=1, min=1, max=24)
render_video: bpy.props.BoolProperty(name="Render Video", description="Enable video rendering mode", default=False)
frame_start: bpy.props.IntProperty(name="Frame Start", description="Start frame of the video", default=1, min=1)
frame_count: bpy.props.IntProperty(name="Frame Count", description="Total number of frames to render", default=121, min=1)
def invoke(self, context, event):
wm = context.window_manager
self.frame_start = context.scene.frame_start
return wm.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
layout.prop(self, "render_CamP")
layout.prop(self, "render_video")
if self.render_video:
layout.prop(self, "frame_start")
layout.prop(self, "frame_count")
def execute(self, context):
import os
import re
# Save the current frame, image settings, use_nodes setting, and timeline frame range
current_frame = bpy.context.scene.frame_current
current_file_format = bpy.context.scene.render.image_settings.file_format
current_use_overwrite = bpy.context.scene.render.use_overwrite
current_use_nodes = bpy.context.scene.use_nodes
original_camera = bpy.context.scene.camera
original_render_filepath = bpy.context.scene.render.filepath
original_frame_start = bpy.context.scene.frame_start
original_frame_end = bpy.context.scene.frame_end
# Set the image settings to PNG format and enable overwrite
bpy.context.scene.render.image_settings.file_format = 'PNG'
bpy.context.scene.render.use_overwrite = True
bpy.context.scene.use_nodes = True
# Calculate the selected frame
current_frame_new = -self.render_CamP
# Get the name of the camera to use for rendering
camera_name = "CamP_sub%02d" % self.render_CamP
# Check if the camera exists
if camera_name not in bpy.data.objects:
self.report({'ERROR'}, f'Camera {camera_name} does not exist')
return {'CANCELLED'}
# Set the camera to use for rendering
bpy.context.scene.camera = bpy.data.objects[camera_name]
# Directory where the PNG files are located
blend_file_dir = os.path.dirname(bpy.data.filepath)
node_tree = bpy.data.scenes[bpy.context.scene.name].node_tree
# Check if the "Output_path_MP" node exists
output_path_node = node_tree.nodes.get("Output_path_MP")
if not output_path_node or not hasattr(output_path_node, "base_path"):
self.report({'ERROR'}, 'Node "Output_path_MP" not found or missing base_path attribute')
return {'CANCELLED'}
# Save the original base_path
original_base_path = output_path_node.base_path
# append camera name to base_path
output_path_node.base_path = os.path.join(output_path_node.base_path, camera_name)
"""
Note that // means a network path in Windows,
so need to remove the slashes in the string that you inputed in Output_path_MP,
then append it with os.listdir(),
otherwise bpy will report that the network path cannot be found.
This is really annoying.
"""
relative_output_directory = output_path_node.base_path.lstrip('/') + '/'
output_directory = os.path.join(blend_file_dir, relative_output_directory)
# Create the output directory if it doesn't exist
os.makedirs(output_directory, exist_ok=True)
# Delete existing files based on the render mode
def delete_files_by_extension(directory, extension):
try:
for filename in os.listdir(directory):
if filename.endswith(extension):
file_path = os.path.join(directory, filename)
os.remove(file_path)
except FileNotFoundError:
pass
if self.render_video:
delete_files_by_extension(output_directory, ".mp4")
else:
delete_files_by_extension(output_directory, ".png")
# Set the render filepath to the output directory
bpy.context.scene.render.filepath = os.path.join(output_directory, camera_name)
if self.render_video:
# Record all current camera markers and their positions
original_markers = [(marker.name, marker.frame, marker.camera) for marker in bpy.context.scene.timeline_markers if marker.camera]
# Remove all camera markers
for marker in bpy.context.scene.timeline_markers:
if marker.camera:
bpy.context.scene.timeline_markers.remove(marker)
# Set render settings for video
bpy.context.scene.frame_start = self.frame_start
bpy.context.scene.frame_end = self.frame_start + self.frame_count - 1
# Set render settings for video
bpy.context.scene.render.image_settings.file_format = 'FFMPEG'
bpy.context.scene.render.ffmpeg.format = 'MPEG4'
bpy.context.scene.render.resolution_percentage = 100
# Render the animation
bpy.ops.render.opengl(animation=True, view_context=True)
# Restore the original camera markers to their original positions
for name, frame, camera in original_markers:
if camera and camera.name in bpy.data.objects:
marker = bpy.context.scene.timeline_markers.new(name=name, frame=frame)
marker.camera = bpy.data.objects[camera.name]
else:
# Jump to the selected frame and render
bpy.context.scene.frame_set(current_frame_new)
# Render the selected frame
bpy.ops.render.render(write_still=True)
# Iterate through all PNG files in the directory and rename them if necessary
for filename in os.listdir(output_directory):
if filename.endswith(".png"):
match = re.search(r'-(?P<frame_number>\d{4})\.png$', filename)
if match:
new_filename = filename.replace(match.group(0), "").replace("{camera}", camera_name) + ".png"
os.rename(os.path.join(output_directory, filename), os.path.join(output_directory, new_filename))
# Delete CamP_sub##.png
CamP_sub_file = os.path.join(output_directory, camera_name + ".png")
if os.path.exists(CamP_sub_file):
os.remove(CamP_sub_file)
# Jump back to the original frame and restore the original settings
bpy.context.scene.frame_set(current_frame)
bpy.context.scene.render.image_settings.file_format = current_file_format
bpy.context.scene.render.use_overwrite = current_use_overwrite
bpy.context.scene.use_nodes = current_use_nodes
bpy.context.scene.camera = original_camera
bpy.context.scene.render.filepath = original_render_filepath
bpy.context.scene.frame_start = original_frame_start
bpy.context.scene.frame_end = original_frame_end
# Restore the original base_path
output_path_node.base_path = original_base_path
# Show a pop-up message
camera_name = bpy.context.scene.camera.name
self.report({'INFO'}, f'{camera_name} rendered successfully')
return {'FINISHED'}
# Register the operator
bpy.utils.register_class(RenderSelectedCamPOperator)
# Invoke the operator
bpy.ops.scene.render_selected_camp('INVOKE_DEFAULT')
import bpy
from bpy.types import Operator, PropertyGroup, UIList
import os
class PATH_INFO_OT_Info(Operator):
bl_idname = "object.path_info"
bl_label = "Path Info"
bl_description = "Copy all title folder paths to clipboard"
def execute(self, context):
backup_text = bpy.data.texts.get('CamP_backups_list.md')
if backup_text is not None:
lines = backup_text.as_string().split('\n')
paths = []
for line in lines:
if line.startswith('------------------------------------') and 'CameraArchive_' in line and line.endswith('------------------------------------'):
title = line.split('CameraArchive_')[1].split('------------------------------------')[0].strip()
base_path = f"//multires_projecting/{title}/"
abs_path = bpy.path.abspath(base_path)
# Replace all forward slashes with backslashes and remove the trailing backslash
abs_path = abs_path.replace('/', '\\').rstrip('\\')
paths.append(abs_path)
if paths:
bpy.context.window_manager.clipboard = '\n'.join(paths)
self.report({'INFO'}, "Paths copied to clipboard.")
else:
self.report({'WARNING'}, "No paths found.")
else:
self.report({'WARNING'}, "No 'CamP_backups_list.md' text block found.")
return {'FINISHED'}
class RESTORE_OT_CamPParameters(Operator):
bl_idname = "object.restore_camp_parameters"
bl_label = "Restore CamP Parameters"
title_index: bpy.props.IntProperty(name="Title Index", default=0)
title: bpy.props.StringProperty(name="Backup Title")
titles: bpy.props.CollectionProperty(type=bpy.types.PropertyGroup)
def execute(self, context):
if self.title_index < len(self.titles):
self.title = self.titles[self.title_index].name # Set the title property based on the selected index
result = self.restore_camera_parameters()
if result == {'FINISHED'}:
self.update_base_path()
return result
else:
self.report({'WARNING'}, "Invalid backup index.")
return {'CANCELLED'}
def restore_camera_parameters(self):
backup_text = bpy.data.texts.get('CamP_backups_list.md')
if backup_text is not None:
lines = backup_text.as_string().split('\n')
i = 0
title_found = False
while i < len(lines):
if lines[i] == f'------------------------------------CameraArchive_{self.title}------------------------------------':
title_found = True
elif lines[i].startswith('------------------------------------CameraArchive_'):
title_found = False
if title_found:
cam = bpy.data.objects.get(lines[i])
if cam is not None and cam.type == 'CAMERA':
self.apply_parameters(cam, lines[i + 1:i + 12])
i += 12
continue
i += 1
else:
self.report({'WARNING'}, "No 'CamP_backups_list.md' text block found.")
return {'CANCELLED'}
return {'FINISHED'}
@staticmethod
def apply_parameters(cam, lines):
import math
prop_map = {
'Location': lambda x: [float(i) for i in x.split(',')],
'Rotation': lambda x: [math.degrees(float(i)) for i in x.split(',')],
'Lens': float,
'Shift X': float,
'Shift Y': float,
'Clip Start': float,
'Clip End': float,
'Sensor Width': float,
'Resolution X': int,
'Resolution Y': int,
'Mist Depth': float,
}
# Save the current frame
current_frame = bpy.context.scene.frame_current
for line in lines:
if ': ' in line:
prop, value = line.split(': ', 1)
if prop in prop_map:
value = prop_map[prop](value)
if prop == 'Rotation':
cam.rotation_euler = [math.radians(val) for val in value]
elif prop == 'Location':
cam.location = value
elif prop in ['Resolution X']:
try:
cam.data.per_camera_resolution.resolution_x = int(value)
except AttributeError:
pass
elif prop in ['Resolution Y']:
try:
cam.data.per_camera_resolution.resolution_y = int(value)
except AttributeError:
pass
elif prop == 'Mist Depth':
# Set the frame for the camera
frame_offset = int(cam.name.split('sub')[1])
frame_number = 0 - frame_offset
bpy.context.scene.frame_set(frame_number)
# Set the mist depth for the current world and frame
world = bpy.context.scene.world
world.mist_settings.depth = value
# Insert a keyframe at the current frame
world.mist_settings.keyframe_insert(data_path='depth', frame=frame_number)
else:
setattr(cam.data, prop.lower().replace(' ', '_'), value)
# Restore the current frame
bpy.context.scene.frame_set(current_frame)
def get_titles(self, context):
backup_text = bpy.data.texts.get('CamP_backups_list.md')
if backup_text is not None:
lines = backup_text.as_string().split('\n')
for i, line in enumerate(lines):
if line.startswith('------------------------------------') and 'CameraArchive_' in line and line.endswith('------------------------------------'):
title = line.split('CameraArchive_')[1].split('------------------------------------')[0].strip()
item = self.titles.add()
item.name = title
def update_titles(self, context):
self.titles.clear()
self.get_titles(context)
def invoke(self, context, event):
self.update_titles(context)
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
self.update_titles(context)
layout = self.layout
row = layout.row()
row.template_list("UI_UL_list", "titles", self, "titles", self, "title_index", rows=5)
remove_op = row.operator("object.remove_camp_backup", text="", icon="TRASH")
if self.title_index < len(self.titles):
remove_op.title = self.titles[self.title_index].name
info_op = row.operator("object.path_info", text="", icon="INFO")
self.update_titles(context)
def update_base_path(self):
base_path = bpy.data.scenes[bpy.context.scene.name].node_tree.nodes["Output_path_MP"].base_path
new_base_path = f"//multires_projecting/{self.title}/"
base_path = new_base_path
bpy.data.scenes[bpy.context.scene.name].node_tree.nodes["Output_path_MP"].base_path = new_base_path
class REMOVE_OT_CamPBackup(Operator):
bl_idname = "object.remove_camp_backup"
bl_label = "Remove CamP Backup"
title: bpy.props.StringProperty(name="Backup Title")
titles: bpy.props.CollectionProperty(type=bpy.types.PropertyGroup)
def execute(self, context):
backup_text = bpy.data.texts.get('CamP_backups_list.md')
if backup_text is not None:
lines = backup_text.as_string().split('\n')
title = self.title # Use the title property
start_title = f'------------------------------------CameraArchive_{title}------------------------------------'
if start_title in lines:
end_title = '------------------'
start_idx = lines.index(start_title)
try:
end_idx = start_idx + lines[start_idx:].index(end_title) + 1
while end_idx < len(lines) and not lines[end_idx].startswith('------------------------------------CameraArchive_'):
end_idx += lines[end_idx:].index(end_title) + 1
except ValueError:
end_idx = len(lines)
lines = lines[:start_idx] + lines[end_idx:]
backup_text.from_string('\n'.join(lines).rstrip('\n') + '\n')
else:
self.report({'WARNING'}, f"No backup found with the title '{title}'.")
return {'CANCELLED'}
else:
self.report({'WARNING'}, "No 'CamP_backups_list.md' text block found.")
return {'CANCELLED'}
return {'FINISHED'}
def get_titles(self, context):
backup_text = bpy.data.texts.get('CamP_backups_list.md')
if backup_text is not None:
lines = backup_text.as_string().split('\n')
for i, line in enumerate(lines):
if line.startswith('------------------------------------') and 'CameraArchive_' in line and line.endswith('------------------------------------'):
title = line.split('CameraArchive_')[1].split('------------------------------------')[0].strip()
item = self.titles.add()
item.name = title
def invoke(self, context, event):
self.titles.clear()
self.get_titles(context)
return self.execute(context)
bpy.utils.register_class(PATH_INFO_OT_Info)
bpy.utils.register_class(RESTORE_OT_CamPParameters)
bpy.utils.register_class(REMOVE_OT_CamPBackup)
bpy.ops.object.restore_camp_parameters('INVOKE_DEFAULT')
import bpy
from bpy.types import Operator
class BackupCamPParametersOperator(Operator):
bl_idname = "object.backup_camp_parameters"
bl_label = "Backup CamP Parameters"
from bpy.props import StringProperty
backup_suffix: StringProperty(name="name (required)")
def get_titles(self, context):
titles = []
if 'CamP_backups_list.md' in bpy.data.texts:
backup_text = bpy.data.texts['CamP_backups_list.md']
lines = backup_text.as_string().split('\n')
for i, line in enumerate(lines):
if line.startswith('------------------------------------') and 'CameraArchive_' in line and line.endswith('------------------------------------'):
title = line.split('CameraArchive_')[1].split('------------------------------------')[0].strip()
titles.append((title, title, ""))
return titles
def execute(self, context):
backup_name = "CamP_backups_list.md"
# Check if the text block exists
if backup_name not in bpy.data.texts:
backup_text = bpy.data.texts.new(backup_name)
else:
backup_text = bpy.data.texts[backup_name]
# Move the cursor to the end of the text block
backup_text.cursor_set(len(backup_text.as_string()))
# Add a suffix to the title if provided, or use "base" if not
title = "CameraArchive"
if self.backup_suffix:
title += "_" + self.backup_suffix
else:
title += "_base"
# Store the original title without the prefix and suffix
original_title = self.backup_suffix if self.backup_suffix else "base"
# Check if the title with the same suffix already exists
existing_title = f"------------------------------------{title}------------------------------------\n"
existing_titles = [title[0] for title in self.get_titles(context)]
if title.split('_')[-1] in existing_titles:
# Replace the existing title and its data with the new data
lines = backup_text.as_string().split('\n')
title_without_newline = existing_title.strip()
if title_without_newline in lines:
start_idx = lines.index(title_without_newline)
end_idx = start_idx + 1
while end_idx < len(lines) and not lines[end_idx].startswith('------------------------------------'):
end_idx += 1
# Create a new record for the given title
new_record = [lines[start_idx]] # Keep the existing title intact
CamP_objects = [f"CamP_sub{str(i).zfill(2)}" for i in range(1, 25)]
existing_CamP = [cam for cam in bpy.data.objects if cam.name in CamP_objects]
for CamP in existing_CamP:
new_record.append(f"{CamP.name}")
new_record.append(f"Location: {CamP.location.x}, {CamP.location.y}, {CamP.location.z}")
new_record.append(f"Rotation: {CamP.rotation_euler.x}, {CamP.rotation_euler.y}, {CamP.rotation_euler.z}")
new_record.append(f"Lens: {CamP.data.lens}")
new_record.append(f"Shift X: {CamP.data.shift_x}")
new_record.append(f"Shift Y: {CamP.data.shift_y}")
new_record.append(f"Clip Start: {CamP.data.clip_start}")
new_record.append(f"Clip End: {CamP.data.clip_end}")
new_record.append(f"Sensor Width: {CamP.data.sensor_width}")
try:
new_record.append(f"Resolution X: {CamP.data.per_camera_resolution.resolution_x}")
new_record.append(f"Resolution Y: {CamP.data.per_camera_resolution.resolution_y}")
except AttributeError:
new_record.append(f"Resolution X: 1920")
new_record.append(f"Resolution Y: 1080")
# Adding the mist_settings.depth attribute
# Save the current frame
current_frame = bpy.context.scene.frame_current
# Set the current frame to the specific frame and record the mist_settings.depth attribute
frame_offset = int(CamP.name.split('sub')[1]) # extract the frame offset from the camera name
frame_number = 0 - frame_offset
bpy.context.scene.frame_set(frame_number) # set the current frame to the specific frame
world_name = bpy.context.scene.world.name # get the current world name
mist_depth = bpy.data.worlds[world_name].mist_settings.depth # get the mist_settings.depth value for the current world and this frame
new_record.append(f"Mist Depth: {mist_depth}")
# Restore the current frame
bpy.context.scene.frame_set(current_frame)
new_record.append("------------------")
if CamP == existing_CamP[-1]:
new_record.append("") # Add an extra line here only if it's the last camera
# Replace the existing record with the new record
lines[start_idx:end_idx] = new_record
# Update the text block with the modified lines
backup_text.from_string('\n'.join(lines))
else:
print(f"Title '{title}' not found in the text block.")
else:
# Write the title to the text block
backup_text.write(existing_title)
# Write the parameters to the text block
CamP_objects = [f"CamP_sub{str(i).zfill(2)}" for i in range(1, 25)]
existing_CamP = [cam for cam in bpy.data.objects if cam.name in CamP_objects]
for CamP in existing_CamP:
backup_text.write(f"{CamP.name}\n")
backup_text.write(f"Location: {CamP.location.x}, {CamP.location.y}, {CamP.location.z}\n")
backup_text.write(f"Rotation: {CamP.rotation_euler.x}, {CamP.rotation_euler.y}, {CamP.rotation_euler.z}\n")
backup_text.write(f"Lens: {CamP.data.lens}\n")
backup_text.write(f"Shift X: {CamP.data.shift_x}\n")
backup_text.write(f"Shift Y: {CamP.data.shift_y}\n")
backup_text.write(f"Clip Start: {CamP.data.clip_start}\n")
backup_text.write(f"Clip End: {CamP.data.clip_end}\n")
backup_text.write(f"Sensor Width: {CamP.data.sensor_width}\n")
try:
backup_text.write(f"Resolution X: {CamP.data.per_camera_resolution.resolution_x}\n")
backup_text.write(f"Resolution Y: {CamP.data.per_camera_resolution.resolution_y}\n")
except AttributeError:
backup_text.write(f"Resolution X: 1920\n")
backup_text.write(f"Resolution Y: 1080\n")
# Adding the mist_settings.depth attribute
# Save the current frame
current_frame = bpy.context.scene.frame_current
# Set the current frame to the specific frame and record the mist_settings.depth attribute
frame_offset = int(CamP.name.split('sub')[1]) # extract the frame offset from the camera name
frame_number = 0 - frame_offset
bpy.context.scene.frame_set(frame_number) # set the current frame to the specific frame
world_name = bpy.context.scene.world.name # get the current world name
mist_depth = bpy.data.worlds[world_name].mist_settings.depth # get the mist_settings.depth value for the current world and this frame
backup_text.write(f"Mist Depth: {mist_depth}\n")
# Restore the current frame
bpy.context.scene.frame_set(current_frame)
backup_text.write("------------------\n")
# Update the base_path with the original title
self.update_base_path(original_title)
return {'FINISHED'}
def update_base_path(self, original_title):
base_path = bpy.data.scenes[bpy.context.scene.name].node_tree.nodes["Output_path_MP"].base_path
new_base_path = f"//multires_projecting/{original_title}/{{camera}}"
bpy.data.scenes[bpy.context.scene.name].node_tree.nodes["Output_path_MP"].base_path = new_base_path
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
# Register the operator
bpy.utils.register_class(BackupCamPParametersOperator)
# Call the operator
bpy.ops.object.backup_camp_parameters('INVOKE_DEFAULT')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment