/ legacy / scene_legacy.py
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)