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