diff --git a/__init__.py b/__init__.py index 257e694..629dbd8 100644 --- a/__init__.py +++ b/__init__.py @@ -416,6 +416,7 @@ def register(): # Looking Glass quilt rendering bpy.utils.register_class(LOOKINGGLASS_OT_render_quilt) + bpy.utils.register_class(LOOKINGGLASS_OT_generate_separate_cameras) # Looking Glass viewport bpy.utils.register_class(LOOKINGGLASS_OT_render_viewport) @@ -575,6 +576,7 @@ def unregister(): # Looking Glass quilt rendering bpy.utils.unregister_class(LOOKINGGLASS_OT_render_quilt) + bpy.utils.unregister_class(LOOKINGGLASS_OT_generate_separate_cameras) # remove the keymap keyconfigs_addon = bpy.context.window_manager.keyconfigs.addon diff --git a/extra_tools/QuiltImporter.jsx b/extra_tools/QuiltImporter.jsx new file mode 100644 index 0000000..713b5ae --- /dev/null +++ b/extra_tools/QuiltImporter.jsx @@ -0,0 +1,263 @@ +// After Effects Script: Multi-Camera Quilt Importer +// Place this file in: After Effects > Scripts > ScriptUI Panels +// Or run via File > Scripts > Run Script File + +(function createQuiltImporter() { + + // Create UI Panel + var panel = new Window("dialog", "Multi-Camera Quilt Importer"); + panel.orientation = "column"; + panel.alignChildren = "fill"; + + // Folder selection + var folderGroup = panel.add("group"); + folderGroup.orientation = "row"; + folderGroup.add("statictext", undefined, "Animation Folder:"); + var folderButton = folderGroup.add("button", undefined, "Browse..."); + var folderPath = null; + + // Quilt settings + var settingsGroup = panel.add("group"); + settingsGroup.orientation = "column"; + settingsGroup.alignChildren = "left"; + + settingsGroup.add("statictext", undefined, "Quilt Layout:"); + var layoutGroup = settingsGroup.add("group"); + layoutGroup.orientation = "row"; + var widthInput = layoutGroup.add("edittext", undefined, "5"); + widthInput.characters = 3; + layoutGroup.add("statictext", undefined, "x"); + var heightInput = layoutGroup.add("edittext", undefined, "9"); + heightInput.characters = 3; + + // Resolution settings + var resGroup = settingsGroup.add("group"); + resGroup.orientation = "row"; + resGroup.add("statictext", undefined, "Individual Camera Resolution:"); + var resWidthInput = resGroup.add("edittext", undefined, "1920"); + resWidthInput.characters = 5; + resGroup.add("statictext", undefined, "x"); + var resHeightInput = resGroup.add("edittext", undefined, "1080"); + resHeightInput.characters = 5; + + // Frame rate + var fpsGroup = settingsGroup.add("group"); + fpsGroup.orientation = "row"; + fpsGroup.add("statictext", undefined, "Frame Rate:"); + var fpsInput = fpsGroup.add("edittext", undefined, "25"); + fpsInput.characters = 4; + + // Preview area + var previewGroup = panel.add("group"); + previewGroup.orientation = "column"; + var previewText = previewGroup.add("statictext", undefined, "Select folder to preview cameras..."); + previewText.preferredSize.width = 400; + + // Buttons + var buttonGroup = panel.add("group"); + buttonGroup.orientation = "row"; + var importButton = buttonGroup.add("button", undefined, "Import & Create Quilt"); + var cancelButton = buttonGroup.add("button", undefined, "Cancel"); + + importButton.enabled = false; + + // Folder selection handler + folderButton.onClick = function() { + var folder = Folder.selectDialog("Select folder containing animation sequences"); + if (folder) { + folderPath = folder; + analyzeCameras(folder); + } + }; + + // Analyze cameras in folder + function analyzeCameras(folder) { + var files = folder.getFiles("*.png"); + var cameras = {}; + var baseName = ""; + + for (var i = 0; i < files.length; i++) { + var fileName = files[i].name; + // Match pattern: name_camera_frame.png + var match = fileName.match(/^(.+)_(\d+)_(\d+)\.png$/); + if (match) { + baseName = match[1]; + var camera = parseInt(match[2]); + var frame = parseInt(match[3]); + + if (!cameras[camera]) { + cameras[camera] = []; + } + cameras[camera].push(frame); + } + } + + var cameraList = []; + for (var cam in cameras) { + cameraList.push(parseInt(cam)); + cameras[cam].sort(function(a, b) { return a - b; }); + } + cameraList.sort(function(a, b) { return a - b; }); + + var previewInfo = "Found: " + cameraList.length + " cameras\\n"; + previewInfo += "Base name: " + baseName + "\\n"; + previewInfo += "Cameras: " + cameraList.join(", ") + "\\n"; + previewInfo += "Frame range: " + Math.min.apply(null, cameras[cameraList[0]]) + + " - " + Math.max.apply(null, cameras[cameraList[0]]); + + previewText.text = previewInfo; + importButton.enabled = cameraList.length > 0; + } + + // Import and create quilt + importButton.onClick = function() { + if (!folderPath) { + alert("Please select a folder first."); + return; + } + + var quiltWidth = parseInt(widthInput.text); + var quiltHeight = parseInt(heightInput.text); + var camWidth = parseInt(resWidthInput.text); + var camHeight = parseInt(resHeightInput.text); + var fps = parseFloat(fpsInput.text); + + if (isNaN(quiltWidth) || isNaN(quiltHeight) || isNaN(camWidth) || isNaN(camHeight) || isNaN(fps)) { + alert("Please enter valid numbers for all settings."); + return; + } + + panel.close(); + createQuilt(folderPath, quiltWidth, quiltHeight, camWidth, camHeight, fps); + }; + + cancelButton.onClick = function() { + panel.close(); + }; + + function createQuilt(folder, qWidth, qHeight, camWidth, camHeight, fps) { + app.beginUndoGroup("Create Multi-Camera Quilt"); + + try { + // Get all cameras + var files = folder.getFiles("*.png"); + var cameras = {}; + var baseName = ""; + + for (var i = 0; i < files.length; i++) { + var fileName = files[i].name; + var match = fileName.match(/^(.+)_(\d+)_(\d+)\.png$/); + if (match) { + baseName = match[1]; + var camera = parseInt(match[2]); + + if (!cameras[camera]) { + cameras[camera] = []; + } + cameras[camera].push(files[i]); + } + } + + var cameraList = []; + for (var cam in cameras) { + cameraList.push(parseInt(cam)); + } + cameraList.sort(function(a, b) { return a - b; }); + + // Import sequences + var importedSequences = {}; + for (var c = 0; c < cameraList.length; c++) { + var camNum = cameraList[c]; + var firstFile = null; + + // Find first frame for this camera + for (var f = 0; f < cameras[camNum].length; f++) { + var fileName = cameras[camNum][f].name; + var match = fileName.match(/^(.+)_(\d+)_(\d+)\.png$/); + if (match) { + var frame = parseInt(match[3]); + if (!firstFile || frame < firstFileFrame) { + firstFile = cameras[camNum][f]; + var firstFileFrame = frame; + } + } + } + + if (firstFile) { + var importOptions = new ImportOptions(firstFile); + importOptions.sequence = true; + if (forceSquarePixels) { + importOptions.forceAlphabetical = true; + } + var sequence = app.project.importFile(importOptions); + sequence.name = baseName + "_Camera_" + String(camNum).padStart(2, '0'); + + // Force square pixels if option is checked + if (forceSquarePixels) { + sequence.pixelAspect = 1.0; + } + + importedSequences[camNum] = sequence; + } + } + + // Create quilt composition + var compWidth = qWidth * camWidth; + var compHeight = qHeight * camHeight; + var comp = app.project.items.addComp(baseName + "_Quilt_" + qWidth + "x" + qHeight, + compWidth, compHeight, 1.0, + importedSequences[cameraList[0]].duration, fps); + + // Arrange cameras in quilt pattern + var camIndex = 0; + for (var row = 0; row < qHeight; row++) { + for (var col = 0; col < qWidth; col++) { + if (camIndex < cameraList.length) { + var camNum = cameraList[camIndex]; + var sequence = importedSequences[camNum]; + + var layer = comp.layers.add(sequence); + layer.name = "Camera_" + String(camNum).padStart(2, '0'); + + // Position: bottom-left is 0,0, so we need to flip Y + var x = col * camWidth + camWidth / 2; + var y = (qHeight - 1 - row) * camHeight + camHeight / 2; + + layer.property("Transform").property("Position").setValue([x, y]); + + camIndex++; + } + } + } + + alert("Quilt composition created successfully!\\n" + + "Composition: " + comp.name + "\\n" + + "Resolution: " + compWidth + "x" + compHeight + "\\n" + + "Cameras imported: " + cameraList.length); + + } catch (error) { + alert("Error creating quilt: " + error.toString()); + } + + app.endUndoGroup(); + } + + // String padding function for older AE versions + if (!String.prototype.padStart) { + String.prototype.padStart = function(targetLength, padString) { + targetLength = targetLength >> 0; + padString = String(padString || ' '); + if (this.length > targetLength) { + return String(this); + } else { + targetLength = targetLength - this.length; + if (targetLength > padString.length) { + padString += padString.repeat(targetLength / padString.length); + } + return padString.slice(0, targetLength) + String(this); + } + }; + } + + panel.show(); +})(); \ No newline at end of file diff --git a/extra_tools/rename_animation.bat b/extra_tools/rename_animation.bat new file mode 100644 index 0000000..03e8046 --- /dev/null +++ b/extra_tools/rename_animation.bat @@ -0,0 +1,145 @@ +@echo off +echo ======================================== +echo Animation File Renamer for After Effects +echo ======================================== +echo. +echo IMPORTANT: This script needs to run in the folder with your animation files! +echo. +echo Current folder: %CD% +echo. +echo Choose an option: +echo 1. Navigate to your animation folder (recommended) +echo 2. Continue in current folder +echo 3. Exit +echo. +set /p choice=Enter your choice (1, 2, or 3): + +if "%choice%"=="1" goto navigate +if "%choice%"=="2" goto diagnostic +if "%choice%"=="3" goto exit +goto invalid + +:navigate +echo. +echo Please drag and drop your animation folder here, then press Enter: +set /p folderPath= +if "%folderPath%"=="" goto navigate + +REM Remove quotes if they exist +set folderPath=%folderPath:"=% + +REM Change to the specified directory +cd /d "%folderPath%" 2>nul +if errorlevel 1 ( + echo Error: Could not access folder "%folderPath%" + echo Please check the path and try again. + pause + goto navigate +) + +echo Changed to: %CD% +echo. + +:diagnostic +echo Checking for animation files... +echo. + +REM Create diagnostic PowerShell script +echo Write-Host "=== CHECKING CURRENT FOLDER ===" > temp_diagnostic.ps1 +echo Write-Host "Current folder: $(Get-Location)" >> temp_diagnostic.ps1 +echo $allPngs = Get-ChildItem -Filter "*.png" >> temp_diagnostic.ps1 +echo Write-Host "Total PNG files found: $($allPngs.Count)" >> temp_diagnostic.ps1 +echo Write-Host "" >> temp_diagnostic.ps1 +echo if ($allPngs.Count -gt 0) { >> temp_diagnostic.ps1 +echo Write-Host "First 10 filenames:" >> temp_diagnostic.ps1 +echo $allPngs ^| Select-Object -First 10 ^| ForEach-Object { Write-Host " $($_.Name)" } >> temp_diagnostic.ps1 +echo Write-Host "" >> temp_diagnostic.ps1 +echo } >> temp_diagnostic.ps1 +echo Write-Host "=== TESTING ANIMATION PATTERNS ===" >> temp_diagnostic.ps1 +echo $pattern1 = Get-ChildItem -Filter "*.png" ^| Where-Object { $_.Name -match "^(.+)_(\d{4})_(\d{2})\.png$" } >> temp_diagnostic.ps1 +echo Write-Host "Pattern 1 (name_4digits_2digits): $($pattern1.Count) matches" >> temp_diagnostic.ps1 +echo $pattern2 = Get-ChildItem -Filter "*.png" ^| Where-Object { $_.Name -match "^(.+)_(\d+)_(\d+)\.png$" } >> temp_diagnostic.ps1 +echo Write-Host "Pattern 2 (name_anydigits_anydigits): $($pattern2.Count) matches" >> temp_diagnostic.ps1 +echo $hotelFiles = Get-ChildItem -Filter "Hotel_V8_Bunnik_*.png" >> temp_diagnostic.ps1 +echo Write-Host "Hotel_V8_Bunnik files: $($hotelFiles.Count)" >> temp_diagnostic.ps1 +echo Write-Host "" >> temp_diagnostic.ps1 +echo if ($pattern2.Count -gt 0) { >> temp_diagnostic.ps1 +echo Write-Host "Sample files that match:" >> temp_diagnostic.ps1 +echo $pattern2 ^| Select-Object -First 3 ^| ForEach-Object { >> temp_diagnostic.ps1 +echo Write-Host " $($_.Name)" >> temp_diagnostic.ps1 +echo if ($_.Name -match "^(.+)_(\d+)_(\d+)\.png$") { >> temp_diagnostic.ps1 +echo $name = $matches[1] >> temp_diagnostic.ps1 +echo $frame = $matches[2] >> temp_diagnostic.ps1 +echo $camera = $matches[3] >> temp_diagnostic.ps1 +echo $newName = $name + "_" + $camera + "_" + $frame + ".png" >> temp_diagnostic.ps1 +echo Write-Host " Would rename to: $newName" >> temp_diagnostic.ps1 +echo } >> temp_diagnostic.ps1 +echo } >> temp_diagnostic.ps1 +echo } elseif ($allPngs.Count -gt 0) { >> temp_diagnostic.ps1 +echo Write-Host "No files match the expected animation pattern." >> temp_diagnostic.ps1 +echo Write-Host "Are you sure these are the right files?" >> temp_diagnostic.ps1 +echo } else { >> temp_diagnostic.ps1 +echo Write-Host "No PNG files found in this folder!" >> temp_diagnostic.ps1 +echo } >> temp_diagnostic.ps1 + +powershell -ExecutionPolicy Bypass -File temp_diagnostic.ps1 + +echo. +if exist temp_diagnostic.ps1 del temp_diagnostic.ps1 + +echo ======================================== +echo. +set /p proceed=Do you want to proceed with renaming? (Y/N): +if /i "%proceed%"=="Y" goto rename +if /i "%proceed%"=="y" goto rename +goto cleanup + +:rename +echo. +echo Starting rename process... + +echo $files = Get-ChildItem -Filter "*.png" ^| Where-Object { $_.Name -match "^(.+)_(\d+)_(\d+)\.png$" } > temp_rename.ps1 +echo Write-Host "Renaming $($files.Count) files..." >> temp_rename.ps1 +echo $count = 0 >> temp_rename.ps1 +echo foreach ($file in $files) { >> temp_rename.ps1 +echo if ($file.Name -match "^(.+)_(\d+)_(\d+)\.png$") { >> temp_rename.ps1 +echo $name = $matches[1] >> temp_rename.ps1 +echo $frame = $matches[2] >> temp_rename.ps1 +echo $camera = $matches[3] >> temp_rename.ps1 +echo $newName = $name + "_" + $camera + "_" + $frame + ".png" >> temp_rename.ps1 +echo try { >> temp_rename.ps1 +echo Rename-Item -Path $file.FullName -NewName $newName >> temp_rename.ps1 +echo $count++ >> temp_rename.ps1 +echo if ($count %% 1000 -eq 0) { >> temp_rename.ps1 +echo Write-Host "Processed $count files..." >> temp_rename.ps1 +echo } >> temp_rename.ps1 +echo } >> temp_rename.ps1 +echo catch { >> temp_rename.ps1 +echo Write-Host "Failed to rename: $($file.Name)" >> temp_rename.ps1 +echo } >> temp_rename.ps1 +echo } >> temp_rename.ps1 +echo } >> temp_rename.ps1 +echo Write-Host "Successfully renamed $count files!" >> temp_rename.ps1 + +powershell -ExecutionPolicy Bypass -File temp_rename.ps1 + +:cleanup +if exist temp_rename.ps1 del temp_rename.ps1 +echo. +echo ======================================== +echo DONE! +echo ======================================== +goto end + +:invalid +echo Invalid choice. Please enter 1, 2, or 3. +pause +goto navigate + +:exit +echo Exiting... +goto end + +:end +echo. +pause \ No newline at end of file diff --git a/lightfield_render.py b/lightfield_render.py index 95b48f3..4c997e3 100644 --- a/lightfield_render.py +++ b/lightfield_render.py @@ -1917,3 +1917,188 @@ def modal(self, context, event): # pass event through return {'PASS_THROUGH'} + + +# ADD THIS AFTER THE LOOKINGGLASS_OT_render_quilt CLASS (around line 1300+) + +# Modal operator for generating separate cameras for render farm usage +class LOOKINGGLASS_OT_generate_separate_cameras(bpy.types.Operator): + bl_idname = "lookingglass.generate_separate_cameras" + bl_label = "Generate Separate Cameras" + bl_description = "Create individual cameras for each quilt view that can be rendered separately on a render farm" + bl_options = {'REGISTER', 'UNDO'} + + # OPERATOR ARGUMENTS + collection_name: bpy.props.StringProperty( + name="Collection Name", + description="Name for the collection containing quilt cameras", + default="QuiltCameras" + ) + + replace_existing: bpy.props.BoolProperty( + name="Replace Existing", + description="Replace existing QuiltCameras collection if it exists", + default=True + ) + + # check if everything is correctly set up + @classmethod + def poll(self, context): + # Check if a Looking Glass camera is selected + scene = context.scene + if hasattr(scene, 'addon_settings') and scene.addon_settings.lookingglassCamera: + return True + return False + + def execute(self, context): + scene = context.scene + addon_settings = scene.addon_settings + + # Get the current Looking Glass camera + lg_camera = addon_settings.lookingglassCamera + if not lg_camera: + self.report({'ERROR'}, "No Looking Glass camera selected") + return {'CANCELLED'} + + try: + # Get quilt settings from the addon - using same pattern as render_quilt operator + + # Get active Looking Glass device or selected emulated device + if addon_settings.render_use_device == True and pylio.DeviceManager.get_active(): + device = pylio.DeviceManager.get_active() + quilt_preset = addon_settings.quiltPreset + else: + device = pylio.DeviceManager.get_device(key="index", value=int(addon_settings.render_device_type)) + quilt_preset = addon_settings.render_quilt_preset + + # Get quilt format settings + qs = pylio.LookingGlassQuilt.formats.get() + quilt_settings = qs[int(quilt_preset)] + + # Extract quilt parameters + view_width = quilt_settings["view_width"] + view_height = quilt_settings["view_height"] + rows = quilt_settings["rows"] + columns = quilt_settings["columns"] + total_views = quilt_settings["total_views"] + + # Camera parameters from addon settings + fov = lg_camera.data.angle + camera_distance = addon_settings.focalPlane + view_cone = device.viewCone + + except (AttributeError, KeyError, IndexError) as e: + self.report({'ERROR'}, f"Could not access quilt settings: {str(e)}") + return {'CANCELLED'} + + # Create or replace the QuiltCameras collection + if self.collection_name in bpy.data.collections: + if self.replace_existing: + # Remove existing collection and its cameras + old_collection = bpy.data.collections[self.collection_name] + for obj in list(old_collection.objects): + if obj.type == 'CAMERA': + # Remove camera data + camera_data = obj.data + bpy.data.objects.remove(obj, do_unlink=True) + bpy.data.cameras.remove(camera_data, do_unlink=True) + + # Remove collection + bpy.data.collections.remove(old_collection) + else: + self.report({'WARNING'}, f"Collection '{self.collection_name}' already exists") + return {'CANCELLED'} + + # Create new collection + quilt_collection = bpy.data.collections.new(self.collection_name) + context.scene.collection.children.link(quilt_collection) + + # Get the base camera's transform - using same logic as render_quilt + base_camera = lg_camera + + # Get camera's modelview matrix (same as render_quilt logic) + view_matrix = base_camera.matrix_world.copy() + + # Correct for camera scaling (same as render_quilt) + view_matrix = view_matrix @ Matrix.Scale(1/base_camera.scale.x, 4, (1, 0, 0)) + view_matrix = view_matrix @ Matrix.Scale(1/base_camera.scale.y, 4, (0, 1, 0)) + view_matrix = view_matrix @ Matrix.Scale(1/base_camera.scale.z, 4, (0, 0, 1)) + + # Calculate inverted view matrix + view_matrix_inv = view_matrix.inverted_safe() + + # Calculate camera size from distance and FOV (same as render_quilt) + camera_size = camera_distance * tan(fov / 2) + + # Generate cameras for each quilt view + cameras_created = 0 + + LookingGlassAddonLogger.info(f"Generating {total_views} separate cameras for quilt ({columns}x{rows})") + + for view in range(total_views): + # Create camera data and object + camera_data = bpy.data.cameras.new(f"QuiltCam_{view:02d}") + camera_obj = bpy.data.objects.new(f"QuiltCamera_{view:02d}", camera_data) + + # Copy camera settings from the original Looking Glass camera + camera_data.lens = base_camera.data.lens + camera_data.sensor_width = base_camera.data.sensor_width + camera_data.sensor_height = base_camera.data.sensor_height + camera_data.clip_start = base_camera.data.clip_start + camera_data.clip_end = base_camera.data.clip_end + camera_data.type = base_camera.data.type + + # Copy Depth of Field settings + camera_data.dof.use_dof = base_camera.data.dof.use_dof + camera_data.dof.focus_distance = base_camera.data.dof.focus_distance + camera_data.dof.aperture_fstop = base_camera.data.dof.aperture_fstop + camera_data.dof.aperture_blades = base_camera.data.dof.aperture_blades + camera_data.dof.aperture_rotation = base_camera.data.dof.aperture_rotation + camera_data.dof.aperture_ratio = base_camera.data.dof.aperture_ratio + + # Copy focus object if it exists + if base_camera.data.dof.focus_object: + camera_data.dof.focus_object = base_camera.data.dof.focus_object + + # Calculate camera position using same logic as render_quilt operator + # Start at view_cone * 0.5 and go to -view_cone * 0.5 + offset_angle = (0.5 - view / (total_views - 1)) * radians(view_cone) + + # Calculate the offset that the camera should move + offset = camera_distance * tan(offset_angle) + + # Apply camera position (same transformation as render_quilt) + camera_obj.matrix_world = view_matrix @ (Matrix.Translation((-offset, 0, 0)) @ (view_matrix_inv @ base_camera.matrix_world.copy())) + + # Apply shift (same as render_quilt) + camera_data.shift_x = base_camera.data.shift_x + 0.5 * offset / camera_size + camera_data.shift_y = base_camera.data.shift_y + + # Add custom properties for render farm identification + camera_obj["quilt_view_index"] = view + camera_obj["quilt_position_x"] = view % columns + camera_obj["quilt_position_y"] = view // columns + camera_obj["quilt_width"] = columns + camera_obj["quilt_height"] = rows + camera_obj["quilt_total_views"] = total_views + camera_obj["is_quilt_camera"] = True + camera_obj["view_cone"] = view_cone + camera_obj["focal_plane"] = camera_distance + + # Store original camera reference + camera_obj["original_camera"] = base_camera.name + + # Link to collection + quilt_collection.objects.link(camera_obj) + cameras_created += 1 + + LookingGlassAddonLogger.debug(f" [#] Created camera {view:02d}: offset={offset:.3f}, angle={degrees(offset_angle):.2f}°") + + LookingGlassAddonLogger.info(f"Created {cameras_created} cameras in collection '{self.collection_name}'") + + self.report({'INFO'}, f"Created {cameras_created} cameras in collection '{self.collection_name}' - Ready for render farm") + return {'FINISHED'} + + def invoke(self, context, event): + # Show a popup with options + return context.window_manager.invoke_props_dialog(self) \ No newline at end of file diff --git a/ui.py b/ui.py index f7d8114..7179f95 100644 --- a/ui.py +++ b/ui.py @@ -1592,6 +1592,32 @@ def draw(self, context): render_quilt.animation = True render_quilt.use_multiview = (context.preferences.addons[__package__].preferences.camera_mode == '1') + # ADD NEW BUTTON: Generate Separate Cameras + # Only show when not rendering + if not LookingGlassAddon.RenderInvoked: + # Add some spacing + layout.separator() + + # Generate Separate Cameras button + row_generate_cameras = layout.row(align = True) + row_generate_cameras.operator("lookingglass.generate_separate_cameras", + text="Generate Separate Cameras", + icon='CAMERA_DATA') + + # Show collection status if QuiltCameras exists + if "QuiltCameras" in bpy.data.collections: + collection = bpy.data.collections["QuiltCameras"] + camera_count = len([obj for obj in collection.objects if obj.type == 'CAMERA']) + if camera_count > 0: + box = layout.box() + box.scale_y = 0.8 + box.label(text=f"QuiltCameras: {camera_count} cameras ready", icon='INFO') + + # Add a small note about render farm usage + sub_box = box.box() + sub_box.scale_y = 0.7 + sub_box.label(text="Render farm ready - each camera can render separately") + # if a lockfile was detected on start-up else: @@ -1641,6 +1667,9 @@ def draw(self, context): row_output.enabled = False row_render_still.enabled = False row_render_animation.enabled = False + # Also disable the generate cameras button + if not LookingGlassAddon.RenderInvoked: + row_generate_cameras.enabled = False # if the settings are to be taken from device selection elif context.scene.addon_settings.render_use_device == True: