Last active
July 15, 2025 10:37
-
-
Save Packsod/47b346cc3dd602d97f8120860950c3d4 to your computer and use it in GitHub Desktop.
Cam_CamP_backup & restore
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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