diff --git a/src/engines/blender/blender_worker.py b/src/engines/blender/blender_worker.py index 7f0067c..ecd6374 100644 --- a/src/engines/blender/blender_worker.py +++ b/src/engines/blender/blender_worker.py @@ -40,10 +40,16 @@ class BlenderRenderWorker(BaseRenderWorker): cmd.append('--python-expr') python_exp = 'import bpy; bpy.context.scene.render.use_overwrite = False;' + # Setup Custom Resolution + if self.args.get('resolution'): + res = self.args.get('resolution') + python_exp += 'bpy.context.scene.render.resolution_percentage = 100;' + python_exp += f'bpy.context.scene.render.resolution_x={res[0]}; bpy.context.scene.render.resolution_y={res[1]};' + # Setup Custom Camera custom_camera = self.args.get('camera', None) if custom_camera: - python_exp = python_exp + f"bpy.context.scene.camera = bpy.data.objects['{custom_camera}'];" + python_exp += f"bpy.context.scene.camera = bpy.data.objects['{custom_camera}'];" # Set Render Device for Cycles (gpu/cpu/any) if blender_engine == 'CYCLES': diff --git a/src/engines/ffmpeg/ffmpeg_worker.py b/src/engines/ffmpeg/ffmpeg_worker.py index 089fc58..fd5f59d 100644 --- a/src/engines/ffmpeg/ffmpeg_worker.py +++ b/src/engines/ffmpeg/ffmpeg_worker.py @@ -19,8 +19,8 @@ class FFMPEGRenderWorker(BaseRenderWorker): cmd = [self.engine_path, '-y', '-stats', '-i', self.input_path] # Resize frame - if self.args.get('x_resolution', None) and self.args.get('y_resolution', None): - cmd.extend(['-vf', f"scale={self.args['x_resolution']}:{self.args['y_resolution']}"]) + if self.args.get('resolution', None): + cmd.extend(['-vf', f"scale={self.args['resolution'][0]}:{self.args['resolution'][1]}"]) # Convert raw args from string if available raw_args = self.args.get('raw', None) diff --git a/src/ui/add_job_window.py b/src/ui/add_job_window.py index b8ae036..00f9f77 100644 --- a/src/ui/add_job_window.py +++ b/src/ui/add_job_window.py @@ -13,17 +13,21 @@ from src.api.server_proxy import RenderServerProxy from src.engines.engine_manager import EngineManager from src.ui.engine_help_window import EngineHelpViewer from src.utilities.zeroconf_server import ZeroconfServer +from src.utilities.misc_helper import COMMON_RESOLUTIONS +from utilities.misc_helper import COMMON_FRAME_RATES class NewRenderJobForm(QWidget): def __init__(self, project_path=None): super().__init__() - self.notes_group = None - self.frame_rate_input = None + self.resolution_options_list = None self.resolution_x_input = None - self.engine_group = None - self.output_settings_group = None self.resolution_y_input = None + self.fps_options_list = None + self.fps_input = None + self.engine_group = None + self.notes_group = None + self.output_settings_group = None self.project_path = project_path # UI @@ -174,34 +178,64 @@ class NewRenderJobForm(QWidget): frame_range_layout.addWidget(QLabel("Frames:")) self.start_frame_input = QSpinBox() self.start_frame_input.setRange(1, 99999) + self.start_frame_input.setFixedWidth(80) self.end_frame_input = QSpinBox() self.end_frame_input.setRange(1, 99999) + self.start_frame_input.setFixedWidth(80) frame_range_layout.addWidget(self.start_frame_input) frame_range_layout.addWidget(QLabel("to")) frame_range_layout.addWidget(self.end_frame_input) + frame_range_layout.addStretch() output_settings_layout.addLayout(frame_range_layout) - # Resolution & FPS - resolution_layout = QHBoxLayout() - resolution_layout.addWidget(QLabel("Resolution:")) + # --- Resolution & FPS Group --- + resolution_group = QGroupBox("Resolution / Frame Rate") + output_settings_layout.addWidget(resolution_group) + resolution_group_layout = QVBoxLayout() + resolution_group.setLayout(resolution_group_layout) + + # Resolution + resolution_layout = QHBoxLayout(resolution_group) + self.resolution_options_list = QComboBox() + self.resolution_options_list.setFixedWidth(200) + self.resolution_options_list.addItem("Original Size") + for res in COMMON_RESOLUTIONS: + self.resolution_options_list.addItem(res) + self.resolution_options_list.currentIndexChanged.connect(self._resolution_preset_changed) + resolution_layout.addWidget(self.resolution_options_list) + resolution_group_layout.addLayout(resolution_layout) self.resolution_x_input = QSpinBox() self.resolution_x_input.setRange(1, 9999) self.resolution_x_input.setValue(1920) + self.resolution_x_input.setFixedWidth(80) + resolution_layout.addWidget(self.resolution_x_input) self.resolution_y_input = QSpinBox() self.resolution_y_input.setRange(1, 9999) self.resolution_y_input.setValue(1080) - self.frame_rate_input = QDoubleSpinBox() - self.frame_rate_input.setDecimals(3) - self.frame_rate_input.setRange(1.0, 999.0) - self.frame_rate_input.setValue(23.976) - - resolution_layout.addWidget(self.resolution_x_input) + self.resolution_y_input.setFixedWidth(80) resolution_layout.addWidget(QLabel("x")) resolution_layout.addWidget(self.resolution_y_input) - resolution_layout.addWidget(QLabel("@")) - resolution_layout.addWidget(self.frame_rate_input) - resolution_layout.addWidget(QLabel("fps")) - output_settings_layout.addLayout(resolution_layout) + resolution_layout.addStretch() + + fps_layout = QHBoxLayout(resolution_group) + self.fps_options_list = QComboBox() + self.fps_options_list.setFixedWidth(200) + self.fps_options_list.addItem("Original FPS") + for fps_option in COMMON_FRAME_RATES: + self.fps_options_list.addItem(fps_option) + self.fps_options_list.currentIndexChanged.connect(self._fps_preset_changed) + fps_layout.addWidget(self.fps_options_list) + + self.fps_input = QDoubleSpinBox() + self.fps_input.setDecimals(3) + self.fps_input.setRange(1.0, 999.0) + self.fps_input.setValue(23.976) + self.fps_input.setFixedWidth(80) + + fps_layout.addWidget(self.fps_input) + fps_layout.addWidget(QLabel("fps")) + fps_layout.addStretch() + resolution_group_layout.addLayout(fps_layout) output_settings_layout.addStretch() @@ -279,6 +313,22 @@ class NewRenderJobForm(QWidget): self.toggle_engine_enablement(False) self.tabs.setCurrentIndex(0) + def _resolution_preset_changed(self, index): + selected_res = COMMON_RESOLUTIONS.get(self.resolution_options_list.currentText()) + if selected_res: + self.resolution_x_input.setValue(selected_res[0]) + self.resolution_y_input.setValue(selected_res[1]) + elif index == 0: + self.resolution_x_input.setValue(self.project_info.get('resolution_x')) + self.resolution_y_input.setValue(self.project_info.get('resolution_y')) + + def _fps_preset_changed(self, index): + selected_fps = COMMON_FRAME_RATES.get(self.fps_options_list.currentText()) + if selected_fps: + self.fps_input.setValue(selected_fps) + elif index == 0: + self.fps_input.setValue(self.project_info.get('fps')) + def update_engine_info(self): # get the engine info and add them all to the ui self.engine_info = self.server_proxy.get_engine_info(response_type='full') @@ -363,7 +413,7 @@ class NewRenderJobForm(QWidget): self.end_frame_input.setValue(self.project_info.get('frame_end')) self.resolution_x_input.setValue(self.project_info.get('resolution_x')) self.resolution_y_input.setValue(self.project_info.get('resolution_y')) - self.frame_rate_input.setValue(self.project_info.get('fps')) + self.fps_input.setValue(self.project_info.get('fps')) # Cameras self.cameras_list.clear() @@ -387,8 +437,7 @@ class NewRenderJobForm(QWidget): # Dynamic Engine Options clear_layout(self.engine_options_layout) # clear old options # dynamically populate option list - system_info = self.engine_info.get(engine.name(), {}).get('system_info', {}) - self.current_engine_options = engine.ui_options(system_info=system_info) + self.current_engine_options = engine().ui_options() for option in self.current_engine_options: h_layout = QHBoxLayout() label = QLabel(option['name'].replace('_', ' ').capitalize() + ':') @@ -488,11 +537,14 @@ class SubmitWorker(QThread): try: hostname = self.window.server_input.currentText() + resolution = (self.window.resolution_x_input.text(), self.window.resolution_y_input.text()) job_json = {'owner': psutil.Process().username() + '@' + socket.gethostname(), 'engine_name': self.window.engine_type.currentText().lower(), 'engine_version': self.window.engine_version_combo.currentText(), 'args': {'raw': self.window.raw_args.text(), - 'export_format': self.window.file_format_combo.currentText()}, + 'export_format': self.window.file_format_combo.currentText(), + 'resolution': resolution, + 'fps': self.window.fps_input.text(),}, 'output_path': self.window.job_name_input.text(), 'start_frame': self.window.start_frame_input.value(), 'end_frame': self.window.end_frame_input.value(), diff --git a/src/utilities/misc_helper.py b/src/utilities/misc_helper.py index 1d622d6..4b20713 100644 --- a/src/utilities/misc_helper.py +++ b/src/utilities/misc_helper.py @@ -370,3 +370,56 @@ def get_gpu_info(): return get_windows_gpu_info() else: # Assume Linux or other return get_linux_gpu_info() + +COMMON_RESOLUTIONS = { + # SD + "SD_480p": (640, 480), + "NTSC_DVD": (720, 480), + "PAL_DVD": (720, 576), + + # HD + "HD_720p": (1280, 720), + "HD_900p": (1600, 900), + "HD_1080p": (1920, 1080), + + # Cinema / Film + "2K_DCI": (2048, 1080), + "4K_DCI": (4096, 2160), + + # UHD / Consumer + "UHD_4K": (3840, 2160), + "UHD_5K": (5120, 2880), + "UHD_8K": (7680, 4320), + + # Ultrawide / Aspect Variants + "UW_1080p": (2560, 1080), + "UW_1440p": (3440, 1440), + "UW_5K": (5120, 2160), + + # Mobile / Social + "VERTICAL_1080x1920": (1080, 1920), + "SQUARE_1080": (1080, 1080), + + # Classic / Legacy + "VGA": (640, 480), + "SVGA": (800, 600), + "XGA": (1024, 768), + "WXGA": (1280, 800), +} + +COMMON_FRAME_RATES = { + "23.976 (NTSC Film)": 23.976, + "24 (Cinema)": 24.0, + "25 (PAL)": 25.0, + "29.97 (NTSC)": 29.97, + "30": 30.0, + "48 (HFR Film)": 48.0, + "50 (PAL HFR)": 50.0, + "59.94": 59.94, + "60": 60.0, + "72": 72.0, + "90 (VR)": 90.0, + "120": 120.0, + "144 (Gaming)": 144.0, + "240 (HFR)": 240.0, +} \ No newline at end of file