scene_legacy.py
1 import importlib 2 import DreamTalk.animation.animation 3 importlib.reload(DreamTalk.animation.animation) 4 from DreamTalk.animation.animation import ScalarAnimation, VectorAnimation 5 from DreamTalk.animation.abstract_animators import ProtoAnimator, AnimationGroup 6 from DreamTalk.objects.camera_objects import TwoDCamera, ThreeDCamera 7 from abc import ABC, abstractmethod 8 from collections import defaultdict 9 from DreamTalk.constants import * 10 import c4d 11 import os 12 import inspect 13 from pprint import pprint 14 15 16 class Scene(ABC): 17 """abstract class acting as blueprint for scenes 18 19 Args: 20 resolution: Render resolution preset (default: "default") 21 alpha: Enable alpha channel (default: True) 22 save: Save render output (default: False) 23 sketch_mode: Enable Sketch & Toon VideoPost (default: True) 24 Set to False when using only stroke_mode objects for faster rendering. 25 generator_mode: Use Python Generators instead of XPresso (default: False) 26 Set to True for MoGraph compatibility and cleaner exports. 27 """ 28 29 def __init__(self, resolution="default", alpha=True, save=False, sketch_mode=True, generator_mode=False): 30 self.resolution = resolution 31 self.alpha = alpha 32 self.save = save 33 self.sketch_mode = sketch_mode 34 self.generator_mode = generator_mode 35 self.time_ini = None 36 self.time_fin = None 37 self.kill_old_document() 38 self.create_new_document() 39 self.set_scene_name() 40 self.insert_document() 41 self.clear_console() 42 self.set_camera() 43 self.construct() 44 self.set_interactive_render_region() 45 self.set_render_settings() 46 self.adjust_timeline() 47 48 def START(self): 49 # writes current time to variable for later use in finish method 50 self.time_ini = self.document.GetTime() 51 52 def STOP(self): 53 # writes current time to variable for later use in finish method 54 self.time_fin = self.document.GetTime() 55 56 def adjust_timeline(self): 57 # set minimum time 58 if self.time_ini is not None: 59 self.document[c4d.DOCUMENT_MINTIME] = self.time_ini 60 self.document[c4d.DOCUMENT_LOOPMINTIME] = self.time_ini 61 62 # set maximum time 63 if self.time_fin is None: 64 self.document[c4d.DOCUMENT_MAXTIME] = self.document.GetTime() 65 self.document[c4d.DOCUMENT_LOOPMAXTIME] = self.document.GetTime() 66 else: 67 self.document[c4d.DOCUMENT_MAXTIME] = self.time_fin 68 self.document[c4d.DOCUMENT_LOOPMAXTIME] = self.time_fin 69 70 def set_render_settings(self): 71 self.render_settings = RenderSettings(alpha=self.alpha, sketch_mode=self.sketch_mode) 72 self.render_settings.set_resolution(self.resolution) 73 if self.save: 74 self.render_settings.set_export_settings() 75 76 def set_camera(self): 77 pass 78 79 def create_new_document(self): 80 """creates a new project and gets the active document""" 81 self.document = c4d.documents.BaseDocument() 82 c4d.documents.InsertBaseDocument(self.document) 83 84 def kill_old_document(self): 85 """kills the old document to always ensure only one document is active""" 86 old_document = c4d.documents.GetActiveDocument() 87 old_document.Remove() 88 c4d.documents.KillDocument(old_document) 89 90 def clear_console(self): 91 """clears the python console""" 92 c4d.CallCommand(13957) 93 94 @abstractmethod 95 def construct(self): 96 """here the actual scene consisting out of objects and animations is constructed 97 this method should be overwritten by the inheriting scene classes""" 98 pass 99 100 @property 101 def scene_name(self): 102 """holds the scene name""" 103 return self._scene_name 104 105 @scene_name.setter 106 def scene_name(self, name): 107 self._scene_name = name 108 109 def set_scene_name(self): 110 """sets the scene name and the document name""" 111 self.scene_name = self.__class__.__name__ 112 self.document.SetDocumentName(self.scene_name) 113 114 def insert_document(self): 115 """inserts the document into cinema""" 116 c4d.documents.InsertBaseDocument(self.document) 117 118 def set_interactive_render_region(self): 119 """creates an IRR window over the full size of the editor view""" 120 c4d.CallCommand(600000022) # call IRR script by ID 121 # workaround because script needs to be executed from main thread not DreamTalk library 122 # ID changes depending on machine 123 # CHANGE THIS IN FUTURE TO MORE ROBUST SOLUTION 124 125 def feed_run_time(self, animations, run_time): 126 """feeds the run time to animations""" 127 for animation in animations: 128 animation.abs_run_time = run_time 129 130 def execute_animations(self, animations): 131 """passes the run time to animations and executes them""" 132 for animation in animations: 133 animation.execute() 134 135 def add_time(self, run_time): 136 """passes the run time in the document timeline""" 137 time_ini = self.document.GetTime() 138 time_fin = time_ini + c4d.BaseTime(run_time) 139 self.document.SetTime(time_fin) 140 c4d.EventAdd() # update cinema 141 142 def flatten(self, animations): 143 """flattens animations by wrapping them in animation group""" 144 animation_group = DreamTalk.animation.animation.AnimationGroup(*animations) 145 flattened_animations = animation_group.animations 146 return flattened_animations 147 148 def get_animation(self, animators): 149 """retreives the animations from the animators depending on type""" 150 animations = [] 151 for animator in animators: 152 if isinstance(animator, DreamTalk.animation.abstract_animators.ProtoAnimator): 153 animation = animator.animations 154 if issubclass(animation.__class__, DreamTalk.animation.animation.VectorAnimation): 155 vector_animation = animator 156 scalar_animations = vector_animation.scalar_animations 157 animations += scalar_animations 158 continue 159 elif issubclass(animator.__class__, DreamTalk.animation.animation.ProtoAnimation): 160 animation = animator 161 elif animator.__class__.__name__ == "AnimationGroup": 162 animation = animator 163 elif issubclass(animator.__class__, DreamTalk.animation.animation.VectorAnimation): 164 vector_animation = animator 165 scalar_animations = vector_animation.scalar_animations 166 animations += scalar_animations 167 continue 168 else: 169 print("Unknown animator input!", animator.__class__) 170 animations.append(animation) 171 return animations 172 173 def play(self, *animators, run_time=1): 174 """handles several tasks for the animations: 175 - handles visibility 176 - flattens animations 177 - links animation chains 178 - feeds them the run time 179 - executes the animations""" 180 animations = self.get_animation(animators) 181 flattened_animations = self.flatten(animations) 182 self.feed_run_time(flattened_animations, run_time) 183 self.execute_animations(flattened_animations) 184 self.add_time(run_time) 185 186 def set(self, *animators): 187 # the set method is just the play method reduced to two frames 188 # one for the initial, one for the final keyframe 189 self.play(*animators, run_time=2/FPS) 190 191 def wait(self, seconds=1): 192 """adds time without any animations""" 193 self.add_time(seconds) 194 195 def render_preview_frames(self, frames=None, output_dir=None, width=640, height=360): 196 """ 197 Render key frames to PNG files for AI-assisted iteration. 198 199 Args: 200 frames: List of frame numbers to render. If None, auto-samples 5 frames 201 across the animation range. 202 output_dir: Directory for output PNGs. Defaults to /tmp/dreamtalk_preview/ 203 width: Preview width in pixels (default 640) 204 height: Preview height in pixels (default 360) 205 206 Returns: 207 List of paths to rendered PNG files. 208 209 Files are named preview_XXXX.png and are overwritten on each call, 210 enabling fast iteration without file accumulation. 211 """ 212 import tempfile 213 214 # Default output directory 215 if output_dir is None: 216 output_dir = os.path.join(tempfile.gettempdir(), "dreamtalk_preview") 217 os.makedirs(output_dir, exist_ok=True) 218 219 # Auto-sample frames if not specified 220 if frames is None: 221 min_frame = int(self.document[c4d.DOCUMENT_MINTIME].GetFrame(FPS)) 222 max_frame = int(self.document[c4d.DOCUMENT_MAXTIME].GetFrame(FPS)) 223 # Sample 5 frames: start, 25%, 50%, 75%, end 224 if max_frame > min_frame: 225 frames = [ 226 min_frame, 227 min_frame + (max_frame - min_frame) // 4, 228 min_frame + (max_frame - min_frame) // 2, 229 min_frame + 3 * (max_frame - min_frame) // 4, 230 max_frame 231 ] 232 else: 233 frames = [min_frame] 234 235 # Get render settings and configure for preview 236 rd = self.document.GetActiveRenderData() 237 original_width = rd[c4d.RDATA_XRES] 238 original_height = rd[c4d.RDATA_YRES] 239 original_format = rd[c4d.RDATA_FORMAT] 240 original_save = rd[c4d.RDATA_SAVEIMAGE] 241 original_alpha = rd[c4d.RDATA_ALPHACHANNEL] 242 243 # Set preview settings 244 rd[c4d.RDATA_XRES] = width 245 rd[c4d.RDATA_YRES] = height 246 rd[c4d.RDATA_FORMAT] = c4d.FILTER_PNG 247 rd[c4d.RDATA_SAVEIMAGE] = False 248 rd[c4d.RDATA_ALPHACHANNEL] = True 249 rd[c4d.RDATA_RENDERENGINE] = c4d.RDATA_RENDERENGINE_STANDARD 250 251 rendered_paths = [] 252 253 try: 254 for frame in frames: 255 # Set frame 256 self.document.SetTime(c4d.BaseTime(frame, FPS)) 257 self.document.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_NONE) 258 259 # Create bitmap 260 bmp = c4d.bitmaps.BaseBitmap() 261 if bmp.Init(width, height, 32) != c4d.IMAGERESULT_OK: 262 print(f"[DreamTalk] Failed to init bitmap for frame {frame}") 263 continue 264 265 # Render 266 render_flags = c4d.RENDERFLAGS_EXTERNAL | c4d.RENDERFLAGS_NODOCUMENTCLONE 267 result = c4d.documents.RenderDocument(self.document, rd.GetData(), bmp, render_flags) 268 269 if result != c4d.RENDERRESULT_OK: 270 print(f"[DreamTalk] Render failed for frame {frame}: {result}") 271 continue 272 273 # Save to file (overwriting) 274 output_path = os.path.join(output_dir, f"preview_{frame:04d}.png") 275 if bmp.Save(output_path, c4d.FILTER_PNG) == c4d.IMAGERESULT_OK: 276 rendered_paths.append(output_path) 277 print(f"[DreamTalk] Rendered frame {frame} -> {output_path}") 278 else: 279 print(f"[DreamTalk] Failed to save frame {frame}") 280 281 bmp.FlushAll() 282 283 finally: 284 # Restore original settings 285 rd[c4d.RDATA_XRES] = original_width 286 rd[c4d.RDATA_YRES] = original_height 287 rd[c4d.RDATA_FORMAT] = original_format 288 rd[c4d.RDATA_SAVEIMAGE] = original_save 289 rd[c4d.RDATA_ALPHACHANNEL] = original_alpha 290 c4d.EventAdd() 291 292 return rendered_paths 293 294 295 class RenderSettings(): 296 """holds and writes the render settings to cinema 297 298 Args: 299 alpha: Enable alpha channel (default: True) 300 sketch_mode: Enable Sketch & Toon VideoPost (default: True) 301 Set to False when using only stroke_mode objects for faster rendering. 302 """ 303 304 def __init__(self, alpha=True, sketch_mode=True): 305 self.alpha = alpha 306 self.sketch_mode = sketch_mode 307 self.document = c4d.documents.GetActiveDocument() # get document 308 self.set_base_settings() 309 if self.sketch_mode: 310 self.set_sketch_settings() 311 312 def set_export_settings(self): 313 """sets the export settings""" 314 # get the caller's directory 315 # get directory from path 316 directory = os.path.dirname(inspect.stack()[3].filename) 317 # get the caller's class name 318 frame = inspect.currentframe().f_back 319 class_name = frame.f_locals.get('self', None).__class__.__name__ 320 # get the path 321 if self.alpha: 322 path = os.path.join(directory, class_name + "_alpha", class_name) # add folder for alpha channel pngs 323 else: 324 path = os.path.join(directory, class_name) 325 self.settings[c4d.RDATA_PATH] = path 326 if self.alpha: 327 self.settings[c4d.RDATA_ALPHACHANNEL] = True # Enable alpha channel 328 self.settings[c4d.RDATA_SAVEIMAGE] = True # set to save image 329 330 def set_base_settings(self): 331 """sets the base settings""" 332 self.settings = self.document.GetActiveRenderData() 333 334 # set parameters 335 self.settings[c4d.RDATA_RENDERENGINE] = c4d.RDATA_RENDERENGINE_STANDARD # ensure Standard renderer (not Redshift) 336 self.settings[c4d.RDATA_FRAMESEQUENCE] = 3 # set range to preview 337 if self.alpha: 338 self.settings[c4d.RDATA_FORMAT] = 1023671 # Set to PNG 339 else: 340 self.settings[c4d.RDATA_FORMAT] = 1125 # set to MP4 341 self.settings[c4d.RDATA_ALPHACHANNEL] = False # set alpha channel 342 self.settings[c4d.RDATA_SAVEIMAGE] = False # set to not save image 343 344 def set_resolution(self, resolution): 345 """sets the resolution for the render 346 347 DreamTalk symbols default to square 1:1 aspect ratio for thumbnail compatibility. 348 DreamSong videos use 16:9 aspect ratio. 349 350 Square presets (DreamTalk symbols): 351 - "default": 1080x1080 (recommended for symbols) 352 - "low": 540x540 353 - "verylow": 270x270 354 - "high": 1440x1440 355 - "veryhigh": 2160x2160 (4K square) 356 357 Widescreen presets (DreamSong videos): 358 - "wide": 1920x1080 (1080p) 359 - "wide_low": 1280x720 (720p) 360 - "wide_high": 2560x1440 (1440p) 361 - "wide_veryhigh": 3840x2160 (4K) 362 """ 363 # Square presets (DreamTalk symbols - 1:1 aspect ratio) 364 if resolution == "verylow": 365 self.settings[c4d.RDATA_XRES] = 270 366 self.settings[c4d.RDATA_YRES] = 270 367 elif resolution == "low": 368 self.settings[c4d.RDATA_XRES] = 540 369 self.settings[c4d.RDATA_YRES] = 540 370 elif resolution == "default": 371 self.settings[c4d.RDATA_XRES] = 1080 372 self.settings[c4d.RDATA_YRES] = 1080 373 elif resolution == "high": 374 self.settings[c4d.RDATA_XRES] = 1440 375 self.settings[c4d.RDATA_YRES] = 1440 376 elif resolution == "veryhigh": 377 self.settings[c4d.RDATA_XRES] = 2160 378 self.settings[c4d.RDATA_YRES] = 2160 379 # Widescreen presets (DreamSong videos - 16:9 aspect ratio) 380 elif resolution == "wide_verylow": 381 self.settings[c4d.RDATA_XRES] = 640 382 self.settings[c4d.RDATA_YRES] = 360 383 elif resolution == "wide_low": 384 self.settings[c4d.RDATA_XRES] = 1280 385 self.settings[c4d.RDATA_YRES] = 720 386 elif resolution == "wide": 387 self.settings[c4d.RDATA_XRES] = 1920 388 self.settings[c4d.RDATA_YRES] = 1080 389 elif resolution == "wide_high": 390 self.settings[c4d.RDATA_XRES] = 2560 391 self.settings[c4d.RDATA_YRES] = 1440 392 elif resolution == "wide_veryhigh": 393 self.settings[c4d.RDATA_XRES] = 3840 394 self.settings[c4d.RDATA_YRES] = 2160 395 396 def set_sketch_settings(self): 397 """sets the sketch and toon settings""" 398 399 sketch_vp = c4d.documents.BaseVideoPost( 400 1011015) # add sketch render settings 401 # set parameters 402 sketch_vp[c4d.OUTLINEMAT_SHADING_BACK] = False # disable background color 403 sketch_vp[c4d.OUTLINEMAT_SHADING_OBJECT] = False # disable shading 404 # set independent of pixel units 405 sketch_vp[c4d.OUTLINEMAT_PIXELUNITS_INDEPENDENT] = True 406 # show lines in editor view 407 sketch_vp[c4d.OUTLINEMAT_EDLINES_SHOWLINES] = True 408 sketch_vp[c4d.OUTLINEMAT_EDLINES_LINE_DRAW] = 1 # 3D lines in editor 409 # set to custom mode 410 sketch_vp[c4d.OUTLINEMAT_PIXELUNITS_INDEPENDENT_MODE] = 1 411 sketch_vp[c4d.OUTLINEMAT_PIXELUNITS_BASEW] = 1080 # set custom width (square) 412 sketch_vp[c4d.OUTLINEMAT_PIXELUNITS_BASEH] = 1080 # set custom height (square) 413 sketch_vp[c4d.OUTLINEMAT_EDLINES_REDRAW_FULL] = True # redraw lines 414 sketch_vp[c4d.OUTLINEMAT_LINE_SPLINES] = True # enable splines 415 416 self.settings.InsertVideoPost( 417 sketch_vp) # insert sketch settings 418 419 420 class TwoDScene(Scene): 421 """a 2D scene uses a 2D camera setup""" 422 423 def set_camera(self): 424 self.camera = TwoDCamera(generator_mode=self.generator_mode) 425 # get basedraw of scene 426 bd = self.document.GetActiveBaseDraw() 427 # set camera of basedraw to scene camera 428 bd.SetSceneCamera(self.camera.camera.obj) 429 430 431 class ThreeDScene(Scene): 432 """a 3D scene uses a 3D camera setup""" 433 434 def set_camera(self): 435 self.camera = ThreeDCamera(generator_mode=self.generator_mode) 436 # get basedraw of scene 437 bd = self.document.GetActiveBaseDraw() 438 # set camera of basedraw to scene camera 439 bd.SetSceneCamera(self.camera.camera.obj)