temps restant + video + download link + left click + show next frame done

This commit is contained in:
sorlinv
2026-03-05 15:43:28 +01:00
parent 9ab59373df
commit b169e69b24
20 changed files with 2128 additions and 30 deletions

142
src/python/render_daemon.py Normal file
View File

@@ -0,0 +1,142 @@
"""Daemon de rendu persistant pour Blender.
Ce script est execute par Blender en mode background (-b) et reste actif.
Il lit des commandes JSON depuis stdin et execute les rendus demandes.
Le fichier .blend est charge une seule fois au demarrage.
"""
import bpy
import json
import sys
import os
import logging
logging.basicConfig(level=logging.INFO, format="[render_daemon] %(message)s")
obj_logger: logging.Logger = logging.getLogger(__name__)
def apply_render_settings(obj_scene: object, obj_settings: dict) -> None:
"""Applique les parametres de rendu a la scene."""
if not obj_settings:
return
if "str_engine" in obj_settings:
obj_scene.render.engine = obj_settings["str_engine"]
if "nb_resolution_percentage" in obj_settings:
obj_scene.render.resolution_percentage = obj_settings["nb_resolution_percentage"]
if "is_film_transparent" in obj_settings:
obj_scene.render.film_transparent = obj_settings["is_film_transparent"]
str_engine: str = obj_scene.render.engine
if str_engine == "CYCLES":
if "nb_samples" in obj_settings:
obj_scene.cycles.samples = obj_settings["nb_samples"]
if "str_device" in obj_settings:
obj_scene.cycles.device = obj_settings["str_device"]
if "is_denoise" in obj_settings:
obj_scene.cycles.use_denoising = obj_settings["is_denoise"]
elif str_engine in ("BLENDER_EEVEE", "BLENDER_EEVEE_NEXT"):
if "nb_samples" in obj_settings:
try:
obj_scene.eevee.taa_render_samples = obj_settings["nb_samples"]
except AttributeError:
try:
obj_scene.eevee.samples = obj_settings["nb_samples"]
except AttributeError:
pass
def apply_collections(list_collections: list) -> None:
"""Applique la visibilite des collections pour le rendu."""
if not list_collections:
return
def _set_layer_collection(obj_lc: object, str_name: str, is_val: bool) -> None:
"""Parcourt recursivement les layer collections pour exclure/inclure."""
for obj_child in obj_lc.children:
if obj_child.name == str_name:
obj_child.exclude = is_val
return
_set_layer_collection(obj_child, str_name, is_val)
obj_view_layer_collection = bpy.context.view_layer.layer_collection
for obj_col in list_collections:
str_name: str = obj_col["str_name"]
is_hide: bool = obj_col["is_hide_render"]
bpy.data.collections[str_name].hide_render = is_hide
_set_layer_collection(obj_view_layer_collection, str_name, is_hide)
def process_render(obj_cmd: dict) -> None:
"""Execute le rendu d'une frame selon la commande recue."""
obj_scene = bpy.context.scene
str_camera: str = obj_cmd["str_camera"]
nb_frame: int = obj_cmd["nb_frame"]
nb_resolution_x: int = obj_cmd["nb_resolution_x"]
nb_resolution_y: int = obj_cmd["nb_resolution_y"]
str_format: str = obj_cmd["str_format"]
str_output_path: str = obj_cmd["str_output_path"]
obj_scene.camera = bpy.data.objects[str_camera]
obj_scene.render.resolution_x = nb_resolution_x
obj_scene.render.resolution_y = nb_resolution_y
obj_scene.render.image_settings.file_format = str_format
obj_scene.frame_set(nb_frame)
str_dir: str = os.path.dirname(str_output_path)
if str_dir and not os.path.exists(str_dir):
os.makedirs(str_dir, exist_ok=True)
obj_scene.render.filepath = str_output_path
apply_render_settings(obj_scene, obj_cmd.get("obj_render_settings"))
apply_collections(obj_cmd.get("list_collections"))
try:
bpy.ops.render.render(write_still=True)
str_result: str = json.dumps({"str_file": str_output_path})
sys.stdout.write("RENDER_DONE:" + str_result + "\n")
sys.stdout.flush()
except Exception as obj_err:
str_error: str = json.dumps({"str_error": str(obj_err)})
sys.stdout.write("RENDER_ERROR:" + str_error + "\n")
sys.stdout.flush()
obj_logger.error("Erreur rendu : %s", str(obj_err))
def main() -> None:
"""Boucle principale du daemon de rendu."""
sys.stdout.write("DAEMON_READY\n")
sys.stdout.flush()
for str_line in sys.stdin:
str_line = str_line.strip()
if not str_line:
continue
try:
obj_cmd: dict = json.loads(str_line)
except json.JSONDecodeError as obj_err:
str_error: str = json.dumps({"str_error": "JSON invalide : " + str(obj_err)})
sys.stdout.write("RENDER_ERROR:" + str_error + "\n")
sys.stdout.flush()
continue
str_cmd: str = obj_cmd.get("str_cmd", "")
if str_cmd == "quit":
break
elif str_cmd == "render":
process_render(obj_cmd)
else:
str_error = json.dumps({"str_error": "Commande inconnue : " + str_cmd})
sys.stdout.write("RENDER_ERROR:" + str_error + "\n")
sys.stdout.flush()
main()