/ docs / mograph-generator-rnd.md
mograph-generator-rnd.md
   1  # Holonic Architecture & MoGraph Integration (R&D)
   2  
   3  This document captures the emerging vision for DreamTalk's architecture: **Python Generators as the universal container for visual holons**, enabling recursive composition and MoGraph compatibility.
   4  
   5  ---
   6  
   7  ## The Unified Vision: Dropping Sketch & Toon Entirely
   8  
   9  ### The Radical Simplification
  10  
  11  We are **completely removing Sketch & Toon** from the DreamTalk universe. Everything becomes geometry + luminance materials, orchestrated by Python Generators.
  12  
  13  #### What We're Dropping
  14  - Sketch & Toon post-effect
  15  - Sketch materials
  16  - Sketch tags
  17  - All XPresso that drove Sketch parameters
  18  
  19  #### What Remains
  20  - **Luminance materials** for stroke color (already MoGraph-native via Color Shader/Fields)
  21  - **Geometry** for everything visible (strokes are swept splines or generated meshes)
  22  - **Python Generators** as the universal orchestrator
  23  
  24  #### Why This Works
  25  
  26  | Aspect | Old (Sketch & Toon) | New (Geometry Generators) |
  27  |--------|---------------------|---------------------------|
  28  | Render pipeline | Post-effect (separate pass) | Standard geometry render |
  29  | MoGraph compatibility | Material-level only | Fully per-clone |
  30  | Viewport feedback | Must render to see | Instant |
  31  | System complexity | Tags + Materials + XPresso | Just Python generators |
  32  | Performance | Baseline | 10-50x faster |
  33  | WebGL export | Impossible | Clean GLTF/USD |
  34  | Mental model | Multiple interacting systems | One unified system |
  35  
  36  ### MoGraph-Native Color and Opacity
  37  
  38  **Color**: MoGraph Multi Shader in the luminance channel provides per-clone color variation. ✅ VERIFIED
  39  
  40  ```python
  41  # Create MoGraph Multi Shader with color layers
  42  multi_shader = c4d.BaseShader(c4d.Xmgmultishader)  # ID: 1019397
  43  multi_shader[c4d.MGMULTISHADER_MODE] = c4d.MGMULTISHADER_MODE_INDEXRATIO
  44  
  45  # Add color shaders as layers
  46  colors = [c4d.Vector(1,0,0), c4d.Vector(1,1,0), c4d.Vector(0,1,0), c4d.Vector(0,0,1)]
  47  for i, color in enumerate(colors):
  48      color_shader = c4d.BaseShader(c4d.Xcolor)
  49      color_shader[c4d.COLORSHADER_COLOR] = color
  50      multi_shader.InsertShader(color_shader)
  51      multi_shader[c4d.DescID(c4d.DescLevel(c4d.MGMULTISHADER_LAYER_LINK + i))] = color_shader
  52  
  53  # Apply to luminance channel
  54  mat[c4d.MATERIAL_LUMINANCE_SHADER] = multi_shader
  55  ```
  56  
  57  **Alternative**: MoGraph Color Shader (`c4d.Xmgcolor`, ID: 1018767) reads from MoGraph Color Tags applied by effectors. Set to `MGCOLORSHADER_MODE_INDEXRATIO` to map clone index to a grayscale gradient.
  58  
  59  **Opacity**: MoGraph Multi Shader in the **Alpha channel** provides per-clone opacity variation. ✅ VERIFIED
  60  
  61  ```python
  62  # Enable Alpha channel on material
  63  mat[c4d.MATERIAL_USE_ALPHA] = True
  64  
  65  # Create Multi Shader with varying opacity levels
  66  alpha_multi = c4d.BaseShader(c4d.Xmgmultishader)
  67  alpha_multi[c4d.MGMULTISHADER_MODE] = c4d.MGMULTISHADER_MODE_INDEXRATIO
  68  
  69  # White = visible, Black = invisible
  70  opacities = [c4d.Vector(1,1,1), c4d.Vector(0.6,0.6,0.6), c4d.Vector(0.3,0.3,0.3)]
  71  for i, alpha in enumerate(opacities):
  72      color_shader = c4d.BaseShader(c4d.Xcolor)
  73      color_shader[c4d.COLORSHADER_COLOR] = alpha
  74      alpha_multi.InsertShader(color_shader)
  75      alpha_multi[c4d.DescID(c4d.DescLevel(c4d.MGMULTISHADER_LAYER_LINK + i))] = color_shader
  76  
  77  mat[c4d.MATERIAL_ALPHA_SHADER] = alpha_multi
  78  ```
  79  
  80  **Note**: Use the **Alpha channel**, not Transparency. Transparency channel doesn't work well with luminance-only materials.
  81  
  82  Both color and opacity are **fully MoGraph-native** when using standard materials on geometry.
  83  
  84  ### The DreamTalkStroke Generator
  85  
  86  A unified generator that replaces all Sketch & Toon functionality:
  87  
  88  ```
  89  DreamTalkStroke Generator
  90  ├── Input: Any C4D object (spline, mesh, or generator)
  91  ├── Detects type:
  92  │   ├── Spline → Sweep with profile, growth parameters
  93  │   ├── Mesh → Silhouette detection → Splines → Sweep
  94  │   └── Generator → Get cache, recurse
  95  ├── Output: Optimized stroke geometry
  96  │   ├── Camera-facing polygons only
  97  │   ├── LOD based on distance
  98  │   └── Luminance material (color/alpha from UserData)
  99  └── MoGraph: Re-evaluates per clone with unique op.GetMg()
 100  ```
 101  
 102  This becomes a **library primitive** - drop it on any object like you would a Sketch tag, but it outputs real geometry.
 103  
 104  ### The Holon as Single Source of Truth
 105  
 106  The Python Generator consolidates EVERYTHING:
 107  
 108  ```python
 109  class MindVirus(Holon):
 110      def specify_parts(self):
 111          self.cube = FoldableCube(...)
 112          self.cable = Cable(...)
 113  
 114      def specify_generator_code(self):
 115          return '''
 116  def main():
 117      fold = get_userdata("Fold")
 118      color = get_userdata("Color")
 119      opacity = get_userdata("Opacity")
 120  
 121      # Structural relationships
 122      set_child_rotation("FrontAxis", fold * PI/2)
 123  
 124      # Stroke generation (replaces Sketch & Toon)
 125      for child in get_children():
 126          stroke_geo = generate_stroke(child, camera, thickness=2)
 127          stroke_geo.set_color(color)
 128          stroke_geo.set_alpha(opacity)
 129  
 130      return None
 131  '''
 132  ```
 133  
 134  The generator handles:
 135  - **Structural relationships** (fold, position, scale)
 136  - **Visual rendering** (stroke geometry generation)
 137  - **Parameter interface** (UserData)
 138  - **Holonic composition** (parent/child relationships)
 139  
 140  ### Holarchy Flow
 141  
 142  UserData flows down through generators exactly as before:
 143  
 144  ```
 145  Cloner (position varies per clone)
 146    └── MindVirus Generator (reads position → fold, color, opacity)
 147          ├── FoldableCube Generator (receives fold → axes rotation)
 148          │     └── Stroke geometry (auto-generated, colored, alpha'd)
 149          └── Cable Generator (receives growth → spline length)
 150                └── Stroke geometry (auto-generated)
 151  ```
 152  
 153  Each level:
 154  1. Receives parameters from parent (or position from Cloner)
 155  2. Applies structural relationships
 156  3. Generates its own stroke geometry
 157  4. Passes relevant parameters to children
 158  
 159  ### The Secret Sauce
 160  
 161  **The Python Generator becomes the single point of truth.** Everything consolidates into code that is:
 162  
 163  - **Git-friendly**: Plain text Python, no binary XPresso or material data
 164  - **AI-readable/writable**: Claude can understand and modify the entire system
 165  - **MoGraph-native**: Generators re-evaluate per-clone with unique transforms
 166  - **Performant**: 10-50x faster than Sketch & Toon
 167  - **Exportable**: Real geometry exports cleanly to GLTF/USD/WebGL
 168  
 169  Sketch & Toon was always a bolted-on post-effect trying to fake something that should have been geometry. We're just... making it geometry.
 170  
 171  ---
 172  
 173  ## Technical Deep-Dive: Why Sketch & Toon Fails
 174  
 175  ### The Problem with Sketch & Toon
 176  
 177  Sketch & Toon is a **post-effect** - it renders lines in 2D screen space after the 3D scene is computed. This creates fundamental limitations:
 178  
 179  1. **Not MoGraph-native**: Material properties (Draw, Color, Opacity) are document-level, not per-clone
 180  2. **No 3D geometry**: Lines exist only in the render, can't be morphed, swept, or used as real splines
 181  3. **Flickering/popping**: Post-effect nature causes instability during animation
 182  4. **Conflicts with other effects**: DOF, motion blur, reflections can break or conflict
 183  5. **Not real-time**: Requires full render pass, incompatible with viewport/WebGL workflows
 184  
 185  ### The Solution: Geometry-Based Line Rendering
 186  
 187  **Core insight**: Replace post-effect line drawing with **real 3D spline geometry** that gets swept into visible strokes.
 188  
 189  #### The Stroke Generator Approach
 190  
 191  All stroke rendering is handled by Python Generators that output optimized geometry directly:
 192  
 193  | Input Type | Generator Behavior |
 194  |------------|-------------------|
 195  | **Spline** | Creates camera-facing ribbon/tube polygons along spline path |
 196  | **Mesh** | Detects silhouette edges from camera perspective, creates stroke geometry |
 197  | **Generator** | Gets cache, recurses on children |
 198  
 199  Key properties:
 200  - **Draw animation**: Control via polygon count or partial geometry generation
 201  - **MoGraph compatible**: Generator re-evaluates per-clone with unique `op.GetMg()`
 202  - **Camera-relative**: Updates as camera moves (for silhouette detection)
 203  - **Viewport visible**: Real geometry renders instantly
 204  - **No intermediate objects**: No Sweep NURBS, no Sketch tags - generator outputs final geometry
 205  
 206  #### Spline-to-Stroke Generator
 207  
 208  For spline inputs (Circle, Line, Arc, etc.), the generator creates camera-facing stroke geometry:
 209  
 210  ```python
 211  def main():
 212      camera = get_active_camera()
 213      spline = op.GetDown()  # Child spline
 214  
 215      # Get spline points
 216      points = spline.GetAllPoints()
 217  
 218      # For each segment, create camera-facing quad
 219      stroke_polys = []
 220      for i in range(len(points) - 1):
 221          p1, p2 = points[i], points[i+1]
 222          # Calculate perpendicular direction facing camera
 223          to_cam = (camera.GetMg().off - p1).GetNormalized()
 224          tangent = (p2 - p1).GetNormalized()
 225          perp = tangent.Cross(to_cam).GetNormalized() * stroke_width
 226  
 227          # Create quad facing camera
 228          stroke_polys.append(create_quad(p1 - perp, p1 + perp, p2 + perp, p2 - perp))
 229  
 230      return build_polygon_object(stroke_polys)
 231  ```
 232  
 233  #### Mesh-to-Silhouette Generator
 234  
 235  For mesh inputs, detects silhouette edges and creates stroke geometry (similar to [Insydium MeshTools mtEdgeSpline](https://insydium.ltd/products/meshtools/)):
 236  
 237  ```python
 238  def main():
 239      camera = get_active_camera()
 240      mesh = op.GetDown().GetCache()
 241      cam_pos = camera.GetMg().off
 242  
 243      polys = mesh.GetAllPolygons()
 244      points = mesh.GetAllPoints()
 245  
 246      # Classify each face as front/back facing
 247      face_facing = []
 248      for poly in polys:
 249          center = get_poly_center(poly, points)
 250          normal = get_poly_normal(poly, points)
 251          view_dir = (cam_pos - center).GetNormalized()
 252          face_facing.append(normal.Dot(view_dir) > 0)
 253  
 254      # Find silhouette edges (where front meets back)
 255      silhouette_edges = []
 256      for edge in get_edges(mesh):
 257          face_a, face_b = get_adjacent_faces(edge)
 258          if face_facing[face_a] != face_facing[face_b]:
 259              silhouette_edges.append(edge)
 260  
 261      # Convert edges to stroke geometry
 262      return edges_to_stroke_geometry(silhouette_edges, stroke_width, cam_pos)
 263  ```
 264  
 265  **Algorithm**:
 266  1. Get camera position
 267  2. Calculate face normals, dot with view direction → front/back classification
 268  3. Edges where adjacent faces differ = silhouette
 269  4. Output as PolygonObject (camera-facing stroke geometry)
 270  
 271  **Properties**:
 272  - ✅ Real 3D geometry (morphable, clonable)
 273  - ✅ MoGraph compatible (generator re-evaluates per clone)
 274  - ✅ Camera-relative (updates as camera moves)
 275  - ✅ Viewport visible (real geometry, not post-effect)
 276  - ✅ Draw animation via partial geometry generation
 277  
 278  ### The DreamTalk Plugin Vision
 279  
 280  Eventually, consolidate all Cinema 4D integration into a **DreamTalk Companion Plugin**:
 281  
 282  ```
 283  DreamTalk Plugin
 284  ├── MCP Server (current implementation, for AI communication)
 285  ├── Silhouette Generator (camera-relative outline splines)
 286  ├── Optimized Primitives (pre-built sweep-based strokes)
 287  └── DreamNode Loader (import symbols directly from git repos)
 288  ```
 289  
 290  This ships WITH DreamTalk as a submodule, not as a separate purchase. The goal: **everything needed for the DreamTalk aesthetic without external plugin dependencies**.
 291  
 292  ### Real-Time Rendering Path
 293  
 294  For increasingly real-time workflows while still in Cinema 4D:
 295  
 296  | Method | Speed | Quality | Use Case |
 297  |--------|-------|---------|----------|
 298  | Viewport (OpenGL) | ~60fps | Low | Positioning, timing |
 299  | Interactive Render Region | ~2-10fps | Medium | Lighting, material tweaks |
 300  | Full Render | Seconds/frame | High | Final output |
 301  
 302  **Key optimizations for IRR**:
 303  - Reduce Anti-Aliasing to Geometry mode
 304  - Disable Global Illumination during iteration
 305  - Use render regions to focus on specific areas
 306  - Bake complex simulations to keyframes
 307  
 308  ### WebGL Migration Path
 309  
 310  The geometry-based approach directly enables WebGL/Three.js migration:
 311  
 312  ```
 313  DreamTalk Symbol
 314    └── Python Generator (C4D)
 315          └── Outputs splines + sweep geometry
 316                └── Export as GLTF/USD
 317                      └── Load in Three.js/WebGL
 318  ```
 319  
 320  Since strokes are real geometry (not post-effects), they export cleanly. The same mathematical definitions that drive the C4D generators can eventually drive WebGL equivalents directly.
 321  
 322  ### Performance Breakdown: Sketch & Toon vs Geometry-Based Strokes
 323  
 324  #### How Each Pipeline Works
 325  
 326  **Sketch & Toon Pipeline** (per frame):
 327  ```
 328  1. Render full 3D scene to depth/normal buffers
 329  2. Edge detection pass (image-space, ALL visible edges)
 330  3. Line tracing (convert detected edges to vector strokes)
 331  4. Stroke rendering (apply thickness, textures, effects)
 332  5. Composite onto final image
 333  ```
 334  Cost scales with: **Screen resolution × edge complexity × stroke effects**
 335  
 336  The killer: Steps 2-4 happen in a black box. Every edge in the scene is evaluated, even ones you don't care about.
 337  
 338  **Geometry-Based Pipeline** (Silhouette Generator, per frame):
 339  ```
 340  1. Get camera position (trivial)
 341  2. For each polygon: dot(normal, view_dir) → front/back (N polygons × 1 dot product)
 342  3. For each edge: check if adjacent faces differ (E edges × 1 comparison)
 343  4. Build spline from silhouette edges (S silhouette points)
 344  5. Sweep renders as normal geometry
 345  ```
 346  Cost scales with: **Polygon count of SOURCE mesh** (not screen resolution)
 347  
 348  #### Scaling Characteristics
 349  
 350  | Factor | Sketch & Toon | Geometry Silhouette |
 351  |--------|---------------|---------------------|
 352  | **Screen resolution** | Linear cost increase | No impact |
 353  | **Source mesh complexity** | Exponential (traces ALL segments) | Linear (just dot products) |
 354  | **Number of objects** | Multiplicative | Additive (parallelizable) |
 355  | **Stroke effects** | Each effect = another pass | One-time geometry, material handles rest |
 356  | **MoGraph clones** | Same material = same render cost | Each clone = independent geometry |
 357  | **Viewport preview** | Requires render | Native viewport display |
 358  
 359  #### Concrete Estimates
 360  
 361  **Simple symbol** (e.g., FoldableCube, ~100 polygons):
 362  
 363  | Aspect | Sketch & Toon | Geometry Silhouette |
 364  |--------|---------------|---------------------|
 365  | Silhouette calculation | ~5-20ms (hidden in render) | <1ms (100 dot products) |
 366  | Render time per frame | 50-500ms | 5-20ms (just geometry) |
 367  | Viewport feedback | None (must render) | Instant |
 368  
 369  **Complex scene** (e.g., 50 MindViruses in Cloner, ~5000 polygons each):
 370  
 371  | Aspect | Sketch & Toon | Geometry Silhouette |
 372  |--------|---------------|---------------------|
 373  | Edge detection | Must process 250K polygons worth of screen edges | 50 generators × 5K dot products each |
 374  | Cloner behavior | All clones share material (no per-clone variation) | Each clone independent |
 375  | Total render | 2-10 seconds | 100-500ms |
 376  
 377  #### Camera-Optimal Geometry (Advanced Optimization)
 378  
 379  Beyond basic silhouette-to-sweep, a smarter approach:
 380  
 381  ```
 382  Silhouette Generator outputs splines
 383 384  Stroke Generator takes splines + camera
 385 386  Outputs MINIMAL geometry that looks like the stroke
 387      (only polygons facing camera, optimal subdivision)
 388 389  Standard material render (luminance = stroke color)
 390  ```
 391  
 392  This is **camera-optimal geometry** - generating only the polygons that will actually be visible from the current camera angle (like game engine billboard/impostor rendering).
 393  
 394  Additional savings:
 395  - Backface culling is "free" - you never generate backfaces
 396  - LOD is automatic - distant strokes get fewer subdivisions
 397  - GPU handles final render natively
 398  
 399  #### Implementation Tiers
 400  
 401  | Scenario | Sketch & Toon | Geometry (Python) | Geometry (C++) |
 402  |----------|---------------|-------------------|----------------|
 403  | Simple symbol | 50-500ms | 5-20ms | <5ms |
 404  | Complex scene | 2-10s | 100-500ms | 20-100ms |
 405  | MoGraph 100 clones | Same cost (shared mat) | 100× single cost | 100× single cost |
 406  | Real-time viable | ❌ | Marginal | ✅ |
 407  | WebGL exportable | ❌ | ✅ | ✅ |
 408  
 409  **Bottom line**: Geometry approach is **10-50x faster** for typical DreamTalk use cases, with MoGraph compatibility, viewport preview, and export capability as bonuses.
 410  
 411  ### C++ Plugin Translation Layer (Future)
 412  
 413  The ultimate optimization: compile DreamTalk holons to native C4D ObjectData plugins.
 414  
 415  ```
 416  DreamTalk Python                    Compiled Plugin
 417  ─────────────────                   ───────────────
 418  class MindVirus(Holon)      →       MindVirus ObjectData (C++)
 419    - specify_parts()         →         Pre-built child structure
 420    - specify_generator_code()→         Compiled generator logic
 421    - UserData parameters     →         Native C4D parameters
 422  ```
 423  
 424  Benefits:
 425  - **10-100x performance** on generator evaluation
 426  - **Native C4D integration** (appears in object menu, has icon)
 427  - **Distributable** to other C4D users without Python
 428  - **Still AI-readable** - DreamTalk Python remains source of truth
 429  
 430  This creates a path where DreamTalk symbols can be "published" as first-class C4D objects, useful both for our own complex scenes and potentially for the broader C4D plugin ecosystem.
 431  
 432  ---
 433  
 434  ## The Vision
 435  
 436  ### DreamTalk = Visual Holons
 437  
 438  A **holon** is something that is simultaneously a whole unto itself AND a part of a larger whole. This is the essence of DreamTalk:
 439  
 440  - A `Circle` is a holon (complete primitive, can be used alone)
 441  - A `FoldableCube` is a holon (composed of face holons, but complete in itself)
 442  - A `MindVirus` is a holon (composed of FoldableCubes, but a sovereign symbol)
 443  - A `Labyrinth` is a holon (composed of MindViruses arranged in space)
 444  
 445  Each level is **sovereign** - it knows how to be itself, exposes meaningful parameters, and can dance with other holons to form higher-level compositions.
 446  
 447  ### The Architectural Principle
 448  
 449  ```
 450  DreamNode (git repo)
 451    └── Symbol.py (Python file)
 452          └── class Symbol(Holon)
 453                └── generates → Python Generator (in C4D)
 454                      ├── UserData (meaningful parameters)
 455                      ├── Python code (structural relationships)
 456                      └── Children (primitives or nested holons)
 457  ```
 458  
 459  **Every non-trivial DreamTalk symbol becomes a Python Generator in Cinema 4D.**
 460  
 461  This mirrors the holonic structure:
 462  - The **Python Generator** IS the holon in C4D space
 463  - **UserData** exposes the holon's meaningful parameters
 464  - **Python code** orchestrates relationships to children
 465  - **Children** can be primitives OR other Python Generators (nested holons)
 466  
 467  ### What This Replaces
 468  
 469  | Old Pattern | New Pattern |
 470  |-------------|-------------|
 471  | Null + XPresso tag | Python Generator |
 472  | XPresso node graph | Python code in generator |
 473  | Per-object XPresso tags | Consolidated into parent generator |
 474  | Binary XPresso data | Plain text Python (git-friendly) |
 475  | Object references (break on clone) | Position-based calculation (per-clone) |
 476  
 477  ### Benefits
 478  
 479  1. **MoGraph Compatible**: Generators re-evaluate per clone with unique `op.GetMg()`
 480  2. **AI-Native**: Claude can read/write all relationship logic
 481  3. **Version Controlled**: No binary XPresso blobs
 482  4. **Recursively Composable**: Generators containing generators = holarchy
 483  5. **Visually Clear**: Python Generator icon instantly identifies holons in Object Manager
 484  
 485  ## Separation of Concerns
 486  
 487  ### Structural Relationships → Python Generator
 488  
 489  Things that define the **shape/structure** based on parameters:
 490  - Fold angle → axis rotations
 491  - Scale → child sizing
 492  - Configuration variants → child visibility
 493  
 494  These live in the generator's `main()` function and execute procedurally.
 495  
 496  ### Temporal Animations → DreamTalk Library Methods
 497  
 498  Things that **change over time** for animation:
 499  - Create/Draw animations
 500  - Movement sequences
 501  - Morphing between states
 502  
 503  These are Python methods that **generate keyframes** - they don't need to run per-frame, just once to set up the animation.
 504  
 505  ```python
 506  # Structural (in generator code)
 507  def main():
 508      fold = op[FOLD_ID]
 509      child.SetRelRot(c4d.Vector(0, 0, fold * PI/2))
 510      return None
 511  
 512  # Temporal (in DreamTalk library)
 513  def animate_fold(self, start=0, end=1, duration=30):
 514      self.keyframe(self.fold, start, frame=0)
 515      self.keyframe(self.fold, end, frame=duration)
 516  ```
 517  
 518  ## The Primitive Question
 519  
 520  Current state: Primitives (Circle, Cube, etc.) have their own Sketch tags and XPresso tags for visibility/material control.
 521  
 522  **Question**: Should primitives also become generators? Or stay as raw C4D objects?
 523  
 524  **Current thinking**: Primitives stay as raw objects. They are the **atoms** - they don't need sovereignty because they're not meaningful units on their own. A circle is just a circle. But a `MindVirus` is a *concept* - it deserves holon status.
 525  
 526  The **holon boundary** is: "Does this object represent a meaningful, sovereign concept that could be its own DreamNode?"
 527  
 528  - Yes → Python Generator (holon)
 529  - No → Raw C4D object (atom)
 530  
 531  ### Visibility & Material Control
 532  
 533  With XPresso gone, how do we handle visibility inheritance and material assignment?
 534  
 535  **Option A**: Python Tags on primitives (one tag per object)
 536  **Option B**: Parent generator controls everything (consolidated)
 537  **Option C**: Hybrid - generator handles structure, minimal tags for rendering concerns
 538  
 539  This needs exploration. The goal is minimal friction while maintaining control.
 540  
 541  ## MoGraph Integration
 542  
 543  ### The Core Discovery
 544  
 545  A Python Generator inside a MoGraph Cloner:
 546  1. Is executed **separately for each clone**
 547  2. Has `op.GetMg()` return the **clone's unique world position**
 548  3. Can vary parameters based on position (or field sampling)
 549  
 550  This means: **Generator-based holons automatically work with MoGraph.**
 551  
 552  ### Critical Settings
 553  
 554  ```python
 555  # In generator code or when creating:
 556  op[c4d.OPYTHON_OPTIMIZE] = False  # MUST disable cache optimization
 557  ```
 558  
 559  Without this, the generator caches output and all clones look identical.
 560  
 561  ### What Works (Verified)
 562  
 563  | Feature | Status | Notes |
 564  |---------|--------|-------|
 565  | `return None` with children | ✅ | Children visible per-clone |
 566  | Position-based variation | ✅ | `op.GetMg()` gives unique clone position |
 567  | External object lookup | ✅ | Can find fields/nulls via `doc.SearchObject()` |
 568  | Distance-based falloff | ✅ | Calculate influence from any reference point |
 569  | Dynamic response | ✅ | Moving field updates all clones in real-time |
 570  
 571  ### What Doesn't Work
 572  
 573  | Feature | Status | Notes |
 574  |---------|--------|-------|
 575  | Effector-modified transforms | ❌ | Generator sees PRE-effector position |
 576  | Direct MoData access | ⚠️ | Needs investigation |
 577  
 578  **Key insight**: Generators execute BEFORE effectors. `op.GetMg()` returns the Cloner's arrangement position, not the post-effector position. For effector-driven parameters, consider using:
 579  - A Python Effector instead (has full MoData access)
 580  - Store effector values in a shared object that generators read
 581  - Use Fields directly (generators can sample field positions)
 582  
 583  ### Cloner Setup
 584  
 585  ```python
 586  cloner = c4d.BaseObject(c4d.Omgcloner)
 587  cloner[c4d.ID_MG_MOTIONGENERATOR_MODE] = 1  # Linear mode
 588  cloner[c4d.MG_LINEAR_COUNT] = 5
 589  cloner[c4d.MG_LINEAR_OBJECT_POSITION] = c4d.Vector(100, 0, 0)
 590  
 591  generator.InsertUnder(cloner)
 592  ```
 593  
 594  ### Generator-as-Controller Pattern
 595  
 596  The generator **modifies its children** rather than generating geometry:
 597  
 598  ```python
 599  def main():
 600      # Get unique position (varies per clone)
 601      mg = op.GetMg()
 602      x = mg.off.x
 603  
 604      # Derive parameter from position
 605      fold = min(1.0, max(0.0, x / 600.0))
 606  
 607      # Modify children
 608      child = op.GetDown()
 609      while child:
 610          if child.GetName() == "LeftAxis":
 611              child.SetRelRot(c4d.Vector(0, 0, fold * PI/2))
 612          child = child.GetNext()
 613  
 614      return None  # Children ARE the output
 615  ```
 616  
 617  **Key insight**: `return None` means "my children are visible" - the generator acts as controller, not geometry source.
 618  
 619  ## Implementation Roadmap
 620  
 621  ### Phase 1: MoGraph Integration ✅ COMPLETE
 622  
 623  **Goal**: Full compatibility with Cinema 4D's MoGraph system - Cloners, Effectors, Fields.
 624  
 625  - [x] Test minimal `return None` generator in Cloner
 626  - [x] Verify children remain visible per-clone
 627  - [x] Confirm position-based parameter variation works
 628  - [x] Test Field sampling from generator code (distance-based falloff works)
 629  - [x] Test Effector influence - generators see pre-effector positions (limitation documented)
 630  - [x] Document the complete MoGraph workflow
 631  
 632  **Key discovery**: Generators execute BEFORE effectors, so use Fields for spatial influence instead.
 633  
 634  ### Phase 2: Nested Holons ✅ COMPLETE
 635  
 636  **Goal**: Prove recursive composition works - a generator containing generators.
 637  
 638  - [x] Create simple two-level test (CubeTriad containing 3 FoldableCubes)
 639  - [x] Verify parent can pass parameters to child generators via UserData
 640  - [x] Test in Cloner - nested generators re-evaluate per-clone ✅
 641  
 642  **Results**: Full holarchic pattern verified:
 643  ```
 644  Cloner
 645    └── CubeTriad (parent generator) - reads Y position
 646          ├── Cube1 (child generator) - receives fold value
 647          ├── Cube2 (child generator) - receives fold value
 648          └── Cube3 (child generator) - receives fold value
 649  ```
 650  
 651  Each clone gets unique position → parent calculates fold → passes to all children → children apply to their structure. Three levels of hierarchy working together.
 652  
 653  ### Phase 3: Primitive Handling ✅ COMPLETE (Superseded)
 654  
 655  **Original findings** led to the unified vision documented above. Key insight: Sketch & Toon materials are not per-clone in MoGraph, which led us to abandon Sketch & Toon entirely in favor of geometry-based strokes.
 656  
 657  **New approach** (see "The Unified Vision" section):
 658  - Generators directly output optimized stroke geometry (no Sweep NURBS intermediate)
 659  - Spline → Generator creates camera-facing ribbon/tube polygons
 660  - Mesh → Generator detects silhouette edges, creates stroke geometry
 661  - Draw animation via geometry point count or visibility
 662  - Color/Opacity via MoGraph Multi Shader or Fields on Luminance material
 663  - No Sketch & Toon, no XPresso
 664  
 665  ### Phase 4: Library Refactor
 666  
 667  **Goal**: Update DreamTalk core to default to generator-based holons.
 668  
 669  - [ ] Rename `CustomObject` → `Holon` (or add alias)
 670  - [ ] Make `generator_mode=True` the default
 671  - [ ] Remove/deprecate XPresso-based relationship system
 672  - [ ] Update `GeneratorMixin` to handle all current relationship patterns
 673  - [ ] Ensure animation methods still work (keyframe generation)
 674  
 675  ## Technical Reference
 676  
 677  ### Python Generator Object
 678  
 679  - Type ID: `1023866`
 680  - Code storage: `c4d.OPYTHON_CODE`
 681  - Cache optimization: `c4d.OPYTHON_OPTIMIZE` (set to `False` for MoGraph)
 682  
 683  ### Key Generator Patterns
 684  
 685  **Read UserData:**
 686  ```python
 687  fold = op[c4d.DescID(
 688      c4d.DescLevel(c4d.ID_USERDATA, c4d.DTYPE_SUBCONTAINER, 0),
 689      c4d.DescLevel(1, c4d.DTYPE_REAL, 0)  # UserData slot 1
 690  )]
 691  ```
 692  
 693  **Modify child rotation:**
 694  ```python
 695  child.SetRelRot(c4d.Vector(angle_x, angle_y, angle_z))
 696  ```
 697  
 698  **Get clone position:**
 699  ```python
 700  mg = op.GetMg()
 701  world_pos = mg.off  # c4d.Vector
 702  ```
 703  
 704  **Pass value to child generator's UserData (by name):**
 705  ```python
 706  def set_userdata_by_name(obj, param_name, value):
 707      """Set UserData value by parameter name."""
 708      ud = obj.GetUserDataContainer()
 709      for desc_id, bc in ud:
 710          if bc[c4d.DESC_NAME] == param_name:
 711              obj[desc_id] = value
 712              return True
 713      return False
 714  
 715  # In parent generator's main():
 716  child = op.GetDown()
 717  while child:
 718      set_userdata_by_name(child, "Fold", fold_value)
 719      child = child.GetNext()
 720  ```
 721  
 722  ### MoGraph Cloner Modes
 723  
 724  | Mode | Value | Use Case |
 725  |------|-------|----------|
 726  | Object | 0 | Clone onto object surface |
 727  | Linear | 1 | Line of clones |
 728  | Radial | 2 | Circular arrangement |
 729  | Grid | 3 | 3D grid |
 730  | Honeycomb | 4 | Hexagonal pattern |
 731  
 732  ## Open Questions
 733  
 734  1. **Visibility inheritance**: How do we elegantly handle "hide parent hides children" without XPresso?
 735     - Generator can set `child[c4d.ID_BASEOBJECT_VISIBILITY_EDITOR]` directly
 736     - Needs testing to confirm it cascades properly
 737  
 738  2. ~~**Material assignment**: Currently XPresso drives Sketch tag parameters. What replaces this?~~
 739     - **ANSWERED**: Generator directly modifies Sketch material for standalone use
 740     - For MoGraph per-clone: use Fields + Shader Effector
 741  
 742  3. **Performance**: With deep holarchies, does generator nesting cause performance issues?
 743     - Early tests with 3-level hierarchy (Cloner→Parent→Children) showed no issues
 744     - Needs stress testing with larger hierarchies
 745  
 746  4. **Animation keyframes**: Can we keyframe UserData on generators the same way we do on Nulls?
 747     - Should work - generators are just BaseObjects with UserData
 748     - Needs verification
 749  
 750  5. **Editor visibility**: Does `return None` preserve Object Manager editability of children in all contexts?
 751     - Verified: Children remain selectable and editable in Object Manager
 752     - Cloner context: children are virtual but template is editable
 753  
 754  ## Session Log
 755  
 756  ### 2025-01-10: Library Audit - Sketch & Toon / XPresso References
 757  
 758  **Summary**: 153 Sketch refs across 13 files, 70 XPresso refs across 20 files.
 759  
 760  #### Files to Refactor (Core Library)
 761  
 762  | File | Sketch | XPresso | Purpose | Refactor Strategy |
 763  |------|--------|---------|---------|-------------------|
 764  | `objects/abstract_objects.py` | 76 | 16 | Base classes (LineObject, SolidObject) | Replace SketchMaterial/Tag with StrokeGen |
 765  | `scene.py` | 16 | 0 | Scene setup, render settings | Remove Sketch VideoPost, use Standard renderer |
 766  | `materials.py` | 2 | 0 | SketchMaterial class | Replace with luminance material helper |
 767  | `tags.py` | 3 | 2 | SketchTag, XPressoTag classes | Remove SketchTag, keep XPressoTag for legacy |
 768  | `animation/sketch_animators.py` | 3 | 0 | Draw/Complete animators | Replace with geometry-based animation |
 769  | `objects/sketch_objects.py` | 19 | 2 | Sketch-specific objects | Remove entire file |
 770  | `xpresso/` directory | 0 | 5+ | XPresso system | Keep for legacy, deprecate in favor of generators |
 771  | `introspection/hierarchy.py` | 14 | 9 | Scene analysis | Update to detect generators instead of XPresso |
 772  
 773  #### Key Classes to Replace
 774  
 775  1. **SketchMaterial** (`materials.py:37-89`)
 776     - Creates Sketch & Toon material (ID 1011014)
 777     - Replace with: Luminance material + MoGraph Multi Shader for color/opacity
 778  
 779  2. **SketchTag** (`tags.py:54-128`)
 780     - Creates Sketch Style tag (ID 1011012)
 781     - Replace with: StrokeGen Python Generator
 782  
 783  3. **LineObject.set_sketch_material()** (`objects/abstract_objects.py`)
 784     - Sets up Sketch material/tag on splines
 785     - Replace with: Wrap in StrokeGen
 786  
 787  4. **SolidObject.sketch_material/sketch_tag** (`objects/abstract_objects.py`)
 788     - Sketch rendering for 3D objects
 789     - Replace with: Wrap in SilhouetteSplineGen → StrokeGen
 790  
 791  5. **Scene.set_sketch_settings()** (`scene.py:376-397`)
 792     - Adds Sketch VideoPost to render settings
 793     - Replace with: Remove entirely (geometry renders with Standard)
 794  
 795  #### XPresso Patterns to Replace
 796  
 797  1. **XPressoTag** (`tags.py:146-168`) - Keep for legacy, deprecate
 798  2. **XIdentity/XRelation** (`xpresso/xpressions.py`) - Replace with generator code
 799  3. **specify_relations()** pattern - Replace with `specify_generator_code()`
 800  4. **Parameter linking** - Replace with UserData + generator reads
 801  
 802  #### Refactor Order (Recommended)
 803  
 804  1. **Phase 1: New Primitives**
 805     - Add `StrokeGen` class that wraps any spline child
 806     - Add `SilhouetteSplineGen` class that converts mesh to silhouette spline
 807     - These become new library primitives alongside Circle, Cube, etc.
 808  
 809  2. **Phase 2: Deprecate Old System**
 810     - Add `generator_mode=True` as default on CustomObject
 811     - Make SketchMaterial/SketchTag raise deprecation warnings
 812     - Update LineObject/SolidObject to use StrokeGen when `generator_mode=True`
 813  
 814  3. **Phase 3: Remove Old System**
 815     - Delete `materials.py:SketchMaterial`
 816     - Delete `tags.py:SketchTag`
 817     - Delete `animation/sketch_animators.py`
 818     - Delete `objects/sketch_objects.py`
 819     - Remove Sketch VideoPost from `scene.py`
 820  
 821  4. **Phase 4: XPresso Cleanup**
 822     - Move `xpresso/` to `xpresso_legacy/`
 823     - Update all holons to use generator code
 824     - Remove XPresso import from `imports.py`
 825  
 826  ### 2025-01-10: Two-Layer Stroke Architecture ✅ VERIFIED
 827  
 828  **The Architecture:**
 829  
 830  ```
 831  Layer 1: SilhouetteSplineGen
 832  ├── Input: Mesh + Camera perspective
 833  ├── Output: MoGraph-compatible SplineObject
 834  └── Use: Can feed into MoSpline, Spline Effector, morphing, etc.
 835  
 836  Layer 2: StrokeGen (Universal)
 837  ├── Input: ANY spline (Circle, Arc, SilhouetteSpline, etc.)
 838  ├── Output: Camera-facing polygon geometry
 839  └── Properties: Optimized quads, always faces camera, MoGraph compatible
 840  ```
 841  
 842  **Why Two Layers:**
 843  1. Silhouette detection outputs a **spline** (not geometry) so it can be manipulated by C4D's native spline tools (MoSpline, Spline Effector, morphing)
 844  2. The stroke generator is **universal** - works on any spline, not just silhouettes
 845  3. Separation of concerns: silhouette detection vs stroke rendering
 846  
 847  **Working Code - SilhouetteSplineGen:**
 848  ```python
 849  def main():
 850      """Mesh → Silhouette Spline. MoGraph compatible."""
 851      # Get camera and generator world matrix
 852      cam_world = cam.GetMg().off
 853      gen_mg = op.GetMg()  # Unique per clone in MoGraph!
 854  
 855      # Get mesh points in world space
 856      child_ml = child.GetMl()
 857      world_points = [gen_mg * (child_ml * p) for p in mesh.GetAllPoints()]
 858  
 859      # Classify faces as front/back facing
 860      for poly in polys:
 861          normal = calculate_normal(poly, world_points)
 862          view_dir = (cam_world - center).GetNormalized()
 863          face_facing.append(normal.Dot(view_dir) > 0)
 864  
 865      # Find silhouette edges (where front meets back)
 866      for edge, faces in edge_faces.items():
 867          if face_facing[faces[0]] != face_facing[faces[1]]:
 868              silhouette_edges.append(edge)
 869  
 870      # Output as multi-segment spline (each edge = segment)
 871      spline = c4d.SplineObject(point_count, c4d.SPLINETYPE_LINEAR)
 872      spline.ResizeObject(point_count, num_segments)
 873      # Transform back to generator local space
 874      for edge in silhouette_edges:
 875          spline.SetSegment(i, 2, False)  # 2 points, not closed
 876  
 877      return spline
 878  ```
 879  
 880  **Working Code - StrokeGen (Universal Spline → Geometry):**
 881  ```python
 882  def main():
 883      """Any Spline → Camera-facing stroke geometry. Universal."""
 884      stroke_width = get_userdata("Stroke Width", default=3.0)
 885      cam_world = cam.GetMg().off
 886      gen_mg = op.GetMg()
 887  
 888      # Handle both SplineObject and LineObject (cache type)
 889      spline = child.GetCache() or child
 890      is_spline = spline.IsInstanceOf(c4d.Ospline) or spline.GetType() == 5137
 891  
 892      # Transform points to world space
 893      world_points = [gen_mg * (child_ml * p) for p in spline.GetAllPoints()]
 894  
 895      # Handle segmented splines (silhouette output has multiple segments)
 896      for segment in segments:
 897          for i in range(num_edges):
 898              p1, p2 = segment[i], segment[(i+1) % len(segment)]
 899  
 900              # Calculate camera-facing perpendicular
 901              tangent = (p2 - p1).GetNormalized()
 902              to_cam = (cam_world - midpoint).GetNormalized()
 903              perp = tangent.Cross(to_cam).GetNormalized() * stroke_width
 904  
 905              # Create quad in generator local space
 906              gen_mg_inv = ~gen_mg
 907              q0 = gen_mg_inv * (p1 - perp)
 908              q1 = gen_mg_inv * (p1 + perp)
 909              q2 = gen_mg_inv * (p2 + perp)
 910              q3 = gen_mg_inv * (p2 - perp)
 911  
 912              stroke_polys.append(quad)
 913  
 914      return PolygonObject(stroke_points, stroke_polys)
 915  ```
 916  
 917  **Key Implementation Details:**
 918  
 919  1. **LineObject (type 5137)** is the cache type for spline primitives - must check for this alongside `IsInstanceOf(c4d.Ospline)`
 920  
 921  2. **Closed spline detection** - for primitives like Circle, check type ID:
 922     ```python
 923     if child.GetType() in [5181, 5176, 5180, ...]:  # Circle, Flower, 4-Side, etc.
 924         is_closed = True
 925     ```
 926  
 927  3. **Multi-segment splines** - silhouette output has multiple segments (one per edge), must iterate:
 928     ```python
 929     for seg_i in range(spline.GetSegmentCount()):
 930         seg_info = spline.GetSegment(seg_i)
 931         # Process segment
 932     ```
 933  
 934  4. **Coordinate spaces** - the key insight:
 935     - Do silhouette detection in WORLD space (for camera perspective)
 936     - Output geometry in GENERATOR LOCAL space (so MoGraph transforms work)
 937     - Use `gen_mg_inv = ~gen_mg` to transform back
 938  
 939  **Verified in MoGraph Cloner:**
 940  - 3x3 grid of icosahedra
 941  - Each with unique perspective-correct silhouettes
 942  - Full pipeline: Cloner > StrokeGen > SilhouetteSplineGen > Mesh
 943  - ~15ms viewport preview for 9 objects
 944  
 945  ### 2025-01-10: Phase 3 - Primitive Handling
 946  **Key discoveries:**
 947  - XPresso on primitives does NOT work inside generators (XPresso never re-evaluates)
 948  - Generator CAN directly modify Sketch material parameters (Draw, Color, Opacity)
 949  - Material modifications are document-level - affect ALL clones, not per-clone
 950  - Structural modifications (rotation, position) ARE per-clone
 951  
 952  **Tested patterns:**
 953  - Minimal generator with circle child + Sketch tag → WORKS in Cloner
 954  - Generator modifying Sketch tag's Complete parameter → Tag doesn't update per-clone
 955  - Generator creating unique materials per clone → Materials can't be inserted from generator code
 956  
 957  **Decision made:**
 958  - Primitives stay as raw C4D objects (atoms, not holons)
 959  - NO XPresso on primitives (doesn't work in generator context)
 960  - Parent generator directly controls Sketch material for standalone use
 961  - For MoGraph per-clone material variation: use Fields + Shader Effector (not generator code)
 962  
 963  **False positive fixed:**
 964  - Generator inside Cloner shows cache=None - this is NORMAL (master template has no cache, clones do)
 965  - Need to update describe_scene to not flag this as error when generator is under Cloner
 966  
 967  ### 2025-01-10: Phase 2 - Nested Holons
 968  **Verified working:**
 969  - Generator containing generators (CubeTriad with 3 FoldableCube children)
 970  - Parent passes parameters to child generators via `set_userdata_by_name()`
 971  - Nested holons work inside Cloners - each clone gets unique hierarchy
 972  - Three-level hierarchy: Cloner → Parent Generator → Child Generators
 973  - Position-based variation cascades through entire holarchy
 974  
 975  **Pattern established:**
 976  ```
 977  Parent reads position → calculates value → passes to children → children apply internally
 978  ```
 979  
 980  ### 2025-01-10: Phase 1 - MoGraph Integration Testing
 981  **Verified working:**
 982  - `return None` generators work in Cloners - children visible per-clone
 983  - Position-based parameter variation (rotation/scale based on X position)
 984  - Field-driven parameters via distance calculation to external objects
 985  - Dynamic response - moving field updates all clones in real-time
 986  - Name-based UserData lookup for robust parameter access
 987  
 988  **Discovered limitation:**
 989  - Generators execute BEFORE effectors - cannot see effector-modified transforms
 990  - For effector integration, need Python Effector or field-based approach
 991  
 992  **Fixed:**
 993  - FoldableCube rotation axes (Front/Back use Y, Right/Left use Z)
 994  - Generator error detection in describe_scene (cache=None = error)
 995  
 996  ### 2025-01-10: Vision Clarification
 997  - Articulated holonic architecture vision
 998  - Python Generator = universal holon container
 999  - Separation: structural relationships (generator) vs temporal animation (keyframes)
1000  - Created phased implementation roadmap
1001  
1002  ### Previous: MoGraph Discovery
1003  - Proved generators re-evaluate per-clone
1004  - Discovered "Optimize Cache" must be OFF
1005  - Created GeneratorMixin for automatic XPresso→Generator translation