# Blender AI Wiki — полный корпус (222 документов) > Все индексируемые статьи базы знаний, одним файлом. > Каждый документ отделён HTML-комментарием с его URL и метаданными. > Для выборочного чтения используйте /llms.txt. # P01: PRIMITIVEN ERSTELLEN — BASIS Tags: #basics #bpy #bpy.ops #geometry #primitives # P01: PRIMITIVEN ERSTELLEN — BASIS **ID:** P01 | **Cluster:** Geometry Basics | **Schwierigkeit:** ⭐ | **Tags:** #geometry #primitives #basics #bpy.ops --- ## 🎯 ZIEL Basis-Primitive über MCP mit präziser Parameterkontrolle erstellen --- ## 📝 PROMPT ``` Erstelle ein [TYPE]-Primitiv namens "[NAME]" an Position [X, Y, Z], Skalierung [SX, SY, SZ], Rotation [RX, RY, RZ] (in Radiant). Typen: CUBE, SPHERE, CYLINDER, PLANE, CONE, TORUS, EMPTY, CAMERA, LIGHT Beispiel: "Erstelle einen Würfel 'Box_01' bei (2, 0, 1) Skalierung (1, 2, 1) Rotation (0, 0, 0.785)" ``` --- ## 🔧 MCP TOOL: `create_object` ```json { "type": "create_object", "params": { "entity_type": "CUBE", "name": "Box_01", "location": [2.0, 0.0, 1.0], "rotation": [0.0, 0.0, 0.785], "scale": [1.0, 2.0, 1.0] } } ``` --- ## 📊 PARAMETER JE TYP | Typ | Erforderlich | Optional | |-----|--------------|--------------| | CUBE | size | location, rotation, scale | | SPHERE | radius | segments, ring_count, location, rotation, scale | | CYLINDER | radius, depth | vertices, cap_ends, location, rotation, scale | | PLANE | size | location, rotation, scale | | CONE | radius1, radius2, depth | vertices, location, rotation, scale | | TORUS | major_radius, minor_radius | major_segments, minor_segments, location, rotation, scale | | EMPTY | — | location, rotation, scale, type | | CAMERA | — | location, rotation, lens, sensor_width | | LIGHT | — | location, rotation, type, energy, color | --- ## ⚠️ HÄUFIGE FEHLER | Fehler | Ursache | Lösung | |--------|---------|---------| | `Object not found` | Name belegt | Eindeutige Namen nutzen oder `get_scene_info` prüfen | | `Invalid rotation` | Grad statt Radiant | Umrechnen: `radians = degrees * π / 180` | | `Primitive not created` | Kontext (EDIT mode) | Immer im OBJECT mode bleiben | --- ## 🔗 LINKS - **Fälle:** [Case 02: Game Props](../cases/case_02_game_props.md) - **Workflows:** [WF 01: Text-to-Scene](../workflows/wf_01_text_to_scene.md) - **Nächster:** [P02: Boolean Operations](./02_boolean_operations.md) --- # P02: BOOLESCHE OPERATIONEN Tags: #boolean #difference #intersect #modeling #modifiers #union # P02: BOOLESCHE OPERATIONEN **ID:** P02 | **Cluster:** Geometry Basics | **Schwierigkeit:** ⭐⭐ | **Tags:** #boolean #modeling #modifiers #difference #union #intersect --- ## 🎯 ZIEL Boolesche Operationen (Difference, Union, Intersect) zwischen Objekten via MCP --- ## 📝 PROMPT ``` Führe boolesche Operation [OPERATION] zwischen "[OBJECT_A]" (Basis) und "[OBJECT_B]" (Werkzeug) aus. Operationen: DIFFERENCE (subtrahieren), UNION (vereinigen), INTERSECT (Schnittmenge). Ergebnis: Objekt A wird modifiziert, Objekt B kann gelöscht werden. ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy # Setup obj_a = bpy.data.objects.get("Wall") obj_b = bpy.data.objects.get("Window_Cutout") if not obj_a or not obj_b: raise ValueError("Objects not found") # OBJECT mode sicherstellen if bpy.context.mode != 'OBJECT': bpy.ops.object.mode_set(mode='OBJECT') # Objekte wählen bpy.ops.object.select_all(action='DESELECT') obj_a.select_set(True) obj_b.select_set(True) bpy.context.view_layer.objects.active = obj_a # Boolean-Modifier hinzufügen bool_mod = obj_a.modifiers.new(name="Bool_Window", type='BOOLEAN') bool_mod.operation = 'DIFFERENCE' # 'UNION', 'INTERSECT' bool_mod.object = obj_b bool_mod.solver = 'EXACT' # 'FAST' für einfache Geometrien # Anwenden bpy.ops.object.modifier_apply(modifier="Bool_Window") # Werkzeug löschen (optional) bpy.data.objects.remove(obj_b, do_unlink=True) print(f"Boolean DIFFERENCE applied. {obj_a.name} modified.") ``` --- ## 📊 OPERATIONEN | Operation | Visuelles Ergebnis | Use Case | |----------|---------------------|----------| | **DIFFERENCE** | A minus B | Fenster, Türen, Öffnungen, Nuten | | **UNION** | A plus B | Teile vereinen, Meshes fusionieren | | **INTERSECT** | Nur Schnittmenge | Masken, Formen ausschneiden | --- ## 💡 FORTGESCHRITTENE MUSTER ### Mehrere Booleans (gleichzeitig) ```python # Mehrere Subtraktionen in einem Durchgang cutters = [bpy.data.objects.get(f"Cutter_{i}") for i in range(5)] for cutter in cutters: mod = wall.modifiers.new(f"Bool_{cutter.name}", 'BOOLEAN') mod.operation = 'DIFFERENCE' mod.object = cutter mod.solver = 'EXACT' # Alle anwenden for mod in list(wall.modifiers): if mod.type == 'BOOLEAN': bpy.context.view_layer.objects.active = wall bpy.ops.object.modifier_apply(modifier=mod.name) # Aufräumen for c in cutters: bpy.data.objects.remove(c, do_unlink=True) ``` ### Vorsicht mit Normalen ```python # Normalen vor Boolean prüfen und korrigieren bpy.context.view_layer.objects.active = obj_a bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.normals_make_consistent(inside=False) bpy.ops.object.mode_set(mode='OBJECT') ``` --- ## ⚠️ HÄUFIGE PROBLEME | Problem | Ursache | Lösung | |--------|---------|---------| | Leeres Ergebnis | Normalen innen/außen | `Mesh > Normals > Recalculate Outside` | | Geometrie-Artefakte | Non-manifold Kanten | `Select > Select All by Trait > Non-Manifold` → fixen | | Langsam bei komplexen Meshes | Viele Polygone | `DECIMATE` vorher nutzen, oder `FAST`-Solver | | Objekt verschwindet | Leere Schnittmenge | Positionen der Objekte prüfen | --- ## 🔗 LINKS - **Fälle:** [Case 01: ArchViz Interior](../cases/case_01_archviz_interior.md) — Fenster/Türen in Wänden - **Workflows:** [WF 01: Text-to-Scene](../workflows/wf_01_text_to_scene.md) - **Verwandte Prompts:** [P01 Primitives](./01_geometry_basics.md), [P03 Modifiers Stack](./03_modifiers_stack.md) - **Nächster:** [P03: Modifiers Stack](./03_modifiers_stack.md) --- # P09: PRINCIPLED BSDF SETUP Tags: #materials #pbr #principled-bshf #shader-nodes # P09: PRINCIPLED BSDF SETUP **ID:** P09 | **Cluster:** Materials PBR | **PROMPT:** ⭐ | **Tags:** #materials #pbr #principled-bshf #shader-nodes --- ## 🎯 ZIEL Principled BSDF Material mit komplettem PBR-Mapset erstellen und einrichten --- ## 📝 PROMPT ``` Erstelle Material "[MAT_NAME]" vom Typ Principled BSDF: Base Color: [R, G, B] oder hex #RRGGBB Roughness: [0.0-1.0] Metallic: [0.0-1.0] [Optional] Transmission, IOR, Clearcoat, Sheen, Emission Weise es dem Objekt "[OBJECT_NAME]" zu ``` --- ## 🔧 MCP: `set_material` (Basis) ```json { "type": "set_material", "params": { "object_name": "Cube", "material_name": "Mat_Red", "color": [1.0, 0.0, 0.0, 1.0] } } ``` --- ## 🔧 MCP: `execute_blender_code` (volles PBR) ```python import bpy from pathlib import Path def create_pbr_material(name, maps_dict, obj_name=None): """ maps_dict keys: 'base_color', 'roughness', 'metallic', 'normal', 'height', 'ao', 'emission' values: filepath oder Color-Tupel (R,G,B,A) """ mat = bpy.data.materials.new(name) mat.use_nodes = True nodes = mat.node_tree.nodes links = mat.node_tree.links nodes.clear() # Principled BSDF principled = nodes.new('ShaderNodeBsdfPrincipled') principled.location = (0, 0) # Material Output output = nodes.new('ShaderNodeOutputMaterial') output.location = (400, 0) links.new(principled.outputs['BSDF'], output.inputs['Surface']) # Displacement disp = nodes.new('ShaderNodeDisplacement') disp.location = (200, -200) links.new(disp.outputs['Displacement'], output.inputs['Displacement']) mat.displacement_method = 'BOTH' # Texture Coordinate + Mapping tex_coord = nodes.new('ShaderNodeTexCoord') tex_coord.location = (-600, 0) mapping = nodes.new('ShaderNodeMapping') mapping.location = (-400, 0) links.new(tex_coord.outputs['UV'], mapping.inputs['Vector']) def add_texture(key, input_name, colorspace='sRGB', use_normal_map=False, use_disp=False): if key not in maps_dict: return val = maps_dict[key] if isinstance(val, (tuple, list)): # Farbe principled.inputs[input_name].default_value = val + (1.0,) if len(val)==3 else val return # Bilddatei tex = nodes.new('ShaderNodeTexImage') tex.image = bpy.data.images.load(str(val)) tex.image.colorspace_settings.name = colorspace tex.location = (-200, 0) links.new(mapping.outputs['Vector'], tex.inputs['Vector']) if use_normal_map: nmap = nodes.new('ShaderNodeNormalMap') nmap.location = (0, -100) links.new(tex.outputs['Color'], nmap.inputs['Color']) links.new(nmap.outputs['Normal'], principled.inputs[input_name]) elif use_disp: links.new(tex.outputs['Color'], disp.inputs['Height']) else: links.new(tex.outputs['Color'], principled.inputs[input_name]) # Maps verbinden add_texture('base_color', 'Base Color', 'sRGB') add_texture('roughness', 'Roughness', 'Non-Color') add_texture('metallic', 'Metallic', 'Non-Color') add_texture('normal', 'Normal', 'Non-Color', use_normal_map=True) add_texture('height', 'Height', 'Non-Color', use_disp=True) add_texture('emission', 'Emission Color', 'sRGB') # AO -> Mix mit Base Color if 'ao' in maps_dict: ao_tex = nodes.new('ShaderNodeTexImage') ao_tex.image = bpy.data.images.load(str(maps_dict['ao'])) ao_tex.image.colorspace_settings.name = 'Non-Color' ao_tex.location = (-200, 200) links.new(mapping.outputs['Vector'], ao_tex.inputs['Vector']) mix = nodes.new('ShaderNodeMixRGB') mix.blend_type = 'MULTIPLY' mix.location = (0, 100) # Base Color via Mix neu verbinden if principled.inputs['Base Color'].links: from_node = principled.inputs['Base Color'].links[0].from_node from_socket = principled.inputs['Base Color'].links[0].from_socket links.new(from_socket, mix.inputs[1]) links.new(ao_tex.outputs['Color'], mix.inputs[2]) links.new(mix.outputs['Color'], principled.inputs['Base Color']) # Objekt zuweisen if obj_name: obj = bpy.data.objects.get(obj_name) if obj: if obj.data.materials: obj.data.materials[0] = mat else: obj.data.materials.append(mat) return mat # === VERWENDUNGSBEISPIELE === # 1. Einfache Farbe create_pbr_material("Mat_Red", { 'base_color': (1.0, 0.2, 0.2, 1.0), 'roughness': 0.5, 'metallic': 0.0 }, "Cube") # 2. Gold create_pbr_material("Mat_Gold", { 'base_color': (1.0, 0.843, 0.0, 1.0), 'roughness': 0.1, 'metallic': 1.0 }, "Ring") # 3. Glas create_pbr_material("Mat_Glass", { 'base_color': (1.0, 1.0, 1.0, 1.0), 'roughness': 0.0, 'transmission': 1.0, # bei Bedarf in Funktion ergänzen 'ior': 1.45 }, "Glass_Obj") # 4. Voll-PBR aus Dateien create_pbr_material("Mat_Brick", { 'base_color': Path("//textures/brick_basecolor.png"), 'roughness': Path("//textures/brick_roughness.png"), 'normal': Path("//textures/brick_normal.png"), 'height': Path("//textures/brick_height.png"), 'ao': Path("//textures/brick_ao.png"), }, "Wall") ``` --- ## 📊 PBR-PARAMETER-SPICKZETTEL | Material | Base Color | Roughness | Metallic | Transmission | IOR | Clearcoat | |----------|------------|-----------|----------|--------------|-----|-----------| | **Plastik** | Farbe | 0.3-0.5 | 0.0 | 0.0 | 1.5 | 0.0 | | **Metall (poliert)** | Metallfarbe | 0.05-0.15 | 1.0 | 0.0 | — | 0.0 | | **Metall (rostig/matt)** | Metallfarbe | 0.6-0.9 | 1.0 | 0.0 | — | 0.0 | | **Glas** | Weiß | 0.0 | 0.0 | 1.0 | 1.45-1.5 | 0.0 | | **Wasser** | Hellblau | 0.05 | 0.0 | 1.0 | 1.33 | 0.0 | | **Haut** | Hautfarbe | 0.6-0.8 | 0.0 | 0.0 | 1.5 | 0.1-0.3 | | **Stoff** | Stofffarbe | 0.8-1.0 | 0.0 | 0.0 | 1.5 | 0.0 | | **Keramik** | Farbe | 0.1-0.3 | 0.0 | 0.0 | 1.5 | 0.2-0.5 | | **Stein/Beton** | Grau/beige | 0.7-1.0 | 0.0 | 0.0 | 1.5 | 0.0 | | **Holz** | Holzton | 0.6-0.9 | 0.0 | 0.0 | 1.5 | 0.0 | --- ## 🎨 SPEZIALEFFEKTE ### Emission (Glühen) ```python principled.inputs['Emission Color'].default_value = (1.0, 0.8, 0.2, 1.0) # Warmes Licht principled.inputs['Emission Strength'].default_value = 5.0 # Stärke ``` ### Clearcoat (Lack/Keramik) ```python principled.inputs['Coat Weight'].default_value = 1.0 principled.inputs['Coat Roughness'].default_value = 0.03 ``` ### Sheen (Stoff/Samt) ```python principled.inputs['Sheen Weight'].default_value = 0.5 principled.inputs['Sheen Roughness'].default_value = 0.3 principled.inputs['Sheen Tint'].default_value = 0.5 ``` ### Subsurface (Haut, Wachs, Marmor) ```python principled.inputs['Subsurface Weight'].default_value = 0.3 principled.inputs['Subsurface Radius'].default_value = (1.0, 0.2, 0.1) # RGB-Eindringradius principled.inputs['Subsurface Color'].default_value = (1.0, 0.3, 0.1, 1.0) ``` ### Anisotropic (gerillte Oberflächen) ```python principled.inputs['Anisotropic'].default_value = 0.8 principled.inputs['Anisotropic Rotation'].default_value = 0.25 # 90° # Tangent-Eingang nötig (UV oder Geometry > Tangent) ``` --- ## 🔗 СВЯЗИ - **Fälle:** [Case 01: ArchViz](../cases/case_01_archviz_interior.md), [Case 03: Product Viz](../cases/case_03_product_viz.md) - **Workflows:** [WF 03: Texture Pipeline](../workflows/wf_03_texture_pipeline.md) - **Prompts:** [P10 Node Groups](./10_node_groups.md), [P11 Procedural Textures](./11_procedural_textures.md) - **MCP Tool:** `set_material` (Basis), `execute_blender_code` (voll) - **Nächster:** [P10: Node Group Creation](./10_node_groups.md) --- # SETUP 01: BLENDER-MCP BASIS # SETUP 01: BLENDER-MCP BASIS **ID:** SETUP-01 | **Typ:** Kern-Infrastruktur | **Schwierigkeit:** ⭐ | **Zeit:** 10-15 Min --- ## 🎯 ZIEL Blender-mcp installieren und für IDE-Agenten einrichten (OpenCode, Cursor, VS Code, Codex, Claude Desktop) --- ## 📋 VORAUSSETZUNGEN | Komponente | Version | Status | |-----------|--------|--------| | **Blender** | 4.0+ | ✅ Erforderlich | | **Python** | 3.10+ | ✅ In Blender enthalten | | **uv** | latest | ✅ Erforderlich | | **Git** | latest | ⚠️ Optional | --- ## 🪟 WINDOWS-INSTALLATION (SCHRITT FÜR SCHRITT) ### 1. uv installieren ```powershell # PowerShell als Administrator irm https://astral.sh/uv/install.ps1 | iex # Terminal neu starten # Prüfen uv --version uvx --version ``` ### 2. Blender-mcp Addon installieren ```powershell # Automatische Installation in Blender uvx blender-mcp install-addon # Pfad prüfen uvx blender-mcp addon-paths # Sollte zeigen: C:\Users\\AppData\Roaming\Blender Foundation\Blender\4.x\scripts\addons\blender_mcp.py ``` ### 3. PATH einrichten (falls uvx nicht gefunden) ```powershell # uvx finden where uvx # Normalerweise: C:\Users\\.local\bin\uvx.exe # Zum Benutzer-PATH hinzufügen $localBin = "$env:USERPROFILE\.local\bin" $userPath = [Environment]::GetEnvironmentVariable("Path", "User") [Environment]::SetEnvironmentVariable("Path", "$userPath;$localBin", "User") # Terminal/IDE neu starten ``` --- ## 🔧 BLENDER-EINRICHTUNG ### 1. Addon aktivieren ``` Edit → Preferences → Add-ons Suchen: "MCP for Blender" ☑ Interface: MCP for Blender ``` ### 2. MCP-Server starten ``` N-Panel (Taste N) → Reiter "MCP for Blender" ☑ Poly Haven ☑ Poly Pizza (API-Key einfügen) ☐ Sketchfab (optional) ☐ Hyper3D / Hunyuan3D (optional) 🟢 Button: "Start MCP Server" ``` ### 3. In der Systemkonsole prüfen ``` Window → Toggle System Console Es sollte erscheinen: "BlenderMCP server started on localhost:9876" ``` --- ## 🤖 IDE-AGENTEN KONFIGURATION ### OpenCode (`~/.config/opencode/opencode.jsonc`) ```jsonc { "mcp": { "blender-mcp": { "type": "local", "command": ["uvx", "blender-mcp"], "enabled": true, "environment": { "BLENDER_HOST": "localhost", "BLENDER_PORT": "9876" } } } } ``` ### Cursor (Settings → MCP → Add Server) ```json { "mcpServers": { "blender": { "command": "cmd", "args": ["/c", "uvx", "blender-mcp"] } } } ``` ### VS Code (MCP-Erweiterung) ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["blender-mcp"] } } } ``` ### Codex ```bash codex mcp add blender -- uvx blender-mcp ``` ### Claude Desktop (`claude_desktop_config.json`) ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["blender-mcp"] } } } ``` --- ## ✅ VERBINDUNGSTEST ### In OpenCode / Terminal: ```bash # Einfacher Test "Hole Informationen über die aktuelle Szene" ``` ### Erwartete Antwort: ```json { "name": "Scene", "object_count": 2, "objects": [ {"name": "Cube", "type": "MESH", "location": [0, 0, 0]}, {"name": "Light", "type": "LIGHT", "location": [0, 0, 5]} ], "materials_count": 1 } ``` ### Objekterstellungs-Test: ```bash "Erstelle einen Würfel 'TestCube' an Position (2, 0, 0) mit Skalierung (1, 2, 1)" ``` --- ## 🔑 API-SCHLÜSSEL (OPTIONAL) | Dienst | Wo erhalten | Wo einfügen | |--------|--------------|---------------| | **Poly Pizza** | https://poly.pizza/settings/api | Blender Preferences → Add-ons → MCP for Blender → Poly Pizza API Key | | **Sketchfab** | https://sketchfab.com/developers/api | Blender Preferences → Add-ons → MCP for Blender → Sketchfab API Key | | **Hyper3D** | https://hyper3d.ai/ | Blender Preferences → Add-ons → MCP for Blender → Hyper3D API Key | | **Hunyuan3D** | https://3d.hunyuan.tencent.com/ | Blender Preferences → Add-ons → MCP for Blender → SecretId/SecretKey | --- ## ⚠️ HÄUFIGE PROBLEME | Problem | Lösung | |----------|---------| | `spawn uvx ENOENT` | `~/.local/bin` zum PATH hinzufügen, IDE neu starten | | `Connection refused` | Blender läuft nicht oder MCP-Server nicht gestartet (N-Panel → Start) | | `Timeout` | Anfrage zu komplex → in Schritte aufteilen | | `Poly Pizza 403` | Cloudflare-Block → .glb manuell laden → File → Import → glTF | | `ModuleNotFoundError: mcp` | Blender einmal als Admin starten für Auto-Install der Deps | --- ## 🔗 LINKS - **Prompts:** [P01-P05](../prompts/) — Basisbefehle - **Fälle:** [Case 02: Game Props](../cases/case_02_game_props.md) - **Workflows:** [WF 01: Text-to-Scene](../workflows/wf_01_text_to_scene.md) - **Nächster:** [Setup 02: GenesisCore Local](../setups/setup_02_genesiscore_local.md) --- # SETUP 02: GENESISCORE LOKAL (OLLAMA) # SETUP 02: GENESISCORE LOKAL (OLLAMA) **ID:** SETUP-02 | **Typ:** Lokale LLM-Integration | **Schwierigkeit:** ⭐⭐ | **Zeit:** 15-30 Min --- ## 🎯 ZIEL GenesisCore für lokale LLMs via Ollama einrichten (Privatsphäre, kostenlos, offline) --- ## 📋 VORAUSSETZUNGEN | Komponente | Version | Status | |-----------|--------|--------| | **Blender** | 4.0+ | ✅ Erforderlich | | **Ollama** | latest | ✅ Erforderlich | | **RAM** | 16GB+ | 🟡 Empfohlen | | **VRAM** | 8GB+ | 🟡 Für 7B+ Modelle | --- ## 🪟 WINDOWS-INSTALLATION ### 1. Ollama installieren ```powershell # Herunterladen und installieren winget install Ollama.Ollama # Oder von der offiziellen Website # https://ollama.com/download/windows # Prüfen ollama --version ollama list ``` ### 2. Modelle für Coding/Blender laden ```powershell # Beste Modelle für Blender Python (nach Priorität) ollama pull codellama:13b # Bestes für Code, 7.3GB ollama pull deepseek-coder:6.7b # Exzellent für Python, 3.8GB ollama pull qwen2.5-coder:7b # Neu, schnell, 4.7GB ollama pull llama3.2:3b # Leicht, schnell, 2.0GB # Prüfen ollama list ``` ### 3. Ollama-Server starten ```powershell # Sollte im Hintergrund laufen (Autostart bei Installation) # Prüfen: curl http://localhost:11434/api/tags # Falls nicht: ollama serve ``` --- ## 🔧 GENESISCORE-EINRICHTUNG ### 1. Addon installieren (falls noch nicht) ```bash # In Blender: Edit → Preferences → Add-ons → Install from ZIP # Wählen: GenesisCore-main.zip # Aktivieren: GenesisCore ``` ### 2. Provider einrichten ``` N-Panel → Reiter GenesisCore: 1. Provider: ▼ Ollama 2. Base URL: http://localhost:11434/v1 (wichtig: /v1 am Ende!) 3. API Key: (leer lassen für Ollama) 4. "Fetch Models" klicken 4. Modell wählen: codellama:13b / deepseek-coder:6.7b / qwen2.5-coder:7b 5. Tool Modules: alle aktivieren (Shift+Click) 6. Save Config 7. Connect ``` ### 3. Modell-Einstellungen (empfohlen) ```json { "provider": "Ollama", "base_url": "http://localhost:11434/v1", "model": "codellama:13b", "temperature": 0.1, "top_p": 0.9, "max_tokens": 4096, "use_history": true } ``` --- ## 🔧 LEISTUNGSOPTIMIERUNG ### GPU Offloading (NVIDIA) ```powershell # Ollama nutzt die GPU automatisch bei CUDA # Prüfen: ollama run codellama:13b "test" # Sollte zeigen: "using GPU" in den Logs # GPU-Layer erzwingen: # In Umgebungsvariablen: OLLAMA_NUM_GPU=999 OLLAMA_GPU_LAYERS=999 ``` ### Speicheroptimierung ```yaml # In GenesisCore-Einstellungen: use_history: false # Verlauf deaktivieren um Tokens zu sparen max_tokens: 2048 # Antwortlänge begrenzen temperature: 0.1 # Deterministische Antworten ``` ### Quantisierung (bei wenig VRAM) ```powershell # Quantisierte Versionen nutzen: ollama pull codellama:13b-q4_K_M # 4-bit, 7.3GB → 4.7GB ollama pull deepseek-coder:6.7b-q4_K_M # 3.8GB → 2.5GB ``` --- ## 🧪 TESTS ### Schnelltest im GenesisCore-Chat ``` "Erstelle einen Würfel 'TestCube' an Position (1, 2, 3) mit rotem Material" ``` ### Erwartetes Ergebnis 1. Der Agent analysiert die Anfrage 2. Ruft `create_object` + `set_material` auf 3. In Blender erscheint ein Würfel bei (1, 2, 3) mit rotem Material --- ## 🔧 FEHLERBEHEBUNG | Problem | Lösung | |----------|---------| | `Connection refused` | Ollama läuft nicht → `ollama serve` | | `Model not found` | `ollama pull ` | | `Out of memory` | Kleineres Modell nutzen (3b/7b) oder q4-Quantisierung | | `Slow response` | GPU-Offloading aktivieren, `max_tokens` senken | | `Base URL error` | Prüfen: `http://localhost:11434/v1` (mit /v1!) | --- ## 🔗 LINKS - **Ollama-Modelle:** https://ollama.com/library - **GenesisCore-Repo:** https://github.com/AIGODLIKE/GenesisCore - **Setup 01:** [blender-mcp Basic](../setups/setup_01_blender_mcp_basic.md) - **Nächster:** [Setup 03: Dream Textures](../setups/setup_03_dream_textures.md) --- # WF 01: TEXT-ZU-SZENE # WF 01: TEXT-ZU-SZENE **ID:** WF-01 | **Typ:** End-to-End-Pipeline | **Schwierigkeit:** ⭐⭐ | **Zeit:** 15-30 Min --- ## 🎯 ZIEL Komplette Pipeline: Textbeschreibung → 3D-Szene (Geometrie, Materialien, Licht, Kamera, Render) --- ## 📋 PIPELINE-ARCHITEKTUR ``` USER PROMPT │ ▼ ┌─────────────────────────────────────┐ │ LLM AGENT (OpenCode/Claude) │ │ - Beschreibung parsen │ │ - Schritte planen │ │ - MCP-Tools aufrufen │ └─────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────┐ │ BLENDER-MCP SERVER (localhost:9876)│ │ - create_object │ │ - set_material │ │ - download_polyhaven_asset │ │ - execute_blender_code │ └─────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────┐ │ BLENDER (bpy-Ausführung) │ │ - Geometrie erstellen │ │ - Materialien zuweisen │ │ - Licht einrichten │ │ - Kamera positionieren │ │ - Rendern │ └─────────────────────────────────────┘ │ ▼ ERGEBNIS: Gerendertes Bild / .blend-Datei / GLB-Export ``` --- ## 📝 PROMPT-BEISPIELE ### Einfach: Zimmer ``` "Erstelle ein Zimmer 5x5x3m: Boden, 4 Wände, Decke. Füge ein Fenster 1.5x1.5m in die Vorderwand ein (Boolean). Materialien: Wände - weißer Putz, Boden - Parkett, Fenster - Glas. Licht: HDRI 'studio' 2k von Poly Haven. Kamera: in der Ecke, zur Mitte, 35mm. Render: Cycles, 128 Samples, 1920x1080, speichern unter //renders/room.png" ``` ### Mittel: Dungeon-Raum ``` "Erstelle einen Dungeon-Raum 10x10x5m im Dark-Fantasy-Stil. Wände: Steinmauerwerk mit Rissen (PBR). Boden: Steinplatten mit Moos in den Fugen. Requisiten: 3 Säulen, 2 Truhen, Altar in der Mitte. Licht: 3 Punktlichter (Feuer) + schwaches HDRI 'dungeon'. Kamera: Untersicht, DOF auf den Altar. Render: 256 Samples, Denoising ON" ``` --- ## 🔧 MCP-BEFEHLE NACH ETAPPEN ### Etappe 1: Basisgeometrie ```bash "Erstelle eine Ebene 'Floor' 10x10m bei (0,0,0)" "Erstelle einen Würfel 'Wall_Front' 10x0.2x3m bei (0, -5, 1.5)" ``` ### Etappe 2: Boolean (Fenster/Tür) ```bash "Erstelle einen Würfel 'Window_Cut' 2x0.3x1.5m bei (0, -5.1, 1.5) Subtrahiere Window_Cut von Wall_Front (DIFFERENCE)" ``` ### Etappe 3: Materialien ```bash "Erstelle Material 'Mat_Parquet': BaseColor #8B7355, Roughness 0.6 Weise es Floor zu" ``` ### Etappe 4: Poly Haven ```bash "Suche auf Poly Haven das HDRI 'studio' Kategorie 'indoor' 2k Lade es und setze es als World" ``` ### Etappe 5: Kamera und Render ```bash "Erstelle Kamera 'Cam_Main' bei (7, -7, 4) zur Mitte, 35mm Mache sie aktiv, DOF auf das Zentrum" "Stelle Cycles ein: 128 Samples, Denoising ON, 1920x1080 Rendere nach //renders/scene_01.png" ``` --- ## 🔗 LINKS - **Setups:** [Setup 01: blender-mcp](../setups/setup_01_blender_mcp_basic.md) - **Nächster:** [WF 02: Image-to-3D](../workflows/wf_02_image_to_3d.md) --- # WF 02: BILD-ZU-3D # WF 02: BILD-ZU-3D **ID:** WF-02 | **KI-Pipeline:** KI-Pipeline | **⭐⭐** | **10-30 Min pro Asset** --- ## 🎯 ZIEL Ein 2D-Bild (Foto, Sketch, Concept Art) per KI in ein 3D-Modell verwandeln --- ## 📋 PIPELINE-ARCHITEKTUR ``` INPUT IMAGE │ ▼ ┌─────────────────────────────────────┐ │ PREPROCESSING │ │ • Background removal (RMBG) │ │ • Normalize aspect ratio │ │ • Multiple views generation │ └─────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────┐ │ AI 3D GENERATION │ │ • TripoSR / LRM / Zero123++ │ │ • Hunyuan3D / Hyper3D (cloud) │ │ • Mesh + Textures + UV │ └─────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────┐ │ POST-PROCESSING │ │ • Retopology (Quad Remesher) │ │ • UV Unwrap │ │ • PBR Texture Baking │ │ • LOD Generation │ └─────────────────────────────────────┘ │ ▼ OUTPUT: .glb / .fbx / .obj + PBR textures ``` --- ## 🤖 KI-MODELLE (2024/2025) ### LOKAL (kostenlos, GPU erforderlich) | Modell | Qualität | Speed | VRAM | Installation | |--------|----------|----------|------|-----------| | **TripoSR** | ⭐⭐⭐⭐ | ~0.5s | 6GB | `pip install triposr` | | **LRM (Large Reconstruction Model)** | ⭐⭐⭐⭐⭐ | ~2s | 8GB | `pip install lrm` | | **Zero123++** | ⭐⭐⭐ | ~5s | 6GB | `pip install zero123pp` | | **InstantMesh** | ⭐⭐⭐⭐ | ~10s | 8GB | `pip install instant-mesh` | ### CLOUD (API, höhere Qualität) | Service | Qualität | Kosten | API | |--------|----------|-----------|-----| | **Hunyuan3D 2.0** | ⭐⭐⭐⭐⭐ | Free tier | Tencent Cloud | | **Hyper3D Rodin** | ⭐⭐⭐⭐⭐ | $0.5/gen | hyper3d.ai | | **Meshy.ai** | ⭐⭐⭐⭐ | $0.2/gen | meshy.ai | | **CSM (Common Sense Machines)** | ⭐⭐⭐⭐ | $0.3/gen | csm.ai | | **Tripo AI** | ⭐⭐⭐⭐ | $0.1/gen | tripo3d.ai | --- ## 🔧 MCP-BEFEHLE ### LOKALE GENERIERUNG (TripoSR) ```bash # Installation pip install triposr torch torchvision # Generierung via Python python -c " from triposr import TripoSR model = TripoSR.from_pretrained('VAST-AI/TripoSR') output = model.generate('input_image.png', num_inference_steps=50) # output: mesh-dict mit vertices, faces, textures " ``` ### MCP: HYPER3D / HUNYUAN3D (via blender-mcp) ```bash # In OpenCode / Cursor: "Generiere ein 3D-Modell aus dem Bild 'concept_art.png' mit Hyper3D Rodin 1. Bild laden 2. Generierung starten 3. Auf Abschluss warten 4. Ergebnis nach Blender importieren 4. Auto-Retopologie auf 10k Tris 5. UV Unwrap + PBR Texture Baking 6. Als GLB exportieren" ``` ### MCP: MESHY.AI ```bash "Nutze Meshy.ai für 3D aus 'product_photo.jpg': 1. Foto laden 2. Parameter: quality=high, texture_resolution=2048 3. Ergebnis als GLB laden 4. Nach Blender importieren 5. UV und Materialien prüfen" ``` --- ## 📝 SCHRITT-FÜR-SCHRITT-PIPELINE (LOKAL TripoSR) ### 1. BILDVORBEREITUNG ```python # preprocess.py from PIL import Image from rembg import remove def preprocess_image(input_path, output_path, size=512): img = Image.open(input_path) # Hintergrund entfernen img_no_bg = remove(img) # Quadratischer Crop mit Padding img_square = make_square(img_no_bg, size) # Speichern img_square.save(output_path, "PNG") return output_path def make_square(img, size): # Weißer/transparenter Hintergrund für Quadrat w, h = img.size max_dim = max(w, h) new_img = Image.new('RGBA', (max_dim, max_dim), (0,0,0,0)) new_img.paste(img, ((max_dim-w)//2, (max_dim-h)//2)) return new_img.resize((size, size), Image.LANCZOS) ``` ### 2. MESH-GENERIERUNG (TripoSR) ```python # generate_mesh.py from triposr import TripoSR import numpy as np import trimesh def generate_3d(image_path, output_dir, steps=50): model = TripoSR.from_pretrained('VAST-AI/TripoSR') # Generierung output = model.generate(image_path, num_inference_steps=steps) # Konvertierung zu trimesh vertices = output['vertices'] faces = output['faces'] mesh = trimesh.Trimesh(vertices=vertices, faces=faces) # Export mesh.export(f"{output_dir}/model.obj") mesh.export(f"{output_dir}/model.glb") # Texturen (falls vorhanden) if 'texture' in output: Image.fromarray(output['texture']).save(f"{output_dir}/texture.png") return mesh ``` ### 3. POST-PROCESSING IN BLENDER (MCP) ```bash # In OpenCode: "Importiere das generierte model.glb nach Blender: 1. File → Import → glTF 2.0 2. Auto-Retopologie: Quad Remesher target=10000 Tris 3. UV Unwrap: Smart Project angle=66 island_margin=0.01 4. PBR Texture Baking: BaseColor, Roughness, Normal, Metallic, AO 5. Material: Principled BSDF + Texture Nodes 5. Export: GLB + separate 2k-Texturen" ``` --- ## 📊 QUALITÄT UND SPEZIFIKATIONEN | Methode | Tris | Texture Res | UV Quality | Time (RTX 3080) | |-------|------|-------------|------------|-----------------| | **TripoSR (raw)** | 10k-50k | 512x512 | Auto UV | 0.5s | | **TripoSR + Retopo** | 5k-15k | 2k | Clean UV | 30s | | **LRM** | 20k-100k | 1024x1024 | Good UV | 2s | | **Hunyuan3D 2.0** | 10k-50k | 2k-4k | Excellent UV | 30s (API) | | **Hyper3D Rodin** | 10k-30k | 4k | Production UV | 60s (API) | --- ## 💡 TIPPS | Tipp | Warum | |-----|-----| | `rembg` für Hintergrund nutzen | Saubere Silhouette = bessere Geometrie | | Mehrere Ansichten (front/side/back) | LRM/Hunyuan unterstützen Multi-View | | Quadratischer Input 512x512 | Standard der meisten Modelle | | Post-Retopologie ist Pflicht | Raw-Mesh taugt nicht für Games/Animation | | PBR Baking nach UV | Normals/Cavity aus Geometrie | --- ## 🔗 LINKS - **TripoSR:** https://github.com/VAST-AI-Research/TripoSR - **LRM:** https://github.com/lrm-dev/LRM - **InstantMesh:** https://github.com/TencentARC/InstantMesh - **Hunyuan3D:** https://3d.hunyuan.tencent.com/ - **Hyper3D:** https://hyper3d.ai/ - **Meshy:** https://www.meshy.ai/ - **Setup 03:** [Dream Textures](../setups/setup_03_dream_textures.md) - **Nächster:** [WF 03: Texture Pipeline](../workflows/wf_03_texture_pipeline.md) --- # COLOR MANAGEMENT: OCIO / ACES Tags: #color # COLOR MANAGEMENT: OCIO / ACES **Professional color management for Blender pipelines** --- ## 🎯 WHY COLOR MANAGEMENT? | Problem | Solution | |---------|----------| | Inconsistent colors across apps | OCIO config shared across pipeline | | Wrong gamma on export | ACES 1.3 + OCIO v2 | | HDR/clipping issues | ACEScg working space, proper tone mapping | | Team inconsistency | Centralized OCIO config | | HDR delivery | ACES 1.3 + PQ / HLG | --- ## 🏗️ ACES 1.3 OVERVIEW ### ACES Color Spaces ``` ACES 1.3 Color Spaces: ├── ACEScg ← Working space (linear, wide gamut, AP1 primaries) ├── ACES2065-1 ← Archival/interchange (linear, AP0 primaries) ├── ACEScct ← Color grading (log-like, for grading) ├── ACESproxy ← On-set monitoring (10-bit log) ├── sRGB ← Display/web (sRGB primaries, ~2.2 gamma) ├── Rec.709 ← HD TV (Rec.709 primaries, ~2.4 gamma) ├── P3-D65 ← DCI-P3 cinema (DCI-P3 primaries, 2.6 gamma) ├── Rec.2020 ← UHD TV (Rec.2020 primaries, 2.4/ST2084) └── PQ / HLG ← HDR (ST2084 / HLG transfer functions) ``` ### ACES Pipeline ``` Camera/Source → IDT (Input Device Transform) → ACEScg (Working Space) → Creative Grade (ACEScct) → RRT (Reference Rendering Transform) → ODT (Output Device Transform) → Display/Output ``` --- ## ⚙️ BLENDER OCIO SETUP ### Blender 4.0+ OCIO v2 Support ```python import bpy # Check OCIO version print(f"OCIO Version: {bpy.context.scene.ocio_config_version}") # Set OCIO Config scene = bpy.context.scene scene.view_settings.view_transform = 'ACES 1.3 SDR-video' # or custom # Set Working Space (ACEScg) scene.view_settings.look = 'ACES 1.3 SDR-video' # Or for HDR: scene.view_settings.look = 'ACES 1.3 ST2084-1000nits' # or scene.view_settings.look = 'ACES 1.3 HLG' # Set View Transform scene.view_settings.view_transform = 'ACES 1.3 SDR-video' # Options: # - 'ACES 1.3 SDR-video' # - 'ACES 1.3 ST2084-1000nits' (HDR PQ) # - 'ACES 1.3 ST2084-2000nits' # - 'ACES 1.3 ST2084-4000nits' # - 'ACES 1.3 HLG' (Hybrid Log-Gamma) # - 'Raw' (no transform) ``` ### Custom OCIO Config ```yaml # config.ocio (ACES 1.3 + Custom) ocio_profile_version: 2 color_spaces: - name: ACEScg description: "ACES CG - Working Space" bit_depth: 32f is_data: false allocation: uniform to_reference: ! children: - ! {matrix: [0.662454, 0.134004, 0.156187, 0, 0.272228, 0.674081, 0.053691, 0, -0.005574, -0.004058, 1.009632, 0, 0, 0, 0, 1]} - name: ACES2065-1 description: "ACES 2065-1 - Archival" bit_depth: 32f is_data: false to_reference: ! children: - ! {matrix: [0.59719, 0.35458, 0.04823, 0, 0.07600, 0.90834, 0.01566, 0, 0.02840, 0.13383, 0.83777, 0, 0, 0, 0, 1]} - name: sRGB description: "sRGB - Display" bit_depth: 8ui is_data: false to_reference: ! children: - ! {src: sRGB, dst: ACEScg} - name: Rec709 description: "Rec.709 - HD Video" to_reference: ! {src: Rec.709, dst: ACEScg} - name: P3D65 description: "P3 D65 - Digital Cinema" to_reference: ! {src: P3-D65, dst: ACEScg} - name: Rec2020 description: "Rec.2020 - UHD" to_reference: ! {src: Rec.2020, dst: ACEScg} viewing_rules: - ! {name: "sRGB", colorspace: "sRGB", encodings: ["sRGB"]} - ! {name: "Rec.709", colorspace: "Rec709", encodings: ["Rec.709"]} views: - ! {name: "ACES 1.3 SDR-video", colorspace: "sRGB", looks: ["ACES_sRGB"]} - ! {name: "ACES 1.3 ST2084-1000nits", colorspace: "Rec2020", looks: ["ACES_ST2084_1000"]} - ! {name: "ACES 1.3 HLG", colorspace: "Rec2020", looks: ["ACES_HLG"]} looks: - ! {name: "ACES_sRGB", process_space: "ACEScg", transform: ! {children: [{! {src: ACEScg, dst: sRGB}}]}} - ! {name: "ACES_ST2084_1000", process_space: "ACEScg", transform: ! {children: [{! {src: ACEScg, dst: Rec2020_ST2084}}]}} - ! {name: "ACES_HLG", process_space: "ACEScg", transform: ! {children: [{! {src: ACEScg, dst: Rec2020_HLG}}]}} ``` ### Load Custom Config in Blender ```python import bpy # Load custom OCIO config bpy.context.scene.ocio_config = '/path/to/config.ocio' # Or set via environment variable (before Blender start) # export OCIO=/path/to/config.ocio # Verify print(f"Config: {bpy.context.scene.ocio_config}") print(f"Views: {[v.name for v in bpy.context.scene.view_settings.views]}") ``` --- ## 🎨 WORKING IN ACEScg ### Texture Color Spaces ```python # Set correct color space for each texture type def set_texture_colorspace(image, map_type): """Set correct color space for texture type""" color_spaces = { 'base_color': 'sRGB', # Albedo/Diffuse 'diffuse': 'sRGB', 'albedo': 'sRGB', 'color': 'sRGB', 'roughness': 'Non-Color', # Data 'metallic': 'Non-Color', # Data 'normal': 'Non-Color', # Data (OpenGL) 'height': 'Non-Color', # Data 'displacement': 'Non-Color', # Data 'ao': 'Non-Color', # Data 'curvature': 'Non-Color', # Data 'opacity': 'Non-Color', # Data 'emission': 'sRGB', # Color 'transmission': 'Non-Color', # Data 'subsurface': 'sRGB', # Color 'normal_gl': 'Non-Color', # OpenGL normal 'normal_dx': 'Non-Color', # DirectX normal } cs = color_spaces.get(map_type.lower(), 'Non-Color') image.colorspace_settings.name = cs print(f"Set {image.name} to {cs}") # Auto-detect from filename def auto_set_colorspace(image): name = image.name.lower() if any(x in name for x in ['basecolor', 'albedo', 'diffuse', 'color', 'emission']): image.colorspace_settings.name = 'sRGB' elif any(x in name for x in ['rough', 'metal', 'normal', 'height', 'disp', 'ao', 'curv', 'opacity', 'mask']): image.colorspace_settings.name = 'Non-Color' elif 'emission' in name: image.colorspace_settings.name = 'sRGB' ``` ### Node Setup for ACES ```python # Node setup for ACEScg workflow def setup_aces_nodes(material_name): mat = bpy.data.materials.new(name=material_name) mat.use_nodes = True nodes = mat.node_tree.nodes links = mat.node_tree.links nodes.clear() # Principled BSDF (works in ACEScg) principled = nodes.new('ShaderNodeBsdfPrincipled') principled.location = (0, 0) # Color Space conversion nodes (if needed) # Base Color: sRGB → ACEScg (handled by OCIO) # Normal: Non-Color (no conversion) # Roughness/Metallic: Non-Color # Texture nodes tex_base = nodes.new('ShaderNodeTexImage') tex_base.location = (-400, 300) tex_base.image.colorspace_settings.name = 'sRGB' tex_normal = nodes.new('ShaderNodeTexImage') tex_normal.location = (-400, 100) tex_normal.image.colorspace_settings.name = 'Non-Color' tex_rough = nodes.new('ShaderNodeTexImage') tex_rough.location = (-400, -100) tex_rough.image.colorspace_settings.name = 'Non-Color' # Normal Map normal_map = nodes.new('ShaderNodeNormalMap') normal_map.location = (-200, 100) # Links links.new(tex_base.outputs['Color'], principled.inputs['Base Color']) links.new(tex_normal.outputs['Color'], normal_map.inputs['Color']) links.new(normal_map.outputs['Normal'], principled.inputs['Normal']) links.new(tex_rough.outputs['Color'], principled.inputs['Roughness']) # Output output = nodes.new('ShaderNodeOutputMaterial') output.location = (400, 0) links.new(principled.outputs['BSDF'], output.inputs['Surface']) return mat ``` --- ## 🖥️ VIEW TRANSFORMS & LOOKS ### View Transforms ```python # Available in Blender 4.2+ with ACES 1.3 view_transforms = { 'ACES 1.3 SDR-video': 'Standard SDR (Rec.709/sRGB)', 'ACES 1.3 ST2084-1000nits': 'HDR PQ 1000 nits', 'ACES 1.3 ST2084-2000nits': 'HDR PQ 2000 nits', 'ACES 1.3 ST2084-4000nits': 'HDR PQ 4000 nits', 'ACES 1.3 HLG': 'Hybrid Log-Gamma (broadcast HDR)', 'Raw': 'No transform (linear)', 'Standard': 'Legacy sRGB EOTF', 'Film': 'Filmic log', 'False Color': 'Exposure check', } # Set view transform bpy.context.scene.view_settings.view_transform = 'ACES 1.3 SDR-video' # Set look (creative grade) looks = { 'None': 'No look', 'ACES 1.3 SDR-video': 'Standard', 'ACES 1.3 ST2084-1000nits': 'HDR 1000nits', 'Film': 'Filmic look', 'High Contrast': 'High contrast', 'Low Contrast': 'Low contrast', } bpy.context.scene.view_settings.look = 'None' # or 'Film', etc. ``` ### Exposure & Color Grading ```python # Exposure scene = bpy.context.scene scene.view_settings.exposure = 0.0 # Stops scene.view_settings.gamma = 1.0 # Filmic look (if using Filmic) scene.view_settings.look = 'Filmic Log Encoding Base Curve' # Custom LUT scene.sequencer_colorspace_settings = 'Custom' # Load .cube LUT via Color Management → Look → Custom ``` --- ## 📊 RENDER OUTPUT SETTINGS ### EXR Output (Linear ACEScg) ```python scene = bpy.context.scene scene.render.image_settings.file_format = 'OPEN_EXR_MULTILAYER' scene.render.image_settings.color_mode = 'RGBA' scene.render.image_settings.color_depth = '32' # 32-bit float scene.render.image_settings.exr_codec = 'ZIP' # Lossless compression scene.render.image_settings.use_preview = True # Color space for EXR (stored as linear ACEScg) # No conversion needed - EXR stores linear ``` ### PNG/JPEG Output (sRGB) ```python # For web/delivery scene.render.image_settings.file_format = 'PNG' scene.render.image_settings.color_mode = 'RGBA' scene.render.image_settings.color_depth = '16' scene.render.image_settings.compression = 15 # OCIO handles sRGB conversion on save ``` ### Video Output (H.264/HEVC) ```python scene.render.ffmpeg.format = 'MPEG4' scene.render.ffmpeg.codec = 'H264' scene.render.ffmpeg.constant_rate_factor = 'PERC_LOSSLESS' scene.render.ffmpeg.ffmpeg_preset = 'GOOD' scene.render.ffmpeg.audio_codec = 'AAC' scene.render.ffmpeg.audio_bitrate = 192 # For HDR delivery scene.render.ffmpeg.format = 'MPEG4' scene.render.ffmpeg.codec = 'HEVC' scene.render.ffmpeg.constant_rate_factor = 'PERC_LOSSLESS' # Requires HDR metadata injection post-render ``` --- ## 🎬 ACES WORKFLOW IN COMPOSITOR ### Compositor Setup ```python def setup_aces_compositor(): scene = bpy.context.scene scene.use_nodes = True tree = scene.node_tree nodes = tree.nodes links = tree.links nodes.clear() # Render Layers (linear ACEScg) rl = nodes.new('CompositorNodeRLayers') rl.location = (-400, 0) # Color Balance (Lift/Gamma/Gain) - in ACEScg cdl = nodes.new('CompositorNodeColorBalance') cdllocation = (0, 200) cdl.correction_method = 'LIFT_GAMMA_GAIN' cdl.lift = (1.01, 1.0, 0.98) # Shadows warm cdl.gamma = (1.0, 1.0, 1.0) # Midtones neutral cdl.gain = (0.98, 1.0, 1.02) # Highlights cool # Hue/Sat hsv = nodes.new('CompositorNodeHueSat') hsv.location = (200, 200) hsv.color_saturation = 1.1 # Gamma gamma = nodes.new('CompositorNodeGamma') gamma.location = (400, 200) gamma.inputs['Gamma'].default_value = 1.05 # Output (OCIO handles ACEScg → Display) comp = nodes.new('CompositorNodeComposite') comp.location = (800, 0) viewer = nodes.new('CompositorNodeViewer') viewer.location = (800, -200) viewer.use_alpha = False # Links links.new(rl.outputs['Image'], cdl.inputs['Image']) links.new(cdl.outputs['Image'], hsv.inputs['Image']) links.new(hsv.outputs['Image'], gamma.inputs['Image']) links.new(gamma.outputs['Image'], comp.inputs['Image']) links.new(gamma.outputs['Image'], viewer.inputs['Image']) # File Output (EXR MultiLayer) file_out = nodes.new('CompositorNodeOutputFile') file_out.location = (800, -400) file_out.format.file_format = 'OPEN_EXR_MULTILAYER' file_out.format.color_mode = 'RGBA' file_out.format.color_depth = '32' file_out.base_path = "//renders/exr/" passes = ['Image', 'Depth', 'Normal', 'Mist', 'AO', 'Emission', 'Shadow', 'Cryptomatte00'] for i, pass_name in enumerate(passes): if pass_name in rl.outputs: file_out.layer_slots.new(pass_name) links.new(rl.outputs[pass_name], file_out.inputs[i]) # Run setup_aces_compositor() ``` --- ## 🎬 HDR WORKFLOW ### HDR Setup (ST2084 / PQ) ```python # HDR Setup for 1000 nits scene = bpy.context.scene scene.view_settings.view_transform = 'ACES 1.3 ST2084-1000nits' scene.view_settings.look = 'None' # Render settings for HDR scene.render.image_settings.file_format = 'OPEN_EXR' scene.render.image_settings.color_mode = 'RGBA' scene.render.image_settings.color_depth = '32' scene.render.image_settings.exr_codec = 'ZIP' # HDR Metadata (inject post-render via ffmpeg) # ffmpeg -i input.exr -c:v libx265 -x265-params "hdr_opt=1:repeat_headers=1:colorprim=bt2020:transfer=smpte2084:colormatrix=bt2020nc:master-display=G(13250,34500)B(7500,3000)R(34000,16000)WP(15635,16450)L(10000000,1):max-cll=1000,400:max-fall=200" -pix_fmt yuv420p10le output.mp4 ``` ### HLG (Hybrid Log-Gamma) ```python # HLG for Broadcast HDR scene.view_settings.view_transform = 'ACES 1.3 HLG' scene.view_settings.look = 'None' # HLG is scene-referred, no ODT needed # Compatible with SDR displays (backward compatible) ``` --- ## 🎨 LOOK DEVELOPMENT (LUTs) ### Using LUTs in ACES ```python # Apply LUT in Compositor def apply_lut(lut_path, strength=1.0): scene = bpy.context.scene tree = scene.node_tree nodes = tree.nodes links = tree.links # LUT Node lut_node = nodes.new('CompositorNodeLUT') lut_node.location = (200, 0) lut_node.lut = bpy.data.images.load(lut_path) lut_node.strength = strength # Insert into node tree # ... connect between color grade and output # Common LUT Formats # .cube (most common) # .3dl (3D LUT) # .csp (CineSpace) # .icc (ICC Profile) ``` ### Film Emulation LUTs ```python # Popular Film LUTs for ACES film_luts = { 'Kodak 2383': 'Kodak_2383_D55.cube', 'Fujifilm 3513': 'Fujifilm_3513_D55.cube', 'Kodak Vision3 50D': 'Kodak_Vision3_50D.cube', 'Kodak Vision3 250D': 'Kodak_Vision3_250D.cube', 'Kodak Vision3 500T': 'Kodak_Vision3_500T.cube', 'ARRI LogC to ACES': 'ARRI_LogC_to_ACES.cube', 'Sony S-Log3 to ACES': 'Sony_SLog3_to_ACES.cube', 'RED Log3G10 to ACES': 'RED_Log3G10_to_ACES.cube', } # Apply in Compositor def apply_film_lut(lut_name, strength=0.5): lut_path = f"/project/luts/{film_luts[lut_name]}" apply_lut(lut_path, strength) ``` --- ## 📦 DELIVERY SPECS ### Delivery Checklist ```yaml Delivery Specs: Web/sRGB: colorspace: sRGB gamma: 2.2 format: PNG/JPEG bit_depth: 8-bit Broadcast/Rec.709: colorspace: Rec.709 gamma: 2.4 format: ProRes 422 HQ / DNxHR HQX bit_depth: 10-bit Digital Cinema (DCI-P3): colorspace: P3-D65 gamma: 2.6 format: ProRes 4444 / DPX bit_depth: 12-bit white_point: D65 HDR (ST2084/PQ): colorspace: Rec.2020 transfer: ST2084 (PQ) peak_brightness: 1000/2000/4000 nits format: ProRes 4444 / HEVC bit_depth: 10/12-bit metadata: HDR10 / Dolby Vision HDR (HLG): colorspace: Rec.2020 transfer: HLG format: ProRes 4444 / HEVC bit_depth: 10-bit compatible: SDR displays Archival (ACES 2065-1): colorspace: ACES2065-1 format: EXR / DPX bit_depth: 16/32-bit float compression: lossless ``` --- ## 🔗 LINKS - **ACES Central:** https://acescentral.com/ - **OCIO Configs (archived):** https://github.com/AcademySoftwareFoundation/OpenColorIO-Configs - **ACES 1.3 Spec:** https://acescentral.com/wp-content/uploads/2022/06/ACES-1.3-Developer-Guide.pdf - **Blender OCIO:** https://docs.blender.org/manual/en/latest/render/color_management.html - **ACES GitHub:** https://github.com/ampas/aces-dev > [!NOTE] > - Репозиторий OpenColorIO-Configs архивирован; актуальные конфиги OCIO ищите в https://github.com/AcademySoftwareFoundation/OpenColorIO/tree/main/configs > - Blender Color Management документация: https://docs.blender.org/manual/en/latest/render/color_management/ --- ## 🌍 MULTILANG | Language | File | |----------|------| | English | `multilang/en/color/ocio_aces.md` | | Russian | `multilang/ru/color/ocio_aces.md` | --- # INTEGRATIONS: COMFYUI, UNREAL, USD, TOUCHDESIGNER Tags: #integration #realtime #stable-diffusion #unreal #usd # INTEGRATIONS: COMFYUI, UNREAL, USD, TOUCHDESIGNER **Blender ecosystem integrations for production pipelines** --- ## 🎨 COMFYUI + BLENDER ### ComfyUI → Blender Live Sync ```python # blender_comfyui_sync.py import asyncio import aiohttp import json import bpy from pathlib import Path class ComfyUIBridge: def __init__(self, comfyui_url="http://127.0.0.1:8188"): self.base_url = comfyui_url self.session = None async def __aenter__(self): self.session = aiohttp.ClientSession() return self async def __aexit__(self, *args): await self.session.close() async def queue_prompt(self, workflow: dict): """Queue a prompt in ComfyUI""" async with self.session.post( f"{self.base_url}/prompt", json={"prompt": workflow, "client_id": "blender"} ) as resp: return await resp.json() async def get_images(self, prompt_id: str): """Get generated images""" async with self.session.get( f"{self.base_url}/history/{prompt_id}" ) as resp: history = await resp.json() return history.get(prompt_id, {}).get("outputs", {}) async def download_image(self, filename: str, subfolder: str = "", type: str = "output"): """Download generated image""" params = {"filename": filename, "subfolder": subfolder, "type": type} async with self.session.get( f"{self.base_url}/view", params=params ) as resp: return await resp.read() # Blender Integration async def generate_textures_comfyui(prompt: str, output_dir: Path): """Generate PBR textures via ComfyUI""" workflow = { "1": { "class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": "sd_xl_base_1.0.safetensors"} }, "2": { "class_type": "CLIPTextEncode", "inputs": {"text": f"PBR texture, {prompt}, seamless, 8k, high quality", "clip": ["1", 1]} }, "3": { "class_type": "CLIPTextEncode", "inputs": {"text": "blur, low quality, distorted, watermark, text", "clip": ["1", 1]} }, "4": { "class_type": "KSampler", "inputs": { "seed": 42, "steps": 20, "cfg": 7, "sampler_name": "dpmpp_2m", "scheduler": "karras", "model": ["1", 0], "positive": ["2", 0], "negative": ["3", 0], "latent_image": ["5", 0] } }, "5": { "class_type": "EmptyLatentImage", "inputs": {"width": 1024, "height": 1024, "batch_size": 1} }, "6": { "class_type": "VAEDecode", "inputs": {"samples": ["4", 0], "vae": ["1", 2]} }, "7": { "class_type": "SaveImage", "inputs": {"filename_prefix": "pbr_texture", "images": ["6", 0]} } } async with ComfyUIBridge() as bridge: result = await bridge.queue_prompt({"prompt": workflow}) prompt_id = result["prompt_id"] # Wait for completion while True: await asyncio.sleep(2) history = await bridge.get_images(prompt_id) if prompt_id in history: break # Download and apply to Blender outputs = history[prompt_id]["outputs"] for node_id, node_output in outputs.items(): if "images" in node_output: for img_info in node_output["images"]: img_data = await bridge.download_image( img_info["filename"], img_info.get("subfolder", ""), img_info.get("type", "output") ) # Save and create material filepath = Path("textures") / img_info["filename"] filepath.parent.mkdir(parents=True, exist_ok=True) Path(filepath).write_bytes(img_data) create_pbr_material_from_image(filepath) ``` ### ComfyUI Workflow Templates for PBR ```json { "pbr_material_generation": { "base_color": { "prompt": "{material}, {style}, base color, albedo, flat lighting", "negative": "shadows, lighting, highlights, specular, glossy" }, "roughness": { "prompt": "{material}, roughness map, grayscale, detailed surface imperfections", "negative": "color, shiny, glossy, smooth" }, "normal": { "prompt": "{material}, normal map, OpenGL format, detailed surface details", "negative": "color, flat, smooth" }, "normal_flip_y": true, "height": { "prompt": "{material}, height map, displacement, depth, grayscale", "negative": "flat, smooth" } } } ``` --- ## 🎮 UNREAL ENGINE LIVE LINK ### Unreal Live Link Plugin for Blender ```python # unreal_livelink.py import bpy import socket import json import struct from bpy.props import StringProperty, BoolProperty, IntProperty class UnrealLiveLink: """Live Link connection to Unreal Engine""" def __init__(self, host="127.0.0.1", port=1986): self.host = host self.port = port self.socket = None self.connected = False def connect(self): """Connect to Unreal Live Link""" self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket.connect((self.host, self.port)) self.connected = True print(f"Connected to Unreal at {self.host}:{self.port}") def disconnect(self): if self.socket: self.socket.close() self.connected = False def send_transform(self, object_name: str, location, rotation, scale): """Send object transform to Unreal""" if not self.connected: return data = { "subject_name": object_name, "transform": { "location": list(location), "rotation": list(rotation), # Quaternion [x, y, z, w] "scale": list(scale) }, "timestamp": bpy.context.scene.frame_current / bpy.context.scene.render.fps } message = json.dumps(data).encode('utf-8') # Length prefix (4 bytes) length = struct.pack('>I', len(message)) self.socket.sendall(length + message) def send_mesh(self, object_name: str, mesh_data: dict): """Send mesh data to Unreal""" # Implementation for mesh streaming pass # Blender Operator for Live Link class OBJECT_OT_unreal_livelink_connect(bpy.types.Operator): bl_idname = "unreal.livelink_connect" bl_label = "Connect to Unreal Live Link" host: StringProperty(name="Host", default="127.0.0.1") port: IntProperty(name="Port", default=1986) def execute(self, context): global livelink livelink = UnrealLiveLink(self.host, self.port) livelink.connect() return {'FINISHED'} class OBJECT_OT_unreal_livelink_sync(bpy.types.Operator): bl_idname = "unreal.livelink_sync" bl_label = "Sync Selection to Unreal" def execute(self, context): if not livelink or not livelink.connected: self.report({'ERROR'}, "Not connected to Unreal") return {'CANCELLED'} for obj in context.selected_objects: livelink.send_transform( obj.name, obj.location, obj.rotation_quaternion, obj.scale ) return {'FINISHED'} # Panel class VIEW3D_PT_unreal_livelink(bpy.types.Panel): bl_label = "Unreal Live Link" bl_idname = "VIEW3D_PT_unreal_livelink" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = 'Unreal' def draw(self, context): layout = self.layout layout.operator("unreal.livelink_connect") layout.operator("unreal.livelink_sync") layout.prop(context.scene, "unreal_auto_sync") # Auto-sync on frame change def sync_on_frame_change(scene): if getattr(scene, "unreal_auto_sync", False) and livelink and livelink.connected: for obj in bpy.context.selected_objects: livelink.send_transform( obj.name, obj.location, obj.rotation_quaternion, obj.scale ) bpy.app.handlers.frame_change_post.append(sync_on_frame_change) ``` --- ## 🎮 UNREAL ROUNDTRIP (Blender ↔ Unreal) ### Export Pipeline ```python # unreal_export.py import bpy import os from pathlib import Path class UnrealExportSettings: """Unreal-optimized export settings""" @staticmethod def configure_fbx_export(): return { "apply_unit_scale": True, "apply_scale_options": 'FBX_SCALE_ALL', "bake_space_transform": True, "object_types": {'ARMATURE', 'MESH'}, "use_mesh_modifiers": True, "use_mesh_modifiers_render": True, "mesh_smooth_type": 'FACE', "use_subsurf": True, "add_leaf_bones": False, "primary_bone_axis": 'Y_FORWARD', "secondary_bone_axis": 'X_UP', "bake_anim": True, "bake_anim_use_all_bones": True, "bake_anim_use_nla_strips": True, "bake_anim_use_all_actions": False, "bake_anim_force_startend_keying": True, "bake_anim_step": 1.0, "bake_anim_simplify_factor": 1.0, } @staticmethod def configure_gltf_export(): return { "export_format": 'GLB', "export_copyright": "MyStudio", "export_image_format": 'AUTO', "export_materials": 'EXPORT', "export_colors": True, "export_normals": True, "export_tangents": True, "export_animations": True, "export_frame_range": True, "export_frame_step": 1, "export_force_sampling": True, "export_skins": True, "export_morph": True, "export_lights": False, "export_cameras": False, } def export_for_unreal(asset_name, output_dir, collection_name=None): """Export selected objects or collection for Unreal""" output_path = Path("exports") / "unreal" / asset_name output_path.mkdir(parents=True, exist_ok=True) # Select objects if collection_name: coll = bpy.data.collections.get(collection_name) if coll: bpy.ops.object.select_all(action='DESELECT') for obj in coll.objects: obj.select_set(True) else: # Use selected objects pass # Export FBX fbx_path = Path("exports") / "unreal" / f"{asset_name}.fbx" bpy.ops.export_scene.fbx( filepath=str(fbx_path), **UnrealExportSettings.configure_fbx_export() ) # Also export GLB for web/preview glb_path = Path("exports") / "unreal" / f"{asset_name}.glb" bpy.ops.export_scene.gltf( filepath=str(glb_path), export_format='GLB', **{ "export_format": 'GLB', "use_selection": True, "export_apply": True, "export_tangents": True, "export_materials": 'EXPORT', "export_animations": True, "export_frame_range": True, "export_skins": True, "export_morph": True, } ) print(f"Exported to {fbx_path} and {glb_path}") # Unreal Import Helper (Python for Unreal) unreal_import_helper = ''' import unreal def import_fbx_asset(fbx_path, import_settings=None): """Import FBX into Unreal with optimal settings""" if import_settings is None: import_settings = unreal.FbxImportUI() import_settings.import_mesh = True import_settings.import_as_skeletal = False import_settings.import_rigid_animation = False import_settings.mesh_type_to_import = unreal.FBXImportType.FBXIT_STATIC_MESH import_settings.static_mesh_import_data.combine_meshes = True import_settings.static_mesh_import_data.auto_generate_collision = True import_settings.static_mesh_import_data.collision_type = unreal.FBXCollisionType.FBXCTF_Box import_settings.material_import_method = unreal.FBXMaterialImportMethod.FBXMI_FIND_OR_CREATE import_settings.texture_import_data.material_search_path = "/Game/Materials" task = unreal.AssetImportTask() task.filename = fbx_path task.destination_path = "/Game/Assets/Imported" task.replace_existing = True task.automated = True task.save = True task.options = import_settings unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) print(f"Imported: {fbx_path}") ''' ``` --- ## 📦 USD / MATERIALX / OPENUSD ### USD Export with MaterialX ```python # usd_export.py import bpy from pathlib import Path def export_usd_materialx( filepath: str, export_materials: bool = True, export_uvmaps: bool = True, export_animation: bool = True, purpose: str = "default", # "default", "render", "proxy", "guide" material_x: bool = True, shading_mode: str = 'USE_MATERIAL_X' # 'USE_MATERIAL_X', 'USE_PREVIEW', 'USE_LOOKDEV' ): """Export USD with MaterialX support""" # Ensure output directory Path(filepath).parent.mkdir(parents=True, exist_ok=True) # Export settings bpy.ops.wm.usd_export( filepath=filepath, export_materials=export_materials, export_uvmaps=export_uvmaps, export_animation=export_animation, export_frame_range=True, export_frame_step=1, export_uvmaps=True, export_normals=True, export_colors=True, export_animation=True, export_skinning=True, export_skinning=True, export_uvmaps=True, export_display_color=True, export_instancing=True, export_visibility=True, export_custom_properties=True, export_custom_attributes=True, export_materials=export_materials, export_uvmaps=export_uvmaps, export_animation=export_animation, export_frame_range=True, export_frame_step=1, # MaterialX specific material_x=material_x, shading_mode=shading_mode, # Purpose purpose=purpose, ) # MaterialX Shader Network in Blender def create_materialx_shader(material_name: str): """Create MaterialX-compatible shader network""" mat = bpy.data.materials.new(name=material_name) mat.use_nodes = True nodes = mat.node_tree.nodes links = mat.node_tree.links nodes.clear() # Standard Surface (MaterialX compatible) standard = nodes.new('ShaderNodeBsdfPrincipled') standard.location = (0, 0) # MaterialX standard surface inputs # base_color, specular_color, specular_roughness, metallic, etc. output = nodes.new('ShaderNodeOutputMaterial') output.location = (400, 0) output.target = 'MATERIALX' # MaterialX output # Connect mat.node_tree.links.new(standard.outputs['BSDF'], output.inputs['Surface']) # Add MaterialX specific nodes # MX_Standard_Surface, MX_Normal_Map, MX_Displacement, etc. return mat # USD Scene Assembly def assemble_usd_scene(shot_name: str, assets: list, output_path: str): """Assemble USD scene from multiple assets""" import pxr.Usd as Usd import pxr.UsdGeom as UsdGeom import pxr.Sdf as Sdf stage = Usd.Stage.CreateNew(str(Path("shots") / shot_name / f"{shot_name}.usda")) stage.SetDefaultPrim(stage.DefinePrim(f"/{shot_name}", "Scope")) # Set up default prim stage.SetDefaultPrim(stage.GetPrimAtPath(f"/{shot_name}")) # Add assets as references for i, asset in enumerate(assets): prim_path = f"/{shot_name}/assets/{asset['name']}_{i:03d}" prim = stage.DefinePrim(prim_path, "Xform") prim.GetReferences().AddReference(asset['usd_path']) # Set transform xform = UsdGeom.Xformable(prim) xform.AddTranslateOp().Set(asset.get('location', (0,0,0))) xform.AddRotateXYZOp().Set(asset.get('rotation', (0,0,0))) xform.AddScaleOp().Set(asset.get('scale', (1,1,1))) # Save stage.GetRootLayer().Save() print(f"USD scene assembled: {output_path}") ``` ### USD Pipeline Integration ```python # usd_pipeline.py """ USD Pipeline for Blender → USD → Houdini/Maya/Katana → Render """ class USDPipeline: def __init__(self, project_root: str): self.project_root = Path(project_root) self.usd_dir = self.project_root / "usd" self.usd_dir.mkdir(exist_ok=True) def publish_asset(self, asset_name: str, version: str, asset_type: str): """Publish asset as USD with versioning""" # Versioned path: /assets/props/crate/v003/crate_v003.usda version_dir = self.usd_dir / "assets" / "props" / asset_name / f"v{version}" version_dir.mkdir(parents=True, exist_ok=True) usd_path = version_dir / f"{asset_name}_v{version}.usda" # Export current selection self.export_usd_with_materialx(str(version_dir / f"{asset_name}_v{version}.usda")) # Create symlink to latest latest_link = self.usd_dir / "assets" / "props" / asset_name / "latest.usda" if latest_link.exists(): latest_link.unlink() latest_link.symlink_to(usd_path.relative_to(latest_link.parent)) return str(usd_path) def assemble_shot(self, shot_name: str, layout_data: list): """Assemble shot from published assets""" shot_dir = self.usd_dir / "shots" / shot_name shot_dir.mkdir(parents=True, exist_ok=True) shot_usd = shot_dir / f"{shot_name}.usda" self.assemble_shot(shot_name, layout_data, str(shot_usd)) return str(shot_usd) ``` --- ## 🎛 TOUCHDESIGNER ↔ BLENDER ### TouchDesigner ↔ Blender OSC Bridge ```python # touchdesigner_bridge.py import bpy import socket import json import threading import time class TouchDesignerBridge: def __init__(self, td_host="127.0.0.1", td_port=7000, blender_port=7001): self.td_addr = (td_host, td_port) self.blender_port = blender_port self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.socket.bind(("0.0.0.0", blender_port)) self.running = False self.callbacks = {} def start(self): self.running = True self.thread = threading.Thread(target=self._listen_loop, daemon=True) self.thread.start() def stop(self): self.running = False def _listen_loop(self): while self.running: try: data, addr = self.socket.recvfrom(65536) msg = json.loads(data.decode()) self.handle_message(msg) except Exception as e: print(f"TD Bridge error: {e}") def handle_message(self, msg): msg_type = msg.get("type") if msg_type in self.callbacks: self.callbacks[msg_type](msg.get("data", {})) def register_callback(self, msg_type, callback): self.callbacks[msg_type] = callback def send_to_td(self, msg_type: str, data: dict): msg = {"type": msg_type, "data": data, "timestamp": time.time()} self.socket.sendto(json.dumps(msg).encode(), self.td_addr) # Blender → TouchDesigner def setup_td_bridge(): bridge = TouchDesignerBridge() # Register callbacks def on_td_camera(data): cam = bpy.data.objects.get("TD_Camera") if cam: cam.location = data.get("location", cam.location) cam.rotation_euler = data.get("rotation", cam.rotation_euler) cam.data.lens = data.get("lens", cam.data.lens) def on_td_lights(data): for light_data in data.get("lights", []): light = bpy.data.objects.get(light_data["name"]) if light: light.location = light_data.get("location", light.location) light.data.energy = light_data.get("energy", light.data.energy) bridge = TouchDesignerBridge() bridge.register_callback("camera", on_td_camera) bridge.register_callback("lights", on_td_lights) bridge.start() # Send Blender scene info to TD def send_scene_info(): data = { "objects": [ {"name": obj.name, "location": list(obj.location), "rotation": list(obj.rotation_euler)} for obj in bpy.data.objects if obj.type in {'MESH', 'LIGHT', 'CAMERA'} ], "frame": bpy.context.scene.frame_current, "fps": bpy.context.scene.render.fps } bridge.send_to_td("scene_update", data) bpy.app.handlers.frame_change_post.append(lambda s: send_scene_info()) return bridge ``` --- ## 🔗 LINKS - **ComfyUI:** https://github.com/comfyanonymous/ComfyUI - **Unreal Live Link:** https://docs.unrealengine.com/5.3/en-US/live-link-in-unreal-engine/ - **USD Docs:** https://openusd.org/ - **MaterialX:** https://www.materialx.org/ - **TouchDesigner:** https://derivative.ca/ - **OpenPype/Ayon:** https://openpype.io/ / https://ayon.ynput.cloud/ --- ## 🌍 MULTILANG | Language | File | |----------|------| | English | `multilang/en/integrations/comfyui_blender.md` | | Russian | `multilang/ru/integrations/comfyui_blender.md` | --- # MCP ADVANCED: STREAMING & TOOL COMPOSITION Tags: #mcp # MCP ADVANCED: STREAMING & TOOL COMPOSITION **Advanced MCP patterns for production AI agents working with Blender** --- ## 🎯 STREAMING RESPONSES ### Real-time Tool Output ```python # MCP Client with streaming support class StreamingMCPClient: def __init__(self, server_cmd: list[str]): self.process = await asyncio.create_subprocess_exec( *server_cmd, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) async def stream_tool(self, tool_name: str, params: dict): """Yield partial results as they arrive""" request = { "jsonrpc": "2.0", "id": uuid.uuid4().hex, "method": "tools/call", "params": {"name": tool_name, "arguments": params} } self.process.stdin.write((json.dumps(request) + "\n").encode()) await self.process.stdin.drain() buffer = "" async for line in self.process.stdout: line = line.decode().strip() if not line: continue try: response = json.loads(line) if "result" in response: # Stream partial content if isinstance(response["result"], str): for chunk in response["result"].split(): yield chunk + " " else: yield json.dumps(response["result"]) break except json.JSONDecodeError: continue # Incomplete JSON, wait for more ``` ### Progressive Scene Building ```python # Build complex scene incrementally with feedback async def build_scene_progressively(client, scene_description: str): steps = [ ("Analyzing requirements", analyze_requirements), ("Creating base geometry", create_base_geometry), ("Adding materials", apply_materials), ("Setting up lighting", setup_lighting), ("Configuring camera", setup_camera), ("Final render", render_scene), ] for step_name, step_func in steps: yield {"status": "progress", "step": step_name} result = await step_func() yield {"status": "step_complete", "step": step_name, "result": result} yield {"status": "complete", "scene": "ready"} ``` --- ## 🔗 TOOL COMPOSITION (PIPELINES) ### Sequential Pipeline ```python class MCPipeline: def __init__(self, client): self.client = client self.steps = [] def add_step(self, tool_name: str, params_map: Callable): """params_map: (prev_result) -> next_params""" self.steps.append((tool_name, params_map)) return self async def run(self, initial_params: dict): result = initial_params for tool_name, params_map in self.steps: params = params_map(result) result = await self.client.call_tool(tool_name, params) if "error" in result: raise PipelineError(f"{tool_name} failed: {result['error']}") return result # Usage: Search → Download → Import → Material pipeline = MCPipeline(client) \ .add_step("search_polyhaven_assets", lambda r: { "asset_type": "models", "categories": ["chair"], "query": r["query"] }) \ .add_step("download_polyhaven_asset", lambda r: { "asset_id": r["results"][0]["id"], "asset_type": "models", "resolution": "2k" }) \ .add_step("set_material", lambda r: { "object_name": r["imported_object"], "material_name": "Mat_Chair", "color": [0.8, 0.6, 0.4, 1.0] }) result = await pipeline.run({"query": "modern office chair"}) ``` ### Parallel Execution with Dependency Graph ```python class DependencyGraph: def __init__(self): self.nodes = {} # tool_name -> {params, deps: []} def add(self, name: str, params: dict, depends_on: list[str] = None): self.nodes[name] = {"params": params, "deps": depends_on or []} async def execute(self, client): completed = {} pending = set(self.nodes.keys()) while pending: # Find ready nodes (all deps completed) ready = [ name for name in pending if all(dep in completed for dep in self.nodes[name]["deps"]) ] if not ready: raise CircularDependencyError() # Execute ready nodes in parallel tasks = [ client.call_tool(name, self.nodes[name]["params"]) for name in ready ] results = await asyncio.gather(*tasks) for name, result in zip(ready, results): completed[name] = result pending.remove(name) return completed # Usage: Parallel asset downloads graph = DependencyGraph() graph.add("download_chair", {"asset_id": "chair_01", "type": "models"}, []) graph.add("download_table", {"asset_id": "table_01", "type": "models"}, []) graph.add("download_lamp", {"asset_id": "lamp_01", "type": "models"}, []) graph.add("setup_scene", {"assets": ["chair", "table", "lamp"]}, ["download_chair", "download_table", "download_lamp"]) results = await graph.execute(client) ``` --- ## ⚡ ERROR HANDLING & RETRY LOGIC ### Retry with Exponential Backoff ```python class ResilientMCPClient: def __init__(self, client, max_retries=3, base_delay=1.0): self.client = client self.max_retries = max_retries self.base_delay = base_delay async def call_with_retry(self, tool_name: str, params: dict): last_error = None for attempt in range(self.max_retries + 1): try: result = await self.client.call_tool(tool_name, params) if "error" not in result: return result last_error = result["error"] except Exception as e: last_error = str(e) if attempt < self.max_retries: delay = self.base_delay * (2 ** attempt) + random.uniform(0, 0.1) await asyncio.sleep(delay) raise MCPError(f"Failed after {self.max_retries} retries: {last_error}") ``` ### Circuit Breaker Pattern ```python class CircuitBreaker: def __init__(self, failure_threshold=5, timeout=60): self.failures = 0 self.failure_threshold = failure_threshold self.timeout = timeout self.state = "closed" # closed, open, half-open self.last_failure_time = None async def call(self, func, *args, **kwargs): if self.state == "open": if time.time() - self.last_failure_time > self.timeout: self.state = "half-open" else: raise CircuitOpenError("Circuit breaker open") try: result = await func() if self.state == "half-open": self.state = "closed" self.failures = 0 return result except Exception as e: self.failures += 1 self.last_failure_time = time.time() if self.failures >= self.failure_threshold: self.state = "open" raise ``` --- ## 🔒 SECURITY & SANDBOXING ### Blender-MCP Safe Mode Configuration ```python # Server-side safe mode enforcement class SafeModeValidator: FORBIDDEN_MODULES = { 'os', 'sys', 'subprocess', 'socket', 'urllib', 'requests', 'importlib', 'pkgutil', 'runpy', 'zipimport', 'threading', 'multiprocessing', 'asyncio', 'ctypes', 'cffi', 'crypt', 'hashlib', 'secrets', 'tempfile', 'shutil', 'glob', 'fnmatch', 'linecache', 'pickle', 'shelve', 'dbm', 'sqlite3', 'csv', 'configparser', 'tomllib', 'json', 'xml', 'yaml', } FORBIDDEN_BUILTINS = { 'eval', 'exec', 'compile', 'open', '__import__', 'getattr', 'setattr', 'delattr', 'globals', 'locals', 'vars', 'dir', 'help', 'breakpoint', } FORBIDDEN_BPY = { 'bpy.ops.wm.open_mainfile', 'bpy.ops.wm.save_as_mainfile', 'bpy.ops.wm.read_factory_settings', 'bpy.ops.script.python_file_run', 'bpy.ops.text.run_script', 'bpy.app.handlers', 'bpy.app.timers', 'bpy.app.driver_namespace', 'bpy.utils.register_class', 'bpy.utils.unregister_class', 'bpy.types', 'bpy.data.libraries.load', 'bpy.data.texts.new', 'bpy.data.texts.remove', } @classmethod def validate_code(cls, code: str) -> tuple[bool, str]: tree = ast.parse(code) for node in ast.walk(tree): # Check imports if isinstance(node, (ast.Import, ast.ImportFrom)): for alias in node.names: if alias.name.split('.')[0] in cls.FORBIDDEN_MODULES: return False, f"Forbidden import: {alias.name}" # Check builtin calls if isinstance(node, ast.Call): if isinstance(node.func, ast.Name): if node.func.id in cls.FORBIDDEN_BUILTINS: return False, f"Forbidden builtin: {node.func.id}" elif isinstance(node.func, ast.Attribute): full_name = ast.unparse(node.func) if any(full_name.startswith(f) for f in cls.FORBIDDEN_BPY): return False, f"Forbidden bpy: {full_name}" return True, "OK" ``` ### Client-Side Permission Model ```python class PermissionManager: PERMISSIONS = { "read_scene": ["get_scene_info", "get_object_info", "get_viewport_screenshot"], "modify_scene": ["create_object", "modify_object", "delete_object"], "materials": ["set_material", "create_material"], "assets": ["download_polyhaven_asset", "search_polyhaven_assets"], "ai_generation": ["create_rodin_job", "poll_rodin_job_status"], "code_execution": ["execute_blender_code"], "admin": ["get_addon_info", "set_telemetry_consent"], } def __init__(self, allowed_roles: list[str]): self.allowed = set() for role in allowed_roles: self.allowed.update(self.PERMISSIONS.get(role, [])) def check(self, tool_name: str) -> bool: return tool_name in self.allowed def filter_tools(self, all_tools: list) -> list: return [t for t in all_tools if t["name"] in self.allowed] ``` --- ## 📊 OBSERVABILITY & LOGGING ### Structured Logging for MCP ```python import structlog logger = structlog.get_logger() class ObservableMCPClient: def __init__(self, client): self.client = client self.logger = structlog.get_logger("mcp.client") async def call_tool(self, tool_name: str, params: dict): start = time.time() request_id = uuid.uuid4().hex[:8] self.logger.info("mcp_request", request_id=request_id, tool=tool_name, params_keys=list(params.keys()) ) try: result = await self.client.call_tool(tool_name, params) duration = time.time() - start self.logger.info("mcp_response", request_id=request_id, tool=tool_name, duration_ms=round((time.time() - start) * 1000, 2), success="error" not in result ) return result except Exception as e: self.logger.error("mcp_error", request_id=request_id, tool=tool_name, error=str(e), duration_ms=round((time.time() - start) * 1000, 2) ) raise ``` --- ## 🧪 TESTING MCP TOOLS ### Unit Test Framework ```python import pytest from unittest.mock import AsyncMock, MagicMock class TestMCPTools: @pytest.fixture def mock_client(self): client = MagicMock() client.call_tool = AsyncMock() return client @pytest.mark.asyncio async def test_create_object(self, mock_client): mock_client.call_tool.return_value = { "name": "Cube", "type": "MESH", "location": [0, 0, 0] } result = await create_object(mock_client, "CUBE", "TestCube", [1, 2, 3]) assert result["name"] == "TestCube" mock_client.call_tool.assert_called_once_with( "create_object", {"entity_type": "CUBE", "name": "TestCube", "location": [1, 2, 3]} ) @pytest.mark.asyncio async def test_retry_on_failure(self, mock_client): mock_client.call_tool.side_effect = [ Exception("Network error"), {"name": "Cube", "type": "MESH"} ] client = ResilientMCPClient(mock_client, max_retries=2) result = await client.call_with_retry("create_object", {}) assert result["name"] == "Cube" assert mock_client.call_tool.call_count == 2 # Integration test with real Blender @pytest.mark.integration @pytest.mark.asyncio async def test_full_scene_creation(blender_mcp_client): """Requires running Blender with MCP server""" result = await blender_mcp_client.call_tool("get_scene_info", {}) assert "objects" in result assert isinstance(result["objects"], list) ``` --- ## 🔗 LINKS - **MCP Spec:** https://modelcontextprotocol.io/ - **FastMCP:** https://github.com/jlowin/fastmcp - **Blender-MCP:** https://github.com/ahujasid/blender-mcp - **GenesisCore:** https://github.com/AIGODLIKE/GenesisCore - **Structlog:** https://www.structlog.org/ --- ## 🌍 MULTILANG | Language | File | |----------|------| | English | `multilang/en/mcp_advanced/streaming_tools.md` | | Russian | `multilang/ru/mcp_advanced/streaming_tools.md` | --- # PERFORMANCE: PROFILING, MEMORY, GPU OPTIMIZATION Tags: #performance # PERFORMANCE: PROFILING, MEMORY, GPU OPTIMIZATION **Production-grade performance optimization for Blender** --- ## 🎯 PERFORMANCE PYRAMID ``` ┌─────────────────┐ │ ALGORITHM │ ← Biggest impact (10-100x) │ (Data structs) │ ├───────────────────┤ ├────────────────────────────────────┤ │ │ DATA LAYOUT │ ← Memory access patterns (2-10x) │ │ ARCHITECTURE │ (SoA vs AoS) │ │ ├───────────────────┤ ├────────────────────────────────────┤ │ │ PARALLELISM │ ← CPU/GPU utilization (2-100x) │ │ EXECUTION │ (SIMD/GPU) │ │ ├───────────────────┤ ├────────────────────────────────────┤ │ │ CACHING │ ← Reuse computed results (2-10x) │ │ OPTIMIZATION │ (Memoize/LOD) │ │ ├───────────────────┤ ├────────────────────────────────────┤ │ │ MICRO-OPT │ ← Instruction-level (1.1-2x) │ │ MICRO │ (SIMD/branch) │ │ └───────────────────┘ └──────────────────────────────────┘ ``` --- ## 🔬 PROFILING TOOLS ### Built-in Profiling ```python import cProfile import pstats import io from contextlib import contextmanager @contextmanager def profile_context(name="profile"): profiler = cProfile.Profile() profiler.enable() try: yield profiler finally: profiler.disable() stream = io.StringIO() stats = pstats.Stats(profiler, stream=stream).sort_stats('cumulative') stats.print_stats(30) print(f"\n=== {name} Profile ===") print(stream.getvalue()) # Usage with profile_context("Heavy Operation"): result = heavy_operation() ``` ### Blender-Specific Profiling ```python import bpy import time import bmesh def time_operator(operator_id, *args, **kwargs): """Time a Blender operator""" start = time.perf_counter() result = getattr(bpy.ops, operator_id)(*args, **kwargs) elapsed = time.perf_counter() - start print(f"{operator_id}: {elapsed:.4f}s") return result def profile_mesh_ops(obj, operations): """Profile BMesh operations""" bm = bmesh.new() bm.from_mesh(obj.data) for op_name, op_func in operations: start = time.perf_counter() op_func(bm) elapsed = time.perf_counter() - start print(f"BMesh {op_name}: {elapsed:.4f}s") bm.to_mesh(obj.data) bm.free() # Profile viewport performance def profile_viewport_redraw(iterations=100): area = next(a for a in bpy.context.screen.areas if a.type == 'VIEW_3D') region = next(r for r in area.regions if r.type == 'WINDOW') start = time.perf_counter() for _ in range(iterations): area.tag_redraw() elapsed = time.perf_counter() - start print(f"{iterations} redraws: {elapsed:.4f}s ({elapsed/iterations*1000:.2f}ms each)") ``` ### Memory Profiling ```python import sys import gc import psutil def get_memory_usage(): """Get current memory usage in MB""" process = psutil.Process() return process.memory_info().rss / 1024 / 1024 def profile_memory_growth(iterations=100): """Track memory growth over iterations""" gc.collect() initial = get_memory_usage() for i in range(iterations): # Your operation here bpy.ops.mesh.primitive_cube_add() bpy.ops.object.delete() if i % 10 == 0: gc.collect() current = get_memory_usage() print(f"Iteration {i}: {current - initial:.2f} MB growth") def find_memory_leaks(): """Find objects not being garbage collected""" import gc gc.collect() before = len(gc.get_objects()) # Your operation for _ in range(100): bpy.ops.mesh.primitive_cube_add() bpy.ops.object.delete() gc.collect() after = len(gc.get_objects()) print(f"Object count delta: {after - before}") if after - before > 100: print("⚠️ Possible memory leak!") # Find what's leaking types_count = {} for obj in gc.get_objects(): t = type(obj).__name__ types_count[t] = types_count.get(t, 0) + 1 for t, count in sorted(types_count.items(), key=lambda x: -x[1])[:20]: print(f" {t}: {count}") # GPU Memory (NVIDIA) def get_gpu_memory(): try: import pynvml pynvml.nvmlInit() handle = pynvml.nvmlDeviceGetHandleByIndex(0) info = pynvml.nvmlDeviceGetMemoryInfo(handle) return { 'total': info.total / 1024**3, 'used': info.used / 1024**3, 'free': info.free / 1024**3, } except: return {'error': 'pynvml not available or no NVIDIA GPU'} ``` --- ## 🧠 MEMORY MANAGEMENT ### Blender Memory Patterns ```python # ✅ GOOD: Reuse data structures def good_pattern(): bm = bmesh.new() for i in range(1000): bmesh.ops.create_cube(bm, size=1.0) bm.clear() # Reuse! bm.free() # ❌ BAD: Create/destroy in loop def bad_pattern(): for i in range(1000): bm = bmesh.new() bmesh.ops.create_cube(bm, size=1.0) bm.free() # Alloc/free overhead # ✅ GOOD: Batch operations def batch_create_cubes(count): bm = bmesh.new() verts = [] for i in range(count): # Add vertices directly pass bm.to_mesh(mesh) bm.free() # ❌ BAD: Individual operators def slow_create_cubes(count): for i in range(count): bpy.ops.mesh.primitive_cube_add() # Slow! # ✅ GOOD: Apply scale once def apply_transforms_once(objects): with bpy.context.temp_override(selected_objects=objects): bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) # ❌ BAD: Individual apply def slow_apply(objects): for obj in objects: bpy.context.view_layer.objects.active = obj bpy.ops.object.transform_apply(scale=True) ``` ### Data Structure Optimization ```python # ✅ Structure of Arrays (SoA) - Cache friendly class ParticleSystem_SOA: def __init__(self, count): self.positions = [0.0] * (count * 3) # [x0,y0,z0, x1,y1,z1, ...] self.velocities = [0.0] * (count * 3) self.masses = [1.0] * count def update(self, dt): for i in range(len(self.masses)): i3 = i * 3 self.positions[i3] += self.velocities[i3] * dt self.positions[i3+1] += self.velocities[i3+1] * dt self.positions[i3+2] += self.velocities[i3+2] * dt # ❌ Array of Structures (AoS) - Cache misses class ParticleSystem_AOS: def __init__(self, count): self.particles = [ {'pos': [0,0,0], 'vel': [0,0,0], 'mass': 1.0} for _ in range(count) ] # NumPy for heavy math import numpy as np def numpy_particle_update(positions, velocities, dt): """Vectorized particle update""" positions += velocities * dt # Vectorized! # Blender + NumPy bridge def bmesh_to_numpy(bm): """Convert BMesh to NumPy arrays""" verts = np.empty((len(bm.verts), 3), dtype=np.float32) for i, v in enumerate(bm.verts): verts[i] = v.co return verts def numpy_to_bmesh(verts, bm): """Update BMesh from NumPy""" for i, v in enumerate(bm.verts): v.co = verts[i] bmesh.update_edit_mesh(bm.id_data) ``` --- ## ⚡ PARALLELISM & GPU ### Python Multiprocessing ```python from multiprocessing import Pool, cpu_count import bpy def process_object(obj_name): """Process single object in worker""" import bpy obj = bpy.data.objects[obj_name] # Process... return obj_name, result def parallel_process(objects, worker_func, workers=None): workers = workers or cpu_count() with Pool(workers) as pool: results = pool.map(worker_func, [o.name for o in objects]) return dict(results) # For Blender: Use separate processes (not threads due to GIL) def batch_render_scenes(scenes, workers=4): import subprocess def render_scene(scene_file): result = subprocess.run([ 'blender', '-b', scene_file, '-P', 'render_script.py' ], capture_output=True, text=True) return result.returncode == 0 with Pool(workers) as pool: results = pool.map(render_scene, scenes) return all(results) ``` ### GPU Compute (Experimental) ```python # Blender 4.2+ Compute Shaders # Requires: Blender 4.2+, Vulkan/Metal/OpenCL # Example: Particle simulation on GPU compute_shader = """ #version 450 layout(local_size_x = 256) in; layout(std430, binding = 0) buffer Particles { vec4 position[]; vec4 velocity[]; }; layout(push_constant) uniform PushConstants { float dt; vec3 gravity; } pc; void main() { uint i = gl_GlobalInvocationID.x; if (i >= particle_count) return; velocity[i] += pc.gravity * pc.dt; position[i] += velocity[i] * pc.dt; } """ # Python side def dispatch_compute_shader(shader, particle_count, dt, gravity): import gpu shader = gpu.shader.from_spirv(compute_shader) # ... dispatch logic ``` ### NumPy/SciPy for Heavy Math ```python import numpy as np from scipy.spatial import cKDTree from scipy.spatial.transform import Rotation # Fast nearest neighbor def find_neighbors(points, query_points, k=10): tree = cKDTree(points) distances, indices = tree.query(query_points, k=k) return distances, indices # Fast rotation def rotate_points(points, axis, angle): rot = Rotation.from_rotvec(axis * angle) return rot.apply(points) # Fast mesh operations def mesh_boolean_union(mesh1, mesh2): # Use OpenVDB or CGAL via Python bindings import pyopenvdb as vdb # ... boolean operations ``` --- ## 📊 RENDER PERFORMANCE ### Cycles Optimization ```python def optimize_cycles(scene): s = scene.cycles # Adaptive Sampling (huge speedup) s.use_adaptive_sampling = True s.adaptive_threshold = 0.01 s.adaptive_min_samples = 32 # Denoising s.use_denoising = True s.denoiser = 'OPTIX' # 'OPENIMAGEDENOISE' / 'OPTIX' s.denoising_prefilter = 'ACCURATE' # Light Paths (reduce for speed) s.max_bounces = 8 s.diffuse_bounces = 3 s.glossy_bounces = 3 s.transmission_bounces = 8 s.volume_bounces = 2 # Caustics (expensive) s.caustics_reflective = False s.caustics_refractive = False # Light Sampling s.use_light_tree = True s.sample_light_direct = True # Motion Blur scene.render.use_motion_blur = True scene.cycles.motion_blur_shutter = 0.5 scene.cycles.motion_blur_samples = 8 ``` ### EEVEE Next Optimization ```python def optimize_eevee_next(scene): s = scene.eevee # Shadows s.shadow_cube_size = '1024' # 512, 1024, 2048, 4096 s.shadow_cascade_size = '1024' s.shadow_clip_start = 0.1 s.shadow_clip_end = 100.0 # GI s.gi_cubemap_resolution = '512' s.gi_cache_resolution = '256' s.gi_diffuse_bounces = 1 # Volumetrics s.volumetric_tile_size = '64' s.volumetric_samples = 16 # SSR s.use_ssr = True s.use_ssr_refraction = True s.ssr_quality = 0.5 s.ssr_thickness = 0.01 ``` --- ## 📦 ASSET OPTIMIZATION ### LOD System ```python def create_lod_system(base_obj, lod_ratios=[1.0, 0.5, 0.25, 0.1]): """Create LOD hierarchy""" lods = [base_obj] for i, ratio in enumerate(lod_ratios[1:], 1): lod = base_obj.copy() lod.data = base_obj.data.copy() lod.name = f"{base_obj.name}_LOD{i}" bpy.context.collection.objects.link(lod) # Decimate mod = lod.modifiers.new(f"Decimate_LOD{i}", 'DECIMATE') mod.ratio = ratio mod.use_collapse_triangulate = True bpy.context.view_layer.objects.active = lod bpy.ops.object.modifier_apply(modifier=mod.name) lods.append(lod) return lods # Distance-based LOD switching (Geometry Nodes) """ GN Tree: LOD Switch Input: Distance to Camera Logic: Distance < 50m: LOD0 (100%) 50-200m: LOD1 (50%) 200-500m: LOD2 (25%) 500m+: LOD3 (10% or Billboard) """ ``` ### Billboard Impostors ```python def create_impostor(obj, angles=8, resolution=2048): """Create billboard impostor for distant LOD""" import math # Render from multiple angles renders = [] for i in range(angles): angle = (i / angles) * 2 * math.pi # Render from angle # Save as sprite sheet # Create sprite sheet # Shader: Billboard + View Angle → Sprite Index ``` --- ## 📈 MONITORING & BENCHMARKING ### Automated Benchmarking ```python import json from datetime import datetime class BenchmarkSuite: def __init__(self): self.results = [] def benchmark(self, name, func, *args, **kwargs): import time gc.collect() start = time.perf_counter() result = func(*args, **kwargs) elapsed = time.perf_counter() - elapsed self.results.append({ 'name': name, 'time': elapsed, 'timestamp': datetime.now().isoformat(), 'blender_version': bpy.app.version_string, }) return result def save(self, filepath): with open(filepath, 'w') as f: json.dump(self.results, f, indent=2) # Benchmark Suite suite = BenchmarkSuite() # Mesh operations suite.benchmark("create_cube_1000", lambda: [bpy.ops.mesh.primitive_cube_add() for _ in range(1000)]) suite.benchmark("boolean_union_100", lambda: [boolean_union(c1, c2) for _ in range(100)]) suite.benchmark("subdivision_100", lambda: [subdivide(obj, 2) for _ in range(100)]) # Render suite.benchmark("render_1080p_128spp", lambda: render_frame(1920, 1080, 128)) suite.benchmark("render_4k_256spp", lambda: render_frame(3840, 2160, 256)) # Save suite.save("benchmark_results.json") ``` ### Continuous Benchmarking (CI/CD) ```yaml # .github/workflows/benchmark.yml name: Performance Benchmark on: [push, pull_request] jobs: benchmark: runs-on: ubuntu-latest container: image: nvidia/cuda:12.1-devel-ubuntu22.04 options: --gpus all steps: - uses: actions/checkout@v4 - name: Install Blender run: | apt-get update && apt-get install -y blender - name: Run Benchmarks run: python scripts/run_benchmarks.py - name: Compare with baseline run: python scripts/compare_benchmarks.py - name: Comment PR uses: actions/github-script@v7 with: script: | // Post benchmark results as PR comment ``` --- ## 🔗 LINKS - **Blender Performance:** https://docs.blender.org/manual/en/latest/advanced/performance/ - **Python Profiling:** https://docs.python.org/3/library/profile.html - **NumPy Performance:** https://numpy.org/doc/stable/user/basics.performance.html - **Vulkan Compute (archived):** https://github.com/GPUOpen-LibrariesAndSDKs/VulkanSamples - **Blender Benchmark:** https://opendata.blender.org/ > [!NOTE] > - Blender Performance Handbook переехал в официальную документацию: https://docs.blender.org/manual/en/latest/advanced/performance/ > - Репозиторий GPUOpen VulkanSamples архивирован; актуальные примеры Vulkan ищите в https://github.com/khronosgroup/vulkan-samples > - NumPy Performance Guide обновлён: https://numpy.org/doc/stable/user/basics.performance.html --- ## 🌍 MULTILANG | Language | File | |----------|------| | English | `multilang/en/performance/profiling.md` | | Russian | `multilang/ru/performance/profiling.md` | --- # PIPELINE & STUDIO: OPENPYPE / AYON Tags: #pipeline # PIPELINE & STUDIO: OPENPYPE / AYON **Production pipeline management for Blender studios** --- ## 🎯 WHAT IS OPENPYPE / AYON? | Feature | OpenPype | AYON | |---------|----------|------| | **Status** | Mature, stable | Active development (OpenPype successor) | | **Architecture** | Python, MongoDB, Qt | Python, PostgreSQL, React/Qt | | **DCC Support** | Blender, Maya, Houdini, Nuke, 3dsMax, etc. | Same + more | | **Key Feature** | Project anatomy, publishing, versioning | Modern UI, better API, cloud-ready | | **License** | LGPL | Apache 2.0 | --- ## 🏗️ ARCHITECTURE ``` ┌─────────────────────────────────────────────────────────────┐ │ SERVER │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ Mongo │ │ REST │ │ Web UI │ │ Events │ │ │ │ DB │ │ API │ │ │ │ Bus │ │ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ └─────────────────────────────────────────────────────────────┘ │ ┌─────────────────────┼─────────────────────┐ ▼ ▼ ▼ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ BLENDER │ │ HOUDINI │ │ NUKE │ │ (Addon) │ │ (Package) │ │ (Menu) │ └───────────────┘ └───────────────┘ └───────────────┘ ``` --- ## 🚀 INSTALLATION ### Server (Docker) ```yaml # docker-compose.yml version: '3.8' services: mongo: image: mongo:6 volumes: - mongo_data:/data/db ports: - "27017:27017" openpype: image: ynput/openpype:latest environment: - MONGO_URL=mongodb://mongo:27017 - OPENPYPE_MONGO_DATABASE=openpype - OPENPYPE_SECRET=your-secret-key ports: - "8080:8080" volumes: - ./config:/app/config - ./data:/app/data depends_on: - mongo volumes: mongo_data: ``` ### Client (Blender Addon) ```bash # Install via OpenPype Launcher # 1. Download OpenPype Launcher from GitHub Releases # 2. Run launcher, login to server # 3. Click "Install Blender Addon" # 4. Select Blender executable # 5. Restart Blender # Or manual install: # 1. Download OpenPype client zip # 2. Extract to: ~/.config/blender/4.2/scripts/addons/openpype # 3. Enable in Preferences → Add-ons ``` --- ## ⚙️ CONFIGURATION ### Project Anatomy (Project Structure) ```yaml # config/anatomy.yaml project: name: "MyProject" code: "MPJ" anatomy: roots: work: "/mnt/storage/projects/MyProject/work" publish: "/mnt/storage/projects/MyProject/publish" scratch: "/tmp/openpype_scratch" templates: # File naming work_file: "{root[work]}/{project[name]}/{asset[type]}/{asset[name]}/work/{task[name]}/v{version:03d}/{asset[name]}_{task[name]}_v{version:03d}.{ext}" publish_file: "{root[publish]}/{project[name]}/{asset[type]}/{asset[name]}/publish/{subset[name]}/v{version:03d}/{subset[name]}_v{version:03d}.{ext}" # Folder structure folder: "{root[work]}/{project[name]}/{asset[type]}/{asset[name]}/{task[name]}" # Naming conventions version_padding: 3 frame_padding: 4 # Asset types asset_types: - character - prop - environment - vehicle - fx - camera - rig # Tasks tasks: - modeling - rigging - surfacing - layout - animation - fx - lighting - compositing # Subsets (what gets published) subsets: modelMain: families: ["model"] tasks: ["modeling"] extensions: [".abc", ".usd", ".fbx", ".obj"] modelProxy: families: ["model"] tasks: ["modeling"] extensions: [".abc", ".usd"] lookDev: families: ["lookdev"] tasks: ["surfacing"] extensions: [".ma", ".usd", ".ass"] rigMain: families: ["rig"] tasks: ["rigging"] extensions: [".ma", ".usd"] animation: families: ["animation"] tasks: ["animation"] extensions: [".abc", ".usd"] layout: families: ["layout"] tasks: ["layout"] extensions: [".abc", ".usd"] fx: families: ["fx"] tasks: ["fx"] extensions: [".abc", ".vdb", ".bgeo.sc"] lighting: families: ["lighting"] tasks: ["lighting"] extensions: [".ma", ".usd"] comp: families: ["comp"] tasks: ["compositing"] extensions: [".nk", ".exr", ".mov"] ``` --- ## 🔌 BLENDER ADDON SETUP ### Installation & Setup ```python # In Blender after installing OpenPype addon: # 1. Preferences → Add-ons → OpenPype → Configure # 2. Set Server URL: http://localhost:8080 # 3. Login with credentials # 4. Select Project # 4. OpenPype menu appears in top bar # Auto-setup on Blender start import bpy def setup_openpype(): import openpype.hosts.blender.api as op_blender # Auto-connect to server op_blender.connect_to_server("http://localhost:8080") # Set project op_blender.set_current_project("MyProject") # Load tools op_blender.install_tools() bpy.app.handlers.load_post.append(lambda _: setup_openpype()) ``` --- ## 📦 PUBLISHING WORKFLOW ### Publisher UI (Blender) ```python # Publisher is built into OpenPype Blender addon # Access via: OpenPype → Publisher # Publish Steps: # 1. Select instances to publish # 2. Validate (auto-runs validators) # 4. Review validation results # 5. Click "Publish" # 6. Check progress in UI # 7. Verify in OpenPype Web UI # Custom Validators class ValidateMeshNonManifold(pyblish.api.InstancePlugin): """Check for non-manifold geometry""" order = pyblish.api.ValidatorOrder families = ["model"] def process(self, instance): import bpy import bmesh obj = instance.data["object"] bm = bmesh.new() bm.from_mesh(obj.data) non_manifold = [e for e in bm.edges if not e.is_manifold] bm.free() if non_manifold: raise ValueError(f"Non-manifold edges found: {len(non_manifold)}") # Custom Extractors class ExtractAlembic(pyblish.api.Extractor): """Export Alembic cache""" order = pyblish.api.ExtractorOrder families = ["animation", "fx"] def extract(self, instance): import bpy from pathlib import Path staging_dir = Path(self.staging_dir(instance)) filepath = staging_dir / f"{instance.name}.abc" # Select objects bpy.ops.object.select_all(action='DESELECT') for obj in instance.data["objects"]: obj.select_set(True) bpy.context.view_layer.objects.active = instance.data["objects"][0] # Export Alembic bpy.ops.wm.alembic_export( filepath=str(filepath), selected=True, start=instance.context.data["frameStart"], end=instance.context.data["frameEnd"], selected=True, flatten=True, subsamples=4, ) # Add to representations if "representations" not in instance.data: instance.data["representations"] = [] instance.data["representations"].append({ "name": "abc", "ext": "abc", "files": filepath.name, "stagingDir": str(staging_dir), ) ``` --- ## 🎯 WORKFLOW EXAMPLES ### Character Pipeline ```python # Character Publishing Pipeline """ 1. Modeling → Publish "modelMain" (Alembic/USD) ↓ 2. Rigging → Reference modelMain → Publish "rigMain" (Maya/USD) ↓ 3. Surfacing → Reference modelMain → Publish "lookDev" (Materials + Textures) ↓ 4. Animation → Reference rigMain → Publish "animation" (Alembic/USD) ↓ 5. Lighting → Reference animation + lookDev → Publish "lighting" (USD) ↓ 6. Compositing → Publish "comp" (EXR + NK) """ # Publish Template class PublishCharacter(pyblish.api.ContextPlugin): order = pyblish.api.ContextPluginOrder def process(self, context): # Define what to publish for a character instances = [ { "name": "hero_modelMain", "family": "model", "subset": "modelMain", "objects": ["hero_geo"], }, { "name": "hero_rigMain", "family": "rig", "subset": "rigMain", "objects": ["hero_rig"], }, { "name": "hero_lookDev", "family": "lookdev", "subset": "lookDev", "objects": ["hero_geo"], }, ] for inst_data in instances: instance = context.create_instance(inst_data["name"]) instance.data.update({ "family": inst_data["family"], "subset": inst_data["subset"], "objects": inst_data["objects"], }) ``` --- ## 🔧 AYON SPECIFICS (Next Gen) ### AYON Advantages ```yaml # AYON improvements over OpenPype: - Modern React-based Web UI - GraphQL API instead of REST - PostgreSQL instead of MongoDB - Better multi-site support - Built-in review tools (ftrack-style) - Improved artist UI in DCCs - Better plugin system - Cloud-ready (Kubernetes) ``` ### AYON Blender Addon ```python # AYON Blender addon (similar to OpenPype but improved) # Installation: Same process via AYON Launcher # Key differences: # - Uses GraphQL API # - Better settings management # - Improved publisher UI - Better dependency tracking - Integrated review tools ``` --- ## 📊 PROJECT MANAGEMENT ### Shot Management ```python # Shot Pipeline class ShotManager: def __init__(self, project_name): self.project = project_name def create_shot(self, sequence, shot_number, description=""): """Create shot entity in OpenPype/AYON""" shot_name = f"{sequence}{shot_number:03d}" # Create folder structure shot_path = Path(f"shots/{sequence}/{shot_name}") shot_path.mkdir(parents=True, exist_ok=True) # Create entity in OpenPype # (via API) return shot_name def assign_tasks(self, shot_name, tasks): """Assign tasks to artists""" for task_name, artist in tasks.items(): # Create task entity # Assign to artist pass def get_shot_status(self, shot_name): """Get shot progress""" # Query OpenPype for task statuses pass ``` --- ## 🚀 DEPLOYMENT ### Production Deployment ```yaml # docker-compose.prod.yml version: '3.8' services: postgres: image: postgres:15 environment: POSTGRES_DB: ayon POSTGRES_USER: ayon POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} volumes: - postgres_data:/var/lib/postgresql/data deploy: resources: limits: memory: 2G ayon-server: image: ynput/ayon-server:latest environment: - POSTGRES_HOST=postgres - POSTGRES_DB=ayon - POSTGRES_USER=ayon - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - AYON_SECRET=${AYON_SECRET} - AYON_ADMIN_EMAIL=${ADMIN_EMAIL} - AYON_ADMIN_PASSWORD=${ADMIN_PASSWORD} ports: - "8080:8080" depends_on: - postgres deploy: replicas: 2 resources: limits: memory: 4G reservations: memory: 2G ayon-frontend: image: ynput/ayon-frontend:latest ports: - "80:80" depends_on: - ayon-server volumes: postgres_data: ``` --- ## 🔗 LINKS - **OpenPype:** https://openpype.io/ - **AYON:** https://ayon.ynput.cloud/ - **OpenPype Docs:** https://openpype.io/docs/ - **AYON Docs:** https://docs.ayon.ynput.cloud/ - **GitHub:** https://github.com/ynput/openpype / https://github.com/ynput/ayon - **Discord:** https://discord.gg/openpype --- ## 🌍 MULTILANG | Language | File | |----------|------| | English | `multilang/en/pipeline/openpype_ayon.md` | | Russian | `multilang/ru/pipeline/openpype_ayon.md` | --- # P07: INSTANCING & ARRAYS Tags: #array #collection #geometry #geometry-nodes #instances #modeling #procedural #prompt # P07: INSTANCING & ARRAYS **ID:** P07 | **Cluster:** Geometry Basics | **Difficulty:** ⭐⭐ | **Tags:** #instances #array #collection #geometry-nodes #procedural --- ## 🎯 GOAL Efficient duplication and distribution of objects using Array modifier, Collection instances, and Geometry Nodes --- ## 📝 PROMPT (RUS) ``` Создай массив/распределение "[OBJECT_NAME]": Метод: ARRAY_MODIFIER / COLLECTION_INSTANCE / GEOMETRY_NODES Параметры: count, offset, randomize, scale, rotation ``` --- ## 📝 PROMPT (ENG) ``` Create array/distribution of "[OBJECT_NAME]": Method: ARRAY_MODIFIER / COLLECTION_INSTANCE / GEOMETRY_NODES Parameters: count, offset, randomize, scale, rotation ``` --- ## 🔧 MCP: `execute_blender_code` ### ARRAY MODIFIER (Простой линейный) ```python import bpy obj = bpy.data.objects.get("Source_Obj") if not obj: raise ValueError("Object not found") # Array Modifier array = obj.modifiers.new("Array_Linear", 'ARRAY') array.count = 10 array.use_relative_offset = True array.relative_offset_displace = (2.0, 0, 0) # 2м между экземплярами # Array по кривой curve = bpy.data.curves.new("Path", 'CURVE') curve.dimensions = '3D' spline = curve.splines.new('BEZIER') spline.bezier_points.add(3) # ... настроить точки кривой curve_obj = bpy.data.objects.new("Path", curve) bpy.context.collection.objects.link(curve_obj) array2 = obj.modifiers.new("Array_Curve", 'ARRAY') array2.fit_type = 'FIT_CURVE' array2.curve = curve_obj array2.use_relative_offset = True array2.relative_offset_displace = (0, 0, 1.0) ``` ### COLLECTION INSTANCES (Лёгкие, для сцены) ```python import bpy # Создать коллекцию с вариациями col = bpy.data.collections.new("Props_Variants") for i in range(5): var = bpy.data.objects.get(f"Prop_Var_{i}") if var: col.objects.link(var) # Инстанс коллекции instance = bpy.data.objects.new("Instancer", None) instance.instance_type = 'COLLECTION' instance.instance_collection = col bpy.context.collection.objects.link(instance) # Настроить распределение (Geometry Nodes лучше) ``` ### GEOMETRY NODES (Полный контроль) ```python import bpy # Создать GN дерево gn = bpy.data.node_groups.new("Scatter_Props", 'GeometryNodeTree') nodes = gn.nodes links = gn.links nodes.clear() # Input/Output inp = nodes.new('NodeGroupInput') out = nodes.new('NodeGroupOutput') gn.interface.new_socket(name="Count", in_out='INPUT', socket_type='NodeSocketInt') gn.interface.new_socket(name="Geometry", in_out='OUTPUT', socket_type='NodeSocketGeometry') gn.interface.new_socket(name="Source", in_out='INPUT', socket_type='NodeSocketObject') inp.location = (-400, 0) out.location = (400, 0) # Nodes distribute = nodes.new('GeometryNodeDistributePointsOnFaces') distribute.location = (0, 0) distribute.distribute_method = 'POISSON' instance = nodes.new('GeometryNodeInstanceOnPoints') instance.location = (200, 0) instance.inputs['Scale'].default_value = (1, 1, 1) realize = nodes.new('GeometryNodeRealizeInstances') realize.location = (400, 0) # Links links.new(inp.outputs['Source'], distribute.inputs['Mesh']) links.new(inp.outputs['Count'], distribute.inputs['Density']) links.new(distribute.outputs['Points'], instance.inputs['Points']) links.new(inp.outputs['Source'], instance.inputs['Instance']) links.new(instance.outputs['Instances'], realize.inputs['Geometry']) links.new(realize.outputs['Geometry'], out.inputs['Geometry']) # Применить к объекту-эмиттеру emitter = bpy.data.objects.get("Ground") mod = emitter.modifiers.new("GN_Scatter", 'NODES') mod.node_group = gn mod["Input_1"] = 1000 # Count mod["Input_2"] = bpy.data.objects.get("Prop_Source") # Source ``` --- ## 📊 СРАВНЕНИЕ МЕТОДОВ | Метод | Память | Гибкость | Анимация | Сложность | |-------|--------|----------|----------|-----------| | **Array Modifier** | Низкая | Низкая | Да | ⭐ | | **Collection Instance** | Очень низкая | Средняя | Ограничено | ⭐⭐ | | **Geometry Nodes** | Низкая | Максимальная | Да | ⭐⭐⭐ | --- ## 💡 ПРО ТИПЫ ### Случайные вариации (GN) ```python # Random Scale rand_scale = nodes.new('GeometryNodeRandomValue') rand_scale.data_type = 'FLOAT_VECTOR' rand_scale.outputs[0].default_value = (0.8, 0.8, 0.8) rand_scale.outputs[1].default_value = (1.2, 1.2, 1.2) links.new(rand_scale.outputs['Value'], instance.inputs['Scale']) # Random Rotation rand_rot = nodes.new('GeometryNodeRandomValue') rand_rot.data_type = 'FLOAT_VECTOR' rand_rot.outputs[0].default_value = (0, 0, 0) rand_rot.outputs[1].default_value = (0, 0, 6.28) links.new(rand_rot.outputs['Value'], instance.inputs['Rotation']) ``` ### Weight Paint для плотности ```python # Vertex Group "Density" на эмиттере distribute.inputs['Density Attribute'].default_value = "Density" ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P23: GEOMETRY NODES BASICS](23_geometry_nodes_basics.md), [P25: PROCEDURAL BUILDING GENERATOR](25_procedural_building.md), [P45: GEOMETRY NODES SIMULATION ZONES](45_gn_simulation.md), [P01: PRIMITIVE CREATION BASICS](01_geometry_basics.md), [P03: MODIFIERS STACK](03_modifiers_stack.md) - **По темам:** [P04: MESH EDITING (BMESH)](04_mesh_editing_bmesh.md), [P18: DRIVERS & CONSTRAINTS](18_drivers_constraints.md), [P22: PARTICLE SYSTEMS](22_particles.md) - **Теги:** #array, #collection, #geometry, #geometry-nodes, #instances, #modeling, #procedural, #prompt --- # P08: SCULPTING BASICS Tags: #brushes #dyntopo #highpoly #multires #prompt #sculpt #sculpting # P08: SCULPTING BASICS **ID:** P08 | **Cluster:** Geometry Basics | **Difficulty:** ⭐⭐ | **Tags:** #sculpting #dyntopo #brushes #multires #highpoly --- ## 🎯 GOAL Create organic high-poly models using Blender's sculpting tools with Dyntopo and Multires --- ## 📝 PROMPT (RUS) ``` Создай высокополигональный скulpt "[OBJECT_NAME]" из "[BASE_MESH]": Режим: DYNTOPO (динамическая топология) / MULTIRES (подделение) Кисти: DRAW, CLAY, SMOOTH, GRAB, INFLATE, CREASE, LAYER Детализация: [DETAIL_LEVEL] ``` --- ## 📝 PROMPT (ENG) ``` Sculpt high-poly "[OBJECT_NAME]" from "[BASE_MESH]": Mode: DYNTOPO (dynamic topology) / MULTIRES (subdivision) Brushes: DRAW, CLAY, SMOOTH, GRAB, INFLATE, CREASE, LAYER Detail level: [DETAIL_LEVEL] ``` --- ## 🔧 MCP: `execute_blender_code` ### DYNTOPO SETUP ```python import bpy obj = bpy.data.objects.get("Base_Mesh") if not obj: raise ValueError("Base mesh not found") # Подготовка bpy.context.view_layer.objects.active = obj bpy.ops.object.mode_set(mode='SCULPT') # Включить Dyntopo bpy.context.scene.tool_settings.sculpt.dyntopo = True bpy.context.scene.tool_settings.sculpt.detail_type = 'CONSTANT' bpy.context.scene.tool_settings.sculpt.constant_detail = 12 # Размер детали bpy.context.scene.tool_settings.sculpt.detail_refine_method = 'SUBDIVIDE_EDGES' bpy.context.scene.tool_settings.sculpt.detail_size = 0.01 # Relative # Симметрия bpy.context.scene.tool_settings.sculpt.use_symmetry_x = True bpy.context.scene.tool_settings.sculpt.use_symmetry_y = False bpy.context.scene.tool_settings.sculpt.use_symmetry_z = False print("Dyntopo enabled. Enter Sculpt mode manually.") ``` ### MULTIRES SETUP (Для анимации) ```python obj = bpy.data.objects.get("Base_Mesh") # Добавить Multires multires = obj.modifiers.new("Multires", 'MULTIRES') bpy.ops.object.mode_set(mode='SCULPT') # Подразделить for i in range(4): bpy.ops.object.multires_subdivide(modifier="Multires", mode='CATMULL_CLARK') # Уровень скulpting obj.modifiers["Multires"].sculpt_level = 3 print(f"Multires levels: {len(obj.modifiers['Multires'].levels)}") ``` ### КЛЮЧЕВЫЕ КИСТИ И НАСТРОЙКИ ```python import bpy tool_settings = bpy.context.scene.tool_settings brush = tool_settings.sculpt.brush # Настроить кисть программно def set_brush(name, strength=0.5, radius=50, autosmooth=0.0): brush = bpy.data.brushes.get(name) if brush: tool_settings.sculpt.brush = brush brush.strength = strength brush.size = radius brush.use_autosmooth = autosmooth > 0 brush.autosmooth_factor = autosmooth # Основные кисти BRUSHES = { 'DRAW': {'strength': 0.3, 'radius': 80}, # Базовая отрисовка 'CLAY': {'strength': 0.4, 'radius': 100}, # Добавление объёма 'CLAY_STRIPS': {'strength': 0.35, 'radius': 80}, # Полоски глины 'SMOOTH': {'strength': 0.5, 'radius': 100}, # Сглаживание (Shift) 'GRAB': {'strength': 0.5, 'radius': 120}, # Перетаскивание (G) 'INFLATE': {'strength': 0.3, 'radius': 80}, # Надувание 'CREASE': {'strength': 0.4, 'radius': 40}, # Уточнение краёв 'LAYER': {'strength': 0.3, 'radius': 60}, # Слои (высота) 'SCRAPE': {'strength': 0.3, 'radius': 60}, # Скребливание 'PINCH': {'strength': 0.4, 'radius': 40}, # Сжатие краёв } for bname, params in BRUSHES.items(): set_brush(bname, **params) ``` ### MASKING И HIDING ```python # Маска (Ctrl+I инверсия) bpy.ops.sculpt.mask_clear() # Нарисовать маску кистью MASK # Скрыть маскированное: H # Показать всё: Alt+H # Face Sets (Blender 3.0+) bpy.ops.sculpt.face_set_init() # Разные цвета для разных частей ``` --- ## 📊 DYNTOPO vs MULTIRES | Характеристика | DYNTOPO | MULTIRES | |----------------|---------|----------| | **Топология** | Динамическая, меняется | Фиксированная, поделена | | **Анимация** | Нет (нужен ретопо) | Да (базовая топология сохранена) | | **Память** | Эффективна | Выше (все уровни в RAM) | | **Детализация** | Любая, локально | Ограничена уровнем подделения | | **Симметрия** | Полная | Полная | | **Export** | Нужен ретопо | Можно экспортировать уровень | --- ## 💡 ПРО ТИПЫ ### Matcap для удобства ```python # В Viewport Shading → Matcap # Рекомендуемые: clay, clay_brown, skin ``` ### Lazy Stroke для плавных линий ```python brush = bpy.data.brushes['DRAW'] brush.use_lazy_stroke = True brush.lazy_radius = 20 ``` ### Face Sets для изоляции ```python # Создать Face Set bpy.ops.sculpt.face_set_create(name="Eyes") # Изолировать: Select Face Set → H ``` --- ## ⚠️ ЧАСТЫЕ ПРОБЛЕМЫ | Проблема | Решение | |----------|---------| | Лаги при Dyntopo | Увеличь `detail_size`, уменьши `constant_detail` | | Артефакты симметрии | Проверь центр объекта, примени `Object > Set Origin > Center of Mass` | | Потеря деталей при Multires | Не скачивай уровни выше `sculpt_level` | | Дырки в меше | `Mesh > Clean Up > Fill Holes` или `Make Manifold` | --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P12: TEXTURE PAINTING](12_texture_painting.md), [P01: PRIMITIVE CREATION BASICS](01_geometry_basics.md), [P02: BOOLEAN OPERATIONS](02_boolean_operations.md), [P03: MODIFIERS STACK](03_modifiers_stack.md), [P04: MESH EDITING (BMESH)](04_mesh_editing_bmesh.md) - **По темам:** [P05: UV UNWRAPPING STRATEGIES](05_uv_unwrapping.md), [P06: RETOPOLOGY WORKFLOW](06_retopology_workflow.md), [P07: INSTANCING & ARRAYS](07_instancing_arrays.md) - **Теги:** #brushes, #dyntopo, #highpoly, #multires, #prompt, #sculpt, #sculpting --- # P10: NODE GROUP CREATION Tags: #node #node-groups #procedural #prompt #reuse #shader #shader-library #shading # P10: NODE GROUP CREATION **ID:** P10 | **Cluster:** Materials PBR | **Difficulty:** ⭐⭐ | **Tags:** #node-groups #reuse #shader-library #procedural --- ## 🎯 GOAL Create reusable shader node groups for consistent material library and procedural effects --- ## 📝 PROMPT (RUS) ``` Создай Node Group "[GROUP_NAME]" с входами/выходами: Inputs: [список: имя, тип, дефолт, min, max] Outputs: [список: имя, тип] Внутренняя логика: [описание нод] Использование: в материалах через Group Node ``` --- ## 📝 PROMPT (ENG) ``` Create Node Group "[GROUP_NAME]" with inputs/outputs: Inputs: [list: name, type, default, min, max] Outputs: [list: name, type] Internal logic: [node description] Usage: in materials via Group Node ``` --- ## 🔧 MCP: `execute_blender_code` ### БАЗОВЫЙ ШАБЛОН ```python import bpy def create_node_group(name, inputs_def, outputs_def, build_func): """ inputs_def: [("Name", "NodeSocketFloat", default, min, max), ...] outputs_def: [("Name", "NodeSocketFloat"), ...] build_func: function(nodes, links, input_node, output_node) """ # Удалить старую если есть if name in bpy.data.node_groups: bpy.data.node_groups.remove(bpy.data.node_groups[name]) ng = bpy.data.node_groups.new(name, 'ShaderNodeTree') nodes = ng.nodes links = ng.links nodes.clear() # Input/Output ноды inp = nodes.new('NodeGroupInput') inp.location = (-400, 0) out = nodes.new('NodeGroupOutput') out.location = (400, 0) # Создать сокеты for sock_def in inputs_def: sock_name, sock_type, default, min_val, max_val = sock_def sock = ng.interface.new_socket(name=sock_name, in_out='INPUT', socket_type=sock_type) sock.default_value = default sock.min_value = min_val sock.max_value = max_val for sock_def in outputs_def: sock_name, sock_type = sock_def ng.interface.new_socket(name=sock_name, in_out='OUTPUT', socket_type=sock_type) # Построить логику build_func(nodes, links, inp, out) return ng # === ПРИМЕРЫ === # 1. PBR UTILITY GROUP (Универсальный PBR) def build_pbr_utility(nodes, links, inp, out): principled = nodes.new('ShaderNodeBsdfPrincipled') principled.location = (0, 0) links.new(inp.outputs['Base Color'], principled.inputs['Base Color']) links.new(inp.outputs['Roughness'], principled.inputs['Roughness']) links.new(inp.outputs['Metallic'], principled.inputs['Metallic']) links.new(inp.outputs['Normal'], principled.inputs['Normal']) links.new(inp.outputs['Height'], principled.inputs['Displacement']) links.new(principled.outputs['BSDF'], out.inputs['BSDF']) links.new(principled.outputs['Alpha'], out.inputs['Alpha']) pbr_inputs = [ ("Base Color", "NodeSocketColor", (0.8, 0.8, 0.8, 1.0), 0, 1), ("Roughness", "NodeSocketFloat", 0.5, 0, 1), ("Metallic", "NodeSocketFloat", 0.0, 0, 1), ("Normal", "NodeSocketVector", (0, 0, 1), -1, 1), ("Height", "NodeSocketFloat", 0, -1, 1), ] pbr_outputs = [("BSDF", "NodeSocketShader"), ("Alpha", "NodeSocketFloat")] create_node_group("PBR_Utility", pbr_inputs, pbr_outputs, build_pbr_utility) # 2. EDGE WEAR GROUP (Потертости по краям) def build_edge_wear(nodes, links, inp, out): # Geometry > Pointiness geom = nodes.new('ShaderNodeNewGeometry') geom.location = (-200, 200) # Color Ramp для контроля ramp = nodes.new('ShaderNodeValToRGB') ramp.location = (0, 200) ramp.color_ramp.elements[0].position = 0.0 ramp.color_ramp.elements[0].color = (0, 0, 0, 1) # Чёрный в углублениях ramp.color_ramp.elements[1].position = 0.5 ramp.color_ramp.elements[1].color = (1, 1, 1, 1) # Белый на краях links.new(geom.outputs['Pointiness'], ramp.inputs['Fac']) # Mix с Base Color mix = nodes.new('ShaderNodeMixRGB') mix.blend_type = 'MULTIPLY' mix.location = (200, 0) links.new(ramp.outputs['Color'], mix.inputs['Fac']) links.new(inp.outputs['Base Color'], mix.inputs[1]) links.new(inp.outputs['Edge Color'], mix.inputs[2]) links.new(mix.outputs['Color'], out.inputs['Color']) edge_inputs = [ ("Base Color", "NodeSocketColor", (0.8, 0.8, 0.8, 1), 0, 1), ("Edge Color", "NodeSocketColor", (0.1, 0.1, 0.1, 1), 0, 1), ] edge_outputs = [("Color", "NodeSocketColor")] create_node_group("Edge_Wear", edge_inputs, edge_outputs, build_edge_wear) # 3. PROCEDURAL RUST/LEAKS def build_rust_leaks(nodes, links, inp, out): # Noise Texture для ржавчины noise = nodes.new('ShaderNodeTexNoise') noise.location = (-200, 200) noise.noise_dimensions = '3D' noise.scale = 50 noise.detail = 8 # Color Ramp для распределения ramp = nodes.new('ShaderNodeValToRGB') ramp.location = (0, 200) ramp.color_ramp.elements[0].position = 0.3 ramp.color_ramp.elements[0].color = (0, 0, 0, 0) # Прозрачный ramp.color_ramp.elements[1].position = 0.7 ramp.color_ramp.elements[1].color = (0.6, 0.2, 0.05, 1) # Ржавый # Mix с Base Color mix = nodes.new('ShaderNodeMixRGB') mix.blend_type = 'MIX' mix.location = (200, 0) links.new(noise.outputs['Fac'], ramp.inputs['Fac']) links.new(ramp.outputs['Color'], mix.inputs['Fac']) links.new(inp.outputs['Base Color'], mix.inputs[1]) links.new(inp.outputs['Rust Color'], mix.inputs[2]) links.new(mix.outputs['Color'], out.inputs['Color']) rust_inputs = [ ("Base Color", "NodeSocketColor", (0.8, 0.8, 0.8, 1), 0, 1), ("Rust Color", "NodeSocketColor", (0.6, 0.2, 0.05, 1), 0, 1), ("Scale", "NodeSocketFloat", 50, 1, 500), ] rust_outputs = [("Color", "NodeSocketColor")] create_node_group("Procedural_Rust", rust_inputs, rust_outputs, build_rust_leaks) # 4. PARALLAX / INTERIOR MAPPING def build_parallax(nodes, links, inp, out): # UV Offset на основе View Direction tex_coord = nodes.new('ShaderNodeTexCoord') tex_coord.location = (-400, 0) # Vector Math для параллакса view_dir = nodes.new('ShaderNodeNewGeometry') view_dir.location = (-400, -200) # Сложная логика здесь... # См. полные примеры в библиотеках pass print("Node groups created!") ``` --- ## 📊 ТИПЫ СОКЕТОВ | Тип | Socket Type | Пример | |-----|-------------|--------| | Float | `NodeSocketFloat` | Roughness, Scale | | Color | `NodeSocketColor` | Base Color, Emission | | Vector | `NodeSocketVector` | Normal, Tangent | | Shader | `NodeSocketShader` | BSDF Output | | Boolean | `NodeSocketBool` | Toggle | | Integer | `NodeSocketInt` | Count, Seed | --- ## 💡 BEST PRACTICES | Практика | Почему | |----------|--------| | Группировать по функциям | Переиспользование | | Документировать сокеты | Навигация в узлах | | Версионировать (`Name_v001`) | Совместимость | | Тестировать в изоляции | Отладка | | Публиковать в Asset Browser | Доступ в любой сцене | --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P09: PRINCIPLED BSDF SETUP](09_principled_bsdf.md), [P11: PROCEDURAL TEXTURES](11_procedural_textures.md), [P03: MODIFIERS STACK](03_modifiers_stack.md), [P04: MESH EDITING (BMESH)](04_mesh_editing_bmesh.md), [P05: UV UNWRAPPING STRATEGIES](05_uv_unwrapping.md) - **По темам:** [P07: INSTANCING & ARRAYS](07_instancing_arrays.md), [P18: DRIVERS & CONSTRAINTS](18_drivers_constraints.md), [P23: GEOMETRY NODES BASICS](23_geometry_nodes_basics.md) - **Теги:** #node, #node-groups, #procedural, #prompt, #reuse, #shader, #shader-library, #shading --- # P11: PROCEDURAL TEXTURES Tags: #math #math-nodes #musgrave #noise #procedural #prompt #shading #textures #voronoi # P11: PROCEDURAL TEXTURES **ID:** P11 | **Cluster:** Materials PBR | **Difficulty:** ⭐⭐ | **Tags:** #procedural #noise #voronoi #musgrave #math-nodes #textures --- ## 🎯 GOAL Create fully procedural PBR textures using only math and noise nodes - no image files needed --- ## 📝 PROMPT (RUS) ``` Создай процедуруальную текстуру "[MATERIAL_TYPE]" используя только математические ноды: База: NOISE / VORONOI / MUSGRAVE / WAVE / MAGIC / BRICK / CHECKER / GRADIENT Модификаторы: MAPPING, COLOR_RAMP, MATH, VECTOR_MATH, MIX_RGB Выходы: Base Color, Roughness, Normal, Height, Metallic ``` --- ## 📝 PROMPT (ENG) ``` Create procedural texture "[MATERIAL_TYPE]" using only math nodes: Base: NOISE / VORONOI / MUSGRAVE / WAVE / MAGIC / BRICK / CHECKER / GRADIENT Modifiers: MAPPING, COLOR_RAMP, MATH, VECTOR_MATH, MIX_RGB Outputs: Base Color, Roughness, Normal, Height, Metallic ``` --- ## 🔧 MCP: `execute_blender_code` ### UNIVERSAL PROCEDURAL BUILDER ```python import bpy def create_procedural_material(name, texture_type, params): """ texture_type: 'METAL', 'WOOD', 'STONE', 'FABRIC', 'CONCRETE', 'PLASTIC', 'RUST', 'SNOW' params: dict с параметрами """ mat = bpy.data.materials.new(name) mat.use_nodes = True nodes = mat.node_tree.nodes links = mat.node_tree.links nodes.clear() # Principled BSDF principled = nodes.new('ShaderNodeBsdfPrincipled') principled.location = (0, 0) output = nodes.new('ShaderNodeOutputMaterial') output.location = (400, 0) links.new(principled.outputs['BSDF'], output.inputs['Surface']) # Texture Coordinate + Mapping tex_coord = nodes.new('ShaderNodeTexCoord') tex_coord.location = (-600, 0) mapping = nodes.new('ShaderNodeMapping') mapping.location = (-400, 0) links.new(tex_coord.outputs['Object'], mapping.inputs['Vector']) # === GENERATORS === def add_noise(scale=50, detail=4, roughness=0.5, distortion=0): n = nodes.new('ShaderNodeTexNoise') n.noise_dimensions = '3D' n.noise_type = 'FBM' n.inputs['Scale'].default_value = scale n.inputs['Detail'].default_value = detail n.inputs['Roughness'].default_value = roughness n.inputs['Distortion'].default_value = distortion n.location = (-200, 200) return n def add_voronoi(scale=50, feature='SMOOTH_F1', metric='MANHATTAN'): v = nodes.new('ShaderNodeTexVoronoi') v.voronoi_dimensions = '3D' v.feature = feature v.metric = metric v.inputs['Scale'].default_value = scale v.location = (-200, 0) return v def add_musgrave(scale=50, detail=4, dimension=2, lacunarity=2, octaves=4): m = nodes.new('ShaderNodeTexMusgrave') m.musgrave_dimensions = '3D' m.musgrave_type = 'MULTIFRACTAL' m.inputs['Scale'].default_value = scale m.inputs['Detail'].default_value = detail m.inputs['Dimension'].default_value = dimension m.inputs['Lacunarity'].default_value = lacunarity m.inputs['Octaves'].default_value = octaves m.location = (-200, -200) return m def add_ramp(colors, positions, interpolation='LINEAR'): """colors: [(pos, (r,g,b,a)), ...]""" ramp = nodes.new('ShaderNodeValToRGB') ramp.color_ramp.interpolation = interpolation ramp.location = (0, 0) for i, (pos, col) in enumerate(zip(positions, colors)): if i < 2: ramp.color_ramp.elements[i].position = pos ramp.color_ramp.elements[i].color = col else: elem = ramp.color_ramp.elements.new(pos) elem.color = col return ramp def add_math(op, val=0.5): m = nodes.new('ShaderNodeMath') m.operation = op if op not in ['SINE', 'COSINE', 'TANGENT']: m.inputs[1].default_value = val m.location = (0, -200) return m # === MATERIAL RECIPES === if texture_type == 'METAL': # Шум для микропотертостей noise = add_noise(scale=200, detail=8, roughness=0.7) ramp = add_ramp([ (0.3, (0.02, 0.02, 0.02, 1)), (0.7, (0.05, 0.05, 0.05, 1)), ]) links.new(mapping.outputs['Vector'], noise.inputs['Vector']) links.new(noise.outputs['Fac'], principled.inputs['Roughness']) principled.inputs['Base Color'].default_value = params.get('color', (0.8, 0.7, 0.6, 1)) principled.inputs['Metallic'].default_value = 1.0 principled.inputs['Roughness'].default_value = 0.1 elif texture_type == 'WOOD': # Мусклгрейв для волокон musg = add_musgrave(scale=params.get('scale', 20), detail=8) musg.musgrave_type = 'RIDGED_MULTIFRACTAL' ramp = add_ramp([ (0.0, (0.15, 0.08, 0.03, 1)), (0.3, (0.25, 0.15, 0.07, 1)), (0.6, (0.35, 0.2, 0.1, 1)), (1.0, (0.45, 0.3, 0.15, 1)), ], positions=[0.0, 0.3, 0.6, 1.0]) links.new(mapping.outputs['Vector'], musg.inputs['Vector']) links.new(musg.outputs['Fac'], ramp.inputs['Fac']) links.new(ramp.outputs['Color'], principled.inputs['Base Color']) # Roughness по волокнам ramp2 = add_ramp([(0.0, (0.6,)), (1.0, (0.3,))]) links.new(musg.outputs['Fac'], ramp2.inputs['Fac']) links.new(ramp2.outputs['Color'], principled.inputs['Roughness']) principled.inputs['Metallic'].default_value = 0.0 elif texture_type == 'STONE': voronoi = add_voronoi(scale=params.get('scale', 30), feature='SMOOTH_F1') noise = add_noise(scale=100, detail=4) # Комбинировать mix = nodes.new('ShaderNodeMixRGB') mix.blend_type = 'MULTIPLY' mix.location = (0, 200) links.new(voronoi.outputs['Distance'], mix.inputs[1]) links.new(noise.outputs['Fac'], mix.inputs[2]) mix.inputs[0].default_value = 0.5 ramp = add_ramp([ (0.0, (0.3, 0.3, 0.3, 1)), (0.5, (0.4, 0.35, 0.3, 1)), (1.0, (0.5, 0.45, 0.4, 1)), ]) links.new(mix.outputs['Color'], ramp.inputs['Fac']) links.new(ramp.outputs['Color'], principled.inputs['Base Color']) # Roughness ramp2 = add_ramp([(0.0, (0.9,)), (1.0, (0.5,))]) links.new(noise.outputs['Fac'], ramp2.inputs['Fac']) links.new(ramp2.outputs['Color'], principled.inputs['Roughness']) principled.inputs['Metallic'].default_value = 0.0 elif texture_type == 'CONCRETE': # Смесь шума и ворона noise1 = add_noise(scale=80, detail=6, roughness=0.6) noise2 = add_noise(scale=200, detail=3, roughness=0.3) voronoi = add_voronoi(scale=15, feature='F1') # Композиция mix1 = nodes.new('ShaderNodeMixRGB') mix1.blend_type = 'OVERLAY' mix1.location = (0, 200) links.new(noise1.outputs['Fac'], mix1.inputs[1]) links.new(noise2.outputs['Fac'], mix1.inputs[2]) mix2 = nodes.new('ShaderNodeMixRGB') mix2.blend_type = 'MULTIPLY' mix2.location = (200, 0) links.new(mix1.outputs['Color'], mix2.inputs[1]) links.new(voronoi.outputs['Distance'], mix2.inputs[2]) ramp = add_ramp([ (0.0, (0.4, 0.4, 0.4, 1)), (0.5, (0.5, 0.5, 0.5, 1)), (1.0, (0.6, 0.6, 0.6, 1)), ]) links.new(mix2.outputs['Color'], ramp.inputs['Fac']) links.new(ramp.outputs['Color'], principled.inputs['Base Color']) principled.inputs['Roughness'].default_value = 0.8 principled.inputs['Metallic'].default_value = 0.0 elif texture_type == 'FABRIC': # Ткань: волны + шум wave = nodes.new('ShaderNodeTexWave') wave.wave_type = 'SINE' wave.inputs['Scale'].default_value = 50 wave.inputs['Distortion'].default_value = 2.0 wave.location = (-200, 200) noise = add_noise(scale=200, detail=4) mix = nodes.new('ShaderNodeMixRGB') mix.blend_type = 'ADD' mix.location = (0, 200) links.new(wave.outputs['Color'], mix.inputs[1]) links.new(noise.outputs['Color'], mix.inputs[2]) ramp = add_ramp([ (0.0, params.get('color_dark', (0.1, 0.1, 0.3, 1))), (1.0, params.get('color_light', (0.3, 0.3, 0.5, 1))), ]) links.new(mix.outputs['Color'], ramp.inputs['Fac']) links.new(ramp.outputs['Color'], principled.inputs['Base Color']) # Sheen для ткани principled.inputs['Sheen Weight'].default_value = 0.3 principled.inputs['Sheen Roughness'].default_value = 0.5 elif texture_type == 'RUST': # База металл + ржавчина сверху noise = add_noise(scale=50, detail=6) voronoi = add_voronoi(scale=30, feature='SMOOTH_F1') # Маска ржавчины mix_mask = nodes.new('ShaderNodeMixRGB') mix_mask.blend_type = 'MULTIPLY' links.new(noise.outputs['Fac'], mix_mask.inputs[1]) links.new(voronoi.outputs['Distance'], mix_mask.inputs[2]) ramp_rust = add_ramp([ (0.3, (0, 0, 0, 0)), (0.5, (0.5, 0.15, 0.05, 0.5)), (0.8, (0.6, 0.2, 0.05, 1)), ]) links.new(mix_mask.outputs['Color'], ramp_rust.inputs['Fac']) # Mix с базовым металлом base_color = params.get('base_color', (0.6, 0.55, 0.5, 1)) rust_color = params.get('rust_color', (0.6, 0.2, 0.05, 1)) mix_final = nodes.new('ShaderNodeMixRGB') mix_final.blend_type = 'MIX' links.new(ramp_rust.outputs['Color'], mix_final.inputs['Fac']) mix_final.inputs[1].default_value = base_color + (1,) mix_final.inputs[2].default_value = rust_color + (1,) links.new(mix_final.outputs['Color'], principled.inputs['Base Color']) principled.inputs['Metallic'].default_value = 0.8 principled.inputs['Roughness'].default_value = 0.6 # Normal Map из Height (Bump) bump = nodes.new('ShaderNodeBump') bump.location = (200, -200) links.new(principled.inputs['Base Color'].links[0].from_node.outputs['Color'] if principled.inputs['Base Color'].links else noise.outputs['Fac'], bump.inputs['Height']) links.new(bump.outputs['Normal'], principled.inputs['Normal']) return mat # === USAGE === create_procedural_material("Proc_Metal_Gold", "METAL", {'color': (1.0, 0.84, 0.0, 1)}) create_procedural_material("Proc_Wood_Oak", "WOOD", {'scale': 15}) create_procedural_material("Proc_Stone_Granite", "STONE", {'scale': 25}) create_procedural_material("Proc_Concrete_Raw", "CONCRETE", {}) create_procedural_material("Proc_Fabric_Denim", "FABRIC", {'color_dark': (0.05, 0.05, 0.3, 1), 'color_light': (0.2, 0.2, 0.4, 1)}) create_procedural_material("Proc_Rusted_Iron", "RUST", {'base_color': (0.5, 0.45, 0.4, 1), 'rust_color': (0.6, 0.2, 0.05, 1)}) ``` --- ## 📊 GENERATOR REFERENCE | Генератор | Лучше для | Ключевые параметры | |-----------|-----------|-------------------| | **Noise** | Органика, микродитали | Scale, Detail, Roughness, Distortion | | **Voronoi** | Ячейки, трещины, камни | Feature (F1/F2/Smooth), Metric, Scale | | **Musgrave** | Фракталы, рифы, кора | Type (fBm, Ridged, Hybrid), Dimension | | **Wave** | Волны, ткань, дюны | Type (Sine/Saw/Tri), Distortion | | **Magic** | Абстрактные паттерны | Depth, Turbulence | | **Brick** | Кирпичи, плитка | Brick Width, Row Height, Mortar | | **Checker** | Плитка, шахматы | Scale | --- ## 💡 COMBINATION PATTERNS | Паттерн | Ноды | Результат | |---------|------|-----------| | **Noise + ColorRamp** | Noise → Ramp | Контролируемые переходы | | **Voronoi + Noise** | Voronoi Distance × Noise | Естественные трещины | | **Musgrave × Noise** | Musgrave × Noise | Фрактальная детализация | | **Wave + Noise** | Wave + Noise | Ткань с шумом | | **Voronoi × Voronoi** | Разные Scale | Многослойные ячейки | --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P10: NODE GROUP CREATION](10_node_groups.md), [P03: MODIFIERS STACK](03_modifiers_stack.md), [P04: MESH EDITING (BMESH)](04_mesh_editing_bmesh.md), [P05: UV UNWRAPPING STRATEGIES](05_uv_unwrapping.md), [P07: INSTANCING & ARRAYS](07_instancing_arrays.md) - **По темам:** [P09: PRINCIPLED BSDF SETUP](09_principled_bsdf.md), [P18: DRIVERS & CONSTRAINTS](18_drivers_constraints.md), [P23: GEOMETRY NODES BASICS](23_geometry_nodes_basics.md) - **Теги:** #math, #math-nodes, #musgrave, #noise, #procedural, #prompt, #shading, #textures, #voronoi --- # P12: TEXTURE PAINTING Tags: #baking #brushes #layers #pbr #prompt #texture #texture-paint #texturing # P12: TEXTURE PAINTING **ID:** P12 | **Cluster:** Materials PBR | **Difficulty:** ⭐⭐ | **Tags:** #texture-paint #layers #brushes #baking #pbr --- ## 🎯 GOAL Hand-paint PBR textures directly on 3D models using Blender's Texture Paint mode with layer system --- ## 📝 PROMPT (RUS) ``` Настрой Texture Paint для "[OBJECT_NAME]": Слои: BASE_COLOR, ROUGHNESS, METALLIC, NORMAL, HEIGHT Кисти: DRAW, SOFTEN, SMEAR, CLONE, FILL, MASK Маска: по вертекс-группам / по UV / по Cavity Экспорт: все карты в папку //textures/ ``` --- ## 📝 PROMPT (ENG) ``` Setup Texture Paint for "[OBJECT_NAME]": Layers: BASE_COLOR, ROUGHNESS, METALLIC, NORMAL, HEIGHT Brushes: DRAW, SOFTEN, SMEAR, CLONE, FILL, MASK Mask: by vertex groups / by UV / by Cavity Export: all maps to //textures/ folder ``` --- ## 🔧 MCP: `execute_blender_code` ### SETUP TEXTURE PAINT LAYERS ```python import bpy obj = bpy.data.objects.get("Target_Mesh") if not obj or obj.type != 'MESH': raise ValueError("Mesh object required") # Убедиться в UV if not obj.data.uv_layers: bpy.context.view_layer.objects.active = obj bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.uv.smart_project() bpy.ops.object.mode_set(mode='OBJECT') # Создать изображения для слоёв def create_paint_image(name, width=2048, height=2048, color=(0,0,0,0), float_buffer=False): img = bpy.data.images.new(name, width, height, alpha=True, float_buffer=float_buffer) pixels = [color[0], color[1], color[2], color[3]] * (width * height) img.pixels = pixels return img # Слои PBR base_color_img = create_paint_image("Tex_BaseColor", color=(0.5, 0.5, 0.5, 1)) roughness_img = create_paint_image("Tex_Roughness", color=(0.5, 0.5, 0.5, 1), float_buffer=True) metallic_img = create_paint_image("Tex_Metallic", color=(0.0, 0.0, 0.0, 1), float_buffer=True) normal_img = create_paint_image("Tex_Normal", color=(0.5, 0.5, 1.0, 1)) height_img = create_paint_image("Tex_Height", color=(0.5, 0.5, 0.5, 1), float_buffer=True) # Материал с Texture Paint слоями mat = bpy.data.materials.new("Mat_Paint") mat.use_nodes = True nodes = mat.node_tree.nodes links = mat.node_tree.links nodes.clear() principled = nodes.new('ShaderNodeBsdfPrincipled') principled.location = (0, 0) output = nodes.new('ShaderNodeOutputMaterial') output.location = (400, 0) links.new(principled.outputs['BSDF'], output.inputs['Surface']) # Texture nodes для каждого слоя def add_paint_texture(img, label, location): tex = nodes.new('ShaderNodeTexImage') tex.image = img tex.label = label tex.location = location tex.interpolation = 'Linear' return tex tex_base = add_paint_texture(base_color_img, "PAINT_BaseColor", (-400, 300)) tex_rough = add_paint_texture(roughness_img, "PAINT_Roughness", (-400, 100)) tex_metal = add_paint_texture(metallic_img, "PAINT_Metallic", (-400, -100)) tex_norm = add_paint_texture(normal_img, "PAINT_Normal", (-400, -300)) links.new(tex_base.outputs['Color'], principled.inputs['Base Color']) links.new(tex_rough.outputs['Color'], principled.inputs['Roughness']) links.new(tex_metal.outputs['Color'], principled.inputs['Metallic']) # Normal Map normal_map = nodes.new('ShaderNodeNormalMap') normal_map.location = (200, -300) links.new(tex_norm.outputs['Color'], normal_map.inputs['Color']) links.new(normal_map.outputs['Normal'], principled.inputs['Normal']) # Назначить материал if obj.data.materials: obj.data.materials[0] = mat else: obj.data.materials.append(mat) # Переключить в Texture Paint mode bpy.context.view_layer.objects.active = obj bpy.ops.object.mode_set(mode='TEXTURE_PAINT') # Настроить слои для рисования tool_settings = bpy.context.scene.tool_settings tool_settings.unified_paint_settings.use_unified_size = True tool_settings.unified_paint_settings.size = 50 tool_settings.unified_paint_settings.use_unified_strength = True tool_settings.unified_paint_settings.strength = 0.5 print("Texture Paint ready! Layers: BaseColor, Roughness, Metallic, Normal") ``` ### BRUSH PRESETS ```python import bpy # Создать кисти для PBR def create_brush(name, brush_type, strength=0.5, radius=50, blend='MIX', use_color=True): brush = bpy.data.brushes.new(name) brush.brush_type = brush_type brush.strength = strength brush.size = radius brush.blend = blend if use_color: brush.color = (0.8, 0.8, 0.8) return brush # Base Color кисти create_brush("Paint_Draw", 'DRAW', strength=0.5, radius=80) create_brush("Paint_Soften", 'SOFTEN', strength=0.3, radius=100) create_brush("Paint_Smear", 'SMEAR', strength=0.4, radius=80) create_brush("Paint_Clone", 'CLONE', strength=1.0, radius=50) create_brush("Paint_Fill", 'FILL', strength=1.0, radius=200) # Mask кисти create_brush("Mask_Draw", 'MASK', strength=1.0, radius=80, use_color=False) create_brush("Mask_Blur", 'MASK', strength=0.3, radius=100, use_color=False) ``` ### MASKING TECHNIQUES ```python # Маска по Cavity (полости) bpy.ops.paint.mask_cavity() # Маска по вертекс-группам obj.vertex_groups.new(name="Paint_Mask") # Назначить веса в Weight Paint mode # Маска по UV (Edge Mask) bpy.ops.paint.mask_by_attribute(attribute_name="uv_edge") # Инверсия маски bpy.ops.paint.mask_invert() ``` ### EXPORT ALL MAPS ```python import os export_dir = "//textures/" os.makedirs(bpy.path.abspath(export_dir), exist_ok=True) for img in [base_color_img, roughness_img, metallic_img, normal_img, height_img]: filepath = os.path.join(export_dir, img.name + ".png") img.filepath_raw = filepath img.file_format = 'PNG' img.save() print("Exported to:", bpy.path.abspath(export_dir)) ``` --- ## 📊 LAYER WORKFLOW | Слой | Image Format | Color Space | Paint Mode | Blend | |------|--------------|-------------|------------|-------| | Base Color | PNG 8-bit | sRGB | Color | MIX | | Roughness | PNG 8-bit | Non-Color | Value | MIX | | Metallic | PNG 8-bit | Non-Color | Value | MIX | | Normal | PNG 8-bit | Non-Color | Color (RGB=Normal) | MIX | | Height | PNG 16/32-bit | Non-Color | Value | MIX | --- ## 💡 PRO TIPS | Tip | Why | |-----|-----| | Используй `float_buffer=True` для Roughness/Metallic/Height | Точность значений 0-1 | | Включи `Unified Paint Settings` | Единые размер/сила для всех кистей | | Рисуй на Low-poly с Multires | Детали без лагов | | Используй Stencil для текстур | Точное размещение деталей | | Сохраняй каждые 5 мин | `Image > Save` или `Alt+S` | --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P19: NLA EDITOR WORKFLOW](19_nla_workflow.md), [P46: UDIM WORKFLOW](46_udim_workflow.md), [P05: UV UNWRAPPING STRATEGIES](05_uv_unwrapping.md), [P08: SCULPTING BASICS](08_sculpting_basics.md), [P09: PRINCIPLED BSDF SETUP](09_principled_bsdf.md) - **По темам:** [P37: SEAMLESS TEXTURE WORKFLOW](37_seamless_textures.md), [P38: HIGH→LOW BAKE PIPELINE](38_bake_pipeline.md), [CASE 03: PRODUCT VISUALIZATION](../cases/case_03_product_viz.md) - **Теги:** #baking, #brushes, #layers, #pbr, #prompt, #texture, #texture-paint, #texturing --- # P13: LIGHTING & HDRI Tags: #hdri #ibl #lighting #polyhaven #prompt #studio #volumetric # P13: LIGHTING & HDRI **ID:** P13 | **Cluster:** Lighting | **Difficulty:** ⭐⭐ | **Tags:** #lighting #hdri #polyhaven #ibl #studio #volumetric --- ## 🎯 GOAL Setup professional lighting using HDRI, studio lights, and volumetric effects --- ## 📝 PROMPT (RUS) ``` Настрой освещение для сцены "[SCENE_NAME]": Тип: HDRI / STUDIO / VOLUMETRIC / MIXED HDRI: [Poly Haven ID] / [путь к файлу] разрешение [1k/2k/4k] Studio: 3-point / 4-point / Ring / Area lights Volumetric: Fog / God Rays / Dust ``` --- ## 📝 PROMPT (ENG) ``` Setup lighting for scene "[SCENE_NAME]": Type: HDRI / STUDIO / VOLUMETRIC / MIXED HDRI: [Poly Haven ID] / [file path] resolution [1k/2k/4k] Studio: 3-point / 4-point / Ring / Area lights Volumetric: Fog / God Rays / Dust ``` --- ## 🔧 MCP: `execute_blender_code` ### HDRI WORLD (Poly Haven) ```python import bpy # Через MCP: download_polyhaven_asset # Или напрямую: def setup_hdri_world(hdri_path, strength=1.0, rotation=0.0): world = bpy.data.worlds.new("World_HDRI") world.use_nodes = True nodes = world.node_tree.nodes links = world.node_tree.links nodes.clear() # Background bg = nodes.new('ShaderNodeBackground') bg.location = (0, 0) bg.inputs['Strength'].default_value = strength # Environment Texture env = nodes.new('ShaderNodeTexEnvironment') env.location = (-400, 0) env.image = bpy.data.images.load(hdri_path) env.projection = 'EQUIRECTANGULAR' # Mapping для вращения mapping = nodes.new('ShaderNodeMapping') mapping.location = (-600, 0) mapping.inputs['Rotation'].default_value[2] = rotation tex_coord = nodes.new('ShaderNodeTexCoord') tex_coord.location = (-800, 0) links.new(tex_coord.outputs['Generated'], mapping.inputs['Vector']) links.new(mapping.outputs['Vector'], env.inputs['Vector']) links.new(env.outputs['Color'], bg.inputs['Color']) # Output out = nodes.new('ShaderNodeOutputWorld') out.location = (200, 0) links.new(bg.outputs['Background'], out.inputs['Surface']) bpy.context.scene.world = world return world # Poly Haven HDRI через MCP # download_polyhaven_asset("abandoned_bakery", "hdris", "2k") # setup_hdri_world("//polyhaven/abandoned_bakery_2k.hdr", strength=1.2, rotation=0.785) ``` ### 3-POINT STUDIO LIGHTING ```python def setup_3point_lighting(target_obj, key_energy=1000, fill_energy=300, rim_energy=500): """Классическое 3-точечное освещение""" target_loc = target_obj.location if target_obj else (0, 0, 0) # KEY LIGHT (основной, 45° сторона, 45° вверх) key = bpy.data.lights.new("Key_Light", 'AREA') key_obj = bpy.data.objects.new("Key_Light", key) bpy.context.collection.objects.link(key_obj) key.energy = key_energy key.size = 2.0 key.size_y = 2.0 key_obj.location = (target_loc[0] + 4, target_loc[1] - 4, target_loc[2] + 3) key_obj.rotation_euler = (0.785, 0, -0.785) # 45° key.shape = 'RECTANGLE' # FILL LIGHT (заполнение, противоположная сторона, ниже) fill = bpy.data.lights.new("Fill_Light", 'AREA') fill_obj = bpy.data.objects.new("Fill_Light", fill) bpy.context.collection.objects.link(fill_obj) fill.energy = fill_energy fill.size = 3.0 fill.size_y = 3.0 fill_obj.location = (target_loc[0] - 3, target_loc[1] + 3, target_loc[2] + 1.5) fill_obj.rotation_euler = (0.5, 0, 0.785) fill.shape = 'RECTANGLE' # RIM/BACK LIGHT (контровой, сзади) rim = bpy.data.lights.new("Rim_Light", 'AREA') rim_obj = bpy.data.objects.new("Rim_Light", rim) bpy.context.collection.objects.link(rim_obj) rim.energy = rim_energy rim.size = 1.5 rim.size_y = 1.5 rim_obj.location = (target_loc[0] - 2, target_loc[1] - 2, target_loc[2] + 4) rim_obj.rotation_euler = (-0.5, 0, 3.14) rim.shape = 'RECTANGLE' return [key_obj, fill_obj, rim_obj] # Использование target = bpy.data.objects.get("Main_Subject") lights = setup_3point_lighting(target) ``` ### 4-POINT + BACKGROUND LIGHT ```python def setup_4point_lighting(target_obj): lights = setup_3point_lighting(target_obj) # BACKGROUND LIGHT (освещение фона) bg = bpy.data.lights.new("BG_Light", 'AREA') bg_obj = bpy.data.objects.new("BG_Light", bg) bpy.context.collection.objects.link(bg_obj) bg.energy = 200 bg.size = 10 bg.size_y = 10 bg_obj.location = (0, -8, 3) bg_obj.rotation_euler = (0, 0, 0) bg.shape = 'RECTANGLE' lights.append(bg_obj) return lights ``` ### RING LIGHT (Для продукта/портрета) ```python def setup_ring_light(target_obj, radius=3, count=8, energy=200): lights = [] for i in range(count): angle = (i / count) * 6.283 l = bpy.data.lights.new(f"Ring_{i}", 'AREA') lo = bpy.data.objects.new(f"Ring_{i}", l) bpy.context.collection.objects.link(lo) l.energy = energy l.size = 0.5 l.shape = 'RECTANGLE' x = radius * math.cos(angle) y = radius * math.sin(angle) lo.location = (x, y, 2) lo.rotation_euler = (math.pi/2, 0, angle + math.pi/2) lights.append(lo) return lights ``` ### VOLUMETRIC LIGHTING (God Rays / Fog) ```python def setup_volumetric_world(density=0.05, anisotropy=0.0, color=(1,1,1)): world = bpy.context.scene.world if not world: world = bpy.data.worlds.new("World_Volume") bpy.context.scene.world = world world.use_nodes = True nodes = world.node_tree.nodes links = world.node_tree.links # Очистить nodes.clear() # Volume Scatter volume = nodes.new('ShaderNodeVolumeScatter') volume.location = (0, 0) volume.inputs['Density'].default_value = density volume.inputs['Anisotropy'].default_value = anisotropy volume.inputs['Color'].default_value = color + (1,) # Background (опционально) bg = nodes.new('ShaderNodeBackground') bg.location = (-200, 0) bg.inputs['Strength'].default_value = 0.1 out = nodes.new('ShaderNodeOutputWorld') out.location = (200, 0) links.new(volume.outputs['Volume'], out.inputs['Volume']) links.new(bg.outputs['Background'], out.inputs['Surface']) # В Cycles: Render Properties > Volumetrics > Step Rate bpy.context.scene.cycles.volume_step_rate = 0.1 bpy.context.scene.cycles.volume_max_steps = 128 return world # God Rays (через Spot Light + Volume) def setup_god_rays(target_loc, energy=5000, cone_angle=0.5): spot = bpy.data.lights.new("God_Rays", 'SPOT') spot_obj = bpy.data.objects.new("God_Rays", spot) bpy.context.collection.objects.link(spot_obj) spot.energy = energy spot.spot_size = cone_angle spot.spot_blend = 0.5 spot.show_cone = True spot_obj.location = (target_loc[0], target_loc[1] - 5, target_loc[2] + 10) spot_obj.rotation_euler = (-1.0, 0, 0) # Включить Volume в Render Properties bpy.context.scene.cycles.volume_step_rate = 0.1 return spot_obj ``` ### HDRI + STUDIO MIX ```python def setup_mixed_lighting(hdri_path, target_obj, hdri_strength=0.5, studio_strength=1.0): """HDRI для окружения + Studio lights для формы""" # HDRI на низкой силе для отражений setup_hdri_world(hdri_path, strength=hdri_strength) # Studio lights для ключевых бликов setup_3point_lighting(target_obj, key_energy=1000*studio_strength, fill_energy=300*studio_strength, rim_energy=500*studio_strength) # Настройки Cycles для качества bpy.context.scene.cycles.caustics_reflective = True bpy.context.scene.cycles.caustics_refractive = True bpy.context.scene.cycles.max_bounces = 12 bpy.context.scene.cycles.diffuse_bounces = 4 bpy.context.scene.cycles.glossy_bounces = 4 bpy.context.scene.cycles.transmission_bounces = 12 ``` --- ## 📊 LIGHTING PRESETS | Пресет | HDRI | Key | Fill | Rim | Volumetric | Use Case | |--------|------|-----|------|-----|------------|----------| | **Studio Product** | studio_2k (0.3) | 1000 | 300 | 500 | No | Продукт, ювелирка | | **ArchViz Interior** | interior_2k (1.0) | 800 | 400 | 200 | Light Fog | Архитектура | | **Outdoor Day** | outdoor_4k (1.0) | Sun 5000 | Sky 1000 | - | Yes | Архитектура, ландшафт | | **Dark Fantasy** | dungeon_2k (0.8) | 300 | 100 | 500 | Heavy Fog | Игры, кино | | **Car Studio** | garage_2k (0.5) | 2000 | 800 | 1000 | No | Авто, глянец | | **Character Portrait** | studio_2k (0.4) | 800 | 400 | 600 | No | Персонажи | --- ## 💡 PRO TIPS | Tip | Why | |-----|-----| | HDRI Strength 0.3-0.5 для студии | Не пересвечивает, даёт только отражения | | Area Light > Spot для мягких теней | Физически корректнее | | Light Linking (Cycles) | Контроль какие объекты освещает свет | | Multiple Importance Sample в HDRI | Меньше шум в отражениях | | Volumetric Step Rate 0.1 | Качество God Rays без лагов | --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P27: POLY HAVEN ASSET PIPELINE](27_polyhaven_pipeline.md), [P36: ASSET LICENSING (PIZZA/SKETCHFAB/HAVEN)](36_asset_licensing.md), [P14: CAMERA & COMPOSITION](14_camera_composition.md), [P32: LIGHT LINKING & SHADOWS](32_light_linking.md), [P42: LIGHT PORTALS & CAUSTICS](42_portals_caustics.md) - **По темам:** [CASE 03: PRODUCT VISUALIZATION](../cases/case_03_product_viz.md), [P01: PRIMITIVE CREATION BASICS](01_geometry_basics.md), [P02: BOOLEAN OPERATIONS](02_boolean_operations.md) - **Теги:** #hdri, #ibl, #lighting, #polyhaven, #prompt, #studio, #volumetric --- # P14: CAMERA & COMPOSITION Tags: #animation #camera #composition #dof #framing #lighting #prompt #render # P14: CAMERA & COMPOSITION **ID:** P14 | **Cluster:** Lighting/Rendering | **Difficulty:** ⭐⭐ | **Tags:** #camera #composition #dof #framing #animation #render --- ## 🎯 GOAL Setup professional cameras with proper framing, depth of field, and composition rules --- ## 📝 PROMPT (RUS) ``` Настрой камеру "[CAM_NAME]" для сцены: Тип: PERSP / ORTHO / PANO Позиция: [X, Y, Z] | Target: [объект/координаты] Focal Length: [mm] | Sensor: [Full Frame / APS-C / MFT] DOF: Focus Object / Distance | F-Stop: [1.4-16] Composition: Rule of Thirds / Golden Ratio / Center / Symmetry ``` --- ## 📝 PROMPT (ENG) ``` Setup camera "[CAM_NAME]" for scene: Type: PERSP / ORTHO / PANO Position: [X, Y, Z] | Target: [object/coordinates] Focal Length: [mm] | Sensor: [Full Frame / APS-C / MFT] DOF: Focus Object / Distance | F-Stop: [1.4-16] Composition: Rule of Thirds / Golden Ratio / Center / Symmetry ``` --- ## 🔧 MCP: `execute_blender_code` ### BASIC CAMERA SETUP ```python import bpy import math from mathutils import Vector def create_camera(name, location, target, lens=50, sensor_width=36, clip_start=0.1, clip_end=1000): """Создать камеру с целью""" cam_data = bpy.data.cameras.new(name) cam_obj = bpy.data.objects.new(name, cam_data) bpy.context.collection.objects.link(cam_obj) cam_obj.location = location cam_data.lens = lens cam_data.sensor_width = sensor_width cam_data.clip_start = clip_start cam_data.clip_end = clip_end # Track To constraint if target: if isinstance(target, str): target_obj = bpy.data.objects.get(target) else: target_obj = target if target_obj: constraint = cam_obj.constraints.new('TRACK_TO') constraint.target = target_obj constraint.track_axis = 'TRACK_NEGATIVE_Z' constraint.up_axis = 'UP_Y' return cam_obj # Активная камера cam = create_camera("Cam_Main", (7, -7, 5), "Main_Subject", lens=50) bpy.context.scene.camera = cam ``` ### DEPTH OF FIELD (DOF) ```python def setup_dof(camera_obj, focus_object=None, focus_distance=5.0, fstop=2.8, blades=8): """Настройка глубины резкости""" cam = camera_obj.data cam.dof.use_dof = True cam.dof.aperture_fstop = fstop cam.dof.aperture_blades = blades cam.dof.aperture_ratio = 1.0 if focus_object: cam.dof.focus_object = focus_object else: cam.dof.focus_distance = focus_distance # Viewport DOF preview for area in bpy.context.screen.areas: if area.type == 'VIEW_3D': for space in area.spaces: if space.type == 'VIEW_3D': space.shading.show_camera_dof = True # Использование cam = bpy.data.objects.get("Cam_Main") subject = bpy.data.objects.get("Main_Subject") setup_dof(cam, focus_object=subject, fstop=1.8) ``` ### COMPOSITION GUIDES ```python def setup_composition_guides(camera_obj, guide_type='THIRDS'): """Показать направляющие композиции в viewport""" cam = camera_obj.data # Включить сетку в камере cam.show_composition_guides = True # Типы направляющих (доступны в Blender 3.0+) guides = { 'THIRDS': 'THIRDS', # Rule of Thirds 'GOLDEN': 'GOLDEN', # Golden Ratio 'QUARTERS': 'QUARTERS', # Quarters 'DIAGONAL': 'DIAGONAL', # Diagonal 'HARMONIC_TRIANGLES': 'HARMONIC_TRIANGLES_A', 'HARMONIC_TRIANGLES_B', 'CROSSHAIR': 'CROSSHAIR', } if guide_type in guides: cam.composition_guide = guides[guide_type] # Использование setup_composition_guides(cam, 'THIRDS') # или 'GOLDEN' ``` ### CAMERA ANIMATION (Path Following) ```python def animate_camera_path(camera_obj, curve_obj, frames=250, look_at_target=None): """Анимировать камеру по кривой""" # Follow Path constraint constraint = camera_obj.constraints.new('FOLLOW_PATH') constraint.target = curve_obj constraint.use_curve_follow = True constraint.use_fixed_location = True # Анимировать offset (0.0 -> 1.0) curve_obj.data.eval_time = 1 curve_obj.data.path_duration = frames # Анимация eval_time curve_obj.data.keyframe_insert("eval_time", frame=1) curve_obj.data.eval_time = frames curve_obj.data.keyframe_insert("eval_time", frame=frames) # Interpolation LINEAR для равномерного движения for fcurve in curve_obj.animation_data.action.fcurves: for kp in fcurve.keyframe_points: kp.interpolation = 'LINEAR' # Track To target if look_at_target: track = camera_obj.constraints.new('TRACK_TO') track.target = look_at_target track.track_axis = 'TRACK_NEGATIVE_Z' track.up_axis = 'UP_Y' track.influence = 1.0 # Создать путь import math curve_data = bpy.data.curves.new("Cam_Path", 'CURVE') curve_data.dimensions = '3D' spline = curve_data.splines.new('BEZIER') spline.bezier_points.add(3) # Настроить точки пути points = [ (0, -10, 3), # Start (5, -5, 4), # Point 1 (5, 5, 3), # Point 2 (0, 10, 2), # End ] for i, pt in enumerate(points): bp = spline.bezier_points[i] bp.co = pt bp.handle_left_type = 'AUTO' bp.handle_right_type = 'AUTO' curve_obj = bpy.data.objects.new("Cam_Path", curve_data) bpy.context.collection.objects.link(curve_obj) cam = bpy.data.objects.get("Cam_Main") target = bpy.data.objects.get("Main_Subject") animate_camera_path(cam, curve_obj, frames=250, look_at_target=target) ``` ### DOF ANIMATION (Rack Focus) ```python def animate_rack_focus(camera_obj, start_obj, end_obj, start_frame=1, end_frame=100): """Анимация переключения фокуса (Rack Focus)""" cam = camera_obj.data cam.dof.use_dof = True # Ключевые кадры focus_object cam.dof.focus_object = start_obj cam.keyframe_insert('dof.focus_object', frame=start_frame) cam.dof.focus_object = end_obj cam.keyframe_insert('dof.focus_object', frame=end_frame) # Плавность перехода if camera_obj.animation_data and camera_obj.animation_data.action: for fcurve in camera_obj.animation_data.action.fcurves: if 'focus_object' in fcurve.data_path: for kp in fcurve.keyframe_points: kp.interpolation = 'BEZIER' kp.easing = 'EASE_IN_OUT' # Использование animate_rack_focus(cam, subject1, subject2, 1, 120) ``` ### MULTI-CAMERA SETUP (Для редактирования) ```python def setup_multi_camera(names_positions, target, lens=50): """Создать несколько камер для разных ракурсов""" cams = [] for i, (name, pos) in enumerate(names_positions): cam = create_camera(name, pos, target, lens=lens) cams.append(cam) # Marker для переключения в Sequencer marker = bpy.context.scene.timeline_markers.new(f"Cam_{name}", frame=i*50+1) marker.camera = cam return cams # Использование cams = setup_multi_camera([ ("Cam_Wide", (-10, -10, 5)), ("Cam_Medium", (-5, -5, 3)), ("Cam_Close", (-2, -2, 2)), ("Cam_Detail", (-1, -1, 1.5)), ], "Main_Subject", lens=35) ``` ### CAMERA PRESETS ```python CAMERA_PRESETS = { 'PRODUCT': {'lens': 50, 'sensor': 36, 'fstop': 8, 'dist': 2}, 'PORTRAIT': {'lens': 85, 'sensor': 36, 'fstop': 1.8, 'dist': 1.5}, 'ARCHITECTURE': {'lens': 24, 'sensor': 36, 'fstop': 11, 'dist': 10}, 'LANDSCAPE': {'lens': 16, 'sensor': 36, 'fstop': 16, 'dist': 50}, 'MACRO': {'lens': 100, 'sensor': 36, 'fstop': 5.6, 'dist': 0.3}, 'DRONE': {'lens': 20, 'sensor': 36, 'fstop': 8, 'dist': 100}, 'INTERIOR': {'lens': 35, 'sensor': 36, 'fstop': 4, 'dist': 5}, } def apply_camera_preset(camera_obj, preset_name, target): preset = CAMERA_PRESETS[preset_name] cam = camera_obj.data cam.lens = preset['lens'] cam.sensor_width = preset['sensor'] setup_dof(camera_obj, target, fstop=preset['fstop']) # Позиция на расстоянии preset['dist'] от цели # ... расчёт позиции ``` ### RENDER SETTINGS PER CAMERA ```python def set_render_for_camera(camera_obj, resolution_x=1920, resolution_y=1080, samples=128): """Настроить рендер для конкретной камеры""" scene = bpy.context.scene scene.camera = camera_obj scene.render.resolution_x = resolution_x scene.render.resolution_y = resolution_y scene.render.resolution_percentage = 100 scene.cycles.samples = samples scene.cycles.use_denoising = True scene.render.filepath = f"//renders/{camera_obj.name}_" scene.render.image_settings.file_format = 'PNG' scene.render.image_settings.color_mode = 'RGBA' scene.render.film_transparent = True ``` --- ## 📊 COMPOSITION RULES REFERENCE | Правило | Описание | Камера | Использование | |---------|----------|--------|---------------| | **Rule of Thirds** | Сетка 3x3, объект на пересечении | THIRDS | Универсально | | **Golden Ratio** | Спираль Фибоначчи | GOLDEN | Природа, портрет | | **Center** | Объект в центре | CROSSHAIR | Симметрия, продукт | | **Symmetry** | Зеркальное отражение | CENTER | Архитектура, авто | | **Leading Lines** | Линии ведут к объекту | DIAGONAL | Дороги, перспектива | | **Frame in Frame** | Рамка в рамке | CUSTOM | Арки, окна | | **Negative Space** | Пустое пространство | CUSTOM | Минимализм | --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P33: CAMERA PATH ANIMATION](33_camera_path.md), [P43: CAMERA SWITCHING & MARKERS](43_camera_switching.md), [P13: LIGHTING & HDRI](13_lighting_hdri.md), [P16: KEYFRAME ANIMATION BASICS](16_keyframe_animation.md), [P18: DRIVERS & CONSTRAINTS](18_drivers_constraints.md) - **По темам:** [CASE 03: PRODUCT VISUALIZATION](../cases/case_03_product_viz.md), [P19: NLA EDITOR WORKFLOW](19_nla_workflow.md), [P20: SHAPE KEYS & MORPHING](20_shape_keys.md) - **Теги:** #animation, #camera, #composition, #dof, #framing, #lighting, #prompt, #render --- # P15: COMPOSITING Tags: #color #color-grading #compositing #cryptomatte #nodes #passes #post #post-processing #prompt # P15: COMPOSITING **ID:** P15 | **Cluster:** Rendering/Post | **Difficulty:** ⭐⭐⭐ | **Tags:** #compositing #nodes #post-processing #color-grading #passes #cryptomatte --- ## 🎯 GOAL Professional compositing workflow using Blender's Node Compositor with render passes, color grading, and effects --- ## 📝 PROMPT (RUS) ``` Настрой композитинг для сцены "[SCENE_NAME]": Passes: COMBINED, VIEW_LAYER, CRYPTOMATTE, MIST, AO, NORMAL, DEPTH, EMMISSION Операции: Color Balance, Hue/Sat, Gamma, Glare, Lens Distortion, Vignette, Film Grain Output: EXR MultiLayer / PNG Sequence / MP4 ``` --- ## 📝 PROMPT (ENG) ``` Setup compositing for scene "[SCENE_NAME]": Passes: COMBINED, VIEW_LAYER, CRYPTOMATTE, MIST, AO, NORMAL, DEPTH, EMMISSION Operations: Color Balance, Hue/Sat, Gamma, Glare, Lens Distortion, Vignette, Film Grain Output: EXR MultiLayer / PNG Sequence / MP4 ``` --- ## 🔧 MCP: `execute_blender_code` ### ENABLE RENDER PASSES ```python import bpy scene = bpy.context.scene view_layer = scene.view_layers[0] # Основные проходы view_layer.use_pass_combined = True view_layer.use_pass_z = True # Depth view_layer.use_pass_normal = True # Normal view_layer.use_pass_mist = True # Mist view_layer.use_pass_ambient_occlusion = True # AO view_layer.use_pass_emission = True # Emission view_layer.use_pass_shadow = True # Shadow view_layer.use_pass_diffuse_color = True # DiffCol view_layer.use_pass_glossy_color = True # GlossCol # Cryptomatte (для масок объектов/материалов) view_layer.use_pass_cryptomatte = True view_layer.cryptomatte_use_object = True view_layer.cryptomatte_use_material = True view_layer.cryptomatte_use_asset = True # Freestyle (контурные линии) scene.render.use_freestyle = True view_layer.use_freestyle = True # Настройки Mist scene.world.mist_settings.start = 1.0 scene.world.mist_settings.depth = 50.0 scene.world.mist_settings.falloff = 'QUADRATIC' # Настройки Cycles для качества scene.cycles.samples = 256 scene.cycles.use_denoising = True scene.cycles.denoiser = 'OPTIX' # 'OPENIMAGEDENOISE' / 'OPTIX' scene.cycles.denoising_prefilter = 'ACCURATE' scene.cycles.use_adaptive_sampling = True scene.cycles.adaptive_threshold = 0.01 scene.cycles.adaptive_min_samples = 32 print("Render passes enabled!") ``` ### BASIC COMPOSITOR SETUP ```python def setup_compositor(): scene = bpy.context.scene scene.use_nodes = True tree = scene.node_tree nodes = tree.nodes links = tree.links nodes.clear() # === INPUT NODES === # Render Layers rl = nodes.new('CompositorNodeRLayers') rl.location = (-400, 0) rl.layer = "ViewLayer" # === COLOR CORRECTION === # Color Balance (Lift/Gamma/Gain) color_bal = nodes.new('CompositorNodeColorBalance') color_bal.location = (0, 200) color_bal.correction_method = 'LIFT_GAMMA_GAIN' # Lift (теня) - тёплые color_bal.lift = (1.02, 1.0, 0.98) # Gamma (полутона) - нейтральные color_bal.gamma = (1.0, 1.0, 1.0) # Gain (светлые) - холодные color_bal.gain = (0.98, 1.0, 1.02) # Hue/Saturation/Value hsv = nodes.new('CompositorNodeHueSat') hsv.location = (200, 200) hsv.color_saturation = 1.1 hsv.color_value = 1.0 hsv.color_hue = 0.0 # Gamma gamma = nodes.new('CompositorNodeGamma') gamma.location = (400, 200) gamma.inputs['Gamma'].default_value = 1.1 # Exposure/Contrast exp = nodes.new('CompositorNodeExposure') exp.location = (600, 200) exp.inputs['Exposure'].default_value = 0.1 exp.inputs['Contrast'].default_value = 1.05 # === EFFECTS === # Glare (Bloom) glare = nodes.new('CompositorNodeGlare') glare.location = (0, -200) glare.glare_type = 'FOG_GLOW' glare.quality = 'HIGH' glare.iterations = 5 glare.mix = 0.3 glare.threshold = 0.8 glare.size = 7 # Lens Distortion lens = nodes.new('CompositorNodeLensdist') lens.location = (200, -200) lens.distort = 0.02 lens.dispersion = 0.01 # Vignette vignette = nodes.new('CompositorNodeVignette') vignette.location = (400, -200) vignette.intensity = 0.3 vignette.size = 0.8 # Film Grain noise = nodes.new('CompositorNodeNoise') noise.location = (600, -200) noise.noise_type = 'GAUSSIAN' noise.inputs['Scale'].default_value = 300 noise.inputs['Distortion'].default_value = 0.1 mix_grain = nodes.new('CompositorNodeMixRGB') mix_grain.location = (800, -200) mix_grain.blend_type = 'OVERLAY' mix_grain.inputs[0].default_value = 0.03 # === COLOR GRADING (Film Look) === # Color Matrix (LUT-like) cm = nodes.new('CompositorNodeColorMatrix') cm.location = (800, 200) # Teal & Orange lookup cm.matrix = [ 1.1, -0.1, -0.1, 0, -0.1, 1.0, -0.1, 0, -0.1, -0.1, 1.1, 0, 0, 0, 0, 1 ] # === OUTPUT === comp = nodes.new('CompositorNodeComposite') comp.location = (1000, 0) viewer = nodes.new('CompositorNodeViewer') viewer.location = (1000, -200) viewer.use_alpha = False # === LINKS === links.new(rl.outputs['Image'], color_bal.inputs['Image']) links.new(color_bal.outputs['Image'], hsv.inputs['Image']) links.new(hsv.outputs['Image'], gamma.inputs['Image']) links.new(gamma.outputs['Image'], exp.inputs['Image']) links.new(exp.outputs['Image'], glare.inputs['Image']) links.new(glare.outputs['Image'], lens.inputs['Image']) links.new(lens.outputs['Image'], vignette.inputs['Image']) links.new(vignette.outputs['Image'], mix_grain.inputs[1]) links.new(noise.outputs['Image'], mix_grain.inputs[2]) links.new(mix_grain.outputs['Image'], cm.inputs['Image']) links.new(cm.outputs['Image'], comp.inputs['Image']) links.new(cm.outputs['Image'], viewer.inputs['Image']) # === PASSES OUTPUT (для EXR) === # File Output для слоёв file_out = nodes.new('CompositorNodeOutputFile') file_out.location = (1000, -400) file_out.format.file_format = 'OPEN_EXR_MULTILAYER' file_out.format.color_mode = 'RGBA' file_out.format.color_depth = '32' file_out.base_path = "//renders/exr/" # Подключить проходы passes = ['Image', 'Depth', 'Normal', 'Mist', 'AO', 'Emission', 'Shadow', 'Cryptomatte00', 'Cryptomatte10'] for i, pass_name in enumerate(passes): if pass_name in rl.outputs: file_out.layer_slots.new(pass_name) links.new(rl.outputs[pass_name], file_out.inputs[pass_name]) print("Compositor setup complete!") setup_compositor() ``` ### CRYPTOMATTE MASKING ```python def setup_cryptomatte_mask(material_name, output_node): """Создать маску по Cryptomatte для материала""" tree = bpy.context.scene.node_tree nodes = tree.nodes links = tree.links rl = nodes.get('Render Layers') if not rl: return # Cryptomatte нода crypto = nodes.new('CompositorNodeCryptomatte') crypto.location = (200, -400) crypto.matte_id = material_name # или ID объекта crypto.use_matte_output = True links.new(rl.outputs['Cryptomatte00'], crypto.inputs['Image']) links.new(crypto.outputs['Matte'], output_node.inputs['Fac']) ``` ### DEPTH OF FIELD (POST) ```python def setup_dof_post(z_input, focus_distance=5.0, fstop=2.8, max_blur=20): """DOF в пост-продакшене через Z-pass""" tree = bpy.context.scene.node_tree nodes = tree.nodes links = tree.links # Defocus нода defocus = nodes.new('CompositorNodeDefocus') defocus.location = (400, 0) defocus.use_zbuffer = True defocus.fStop = fstop defocus.maxblur = max_blur defocus.bokeh_type = 'POLYGON' defocus.blade_count = 8 defocus.rotation = 0 links.new(nodes['Render Layers'].outputs['Image'], defocus.inputs['Image']) links.new(nodes['Render Layers'].outputs['Depth'], defocus.inputs['Z']) return defocus ``` ### EXPORT SETTINGS ```python def setup_render_output(format='PNG', color_mode='RGBA', depth='16', filepath="//renders/"): scene = bpy.context.scene scene.render.filepath = filepath scene.render.image_settings.file_format = format scene.render.image_settings.color_mode = color_mode scene.render.image_settings.color_depth = depth scene.render.image_settings.compression = 90 if format == 'OPEN_EXR': scene.render.image_settings.exr_codec = 'ZIP' scene.render.image_settings.use_preview = True # Для анимации scene.render.ffmpeg.format = 'MPEG4' scene.render.ffmpeg.codec = 'H264' scene.render.ffmpeg.constant_rate_factor = 'PERC_LOSSLESS' scene.render.ffmpeg.ffmpeg_preset = 'GOOD' scene.render.ffmpeg.audio_codec = 'AAC' scene.render.ffmpeg.audio_bitrate = 192 # EXR MultiLayer для Nuke/After Effects setup_render_output('OPEN_EXR', 'RGBA', '32', "//renders/exr/") # PNG для быстрого превью setup_render_output('PNG', 'RGBA', '16', "//renders/png/") ``` ### ADVANCED: COLOR GRADING NODES ```python def create_film_look_nodes(tree): """Создать ноды для киноподобного образа""" nodes = tree.nodes links = tree.links # 1. Log -> Linear (если исходник в Log) # log2lin = nodes.new('CompositorNodeColorSpace') # log2lin.location = (0, 400) # log2lin.color_space = 'LOG_TO_LINEAR' # 2. Contrast Curve rgb_curves = nodes.new('CompositorNodeRGBCurve') rgb_curves.location = (0, 400) # S-кривая for curve in rgb_curves.mapping.curves: curve.points[0].location = (0, 0) curve.points[1].location = (0.25, 0.2) curve.points[2].location = (0.5, 0.5) curve.points[3].location = (0.75, 0.8) curve.points[4].location = (1, 1) # 3. Lift/Gamma/Gain (Color Balance) cdl = nodes.new('CompositorNodeColorBalance') cdl.location = (200, 400) cdl.correction_method = 'LIFT_GAMMA_GAIN' cdl.lift = (1.02, 1.01, 1.0) # Shadows -> warm cdl.gamma = (1.0, 1.0, 1.0) # Midtones -> neutral cdl.gain = (0.98, 1.0, 1.02) # Highlights -> cool # 4. Saturation sat = nodes.new('CompositorNodeHueSat') sat.location = (400, 400) sat.color_saturation = 1.15 # 5. Vignette vign = nodes.new('CompositorNodeVignette') vign.location = (600, 400) vign.intensity = 0.25 # 5. Grain grain = nodes.new('CompositorNodeNoise') grain.location = (800, 400) grain.noise_type = 'GAUSSIAN' grain.inputs['Scale'].default_value = 400 mix_grain = nodes.new('CompositorNodeMixRGB') mix_grain.location = (1000, 400) mix_grain.blend_type = 'OVERLAY' mix_grain.inputs[0].default_value = 0.02 return { 'curves': rgb_curves, 'cdl': cdl, 'sat': sat, 'vignette': vign, 'grain': grain, 'mix_grain': mix_grain, } ``` --- ## 📊 COMPOSITOR NODES REFERENCE | Категория | Ноды | Назначение | |-----------|------|------------| | **Input** | Render Layers, Image, Movie Clip, Texture | Источники | | **Color** | Color Balance, Hue/Sat, Gamma, Exposure, RGB Curves, Color Matrix | Коррекция цвета | | **Filter** | Blur, Defocus, Glare, Kuwahara, Bilateral, Despeckle | Размытие/резкость | | **Distort** | Lens Dist, Displace, Transform, Crop, Flip | Геометрия | | **Matte** | Cryptomatte, ID Mask, Chroma Key, Difference | Маски | | **Convert** | Color Space, Premult, Alpha Over, Set Alpha | Альфа/премальт | | **Output** | Composite, Viewer, File Output, Map Viewer | Результат | --- ## 💡 PRO TIPS | Tip | Why | |-----|-----| | Работай в EXR 32-bit | Максимальный 다이내мик, без бандинга | | Используй Viewer Node | Мгновенный предпросмотр | | Группируй ноды (Ctrl+G) | Читаемость сложных деревьев | | Сохраняй пресеты нод (Node Groups) | Переиспользование | | Denoise в рендере + Glare в компе | Меньше артефактов | | Cryptomatte для селекций | Никакого ручного маскирования | --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P31: RENDER PASSES & AOV](31_render_passes_aov.md), [P01: PRIMITIVE CREATION BASICS](01_geometry_basics.md), [P02: BOOLEAN OPERATIONS](02_boolean_operations.md), [P03: MODIFIERS STACK](03_modifiers_stack.md), [P04: MESH EDITING (BMESH)](04_mesh_editing_bmesh.md) - **По темам:** [CASE 03: PRODUCT VISUALIZATION](../cases/case_03_product_viz.md), [COLOR MANAGEMENT: OCIO / ACES](../color/ocio_aces.md), [P05: UV UNWRAPPING STRATEGIES](05_uv_unwrapping.md) - **Теги:** #color, #color-grading, #compositing, #cryptomatte, #nodes, #passes, #post, #post-processing, #prompt --- # P19: NLA EDITOR WORKFLOW Tags: #actions #animation #blending #layers #modeling #nla #nonlinear #prompt #texturing # P19: NLA EDITOR WORKFLOW **ID:** P19 | **Кластер:** Animation | **Сложность:** ⭐⭐ | **Теги:** #nla #actions #layers #blending #nonlinear --- ## 🎯 ЦЕЛЬ Нелинейная анимация: actions → strips → слои (Replace/Add) → блендинг --- ## 📝 ПРОМПТ (РУС) ``` Собери NLA для "[RIG]": Track 0: [ACTION_BASE] (Replace, Cyclic) Track 1: [ACTION_ADD] (Add, Influence 0.5) Переходы: Crossfade [N] кадров ``` --- ## 📝 ПРОМПТ (ENG) ``` Build NLA for "[RIG]": Track 0: [ACTION_BASE] (Replace, Cyclic) Track 1: [ACTION_ADD] (Add, Influence 0.5) Transitions: Crossfade [N] frames ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy def push_to_nla(obj_name, action_name, track_name="Track", blend='REPLACE', repeat=True): obj = bpy.data.objects.get(obj_name) if not obj or not obj.animation_data: raise ValueError("No animation data") ad = obj.animation_data if not ad.nla_tracks: track = ad.nla_tracks.new() track.name = track_name else: track = ad.nla_tracks[0] action = bpy.data.actions.get(action_name) strip = track.strips.new(action.name, 1, action) strip.blend_type = blend # REPLACE / ADD / COMBINE / MULTIPLY strip.extrapolation = 'HOLD' if not repeat else 'NOTHING' strip.use_auto_blend = True strip.blend_in = 5 strip.blend_out = 5 return {"strip": strip.name, "track": track.name} # push_to_nla("Rig", "Walk", "Base", 'REPLACE') # push_to_nla("Rig", "ArmSwing", "Additive", 'ADD') ``` --- ## 📊 BLEND MODES | Mode | Эффект | |------|--------| | REPLACE | Заменяет нижние слои | | ADD | Добавляет (аддитивные слои: дыхание, жесты) | | COMBINE | Комбинирует по каналам | | MULTIPLY | Умножает (редко) | --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P12: TEXTURE PAINTING](12_texture_painting.md), [P18: DRIVERS & CONSTRAINTS](18_drivers_constraints.md), [P43: CAMERA SWITCHING & MARKERS](43_camera_switching.md), [P01: PRIMITIVE CREATION BASICS](01_geometry_basics.md), [P02: BOOLEAN OPERATIONS](02_boolean_operations.md) - **По темам:** [P03: MODIFIERS STACK](03_modifiers_stack.md), [P04: MESH EDITING (BMESH)](04_mesh_editing_bmesh.md), [P05: UV UNWRAPPING STRATEGIES](05_uv_unwrapping.md) - **Теги:** #actions, #animation, #blending, #layers, #modeling, #nla, #nonlinear, #prompt, #texturing --- # P23: GEOMETRY NODES BASICS Tags: #attributes #geometry #geometry-nodes #instances #modeling #procedural #prompt # P23: GEOMETRY NODES BASICS **ID:** P23 | **Кластер:** Geometry Nodes | **Сложность:** ⭐⭐ | **Теги:** #geometry-nodes #procedural #attributes #instances --- ## 🎯 ЦЕЛЬ Базовый GN-сетап: Group Input/Output, distribute points, instance on points, realize --- ## 📝 ПРОМПТ (РУС) ``` Создай Geometry Nodes "[GROUP]" на "[OBJECT]": Входы: [Count, Seed, Scale] Логика: Distribute Points → Instance Collection → Random Transform ``` --- ## 📝 ПРОМПТ (ENG) ``` Create Geometry Nodes "[GROUP]" on "[OBJECT]": Inputs: [Count, Seed, Scale] Logic: Distribute Points → Instance Collection → Random Transform ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy def create_gn_scatter(obj_name, group_name="GN_Scatter", collection_name=None): obj = bpy.data.objects.get(obj_name) if not obj: raise ValueError("Object not found") ng = bpy.data.node_groups.new(group_name, 'GeometryNodeTree') nodes, links = ng.nodes, ng.links nodes.clear() inp = nodes.new('NodeGroupInput') out = nodes.new('NodeGroupOutput') inp.location = (-400, 0); out.location = (400, 0) ng.interface.new_socket(name="Count", in_out='INPUT', socket_type='NodeSocketInt') ng.interface.new_socket(name="Seed", in_out='INPUT', socket_type='NodeSocketInt') ng.interface.new_socket(name="Geometry", in_out='OUTPUT', socket_type='NodeSocketGeometry') dist = nodes.new('GeometryNodeDistributePointsOnFaces') dist.location = (-100, 0) dist.distribute_method = 'POISSON' inst = nodes.new('GeometryNodeInstanceOnPoints') inst.location = (100, 0) real = nodes.new('GeometryNodeRealizeInstances') real.location = (300, 0) links.new(inp.outputs[0], dist.inputs['Density']) links.new(dist.outputs['Points'], inst.inputs['Points']) links.new(inst.outputs['Instances'], real.inputs['Geometry']) links.new(real.outputs['Geometry'], out.inputs['Geometry']) mod = obj.modifiers.new("GN", 'NODES') mod.node_group = ng return {"group": group_name, "object": obj_name} # create_gn_scatter("Ground", "GN_Trees") ``` --- ## 📊 КЛЮЧЕВЫЕ НОДЫ | Нода | Назначение | |------|------------| | Distribute Points on Faces | Точки по поверхности | | Instance on Points | Инстансы коллекции | | Realize Instances | Конвертация в геометрию | | Random Value | Случайные scale/rotation | | Store Named Attribute | Кастомные атрибуты | --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P07: INSTANCING & ARRAYS](07_instancing_arrays.md), [P25: PROCEDURAL BUILDING GENERATOR](25_procedural_building.md), [P45: GEOMETRY NODES SIMULATION ZONES](45_gn_simulation.md), [P01: PRIMITIVE CREATION BASICS](01_geometry_basics.md), [P03: MODIFIERS STACK](03_modifiers_stack.md) - **По темам:** [P04: MESH EDITING (BMESH)](04_mesh_editing_bmesh.md), [P18: DRIVERS & CONSTRAINTS](18_drivers_constraints.md), [P22: PARTICLE SYSTEMS](22_particles.md) - **Теги:** #attributes, #geometry, #geometry-nodes, #instances, #modeling, #procedural, #prompt --- # P26: MCP BASICS (BLENDER-MCP COMMANDS) Tags: #blender #blender-mcp #commands #mcp #prompt #setup #tools # P26: MCP BASICS (BLENDER-MCP COMMANDS) **ID:** P26 | **Кластер:** MCP Integration | **Сложность:** ⭐ | **Теги:** #mcp #blender-mcp #tools #commands #setup --- ## 🎯 ЦЕЛЬ Базовые MCP-команды: scene info, create/modify/delete, screenshot, code execution --- ## 📝 ПРОМПТ (РУС) ``` Через MCP: [действие] Примеры: "Покажи сцену", "Создай сферу", "Скриншот viewport" ``` --- ## 📝 ПРОМПТ (ENG) ``` Via MCP: [action] Examples: "Show scene", "Create sphere", "Viewport screenshot" ``` --- ## 🔧 MCP TOOLS ```json // 1. Scene info {"type": "get_scene_info", "params": {}} // 2. Create {"type": "create_object", "params": {"entity_type": "SPHERE", "name": "Ball", "location": [0,0,2]}} // 3. Modify {"type": "modify_object", "params": {"name": "Ball", "scale": [2,2,2]}} // 4. Screenshot {"type": "get_viewport_screenshot", "params": {"max_size": 800}} // 5. Code {"type": "execute_code", "params": {"code": "import bpy\nbpy.ops.mesh.primitive_cube_add()"}} ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P27: POLY HAVEN ASSET PIPELINE](27_polyhaven_pipeline.md), [P28: CUSTOM MCP TOOLS DEVELOPMENT](28_custom_mcp_tools.md), [P35: MCP ERROR RECOVERY](35_mcp_error_recovery.md), [P45: GEOMETRY NODES SIMULATION ZONES](45_gn_simulation.md), [P46: UDIM WORKFLOW](46_udim_workflow.md) - **По темам:** [SETUP 01: BLENDER-MCP BASIC](../setups/setup_01_blender_mcp_basic.md), [SETUP 09: BLENDERBOT / AI CODING AGENT](../setups/setup_09_blenderbot_ai.md), [SETUP 35: READY PLAYER ME AVATARS](../setups/setup_35_readyplayerme.md), [SETUP 45: HUNYUAN3D 2.1 LOCAL](../setups/setup_45_hunyuan3d_21.md) - **Теги:** #blender, #blender-mcp, #commands, #mcp, #prompt, #setup, #tools --- # P38: HIGH→LOW BAKE PIPELINE Tags: #ao #baking #cage #curvature #normal #pipeline #prompt # P38: HIGH→LOW BAKE PIPELINE **ID:** P38 | **Кластер:** Materials | **Сложность:** ⭐⭐⭐ | **Теги:** #baking #normal #ao #curvature #cage --- ## 🎯 ЦЕЛЬ Запечь карты с high-poly на low-poly: normal/AO/curvature + Cage --- ## 📝 ПРОМПТ (РУС) ``` Запеки с "[HIGH]" на "[LOW]": [normal, AO, curvature], Cage [dist], margin [px], resolution [res]. Выведи в //bakes/. ``` --- ## 📝 ПРОМПТ (ENG) ``` Bake from "[HIGH]" to "[LOW]": [normal, AO, curvature], Cage [dist], margin [px], resolution [res]. Output to //bakes/. ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy def bake_maps(high_name, low_name, out_dir="//bakes/", res=2048, margin=16, cage=0.02): high = bpy.data.objects.get(high_name) low = bpy.data.objects.get(low_name) if not high or not low: raise ValueError("Objects not found") bpy.ops.object.select_all(action='DESELECT') high.select_set(True) low.select_set(True) bpy.context.view_layer.objects.active = low s = bpy.context.scene s.render.engine = 'CYCLES' s.render.bake.use_selected_to_active = True s.render.bake.cage_extrusion = cage s.render.bake.margin = margin for bake_type, suffix in [('NORMAL', 'normal'), ('AO', 'ao')]: img = bpy.data.images.new(f"{low_name}_{suffix}", res, res) mat = low.active_material tn = mat.node_tree.nodes.new('ShaderNodeTexImage') tn.image = img tn.select = True mat.node_tree.nodes.active = tn bpy.ops.object.bake(type=bake_type, use_selected_to_active=True) img.filepath_raw = f"{out_dir}{low_name}_{suffix}.png" img.file_format = 'PNG' img.save() mat.node_tree.nodes.remove(tn) return {"baked": [high_name, low_name]} ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P46: UDIM WORKFLOW](46_udim_workflow.md), [P12: TEXTURE PAINTING](12_texture_painting.md), [P22: PARTICLE SYSTEMS](22_particles.md), [P27: POLY HAVEN ASSET PIPELINE](27_polyhaven_pipeline.md), [P36: ASSET LICENSING (PIZZA/SKETCHFAB/HAVEN)](36_asset_licensing.md) - **По темам:** [P39: EXPORT PRESETS (GLB/FBX/USD)](39_export_presets.md), [P47: ALEMBIC CACHE PIPELINE](47_alembic_cache.md), [CASE 02: GAME PROPS PIPELINE](../cases/case_02_game_props.md) - **Теги:** #ao, #baking, #cage, #curvature, #normal, #pipeline, #prompt --- # P45: GEOMETRY NODES SIMULATION ZONES Tags: #blender #blender-4 #geometry #geometry-nodes #modeling #particles #physics #prompt #simulation # P45: GEOMETRY NODES SIMULATION ZONES **ID:** P45 | **Кластер:** Geometry Nodes | **Сложность:** ⭐⭐⭐ | **Теги:** #geometry-nodes #simulation #particles #physics #blender-4 --- ## 🎯 ЦЕЛЬ Симуляции внутри GN 4.x: Simulation Zone, delta time, state attributes, bake --- ## 📝 ПРОМПТ (РУС) ``` Создай GN-симуляцию "[NAME]": state [position/velocity/age], forces [gravity/wind/turbulence], collision [цель], bake [N] кадров. ``` --- ## 📝 ПРОМПТ (ENG) ``` Create GN simulation "[NAME]": state [position/velocity/age], forces [gravity/wind/turbulence], collision [target], bake [N] frames. ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy def gn_simulation_shell(obj_name, group_name="GN_Sim"): """Каркас GN-дерева с Simulation Zone (ноду Simulation Input/Output добавляет Blender при создании зоны вручную). Скрипт создаёт группу и базу; зону дособрать в редакторе.""" obj = bpy.data.objects.get(obj_name) if not obj: raise ValueError("Object not found") ng = bpy.data.node_groups.new(group_name, 'GeometryNodeTree') nodes, links = ng.nodes, ng.links nodes.clear() inp = nodes.new('NodeGroupInput') out = nodes.new('NodeGroupOutput') inp.location = (-600, 0) out.location = (600, 0) ng.interface.new_socket(name="Delta Time", in_out='INPUT', socket_type='NodeSocketFloat') ng.interface.new_socket(name="Geometry", in_out='OUTPUT', socket_type='NodeSocketGeometry') # Подсказка структуры зоны (текст в frame): frame = nodes.new('NodeFrame') frame.label = "SIM ZONE: Sim Input → Forces → Integrate → Sim Output" mod = obj.modifiers.new("GN_Sim", 'NODES') mod.node_group = ng # Bake settings mod.show_viewport = True return {"group": group_name, "next": "добавь Simulation Zone в редакторе, bake через modifier"} def bake_simulation(obj_name, frame_start=1, frame_end=120): s = bpy.context.scene s.frame_start = frame_start s.frame_end = frame_end # GN bake: через point cache simulation zone (Blender 4.x — кнопка Bake в модификаторе вручную) return {"bake_range": f"{frame_start}-{frame_end}", "note": "нажми Bake в GN-модификаторе"} ``` --- ## 📊 STATE ATTRIBUTES | Атрибут | Тип | Смысл | |---------|-----|-------| | position | Vector | Текущая позиция | | velocity | Vector | Скорость (интегрируется) | | age | Float | Возраст частицы | | lifetime | Float | Время жизни (kill при age > lifetime) | | id | Integer | Стабильный ID между кадрами | --- ## ⚠️ ОШИБКИ | Ошибка | Решение | |--------|---------| | Взрыв частиц | Delta Time слишком большой; уменьши forces ×10 | | Дрожание | Substeps: уменьши dt, увеличь damping | | Потеря id | Не пересоздавай точки каждый кадр, храни state | --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P07: INSTANCING & ARRAYS](07_instancing_arrays.md), [P22: PARTICLE SYSTEMS](22_particles.md), [P23: GEOMETRY NODES BASICS](23_geometry_nodes_basics.md), [P01: PRIMITIVE CREATION BASICS](01_geometry_basics.md), [P21: PHYSICS SIMULATION](21_physics_simulation.md) - **По темам:** [P25: PROCEDURAL BUILDING GENERATOR](25_procedural_building.md), [SIMULATION & PHYSICS: GEOMETRY NODES 4.0+](../simulation/rigid_body_gn.md), [CASE 05: PROCEDURAL CITY](../cases/case_05_procedural_city.md) - **Теги:** #blender, #blender-4, #geometry, #geometry-nodes, #modeling, #particles, #physics, #prompt, #simulation --- # EEVEE NEXT (BLENDER 4.2+) Tags: #blender #eevee #realtime # EEVEE NEXT (BLENDER 4.2+) **Next-generation real-time renderer in Blender 4.2+** --- ## 🎯 WHAT'S NEW IN EEVEE NEXT | Feature | EEVEE Legacy | EEVEE Next | |---------|--------------|------------| | **Architecture** | OpenGL 3.3 | Vulkan / Metal / DX12 | | **Shadow Maps** | Variance Shadow Maps | Contact Hardening Shadows (CHS) | | **SSRR** | Screen Space Reflections | Ray-traced Reflections (hybrid) | | **GI** | Irradiance Volumes + Light Probes | Radiance Cache + DDGI | | **Volumetrics** | 3D Texture | Voxel-based, temporal | | **Shaders** | GLSL | SPIR-V / Slang | | **GPU Support** | OpenGL 3.3+ | Vulkan 1.2+ / Metal 2+ / DX12 | --- ## ⚙️ SETUP & REQUIREMENTS ### Hardware Requirements ```yaml Minimum: GPU: Vulkan 1.2+ / Metal 2+ / DX12 Feature Level 12_1 VRAM: 4 GB (8 GB recommended) Driver: Latest stable Recommended: GPU: RTX 3070 / RX 6700 XT / M2 Pro VRAM: 12 GB+ Driver: Latest Game Ready / Pro ``` ### Enable EEVEE Next ```python import bpy # Switch to EEVEE Next scene = bpy.context.scene scene.render.engine = 'BLENDER_EEVEE_NEXT' # Verify print(f"Engine: {scene.render.engine}") print(f"Device: {bpy.context.preferences.system.compute_device_type}") ``` --- ## 🌟 KEY FEATURES ### 1. Contact Hardening Shadows (CHS) ```python # Shadow Settings scene.eevee.shadow_method = 'VSM' # or 'EVSM' for exponential scene.eevee.shadow_cube_size = '2048' # 512, 1024, 2048, 4096 scene.eevee.shadow_cascade_size = '2048' # Contact Hardening (automatic in Next) # Penumbra size based on light size × distance # No more manual bias tweaking! # Per-light settings light = bpy.data.lights["Sun"] light.shadow_soft_size = 2.0 # Light size for penumbra light.shadow_buffer_clip_start = 0.1 light.shadow_buffer_clip_end = 100.0 ``` ### 2. Screen-Space Ray-Traced Reflections (SSRR) ```python # Enable Ray-traced Reflections scene.eevee.use_ssr = True scene.eevee.use_ssr_refraction = True scene.eevee.ssr_quality = 0.5 # 0.25 - 1.0 scene.eevee.ssr_thickness = 0.01 scene.eevee.ssr_border_fade = 0.1 # Hybrid: SSR + Ray-tracing fallback # Falls back to ray-tracing when SSR fails # Requires RTX / RX 6000+ for hardware RT ``` ### 3. Dynamic Diffuse Global Illumination (DDGI) ```python # DDGI Probe Grid scene.eevee.gi_diffuse_bounces = 3 scene.eevee.gi_cubemap_resolution = '512' # 256, 512, 1024 scene.eevee.gi_probe_count = 64 # Per probe grid scene.eevee.gi_probe_distance = 10.0 # Spacing # Probe Grid Object grid = bpy.data.objects.new("DDGI_Probe_Grid", None) grid.empty_display_type = 'CUBE' grid.empty_display_size = 10.0 bpy.context.collection.objects.link(grid) # Add DDGI Probe Grid modifier mod = grid.modifiers.new("DDGI_Probe_Grid", 'NODES') # Configure in Geometry Nodes ``` ### 4. Radiance Cache (Real-time GI) ```python # Radiance Cache (screen-space GI fallback) scene.eevee.gi_cache_resolution = '512' scene.eevee.gi_cache_frames = 10 # Temporal accumulation scene.eevee.gi_cache_quality = 'HIGH' # Works with DDGI for hybrid GI # Radiance Cache = Screen-space (fast, noisy) # DDGI = World-space (stable, memory) ``` ### 5. Volumetric Rendering (Voxel-based) ```python # Volumetric Settings scene.eevee.volumetric_tile_size = '64' # 32, 64, 128 scene.eevee.volumetric_samples = 32 scene.eevee.volumetric_shadow_samples = 8 # Volumetric Lighting light = bpy.data.lights["Spot"] light.use_volumetric = True light.volumetric_shadow_samples = 4 # Volumetric Material mat = bpy.data.materials.new("Volume_Fog") mat.use_nodes = True nodes = mat.node_tree.nodes nodes.clear() volume = nodes.new('ShaderNodeVolumePrincipled') volume.inputs['Density'].default_value = 0.1 volume.inputs['Anisotropy'].default_value = 0.5 output = nodes.new('ShaderNodeOutputMaterial') output.target = 'VOLUME' links.new(volume.outputs['Volume'], output.inputs['Volume']) ``` ### 6. Subsurface Scattering (Real-time) ```python # SSS in EEVEE Next mat = bpy.data.materials.new("Skin") mat.use_nodes = True nodes = mat.node_tree.nodes principled = nodes['Principled BSDF'] # Enable SSS principled.inputs['Subsurface Weight'].default_value = 0.5 principled.inputs['Subsurface Radius'].default_value = (1.0, 0.2, 0.1) principled.inputs['Subsurface Color'].default_value = (1.0, 0.3, 0.1, 1.0) principled.inputs['Subsurface IOR'].default_value = 1.4 # EEVEE Next: Real-time SSS (no bake needed!) # Screen-space + Ray-traced fallback ``` --- ## 🎨 MATERIAL NODES (EEVEE Next) ### New Nodes ```python # New Shader Nodes in EEVEE Next # ----------------------------------- # Ray Portal: Portal for interior rendering # Irradiance Volume: Light probe data # Radiance Probe: DDGI probe data # Light Probe: Generic light probe # Volume Info: Volume properties # Ray Length: Ray distance # Usage in Shader Editor: # 1. Add → Ray Portal # 2. Connect to World Output → Volume # 3. Position at windows/doors ``` ### Screen-Space Effects ```python # Ambient Occlusion scene.eevee.use_gtao = True scene.eevee.gtao_distance = 0.5 scene.eevee.gtao_factor = 1.0 scene.eevee.gtao_bounce_factor = 1.0 # Bloom scene.eevee.use_bloom = True scene.eevee.bloom_threshold = 0.8 scene.eevee.bloom_intensity = 0.05 scene.eevee.bloom_radius = 6.5 scene.eevee.bloom_threshold = 0.8 # Motion Blur scene.eevee.use_motion_blur = True scene.eevee.motion_blur_shutter = 0.5 scene.eevee.motion_blur_samples = 16 scene.eevee.motion_blur_shutter_curve = [(0,0), (0.5,1), (1,0)] ``` --- ## 🎬 RENDER SETTINGS ### Quality Presets ```python # Preset: Draft (Real-time) def preset_draft(): s = bpy.context.scene.eevee s.shadow_cube_size = '512' s.shadow_cascade_size = '512' s.gi_cubemap_resolution = '256' s.gi_cache_resolution = '256' s.volumetric_tile_size = '32' s.volumetric_samples = 8 s.gtao_distance = 0.2 s.bloom_intensity = 0.05 # Preset: Production def preset_production(): s = bpy.context.scene.eevee s.shadow_cube_size = '2048' s.shadow_cascade_size = '2048' s.gi_cubemap_resolution = '1024' s.gi_cache_resolution = '512' s.volumetric_tile_size = '64' s.volumetric_samples = 64 s.gtao_distance = 1.0 s.bloom_intensity = 0.1 # Preset: Cinematic def preset_cinematic(): s = bpy.context.scene.eevee s.shadow_cube_size = '4096' s.shadow_cascade_size = '4096' s.gi_cubemap_resolution = '2048' s.gi_cache_resolution = '1024' s.volumetric_tile_size = '128' s.volumetric_samples = 128 s.gtao_distance = 2.0 s.bloom_intensity = 0.15 s.use_ssr = True s.use_ssr_refraction = True ``` --- ## 🎮 GAME ENGINE EXPORT (EEVEE → Engine) ### Unreal Engine Compatibility ```python def export_eevee_material_for_unreal(mat_name): """Export EEVEE material as compatible UE material""" mat = bpy.data.materials[mat_name] # EEVEE Next → UE5 Material mapping: mapping = { 'Base Color': 'BaseColor', 'Metallic': 'Metallic', 'Roughness': 'Roughness', 'Normal': 'Normal', 'Emission': 'EmissiveColor', 'Alpha': 'Opacity', 'Subsurface': 'Subsurface', 'Clearcoat': 'ClearCoat', 'Clearcoat Roughness': 'ClearcoatRoughness', 'Anisotropic': 'Anisotropy', 'Sheen': 'Sheen', 'Transmission': 'Transmission', } # Export textures for slot in mat.node_tree.nodes: if slot.type == 'TEX_IMAGE' and slot.image: # Export as PNG/EXR pass ``` --- ## ⚡ PERFORMANCE OPTIMIZATION ### Viewport Optimization ```python # Viewport Settings (not render) for area in bpy.context.screen.areas: if area.type == 'VIEW_3D': space = area.spaces.active space.shading.type = 'RENDERED' space.shading.use_scene_lights = True space.shading.use_scene_world = True # Performance space.shading.show_shadows = True space.shading.show_cavity = True space.shading.cavity_type = 'SCREEN' space.shading.cavity_valley_factor = 0.5 space.shading.cavity_ridge_factor = 0.5 ``` ### Instancing for Massive Scenes ```python # Collection Instancing (EEVEE Next handles efficiently) def setup_instancing(collection_name, count=1000): coll = bpy.data.collections.new(f"Instances_{collection_name}") # Source object src = bpy.data.objects.get(collection_name) if not src: return # Instancer instancer = bpy.data.objects.new(f"Instancer_{collection_name}", None) instancer.instance_type = 'COLLECTION' instancer.instance_collection = bpy.data.collections[collection_name] bpy.context.collection.objects.link(instancer) # Geometry Nodes for distribution gn = bpy.data.node_groups.new("Distribute", 'GeometryNodeTree') # ... distribute points on faces, instance collection ``` --- ## 🐛 TROUBLESHOOTING | Issue | Cause | Fix | |-------|-------|-----| | Black shadows | Shadow bias too high | Reduce `shadow_buffer_bias` | | Flickering shadows | Cascade transition | Increase cascade count, adjust distances | | SSR artifacts | Screen-space limits | Enable ray-tracing fallback, increase thickness | | GI flickering | DDGI probe update | Increase probe count, reduce update frequency | | Volumetric noise | Low samples | Increase `volumetric_samples`, use denoiser | | VRAM OOM | High res shadows/textures | Reduce shadow map size, use texture streaming | --- ## 🔗 LINKS - **EEVEE Next Announcement:** https://developer.blender.org/ (ищите в Projects → EEVEE Next) - **Vulkan Backend:** https://developer.blender.org/ (ищите в Projects → Vulkan) - **Vulkan Validation Layers:** https://vulkan.lunarg.com/doc/view/latest/windows/layer_configuration.html - **Blender GPU Requirements:** https://www.blender.org/download/requirements/ > [!NOTE] > Ссылки на конкретные задачи (T112345, T112346) убраны — такие тикеты не существуют в публичном трекере Blender. Используйте поиск на developer.blender.org по ключевым словам «EEVEE Next» и «Vulkan». --- ## 🌍 MULTILANG | Language | File | |----------|------| | English | `multilang/en/realtime/eevee_next.md` | | Russian | `multilang/ru/realtime/eevee_next.md` | --- # ANIMATION WORKFLOWS Tags: #animation #frame #reference # ANIMATION WORKFLOWS ## Overview Rigging, animation, and rendering pipelines for Blender. --- ## Rigging ### Rigify (Humanoid) ``` 1. Import character mesh (T-pose or A-pose) 2. Add Armature → Human (Meta-Rig) 3. Scale/position meta-rig to match mesh 4. Generate Rig (Rigify buttons → Generate) 5. Parent: Mesh → Armature (Automatic Weights) 6. Weight paint cleanup if needed ``` ### Auto-Rig Pro / Rigify Pro (Advanced) - Better facial rigs - IK/FK switching - Custom bone shapes ### Simple Rig (Props/Mechanical) ``` 1. Add bones for each moving part 2. Parent bones to mesh (Bone Heat / Envelope) 3. Add constraints: Copy Location/Rotation/Scale 4. Add drivers for mechanical relationships ``` --- ## Keyframe Animation ### Basic Workflow ``` 1. Set frame range: Scene → Frame Start/End 2. Pose at frame 1 → Insert keyframes (I → Location/Rotation/Scale) 3. Move to frame 50 → Pose → Insert keyframes 4. Graph Editor: Clean curves, set interpolation 5. Dope Sheet: Adjust timing ``` ### Interpolation Types | Type | Use Case | |------|----------| | Bezier | Smooth, natural motion (default) | | Linear | Mechanical, constant speed | | Constant | Hold pose (stepped) | | Elastic/Bounce | Cartoon effects | ### Graph Editor Tips ``` - T: Interpolation menu - V: Handle type (Auto, Vector, Aligned, Free) - Shift+E: Auto-clamp handles - Period (.) on numpad: Frame selected - Ctrl+Click: Add keyframe at cursor ``` --- ## NLA (Non-Linear Animation) ### Action Strips ``` 1. Create actions: Walk, Run, Idle, Attack 2. NLA Editor → Push Down (stash) each action 3. Blend strips: Overlap for transitions 4. Extrapolation: Hold / Nothing / Repeat 5. Layer strips: Additive layers for details ``` ### Workflow ``` Base Layer: Walk cycle (repeat) Layer 1: Arm swing variation (additive) Layer 2: Head turn (override) Layer 3: Facial expression (override) ``` --- ## Motion Capture / Retargeting ### Import BVH/FBX ``` 1. File → Import → BVH / FBX 2. Creates armature with animation 3. Retarget to your rig: - Add constraints: Copy Rotation (target mocap bone) - Or use Retargeting addon (Rigify has built-in) ``` ### Retargeting Workflow (Rigify) ``` 1. Import mocap → creates "Mocap" armature 2. Select your Rigify rig 3. Bone Constraints → Copy Rotation → Target: Mocap bone 4. Bake Action (Pose → Animation → Bake Action) 5. Clean up: Remove constraints, simplify keys ``` --- ## Procedural Animation ### Drivers ```python # Driver: Gear rotation based on another gear # Driver expression: -var * (teeth_a / teeth_b) # Setup via Python driver = gear_b.driver_add('rotation_euler', 2).driver driver.type = 'SCRIPTED' var = driver.variables.new() var.name = 'gear_a_rot' var.targets[0].id = gear_a var.targets[0].data_path = 'rotation_euler[2]' driver.expression = '-gear_a_rot * 0.5' # 2:1 ratio ``` ### Geometry Nodes Animation ``` 1. Geometry Nodes → Object Info → Get animated object 2. Use frame number: #frame 3. Math nodes for procedural motion 4. Instance on points with animation offset ``` ### Simulation (Physics) ``` - Rigid Body: Falling objects, destruction - Cloth: Flags, capes, clothing - Fluid: Water, smoke, fire - Particles: Magic effects, debris, crowds - Soft Body: Jelly, flesh deformation ``` --- ## Camera Animation ### Dolly/Crane ``` 1. Add Curve (Bezier) for path 2. Camera → Follow Path constraint 3. Target Empty → Track To constraint 4. Animate Curve Evaluation (0→1 over frames) ``` ### Handheld Shake ``` 1. Camera → Add Noise modifier (Graph Editor) 2. Scale: 0.01-0.05, Strength: 0.1-0.5 3. Or: Python script for procedural shake ``` ### DOF (Depth of Field) ``` 1. Camera → Depth of Field → Focus Object: Target Empty 2. F-Stop: 1.4-2.8 (shallow) / 8-16 (deep) 3. Animate Focus Distance for rack focus ``` --- ## Rendering Animation ### Settings ```python scene = bpy.context.scene scene.frame_start = 1 scene.frame_end = 250 scene.render.fps = 24 scene.render.resolution_x = 1920 scene.render.resolution_y = 1080 # Output scene.render.filepath = "//renders/anim_####.png" scene.render.image_settings.file_format = 'PNG' scene.render.image_settings.color_mode = 'RGBA' scene.render.image_settings.compression = 15 scene.render.use_motion_blur = True scene.render.motion_blur_shutter = 0.5 scene.render.motion_blur_samples = 16 ``` ### Motion Blur ``` Cycles: - Render → Motion Blur → Position: Center/Start/End - Shutter: 0.5 (180° shutter) - Samples: 16+ for quality EEVEE: - Render → Motion Blur → Enable - Steps: 8-16 ``` ### Frame Sequencing ```bash # Render frames blender -b scene.blend -E CYCLES -o //renders/frame_#### -f 1..250 # Encode to video (ffmpeg) ffmpeg -framerate 24 -i //renders/frame_%04d.png -c:v libx264 -pix_fmt yuv420p output.mp4 # Or in Blender: Video Sequencer → Add → Image Sequence → Export FFmpeg ``` --- ## Alembic Cache (Pipeline) ### Export ```python bpy.ops.wm.alembic_export( filepath="animation.abc", start=1, end=250, selected=True, flatten=True, subsamples=4 ) ``` ### Import ```python bpy.ops.wm.alembic_import( filepath="animation.abc", as_background_job=False ) ``` --- ## Crowd Animation ### Particle Instancing ``` 1. Emitter plane → Particle System (Hair) 2. Render → Object → Collection (pick character collection) 3. Advanced → Rotation: Velocity/Random 4. Scale: Random 0.9-1.1 5. Use Collection instances for variation ``` ### Geometry Nodes Crowd ``` 1. Grid of points 2. Random select character from collection 3. Instance with random animation offset 4. Look-at target for direction ``` --- ## Lip Sync ### Papagayo / Rhubarb ``` 1. Audio → Rhubarb Lip Sync → .json/.tsv 2. Import to Blender (Papagayo addon or custom script) 3. Map phonemes to shape keys 4. Bake to shape key animation ``` ### Shape Keys Setup ``` 1. Create basis + phoneme shapes (A, E, I, O, U, M, etc.) 2. Driver each shape key from phoneme track 3. Smooth with Graph Editor ``` --- ## Export for Game Engines ### Unity/Unreal (FBX) ```python bpy.ops.export_scene.fbx( filepath="character.fbx", use_selection=True, apply_unit_scale=True, apply_scale_options='FBX_SCALE_ALL', bake_space_transform=True, object_types={'ARMATURE', 'MESH'}, use_armature_deform_only=True, add_leaf_bones=False, primary_bone_axis='Y_FORWARD', secondary_bone_axis='X_UP' ) ``` ### Web (GLTF) ```python bpy.ops.export_scene.gltf( filepath="character.glb", export_format='GLB', use_selection=True, export_animations=True, export_frame_range=True, export_frame_step=1, export_force_sampling=True, export_skins=True, export_morph=True ) ``` --- ## Performance Tips ### Animation Optimization ``` - Reduce keyframes: Simplify curves (Graph Editor → Key → Simplify) - Use actions for reusable cycles - Bake physics to keyframes for render farm - Proxy/low-res meshes for viewport animation - Hide heavy collections while animating ``` ### Render Farm ``` 1. Split frames across machines 2. Use Flamenco (Blender's render farm) or Deadline 3. Shared storage for assets 4. Alembic caches for sim data 5. Collect → Encode → Review ``` --- ## AI-Assisted Animation (via MCP) ### Prompt Examples ``` "Animate camera: 360° orbit around object over 120 frames, ease in/out" "Create walk cycle for rigged character: 24 frames, loop seamlessly" "Add noise modifier to camera rotation for handheld feel" "Set up DOF rack focus from object A to B over 60 frames" "Create particle explosion at frame 50, 200 particles, 2 second life" "Bake rigid body simulation for 100 falling boxes to keyframes" ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [BLENDER MATERIALS API REFERENCE](../api/materials.md), [BLENDER MESH OPERATIONS API REFERENCE](../api/mesh-operations.md), [BLENDER PYTHON API REFERENCE (4.x)](../api/blender-api.md), [BLENDER SCRIPTING PATTERNS - SAFE & EFFECTIVE](../api/scripting-patterns.md), [BLENDER-MCP - FULL DOCUMENTATION](../addons/blender-mcp-full.md) - **По темам:** [BLENDER-MCP ADDON - DEEP DIVE](../addons/blender-mcp.md), [CASE 04: CHARACTER RIG & ANIMATION](../../../cases/case_04_character_rig.md), [CASE 06: ANIMATION LOOP](../../../cases/case_06_animation_loop.md) - **Теги:** #animation, #frame, #reference --- --- ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- # BLENDER MATERIALS API REFERENCE Tags: #api #blender #reference #shading # BLENDER MATERIALS API REFERENCE ## Principled BSDF (Standard PBR) ### Input Sockets ```python principled = nodes["Principled BSDF"] # Base Layer principled.inputs['Base Color'].default_value # RGBA (0-1) principled.inputs['Subsurface'].default_value # Float (0-1) principled.inputs['Subsurface Radius'].default_value # RGB principled.inputs['Subsurface Color'].default_value # RGBA principled.inputs['Subsurface IOR'].default_value # Float principled.inputs['Subsurface Anisotropy'].default_value # Float # Metallic principled.inputs['Metallic'].default_value # Float (0-1) principled.inputs['Specular'].default_value # Float (0-1) principled.inputs['Specular Tint'].default_value # Float (0-1) # Roughness principled.inputs['Roughness'].default_value # Float (0-1) principled.inputs['Anisotropic'].default_value # Float (0-1) principled.inputs['Anisotropic Rotation'].default_value # Float (0-1) # Transmission (Glass) principled.inputs['Transmission'].default_value # Float (0-1) principled.inputs['Transmission Roughness'].default_value # Float # Coat (Clearcoat) principled.inputs['Coat Weight'].default_value # Float (0-1) principled.inputs['Coat Roughness'].default_value # Float (0-1) # Sheen (Fabric) principled.inputs['Sheen Weight'].default_value # Float (0-1) principled.inputs['Sheen Roughness'].default_value # Float (0-1) principled.inputs['Sheen Tint'].default_value # Float (0-1) # Emission principled.inputs['Emission Color'].default_value # RGBA principled.inputs['Emission Strength'].default_value # Float # Alpha principled.inputs['Alpha'].default_value # Float (0-1) # Normal principled.inputs['Normal'].default_value # Vector (from Normal Map) principled.inputs['Clearcoat Normal'].default_value # Vector # Tangent principled.inputs['Tangent'].default_value # Vector (for Anisotropic) ``` ### Output Sockets ```python principled.outputs['BSDF'] # Shader (to Material Output) principled.outputs['Alpha'] # Float (for transparency) ``` --- ## Complete PBR Material Setup ### From Texture Maps ```python def create_pbr_material(name, maps, obj=None): """maps: dict with keys: base_color, roughness, normal, metallic, height, ao, emission""" mat = bpy.data.materials.new(name) mat.use_nodes = True nodes = mat.node_tree.nodes links = mat.node_tree.links nodes.clear() # Principled BSDF principled = nodes.new('ShaderNodeBsdfPrincipled') principled.location = (0, 0) # Material Output output = nodes.new('ShaderNodeOutputMaterial') output.location = (400, 0) links.new(principled.outputs['BSDF'], output.inputs['Surface']) # Displacement output (for height) disp = nodes.new('ShaderNodeDisplacement') disp.location = (200, -200) links.new(disp.outputs['Displacement'], output.inputs['Displacement']) mat.displacement_method = 'BOTH' # Texture Coordinate + Mapping (for all textures) tex_coord = nodes.new('ShaderNodeTexCoord') tex_coord.location = (-600, 0) mapping = nodes.new('ShaderNodeMapping') mapping.location = (-400, 0) links.new(tex_coord.outputs['UV'], mapping.inputs['Vector']) def add_texture(map_key, input_name, colorspace='sRGB', use_normal_map=False, use_disp=False): if map_key not in maps: return img = maps[map_key] tex = nodes.new('ShaderNodeTexImage') tex.image = img tex.image.colorspace_settings.name = colorspace tex.location = (-200, 0) links.new(mapping.outputs['Vector'], tex.inputs['Vector']) if use_normal_map: nmap = nodes.new('ShaderNodeNormalMap') nmap.location = (0, -100) links.new(tex.outputs['Color'], nmap.inputs['Color']) links.new(nmap.outputs['Normal'], principled.inputs[input_name]) elif use_disp: links.new(tex.outputs['Color'], disp.inputs['Height']) else: links.new(tex.outputs['Color'], principled.inputs[input_name]) # Connect maps add_texture('base_color', 'Base Color', 'sRGB') add_texture('roughness', 'Roughness', 'Non-Color') add_texture('metallic', 'Metallic', 'Non-Color') add_texture('normal', 'Normal', 'Non-Color', use_normal_map=True) add_texture('height', 'Height', 'Non-Color', use_disp=True) add_texture('ao', 'Base Color', 'Non-Color') # Mix with AO separately add_texture('emission', 'Emission Color', 'sRGB') # AO Mix (Multiply with Base Color) if 'ao' in maps: ao_tex = nodes.new('ShaderNodeTexImage') ao_tex.image = maps['ao'] ao_tex.image.colorspace_settings.name = 'Non-Color' ao_tex.location = (-200, 200) links.new(mapping.outputs['Vector'], ao_tex.inputs['Vector']) mix = nodes.new('ShaderNodeMixRGB') mix.blend_type = 'MULTIPLY' mix.location = (0, 100) links.new(principled.inputs['Base Color'].links[0].from_node.outputs['Color'], mix.inputs[1]) links.new(ao_tex.outputs['Color'], mix.inputs[2]) links.new(mix.outputs['Color'], principled.inputs['Base Color']) # Assign to object if obj: if obj.data.materials: obj.data.materials[0] = mat else: obj.data.materials.append(mat) return mat ``` --- ## Node Types Reference ### Texture Nodes ```python # Image Texture tex = nodes.new('ShaderNodeTexImage') tex.image = bpy.data.images.load("path.png") tex.projection = 'FLAT' # 'BOX', 'SPHERE', 'TUBE' tex.interpolation = 'Linear' # 'Closest', 'Cubic', 'Smart' # Environment Texture (HDRI) env = nodes.new('ShaderNodeTexEnvironment') env.image = bpy.data.images.load("hdri.exr") env.projection = 'EQUIRECTANGULAR' # Procedural Textures noise = nodes.new('ShaderNodeTexNoise') noise.noise_dimensions = '4D' noise.noise_type = 'FBM' voronoi = nodes.new('ShaderNodeTexVoronoi') voronoi.feature = 'SMOOTH_F1' # 'F1', 'F2', 'SMOOTH_F1', 'DISTANCE_TO_EDGE' voronoi.metric = 'MANHATTAN' # 'EUCLIDEAN', 'MANHATTAN', 'CHEBYCHEV', 'MINKOWSKI' musgrave = nodes.new('ShaderNodeTexMusgrave') musgrave.musgrave_dimensions = '3D' musgrave.musgrave_type = 'MULTIFRACTAL' magic = nodes.new('ShaderNodeTexMagic') wave = nodes.new('ShaderNodeTexWave') wave.wave_type = 'SINE' # 'SAW', 'TRI' brick = nodes.new('ShaderNodeTexBrick') checker = nodes.new('ShaderNodeTexChecker') gradient = nodes.new('ShaderNodeTexGradient') gradient.gradient_type = 'LINEAR' # 'QUADRATIC', 'EASING', 'DIAGONAL', 'RADIAL', 'QUADRATIC_SPHERE' ``` ### Color Nodes ```python # Mix RGB mix = nodes.new('ShaderNodeMixRGB') mix.blend_type = 'MIX' # 'MULTIPLY', 'SCREEN', 'OVERLAY', 'SOFT_LIGHT', 'HARD_LIGHT', 'DIFFERENCE', 'EXCLUSION', 'HUE', 'SATURATION', 'VALUE', 'COLOR' # Color Ramp ramp = nodes.new('ShaderNodeValToRGB') ramp.color_ramp.interpolation = 'LINEAR' # 'CONSTANT', 'EASE', 'CARDINAL', 'B_SPLINE' # ramp.color_ramp.elements[0].position = 0.0 # ramp.color_ramp.elements[0].color = (0,0,0,1) # RGB Curves curves = nodes.new('ShaderNodeRGBCurve') curves.mapping.curves[0].points[0].location = (0, 0) curves.mapping.curves[0].points[1].location = (1, 1) # Hue/Saturation/Value hsv = nodes.new('ShaderNodeHueSaturation') hsv.hue = 0.5 hsv.saturation = 1.0 hsv.value = 1.0 hsv.fac = 1.0 # Gamma gamma = nodes.new('ShaderNodeGamma') gamma.gamma = 2.2 # Brightness/Contrast bc = nodes.new('ShaderNodeBrightContrast') bc.bright = 0.0 bc.contrast = 0.0 # Invert invert = nodes.new('ShaderNodeInvert') invert.fac = 1.0 ``` ### Vector/Math Nodes ```python # Mapping mapping = nodes.new('ShaderNodeMapping') mapping.vector_type = 'POINT' # 'TEXTURE', 'VECTOR', 'NORMAL' # mapping.inputs['Location'].default_value = (0,0,0) # mapping.inputs['Rotation'].default_value = (0,0,0) # mapping.inputs['Scale'].default_value = (1,1,1) # Texture Coordinate tex_coord = nodes.new('ShaderNodeTexCoord') # Outputs: Generated, Normal, UV, Object, Camera, Window, Reflection # Vector Math vmath = nodes.new('ShaderNodeVectorMath') vmath.operation = 'ADD' # 'SUBTRACT', 'MULTIPLY', 'DIVIDE', 'CROSS_PRODUCT', 'PROJECT', 'REFLECT', 'DOT_PRODUCT', 'DISTANCE', 'LENGTH', 'SCALE', 'NORMALIZE', 'ABSOLUTE', 'FLOOR', 'CEIL', 'MODULO', 'FRACTION', 'MINIMUM', 'MAXIMUM', 'SNAP', 'WRAP', 'PINGPONG', 'SINE', 'COSINE', 'TANGENT' # Math math = nodes.new('ShaderNodeMath') math.operation = 'ADD' # 'SUBTRACT', 'MULTIPLY', 'DIVIDE', 'SINE', 'COSINE', 'TANGENT', 'ARCSINE', 'ARCCOSINE', 'ARCTANGENT', 'ARCTAN2', 'POWER', 'LOGARITHM', 'MINIMUM', 'MAXIMUM', 'ROUND', 'LESS_THAN', 'GREATER_THAN', 'MODULO', 'ABSOLUTE', 'EXPONENT', 'SIGN', 'COMPARE', 'SMOOTH_MIN', 'SMOOTH_MAX', 'ROUND', 'TRUNC', 'FRACT', 'FLOOR', 'CEIL', 'FRACTION', 'PINGPONG', 'SNAP', 'WRAP' math.use_clamp = False ``` ### Shader Nodes ```python # Glossy BSDF glossy = nodes.new('ShaderNodeBsdfGlossy') glossy.distribution = 'GGX' # 'ASHIKHMIN', 'BECKMANN', 'MULTI_GGX' # Glass BSDF glass = nodes.new('ShaderNodeBsdfGlass') glass.distribution = 'GGX' # Transparent BSDF transp = nodes.new('ShaderNodeBsdfTransparent') # Emission emission = nodes.new('ShaderNodeEmission') emission.inputs['Color'].default_value = (1,1,1,1) emission.inputs['Strength'].default_value = 1.0 # Volume volume = nodes.new('ShaderNodeVolumePrincipled') volume_abs = nodes.new('ShaderNodeVolumeAbsorption') volume_scat = nodes.new('ShaderNodeVolumeScatter') # Mix Shader mix_shader = nodes.new('ShaderNodeMixShader') # Factor 0-1, Shader1, Shader2 # Add Shader add_shader = nodes.new('ShaderNodeAddShader') ``` ### Output Nodes ```python # Material Output output = nodes.new('ShaderNodeOutputMaterial') # Inputs: Surface, Volume, Displacement, Thickness # World Output world_out = nodes.new('ShaderNodeOutputWorld') # Inputs: Surface, Volume ``` --- ## Utility Functions ### Find/Replace Nodes ```python def get_or_create_node(nodes, type_name, name=None): """Find existing node or create new""" for node in nodes: if node.bl_idname == type_name: return node node = nodes.new(type_name) if name: node.name = name return node def clear_node_tree(node_tree): """Remove all nodes""" for node in list(node_tree.nodes): node_tree.nodes.remove(node) ``` ### Auto-Layout ```python def auto_layout_nodes(nodes, start_x=-600, y_step=200): """Simple horizontal layout""" x = start_x for i, node in enumerate(nodes): node.location.x = x node.location.y = i * y_step x += 300 ``` ### Bake Texture Maps ```python def bake_texture(obj, map_type, output_image, margin=16): """Bake map_type to output_image""" # map_type: 'DIFFUSE', 'NORMAL', 'ROUGHNESS', 'EMIT', 'AO', 'COMBINED' bpy.context.view_layer.objects.active = obj obj.select_set(True) # Create bake node mat = obj.active_material nodes = mat.node_tree.nodes bake_node = nodes.new('ShaderNodeTexImage') bake_node.image = output_image bake_node.select = True nodes.active = bake_node # Bake bpy.ops.object.bake( type=map_type, use_selected_to_active=False, margin=margin ) nodes.remove(bake_node) output_image.save() ``` --- ## Common Material Presets ### Metal (Gold) ```python principled.inputs['Base Color'].default_value = (1.0, 0.843, 0.0, 1.0) # Gold principled.inputs['Metallic'].default_value = 1.0 principled.inputs['Roughness'].default_value = 0.1 ``` ### Metal (Chrome) ```python principled.inputs['Base Color'].default_value = (1.0, 1.0, 1.0, 1.0) principled.inputs['Metallic'].default_value = 1.0 principled.inputs['Roughness'].default_value = 0.0 ``` ### Glass ```python principled.inputs['Transmission'].default_value = 1.0 principled.inputs['Roughness'].default_value = 0.0 principled.inputs['IOR'].default_value = 1.45 ``` ### Emission (Neon) ```python principled.inputs['Emission Color'].default_value = (0.0, 1.0, 1.0, 1.0) # Cyan principled.inputs['Emission Strength'].default_value = 10.0 ``` ### Subsurface (Skin/Wax) ```python principled.inputs['Subsurface'].default_value = 0.5 principled.inputs['Subsurface Radius'].default_value = (1.0, 0.2, 0.1) principled.inputs['Subsurface Color'].default_value = (1.0, 0.3, 0.1, 1.0) ``` ### Fabric (Sheen) ```python principled.inputs['Sheen Weight'].default_value = 0.5 principled.inputs['Sheen Roughness'].default_value = 0.3 principled.inputs['Sheen Tint'].default_value = 0.5 ``` ### Car Paint (Coat) ```python principled.inputs['Base Color'].default_value = (0.8, 0.0, 0.0, 1.0) principled.inputs['Metallic'].default_value = 0.8 principled.inputs['Roughness'].default_value = 0.2 principled.inputs['Coat Weight'].default_value = 1.0 principled.inputs['Coat Roughness'].default_value = 0.03 ``` --- ## Material Utilities ### Copy Material ```python new_mat = mat.copy() new_mat.name = "NewMaterial" ``` ### Link vs Append ```python # Link (reference original) with bpy.data.libraries.load("file.blend", link=True) as (data_from, data_to): data_to.materials = ["MaterialName"] # Append (copy into blend) with bpy.data.libraries.load("file.blend", link=False) as (data_from, data_to): data_to.materials = ["MaterialName"] ``` ### Remove Unused ```python # Remove unused materials for mat in list(bpy.data.materials): if mat.users == 0: bpy.data.materials.remove(mat) # Or: File → Clean Up → Unused Data Blocks ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [BLENDER MESH OPERATIONS API REFERENCE](mesh-operations.md), [BLENDER PYTHON API REFERENCE (4.x)](blender-api.md), [BLENDER SCRIPTING PATTERNS - SAFE & EFFECTIVE](scripting-patterns.md), [BLENDER-MCP - FULL DOCUMENTATION](../addons/blender-mcp-full.md), [BLENDER-MCP ADDON - DEEP DIVE](../addons/blender-mcp.md) - **По темам:** [INSTALLATION GUIDE - COMPLETE SETUP](../guides/installation.md), [MCP ARCHITECTURE - MODEL CONTEXT PROTOCOL FOR BLENDER](../mcp/architecture.md), [MCP SERVER API REFERENCE](../mcp/server-api.md) - **Теги:** #api, #blender, #reference, #shading --- ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- # BLENDER MESH OPERATIONS API REFERENCE Tags: #api #blender #modeling #reference # BLENDER MESH OPERATIONS API REFERENCE ## Primitive Creation ### bpy.ops (Context-dependent) ```python # Requires proper context (view_layer, active object) bpy.ops.mesh.primitive_cube_add(size=2, location=(0,0,0), rotation=(0,0,0)) bpy.ops.mesh.primitive_uv_sphere_add(radius=1, segments=32, ring_count=16) bpy.ops.mesh.primitive_ico_sphere_add(radius=1, subdivisions=2) bpy.ops.mesh.primitive_cylinder_add(radius=1, depth=2, vertices=32, cap_ends=True) bpy.ops.mesh.primitive_cone_add(radius1=1, radius2=0, depth=2, vertices=32) bpy.ops.mesh.primitive_torus_add( major_radius=1, minor_radius=0.3, major_segments=48, minor_segments=12 ) bpy.ops.mesh.primitive_plane_add(size=2) bpy.ops.mesh.primitive_grid_add(x_subdivisions=10, y_subdivisions=10, size=2) bpy.ops.mesh.primitive_monkey_add(size=1) # Suzanne ``` ### Low-Level (No Context) - PREFERRED ```python import bpy import bmesh from mathutils import Vector def create_cube(name="Cube", size=2, location=(0,0,0)): mesh = bpy.data.meshes.new(name) obj = bpy.data.objects.new(name, mesh) bpy.context.collection.objects.link(obj) obj.location = location bm = bmesh.new() bmesh.ops.create_cube(bm, size=size) bm.to_mesh(mesh) bm.free() return obj def create_uv_sphere(name="Sphere", radius=1, segments=32, ring_count=16): mesh = bpy.data.meshes.new(name) obj = bpy.data.objects.new(name, mesh) bpy.context.collection.objects.link(obj) bm = bmesh.new() bmesh.ops.create_uvsphere(bm, radius=radius, u_segments=segments, v_segments=ring_count) bm.to_mesh(mesh) bm.free() return obj def create_cylinder(name="Cylinder", radius=1, depth=2, vertices=32): mesh = bpy.data.meshes.new(name) obj = bpy.data.objects.new(name, mesh) bpy.context.collection.objects.link(obj) bm = bmesh.new() bmesh.ops.create_cone(bm, radius1=radius, radius2=radius, depth=depth, segments=vertices) bm.to_mesh(mesh) bm.free() return obj ``` --- ## BMesh Operations (Edit Mode Equivalent) ### Setup ```python import bmesh # Object mode BMesh (creates new mesh) bm = bmesh.new() # ... operations ... bm.to_mesh(mesh) bm.free() # Edit mode BMesh (modifies existing) bm = bmesh.from_edit_mesh(obj.data) # ... operations ... bmesh.update_edit_mesh(obj.data) ``` ### Vertex Operations ```python # Create vertices v1 = bm.verts.new((0, 0, 0)) v2 = bm.verts.new((1, 0, 0)) v3 = bm.verts.new((0, 1, 0)) # Select/deselect v1.select = True bm.verts.ensure_lookup_table() # Transform v1.co = (2, 0, 0) v1.co += Vector((0.1, 0, 0)) # Delete bm.verts.remove(v1) ``` ### Edge Operations ```python # Create edge e = bm.edges.new((v1, v2)) # Edge loops bmesh.ops.edge_loops(bm, edges=[e]) # Subdivide edges bmesh.ops.subdivide_edges(bm, edges=bm.edges, cuts=2) # Bevel edges bmesh.ops.bevel(bm, geom=bm.edges, offset=0.1, segments=3, profile=0.5) # Dissolve edges bmesh.ops.dissolve_edges(bm, edges=[e]) ``` ### Face Operations ```python # Create face f = bm.faces.new((v1, v2, v3)) # Triangle f = bm.faces.new((v1, v2, v3, v4)) # Quad # Face properties f.select = True f.material_index = 0 f.normal # mathutils.Vector # Extrude result = bmesh.ops.extrude_face_region(bm, geom=[f]) extruded_geom = result['geom'] # Move extruded faces for elem in extruded_geom: if isinstance(elem, bmesh.types.BMFace): bmesh.ops.translate(bm, verts=elem.verts, vec=(0, 0, 1)) # Inset bmesh.ops.inset_region(bm, faces=[f], thickness=0.1, depth=0.1) # Poke (triangle fan) bmesh.ops.poke(bm, faces=[f]) # Triangulate bmesh.ops.triangulate(bm, faces=bm.faces) # Subdivide faces bmesh.ops.subdivide_edges(bm, edges=[e for f in bm.faces for e in f.edges], cuts=1) ``` ### Selection & Queries ```python # Select all for v in bm.verts: v.select = True for e in bm.edges: e.select = True for f in bm.faces: f.select = True # Get selected selected_verts = [v for v in bm.verts if v.select] selected_edges = [e for e in bm.edges if e.select] selected_faces = [f for f in bm.faces if f.select] # By index v = bm.verts[0] e = bm.edges[0] f = bm.faces[0] # Lookup tables (required after topology changes) bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() bm.faces.ensure_lookup_table() ``` ### Mesh Modifiers (BMesh) ```python # Remesh (voxel) bmesh.ops.remesh(bm, voxel_size=0.1, adaptivity=0.0) # Smooth bmesh.ops.smooth_vert(bm, verts=bm.verts, factor=0.5, iterations=10) # Decimate # Not directly in bmesh - use modifier or bpy.ops # Delete loose bmesh.ops.delete_loose(bm, dist=0.001) ``` --- ## Mesh Data Access (bpy.data.meshes) ### Read Mesh Data ```python mesh = obj.data # Vertices for v in mesh.vertices: v.co # local coordinates v.normal # vertex normal v.groups # vertex groups v.index # index # Edges for e in mesh.edges: e.vertices # (v1_idx, v2_idx) e.use_seam # UV seam e.use_sharp # Sharp edge e.bevel_weight # Bevel weight # Polygons (faces) for poly in mesh.polygons: poly.vertices # vertex indices poly.loop_indices # loop indices poly.normal # face normal poly.center # face center poly.area # face area poly.material_index # material slot index # Loops (per-vertex-per-face data) for loop in mesh.loops: loop.vertex_index # vertex index loop.normal # loop normal (for smooth shading) # UV Layers for uv_layer in mesh.uv_layers: for uv in uv_layer.data: uv.uv # Vector (u, v) # Vertex Colors for vc_layer in mesh.vertex_colors: for vc in vc_layer.data: vc.color # RGBA (0-1) # Shape Keys if mesh.shape_keys: for key in mesh.shape_keys.key_blocks: key.value = 0.5 ``` --- ## Modifiers (via API) ### Add Modifier ```python mod = obj.modifiers.new("Subdivision", 'SUBSURF') mod.levels = 2 mod.render_levels = 3 mod.subdivision_type = 'CATMULL_CLARK' # 'SIMPLE' mod = obj.modifiers.new("Bevel", 'BEVEL') mod.width = 0.02 mod.segments = 3 mod.profile = 0.5 mod.limit_method = 'ANGLE' # 'WEIGHT', 'VGROUP', 'NONE' mod.angle_limit = 1.0472 # 60 degrees mod = obj.modifiers.new("Array", 'ARRAY') mod.count = 5 mod.use_relative_offset = True mod.relative_offset_displace = (1.5, 0, 0) mod = obj.modifiers.new("Mirror", 'MIRROR') mod.use_axis = (True, False, False) mod.use_bisect_axis = (True, False, False) mod = obj.modifiers.new("Boolean", 'BOOLEAN') mod.operation = 'DIFFERENCE' # 'UNION', 'INTERSECT' mod.object = other_obj mod.solver = 'EXACT' # 'FAST' mod = obj.modifiers.new("Solidify", 'SOLIDIFY') mod.thickness = 0.1 mod.offset = 0 # -1 to 1 mod.use_even_offset = True mod = obj.modifiers.new("Decimate", 'DECIMATE') mod.ratio = 0.5 # 0.0-1.0 mod.decimate_type = 'COLLAPSE' # 'UNSUBDIV', 'PLANNAR' mod = obj.modifiers.new("Remesh", 'REMESH') mod.mode = 'VOXEL' # 'SMOOTH', 'SHARP' mod.voxel_size = 0.1 mod.adaptivity = 0.0 ``` ### Apply Modifier ```python # Single bpy.ops.object.modifier_apply(modifier="Subdivision") # All for mod in list(obj.modifiers): bpy.ops.object.modifier_apply(modifier=mod.name) # Via Python (no context) bpy.context.view_layer.objects.active = obj bpy.ops.object.modifier_apply(modifier=mod.name) ``` --- ## Boolean Operations ### Using Modifier (Recommended) ```python def boolean_union(obj_a, obj_b): mod = obj_a.modifiers.new("Bool", 'BOOLEAN') mod.operation = 'UNION' mod.object = obj_b mod.solver = 'EXACT' bpy.context.view_layer.objects.active = obj_a bpy.ops.object.modifier_apply(modifier="Bool") bpy.data.objects.remove(obj_b, do_unlink=True) return obj_a def boolean_difference(obj_a, obj_b): mod = obj_a.modifiers.new("Bool", 'BOOLEAN') mod.operation = 'DIFFERENCE' mod.object = obj_b mod.solver = 'EXACT' bpy.context.view_layer.objects.active = obj_a bpy.ops.object.modifier_apply(modifier="Bool") bpy.data.objects.remove(obj_b, do_unlink=True) return obj_a def boolean_intersect(obj_a, obj_b): mod = obj_a.modifiers.new("Bool", 'BOOLEAN') mod.operation = 'INTERSECT' mod.object = obj_b mod.solver = 'EXACT' bpy.context.view_layer.objects.active = obj_a bpy.ops.object.modifier_apply(modifier="Bool") bpy.data.objects.remove(obj_b, do_unlink=True) return obj_a ``` --- ## UV Unwrapping ### Basic Unwrap ```python # Smart UV Project (best for hard surface) bpy.context.view_layer.objects.active = obj bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.uv.smart_project(angle_limit=1.0472, island_margin=0.02) bpy.ops.object.mode_set(mode='OBJECT') # Lightmap Pack (for baking) bpy.ops.uv.lightmap_pack(PADDING=4, Px_MARGIN=4) # Cube Projection bpy.ops.uv.cube_project() # Sphere Projection bpy.ops.uv.sphere_project() ``` ### Seam-Based Unwrap ```python bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='DESELECT') # Mark seams for e in bmesh.from_edit_mesh(obj.data).edges: if e.calc_face_angle() > 1.0472: # 60 degrees e.use_seam = True bpy.ops.mesh.select_all(action='SELECT') bpy.ops.uv.unwrap(method='ANGLE_BASED', margin=0.001) bpy.ops.object.mode_set(mode='OBJECT') ``` ### UV Manipulation ```python # Scale UVs uv_layer = mesh.uv_layers.active for uv in uv_layer.data: uv.uv *= 2.0 # Translate UVs for uv in uv_layer.data: uv.uv += (0.5, 0.5) # Rotate UVs import math angle = math.pi / 4 for uv in uv_layer.data: u, v = uv.uv uv.uv = (u * math.cos(angle) - v * math.sin(angle), u * math.sin(angle) + v * math.cos(angle)) ``` --- ## Vertex Groups / Weights ### Create & Assign ```python vg = obj.vertex_groups.new(name="GroupName") # Assign vertices with weight vg.add([0, 1, 2, 3], 1.0, 'REPLACE') # vertex indices, weight, type # Get weight weight = vg.weight(vertex_index) # Remove vg.remove([0, 1, 2]) ``` ### Vertex Group Operations ```python # Normalize all groups for vertex vg.normalize() # Clean (remove zero weights) vg.clean(0.001, 0.0, 0.0) # Mirror (for symmetrical rigs) vg.mirror("GroupName.R", "GroupName.L", True) # Copy new_vg = obj.vertex_groups.new(name="Copy") for v in obj.data.vertices: try: w = vg.weight(v.index) new_vg.add([v.index], w, 'REPLACE') except RuntimeError: pass # Vertex not in group ``` --- ## Normals & Shading ### Auto Smooth ```python obj.data.use_auto_smooth = True obj.data.auto_smooth_angle = 1.0472 # 60 degrees ``` ### Custom Normals ```python # Set custom normals mesh = obj.data mesh.use_auto_smooth = True normals = [] for poly in mesh.polygons: for loop_idx in poly.loop_indices: normals.append(poly.normal) mesh.normals_split_custom_set(normals) mesh.use_auto_smooth = True # Or from faces mesh.normals_split_custom_set_from_vertices([v.normal for v in mesh.vertices]) ``` ### Sharp Edges ```python # Mark sharp for e in mesh.edges: e.use_sharp = True # For auto smooth # Bevel weight for e in mesh.edges: e.bevel_weight = 1.0 ``` --- ## Vertex Colors ### Paint via Python ```python # Create layer vc_layer = mesh.vertex_colors.new(name="Paint") # Paint by face for poly in mesh.polygons: for loop_idx in poly.loop_indices: loop = mesh.loops[loop_idx] vc_layer.data[loop_idx].color = (1.0, 0.0, 0.0, 1.0) # Red # Gradient by height for poly in mesh.polygons: for loop_idx in poly.loop_indices: v = mesh.vertices[mesh.loops[loop_idx].vertex_index] h = (v.co.z - min_z) / (max_z - min_z) vc_layer.data[loop_idx].color = (h, h, h, 1.0) ``` --- ## Performance Tips ### Batch Operations ```python # Use foreach for bulk attribute access co_list = [0.0] * (len(mesh.vertices) * 3) mesh.vertices.foreach_get("co", co_list) # Modify for i in range(0, len(co_list), 3): co_list[i+2] += 0.1 # Raise Z # Set back mesh.vertices.foreach_set("co", co_list) mesh.update() ``` ### Efficient Selection ```python # Instead of loop: # for v in bm.verts: v.select = True # Use: bm.verts.ensure_lookup_table() for v in bm.verts: v.select_set(True) # or v.select = True ``` ### Avoid bpy.ops in Loops ```python # BAD for obj in objects: bpy.ops.object.modifier_apply(modifier="Subdivision") # GOOD - Single context override with bpy.context.temp_override(selected_objects=objects): bpy.ops.object.modifier_apply(modifier="Subdivision") ``` --- ## Common Patterns ### Create Mesh from Coordinates ```python def create_mesh_from_vertices(name, vertices, faces): """vertices: [(x,y,z), ...], faces: [(v1,v2,v3), ...]""" mesh = bpy.data.meshes.new(name) mesh.from_pydata(vertices, [], faces) mesh.update() obj = bpy.data.objects.new(name, mesh) bpy.context.collection.objects.link(obj) return obj # Usage verts = [(0,0,0), (1,0,0), (0,1,0), (1,1,0)] faces = [(0,1,3,2)] # Quad obj = create_mesh_from_vertices("Quad", verts, faces) ``` ### Duplicate & Separate ```python # Duplicate object + mesh new_obj = obj.copy() new_obj.data = obj.data.copy() bpy.context.collection.objects.link(new_obj) # Separate loose parts (Edit mode) bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.separate(type='LOOSE') bpy.ops.object.mode_set(mode='OBJECT') ``` ### Convert to Mesh ```python # Convert curve/text/meta to mesh bpy.context.view_layer.objects.active = obj bpy.ops.object.convert(target='MESH') ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [BLENDER MATERIALS API REFERENCE](materials.md), [BLENDER PYTHON API REFERENCE (4.x)](blender-api.md), [BLENDER SCRIPTING PATTERNS - SAFE & EFFECTIVE](scripting-patterns.md), [BLENDER-MCP - FULL DOCUMENTATION](../addons/blender-mcp-full.md), [BLENDER-MCP ADDON - DEEP DIVE](../addons/blender-mcp.md) - **По темам:** [INSTALLATION GUIDE - COMPLETE SETUP](../guides/installation.md), [MCP ARCHITECTURE - MODEL CONTEXT PROTOCOL FOR BLENDER](../mcp/architecture.md), [MCP SERVER API REFERENCE](../mcp/server-api.md) - **Теги:** #api, #blender, #modeling, #reference --- ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- # BLENDER PYTHON API REFERENCE (4.x) Tags: #api #blender #reference # BLENDER PYTHON API REFERENCE (4.x) ## Quick Reference ### Context & Data Access ```python import bpy # Context (read-only in background) bpy.context.scene # Current scene bpy.context.view_layer # Current view layer bpy.context.active_object # Active object (can be None) bpy.context.selected_objects # List of selected objects bpy.context.object # Same as active_object bpy.context.mode # Current mode ('OBJECT', 'EDIT', etc.) # Data (always accessible) bpy.data.objects # All objects bpy.data.meshes # All meshes bpy.data.materials # All materials bpy.data.collections # All collections bpy.data.scenes # All scenes bpy.data.images # All images bpy.data.node_groups # All node groups ``` ### Object Creation ```python # Primitives (bpy.ops - requires context) bpy.ops.mesh.primitive_cube_add(size=2, location=(0,0,0), rotation=(0,0,0)) bpy.ops.mesh.primitive_uv_sphere_add(radius=1, segments=32, ring_count=16) bpy.ops.mesh.primitive_cylinder_add(radius=1, depth=2, vertices=32) bpy.ops.mesh.primitive_plane_add(size=2) bpy.ops.mesh.primitive_cone_add(radius1=1, radius2=0, depth=2) bpy.ops.mesh.primitive_torus_add(major_radius=1, minor_radius=0.3) # Low-level (no context needed) - PREFERRED mesh = bpy.data.meshes.new("MyMesh") obj = bpy.data.objects.new("MyObject", mesh) bpy.context.collection.objects.link(obj) # Create mesh data bpy.ops.object.mode_set(mode='EDIT') bm = bmesh.from_edit_mesh(mesh) # ... bmesh operations ... bmesh.update_edit_mesh(mesh) bpy.ops.object.mode_set(mode='OBJECT') ``` ### Transform Operations ```python obj = bpy.data.objects["Cube"] # Location / Rotation / Scale obj.location = (1.0, 2.0, 3.0) obj.rotation_euler = (0, 0, 1.5708) # Radians obj.scale = (2.0, 2.0, 2.0) # Matrix (world transform) obj.matrix_world = mathutils.Matrix.Translation((1,2,3)) @ \ mathutils.Matrix.Rotation(1.57, 4, 'Z') # Apply transforms (Ctrl+A) bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) # Origin bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS') bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_MASS') ``` ### Mesh Editing (bmesh) ```python import bmesh # Edit mode bmesh bm = bmesh.from_edit_mesh(obj.data) # Create geometry v1 = bm.verts.new((0,0,0)) v2 = bm.verts.new((1,0,0)) v3 = bm.verts.new((0,1,0)) bm.faces.new((v1, v2, v3)) # Modifiers on bmesh bmesh.ops.subdivide_edges(bm, edges=bm.edges, cuts=2) bmesh.ops.bevel(bm, geom=bm.edges, offset=0.1, segments=3) bmesh.ops.extrude_face_region(bm, geom=[f for f in bm.faces]) # Update bmesh.update_edit_mesh(obj.data) # Object mode bmesh (new mesh) bm = bmesh.new() bmesh.ops.create_cube(bm, size=1.0) bm.to_mesh(mesh) bm.free() ``` ### Materials & Nodes ```python # Create material mat = bpy.data.materials.new("MyMaterial") mat.use_nodes = True nodes = mat.node_tree.nodes links = mat.node_tree.links # Clear default nodes.clear() # Principled BSDF (standard) principled = nodes.new('ShaderNodeBsdfPrincipled') principled.location = (0, 0) output = nodes.new('ShaderNodeOutputMaterial') output.location = (400, 0) links.new(principled.outputs['BSDF'], output.inputs['Surface']) # Set properties principled.inputs['Base Color'].default_value = (1.0, 0.0, 0.0, 1.0) # RGBA principled.inputs['Roughness'].default_value = 0.5 principled.inputs['Metallic'].default_value = 0.0 principled.inputs['Transmission'].default_value = 0.0 principled.inputs['Emission Strength'].default_value = 0.0 # Texture nodes tex_image = nodes.new('ShaderNodeTexImage') tex_image.image = bpy.data.images.load("path/to/texture.png") tex_coord = nodes.new('ShaderNodeTexCoord') mapping = nodes.new('ShaderNodeMapping') links.new(tex_coord.outputs['UV'], mapping.inputs['Vector']) links.new(mapping.outputs['Vector'], tex_image.inputs['Vector']) links.new(tex_image.outputs['Color'], principled.inputs['Base Color']) # Assign to object if obj.data.materials: obj.data.materials[0] = mat else: obj.data.materials.append(mat) ``` ### Modifiers ```python mod = obj.modifiers.new("Subdivision", 'SUBSURF') mod.levels = 2 mod.render_levels = 3 mod = obj.modifiers.new("Bevel", 'BEVEL') mod.width = 0.02 mod.segments = 3 mod.profile = 0.5 mod = obj.modifiers.new("Array", 'ARRAY') mod.count = 5 mod.use_relative_offset = True mod.relative_offset_displace = (1.5, 0, 0) mod = obj.modifiers.new("Boolean", 'BOOLEAN') mod.operation = 'DIFFERENCE' # UNION, INTERSECT mod.object = other_obj mod.solver = 'EXACT' # 'FAST' for simple # Apply modifier bpy.ops.object.modifier_apply(modifier="Subdivision") ``` ### Collections & Hierarchy ```python # Create collection col = bpy.data.collections.new("MyCollection") bpy.context.scene.collection.children.link(col) # Move object to collection col.objects.link(obj) bpy.context.scene.collection.objects.unlink(obj) # Parenting child.parent = parent child.parent_type = 'OBJECT' # 'BONE', 'VERTEX' # Collections for organization bpy.context.view_layer.active_layer_collection = \ bpy.context.view_layer.layer_collection.children["MyCollection"] ``` ### Animation ```python # Keyframes obj.location = (0, 0, 0) obj.keyframe_insert(data_path="location", frame=1) obj.location = (10, 0, 0) obj.keyframe_insert(data_path="location", frame=250) # Interpolation for fcurve in obj.animation_data.action.fcurves: for kp in fcurve.keyframe_points: kp.interpolation = 'BEZIER' # 'LINEAR', 'CONSTANT' # Drivers driver = obj.driver_add("location", 0).driver driver.type = 'SCRIPTED' var = driver.variables.new() var.name = "target_x" var.targets[0].id = target_obj var.targets[0].data_path = "location[0]" driver.expression = "target_x * 2" # NLA track = obj.animation_data.nla_tracks.new() strip = track.strips.new("ActionName", 1, obj.animation_data.action) strip.scale = 2.0 # Time scale ``` ### Geometry Nodes ```python # Create node group ng = bpy.data.node_groups.new("MyGeoNodes", 'GeometryNodeTree') nodes = ng.nodes links = ng.links # Input/Output input_node = nodes.new('NodeGroupInput') output_node = nodes.new('NodeGroupOutput') # Add sockets ng.interface.new_socket(name="Count", in_out='INPUT', socket_type='NodeSocketInt') ng.interface.new_socket(name="Geometry", in_out='OUTPUT', socket_type='NodeSocketGeometry') # Nodes mesh_line = nodes.new('GeometryNodeMeshLine') mesh_line.location = (-200, 0) mesh_line.mode = 'POINTS' links.new(input_node.outputs['Count'], mesh_line.inputs['Count']) instance = nodes.new('GeometryNodeInstanceOnPoints') instance.location = (200, 0) links.new(mesh_line.outputs['Mesh'], instance.inputs['Points']) links.new(input_node.outputs['Geometry'], instance.inputs['Instance']) links.new(instance.outputs['Instances'], output_node.inputs['Geometry']) # Apply to object mod = obj.modifiers.new("GeoNodes", 'NODES') mod.node_group = ng ``` ### Rendering ```python scene = bpy.context.scene # Engine scene.render.engine = 'CYCLES' # 'BLENDER_EEVEE', 'BLENDER_WORKBENCH' # Cycles settings scene.cycles.samples = 256 scene.cycles.use_denoising = True scene.cycles.device = 'GPU' # 'CPU', 'GPU' # EEVEE settings scene.eevee.taa_render_samples = 64 scene.eevee.use_bloom = True scene.eevee.use_ssr = True scene.eevee.use_ssr_refraction = True # Output scene.render.resolution_x = 1920 scene.render.resolution_y = 1080 scene.render.resolution_percentage = 100 scene.render.filepath = "//render_" scene.render.image_settings.file_format = 'PNG' scene.render.image_settings.color_mode = 'RGBA' scene.render.film_transparent = True # Render bpy.ops.render.render(write_still=True) bpy.ops.render.render(animation=True) ``` ### Import/Export ```python # Import bpy.ops.import_scene.gltf(filepath="model.glb") bpy.ops.import_scene.fbx(filepath="model.fbx") bpy.ops.import_mesh.stl(filepath="model.stl") bpy.ops.wm.obj_import(filepath="model.obj") bpy.ops.wm.ply_import(filepath="model.ply") # Export bpy.ops.export_scene.gltf(filepath="model.glb", export_format='GLB') bpy.ops.export_scene.fbx(filepath="model.fbx") bpy.ops.export_mesh.stl(filepath="model.stl") bpy.ops.wm.obj_export(filepath="model.obj") bpy.ops.wm.ply_export(filepath="model.ply") ``` ### Utilities ```python # Selection bpy.ops.object.select_all(action='DESELECT') obj.select_set(True) bpy.context.view_layer.objects.active = obj # Mode switching bpy.ops.object.mode_set(mode='EDIT') bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.mode_set(mode='SCULPT') # Delete bpy.data.objects.remove(obj, do_unlink=True) bpy.data.meshes.remove(mesh, do_unlink=True) # Duplicate new_obj = obj.copy() new_obj.data = obj.data.copy() # Unique mesh bpy.context.collection.objects.link(new_obj) # Convert to mesh bpy.ops.object.convert(target='MESH') # Shade bpy.ops.object.shade_smooth() bpy.ops.object.shade_flat() bpy.ops.object.shade_auto() # Units scene = bpy.context.scene scene.unit_settings.system = 'METRIC' scene.unit_settings.scale_length = 1.0 ``` ### Performance Tips ```python # Batch operations with bpy.context.temp_override(selected_objects=objects): bpy.ops.object.shade_smooth() # Disable updates during batch depsgraph = bpy.context.evaluated_depsgraph_get() depsgraph.update() # Use foreach for bulk property setting mesh.vertices.foreach_set("co", flat_coordinate_list) # Profile import cProfile, pstats profiler = cProfile.Profile() profiler.enable() # ... code ... profiler.disable() stats = pstats.Stats(profiler).sort_stats('cumulative') stats.print_stats(20) ``` --- ## Common Patterns for AI Automation ### Safe Object Access ```python # ALWAYS check existence obj = bpy.data.objects.get("Name") if obj is None: raise ValueError("Object not found") # ALWAYS use explicit references obj = bpy.data.objects["Name"] # Raises KeyError if missing ``` ### Thread-Safe Execution (for MCP) ```python # In blender-mcp addon: commands run on main thread via timer # In GenesisCore: direct bpy access (same process) # For background scripts: def run_in_main_thread(func): def wrapper(): return func() bpy.app.timers.register(wrapper, first_interval=0.01) ``` ### Error Handling Template ```python def safe_execute(code_func): try: result = code_func() return {"success": True, "result": result} except Exception as e: import traceback traceback.print_exc() return {"success": False, "error": str(e)} # Usage result = safe_execute(lambda: create_complex_scene()) ``` ### Version Compatibility ```python # Blender version check if bpy.app.version >= (4, 0, 0): # New API bpy.ops.mesh.primitive_cube_add(size=2) else: # Old API bpy.ops.mesh.primitive_cube_add(radius=1) # Feature detection has_asset_browser = hasattr(bpy.ops, 'asset') ``` --- ## Reference Links - [Blender Python API Docs](https://docs.blender.org/api/current/) - [bpy.types Documentation](https://docs.blender.org/api/current/bpy.types.html) - [bmesh Documentation](https://docs.blender.org/api/current/bmesh.html) - [mathutils Documentation](https://docs.blender.org/api/current/mathutils.html) --- ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- # BLENDER SCRIPTING PATTERNS - SAFE & EFFECTIVE Tags: #blender #reference # BLENDER SCRIPTING PATTERNS - SAFE & EFFECTIVE ## Core Principles ### 1. Always Check Existence ```python # BAD obj = bpy.data.objects["Cube"] obj.location.x = 1 # GOOD obj = bpy.data.objects.get("Cube") if obj: obj.location.x = 1 else: print("Object not found") ``` ### 2. Explicit Context Over Implicit ```python # BAD (relies on bpy.context.active_object) bpy.ops.object.shade_smooth() # GOOD (explicit) obj = bpy.data.objects["Cube"] bpy.context.view_layer.objects.active = obj obj.select_set(True) bpy.ops.object.shade_smooth() ``` ### 3. Mode Awareness ```python # Ensure correct mode before operations def ensure_mode(mode='OBJECT'): if bpy.context.mode != mode: bpy.ops.object.mode_set(mode=mode) # Usage ensure_mode('OBJECT') # ... object operations ... ensure_mode('EDIT') bm = bmesh.from_edit_mesh(obj.data) # ... bmesh operations ... bmesh.update_edit_mesh(obj.data) ensure_mode('OBJECT') ``` --- ## Safe Execution Template ### For MCP `execute_blender_code` ```python def safe_execute(operation_name, func): """ Wrapper for safe code execution in MCP. Returns dict with success/error info. """ try: # Ensure we're in object mode if bpy.context.mode != 'OBJECT': bpy.ops.object.mode_set(mode='OBJECT') result = func() # Force update bpy.context.view_layer.update() return { "success": True, "result": result, "operation": operation_name } except Exception as e: import traceback traceback.print_exc() return { "success": False, "error": str(e), "operation": operation_name, "traceback": traceback.format_exc() } # Usage def create_scene(): # ... your code ... return {"created": ["Cube", "Light", "Camera"]} result = safe_execute("create_scene", create_scene) ``` --- ## Common Patterns ### Pattern 1: Create Object with Material ```python def create_object_with_material(name, primitive_type, location, material_name, color): """Create primitive, add material, assign""" # Create mesh if primitive_type == 'CUBE': bpy.ops.mesh.primitive_cube_add(location=location) elif primitive_type == 'SPHERE': bpy.ops.mesh.primitive_uv_sphere_add(location=location) # ... other types obj = bpy.context.active_object obj.name = name # Create material mat = bpy.data.materials.new(material_name) mat.use_nodes = True nodes = mat.node_tree.nodes links = mat.node_tree.links nodes.clear() principled = nodes.new('ShaderNodeBsdfPrincipled') output = nodes.new('ShaderNodeOutputMaterial') links.new(principled.outputs['BSDF'], output.inputs['Surface']) principled.inputs['Base Color'].default_value = color + (1.0,) if len(color) == 3 else color principled.inputs['Roughness'].default_value = 0.5 principled.inputs['Metallic'].default_value = 0.0 # Assign if obj.data.materials: obj.data.materials[0] = mat else: obj.data.materials.append(mat) return obj ``` ### Pattern 2: Batch Create with Variations ```python def create_variations(base_name, count, position_func, variation_func): """Create multiple objects with variations""" objects = [] for i in range(count): loc = position_func(i) obj = create_object_with_material( f"{base_name}_{i:03d}", 'CUBE', loc, f"Mat_{base_name}_{i:03d}", variation_func(i) ) objects.append(obj) return objects # Usage def grid_position(i, cols=5): row = i // cols col = i % cols return (col * 3, row * 3, 0) def color_variation(i): import colorsys hue = (i * 0.618) % 1.0 # Golden ratio distribution r, g, b = colorsys.hsv_to_rgb(hue, 0.7, 0.9) return (r, g, b) objs = create_variations("Brick", 20, grid_position, color_variation) ``` ### Pattern 3: Hierarchical Scene Setup ```python def setup_scene_hierarchy(): """Create organized collection hierarchy""" # Root collection root = bpy.data.collections.new("Scene_Root") bpy.context.scene.collection.children.link(root) # Sub-collections collections = {} for name in ["Geometry", "Lights", "Cameras", "FX", "References"]: col = bpy.data.collections.new(name) root.children.link(col) collections[name] = col return collections def move_to_collection(obj, collection_name, collections): """Move object to named collection""" # Unlink from all for col in obj.users_collection: col.objects.unlink(obj) # Link to target collections[collection_name].objects.link(obj) ``` ### Pattern 4: Animation Keyframing ```python def animate_property(obj, prop_path, keyframes, interpolation='BEZIER'): """ keyframes: {frame: value, ...} prop_path: e.g., "location", "rotation_euler", "scale" """ for frame, value in keyframes.items(): setattr(obj, prop_path, value) obj.keyframe_insert(data_path=prop_path, frame=frame) # Set interpolation if obj.animation_data and obj.animation_data.action: for fcurve in obj.animation_data.action.fcurves: if fcurve.data_path == prop_path: for kp in fcurve.keyframe_points: kp.interpolation = interpolation # Usage animate_property(obj, "location", { 1: (0, 0, 0), 50: (10, 0, 0), 100: (10, 10, 0), 150: (0, 10, 0), 200: (0, 0, 0) }) ``` ### Pattern 5: Driver Setup ```python def add_driver(obj, prop_path, target_obj, target_prop, expression="var"): """Add driver to obj.prop_path driven by target_obj.target_prop""" driver = obj.driver_add(prop_path).driver driver.type = 'SCRIPTED' var = driver.variables.new() var.name = "var" var.targets[0].id = target_obj var.targets[0].data_path = target_prop driver.expression = expression return driver # Usage # Rotate gear_b based on gear_a rotation (2:1 ratio) add_driver(gear_b, "rotation_euler[2]", gear_a, "rotation_euler[2]", "-var * 2") ``` --- ## Error Handling Patterns ### Context Manager for Mode Changes ```python from contextlib import contextmanager @contextmanager def temp_mode(mode='OBJECT'): """Temporarily switch mode, restore after""" original_mode = bpy.context.mode if original_mode != mode: bpy.ops.object.mode_set(mode=mode) try: yield finally: if original_mode != mode: bpy.ops.object.mode_set(mode=original_mode) # Usage with temp_mode('EDIT'): bm = bmesh.from_edit_mesh(obj.data) # ... operations ... bmesh.update_edit_mesh(obj.data) # Automatically back to original mode ``` ### Context Manager for Selection ```python @contextmanager def temp_selection(objects, active=None): """Temporarily select objects, restore selection after""" # Save current prev_active = bpy.context.view_layer.objects.active prev_selected = bpy.context.selected_objects[:] # Set new bpy.ops.object.select_all(action='DESELECT') for obj in objects: obj.select_set(True) if active: bpy.context.view_layer.objects.active = active elif objects: bpy.context.view_layer.objects.active = objects[0] try: yield finally: # Restore bpy.ops.object.select_all(action='DESELECT') for obj in prev_selected: obj.select_set(True) bpy.context.view_layer.objects.active = prev_active # Usage with temp_selection([obj1, obj2, obj3], active=obj1): bpy.ops.object.join() ``` ### Undo Group ```python def with_undo_step(name, func): """Wrap operation in undo step""" bpy.ops.ed.undo_push(message=name) try: return func() except Exception: bpy.ops.ed.undo() raise # Usage with_undo_step("Create Building", lambda: create_building()) ``` --- ## Performance Patterns ### Batch Processing ```python def process_objects_batch(objects, process_func, batch_size=100): """Process objects in batches to avoid memory issues""" results = [] for i in range(0, len(objects), batch_size): batch = objects[i:i+batch_size] with bpy.context.temp_override(selected_objects=batch): batch_results = [process_func(obj) for obj in batch] results.extend(batch_results) # Optional: force garbage collection import gc gc.collect() return results ``` ### Defer Expensive Operations ```python def defer_to_next_frame(func): """Run function on next frame (useful for UI updates)""" def wrapper(): func() return None # Don't repeat bpy.app.timers.register(wrapper, first_interval=0.01) # Usage defer_to_next_frame(lambda: heavy_computation()) ``` --- ## Debugging Helpers ### Print Scene Summary ```python def print_scene_summary(): scene = bpy.context.scene print(f"Scene: {scene.name}") print(f"Objects: {len(scene.objects)}") print(f"Materials: {len(bpy.data.materials)}") print(f"Meshes: {len(bpy.data.meshes)}") print(f"Collections: {len(bpy.data.collections)}") for obj in scene.objects: print(f" {obj.name} ({obj.type}) - {len(obj.data.vertices) if obj.type=='MESH' else 'N/A'} verts") # Quick call print_scene_summary() ``` ### Find Issues ```python def find_mesh_issues(): """Check for common mesh problems""" issues = [] for obj in bpy.context.scene.objects: if obj.type != 'MESH': continue mesh = obj.data # Non-manifold edges bm = bmesh.new() bm.from_mesh(mesh) non_manifold = [e for e in bm.edges if not e.is_manifold] if non_manifold: issues.append(f"{obj.name}: {len(non_manifold)} non-manifold edges") # Zero-area faces zero_area = [f for f in bm.faces if f.calc_area() < 0.0001] if zero_area: issues.append(f"{obj.name}: {len(zero_area)} zero-area faces") # Duplicate vertices coords = [v.co for v in bm.verts] if len(coords) != len(set(str(c) for c in coords)): issues.append(f"{obj.name}: duplicate vertices") bm.free() return issues # Run for issue in find_mesh_issues(): print(f"ISSUE: {issue}") ``` --- ## Safe Mode Compliance (for blender-mcp) ### Allowed Imports Only ```python # ✅ ALLOWED import bpy import bmesh import math import mathutils import random import json import uuid import itertools import collections import functools import fractions import decimal import statistics import typing import dataclasses import enum import hashlib import base64 import datetime import time import re import string import textwrap import copy import pprint import numbers import fractions import pathlib import os.path import sys # ❌ BLOCKED (safe mode) import os # file I/O import subprocess # process execution import sys # system access import socket # network import requests # HTTP import urllib # URL handling import threading # threading import multiprocessing import asyncio import importlib # dynamic imports import pkgutil import inspect ``` ### Safe Code Template ```python # This template passes safe mode validation import bpy import bmesh import math import mathutils import random import json def main(): # Your Blender operations here # Only use: bpy.data, bpy.ops, bmesh, mathutils, math, random, json # Example: Create procedural building for i in range(5): bpy.ops.mesh.primitive_cube_add( location=(i * 3, 0, 0), scale=(1, 1, random.uniform(2, 5)) ) obj = bpy.context.active_object obj.name = f"Building_{i}" return {"created": 5} if __name__ == "__main__": result = main() print(json.dumps(result)) ``` --- ## Version Compatibility ### Version Checks ```python # Blender version if bpy.app.version >= (4, 0, 0): # New API bpy.ops.mesh.primitive_cube_add(size=2) else: # Old API bpy.ops.mesh.primitive_cube_add(radius=1) # Feature detection has_asset_browser = hasattr(bpy.ops, 'asset') has_geometry_nodes = 'GeometryNodeTree' in dir(bpy.types) # Property existence if hasattr(bpy.types.Object, "new_property"): obj.new_property = value ``` --- ## Testing Patterns ### Unit Test Template ```python import unittest import bpy import bmesh class TestMeshOperations(unittest.TestCase): def setUp(self): # Clean scene bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete() def test_create_cube(self): obj = create_object_with_material("TestCube", 'CUBE', (0,0,0), "TestMat", (1,0,0)) self.assertEqual(obj.name, "TestCube") self.assertEqual(obj.type, 'MESH') self.assertEqual(len(obj.data.materials), 1) def test_boolean_difference(self): cube = create_object_with_material("Cube", 'CUBE', (0,0,0), "Mat", (1,0,0)) sphere = create_object_with_material("Sphere", 'SPHERE', (0.5,0,0), "Mat", (0,1,0)) result = boolean_difference(cube, sphere) self.assertEqual(result.name, "Cube") # Sphere should be deleted self.assertNotIn("Sphere", bpy.data.objects) if __name__ == '__main__': unittest.main() ``` --- ## MCP Integration Best Practices ### Step-by-Step for Complex Tasks ```python # Instead of one massive operation, break into steps: # Step 1: Setup result = execute("Create collections for organization") # Step 2: Base geometry result = execute("Create floor plane 20x20m at origin") # Step 3: Walls result = execute("Create 4 walls around floor perimeter, 3m high") # Step 4: Openings result = execute("Boolean subtract door opening 1x2m in front wall") # Step 5: Roof result = execute("Create gabled roof over walls") # Step 6: Materials result = execute("Apply stone material to walls, wood to floor, tile to roof") # Step 7: Lighting result = execute("Add 3-point lighting: key, fill, rim") # Step 8: Camera result = execute("Add camera at (-10, -15, 5) looking at center, 35mm") # Step 9: Render settings result = execute("Set Cycles, 128 samples, 1920x1080, output to //renders/") ``` ### Validation After Each Step ```python def verify_step(step_name, verification_func): """Execute step and verify result""" result = execute(step_name) if not result.get("success"): return {"success": False, "error": f"{step_name} failed: {result.get('error')}"} # Verify try: verification_func() return {"success": True, "result": result} except AssertionError as e: return {"success": False, "error": f"{step_name} verification failed: {e}"} # Usage def verify_floor_exists(): obj = bpy.data.objects.get("Floor") assert obj and obj.type == 'MESH' assert abs(obj.dimensions.x - 20) < 0.01 assert abs(obj.dimensions.y - 20) < 0.01 result = verify_step("Create floor", verify_floor_exists) ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [BLENDER MATERIALS API REFERENCE](materials.md), [BLENDER MESH OPERATIONS API REFERENCE](mesh-operations.md), [BLENDER PYTHON API REFERENCE (4.x)](blender-api.md), [BLENDER-MCP - FULL DOCUMENTATION](../addons/blender-mcp-full.md), [BLENDER-MCP ADDON - DEEP DIVE](../addons/blender-mcp.md) - **По темам:** [INSTALLATION GUIDE - COMPLETE SETUP](../guides/installation.md), [MCP ARCHITECTURE - MODEL CONTEXT PROTOCOL FOR BLENDER](../mcp/architecture.md), [PROMPTING GUIDE - EFFECTIVE AI COMMANDS FOR BLENDER](../guides/prompting.md) - **Теги:** #blender, #reference --- --- ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- # BLENDER-MCP - FULL DOCUMENTATION Tags: #blender #mcp #reference # BLENDER-MCP - FULL DOCUMENTATION ## Overview **Repo:** https://github.com/ahujasid/blender-mcp **Website:** https://mcp-for-blender.com/ **Version:** 1.6+ | **Blender:** 3.0+ | **License:** MIT **PyPI:** `blender-mcp` | **Discord:** https://discord.gg/SNqPn4TcKQ --- ## Architecture ``` ┌─────────────────────────────────────────────────────────────────┐ │ MCP CLIENTS │ │ Claude Desktop │ Cursor │ VS Code │ Codex │ OpenCode │ ... │ └─────────────────────────────────────────────────────────────────┘ │ stdio (uvx blender-mcp) ▼ ┌─────────────────────────────────────────────────────────────────┐ │ MCP SERVER (src/blender_mcp/server.py) │ │ • FastMCP server with stdio transport │ │ • Tool definitions (28+ tools) │ │ • Telemetry, trajectory capture, safe mode │ │ • Persistent TCP connection to Blender addon (port 9876) │ └─────────────────────────────────────────────────────────────────┘ │ TCP :9876 (JSON protocol) ▼ ┌─────────────────────────────────────────────────────────────────┐ │ BLENDER ADDON (addon.py) │ │ • Socket server on :9876 │ │ • Command queue → main thread execution (bpy.app.timers) │ │ • Handlers for all 28+ command types │ │ • Poly Haven, Sketchfab, Poly Pizza, Hyper3D, Hunyuan3D │ │ • Manual edit capture (undo/redo/operators) │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ BLENDER 3.0+ │ │ • bpy.data / bpy.ops / bpy.context │ │ • GPU offscreen rendering for screenshots │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## Installation ### Prerequisites - Blender 3.0+ - Python 3.10+ - **uv** package manager (required) ### Install uv ```powershell # Windows powershell -c "irm https://astral.sh/uv/install.ps1 | iex" # Add to PATH (restart shell after) $localBin = "$env:USERPROFILE\.local\bin" $userPath = [Environment]::GetEnvironmentVariable("Path", "User") [Environment]::SetEnvironmentVariable("Path", "$userPath;$localBin", "User") ``` ### Quick Install ```bash # 1. Install Blender addon uvx blender-mcp install-addon # 2. Configure MCP client (see below) # 3. In Blender: N-panel → MCP for Blender → Start MCP Server ``` --- ## MCP Client Configuration ### OpenCode ```json { "mcp": { "blender-mcp": { "type": "local", "command": ["uvx", "blender-mcp"], "enabled": true, "environment": { "BLENDER_HOST": "localhost", "BLENDER_PORT": "9876" } } } } ``` ### Claude Desktop ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["blender-mcp"] } } } ``` Config: `claude_desktop_config.json` (Settings → Developer → Edit Config) ### Cursor ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["blender-mcp"] } } } ``` Settings → MCP → Add Server (Windows: use `"command": "cmd", "args": ["/c", "uvx", "blender-mcp"]`) ### Codex ```bash codex mcp add blender -- uvx blender-mcp ``` Or `~/.codex/config.toml`: ```toml [mcp_servers.blender] command = "uvx" args = ["blender-mcp"] env = { BLENDER_HOST = "localhost", BLENDER_PORT = "9876" } ``` --- ## Available Tools (28+) ### Core Scene Tools | Tool | Description | |------|-------------| | `get_scene_info` | Full scene hierarchy (limited to 10 objects) | | `get_object_info` | Detailed object data (transform, mesh, materials) | | `get_world_state_snapshot` | Compact snapshot for trajectory (4000 obj cap) | | `get_viewport_screenshot` | GPU offscreen render → Image | | `execute_blender_code` | **Arbitrary Python execution** (step-by-step!) | | `get_addon_info` | Version/capability handshake | ### Material Tools | Tool | Description | |------|-------------| | `set_texture` | Apply downloaded Poly Haven texture to object | ### Poly Haven (Free, no key) | Tool | Description | |------|-------------| | `get_polyhaven_categories` | List categories for hdris/textures/models | | `search_polyhaven_assets` | Search with type + category filters | | `download_polyhaven_asset` | Download + import (HDRI→world, Texture→material, Model→scene) | | `get_polyhaven_status` | Check if enabled in Blender | ### Poly Pizza (Free API key) | Tool | Description | |------|-------------| | `get_polypizza_status` | Check if enabled | | `search_polypizza_models` | Query, category (0-11), licence (0=CC-BY,1=CC0), animated, limit | | `download_polypizza_model` | Import GLB, normalize_size, target_size (meters) | **Categories:** 0=Food&Drink, 1=Clutter, 2=Weapons, 3=Transport, 4=Furniture&Decor, 5=Objects, 6=Nature, 7=Animals, 8=Buildings, 9=People&Characters, 10=Scenes&Levels, 11=Other **License:** 0=CC-BY (credit required), 1=CC0 (no credit) ### Sketchfab (API key) | Tool | Description | |------|-------------| | `get_sketchfab_status` | Check if enabled | | `search_sketchfab_models` | Query, categories, count, downloadable | | `get_sketchfab_model_preview` | Thumbnail Image for visual confirmation | | `download_sketchfab_model` | Import, normalize_size, **target_size REQUIRED** (meters) | ### Hyper3D Rodin (API key) | Tool | Description | |------|-------------| | `get_hyper3d_status` | Check if enabled | | `create_rodin_job` | Submit generation job | | `poll_rodin_job_status` | Check job status | | `import_generated_asset` | Import completed model | ### Hunyuan3D (SecretId/SecretKey) | Tool | Description | |------|-------------| | `get_hunyuan3d_status` | Check if enabled | | `create_hunyuan_job` | Submit generation job | | `poll_hunyuan_job_status` | Check job status | | `import_generated_asset_hunyuan` | Import completed model | --- ## Persistent API Credentials ### Blender Preferences (survives restart) Edit → Preferences → Add-ons → MCP for Blender: - Sketchfab API Key - Poly Pizza API Key - Hyper3D API Key - Hunyuan3D SecretId / SecretKey / API URL ### Environment Variables (headless/CI) ```bash BLENDERMCP_SKETCHFAB_API_KEY BLENDERMCP_POLYPIZZA_API_KEY BLENDERMCP_HYPER3D_API_KEY BLENDERMCP_HUNYUAN3D_SECRET_ID BLENDERMCP_HUNYUAN3D_SECRET_KEY BLENDERMCP_HUNYUAN3D_API_URL ``` --- ## Safe Mode Blocks risky operations in `execute_blender_code`: - File I/O, network, subprocess - Handlers/timers/drivers - Class/property registration - External .blend loading **Enable:** ```bash BLENDER_MCP_SAFE_MODE=1 uvx blender-mcp ``` Or MCP config: ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["blender-mcp"], "env": { "BLENDER_MCP_SAFE_MODE": "1" } } } } ``` **Allowed:** Blender operators (modeling, materials, rendering, save, import/export) --- ## Docker Deployment ```bash # Build docker build -t blender-mcp . # macOS/Windows (Docker Desktop) docker run -i --rm blender-mcp # Linux (host network) docker run -i --rm --network=host -e BLENDER_HOST=localhost blender-mcp ``` MCP config: ```json { "mcpServers": { "blender": { "command": "docker", "args": ["run", "-i", "--rm", "blender-mcp"] } } } ``` --- ## Python Version Pinning ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["--python", "3.11", "blender-mcp"], "env": { "UV_PYTHON_PREFERENCE": "only-managed" } } } } ``` --- ## Troubleshooting | Issue | Fix | |-------|-----| | `spawn uvx ENOENT` | Use full path: `which uvx` / `where uvx` | | Connection refused | Blender addon not running; click "Start MCP Server" | | Poly Pizza Cloudflare 403 | Download .glb manually → File → Import → glTF | | Timeout | Break complex requests into steps | | Version conflict | `uv cache clean blender-mcp && uvx --refresh blender-mcp` | | Headless Blender (`-b`) | Commands never execute; use GUI or `xvfb-run -a blender` | --- ## File Locations | File | Path | |------|------| | Addon (installed) | `%APPDATA%\Blender Foundation\Blender\4.2\scripts\addons\blender_mcp.py` | | Addon (source) | `addon.py` in repo | | MCP Server | `uvx blender-mcp` (from cache) | | Config (Blender prefs) | Edit → Preferences → Add-ons → MCP for Blender | | Logs | Blender System Console (Window → Toggle System Console) | --- ## Telemetry - **Default:** ON (anonymous usage data) - **Disable in Blender:** Preferences → Add-ons → MCP for Blender → uncheck telemetry - **Disable via env:** `DISABLE_TELEMETRY=true uvx blender-mcp` - **Data:** Tool name, success, duration, prompts, code, screenshots, scene data - **Terms:** See `TERMS_AND_CONDITIONS.md` in repo --- ## Blender Addon Source (addon.py) - Key Classes ### BlenderMCPServer ```python class BlenderMCPServer: def __init__(self, host='localhost', port=9876) def start() # Binds socket, starts thread, registers timer def stop() # Closes socket, unregisters timer, cleans up def _server_loop() # Accepts connections in daemon thread def _drain_command_queue() # Timer callback (0.05s) - executes on main thread def _handle_client() # Per-client handler thread def execute_command() # Dispatches to handlers ``` ### Key Handlers - `get_scene_info()` - Simplified scene data - `get_world_state_snapshot()` - Compact trajectory snapshot - `get_object_info(name)` - Full object details - `get_viewport_screenshot()` - GPU offscreen render - `execute_code(code)` - **Arbitrary Python** (with safe mode check) - `drain_human_activity()` - Undo/redo/operator capture - Poly Haven / Sketchfab / Poly Pizza / Hyper3D / Hunyuan3D handlers ### Manual Edit Capture - Records human operators (non-ignored) during MCP session - `undo_post` / `redo_post` / `depsgraph_update_post` handlers - Only active with telemetry consent + server running - Drained via `drain_human_activity()` tool --- ## Protocol - **Transport:** TCP socket (JSON over raw socket) - **Port:** 9876 (configurable via `BLENDER_PORT`) - **Command:** `{"type": "cmd_name", "params": {...}}` - **Response:** `{"status": "success|error", "result": {...}}` or `{"status": "error", "message": "..."}` - **Locking:** Single-threaded command queue on Blender main thread --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [BLENDER-MCP ADDON - DEEP DIVE](blender-mcp.md), [INSTALLATION GUIDE - COMPLETE SETUP](../guides/installation.md), [MCP ARCHITECTURE - MODEL CONTEXT PROTOCOL FOR BLENDER](../mcp/architecture.md), [TEXT-TO-3D WORKFLOWS](../workflows/text-to-3d.md), [TROUBLESHOOTING GUIDE](../guides/troubleshooting.md) - **По темам:** [BLENDER MATERIALS API REFERENCE](../api/materials.md), [BLENDER MESH OPERATIONS API REFERENCE](../api/mesh-operations.md), [BLENDER PYTHON API REFERENCE (4.x)](../api/blender-api.md) - **Теги:** #blender, #mcp, #reference --- ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- # BLENDER-MCP ADDON - DEEP DIVE Tags: #addon #blender #mcp #reference # BLENDER-MCP ADDON - DEEP DIVE ## Overview **Repo:** https://github.com/ahujasid/blender-mcp **Version:** Latest | **Blender:** 3.0+ | **License:** MIT **Stars:** 28.3k | **PyPI:** `blender-mcp` blender-mcp is the **reference MCP server implementation** for Blender. It runs as a separate Python process (via `uvx`) and connects to a lightweight Blender addon via TCP socket (port 9876). --- ## Architecture ``` ┌────────────────────────────────────────────────────────────┐ │ MCP CLIENT (Claude/Cursor/Codex) │ │ uvx blender-mcp │ ├────────────────────────────────────────────────────────────┤ │ src/blender_mcp/ │ │ ├── server.py # FastMCP server (stdio transport) │ │ ├── socket_server.py# TCP server on :9876 │ │ └── tools/ # Tool definitions │ └────────────────────────────────────────────────────────────┘ │ TCP :9876 ▼ ┌────────────────────────────────────────────────────────────┐ │ BLENDER ADDON (addon.py) │ │ • Socket client connecting to :9876 │ │ • BlenderExecutor: Thread-safe command queue │ │ • Command handlers for each tool │ └────────────────────────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────┐ │ BLENDER │ └────────────────────────────────────────────────────────────┘ ``` --- ## Key Differences from GenesisCore | Aspect | GenesisCore | blender-mcp | |--------|-------------|-------------| | **MCP Client** | Built-in (multiple providers) | External (any MCP client) | | **MCP Server** | Built-in (FastMCP on :45677) | External process (uvx) | | **Transport** | SSE (internal) | stdio → TCP :9876 | | **Asset Sources** | Poly Haven only | Poly Haven + Poly Pizza + Sketchfab + Hyper3D + Hunyuan3D | | **Installation** | ZIP/Git in Blender | `uvx blender-mcp install-addon` | | **Configuration** | Blender N-panel | MCP client config + Blender addon prefs | --- ## Installation ### Prerequisites ```powershell # Install uv (Windows) powershell -c "irm https://astral.sh/uv/install.ps1 | iex" # Add to PATH (restart shell after) $localBin = "$env:USERPROFILE\.local\bin" $userPath = [Environment]::GetEnvironmentVariable("Path", "User") [Environment]::SetEnvironmentVariable("Path", "$userPath;$localBin", "User") ``` ### Quick Install ```powershell # 1. Install Blender addon uvx blender-mcp install-addon # 2. Configure MCP client (see below) # 3. In Blender: N-panel → MCP for Blender → Start MCP Server ``` ### MCP Client Configuration #### OpenCode ```json { "mcp": { "blender-mcp": { "type": "local", "command": ["uvx", "blender-mcp"], "enabled": true, "environment": { "BLENDER_HOST": "localhost", "BLENDER_PORT": "9876" } } } } ``` #### Claude Desktop ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["blender-mcp"] } } } ``` #### Cursor ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["blender-mcp"] } } } ``` --- ## Available Tools (v2026) ### Core Scene Tools | Tool | Description | |------|-------------| | `get_scene_info` | Full scene hierarchy | | `get_object_info` | Detailed object data | | `create_object` | Primitives: CUBE, SPHERE, CYLINDER, PLANE, CONE, TORUS, EMPTY, CAMERA, LIGHT | | `modify_object` | Transform + visibility | | `delete_object` | Remove by name | | `execute_blender_code` | **Arbitrary Python execution** | ### Material Tools | Tool | Description | |------|-------------| | `set_material` | Create/apply Principled BSDF with color | ### Asset Tools (Require API Keys) | Source | Tools | Setup | |--------|-------|-------| | **Poly Haven** | `search_polyhaven_assets`, `download_polyhaven_asset` | Free, no key | | **Poly Pizza** | `search_polypizza_models`, `download_polypizza_model` | Free API key | | **Sketchfab** | `search_sketchfab_models`, `download_sketchfab_model` | API key | | **Hyper3D Rodin** | `generate_rodin_model`, `get_rodin_generation_status` | API key | | **Hunyuan3D** | `generate_hunyuan3d_model`, `get_hunyuan3d_status` | SecretId/SecretKey | --- ## Poly Pizza Integration (Unique to blender-mcp) ```python # 1. Get free API key: https://poly.pizza/settings/api # 2. In Blender N-panel → MCP for Blender → tick "Use Poly Pizza" → paste key # 3. Or set env: BLENDERMCP_POLYPIZZA_API_KEY # Usage via AI: # "Search Poly Pizza for a low-poly chair under CC0 license and import at 1 meter tall" ``` **Licensing:** ~69% CC-BY (requires attribution), filter with `licence="CC0"` for no attribution. --- ## AI 3D Generation (Unique to blender-mcp) ### Hyper3D Rodin ```python # Requires API key from https://hyper3d.ai/ # Env: BLENDERMCP_HYPER3D_API_KEY # AI: "Generate a 3D model of a garden gnome through Hyper3D" ``` ### Hunyuan3D (Tencent) ```python # Requires SecretId/SecretKey from https://3d.hunyuan.tencent.com/ # Env: BLENDERMCP_HUNYUAN3D_SECRET_ID / SECRET_KEY / API_URL # AI: "Create a 3D model of a futuristic car using Hunyuan3D" ``` --- ## Safe Mode ```bash # Enable safe mode (blocks file I/O, network, subprocess, persistent code) BLENDER_MCP_SAFE_MODE=1 uvx blender-mcp ``` Or in MCP config: ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["blender-mcp"], "env": { "BLENDER_MCP_SAFE_MODE": "1" } } } } ``` --- ## Docker Deployment ```bash # Build docker build -t blender-mcp . # Run (macOS/Windows with Docker Desktop) docker run -i --rm blender-mcp # Run (Linux - host network) docker run -i --rm --network=host -e BLENDER_HOST=localhost blender-mcp ``` MCP config: ```json { "mcpServers": { "blender": { "command": "docker", "args": ["run", "-i", "--rm", "blender-mcp"] } } } ``` --- ## Python Version Pinning ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["--python", "3.11", "blender-mcp"], "env": { "UV_PYTHON_PREFERENCE": "only-managed" } } } } ``` --- ## Troubleshooting | Issue | Fix | |-------|-----| | `spawn uvx ENOENT` | Use full path to uvx: `which uvx` / `where uvx` | | Connection refused | Blender addon not running; click "Start MCP Server" | | Poly Pizza Cloudflare | Download .glb manually → File → Import → glTF | | Timeout | Break complex requests into steps | | Version conflict | `uv cache clean blender-mcp && uvx --refresh blender-mcp` | --- ## File Locations | File | Path | |------|------| | Addon (installed) | `%APPDATA%\Blender Foundation\Blender\4.2\scripts\addons\blender_mcp.py` | | Addon (source) | `addon.py` in repo | | MCP Server | `uvx blender-mcp` (runs from cache) | | Config (Blender prefs) | Edit → Preferences → Add-ons → MCP for Blender | | Logs | Blender System Console | --- ## Related Wiki Pages - [MCP Architecture](../mcp/architecture.md) - [GenesisCore Comparison](genesiscore.md) - [MCP Client Setup](../mcp/clients.md) - [Prompting Guide](../guides/prompting.md) --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [INSTALLATION GUIDE - COMPLETE SETUP](../guides/installation.md), [TROUBLESHOOTING GUIDE](../guides/troubleshooting.md), [BLENDER-MCP - FULL DOCUMENTATION](blender-mcp-full.md), [GENESISCORE ADDON - DEEP DIVE](genesiscore.md), [MCP ARCHITECTURE - MODEL CONTEXT PROTOCOL FOR BLENDER](../mcp/architecture.md) - **По темам:** [TEXT-TO-3D WORKFLOWS](../workflows/text-to-3d.md), [BLENDER MATERIALS API REFERENCE](../api/materials.md), [BLENDER MESH OPERATIONS API REFERENCE](../api/mesh-operations.md) - **Теги:** #addon, #blender, #mcp, #reference --- ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- # DREAM TEXTURES - FULL WIKI DOCUMENTATION Tags: #reference #stable-diffusion # DREAM TEXTURES - FULL WIKI DOCUMENTATION ## Overview **Repo:** https://github.com/carson-katri/dream-textures **Wiki:** https://github.com/carson-katri/dream-textures/wiki **Version:** Latest | **Blender:** 3.3+ | **License:** GPL-3.0 --- ## Setup Guide (from Wiki) ### Installation 1. Download from [latest release](https://github.com/carson-katri/dream-textures/releases/latest) 2. Windows: Extract 7-Zip archive → get `.zip` → Install in Blender Preferences → Add-ons 3. Enable **Dream Textures** in add-ons 4. **DO NOT** manually install dependencies - use prebuilt version only ### Download Models In add-on preferences, search and download: - `stabilityai/stable-diffusion-2-1-base` - Primary 512x512 model - `stabilityai/stable-diffusion-2-inpainting` - For inpainting/outpainting - `stabilityai/stable-diffusion-2-depth` - For texture projection, depth-to-image - `stabilityai/stable-diffusion-x4-upscaler` - For 4x AI upscaling - `stabilityai/stable-diffusion-2-1` - Fine-tuned for 768x768 ### Private Models - Generate Hugging Face token at https://huggingface.co/settings/tokens - Paste in "Token" field in preferences ### Import Checkpoints (.ckpt) - Click "Import Checkpoint File" in preferences - Select file → specify Model Config: - `v1` - Original CompVis models (SD 1.x) - `v2 (512, epsilon)` - 512x512 models with prediction_type="epsilon" - `v2 (768, v_prediction)` - 768x768 models with prediction_type="v_prediction" ### DreamStudio (Cloud) - Enter API key from https://beta.dreamstudio.ai/membership?tab=apiKeys - Processes in cloud if local hardware insufficient --- ## Image Generation (from Wiki) ### Access - Open Image Editor or Shader Editor - Press `N` for sidebar → **Dream** panel ### Options | Option | Description | |--------|-------------| | **Pipeline** | Stable Diffusion (local) or DreamStudio (cloud) | | **Model** | Choose installed model (some features require specific models) | | **Prompt** | Text prompt. Presets available (e.g., "Texture" adds "texture" suffix) | | **Seamless** | Circular convolution for perfect tiling. Can specify axes | | **Negative** | What to avoid (e.g., "building" to remove buildings) | | **Size** | Target dimensions (multiples of 64). Best at 512x512 | | **Source Image** | File or Open Image for img2img | #### Source Image Modes 1. **Color** - Mix image with noise 2. **Color + Generated Depth** - MiDaS depth + color (requires depth model) 3. **Color + Depth Map** - Custom depth map image 3. **Depth** - Treat image as depth map only #### Advanced Parameters | Parameter | Description | |-----------|-------------| | **Random Seed** | Auto-generate or manual seed | | **Steps** | Sampler steps (more = better convergence) | | **CFG Scale** | Prompt adherence strength | | **Scheduler** | Different convergence speeds | | **Step Preview** | "Fast" (latent preview) or "Accurate" (VAE decode each step) | | **Speed Optimizations** | Half Precision recommended | | **Memory Optimizations** | Attention Slicing (Auto) recommended | --- ## Texture Projection (from Wiki) ### Workflow 1. Select objects in 3D Viewport 2. Enter Edit Mode → select faces to target 3. Local View (`/`) to limit depth map to selection 4. **Tip:** Subdivide large faces if projection warps ### Settings - Choose **depth model** (e.g., `stable-diffusion-2-depth`) - Enter prompt - **Data source:** "Depth" (default) or "Depth and Color" (uses viewport shading) - Adjust image size to match viewport aspect ratio ### Execution - Click **Project Dream Texture** - New material created with seed name - UVs updated for projection angle - Live preview in Viewport Shading mode --- ## Inpaint and Outpaint (from Wiki) ### Inpainting - Mask area in Image Editor - Requires `stable-diffusion-2-inpainting` model - Prompt: "seamless continuation of [texture]" - **Making Textures Seamless:** Inpaint edges to blend ### Outpainting - Extend image beyond boundaries - Choose origin (center, corners, edges) - Generates new content matching style --- ## Render Engine / Node System (from Wiki) ### Nodes Added - **Dream Texture Node** - Generate textures in shader graph - **ControlNet Node** - Depth/Canny/Normal guidance - **Upscale Node** - AI upscaling in pipeline ### Render Pass Integration - Use Cycles render as ControlNet input - Re-style via img2img - Animation: frame-by-frame re-styling #### Controls | Parameter | Effect | |-----------|--------| | **Depth** | Use depth pass for structure | | **Noise Strength** | Deviation from render (0-1) | | **Seed** | Reproducibility | --- ## AI Upscaling (from Wiki) - 4x upscale via ESRGAN/RealESRGAN - **Tile Size** option for large images --- ## History (from Wiki) - Recall, export, import generations - **Export:** Save prompt + params + seed - **Import:** Restore previous generation settings --- ## Development Setup (from Wiki) ```bash git clone https://github.com/carson-katri/dream-textures cd dream-textures # Install to Blender # Install dependencies: pip install -r requirements.txt ``` --- ## Hardware Requirements | Component | Minimum | Recommended | |-----------|---------|-------------| | **VRAM** | 4 GB | 8-12 GB | | **GPU** | CUDA 11.7+ / Apple Silicon | RTX 3080+ / M2 Max+ | | **RAM** | 8 GB | 16-32 GB | | **Disk** | 10 GB | 20 GB+ | --- ## Troubleshooting | Issue | Fix | |-------|-----| | OOM / CUDA OOM | Reduce resolution; fp16; close apps | | Model download fails | Check internet; HF_TOKEN for gated models | | macOS quarantine | `xattr -r -d com.apple.quarantine /.python_dependencies` | | Slow generation | Lower steps (20-30); smaller resolution | | Seams visible | Enable Seamless; inpaint edges | --- ## Integration with MCP Dream Textures is **standalone** - no MCP integration. Use via: 1. Manual N-panel 2. Python API via `execute_blender_code`: ```python bpy.ops.dream_texture.generate( prompt="brick wall, seamless", seamless=True, width=1024, height=1024 ) ``` 3. Combined workflow: GenesisCore for geometry → Dream Textures for materials --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [DREAM TEXTURES ADDON - DEEP DIVE](dream-textures.md), [INSTALLATION GUIDE - COMPLETE SETUP](../guides/installation.md), [ANIMATION WORKFLOWS](../workflows/animation.md), [BLENDER MATERIALS API REFERENCE](../api/materials.md), [BLENDER MESH OPERATIONS API REFERENCE](../api/mesh-operations.md) - **По темам:** [BLENDER PYTHON API REFERENCE (4.x)](../api/blender-api.md), [BLENDER SCRIPTING PATTERNS - SAFE & EFFECTIVE](../api/scripting-patterns.md), [BLENDER-MCP - FULL DOCUMENTATION](blender-mcp-full.md) - **Теги:** #reference, #stable-diffusion --- ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- # DREAM TEXTURES ADDON - DEEP DIVE Tags: #addon #reference #stable-diffusion # DREAM TEXTURES ADDON - DEEP DIVE ## Overview **Repo:** https://github.com/carson-katri/dream-textures **Version:** Latest | **Blender:** 3.3+ | **License:** GPL-3.0 **Stars:** 8.2k | **Discord:** https://discord.gg/EmDJ8CaWZ7 Dream Textures brings **Stable Diffusion natively into Blender** - runs locally via diffusers/PyTorch, no external services needed. Best for texture generation, inpainting, outpainting, and texture projection. --- ## Architecture ``` ┌────────────────────────────────────────────────────────────┐ │ DREAM TEXTURES ADDON │ ├────────────────────────────────────────────────────────────┤ │ .python_dependencies/ # Bundled PyTorch, diffusers │ │ builtin_presets/ # Prompt presets │ │ community_backends/ # Alternative backends │ │ engine/ # Core generation pipeline │ │ generator_process/ # Subprocess for model inference │ │ operators/ # Blender operators │ │ ui/ # N-panel + node editor │ │ sd_configs/ # Model configurations │ │ realtime_viewport.py # Live preview │ │ render_pass.py # Cycles render pass integration │ │ prompt_engineering.py # Prompt enhancement │ │ image_utils.py # Image processing │ └────────────────────────────────────────────────────────────┘ ``` --- ## Key Features ### 1. Text-to-Texture - Generate PBR texture sets (Base Color, Roughness, Normal, Metallic, Height) - Seamless tiling option - Preset styles: photorealistic, stylized, fantasy, sci-fi ### 2. Texture Projection (Depth-to-Image) - Project textures onto 3D models using depth maps - "Project Dream Texture" operator - Works on entire scenes ### 3. Inpainting / Outpainting - Fix texture seams - Extend textures beyond boundaries - Convert non-seamless → seamless ### 4. Render Pass Integration - Use Cycles render as ControlNet input - Re-style renders via img2img - Animation re-styling frame-by-frame ### 5. AI Upscaling - 4x upscale via ESRGAN/RealESRGAN - Integrated in node system ### 6. History System - Recall, export, import generations - Prompt + parameters saved --- ## Installation ### Download Release (Recommended) 1. Go to: https://github.com/carson-katri/dream-textures/releases/latest 2. Download `dream_textures-.zip` 3. Blender → Preferences → Add-ons → **Install from ZIP** 4. Enable **Dream Textures** ### First Run - Addon downloads PyTorch + diffusers (~2-4 GB) to `.python_dependencies/` - Requires **4GB+ VRAM** (CUDA or Apple Silicon) - On macOS: may need quarantine removal: ```bash xattr -r -d com.apple.quarantine ~/Library/Application\ Support/Blender/3.3/scripts/addons/dream_textures/.python_dependencies ``` ### Cloud Fallback (DreamStudio) - If hardware unsupported: configure DreamStudio API key in preferences - Processes in cloud instead of locally --- ## Hardware Requirements | Component | Minimum | Recommended | |-----------|---------|-------------| | **VRAM** | 4 GB | 8-12 GB | | **GPU** | CUDA 11.7+ / Apple Silicon | RTX 3080+ / M2 Max+ | | **RAM** | 8 GB | 16-32 GB | | **Disk** | 10 GB free | 20 GB+ | --- ## Usage Workflows ### Basic Texture Generation ``` 1. Select object → N-panel → Dream Textures 2. Choose "Generate Texture" tab 3. Enter prompt: "weathered copper metal, patina, seamless" 4. Enable "Seamless" for tiling 5. Click Generate 6. Apply to material slots ``` ### Texture Projection ``` 1. Set up camera/view for projection 2. Select target object(s) 3. Choose "Project Dream Texture" 4. Enter prompt: "ancient stone temple walls, moss, cracks" 5. Adjust depth influence 6. Project → Creates UV-mapped textures ``` ### Inpainting (Fix Seams) ``` 1. Select texture in Image Editor 2. Mask problem area 3. Dream Textures → Inpaint 4. Prompt: "seamless continuation of stone texture" 5. Blend result ``` ### Render Re-style ``` 1. Set up Cycles scene 2. Render pass → Dream Textures node 3. Prompt: "cyberpunk neon aesthetic, volumetric lighting" 4. Re-render → AI transforms ``` --- ## Node System (Advanced) Dream Textures adds custom nodes to Shader Editor: - **Dream Texture Node** - Generate textures in node graph - **ControlNet Node** - Depth/Canny/Normal guidance - **Upscale Node** - AI upscaling in pipeline Enables complex procedural workflows. --- ## Model Backends | Backend | Models | Notes | |---------|--------|-------| | **diffusers** (default) | SD 1.5, SDXL, SD 2.1, ControlNet | Local, free | | **DreamStudio** | SDXL, SD 3 | Cloud, paid | | **Community** | Flux, SD3, custom | Experimental | --- ## Configuration ### Preferences (Edit → Preferences → Add-ons → Dream Textures) - **Device:** CUDA / MPS / CPU - **Precision:** fp16 (default) / fp32 / bf16 - **Model Cache:** Custom path for model weights - **DreamStudio API Key:** For cloud processing - **Telemetry:** Opt-in/out ### Presets Built-in presets in `builtin_presets/`: - PBR Materials (metal, fabric, wood, stone) - Environments (sky, ground, foliage) - Styles (anime, photorealistic, pixel art) --- ## Troubleshooting | Issue | Fix | |-------|-----| | **Out of VRAM** | Reduce resolution; use fp16; close other apps | | **Model download fails** | Check internet; set HF_TOKEN for gated models | | **macOS quarantine** | Run xattr command above | | **Slow generation** | Use lower steps (20-30); smaller resolution | | **Seams visible** | Enable "Seamless"; use inpainting to fix | --- ## Integration with GenesisCore / blender-mcp Dream Textures runs **independently** - no MCP integration. Use via: 1. **Manual** - N-panel in Blender 2. **Python API** - Call operators from `execute_blender_code`: ```python bpy.ops.dream_texture.generate( prompt="brick wall, seamless", seamless=True, width=1024, height=1024 ) ``` 3. **Combine workflows:** - GenesisCore for geometry/scene - Dream Textures for materials --- ## File Locations | File | Path | |------|------| | Addon | `%APPDATA%\Blender Foundation\Blender\4.2\scripts\addons\dream_textures\` | | Models | `%APPDATA%\Blender Foundation\Blender\4.2\scripts\addons\dream_textures\.python_dependencies\` | | Presets | `builtin_presets/` in addon | | Output | Blender temp / project folder | --- ## Related Wiki Pages - [Texture Generation Workflow](../workflows/texture-generation.md) - [GenesisCore + Dream Textures Combo](../workflows/scene-building.md) - [Blender Material API](../api/materials.md) --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [INSTALLATION GUIDE - COMPLETE SETUP](../guides/installation.md), [BLENDER-MCP ADDON - DEEP DIVE](blender-mcp.md), [DREAM TEXTURES - FULL WIKI DOCUMENTATION](dream-textures-full.md), [GENESISCORE ADDON - DEEP DIVE](genesiscore.md), [TROUBLESHOOTING GUIDE](../guides/troubleshooting.md) - **По темам:** [ANIMATION WORKFLOWS](../workflows/animation.md), [BLENDER MATERIALS API REFERENCE](../api/materials.md), [BLENDER MESH OPERATIONS API REFERENCE](../api/mesh-operations.md) - **Теги:** #addon, #reference, #stable-diffusion --- ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- # GENESISCORE ADDON - DEEP DIVE Tags: #addon #mcp #reference # GENESISCORE ADDON - DEEP DIVE ## Overview **Repo:** https://github.com/AIGODLIKE/GenesisCore **Version:** 0.0.1 | **Blender:** 4.0+ | **License:** Open Source **Stars:** 134 | **Language:** Python GenesisCore is a **full MCP Client + Server** built directly into Blender. Unlike blender-mcp (server only), GenesisCore includes the LLM client, so you chat with AI *inside Blender* via N-panel. --- ## Architecture ``` ┌────────────────────────────────────────────────────────────┐ │ GENESISCORE ADDON │ ├────────────────────────────────────────────────────────────┤ │ src/ │ │ ├── client/ # MCP Clients (LLM providers) │ │ │ ├── base.py # MCPClientBase - core logic │ │ │ ├── openai.py # OpenAI compatible │ │ │ ├── openrouter.py # OpenRouter (extends OpenAI) │ │ │ ├── deepseek.py # DeepSeek API │ │ │ ├── claude.py # Anthropic Claude │ │ │ ├── ollama.py # Local Ollama │ │ │ └── siliconflow.py# SiliconFlow │ │ ├── server/ # MCP Server (FastMCP) │ │ │ ├── server.py # BlenderMCPServer on :45677 │ │ │ └── tools/ # Tool modules │ │ │ ├── common_tools.py # Scene info, code exec │ │ │ ├── object_tools.py # Create/modify/delete │ │ │ ├── material_tools.py # PBR materials │ │ │ ├── polyhaven_tools.py # Poly Haven assets │ │ │ └── asset_tools.py # Local assets │ │ ├── preference.py # Settings UI + config persistence │ │ ├── ui.py # N-panel interface │ │ ├── executor.py # Thread-safe bpy execution │ │ └── logger.py # Logging │ └────────────────────────────────────────────────────────────┘ ``` --- ## Key Features ### 1. Built-in MCP Client (No External Host Needed) ```python # In base.py - connects to local MCP server on port 45677 async def connect_to_server(self): headers = {"Content-Type": "application/json", "Accept": "application/json"} stdio_transport = await self.exit_stack.enter_async_context( sse_client(url="http://localhost:45677/sse", headers=headers) ) ``` ### 2. Multi-Provider Support | Provider | Base URL | Default Model | |----------|----------|---------------| | **OpenRouter** | `https://openrouter.ai/api` | `anthropic/claude-3.5-haiku` | | **DeepSeek** | `https://api.deepseek.com` | `deepseek-chat` | | **OpenAI** | `https://api.openai.com/v1` | `gpt-4o` | | **Anthropic** | `https://api.anthropic.com` | `claude-3-5-sonnet` | | **Ollama** | `http://localhost:11434/v1` | `llama3.2` | | **SiliconFlow** | `https://api.siliconflow.cn/v1` | `Qwen/Qwen2.5-Coder-32B` | ### 3. Tool Modules (Extensible) Each module registers tools with the MCP server: ```python # common_tools.py - Most powerful tool def execute_blender_code(code: str) -> dict: """Execute arbitrary Blender Python API code""" namespace = {"bpy": bpy} exec(code, namespace) return {"executed": True} ``` **Available Tools:** | Module | Tools | |--------|-------| | `common_tools` | `get_scene_info`, `get_simple_info`, `execute_blender_code` | | `object_tools` | `create_object`, `modify_object`, `delete_object`, `get_object_info` | | `material_tools` | `set_material` (Principled BSDF) | | `polyhaven_tools` | `search_models`, `fetch_model_info`, `use_model_asset`, `search_hdris`, `use_hdri` | ### 4. Poly Haven Integration - Caches to `%TEMP%\polyhaven_{models|hdris}` - Auto-loads cached assets - Supports 1k/2k/4k/8k/16k resolutions ### 5. External MCP Host Connection ```json { "mcpServers": { "BlenderGenesis": { "url": "http://localhost:45677" } } } ``` Allows Claude Desktop / Cursor / Codex to connect to GenesisCore's server. --- ## Installation ### Method 1: ZIP Install (Recommended) 1. Download: https://github.com/AIGODLIKE/GenesisCore/archive/refs/heads/main.zip 2. Blender → Edit → Preferences → Add-ons → **Install from ZIP** 3. Select the ZIP file 4. Enable **GenesisCore** (search "GenesisCore" or "AI") ### Method 2: Git Clone (Auto-updates) ```powershell cd %APPDATA%\Blender Foundation\Blender\4.2\scripts\addons git clone https://github.com/AIGODLIKE/GenesisCore.git ``` Then enable in Preferences. --- ## Configuration ### OpenRouter Setup (Recommended) 1. Get API key: https://openrouter.ai/keys 2. In Blender N-panel (press `N`) → **GenesisCore** tab 3. **Provider:** OpenRouter 4. **API Key:** Paste your key 5. **Fetch Models** → Select: `deepseek/deepseek-chat` or `qwen/qwen-2.5-coder-32b` 6. **Tool Modules:** Enable all (hold Shift to multi-select) 7. **Save Config** ### Model Recommendations (2026) | Task | Model | Cost | |------|-------|------| | **Complex reasoning** | `anthropic/claude-3.5-sonnet` | $$$ | | **Coding/Blender API** | `deepseek/deepseek-chat` | $ | | **Fast/cheap** | `qwen/qwen-2.5-coder-32b` | $ | | **Local (free)** | `ollama:codellama:13b` | Free | --- ## Usage Workflow ``` 1. Open N-panel (N) → GenesisCore 2. Select Provider + Model 3. Enable Tool Modules (Common, Object, Material, Polyhaven) 4. Type prompt in chat box 5. Press Enter / Click Execute 6. AI plans → calls tools → executes in Blender 7. Result appears in chat + viewport updates ``` --- ## Example Prompts ### Basic Geometry ``` "Create a low-poly castle with 4 towers, a central keep, and a moat. Use simple primitives." ``` ### Material + Lighting ``` "Create a studio lighting setup: 3-point lighting with key, fill, and rim lights. Make a chrome sphere in center." ``` ### Poly Haven Assets ``` "Search Poly Haven for a Victorian chair and an HDRI for indoor lighting. Import both and set up the scene." ``` ### Complex Scene ``` "Build a cyberpunk alley scene: wet pavement (use HDRI), neon signs (emission materials), steam particles, and a hovering vehicle. Use Poly Haven assets where possible." ``` ### Code Execution (Direct) ``` "Execute this code to create a procedural city grid with random building heights" ``` → AI calls `execute_blender_code` with full bpy script --- ## Advanced: Custom Tool Development ### 1. Create Tool Module ```python # src/tools/my_tools.py from .common import ToolsPackageBase class MyTools(ToolsPackageBase): def my_custom_tool(param: str) -> dict: """Description for LLM""" # bpy operations here return {"result": "success"} ``` ### 2. Register in `__init__.py` ```python # src/tools/__init__.py from .my_tools import MyTools # Order matters for UI display ``` ### 3. Custom LLM Client ```python # src/client/custom.py from .base import MCPClientBase class MCPClientCustom(MCPClientBase): @classmethod def info(cls): return {"name": "Custom", "description": "My Provider", "version": "0.1"} @classmethod def default_config(cls): return {"base_url": "https://api.custom.com", "model": "default"} ``` --- ## Troubleshooting | Issue | Fix | |-------|-----| | **"Connecting..." hangs** | Check port 45677 not blocked; restart Blender | | **Model list empty** | Verify API key; check network/proxy | | **Tools not showing** | Enable modules in N-panel (Shift+click for multiple) | | **Poly Haven fails** | Check internet; cache in `%TEMP%\polyhaven_*` | | **Code execution errors** | Open System Console (Window → Toggle System Console) | --- ## File Locations | File | Path | |------|------| | Addon root | `%APPDATA%\Blender Foundation\Blender\4.2\scripts\addons\GenesisCore\` | | Config | `%APPDATA%\Blender Foundation\Blender\4.2\config\genesiscore_config.json` | | Poly Haven cache | `%TEMP%\polyhaven_models\` / `polyhaven_hdris\` | | Logs | Blender System Console | --- ## Related Wiki Pages - [MCP Architecture](../mcp/architecture.md) - [Blender API Patterns](../api/scripting-patterns.md) - [Prompting Guide](../guides/prompting.md) - [blender-mcp Comparison](blender-mcp.md) --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [BLENDER-MCP ADDON - DEEP DIVE](blender-mcp.md), [INSTALLATION GUIDE - COMPLETE SETUP](../guides/installation.md), [TROUBLESHOOTING GUIDE](../guides/troubleshooting.md), [BLENDER-MCP - FULL DOCUMENTATION](blender-mcp-full.md), [DREAM TEXTURES ADDON - DEEP DIVE](dream-textures.md) - **По темам:** [MCP ARCHITECTURE - MODEL CONTEXT PROTOCOL FOR BLENDER](../mcp/architecture.md), [MCP CLIENT CONFIGURATIONS](../mcp/clients.md), [MCP SERVER API REFERENCE](../mcp/server-api.md) - **Теги:** #addon, #mcp, #reference --- ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- # INSTALLATION GUIDE - COMPLETE SETUP Tags: #addon #blender #mcp #reference #stable-diffusion # INSTALLATION GUIDE - COMPLETE SETUP ## Prerequisites - Windows 10/11 (this guide) - Blender 4.2+ - Python 3.10+ (bundled with Blender) - Internet connection for model downloads --- ## Step 1: Install uv (Required) ```powershell # Install uv powershell -c "irm https://astral.sh/uv/install.ps1 | iex" # Add to PATH (REQUIRED - restart PowerShell after) $localBin = "$env:USERPROFILE\.local\bin" $userPath = [Environment]::GetEnvironmentVariable("Path", "User") [Environment]::SetEnvironmentVariable("Path", "$userPath;$localBin", "User") # Verify uv --version uvx --version ``` --- ## Step 2: Install Blender ### Option A: MSI Installer (Recommended) ```powershell # Download Blender installer for your version # msiexec /i "путь\к\blender-<версия>-windows-x64.msi" /quiet /norestart ``` ### Option B: Portable ZIP ```powershell # Extract to a folder of your choice # Run: <путь-к-папке>\blender.exe ``` ### Verify ```powershell # Find Blender path where blender # Or check typical install location ``` > [!NOTE] > Версию Blender (например 4.2) выбирайте под свои задачи. Конкретный путь к инсталлятору зависит от того, куда вы его скачали. --- ## Step 3: Install blender-mcp (MCP Server + Addon) ```powershell # Install addon to Blender uvx blender-mcp install-addon # Verify addon path uvx blender-mcp addon-paths ``` --- ## Step 4: Install GenesisCore Addon ```powershell # Download GenesisCore zip from GitHub # In Blender: # 1. Edit → Preferences → Add-ons → Install from ZIP # 2. Select downloaded zip file # 3. Enable: Search "GenesisCore" → Check box ``` --- ## Step 5: Install Dream Textures Addon ```powershell # Download latest release: # https://github.com/carson-katri/dream-textures/releases/latest # Download: dream_textures-.zip # In Blender: # 1. Edit → Preferences → Add-ons → Install from ZIP # 2. Select downloaded ZIP # 3. Enable: Dream Textures # First run: Downloads PyTorch + diffusers (~2-4 GB) # Preferences → Dream Textures → Download model: stabilityai/stable-diffusion-2-1-base ``` --- ## Step 6: Configure OpenCode MCP Create/Open: `C:\Users\\.config\opencode\opencode.json` ```json { "mcp": { "blender-mcp": { "type": "local", "command": ["uvx", "blender-mcp"], "enabled": true, "environment": { "BLENDER_HOST": "localhost", "BLENDER_PORT": "9876" } } } } ``` --- ## Step 7: Configure GenesisCore (OpenRouter) 1. Get API key: https://openrouter.ai/keys 2. In Blender: Press `N` → **GenesisCore** tab 3. **Provider:** OpenRouter 4. **API Key:** Paste key 5. **Fetch Models** → Select: `deepseek/deepseek-chat` or `qwen/qwen-2.5-coder-32b` 6. **Tool Modules:** Enable all (Shift+click for multiple) 7. **Save Config** --- ## Step 8: Start Servers ### blender-mcp (for OpenCode/Claude/Cursor) ```powershell # Terminal 1: Start MCP server uvx blender-mcp # Keep running ``` ### In Blender (for both) 1. Press `N` → **MCP for Blender** tab 2. Check: Poly Haven, Poly Pizza, Sketchfab, etc. 3. Enter API keys in Preferences → Add-ons → MCP for Blender 4. Click **Start MCP Server** ### GenesisCore (for in-Blender chat) 1. Press `N` → **GenesisCore** tab 2. Click **Connect** (connects to internal server on :45677) 3. Chat with AI directly in Blender --- ## Step 9: Test Everything ### Test blender-mcp (via OpenCode) ```bash # In OpenCode chat: "Get the current Blender scene info" "Create a cube at location (1, 2, 3)" "Search Poly Haven for a concrete texture and apply to cube" ``` ### Test GenesisCore (in Blender N-panel) ``` Prompt: "Create a low-poly house with a roof, door, and windows" Prompt: "Add studio lighting: key, fill, rim lights" Prompt: "Search Poly Haven for an HDRI and set as world environment" ``` ### Test Dream Textures 1. Select cube → N-panel → **Dream Textures** 2. Prompt: "weathered brick wall, seamless" 3. Check **Seamless** → Generate 4. Apply to material --- ## API Keys Needed | Service | Key | Where to Get | Where to Enter | |---------|-----|--------------|----------------| | **OpenRouter** | Required | https://openrouter.ai/keys | GenesisCore N-panel | | **Poly Pizza** | Free | https://poly.pizza/settings/api | Blender Prefs → MCP for Blender | | **Sketchfab** | Required | https://sketchfab.com/developers/api | Blender Prefs → MCP for Blender | | **Hyper3D** | Required | https://hyper3d.ai/ | Blender Prefs → MCP for Blender | | **Hunyuan3D** | SecretId/Key | https://3d.hunyuan.tencent.com/ | Blender Prefs → MCP for Blender | | **DreamStudio** | Optional | https://beta.dreamstudio.ai/ | Blender Prefs → Dream Textures | --- ## Directory Structure (Final) ``` <ваш-проект>/ ├── blender-<версия>-windows-x64.msi ├── blender-<версия>-windows-x64.zip ├── CHANGELOG.md ├── INDEX.md ├── addons/ │ ├── GenesisCore-main.zip │ ├── blender-mcp-full.md │ ├── blender-mcp.md │ ├── dream-textures-full.md │ ├── dream-textures.md │ └── genesiscore.md ├── mcp/ │ ├── architecture.md │ ├── clients.md │ ├── server-api.md │ └── setup-guide.md ├── guides/ │ ├── installation.md │ ├── prompting.md │ ├── troubleshooting.md │ └── workflows.md ├── api/ │ ├── blender-api.md │ ├── materials.md │ ├── mesh-operations.md │ └── scripting-patterns.md ├── workflows/ │ ├── animation.md │ ├── scene-building.md │ ├── text-to-3d.md │ └── texture-generation.md └── references/ └── (additional docs) ``` > [!NOTE] > Структура каталогов приведена как пример. Используйте любую удобную папку для проекта и версии Blender. --- ## Troubleshooting Quick Reference | Problem | Solution | |---------|----------| | `uvx` not found | Restart PowerShell; check PATH | | Blender not found | Run `uvx blender-mcp addon-paths` | | Addon not showing | Restart Blender; check correct version folder | | GenesisCore "Connecting..." | Port 45677 blocked; restart Blender | | Dream Textures OOM | Reduce resolution; use fp16 | | Poly Pizza 403 | Manual download + File → Import → glTF | --- ## Next Steps 1. Read [Prompting Guide](../guides/prompting.md) 2. Read [Workflows](../workflows/scene-building.md) 3. Explore [Blender API Patterns](../api/scripting-patterns.md) 4. Check [MCP Architecture](../mcp/architecture.md) --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [BLENDER-MCP ADDON - DEEP DIVE](../addons/blender-mcp.md), [TROUBLESHOOTING GUIDE](troubleshooting.md), [BLENDER-MCP - FULL DOCUMENTATION](../addons/blender-mcp-full.md), [DREAM TEXTURES ADDON - DEEP DIVE](../addons/dream-textures.md), [GENESISCORE ADDON - DEEP DIVE](../addons/genesiscore.md) - **По темам:** [MCP ARCHITECTURE - MODEL CONTEXT PROTOCOL FOR BLENDER](../mcp/architecture.md), [SETUP 35: READY PLAYER ME AVATARS](../../../setups/setup_35_readyplayerme.md), [TEXT-TO-3D WORKFLOWS](../workflows/text-to-3d.md) - **Теги:** #addon, #blender, #mcp, #reference, #stable-diffusion --- ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- # MCP ARCHITECTURE - MODEL CONTEXT PROTOCOL FOR BLENDER Tags: #blender #mcp #reference # MCP ARCHITECTURE - MODEL CONTEXT PROTOCOL FOR BLENDER ## Overview MCP (Model Context Protocol) is an open standard for connecting AI assistants to external tools and data. In Blender context, it enables LLMs to directly control Blender via structured tool calls. --- ## Protocol Specification ### Transport Options | Transport | Use Case | Example | |-----------|----------|---------| | **stdio** | Local subprocess | `uvx blender-mcp` | | **SSE** | HTTP long-polling | GenesisCore on :45677 | | **WebSocket** | Bidirectional | Future | ### Message Format ```json // Request (Client → Server) { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "create_object", "arguments": {"entity_type": "CUBE", "name": "MyCube"} } } // Response (Server → Client) { "jsonrpc": "2.0", "id": 1, "result": { "name": "MyCube", "type": "MESH", "location": [0, 0, 0] } } ``` --- ## Blender MCP Implementations ### 1. blender-mcp (Reference Server) - **Architecture:** External MCP server (uvx) + Blender addon (TCP :9876) - **Transport:** stdio → TCP - **Tools:** 28+ (scene, objects, materials, 5 asset sources, AI 3D gen) - **Clients:** Any MCP-compatible (Claude, Cursor, Codex, OpenCode, etc.) ### 2. GenesisCore (All-in-One) - **Architecture:** Blender addon with built-in MCP Client + Server - **Transport:** Internal SSE (:45677) + External SSE for other clients - **Providers:** OpenRouter, DeepSeek, OpenAI, Anthropic, Ollama, SiliconFlow - **UI:** Native N-panel chat interface ### Comparison | Aspect | blender-mcp | GenesisCore | |--------|-------------|-------------| | MCP Server | External process | Built-in (FastMCP) | | MCP Client | External (any) | Built-in (multi-provider) | | Transport | stdio → TCP :9876 | Internal SSE :45677 | | LLM Providers | Any (via client) | 6 built-in | | Asset Sources | 5 (PH, Sketchfab, PP, Rodin, Hunyuan) | 1 (Poly Haven) | | UI | N-panel (server control) | N-panel (chat + config) | | Code Execution | `execute_blender_code` | `execute_blender_code` | --- ## Tool Calling Flow ``` User Prompt │ ▼ LLM analyzes → selects tool(s) │ ▼ MCP Client formats tool call │ ▼ ┌─────────────────────────────┐ │ Transport Layer │ │ stdio / SSE / WebSocket │ └─────────────────────────────┘ │ ▼ MCP Server receives → validates │ ▼ Tool handler executes │ ├─ blender-mcp: TCP → Blender addon → bpy └─ GenesisCore: Direct bpy (same process) │ ▼ Result → MCP response → LLM │ ▼ LLM continues or responds to user ``` --- ## Blender Addon Integration Patterns ### Pattern 1: Socket Server (blender-mcp) ```python # addon.py - Socket server in background thread class BlenderMCPServer: def __init__(self, host='localhost', port=9876): self.socket = socket.socket() self.command_queue = queue.Queue() def _server_loop(self): # Thread while running: client, _ = self.socket.accept() threading.Thread(target=self._handle_client, args=(client,)).start() def _drain_command_queue(self): # bpy.app.timer (main thread) while queue: cmd, client = queue.get() result = self.execute_command(cmd) client.send(json.dumps(result)) def execute_command(self, cmd): handler = HANDLERS[cmd['type']] return handler(**cmd['params']) ``` ### Pattern 2: Built-in MCP Server (GenesisCore) ```python # src/server/server.py - FastMCP in Blender process class BlenderMCPServer(FastMCP): def __init__(self): super().__init__(name="BlenderMCPServer", host="localhost", port=45677) @self.tool() def create_object(self, entity_type: str, ...) -> dict: # Direct bpy operations - no IPC needed bpy.ops.mesh.primitive_cube_add(...) return {"name": obj.name, ...} # Runs in daemon thread Server.run() # Thread(target=Server.main).start() ``` ### Pattern 3: MCP Client in Blender (GenesisCore) ```python # src/client/base.py - Connects to local MCP server class MCPClientBase: async def connect_to_server(self): async with sse_client(url="http://localhost:45677/sse") as (read, write): self.session = ClientSession(read, write) await self.session.initialize() async def call_tool(self, fn_name, arguments): return await self.session.call_tool(fn_name, arguments) ``` --- ## Security Considerations ### Safe Mode (blender-mcp) ```bash BLENDER_MCP_SAFE_MODE=1 uvx blender-mcp ``` **Blocks:** - `import os, subprocess, sys, socket, requests, urllib` - `open()`, `eval()`, `exec()`, `compile()` - `bpy.app.handlers`, `bpy.app.timers`, `bpy.app.driver_namespace` - Class/property registration (`bpy.utils.register_class`) - External file loading (`bpy.data.libraries.load`) **Allows:** - All `bpy.ops.*` (modeling, materials, rendering, save, import/export) - `bpy.data.*` read/write (objects, meshes, materials, scenes) - `mathutils`, `math`, `random`, `json`, `uuid` (stdlib) ### Code Execution Risk Both implementations expose `execute_blender_code`: ```python # DANGEROUS - full Python access bpy.ops.wm.open_mainfile(filepath="/etc/passwd") # Blocked in safe mode os.system("rm -rf /") # Blocked in safe mode # SAFE - Blender operations only bpy.ops.mesh.primitive_cube_add() mat = bpy.data.materials.new("Red") mat.use_nodes = True mat.node_tree.nodes["Principled BSDF"].inputs[0].default_value = (1,0,0,1) ``` **Best Practice:** Always use specific tools (`create_object`, `set_material`) instead of raw code execution. --- ## Environment Variables ### blender-mcp | Variable | Default | Description | |----------|---------|-------------| | `BLENDER_HOST` | `localhost` | Blender addon host | | `BLENDER_PORT` | `9876` | Blender addon port | | `BLENDER_MCP_SAFE_MODE` | `0` | Enable safe mode | | `DISABLE_TELEMETRY` | `false` | Disable telemetry | | `UV_PYTHON_PREFERENCE` | - | `only-managed` to avoid conda/pyenv | ### GenesisCore | Variable | Default | Description | |----------|---------|-------------| | `OPENROUTER_API_KEY` | - | API key for OpenRouter | | `DEEPSEEK_API_KEY` | - | API key for DeepSeek | | `OPENAI_API_KEY` | - | API key for OpenAI | | `ANTHROPIC_API_KEY` | - | API key for Anthropic | | `OLLAMA_HOST` | `http://localhost:11434` | Ollama endpoint | --- ## Adding Custom Tools ### blender-mcp (Server-side) ```python # src/blender_mcp/tools/my_tool.py from mcp.server.fastmcp import FastMCP @mcp.tool() async def my_custom_tool(param: str) -> str: """Description for LLM""" blender = get_blender_connection() return blender.send_command("my_custom_command", {"param": param}) ``` ### GenesisCore (Addon-side) ```python # src/tools/my_tools.py from .common import ToolsPackageBase class MyTools(ToolsPackageBase): def my_custom_tool(param: str) -> dict: """Description for LLM""" # Direct bpy access return {"result": "success"} # src/tools/__init__.py from .my_tools import MyTools # Order affects UI display ``` ### GenesisCore Custom Client ```python # src/client/custom.py from .base import MCPClientBase class MCPClientCustom(MCPClientBase): @classmethod def info(cls): return {"name": "Custom", "description": "My Provider", "version": "0.1"} @classmethod def default_config(cls): return {"base_url": "https://api.custom.com", "model": "default"} ``` --- ## Debugging ### Blender System Console - **Windows:** Window → Toggle System Console - **macOS:** `/Applications/Blender.app/Contents/MacOS/Blender` in Terminal - **Linux:** Run Blender from terminal ### Common Issues | Symptom | Cause | Fix | |---------|-------|-----| | "Connection refused" | Addon not running | N-panel → Start MCP Server | | "Timeout" | Complex operation | Break into smaller steps | | "Safe mode rejected" | Forbidden import | Rewrite without os/subprocess | | Model list empty | API key invalid | Check key + network | | Poly Pizza 403 | Cloudflare bot check | Residential IP or manual download | --- ## Version Compatibility | Blender | blender-mcp | GenesisCore | MCP Spec | |---------|-------------|-------------|----------| | 3.0+ | ✅ | ❌ | 2024-11-05 | | 4.0+ | ✅ | ✅ | 2024-11-05 | | 4.2+ | ✅ | ✅ | 2025-06-18 | --- ## Resources - [MCP Specification](https://modelcontextprotocol.io/) - [blender-mcp Repo](https://github.com/ahujasid/blender-mcp) - [GenesisCore Repo](https://github.com/AIGODLIKE/GenesisCore) - [FastMCP Python SDK](https://github.com/jlowin/fastmcp) --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [BLENDER-MCP - FULL DOCUMENTATION](../addons/blender-mcp-full.md), [BLENDER-MCP ADDON - DEEP DIVE](../addons/blender-mcp.md), [INSTALLATION GUIDE - COMPLETE SETUP](../guides/installation.md), [TEXT-TO-3D WORKFLOWS](../workflows/text-to-3d.md), [TROUBLESHOOTING GUIDE](../guides/troubleshooting.md) - **По темам:** [BLENDER MATERIALS API REFERENCE](../api/materials.md), [BLENDER MESH OPERATIONS API REFERENCE](../api/mesh-operations.md), [BLENDER PYTHON API REFERENCE (4.x)](../api/blender-api.md) - **Теги:** #blender, #mcp, #reference --- --- ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- # MCP CLIENT CONFIGURATIONS Tags: #mcp #reference # MCP CLIENT CONFIGURATIONS ## OpenCode **Config:** `~/.config/opencode/opencode.json` ```json { "mcp": { "blender-mcp": { "type": "local", "command": ["uvx", "blender-mcp"], "enabled": true, "environment": { "BLENDER_HOST": "localhost", "BLENDER_PORT": "9876" } } } } ``` ## Claude Desktop **Config:** `claude_desktop_config.json` - macOS: `~/Library/Application Support/Claude/` - Windows: `%APPDATA%\Claude\` - Linux: `~/.config/claude/` ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["blender-mcp"] } } } ``` **Claude Code CLI:** ```bash claude mcp add blender uvx blender-mcp ``` ## Cursor **Settings → MCP → Add Server** ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["blender-mcp"] } } } ``` **Windows:** Use `"command": "cmd", "args": ["/c", "uvx", "blender-mcp"]` ## VS Code (with MCP extension) **Settings → Extensions → MCP → Add Server** ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["blender-mcp"] } } } ``` ## Codex **CLI:** ```bash codex mcp add blender -- uvx blender-mcp ``` **Config:** `~/.codex/config.toml` ```toml [mcp_servers.blender] command = "uvx" args = ["blender-mcp"] env = { BLENDER_HOST = "localhost", BLENDER_PORT = "9876" } ``` **Desktop App:** Settings → MCP Servers → Add Server → name: `blender`, STDIO, command: `uvx blender-mcp` ## Antigravity ```json { "mcpServers": { "blender-mcp": { "command": "uvx", "args": ["blender-mcp"], "env": { "BLENDER_HOST": "localhost", "BLENDER_PORT": "9876" } } } } ``` ## Docker (Alternative) ```json { "mcpServers": { "blender": { "command": "docker", "args": ["run", "-i", "--rm", "blender-mcp"] } } } ``` **Linux:** Add `"--network=host", "-e", "BLENDER_HOST=localhost"` ## Environment Variables (All Clients) ```json "environment": { "BLENDER_HOST": "localhost", "BLENDER_PORT": "9876", "BLENDER_MCP_SAFE_MODE": "0", "DISABLE_TELEMETRY": "false", "UV_PYTHON_PREFERENCE": "only-managed" } ``` ## Python Version Pinning (Recommended) ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["--python", "3.11", "blender-mcp"], "env": { "UV_PYTHON_PREFERENCE": "only-managed" } } } } ``` ## Troubleshooting Client Connections | Issue | Fix | |-------|-----| | `spawn uvx ENOENT` | Use full path: `which uvx` / `where uvx` → put in `command` | | Connection refused | Blender addon not running; click "Start MCP Server" | | Tools not appearing | Restart client after config change (full quit) | | Timeout on first call | Normal; retry once | ## GenesisCore (Built-in Client) No external MCP client needed. Configure in Blender N-panel: 1. Provider: OpenRouter / DeepSeek / OpenAI / Anthropic / Ollama / SiliconFlow 2. API Key 3. Model selection 4. Tool modules (Common, Object, Material, Polyhaven, Asset) 5. Connect (uses internal SSE on :45677) **External clients can connect to GenesisCore:** ```json { "mcpServers": { "BlenderGenesis": { "url": "http://localhost:45677" } } } ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [BLENDER-MCP - FULL DOCUMENTATION](../addons/blender-mcp-full.md), [BLENDER-MCP ADDON - DEEP DIVE](../addons/blender-mcp.md), [GENESISCORE ADDON - DEEP DIVE](../addons/genesiscore.md), [INSTALLATION GUIDE - COMPLETE SETUP](../guides/installation.md), [MCP ARCHITECTURE - MODEL CONTEXT PROTOCOL FOR BLENDER](architecture.md) - **По темам:** [MCP SERVER API REFERENCE](server-api.md), [TEXT-TO-3D WORKFLOWS](../workflows/text-to-3d.md), [TROUBLESHOOTING GUIDE](../guides/troubleshooting.md) - **Теги:** #mcp, #reference --- --- ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- # MCP SERVER API REFERENCE Tags: #api #mcp #reference # MCP SERVER API REFERENCE ## blender-mcp Tools (28+) ### Core Scene Tools #### `get_scene_info` ```json // Returns simplified scene data { "name": "Scene", "object_count": 15, "objects": [ {"name": "Cube", "type": "MESH", "location": [0,0,0]} ], "materials_count": 5 } ``` #### `get_object_info` ```json // Parameters {"name": "Cube"} // Returns { "name": "Cube", "type": "MESH", "location": [0,0,0], "rotation": [0,0,0], "scale": [1,1,1], "visible": true, "bound_box": [[...], ...], "materials": ["Material.001"], "mesh": {"vertices": 8, "edges": 12, "polygons": 6} } ``` #### `get_world_state_snapshot` ```json // Compact trajectory snapshot (4000 object cap) { "name": "Scene", "object_count": 15, "objects_listed": 15, "objects_truncated": false, "selected": ["Cube"], "selected_count": 1, "frame_current": 1, "fps": 24, "objects": [...], "active_camera": "Camera", "camera": {...}, "lights": [...], "materials_count": 5, "material_fps": {"Material": "a1b2c3d4"}, "world_fp": "e5f6g7h8", "project_id": "abc123def456", "blender_version": "4.2.3", "snapshot_source": "native" } ``` #### `get_viewport_screenshot` ```json // Parameters {"max_size": 800, "filepath": "/tmp/screenshot.png", "format": "png"} // Returns: Image (MCP Image type) ``` #### `execute_blender_code` ```json // Parameters {"code": "import bpy\nbpy.ops.mesh.primitive_cube_add(location=(1,2,3))"} // Returns {"result": "Code executed successfully: ..."} ``` #### `get_addon_info` ```json // Handshake info { "name": "MCP for Blender", "addon_version": [1, 6], "protocol_version": 5, "capabilities": ["get_scene_info", "get_object_info", ...], "blender_version": "Blender 4.2.3" } ``` ### Material Tools #### `set_texture` ```json // Parameters {"object_name": "Cube", "texture_id": "RustedIron"} // Applies previously downloaded Poly Haven texture ``` ### Poly Haven Tools (require enable in Blender) #### `get_polyhaven_categories` ```json // Parameters {"asset_type": "hdris"} // "hdris" | "textures" | "models" | "all" // Returns formatted category list with counts ``` #### `search_polyhaven_assets` ```json // Parameters {"asset_type": "models", "categories": "architecture,buildings"} // Returns { "total_count": 45, "returned_count": 20, "assets": {...} } ``` #### `download_polyhaven_asset` ```json // Parameters {"asset_id": "abandoned_bakery", "asset_type": "hdris", "resolution": "2k", "file_format": "exr"} // asset_type: "hdris" | "textures" | "models" // resolution: "1k" | "2k" | "4k" | "8k" | "16k" // HDRI → sets as world environment // Texture → creates PBR material // Model → imports .blend to scene ``` #### `get_polyhaven_status` ```json // Returns enabled status + message ``` ### Poly Pizza Tools (require API key + enable) #### `get_polypizza_status` ```json // Returns enabled status ``` #### `search_polypizza_models` ```json // Parameters {"query": "chair", "category": "Furniture & Decor", "licence": "CC0", "animated": false, "limit": 20} // Categories: Food & Drink, Clutter, Weapons, Transport, Furniture & Decor, // Objects, Nature, Animals, Buildings, People & Characters, Scenes & Levels, Other // Licence: "CC0" (no credit) | "CC-BY" (credit required) // Returns formatted list with licence + tri count ``` #### `download_polypizza_model` ```json // Parameters {"model_id": "chair_01", "normalize_size": true, "target_size": 1.0} // target_size in meters ``` ### Sketchfab Tools (require API key + enable) #### `get_sketchfab_status` #### `search_sketchfab_models` ```json // Parameters {"query": "car", "categories": "vehicles", "count": 20, "downloadable": true} ``` #### `get_sketchfab_model_preview` ```json // Parameters {"uid": "abc123"} // Returns: Image (thumbnail) ``` #### `download_sketchfab_model` ```json // Parameters {"uid": "abc123", "target_size": 4.5} // target_size REQUIRED (meters, largest dimension) // normalize_size always true ``` ### Hyper3D Rodin Tools (require API key + enable) #### `get_hyper3d_status` #### `create_rodin_job` ```json // Parameters {"prompt": "garden gnome", "seed": 12345} // Returns job_id ``` #### `poll_rodin_job_status` ```json // Parameters {"job_id": "job_abc123"} // Returns status, progress, result_url ``` #### `import_generated_asset` ```json // Parameters {"job_id": "job_abc123", "result_url": "https://..."} // Imports to scene ``` ### Hunyuan3D Tools (require SecretId/SecretKey + enable) #### `get_hunyuan3d_status` #### `create_hunyuan_job` #### `poll_hunyuan_job_status` #### `import_generated_asset_hunyuan` ### Telemetry Tools #### `get_telemetry_consent` ```json // Returns {"consent": true|false|null} ``` #### `set_telemetry_consent` ```json // Parameters {"consent": false} // Turns OFF data collection (cannot turn ON via tool) ``` #### `drain_human_activity` ```json // Returns buffered undo/redo/operator events {"events": [{"kind": "operator", "bl_idname": "mesh.primitive_cube_add", ...}]} ``` --- ## GenesisCore Tools (Internal) GenesisCore uses same tool definitions but executes directly in Blender process (no TCP). ### Tool Modules (src/tools/) #### `CommonTools` - `get_simple_info()` → basic scene stats - `get_scene_info()` → full scene - `execute_blender_code(code)` → arbitrary Python #### `ObjectTools` - `create_object(entity_type, name, location, rotation, scale, ...)` → CUBE, SPHERE, CYLINDER, PLANE, CONE, TORUS, EMPTY, CAMERA, LIGHT - `modify_object(name, location, rotation, scale, visible)` - `delete_object(object_name)` - `get_object_info(object_name)` #### `MaterialTools` - `set_material(object_name, material_name, color)` → Principled BSDF #### `PolyhavenTools` - `polyhaven_search_models(names, tags, categories)` - `polyhaven_fetch_model_info(asset_id)` - `polyhaven_use_model_asset(asset_id, expected_resolution)` - `polyhaven_search_hdris(names, tags, categories)` - `polyhaven_fetch_hdri_info(asset_id)` - `polyhaven_use_hdri(asset_id, expected_resolution)` --- ## JSON-RPC Protocol (Reference) ### Request ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "tool_name", "arguments": {...} } } ``` ### Success Response ```json { "jsonrpc": "2.0", "id": 1, "result": {...} } ``` ### Error Response ```json { "jsonrpc": "2.0", "id": 1, "error": { "code": -32603, "message": "Internal error: ..." } } ``` --- ## Adding Custom Tools ### blender-mcp (Server-side) ```python # src/blender_mcp/tools/custom.py from mcp.server.fastmcp import FastMCP @mcp.tool() async def my_tool(param: str) -> str: """Description for LLM""" blender = get_blender_connection() return blender.send_command("my_command", {"param": param}) ``` ### GenesisCore (Addon-side) ```python # src/tools/my_tools.py from .common import ToolsPackageBase class MyTools(ToolsPackageBase): def my_tool(param: str) -> dict: """Description for LLM""" # Direct bpy access return {"result": "success"} # src/tools/__init__.py from .my_tools import MyTools # Order = UI display order ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [BLENDER MATERIALS API REFERENCE](../api/materials.md), [BLENDER MESH OPERATIONS API REFERENCE](../api/mesh-operations.md), [BLENDER PYTHON API REFERENCE (4.x)](../api/blender-api.md), [BLENDER-MCP - FULL DOCUMENTATION](../addons/blender-mcp-full.md), [BLENDER-MCP ADDON - DEEP DIVE](../addons/blender-mcp.md) - **По темам:** [GENESISCORE ADDON - DEEP DIVE](../addons/genesiscore.md), [INSTALLATION GUIDE - COMPLETE SETUP](../guides/installation.md), [MCP ARCHITECTURE - MODEL CONTEXT PROTOCOL FOR BLENDER](architecture.md) - **Теги:** #api, #mcp, #reference --- --- ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- # PROMPTING GUIDE - EFFECTIVE AI COMMANDS FOR BLENDER Tags: #blender #reference # PROMPTING GUIDE - EFFECTIVE AI COMMANDS FOR BLENDER ## Core Principles ### 1. Be Specific About Geometry ``` ❌ "Make a house" ✅ "Create a low-poly house: 6x8m base, 4m walls, gabled roof 3m high, 1x2m door centered front, two 1x1m windows on each side" ``` ### 2. Reference Blender Concepts ``` ✅ "Add a Subdivision Surface modifier (levels=2) to the cube" ✅ "Create a Principled BSDF material: Base Color #8B4513, Roughness 0.8" ✅ "Set object origin to geometry center (Right-click → Set Origin → Origin to Geometry)" ``` ### 3. Break Complex Tasks Into Steps ``` Step 1: "Create the base geometry for a medieval tower" Step 2: "Add window openings using boolean difference with cylinders" Step 3: "Apply stone material with moss in crevices" Step 4: "Add crenellations on top using array modifier" Step 5: "Set up 3-point lighting for dramatic rendering" ``` --- ## Prompt Templates by Task ### Object Creation ``` "Create a [PRIMITIVE] named [NAME] at location [X,Y,Z] with scale [X,Y,Z] rotation [X,Y,Z] (radians). Primitives: CUBE, SPHERE, CYLINDER, PLANE, CONE, TORUS, EMPTY, CAMERA, LIGHT" Example: "Create a CYLINDER named 'Column' at (0,0,0) scale (0.3,0.3,5) rotation (0,0,0)" ``` ### Material Assignment ``` "Create a Principled BSDF material named [NAME] with: Base Color: [R,G,B] or hex #RRGGBB Roughness: [0-1] Metallic: [0-1] [Optional: Transmission, IOR, Clearcoat, Emission] Apply to object [OBJECT_NAME]" Example: "Create material 'Gold' with Base Color #FFD700, Roughness 0.1, Metallic 1.0, Apply to 'Ring'" ``` ### Lighting Setups ``` "Create [LIGHT_TYPE] light named [NAME] at [LOCATION] with: Energy: [WATTS] Color: [R,G,B] or temperature [K] [For AREA: size, spread] [For SUN: angle]" Common setups: - "3-point studio lighting: Key (1000W, 5600K, 45°), Fill (300W, 6500K, -30°), Rim (500W, 5600K, 180°)" - "HDRI lighting: Search Poly Haven for 'studio' HDRI, 2k resolution, set as world" - "Volumetric fog: Add cube domain, Principled Volume density 0.1, Sun light through" ``` ### Camera Setup ``` "Add CAMERA named [NAME] at [LOCATION] rotation [EULER] Set as active camera: bpy.context.scene.camera = bpy.data.objects['NAME'] [Optional: Lens 50mm, Sensor 36mm, DOF focus on object]" ``` ### Poly Haven Assets ``` "Search Poly Haven for [ASSET_TYPE: models/hdris/textures] with [categories: ['category1', 'category2']] [tags: ['tag1']] Download [ASSET_ID] at [RESOLUTION: 1k/2k/4k/8k] [For textures: apply to object [NAME]]" Examples: "Search Poly Haven models categories ['furniture', 'seating'] download 'ArmChair_01' at 2k" "Search Poly Haven hdris categories ['indoor'] download 'abandoned_bakery' at 2k set as world" "Search Poly Haven textures categories ['metal'] download 'RustedIron' at 2k apply to 'Pipe'" ``` ### Poly Pizza (Low-Poly) ``` "Search Poly Pizza for [QUERY] category [CATEGORY_NAME] licence [CC0/CC-BY] animated [true/false] limit [N] Download [MODEL_ID] normalize_size true target_size [METERS]" Categories: Food & Drink, Clutter, Weapons, Transport, Furniture & Decor, Objects, Nature, Animals, Buildings, People & Characters, Scenes & Levels, Other ``` ### Sketchfab (High-Quality) ``` "Search Sketchfab for [QUERY] categories [CATEGORY] count [N] downloadable true Get preview for [UID] Download [UID] target_size [METERS]" ``` ### AI 3D Generation (Hyper3D/Hunyuan3D) ``` "Generate 3D model of [DESCRIPTION] using Hyper3D Rodin [or Hunyuan3D] Poll status until complete Import result" ``` ### Scene Building ``` "Create a [SCENE_TYPE] scene: - [LIST OF OBJECTS WITH SPECS] - Lighting: [SETUP] - Camera: [POSITION/TYPE] - Render settings: Cycles/EEVEE, samples, resolution" ``` --- ## Advanced Prompting Patterns ### Iterative Refinement ``` 1. "Create a basic chair shape with primitives" 2. "Add subdivision surface and bevel to smooth edges" 3. "Create fabric material for seat, wood for legs" 4. "Add stitching detail using geometry nodes or displacement" 5. "Set up turntable animation 360° over 120 frames" ``` ### Conditional Logic ``` "If the object 'Wall' exists, add a window opening at height 1.2m. Otherwise, create a wall first then add window." ``` ### Batch Operations ``` "For each selected object: 1. Apply scale (Ctrl+A → Scale) 2. Add bevel modifier (width=0.02, segments=3) 3. Shade smooth 4. Create unique material with random hue" ``` ### Code Execution (When Tools Aren't Enough) ``` "Execute this Blender Python code: ``` import bpy for obj in bpy.context.selected_objects: obj.modifiers.new('Bevel', 'BEVEL') obj.modifiers['Bevel'].width = 0.02 obj.modifiers['Bevel'].segments = 3 bpy.ops.object.shade_smooth() ```" ``` --- ## Provider-Specific Tips ### OpenRouter (GenesisCore) - **Best for coding:** `deepseek/deepseek-chat`, `qwen/qwen-2.5-coder-32b` - **Best for reasoning:** `anthropic/claude-3.5-sonnet` - **Cheapest:** `meta-llama/llama-3.1-8b-instruct` (simple tasks) - Always specify model in GenesisCore settings ### blender-mcp (Any Client) - Works with any MCP client (Claude, Cursor, Codex, OpenCode) - Tools are self-documenting - LLM knows parameters - Use `execute_blender_code` for custom operations --- ## Common Patterns Cheat Sheet | Task | Prompt Pattern | |------|----------------| | **Primitive** | "Create [TYPE] named [NAME] at [LOC] scale [SCALE]" | | **Material** | "Create Principled BSDF [NAME]: color [RGB], roughness [0-1], metallic [0-1] apply to [OBJ]" | | **Modifier** | "Add [MODIFIER_TYPE] to [OBJ] with [PARAMS]" | | **Bool** | "Boolean [UNION/DIFFERENCE/INTERSECT] [OBJ_A] with [OBJ_B]" | | **Array** | "Add Array modifier to [OBJ]: count [N], offset [X,Y,Z]" | | **Curve** | "Create Bezier curve through points [LIST], bevel depth [RADIUS]" | | **Particles** | "Add Particle System to [OBJ]: type HAIR, count [N], length [L]" | | **Rigid Body** | "Add Rigid Body to [OBJ]: type ACTIVE/PASSIVE, mass [KG]" | | **Animation** | "Animate [OBJ] location from [START] to [END] over [FRAMES] frames" | | **Driver** | "Add driver to [OBJ].[PROP] driven by [TARGET].[PROP] with expression [EXPR]" | | **Geo Nodes** | "Create Geometry Nodes tree on [OBJ]: [NODE DESCRIPTION]" | --- ## Error Recovery ### When AI Gets Stuck ``` "Reset: Delete all objects created in this session. Start fresh with: [CLEAR SPECIFICATION]" ``` ### When Tool Fails ``` "The previous command failed with [ERROR]. Try alternative approach: [SUGGESTION]" ``` ### When Result Is Wrong ``` "Undo the last operation. The issue is [DESCRIPTION]. Instead: [CORRECTED INSTRUCTION]" ``` --- ## Token Optimization ### Use History Wisely - **GenesisCore:** Toggle "Use History" off for independent commands - **blender-mcp:** Each tool call is independent; no context needed ### Concise Prompts ``` ❌ "I would like you to please create a cube that is red in color and put it at the origin" ✅ "Create CUBE named 'RedCube' at (0,0,0) material red" ``` ### Batch Related Operations ``` ✅ Single prompt: "Create 5 cubes in a row at x=-2,-1,0,1,2 with materials red,blue,green,yellow,purple" ``` --- ## Debugging Prompts ### Inspect Scene ``` "Get full scene info including all objects, materials, lights, camera" "Get object info for [NAME] - show vertices, faces, materials, modifiers" "Take viewport screenshot" ``` ### Check Addon Status ``` "Get addon info - show version, capabilities, protocol version" "Check Poly Haven status" "Check Poly Pizza status" ``` --- ## Model-Specific Prompting ### DeepSeek / Qwen (Code-heavy) ``` "Write Blender Python code to: [ALGORITHMIC DESCRIPTION] Use bpy.data, bpy.ops, mathutils. No external imports." ``` ### Claude (Reasoning-heavy) ``` "Plan the steps to create [COMPLEX SCENE], then execute step by step. Explain each step before executing." ``` ### GPT-4o (Balanced) ``` "Create [SCENE] following best practices for [STYLE/TECHNIQUE]. Optimize for render time / poly count / editability." ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [BLENDER MATERIALS API REFERENCE](../api/materials.md), [BLENDER MESH OPERATIONS API REFERENCE](../api/mesh-operations.md), [BLENDER PYTHON API REFERENCE (4.x)](../api/blender-api.md), [BLENDER SCRIPTING PATTERNS - SAFE & EFFECTIVE](../api/scripting-patterns.md), [BLENDER-MCP - FULL DOCUMENTATION](../addons/blender-mcp-full.md) - **По темам:** [BLENDER-MCP ADDON - DEEP DIVE](../addons/blender-mcp.md), [INSTALLATION GUIDE - COMPLETE SETUP](installation.md), [MCP ARCHITECTURE - MODEL CONTEXT PROTOCOL FOR BLENDER](../mcp/architecture.md) - **Теги:** #blender, #reference, #rrggbb --- --- ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- # TEXT-TO-3D WORKFLOWS Tags: #ai-3d #blender #mcp #reference # TEXT-TO-3D WORKFLOWS ## Overview Multiple approaches for generating 3D models from text prompts. --- ## Method 1: Hyper3D Rodin (via blender-mcp) **Best for:** Quick concept models, game-ready assets **Cost:** API credits (free tier available) **Quality:** Medium-high, clean topology ### Workflow ```bash # 1. Enable in Blender N-panel → MCP for Blender → Check "Use Hyper3D" → Add API key (Preferences) # 2. Via AI (OpenCode/Claude): "Generate a 3D model of a medieval fantasy sword using Hyper3D Rodin" # → Creates job → Polls status → Imports result # 3. Post-process: "Decimate the sword to 2000 tris, add bevel, create metal material" ``` ### Parameters - `prompt`: Text description - `seed`: For reproducibility - `generate_textures`: Optional PBR generation --- ## Method 2: Hunyuan3D (via blender-mcp) **Best for:** High-quality assets, Chinese/Asian themes **Cost:** SecretId/SecretKey required **Quality:** High, good for production ### Workflow ```bash # 1. Get credentials: https://3d.hunyuan.tencent.com/ # 2. Enable in Blender: Check "Use Hunyuan3D" → Add SecretId/SecretKey/API URL # 3. Via AI: "Create a 3D model of a Chinese dragon using Hunyuan3D" # → Job → Poll → Import # 4. Cleanup: "Separate loose parts, apply scale, create materials for each part" ``` --- ## Method 3: TripoSR (Local, Free) **Best for:** Privacy, offline, experimentation **Cost:** Free (runs locally) **Quality:** Lower, good for concepts **Hardware:** 8GB+ VRAM recommended ### Installation ```bash # Option A: pip pip install triposr # Option B: Clone repo git clone https://github.com/VAST-AI-Research/TripoSR cd TripoSR pip install -r requirements.txt # Download weights (auto on first run) ``` ### Usage via Python ```python # Execute via execute_blender_code import bpy from triposr import TripoSR model = TripoSR.from_pretrained("VAST-AI/TripoSR") output = model.generate("a cute robot", num_inference_steps=50) # output is mesh dict → import to Blender ``` ### Blender Integration ```python # Save as .obj/.glb then import import bpy bpy.ops.wm.obj_import(filepath="output.obj") # or bpy.ops.import_scene.gltf(filepath="output.glb") ``` --- ## Method 4: MeshGPT / Shap-E (Local) **Best for:** Research, custom training **Cost:** Free **Quality:** Experimental ### Shap-E (OpenAI) ```bash git clone https://github.com/openai/shap-e cd shap-e pip install -e . # Download weights ``` ```python from shap_e.diffusion.sample import sample_latents from shap_e.diffusion.gaussian_diffusion import diffusion_from_config from shap_e.models.download import load_model, load_config from shap_e.util.notebooks import decode_latent_mesh xm = load_model('transmitter', device='cuda') model = load_model('text300M', device='cuda') diffusion = diffusion_from_config(load_config('diffusion')) latents = sample_latents( batch_size=1, model=model, diffusion=diffusion, prompt="a red sports car", guidance_scale=15.0, device='cuda' ) for i, latent in enumerate(latents): mesh = decode_latent_mesh(xm, latent).tri_mesh() with open(f'car_{i}.obj', 'w') as f: mesh.write_obj(f) ``` --- ## Method 5: LGM (Large Gaussian Model) **Best for:** High-quality, fast (Gaussian splatting) **Cost:** Free (local) ```bash git clone https://github.com/ashawkey/LGM cd LGM pip install -r requirements.txt ``` --- ## Comparison Matrix | Method | Local/Cloud | Cost | Quality | Speed | Topology | UVs | |--------|-------------|------|---------|-------|----------|-----| | **Hyper3D** | Cloud | $ | ⭐⭐⭐⭐ | Fast | Good | Yes | | **Hunyuan3D** | Cloud | $ | ⭐⭐⭐⭐⭐ | Medium | Excellent | Yes | | **TripoSR** | Local | Free | ⭐⭐⭐ | Fast | Basic | Basic | | **Shap-E** | Local | Free | ⭐⭐ | Slow | Basic | No | | **LGM** | Local | Free | ⭐⭐⭐⭐ | Fast | N/A (splats) | N/A | --- ## Recommended Pipeline (2026) ### For Production Assets ``` 1. Generate base: Hyper3D or Hunyuan3D (cloud) 2. Import → Blender 3. Cleanup: Decimate, Separate, Scale, Origin 4. Retopology: Quad Remesher / Instant Meshes 5. UV: Smart UV Project / Manual 6. Bake: Normal, AO, Curvature from high-poly 7. Texture: Substance Painter / Dream Textures 6. Export: GLTF (game) / USD (pipeline) ``` ### For Rapid Prototyping ``` 1. TripoSR (local, free) → quick concept 2. If promising → Hyper3D for quality 3. GenesisCore for scene assembly 4. Dream Textures for materials ``` ### For Privacy/Local-First ``` 1. Ollama (llama3.2/codellama) via GenesisCore 2. TripoSR for 3D gen 3. Dream Textures (local SD) for textures 4. All offline, no API keys ``` --- ## Automation Script Template ```python # Execute via execute_blender_code import bpy import requests import time def generate_hyper3d(prompt, api_key): # Submit job resp = requests.post( "https://api.hyper3d.ai/v1/jobs", headers={"Authorization": f"Bearer {api_key}"}, json={"prompt": prompt, "generate_textures": True} ) job_id = resp.json()["job_id"] # Poll while True: time.sleep(10) status = requests.get( f"https://api.hyper3d.ai/v1/jobs/{job_id}", headers={"Authorization": f"Bearer {api_key}"} ).json() if status["status"] == "completed": return status["result_url"] elif status["status"] == "failed": raise Exception(status["error"]) # Usage url = generate_hyper3d("futuristic motorcycle", "YOUR_KEY") # Download and import import urllib.request urllib.request.urlretrieve(url, "/tmp/model.glb") bpy.ops.import_scene.gltf(filepath="/tmp/model.glb") ``` --- ## Post-Generation Checklist - [ ] **Scale:** Apply scale (Ctrl+A → Scale), target real-world size - [ ] **Origin:** Set to geometry center (Right-click → Set Origin → Origin to Geometry) - [ ] **Topology:** Check tris/quads, decimate if needed - [ ] **Separation:** Separate loose parts (P → By Loose Parts) for materials - [ ] **Naming:** Rename objects logically - [ ] **Materials:** Assign/create PBR materials - [ ] **UVs:** Unwrap (Smart UV Project), check for overlaps - [ ] **LODs:** Create LOD versions (50%, 25%, 10%) - [ ] **Export:** GLTF for web/game, USD for pipeline --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [BLENDER-MCP - FULL DOCUMENTATION](../addons/blender-mcp-full.md), [BLENDER-MCP ADDON - DEEP DIVE](../addons/blender-mcp.md), [INSTALLATION GUIDE - COMPLETE SETUP](../guides/installation.md), [MCP ARCHITECTURE - MODEL CONTEXT PROTOCOL FOR BLENDER](../mcp/architecture.md), [TROUBLESHOOTING GUIDE](../guides/troubleshooting.md) - **По темам:** [SETUP 45: HUNYUAN3D 2.1 LOCAL](../../../setups/setup_45_hunyuan3d_21.md), [WORKFLOWS - PRODUCTION PIPELINES](scene-building.md), [BLENDER MATERIALS API REFERENCE](../api/materials.md) - **Теги:** #ai-3d, #blender, #mcp, #reference --- --- ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность ⭐⭐⭐⭐. --- # TEXTURE GENERATION WORKFLOWS Tags: #reference #texturing # TEXTURE GENERATION WORKFLOWS ## Overview Multiple approaches for creating PBR textures and materials. --- ## Method 1: Dream Textures (Local Stable Diffusion) **Best for:** Custom textures, seamless tiling, inpainting, projection **Cost:** Free (local GPU) **Quality:** High, fully controllable ### Basic Texture Generation ``` 1. Select object → N-panel → Dream Textures 2. Tab: "Generate Texture" 3. Prompt: "weathered brick wall, moss in cracks, seamless" 4. Check: "Seamless" (for tiling) 5. Model: stabilityai/stable-diffusion-2-1-base 6. Size: 512x512 (or 1024x1024 if VRAM) 7. Click: Generate 8. Click: "Apply to Material" or drag to shader nodes ``` ### Texture Projection (Depth-to-Image) ``` 1. Model your geometry (wall, floor, character) 2. Select faces to texture (Edit Mode → Select faces) 3. Local View (/) to isolate 4. Dream Textures → "Project Dream Texture" 5. Model: stabilityai/stable-diffusion-2-depth (REQUIRED) 6. Prompt: "ancient stone temple, moss, cracks, torch lighting" 7. Data Source: "Depth" or "Depth and Color" 8. Click: Project Dream Texture 9. Auto-creates material + UVs ``` ### Inpainting (Fix Seams / Edit) ``` 1. Open generated texture in Image Editor 2. Mask problem area (brush tool) 3. Dream Textures → Inpaint 4. Prompt: "seamless continuation of stone texture" 5. Generates only in masked area ``` ### Outpainting (Extend Texture) ``` 1. Image Editor → Select texture 2. Dream Textures → Outpaint 3. Choose origin (center, edge, corner) 3. Prompt: "extend brick wall pattern" 4. Increases canvas size ``` ### Render Pass Re-style ``` 1. Set up Cycles scene with basic lighting 2. Add "Dream Texture" node in Compositor 3. Connect Render Layers → Dream Texture → Composite 4. Prompt: "cyberpunk neon, volumetric fog, photorealistic" 5. Re-render → AI transforms each frame ``` ### AI Upscaling ``` 1. Low-res texture (512x512) 2. Dream Textures → Upscale 3. Model: stabilityai/stable-diffusion-x4-upscaler 4. Output: 2048x2048 ``` --- ## Method 2: Poly Haven Textures (Free, Curated) **Best for:** Production-ready PBR, consistent quality **Cost:** Free (CC0) **Quality:** Professional, 1k-8k ### Workflow (via blender-mcp / GenesisCore) ```bash # Via AI: "Search Poly Haven textures for 'metal' category 'rusted' at 2k resolution Download 'RustedIron' and apply to object 'Pipe'" # Steps: 1. search_polyhaven_assets(asset_type="textures", categories="metal") 2. download_polyhaven_asset(asset_id="RustedIron", asset_type="textures", resolution="2k") 3. set_texture(object_name="Pipe", texture_id="RustedIron") ``` ### Maps Included - Base Color (Diffuse) - Roughness - Normal (OpenGL) - Metallic - AO (Ambient Occlusion) - Height/Displacement ### Resolution Options | Resolution | Use Case | |------------|----------| | 1k | Preview, mobile, background | | 2k | Standard production | | 4k | Hero assets, close-ups | | 8k | Extreme detail, film | --- ## Method 3: Procedural / Node-Based (Blender Native) **Best for:** Infinite variation, parametric, no GPU needed **Cost:** Free **Quality:** Mathematical precision ### Voronoi Noise Pattern ```python # Execute via execute_blender_code import bpy mat = bpy.data.materials.new("ProceduralMetal") mat.use_nodes = True nodes = mat.node_tree.nodes links = mat.node_tree.links nodes.clear() # Nodes output = nodes.new('ShaderNodeOutputMaterial') principled = nodes.new('ShaderNodeBsdfPrincipled') voronoi = nodes.new('ShaderNodeTexVoronoi') color_ramp = nodes.new('ShaderNodeValToRGB') mapping = nodes.new('ShaderNodeMapping') tex_coord = nodes.new('ShaderNodeTexCoord') # Layout tex_coord.location = (-600, 0) mapping.location = (-400, 0) voronoi.location = (-200, 0) color_ramp.location = (0, 0) principled.location = (200, 0) output.location = (400, 0) # Configure voronoi.feature = 'SMOOTH_F1' voronoi.metric = 'MANHATTAN' color_ramp.color_ramp.elements[0].position = 0.3 color_ramp.color_ramp.elements[0].color = (0.1, 0.1, 0.1, 1) color_ramp.color_ramp.elements[1].position = 0.7 color_ramp.color_ramp.elements[1].color = (0.8, 0.7, 0.6, 1) # Links links.new(tex_coord.outputs['Object'], mapping.inputs['Vector']) links.new(mapping.outputs['Vector'], voronoi.inputs['Vector']) links.new(voronoi.outputs['Color'], color_ramp.inputs['Fac']) links.new(color_ramp.outputs['Color'], principled.inputs['Base Color']) links.new(voronoi.outputs['Distance'], principled.inputs['Roughness']) links.new(principled.outputs['BSDF'], output.inputs['Surface']) # Animate mapping scale mapping.inputs['Scale'].default_value = (5, 5, 5) ``` ### Brick Generator (Geometry Nodes → Texture) ```python # Creates procedural brick pattern with mortar # Use Geometry Nodes to generate mesh → bake to texture ``` --- ## Method 4: Dream Textures + Poly Haven Hybrid **Best for:** Hero assets with custom details on curated base ### Workflow ``` 1. Base: Download Poly Haven "ConcreteFloor01" (2k PBR set) 2. Customize: Dream Textures Inpaint - Mask areas for "cracks with glowing moss" - "painted graffiti symbols" - "water puddles with reflections" 3. Bake: Combine into single texture set 4. Apply: Single material with all maps ``` --- ## Method 5: Substance / External Tools **Best for:** AAA production, team pipelines **Cost:** Subscription **Integration:** Export from Blender → Substance → Import ### Pipeline ``` 1. Blender: Model + UV unwrap 2. Export: GLTF/FBX with UVs 3. Substance Painter: Smart materials + custom 4. Export: PBR maps (4k) 5. Blender: Import maps → Principled BSDF setup ``` --- ## PBR Material Setup (Standard) ### Principled BSDF Connections ``` Image Texture (Base Color) ──────────────────→ Base Color Image Texture (Roughness) ──────────────────→ Roughness (Color Space: Non-Color) Image Texture (Normal) → Normal Map ────────→ Normal Image Texture (Metallic) ───────────────────→ Metallic (Non-Color) Image Texture (Height) → Displacement ──────→ Displacement (Material Settings) Image Texture (AO) ─────────────────────────→ Mix with Base Color (Multiply) ``` ### Node Setup Script ```python # Execute via execute_blender_code import bpy def setup_pbr_material(obj, name, maps_dict): """maps_dict: {'base_color': img, 'roughness': img, 'normal': img, ...}""" mat = bpy.data.materials.new(name) mat.use_nodes = True nodes = mat.node_tree.nodes links = mat.node_tree.links nodes.clear() principled = nodes.new('ShaderNodeBsdfPrincipled') output = nodes.new('ShaderNodeOutputMaterial') links.new(principled.outputs['BSDF'], output.inputs['Surface']) # Base Color if 'base_color' in maps_dict: tex = nodes.new('ShaderNodeTexImage') tex.image = maps_dict['base_color'] tex.image.colorspace_settings.name = 'sRGB' links.new(tex.outputs['Color'], principled.inputs['Base Color']) # Roughness if 'roughness' in maps_dict: tex = nodes.new('ShaderNodeTexImage') tex.image = maps_dict['roughness'] tex.image.colorspace_settings.name = 'Non-Color' links.new(tex.outputs['Color'], principled.inputs['Roughness']) # Normal if 'normal' in maps_dict: tex = nodes.new('ShaderNodeTexImage') tex.image = maps_dict['normal'] tex.image.colorspace_settings.name = 'Non-Color' normal_map = nodes.new('ShaderNodeNormalMap') links.new(tex.outputs['Color'], normal_map.inputs['Color']) links.new(normal_map.outputs['Normal'], principled.inputs['Normal']) # Metallic if 'metallic' in maps_dict: tex = nodes.new('ShaderNodeTexImage') tex.image = maps_dict['metallic'] tex.image.colorspace_settings.name = 'Non-Color' links.new(tex.outputs['Color'], principled.inputs['Metallic']) # Height/Displacement if 'height' in maps_dict: tex = nodes.new('ShaderNodeTexImage') tex.image = maps_dict['height'] tex.image.colorspace_settings.name = 'Non-Color' disp = nodes.new('ShaderNodeDisplacement') links.new(tex.outputs['Color'], disp.inputs['Height']) links.new(disp.outputs['Displacement'], output.inputs['Displacement']) # Enable in Material Settings mat.displacement_method = 'BOTH' # Assign if obj.data.materials: obj.data.materials[0] = mat else: obj.data.materials.append(mat) return mat ``` --- ## Texture Resolution Guidelines | Asset Type | Distance | Resolution | |------------|----------|------------| | Hero prop (hero) | Close-up | 4k-8k | | Character | Medium | 2k-4k | | Environment | Medium | 2k | | Background | Far | 1k-2k | | Mobile/Web | Any | 1k-2k | | VR/AR | Close | 2k-4k | --- ## Optimization Tips ### VRAM Management (Dream Textures) ```bash # Preferences → Dream Textures: - Precision: fp16 (default) - Speed: Half Precision - Memory: Attention Slicing (Auto) - Tile Size: 512 (for upscaling) ``` ### Batch Texture Generation ```python # Generate multiple variations for i, prompt in enumerate(prompts): bpy.ops.dream_texture.generate( prompt=prompt, seed=i * 1000, width=512, height=512, seamless=True ) # Save each img = bpy.data.images[-1] img.filepath_raw = f"//textures/tex_{i:03d}.png" img.save() ``` ### Naming Convention ``` {asset}_{map}_{resolution}.{ext} Examples: - wall_basecolor_2k.png - wall_roughness_2k.png - wall_normal_2k.exr - wall_height_2k.png - wall_metallic_2k.png - wall_ao_2k.png ``` --- ## Troubleshooting | Issue | Fix | |-------|-----| | Seams visible | Enable "Seamless" in Dream Textures; use Inpaint on edges | | Blurry texture | Increase resolution; check UV scale; use higher quality model | | Wrong colors | Check Color Space (sRGB vs Non-Color) on each map | | Normal map inverted | Flip Y in Normal Map node; check OpenGL vs DirectX | | Displacement not working | Material Settings → Displacement: "BOTH" or "Displacement Only" | | OOM | Reduce resolution; enable Attention Slicing; close other apps | --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [ANIMATION WORKFLOWS](animation.md), [BLENDER MATERIALS API REFERENCE](../api/materials.md), [BLENDER MESH OPERATIONS API REFERENCE](../api/mesh-operations.md), [BLENDER PYTHON API REFERENCE (4.x)](../api/blender-api.md), [BLENDER SCRIPTING PATTERNS - SAFE & EFFECTIVE](../api/scripting-patterns.md) - **По темам:** [BLENDER-MCP - FULL DOCUMENTATION](../addons/blender-mcp-full.md), [BLENDER-MCP ADDON - DEEP DIVE](../addons/blender-mcp.md), [CASE 03: PRODUCT VISUALIZATION](../../../cases/case_03_product_viz.md) - **Теги:** #reference, #texturing --- ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- # TROUBLESHOOTING GUIDE Tags: #addon #blender #mcp #reference # TROUBLESHOOTING GUIDE ## Blender Installation Issues ### MSI Install Fails (Exit Code 1620) ```powershell # Check install log Get-Content C:\BLENDER ai\install.log -Tail 50 # Common causes: # - Pending reboot required # - Another MSI running # - Insufficient permissions (run as Admin) # - Corrupt download # Fix: Re-download, run as Administrator Start-Process msiexec.exe -ArgumentList "/i C:\BLENDER ai\blender-4.2.3-windows-x64.msi /qb" -Verb RunAs -Wait ``` ### Blender Not Found After Install ```powershell # Check common locations Test-Path "C:\Program Files\Blender Foundation\Blender 4.2\blender.exe" Test-Path "C:\Program Files\Blender Foundation\Blender 4.1\blender.exe" Get-ChildItem "$env:LOCALAPPDATA\Programs\Blender Foundation\" -Recurse -Filter blender.exe Get-ChildItem "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\" -Recurse -Filter "*lender*" # Use portable version instead # Extract ZIP to C:\BLENDER ai\Blender\ and run blender.exe directly ``` --- ## uv / uvx Issues ### `uvx` Not Found / `spawn uvx ENOENT` ```powershell # 1. Restart PowerShell/terminal completely # 2. Verify PATH $env:PATH -split ";" | Where-Object { $_ -like "*\.local\bin*" } # 3. Add manually if missing $localBin = "$env:USERPROFILE\.local\bin" $userPath = [Environment]::GetEnvironmentVariable("Path", "User") [Environment]::SetEnvironmentVariable("Path", "$userPath;$localBin", "User") # 4. Use full path in MCP config where uvx # → C:\Users\\.local\bin\uvx.exe ``` ### Version Conflicts / Cache Issues ```bash uv cache clean blender-mcp uv cache clean --all uvx --refresh blender-mcp ``` ### Python Version Problems ```json // In MCP config, pin Python 3.11 { "args": ["--python", "3.11", "blender-mcp"], "env": { "UV_PYTHON_PREFERENCE": "only-managed" } } ``` --- ## MCP Connection Issues ### "Connection Refused" / "Cannot Connect to Blender" 1. **Blender addon not running:** - Open Blender → N-panel → **MCP for Blender** → Click **Start MCP Server** - Or for GenesisCore: N-panel → **GenesisCore** → Click **Connect** 2. **Wrong port:** - blender-mcp: port 9876 (configurable via `BLENDER_PORT`) - GenesisCore: port 45677 (internal) 3. **Firewall/Antivirus blocking:** - Allow `python.exe` / `blender.exe` through Windows Firewall - Check private network profile ### Timeout on Commands ```bash # Break complex requests into smaller steps # Instead of: "Build entire city with 50 buildings" # Do: "Create one building template" → "Array it 10x" → "Add variations" # Increase timeout in client config if needed ``` ### Tools Not Appearing in AI Client ```bash # 1. Restart AI client COMPLETELY (not just window) # - Windows: Quit from system tray # - macOS: Cmd+Q # 2. Check config syntax (valid JSON/TOML) # 3. Verify uvx works in terminal: uvx blender-mcp --help ``` --- ## GenesisCore Specific ### "Connecting..." Hangs Forever ```bash # 1. Check port 45677 not in use netstat -an | findstr 45677 # 2. Restart Blender completely # 3. Check System Console (Window → Toggle System Console) for errors # 4. Disable other addons that might conflict ``` ### Model List Empty / "Failed to Fetch Models" ```bash # 1. Verify API key is correct # 2. Check internet connection / proxy # 3. Try different provider (DeepSeek, OpenRouter, Ollama) # 4. For Ollama: ensure ollama serve running, model pulled (ollama pull deepseek-coder) ``` ### Tools Not Executing ```bash # 1. Enable tool modules in N-panel (Shift+click for multiple) # Required: Common Tools, Object Tools, Material Tools # Optional: Polyhaven Tools, Asset Tools # 2. Check System Console for Python errors # 3. Some tools require specific context (e.g., active object) ``` --- ## Dream Textures Issues ### Out of VRAM / CUDA OOM ```bash # Reduce resolution: 512x512 max for 4GB VRAM # Use fp16 (half precision) - default # Close other GPU apps # Enable Attention Slicing (Auto) in preferences # Reduce steps: 20-30 instead of 50 ``` ### Model Download Fails ```bash # 1. Check internet # 2. For gated models: set HF_TOKEN in preferences # 3. Try different model: stabilityai/stable-diffusion-2-1-base # 4. Manual download: huggingface.co → download → Import Checkpoint ``` ### macOS Quarantine (Not Windows) ```bash xattr -r -d com.apple.quarantine ~/Library/Application\ Support/Blender/3.3/scripts/addons/dream_textures/.python_dependencies ``` ### Slow Generation ```bash # Use lower steps (20-30) # Smaller resolution (512x512) # Scheduler: DPM++ 2M Karras (faster) # Enable Speed Optimizations: Half Precision ``` --- ## blender-mcp Specific ### Poly Pizza Download Fails (Cloudflare 403) ``` Error: "Poly Pizza's CDN returned a Cloudflare bot-protection challenge" Fix: 1. Download .glb manually from https://poly.pizza 2. File → Import → glTF 2.0 3. Or use residential IP (not VPN/datacenter) ``` ### Headless Blender (`blender -b`) Commands Don't Execute ``` blender-mcp requires Blender GUI for command execution. Use: blender (with GUI) or xvfb-run -a blender (Linux headless) ``` ### Safe Mode Blocks Script ``` Error: "Rejected by safe mode - ..." Fix: 1. Rewrite without: os, subprocess, open(), eval(), exec() 2. Use only: bpy, bmesh, mathutils, math, random, json, uuid 3. Or disable: BLENDER_MCP_SAFE_MODE=0 (default) ``` --- ## Asset Integration Issues ### Poly Haven Assets Not Appearing ```bash # 1. Enable "Use Poly Haven" in Blender N-panel → MCP for Blender # 2. Check cache: %TEMP%\polyhaven_models\ / polyhaven_hdris\ # 3. Clear cache and retry ``` ### Sketchfab Requires API Key ```bash # 1. Get key: https://sketchfab.com/developers/api # 2. Blender Preferences → Add-ons → MCP for Blender → Sketchfab API Key # 3. Or env: BLENDERMCP_SKETCHFAB_API_KEY ``` ### Hyper3D / Hunyuan3D Not Working ```bash # 1. Enable in N-panel # 2. Add API keys in Preferences # 3. Check credits/quota on provider dashboard # 4. Free tier may have limits ``` --- ## Performance Issues ### Blender Freezes / Unresponsive ```bash # 1. Check System Console for infinite loops # 2. Reduce object count in scene # 3. Disable heavy modifiers (Subsurf high levels) # 4. Use simpler geometry for AI generation ``` ### Large .blend Files ```bash # File → Clean Up → Unused Data Blocks # File → External Data → Automatically Pack Resources (or unpack) # Delete unused materials, textures, meshes # Use Linked libraries for repeated assets ``` --- ## Debugging Tools ### Blender System Console ``` Windows: Window → Toggle System Console macOS: /Applications/Blender.app/Contents/MacOS/Blender (in Terminal) Linux: Run blender from terminal ``` ### MCP Server Logs ```bash # blender-mcp: Runs in terminal where you started uvx # GenesisCore: Check Blender System Console ``` ### Enable Verbose Logging ```python # In Blender Python console: import logging logging.basicConfig(level=logging.DEBUG) ``` --- ## Common Error Messages | Error | Cause | Fix | |-------|-------|-----| | `KeyError: 'object_name'` | Object doesn't exist | Check spelling, use `bpy.data.objects.get()` | | `AttributeError: 'NoneType'` | Context is None (background mode) | Run Blender with GUI | | `RuntimeError: Operator not available` | Wrong mode (EDIT vs OBJECT) | `bpy.ops.object.mode_set(mode='OBJECT')` | | `MemoryError` | Out of RAM/VRAM | Reduce resolution, close apps | | `ModuleNotFoundError` | Missing Python package | uvx handles deps; check .python_dependencies | --- ## Getting Help 1. **Blender System Console** - First place to look 2. **GitHub Issues:** - GenesisCore: https://github.com/AIGODLIKE/GenesisCore/issues - blender-mcp: https://github.com/ahujasid/blender-mcp/issues - Dream Textures: https://github.com/carson-katri/dream-textures/issues 3. **Discord:** - blender-mcp: https://discord.gg/SNqPn4TcKQ - Dream Textures: https://discord.gg/EmDJ8CaWZ7 4. **Documentation:** This wiki + official docs --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [BLENDER-MCP ADDON - DEEP DIVE](../addons/blender-mcp.md), [INSTALLATION GUIDE - COMPLETE SETUP](installation.md), [BLENDER-MCP - FULL DOCUMENTATION](../addons/blender-mcp-full.md), [GENESISCORE ADDON - DEEP DIVE](../addons/genesiscore.md), [MCP ARCHITECTURE - MODEL CONTEXT PROTOCOL FOR BLENDER](../mcp/architecture.md) - **По темам:** [TEXT-TO-3D WORKFLOWS](../workflows/text-to-3d.md), [BLENDER MATERIALS API REFERENCE](../api/materials.md), [BLENDER MESH OPERATIONS API REFERENCE](../api/mesh-operations.md) - **Теги:** #addon, #blender, #mcp, #reference --- ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- # WORKFLOWS - PRODUCTION PIPELINES Tags: #ai-3d #animation #assets #automation #blender #geometry-nodes #mcp #modeling # WORKFLOWS - PRODUCTION PIPELINES ## Workflow 1: Text-to-3D Scene Generation ### Using GenesisCore (In-Blender Chat) ``` 1. Open Blender → N-panel → GenesisCore 2. Configure OpenRouter + Model (DeepSeek/Qwen) 3. Enable tools: Common, Object, Material, Polyhaven 4. Prompt sequence: "Create a cyberpunk alley scene: - Wet pavement plane 20x20m - Neon signs using emission materials (cyan, magenta, yellow) - Steam particles from grates - Hovering vehicle (simple primitive shapes) - Search Poly Haven for 'urban' HDRI at 2k, set as world - Camera: low angle looking down alley, 35mm lens" ``` ### Using blender-mcp (External AI) ``` OpenCode/Claude prompt: "Build a cyberpunk alley scene in Blender: 1. Create 20x20m plane for wet pavement at z=0 2. Add emission materials for neon signs (cyan, magenta, yellow) on cubes 3. Create particle system for steam on plane areas 4. Build simple hovering vehicle from primitives 5. Search Poly Haven for 'urban' HDRI, download 2k, set as world 6. Add camera at (-5, -10, 3) looking at (0,0,1), 35mm lens 7. Set render to Cycles, 256 samples, denoising on" ``` --- ## Workflow 2: Texture Generation Pipeline ### GenesisCore + Dream Textures Combo ``` Phase 1 - Geometry (GenesisCore): "Create a medieval castle wall section: - 10x5m stone wall with crenellations - Wooden door with iron hinges - Arrow slits as boolean cutouts - Stone floor tiles" Phase 2 - Materials (Dream Textures): 1. Select wall object → N-panel → Dream Textures 2. Generate: "weathered medieval stone wall, moss in cracks, seamless" → Check Seamless → Generate → Apply 3. Select door → "aged oak wood, iron studs, seamless" → Apply 4. Select floor → "cobblestone pathway, worn, seamless" → Apply Phase 3 - Polish (GenesisCore): "Add vertex paint for moss variation on wall Add geometry nodes for procedural ivy on top edges Set up 3-point lighting for texture showcase" ``` ### Pure Dream Textures (Texture Projection) ``` 1. Model base geometry (wall, floor, props) 2. Select all faces to texture 3. Enter Local View (/) 4. Dream Textures → Project Dream Texture 5. Prompt: "ancient dungeon corridor, stone bricks, torch lighting, moss" 6. Model: stabilityai/stable-diffusion-2-depth 7. Click Project Dream Texture 8. Adjusts UVs automatically ``` --- ## Workflow 3: Asset-Driven Scene Assembly ### Poly Haven + Poly Pizza + Sketchfab ``` "Create a cozy library scene using free assets: 1. Poly Haven: - Search models: 'bookshelf', 'wooden chair', 'desk lamp' - Search HDRI: 'library', 'indoor', 2k - Download all at 2k 2. Poly Pizza (low-poly fill): - Search: 'book', 'scroll', 'candle', 'potion bottle' - Licence: CC0 (no attribution) - Download, normalize_size=1.0, target_size=0.15 3. Sketchfab (hero pieces): - Search: 'ornate desk', 'grandfather clock' - Get preview, download at target_size=1.5m 4. Assembly: - Position bookshelves along walls - Scatter Poly Pizza books on shelves (random rotation) - Place hero desk center, chair beside - Add HDRI as world environment - Add area lights for desk lamp, candle glow - Camera: wide angle from doorway" ``` --- ## Workflow 4: AI 3D Generation + Cleanup ### Hyper3D Rodin / Hunyuan3D ``` "Generate a fantasy creature concept: 1. Create Hyper3D job: 'dragon whelp, cute, stylized, low poly' 2. Poll status every 30s until complete 3. Import result 4. Cleanup: - Decimate modifier (ratio 0.3) for game-ready - Separate loose parts (P → By Loose Parts) - Create materials for each part (scales, eyes, horns) - UV unwrap (Smart UV Project) - Bake normal map from high-poly (if needed) 5. Rig: Add armature, auto-weight, basic pose" ``` --- ## Workflow 5: Procedural Generation with Geometry Nodes ### City Block Generator ``` "Create Geometry Nodes setup for procedural city block: 1. Base grid: 10x10, 20m spacing 2. Building height: random 10-50m (seed controlled) 3. Building shape: extruded random polygons 4. Windows: array on faces, random offset 5. Roofs: flat/hipped/gabled (weighted random) 6. Street level: sidewalk, road markings, lamps 7. Variations: - Park lots (empty with trees) - Plaza (fountain + benches) - Alleyways (dumpsters, fire escapes) Controls exposed in modifier panel: - Seed, Block Size, Density, Height Range, Style Mix" ``` ### Blender Python for Geo Nodes ```python # Execute via execute_blender_code import bpy ng = bpy.data.node_groups.new("CityBlock", 'GeometryNodeTree') # ... build node tree programmatically ... # See api/blender-api.md for Geometry Nodes API ``` --- ## Workflow 6: Animation & Rigging ### Character Animation Pipeline ``` 1. Import/Generate character (Mixamo, Hyper3D, manual) 2. Rigify: - Add Rigify meta-rig (Human) - Align bones to mesh - Generate rig - Parent with automatic weights 3. Animation: - Import motion capture (BVH/FBX) - Retarget to Rigify - Clean up: NLA strips, action layers 4. Export: - FBX for game engines - GLTF for web - Alembic for simulation cache ``` ### Camera Animation ``` "Create cinematic camera path: 1. Add Bezier curve for dolly path 2. Add camera, Follow Path constraint 3. Add Empty for target, Track To constraint 4. Animate curve evaluation (0-1 over frames) 5. Add DOF: focus on target Empty, f-stop 1.8 6. Motion blur: Cycles → 0.5 shutter, 16 samples" ``` --- ## Workflow 7: Optimization for Real-Time ### Game-Ready Asset Pipeline ``` Input: High-poly model (Sketchfab/Hyper3D/manual) 1. Retopology: - Instant Meshes / Quad Remesher - Target: 1-5k tris (props), 10-30k (characters) 2. UV Unwrap: - Seams on hard edges - Pack islands (margin 0.005) - UDIM if >4k textures 3. Baking (Cycles): - Normal (Tangent Space, 4k) - AO (16 samples, 4k) - Curvature (4k) - Position (for masking) - Material ID 4. Texturing (Substance/Blender): - PBR metal/rough workflow - Smart materials + custom details 5. Export: - GLTF: separate textures, Draco compression - Validate: gltf-transform inspect - LODs: 100%, 50%, 25%, 10% poly count ``` --- ## Workflow 8: Batch Processing / Automation ### Render Farm Setup ``` # Blender CLI rendering blender -b scene.blend -E CYCLES -o //render/frame_#### -f 1..250 # Distributed (using Flamenco or custom) # 1. Split frames across machines # 2. Sync assets via shared storage # 3. Collect output ``` ### Python Batch Script Template ```python # Run: blender -b -P batch_script.py import bpy import sys def process_scene(scene_path, output_dir): bpy.ops.wm.open_mainfile(filepath=scene_path) scene = bpy.context.scene scene.render.filepath = output_dir scene.render.engine = 'CYCLES' scene.cycles.samples = 128 bpy.ops.render.render(animation=True, write_still=False) bpy.ops.wm.quit_blender() if __name__ == "__main__": # Args after -- argv = sys.argv[sys.argv.index("--")+1:] process_scene(argv[0], argv[1]) ``` --- ## Workflow 9: Version Control for .blend ### Git LFS Setup ```bash # .gitattributes *.blend filter=lfs diff=lfs merge=lfs -text *.png filter=lfs diff=lfs merge=lfs -text *.jpg filter=lfs diff=lfs merge=lfs -text *.hdr filter=lfs diff=lfs merge=lfs -text *.exr filter=lfs diff=lfs merge=lfs -text *.glb filter=lfs diff=lfs merge=lfs -text *.gltf filter=lfs diff=lfs merge=lfs -text # Initialize git lfs install git lfs track "*.blend" "*.png" "*.hdr" "*.exr" "*.glb" git add .gitattributes ``` ### Blend File Diff ```bash # Use blendfile-utils or custom script # For meaningful diffs, export to interchange format: blender -b scene.blend -P export_gltf.py # Then diff .gltf (JSON) ``` --- ## Decision Matrix: Which Tool When? | Task | Primary Tool | Fallback | |------|--------------|----------| | Quick geometry | GenesisCore chat | blender-mcp | | Complex scene logic | blender-mcp + Codex/Claude | GenesisCore | | Texture generation | Dream Textures | Poly Haven textures | | Hero assets | Sketchfab / Hyper3D | Manual modeling | | Background props | Poly Pizza | Poly Haven models | | HDRI lighting | Poly Haven (both) | Poly Haven | | Code automation | execute_blender_code | Geometry Nodes | | Game assets | Dream Textures + Retopo | Substance Painter | | Animation | Rigify + NLA | Manual keyframes | | Batch render | CLI + Flamenco | Manual | --- ## Tips for Reliable Automation 1. **Always verify before acting:** `get_scene_info` → plan → execute 2. **Use named objects:** Explicit names > "the cube I just made" 3. **Check mode:** Ensure `OBJECT` mode before transforms 4. **Apply scale:** `Ctrl+A → Scale` before bevel/array/modifiers 5. **Unique data:** `.copy()` meshes when duplicating 6. **Save incrementally:** `bpy.ops.wm.save_as_mainfile(filepath="scene_v001.blend")` 7. **Use collections:** Organize by type (Geometry, Lights, Cameras, FX) --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [TEXT-TO-3D WORKFLOWS](text-to-3d.md), [BLENDER MESH OPERATIONS API REFERENCE](../api/mesh-operations.md), [BLENDER-MCP - FULL DOCUMENTATION](../addons/blender-mcp-full.md), [BLENDER-MCP ADDON - DEEP DIVE](../addons/blender-mcp.md), [INSTALLATION GUIDE - COMPLETE SETUP](../guides/installation.md) - **По темам:** [P18: DRIVERS & CONSTRAINTS](../../../prompts/18_drivers_constraints.md), [P45: GEOMETRY NODES SIMULATION ZONES](../../../prompts/45_gn_simulation.md), [SETUP 35: READY PLAYER ME AVATARS](../../../setups/setup_35_readyplayerme.md), [SETUP 45: HUNYUAN3D 2.1 LOCAL](../../../setups/setup_45_hunyuan3d_21.md) - **Теги:** #ai-3d, #animation, #assets, #automation, #blender, #geometry-nodes, #mcp, #modeling --- --- ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- # SECURITY & SAFE MODE Tags: #security # SECURITY & SAFE MODE **MCP/Blender security best practices and safe mode configuration** --- ## 🎯 THREAT MODEL | Threat Vector | Risk | Mitigation | |---------------|------|------------| | **Code Injection** | Arbitrary Python execution | Safe Mode, AST validation | | **File System Access** | Read/write/delete files | Path allowlists, sandbox | | **Network Access** | Data exfiltration, C2 | Network policies, allowlists | | **Process Execution** | Command injection | Subprocess blocking | | **Data Exfiltration** | Scene data, textures | Output controls, audit logs | | **Privilege Escalation** | Blender runs as user | User isolation, containers | | **Supply Chain** | Malicious addons | Signed addons, verification | --- ## 🛡️ BLENDER-MCP SAFE MODE ### Server-Side Safe Mode ```bash # Enable safe mode BLENDER_MCP_SAFE_MODE=1 uvx blender-mcp # Or in config export BLENDER_MCP_SAFE_MODE=1 uvx blender-mcp ``` ### What Safe Mode Blocks ```python # Server-side validation (blender-mcp/src/safe_mode.py) FORBIDDEN_MODULES = { 'os', 'sys', 'subprocess', 'socket', 'urllib', 'requests', 'importlib', 'pkgutil', 'runpy', 'zipimport', 'threading', 'multiprocessing', 'asyncio', 'ctypes', 'cffi', 'crypt', 'hashlib', 'secrets', 'tempfile', 'shutil', 'glob', 'fnmatch', 'linecache', 'pickle', 'shelve', 'dbm', 'sqlite3', 'csv', 'configparser', 'tomllib', 'json', 'xml', 'yaml', } FORBIDDEN_BUILTINS = { 'eval', 'exec', 'compile', 'open', '__import__', 'getattr', 'setattr', 'delattr', 'globals', 'locals', 'vars', 'dir', 'help', 'breakpoint', } FORBIDDEN_BPY = { 'bpy.ops.wm.open_mainfile', 'bpy.ops.wm.save_as_mainfile', 'bpy.ops.wm.read_factory_settings', 'bpy.ops.script.python_file_run', 'bpy.ops.text.run_script', 'bpy.app.handlers', 'bpy.app.timers', 'bpy.app.driver_namespace', 'bpy.utils.register_class', 'bpy.utils.unregister_class', 'bpy.types', 'bpy.data.libraries.load', 'bpy.data.texts.new', 'bpy.data.texts.remove', } ``` ### Safe Mode Validator ```python # blender-mcp/src/safe_mode.py import ast class SafeModeValidator: @classmethod def validate_code(cls, code: str) -> tuple[bool, str]: try: tree = ast.parse(code) except SyntaxError as e: return False, f"Syntax error: {e}" for node in ast.walk(tree): # Check imports if isinstance(node, (ast.Import, ast.ImportFrom)): for alias in node.names: if alias.name.split('.')[0] in cls.FORBIDDEN_MODULES: return False, f"Forbidden import: {alias.name}" # Check builtin calls if isinstance(node, ast.Call): if isinstance(node.func, ast.Name): if node.func.id in cls.FORBIDDEN_BUILTINS: return False, f"Forbidden builtin: {node.func.id}" elif isinstance(node.func, ast.Attribute): full_name = ast.unparse(node.func) if any(full_name.startswith(f) for f in cls.FORBIDDEN_BPY): return False, f"Forbidden bpy: {full_name}" # Check attribute access if isinstance(node, ast.Attribute): if isinstance(node.value, ast.Name): if node.value.id == 'bpy': attr_path = f"bpy.{node.attr}" if any(attr_path.startswith(f) for f in cls.FORBIDDEN_BPY): return False, f"Forbidden bpy access: {attr_path}" return True, "OK" ``` ### Safe Mode Configuration ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["blender-mcp"], "env": { "BLENDER_MCP_SAFE_MODE": "1", "BLENDER_MCP_ALLOWED_MODULES": "bpy,bmesh,mathutils,math,random,json,uuid", "BLENDER_MCP_MAX_EXECUTION_TIME": "30", "BLENDER_MCP_MAX_MEMORY_MB": "1024" } } } } ``` --- ## 🔒 CLIENT-SIDE PERMISSIONS ### Permission Model ```python class PermissionManager: PERMISSIONS = { "read_scene": [ "get_scene_info", "get_object_info", "get_viewport_screenshot" ], "modify_scene": [ "create_object", "modify_object", "delete_object" ], "materials": [ "set_material", "create_material" ], "assets": [ "download_polyhaven_asset", "search_polyhaven_assets" ], "ai_generation": [ "create_rodin_job", "poll_rodin_job_status" ], "code_execution": [ "execute_blender_code" ], "admin": [ "get_addon_info", "set_telemetry_consent" ], } def __init__(self, allowed_roles: list[str]): self.allowed = set() for role in allowed_roles: self.allowed.update(self.PERMISSIONS.get(role, [])) def check(self, tool_name: str) -> bool: return tool_name in self.allowed def filter_tools(self, all_tools: list) -> list: return [t for t in all_tools if t["name"] in self.allowed] # Usage in OpenCode config opencode_config = { "mcp": { "blender-mcp": { "type": "local", "command": ["uvx", "blender-mcp"], "enabled": True, "environment": { "BLENDER_MCP_SAFE_MODE": "1", "BLENDER_MCP_ROLE": "read_scene,modify_scene,materials" } } } } ``` --- ## 🔐 NETWORK SECURITY ### Network Policies ```python # Network Policy for Blender-MCP class NetworkPolicy: def __init__(self): self.allowed_domains = { 'polyhaven.com', # Asset downloads 'api.poly.pizza', # Poly Pizza API 'api.sketchfab.com', # Sketchfab API 'api.hyper3d.ai', # Hyper3D 'api.meshy.ai', # Meshy } self.blocked_ports = {22, 23, 3389, 4444, 5432, 3306, 6379} self.allowed_ports = {80, 443} def check_url(self, url: str) -> bool: from urllib.parse import urlparse parsed = urlparse(url) # Check domain if parsed.netloc not in self.allowed_domains: return False # Check port port = parsed.port or (443 if parsed.scheme == 'https' else 80) if port not in self.allowed_ports: return False # Block private IPs import ipaddress try: ip = ipaddress.ip_address(parsed.hostname) if ip.is_private or ip.is_loopback: return False except ValueError: pass # Hostname, not IP return True # Blocked: localhost, private IPs, non-standard ports # Allowed: HTTPS to approved domains only ``` ### API Key Management ```python # Secure API Key Storage import keyring import os class APIKeyManager: SERVICE = "blender-mcp" @staticmethod def store_key(service: str, key: str): keyring.set_password(APIKeyManager.SERVICE, service, key) @staticmethod def get_key(service: str) -> str: return keyring.get_password(APIKeyManager.SERVICE, service) @staticmethod def delete_key(service: str): keyring.delete_password(APIKeyManager.SERVICE, service) # Environment variable fallback def get_api_key(service: str) -> str: # Priority: keyring > env var > config file key = keyring.get_password("blender-mcp", service) if key: return key env_var = f"BLENDER_MCP_{service.upper()}_API_KEY" return os.getenv(env_var, "") ``` --- ## 📋 AUDIT LOGGING ### Structured Audit Logging ```python import structlog import json from datetime import datetime class AuditLogger: def __init__(self, log_file="audit.log"): self.logger = structlog.get_logger("audit") handler = logging.FileHandler(log_file) handler.setFormatter(logging.Formatter('%(message)s')) self.logger.addHandler(handler) def log_tool_call(self, tool_name: str, params: dict, user: str, result: dict): self.logger.info("tool_call", timestamp=datetime.now().isoformat(), user=user, tool=tool_name, params=self._sanitize_params(params), success="error" not in result, error=result.get("error") if "error" in result else None ) def log_file_access(self, user: str, path: str, operation: str): self.logger.info("file_access", timestamp=datetime.now().isoformat(), user=user, path=path, operation=operation ) def log_network_request(self, user: str, url: str, method: str): self.logger.info("network_request", timestamp=datetime.now().isoformat(), user=user, url=url, method=method ) def _sanitize_params(self, params: dict) -> dict: """Remove sensitive data from logs""" sanitized = {} sensitive_keys = {'api_key', 'password', 'token', 'secret', 'key'} for k, v in params.items(): if any(s in k.lower() for s in ['key', 'password', 'token', 'secret']): sanitized[k] = "***REDACTED***" else: sanitized[k] = v return sanitized ``` ### Log Analysis ```bash # Find failed tool calls jq 'select(.success == false) | {tool, error, timestamp}' audit.log # Find network requests jq 'select(.event == "network_request") | {url, method, timestamp}' audit.log # Find file writes jq 'select(.event == "file_access" and .operation == "write") | {path, user, timestamp}' audit.log # Tool usage statistics jq -r '.tool' audit.log | sort | uniq -c | sort -rn ``` --- ## 🐳 CONTAINER SECURITY ### Blender in Docker (Secure) ```dockerfile # Dockerfile.blender-secure FROM nvidia/cuda:12.1-devel-ubuntu22.04 # Non-root user RUN useradd -m -u 1000 -s /bin/bash blender USER blender # Install Blender RUN apt-get update && apt-get install -y \ blender \ xvfb \ python3-pip \ && rm -rf /var/lib/apt/lists/* # Install Python deps as user USER blender RUN pip install --user \ bpy-sdk \ pynvml \ psutil # Working directory WORKDIR /home/blender/work # Entry point ENTRYPOINT ["xvfb-run", "-a", "blender", "-b"] CMD ["--help"] ``` ### Docker Compose (Secure) ```yaml # docker-compose.secure.yml version: '3.8' services: blender-mcp: build: . user: "1000:1000" security_opt: - no-new-privileges:true read_only: true tmpfs: - /tmp - /home/blender/work cap_drop: - ALL network_mode: "none" # No network by default volumes: - ./scenes:/home/blender/work/scenes:ro - ./output:/home/blender/work/output:rw environment: - BLENDER_MCP_SAFE_MODE=1 deploy: resources: limits: cpus: '2' memory: 4G reservations: memory: 2G # Network access only for asset download blender-mcp-network: extends: blender-mcp network_mode: "bridge" environment: - BLENDER_MCP_ALLOWED_DOMAINS=polyhaven.com,api.poly.pizza ``` --- ## 🔍 VULNERABILITY SCANNING ### Dependency Scanning ```bash # Scan Python dependencies pip install safety bandit safety check -r requirements.txt bandit -r scripts/ -f json -o bandit-report.json # Scan Docker image docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ aquasec/trivy image blender-mcp:latest # Scan Blender addons python -m py_compile scripts/*.py ``` ### Runtime Monitoring ```python # Runtime security monitor import threading import time class SecurityMonitor: def __init__(self): self.alerts = [] self.running = False def start(self): self.running = True self.thread = threading.Thread(target=self._monitor, daemon=True) self.thread.start() def _monitor(self): while self.running: # Check for suspicious activity self.check_process_tree() self.check_file_access() self.check_network_connections() time.sleep(5) def check_process_tree(self): import psutil current = psutil.Process() children = current.children(recursive=True) for child in children: if child.name() not in ['blender', 'python', 'xvfb-run']: self.alert("Unexpected child process", { 'pid': child.pid, 'name': child.name(), 'cmdline': child.cmdline() }) def alert(self, message, details): alert = { 'timestamp': datetime.now().isoformat(), 'level': 'WARNING', 'message': message, 'details': details } self.alerts.append(alert) # Send to SIEM/webhook ``` --- ## 📋 SECURITY CHECKLIST ### Pre-Deployment - [ ] Safe mode enabled (`BLENDER_MCP_SAFE_MODE=1`) - [ ] Network policies configured - [ ] API keys in keyring (not env vars) - [ ] Audit logging enabled - [ ] Container running as non-root - [ ] Read-only filesystem where possible - [ ] Resource limits set (CPU, RAM) - [ ] Dependency scanning passed - [ ] Container image scanned ### Runtime Monitoring - [ ] Audit logs being collected - [ ] Alerting on anomalies - [ ] Resource usage monitored - [ ] Network traffic logged - [ ] File access audited ### Incident Response - [ ] Incident response plan documented - [ ] Rollback procedure tested - [ ] Forensic evidence preservation - [ ] Communication plan ready --- ## 🔗 LINKS - **Blender-MCP Security:** https://github.com/ahujasid/blender-mcp/blob/main/src/safe_mode.py - **OCIO Security:** https://opencolorio.org/ - **Docker Security:** https://docs.docker.com/engine/security/ - **Python Security:** https://python-security.readthedocs.io/ - **OWASP Python:** https://owasp.org/www-project-python-security/ --- ## 🌍 MULTILANG | Language | File | |----------|------| | English | `multilang/en/security/safe_mode.md` | | Russian | `multilang/ru/security/safe_mode.md` | --- # SIMULATION & PHYSICS: GEOMETRY NODES 4.0+ Tags: #geometry-nodes #modeling #simulation # SIMULATION & PHYSICS: GEOMETRY NODES 4.0+ **Advanced simulation workflows in Blender 4.0+ Geometry Nodes** --- ## 🎯 SIMULATION ZONE (Blender 4.0+) ### Core Concepts ``` ┌─────────────────────────────────────────────────────────────┐ │ SIMULATION ZONE │ ├─────────────────────────────────────────────────────────────┤ │ Inputs (from outside) State (persisted per frame) │ │ ┌─────────────────┐ ┌─────────────────────────┐ │ │ │ Geometry Input │ │ position (Vector) │ │ │ │ Parameters │ │ velocity (Vector) │ │ │ │ Time/Frame │ │ age (Float) │ │ │ └─────────────────┘ │ custom attributes... │ │ │ └─────────────────────────┘ │ │ ┌─────────────────────┐ │ │ │ Simulation Body │ │ │ │ (runs per frame) │ │ │ │ │ │ │ │ 1. Read State │ │ │ │ 2. Compute Forces │ │ │ │ 3. Update Velocity │ │ │ │ 4. Update Position │ │ │ │ 5. Handle Collisions│ │ │ │ 6. Write State │ │ │ └─────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────┐ │ │ │ Output Geometry │ │ │ │ (next frame) │ │ │ └─────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` --- ## 🔧 SIMULATION NODES REFERENCE ### Simulation Zone Setup ```python # In Geometry Nodes Editor: # 1. Add "Simulation Zone" node (Shift+A → Simulation → Simulation Zone) # 2. Connect input geometry to "Geometry" input # 3. Inside zone: Build simulation logic # 4. Connect output geometry to "Geometry" output ``` ### State Attributes (Auto-created) ```python # Built-in state attributes (auto-managed): # - position (Vector) - particle/object position # - velocity (Vector) - current velocity # - angular_velocity (Vector) - rotation speed # - age (Float) - simulation age in frames # - lifetime (Float) - when to die (0 = infinite) # - id (Integer) - unique particle ID # - mass (Float) - for physics calculations # - drag (Float) - air resistance factor ``` ### Simulation Nodes Reference ```python # === INPUT NODES === # Simulation Input: Access external geometry/parameters # Simulation Output: Write results back # Frame: Current frame number # Delta Time: Time per frame (1/FPS) # === STATE NODES === # Store Named Attribute: Create/update state # Named Attribute: Read state # Remove Named Attribute: Cleanup # === PHYSICS NODES === # Accumulate Field: Add forces (gravity, wind, noise) # Drag: Velocity damping # Gravity: Constant acceleration # Wind: Directional force with noise # === COLLISION NODES === # Collision: Detect collisions with geometry # Response: Bounce, slide, stick, die # Proximity: Distance-based interactions # === PARTICLE NODES === # Emit Particles: Spawn new particles # Kill Particles: Remove by condition # Resample Particles: Maintain count # === UTILITY NODES === # Index Switch: Branch by index # Menu Switch: Branch by enum # Compare: Logic operations # Math/Vector Math: Calculations # Random Value: Noise/randomness # Attribute Statistic: Min/Max/Mean ``` --- ## 🌊 PARTICLE SYSTEMS ### Basic Particle System ```python # GN Tree: Basic_Particles # 1. Input: Emitter geometry (plane, sphere, etc.) # 2. Simulation Zone: # a. Emit Particles: Rate=100, Velocity=(0,0,5), Spread=0.5 # b. Accumulate Field: Gravity (0,0,-9.8) # c. Drag: Factor=0.01 # d. Kill Particles: Age > 100 # e. Output: Instance on Points → Collection "Particle_Prefabs" # 3. Output Geometry # Advanced: Particle Attributes """ Attribute: age (Float) - frame age Attribute: lifetime (Float) - max frames Attribute: size (Float) - per-particle scale Attribute: color (Color) - per-particle color Attribute: rotation (Euler/Quat) - orientation Attribute: velocity (Vector) - for motion blur """ ``` ### Particle Forces ```python # Gravity + Wind + Turbulence """ Accumulate Field (Gravity): Strength: -9.8 Direction: (0, 0, -1) Accumulate Field (Wind): Strength: 5.0 Direction: (1, 0, 0) Noise: 0.5 Accumulate Field (Turbulence): Strength: 2.0 Noise Scale: 5.0 Noise Detail: 4 # Combine forces Vector Math: Add (Gravity + Wind + Turbulence) → Total Force Vector Math: Multiply (Force × Delta Time) → Velocity Change Vector Math: Add (Velocity + Velocity Change) → New Velocity """ # Force Fields from Objects """ Object Info (Force Field Object) → Position Vector Math: Subtract (Particle Pos - Field Pos) → Direction Math: Power (1/Distance²) → Falloff Vector Math: Scale (Direction × Falloff × Strength) → Force """ ``` ### Particle Collision ```python # Collision with Geometry """ Collision Node: Target: Collision Geometry Mode: "Outside" / "Inside" / "Both" Response Options: - Die: Kill particle - Bounce: Reflect velocity × restitution - Slide: Project velocity to surface tangent - Stick: Velocity = 0, parent to surface Advanced: - Friction: Reduce tangential velocity - Restitution: Bounciness (0-1) - Thickness: Offset collision surface """ ``` --- ## 🧱 RIGID BODY SIMULATION (GN) ### Rigid Body in GN (Blender 4.0+) ```python # Rigid Body Simulation via GN """ 1. Input: Collection of rigid bodies (convex hulls) 2. Simulation Zone: a. Broad Phase: BVH broadphase collision detection b. Narrow Phase: GJK/EPA collision detection c. Constraint Solver: Sequential impulse solver d. Integration: Semi-implicit Euler 3. Output: Transformed instances Nodes Used: - Simulation Input (rigid body collection) - Rigid Body Properties (mass, inertia, friction, restitution) - Rigid Body Constraints (hinge, slider, fixed, spring) - Collision Detection (broad + narrow phase) - Constraint Solver (iterations=10, tolerance=0.001) - Integration (velocity + position update) """ # Rigid Body Properties Node """ Properties per body: - Mass (0 = static/infinite) - Inertia Tensor (3x3 matrix, auto from mesh) - Friction (0-1) - Restitution/Bounciness (0-1) - Linear Damping (air resistance) - Angular Damping (air resistance) - Sleep Threshold (auto-sleep when slow) - Collision Shape: Convex Hull / Mesh / Box / Sphere / Capsule """ ``` ### Constraint System ```python # Constraint Types """ Fixed: Weld two bodies together Hinge: Rotation around single axis Slider: Translation along single axis Cone Twist: Limited rotation (ragdoll joints) Spring: Soft constraint with stiffness/damping Motor: Drive to target velocity/position """ ``` --- ## 🧵 CLOTH & SOFT BODY (GN) ### Cloth Simulation ```python # Cloth Simulation in GN """ Cloth Properties: - Bending Stiffness: Resistance to bending - Stretch Stiffness: Resistance to stretching - Shear Stiffness: Resistance to shearing - Damping: Velocity damping - Pressure: Volume preservation (for balloons) - Self Collision: Check against self - Vertex Groups: Pinning, stiffness maps Solver: Projective Dynamics (XPBD) - Iterations: 4-8 (more = stiffer) - Time Step: Fixed substeps for stability """ # Cloth Pinning """ Vertex Group "Pin": Weight 1.0 = fully pinned Vertex Group "Stiffness": Per-vertex stiffness multiplier """ ### Soft Body ``` ```python # Soft Body (Tetrahedral) """ Tetrahedralize input mesh → Tetrahedra Properties: - Young's Modulus: Stiffness - Poisson Ratio: Volume preservation - Damping: Velocity decay - Plasticity: Permanent deformation threshold - Fracture: Break threshold Solver: FEM (Finite Element Method) or XPBD """ ``` --- ## 💧 FLUID & SMOKE (GN + Native) ### FLIP Fluid (Geometry Nodes) ```python # FLIP Fluid in GN (Experimental) """ Grid-Based FLIP: 1. Particles carry velocity/mass 2. Transfer to grid (PIC/FLIP blend) 3. Pressure projection (Poisson solve) 4. Velocity extrapolation 4. Transfer back to particles 5. Surface reconstruction (Marching Cubes) Nodes: - Particle to Grid (PIC/FLIP ratio) - Grid Operations (Diffusion, Advection, Pressure) - Pressure Solve (Multigrid/Conjugate Gradient) - Grid to Particle - Surface Reconstruction (Dual Contouring) """ # Quick Fluid (Native Blender) """ Quick Fluid Setup: 1. Domain: Cube → Quick Fluid → Domain 2. Flow: Object → Quick Fluid → Flow 3. Effector: Object → Quick Fluid → Effector 4. Cache: Bake to disk """ ``` ### Smoke/Fire (Native + GN) ```python # Smoke Simulation """ Smoke Domain Settings: - Resolution: 64-256 (powers of 2) - Time Scale: 1.0 - Vorticity: 2.0 (turbulence) - Dissolve: 0.01 (smoke decay) Flow Settings: - Type: Smoke / Fire / Fire+Smoke - Fuel: 1.0 - Temperature: 1.0 (fire) / 0.0 (smoke) - Density: 1.0 GN Integration: - Cache smoke to VDB/VDB sequence - GN: Volume to Mesh (Marching Cubes) - GN: Instance on Volume Points """ ``` --- ## 🌊 ADVANCED TECHNIQUES ### Substeps & Stability ```python # Fixed Substeps for Stability """ Substeps: 4-8 per frame (more = stable, slower) Time Step: 1/FPS / Substeps Adaptive Substepping: - Check max velocity - If velocity × dt > cell_size: increase substeps - Max substeps: 16 """ ### Collision Optimization ``` ```python # Broad Phase """ BVH Tree: Build once per frame AABB Test: Fast rejection Pairs: Only test overlapping AABBs # Narrow Phase GJK: Distance between convex shapes EPA: Penetration depth/direction Contact Generation: Manifold (up to 4 contacts) """ ### GPU Acceleration (Future/Experimental) ``` ```python # Compute Shaders for Physics """ Blender 4.2+ Compute Shader Support: - Particle systems on GPU - Cloth/Soft body on GPU - Fluid pressure solve on GPU - Broad phase on GPU Requirements: - Blender 4.2+ - Vulkan/Metal/OpenCL support - Experimental features enabled """ ``` --- ## 🎨 CREATIVE EXAMPLES ### Rain Simulation ```python # Rain with Splash """ 1. Emitter: Plane above scene 2. Particles: Rain drops (stretched spheres) 3. Forces: Gravity + Wind + Turbulence 4. Collision: Ground plane → Splash particles 5. Splash: Secondary emitter at collision points 5. Render: Motion blur + Depth of Field """ ``` ### Destruction (Fracture + RBD) ```python # Procedural Fracture + RBD """ 1. Input Mesh → Voronoi Fracture (GN) → Fractured Pieces 2. Assign Rigid Body Properties (mass from volume) 3. Impulse Force at Impact Point 4. RBD Simulation 5. Dust/Debris Particles from fracture edges 5. Fracture Sound (by impact energy) """ ``` ### Cloth on Character ```python # Cloth on Rigged Character """ 1. Character Mesh → Vertex Groups for pinning 2. Cloth Mesh (shirt/pants) → Shrinkwrap to body (bind) 3. Cloth Sim: Pin groups (shoulders, waist, collar) 4. Collision: Body mesh (thickened) 5. Wind/Turbulence for movement 6. Cache → Alembic → Engine """ ``` --- ## 🔧 PERFORMANCE TIPS | Technique | Speedup | Trade-off | |-----------|---------|-----------| | **Instance on Points** | 100x vs objects | All instances identical | | **Point Cache (Alembic/USD)** | Playback only | No runtime sim | | **Decimate LOD** | 10x viewport | Lower detail far | | **Billboard Impostors** | 100x far | No 3D rotation | | **GPU Compute** | 10-50x | Experimental | | **Fixed Substeps** | Stable | Predictable cost | --- ## 🔗 LINKS - **GN Simulation Docs:** https://docs.blender.org/manual/en/latest/modeling/geometry_nodes/simulation/ - **XPBD Paper:** https://matthias-research.github.io/pages/publications/XPBD.pdf - **Blender Physics:** https://docs.blender.org/manual/en/latest/physics/ - **XPBD in GN:** https://developer.blender.org/ (ищите в Projects → Physics/Simulation) > [!NOTE] > Ссылка на задачу T112345 убрана — такой тикет не существует в публичном трекере Blender. Используйте поиск на developer.blender.org по ключевым словам «XPBD» и «Geometry Nodes». --- ## 🌍 MULTILANG | Language | File | |----------|------| | English | `multilang/en/simulation/rigid_body_gn.md` | | Russian | `multilang/ru/simulation/rigid_body_gn.md` | --- # USD / MATERIALX / OPENUSD PIPELINE Tags: #pipeline #usd # USD / MATERIALX / OPENUSD PIPELINE **OpenUSD pipeline for Blender → USD → Houdini/Maya/Katana → Render** --- ## 🎯 USD IN BLENDER ### Blender USD Support (4.0+) ``` Blender USD Features: ├── Export: Full scene, selection, collections ├── Import: Full scene, specific prims, variants ├── Materials: Principled BSDF → MaterialX / UsdPreviewSurface ├── Animation: Full animation, shape keys, constraints ├── Cameras: Full camera data, DOF, stereo ├── Lights: All light types, IES profiles ├── Volumes: VDB, OpenVDB, custom volumes ├── Instancing: Native USD instancing, point instancer ├── Variants: Variant sets, variant selection ├── Composition: References, payloads, sublayers ├── Custom Data: Custom properties, custom attributes └── Hydra: Viewport rendering via Hydra delegates ``` --- ## ⚙️ USD EXPORT ### Basic Export ```python import bpy from pathlib import Path def export_usd( filepath: str, selection_only: bool = False, export_materials: bool = True, export_uvmaps: bool = True, export_animation: bool = True, export_frame_range: bool = True, export_frame_step: int = 1, export_uvmaps: bool = True, export_normals: bool = True, export_colors: bool = True, export_animation: bool = True, export_skinning: bool = True, export_uvmaps: bool = True, export_display_color: bool = True, export_instancing: bool = True, export_visibility: bool = True, export_custom_properties: bool = True, export_custom_attributes: bool = True, export_materials: bool = True, export_uvmaps: bool = True, export_animation: bool = True, export_frame_range: bool = True, export_frame_step: int = 1, # MaterialX material_x: bool = True, shading_mode: str = 'USE_MATERIAL_X', # 'USE_MATERIAL_X', 'USE_PREVIEW', 'USE_LOOKDEV' # Purpose purpose: str = 'default', # 'default', 'render', 'proxy', 'guide' # Instancing export_instancing: bool = True, # Visibility export_visibility: bool = True, # Custom Properties export_custom_properties: bool = True, export_custom_attributes: bool = True, ): """Export USD with full options""" Path(filepath).parent.mkdir(parents=True, exist_ok=True) bpy.ops.wm.usd_export( filepath=filepath, selection_only=selection_only, export_materials=export_materials, export_uvmaps=export_uvmaps, export_animation=export_animation, export_frame_range=export_frame_range, export_frame_step=export_frame_step, export_uvmaps=export_uvmaps, export_normals=export_normals, export_colors=export_colors, export_animation=export_animation, export_skinning=True, export_uvmaps=True, export_display_color=True, export_instancing=export_instancing, export_visibility=export_visibility, export_custom_properties=export_custom_properties, export_custom_attributes=export_custom_attributes, export_materials=export_materials, export_uvmaps=export_uvmaps, export_animation=export_animation, export_frame_range=export_frame_range, export_frame_step=export_frame_step, # MaterialX material_x=material_x, shading_mode=shading_mode, # Purpose purpose=purpose, export_instancing=export_instancing, export_visibility=export_visibility, export_custom_properties=export_custom_properties, export_custom_attributes=export_custom_attributes, ) # Usage export_usd("/project/assets/hero/hero_v001.usda", selection_only=True) ``` ### Export with Variants ```python def export_usd_with_variants(filepath, variant_sets=None): """ variant_sets = { "modeling": ["high", "low", "proxy"], "material": ["clean", "damaged", "rusty"], "lod": ["LOD0", "LOD1", "LOD2", "LOD3"] } """ # Create variant sets in USD # This requires USD Python API (pxr) from pxr import Usd, UsdGeom, Sdf stage = Usd.Stage.CreateNew(filepath) # Define variant sets if variant_sets: for set_name, variants in variant_sets.items(): variant_set = stage.GetRootPrim().GetVariantSets().AddVariantSet(set_name) for variant in variants: variant_set.AddVariant(variant) variant_set.SetVariantSelection(variant) with variant_set.GetVariantEditContext(): # Add variant-specific content pass stage.GetRootLayer().Save() # Usage export_usd_with_variants("/assets/hero/hero.usda", { "modeling": ["high", "low", "proxy"], "material": ["clean", "dirty", "wet"], "lod": ["LOD0", "LOD1", "LOD2", "LOD3"] }) ``` --- ## 🎨 MATERIALX IN BLENDER ### MaterialX Export ```python def export_materialx(material_name, filepath): """Export Blender material as MaterialX""" mat = bpy.data.materials[material_name] if not mat.use_nodes: raise ValueError("Material must use nodes") # Ensure MaterialX output mat.node_tree.nodes['Material Output'].target = 'MATERIALX' # Export via USD with MaterialX bpy.ops.wm.usd_export( filepath=f"{filepath}.usda", material_x=True, shading_mode='USE_MATERIAL_X', selection_only=True, ) # MaterialX Standard Surface def create_materialx_standard_surface(mat_name): """Create MaterialX-compatible standard surface""" mat = bpy.data.materials.new(name=mat_name) mat.use_nodes = True nodes = mat.node_tree.nodes links = mat.node_tree.links nodes.clear() # Standard Surface (MaterialX compatible) standard = nodes.new('ShaderNodeBsdfPrincipled') standard.location = (0, 0) # MaterialX outputs output = nodes.new('ShaderNodeOutputMaterial') output.location = (400, 0) output.target = 'MATERIALX' # Connect mat.node_tree.links.new(standard.outputs['BSDF'], output.inputs['Surface']) # MaterialX specific: Use standard surface parameters # base_color, specular_color, specular_roughness, metallic, etc. return mat ``` ### MaterialX Node Reference ```python # MaterialX Nodes in Blender (4.2+) # ----------------------------------- # Standard Surface: mx_standard_surface # Normal Map: mx_normal_map # Displacement: mx_displacement # Image: mx_image # Color Correct: mx_color_correct # Mix: mx_mix # Switch: mx_switch # Condition: mx_condition # Transform: mx_transform # Position: mx_position # Normal: mx_normal # Geometric Properties: mx_geometric_properties # Example: MaterialX Network def create_materialx_network(): mat = bpy.data.materials.new("MX_Standard") mat.use_nodes = True nodes = mat.node_tree.nodes links = mat.node_tree.links nodes.clear() # MaterialX Standard Surface standard = nodes.new('ShaderNodeBsdfPrincipled') standard.location = (0, 0) # Note: Blender's Principled maps to MX standard_surface # Image Texture tex = nodes.new('ShaderNodeTexImage') tex.location = (-400, 0) # Color Correct cc = nodes.new('ShaderNodeColorBalance') cc.location = (-200, 0) cc.correction_method = 'LIFT_GAMMA_GAIN' # Output output = nodes.new('ShaderNodeOutputMaterial') output.location = (200, 0) output.target = 'MATERIALX' # Links links.new(tex.outputs['Color'], cc.inputs['Color']) links.new(cc.outputs['Color'], standard.inputs['Base Color']) links.new(standard.outputs['BSDF'], output.inputs['Surface']) return mat ``` --- ## 🏗️ USD PIPELINE ### Asset Publishing ```python # usd_pipeline.py """ USD Pipeline: Blender → USD → Houdini/Maya/Katana → Render """ class USDPipeline: def __init__(self, project_root: str): self.project_root = Path(project_root) self.usd_dir = self.project_root / "usd" self.usd_dir.mkdir(exist_ok=True) def publish_asset(self, asset_name: str, version: str, asset_type: str): """Publish asset as USD with versioning""" # Versioned path: /assets/props/crate/v003/crate_v003.usda version_dir = self.usd_dir / "assets" / asset_type / asset_name / f"v{version}" version_dir.mkdir(parents=True, exist_ok=True) usd_path = version_dir / f"{asset_name}_v{version}.usda" # Export current selection self.export_usd_with_materialx(str(version_dir / f"{asset_name}_v{version}.usda")) # Create symlink to latest latest_link = self.usd_dir / "assets" / asset_type / asset_name / "latest.usda" if latest_link.exists(): latest_link.unlink() latest_link.symlink_to(usd_path.relative_to(latest_link.parent)) return str(usd_path) def assemble_shot(self, shot_name: str, layout_data: list): """Assemble shot from published assets""" shot_dir = self.usd_dir / "shots" / shot_name shot_dir.mkdir(parents=True, exist_ok=True) shot_usd = shot_dir / f"{shot_name}.usda" self.assemble_shot(shot_name, layout_data, str(shot_usd)) return str(shot_usd) def assemble_shot(self, shot_name: str, layout_data: list, output_path: str): """Assemble shot from published assets""" import pxr.Usd as Usd import pxr.UsdGeom as UsdGeom import pxr.Sdf as Sdf stage = Usd.Stage.CreateNew(output_path) stage.SetDefaultPrim(stage.DefinePrim(f"/{shot_name}", "Scope")) stage.SetDefaultPrim(stage.GetPrimAtPath(f"/{shot_name}")) # Add assets as references for i, asset in enumerate(layout_data): prim_path = f"/{shot_name}/assets/{asset['name']}_{i:03d}" prim = stage.DefinePrim(prim_path, "Xform") prim.GetReferences().AddReference(asset['usd_path']) # Set transform import pxr.UsdGeom as UsdGeom xform = UsdGeom.Xformable(prim) xform.AddTranslateOp().Set(asset.get('location', (0,0,0))) xform.AddRotateXYZOp().Set(asset.get('rotation', (0,0,0))) xform.AddScaleOp().Set(asset.get('scale', (1,1,1))) stage.GetRootLayer().Save() print(f"USD scene assembled: {output_path}") # Usage pipeline = USDPipeline("/project") pipeline.publish_asset("hero", "003", "characters") pipeline.assemble_shot("sh0010", [ {"name": "hero", "usd_path": "/project/usd/assets/characters/hero/v003/hero_v003.usda", "location": (0, 0, 0), "rotation": (0, 0, 0), "scale": (1, 1, 1)}, {"name": "environment", "usd_path": "/project/usd/assets/environments/city/v001/city_v001.usda", "location": (0, 0, 0), "rotation": (0, 0, 0), "scale": (1, 1, 1)}, ]) ``` --- ## 🎨 MATERIALX WORKFLOWS ### MaterialX in Blender → Houdini ```python # Blender → MaterialX → Houdini """ 1. Blender: Create MaterialX-compatible shader 2. Export USD with material_x=True 3. Houdini: USD Import → MaterialX Shader Builder 4. Houdini: Assign to geometry """ # Houdini Python (for reference) """ # In Houdini Python Shell: import hou # Load USD node = hou.node('/stage').createNode('usdimport', 'import_asset') node.parm('filepath1').set('/assets/hero/hero_v003.usda') # MaterialX will be imported as MaterialX Shader # Can edit in MaterialX Shader Builder """ ``` ### MaterialX → Arnold / RenderMan / Karma ```python # MaterialX → Arnold """ Arnold 7+ supports MaterialX natively: 1. Export .mtlx from Blender 2. Arnold: MaterialX Shader → Load .mtlx 4. Assign to geometry """ # MaterialX → RenderMan """ RenderMan 25+: 1. PxrMaterialX node 5. Load .mtlx file 6. Connect to PxrSurface """ # MaterialX → Karma (Houdini) """ Karma (Houdini's render engine): 1. Native MaterialX support 2. MaterialX Shader Builder 3. Live editing in viewport """ ``` --- ## 🎬 USD SCENE ASSEMBLY ### Shot Assembly ```python def assemble_shot_usd(shot_name, layout_data, output_path): """Assemble shot from asset references""" from pxr import Usd, UsdGeom, Sdf, Gf stage = Usd.Stage.CreateNew(output_path) stage.SetDefaultPrim(stage.DefinePrim(f"/{shot_name}", "Scope")) stage.SetDefaultPrim(stage.GetPrimAtPath(f"/{shot_name}")) # Add assets as references for i, asset in enumerate(layout_data): prim_path = f"/{shot_name}/assets/{asset['name']}_{i:03d}" prim = stage.DefinePrim(prim_path, "Xform") prim.GetReferences().AddReference(asset['usd_path']) # Set transform xform = UsdGeom.Xformable(prim) xform.AddTranslateOp().Set(Gf.Vec3d(*asset.get('location', (0,0,0)))) xform.AddRotateXYZOp().Set(Gf.Vec3f(*asset.get('rotation', (0,0,0)))) xform.AddScaleOp().Set(Gf.Vec3f(*asset.get('scale', (1,1,1)))) # Add lights for light_data in layout_data.get('lights', []): light_path = f"/{shot_name}/lights/{light_data['name']}" light_prim = stage.DefinePrim(light_path, "RectLight") light_prim.GetAttribute("intensity").Set(light_data['intensity']) light_prim.GetAttribute("color").Set(Gf.Vec3f(*light_data['color'])) light_prim.GetAttribute("width").Set(light_data.get('width', 1.0)) light_prim.GetAttribute("height").Set(light_data.get('height', 1.0)) # Add camera cam_data = layout_data.get('camera', {}) if cam_data: cam_path = f"/{shot_name}/camera" cam_prim = stage.DefinePrim(cam_path, "Camera") cam_prim.GetAttribute("focalLength").Set(cam_data.get('focal_length', 50)) cam_prim.GetAttribute("horizontalAperture").Set(cam_data.get('aperture', 36)) cam_prim.GetAttribute('focusDistance').Set(cam_data.get('focus_distance', 10)) cam_prim.GetAttribute('fStop').Set(cam_data.get('fstop', 2.8)) # Save stage.GetRootLayer().Save() print(f"Shot USD assembled: {output_path}") # Usage assemble_shot_usd("sh0010", { "assets": [ {"name": "hero", "usd_path": "/assets/hero/hero_v003.usda", "location": (0, 0, 0), "rotation": (0, 0, 0), "scale": (1, 1, 1)}, {"name": "environment", "usd_path": "/assets/env/city_v001.usda", "location": (0, 0, 0), "rotation": (0, 0, 0), "scale": (1, 1, 1)}, ], "lights": [ {"name": "key_light", "intensity": 5000, "color": (1.0, 0.95, 0.9), "width": 2, "height": 2}, {"name": "fill_light", "intensity": 1500, "color": (0.9, 0.95, 1.0), "width": 4, "height": 4}, ], "camera": {"focal_length": 35, "aperture": 36, "focus_distance": 5, "fstop": 2.8} }, "/project/shots/sh0010/sh0010.usda") ``` --- ## 🔧 USD PYTHON API (PXR) ### Common Operations ```python from pxr import Usd, UsdGeom, UsdShade, Sdf, Gf, Vt # Open Stage stage = Usd.Stage.Open("/path/to/scene.usda") # Traverse for prim in stage.Traverse(): print(prim.GetPath(), prim.GetTypeName()) # Find Prims prim = stage.GetPrimAtPath("/path/to/prim") # Attributes attr = prim.GetAttribute("attr_name") value = attr.Get() attr.Set(new_value) # Relationships rel = prim.GetRelationship("rel_name") targets = rel.GetTargets() rel.SetTargets([Sdf.Path("/new/target")]) # Variants variant_set = prim.GetVariantSets().AddVariantSet("modeling") variant_set.AddVariant("high") variant_set.SetVariantSelection("high") with variant_set.GetVariantEditContext(): # Edit high-res variant pass # Composition stage.GetRootLayer().subLayerPaths.append("./layer.usda") # Save stage.GetRootLayer().Save() ``` --- ## 🔗 LINKS - **OpenUSD:** https://openusd.org/ - **MaterialX:** https://www.materialx.org/ - **USD Python API:** https://openusd.org/release/api.html - **USD in Blender:** https://docs.blender.org/manual/en/latest/addons/import_export/scene_usd.html - **Hydra Delegates:** https://github.com/kitware/hydra > [!NOTE] > - USD Python API: если ссылка не работает, используйте поиск на openusd.org → Documentation → API > - Kitware Hydra репозиторий может быть в нижнем регистре: github.com/kitware/hydra --- ## 🌍 MULTILANG | Language | File | |----------|------| | English | `multilang/en/usd/materialx.md` | | Russian | `multilang/ru/usd/materialx.md` | --- # P01: CREACIÓN DE PRIMITIVAS — BASE Tags: #basics #bpy #bpy.ops #geometry #primitives # P01: CREACIÓN DE PRIMITIVAS — BASE **ID:** P01 | **Clúster:** Geometry Basics | **Dificultad:** ⭐ | **Tags:** #geometry #primitives #basics #bpy.ops --- ## 🎯 OBJETIVO Crear primitivas básicas vía MCP con control preciso de parámetros --- ## 📝 PROMPT ``` Crea una primitiva [TYPE] llamada "[NAME]" en posición [X, Y, Z], escala [SX, SY, SZ], rotación [RX, RY, RZ] (en radianes). Tipos: CUBE, SPHERE, CYLINDER, PLANE, CONE, TORUS, EMPTY, CAMERA, LIGHT Ejemplo: "Crea un cubo 'Box_01' en (2, 0, 1) escala (1, 2, 1) rotación (0, 0, 0.785)" ``` --- ## 🔧 MCP TOOL: `create_object` ```json { "type": "create_object", "params": { "entity_type": "CUBE", "name": "Box_01", "location": [2.0, 0.0, 1.0], "rotation": [0.0, 0.0, 0.785], "scale": [1.0, 2.0, 1.0] } } ``` --- ## 📊 PARÁMETROS POR TIPO | Tipo | Obligatorios | Opcionales | |-----|--------------|--------------| | CUBE | size | location, rotation, scale | | SPHERE | radius | segments, ring_count, location, rotation, scale | | CYLINDER | radius, depth | vertices, cap_ends, location, rotation, scale | | PLANE | size | location, rotation, scale | | CONE | radius1, radius2, depth | vertices, location, rotation, scale | | TORUS | major_radius, minor_radius | major_segments, minor_segments, location, rotation, scale | | EMPTY | — | location, rotation, scale, type | | CAMERA | — | location, rotation, lens, sensor_width | | LIGHT | — | location, rotation, type, energy, color | --- ## ⚠️ ERRORES FRECUENTES | Error | Causa | Solución | |--------|---------|---------| | `Object not found` | Nombre ocupado | Usa nombres únicos o verifica `get_scene_info` | | `Invalid rotation` | Grados en vez de radianes | Convierte: `radians = degrees * π / 180` | | `Primitive not created` | Contexto (EDIT mode) | Quédate siempre en OBJECT mode | --- ## 🔗 ENLACES - **Casos:** [Case 02: Game Props](../cases/case_02_game_props.md) - **Flujos:** [WF 01: Text-to-Scene](../workflows/wf_01_text_to_scene.md) - **Siguiente:** [P02: Boolean Operations](./02_boolean_operations.md) --- # P02: OPERACIONES BOOLEANAS Tags: #boolean #difference #intersect #modeling #modifiers #union # P02: OPERACIONES BOOLEANAS **ID:** P02 | **Clúster:** Geometry Basics | **Dificultad:** ⭐⭐ | **Tags:** #boolean #modeling #modifiers #difference #union #intersect --- ## 🎯 OBJETIVO Operaciones booleanas (Difference, Union, Intersect) entre objetos vía MCP --- ## 📝 PROMPT ``` Ejecuta la operación booleana [OPERATION] entre "[OBJECT_A]" (base) y "[OBJECT_B]" (herramienta). Operaciones: DIFFERENCE (restar), UNION (unir), INTERSECT (intersección). Resultado: el objeto A se modifica, el objeto B puede eliminarse. ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy # Configuración obj_a = bpy.data.objects.get("Wall") obj_b = bpy.data.objects.get("Window_Cutout") if not obj_a or not obj_b: raise ValueError("Objects not found") # Asegurar OBJECT mode if bpy.context.mode != 'OBJECT': bpy.ops.object.mode_set(mode='OBJECT') # Seleccionar objetos bpy.ops.object.select_all(action='DESELECT') obj_a.select_set(True) obj_b.select_set(True) bpy.context.view_layer.objects.active = obj_a # Añadir modificador Boolean bool_mod = obj_a.modifiers.new(name="Bool_Window", type='BOOLEAN') bool_mod.operation = 'DIFFERENCE' # 'UNION', 'INTERSECT' bool_mod.object = obj_b bool_mod.solver = 'EXACT' # 'FAST' para geometrías simples # Aplicar bpy.ops.object.modifier_apply(modifier="Bool_Window") # Eliminar herramienta (opcional) bpy.data.objects.remove(obj_b, do_unlink=True) print(f"Boolean DIFFERENCE applied. {obj_a.name} modified.") ``` --- ## 📊 OPERACIONES | Operación | Resultado visual | Caso de uso | |----------|---------------------|----------| | **DIFFERENCE** | A menos B | Ventanas, puertas, agujeros, ranuras | | **UNION** | A más B | Unir piezas, fusionar mallas | | **INTERSECT** | Solo intersección | Máscaras, recorte de formas | --- ## 💡 PATRONES AVANZADOS ### Booleanos múltiples (a la vez) ```python # Varias restas en una pasada cutters = [bpy.data.objects.get(f"Cutter_{i}") for i in range(5)] for cutter in cutters: mod = wall.modifiers.new(f"Bool_{cutter.name}", 'BOOLEAN') mod.operation = 'DIFFERENCE' mod.object = cutter mod.solver = 'EXACT' # Aplicar todos for mod in list(wall.modifiers): if mod.type == 'BOOLEAN': bpy.context.view_layer.objects.active = wall bpy.ops.object.modifier_apply(modifier=mod.name) # Limpieza for c in cutters: bpy.data.objects.remove(c, do_unlink=True) ``` ### Cuidado con las normales ```python # Verificar y corregir normales antes del booleano bpy.context.view_layer.objects.active = obj_a bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.normals_make_consistent(inside=False) bpy.ops.object.mode_set(mode='OBJECT') ``` --- ## ⚠️ PROBLEMAS FRECUENTES | Problema | Causa | Solución | |--------|---------|---------| | Resultado vacío | Normales dentro/fuera | `Mesh > Normals > Recalculate Outside` | | Artefactos | Aristas no-manifold | `Select > Select All by Trait > Non-Manifold` → corregir | | Lento en mallas complejas | Muchos polígonos | Usa `DECIMATE` antes, o solver `FAST` | | El objeto desaparece | Intersección vacía | Verifica la posición de los objetos | --- ## 🔗 ENLACES - **Casos:** [Case 01: ArchViz Interior](../cases/case_01_archviz_interior.md) — ventanas/puertas en paredes - **Flujos:** [WF 01: Text-to-Scene](../workflows/wf_01_text_to_scene.md) - **Prompts relacionados:** [P01 Primitives](./01_geometry_basics.md), [P03 Modifiers Stack](./03_modifiers_stack.md) - **Siguiente:** [P03: Modifiers Stack](./03_modifiers_stack.md) --- # P09: CONFIGURACIÓN PRINCIPLED BSDF Tags: #materials #pbr #principled-bshf #shader-nodes # P09: CONFIGURACIÓN PRINCIPLED BSDF **ID:** P09 | **Clúster:** Materials PBR | **PROMPT:** ⭐ | **Tags:** #materials #pbr #principled-bshf #shader-nodes --- ## 🎯 OBJETIVO Crear y configurar un material Principled BSDF con el set PBR completo --- ## 📝 PROMPT ``` Crea el material "[MAT_NAME]" tipo Principled BSDF: Base Color: [R, G, B] o hex #RRGGBB Roughness: [0.0-1.0] Metallic: [0.0-1.0] [Opcional] Transmission, IOR, Clearcoat, Sheen, Emission Asígnaselo al objeto "[OBJECT_NAME]" ``` --- ## 🔧 MCP: `set_material` (básico) ```json { "type": "set_material", "params": { "object_name": "Cube", "material_name": "Mat_Red", "color": [1.0, 0.0, 0.0, 1.0] } } ``` --- ## 🔧 MCP: `execute_blender_code` (PBR completo) ```python import bpy from pathlib import Path def create_pbr_material(name, maps_dict, obj_name=None): """ maps_dict keys: 'base_color', 'roughness', 'metallic', 'normal', 'height', 'ao', 'emission' values: filepath o tupla Color (R,G,B,A) """ mat = bpy.data.materials.new(name) mat.use_nodes = True nodes = mat.node_tree.nodes links = mat.node_tree.links nodes.clear() # Principled BSDF principled = nodes.new('ShaderNodeBsdfPrincipled') principled.location = (0, 0) # Material Output output = nodes.new('ShaderNodeOutputMaterial') output.location = (400, 0) links.new(principled.outputs['BSDF'], output.inputs['Surface']) # Displacement disp = nodes.new('ShaderNodeDisplacement') disp.location = (200, -200) links.new(disp.outputs['Displacement'], output.inputs['Displacement']) mat.displacement_method = 'BOTH' # Texture Coordinate + Mapping tex_coord = nodes.new('ShaderNodeTexCoord') tex_coord.location = (-600, 0) mapping = nodes.new('ShaderNodeMapping') mapping.location = (-400, 0) links.new(tex_coord.outputs['UV'], mapping.inputs['Vector']) def add_texture(key, input_name, colorspace='sRGB', use_normal_map=False, use_disp=False): if key not in maps_dict: return val = maps_dict[key] if isinstance(val, (tuple, list)): # Color principled.inputs[input_name].default_value = val + (1.0,) if len(val)==3 else val return # Archivo de imagen tex = nodes.new('ShaderNodeTexImage') tex.image = bpy.data.images.load(str(val)) tex.image.colorspace_settings.name = colorspace tex.location = (-200, 0) links.new(mapping.outputs['Vector'], tex.inputs['Vector']) if use_normal_map: nmap = nodes.new('ShaderNodeNormalMap') nmap.location = (0, -100) links.new(tex.outputs['Color'], nmap.inputs['Color']) links.new(nmap.outputs['Normal'], principled.inputs[input_name]) elif use_disp: links.new(tex.outputs['Color'], disp.inputs['Height']) else: links.new(tex.outputs['Color'], principled.inputs[input_name]) # Conectar mapas add_texture('base_color', 'Base Color', 'sRGB') add_texture('roughness', 'Roughness', 'Non-Color') add_texture('metallic', 'Metallic', 'Non-Color') add_texture('normal', 'Normal', 'Non-Color', use_normal_map=True) add_texture('height', 'Height', 'Non-Color', use_disp=True) add_texture('emission', 'Emission Color', 'sRGB') # AO -> Mix con Base Color if 'ao' in maps_dict: ao_tex = nodes.new('ShaderNodeTexImage') ao_tex.image = bpy.data.images.load(str(maps_dict['ao'])) ao_tex.image.colorspace_settings.name = 'Non-Color' ao_tex.location = (-200, 200) links.new(mapping.outputs['Vector'], ao_tex.inputs['Vector']) mix = nodes.new('ShaderNodeMixRGB') mix.blend_type = 'MULTIPLY' mix.location = (0, 100) # Reconectar Base Color vía Mix if principled.inputs['Base Color'].links: from_node = principled.inputs['Base Color'].links[0].from_node from_socket = principled.inputs['Base Color'].links[0].from_socket links.new(from_socket, mix.inputs[1]) links.new(ao_tex.outputs['Color'], mix.inputs[2]) links.new(mix.outputs['Color'], principled.inputs['Base Color']) # Asignar al objeto if obj_name: obj = bpy.data.objects.get(obj_name) if obj: if obj.data.materials: obj.data.materials[0] = mat else: obj.data.materials.append(mat) return mat # === EJEMPLOS DE USO === # 1. Color simple create_pbr_material("Mat_Red", { 'base_color': (1.0, 0.2, 0.2, 1.0), 'roughness': 0.5, 'metallic': 0.0 }, "Cube") # 2. Oro create_pbr_material("Mat_Gold", { 'base_color': (1.0, 0.843, 0.0, 1.0), 'roughness': 0.1, 'metallic': 1.0 }, "Ring") # 3. Cristal create_pbr_material("Mat_Glass", { 'base_color': (1.0, 1.0, 1.0, 1.0), 'roughness': 0.0, 'transmission': 1.0, # añadir a la función si se necesita 'ior': 1.45 }, "Glass_Obj") # 4. PBR completo desde archivos create_pbr_material("Mat_Brick", { 'base_color': Path("//textures/brick_basecolor.png"), 'roughness': Path("//textures/brick_roughness.png"), 'normal': Path("//textures/brick_normal.png"), 'height': Path("//textures/brick_height.png"), 'ao': Path("//textures/brick_ao.png"), }, "Wall") ``` --- ## 📊 CHULETA DE PARÁMETROS PBR | Material | Base Color | Roughness | Metallic | Transmission | IOR | Clearcoat | |----------|------------|-----------|----------|--------------|-----|-----------| | **Plástico** | Color | 0.3-0.5 | 0.0 | 0.0 | 1.5 | 0.0 | | **Metal (pulido)** | Color del metal | 0.05-0.15 | 1.0 | 0.0 | — | 0.0 | | **Metal (oxidado/mate)** | Color del metal | 0.6-0.9 | 1.0 | 0.0 | — | 0.0 | | **Cristal** | Blanco | 0.0 | 0.0 | 1.0 | 1.45-1.5 | 0.0 | | **Agua** | Azul claro | 0.05 | 0.0 | 1.0 | 1.33 | 0.0 | | **Piel** | Color piel | 0.6-0.8 | 0.0 | 0.0 | 1.5 | 0.1-0.3 | | **Tela** | Color tela | 0.8-1.0 | 0.0 | 0.0 | 1.5 | 0.0 | | **Cerámica** | Color | 0.1-0.3 | 0.0 | 0.0 | 1.5 | 0.2-0.5 | | **Piedra/Hormigón** | Gris/beige | 0.7-1.0 | 0.0 | 0.0 | 1.5 | 0.0 | | **Madera** | Madera | 0.6-0.9 | 0.0 | 0.0 | 1.5 | 0.0 | --- ## 🎨 EFECTOS ESPECIALES ### Emission (Brillo) ```python principled.inputs['Emission Color'].default_value = (1.0, 0.8, 0.2, 1.0) # Luz cálida principled.inputs['Emission Strength'].default_value = 5.0 # Intensidad ``` ### Clearcoat (Barniz/Cerámica) ```python principled.inputs['Coat Weight'].default_value = 1.0 principled.inputs['Coat Roughness'].default_value = 0.03 ``` ### Sheen (Tela/Terciopelo) ```python principled.inputs['Sheen Weight'].default_value = 0.5 principled.inputs['Sheen Roughness'].default_value = 0.3 principled.inputs['Sheen Tint'].default_value = 0.5 ``` ### Subsurface (Piel, cera, mármol) ```python principled.inputs['Subsurface Weight'].default_value = 0.3 principled.inputs['Subsurface Radius'].default_value = (1.0, 0.2, 0.1) # Radio RGB de penetración principled.inputs['Subsurface Color'].default_value = (1.0, 0.3, 0.1, 1.0) ``` ### Anisotropic (Superficies estriadas) ```python principled.inputs['Anisotropic'].default_value = 0.8 principled.inputs['Anisotropic Rotation'].default_value = 0.25 # 90° # Se necesita entrada Tangent (UV o Geometry > Tangent) ``` --- ## 🔗 СВЯЗИ - **Casos:** [Case 01: ArchViz](../cases/case_01_archviz_interior.md), [Case 03: Product Viz](../cases/case_03_product_viz.md) - **Flujos:** [WF 03: Texture Pipeline](../workflows/wf_03_texture_pipeline.md) - **Prompts:** [P10 Node Groups](./10_node_groups.md), [P11 Procedural Textures](./11_procedural_textures.md) - **MCP Tool:** `set_material` (básico), `execute_blender_code` (completo) - **Siguiente:** [P10: Node Group Creation](./10_node_groups.md) --- # CONFIGURACIÓN 01: BLENDER-MCP BÁSICO # CONFIGURACIÓN 01: BLENDER-MCP BÁSICO **ID:** SETUP-01 | **Tipo:** Infraestructura central | **Dificultad:** ⭐ | **Tiempo:** 10-15 min --- ## 🎯 OBJETIVO Instalar y configurar blender-mcp para trabajar con agentes IDE (OpenCode, Cursor, VS Code, Codex, Claude Desktop) --- ## 📋 REQUISITOS PREVIOS | Componente | Versión | Estado | |-----------|--------|--------| | **Blender** | 4.0+ | ✅ Obligatorio | | **Python** | 3.10+ | ✅ Incluido en Blender | | **uv** | latest | ✅ Obligatorio | | **Git** | latest | ⚠️ Opcional | --- ## 🪟 INSTALACIÓN EN WINDOWS (PASO A PASO) ### 1. Instalar uv ```powershell # PowerShell como administrador irm https://astral.sh/uv/install.ps1 | iex # Reiniciar la terminal # Verificar uv --version uvx --version ``` ### 2. Instalar el addon blender-mcp ```powershell # Instalación automática en Blender uvx blender-mcp install-addon # Verificar la ruta uvx blender-mcp addon-paths # Debe mostrar: C:\Users\\AppData\Roaming\Blender Foundation\Blender\4.x\scripts\addons\blender_mcp.py ``` ### 3. Configurar PATH (si no se encuentra uvx) ```powershell # Encontrar uvx where uvx # Normalmente: C:\Users\\.local\bin\uvx.exe # Añadir al PATH de usuario $localBin = "$env:USERPROFILE\.local\bin" $userPath = [Environment]::GetEnvironmentVariable("Path", "User") [Environment]::SetEnvironmentVariable("Path", "$userPath;$localBin", "User") # Reiniciar terminal/IDE ``` --- ## 🔧 CONFIGURACIÓN DE BLENDER ### 1. Activar el addon ``` Edit → Preferences → Add-ons Buscar: "MCP for Blender" ☑ Interface: MCP for Blender ``` ### 2. Iniciar el servidor MCP ``` Panel N (tecla N) → pestaña "MCP for Blender" ☑ Poly Haven ☑ Poly Pizza (pegar la API key) ☐ Sketchfab (opcional) ☐ Hyper3D / Hunyuan3D (opcional) 🟢 Botón: "Start MCP Server" ``` ### 3. Verificar en la consola del sistema ``` Window → Toggle System Console Debe aparecer: "BlenderMCP server started on localhost:9876" ``` --- ## 🤖 CONFIGURACIÓN DE AGENTES IDE ### OpenCode (`~/.config/opencode/opencode.jsonc`) ```jsonc { "mcp": { "blender-mcp": { "type": "local", "command": ["uvx", "blender-mcp"], "enabled": true, "environment": { "BLENDER_HOST": "localhost", "BLENDER_PORT": "9876" } } } } ``` ### Cursor (Settings → MCP → Add Server) ```json { "mcpServers": { "blender": { "command": "cmd", "args": ["/c", "uvx", "blender-mcp"] } } } ``` ### VS Code (extensión MCP) ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["blender-mcp"] } } } ``` ### Codex ```bash codex mcp add blender -- uvx blender-mcp ``` ### Claude Desktop (`claude_desktop_config.json`) ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["blender-mcp"] } } } ``` --- ## ✅ PRUEBA DE CONEXIÓN ### En OpenCode / terminal: ```bash # Prueba simple "Obtén información de la escena actual" ``` ### Respuesta esperada: ```json { "name": "Scene", "object_count": 2, "objects": [ {"name": "Cube", "type": "MESH", "location": [0, 0, 0]}, {"name": "Light", "type": "LIGHT", "location": [0, 0, 5]} ], "materials_count": 1 } ``` ### Prueba de creación de objeto: ```bash "Crea un cubo 'TestCube' en la posición (2, 0, 0) con escala (1, 2, 1)" ``` --- ## 🔑 CLAVES API (OPCIONAL) | Servicio | Dónde obtenerla | Dónde pegarla | |--------|--------------|---------------| | **Poly Pizza** | https://poly.pizza/settings/api | Blender Preferences → Add-ons → MCP for Blender → Poly Pizza API Key | | **Sketchfab** | https://sketchfab.com/developers/api | Blender Preferences → Add-ons → MCP for Blender → Sketchfab API Key | | **Hyper3D** | https://hyper3d.ai/ | Blender Preferences → Add-ons → MCP for Blender → Hyper3D API Key | | **Hunyuan3D** | https://3d.hunyuan.tencent.com/ | Blender Preferences → Add-ons → MCP for Blender → SecretId/SecretKey | --- ## ⚠️ PROBLEMAS FRECUENTES | Problema | Solución | |----------|---------| | `spawn uvx ENOENT` | Añadir `~/.local/bin` al PATH, reiniciar el IDE | | `Connection refused` | Blender no está en marcha o el servidor MCP no se inició (N-panel → Start) | | `Timeout` | Solicitud demasiado compleja → dividir en pasos | | `Poly Pizza 403` | Bloqueo Cloudflare → descargar .glb manualmente → File → Import → glTF | | `ModuleNotFoundError: mcp` | Ejecutar Blender como admin una vez para autoinstalar dependencias | --- ## 🔗 ENLACES - **Prompts:** [P01-P05](../prompts/) — comandos básicos - **Casos:** [Case 02: Game Props](../cases/case_02_game_props.md) - **Flujos:** [WF 01: Text-to-Scene](../workflows/wf_01_text_to_scene.md) - **Siguiente:** [Setup 02: GenesisCore Local](../setups/setup_02_genesiscore_local.md) --- # CONFIGURACIÓN 02: GENESISCORE LOCAL (OLLAMA) # CONFIGURACIÓN 02: GENESISCORE LOCAL (OLLAMA) **ID:** SETUP-02 | **Tipo:** Integración LLM local | **Dificultad:** ⭐⭐ | **Tiempo:** 15-30 min --- ## 🎯 OBJETIVO Configurar GenesisCore para trabajar con LLM locales vía Ollama (privacidad, gratis, sin conexión) --- ## 📋 REQUISITOS PREVIOS | Componente | Versión | Estado | |-----------|--------|--------| | **Blender** | 4.0+ | ✅ Obligatorio | | **Ollama** | latest | ✅ Obligatorio | | **RAM** | 16GB+ | 🟡 Recomendado | | **VRAM** | 8GB+ | 🟡 Para modelos 7B+ | --- ## 🪟 INSTALACIÓN EN WINDOWS ### 1. Instalar Ollama ```powershell # Descargar e instalar winget install Ollama.Ollama # O desde el sitio oficial # https://ollama.com/download/windows # Comprobar ollama --version ollama list ``` ### 2. Descargar modelos para coding/Blender ```powershell # Mejores modelos para Blender Python (por prioridad) ollama pull codellama:13b # El mejor para código, 7.3GB ollama pull deepseek-coder:6.7b # Excelente para Python, 3.8GB ollama pull qwen2.5-coder:7b # Nuevo, rápido, 4.7GB ollama pull llama3.2:3b # Ligero, rápido, 2.0GB # Comprobar ollama list ``` ### 3. Iniciar el servidor Ollama ```powershell # Debe ejecutarse en segundo plano (autostart al instalar) # Comprobar: curl http://localhost:11434/api/tags # Si no funciona: ollama serve ``` --- ## 🔧 CONFIGURACIÓN DE GENESISCORE ### 1. Instalar el addon (si aún no) ```bash # En Blender: Edit → Preferences → Add-ons → Install from ZIP # Elegir: GenesisCore-main.zip # Activar: GenesisCore ``` ### 2. Configurar el proveedor ``` N-Panel → pestaña GenesisCore: 1. Provider: ▼ Ollama 2. Base URL: http://localhost:11434/v1 (¡importante: /v1 al final!) 3. API Key: (dejar vacío para Ollama) 4. Pulsar "Fetch Models" 4. Elegir modelo: codellama:13b / deepseek-coder:6.7b / qwen2.5-coder:7b 5. Tool Modules: activar todos (Shift+Click) 6. Save Config 7. Connect ``` ### 3. Ajustes del modelo (recomendados) ```json { "provider": "Ollama", "base_url": "http://localhost:11434/v1", "model": "codellama:13b", "temperature": 0.1, "top_p": 0.9, "max_tokens": 4096, "use_history": true } ``` --- ## 🔧 OPTIMIZACIÓN DEL RENDIMIENTO ### GPU Offloading (NVIDIA) ```powershell # Ollama usa la GPU automáticamente si hay CUDA # Comprobar: ollama run codellama:13b "test" # Debe mostrar: "using GPU" en los logs # Forzar GPU layers: # En variables de entorno: OLLAMA_NUM_GPU=999 OLLAMA_GPU_LAYERS=999 ``` ### Optimización de memoria ```yaml # En ajustes de GenesisCore: use_history: false # Desactivar historial para ahorrar tokens max_tokens: 2048 # Limitar la longitud de respuesta temperature: 0.1 # Respuestas deterministas ``` ### Cuantización (si hay poca VRAM) ```powershell # Usar versiones cuantizadas: ollama pull codellama:13b-q4_K_M # 4-bit, 7.3GB → 4.7GB ollama pull deepseek-coder:6.7b-q4_K_M # 3.8GB → 2.5GB ``` --- ## 🧪 PRUEBAS ### Prueba rápida en el chat de GenesisCore ``` "Crea un cubo 'TestCube' en la posición (1, 2, 3) con material rojo" ``` ### Resultado esperado 1. El agente analiza la petición 2. Llama a `create_object` + `set_material` 3. En Blender aparece un cubo en (1, 2, 3) con material rojo --- ## 🔧 SOLUCIÓN DE PROBLEMAS | Problema | Solución | |----------|---------| | `Connection refused` | Ollama no está en marcha → `ollama serve` | | `Model not found` | `ollama pull ` | | `Out of memory` | Usar un modelo menor (3b/7b) o cuantización q4 | | `Slow response` | Activar GPU offloading, reducir `max_tokens` | | `Base URL error` | Verificar `http://localhost:11434/v1` (¡con /v1!) | --- ## 🔗 ENLACES - **Modelos Ollama:** https://ollama.com/library - **Repo GenesisCore:** https://github.com/AIGODLIKE/GenesisCore - **Setup 01:** [blender-mcp Basic](../setups/setup_01_blender_mcp_basic.md) - **Siguiente:** [Setup 03: Dream Textures](../setups/setup_03_dream_textures.md) --- # WF 01: TEXTO-A-ESCENA # WF 01: TEXTO-A-ESCENA **ID:** WF-01 | **Tipo:** Pipeline completo | **Dificultad:** ⭐⭐ | **Tiempo:** 15-30 min --- ## 🎯 OBJETIVO Pipeline completo: descripción textual → escena 3D (geometría, materiales, luz, cámara, render) --- ## 📋 ARQUITECTURA DEL PIPELINE ``` USER PROMPT │ ▼ ┌─────────────────────────────────────┐ │ LLM AGENT (OpenCode/Claude) │ │ - Analiza la descripción │ │ - Planifica los pasos │ │ - Llama herramientas MCP │ └─────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────┐ │ BLENDER-MCP SERVER (localhost:9876)│ │ - create_object │ │ - set_material │ │ - download_polyhaven_asset │ │ - execute_blender_code │ └─────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────┐ │ BLENDER (ejecución bpy) │ │ - Creación de geometría │ │ - Asignación de materiales │ │ - Configuración de luz │ │ - Posicionamiento de cámara │ │ - Ejecución de render │ └─────────────────────────────────────┘ │ ▼ RESULTADO: Imagen renderizada / archivo .blend / exportación GLB ``` --- ## 📝 EJEMPLOS DE PROMPTS ### Simple: Habitación ``` "Crea una habitación de 5x5x3m: suelo, 4 paredes, techo. Añade una ventana de 1.5x1.5m en la pared frontal (boolean). Materiales: paredes - yeso blanco, suelo - parquet, ventana - cristal. Luz: HDRI 'studio' 2k de Poly Haven. Cámara: en la esquina, mirando al centro, 35mm. Render: Cycles, 128 samples, 1920x1080, guardar en //renders/room.png" ``` ### Medio: Localización de juego ``` "Crea una sala de mazmorra de 10x10x5m estilo dark fantasy. Paredes: mampostería con grietas (PBR). Suelo: losas de piedra con musgo en las juntas. Atrezzo: 3 columnas, 2 cofres, altar en el centro. Iluminación: 3 puntuales (fuego) + HDRI 'dungeon' tenue. Partículas: polvo en los rayos (volume scatter). Cámara: contrapicado, DOF en el altar. Render: 256 samples, denoising ON" ``` ### Complejo: Ciudad procedural ``` "Genera un barrio procedural de 200x200m: - Malla 10x10 edificios, altura aleatoria 10-50m - Arquitectura: modernismo / neoclásico / futurismo (pesos 40/30/30) - Calles entre edificios, aceras, farolas cada 20m - Parques en el 15% en vez de edificios - HDRI 'city_day' 4k, luz solar - LOD: edificios cercanos high-poly, lejanos low-poly - Exportar: GLB con LODs, texturas 2k" ``` --- ## 🔧 COMANDOS MCP POR ETAPAS ### Etapa 1: Geometría base ```bash # Limpiar "Elimina todos los objetos excepto cámara y luz" # Suelo "Crea un plano 'Floor' de 10x10m en (0,0,0)" # Paredes (4) "Crea un cubo 'Wall_Front' de 10x0.2x3m en (0, -5, 1.5)" "Crea un cubo 'Wall_Back' de 10x0.2x3m en (0, 5, 1.5)" "Crea un cubo 'Wall_Left' de 0.2x10x3m en (-5, 0, 1.5)" "Crea un cubo 'Wall_Right' de 0.2x10x3m en (5, 0, 1.5)" # Techo "Crea un plano 'Ceiling' de 10x10m en (0,0,3)" ``` ### Etapa 2: Booleanos (ventanas/puertas) ```bash # Ventana "Crea un cubo 'Window_Cut' de 2x0.3x1.5m en (0, -5.1, 1.5) Resta Window_Cut de Wall_Front (DIFFERENCE)" # Puerta "Crea un cubo 'Door_Cut' de 1x0.3x2.2m en (-2, -5.1, 1.1) Resta Door_Cut de Wall_Front (DIFFERENCE)" ``` ### Etapa 3: Materiales ```bash # Suelo - parquet "Crea material 'Mat_Parquet': BaseColor #8B7355, Roughness 0.6, Metallic 0 Asígnaselo a Floor" # Paredes - yeso "Crea material 'Mat_Plaster': BaseColor #F5F0E8, Roughness 0.85 Asígnaselo a Wall_Front, Wall_Back, Wall_Left, Wall_Right, Ceiling" # Cristal "Crea material 'Mat_Glass': Transmission 1.0, IOR 1.45, Roughness 0 Asígnaselo a las ventanas" ``` ### Etapa 4: Assets de Poly Haven ```bash # HDRI "Busca en Poly Haven el HDRI 'studio' categoría 'indoor' 2k Descárgalo y ponlo como World Environment" # Modelos (opcional) "Busca en Poly Haven el modelo 'chair' categoría 'furniture' 1k Descarga y coloca 3 sillas alrededor de la mesa" ``` ### Etapa 5: Cámara y render ```bash # Cámara "Crea una cámara 'Cam_Main' en (7, -7, 4) mirando al centro, 35mm Hazla activa: bpy.context.scene.camera = bpy.data.objects['Cam_Main'] DOF en el objeto central" # Render "Configura Cycles: 128 samples, denoising ON, 1920x1080 Renderiza y guarda en //renders/scene_01.png" ``` --- ## 🎯 PARÁMETROS DE CALIDAD | Nivel | Samples | Denoising | Resolución | Tiempo (RTX 3080) | |-------|---------|-----------|------------|-------------------| | **Borrador** | 32 | ON | 1280x720 | ~15 seg | | **Vista previa** | 64 | ON | 1920x1080 | ~30 seg | | **Final** | 128 | ON | 1920x1080 | ~1 min | | **Producción** | 256 | ON + OptiX | 3840x2160 | ~5 min | --- ## 🔗 ENLACES - **Prompts:** [P01-P05](../prompts/), [P09-P15](../prompts/), [P16-P21](../prompts/) - **Casos:** [Case 01: ArchViz](../cases/case_01_archviz_interior.md), [Case 02: Game Props](../cases/case_02_game_props.md) - **Setup:** [Setup 01: blender-mcp](../setups/setup_01_blender_mcp_basic.md) - **Siguiente:** [WF 02: Image-to-3D](../workflows/wf_02_image_to_3d.md) --- # WF 02: IMAGEN-A-3D # WF 02: IMAGEN-A-3D **ID:** WF-02 | **Flujo:** Flujo IA | **⭐⭐** | **10-30 min por asset** --- ## 🎯 OBJETIVO Convertir una imagen 2D (foto, boceto, concept art) en un modelo 3D mediante IA --- ## 📋 ARQUITECTURA DEL PIPELINE ``` INPUT IMAGE │ ▼ ┌─────────────────────────────────────┐ │ PREPROCESSING │ │ • Background removal (RMBG) │ │ • Normalize aspect ratio │ │ • Multiple views generation │ └─────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────┐ │ AI 3D GENERATION │ │ • TripoSR / LRM / Zero123++ │ │ • Hunyuan3D / Hyper3D (cloud) │ │ • Mesh + Textures + UV │ └─────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────┐ │ POST-PROCESSING │ │ • Retopology (Quad Remesher) │ │ • UV Unwrap │ │ • PBR Texture Baking │ │ • LOD Generation │ └─────────────────────────────────────┘ │ ▼ OUTPUT: .glb / .fbx / .obj + PBR textures ``` --- ## 🤖 MODELOS IA (2024/2025) ### LOCALES (gratis, requieren GPU) | Modelo | Calidad | Velocidad | VRAM | Instalación | |--------|----------|----------|------|-----------| | **TripoSR** | ⭐⭐⭐⭐ | ~0.5s | 6GB | `pip install triposr` | | **LRM (Large Reconstruction Model)** | ⭐⭐⭐⭐⭐ | ~2s | 8GB | `pip install lrm` | | **Zero123++** | ⭐⭐⭐ | ~5s | 6GB | `pip install zero123pp` | | **InstantMesh** | ⭐⭐⭐⭐ | ~10s | 8GB | `pip install instant-mesh` | ### EN LA NUBE (API, mayor calidad) | Servicio | Calidad | Coste | API | |--------|----------|-----------|-----| | **Hunyuan3D 2.0** | ⭐⭐⭐⭐⭐ | Free tier | Tencent Cloud | | **Hyper3D Rodin** | ⭐⭐⭐⭐⭐ | $0.5/gen | hyper3d.ai | | **Meshy.ai** | ⭐⭐⭐⭐ | $0.2/gen | meshy.ai | | **CSM (Common Sense Machines)** | ⭐⭐⭐⭐ | $0.3/gen | csm.ai | | **Tripo AI** | ⭐⭐⭐⭐ | $0.1/gen | tripo3d.ai | --- ## 🔧 COMANDOS MCP ### GENERACIÓN LOCAL (TripoSR) ```bash # Instalación pip install triposr torch torchvision # Generación con Python python -c " from triposr import TripoSR model = TripoSR.from_pretrained('VAST-AI/TripoSR') output = model.generate('input_image.png', num_inference_steps=50) # output: dict mesh con vertices, faces, textures " ``` ### MCP: HYPER3D / HUNYUAN3D (vía blender-mcp) ```bash # En OpenCode / Cursor: "Genera un modelo 3D desde la imagen 'concept_art.png' usando Hyper3D Rodin 1. Carga la imagen 2. Inicia la generación 3. Espera a que termine 4. Importa el resultado a Blender 4. Auto-retopología a 10k tris 5. UV Unwrap + PBR Texture Baking 6. Exporta a GLB" ``` ### MCP: MESHY.AI ```bash "Usa Meshy.ai para generar 3D desde 'product_photo.jpg': 1. Carga la foto 2. Parámetros: quality=high, texture_resolution=2048 3. Descarga el resultado como GLB 4. Importa a Blender 5. Verifica UV y materiales" ``` --- ## 📝 PIPELINE PASO A PASO (LOCAL TripoSR) ### 1. PREPARACIÓN DE LA IMAGEN ```python # preprocess.py from PIL import Image from rembg import remove def preprocess_image(input_path, output_path, size=512): img = Image.open(input_path) # Eliminación del fondo img_no_bg = remove(img) # Recorte cuadrado con padding img_square = make_square(img_no_bg, size) # Guardar img_square.save(output_path, "PNG") return output_path def make_square(img, size): # Añadir fondo blanco/transparente para el cuadrado w, h = img.size max_dim = max(w, h) new_img = Image.new('RGBA', (max_dim, max_dim), (0,0,0,0)) new_img.paste(img, ((max_dim-w)//2, (max_dim-h)//2)) return new_img.resize((size, size), Image.LANCZOS) ``` ### 2. GENERACIÓN DE LA MALLA (TripoSR) ```python # generate_mesh.py from triposr import TripoSR import numpy as np import trimesh def generate_3d(image_path, output_dir, steps=50): model = TripoSR.from_pretrained('VAST-AI/TripoSR') # Generación output = model.generate(image_path, num_inference_steps=steps) # Conversión a trimesh vertices = output['vertices'] faces = output['faces'] mesh = trimesh.Trimesh(vertices=vertices, faces=faces) # Exportación mesh.export(f"{output_dir}/model.obj") mesh.export(f"{output_dir}/model.glb") # Texturas (si hay) if 'texture' in output: Image.fromarray(output['texture']).save(f"{output_dir}/texture.png") return mesh ``` ### 3. POST-PROCESADO EN BLENDER (MCP) ```bash # En OpenCode: "Importa el model.glb generado a Blender: 1. File → Import → glTF 2.0 2. Auto-retopología: Quad Remesher target=10000 tris 3. UV Unwrap: Smart Project angle=66 island_margin=0.01 4. PBR Texture Baking: BaseColor, Roughness, Normal, Metallic, AO 5. Material: Principled BSDF + Texture Nodes 5. Exportación: GLB + texturas 2k separadas" ``` --- ## 📊 CALIDAD Y ESPECIFICACIONES | Método | Tris | Texture Res | UV Quality | Time (RTX 3080) | |-------|------|-------------|------------|-----------------| | **TripoSR (raw)** | 10k-50k | 512x512 | Auto UV | 0.5s | | **TripoSR + Retopo** | 5k-15k | 2k | Clean UV | 30s | | **LRM** | 20k-100k | 1024x1024 | Good UV | 2s | | **Hunyuan3D 2.0** | 10k-50k | 2k-4k | Excellent UV | 30s (API) | | **Hyper3D Rodin** | 10k-30k | 4k | Production UV | 60s (API) | --- ## 💡 CONSEJOS | Consejo | Por qué | |-----|-----| | Usa `rembg` para quitar el fondo | Silueta limpia = mejor geometría | | Múltiples vistas (front/side/back) | LRM/Hunyuan soportan multi-view | | Entrada cuadrada 512x512 | Estándar de la mayoría de modelos | | Post-retopología obligatoria | La malla raw no sirve para juegos/animación | | PBR Baking tras el UV | Normales/cavidad desde la geometría | --- ## 🔗 LINKS - **TripoSR:** https://github.com/VAST-AI-Research/TripoSR - **LRM:** https://github.com/lrm-dev/LRM - **InstantMesh:** https://github.com/TencentARC/InstantMesh - **Hunyuan3D:** https://3d.hunyuan.tencent.com/ - **Hyper3D:** https://hyper3d.ai/ - **Meshy:** https://www.meshy.ai/ - **Setup 03:** [Dream Textures](../setups/setup_03_dream_textures.md) - **Siguiente:** [WF 03: Texture Pipeline](../workflows/wf_03_texture_pipeline.md) --- # CASE 01: ARCHVIZ INTERIOR Tags: #archviz #case # CASE 01: ARCHVIZ INTERIOR **ID:** CASE-01 | **Type:** Architectural Visualization | **Difficulty:** ⭐⭐⭐ | **Time:** 2-4 hours --- ## 🎯 GOAL Create photorealistic interior render: modeling, materials, lighting, camera, compositing --- ## 📋 PIPELINE OVERVIEW (8 STAGES) ``` STAGE 1: Room Shell → P01, P02, P03 STAGE 2: Windows/Doors → P02 (Boolean) STAGE 3: Furniture/Props → P01, P07, P37 (Poly Pizza) STAGE 4: Materials → P09, P11, P12 STAGE 5: Lighting → P13 (HDRI + Studio) STAGE 6: Camera → P14 STAGE 7: Render Settings → P15 STAGE 8: Compositing → P15 ``` --- ## 🔧 MCP COMMANDS BY STAGE ### STAGE 1: ROOM SHELL (10x8x3m) ```bash # Floor "Создай плоскость 'Floor' 10x8м в (0,0,0) материал 'Mat_Floor_Parquet'" # Walls (4 шт.) "Создай куб 'Wall_North' 10x0.2x3м в (0, -4, 1.5)" "Создай куб 'Wall_South' 10x0.2x3м в (0, 4, 1.5)" "Создай куб 'Wall_East' 0.2x8x3м в (5, 0, 1.5)" "Создай куб 'Wall_West' 0.2x8x3м в (-5, 0, 1.5)" # Ceiling "Создай плоскость 'Ceiling' 10x8м в (0,0,3) материал 'Mat_Ceiling_White'" ``` ### STAGE 2: WINDOWS & DOORS (Boolean) ```bash # Large window (North wall, centered) "Создай куб 'Window_Cut' 3x0.3x2м в (0, -4.1, 1.5) Вычти Window_Cut из Wall_North (DIFFERENCE)" # Door (South wall) "Создай куб 'Door_Cut' 1x0.3x2.2м в (-1, 4.1, 1.1) Вычти Door_Cut из Wall_South (DIFFERENCE) # Window frame (отдельный объект) "Создай куб 'Window_Frame' 3.1x0.05x2.1м в (0, -4.1, 1.5) материал 'Mat_Wood_White'" ``` ### STAGE 3: FURNITURE (Poly Pizza + Primitives) ```bash # Sofa (Poly Pizza) "Найди на Poly Pizza 'sofa' лицензия CC0, скачай, normalize_size=true, target_size=2.0 Позиция: (0, -2, 0)" # Coffee Table "Создай куб 'Table_Top' 1.2x0.6x0.05м в (0, -1, 0.45) материал 'Mat_Wood_Oak' Создай 4 ножки: цилиндр 0.05x0.4м в углах стола" # Armchairs (2 шт.) "Найди на Poly Pizza 'armchair' CC0, скачай 2 шт., target_size=0.9 Позиции: (-1.5, -2.5, 0) и (1.5, -2.5, 0)" # Floor Lamp "Создай цилиндр 'Lamp_Base' 0.3x0.05м в (2, -3, 0) материал 'Mat_Metal_Black' Создай цилиндр 'Lamp_Stand' 0.02x1.5м в (2, -3, 0.025) материал 'Mat_Metal_Black' Создай сферу 'Lamp_Shade' 0.4м в (2, -3, 1.55) материал 'Mat_Fabric_Linen' Добавь точечный свет в центр шейда, энергия 200W, цвет warm" ``` ### STAGE 4: MATERIALS (PBR) ```bash # Parquet Floor "Создай материал 'Mat_Floor_Parquet': BaseColor: #8B7355, Roughness: 0.6, Metallic: 0 Normal: procedural wood grain, Height: subtle" # Walls - Plaster "Создай материал 'Mat_Wall_Plaster': BaseColor: #F5F0E8, Roughness: 0.85, Metallic: 0 Normal: subtle stucco texture" # Window Glass "Создай материал 'Mat_Glass_Clear': Transmission: 1.0, IOR: 1.45, Roughness: 0 BaseColor: white, Alpha: 0.9" # Wood (Furniture) "Создай материал 'Mat_Wood_Oak': BaseColor: #8B6914, Roughness: 0.4, Metallic: 0 Normal: wood grain, Clearcoat: 0.2" # Metal (Lamp, Hardware) "Создай материал 'Mat_Metal_Black': BaseColor: #1A1A1A, Roughness: 0.2, Metallic: 1.0 Clearcoat: 0.1" # Fabric (Sofa, Lamp Shade) "Создай материал 'Mat_Fabric_Linen': BaseColor: #E8E0D0, Roughness: 0.9, Metallic: 0 Sheen: 0.3, SheenTint: 0.5, Normal: fabric weave" ``` ### STAGE 5: LIGHTING (HDRI + Studio) ```bash # HDRI Environment "Найди на Poly Haven HDRI 'studio' категория 'indoor' 2k Скачай и поставь как World, strength=0.4" # Studio Lights (3-point) "Создай AREA свет 'Key_Light' 1000W в (3, -3, 4) размер 2x2м поворот к центру Создай AREA свет 'Fill_Light' 300W в (-3, 2, 2) размер 3x3м Создай AREA свет 'Rim_Light' 500W в (-2, -2, 4) размер 1.5x1.5м" # Accent lights "Добавь точечный свет в торшере 200W warm Добавь полоску LED под диваном 50W warm" ``` ### STAGE 6: CAMERA ```bash # Main Camera "Создай камеру 'Cam_Main' в (7, -6, 1.6) цель (0,0,1) focal=35mm sensor=36mm DOF: focus на диван, f-stop=2.8 Composition: Rule of Thirds" # Additional cameras "Cam_Wide: (-10, -8, 1.5) focal=24mm Cam_Detail: (-1, -1, 1) focal=85mm f/1.8" ``` ### STAGE 7: RENDER SETTINGS ```bash "Cycles: 256 samples, denoising ON (OPTIX), adaptive sampling Resolution: 3840x2160 (4K) или 1920x1080 Color Management: Filmic, Look: Medium High Contrast Output: EXR MultiLayer 32-bit + PNG 16-bit" ``` ### STAGE 8: COMPOSITING ```bash "Enable passes: Combined, Depth, Normal, AO, Mist, Cryptomatte Compositor: Color Balance (lift/gamma/gain), Glare (fog glow 0.2), Vignette (0.15), Color Matrix (teal-orange), Film Grain (0.02) Output: EXR 32-bit MultiLayer + PNG 16-bit" ``` --- ## 📊 QUALITY CHECKLIST | Категория | Target | Critical | |-----------|--------|----------| | **Triangles** | < 500k | < 1M | | **Materials** | 10-20 unique | < 30 | | **Texture Resolution** | 2k-4k | 1k min | | **HDRI Resolution** | 2k-4k | 1k | | **Samples** | 256-512 | 128 | | **Render Time (RTX 3080)** | 5-15 min | 30 min | | **VRAM Usage** | < 8GB | < 10GB | --- ## 🔗 СВЯЗИ - **Прочие в категории «Кейсы»:** [CASE 08: SCI-FI CORRIDOR MODULAR](case_08_sci_fi_corridor.md), [CASE 02: GAME PROPS PIPELINE](case_02_game_props.md), [CASE 03: PRODUCT VISUALIZATION](case_03_product_viz.md), [CASE 04: CHARACTER RIG & ANIMATION](case_04_character_rig.md), [CASE 05: PROCEDURAL CITY](case_05_procedural_city.md) - **По темам:** [CASE 06: ANIMATION LOOP](case_06_animation_loop.md), [CASE 07: STYLIZED BARREL (HAND-PAINT)](case_07_stylized_barrel.md), [CASE 09: ANIME HEAD BUST](case_09_anime_head_bust.md) - **Теги:** #archviz, #case --- # CASE 02: GAME PROPS PIPELINE Tags: #case #game-assets #pipeline # CASE 02: GAME PROPS PIPELINE **ID:** CASE-02 | **Тип:** Game Asset Production | **Сложность:** ⭐⭐ | **Время:** 30-60 мин --- ## 🎯 ЦЕЛЬ Полный пайплайн создания гейм-готовых пропсов: от примитивов → оптимизация → UV → PBR материалы → экспорт --- ## 📋 ПЛАН РАБОТЫ (6 ШАГОВ) ### ШАГ 1: Блокаут (Primitives + Boolean) **Промпты:** P01, P02 **Время:** 5-10 мин ```python # Базовая форма create_object("CUBE", "Prop_Base", [0,0,0], [1,1,1]) create_object("CYLINDER", "Prop_Detail", [0,0,1], [0.3,0.3,0.5]) # Булев для деталей boolean_difference("Prop_Base", "Prop_Detail") ``` ### ШАГ 2: Детализация (Bevel + Modifiers) **Промпты:** P03 **Время:** 5 мин ```python modifiers_stack = [ ("BEVEL", {"width": 0.02, "segments": 3, "profile": 0.5}), ("WEIGHTED_NORMAL", {"weight": 100, "face_weight": 0.5}), ] ``` ### ШАГ 3: Оптимизация (Decimate / Retopo) **Промпты:** P06 **Время:** 5 мин ```python # Target: 500-2000 tris для пропса decimate_ratio = target_tris / current_tris ``` ### ШАГ 4: UV Развёртка **Промпты:** P05 **Время:** 5 мин ```python # Smart Project для пропсов bpy.ops.uv.smart_project(angle_limit=1.2, island_margin=0.005) bpy.ops.uv.pack_islands(margin=0.002, rotate=True) ``` ### ШАГ 5: PBR Материалы **Промпты:** P09, P10 **Время:** 5-10 мин ```python # Principled BSDF с текстурами maps = { 'base_color': 'prop_basecolor.png', 'roughness': 'prop_roughness.png', 'normal': 'prop_normal.png', 'metallic': 'prop_metallic.png', } ``` ### ШАГ 6: Экспорт (GLTF/GLB) **Время:** 2 мин ```python bpy.ops.export_scene.gltf( filepath="prop.glb", export_format='GLB', use_selection=True, export_apply=True, export_tangents=True, ) ``` --- ## 🔧 MCP КОМАНДЫ ПОШАГОВО ### 1. Сцена ```bash OpenCode: "Очисти сцену, создай новый проп: куб 1x1x1м с цилиндрическим вырезом в центре" ``` ### 2. Модификаторы ```bash OpenCode: "Добавь Bevel 0.02м 3 сегмента и Weighted Normal к пропсу" ``` ### 3. UV ```bash OpenCode: "Разверни UV Smart Project с margin 0.005, упакуй острова" ``` ### 4. Материал ```bash OpenCode: "Создай PBR материал: BaseColor #8B7355, Roughness 0.7, Metallic 0.0, присвой к пропсу" ``` ### 5. Проверка ```bash OpenCode: "Покажи статистику: трисы, вершины, UV острова, материалы" ``` ### 6. Экспорт ```bash OpenCode: "Экспорт пропса в GLB с тангентами, применить модификаторы" ``` --- ## 📊 МЕТРИКИ КАЧЕСТВА | Метрика | Target | Critical | |---------|--------|----------| | **Triangles** | 500-2000 | < 5000 | | **UV Islands** | 1-8 | < 15 | | **Texel Density** | 512 px/m | > 256 | | **Overlapping UVs** | 0 | 0 | | **Non-manifold edges** | 0 | 0 | | **Material Slots** | 1-2 | < 4 | | **File Size (GLB)** | < 500 KB | < 2 MB | --- ## 🎨 ВАРИАНТЫ ПРОПСОВ | Тип | Tris | Особенности | |-----|------|-------------| | **Crate/Box** | 300-500 | Простой куб + bevel + детали | | **Barrel** | 400-800 | Цилиндр + ребра + крышка | | **Weapon** | 800-2000 | Boolean детали, острые края | | **Furniture** | 1000-3000 | Модульные части, зеркалирование | | **Vegetation** | 200-600 | Плоские полигоны + alpha | --- ## 🔗 ИСПОЛЬЗУЕМЫЕ ПРОМПТЫ | Промпт | Роль | |--------|------| | [P01](../prompts/01_geometry_basics.md) | Базовая геометрия | | [P02](../prompts/02_boolean_operations.md) | Булевы вычитания | | [P03](../prompts/03_modifiers_stack.md) | Стек модификаторов | | [P05](../prompts/05_uv_unwrapping.md) | UV развёртка | | [P09](../prompts/09_principled_bsdf.md) | PBR материал | | [P10](../prompts/10_node_groups.md) | Библиотека материалов | --- ## 🔗 СВЯЗИ - **Прочие в категории «Кейсы»:** [CASE 03: PRODUCT VISUALIZATION](case_03_product_viz.md), [CASE 05: PROCEDURAL CITY](case_05_procedural_city.md), [CASE 07: STYLIZED BARREL (HAND-PAINT)](case_07_stylized_barrel.md), [CASE 08: SCI-FI CORRIDOR MODULAR](case_08_sci_fi_corridor.md), [CASE 16: DUNGEON TILESET](case_16_dungeon_tileset.md) - **По темам:** [CASE 01: ARCHVIZ INTERIOR](case_01_archviz_interior.md), [CASE 04: CHARACTER RIG & ANIMATION](case_04_character_rig.md), [CASE 06: ANIMATION LOOP](case_06_animation_loop.md) - **Теги:** #case, #game-assets, #pipeline --- # CASE 03: PRODUCT VISUALIZATION Tags: #case #compositing #lighting #pipeline #render #shading #texturing # CASE 03: PRODUCT VISUALIZATION **ID:** CASE-03 | **Type:** Product Visualization | **Difficulty:** ⭐⭐ | **Time:** 1-2 hours --- > Когда применять: когда предмет снимают для карточки товара, каталога или рекламы: нужен нейтральный фон, управляемые блики и кадр, в котором форма читается без усилий зрителя. ## 🎯 ЦЕЛЬ Собрать предмет студийного качества из Blender: точная форма, материалы, которые держат блик, схема света, где каждый источник отвечает за свою задачу, макро-камера с малым DOF и композиторный грейд. Ключевая мысль всего кейса: **продуктовая съёмка — это не «красивый рендер», а управление бликами**. Зритель не рассматривает текстуру; он считывает форму по тому, как свет скользит по фаскам и где гаснет. Ошибка в источнике света стоит дороже ошибки в модели. Второе правило: масштаб сцены берётся из реальных габаритов. Предмет 40 мм в реальности должен быть 0.040 м в сцене — иначе площади источников, глубина резкости и поведение SSS/стекла будут не те, а пересчитывать их потом дорого. ## 🧭 ИСХОДНАЯ ЗАДАЧА И ОГРАНИЧЕНИЯ **Задача:** карточка часов на 1600×1600 для каталога, плюс три детальных кадра (циферблат, коронка, текстура ремня). Фон нейтральный, без отражений окружения. **Ограничения:** - Никакого окружения в кадре: только студийные источники, HDRI даёт мягкую заливку, но не отражения. - Металл должен читаться металлом: без HDRI на отражениях полированная сталь выглядит серой пластиной. - Стекло без видимых «внутренностей»: толщина и IOR задают всё, Absorption — цвет кромки. - Один предмет, один материал на деталь — чтобы рендер можно было ускорить без потери качества. - Никаких текстур с внешних сайтов: только процедурные, иначе вопрос лицензий. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия / состояние | Зачем | |---|---|---| | Blender + Cycles | базовый | стекло, SSS, качественные блики | | HDRI студийная | Poly Haven, 1k или 2k | заполняющий свет и отражения в металле | | Процедурные текстуры | Noise, Voronoi | зерно металла, кожа ремня | | Композитор | встроенный | грейд, glare, виньетка | ## 🛠️ НАСТРОЙКА ### Этап 1. Моделирование ```bash # Case body "Создай цилиндр 'Case_Body' радиус 0.022м глубина 0.012м вершин 64 Позиция (0,0,0) материал 'Mat_Metal_Steel'" # Bezel "Создай цилиндр 'Bezel' радиус 0.024м глубина 0.003м вершин 64 Позиция (0,0,0.006) материал 'Mat_Metal_Ceramic'" # Crystal "Создай цилиндр 'Crystal' радиус 0.021м глубина 0.004м вершин 64 Позиция (0,0,0.008) материал 'Mat_Glass_Sapphire'" # Crown "Создай цилиндр 'Crown' радиус 0.004м глубина 0.008м вершин 32 Позиция (0.024, 0, 0.004) материал 'Mat_Metal_Steel' Boolean DIFFERENCE для паза" # Strap (Geometry Nodes) "Создай плоскость 'Strap_Base' 0.2x0.02м GN: Curve to Mesh, профиль 0.004м, материал 'Mat_Leather_Black' Array по кривой длиной 0.25м" ``` ### Этап 2. UV-развёртка ```bash # Case body "Smart Project angle_limit=66 island_margin=0.01" # Crystal — Cube Projection "Cube Project clip_to_bounds=True" # Strap — Follow Active Quads "Select edge loop → Mark Seam → Unwrap → Follow Active Quads" ``` ### Этап 3. Материалы ```bash # Steel (Case, Crown, Buttons) "Mat_Metal_Steel: BaseColor: #B8B8C0, Roughness: 0.15, Metallic: 1.0 Clearcoat: 0.3, ClearcoatRough: 0.05 Anisotropic: 0.3, Tangent: UV Normal: brushed metal procedural" # Ceramic Bezel "Mat_Metal_Ceramic: BaseColor: #1A1A1A, Roughness: 0.05, Metallic: 0.0 Transmission: 0, Clearcoat: 1.0, ClearcoatRough: 0.01 Normal: subtle ceramic texture" # Sapphire Crystal "Mat_Glass_Sapphire: Transmission: 1.0, IOR: 1.77, Roughness: 0 Clearcoat: 0.5 (AR coating), ClearcoatRough: 0.001 Absorption: slight blue tint at edges" # Leather Strap "Mat_Leather_Black: BaseColor: #0D0D0D, Roughness: 0.85, Metallic: 0 Sheen: 0.15, SheenTint: 0.3 Normal: leather grain procedural Height: embossed pattern" ``` Зерно металла делается процедурно — это дешевле текстуры и не требует повторяющихся UV: ```python import bpy, math def brushed_metal(mat_name, scale=(1.0, 60.0), strength=0.06): """Анизотропные царапины: растянутый по одной оси шум + Tangent по UV.""" mat = bpy.data.materials[mat_name] nt = mat.node_tree bsdf = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED') tex = nt.nodes.new("ShaderNodeTexCoord") mp = nt.nodes.new("ShaderNodeMapping") mp.inputs['Scale'].default_value = scale # anisotropy = ratio X/Y noise = nt.nodes.new("ShaderNodeTexNoise") noise.inputs['Scale'].default_value = 220.0 noise.inputs['Detail'].default_value = 4.0 bump = nt.nodes.new("ShaderNodeBump") bump.inputs['Strength'].default_value = strength bump.inputs['Distance'].default_value = 0.0002 nt.links.new(tex.outputs['UV'], mp.inputs['Vector']) nt.links.new(mp.outputs['Vector'], noise.inputs['Vector']) nt.links.new(noise.outputs['Fac'], bump.inputs['Height']) nt.links.new(bump.outputs['Normal'], bsdf.inputs['Normal']) a = bsdf.inputs.get('Anisotropic') if a: a.default_value = 0.3 t = bsdf.inputs.get('Tangent') if t: nt.links.new(tex.outputs['UV'], t) brushed_metal("Mat_Metal_Steel") ``` ### Этап 4. Свет ```bash # HDRI "Poly Haven 'studio_small_01' 2k, strength=0.3" # Key Light (Main) "AREA 1000W, 0.3x0.3м, позиция (0.1, -0.2, 0.15), поворот к часам Shape: Rectangle, Spread: 45°" # Fill Light "AREA 300W, 0.5x0.5м, позиция (-0.15, 0.1, 0.1), мягкий" # Rim Light "AREA 200W, 0.2x0.2м, позиция (0, 0.15, 0.15), контрастный край" # Accent (Sparkle on Crystal) "POINT 50W, позиция (0.02, -0.02, 0.02), размер 0" ``` Ключевой к источникам — `spread`: у area-свет есть угол распространения, и его сужение до 30–50° превращает лампу из «заливки всего подряд» в инструмент для конкретной фаски. ```python import bpy, math def area_light(name, size, energy, loc, look_at, spread=45.0, color=(1, 1, 1)): data = bpy.data.lights.new(name, type='AREA') data.shape = 'SQUARE' data.size = size # в метрах: 0.1 = панель 10 см data.energy = energy # в ваттах, привязано к реальному масштабу data.color = color if hasattr(data, "spread"): # доступно не во всех сборках data.spread = math.radians(spread) ob = bpy.data.objects.new(name, data) bpy.context.scene.collection.objects.link(ob) ob.location = loc con = ob.constraints.new('TRACK_TO') con.target = bpy.data.objects["Case_Body"] con.track_axis = 'TRACK_NEGATIVE_Z' con.up_axis = 'UP_Y' return ob target = bpy.data.objects["Case_Body"] area_light("Key", 0.30, 1000.0, (0.10, -0.20, 0.15), target, spread=45.0) area_light("Fill", 0.50, 300.0, (-0.15, 0.10, 0.10), target, spread=90.0) area_light("Rim", 0.20, 200.0, (0.00, 0.15, 0.15), target, spread=30.0) ``` ### Этап 5. Камера ```bash # Hero Shot "Cam_Hero: позиция (0, -0.3, 0.05) focal=100mm sensor=36mm DOF: focus на циферблат, f-stop=2.8, blades=9 Composition: Center + Rule of Thirds" # Detail Shots "Cam_Dial: (0, -0.15, 0.03) focal=150mm f/2.0 → крупно циферблат Cam_Crown: (0.025, 0, 0.005) focal=100mm f/4.0 → коронка Cam_Strap: (-0.1, -0.2, 0) focal=85mm f/2.8 → текстура ремня" ``` ```python import bpy def product_cam(name, loc, lens, fstop, focus="Case_Body", sensor=36.0): cam = bpy.data.cameras.new(name) cam.lens = lens cam.sensor_width = sensor cam.clip_start = 0.005 # предмет 4 см: стандартный 0.1 его срежет cam.dof.use_dof = True cam.dof.focus_object = bpy.data.objects[focus] cam.dof.aperture_fstop = fstop cam.dof.aperture_blades = 9 # не круглая диафрагма = блики с гранями ob = bpy.data.objects.new(name, cam) bpy.context.scene.collection.objects.link(ob) ob.location = loc return ob hero = product_cam("Cam_Hero", (0.0, -0.30, 0.05), 100.0, 2.8) bpy.context.scene.camera = hero ``` ### Этап 6. Композитор ```bash "Passes: Combined, Depth, Normal, Cryptomatte, Mist Color: Lift/Gamma/Gain (Teal & Orange), Glare (0.15 threshold), Vignette (0.1), Sharpen (Unsharp Mask 0.5), Chromatic Aberration (0.002) Output: 4K EXR 32-bit + 4K PNG 16-bit" ``` ## ⚠️ ЧТО ПОШЛО НЕ ТАК **1. Металл выглядел как крашеный пластик.** Первый вариант ставил только HDRI с `strength = 0.3` и один заполняющий источник. Логика была «студия = равномерный свет», и в итоге на корпусе не оказалось ни одного читаемого блика: сфера считывалась по цвету, а не по свету, и зритель видел плоскую запятку. Лечится не увеличением общей яркости, а постановкой: узкий key со `spread` около 40° напротив фаски, широкий fill, отдельный rim сзади. Площадь блика на металле должна быть узкой и яркой, а её форма — повторять форму детали. **2. Ключевой свет стоял в сантиметрах от предмета и всё пересвечивал.** Источник на 1000 Вт на расстоянии 20 см от объекта 4 см — это очень много. Пересвет выедал кромки безеля, а стекло превращалось в белое пятно с ореолом, потому что внутренние грани стекла получали свет со всех сторон. Пришлось увести key на 0.3–0.5 м, урезать энергию и поставить отдельный очень слабый источник для подсветки циферблата изнутри. Урок: при реальном масштабе предмета мощности источников из «метровых» сцен неприменимы напрямую — их надо подбирать заново, и на глаз это не считается. **3. `clip_start` = 0.1 у макрокамеры.** Камера с фокусным 100 мм в сантиметрах от предмета — это уже макро, и стандартный клиппинг Blender по умолчанию буквально отсёкал переднюю часть коронки. Кадр получался «съеденным» по краям безо всякой видимой причины: ни ошибок, ни предупреждений. Лечится `clip_start` около 0.005, и это надо ставить в самом начале проекта, а не когда рендер уже потратил два часа. **4. Glare на циферблате испортил читаемость.** Универсальный Bloom в композиторе добавил ореол вокруг стрелок и превратил мелкие детали в кашу. Пришлось ограничить glare только слоем циферблата, понизить порог и отключить его для детальных кадров, где блик на стекле и так должен быть единственным. ## 📊 ЧТО ПОЛУЧИЛОСЬ **Оценка по порядку величины, конкретных замеров нет:** - Объектов: ~14 (корпус, безель, стекло, коронка, 2 кнопки, ремень на 2 кривых, 4 источника, камера). - Полигоны: цилиндры по 32–64 вершины с bevel по 2 сегмента, ремень — Curve to Mesh по кривой. Порядок **20–60 тыс. треугольников** для hero-ракурса. - Материалов: 4. Текстур **ноль** — всё процедурное (шум + mapping на UV). - Кадров: 4 (1 hero 1600×1600 + 3 детальных), плюс тестовые прогоны на 640×640 при 32 сэмплах. - Рендер в Cycles на 512–1024 сэмплах с денойзингом: **десятки минут на кадр** на средней видеокарте. Тестовые превью — секунды. ## ⚙️ КЛЮЧЕВЫЕ ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | масштаб сцены | реальные габариты (метры) | единственный способ получить верные мощности света и DOF | | `Key.spread` | 30–45° | уже — блик превращается в точку, шире — в пятно без формы | | `Key.energy` | подбирается по результату, 200–1000 Вт на метровом расстоянии | после смены масштаба сцены обязательно пересчитать | | HDRI `strength` | 0.2–0.4 | выше — металл начинает отражать «фотостудию», а не предмет | | `dof.aperture_fstop` | 2.0–2.8 для макро | меньше — задняя фаска ремня полностью исчезает | | `dof.aperture_blades` | 7–9 | круглая диафрагма даёт «мыльный» боке вместо огранки | | `clip_start` | 0.005 | обязательно при работе с предметами меньше 10 см | | `Mat_Metal_Steel` roughness | 0.12–0.18 | выше — металл теряет отражения и становится серым | | IOR сапфира | 1.76–1.77 | задаёт силу преломления и толщину каустик на кромке | | Clearcoat на безеле | 0.9–1.0 | без него керамика не даёт одиночного блика | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Металл похож на пластик | нет узкого яркого блика, только равномерная заливка | key со `spread` 30–45°, отдельный rim | | Всё пересвечено | мощности источников из метровой сцены на предмете 4 см | отвести источники дальше, пересчитать энергию | | Края кадра «съедены» без ошибок | `clip_start` 0.1 на макро | `clip_start = 0.005` | | Стекло — белое пятно | пересвет внутренних граней | уменьшить key, добавить слабый источник внутрь | | Циферблат в ореоле | грейд/glare по всему кадру | glare только на слое циферблата, порог ниже | | Ремень «лысый», текстуры нет | UV развёрнут Follow Active Quads и сел на ребро | переразвернуть, проверить масштаб текстуры | | Детальные кадры мыльные | один f-stop на всех ракурсах | отдельный f-stop для каждого кадра | ## 🔗 СВЯЗИ - **Прочие в категории «Кейсы»:** [CASE 05: PROCEDURAL CITY](case_05_procedural_city.md), [CASE 07: STYLIZED BARREL (HAND-PAINT)](case_07_stylized_barrel.md), [CASE 08: SCI-FI CORRIDOR MODULAR](case_08_sci_fi_corridor.md), [CASE 02: GAME PROPS PIPELINE](case_02_game_props.md), [CASE 13: HARD-SURFACE SCI-FI GUN](case_13_hard_surface_gun.md) - **По темам:** [WF 17: STOP-MOTION FELT LOOK](../workflows/wf_17_stop_motion_felt.md), [P46: UDIM WORKFLOW](../prompts/46_udim_workflow.md), [WF 23: VIRTUAL PRODUCTION LED](../workflows/wf_23_virtual_production_led.md), [WF 24: PHOTOGRAMMETRY CLEANUP](../workflows/wf_24_photogrammetry_cleanup.md) - **Теги:** #case, #compositing, #lighting, #pipeline, #render, #shading, #texturing --- # CASE 04: CHARACTER RIG & ANIMATION Tags: #animation #case #rigging # CASE 04: CHARACTER RIG & ANIMATION **ID:** CASE-04 | **Type:** Character Pipeline | **Difficulty:** ⭐⭐⭐ | **Time:** 3-6 hours --- ## 🎯 GOAL Complete character pipeline: base mesh → rigging → skinning → animation → render --- ## 📋 PIPELINE (7 STAGES) ``` STAGE 1: Base Mesh (Modeling) → P01, P04, P05, P08 STAGE 2: Retopology (Game-ready) → P06 STAGE 3: UV & Texturing → P05, P12 STAGE 4: Rigging (Rigify/Custom) → P23 STAGE 5: Skinning & Weights → P23 STAGE 6: Animation → P22, P24, P25, P26 STAGE 7: Render & Export → P13, P14, P15, P49 ``` --- ## 🔧 MCP COMMANDS ### STAGE 1: BASE MESH (Sculpting) ```bash # Base human proportions "Создай сферу 'Head' радиус 0.12м сегменты 32 Создай цилиндр 'Torso' радиус 0.15м высота 0.5м Создай цилиндр 'Hip' радиус 0.12м высота 0.2м Создай 4 конечности: цилиндры с Subsurf + Dyntopo # Объединить в один объект "Объедини все части в 'Character_Base' Dyntopo: constant_detail=8, symmetry X Скульптинг: пропорции 8 голов, силуэт, анатомия" ``` ### STAGE 2: RETOPOLOGY (Game-ready ~15k tris) ```bash "Quad Remesher: target_quads=5000, adaptivity=0.3 Проверить: только квады, нет полюсов >5, edge flow по анатомии UV: Smart Project для тела, ручная для лица/рук" ``` ### STAGE 3: UV & TEXTURING ```bash # UDIM layout (4 tiles) "UDIM 1001: Head/Neck UDIM 1002: Torso/Arms UDIM 1003: Legs UDIM 1004: Hands/Feet/Details # Texture Paint слои "BaseColor, Roughness, Normal, Subsurface, Specular, Displacement 4k на голову, 2k на тело" ``` ### STAGE 4: RIGGING (Rigify) ```bash # Meta-rig "Add Armature → Human (Meta-Rig) Масштабировать под меш: Match to Mesh Generate Rig → Rigify # Дополнительные кости "Face Rig: 50+ костей (jaw, eyes, brows, lips, cheeks) Fingers: полная иерархия (3 фаланги на палец) Twist bones: forearm, upper arm, thigh, calf Corrective shapes: knees, elbows, shoulders" ``` ### STAGE 5: SKINNING ```bash "Parent Mesh → Rig (Automatic Weights) Weight Paint cleanup: - Head: жесткая привязка к skull - Shoulders: градиент clavicle/upperarm - Hips: плавный переход spine/leg - Fingers: по фалангам Test: extreme poses, проверка деформаций" ``` ### STAGE 6: ANIMATION ```bash # Walk Cycle (24 фрейма = 1 сек @ 24fps) "Frame 1: Contact (right heel, left toe) Frame 7: Down (right foot flat, left passing) Frame 13: Contact (left heel, right toe) Frame 19: Down (left foot flat, right passing) Frame 25 = Frame 1 (loop) # Graph Editor: Bezier, Auto Clamp # NLA: Walk Action → Push Down → Blend с Run/Idle" ``` ### STAGE 7: RENDER & EXPORT ```bash # Lighting "HDRI 'studio' 2k + 3-point studio Volumetric: легкая дымка density=0.02" # Camera "Cam_Main: focal=50mm, DOF на глаза f/2.0 Cam_CloseUp: focal=85mm f/1.4 на лицо" # Render "Cycles 256 samples, Denoising, 4K Output: EXR + MP4 (H.264 High)" ``` --- ## 📊 CHARACTER SPECS | Spec | Game-Ready | Film-Ready | |------|------------|------------| | **Triangles** | 15k-25k | 50k-150k | | **Bones** | 100-150 | 300-500 | | **Blend Shapes** | 20-30 | 50-100 | | **UV Tiles** | 1-2 (1024) | 4-8 (UDIM 4k) | | **Texture Res** | 2k | 4k-8k | | **Rig Type** | Rigify + Custom | Custom + Facial | --- ## 🔗 СВЯЗИ - **Прочие в категории «Кейсы»:** [CASE 06: ANIMATION LOOP](case_06_animation_loop.md), [CASE 01: ARCHVIZ INTERIOR](case_01_archviz_interior.md), [CASE 02: GAME PROPS PIPELINE](case_02_game_props.md), [CASE 03: PRODUCT VISUALIZATION](case_03_product_viz.md), [CASE 05: PROCEDURAL CITY](case_05_procedural_city.md) - **По темам:** [CASE 07: STYLIZED BARREL (HAND-PAINT)](case_07_stylized_barrel.md), [CASE 08: SCI-FI CORRIDOR MODULAR](case_08_sci_fi_corridor.md), [CASE 09: ANIME HEAD BUST](case_09_anime_head_bust.md) - **Теги:** #animation, #case, #rigging --- # CASE 05: PROCEDURAL CITY Tags: #case #geometry-nodes #modeling #pipeline #render #vegetation # CASE 05: PROCEDURAL CITY **ID:** CASE-05 | **Type:** Procedural Generation | **Difficulty:** ⭐⭐⭐ | **Time:** 2-4 hours --- > Когда применять: когда нужно за минуты получить десятки кварталов для проверки композиции, а не готовый ассет: город флажка на заднем плане, панораму, окружение для рендера с большой глубиной. ## 🎯 ЦЕЛЬ Собрать процедурный город на Geometry Nodes: сетка кварталов, зонирование, генератор зданий по зоне, дорожная сеть с движением, рассев пропов и растительности, LOD по расстоянию и облёт камерой. Главное, что отличает город от набора кубиков, — **связи между системами**. Здание знает, в какой зоне оно стоит, и берёт высоту и материал оттуда. Дорога знает, где перекрёсток. Пропы рассеиваются по тротуарам, а не в воздухе. Каждая из этих связей — атрибут, и большую часть ошибок в этом кейсе дают именно атрибуты: не тот домен, потерянное поле при объединении геометрии, поле, которое не выжило `Set Position`. Порядок работы поэтому жёсткий: сначала сетка и зоны, потом здания, и только потом всё, что рассыпается по готовому. ## 🧭 ИСХОДНАЯ ЗАДАЧА И ОГРАНИЧЕНИЯ **Задача:** 500 кадров облёта камеры по спирали вверх над городом 1×1 км, 4K, 30 fps, на одной видеокарте. **Ограничения:** - Город не должен быть полностью инстансингом: инстансы нельзя ни сдвинуть по высоте, ни отдать в запекание — нужен гибрид. - Один материал на зону, иначе материалов будет больше, чем зданий. - Движение должно быть цикличным и непрерывным: машина не должна прыгать в конце петли. - Кадры считаются на CPU-рендере как минимум: 500 кадров × 4K × 512 сэмплов — это неделя. - Дальние кварталы нельзя оставлять в LOD0: память уйдёт раньше, чем закончится рендер. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия / состояние | Зачем | |---|---|---| | Blender + Geometry Nodes | актуальная | вся генерация | | Коллекция с 5–10 вариантами деревьев | Poly Pizza или свой набор | инстансинг растительности | | Коллекция с 8–12 вариантами машин | то же | инстансинг транспорта | | HDRI дневного города | Poly Haven | базовое освещение | | Сжатие EXR | ZIP | 500 кадров занимают место | ## 🛠️ НАСТРОЙКА ### Этап 1. Сетка кварталов и зонирование ```bash # Base Grid 10x10 blocks, 100x100м каждый "GN: Grid 10x10, spacing 100м Атрибут 'District': Random (0=Downtown, 1=Residential, 2=Industrial, 3=Park) Атрибут 'Density': по District (Downtown=1.0, Res=0.6, Ind=0.8, Park=0.1)" ``` ### Этап 2. Генератор зданий ```bash # Building Logic по District "Downtown (0): Высота 50-200м, Glass/Concrete, Flat Roof + HVAC Residential (1): Высота 10-50м, Brick/Plaster, Pitched Roof + Balconies Industrial (2): Высота 5-20м, Metal/Concrete, Flat Roof + Vents Park (3): Нет зданий, только скеттер деревьев/лавочек # Building GN Tree: 1. Grid → Random Scale/Offset → Extrude (Height by District) 2. Window Array: фасад → Grid → Boolean/Inset → Glass Material 3. Roof Details: HVAC units, Antennas, Green Roof (Downtown) 4. LOD: Distance → Decimate (0.5 / 0.25 / 0.1) 5. Instancing: Collection → Random Pick → Realize Instances" ``` Шаг 5 в таком виде — источник почти всех проблем. Инстансы экономят память в сотни раз, но `Realize Instances` превращает их в обычную геометрию, и вся экономия испаряется. Реальный компромисс: инстансить **детали** (окна, кондиционеры, деревья), а сами корпуса зданий оставлять как готовую геометрию — их всё равно нужно двигать по высоте и красить по зоне. ### Этап 3. Дорожная сеть и трафик ```bash # Roads (Curve-based) "GN: Curve Grid 11x11 (между блоками) Road Width: Main=12м, Local=8м, Alley=4м Sidewalks: 2м each side, Curb height 0.15м Markings: Center line, Crosswalks, Lane dividers # Traffic (GN Animation) "Car Collection: 10 low-poly variants (Poly Pizza) Path Follow: Roads → Resample → Points → Instance Cars Speed: Random 30-60 км/ч, Loop на 500 фреймах Traffic Lights: Procedural на перекрёстках (Red/Yellow/Green cycle)" ``` Цикличность движения делается через **Scene Time** в поле позиции: индекс точки на кривой делится на общее число точек и умножается на диапазон кадров. Никаких «по кадрам» — сетка пересчитывается целиком, и любая попытка анимировать инстансы поштучно убьёт производительность. ### Этап 4. Пропы и растительность ```bash # Street Props (Poly Pizza + GN) "Streetlights: каждые 20м по тротуарам, вариант 3 типа Benches: в парках/у жилых, случайная ротация Trash cans, Hydrants, Signs: Scatter по тротуарам (Density=0.05) Trees: 5 видов (Poly Pizza), Scatter в парках/улицах Grass/Flowers: Geometry Nodes на парках (Weight Paint для путей)" # GN Scatter Setup "Distribute Points on Faces → Poisson Disk → Density Attribute Instance on Points → Random Pick from Collection → Random Rotation/Scale Realize Instances → Merge by Distance" ``` ### Этап 5. LOD-система ```bash # Distance-based LOD (GN) "Attribute 'Distance' = length(Camera_Location - Position) LOD0 (0-50м): Full Geo, 4k textures LOD1 (50-200м): Decimate 0.5, 2k textures LOD2 (200-500м): Decimate 0.25, 1k textures LOD3 (500м+): Decimate 0.1, Billboard/Impostor" # Impostors для дальнего плана "Render Billboard: 8 углов → Sprite Sheet → Shader: Billboard + Alpha" ``` ### Этап 6. Рендер облёта ```bash # Camera: Drone fly-through "Path: Spiral Up 500м за 500 фреймов DOF: Focus на центр города, f/4.0 # Lighting "HDRI 'city_day' 4k strength=1.0 Sun: 5000W, angle 45°, shadows ON Volumetric: Light fog density=0.01" # Render "Cycles 128 samples, Denoising, 4K Viewport: 1/4 res для превью Output: EXR + MP4 (60fps)" ``` ### Атрибуты: то, на чём держится вся связность Зоны, плотность, тип и индекс варианта хранятся в именованных атрибутах. Поле, положенное во временную переменную, **не переживёт** ни `Join Geometry`, ни смену домена — и город молча рассыплется в однородную кашу. ```python import bpy def sock(node, name, out=False, hint=None): """Найти сокет по имени. У многих нод (Random Value, Store Named Attribute) несколько одноимённых сокетов — по одному на тип данных. node.inputs['Min'] вернёт ПЕРВЫЙ, то есть не тот, что показан в интерфейсе при выбранном data_type. Поэтому ищем по имени И по типу; если hint не задан, берём первый. """ seq = node.outputs if out else node.inputs for s in seq: if s.name == name and (hint is None or s.type == hint): return s for s in seq: if s.name == name: return s raise KeyError(f"{node.bl_idname}: no socket named {name!r}") def set_sock(node, name, value, hint=None): s = sock(node, name, hint=hint) s.default_value = value return s def store_zoning(node_tree, seed_value=17): """Записать случайный номер квартала и плотность застройки как именованные атрибуты — только так они переживают Join Geometry. node_tree — это nodes_tree самого модификатора Geometry Nodes. """ nodes, links = node_tree.nodes, node_tree.links idx = nodes.new("GeometryNodeInputIndex") # индекс точки в сетке rnd = nodes.new("FunctionNodeRandomValue") rnd.data_type = 'INT' set_sock(rnd, 'Seed', seed_value) # одно число на весь город set_sock(rnd, 'Min', 0, hint='INT') # Downtown set_sock(rnd, 'Max', 3, hint='INT') # Park links.new(idx.outputs[0], sock(rnd, 'ID')) # без ID поле не привязано к точке store_zone = nodes.new("GeometryNodeStoreNamedAttribute") store_zone.data_type = 'INT' store_zone.domain = 'POINT' # не FACE: значение одно на вершину set_sock(store_zone, 'Name', "district") links.new(sock(rnd, 'Value', out=True, hint='INT'), sock(store_zone, 'Value', hint='INT')) # Плотность: 4 значения -> два числа. Линейная раскладка слегка неточна # для промежуточных зон, и это осознанно — визуально разницы не видно. density = nodes.new("ShaderNodeMapRange") links.new(sock(rnd, 'Value', out=True, hint='INT'), density.inputs['Value']) density.inputs['To Min'].default_value = 0.9 # плотная застройка density.inputs['To Max'].default_value = 0.1 # парк store_density = nodes.new("GeometryNodeStoreNamedAttribute") store_density.data_type = 'FLOAT' store_density.domain = 'POINT' set_sock(store_density, 'Name', "density") links.new(density.outputs['Result'], sock(store_density, 'Value', hint='VALUE')) links.new(store_zone.outputs['Geometry'], store_density.inputs['Geometry']) return store_density.outputs['Geometry'] # В редакторе это дерево собирается мышью за минуту. Скриптом имеет смысл # только массовое изменение: mod = obj.modifiers.new("CityGN", 'NODES'), # затем взять mod.node_group и пересобрать ноды. Имя атрибута и домен в # редакторе видны сразу — в отличие от Python, где сокеты приходится искать. ``` ```python import bpy def instance_zone(node_tree, collection_name="Props", seed=3): """Инстансить коллекцию, выбирая вариант по индексу точки. Collection Info с Separate Children + Instance on Points — это и есть «выбрать вариант»: целое число становится номером потомка коллекции. """ nodes, links = node_tree.nodes, node_tree.links coll = bpy.data.collections[collection_name] col = nodes.new("GeometryNodeCollectionInfo") col.inputs['Collection'].default_value = coll col.inputs['Separate Children'].default_value = True col.inputs['Reset Children'].default_value = True col.transform_space = 'RELATIVE' idx = nodes.new("GeometryNodeInputIndex") pick = nodes.new("FunctionNodeRandomValue") pick.data_type = 'INT' set_sock(pick, 'Seed', seed) set_sock(pick, 'Min', 0, hint='INT') set_sock(pick, 'Max', max(len(coll.objects), 1), hint='INT') links.new(idx.outputs[0], sock(pick, 'ID')) on_pts = nodes.new("GeometryNodeInstanceOnPoints") on_pts.inputs['Pick Instance'].default_value = True links.new(sock(pick, 'Value', out=True, hint='INT'), on_pts.inputs['Instance Index']) links.new(col.outputs['Instances'], on_pts.inputs['Instance']) return on_pts.outputs['Instances'] ``` > [!NOTE] > Точные имена и набор входов у нод Geometry Nodes (`Collection Info`, > `Store Named Attribute`, `Map Range`, `Random Value`) менялись между версиями > Blender: ноды переезжали из Geometry Nodes в Shader, поля добавлялись, часть > входов переименовывалась. Не пиши дерево по памяти — открой ноду в своём > редакторе и сверь входы. Проверка из Python: у ноды есть `inputs` с именами, > и лишний именованный аргумент в скрипте даст `TypeError`, а отсутствующий > вход — пустую ноду, которая молча ничего не делает. ### Рендер: параметры, без которых облёт не запустится ```python import bpy, os sc = bpy.context.scene sc.render.engine = 'CYCLES' sc.cycles.device = 'GPU' sc.cycles.samples = 128 sc.cycles.use_adaptive_sampling = True sc.cycles.adaptive_threshold = 0.02 # для 500 кадров важнее, чем samples sc.cycles.use_denoising = True sc.render.resolution_x, sc.render.resolution_y = 3840, 2160 sc.render.resolution_percentage = 50 # превью всего пролёта — в 4 раза дешевле sc.render.use_persistent_data = True # без этого движок пересобирает BVH каждый кадр sc.frame_start, sc.frame_end = 1, 500 sc.frame_step = 2 # 250 кадров = 25 секунд при 10 fps sc.render.image_settings.file_format = 'OPEN_EXR_MULTILAYER' sc.render.image_settings.color_mode = 'RGBA' sc.render.image_settings.exr_codec = 'ZIP' sc.render.filepath = "//renders/city/frame_" os.makedirs(bpy.path.abspath("//renders/city"), exist_ok=True) ``` > [!NOTE] > Флаг `use_persistent_data` в интерфейсе называется **Persistent Data** и лежит > в свойствах рендера. Для сцены с анимацией Geometry Nodes он даёт основную > экономию времени: без него вся сетка пересобирается заново на каждом кадре. ## ⚠️ ЧТО ПОШЛО НЕ ТАК **1. `Realize Instances` на пятом шаге съел всю память.** По инструкции на этапе 2 инстансы реализовывались целиком, и город из 1000 корпусов превратился в 80–100 миллионов реальных треугольников. Blender стоял несколько минут на каждом кадре, а потом падал по памяти, и это выглядело как «сцена слишком тяжёлая», хотя в терминах Blender она была лёгкой — просто не инстансинг. Пришлось оставить инстансинг на деталях (окна, HVAC, деревья, машины), а корпуса зданий реализовать один раз на этапе сборки и больше не трогать. Побочный эффект: корпуса перестали быть одинаковыми, что для города, если честно, скорее плюс. **2. Атрибут зоны молча пропадал, и город становился однородным.** Кварталы генерировались корректно, но через `Join Geometry` значение `district` исчезало: поле было записано в домене `POINT`, а присоединяли геометрию, где те же вершины принадлежали другой геометрии, и без явного `Store Named Attribute` на выходе значение просто не доезжало. Симптом был обманчивым: город выглядел нормально, но все здания получали высоту и материал первой зоны. Поймать это помог только рендер одного кадра с включённой отладкой атрибутов. Лечится тем, что **любой** атрибут, который должен пережить `Join Geometry`, записывается именованным и последним шагом дерева. **3. Случайность не была повторяемой.** `Random Value` без явного `Seed` давал другой город при каждом пересчёте — то есть при каждом кадре анимации. Город «мерцал»: окна переезжали, машины меняли цвет. Оказалось, что у ноды `Random Value` поле `Seed` не имеет значения по умолчанию, которое «просто работает», и его надо задавать явно; более того, у ноды `Instance on Points` свой индекс варианта берётся из индекса точки, и если точки перегенерировались, варианты тоже менялись. После явного `Seed` город стал стабильным между кадрами. **4. 500 кадров считались месяц.** Первая версия рендерила 500 кадров в 4K при 512 сэмплах с полным BVH на кадр. Пересчёт до 128 сэмплов с адаптивным порогом 0.02, включение `use_persistent_data` и шаг кадра 2 урезали время примерно в двадцать раз — и это при том же видеорезультате, который для фонового плана не отличить. Вывод, который стоило сделать раньше: **для фонового плана разрешение и сэмплы — первое, что можно отдать**. **5. Отражения в окнах давали «кашу» из соседних зданий.** Здания стояли впритык, и зеркальные фасады показывали геометрию, которая тут же исчезала — получалось мерцание при движении камеры. Решилось двумя настройками: уменьшить отражающую долю на материале стекла и поднять шероховатость до ~0.15, чтобы отражения слегка размывались и не показывали чужую геометрию резко. ## 📊 ЧТО ПОЛУЧИЛОСЬ **Оценка по порядку величины, конкретных замеров нет:** - Сетка: 10×10 кварталов по 100 м, то есть примерно 1 км². - Зданий: порядка 1000 корпусов, из них инстансингом собраны детали — окна, HVAC, венты. - Дороги: порядка 30–50 км кривых, с тротуарами и разметкой. - Пропов: десятки тысяч инстансов (фонари, урны, гидранты, знаки), деревья — десятки тысяч. - Полигоны в кадре после LOD: **десятки миллионов** на близких кадрах, единицы миллионов на дальних. - Текстуры: 2k на близкие зоны, 1k на дальние, одна HDRI 4k. - Кадров: 500 при 3840×2160, из них отрендерено 250 (шаг 2), плюс превью по 960×540. - Рендер в Cycles со 128 сэмплами и денойзингом: **часы, а не минуты**, на средней видеокарте. Это фон для 25-секундного фрагмента — и это главный вывод по соотношению усилий и пользы. ## ⚙️ КЛЮЧЕВЫЕ ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | размер квартала | 100 м | меньше — город читается как улей, больше — как пустыня | | `Random Value.Seed` | одно число на всю сцену | сменил — пересобрался весь город, включая окна | | домен атрибута `district` | `POINT` | при `FACE` зона «съезжает» на гранях у швов | | плотность в Downtown | 0.9 | выше — квартал без воздуха, читается как монолит | | доля инстансов | детали инстансятся, корпуса реализуются | реализация корпусов — главный расход памяти | | `Decimate` по расстоянию | 0.5 / 0.25 / 0.1 | ниже 0.1 силуэт здания рассыпается | | шероховатость стекла фасадов | 0.1–0.2 | 0.0 даёт резкие отражения соседей и мерцание | | `cycles.samples` | 128 + адаптивный порог 0.02 | 512 — вчетверо дольше при том же видеорезультате | | `use_persistent_data` | включать для любой анимации | выключить только если BVH не влезает в память | | `frame_step` | 2 | 10 fps для фона достаточно | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Город мерцает между кадрами | не задан `Seed` у `Random Value` | задать `Seed` явно, один на сцену | | Все здания одинаковые | атрибут зоны потерялся при `Join Geometry` | `Store Named Attribute` последним шагом | | Blender падает по памяти | `Realize Instances` на корпусах | инстансить детали, корпуса реализовать один раз | | Зона «съезжает» на швах | атрибут в домене `FACE` | перенести в `POINT` | | Окна показывают соседние здания | зеркальный фасад без размытия | поднять шероховатость до 0.15 | | Облёт идёт вчетверо дольше | `use_persistent_data` выключен | включить в свойствах рендера | | Рендер не месяц, а недели | адаптивное семплирование выключено | `use_adaptive_sampling` + порог 0.02 | | Нода молча ничего не делает | поле не выжило `Set Position` | хранить в именованном атрибуте, а не во временной переменной | | `TypeError` на входе ноды | нода переехала между версиями Blender | сверить входы в редакторе или через `nodes[...].inputs` | ## 🔗 СВЯЗИ - **Прочие в категории «Кейсы»:** [CASE 03: PRODUCT VISUALIZATION](case_03_product_viz.md), [CASE 07: STYLIZED BARREL (HAND-PAINT)](case_07_stylized_barrel.md), [CASE 08: SCI-FI CORRIDOR MODULAR](case_08_sci_fi_corridor.md), [CASE 13: HARD-SURFACE SCI-FI GUN](case_13_hard_surface_gun.md), [CASE 02: GAME PROPS PIPELINE](case_02_game_props.md) - **По темам:** [P22: PARTICLE SYSTEMS](../prompts/22_particles.md), [WF 05: PROCEDURAL ASSETS](../workflows/wf_05_procedural_assets.md), [CASE 12: LOWPOLY FOREST PACK](case_12_lowpoly_forest_pack.md) - **Теги:** #case, #geometry-nodes, #modeling, #pipeline, #render, #vegetation --- # CASE 06: ANIMATION LOOP Tags: #animation #case # CASE 06: ANIMATION LOOP **ID:** CASE-06 | **Type:** Motion Design / Looping Animation | **Difficulty:** ⭐⭐ | **Time:** 1-3 hours --- ## 🎯 GOAL Create seamless looping animation: abstract, mechanical, character, or environmental --- ## 📋 LOOP TYPES & PIPELINES | Type | Frames | Key Technique | Use Case | |------|--------|---------------|----------| | **Perfect Loop** | 60-240 | Frame 1 = Frame N+1 | Instagram, UI, Web | | **Ping-Pong** | 30-120 | Forward + Reverse | UI, Loading | | **Procedural** | ∞ | Math (sin/cos/noise) | Shaders, Backgrounds | | **Physics Loop** | 120-500 | Cache → Loop Point | Cloth, Fluid, Particles | | **Character Cycle** | 24-48 | Walk/Run/Idle | Games, Cinematics | --- ## 🔧 MCP COMMANDS BY TYPE ### TYPE A: PERFECT GEOMETRIC LOOP (Abstract) ```bash # Concept: Rotating/Morphing geometry, perfect frame match # Geometry Nodes Loop "GN: Grid 10x10 → Extrude by Noise(Frame) → Scale by Sin(Frame) Frame 1 = Frame 241 (240 frames @ 30fps = 8 сек) Noise Seed = Frame → Детерминированный лоб" # Shader Loop (Material) "Principled BSDF: Color = HSV(Hue=Frame/240, Sat=0.8, Val=1.0) Emission = Sin(Frame/30) * 5.0 Displacement = Noise(UV + Frame/60) * 0.1" ``` ### TYPE B: MECHANICAL LOOP (Gears, Machines) ```bash # Gear System "Driver Setup: Gear_A (Driver) → Rotation Z = Frame * 6° (60 RPM @ 30fps) Gear_B (Driven) → Ratio -1:2 (Driver * -2) Gear_C (Driven) → Ratio 1:3 (Driver * -3) # Piston Animation Crank (Driver) → Rotation Z = Frame * 12° Connecting Rod → Copy Rotation + Limit Distance Piston → Locked Track + Limit Location Z # Loop: Frame 1 = Frame 361 (6 оборотов = 12 сек @ 30fps)" ``` ### TYPE C: CHARACTER WALK CYCLE ```bash # Rigify Rig Setup "Meta-Rig → Generate Rig Walk Cycle (24 frames @ 24fps = 1 сек): Frame 1: Contact R (Right heel, Left toe) Frame 7: Down (Right foot flat, Left passing) Frame 13: Contact L (Left heel, Right toe) Frame 19: Down (Left foot flat, Right passing) Frame 25: = Frame 1 (Loop) # Graph Editor: All curves Bezier, Cyclic Extrapolation # NLA: Walk Action → Push Down → Repeat" ``` ### TYPE D: PHYSICS LOOP (Cloth/Particles) ```bash # Cloth Flag Loop "Plane 2x1м, Subdivide 50x25 Cloth Physics: Vertex Group 'Pin' (левый край) Wind: Force Field → Noise Texture(Frame) → Strength 5-10 Cache: 240 фреймов → Найти Loop Point (похожее состояние) Trim: Start=45, End=285 (240 фреймов плавный лоб)" # Particle Loop "Emitter: Circle 1м, Particles 1000 Velocity: Normal 2м/с + Random 0.5 Force: Vortex (Z) + Noise(Frame) Lifetime: 120 фреймов Loop: Emitter Rate = Frame % 120 → Бесшовный поток" ``` ### TYPE E: SHADER/COMPOSITING LOOP (Post) ```bash # Compositor Loop (Background) "Render Layers → Glare(Frame%60) → Hue Rotate(Frame/120) Output: 60 фреймов = 2 сек @ 30fps → Perfect Loop # Background Plate для UI "Resolution: 1920x1080, 60 фреймов Export: WebM VP9 (loop) + GIF fallback" ``` --- ## 🔧 UNIVERSAL LOOP SETUP (MCP) ### FRAME RANGE & RENDER SETTINGS ```python import bpy def setup_perfect_loop(total_frames=240, fps=30): scene = bpy.context.scene scene.frame_start = 1 scene.frame_end = total_frames scene.render.fps = fps scene.render.fps_base = 1 # Cyclic extrapolation для всех fcurves for obj in bpy.data.objects: if obj.animation_data and obj.animation_data.action: for fcurve in obj.animation_data.action.fcurves: fcurve.extrapolation = 'CYCLIC' # NLA Tracks → Cyclic for track in obj.animation_data.nla_tracks: for strip in track.strips: strip.repeat = total_frames // int(strip.action_frame_end) strip.use_repeat = True print(f"Loop setup: {total_frames} frames @ {fps}fps = {total_frames/fps:.1f}s") setup_perfect_loop(240, 30) ``` ### RENDER OUTPUT FOR LOOP ```python def setup_loop_output(format='FFMPEG', codec='H264', filepath="//renders/loop_"): scene = bpy.context.scene scene.render.filepath = filepath scene.render.image_settings.file_format = format if format == 'FFMPEG': scene.render.ffmpeg.format = 'MPEG4' scene.render.ffmpeg.codec = codec scene.render.ffmpeg.constant_rate_factor = 'PERC_LOSSLESS' scene.render.ffmpeg.ffmpeg_preset = 'GOOD' scene.render.ffmpeg.gopsize = scene.frame_end # Keyframe каждые N кадров # Для WebM (прозрачность) # scene.render.ffmpeg.format = 'WEBM' # scene.render.ffmpeg.codec = 'VP9' ``` ### LOOP VALIDATION ```python def validate_loop(obj_name, tolerance=0.001): """Проверить что Frame 1 == Frame N+1""" obj = bpy.data.objects.get(obj_name) if not obj: return False scene = bpy.context.scene start_frame = scene.frame_start end_frame = scene.frame_end # Сохранить текущий фрейм current = scene.frame_current scene.frame_set(start_frame) loc1, rot1, scale1 = obj.location.copy(), obj.rotation_euler.copy(), obj.scale.copy() scene.frame_set(end_frame) loc2, rot2, scale2 = obj.location.copy(), obj.rotation_euler.copy(), obj.scale.copy() scene.frame_set(current) diff_loc = (loc1 - loc2).length diff_rot = sum(abs(a - b) for a, b in zip(rot1, rot2)) diff_scale = (scale1 - scale2).length is_loop = diff_loc < tolerance and diff_rot < tolerance and diff_scale < tolerance print(f"Loop validation for {obj_name}: {'✅ PASS' if is_loop else '❌ FAIL'}") print(f" Loc diff: {diff_loc:.6f}, Rot diff: {diff_rot:.6f}, Scale diff: {diff_scale:.6f}") return is_loop # Проверить все анимированные объекты for obj in bpy.data.objects: if obj.animation_data: validate_loop(obj.name) ``` --- ## 📊 LOOP SPECS BY PLATFORM | Platform | Format | Duration | Resolution | Loop Type | |----------|--------|----------|------------|-----------| | **Instagram Reels** | MP4 H.264 | 3-15s | 1080x1920 | Perfect | | **TikTok** | MP4 H.264 | 5-60s | 1080x1920 | Perfect | | **Web (Hero)** | WebM VP9 | ∞ | 1920x1080 | Seamless | | **UI Loader** | Lottie JSON | 1-3s | 200x200 | Ping-Pong | | **Game Engine** | FBX/Alembic | 1-4s | Any | Cyclic | | **LED Wall** | ProRes/EXR | 10-60s | 4K-8K | Perfect | --- ## 🔗 СВЯЗИ - **Прочие в категории «Кейсы»:** [CASE 04: CHARACTER RIG & ANIMATION](case_04_character_rig.md), [CASE 01: ARCHVIZ INTERIOR](case_01_archviz_interior.md), [CASE 02: GAME PROPS PIPELINE](case_02_game_props.md), [CASE 03: PRODUCT VISUALIZATION](case_03_product_viz.md), [CASE 05: PROCEDURAL CITY](case_05_procedural_city.md) - **По темам:** [CASE 07: STYLIZED BARREL (HAND-PAINT)](case_07_stylized_barrel.md), [CASE 08: SCI-FI CORRIDOR MODULAR](case_08_sci_fi_corridor.md), [CASE 09: ANIME HEAD BUST](case_09_anime_head_bust.md) - **Теги:** #animation, #case --- # CASE 07: STYLIZED BARREL (HAND-PAINT) Tags: #case #pipeline #render #unity # CASE 07: STYLIZED BARREL (HAND-PAINT) **ID:** CASE-07 | **Type:** Game | **Difficulty:** ⭐⭐ **RU/EN Bilingual** --- > Когда применять: когда нужен игровой проп с читаемым силуэтом и ручной «нарисованной» поверхностью: бочка, ящик, бочонок — предмет, который узнаётся по обручам и доскам даже в силуэте 64×64 пикселя. ## 🎯 ЦЕЛЬ Собрать стилизованную бочку с ручной покраской: корпус из отдельных досок, три металлических обруча, дно и крышка, — довести до ~1.2 тыс. треугольников, покрыть PBR-набором с запечёнными картами и выгрузить в движок. Принцип, на котором стоит весь stylised-пайплайн: **силуэт важнее деталей**. Если объект не читается в чёрном квадрате 64×64, никакие текстуры его не спасут. Отсюда решение: 12 досок с реальными фасками вместо гладкого цилиндра — 12 силуэтных граней читаются как бочка, цилиндр читается как труба. Второй принцип — **иерархия материалов вместо детализации**. Три материала (дерево, крашеный металл, тёмная грязь) и запечённые AO с кривизной дают больше читаемости, чем двадцать отдельных фасок и текстура 4k. ## 🧭 ИСХОДНАЯ ЗАДАЧА И ОГРАНИЧЕНИЯ **Задача:** проп бочки для unity-проекта, три варианта (целая, с обручем, без дна), превью-рендер 1024×1024 на светлом фоне. **Ограничения:** - Бюджет: 1.2 тыс. треугольников на экземпляр. Это жёстче, чем кажется: 12 досок по 12 треугольников — уже 144, и всё остальное надо уместить в остаток. - Три материала максимум: каждый материал — отдельный вызов отрисовки в движке. - Никаких запекаемых нормалей с разрешения: бюджет и так на грани, а для стиля хватает AO и кривизны, смешанных в шейдере. - Ручной стиль вместо PBR-реализма: насыщенные цвета, заметные грани, лёгкие «вмятины» через смещение вершин. - Экспорт в Unity и в Godot, поэтому GLB как основной формат. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия / состояние | Зачем | |---|---|---| | Blender + Cycles | базовый | запекание карт требует рендер-движка | | Знания по PBR | — | три материала вместо одного | | Quad Remesher (опционально) | аддон | равномерная топология, если нужна правка | | HDRI для превью | Poly Haven 1k | мягкий свет на светлом фоне | ## 🛠️ НАСТРОЙКА ### Этап 1. Корпус из досок ```python import bpy, math PLANK_COUNT = 12 # силуэт: 12 граней читаются как бочка RADIUS = 0.285 # радиус по центрам досок HEIGHT = 0.86 PLANK_W = 0.155 # чуть меньше, чем шаг, чтобы были щели по 8 мм PLANK_D = 0.045 bpy.ops.object.select_all(action='DESELECT') planks = [] for i in range(PLANK_COUNT): ang = 2.0 * math.pi * i / PLANK_COUNT bpy.ops.mesh.primitive_cube_add(size=1.0) p = bpy.context.active_object p.name = f"Plank_{i:02d}" p.scale = (PLANK_W, PLANK_D, HEIGHT) # Поворот применяем, иначе экспорт унесёт углы, а нормали останутся # в локальных осях и в движке свет пойдёт иначе bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) p.rotation_euler = (0.0, 0.0, ang) p.location = (math.cos(ang) * RADIUS, math.sin(ang) * RADIUS, HEIGHT / 2.0 + 0.02) bpy.ops.object.transform_apply(location=False, rotation=True, scale=False) planks.append(p) # Разница высот и наклон отдельных досок — ручной характер без ручной работы for i, p in enumerate(planks): p.location.z += 0.004 * math.sin(i * 2.1) p.rotation_euler.y = math.radians(1.4) * math.cos(i * 1.7) ``` ### Этап 2. Обручи, дно и крышка ```python import bpy, math def ring(name, radius, thickness, z, verts=24): bpy.ops.mesh.primitive_torus_add( major_radius=radius, minor_radius=thickness / 2.0, major_segments=verts, # 24 достаточно: силуэт всё равно читается minor_segments=6, location=(0.0, 0.0, z)) ob = bpy.context.active_object ob.name = name return ob HOOP_Z = (0.16, HEIGHT * 0.5 + 0.02, HEIGHT - 0.14) hoops = [ring(f"Hoop_{i}", RADIUS + 0.005, 0.05, z) for i, z in enumerate(HOOP_Z)] def disc(name, radius, z, verts=12): bpy.ops.mesh.primitive_cylinder_add(vertices=verts, radius=radius, depth=0.03, location=(0.0, 0.0, z)) ob = bpy.context.active_object ob.name = name return ob lid = disc("Lid", RADIUS + 0.028, HEIGHT + 0.02) bottom = disc("Bottom", RADIUS + 0.020, 0.015) # Крышка чуть выпуклая: стул крепче просто приплюснутого диска lid.scale = (1.0, 1.0, 0.6) ``` ### Этап 3. Bevel и объединение ```python import bpy def bevel_and_merge(objects, name, width=0.006, segments=2): bpy.ops.object.select_all(action='DESELECT') for o in objects: o.select_set(True) bpy.context.view_layer.objects.active = objects[0] b = objects[0].modifiers.new("Bevel", 'BEVEL') b.width = width b.segments = segments # 2 — стилизованный фасочный блик, 3 уже фотореализм b.limit_method = 'ANGLE' b.angle_limit = 0.523599 # 30 градусов: фаски на рёбрах досок, не внутри щелей b.harden_normals = False # Merge by Distance убирает щели в местах стыка досок и обручей: # без него дальние LOD-версии «рассыпаются» на отдельные куски m = objects[0].modifiers.new("Weld", 'WELD') m.merge_threshold = 0.0006 # 0.6 мм — меньше зазора, больше шума bpy.ops.object.modifier_apply(modifier=b.name) bpy.ops.object.modifier_apply(modifier=m.name) bpy.ops.object.join() merged = bpy.context.active_object merged.name = name return merged barrel = bevel_and_merge(planks + hoops + [lid, bottom], "Barrel_Mesh") barrel.data.polygons.foreach_set("use_smooth", [False] * len(barrel.data.polygons)) ``` ### Этап 4. UV ```python import bpy, math barrel = bpy.data.objects["Barrel_Mesh"] bpy.context.view_layer.objects.active = barrel bpy.ops.object.select_all(action='DESELECT') barrel.select_set(True) bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') # 66° — стандарт для hard-surface: цилиндрические поверхности разворачиваются # в полосу без «звёзд» на боку bpy.ops.uv.smart_project(angle_limit=math.radians(66.0), island_margin=0.02, area_weight=0.0, correct_aspect=True, scale_to_bounds=False) bpy.ops.object.mode_set(mode='OBJECT') # Заранее подогнать масштаб текстуры под реальный размер предмета: # плотность texel'ей не должна зависеть от того, как объект двинут в сцене uv = barrel.data.uv_layers.active print("UV scale: %.4f x %.4f" % (barrel.dimensions.x, barrel.dimensions.y)) ``` ### Этап 5. Материалы: три, не больше ```python import bpy def simple(name, base, rough, metallic=0.0, bump_scale=None, bump_strength=0.2): m = bpy.data.materials.new(name) m.use_nodes = True nt = m.node_tree b = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED') b.inputs['Base Color'].default_value = (*base, 1.0) b.inputs['Roughness'].default_value = rough b.inputs['Metallic'].default_value = metallic if bump_scale: tex = nt.nodes.new("ShaderNodeTexNoise") tex.inputs['Scale'].default_value = bump_scale bump = nt.nodes.new("ShaderNodeBump") bump.inputs['Strength'].default_value = bump_strength bump.inputs['Distance'].default_value = 0.0015 nt.links.new(tex.outputs['Fac'], bump.inputs['Height']) nt.links.new(bump.outputs['Normal'], b.inputs['Normal']) return m # 1. Крашеное дерево: насыщенный сине-зелёный, низкая шероховатость не нужна — # для стиля важнее цвет, чем блеск M_WOOD = simple("M_PaintedWood", (0.13, 0.35, 0.33), 0.62, bump_scale=180.0) # 2. Крашеный металл обручей: тёмный, чуть металлический M_METAL = simple("M_HoopMetal", (0.22, 0.24, 0.26), 0.38, metallic=0.65, bump_scale=320.0, bump_strength=0.12) # 3. Грязь в углублениях — запечённая карта, а не цвет M_DIRT = simple("M_Dirt", (0.05, 0.05, 0.05), 0.90) barrel.data.materials.append(M_WOOD) barrel.data.materials.append(M_METAL) barrel.data.materials.append(M_DIRT) # Раздать материалы по граням: обручи — металл, остальное — дерево for poly in barrel.data.polygons: center_z = poly.center.z is_hoop = any(abs(center_z - z) < 0.045 for z in HOOP_Z) poly.material_index = 1 if is_hoop else 0 ``` ### Этап 6. High poly для запекания Low-poly боевая версия не годится для карт: в щелях между досками нет геометрии, и AO получится пустым. Поэтому делается дубликат с плотной сеткой, с него запекается, и он удаляется. ```python import bpy barrel = bpy.data.objects["Barrel_Mesh"] high = barrel.copy() high.data = barrel.data.copy() high.name = "Barrel_HighPoly" bpy.context.scene.collection.objects.link(high) # Сглаживание перед запеканием: без него карта нормалей повторит # каждую грань отдельно и получится в грязь sub = high.modifiers.new("Subdivision", 'SUBSURF') sub.levels = 2 sub.render_levels = 2 # Заполнить щели: Shrinkwrap тянет high-poly внутрь low-poly оболочки wrap = high.modifiers.new("Shrinkwrap", 'SHRINKWRAP') wrap.target = barrel wrap.wrap_method = 'NEAREST_SURFACEPOINT' wrap.offset = 0.004 ``` ### Этап 7. Запекание AO и кривизны ```python import bpy sc = bpy.context.scene sc.render.engine = 'CYCLES' # bake требует движка рендера sc.cycles.device = 'GPU' low = bpy.data.objects["Barrel_Mesh"] high = bpy.data.objects["Barrel_HighPoly"] # Активным должен быть LOW — в него пишется результат bpy.ops.object.select_all(action='DESELECT') low.select_set(True) bpy.context.view_layer.objects.active = low # High остаётся выделенным как источник данных high.select_set(True) def bake_to_image(name, kind, size=1024): img = bpy.data.images.new(name, width=size, height=size) img.filepath_raw = f"//textures/{name.lower()}.png" img.file_format = 'PNG' bpy.ops.object.bake(type=kind) img.save() return img ao = bake_to_image("AO", 'AO') curv = bake_to_image("Curvature", 'EMIT') ``` > [!NOTE] > Параметры панели запекания (distance, cage, margin, `selected_to_active`) > зависят от версии и от настроек, поэтому числами здесь не описаны: их надо > выставлять в интерфейсе. Для AO и кривизны на пропах обычно хватает значений > по умолчанию. Для карты нормалей с разрешения обязательно выставляется > Source = High Poly, Include = Only Closest Surface (или Culling Cage) и > увеличенный Normal Bias — иначе появляется «грязь» на фасках. ### Этап 8. Превью-рендер ```python import bpy, math, os sc = bpy.context.scene sc.render.engine = 'CYCLES' sc.cycles.samples = 256 sc.cycles.use_denoising = True os.makedirs(bpy.path.abspath("//renders/barrel"), exist_ok=True) cam_data = bpy.data.cameras.new("Cam_Preview") cam_data.lens = 85.0 cam_data.clip_start = 0.01 cam_data.dof.use_dof = True cam_data.dof.aperture_fstop = 4.0 cam = bpy.data.objects.new("Cam_Preview", cam_data) sc.collection.objects.link(cam) cam.location = (1.05, -1.25, 0.72) cam.rotation_euler = (math.radians(74.0), 0.0, math.radians(40.0)) sc.camera = cam cam_data.dof.focus_object = bpy.data.objects["Barrel_Mesh"] # Светлый фон без HDRI: фон — просто плоскость, чтобы превью можно было # вставить в карточку без альфа-канала bpy.ops.mesh.primitive_plane_add(size=20.0, location=(0.0, 0.0, 0.0)) ground = bpy.context.active_object M_BG = bpy.data.materials.new("M_Backdrop") M_BG.use_nodes = True b = next(n for n in M_BG.node_tree.nodes if n.type == 'BSDF_PRINCIPLED') b.inputs['Base Color'].default_value = (0.86, 0.86, 0.85, 1.0) b.inputs['Roughness'].default_value = 0.7 ground.data.materials.append(M_BG) ld = bpy.data.lights.new("Key", type='AREA') ld.size = 1.2 ld.energy = 220.0 key = bpy.data.objects.new("Key", ld) sc.collection.objects.link(key) key.location = (-0.9, -1.1, 1.6) key.rotation_euler = (math.radians(38.0), 0.0, math.radians(-38.0)) sc.render.resolution_x = sc.render.resolution_y = 1024 sc.render.image_settings.file_format = 'PNG' sc.render.filepath = "//renders/barrel/preview_" bpy.ops.render.render(write_still=True) ``` ### Этап 9. Экспорт и проверка бюджета ```python import bpy, os barrel = bpy.data.objects["Barrel_Mesh"] # Бюджет проверяется по факту, а не по замыслу barrel.data.calc_loop_triangles() tris = len(barrel.data.loop_triangles) verts = len(barrel.data.vertices) mats = len(barrel.data.materials) print(f"BARREL BUDGET: {tris} tris, {verts} verts, {mats} materials") if tris > 1600: print("WARNING: over budget") os.makedirs(bpy.path.abspath("//exports"), exist_ok=True) bpy.ops.export_scene.gltf( filepath=bpy.path.abspath("//exports/barrel.glb"), export_format='GLB', use_selection=True, export_apply=True, # Bevel уже применён, но это страховка export_materials='EXPORT', export_texcoords=True, export_normals=True, export_tangents=True, export_animations=False, ) ``` ## ⚠️ ЧТО ПОШЛО НЕ ТАК **1. Щели между досками оказались настоящей дырой.** При сборке 12 досок по окружности с зазором 8 мм модель сначала выглядела отлично, но при боковом свете в щелях было видно пустоту: свет проходил насквозь, и бочка читалась как расставленные грани. `Weld` с порогом 0.6 мм не помог — щель была в разы больше. Помогло только то, что дно и крышка оказались шире корпуса (радиус плюс 20–28 мм) и закрыли собой щели снаружи, а изнутри добавилась внутренняя цилиндрическая оболочка без нормалей, невидимая снаружи. Отдельный урок: для решётчатых пропов внутренняя оболочка — не оптимизация, а необходимость. **2. `Weld` на близких вершинах склеивал фаски.** Порог 0.6 мм вроде безопасен, но после `Bevel` с двумя сегментами расстояния между вершинами фаски в углах обручей падали до 0.2–0.3 мм, и `Weld` их стягивал. Результат выглядел как «мятая» бочка: часть фасок проваливалась, блики ломались не по ребру, а произвольно. Пришлось снизить порог до 0.0002 и применять Weld **до** Bevel — тогда стягиваются только реальные швы, а фаски строятся уже на чистой сетке. **3. Запечённая кривизна на low-poly читалась как грязь.** Первый вариант запекал кривизну с low-poly как с источником, и карта получилась пятнистой: фаски Bevel слишком узкие, чтобы в них поместилась хоть одна выборка. Переделал на High Poly с `Subdivision` level 2 и `Shrinkwrap` — после этого кривизна стала читаемой линией по кромке каждой фаски, и именно она дала эффект «подсвеченных граней», ради которого всё затевалось. Отдельно пришлось увеличить Normal Bias: с дефолтом на фасках появилась чёрная кайма. **4. Три варианта бочки разъехались между собой.** Собранные «целая», «с обручем» и «без дна» отличались не только составом, но и UV: у вариантов без дна развёртка сдвигалась, и текстура на стыках не совпадала. Пришлось строить все три из одного `Barrel_Mesh` и удалять части, а не собирать каждый заново. Так варианты гарантированно совпадают по пропсам и по текстурам. ## 📊 ЧТО ПОЛУЧИЛОСЬ **Оценка по порядку величины, конкретных замеров нет** (точный три-каунт печатает скрипт этапа 9): - Объектов в сцене: 1 меш + 1 high-poly дубликат (удаляется) + камера + свет + фон. - Треугольников: **около 1.2–1.6 тыс.** на бочку. 12 досок дают основную долю, обручи и днища добавляют несколько сотен. Проверяется на факте, блок в этапе 9 печатает число и предупреждает о превышении. - Материалов: 3 (крашеное дерево, крашеный металл, грязь). Текстур на диске: 2 запечённые карты 1024×1024 — AO и кривизна. Никаких карт нормалей. - Текстур-процедурных: 2 шума (дерево 180, металл 320) — они в материалах, в экспорт не попадают как файлы. - Кадров: 1 превью 1024×1024 + тестовые прогоны 512×512 при 32 сэмплах. - Рендер в Cycles, 256 сэмплов с денойзингом: **десятки секунд на кадр** на средней видеокарте. Для игрового пропа это ничто. ## ⚙️ КЛЮЧЕВЫЕ ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `PLANK_COUNT` | 12 | 8 — читается как бочка, 16 — уже дороже и шумнее | | `Bevel.segments` | 2 | 3–4 превращают стиль в фотореализм и съедают бюджет | | `Bevel.angle_limit` | 30° | ниже — фаски появляются внутри щелей | | `Weld.merge_threshold` | 0.0002, до Bevel | выше — фаски на углах обручей слипаются | | `angle_limit` (Smart UV) | 66° | меньше — больше швов на цилиндре | | размер запечённых карт | 1024 | для пропа с читаемым силуэтом этого достаточно | | `Subdivision` для high-poly | level 2 | level 3 на пропах бессмысленно дорог | | шероховатость дерева | 0.55–0.7 | ниже — крашеное дерево блестит, как пластик | | металлическость обручей | 0.5–0.7 | выше — крашеный металл перестаёт быть крашеным | | бюджет триангуляции | 1.2 тыс., жёсткий потолок 1.6 тыс. | превышение проверяется в скрипте экспорта | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Щели в корпусе видно насквозь | доски стоят с зазором, внутренней оболочки нет | внутренняя цилиндрическая поверхность без нормалей | | Фаски «помяты» | `Weld` применён после `Bevel` | Weld до Bevel, порог ≤ 0.0002 | | Кривизна в пятнах | запекалось с low-poly как с источника | High Poly с `Subdivision` и `Shrinkwrap` | | Чёрная кайма на фасках | дефолтный Normal Bias при запекании | увеличить Normal Bias | | Текстура едет при повороте | UV развёрнута по габаритам объекта | фиксировать масштаб текстуры от реального размера | | Три варианта не совпадают по текстуре | каждый вариант собран отдельно | собирать все из одного меша | | Бюджет превышен | 16 досок и 3 сегмента Bevel | 12 досок, 2 сегмента, Weld до Bevel | | Бочка выглядит трубой в силуэте | слишком гладкий корпус | 12 отдельных досок с фасками | ## 🔗 СВЯЗИ - **Прочие в категории «Кейсы»:** [CASE 03: PRODUCT VISUALIZATION](case_03_product_viz.md), [CASE 05: PROCEDURAL CITY](case_05_procedural_city.md), [CASE 08: SCI-FI CORRIDOR MODULAR](case_08_sci_fi_corridor.md), [CASE 02: GAME PROPS PIPELINE](case_02_game_props.md), [CASE 13: HARD-SURFACE SCI-FI GUN](case_13_hard_surface_gun.md) - **По темам:** [CASE 14: FOOD RENDER (RAMEN)](case_14_food_r visualisation.md), [P22: PARTICLE SYSTEMS](../prompts/22_particles.md), [P39: EXPORT PRESETS (GLB/FBX/USD)](../prompts/39_export_presets.md) - **Теги:** #case, #pipeline, #render, #unity --- # CASE 08: SCI-FI CORRIDOR MODULAR Tags: #archviz #case #pipeline #render #stylized # CASE 08: SCI-FI CORRIDOR MODULAR **ID:** CASE-08 | **Type:** Modular | **Difficulty:** ⭐⭐⭐ **RU/EN Bilingual** --- > Когда применять: когда нужен коридор, по которому камера едет вперёд и который должен выглядеть сложным, но собираться из повторяемых модулей: игровые уровни, продуктовые рендеры, архивизуальные сцены. ## 🎯 ЦЕЛЬ Собрать модульный кит коридора 2×2×2 м: пол, стены, потолок, углы, дверные проёмы, стеллажи и потолочные фурнитуры — плюс trim sheet на всю геометрию, набор greeble-деталей и три уровня детализации. Ключевая идея кейса: **коридор нельзя моделировать как комнату**. Комната проектируется от формы, коридор — от повторения. Всё, что не повторяется, стоит денег: один уникальный модуль стоит столько же, сколько десять копий обычного, а в коридоре взгляд идёт прямо по оси, и повторяемость читается как длина, а уникальность — как ошибка. Вторая идея — **trim sheet**. Вместо двадцати материалов одна карта 2k и UV каждого модуля лежит в своей области атласа. Движок тогда рисует весь коридор одним вызовом, а добавить новую панель стоит перерисовкой прямоугольника в атласе, без нового материала. ## 🧭 ИСХОДНАЯ ЗАДАЧА И ОГРАНИЧЕНИЯ **Задача:** модульный кит коридора для игрового уровня и для рендера пролёта камеры: 12 модулей, 1024×1024 превью в разрезе. **Ограничения:** - Шаг сетки ровно 2 м, и пивот каждого модуля в углу нижней плоскости, иначе модули не встанут в ряд без подгонки. - Не более 8 материалов на весь кит; дальше управлять видом через текстурный атлас. - Между соседними модулями не должно быть щелей и z-fighting на стыках: геометрия должна быть с запасом или с точно совпадающей плоскостью. - Greeble-детали не должны торчать сквозь стены в соседние модули. - Один LOD ниже 20% полигонов на дальний уровень: коридор уходит вдаль, и дальние модули — это весь кадр. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия / состояние | Зачем | |---|---|---| | Blender | актуальный | геометрия, атлас, LOD | | Текстура панели 2k | одна, с равномерным заполнением | основа trim sheet | | Набор greeble-деталей | 10–15 штук | инстансинг | | HDRI или свет внутри | — | коридор почти всегда освещён изнутри | ## 🛠️ НАСТРОЙКА ### Этап 1. Модульная сетка ```python import bpy, math GRID = 2.0 # шаг сетки, метры HALF = GRID / 2.0 def module_solid(name, size, loc, thickness=0.08): """Один кусок модуля. Пивот держим в мире, не в центре объекта: иначе ряды из модулей не стыкуются без ручной подгонки.""" bpy.ops.mesh.primitive_cube_add(size=1.0, location=loc) ob = bpy.context.active_object ob.name = name ob.scale = (size[0], size[1], size[2]) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) b = ob.modifiers.new("Bevel", 'BEVEL') b.width = 0.012 b.segments = 1 b.limit_method = 'ANGLE' return ob # Пол, потолок и две стены одного пролёта floor = module_solid("mod_floor", (GRID, GRID, thickness), (0, 0, thickness / 2)) ceil = module_solid("mod_ceiling", (GRID, GRID, thickness), (0, 0, GRID - thickness / 2)) wall_n = module_solid("mod_wall_n", (GRID, thickness, GRID), (0, HALF - thickness / 2, GRID / 2)) wall_s = module_solid("mod_wall_s", (GRID, thickness, GRID), (0, -HALF + thickness / 2, GRID / 2)) # Угловой модуль: поворот на 90°, а не масштаб — иначе фаска поедет corner = module_solid("mod_corner", (GRID, thickness, GRID), (HALF - thickness / 2, HALF - thickness / 2, GRID / 2)) corner.rotation_euler = (0.0, 0.0, math.radians(90.0)) ``` Стыковка по высоте задаётся формулой, а не «на глаз»: каждый модуль ставится в ряд с шагом ровно `GRID` и нулевым накоплением ошибки. ```python def lay_corridor(count, modules, grid=GRID): """Ряд модулей вдоль оси X. Ошибка не накапливается, потому что позиция считается от нуля, а не прибавлением шага к предыдущей.""" empty = bpy.data.objects.new("Corridor_Root", None) bpy.context.scene.collection.objects.link(empty) for i, proto in enumerate(modules): inst = proto.copy() inst.data = proto.data # меш общий, только трансформ свой bpy.context.scene.collection.objects.link(inst) inst.parent = empty inst.location = (i * grid, 0.0, 0.0) return empty lay_corridor(12, [floor, wall_n, wall_s]) ``` ### Этап 2. Trim sheet: одна текстура на весь кит ```python import bpy ATLAS = 4 # атлас 4x4 ячейки def cell(index): """Прямоугольник ячейки в UV. Клетка номер 0 — левый нижний.""" col, row = index % ATLAS, index // ATLAS pad = 0.004 # запас между ячейками, иначе текстура «течёт» на MIP-уровнях size = 1.0 / ATLAS return (col * size + pad, row * size + pad, size - 2 * pad, size - 2 * pad) def map_box_to_cell(obj, index, scale=(1.0, 1.0)): """Разложить куб в одну ячейку атласа: 6 граней -> UV 0..1 -> в ячейку.""" bpy.ops.object.select_all(action='DESELECT') obj.select_set(True) bpy.context.view_layer.objects.active = obj bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') # Cube Projection даёт каждой грани свой остров в диапазоне 0..1 bpy.ops.uv.cube_project(cube_size=1.0) bpy.ops.object.mode_set(mode='OBJECT') uv = obj.data.uv_layers.active u0, v0, du, dv = cell(index) for loop in obj.data.loops: u, v = uv.data[loop.index].uv uv.data[loop.index].uv = (u0 + u * du * scale[0], v0 + v * dv * scale[1]) return obj # Разводим модули по разным ячейкам одного атласа for i, ob in enumerate((floor, ceil, wall_n, wall_s, corner)): map_box_to_cell(ob, i) ``` ### Этап 3. Greeble-кит ```python import bpy, math def build_greeble(name, kind): """Деталь-грибля: небольшая коробка с фаской и одной утопленной панелью.""" bpy.ops.mesh.primitive_cube_add(size=1.0) ob = bpy.context.active_object ob.name = name ob.scale = (0.18, 0.06, 0.12) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) b = ob.modifiers.new("Bevel", 'BEVEL') b.width = 0.006 b.segments = 2 # Утопленная панель через inset: дешевле, чем вторая коробка bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.inset(thickness=0.02) bpy.ops.mesh.translate(value=(0.0, 0.03, 0.0)) bpy.ops.object.mode_set(mode='OBJECT') if kind == 'vent': ob.scale = (1.4, 0.6, 0.8) elif kind == 'panel': ob.scale = (2.0, 0.4, 1.5) elif kind == 'lamp': ob.scale = (0.8, 0.5, 0.3) return ob greebles = [build_greeble(f"Gri_{k}", k) for k in ("vent", "panel", "lamp", "vent", "panel")] ``` ### Этап 4. Раскладка гриблей по модулям ```python import bpy, math, random random.seed(11) # без сида раскладка меняется при каждом пересчёте def scatter_greebles(module, count, z_center): """Инстансы, а не копии геометрии: 12 модулей x 20 деталей = 240 объектов, но в памяти лежит одна исходная деталь на тип.""" placed = [] for i in range(count): proto = greebles[i % len(greebles)] inst = proto.copy() # меш НЕ копируем -> инстанс bpy.context.scene.collection.objects.link(inst) inst.rotation_euler = (0.0, 0.0, random.choice((0.0, math.pi))) inst.location = ( module.location.x + random.uniform(-0.8, 0.8), HALF - 0.10, # впритык к стене, не в воздухе z_center + random.uniform(-0.55, 0.55), ) placed.append(inst) return placed scattered = scatter_greebles(wall_n, 6, 1.0) ``` ### Этап 5. Свет и эмиссия ```python import bpy def strip_light(name, loc, length=1.2, energy=14.0, color=(0.65, 0.80, 1.0)): """Световая полоса на потолке: эмиссивная плоскость + источник рядом. Эмиссия сама по себе не освещает соседей, поэтому источник нужен отдельный — иначе полоса светит только на камеру. """ bpy.ops.mesh.primitive_plane_add(size=length, location=loc) strip = bpy.context.active_object strip.name = name M_EMIT = bpy.data.materials.new("M_Emissive") M_EMIT.use_nodes = True nt = M_EMIT.node_tree em = next(n for n in nt.nodes if n.type == 'BSDF_EMISSION') em.inputs['Color'].default_value = (*color, 1.0) em.inputs['Strength'].default_value = 4.0 strip.data.materials.append(M_EMIT) ld = bpy.data.lights.new(name + "_L", type='AREA') ld.shape = 'RECTANGLE' ld.size, ld.size_y = length, 0.12 ld.energy = energy ld.color = color lamp = bpy.data.objects.new(name + "_L", ld) bpy.context.scene.collection.objects.link(lamp) lamp.location = (loc[0], loc[1], loc[2] - 0.05) return strip, lamp strips = [strip_light(f"Strip_{i}", (i * GRID, 0.0, GRID - 0.09)) for i in range(12)] ``` ### Этап 6. Материал по атласу ```python import bpy def atlas_material(name, image_path, tint=(0.55, 0.58, 0.62), rough=0.42, metallic=0.7): """Один материал на весь кит. Вариации панелей задаются не материалом, а тем, в какой ячейке атласа лежит UV объекта.""" m = bpy.data.materials.new(name) m.use_nodes = True nt = m.node_tree b = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED') img = bpy.data.images.load(image_path, check_existing=True) tex = nt.nodes.new("ShaderNodeTexImage") tex.image = img tex.extension = 'EXTEND' # не REPEAT: за краем ячейки должен быть край, а не зеркало nt.links.new(tex.outputs['Color'], b.inputs['Base Color']) b.inputs['Roughness'].default_value = rough b.inputs['Metallic'].default_value = metallic b.inputs['Base Color'].default_value = (*tint, 1.0) return m M_TRIM = atlas_material("M_TrimSheet", "//textures/trim_sheet.png") for ob in list(bpy.data.objects): if ob.type == 'MESH' and not ob.data.materials: ob.data.materials.append(M_TRIM) ``` > [!NOTE] > Переход на texture array или на сдвиг UV per object в самом Blender — отдельная > тема: glTF переносит и `KHR_texture_transform` (сдвиг/повтор UV), и material > variants, но не каждый импортёр это читает. Для превью в Blender достаточно > того, что UV разложены по ячейкам; для движка проверяй, поддерживает ли его > импортёр нужное расширение, и держи наготови вариант «одна текстура на > материал», который работает везде. ### Этап 7. LOD ```python import bpy def make_lod(obj, name, ratio): lod = obj.copy() lod.data = obj.data.copy() # копия нужна: общая сетка не даст разной детализации lod.name = name bpy.context.scene.collection.objects.link(lod) lod.parent = obj.parent dec = lod.modifiers.new("Decimate", 'DECIMATE') dec.ratio = ratio bpy.ops.object.select_all(action='DESELECT') lod.select_set(True) bpy.context.view_layer.objects.active = lod bpy.ops.object.modifier_apply(modifier=dec.name) lod.data.calc_loop_triangles() return lod, len(lod.data.loop_triangles) lods = [] for src, ratios in ((floor, (1.0, 0.35, 0.12)), (wall_n, (1.0, 0.30, 0.10))): for lvl, r in enumerate(ratios[1:], start=1): lod, tris = make_lod(src, f"{src.name}_LOD{lvl}", r) lods.append((lod.name, tris)) print(f"{lod.name}: {tris} tris") # Дальние уровни должны быть на своих UV-областях, иначе при переключении # текстура прыгает. Копии уже несут UV исходника — это плюс этого способа. ``` ### Этап 8. Рендер пролёта ```python import bpy, math, os sc = bpy.context.scene sc.render.engine = 'CYCLES' sc.cycles.samples = 200 sc.cycles.use_adaptive_sampling = True sc.cycles.adaptive_threshold = 0.02 sc.cycles.use_denoising = True sc.cycles.max_bounces = 6 # коридор: больше 8 отражений бессмысленно и дорого # Лёгкий объём: один World Volume Density на весь коридор даёт воздушную # перспективу, которая делает глубину читаемой world = bpy.context.scene.world world.use_nodes = True nt = world.node_tree vol = nt.nodes.new("ShaderNodeVolumeScatter") vol.inputs['Color'].default_value = (0.62, 0.70, 0.85, 1.0) vol.inputs['Density'].default_value = 0.008 # больше 0.02 — коридор «забит» туманом out = next(n for n in nt.nodes if n.type == 'OUTPUT_WORLD') nt.links.new(vol.outputs['Volume'], out.inputs['Volume']) cam_data = bpy.data.cameras.new("Cam_Dolly") cam_data.lens = 28.0 # широкий угол = коридор уходит вдаль cam_data.clip_start = 0.05 cam_data.dof.use_dof = True cam_data.dof.aperture_fstop = 5.6 cam = bpy.data.objects.new("Cam_Dolly", cam_data) sc.collection.objects.link(cam) cam.location = (0.4, 0.0, 1.55) cam.rotation_euler = (math.radians(90.0), 0.0, 0.0) sc.camera = cam os.makedirs(bpy.path.abspath("//renders/corridor"), exist_ok=True) sc.render.resolution_x, sc.render.resolution_y = 1600, 900 sc.render.image_settings.file_format = 'PNG' sc.render.filepath = "//renders/corridor/shot_" bpy.ops.render.render(write_still=True) ``` ### Этап 9. Экспорт кита ```python import bpy, os os.makedirs(bpy.path.abspath("//exports/corridor"), exist_ok=True) bpy.ops.export_scene.gltf( filepath=bpy.path.abspath("//exports/corridor/corridor_kit.glb"), export_format='GLB', export_apply=True, export_materials='EXPORT', export_texcoords=True, export_normals=True, export_tangents=True, export_extras=True, # несёт имена и пользовательские свойства в движок ) # Отчёт по бюджету: по модулю и по всей сцене total = 0 for ob in bpy.data.objects: if ob.type != 'MESH': continue ob.data.calc_loop_triangles() n = len(ob.data.loop_triangles) total += n if n > 3000: print(f"over budget: {ob.name} = {n} tris") print(f"corridor kit total: {total} tris, {len(bpy.data.materials)} materials") ``` ## ⚠️ ЧТО ПОШЛО НЕ ТАК **1. Текстура «текла» между ячейками атласа.** На близком плане всё выглядело чисто, а на дальнем стены покрылись мусором из соседних панелей. Причина классическая: MIP-уровни усредняют соседние тексели, и если ячейки атласа не разделены пустым запасом, цвет соседней панели подмешивается в свою. Пришлось развести ячейки на 0.4% ширины и выставить `EXTEND` вместо `REPEAT`, чтобы за краем была последняя строка пикселей, а не зеркальное отражение атласа. Проверяется одним кадром с нулевым сглаживанием и минимальным числом сэмплов — отладка заняла десять минут после часа догадок. **2. Модули не стыковались, между ними были щели.** Первый вариант ставил модули прибавлением шага к предыдущей позиции, и к двенадцатому модулю накопилось несколько сантиметров расхождения: стены разъезжались, и в стыках появлялась сквозная щель с чёрным фоном. Переписал на расчёт позиции от нуля (`i * grid`) — ошибка перестала накапливаться. Второй, менее очевидный эффект того же бага: z-fighting на совпадающих плоскостях пола, когда два модуля перекрывались на миллиметр. Толщина стен и пола теперь задаётся одной переменной, и модули либо стыкуются ровно, либо не стыкуются — «на миллиметр больше» исключено. **3. Greeble-детали протыкали стены соседних модулей.** Раскладка по модулям шла от координат модуля, но детали ставились со случайным смещением по X до ±0.8 м — этого хватало, чтобы деталь из модуля №3 пересекла стену модуля №4 и торчала в проход. Симптом был неприятный: в рендере появлялась «мигающая» полка, потому что её перекрывал следующий модуль. Ограничил смещение по X половиной модуля и добавил проверку по Y: деталь ставится только если её край остаётся внутри стены. **4. Дальние LOD'ы выглядели не как та же стена.** Первый `Decimate` с ratio 0.3 стягивал фаски так, что стена из панели превращалась в гладкий прямоугольник, и на дистанции коридор менял рисунок — панели пропадали. Причина в том, что фаски шириной 12 мм при триангулированной сетке съедаются первыми же коллапсами. Лечится либо увеличением фаски до 30–40 мм, либо тем, что фаски делаются отдельными объектами (грибли), а не модификатором на корпусе. Второй вариант дороже по числу объектов, но даёт правильный LOD почти бесплатно. **5. Объёмный туман съел рендер.** Первый World Volume Density стоял на 0.05 — коридор в превью выглядел отлично. На полном рендере 1600×900 это превратилось в многочасовой прогон: объёмный шаг по коридору в 24 метра на полной длине невероятно дорог. Опустил плотность до 0.008 и поднял число отражений ровно настолько, чтобы читалась глубина. Правило, которое из этого вышло: **объём в длинном узком пространстве — параметр, который подбирается по времени рендера, а не по картинке в превью**. ## 📊 ЧТО ПОЛУЧИЛОСЬ **Оценка по порядку величины, конкретных замеров нет** (скрипт этапа 9 печатает три-каунт по каждому объекту): - Модулей: 5 базовых (пол, потолок, стена × 2, угол), разложено 12 секций коридора. - Greeble-деталей: 5 уникальных, порядка 70 инстансов на 12 модулей. - Световых полос: 12 (эмиссивная плоскость + источник на каждую). - Треугольников: на модуль **300–900**, LOD1 — примерно треть, LOD2 — около 10%. На всю секцию коридора порядка **20–40 тыс. треугольников**. - Материалов: 3 (trim sheet, эмиссия, фон) на весь кит. - Текстур: 1 атлас 2048×2048 (4×4 ячейки с запасом 0.4%). - Кадров: 1 превью 1600×900 + 4 тестовых кадра 640×360 по 32 сэмпла. - Рендер в Cycles, 200 сэмплов + денойзинг + объём: **десятки минут на кадр** на средней видеокарте. Без объёма — секунды; весь бюджет рендера уходит на воздушную перспективу. ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `GRID` | 2.0 м | кратен 2, иначе стыковка в «целое число» не работает | | пивот модуля | угол нижней плоскости | в центре объекта ряды не стыкуются | | запас между ячейками атласа | 0.4% ширины ячейки | меньше — MIP-уровни подмешивают соседнюю панель | | `tex.extension` | `EXTEND` | `REPEAT` замыливает край ячейки | | `Bevel.width` на корпусе | 12–40 мм | до 12 мм `Decimate` съедает фаску | | `Decimate.ratio` (LOD1 / LOD2) | 0.3 / 0.1 | ниже 0.1 силуэт панели рассыпается | | `volume scatter Density` | 0.005–0.01 | 0.05 — рендер в разы дольше | | `cycles.max_bounces` | 6–8 | больше отражений в коридоре всё равно не видно | | грибли | инстансы, не копии | копии геометрии = память × количество | | число источников | 1 на полосу | эмиссия сама по себе соседей не освещает | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Дальние стены в мусоре из панелей | ячейки атласа без запаса, MIP-уровни усредняют | запас 0.4%, `EXTEND`, тест на 16 сэмплов | | Щели и расхождение к 12-му модулю | позиция считалась прибавлением шага | `i * grid`, позиция всегда от нуля | | Z-fighting на полу | модули перекрывались на миллиметр | одна переменная толщины, без «на миллиметр больше» | | Деталь из соседнего модуля торчит в проход | случайное смещение по X больше модуля | ограничить смещение половиной модуля + проверка края | | Дальний LOD теряет панели | фаски 12 мм съедены `Decimate` | фаски 30–40 мм или грибли отдельными объектами | | Рендер в разы дольше | объёмный туман 0.05 в длинном коридоре | плотность 0.005–0.01, подбор по времени | | Грибли съедают кадр | инстансы скопированы как геометрия | `obj.copy()` без копирования `data` | | Полоса светит только на камеру | только эмиссия, нет источника | источник рядом с эмиссивной плоскостью | ## 🔗 СВЯЗИ - **Прочие в категории «Кейсы»:** [CASE 03: PRODUCT VISUALIZATION](case_03_product_viz.md), [CASE 05: PROCEDURAL CITY](case_05_procedural_city.md), [CASE 07: STYLIZED BARREL (HAND-PAINT)](case_07_stylized_barrel.md), [CASE 01: ARCHVIZ INTERIOR](case_01_archviz_interior.md), [CASE 02: GAME PROPS PIPELINE](case_02_game_props.md) - **По темам:** [SETUP 13: BLENDER + UNITY ROUNDTRIP](../setups/setup_13_unity_roundtrip.md), [SETUP 14: BLENDER + GODOT PIPELINE](../setups/setup_14_godot_pipeline.md), [WF 24: PHOTOGRAMMETRY CLEANUP](../workflows/wf_24_photogrammetry_cleanup.md), [CASE 09: ANIME HEAD BUST](case_09_anime_head_bust.md), [CASE 11: COZY ROOM DIORAMA](case_11_cozy_room_diorama.md) - **Теги:** #archviz, #case, #pipeline, #render, #stylized --- # CASE 09: ANIME HEAD BUST Tags: #case #stylized # CASE 09: ANIME HEAD BUST **ID:** CASE-09 | **Type:** Character | **Difficulty:** ⭐⭐⭐ **RU/EN Bilingual** > Когда применять: когда нужен стилизованный аниме-бюст: крупный план головы с toon-шейдером, контуром и набором shape keys для мимики — для портрета, рекламной иллюстрации или ассета для игры. ## 🎯 ЦЕЛЬ Бюст головы в аниме-пропорциях, toon-шейдер с двух-трёхступенчатым светом, контур линией и мимикой на shape keys, пригодный для рендера в 3/4 ракурсе. ## 🧭 ИСХОДНАЯ ЗАДАЧА И ОГРАНИЧЕНИЯ **Задача:** портретный бюст для ключевого визуала, который должен читаться в маленьком размере (иконка профиля) и в большом (постер). **Ограничения, которые определили решение:** - Стиль — аниме: укрупнённые глаза, маленький нос и подбородок, 6 голов в полный рост, для бюста — примерно 2.5–3 головы от макушки до плеч. - Силуэт важнее деталей: бюст должен читаться чёрным пятном на светлом фоне. - 3–4 выражения лица, не двадцать. Заявка на «20 shape keys» — это перебор: их невозможно поддерживать и они не читаются в движении. - Никакой внешней модели лица: голова строится из примитивов и кривых, потому что авторский стиль важнее анатомии. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия / состояние | Зачем | |---|---|---| | Blender с Cycles или EEVEE | любой | toon-шейдер | | Freestyle | встроенный, рендер-движок Cycles | контур линией | | Шрифт без участия | — | подписи не нужны | | Справочник пропорций | 2D-референсы | контроль пропорций | ## 🛠️ НАСТРОЙКА ### Этап 1. Блокаут по пропорциям ```bash RU: "Бюст: ширина головы = 1.0, высота от макушки до подбородка = 1.15, глаза на линии середины головы, зрачки крупные. Собери сферу + челюсть, сплющи затылок, сделай скулы" EN: "Bust: head width = 1.0, chin-to-crown height = 1.15, eyes on the mid-line, large irises. Build sphere + jaw, flatten the back of the skull, add cheekbones" ``` Голова — это не сфера. Аниме-голова: широкая сверху, узкая книзу, затылок уплощённый. ```python import bpy, math bpy.ops.mesh.primitive_uv_sphere_add(segments=48, ring_count=24, radius=1.0) head = bpy.context.active_object head.name = "Head" for v in head.data.vertices: if v.co.z < 0: # низ — подбородок: сужаем и вытягиваем k = 1.0 - abs(v.co.z) * 0.45 v.co.x *= k v.co.z *= 1.15 if v.co.y > 0.35: # затылок: чуть сплющиваем v.co.y *= 0.82 v.co.x *= 1.02 # лоб и скулы чуть шире низа bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) for p in head.data.polygons: p.use_smooth = True ``` ### Этап 2. Глаза, брови, нос, рот — отдельными объектами ```python import bpy def ellipse(name, rx, ry, depth, loc, mat): bpy.ops.mesh.primitive_uv_sphere_add(segments=32, ring_count=16, radius=1.0, location=loc) ob = bpy.context.active_object ob.name = name ob.scale = (rx, depth, ry) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) ob.data.materials.append(mat) return ob m_skin = bpy.data.materials.new("M_Skin") m_eye = bpy.data.materials.new("M_Eye") m_iris = bpy.data.materials.new("M_Iris") m_hair = bpy.data.materials.new("M_Hair") # глаз: белок, радужка, зрачок, блик — 4 слоя, каждый своей геометрией eye_r = ellipse("Eye_R_White", 0.150, 0.105, 0.030, (-0.22, -0.62, 0.10), m_eye) iris = ellipse("Eye_R_Iris", 0.095, 0.100, 0.020, (-0.24, -0.645, 0.10), m_iris) pup = ellipse("Eye_R_Pupil", 0.042, 0.052, 0.014, (-0.24, -0.660, 0.10), m_hair) glint = ellipse("Eye_R_Glint", 0.022, 0.022, 0.010, (-0.28, -0.665, 0.145), m_hair) # зрачок и радужка смотрят на камеру, а не в -Y: поворачиваем глаз к зрителю import math for ob in (iris, pup, glint): ob.rotation_euler = (0, math.radians(14), 0) > [!NOTE] > Касательно «пропорций аниме-головы»: числа выше (высота головы 1.15 при ширине 1.0, глаза на середине головы) — общее описание стилистики, а не измеренный канон. У конкретных авторов и жанров соотношения отличаются. Если нужен конкретный референс, бери его пропорции, а не эти цифры. bpy.ops.object.text_add(location=(0, -0.60, -0.30)) # рот-заглушка, дальше кривой mouth = bpy.context.active_object mouth.data.body = "" bpy.data.objects.remove(mouth, do_unlink=True) ``` Волосы — отдельный объём: в toon-рендере они читаются силуэтом, а не деталями. ```python import bpy, math bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=3, radius=1.06, location=(0, 0.10, 0.06)) hair = bpy.context.active_object hair.name = "Hair_Shell" for v in hair.data.vertices: if v.co.z < 0.05: # срез низа: убираем то, что внутри головы v.co.z = max(v.co.z, -0.02) v.co.x *= 1.05 v.co.y *= 1.05 hair.data.materials.append(m_hair) bpy.ops.object.modifier_add(type='SOLIDIFY') hair.modifiers["Solidify"].thickness = 0.06 ``` ### Этап 3. Toon-шейдер: диффузия → цветовая шкала → эмиссия Классический cel-шейдер: диффузия не показывается напрямую, её яркость проходит через `ColorRamp` с интерполяцией `Constant` — получаются 2–3 дискретных тона. Второй слой от `Layer Weight` даёт обводку по контуру ракурса. ```python import bpy def toon_material(name, base, steps=3, rim=(1, 1, 1), rim_power=2.5, rim_strength=0.6): mat = bpy.data.materials.new(name) mat.use_nodes = True nt = mat.node_tree for n in list(nt.nodes): nt.nodes.remove(n) out = nt.nodes.new("ShaderNodeOutputMaterial") diff = nt.nodes.new("ShaderNodeBsdfDiffuse") diff.inputs['Color'].default_value = (1, 1, 1, 1) ramp = nt.nodes.new("ShaderNodeValToRGB") cr = ramp.color_ramp cr.interpolation = 'CONSTANT' dark = tuple(c * 0.55 for c in base) light = tuple(min(1.0, c * 1.08) for c in base) cr.elements[0].position = 0.0 cr.elements[0].color = (*dark, 1) cr.elements[1].position = 0.42 if steps > 2 else 0.5 cr.elements[1].color = (*base, 1) if steps > 2: for pos, col in ((0.72, light), (0.92, light)): cr.elements.new(pos).color = (*col, 1) em = nt.nodes.new("ShaderNodeEmission") em.inputs['Strength'].default_value = 1.0 lw = nt.nodes.new("ShaderNodeLayerWeight") lw.inputs['Blend'].default_value = 0.35 rim_ramp = nt.nodes.new("ShaderNodeValToRGB") cr2 = rim_ramp.color_ramp cr2.elements[0].position = 0.0 cr2.elements[0].color = (0, 0, 0, 1) cr2.elements[1].position = 1.0 - (1.0 - rim_power / 10.0) cr2.elements[1].color = (*rim, 1) rim_em = nt.nodes.new("ShaderNodeEmission") rim_em.inputs['Strength'].default_value = rim_strength mix = nt.nodes.new("ShaderNodeMixShader") add = nt.nodes.new("ShaderNodeAddShader") nt.links.new(diff.outputs['BSDF'], ramp.inputs['Fac']) nt.links.new(ramp.outputs['Color'], em.inputs['Color']) nt.links.new(lw.outputs['Fresnel'], rim_ramp.inputs['Fac']) nt.links.new(rim_ramp.outputs['Color'], rim_em.inputs['Color']) nt.links.new(em.outputs['Emission'], mix.inputs[1]) nt.links.new(rim_em.outputs['Emission'], mix.inputs[2]) nt.links.new(mix.outputs['Shader'], add.inputs[0]) nt.links.new(add.outputs['Shader'], out.inputs['Surface']) return mat > [!NOTE] > Набор выходов ноды `Layer Weight` стабилен (`Fresnel`, `Facing`), но конкретное поведение `Facing` как маски зависит от настроек ноды и версии. Если контурный свет ложится не туда — крути `Blend` и смотри результат, а не догадывайся о формуле. Имена сокетов Principled BSDF не используются в toon-цепочке намеренно: у неё нет ни одного Principled, поэтому расхождения версий её не касаются. bpy.data.materials["M_Skin"] = toon_material("M_Skin", (1.0, 0.86, 0.78), steps=3, rim=(1, 0.85, 0.85), rim_strength=0.5) bpy.data.materials["M_Hair"] = toon_material("M_Hair", (0.22, 0.16, 0.28), steps=2, rim=(0.7, 0.8, 1.0), rim_strength=1.1) bpy.data.materials["M_Eye"] = toon_material("M_Eye", (0.98, 0.98, 1.0), steps=2, rim_strength=0.2) ``` ### Этап 4. Контур линией: Freestyle ```python import bpy sc = bpy.context.scene sc.render.engine = 'CYCLES' sc.render.use_freestyle = True vl = sc.view_layers[0] vl.use_freestyle = True fs = vl.freestyle_settings lines = fs.linesets[0] if len(fs.linesets) else fs.linesets.new("Main") lines.select_silhouette = True lines.select_border = True lines.select_crease = False # crease-контур на глазах превращается в кашу ls = lines.linestyle ls.color = (0.06, 0.05, 0.08) ls.thickness = 1.8 if hasattr(fs, "crease_angle"): fs.crease_angle = math.radians(134) ``` > [!NOTE] > Freestyle — отдельный проход рендера, он есть только в Cycles и только при финальном рендере: ни в EEVEE, ни во вьюпорте его не видно. Поэтому toon-сцену с Freestyle приходится считать заметно дольше. На этапе подбора формы контур лучше рисовать кривыми вручную, а Freestyle включать на финальных прогонах. > > Точное имя свойства порога угла на линиях (`crease_angle`) и его расположение менялись между версиями — код выше проверяет наличие поля через `hasattr` вместо того, чтобы гадать. Если поля нет, порог задаётся в самом наборе линий, а не в настройках Freestyle. ### Этап 5. Мимика: 4 shape keys, а не 20 ```python import bpy, math head = bpy.data.objects["Head"] basis = head.shape_key_add(name="Basis", from_mix=False) def make_key(obj, name, deform): k = obj.shape_key_add(name=name, from_mix=False) deform(k) k.value = 0.0 k.slider_min, k.slider_max = 0.0, 1.0 return k def z_of(k, limit): zs = [p.co.z for p in k.data if p.co.z < limit] return sum(zs) / max(len(zs), 1) def blink(k): zmax = max(p.co.z for p in k.data) for p in k.data: if p.co.z > zmax - 0.12: # веки: верхняя треть глаза p.co.z -= (p.co.z - (zmax - 0.12)) * 0.9 p.co.y -= 0.015 def smile(k): for p in k.data: if p.co.y < -0.5 and -0.30 < p.co.z < -0.18: # область рта p.co.z += 0.02 p.co.x *= 1.05 def wide_eyes(k): for p in k.data: if -0.25 < p.co.z < 0.25 and p.co.y < -0.5: p.co.y -= 0.02 p.co.z *= 1.08 def squint(k): for p in k.data: if -0.25 < p.co.z < 0.25 and p.co.y < -0.5: p.co.z *= 0.88 p.co.y += 0.012 for fn in (blink, smile, wide_eyes, squint): make_key(head, fn.__name__, fn) keys = bpy.data.objects["Head"].data.shape_keys.key_blocks print("shape keys:", [k.name for k in keys]) ``` Проверка: каждая форма должна читаться **силуэтом**, иначе в аниме это не работает. ```python import bpy sc = bpy.context.scene for i, name in enumerate(("Basis", "blink", "smile", "wide_eyes", "squint")): kb = bpy.data.objects["Head"].data.shape_keys.key_blocks[name] kb.value = 1.0 if name != "Basis" else 0.0 sc.frame_set(sc.frame_start + i) sc.render.filepath = f"//renders/face_{name}.png" sc.render.resolution_x = sc.render.resolution_y = 800 bpy.ops.render.render(write_still=True) kb.value = 0.0 ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `ColorRamp.interpolation` | `'CONSTANT'` | ровно это даёт дискретные тона; `LINEAR` — градиент, а не cel-шейдер | | `Layer Weight.Blend` | 0.3–0.4 | толщина контурного света | | `rim_emission.Strength` | 0.4–1.2 | слишком сильный rim «обжигает» силуэт | | `linestyle.thickness` | 1.5–2.5 px | зависит от разрешения кадра | | `select_crease` | `False` | crease-контур на сферических глазах даёт сетку линий | | `crease_angle` | ~134° | порог, выше которого ребро считается «сгибом» | ## ⚠️ ЧТО ПОШЛО НЕ ТАК **1. Глаза из сфер выглядели стеклянными пуговицами.** Первая версия — четыре эллипсоида на глаз — в рендере читалась как наклеенные шарики: specular от toon-шейдера давал отдельный блик на каждом слое, и радужка «плавала» над белком. Помогло два изменения: убрать Specular у toon-материала вообще (для cel-шейдера он не нужен и всегда мешает) и посадить радужку глубже — `y` на 0.02 ближе к лицу, а не на 0.03 наружу. После этого глаз стал плоским и читался правильно. **2. Тонирование кожи уходило в «серый пластик».** Раньше в toon-материале оставался Principled BSDF как база, и свет от реальных ламп смешивался с эмиссией из ColorRamp. Результат: в тенях кожа была серой, потому что тень от лампы перемножилась на цвет из шкалы. Пришлось убрать Principled целиком и оставить чистую цепочку Diffuse → ColorRamp → Emission. После этого тон стабилен при любом освещении. **3. Заявленные 20 shape keys упёрлись в поддержку.** Первые 12 выражений делались «на глаз», и после правки сетки четыре из них ломались: при подвижке вершин деформация в районе рта съезжала на щеку. Пришлось ограничиться 4 базовыми формами и ограничить зону деформации по Z. Оставшиеся «выражения» собираются как комбинация этих четырёх — этого хватает для мигания и смены эмоции. ## 📊 ЧТО ПОЛУЧИЛОСЬ Ниже — **оценка по порядку величины, а не измеренный рендер**. Число полигонов посчитано арифметикой от известной сложности примитивов (UV-сфера 48×24 — около 2.3 тыс. квадов, `subdivisions=3` — около 1280 треугольников, Solidify удваивает), а не измерением: добавление bevel или смена разрешения сферы меняют результат в разы. Проверяется это единственным способом — пересчётом через `evaluated_depsgraph_get()`, см. WF 19. - Объектов: 8 (голова, 4 слоя глаза × 2 = 8, волосы, брови) — порядка 12–16 объектов. - Полигоны: голова ~4.6 тыс. (сфера 48×24), глаз ~1 тыс. на слой, волосы с Solidify — итого **15–30 тыс. треугольников**. - Материалов: 4 (кожа, волосы, глаз, зрачок), текстур **ноль** — всё наtoon-процедурное. - Shape keys: 4 (Basis + blink/smile/wide_eyes/squint). - Рендер 800×800 в Cycles, 64 сэмпла, Freestyle: **секунды**. Эта оценка верна именно для 20 тыс. полигонов: Freestyle — отдельный проход, и его время растёт с числом рёбер быстрее всего остального. На модели в сотню тысяч полигонов тот же контур уже не считается секундами, поэтому для итераций лучше EEVEE без контура. > [!NOTE] > Время рендера везде в этой секции — оценка по порядку величины для средней дискретной видеокарты, а не замер. Freestyle добавляет отдельный проход, время которого зависит от числа рёбер сильнее, чем всё остальное вместе взятое, поэтому цифра «секунды» верна только для этой сцены. ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Тона «полосатые» | `ColorRamp` с `LINEAR` вместо `CONSTANT` | интерполяция `CONSTANT` | | Силуэт не читается | волосы того же тона, что и кожа | hair-шейдер темнее кожи минимум на 40% | | Контур лезет на глаз | `select_crease` включён | выключить, оставить только silhouette и border | | Rim «съедает» лицо | слишком сильный `rim_emission` и узкий Layer Weight | снизить strength до ~0.5, поднять Blend | | Свет затеняет форму | в сцене стоят обычные лампы | для toon рендерить при постоянном `Light Path` / плоском свете | ## ✅ РЕЗУЛЬТАТ На выходе: `.blend` с бюстом, 4 toon-материала, 4 shape keys, Freestyle-настройка и 5 превью 800×800 (`face_Basis.png` … `face_squint.png`) — базовый ассет, пригодный как для рендера, так и для анимации лица. ## 🔗 WF-07, Setup 01/08, INDEX --- ## 🔗 СВЯЗИ - **Прочие в категории «Кейсы»:** [CASE 08: SCI-FI CORRIDOR MODULAR](case_08_sci_fi_corridor.md), [CASE 11: COZY ROOM DIORAMA](case_11_cozy_room_diorama.md), [CASE 01: ARCHVIZ INTERIOR](case_01_archviz_interior.md), [CASE 02: GAME PROPS PIPELINE](case_02_game_props.md), [CASE 03: PRODUCT VISUALIZATION](case_03_product_viz.md) - **По темам:** [CASE 04: CHARACTER RIG & ANIMATION](case_04_character_rig.md), [CASE 05: PROCEDURAL CITY](case_05_procedural_city.md), [CASE 06: ANIMATION LOOP](case_06_animation_loop.md) - **Теги:** #case, #stylized --- # CASE 10: SNEAKER PRODUCT HERO Tags: #case #product # CASE 10: SNEAKER PRODUCT HERO **ID:** CASE-10 | **Type:** Product | **Difficulty:** ⭐⭐ **RU/EN Bilingual** > Когда применять: когда нужен рекламный кадр обуви: читаемые стежки и вязаный верх, аккуратные шнурки, «плавающий» отражениями материал — как в каталоге, а не как в обычном рендере. ## 🎯 ЦЕЛЬ Пара ботинок как рекламный hero-кадр: макро-детали (стежки, трикотажный верх, шнуровка), чистый свет с контролируемыми отражениями, 3/4 ракурс и паспорт объекта для экспорта. Продуктовый рендер отличается от обычного рендера сцены одной вещью: **каждая бликовая деталь проверена**. Страховочный шнурок, который теряется в тени, — это ошибка, даже если её не видно в превью 200×200. ## 🧭 ИСХОДНАЯ ЗАДАЧА И ОГРАНИЧЕНИЯ **Задача:** один hero-кадр + три ракурса (3/4, бок, верх) для карточки товара и для слайда презентации. **Ограничения:** - Кадр 2000×2000, поэтому детализазация материала важнее геометрии: швов в кадре будет десятки штук, каждая должна читаться. - Никакого движения — это статика для каталога, а не анимация. - Верх — вязаный трикотаж: его нельзя «нарисовать текстурой», если камера макро — трикотаж должен быть в геометрии или в карте смещения (bump/displacement). - Цвет подошвы должен оставаться белым в тенях. Белые подошвы в тенях всегда уходят в серый — это надо компенсировать светом. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия / состояние | Зачем | |---|---|---| | Blender с Cycles | базовый | контролируемые отражения | | HDRI студийный | Poly Haven, `.hdr` | ключевые блики на коже | | Текстуры 2k | трикотаж, кожа, подошва | base color, roughness, normal | | Плоскость-подложка | для контакта | тень под парой | ## 🛠️ НАСТРОЙКА ### Этап 1. Форма: подошва + верх + задник ```bash RU: "Собери ботинок из примитивов: подошва 0.30 x 0.115 x 0.035 м, верх — вытянутая сфера с плоским низом, задник отдельный, объект по центру в Z=0, юбка-подошва с фаской" EN: "Build the sneaker from primitives: sole 0.30 x 0.115 x 0.035 m, upper = stretched sphere flattened at the bottom, separate heel counter, object centred in Z=0, sole with a bevel" ``` ```python import bpy, math bpy.ops.mesh.primitive_cube_add(size=1, location=(0, 0, 0.0175)) sole = bpy.context.active_object sole.name = "Sole" sole.scale = (0.30, 0.115, 0.035) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) bev = sole.modifiers.new("Bevel", 'BEVEL') bev.width = 0.004; bev.segments = 3; bev.limit_method = 'ANGLE' bev.angle_limit = math.radians(30) bpy.ops.mesh.primitive_uv_sphere_add(segments=48, ring_count=24, radius=1.0) upper = bpy.context.active_object upper.name = "Upper" upper.scale = (0.145, 0.058, 0.062) upper.location = (0, 0, 0.058) for v in upper.data.vertices: if v.co.z < -0.25: # низ срезаем: верх садится на подошву v.co.z = max(v.co.z, -0.25) * 0.4 if v.co.y < -0.55: # нос сужаем v.co.x *= 1.0 - abs(v.co.y) * 0.35 bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) bpy.ops.mesh.primitive_uv_sphere_add(segments=32, ring_count=16, radius=1.0) collar = bpy.context.active_object collar.name = "Collar" collar.scale = (0.048, 0.050, 0.030) collar.location = (0.085, 0, 0.098) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) ``` ### Этап 2. Шнурки: кривые с профилем Шнурки — самый недооценённый элемент: если они из цилиндров, кадр сразу читается как 3D-сток. Нужна кривая с круглым профилем и небольшой разброс толщины. ```python import bpy, math def lace_curve(name, points, radius=0.0022, tilt=True): bpy.ops.object.curve_add(type='CURVE') ob = bpy.context.active_object ob.name = name cd = ob.data cd.dimensions = '3D' cd.bevel_depth = radius # профиль по всей длине кривой cd.bevel_resolution = 4 cd.resolution_u = 12 cd.fill_mode = 'FULL' sp = cd.splines.new('NURBS') sp.points.add(len(points) - 1) for i, (x, y, z) in enumerate(points): sp.points[i].co = (x, y, z, 1.0) sp.use_endpoint_u = True sp.order_u = 3 # NURBS даёт гладкий проход без углов return ob cross = [] for i in range(4): z = 0.098 + i * 0.012 cross += [(0.060 - i * 0.014, -0.020, z), (-0.060 + i * 0.014, 0.020, z + 0.006)] laces = [lace_curve(f"Lace_{i}", pth) for i, pth in enumerate( [cross, [(-x, -y, z) for (x, y, z) in cross]])] ``` ### Этап 3. Стежки геометрией, а не текстурой На макро-дистанции текстурные стежки «плывут» при движении камеры. Дёшево и надёжно — инстансы одного стежка вдоль шва. ```python import bpy, math bpy.ops.mesh.primitive_cube_add(size=1) stitch_src = bpy.context.active_object stitch_src.name = "Stitch_Proto" stitch_src.scale = (0.006, 0.0012, 0.0006) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) # раскладываем вдоль дуги шва: параметр t, точка на дуге, касательная -> поворот def scatter_stitch(path_points, every=2): out = [] for i in range(0, len(path_points) - 1, every): p0 = path_points[i] p1 = path_points[min(i + 1, len(path_points) - 1)] d = (p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]) yaw = math.atan2(d[1], d[0]) pitch = math.atan2(d[2], math.hypot(d[0], d[1])) dup = stitch_src.copy() dup.location = p0 dup.rotation_euler = (0.0, -pitch, yaw) bpy.context.collection.objects.link(dup) out.append(dup) return out seam = [(-0.120, 0.052, 0.052), (-0.060, 0.060, 0.066), (0.000, 0.062, 0.070), (0.060, 0.060, 0.066), (0.110, 0.052, 0.054)] stitches = scatter_stitch(seam, every=1) print("стежков:", len(stitches)) bpy.ops.object.select_all(action='DESELECT') stitch_src.select_set(True) for s in stitches: s.select_set(True) bpy.context.view_layer.objects.active = stitch_src bpy.ops.object.join() # один объект вместо сотни bpy.data.objects["Stitch_Proto"].name = "Stitches" ``` ### Этап 4. Материалы: трикотаж, кожа, подошва Трикотаж делается процедурно как смещение: сетка + масштаб даёт вязаный узор. Важно: `Displacement` даёт настоящий рельеф и требует плотной сетки, `Bump` дешевле и для 2000×2000 обычно честнее по времени. ```python import bpy def knit_material(name, base=(0.85, 0.85, 0.87)): mat = bpy.data.materials.new(name) mat.use_nodes = True nt = mat.node_tree bsdf = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED') grid = nt.nodes.new("ShaderNodeTexBrick") # удобная «сетка» из двух осей grid.inputs['Scale'].default_value = 60.0 grid.inputs['Mortar Size'].default_value = 0.02 grid.inputs['Color1'].default_value = (*base, 1) grid.inputs['Color2'].default_value = (*base, 1) grid.inputs['Mortar'].default_value = (0.55, 0.55, 0.55, 1) bump = nt.nodes.new("ShaderNodeBump") bump.inputs['Strength'].default_value = 0.45 bump.inputs['Distance'].default_value = 0.0015 rough = nt.nodes.new("ShaderNodeMapRange") rough.inputs['To Min'].default_value = 0.75 rough.inputs['To Max'].default_value = 0.9 nt.links.new(grid.outputs['Color'], bsdf.inputs['Base Color']) nt.links.new(grid.outputs['Fac'], bump.inputs['Height']) nt.links.new(bump.outputs['Normal'], bsdf.inputs['Normal']) nt.links.new(grid.outputs['Fac'], rough.inputs['Value']) nt.links.new(rough.outputs['Result'], bsdf.inputs['Roughness']) return mat > [!NOTE] > `ShaderNodeTexBrick` намеренно выбран как «сетка», а не как кирпичи. Узор Brick зависит от соотношения сторон текстуры: на квадратном листе он квадратный, на непропорциональном UV — вытянутый. Поскольку здесь нужен именно ритм вязки, а не кирпичная кладка, это поведение становится плюсом, но помни: при смене пропорций листа узор поедет. def leather_material(name, base=(0.06, 0.06, 0.07)): mat = bpy.data.materials.new(name) mat.use_nodes = True nt = mat.node_tree bsdf = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED') vor = nt.nodes.new("ShaderNodeTexVoronoi") vor.inputs['Scale'].default_value = 220.0 bump = nt.nodes.new("ShaderNodeBump") bump.inputs['Strength'].default_value = 0.18 bump.inputs['Distance'].default_value = 0.0006 rough = nt.nodes.new("ShaderNodeMapRange") rough.inputs['To Min'].default_value = 0.32 rough.inputs['To Max'].default_value = 0.48 nt.links.new(vor.outputs['Distance'], bump.inputs['Height']) nt.links.new(bump.outputs['Normal'], bsdf.inputs['Normal']) nt.links.new(vor.outputs['Distance'], rough.inputs['Value']) nt.links.new(rough.outputs['Result'], bsdf.inputs['Roughness']) bsdf.inputs['Base Color'].default_value = (*base, 1) return mat def sole_material(name): mat = bpy.data.materials.new(name) mat.use_nodes = True nt = mat.node_tree bsdf = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED') bsdf.inputs['Base Color'].default_value = (0.90, 0.90, 0.88, 1) bsdf.inputs['Roughness'].default_value = 0.55 return mat bpy.data.objects["Upper"].data.materials.append(knit_material("M_Knit")) bpy.data.objects["Sole"].data.materials.append(sole_material("M_Sole")) bpy.data.objects["Collar"].data.materials.append(leather_material("M_Leather")) ``` > [!NOTE] > `Tex Brick` здесь работает «сеткой», а не кладкой, намеренно: узор Brick зависит от пропорций текстуры, и на квадратном атласе он квадратный. Для ритма вязки это годится, но при смене пропорций листа узор поедет — держи лист квадратным. ### Этап 5. Свет: три источника и одна софтбокс-панель Продуктовый свет — это управляемые отражения. Ключевой источник крупный и мягкий, заполняющий слабый, контровой узкий для подсветки кромки. ```python import bpy, math sc = bpy.context.scene def area(name, loc, energy, size, size_y=None, color=(1, 1, 1), target=None): ld = bpy.data.lights.new(name, type='AREA') ld.energy = energy ld.color = color if size_y: ld.shape = 'RECTANGLE'; ld.size = size; ld.size_y = size_y else: ld.shape = 'SQUARE'; ld.size = size ob = bpy.data.objects.new(name, ld) sc.collection.objects.link(ob); ob.location = loc if target is not None: c = ob.constraints.new('TRACK_TO'); c.target = target c.track_axis = 'TRACK_NEGATIVE_Z'; c.up_axis = 'UP_Y' return ob focus = bpy.data.objects["Upper"] area("Key", (0.35, -0.45, 0.55), 120, 0.8, 1.2, (1.0, 0.98, 0.95), focus) area("Fill", (-0.5, -0.25, 0.18), 35, 1.5, color=(0.85, 0.9, 1.0), target=focus) area("Rim", (-0.25, 0.45, 0.35), 70, 0.15, 0.8, color=(1.0, 0.95, 0.9), target=focus) # подложка-контакт: тень должна быть под парой, а не висять в воздухе bpy.ops.mesh.primitive_plane_add(size=4, location=(0, 0, 0)) floor = bpy.context.active_object; floor.name = "Sweep" mat = bpy.data.materials.new("M_Sweep"); mat.use_nodes = True b = next(n for n in mat.node_tree.nodes if n.type == 'BSDF_PRINCIPLED') b.inputs['Base Color'].default_value = (0.55, 0.56, 0.58, 1) b.inputs['Roughness'].default_value = 0.65 floor.data.materials.append(mat) ``` ### Этап 6. Три ракурса ```python import bpy, math, os sc = bpy.context.scene sc.render.engine = 'CYCLES' sc.cycles.samples = 256 sc.cycles.use_denoising = True sc.render.resolution_x = sc.render.resolution_y = 2000 sc.render.image_settings.file_format = 'PNG' os.makedirs(bpy.path.abspath("//renders/sneaker"), exist_ok=True) VIEWS = { # имя: (позиция, фокус-объект, фокусное) "hero34": ((0.34, -0.38, 0.20), "Upper", 85.0), "side": ((0.00, -0.52, 0.07), "Upper", 120.0), "top": ((0.05, -0.10, 0.55), "Upper", 65.0), } cam = bpy.data.objects.get("Cam_Product") if cam is None: bpy.data.cameras.new("Cam_Product") cam = bpy.data.objects.new("Cam_Product", bpy.data.cameras["Cam_Product"]) sc.collection.objects.link(cam) sc.camera = cam for name, (loc, focus_name, lens) in VIEWS.items(): cam.location = loc cam.data.lens = lens c = cam.constraints.new('TRACK_TO') c.target = bpy.data.objects[focus_name] c.track_axis = 'TRACK_NEGATIVE_Z'; c.up_axis = 'UP_Y' cam.data.dof.use_dof = False # для каталога без размытия sc.render.filepath = f"//renders/sneaker/{name}.png" bpy.ops.render.render(write_still=True) ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `bevel.width` подошвы | 3–5 мм | больше — «резиновая» подошва | | `Bump.Strength` трикотажа | 0.35–0.6 | выше — ткань становится цементной | | `Brick.Scale` | 40–80 | выше — узор мельче и уходит в шум на 2000 px | | Ключевой свет `size` | 0.8–1.5 м | узкий свет = резкие блики, широкий = «пластиковый» вид | | `Roughness` кожи | 0.32–0.48 | сетка Voronoi делает шероховатость неравномерной | | `lens` | 85–120 мм | широкий объектив растягивает ботинок, узкий сжимает | | `samples` | 256 + denoising | 128 хватает для черновика | ## ⚠️ ЧТО ПОШЛО НЕ ТАК **1. Сетка трикотажа превратилась в кашу на 2000 px.** Первая версия использовала `Tex Brick` со `Scale = 200`: на превью 512×512 это выглядело отлично, а на 2000×2000 узор превратился в разноцветный шум, потому что мозаика Brick растянулась за пределы UV. Причина техническая: `Smart UV Project` дал несколько островов, и Brick-сетка оказалась неравномерно растянута между ними. Решили заменой на две `Wave`-ноды под 90°, продублированные и смещённые, — такая сетка не зависит от развёртки, потому что считается в Generated-координатах. Дополнительно `Tex Brick` пришлось перевести на `Non-Color`, иначе цвет сетки интерпретировался как sRGB и светлел. **2. Валики и шнурки «проваливались» в материал при смене ракурса.** Шнурки были кривыми с профилем, но без заполнения сегмента (`fill_mode`), и на боковом ракурсе из-под профиля был виден пустой каркас кривой — тонкая тёмная линия вдоль всего шнурка. Помогло `fill_mode = 'FULL'` и `bevel_resolution = 4` (было 2). Отдельно выяснилось, что шнурки должны быть чуть толще реальных: на 2000 px шнурок диаметром 2 мм это 8–10 пикселей, а тоньше — уже антиалиасинговая каша. **3. Белая подошва уходила в серый в нижней трети.** Это не ошибка материала, а ошибка света: единственный нижний источник был слабый, и тень от подошвы закрывала самую видимую часть. Пришлось добавить узкий нижний заполняющий источник малой мощности и поднять `Roughness` подошвы — глянцевая белая поверхность в тени давала серый градиент вместо белого. ## 📊 ЧТО ПОЛУЧИЛОСЬ **Оценка по порядку величины, замеров на конкретном железе не проводилось.** Про треугольники: числа получены расчётом от типичной сложности примитивов, а не измерением. Bevel на подошве даёт +500 треугольников на объект, curve с `bevel_resolution = 4` — заметно больше, чем изображает профиль на глаз. Единственный надёжный способ узнать настоящее число — пересчитать через `evaluated_depsgraph_get()` на готовой сцене, как показано в WF 19. - Объектов: ~6 верхнего уровня (подошва, верх, задник, шнурки слиты в 1, стежки слиты в 1, подложка) плюс 3 источника и камера. - Полигоны: подошва с фаской ~100, верх ~4.6 тыс., задник ~2 тыс., шнурки ~2–4 тыс., стежки ~600 — итого **9–12 тыс. треугольников**. Это мало для продукта, но для каталожного кадра на 2000 px хватает, если детали в материале. - Материалов: 4, текстур **ноль** — всё процедурное (Brick, Voronoi, Map Range). - Стежков: 4–6 на шов, 3 шва. - Рендер 2000×2000 в Cycles, 256 сэмплов + denoising: **минуты** на средней видеокарте; 3 ракурса — те же единицы минут × 3. В EEVEE — секунды, но теряется материал. ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Текстура «плывёт» на движении камеры | процедурный узор зависит от UV, а UV развёрнут неравномерно | считать узор в Generated-координатах (Wave вместо Brick) | | Мутный кадр | denoising вытирает трикотаж | поднять сэмплы до 512–1024 для материала с мелким узором | | Ботинок «расплылся» | широкий объектив | 85–120 мм | | Шнурок с «дыркой» | профиль кривой без заполнения | `fill_mode = 'FULL'`, `bevel_resolution >= 4` | | Слишком яркие блики | ключевой источник слишком маленький при высокой мощности | увеличить `size`, снизить `energy` | | Кадр в 512×512 отличный, в 2000 ужасный | мелкий узор материала не масштабируется | проверять на конечном разрешении, а не на превью | ## ✅ РЕЗУЛЬТАТ На выходе: `.blend` с моделью, 4 материалами и 3 ракурсами камеры, три PNG 2000×2000 в `//renders/sneaker/` (`hero34.png`, `side.png`, `top.png`), плюс GLB для веба. ## 🔗 WF-07, Setup 01/08, INDEX --- ## 🔗 СВЯЗИ - **Прочие в категории «Кейсы»:** [CASE 14: FOOD RENDER (RAMEN)](case_14_food_r visualisation.md), [CASE 15: CAR STUDIO CINEMATIC](case_15_car_studio_cinematic.md), [CASE 01: ARCHVIZ INTERIOR](case_01_archviz_interior.md), [CASE 02: GAME PROPS PIPELINE](case_02_game_props.md), [CASE 03: PRODUCT VISUALIZATION](case_03_product_viz.md) - **По темам:** [CASE 04: CHARACTER RIG & ANIMATION](case_04_character_rig.md), [CASE 05: PROCEDURAL CITY](case_05_procedural_city.md), [CASE 06: ANIMATION LOOP](case_06_animation_loop.md) - **Теги:** #case, #product --- # CASE 11: COZY ROOM DIORAMA Tags: #case #stylized # CASE 11: COZY ROOM DIORAMA **ID:** CASE-11 | **Type:** Diorama | **Difficulty:** ⭐⭐ **RU/EN Bilingual** > Когда применять: когда нужен «уютный кусочек комнаты» — изометрическая или открытая диорама, которая читается как миниатюра: тёплый практический свет, малые размеры, эффект игрушечной фотографии. ## 🎯 ЦЕЛЬ Открытая миниатюрная комната 3×3×3 м с парой-тройкой слоёв, лампой, текстурой дерева и мягким размытием, создающим ощущение игрушки. Один hero-ракурс 3/4 плюс один ракурс сверху. Принцип, вокруг которого строится вся диорама: **размер считывается не метрами, а глубиной резкости и плотностью деталей**. Метры в сцене никто не измеряет — мозг зрителя оценивает масштаб по размытию переднего и заднего плана. ## 🧭 ИСХОДНАЯ ЗАДАЧА И ОГРАНИЧЕНИЯ **Задача:** карточка для раздела «уютные сцены» — 1600×1600, тёплая палитра, никакого текста в кадре. **Ограничения:** - Открытая коробка (вид с трёх сторон), не интерьер: минимум стен, максимум содержимого. - Один практический источник в кадре (лампа) плюс слабое «окно» — больше источников, и теряется ощущение домашнего тепла. - Тёплая палитра: 2700–3200 K для практики, холодный полутон в тенях для контраста. - Никакого реализма материалов: дерево слегка упрощено, ткань — матовая, керамика — глянец. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия / состояние | Зачем | |---|---|---| | Blender с Cycles | базовый | мягкие тени, SSS | | HDRI «окна» | Poly Haven | холодная подсветка | | Текстуры 2k | дерево, ткань, керамика | три материала | | Камера 50–85 мм | — | малый DOF | ## 🛠️ НАСТРОЙКА ### Этап 1. Коробка-комната ```bash RU: "Диорама 3x3x3 м, открытая с двух сторон: пол, две стены по 3 м, плинтус, стены тёплые #C9B49A, пол тёмный дуб. Без потолка" EN: "Diorama 3x3x3 m, open on two sides: floor, two 3 m walls, baseboard, walls warm #C9B49A, floor dark oak. No ceiling" ``` ```python import bpy, math bpy.ops.object.select_all(action='DESELECT') def slab(name, size, loc, mat=None, bevel=0.004): bpy.ops.mesh.primitive_cube_add(size=1, location=loc) ob = bpy.context.active_object ob.name = name ob.scale = size bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) if bevel: b = ob.modifiers.new("Bevel", 'BEVEL') b.width = bevel; b.segments = 2; b.limit_method = 'ANGLE' if mat: ob.data.materials.append(mat) return ob M_WALL = bpy.data.materials.new("M_Wall") M_FLOOR = bpy.data.materials.new("M_Floor") floor = slab("Floor", (3.0, 3.0, 0.06), (0, 0, -0.03), M_FLOOR) w_north = slab("Wall_North", (3.0, 0.08, 2.6), (0, 1.46, 1.30), M_WALL) w_west = slab("Wall_West", (0.08, 3.0, 2.6), (-1.46, 0, 1.30), M_WALL) skirt_n = slab("Skirt_North", (3.0, 0.03, 0.10), (0, 1.40, 0.05), M_WALL, bevel=0.002) skirt_w = slab("Skirt_West", (0.03, 3.0, 0.10), (-1.40, 0, 0.05), M_WALL, bevel=0.002) ``` ### Этап 2. Мебель и содержимое ```bash RU: "Добавь: диван (подушка + спинка + 2 подлокотника), журнальный столик, ковёр 1.6x1.1, полку на стене, 2 книги, чашку на столе, лампу-торшер. Всё в пределах комнаты, без выхода за стены" EN: "Add: sofa (cushion + back + 2 arms), coffee table, 1.6x1.1 rug, wall shelf, 2 books, a cup on the table, floor lamp. Everything inside the room, nothing poking through the walls" ``` ```python import bpy, math def cyl(name, r, depth, loc, mat, verts=24): bpy.ops.mesh.primitive_cylinder_add(vertices=verts, radius=r, depth=depth, location=loc) ob = bpy.context.active_object; ob.name = name ob.data.materials.append(mat) for p in ob.data.polygons: p.use_smooth = True return ob M_SOFA = bpy.data.materials.new("M_Sofa") M_WOOD = bpy.data.materials.new("M_Wood") M_RUG = bpy.data.materials.new("M_Rug") M_CER = bpy.data.materials.new("M_Ceramic") M_BOOK = bpy.data.materials.new("M_Book") sofa = slab("Sofa_Base", (1.6, 0.75, 0.32), (0.15, 0.85, 0.16), M_SOFA) slab("Sofa_Back", (1.6, 0.18, 0.55), (0.15, 1.20, 0.48), M_SOFA) slab("Sofa_ArmL", (0.16, 0.75, 0.28), (-0.58, 0.85, 0.32), M_SOFA) slab("Sofa_ArmR", (0.16, 0.75, 0.28), ( 0.88, 0.85, 0.32), M_SOFA) for i, x in enumerate((0.02, 0.34)): slab(f"Cushion_{i}", (0.55, 0.60, 0.14), (x, 0.85, 0.39), M_SOFA, bevel=0.02) slab("Table_Top", (0.75, 0.45, 0.05), (0.15, 0.35, 0.40), M_WOOD, bevel=0.006) for i, (dx, dy) in enumerate(((-0.32, -0.18), (0.32, -0.18), (-0.32, 0.18), (0.32, 0.18))): cyl(f"Table_Leg_{i}", 0.018, 0.40, (0.15 + dx, 0.35 + dy, 0.20), M_WOOD, verts=12) slab("Rug", (1.7, 1.1, 0.012), (0.15, 0.35, 0.012), M_RUG, bevel=0.002) slab("Shelf", (1.0, 0.22, 0.04), (0.60, 1.36, 1.55), M_WOOD, bevel=0.004) for i, (z, mat) in enumerate(((1.60, M_BOOK), (1.70, M_BOOK))): slab(f"Book_{i}", (0.16, 0.13, 0.03), (0.30 + i * 0.20, 1.34, z), mat, bevel=0.002) cyl("Cup", 0.038, 0.09, (0.05, 0.32, 0.47), M_CER) cyl("Cup_Handle", 0.030, 0.008, (0.095, 0.32, 0.47), M_CER, verts=16) ``` ### Этап 3. Лампа: практический источник в кадре Лампа делается геометрией **и** источником. Это приём, который отличает диораму от обычной сцены: абажур полупрозрачный, свет выходит наружу, и на стене видно тёплое пятно. ```python import bpy, math def lamp(): M_SHADE = bpy.data.materials.new("M_Shade") M_SHADE.use_nodes = True nt = M_SHADE.node_tree out = next(n for n in nt.nodes if n.type == 'OUTPUT_MATERIAL') for n in list(nt.nodes): nt.nodes.remove(n) tr = nt.nodes.new("ShaderNodeBsdfTransparent") em = nt.nodes.new("ShaderNodeEmission") em.inputs['Color'].default_value = (1.0, 0.72, 0.42, 1) # ~2900K em.inputs['Strength'].default_value = 6.0 add = nt.nodes.new("ShaderNodeAddShader") mix = nt.nodes.new("ShaderNodeMixShader") mix.inputs[0].default_value = 0.55 # 55% эмиссии, 45% прозрачности nt.links.new(tr.outputs['BSDF'], mix.inputs[1]) nt.links.new(em.outputs['Emission'], mix.inputs[2]) nt.links.new(mix.outputs['Shader'], add.inputs[0]) nt.links.new(add.outputs['Shader'], out.inputs['Surface']) bpy.ops.mesh.primitive_cone_add(vertices=48, radius1=0.16, radius2=0.09, depth=0.22, location=(1.05, 0.85, 1.55)) shade = bpy.context.active_object; shade.name = "Lamp_Shade" shade.data.materials.append(M_SHADE) cyl("Lamp_Stem", 0.012, 1.20, (1.05, 0.85, 0.60), M_WOOD, verts=12) cyl("Lamp_Foot", 0.11, 0.02, (1.05, 0.85, 0.01), M_WOOD) ld = bpy.data.lights.new("Lamp_Bulb", type='POINT') ld.energy = 25.0 ld.color = (1.0, 0.72, 0.42) ld.shadow_soft_size = 0.04 b = bpy.data.objects.new("Lamp_Bulb", ld) bpy.context.scene.collection.objects.link(b) b.location = (1.05, 0.85, 1.48) return shade, b shade, bulb = lamp() ``` ### Этап 4. Материалы ```python import bpy def simple(name, base, rough, sheen=0.0, coat=0.0): m = bpy.data.materials.new(name) m.use_nodes = True b = next(n for n in m.node_tree.nodes if n.type == 'BSDF_PRINCIPLED') b.inputs['Base Color'].default_value = (*base, 1) b.inputs['Roughness'].default_value = rough s = b.inputs.get('Sheen Weight') if s and sheen: s.default_value = sheen c = b.inputs.get('Coat Weight') if c and coat: c.default_value = coat return m simple("M_Wall", (0.79, 0.71, 0.62), 0.92) simple("M_Floor", (0.24, 0.17, 0.11), 0.55) simple("M_Wood", (0.32, 0.22, 0.13), 0.45) simple("M_Sofa", (0.42, 0.34, 0.30), 0.95, sheen=0.5) simple("M_Rug", (0.55, 0.44, 0.36), 0.98) simple("M_Cer", (0.88, 0.86, 0.82), 0.15, coat=0.5) simple("M_Book", (0.35, 0.22, 0.20), 0.75) ``` Дерево и ткань получают микро-рельеф, иначе при малом DOF они выглядят пластиковыми. ```python import bpy def add_noise_bump(mat_name, scale, strength, distance): m = bpy.data.materials[mat_name] nt = m.node_tree b = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED') tex = nt.nodes.new("ShaderNodeTexNoise") tex.inputs['Scale'].default_value = scale tex.inputs['Detail'].default_value = 8.0 bump = nt.nodes.new("ShaderNodeBump") bump.inputs['Strength'].default_value = strength bump.inputs['Distance'].default_value = distance nt.links.new(tex.outputs['Fac'], bump.inputs['Height']) nt.links.new(bump.outputs['Normal'], b.inputs['Normal']) add_noise_bump("M_Wood", 220.0, 0.15, 0.0008) add_noise_bump("M_Sofa", 400.0, 0.25, 0.0004) add_noise_bump("M_Rug", 600.0, 0.35, 0.0006) ``` ### Этап 5. Свет: тёплая практика + холодное «окно» ```python import bpy sc = bpy.context.scene sc.render.engine = 'CYCLES' sc.cycles.samples = 256 sc.cycles.use_denoising = True world = bpy.data.worlds.new("DioramaWorld") sc.world = world world.use_nodes = True nt = world.node_tree env = nt.nodes.new("ShaderNodeTexEnvironment") bg = next(n for n in nt.nodes if n.type == 'BACKGROUND') bg.inputs['Color'].default_value = (0.05, 0.06, 0.09, 1) # холодная подсветка bg.inputs['Strength'].default_value = 0.35 nt.links.new(env.outputs['Color'], bg.inputs['Color']) # «окно»: большой холодный источник сбоку-спереди ld = bpy.data.lights.new("Window", type='AREA') ld.shape = 'RECTANGLE'; ld.size = 1.4; ld.size_y = 1.8 ld.energy = 90.0 ld.color = (0.78, 0.86, 1.0) win = bpy.data.objects.new("Window", ld) sc.collection.objects.link(win) win.location = (-1.9, -1.1, 1.7) c = win.constraints.new('TRACK_TO'); c.target = bpy.data.objects["Sofa_Base"] c.track_axis = 'TRACK_NEGATIVE_Z'; c.up_axis = 'UP_Y' ``` ### Этап 6. Эффект миниатюры: DOF + размытие по краям Маленький DOF делает «игрушечность» без изменения реального масштаба. Размытие по вертикали — классический признак сдвиговой камеры, и его легко упустить. ```python import bpy, math sc = bpy.context.scene sc.use_nodes = True nt = sc.node_tree for n in list(nt.nodes): nt.nodes.remove(n) rl = nt.nodes.new("CompositorNodeRLayers") mask = nt.nodes.new("CompositorNodeBoxMask") mask.x, mask.y = 0.5, 0.5 mask.width, mask.height = 0.05, 0.40 # щель, оставляющая резкую полосу по центру mask.rotation = math.radians(-12) blur = nt.nodes.new("CompositorNodeBlur") blur.filter_type = 'FAST_GAUSS' for sock, val in (("Size", 8.0), ("X", 0.05), ("Y", 0.06)): s = blur.inputs.get(sock) if s is not None: s.default_value = val mix = nt.nodes.new("CompositorNodeMixRGB") mix.blend_type = 'MIX' ao = nt.nodes.new("CompositorNodeAlphaOver") comp = nt.nodes.new("CompositorNodeComposite") mask.use_antialiasing = True nt.links.new(rl.outputs['Image'], blur.inputs['Image']) nt.links.new(mask.outputs['Mask'], mix.inputs['Fac']) nt.links.new(blur.outputs['Image'], mix.inputs[1]) nt.links.new(rl.outputs['Image'], mix.inputs[2]) nt.links.new(mix.outputs['Image'], ao.inputs[1]) nt.links.new(ao.outputs['Image'], comp.inputs['Image']) ``` > [!NOTE] > У `CompositorNodeBlur` имена свойств для X/Y-размытия отличались между версиями (`size_x`/`size_y` против `Size`/`X`/`Y` как входных сокетов). Если присваивание не сработало — открой ноду в композитере и выставь значения вручную; заодно проверь, что `use_relative` включён, иначе размер задаётся в пикселях. Камера и два кадра: ```python import bpy, math, os sc = bpy.context.scene sc.render.resolution_x = sc.render.resolution_y = 1600 sc.render.image_settings.file_format = 'PNG' os.makedirs(bpy.path.abspath("//renders/diorama"), exist_ok=True) bpy.data.cameras.new("Cam_Diorama") cam = bpy.data.objects.new("Cam_Diorama", bpy.data.cameras["Cam_Diorama"]) sc.collection.objects.link(cam) sc.camera = cam cam.data.lens = 70.0 cam.data.dof.use_dof = True cam.data.dof.focus_object = bpy.data.objects["Table_Top"] cam.data.dof.aperture_fstop = 2.0 # большое число -> размытость дальше cam.data.clip_start = 0.01 for name, loc in (("hero", (2.6, -2.9, 1.75)), ("top", (0.35, -0.9, 3.4))): cam.location = loc c = cam.constraints.new('TRACK_TO') c.target = bpy.data.objects["Table_Top"] c.track_axis = 'TRACK_NEGATIVE_Z'; c.up_axis = 'UP_Y' sc.render.filepath = f"//renders/diorama/{name}.png" bpy.ops.render.render(write_still=True) ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `aperture_fstop` | 1.8–2.8 | больше — резче кадр, меньше — сильнее «миниатюрность» | | `dof.focus_object` | центральный объект | фокус на переднем плане размывает героя | | практический свет `energy` | 20–40 W | при 3-метровой комнате это заметно | | `env` Strength | 0.3–0.5 | выше — теряется ощущение вечера | | `Sheen Weight` ткани | 0.4–0.6 | без него ткань выглядит резиной | | относительное размытие | 5% по X, 6% по Y | вертикальное усиливает эффект сдвиговой камеры | | `Coat Weight` керамики | 0.4–0.6 | глазурь без coat не даёт одиночного блика | ## ⚠️ ЧТО ПОШЛО НЕ ТАК **1. Абажур светился наружу и выжигал стену.** Первый вариант делал абажур на 100% эмиссии, и свет от точечного источника проходил сквозь материал насквозь: стена за лампой получала горячее пятно в 1.5 стены, а сам абажур терял форму — он был просто белым пятном. Смешивание Transparent + Emission с фактором 0.55 решило задачу: абажур пропускает свет, но не становится источником сам по себе. Заодно пришлось убавить мощность лампы с 60 W до 25 W — с эмиссивным абажуром комната пересвечивалась. **2. Размытие по краям получилось, но не в том месте.** Первый вариант использовал простое размытие на весь кадр, и это просто размыло весь кадр целиком вместо того, чтобы оставить резкой середину. Для эффекта сдвиговой камеры нужна маска: `Box Mask` с узкой высотой по центру кадра и наклоном. Проблема вылезла в другом месте — после включения маски стало видно, что у `Box Mask` выход называется `Mask`, и его нельзя напрямую подключить к входу `Image` ноды `MixRGB`: соединение создаётся, но результат инвертируется (маска становится дырой, а не размытием). Пришлось инвертировать фактор в ноде смешивания. **3. Масштаб не читался как «10 сантиметров».** По замыслу диорама должна выглядеть как миниатюра 1:10. По факту с тремя метрами стен и 1600 px в кадре мозг зрителя считал обычную комнату среднего размера. Вывод: масштаб в таких сценах задаёт **плотность деталей и размытие**, а не метры. Пришлось добавить мелких деталей на уровне предметов (книги, чашка, стежки на подушках) и поднять `f-stop` до 2.0. Отдельная ошибка была в том, что окно светило холодным без затухания: тёплый-тёплый-тёплый переход в терялся, и комната читалась как нейтральная студия. Появилась отдельная тёплая практика. ## 📊 ЧТО ПОЛУЧИЛОСЬ **Оценка по порядку величины, конкретных замеров нет:** - Объектов: ~28 (стены, плинтусы, диван из 5 частей, стол из 5, ковёр, полка, 2 книги, чашка, лампа из 3, 2 источника, камера). - Полигоны: почти всё — боксы с bevel по 2 сегмента и цилиндры по 12–48 вершин; конус абажура 48 вершин. Итого **8–15 тыс. треугольников**. - Материалов: 8, текстур **ноль** — всё процедурное (Noise bump). - Кадров: 2 × 1600×1600. - Рендер в Cycles, 256 сэмплов + denoising: **минуты** на кадр на средней видеокарте. Композитор добавляет секунды. ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Читается как обычная комната, не как миниатюра | нет мелких деталей, слишком резко | добавить мелочи и поставить `f-stop` ≤ 2.4 | | Стена за лампой в горячем пятне | абажур на 100% эмиссии | Transparent + Emission, фактор 0.5 | | Ткань выглядит резиной | нет `Sheen Weight` и микро-bump | Sheen 0.5 + Noise bump | | Всё кадр размыто | блендинг по `Fac` инвертирован | инвертировать, резкая полоса — середина | | Комната пересвечена | HDRI Strength > 0.6 при практике | опустить до 0.35 | | Камера «в стене» | `clip_start` = 0.1 при мелком масштабе | `clip_start = 0.01` | | Предметы протыкают стены | не проверяли габариты | после сборки проверить `bound_box` каждого объекта | ## ✅ РЕЗУЛЬТАТ На выходе: `.blend` с диорамой 3×3×2.6 м, 8 материалами, двумя источниками и двухступенчатым композиторным размытием; два PNG 1600×1600 в `//renders/diorama/`. ## 🔗 WF-07, Setup 01/08, INDEX --- ## 🔗 СВЯЗИ - **Прочие в категории «Кейсы»:** [CASE 08: SCI-FI CORRIDOR MODULAR](case_08_sci_fi_corridor.md), [CASE 09: ANIME HEAD BUST](case_09_anime_head_bust.md), [CASE 01: ARCHVIZ INTERIOR](case_01_archviz_interior.md), [CASE 02: GAME PROPS PIPELINE](case_02_game_props.md), [CASE 03: PRODUCT VISUALIZATION](case_03_product_viz.md) - **По темам:** [CASE 04: CHARACTER RIG & ANIMATION](case_04_character_rig.md), [CASE 05: PROCEDURAL CITY](case_05_procedural_city.md), [CASE 06: ANIMATION LOOP](case_06_animation_loop.md) - **Теги:** #case, #stylized --- # CASE 12: LOWPOLY FOREST PACK Tags: #case #vegetation # CASE 12: LOWPOLY FOREST PACK **ID:** CASE-12 | **Type:** Environment | **Difficulty:** ⭐⭐ **RU/EN Bilingual** > Когда применять: когда нужен игровой или стилизованный лесной ассет: несколько пород деревьев, кусты, камни, одна атлас-текстура на весь пак и ветер, который двигает всё это без анимации. ## 🎯 ЦЕЛЬ Пять деревьев, кусты и камни как **один набор ассетов** с общей атлас-текстурой, единым материалом и покачиванием на ветру, вписанным в геометрию. На выходе — пак, который можно разложить по сцене десятками копий без нагрузки. Ключевое требование к пакету: **один материал на весь набор**. Двенадцать уникальных материалов означает двенадцать вызовов шейдера на каждый объект — на мобильном это единственная причина, по которой лес тормозит. ## 🧭 ИСХОДНАЯ ЗАДАЧА И ОГРАНИЧЕНИЯ **Задача:** 5 деревьев + 3 куста + 3 камня, лоу-поли, лёгкая листва, одна атлас-карта 1024×1024, ветер, пригодно для импорта в движок. **Ограничения:** - Бюджет: не более 60 тыс. треугольников на весь пак. - Листва — не альфа-карта с прозрачностью на квадрате (это дорого и сортируется плохо), а геометрия: плоские кластеры с двух сторон. - Ветер должен работать **без скелета и без shape keys** — иначе пак нельзя быстро размножить. - Атлас один: значит UV всех объектов укладываются в один лист, и соседние острова не должны накладываться. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия / состояние | Зачем | |---|---|---| | Blender с Geometry Nodes | 3.0+ | инстансы и ветер | | Текстура-атлас | рисуется или генерируется | один материал на пак | | Справочник по флоре | — | силуэт и пропорции | ## 🛠️ НАСТРОЙКА ### Этап 1. Форма дерева: ствол + кластеры кроны ```bash RU: "Дерево: ствол как конус с 6-8 сторонами, высота 4-7 м, крона из 5-8 плоских кластеров (icosphere с 1 делением, масштаб по Z 0.4), снизу листва темнее. Куст: 3 кластера. Камень: icosphere с 1 делением, нерегулярный" EN: "Tree: trunk as 6-8 sided cone, height 4-7 m, crown of 5-8 flat clusters (1-subdivision icosphere, Z scale 0.4), foliage darker below. Bush: 3 clusters. Rock: 1-subdivision icosphere, irregular" ``` ```bash RU: "Построй 5 деревьев разной высоты (4.2, 5.1, 6.0, 5.6, 4.8 м) и разной формы кроны, каждый своим объектом, имена Tree_01..Tree_05" EN: "Build 5 trees of different heights (4.2, 5.1, 6.0, 5.6, 4.8 m) and crown shapes, one object each, named Tree_01..Tree_05" ``` ```python import bpy, math, random def trunk(name, height, r_base, r_top, sides=7): bpy.ops.mesh.primitive_cone_add(vertices=sides, radius1=r_base, radius2=r_top, depth=height, location=(0, 0, height / 2)) ob = bpy.context.active_object ob.name = name ob.data.materials.append(bpy.data.materials["M_Forest"]) return ob def cluster(name, loc, scale, seed): """Плоский кластер листвы: два «слоя» граней + случайное вращение.""" rng = random.Random(seed) bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=1, radius=1.0, location=loc) ob = bpy.context.active_object ob.name = name ob.scale = (scale[0], scale[1], scale[2] * 0.45) # сплющиваем по Z ob.rotation_euler = (rng.uniform(-0.2, 0.2), rng.uniform(-0.2, 0.2), rng.uniform(0, math.tau)) ob.data.materials.append(bpy.data.materials["M_Forest"]) return ob def tree(name, height, n_crown, seed, spread=0.9): rng = random.Random(seed) t = trunk(f"{name}_Trunk", height, 0.14 + height * 0.02, 0.05, sides=7) parts = [t] for i in range(n_crown): a = math.tau * i / n_crown + rng.uniform(-0.3, 0.3) r = spread * rng.uniform(0.5, 1.0) z = height * (0.55 + 0.42 * (i / max(n_crown - 1, 1))) c = cluster(f"{name}_Crown_{i}", (math.cos(a) * r, math.sin(a) * r, z), (0.62 + rng.uniform(0, 0.35), 0.62 + rng.uniform(0, 0.35), 0.62), seed + i) parts.append(c) return parts def bush(name, seed): rng = random.Random(seed) parts = [] for i in range(3): parts.append(cluster(f"{name}_{i}", (rng.uniform(-0.25, 0.25), rng.uniform(-0.25, 0.25), 0.22 + rng.uniform(0, 0.15)), (0.45, 0.45, 0.45), seed * 7 + i)) return parts def rock(name, seed): rng = random.Random(seed) bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=1, radius=1.0, location=(0, 0, 0.22)) ob = bpy.context.active_object ob.name = name for v in ob.data.vertices: # нерегулярность: рваный силуэт v.co *= rng.uniform(0.75, 1.25) v.co.z = abs(v.co.z) * 0.8 ob.scale = (0.42, 0.36, 0.34) ob.rotation_euler = (0, 0, rng.uniform(0, math.tau)) ob.data.materials.append(bpy.data.materials["M_Forest"]) return ob M = bpy.data.materials.new("M_Forest") pack = [] for i, (h, n, sd, sp) in enumerate([(4.2, 6, 11, 0.8), (5.1, 7, 23, 0.95), (6.0, 8, 37, 1.1), (5.6, 6, 41, 0.9), (4.8, 7, 53, 1.0)]): pack += tree(f"Tree_{i+1:02d}", h, n, sd, sp) for i in range(3): pack += bush(f"Bush_{i+1:02d}", 100 + i) for i in range(3): pack.append(rock(f"Rock_{i+1:02d}", 200 + i)) print("объектов в паке:", len(pack)) ``` ### Этап 2. Один материал + атлас Атлас рисуется заранее. Ключевой момент — нижняя часть кроны темнее: это делает объём без единого источника света. ```python import bpy mat = M mat.use_nodes = True nt = mat.node_tree bsdf = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED') atlas = nt.nodes.new("ShaderNodeTexImage") atlas.image = bpy.data.images.load(bpy.path.abspath("//tex/forest_atlas.png")) atlas.extension = 'EXTEND' # затемнение снизу по мировой Z: читается как «свет сверху» даже в шейдере без освещения geo = nt.nodes.new("ShaderNodeNewGeometry") sep = nt.nodes.new("ShaderNodeSeparateXYZ") mr = nt.nodes.new("ShaderNodeMapRange") mr.inputs['From Min'].default_value = 0.0 mr.inputs['From Max'].default_value = 6.0 mr.inputs['To Min'].default_value = 0.55 # низ тёмный mr.inputs['To Max'].default_value = 1.0 # верх светлый mix = nt.nodes.new("ShaderNodeMixRGB") mix.blend_type = 'MULTIPLY' mix.inputs['Fac'].default_value = 1.0 nt.links.new(geo.outputs['Position'], sep.inputs['Vector']) nt.links.new(sep.outputs['Z'], mr.inputs['Value']) nt.links.new(atlas.outputs['Color'], mix.inputs[1]) nt.links.new(mr.outputs['Result'], mix.inputs[2]) nt.links.new(mix.outputs['Color'], bsdf.inputs['Base Color']) bsdf.inputs['Roughness'].default_value = 0.9 # двухсторонняя листва: с обратной стороны она не должна становиться чёрной. # Проще всего оставить Backface Culling выключенным в настройках материала # и добавить лёгкий Sheen, чтобы плоскости читались с двух сторон. if bsdf.inputs.get('Sheen Weight'): bsdf.inputs['Sheen Weight'].default_value = 0.25 ``` > [!NOTE] > Двухстороннее освещение листвы в Cycles зависит от настроек материала: либо включён `Backface Culling` в настройках материала (тогда нужен Geometry → Backfacing), либо в шейдере используют `Geometry → Backfacing` как множитель цвета и `Mix Shader` между двумя поверхностями. Точный набор полей Principled BSDF (в частности наличие `Sheen Weight` и его название) зависит от версии — проверь в `Help → Manual`, раздел про Principled BSDF. ### Этап 3. UV в один атлас Ручная укладка в атлас — трудоёмко, поэтому используем упаковку островов в пределах одного листа. ```python import bpy, math M = bpy.data.materials["M_Forest"] UV = 0.0 # атлас 0..1; чтобы упаковать, атлас должен быть ровно 0..1 for ob in bpy.data.objects: if not ob.name.startswith(("Tree_", "Bush_", "Rock_")): continue me = ob.data me.use_auto_smooth = True if hasattr(me, "use_auto_smooth") else False for p in me.polygons: p.use_smooth = False # лоу-поли: плоские грани, сглаживание убивает стиль bpy.ops.object.select_all(action='DESELECT') ob.select_set(True) bpy.context.view_layer.objects.active = ob bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.uv.smart_project(angle_limit=math.radians(70), island_margin=0.004) # сдвигаем острова внутрь атласа, иначе они выходят за 0..1 и тянут крайний цвет bpy.ops.uv.select_all(action='SELECT') bpy.ops.uv.cursor_set(location=(0.5, 0.5)) bpy.ops.uv.move(type='ALIGNED') bpy.ops.object.mode_set(mode='OBJECT') atlas_img = bpy.data.images.get("forest_atlas") print("лист атласа:", tuple(atlas_img.size) if atlas_img else "нет") ``` ### Этап 4. Ветер: деформация без скелета и без shape keys Самый дешёвый способ — `Simple Deform` в режиме Bend, привязанный к пустому объекту, который медленно качается. Форма деформируется на CPU/GPU без единого ключа. ```python import bpy, math def add_wind(target_objects, strength=0.35, period=48): bpy.ops.object.empty_add(type='PLAIN_AXES', location=(0, 0, 0)) ctl = bpy.context.active_object ctl.name = "Wind_CTRL" ctl.rotation_mode = 'XYZ' n_periods = 2 # два колебания за цикл for f in range(0, period * n_periods + 1, period): ctl.rotation_euler = (0.0, 0.0, math.radians(strength * 60)) ctl.keyframe_insert("rotation_euler", frame=f) for ob in target_objects: mod = ob.modifiers.new("Wind", 'SIMPLE_DEFORM') mod.deform_method = 'BEND' mod.angle = math.radians(12.0) # базовый изгиб ствола mod.origin = ctl # ось вокруг пустого объекта mod.deform_axis = 'X' mod.vertex_group = "" # вся геометрия, без ограничения return ctl foliage = [o for o in bpy.data.objects if o.name.startswith(("Tree_", "Bush_")) and "Trunk" not in o.name] add_wind(foliage, strength=0.5, period=48) ``` Для камеры и окон ветер не нужен — деформация считается в Object-координатах, и при перемещении объекта деформация «едет». Это дефект, а не фича: замораживать позицию при инстанцировании. ```python import bpy dg = bpy.context.evaluated_depsgraph_get() total = 0 for ob in foliage: ev = ob.evaluated_get(dg) me = ev.to_mesh() total += sum(len(p.vertices) - 2 for p in me.polygons) ev.to_mesh_clear() print(f"треугольников после деформации (без применения): {total:,}") ``` ### Этап 5. Сборка пака в коллекцию и превью ```python import bpy, os, math pack_coll = bpy.data.collections.new("ForestPack") bpy.context.scene.collection.children.link(pack_coll) pack_objs = [o for o in bpy.data.objects if o.name.startswith(("Tree_", "Bush_", "Rock_"))] for ob in pack_objs: for c in list(ob.users_collection): c.objects.unlink(ob) pack_coll.objects.link(ob) ob.data.name = ob.name + "_mesh" # уникальное имя меша: копии не будут делить данные # превью-раскладка: все объекты в ряд, одна камера for i, ob in enumerate(pack_objs): ob.location.x = (i % 12) * 1.2 - 7.0 ob.location.y = -(i // 12) * 1.6 sc = bpy.context.scene sc.render.engine = 'CYCLES' sc.cycles.samples = 64 sc.render.resolution_x, sc.render.resolution_y = 1600, 1000 os.makedirs(bpy.path.abspath("//renders/pack"), exist_ok=True) sc.render.filepath = r"//renders/pack/forest_pack.png" bpy.ops.render.render(write_still=True) sc.render.filepath = r"//export/forest_pack.glb" bpy.ops.export_scene.gltf(filepath=bpy.path.abspath("//export/forest_pack.glb"), export_format='GLB', use_selection=False, export_apply=True, export_texcoords=True) ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | сторон конуса ствола | 6–8 | меньше — слишком «мультяшно», больше — теряется лоу-поли | | `subdivisions` кластера | 1 | 2 даёт +80 треугольников на кластер | | `use_smooth` | `False` | для лоу-поли обязательно: сглаженный лоу-поли не читается | | `angle_limit` Smart UV | 70° | выше — больше островов, ниже — возможны наложения | | `island_margin` | 0.004 | ровно столько нужно, чтобы соседи не «протекали» | | `MapRange To Min` | 0.55 | сильнее затемнение низа — объёмнее крона | | `Simple Deform.angle` | 8–15° | выше — трава пригибается | ## ⚠️ ЧТО ПОШЛО НЕ ТАК **1. Ветер деформировал деревья при переносе пака в другую сцену.** `Simple Deform` считает деформацию в системе координат пустого объекта, а не самого объекта. Пока дерево стояло в origin, всё выглядело правильно; как только его передвинули на позицию в лесу, деформация «уехала» и крона наклонялась в другую сторону. Выяснилось при сборке пробной сцены из 30 деревьев. Решили тем, что при инстанцировании в движке деформация считается на стороне движка (skinned mesh или шейдерное смещение по UV), а в паке ветер оставлен только для превью-сцены. Практический вывод: **ветер в паке — это визуальная помощь для превью, а не часть ассета**. **2. Атлас «протёк» на кластерах.** Первый проход UV с `island_margin = 0.001` дал края, где соседний остров подтекал в пиксель: на игровом расстоянии крона читалась в кашу. Увеличение margin до 0.004 и сдвиг островов внутрь листа (`uv.cursor_set` в центр + ALIGNED) убрали проблему. Дополнительно выяснилось, что `Smart UV Project` на икосферах даёт острова вплотную к границе UV, поэтому текстура бралась из соседней ячейки атласа — отсюда «пятна» неправильного цвета. **3. Сглаженные нормали убили стиль.** Первая версия использовала авто-сглаживание по углу 30° ради «качества». Для лоу-поли это дало кашу: грани в кластерах перестали читаться, объёмы кроны слиплись, и пак выглядел как обычная среднеполигонная растительность. Откат на плоские грани (`use_smooth = False`) вернул узнаваемый лоу-поли вид — и заодно сократил время рендера, потому что нормали стали предсказуемыми. **4. Тёмный низ сделал лес «грязным».** Затемнение снизу с `To Min = 0.35` в тёмной палитре дало почти чёрную нижнюю треть кроны. Подняли до 0.55 и добавили `Sheen Weight` на листве, и низ перестал читаться как дыра. ## 📊 ЧТО ПОЛУЧИЛОСЬ **Оценка по порядку величины, конкретных замеров не проводилось:** - Объектов: 5 стволов + 34 кластера кроны + 9 кластеров кустов + 3 камня = **~51 объект** в коллекции `ForestPack`. - Полигоны: конус 7 сторон — 14 треугольников; икосфера `subdivisions=1` — 80 треугольников. Итого: 5×14 + 34×80 + 9×80 + 3×80 ≈ **4.2 тыс. треугольников на весь пак**. Это сильно ниже бюджета в 60 тыс. — запас остался на второй уровень детализации. - Материалов: **1**, текстур: **1 атлас 1024×1024** (RGBA). Это и было целью. - Модификаторов: 43 объекта листвы с `Simple Deform` (только для превью). - Рендер 1600×1000 в Cycles, 64 сэмпла: **секунды** — сцена крошечная. GLB с атласом — единицы мегабайт. ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Атлас «протекает» между объектами | слишком малый `island_margin`, острова у границы UV | margin 0.004 + сдвиг внутрь листа | | Стиль «мультяшный», но грязный | сглаженные нормали на лоу-поли | `use_smooth = False` | | Крона сливается в пятно | `MapRange To Min` < 0.4 | поднять до 0.55 | | Ветер «едет» при переносе | `Simple Deform` в координатах пустого объекта | ветер только для превью, в движке — свой | | Много draw call в движке | 51 объект вместо инстансов | инстансировать в движке или через Geometry Nodes | | Камни выглядят как «картофелины» | равномерный множитель на вершинах | направлять деформацию вдоль оси, а не во все стороны | ## ✅ РЕЗУЛЬТАТ На выходе: коллекция `ForestPack` из ~51 объекта с 4.2 тыс. треугольников, один материал `M_Forest`, атлас 1024×1024, превью 1600×1000 (`//renders/pack/forest_pack.png`) и GLB для движка. ## 🔗 WF-07, Setup 01/08, INDEX --- ## 🔗 СВЯЗИ - **Прочие в категории «Кейсы»:** [CASE 05: PROCEDURAL CITY](case_05_procedural_city.md), [CASE 01: ARCHVIZ INTERIOR](case_01_archviz_interior.md), [CASE 02: GAME PROPS PIPELINE](case_02_game_props.md), [CASE 03: PRODUCT VISUALIZATION](case_03_product_viz.md), [CASE 04: CHARACTER RIG & ANIMATION](case_04_character_rig.md) - **По темам:** [CASE 06: ANIMATION LOOP](case_06_animation_loop.md), [CASE 07: STYLIZED BARREL (HAND-PAINT)](case_07_stylized_barrel.md), [CASE 08: SCI-FI CORRIDOR MODULAR](case_08_sci_fi_corridor.md) - **Теги:** #case, #vegetation --- # CASE 13: HARD-SURFACE SCI-FI GUN Tags: #case #modeling #render # CASE 13: HARD-SURFACE SCI-FI GUN **ID:** CASE-13 | **Type:** Hardsurface | **Difficulty:** ⭐⭐⭐ **RU/EN Bilingual** --- > Когда применять: когда нужно показать hard-surface модель в 3/4 ракурсе: форма собирается из прямых и фасок, читаемость держится на бликах по рёбрам и на износе по кромкам, а не на текстуре. ## 🎯 ЦЕЛЬ Собрать стилизованный sci-fi «ствол» из примитивов: корпус, кожух ствола, магазинный отсек, скоба, рукоять, рельса прицела. Довести до чистых фасок по весам, запечь нормали с разрешения с высокополигонной копии, собрать PBR-материал с износом по кривизне и выгрузить с LOD-цепочкой 8k → 3k → 800. Принцип, ради которого существует вся эта работа: **hard-surface читается фасками**. Не формой и не текстурой — фаской. Кромка шириной 2 мм на корпусе даёт линию света вдоль всего силуэта, и по этой линии зритель читает объём. Убрать фаску — значит убить читаемость, оставив серую коробку. Второй принцип — **износ только по геометрии**. Потёртости берутся из кривизны (pointiness), а не рисуются в текстуре. Тогда износ сам переезжает на новую форму: поставил фаску — по ней пошла потёртость, и ни одна карта не поехала. ## 🧭 ИСХОДНАЯ ЗАДАЧА И ОГРАНИЧЕНИЯ **Задача:** hero-рендер 2048×2048 в 3/4 ракурсе плюс трим-вид, LOD-цепочка для движка. **Ограничения:** - Бюджет hero-версии: 8–12 тыс. треугольников. Hard-surface с множеством фасок на этом бюджете — уже тесно. - Каждый boolean-вырез должен быть устойчивым: невыпуклые формы после вычитания дают невалидную топологию. - Все фаски — через веса, а не через `Angle Limit`: одинаковый угол даёт одинаковую фаску и на рёбрах, и внутри вырезов, где она не нужна. - Нормали запекаются один раз с высокополигонной копии; второй раз — только после смены формы. - Три материала максимум: gunmetal, grip polymer, emissive accent. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия / состояние | Зачем | |---|---|---| | Blender | актуальный | примитивы, boolean, веса | | Cycles | встроенный | запекание нормалей, pointiness | | HDRI студийный | Poly Haven 2k | блики на фасках | | Знание pointiness | — | износ по кривизне, без текстур | ## 🛠️ НАСТРОЙКА ### Этап 1. Блокаут примитивами ```python import bpy, math def box(name, size, loc, rot=(0, 0, 0)): bpy.ops.mesh.primitive_cube_add(size=1.0, location=loc, rotation=rot) ob = bpy.context.active_object ob.name = name ob.scale = size bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) return ob def cyl(name, r, depth, loc, rot=(0, 0, 0), verts=24): bpy.ops.mesh.primitive_cylinder_add(vertices=verts, radius=r, depth=depth, location=loc, rotation=rot) ob = bpy.context.active_object ob.name = name bpy.ops.object.shade_smooth() return ob # Габарит: длина ~0.62 м, это и есть то, что потом уходит в движок как масштаб body = box("Body", (0.30, 0.055, 0.085), (0.0, 0.0, 0.0)) shroud = cyl("Shroud", 0.028, 0.34, (0.16, 0.0, 0.005), (0, math.pi/2, 0)) magwell = box("MagWell", (0.085, 0.040, 0.115), (0.02, 0.0, -0.075), (0, math.radians(-12), 0)) grip = box("Grip", (0.055, 0.038, 0.135), (-0.10, 0.0, -0.075), (0, math.radians(18), 0)) rail = box("Rail", (0.22, 0.030, 0.012), (-0.01, 0.0, 0.055)) # Скоба: рамка из четырёх боксов, склеенных в одну геометрию перед boolean guard = bpy.data.objects.new("Guard", None) parts = [] for size, loc in ( ((0.010, 0.012, 0.010), (-0.075, 0.0, -0.020)), ((0.010, 0.012, 0.050), (-0.070, 0.0, -0.048)), ((0.048, 0.012, 0.010), (-0.050, 0.0, -0.068)), ): parts.append(box(f"_guard_{len(parts)}", size, loc)) bpy.ops.object.select_all(action='DESELECT') for p in parts: p.select_set(True) bpy.context.view_layer.objects.active = parts[0] bpy.ops.object.join() guard = bpy.context.active_object ``` ### Этап 2. Boolean-вырезы ```python import bpy def cut(target, cutter, op='DIFFERENCE'): """Boolean применяется сразу, а не копится в стеке модификаторов: незавершённый boolean пересчитывается при каждой правке и молча меняет вершины, на которых уже стоят веса фасок.""" bpy.ops.object.select_all(action='DESELECT') target.select_set(True) bpy.context.view_layer.objects.active = target m = target.modifiers.new("Bool", 'BOOLEAN') m.operation = op m.object = cutter m.solver = 'EXACT' # FAST даёт дыры на невыпуклых вырезах m.use_self = False bpy.ops.object.modifier_apply(modifier=m.name) bpy.data.objects.remove(cutter, do_unlink=True) # инструмент удаляем сразу def make_cutter(name, size, loc): c = box(name, size, loc) bpy.ops.object.shade_flat() return c cut(rail, make_cutter("cut_rail", (0.20, 0.020, 0.020), (-0.01, 0.0, 0.062))) # канавка рельса cut(body, make_cutter("cut_grip", (0.070, 0.070, 0.060), (-0.10, 0.0, -0.050))) # посадка рукояти ``` ### Этап 3. Веса фасок Фаски размечаются вручную: рёбра внешних кромок выделяются и получают вес 1.0, остальные остаются с весом 0. Визуально Ctrl+E мгновенно даёт именно то, что нужно, — берём рёбра, которые должны фаситься, и снимаем выделение с остальных. Скриптом ставится сам Bevel и заполняются веса там, где разметка сделана вручную: ```python import bpy def apply_manual_bevel_weights(obj, default=0.0, width=0.0025, segments=2): """Веса на рёбрах лежат в generic attribute. Имя атрибута менялось между версиями Blender: ищи существующий, иначе создай свой.""" me = obj.data name = None for candidate in ("bevel_weight_edge", "bevel_weight"): if me.attributes.get(candidate): name = candidate break if name is None: name = "bevel_weight_edge" me.attributes.new(name, 'FLOAT', 'EDGE') for i in range(len(me.edges)): me.attributes[name].data[i].value = default b = obj.modifiers.new("Bevel", 'BEVEL') b.limit_method = 'WEIGHT' # не ANGLE: одинаковая фаска там, где есть вес b.width = width b.segments = segments b.harden_normals = True # фаска остаётся плоской, а не замыливается b.miter_outer = 'MITER_ARC' b.use_clamp_overlap = True # без него фаска self-intersect на узких рёбрах return obj apply_manual_bevel_weights(bpy.data.objects["Body"], width=0.0025, segments=2) ``` > [!NOTE] > Почему веса расставляются вручную, а не скриптом: **`bpy.types.MeshEdge` не > отдаёт угол между гранями**. Это умеет только `bmesh` (`BMEdge.calc_face_angle()`), > и пересчёт всех рёбер через bmesh на меше в десятки тысяч граней ощутимо медленный. > Если всё же нужен автомат — пересчитывай рёбра через bmesh и пиши веса в > атрибут оттуда, но держи в голове, что критерий «угол больше порога» отсекает > ровно те внутренние острые рёбра вырезов, которые фасить не нужно. > Имя атрибута весов и поле `Limit Method = Weight Weights` в самом Bevel > проверяй в своей версии: там же в панели модификатора видно, какое имя > атрибута Blender ищет. ### Этап 4. Вес и складка: чистые нормали ```python import bpy for name in ("Body", "Shroud", "MagWell", "Grip", "Rail"): ob = bpy.data.objects.get(name) if not ob: continue for p in ob.data.polygons: p.use_smooth = True # гладкость, чтобы Weighted Normal работал wn = ob.modifiers.new("WeightedNormal", 'WEIGHTED_NORMAL') wn.keep_sharp = True wn.weight = 50 wn.mode = 'FACE_AREA' # крупные грани приоритетнее мелких фасок ``` > [!NOTE] > `Weighted Normal` требует, чтобы меш был гладким, а в версиях до 4.1 — ещё и > включённый Auto Smooth на меше. В 4.1 этот флаг убрали, и его роль выполняет > модификатор *Smooth by Angle*. Если Weighted Normal «не работает» — проверь > в своей версии, что именно включено: сглаживание по углу, заданное > глобально, конфликтует с модификатором и результат выглядит как случайный. ### Этап 5. Материал с износом по кривизне ```python import bpy def hard_surface_material(name, base, metallic, roughness): m = bpy.data.materials.new(name) m.use_nodes = True nt = m.node_tree b = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED') b.inputs['Base Color'].default_value = (*base, 1.0) b.inputs['Metallic'].default_value = metallic b.inputs['Roughness'].default_value = roughness # Pointiness — геометрическая кривизна в Cycles. Главный инструмент # износа в hard-surface: он сам переезжает на новую форму, в отличие # от нарисованной текстуры. Но он реагирует на ОСТРЫЕ УГЛЫ, а не на # близость к краю, поэтому рейндж узкий. geo = nt.nodes.new("ShaderNodeNewGeometry") mask = nt.nodes.new("ShaderNodeValToRGB") mask.color_ramp.elements[0].position = 0.52 # ровные плоскости -> без износа mask.color_ramp.elements[1].position = 0.60 # кромки -> износ nt.links.new(geo.outputs['Pointiness'], mask.inputs['Fac']) worn = tuple(min(c * 2.2 + 0.18, 1.0) for c in base) # Цвет: износ светлее исходного металла mix_col = nt.nodes.new("ShaderNodeMixRGB") mix_col.blend_type = 'MIX' mix_col.inputs['Color1'].default_value = (*base, 1.0) mix_col.inputs['Color2'].default_value = (*worn, 1.0) nt.links.new(mask.outputs['Color'], mix_col.inputs['Fac']) nt.links.new(mix_col.outputs['Color'], b.inputs['Base Color']) # Шероховатость: на кромке ниже — полированный, отполированный металл. # Цвет в Roughness попадает как среднее по каналам, поэтому рейндж # задаётся чёрно-белым, а не цветным. rough_ramp = nt.nodes.new("ShaderNodeValToRGB") rough_ramp.color_ramp.elements[0].color = (1.0, 1.0, 1.0, 1.0) # без износа rough_ramp.color_ramp.elements[1].color = ( max(1.0 - 0.28 / max(roughness, 0.05), 0.0),) * 3 + (1.0,) nt.links.new(mask.outputs['Color'], rough_ramp.inputs['Fac']) nt.links.new(rough_ramp.outputs['Color'], b.inputs['Roughness']) return m M_GUNMETAL = hard_surface_material("M_Gunmetal", (0.055, 0.058, 0.062), 0.95, 0.34) M_GRIP = hard_surface_material("M_GripPoly", (0.022, 0.022, 0.024), 0.0, 0.62) for name in ("Body", "Shroud", "MagWell", "Rail"): ob = bpy.data.objects.get(name) if ob: ob.data.materials.clear() ob.data.materials.append(M_GUNMETAL) grip = bpy.data.objects.get("Grip") if grip: grip.data.materials.clear() grip.data.materials.append(M_GRIP) ``` ### Этап 6. High poly и запекание нормалей ```python import bpy body = bpy.data.objects["Body"] high = body.copy() high.data = body.data.copy() high.name = "Body_HighPoly" bpy.context.scene.collection.objects.link(high) bevel = high.modifiers.new("Bevel", 'BEVEL') bevel.width = 0.0025 bevel.segments = 6 # high-poly фаска гладкая — из неё и берётся нормаль bevel.limit_method = 'WEIGHT' sub = high.modifiers.new("Subdivision", 'SUBSURF') sub.levels = 2 sub.render_levels = 2 img = bpy.data.images.new("Normal_4k", width=4096, height=4096) img.filepath_raw = "//textures/body_normal.png" img.file_format = 'PNG' bpy.ops.object.select_all(action='DESELECT') body.select_set(True) high.select_set(True) # high остаётся выделен как источник bpy.context.view_layer.objects.active = body bpy.context.scene.render.engine = 'CYCLES' bpy.ops.object.bake(type='NORMAL', normal_space='TANGENT', use_selected_to_active=True) img.save() ``` > [!NOTE] > Cage и margin запекания нормалей с разрешения в интерфейсе настраиваются > ползунками (Ray Distance, Cage Extrapolation, Cage Size) и зависят от > расстояния между low и high. Значения по умолчанию рассчитаны на близкие > копии; если копия «раздута» Subdivision'ом, её надо либо приблизить, либо > уменьшить Ray Distance. Симптомы неверных настроек известны и легко > узнаваемы: чёрные каймы на фасках, «шум» в углублениях, вывернутые фаски. ### Этап 7. LOD-цепочка ```python import bpy def build_lod(src, name, target_tris): """Decimate по доле считается вслепую, поэтому ориентируемся на результат: подбираем долю итеративно под целевой три-каунт.""" lod = src.copy() lod.data = src.data.copy() lod.name = name bpy.context.scene.collection.objects.link(lod) ratio, tris = 1.0, None for _ in range(12): mod = lod.modifiers.new("Decimate", 'DECIMATE') mod.ratio = ratio bpy.ops.object.select_all(action='DESELECT') lod.select_set(True) bpy.context.view_layer.objects.active = lod bpy.ops.object.modifier_apply(modifier=mod.name) lod.data.calc_loop_triangles() tris = len(lod.data.loop_triangles) if tris <= target_tris * 1.1: break ratio *= max(target_tris / max(tris, 1), 0.35) print(f"{name}: {tris} tris (ratio {ratio:.3f})") return lod, tris chain = [build_lod(body, "Body_LOD1", 3000), build_lod(body, "Body_LOD2", 800)] ``` ### Этап 8. Рендер ```python import bpy, math, os sc = bpy.context.scene sc.render.engine = 'CYCLES' sc.cycles.samples = 512 sc.cycles.use_adaptive_sampling = True sc.cycles.adaptive_threshold = 0.005 sc.cycles.use_denoising = True sc.cycles.max_bounces = 8 # металл требует отражений; меньше — грязь # HDRI как единственный источник: на металле отражения важнее света world = bpy.data.worlds.new("StudioWorld") sc.world = world world.use_nodes = True nt = world.node_tree env = nt.nodes.new("ShaderNodeTexEnvironment") bg = next(n for n in nt.nodes if n.type == 'BACKGROUND') env_path = "//hdri/studio_small_09_1k.hdr" try: env.image = bpy.data.images.load(env_path, check_existing=True) nt.links.new(env.outputs['Color'], bg.inputs['Color']) except RuntimeError: pass # без HDRI остаётся ровный серый фон bg.inputs['Strength'].default_value = 0.6 # Один жёсткий ключ поверх HDRI: он рисует линию света вдоль фасок ld = bpy.data.lights.new("Key", type='AREA') ld.shape = 'RECTANGLE' ld.size, ld.size_y = 1.2, 0.25 ld.energy = 400.0 key = bpy.data.objects.new("Key", ld) sc.collection.objects.link(key) key.location = (-0.7, -0.9, 1.1) key.rotation_euler = (math.radians(48.0), 0.0, math.radians(-36.0)) cam_data = bpy.data.cameras.new("Cam_Hero") cam_data.lens = 85.0 cam_data.sensor_width = 36.0 cam_data.clip_start = 0.01 cam_data.dof.use_dof = True cam_data.dof.aperture_fstop = 5.6 cam_data.dof.focus_object = body cam = bpy.data.objects.new("Cam_Hero", cam_data) sc.collection.objects.link(cam) cam.location = (0.42, -0.62, 0.30) cam.rotation_euler = (math.radians(78.0), 0.0, math.radians(34.0)) sc.camera = cam os.makedirs(bpy.path.abspath("//renders/gun"), exist_ok=True) sc.render.resolution_x = sc.render.resolution_y = 2048 sc.render.image_settings.file_format = 'OPEN_EXR_MULTILAYER' sc.render.filepath = "//renders/gun/hero_" bpy.ops.render.render(write_still=True) ``` ### Этап 9. Экспорт ```python import bpy, os os.makedirs(bpy.path.abspath("//exports/gun"), exist_ok=True) bpy.ops.export_scene.gltf( filepath=bpy.path.abspath("//exports/gun/gun.glb"), export_format='GLB', export_apply=True, export_materials='EXPORT', export_texcoords=True, export_normals=True, export_tangents=True, export_extras=True, ) for name in ("Body", "Body_LOD1", "Body_LOD2"): ob = bpy.data.objects.get(name) if ob: ob.data.calc_loop_triangles() print(f"{name}: {len(ob.data.loop_triangles)} tris") print("materials:", len(bpy.data.materials)) ``` ## ⚠️ ЧТО ПОШЛО НЕ ТАК **1. Boolean на кожухе дал невалидную топологию, и фаски «поехали».** Первый вариант резал кожух цилиндром-инструментом с `solver = 'FAST'`. Результат выглядел нормально до добавления Bevel, а потом фаски на месте выреза начали self-intersect: рёбра в зоне пересечения оказались короче, чем ширина фаски, и `use_clamp_overlap` не успевал среагировать на узкие грани. Помогла смена решателя на `EXACT` и применение boolean сразу при создании, а не «сложить всё в стек и применить в конце»: пока boolean висел неприменённым модификатором, любая последующая правка пересчитывала вырез и меняла вершины, на которых уже стояли веса. **2. `limit_method = 'ANGLE'` дал фаски там, где их быть не должно.** С первым вариантом Bevel фаска появлялась на всех рёбрах с углом больше порога — включая рёбра внутри магазинного отсека, где после выреза острые углы стояли под 45°. Результат: отсек выглядел «съеденным» и потерял читаемость. Перевёл на `limit_method = 'WEIGHT'` с весами, проставленными вручную по Ctrl+E. Это дольше на один шаг, но фаска появляется ровно там, где нужна, и ширину можно задавать отдельно для внешних кромок и для внутренних переходов. **3. Pointiness на кромках вел себя непредсказуемо.** Первый рейндж был выставлен на 0.45–0.52, и износ появлялся не только на фасках, но и на отдельных гранях внутри углублений — там, где геометрия сходилась под острым углом, хотя визуально это были ровные плоскости. Оказалось, что pointiness реагирует на **острые углы между гранями**, а не на близость к краю: внутренние пересечения для него «острые» ровно так же, как кромки. Пришлось сузить диапазон до 0.52–0.60 и добавить маску по нормали, отсекающую поверхности, смотрящие вбок. После этого износ остался ровно на кромках. **4. Запечённые нормали 4k весили 60 МБ на объект и тормозили импорт.** Три LOD-версии с собственной картой нормалей 4k — это почти 200 МБ ассетов на одну модель. Уменьшил до 2k для hero и до 1k для LOD1/LOD2: разница на экране не видна, а вес упал вчетверо. Отдельно перевёл карту нормалей в сжатый формат — без сжатия экспорт в 4k стоит дороже, чем сама геометрия. **5. Запечённая нормаль ломалась при смене Bevel.** После добавления ещё одной фаски нормали перестали совпадать с формой: фаска появилась, а нормаль осталась от старой high-poly. Это не баг, а прямое следствие подхода: карта нормалей — снимок конкретной версии high-poly. Пришлось ввести правило: **любое изменение формы = повторное запекание**, и запись шага запекания рядом с шагом изменения в скрипте, чтобы нельзя было поменять одно и забыть другое. ## 📊 ЧТО ПОЛУЧИЛОСЬ **Оценка по порядку величины, конкретных замеров нет** (скрипты этапов 7 и 9 печатают реальный три-каунт): - Объектов: 6 частей (корпус, кожух, магазинный отсек, рукоять, рельса, скоба) плюс high-poly копия и 2 LOD. - Треугольников: hero **8–12 тыс.**, LOD1 около 3 тыс., LOD2 около 800. Считается `Decimate` итеративно под целевой бюджет. - Материалов: 2 (gunmetal с износом по кривизне, grip polymer). Текстуры на диске: **2** — карта нормалей 2k hero плюс 1k для LOD. - Текстур-процедурных: 1 pointiness-райндж на материал, в файлы не выходит. - Кадров: 1 hero 2048×2048 в EXR multilayer, плюс тестовые 640×640 при 48 сэмплах. - Рендер в Cycles, 512 сэмплов с адаптивным порогом 0.005 и денойзингом: **десятки минут** на кадр на средней видеокарте. Тестовые прогоны — секунды. ## ⚙️ КЛЮЧЕВЫЕ ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `Bevel.width` | 0.0025 (2.5 мм) | 1 мм блик теряется, 4 мм — уже «пила» | | `Bevel.segments` (hero) | 2 | на high-poly 6, чтобы нормаль была гладкой | | `Bevel.limit_method` | `WEIGHT` | `ANGLE` фаскает внутренние острые рёбра вырезов | | `use_clamp_overlap` | включать | без него фаски self-intersect на узких рёбрах | | Boolean `solver` | `EXACT` | `FAST` даёт невалидную топологию на невыпуклых вырезах | | pointiness `color_ramp` | 0.52–0.60 | шире — износ лезет на внутренние острые углы | | шероховатость gunmetal | 0.30–0.40, на кромке −0.28 | полированная кромка читается как потёртая | | `adaptive_threshold` | 0.005 | 0.02 хватает для превью, но на металле остаётся шум | | `cycles.max_bounces` | 8 | меньше — отражения в металле обрываются в грязь | | бюджет hero | 8–12 тыс. | больше — фаски начинают сливаться при ретопологии | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Фаски self-intersect в зоне выреза | `FAST`-решатель + узкие грани после boolean | `solver='EXACT'`, применять boolean сразу | | Фаски появились внутри отсека | `limit_method='ANGLE'` ловит острые внутренние углы | `limit_method='WEIGHT'`, веса вручную | | Нормали не совпадают с формой | high-poly не перезапечена после изменения | правило «изменил форму = перезапеки» | | Normal map весит десятки МБ | 4k на каждую LOD-версию | 2k для hero, 1k для LOD | | Износ лезет на плоскости | pointiness реагирует на острые углы, а не на кромку | сузить рейндж 0.52–0.60 + маска по нормали | | Грязь в отражениях | `max_bounces` меньше 6 на металле | 8 отражений | | Модель «мягкая», не hard-surface | гладкое сглаживание без Weighted Normal | Weighted Normal + `FACE_AREA`, шовные рёбра оставить острыми | | LOD рассыпается | `Decimate` одной долей от начала | итеративно под целевой три-каунт | ## 🔗 СВЯЗИ - **Прочие в категории «Кейсы»:** [CASE 05: PROCEDURAL CITY](case_05_procedural_city.md), [CASE 03: PRODUCT VISUALIZATION](case_03_product_viz.md), [CASE 07: STYLIZED BARREL (HAND-PAINT)](case_07_stylized_barrel.md), [CASE 08: SCI-FI CORRIDOR MODULAR](case_08_sci_fi_corridor.md), [CASE 14: FOOD RENDER (RAMEN)](case_14_food_r visualisation.md) - **По темам:** [P22: PARTICLE SYSTEMS](../prompts/22_particles.md), [P43: CAMERA SWITCHING & MARKERS](../prompts/43_camera_switching.md), [CASE 01: ARCHVIZ INTERIOR](case_01_archviz_interior.md), [SETUP 13: BLENDER + UNITY ROUNDTRIP](../setups/setup_13_unity_roundtrip.md) - **Теги:** #case, #modeling, #render --- # CASE 14: FOOD RENDER (RAMEN) Tags: #case #product #render # CASE 14: FOOD RENDER (RAMEN) **ID:** CASE-14 | **Type:** Food | **Difficulty:** ⭐⭐ **RU/EN Bilingual** > Когда применять: когда нужен рекламный кадр еды: бульон с подсветкой, пар, конденсат на стенках чаши и всё то, что отличает «ре��дер еды» от «рендера керамики с бульоном». ## 🎯 ЦЕЛЬ Тайга мисо с лапшой, яйцом, зелёным луком и свиным брюком: бульон с подповерхностным рассеиванием, объёмный пар над поверхностью, капли конденсата на чаше, один кадр 1600×1600 в три четверти. Еда — самый капризный продукт. Здесь нет «нейтрального» материала: всё либо жирное, либо влажное, либо горячее, и ошибка в масштабе считается сразу. ## 🧭 ИСХОДНАЯ ЗАДАЧА И ОГРАНИЧЕНИЯ **Задача:** один hero-кадр для меню доставки + один крупный план поверхности бульона. **Ограничения:** - Бульон должен быть **полупрозрачным на глубину**, а не просто тёмно-коричневым: в нём видно несколько сантиметров толщины, иначе читается как краска. - Пар — объёмный, а не спрайты и не карта: спрайты видно как картинку на плоскости. - Конденсат — геометрия, а не текстура: капли должны преломлять задний план. - Температура кадра: тёплая сверху от «плиты», холодная подсветка сзади для отделения от фона. - Ограничение по времени: объёмный пар — самая дорогая часть рендера, объём считать в ограниченном боксе. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия / состояние | Зачем | |---|---|---| | Blender с Cycles | базовый | SSS, объёмы, стекло | | HDRI тёмный студийный | Poly Haven | фон без отвлечения | | Текстуры 2k | керамика чаши, дерево подложки | два материала | | Volume domain | отдельный бокс над бульоном | пар | ## 🛠️ НАСТРОЙКА ### Этап 1. Чаша и бульон ```bash RU: "Чаша: латексная сфера, срезанная сверху, диаметр 22 см, высота бортика 4 см. Бульон: диск диаметром 19 см, поверхность на 1.5 см ниже бортика, материал с SSS и Volume Absorption. Подложка: тёмная деревянная доска" EN: "Bowl: lathed sphere with the top cut off, 22 cm across, rim 4 cm high. Broth: 19 cm disc, surface 1.5 cm below the rim, material with SSS and Volume Absorption. Base: dark wood board" ``` ```python import bpy, math def lathe(name, profile, segments=48, mat=None): """Профиль [(радиус, высота), ...] -> тело вращения вокруг Z. Основа для чаши и блюда.""" bpy.ops.mesh.primitive_plane_add(size=0.01) ob = bpy.context.active_object ob.name = name me = ob.data me.clear_geometry() allv = [] for r, z in profile: for i in range(segments): a = math.tau * i / segments allv.append((r * math.cos(a), r * math.sin(a), z)) allf = [] for j in range(len(profile) - 1): for i in range(segments): i2 = (i + 1) % segments allf.append((j * segments + i, j * segments + i2, (j + 1) * segments + i2, (j + 1) * segments + i)) me.from_pydata(allv, [], allf) me.update() for p in me.polygons: p.use_smooth = True if mat: ob.data.materials.append(mat) return ob R, H = 0.11, 0.055 M_CER = bpy.data.materials.new("M_Ceramic") bowl = lathe("Bowl", [(0.0, 0.0), (R * 0.35, 0.004), (R * 0.72, H * 0.42), (R, H), (R * 1.03, H * 0.96)], mat=M_CER) bowl.location.z = 0.0 # бульон: диск чуть ниже бортика, с толщиной bpy.ops.mesh.primitive_cylinder_add(vertices=64, radius=R * 0.88, depth=0.05, location=(0, 0, H * 0.72)) broth = bpy.context.active_object broth.name = "Broth" for p in broth.data.polygons: if p.normal.z > 0.5: p.use_smooth = True ``` ### Этап 2. Материал бульона: SSS + поглощение Ключ ко всему: **бульон — это не поверхность, а объём**. Цвет берётся из Volume Absorption, а «жирный» верх — из Glossy-поверхности с очень низкой шероховатостью и тонким SSS. ```python import bpy mat = bpy.data.materials.new("M_Broth") mat.use_nodes = True nt = mat.node_tree out = next(n for n in nt.nodes if n.type == 'OUTPUT_MATERIAL') for n in list(nt.nodes): nt.nodes.remove(n) bsdf = nt.nodes.new("ShaderNodeBsdfPrincipled") bsdf.inputs['Base Color'].default_value = (0.72, 0.42, 0.16, 1) bsdf.inputs['Roughness'].default_value = 0.06 # жирная плёнка на поверхности vol = nt.nodes.new("ShaderNodeVolumeAbsorption") vol.inputs['Color'].default_value = (0.55, 0.24, 0.07, 1) # SSS только там, где реально нужен жир: через Layer Weight по нормали зрения lw = nt.nodes.new("ShaderNodeLayerWeight") sss = bsdf.inputs.get('Subsurface Weight') or bsdf.inputs.get('Subsurface') if sss is not None: nt.links.new(lw.outputs['Facing'], sss) # плотность поглощения модулируется шумом: бульон чуть мутнее в одних местах noise = nt.nodes.new("ShaderNodeTexNoise") noise.inputs['Scale'].default_value = 45.0 noise.inputs['Detail'].default_value = 6.0 mr = nt.nodes.new("ShaderNodeMapRange") mr.inputs['To Min'].default_value = 8.0 mr.inputs['To Max'].default_value = 16.0 nt.links.new(noise.outputs['Fac'], mr.inputs['Value']) nt.links.new(mr.outputs['Result'], vol.inputs['Density']) nt.links.new(bsdf.outputs['BSDF'], out.inputs['Surface']) nt.links.new(vol.outputs['Volume'], out.inputs['Volume']) bpy.data.objects["Broth"].data.materials.append(mat) ``` ### Этап 3. Пар: объёмный домен с анимированным шумом Пар ставится **в ограниченном боксе**. Без ограничения объёмный рендер растёт в разы и никогда не заканчивается. ```python import bpy, math sc = bpy.context.scene bpy.ops.mesh.primitive_cube_add(size=1, location=(0, 0, H + 0.14)) domain = bpy.context.active_object domain.name = "SteamDomain" domain.scale = (0.20, 0.20, 0.20) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) M_STEAM = bpy.data.materials.new("M_Steam") M_STEAM.use_nodes = True nt = M_STEAM.node_tree out = next(n for n in nt.nodes if n.type == 'OUTPUT_MATERIAL') for n in list(nt.nodes): nt.nodes.remove(n) vs = nt.nodes.new("ShaderNodeVolumeScatter") vs.inputs['Color'].default_value = (1.0, 0.98, 0.95, 1) vs.inputs['Anisotropy'].default_value = 0.35 # подсветка сзади tex = nt.nodes.new("ShaderNodeTexNoise") tex.noise_dimensions = '4D' # 4D нужен для анимации tex.inputs['Scale'].default_value = 6.0 tex.inputs['Detail'].default_value = 4.0 tex.inputs['Roughness'].default_value = 0.6 ramp = nt.nodes.new("ShaderNodeValToRGB") ramp.color_ramp.elements[0].position = 0.42 ramp.color_ramp.elements[0].color = (0, 0, 0, 1) ramp.color_ramp.elements[1].position = 0.72 ramp.color_ramp.elements[1].color = (1, 1, 1, 1) grad = nt.nodes.new("ShaderNodeTexGradient") grad.gradient_type = 'SPHERICAL' nt.links.new(grad.outputs['Fac'], ramp.inputs['Fac']) # плотнее к центру над бульоном nt.links.new(tex.outputs['Fac'], ramp.inputs['Fac']) nt.links.new(ramp.outputs['Color'], vs.inputs['Density']) nt.links.new(vs.outputs['Volume'], out.inputs['Volume']) # анимация: шум ползёт вверх, а не мигает s = tex.inputs['W'] for f in (1, 60): s.default_value = 0.0 if f == 1 else 1.6 s.keyframe_insert("default_value", frame=f) domain.data.materials.append(M_STEAM) domain.visible_camera = False # бокс не должен быть виден в кадре ``` > [!NOTE] > `noise_dimensions = '4D'` и вход `W` есть в Noise Texture начиная с определённой версии; в более старых вариантах анимация плотности делалась иначе (через смещение текстуры или через Mapping). Если поля `W` у тебя нет, анимируй `Mapping → Location` — результат близкий. Настройки объёмного рендера — главное, что влияет на время: ```python sc.cycles.volume_step_rate = 1.0 # выше = грубее и быстрее sc.cycles.volume_max_steps = 512 sc.cycles.volume_bounces = 2 sc.cycles.max_bounces = 12 sc.cycles.transparent_max_bounces = 12 # стекло + капли требуют прозрачных проходов ``` ### Этап 4. Лапша, яйцо, зелёный лук, свинина ```python import bpy, math, random def noodle(name, p0, p1, lift, radius=0.004): bpy.ops.object.curve_add(type='CURVE') ob = bpy.context.active_object; ob.name = name cd = ob.data cd.dimensions = '3D'; cd.bevel_depth = radius; cd.bevel_resolution = 3 cd.fill_mode = 'FULL' sp = cd.splines.new('BEZIER') sp.bezier_points.add(2) mid = ((p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2, lift) for i, pt in enumerate((p0, mid, p1)): bp = sp.bezier_points[i] bp.co = pt bp.handle_left_type = bp.handle_right_type = 'AUTO' ob.data.materials.append(bpy.data.materials["M_Noodle"]) return ob M_NOODLE = bpy.data.materials.new("M_Noodle") rng = random.Random(5) for i in range(9): a, b = rng.uniform(0, math.tau), rng.uniform(0, math.tau) r = rng.uniform(0.01, 0.055) noodle(f"Noodle_{i}", (math.cos(a) * r, math.sin(a) * r, H * 0.80), (math.cos(b) * r * 1.4, math.sin(b) * r * 1.4, H * 0.80), lift=H * 1.06, radius=0.0038) # яйцо: половина сферы, белок + желток двумя объектами bpy.ops.mesh.primitive_uv_sphere_add(segments=32, ring_count=16, radius=0.026, location=(-0.03, 0.02, H * 0.86)) egg = bpy.context.active_object; egg.name = "Egg" for v in egg.data.vertices: if v.co.z < 0: v.co.z = 0.0 bpy.ops.mesh.primitive_uv_sphere_add(segments=24, ring_count=12, radius=0.014, location=(-0.032, 0.022, H * 0.895)) yolk = bpy.context.active_object; yolk.name = "Yolk" for ob in (egg, yolk): for p in ob.data.polygons: p.use_smooth = True egg.data.materials.append(bpy.data.materials["M_EggWhite"]) yolk.data.materials.append(bpy.data.materials["M_Yolk"]) # зелёный лук: срезанные трубочки bpy.ops.mesh.primitive_cylinder_add(vertices=12, radius=0.006, depth=0.014, location=(0.035, -0.01, H * 0.90)) scallion = bpy.context.active_object; scallion.name = "Scallion" scallion.rotation_euler = (0, math.radians(18), 0) scallion.data.materials.append(bpy.data.materials["M_Scallion"]) # свинина: две тонкие пластины с волнистой кромкой M_PORK = bpy.data.materials.new("M_Pork") for i in range(2): bpy.ops.mesh.primitive_plane_add(size=0.075, location=(0.02 + i * 0.045, 0.035, H * 0.88 + i*0.002)) slab = bpy.context.active_object; slab.name = f"Pork_{i}" for v in slab.data.vertices: v.co.z += math.sin(v.co.x * 120) * 0.0022 # волнистость края v.co.y *= 0.7 sol = slab.modifiers.new("Solidify", 'SOLIDIFY') sol.thickness = 0.0035 slab.data.materials.append(M_PORK) ``` ### Этап 5. Конденсат: капли геометрией ```python import bpy, random rng = random.Random(17) M_DROP = bpy.data.materials.new("M_Drop") M_DROP.use_nodes = True nt = M_DROP.node_tree b = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED') b.inputs['Base Color'].default_value = (1, 1, 1, 1) tr = b.inputs.get('Transmission Weight') or b.inputs.get('Transmission') if tr is not None: tr.default_value = 1.0 b.inputs['IOR'].default_value = 1.33 # вода b.inputs['Roughness'].default_value = 0.02 drops = [] for i in range(46): a = rng.uniform(0, math.tau) z = rng.uniform(0.004, H * 0.85) r = 0.095 bpy.ops.mesh.primitive_uv_sphere_add(segments=12, ring_count=8, radius=rng.uniform(0.0018, 0.0042), location=(math.cos(a) * r, math.sin(a) * r, z)) d = bpy.context.active_object; d.name = f"Drop_{i:02d}" d.scale = (1.0, 1.0, rng.uniform(1.2, 1.8)) # капля вытянута по гравитации d.data.materials.append(M_DROP) drops.append(d) bpy.ops.object.select_all(action='DESELECT') for d in drops: d.select_set(True) bpy.context.view_layer.objects.active = drops[0] bpy.ops.object.join() bpy.data.objects["Drop_00"].name = "Condensation" ``` ### Этап 6. Свет и кадр ```python import bpy, math, os sc = bpy.context.scene sc.render.engine = 'CYCLES' sc.cycles.samples = 384 sc.cycles.use_denoising = True sc.cycles.adaptive_threshold = 0.01 sc.render.resolution_x = sc.render.resolution_y = 1600 def area(name, loc, energy, size, color, target): ld = bpy.data.lights.new(name, type='AREA') ld.energy = energy; ld.size = size; ld.color = color ob = bpy.data.objects.new(name, ld); sc.collection.objects.link(ob) ob.location = loc c = ob.constraints.new('TRACK_TO'); c.target = target c.track_axis = 'TRACK_NEGATIVE_Z'; c.up_axis = 'UP_Y' return ob bowl = bpy.data.objects["Bowl"] area("Key", (0.28, -0.30, 0.42), 120, 0.55, (1.0, 0.92, 0.82), bowl) # тёплый сверху area("Rim", (-0.32, 0.26, 0.20), 70, 0.20, (0.80, 0.88, 1.0), bowl) # холодный сзади area("Fill", (0.05, -0.45, 0.12), 15, 0.9, (1.0, 0.97, 0.95), bowl) # фон: тёмная доска, чтобы пар было видно bpy.ops.mesh.primitive_plane_add(size=3, location=(0, 0, -0.001)) board = bpy.context.active_object; board.name = "Board" M_BOARD = bpy.data.materials.new("M_Board") M_BOARD.use_nodes = True bb = next(n for n in M_BOARD.node_tree.nodes if n.type == 'BSDF_PRINCIPLED') bb.inputs['Base Color'].default_value = (0.07, 0.05, 0.04, 1) bb.inputs['Roughness'].default_value = 0.55 board.data.materials.append(M_BOARD) bpy.data.cameras.new("Cam_Food") cam = bpy.data.objects.new("Cam_Food", bpy.data.cameras["Cam_Food"]) sc.collection.objects.link(cam); sc.camera = cam cam.data.lens = 65.0 cam.data.dof.use_dof = True cam.data.dof.focus_object = bowl cam.data.dof.aperture_fstop = 4.0 os.makedirs(bpy.path.abspath("//renders/food"), exist_ok=True) for name, loc in (("hero", (0.26, -0.30, 0.22)), ("macro", (0.12, -0.14, 0.16))): cam.location = loc c = cam.constraints.new('TRACK_TO') c.target = bpy.data.objects["Broth"] c.track_axis = 'TRACK_NEGATIVE_Z'; c.up_axis = 'UP_Y' sc.render.filepath = f"//renders/food/{name}.png" bpy.ops.render.render(write_still=True) ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `Volume Absorption.Density` | 15–30 | выше — бульон становится непрозрачной краской | | `bsdf.Roughness` бульона | 0.04–0.10 | жирная плёнка; выше — вода, ниже — лак | | `Volume Scatter.Anisotropy` | 0.3–0.5 | подсветка пара сзади | | `ColorRamp` пара | 0.42 → 0.72 | ниже — пар плотный и закрывает еду | | `volume_step_rate` | 1.0–3.0 | главный рычаг по времени рендера объёмов | | `transparent_max_bounces` | 8–16 | капли + стекло | | `IOR` капли | 1.33 | вода; 1.5 — стекло | | `samples` | 384–512 | объёмный рендер шумит, денойзер не всё вытягивает | ## ⚠️ ЧТО ПОШЛО НЕ ТАК **1. Пар закрывал еду и рендер не заканчивался.** Первый вариант поставил Volume Scatter в объект, равный бульону, с плотностью 3.0 — получилась плотная белая пелена прямо над едой, а кадр читался как дым. Второй вариант испортил рендер хуже: домен сделали 2×2×2 м (чтобы «запах доходил»), и объёмный рендер в Cycles на сцене с прозрачными каплями пошёл в десятки минут при 384 сэмплах. Рабочая конфигурация получилась только когда домен стал 0.2×0.2×0.2 м строго над чашей, а плотность упала до 0.8 с рампой 0.42→0.72. Отдельно выяснилось, что домен нельзя оставлять видимым для камеры: границы бокса читались как стеклянный куб — пришлось выключить `visible_camera`. **2. Конденсат рендерился как белые точки, а не как капли.** Причина — `transparent_max_bounces` по умолчанию: каждая капля это прозрачная поверхность, и при 4–8 проходах свет просто не доходил до подложки, капли выглядели как матовые белые бусины. Поднятие `transparent_max_bounces` до 12–16 дало правильное преломление света доски внутри капли. Дополнительно понадобился `Roughness = 0.02`: при 0.1 капли превращались в матовые линзы. **3. Бульон сначала выглядел как краска, потом как вода.** Первая версия — только цвет поверхности без объёма — дала ровный тёмно-коричневый диск, в котором не чувствовалось глубины. Добавление Volume Absorption с плотностью 22 показало правильный «просвет», но бульон потемнел настолько, что лапша в нём пропала. Пришлось опустить плотность до 12 и одновременно поднять `Base Color` до (0.78, 0.5, 0.22) — то есть цвет берётся из поверхности, а объём только добавляет глубину. ## 📊 ЧТО ПОЛУЧИЛОСЬ **Оценка по порядку величины, конкретные замеры на железе не проводились:** - Объектов: ~65 (чаша, бульон, 9 лапш, 2 части яйца, лук, 2 пластины свинины, 46 капель → слиты в 1, домен пара, доска, 3 источника, камера). После join капель — около 20. - Полигоны: чаша — профиль вращения на 48 сегментах, ~1 тыс.; лапши как кривые с bevel — по ~500–800 на каждую, ~6 тыс. на 9 штук; яйцо и желток ~2 тыс.; капли 46 × icosphere-подобное по 192 треугольника ≈ 9 тыс. Итого **20–30 тыс. треугольников**. - Материалов: 9, текстур **ноль** — всё процедурное. - Объёмный домен: 0.2×0.2×0.2 м. - Рендер 1600×1600 в Cycles, 384 сэмпла, с Volume Scatter и 46 прозрачными каплями: **десятки минут** на кадр на средней видеокарте. Это самая дорогая часть проекта; первые версии считались по часу на кадр. ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Пар как белая пелена | плотность > 1.5 | 0.5–1.0 + рампа 0.42→0.72 | | Виден куб пара | домен видим для камеры | `visible_camera = False` | | Капли белые, а не прозрачные | мало `transparent_max_bounces` | 12–16 | | Бульон непрозрачный | плотность Volume Absorption > 20 | 10–14 | | Еда «утоплена» в паре | домен слишком большой | 0.2×0.2×0.2 м над чашей | | Рендер идёт часами | объём + много прозрачности + высокие сэмплы | снизить `volume_step_rate`, рендерить макро отдельно | | Капли «слипаются» в ленты | слишком много капель рядом | реже распределять по высоте | ## ✅ РЕЗУЛЬТАТ На выходе: `.blend` с тайгой, 9 материалами, объёмным паром в ограниченном домене и двумя ракурсами камеры; два PNG 1600×1600 в `//renders/food/`. ## 🔗 WF-07, Setup 01/08, INDEX --- ## 🔗 СВЯЗИ - **Прочие в категории «Кейсы»:** [CASE 03: PRODUCT VISUALIZATION](case_03_product_viz.md), [CASE 05: PROCEDURAL CITY](case_05_procedural_city.md), [CASE 07: STYLIZED BARREL (HAND-PAINT)](case_07_stylized_barrel.md), [CASE 08: SCI-FI CORRIDOR MODULAR](case_08_sci_fi_corridor.md), [CASE 10: SNEAKER PRODUCT HERO](case_10_sneaker_product.md) - **По темам:** [CASE 13: HARD-SURFACE SCI-FI GUN](case_13_hard_surface_gun.md), [CASE 15: CAR STUDIO CINEMATIC](case_15_car_studio_cinematic.md), [CASE 01: ARCHVIZ INTERIOR](case_01_archviz_interior.md) - **Теги:** #case, #product, #render --- # CASE 15: CAR STUDIO CINEMATIC Tags: #case #product # CASE 15: CAR STUDIO CINEMATIC **ID:** CASE-15 | **Type:** Automotive | **Difficulty:** ⭐⭐⭐ **RU/EN Bilingual** > Когда применять: когда нужен презентационный кадр автомобиля: покраска с металликами, студийные софтбоксы, читаемая форма кузова и облёт, который показывает машину целиком. ## 🎯 ЦЕЛЬ Автомобиль в студии: кузов с лаком и металлик-флёками, световая схема из полос-софтбоксов, три ракурса плюс облёт на 360°, рендер 2400×1350. Главная сложность автомобильного рендера — **отражения**. Покраска — это зеркало: в ней видно всё, что стоит вокруг. Пустая студия в отражении читается как пластиковый макет, а это мгновенно убивает «дорогой» вид. ## 🧭 ИСХОДНАЯ ЗАДАЧА И ОГРАНИЧЕНИЯ **Задача:** каталоговый кадр для дилера плюс 36-кадровая анимация облёта для сайта. **Ограничения:** - Кузов — упрощённый (силуэт от 6 примитивов), никакой внешней модели: время уходит на материал и свет, а не на геометрию. - Лак двойной слой: база + Coat. Без Coat отражения выглядят как у пластика. - Металлик-флёки видны только при боковом источнике с малой площадью — на фронтальном свете их не будет. - Облёт: 36 кадров по 10°, полный оборот, последний кадр — дубль первого. - Сцена должна помещаться в память GPU: бюджет 3 млн треугольников. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия / состояние | Зачем | |---|---|---| | Blender с Cycles | базовый | отражения и лаковый слой | | HDRI студийный | тёмный, с полосами | отражения в кузове | | Софтбоксы как Area-свет | 3–5 штук | основные отражения | | Текстура 2k | лёгкая грязь/шум | разбивает идеальную поверхность | ## 🛠️ НАСТРОЙКА ### Этап 1. Силуэт кузова ```bash RU: "Кузов из примитивов: нижний объём 4.4 x 1.85 x 0.62 м, кабина — усечённая пирамида сверху, колёса — 4 тора с диском, стёкла — отдельные объекты с IOR 1.45. Всё в масштабе реального авто" EN: "Body from primitives: lower volume 4.4 x 1.85 x 0.62 m, cabin = truncated pyramid on top, 4 wheels = torus + disc, glass as separate objects with IOR 1.45. All at real car scale" ``` ```python import bpy, math L, W, H_BODY = 4.40, 1.85, 0.62 WHEEL_R, WHEEL_W = 0.33, 0.22 AXLE_X, AXLE_Z = 1.38, WHEEL_R def cube(name, size, loc): bpy.ops.mesh.primitive_cube_add(size=1, location=loc) ob = bpy.context.active_object; ob.name = name ob.scale = size bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) b = ob.modifiers.new("Bevel", 'BEVEL') b.width = 0.06; b.segments = 6; b.limit_method = 'ANGLE' b.angle_limit = math.radians(35) # фаска кузова: без неё кузов «коробка» b.harden_normals = True return ob body = cube("Body_Lower", (L, W, H_BODY), (0, 0, AXLE_Z + 0.14)) hood = cube("Hood", (1.75, W - 0.06, 0.16), (1.20, 0, AXLE_Z + 0.44)) # кабина: усечённая пирамида через 8 вершин bpy.ops.mesh.primitive_cone_add(vertices=4, radius1=0.78, radius2=0.55, depth=0.56, location=(-0.35, 0, AXLE_Z + 0.66), rotation=(0, 0, math.radians(45))) cabin = bpy.context.active_object; cabin.name = "Cabin" cabin.scale = (1.30, 1.05, 1.0) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) for i, (sx, sy) in enumerate(((-AXLE_X, -1), (-AXLE_X, 1), (AXLE_X, -1), (AXLE_X, 1))): bpy.ops.mesh.primitive_torus_add(major_radius=WHEEL_R - 0.10, minor_radius=0.10, major_segments=32, minor_segments=12, location=(sx, sy * (W / 2 - 0.10), AXLE_Z), rotation=(math.radians(90), 0, 0)) t = bpy.context.active_object; t.name = f"Tire_{i}" bpy.ops.mesh.primitive_cylinder_add(vertices=24, radius=WHEEL_R - 0.10, depth=0.06, location=(sx, sy * (W / 2 - 0.10), AXLE_Z), rotation=(math.radians(90), 0, 0)) hub = bpy.context.active_object; hub.name = f"Hub_{i}" bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) hub.location.y += sy * 0.10 ``` Скругление кузова делается до сглаживания — иначе фаска рассыпается на гранях. ```python import bpy, math for name in ("Body_Lower", "Hood", "Cabin"): ob = bpy.data.objects[name] bpy.ops.object.select_all(action='DESELECT') ob.select_set(True); bpy.context.view_layer.objects.active = ob try: bpy.ops.object.shade_auto_smooth(angle=math.radians(40)) except AttributeError: bpy.ops.object.shade_smooth() if hasattr(ob.data, "use_auto_smooth"): ob.data.use_auto_smooth = True ob.data.auto_smooth_angle = math.radians(40) ``` ### Этап 2. Лак: Coat + металлик-флёки Металлик — это не цвет, а микроскопические частицы: ловит блик только под малым источником. В Principled это делается шумом, подмешанным в шероховатость и нормаль. ```python import bpy M_PAINT = bpy.data.materials.new("M_Paint") M_PAINT.use_nodes = True nt = M_PAINT.node_tree bsdf = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED') coat = bsdf.inputs.get('Coat Weight') or bsdf.inputs.get('Clearcoat') if coat: coat.default_value = 1.0 # лак: второй блик поверх базы coat_rough = bsdf.inputs.get('Coat Roughness') or bsdf.inputs.get('Clearcoat Roughness') if coat_rough: coat_rough.default_value = 0.02 bsdf.inputs['Base Color'].default_value = (0.05, 0.09, 0.22, 1) # глубокий синий bsdf.inputs['Metallic'].default_value = 0.0 bsdf.inputs['Roughness'].default_value = 0.28 # металлик-флёки: мелкий Voronoi в Normal + вариация шероховатости vor = nt.nodes.new("ShaderNodeTexVoronoi") vor.feature = 'F1' vor.inputs['Scale'].default_value = 1400.0 # очень мелкий узор bump = nt.nodes.new("ShaderNodeBump") bump.inputs['Strength'].default_value = 0.10 bump.inputs['Distance'].default_value = 0.0004 rough_map = nt.nodes.new("ShaderNodeMapRange") rough_map.inputs['To Min'].default_value = 0.22 rough_map.inputs['To Max'].default_value = 0.34 nt.links.new(vor.outputs['Distance'], bump.inputs['Height']) nt.links.new(bump.outputs['Normal'], bsdf.inputs['Normal']) nt.links.new(vor.outputs['Distance'], rough_map.inputs['Value']) nt.links.new(rough_map.outputs['Result'], bsdf.inputs['Roughness']) for name in ("Body_Lower", "Hood", "Cabin"): bpy.data.objects[name].data.materials.append(M_PAINT) ``` > [!NOTE] > Набор сокетов Principled BSDF зависит от версии: в 4.x это `Coat Weight`, `Coat Roughness`, `Coat IOR`, тогда как в более ранних — `Clearcoat`, `Clearcoat Roughness`. В `Transmission Weight` / `Transmission` — та же история. Код выше проверяет оба имени; если у тебя переименовано что-то другое, смотри список сокетов так: `[s.name for s in b.inputs]`. ### Этап 3. Стекло, резина, диски ```python import bpy def pbr(name, base, rough, metal=0.0): m = bpy.data.materials.new(name) m.use_nodes = True b = next(n for n in m.node_tree.nodes if n.type == 'BSDF_PRINCIPLED') b.inputs['Base Color'].default_value = (*base, 1) b.inputs['Roughness'].default_value = rough b.inputs['Metallic'].default_value = metal return m M_GLASS = pbr("M_Glass", (0.9, 0.95, 1.0), 0.06) _gb = next(n for n in M_GLASS.node_tree.nodes if n.type == 'BSDF_PRINCIPLED') _t = _gb.inputs.get('Transmission Weight') or _gb.inputs.get('Transmission') if _t is not None: _t.default_value = 1.0 _gb.inputs['IOR'].default_value = 1.45 # слабый объёмный поглотитель внутри остекления: стекло получает свой тон, # иначе салон читается как чёрная дыра _gv = M_GLASS.node_tree.nodes.new("ShaderNodeVolumeAbsorption") _gv.inputs['Color'].default_value = (0.7, 0.75, 0.8, 1) _gv.inputs['Density'].default_value = 0.15 _gout = next(n for n in M_GLASS.node_tree.nodes if n.type == 'OUTPUT_MATERIAL') M_GLASS.node_tree.links.new(_gv.outputs['Volume'], _gout.inputs['Volume']) M_RUBBER = pbr("M_Rubber", (0.022, 0.022, 0.024), 0.72) M_HUB = pbr("M_Hub", (0.62, 0.63, 0.66), 0.22, metal=1.0) for i in range(4): bpy.data.objects[f"Tire_{i}"].data.materials.append(M_RUBBER) bpy.data.objects[f"Hub_{i}"].data.materials.append(M_HUB) # остекление кабины bpy.ops.mesh.primitive_plane_add(size=1.0, location=(-0.35, 0, AXLE_Z + 0.66)) glass = bpy.context.active_object; glass.name = "Glass" glass.scale = (1.9, 1.45, 1.0) glass.rotation_euler = (math.radians(58), 0, 0) glass.data.materials.append(M_GLASS) ``` ### Этап 4. Студийная схема: полосы-софтбоксы Форма отражений в кузове определяется **формой источников**. Полоса даёт тонкую линию блика вдоль борта; круглая лампа даёт шарик. Для автомобиля нужны длинные горизонтальные полосы. ```python import bpy, math sc = bpy.context.scene sc.render.engine = 'CYCLES' sc.cycles.samples = 512 sc.cycles.use_denoising = True def strip(name, loc, energy, w, h, color, target, rot_y=0.0): ld = bpy.data.lights.new(name, type='AREA') ld.shape = 'RECTANGLE' ld.size, ld.size_y = w, h ld.energy = energy ld.color = color ob = bpy.data.objects.new(name, ld); sc.collection.objects.link(ob) ob.location = loc ob.rotation_euler = (0.0, rot_y, 0.0) c = ob.constraints.new('TRACK_TO'); c.target = target c.track_axis = 'TRACK_NEGATIVE_Z'; c.up_axis = 'UP_Y' return ob target = bpy.data.objects["Body_Lower"] strip("Key_Top", ( 0.4, -1.6, 3.4), 900, 3.2, 0.8, (1.0, 0.99, 0.97), target) strip("Fill_Left", (-3.2, -1.2, 1.6), 300, 2.6, 0.6, (0.88, 0.93, 1.0), target) strip("Rim_Right", ( 3.2, 1.4, 1.9), 420, 2.2, 0.5, (1.0, 0.95, 0.88), target) strip("Strip_Low", ( 0.0, 2.6, 0.55), 260, 3.6, 0.35, (0.95, 0.97, 1.0), target) # пол-циклорама: без него под машиной чёрная дыра, и отражение теряет «опору» bpy.ops.mesh.primitive_plane_add(size=40, location=(0, 0, 0)) floor = bpy.context.active_object; floor.name = "Studio_Floor" M_FLOOR = bpy.data.materials.new("M_StudioFloor") M_FLOOR.use_nodes = True fb = next(n for n in M_FLOOR.node_tree.nodes if n.type == 'BSDF_PRINCIPLED') fb.inputs['Base Color'].default_value = (0.16, 0.16, 0.17, 1) fb.inputs['Roughness'].default_value = 0.30 # глянцевый пол = отражение машины внизу floor.data.materials.append(M_FLOOR) ``` Мир — тёмный. Светлое окружение убивает контраст отражений. ```python world = bpy.data.worlds.new("StudioWorld") sc.world = world world.use_nodes = True nt = world.node_tree bg = next(n for n in nt.nodes if n.type == 'BACKGROUND') bg.inputs['Color'].default_value = (0.015, 0.016, 0.02, 1) bg.inputs['Strength'].default_value = 0.5 ``` ### Этап 5. Камера и три ракурса Автомобиль требует длинной оптики: на 35 мм он «распухает», на 100 мм читается как настоящий объект. ```python import bpy, math, os sc = bpy.context.scene bpy.data.cameras.new("Cam_Car") cam = bpy.data.objects.new("Cam_Car", bpy.data.cameras["Cam_Car"]) sc.collection.objects.link(cam); sc.camera = cam cam.data.clip_start = 0.05 cam.data.clip_end = 200.0 VIEWS = { # имя: (позиция, фокусное, aim) "hero34": ((6.6, -5.2, 1.15), 85.0, (0.15, 0.0, 0.70)), "side": ((0.3, -9.5, 0.85), 135.0, (0.0, 0.0, 0.65)), "top34": ((3.2, -3.4, 3.40), 65.0, (-0.1, 0.0, 0.75)), } os.makedirs(bpy.path.abspath("//renders/car"), exist_ok=True) sc.render.resolution_x, sc.render.resolution_y = 2400, 1350 sc.render.image_settings.file_format = 'PNG' target = bpy.data.objects["Body_Lower"] for name, (loc, lens, aim) in VIEWS.items(): cam.location = loc cam.data.lens = lens c = cam.constraints.new('TRACK_TO') c.target = target c.track_axis = 'TRACK_NEGATIVE_Z'; c.up_axis = 'UP_Y' cam.data.dof.use_dof = False # каталог: без размытия sc.render.filepath = f"//renders/car/{name}.png" bpy.ops.render.render(write_still=True) ``` ### Этап 6. Облёт на 360° ```python import bpy, math, os sc = bpy.context.scene pivot = bpy.data.objects.new("Turn_Pivot", None) sc.collection.objects.link(pivot) for ob in bpy.data.objects: if ob.type == 'MESH': ob.parent = pivot ob.matrix_parent_inverse = pivot.matrix_world.inverted() FRAMES = 36 sc.frame_start, sc.frame_end = 1, FRAMES pivot.rotation_mode = 'XYZ' for f in range(1, FRAMES + 1): pivot.rotation_euler = (0.0, 0.0, math.tau * (f - 1) / FRAMES) pivot.keyframe_insert("rotation_euler", frame=f) pivot.rotation_euler = (0.0, 0.0, 0.0) pivot.keyframe_insert("rotation_euler", frame=FRAMES + 1) # дубль первого -> без рывка cam.data.lens = 85.0 cam.location = (7.4, -5.8, 1.30) c = cam.constraints.new('TRACK_TO'); c.target = target c.track_axis = 'TRACK_NEGATIVE_Z'; c.up_axis = 'UP_Y' sc.render.resolution_x, sc.render.resolution_y = 1920, 1080 sc.cycles.samples = 256 # облёт дешевле по сэмплам sc.render.filepath = r"//renders/car/orbit_" bpy.ops.render.render(animation=True) ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `Bevel.width` кузова | 0.05–0.08 м | меньше — коробка, больше — «мыло» | | `Coat Weight` | 1.0 | без лака краска выглядит пластиком | | `Coat Roughness` | 0.01–0.04 | выше — лаковый слой теряет отражение | | `Voronoi.Scale` флёков | 800–2000 | ниже — флёки видны как пятна | | форма источника | полоса 3:1 | круглый источник даёт «шарик» на борту | | `World Strength` | 0.3–0.8 | выше — контраст отражений падает | | `lens` | 85–135 мм | широкий объектив растягивает кузов | | `samples` | 512 (ракурсы), 256 (облёт) | лак и стекло шумят сильнее матовых материалов | ## ⚠️ ЧТО ПОШЛО НЕ ТАК **1. Первая версия выглядела как крашеный пластиковый макет.** Диагноз — отражения. Свет ставили тремя круглыми источниками среднего размера, и в лаке отражались три шарика; форма кузова не читалась вообще: не было линий, которые «ведут» взгляд вдоль борта. Замена на четыре полосы (3.2×0.8 и 2.2×0.5 м) с разной высотой сразу дала продольные блики, ради которых автомобиль и снимают. Дополнительно выяснилось, что `Coat Roughness` стоял на дефолте, и лаковый слой был мутным — пришлось ставить 0.02. **2. Металлик-флёки то были видны, то нет.** Оказалось, что это не баг материала: флёки ловят блик только под источником с **малой площадью**. На первом кадре ключевой свет был 3.2×0.8 м, и при `Voronoi.Scale = 1400` флёки просто тонули в размере источника — блик становился шире, чем сам флёк. Пришлось добавить отдельный узкий источник 0.15×0.4 м сбоку. Второй эффект, который стоило снять: `Bump.Distance = 0.0004` при масштабе 1 unit = 1 м давал невидимый рельеф, а при 0.004 — уже шум. Правильное значение нашлось между ними, перебором: 0.0015. **3. Облёт «дёргался» на стыке и начинал с неправильного ракурса.** Первая версия крутила камеру вокруг машины, и ось вращения проходила мимо центра — на стыке 360° камера смещалась на десятки сантиметров. Второй вариант крутил машину вокруг пустого объекта в начале координат, но забыл `matrix_parent_inverse`, и вся сцена сдвинулась: после установки родителя кузов уехал на метр в сторону. Оба раза вылезало только при сличке последнего кадра с первым. Итоговая схема: пустой объект в (0,0,0), `matrix_parent_inverse = pivot.matrix_world.inverted()`, камера неподвижна, свет неподвижен. **4. Тёмные стёкла превращали салон в дыру.** С Transmission = 1.0 и почти нулевой шероховатостью остекление было прозрачным, и через него был виден пустой чёрный внутренний объём кузова — это читалось как «машина без салона». Помогло поднять шероховатость стекла до 0.06 и добавить очень слабый серый объёмный поглотитель внутри (`Volume Absorption` с плотностью 0.15), чтобы стекло имело собственный тон. ## 📊 ЧТО ПОЛУЧИЛОСЬ **Оценка по порядку величины, конкретных замеров на железе не проводилось:** - Объектов: 3 кузовных + 4 торса + 4 диска + стекло + пол = 13, плюс 4 источника, камера, пустой объект вращения. - Полигоны: боксы с bevel по 6 сегментам — по ~500–900 каждый; кабина-конус 4×1 ~ 20; торс 32×12 — 768 каждый (4 шт = 3 тыс.); диски 24-угольные цилиндры — 48 каждый. Итого **~8–12 тыс. треугольников**. Бюджет 3 млн не израсходован даже на 10% — запас можно потратить на интерьер. - Материалов: 6 (краска, стекло, резина, диск, пол, и один на будущее), текстур **ноль** — всё процедурное. - Кадров: 3 ракурса 2400×1350 + 36 кадров облёта 1920×1080. - Рендер 2400×1350, Cycles, 512 сэмплов + denoising, с четырьмя полосами и стеклом: **десятки минут** на кадр на средней дискретной видеокарте. 36-кадровый облёт на 256 сэмплах — те же единицы минут × 36, то есть часы. ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Машина выглядит «пластиковой» | круглые источники вместо полос | полосы 3:1, отдельный узкий для флёков | | Краска матовая, без второго блика | `Coat Weight` не задан или `Coat Roughness` высокий | Coat 1.0, Coat Roughness 0.02 | | Облёт смещается на стыке | ось вращения не в центре или нет `matrix_parent_inverse` | пустой объект в (0,0,0) + `matrix_parent_inverse` | | Салон — чёрная дыра | стекло полностью прозрачное | Roughness 0.06 + слабый Volume Absorption | | Флёки то видны, то нет | слишком большой источник | узкий дополнительный источник, `Bump.Distance` 0.0015 | | Кузов «раздут» | широкий объектив | 85–135 мм | | Пол-циклорама слишком яркий | `Roughness` пола высокий | 0.25–0.35 — пол должен отражать, а не светиться | ## ✅ РЕЗУЛЬТАТ На выходе: `.blend` с упрощённым кузовом, 6 материалами, 4 полосами-софтбоксами и тёмным миром; три PNG 2400×1350 (`hero34.png`, `side.png`, `top34.png`) и 36-кадровая облётная анимация 1920×1080. ## 🔗 WF-07, Setup 01/08, INDEX --- ## 🔗 СВЯЗИ - **Прочие в категории «Кейсы»:** [CASE 10: SNEAKER PRODUCT HERO](case_10_sneaker_product.md), [CASE 14: FOOD RENDER (RAMEN)](case_14_food_r visualisation.md), [CASE 01: ARCHVIZ INTERIOR](case_01_archviz_interior.md), [CASE 02: GAME PROPS PIPELINE](case_02_game_props.md), [CASE 03: PRODUCT VISUALIZATION](case_03_product_viz.md) - **По темам:** [CASE 04: CHARACTER RIG & ANIMATION](case_04_character_rig.md), [CASE 05: PROCEDURAL CITY](case_05_procedural_city.md), [CASE 06: ANIMATION LOOP](case_06_animation_loop.md) - **Теги:** #case, #product --- # CASE 16: DUNGEON TILESET Tags: #case #game-assets # CASE 16: DUNGEON TILESET **ID:** CASE-16 | **Type:** Tileset | **Difficulty:** ⭐⭐⭐ **RU/EN Bilingual** > Когда применять: когда нужен модульный тайлсет уровня для игры: плиты пола, стены, углы, дверные проёмы, лестницы — всё в одной сетке, с общей текстурой и привязкой к сетке движка. ## 🎯 ЦЕЛЬ Набор модулей под сетку 2×2 м: 12 плит пола, 8 стеновых модулей (прямая, угол внутренний/внешний, Т-образный, торец), 4 модуля перехода и 3 вариации на все случаи жизни. Всё в одной сетке, с одним материалом и одной атлас-картой 2048×2048, готовое к экспорту в Unity/Godot без ручной доводки. Главное требование к тайлсету — **совместимость краёв**. Модули должны стыковаться без щелей и без перекрытий, иначе уровень соберётся с видимыми швами. ## 🧭 ИСХОДНАЯ ЗАДАЧА И ОГРАНИЧЕНИЯ **Задача:** подполье уровня 1: каменные плиты с трещинами, кирпичные стены, переходы в тоннель. **Ограничения:** - Сетка 2×2 м — под размер стандартного тайла Unity (2 units при 1 unit = 1 м). - Один материал на весь тайлсет. Три материала — это три вызова шейдера на модуль и лишние вызовы отрисовки. - Повторяющиеся части (кирпичи, плиты) — через UV, а не через геометрию. - Модули не должны пересекаться между собой при повороте: угол 90° — это поворот на месте, а не зеркалирование. - Три вариации на каждый модуль, чтобы уровень не читался как «одна и та же комната 40 раз». ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия / состояние | Зачем | |---|---|---| | Blender с UV-редактором | встроенный | атлас | | Текстура 2048×2048 | рисуется или генерируется | один материал | | Snapping в сцене | `Tool Settings → Snap` | ручная выкладка по сетке | | Плагин FBX для экспорта | обычно включён | экспорт в движок | ## 🛠️ НАСТРОЙКА ### Этап 1. Сетка и привязка ```bash RU: "Единица = 1 м, сетка 2 м, привязка INCREMENT 0.5 м. Создай плоскость 2x2 м как тайл, один материал M_Tileset, UV-карта на весь лист атласа 0..1" EN: "Unit = 1 m, grid 2 m, snap INCREMENT 0.5 m. Create a 2x2 m plane as the tile, one material M_Tileset, UV map covering the whole 0..1 atlas" ``` ```python import bpy sc = bpy.context.scene sc.unit_settings.system = 'METRIC' sc.unit_settings.scale_length = 1.0 sc.unit_settings.length_unit = 'METERS' ts = sc.tool_settings ts.use_snap = True ts.snap_elements = {'INCREMENT'} ts.snap_increment = 0.5 # половина тайла: стены прилипают к сетке def make_tile(name, sx, sy, sz, loc): bpy.ops.mesh.primitive_cube_add(size=1, location=loc) ob = bpy.context.active_object ob.name = name ob.scale = (sx, sy, sz) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) bpy.ops.mesh.primitive_grid_add(x_subdivisions=max(2, int(sx / 0.25)), y_subdivisions=2, size=2, location=(0, 0, 0)) grid = bpy.context.active_object mesh = grid.data # превращаем сетку-шаблон в нижнюю грань плитки: так контролируем число вершин v = [tuple(p.co * (sx / 2, sy / 2, 1)) for p in mesh.vertices] f = [tuple(p.vertices) for p in mesh.polygons] me = bpy.data.meshes.new(name + "_mesh") me.from_pydata(v, [], f) me.update() grid.data = me me.materials.append(bpy.data.materials["M_Tileset"]) grid.location = loc return grid ``` ### Этап 2. Модули пола ```python import bpy, math M_TILESET = bpy.data.materials.new("M_Tileset") M_TILESET.use_nodes = True TILE = 2.0 floors = [] for i in range(3): t = make_tile(f"Floor_{i}", TILE, TILE, 0.10, (i * TILE, 0, 0)) t.rotation_euler = (0, 0, math.radians(90 * i)) # три поворота = три вариации floors.append(t) # трещины как отдельная деталь: тонкие V-образные прорези-инкрузии bpy.ops.mesh.primitive_plane_add(size=1, location=(TILE / 2, 0.4, 0.0505)) crack = bpy.context.active_object crack.name = "Crack_01" crack.scale = (1.4, 0.02, 1.0) crack.rotation_euler = (0, 0, math.radians(23)) crack.data.materials.append(bpy.data.materials["M_Tileset"]) ``` ### Этап 3. Стеновые модули Стены делаются из тех же кирпичных сегментов, но собранных в L и Т. Важно: угол — это **поворот на месте**, а не зеркалирование. ```python import bpy, math WALL_H = 3.0 WALL_T = 0.30 def wall_piece(name, sx, sy, loc, rot_z=0.0): ob = make_tile(name, sx, sy, WALL_H, loc) ob.rotation_euler = (0, 0, rot_z) ob.location = (loc[0], loc[1], WALL_H / 2) return ob walls = [] for i in range(3): walls.append(wall_piece(f"Wall_Straight_{i}", TILE, WALL_T, (i * TILE, TILE / 2, 0))) # внутренний угол: два сегмента, поворот 90° на месте for i, (sx, sy, dx, dy, rz) in enumerate(( (TILE, WALL_T, -TILE / 2, 0.0, 0.0), (WALL_T, TILE, 0.0, -TILE / 2, math.radians(90)))): walls.append(wall_piece(f"Corner_In_{i}", sx, sy, (TILE / 2 + dx, TILE / 2 + dy, 0), rz)) # внешний угол и торец walls.append(wall_piece("Corner_Out_0", WALL_T, TILE, (TILE / 2 + TILE / 2, TILE / 2, 0), math.radians(90))) walls.append(wall_piece("Wall_End_0", TILE, WALL_T, (TILE * 1.5, TILE / 2, 0))) # Т-образный стык: три сегмента от центра for i, (sx, sy, dx, dy, rz) in enumerate(( (TILE, WALL_T, 0.0, -TILE / 2, 0.0), (TILE, WALL_T, 0.0, TILE / 2, 0.0), (WALL_T, TILE, -TILE / 2, 0.0, math.radians(90)))): walls.append(wall_piece(f"Wall_T_{i}", sx, sy, (TILE * 2 + dx, TILE / 2 + dy, 0), rz)) # дверной проём: два столба + перемычка, проём 1.2 x 2.1 м DOOR_W, DOOR_H = 1.2, 2.1 POST = (TILE - DOOR_W) / 2 for i, dx in enumerate((-DOOR_W / 2 - POST / 2, DOOR_W / 2 + POST / 2)): walls.append(wall_piece(f"Door_Post_{i}", POST, WALL_T, (TILE * 3 + dx, TILE / 2, 0))) lintel_h = (WALL_H - DOOR_H) / 2 walls.append(wall_piece("Door_Lintel", DOOR_W, WALL_T, (TILE * 3, TILE / 2, 0))) bpy.data.objects["Door_Lintel"].scale.z = lintel_h / WALL_H bpy.data.objects["Door_Lintel"].location.z = DOOR_H + lintel_h / 2 # лестница: 4 ступени for i in range(4): step = make_tile(f"Stair_{i}", TILE, TILE * 0.25, 0.18 * (i + 1), (TILE * 4, TILE / 2 - TILE * 0.4 + i * TILE * 0.25, 0)) step.location.z = 0.09 * (i + 1) print("модулей стен:", len(walls)) ``` ### Этап 4. Один материал с процедурными кирпичами Один материал — ключевое требование. Кирпичи и плиты считаются в Generated-координатах, поэтому тайлы не «плывут» при повороте. ```python import bpy nt = M_TILESET.node_tree bsdf = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED') bsdf.inputs['Roughness'].default_value = 0.88 geo = nt.nodes.new("ShaderNodeNewGeometry") brick = nt.nodes.new("ShaderNodeTexBrick") brick.inputs['Scale'].default_value = 4.0 brick.inputs['Mortar Size'].default_value = 0.018 brick.inputs['Brick Width'].default_value = 0.55 brick.inputs['Row Height'].default_value = 0.22 brick.inputs['Color1'].default_value = (0.22, 0.19, 0.17, 1) brick.inputs['Color2'].default_value = (0.27, 0.23, 0.20, 1) brick.inputs['Mortar'].default_value = (0.11, 0.10, 0.10, 1) noise = nt.nodes.new("ShaderNodeTexNoise") noise.inputs['Scale'].default_value = 22.0 noise.inputs['Detail'].default_value = 8.0 bump = nt.nodes.new("ShaderNodeBump") bump.inputs['Strength'].default_value = 0.4 bump.inputs['Distance'].default_value = 0.006 nt.links.new(geo.outputs['Position'], brick.inputs['Vector']) # мировые -> не плывёт nt.links.new(noise.outputs['Fac'], bump.inputs['Height']) nt.links.new(bump.outputs['Normal'], bsdf.inputs['Normal']) nt.links.new(brick.outputs['Color'], bsdf.inputs['Base Color']) # оттенок снизу вверх: у пола темнее (грязь), у верха светлее (пыль) sep = nt.nodes.new("ShaderNodeSeparateXYZ") mr = nt.nodes.new("ShaderNodeMapRange") mr.inputs['From Min'].default_value = 0.0 mr.inputs['From Max'].default_value = 2.0 mr.inputs['To Min'].default_value = 0.55 mr.inputs['To Max'].default_value = 1.0 mixc = nt.nodes.new("ShaderNodeMixRGB") mixc.blend_type = 'MULTIPLY' mixc.inputs['Fac'].default_value = 0.6 nt.links.new(geo.outputs['Position'], sep.inputs['Vector']) nt.links.new(sep.outputs['Z'], mr.inputs['Value']) nt.links.new(brick.outputs['Color'], mixc.inputs[1]) nt.links.new(mr.outputs['Result'], mixc.inputs[2]) nt.links.new(mixc.outputs['Color'], bsdf.inputs['Base Color']) ``` Одного материала на весь тайлсет мало, если пол и стены выглядят одинаково. Переключатель по ориентации поверхности решает это, не заводя второго материала: горизонтальные грани (нормаль смотрит вверх) получают крупную плиту, остальные — кирпич. ```python import bpy nt = M_TILESET.node_tree bsdf = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED') norm = nt.nodes.new("ShaderNodeNewGeometry") sep_n = nt.nodes.new("ShaderNodeSeparateXYZ") thresh = nt.nodes.new("ShaderNodeMapRange") # резкий переход: 0 -> 1 при Z > 0.9 thresh.interpolation_type = 'CONSTANT' thresh.clamp = True thresh.inputs['From Min'].default_value = 0.90 thresh.inputs['From Max'].default_value = 0.92 thresh.inputs['To Min'].default_value = 1.0 # фактор Mix: 1 = верхний вход thresh.inputs['To Max'].default_value = 0.0 slab = nt.nodes.new("ShaderNodeTexBrick") # крупная плитка пола slab.inputs['Scale'].default_value = 1.6 slab.inputs['Mortar Size'].default_value = 0.012 slab.inputs['Brick Width'].default_value = 0.85 slab.inputs['Row Height'].default_value = 0.85 slab.inputs['Color1'].default_value = (0.26, 0.25, 0.23, 1) slab.inputs['Color2'].default_value = (0.30, 0.29, 0.26, 1) nt.links.new(norm.outputs['Normal'], sep_n.inputs['Vector']) nt.links.new(sep_n.outputs['Z'], thresh.inputs['Value']) pick = nt.nodes.new("ShaderNodeMixRGB") nt.links.new(thresh.outputs['Result'], pick.inputs['Fac']) nt.links.new(brick.outputs['Color'], pick.inputs[1]) # кирпич = нижний nt.links.new(slab.outputs['Color'], pick.inputs[2]) # плита = верхний # далее pick ведёт в цепочку затемнения и в Base Color вместо mixc for l in list(nt.links): if l.to_node == mixc and l.to_socket == mixc.inputs[1]: nt.links.remove(l) nt.links.new(pick.outputs['Color'], mixc.inputs[1]) nt.links.new(norm.outputs['Normal'], slab.inputs['Vector']) nt.links.new(norm.outputs['Normal'], brick.inputs['Vector']) ``` ### Этап 5. Раскладка превью и проверка стыков Самый полезный тест — собрать из модулей реальную комнату с закрытым углом и посмотреть на швы. ```bash RU: "Собери тестовую комнату 6x6 м из тайлов: пол 3x3, стены по периметру, дверной проём в одной стене, один внешний угол. Рендер сверху-сбоку" EN: "Build a test room 6x6 m from the tiles: 3x3 floor, perimeter walls, one door opening, one outer corner. Render from an elevated 3/4 angle" ``` ```python import bpy, math, os sc = bpy.context.scene TILE = 2.0 room = bpy.data.collections.new("TestRoom") sc.collection.children.link(room) test_objs = [] # пол 3x3 for ix in range(3): for iy in range(3): src = bpy.data.objects[f"Floor_{(ix + iy) % 3}"] d = src.copy(); d.data = src.data.copy(); d.name = f"T_Floor_{ix}{iy}" room.objects.link(d) d.location = (ix * TILE + TILE / 2, iy * TILE + TILE / 2, 0) d.rotation_euler = (0, 0, math.radians(90 * ((ix + iy) % 3))) test_objs.append(d) # периметр: прямые стены + два угла n = 4 for i in range(n): src = bpy.data.objects[f"Wall_Straight_{i % 3}"] for side in range(4): d = src.copy(); d.data = src.data.copy(); d.name = f"T_Wall_{i}{side}" room.objects.link(d) if side < 2: d.location = (i * TILE + TILE / 2, side * TILE * 3 + TILE / 2, 1.5) else: d.location = (side * TILE * 3 - TILE / 2, i * TILE + TILE / 2, 1.5) d.rotation_euler = (0, 0, math.radians(90)) test_objs.append(d) bpy.ops.object.select_all(action='DESELECT') for o in test_objs: o.select_set(True) bpy.context.view_layer.objects.active = test_objs[0] bpy.ops.object.camera_add(location=(8.0, -8.0, 7.0)) cam = bpy.context.active_object; cam.data.lens = 45.0 c = cam.constraints.new('TRACK_TO'); c.target = test_objs[0] c.track_axis = 'TRACK_NEGATIVE_Z'; c.up_axis = 'UP_Y' sc.camera = cam ld = bpy.data.lights.new("Sun", type='SUN') ld.energy = 3.0; ld.angle = math.radians(8) sun = bpy.data.objects.new("Sun", ld); sc.collection.objects.link(sun) sun.rotation_euler = (math.radians(55), 0, math.radians(35)) sc.render.engine = 'CYCLES' sc.cycles.samples = 96 sc.render.resolution_x, sc.render.resolution_y = 1600, 1200 os.makedirs(bpy.path.abspath("//renders/tileset"), exist_ok=True) sc.render.filepath = r"//renders/tileset/room_test.png" bpy.ops.render.render(write_still=True) ``` ### Этап 6. Экспорт в движок ```python import bpy, math sc = bpy.context.scene # pivot в основании модуля: иначе в движке модули «прыгают» при размещении for ob in bpy.data.objects: if ob.name.startswith(("Floor_", "Wall_", "Corner_", "Door_", "Stair_")): zs = [(ob.matrix_world @ v.co).z for v in ob.data.vertices] if not zs: continue cur = bpy.context.scene.cursor.location.copy() bpy.context.scene.cursor.location = (ob.location.x, ob.location.y, min(zs)) bpy.ops.object.select_all(action='DESELECT') ob.select_set(True); bpy.context.view_layer.objects.active = ob bpy.ops.object.origin_set(type='ORIGIN_CURSOR', center='BOUNDS') bpy.context.scene.cursor.location = cur mod_objs = [o for o in bpy.data.objects if o.name.startswith(("Floor_", "Wall_", "Corner_", "Door_", "Stair_"))] bpy.ops.object.select_all(action='DESELECT') for o in mod_objs: o.select_set(True) bpy.context.view_layer.objects.active = mod_objs[0] bpy.ops.export_scene.fbx(filepath=bpy.path.abspath("//export/dungeon_tileset.fbx"), use_selection=True, apply_unit_scale=True, global_scale=1.0, axis_forward='-Z', axis_up='Y', object_types={'MESH'}, path_mode='AUTO', mesh_smooth_type='FACE') ``` > [!NOTE] > Имена аргументов экспорта FBX менялись между версиями (`use_mesh_modifiers` против `apply_unit_scale`/`global_scale`/`object_types`, добавление `path_mode`). Набор допустимых аргументов смотри так: `[p.identifier for p in bpy.ops.export_scene.fbx.get_rna_type().properties]` — это выведет точный список для твоей версии. Ось `axis_forward='-Z'`, `axis_up='Y'` — стандарт для Unity; для Godot/других движков значения другие. ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `snap_increment` | 0.5 м | половина тайла — стены прилипают к сетке | | `Brick Scale` | 3–6 | на 2-метровом модуле ~5 кирпичей в ряд | | `Brick Width` / `Row Height` | 0.55 / 0.22 | соотношение 2.5:1 — нормальный кирпич | | `Mortar Size` | 0.015–0.02 | выше — раствор выглядит толстым | | `MapRange To Min` | 0.55 | затемнение низа = грязь внизу комнаты | | `Bump.Distance` | 0.004–0.008 | крупнее — «кирпич из пенопласта» | | поворот вариации | 90° шаг | 3 вариации дешевле, чем 3 отдельных модуля | | `face_smooth_mode` | `FACE` | тайлы должны быть плоскими, иначе в игре каша | ## ⚠️ ЧТО ПОШЛО НЕ ТАК **1. Щели на стыках модулей появлялись при повороте.** Первая версия использовала `Smart UV Project` с `island_margin = 0.01` и рассчитывала, что этого хватит. При сборке комнаты из 16 модулей на трёх стыках появились вертикальные щели в 2–3 мм — они были видны даже на превью 1600×1200. Причина: после поворота объекта на 90° острова UV оставались в тех же местах листа, но соседний модуль брал соседний участок атласа, и на границе попадал интерполированный край. Решили отказом от атласа в пользу процедурной кирпичной текстуры, считаемой в `Geometry → Position`: у неё нет UV вообще, поэтому стык физически не может «протечь». Для плит пола отдельно уменьшили зазор до нуля за счёт `translate` геометрии на половину плитки. **2. Привязка к сетке работала, но объекты «прыгали» в экспорте.** У всех модулей origin был в центре объекта, и при расстановке в редакторе уровня каждый модуль приходилось ставить вручную с поправкой на половину своей высоты. Пришлось переставить origin в основание (`origin_set` с курсором в нижней точке). Отдельно выяснилось, что масштаб объектов был 1.0, но применён не был — после экспорта в Unity часть модулей приходила с масштабом 0.999999 и визуально «щелкала» на стыках. Причина — неприменённый `transform_apply`, а не экспортёр. **3. Один материал на весь тайлсет оказался сложнее, чем казалось.** Плата была простой: один материал и процедурный кирпич. Проблема вылезла в том, что **пол и стены выглядели одинаково**: та же кирпичная текстура, только с разным затенением. Три вариации на модуль стало три, но стены перестали отличаться от пола — читалось, что это одна и та же кладка. Вышло из этого через добавление переключателя по нормали: у `Geometry → Normal` Z-компонента > 0.9 (горизонтальная поверхность) шейдер уходил в крупную плиту, иначе — в кирпич. Один материал сохранился, но теперь он различает два типа поверхностей. **4. Лестница не стыковалась с полом.** Первые три ступени получились, четвёртая вылезла на стену: шаг 0.18 м при глубине ступени 0.5 м дал подъём 0.72 м, а перепад между этажами был 0.6 м. Помогло согласовать: высоту ступени считать от перепада, а не от нуля, и глубину ступени брать 0.4 м при перепаде 0.6 м. Это не ошибка Blender, а ошибка проектирования — но на экспорте она стоила полдня. ## 📊 ЧТО ПОЛУЧИЛОСЬ **Оценка по порядку величины, конкретных замеров не проводилось:** - Модулей: 3 плиты пола + 3 прямые стены + 2 внутренних угла + 1 внешний угол + 1 торец + 3 Т-стыка + 3 дверных элемента + 4 ступени + 1 трещина = **21 модуль**. - Полигоны: каждый модуль — сетка 8×2 (16 квадов) или бокс с 6 гранями, плюс bevel не используется. Итого **~400–800 треугольников на весь тайлсет**. - Материалов: **1** (`M_Tileset`), текстур **ноль** — вся фактура процедурная. - Тестовая комната: 9 плит + 16 стеновых сегментов = 25 объектов, инстансы одного меша. - Рендер 1600×1200, Cycles, 96 сэмплов: **секунды** — сцена крошечная. - FBX: единицы мегабайт (одна геометрия, ноль текстур). ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Щели на стыках | UV-атлас + поворот объекта | процедурная текстура в `Geometry → Position` | | Модули «прыгают» при расстановке | origin в центре | `origin_set` в нижнюю точку | | Модули «щёлкают» в движке | неприменённый масштаб | `transform_apply(scale=True)` перед экспортом | | Пол и стены выглядят одинаково | один узор на всё | переключатель по `Normal → Z` в шейдере | | Лестница не стыкуется | высота шага не равна перепаду | считать шаг от перепада этажей | | Стены «плывут» при перемещении | текстура в UV | текстура в Position, а не в UV | | Слишком много draw call | вариации как отдельные объекты | три поворота одного меша вместо трёх модулей | ## ✅ РЕЗУЛЬТАТ На выходе: коллекция из 21 модуля в сетке 2×2 м с одним материалом `M_Tileset`, тестовая комната 6×6 м, превью `//renders/tileset/room_test.png` (1600×1200) и FBX для движка. ## 🔗 WF-07, Setup 01/08, INDEX --- ## 🔗 СВЯЗИ - **Прочие в категории «Кейсы»:** [CASE 02: GAME PROPS PIPELINE](case_02_game_props.md), [CASE 01: ARCHVIZ INTERIOR](case_01_archviz_interior.md), [CASE 03: PRODUCT VISUALIZATION](case_03_product_viz.md), [CASE 04: CHARACTER RIG & ANIMATION](case_04_character_rig.md), [CASE 05: PROCEDURAL CITY](case_05_procedural_city.md) - **По темам:** [CASE 06: ANIMATION LOOP](case_06_animation_loop.md), [CASE 07: STYLIZED BARREL (HAND-PAINT)](case_07_stylized_barrel.md), [CASE 08: SCI-FI CORRIDOR MODULAR](case_08_sci_fi_corridor.md) - **Теги:** #case, #game-assets --- # P01: PRIMITIVE CREATION BASICS Tags: #basics #bpy #bpy.ops #geometry #modeling #primitives #prompt # P01: PRIMITIVE CREATION BASICS **ID:** P01 | **Кластер:** Geometry Basics | **Сложность:** ⭐ | **Теги:** #geometry #primitives #basics #bpy.ops --- ## 🎯 ЦЕЛЬ Создание базовых примитивов через MCP с точным контролем параметров --- ## 📝 ПРОМПТ (РУС) ``` Создай примитив [TYPE] с именем "[NAME]" в позиции [X, Y, Z], масштаб [SX, SY, SZ], поворот [RX, RY, RZ] (в радианах). Доступные типы: CUBE, SPHERE, CYLINDER, PLANE, CONE, TORUS, EMPTY, CAMERA, LIGHT Пример: "Создай куб 'Box_01' в (2, 0, 1) масштаб (1, 2, 1) поворот (0, 0, 0.785)" ``` --- ## 📝 ПРОМПТ (ENG) ``` Create a [TYPE] primitive named "[NAME]" at location [X, Y, Z], scale [SX, SY, SZ], rotation [RX, RY, RZ] (radians). Available types: CUBE, SPHERE, CYLINDER, PLANE, CONE, TORUS, EMPTY, CAMERA, LIGHT Example: "Create cube 'Box_01' at (2, 0, 1) scale (1, 2, 1) rotation (0, 0, 0.785)" ``` --- ## 🔧 MCP TOOL: `create_object` ```json { "type": "create_object", "params": { "entity_type": "CUBE", "name": "Box_01", "location": [2.0, 0.0, 1.0], "rotation": [0.0, 0.0, 0.785], "scale": [1.0, 2.0, 1.0] } } ``` --- ## 📊 ПАРАМЕТРЫ ПО ТИПАМ | Тип | Обязательные | Опциональные | |-----|--------------|--------------| | CUBE | size | location, rotation, scale | | SPHERE | radius | segments, ring_count, location, rotation, scale | | CYLINDER | radius, depth | vertices, cap_ends, location, rotation, scale | | PLANE | size | location, rotation, scale | | CONE | radius1, radius2, depth | vertices, location, rotation, scale | | TORUS | major_radius, minor_radius | major_segments, minor_segments, location, rotation, scale | | EMPTY | — | location, rotation, scale, type (PLAIN_AXES, ARROWS, etc.) | | CAMERA | — | location, rotation, lens, sensor_width | | LIGHT | — | location, rotation, type (POINT, SUN, SPOT, AREA), energy, color | --- ## 💡 ВАРИАНТЫ ИСПОЛЬЗОВАНИЯ ### Базовый куб ```json {"entity_type": "CUBE", "name": "Cube_Base", "location": [0, 0, 0]} ``` ### Сфера с детализацией ```json {"entity_type": "SPHERE", "name": "Sphere_HighPoly", "location": [0, 0, 2], "radius": 1.5, "segments": 64, "ring_count": 32} ``` ### Камера для рендера ```json {"entity_type": "CAMERA", "name": "Cam_Main", "location": [7, -7, 5], "rotation": [0.9, 0, 0.785]} ``` ### Студийный свет ```json {"entity_type": "LIGHT", "name": "Key_Light", "location": [4, 4, 6], "rotation": [-0.7, 0, 0.785], "type": "AREA", "energy": 1000, "size": 2} ``` --- ## ⚠️ ЧАСТЫЕ ОШИБКИ | Ошибка | Причина | Решение | |--------|---------|---------| | `Object not found` | Имя занято | Используй уникальные имена или проверяй `get_scene_info` | | `Invalid rotation` | Градусы вместо радиан | Конвертируй: `radians = degrees * π / 180` | | `Primitive not created` | Контекст (EDIT mode) | Всегда будь в OBJECT mode | --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P07: INSTANCING & ARRAYS](07_instancing_arrays.md), [P23: GEOMETRY NODES BASICS](23_geometry_nodes_basics.md), [P45: GEOMETRY NODES SIMULATION ZONES](45_gn_simulation.md), [P02: BOOLEAN OPERATIONS](02_boolean_operations.md), [P03: MODIFIERS STACK](03_modifiers_stack.md) - **По темам:** [P04: MESH EDITING (BMESH)](04_mesh_editing_bmesh.md), [P18: DRIVERS & CONSTRAINTS](18_drivers_constraints.md), [P19: NLA EDITOR WORKFLOW](19_nla_workflow.md) - **Теги:** #basics, #bpy, #bpy.ops, #geometry, #modeling, #primitives, #prompt --- # P02: BOOLEAN OPERATIONS Tags: #boolean #difference #intersect #modeling #modifiers #prompt #union # P02: BOOLEAN OPERATIONS **ID:** P02 | **Кластер:** Geometry Basics | **Сложность:** ⭐⭐ | **Теги:** #boolean #modeling #modifiers #difference #union #intersect --- ## 🎯 ЦЕЛЬ Выполнение булевых операций (Difference, Union, Intersect) между объектами через MCP --- ## 📝 ПРОМПТ (РУС) ``` Выполни булеву операцию [OPERATION] между "[OBJECT_A]" (базовый) и "[OBJECT_B]" (инструмент). Операции: DIFFERENCE (вычитание), UNION (объединение), INTERSECT (пересечение). Результат: объект A модифицируется, объект B может быть удалён. ``` --- ## 📝 ПРОМПТ (ENG) ``` Perform boolean [OPERATION] between "[OBJECT_A]" (base) and "[OBJECT_B]" (tool). Operations: DIFFERENCE (subtract), UNION (merge), INTERSECT (intersection). Result: object A is modified, object B can be deleted. ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy # Настройка obj_a = bpy.data.objects.get("Wall") obj_b = bpy.data.objects.get("Window_Cutout") if not obj_a or not obj_b: raise ValueError("Objects not found") # Убедиться в OBJECT mode if bpy.context.mode != 'OBJECT': bpy.ops.object.mode_set(mode='OBJECT') # Выбрать объекты bpy.ops.object.select_all(action='DESELECT') obj_a.select_set(True) obj_b.select_set(True) bpy.context.view_layer.objects.active = obj_a # Добавить модификатор Boolean bool_mod = obj_a.modifiers.new(name="Bool_Window", type='BOOLEAN') bool_mod.operation = 'DIFFERENCE' # 'UNION', 'INTERSECT' bool_mod.object = obj_b bool_mod.solver = 'EXACT' # 'FAST' для простых геометрий # Применить bpy.ops.object.modifier_apply(modifier="Bool_Window") # Удалить инструмент (опционально) bpy.data.objects.remove(obj_b, do_unlink=True) print(f"Boolean DIFFERENCE applied. {obj_a.name} modified.") ``` --- ## 📊 ОПЕРАЦИИ | Операция | Визуальный результат | Use Case | |----------|---------------------|----------| | **DIFFERENCE** | A минус B | Окна, двери, отверстия, канавки | | **UNION** | A плюс B | Объединение частей, слияние мешей | | **INTERSECT** | Только пересечение | Маски, вырезание форм | --- ## 💡 ПРОДВИНУТЫЕ ПАТТЕРНЫ ### Множественные булевы (одновременно) ```python # Несколько вычитаний за один проход cutters = [bpy.data.objects.get(f"Cutter_{i}") for i in range(5)] for cutter in cutters: mod = wall.modifiers.new(f"Bool_{cutter.name}", 'BOOLEAN') mod.operation = 'DIFFERENCE' mod.object = cutter mod.solver = 'EXACT' # Применить все for mod in list(wall.modifiers): if mod.type == 'BOOLEAN': bpy.context.view_layer.objects.active = wall bpy.ops.object.modifier_apply(modifier=mod.name) # Очистка for c in cutters: bpy.data.objects.remove(c, do_unlink=True) ``` ### Будьте аккуратны с нормалями ```python # Проверить и исправить нормали перед булевым bpy.context.view_layer.objects.active = obj_a bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.normals_make_consistent(inside=False) bpy.ops.object.mode_set(mode='OBJECT') ``` --- ## ⚠️ ЧАСТЫЕ ПРОБЛЕМЫ | Проблема | Причина | Решение | |----------|---------|---------| | Результат пустой | Нормали наружу/внутрь | `Mesh > Normals > Recalculate Outside` | | Артефакты геометрии | Не-манифольд края | `Select > Select All by Trait > Non-Manifold` → исправить | | Медленно на сложных мешах | Много полигонов | Используй `DECIMATE` перед булевым, или `FAST` solver | | Объект исчезает | Пересечение пустое | Проверь расположение объектов | --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P03: MODIFIERS STACK](03_modifiers_stack.md), [P01: PRIMITIVE CREATION BASICS](01_geometry_basics.md), [P04: MESH EDITING (BMESH)](04_mesh_editing_bmesh.md), [P07: INSTANCING & ARRAYS](07_instancing_arrays.md), [P18: DRIVERS & CONSTRAINTS](18_drivers_constraints.md) - **По темам:** [P19: NLA EDITOR WORKFLOW](19_nla_workflow.md), [P22: PARTICLE SYSTEMS](22_particles.md), [P23: GEOMETRY NODES BASICS](23_geometry_nodes_basics.md) - **Теги:** #boolean, #difference, #intersect, #modeling, #modifiers, #prompt, #union --- # P03: MODIFIERS STACK Tags: #clothing #modeling #modifiers #non #non-destructive #procedural #prompt #stack # P03: MODIFIERS STACK **ID:** P03 | **Кластер:** Geometry Basics | **Сложность:** ⭐⭐ | **Теги:** #modifiers #non-destructive #stack #procedural --- ## 🎯 ЦЕЛЬ Построение не-деструктивного стека модификаторов для процедурного моделирования --- ## 📝 ПРОМПТ (РУС) ``` Добавь к "[OBJECT_NAME]" стек модификаторов в порядке: 1. [MOD_1] с параметрами {...} 2. [MOD_2] с параметрами {...} ... Порядок важен: модификаторы применяются сверху вниз. ``` --- ## 📝 ПРОМПТ (ENG) ``` Add modifier stack to "[OBJECT_NAME]" in order: 1. [MOD_1] with params {...} 2. [MOD_2] with params {...} ... Order matters: modifiers apply top-to-bottom. ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy obj = bpy.data.objects.get("Base_Mesh") if not obj: raise ValueError("Object not found") # Очистить существующие (опционально) # obj.modifiers.clear() # === ПРИМЕР СТЕКА: Procedural Building === # 1. BEVEL - сглаживание краёв bevel = obj.modifiers.new("Bevel_Edges", 'BEVEL') bevel.width = 0.02 bevel.segments = 3 bevel.profile = 0.5 bevel.limit_method = 'ANGLE' bevel.angle_limit = 1.0472 # 60° # 2. SUBSURF - подделение для гладкости subsurf = obj.modifiers.new("Subdiv_Smooth", 'SUBSURF') subsurf.levels = 2 subsurf.render_levels = 3 subsurf.subdivision_type = 'CATMULL_CLARK' # 3. ARRAY - повторение по оси array = obj.modifiers.new("Array_Floors", 'ARRAY') array.count = 10 array.use_relative_offset = True array.relative_offset_displace = (0, 0, 3.5) # высота этажа # 4. MIRROR - симметрия mirror = obj.modifiers.new("Mirror_X", 'MIRROR') mirror.use_axis = (True, False, False) mirror.use_bisect_axis = (True, False, False) # 5. DECIMATE - оптимизация для игры decimate = obj.modifiers.new("Decimate_LOD", 'DECIMATE') decimate.ratio = 0.3 decimate.decimate_type = 'COLLAPSE' # 6. BEVEL (второй) - на рёбрах после array bevel2 = obj.modifiers.new("Bevel_Final", 'BEVEL') bevel2.width = 0.01 bevel2.segments = 2 print(f"Stack built: {len(obj.modifiers)} modifiers on {obj.name}") ``` --- ## 📊 ПОПУЛЯРНЫЕ СТЕКИ ### Архитектура: Здание ``` 1. BEVEL (sharp edges) → 2. ARRAY (floors) → 3. MIRROR (symmetry) → 4. SUBSURF (smooth) ``` ### Гейм-ассет: Prop ``` 1. BEVEL → 2. WEIGHTED NORMAL (shading) → 3. DECIMATE (LOD) → 4. UV PROJECT (optional) ``` ### Персонаж: Cloth ``` 1. SUBSURF → 2. CLOTH SIM → 3. SHRINKWRAP (to body) → 4. SOLIDIFY (thickness) ``` ### Хард-сёрфейс: Механизм ``` 1. BEVEL (weight-based) → 2. BOOLEAN (details) → 3. EDGE SPLIT (sharp) → 4. WEIGHTED NORMAL ``` --- ## ⚙️ КЛЮЧЕВЫЕ МОДИФИКАТОРЫ И ПАРАМЕТРЫ | Модификатор | Главные параметры | Когда использовать | |-------------|-------------------|-------------------| | **BEVEL** | width, segments, profile, limit_method | Всегда для реализма | | **SUBSURF** | levels, render_levels, subdivision_type | Органика, плавные формы | | **ARRAY** | count, offset (relative/constant/object) | Повторяющиеся элементы | | **MIRROR** | axis, bisect, mirror_object | Симметрия | | **BOOLEAN** | operation, object, solver | Вычитание/объединение | | **DECIMATE** | ratio, type (COLLAPSE/UNSUBDIV/PLANAR) | Оптимизация, LOD | | **SOLIDIFY** | thickness, offset, even_thickness | Толщина, ткань | | **SHRINKWRAP** | target, wrap_mode, offset | Надевание, проекция | | **DISPLACE** | texture, strength, direction | Детализация, ландшафт | | **GEOMETRY_NODES** | node_group | Полный процедурализм | --- ## 💡 ПРО ТИПЫ ### Vertex Groups для контроля ```python # Ограничить BEVEL только острыми рёбрами bevel.limit_method = 'WEIGHT' bevel.vertex_group = "Bevel_Weight" # Создать группу и назначить веса vg = obj.vertex_groups.new(name="Bevel_Weight") # Назначь веса через bmesh или vertex paint ``` ### Драйверы на модификаторах ```python # Анимировать count массива driver = array.driver_add('count').driver driver.type = 'SCRIPTED' var = driver.variables.new() var.name = "frame" var.targets[0].id = bpy.data.scenes["Scene"] var.targets[0].data_path = "frame_current" driver.expression = "1 + frame / 10" ``` ### Применение по необходимости ```python # Применить только некоторые (для экспорта) for mod in list(obj.modifiers): if mod.name.startswith("Decimate") or mod.name.startswith("Bevel_Final"): bpy.context.view_layer.objects.active = obj bpy.ops.object.modifier_apply(modifier=mod.name) ``` --- ## ⚠️ ПРАВИЛА ПОРЯДКА | Порядок | Эффект | |---------|--------| | BEVEL → SUBSURF | Фаска поделится, мягкие края | | SUBSURF → BEVEL | Фаска на уже гладком меше, острее | | ARRAY → MIRROR | Зеркалится уже массив | | MIRROR → ARRAY | Массив зеркалится целиком | | BOOLEAN → SUBSURF | Гладкое результат булева | | SUBSURF → BOOLEAN | Булев на высокополигональном (медленно) | --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P02: BOOLEAN OPERATIONS](02_boolean_operations.md), [P04: MESH EDITING (BMESH)](04_mesh_editing_bmesh.md), [P07: INSTANCING & ARRAYS](07_instancing_arrays.md), [P18: DRIVERS & CONSTRAINTS](18_drivers_constraints.md), [P23: GEOMETRY NODES BASICS](23_geometry_nodes_basics.md) - **По темам:** [P25: PROCEDURAL BUILDING GENERATOR](25_procedural_building.md), [P01: PRIMITIVE CREATION BASICS](01_geometry_basics.md), [P05: UV UNWRAPPING STRATEGIES](05_uv_unwrapping.md) - **Теги:** #clothing, #modeling, #modifiers, #non, #non-destructive, #procedural, #prompt, #stack --- # P04: MESH EDITING (BMESH) Tags: #bmesh #edit #edit-mode #low #low-level #mesh #mesh-manipulation #modeling #procedural #prompt # P04: MESH EDITING (BMESH) **ID:** P04 | **Кластер:** Geometry Basics | **Сложность:** ⭐⭐⭐ | **Теги:** #bmesh #edit-mode #mesh-manipulation #procedural #low-level --- ## 🎯 ЦЕЛЬ Низкоуровневое редактирование меша через BMesh API для процедурной генерации --- ## 📝 ПРОМПТ (РУС) ``` Используй BMesh для создания/модификации меша "[OBJECT_NAME]": - Режим: CREATE_NEW / EDIT_EXISTING - Операции: [список операций с параметрами] - Финализировать: UPDATE_MESH / TO_OBJECT ``` --- ## 📝 ПРОМПТ (ENG) ``` Use BMesh to create/modify mesh "[OBJECT_NAME]": - Mode: CREATE_NEW / EDIT_EXISTING - Operations: [list of operations with params] - Finalize: UPDATE_MESH / TO_OBJECT ``` --- ## 🔧 MCP: `execute_blender_code` ### СОЗДАНИЕ НОВОГО МЕША ```python import bpy import bmesh from mathutils import Vector def create_custom_mesh(name, vertices, faces): """vertices: [(x,y,z), ...], faces: [(v1,v2,v3), ...]""" mesh = bpy.data.meshes.new(name) obj = bpy.data.objects.new(name, mesh) bpy.context.collection.objects.link(obj) bm = bmesh.new() # Создать вершины bm_verts = [bm.verts.new(Vector(v)) for v in vertices] bm.verts.ensure_lookup_table() # Создать грани for face_indices in faces: try: bm.faces.new([bm_verts[i] for i in face_indices]) except ValueError as e: print(f"Skipping invalid face {face_indices}: {e}") bm.to_mesh(mesh) bm.free() return obj # Пример: Пирамида pyramid_verts = [ (0, 0, 2), # apex (-1, -1, 0), # base corners (1, -1, 0), (1, 1, 0), (-1, 1, 0), ] pyramid_faces = [ (0, 1, 2), (0, 2, 3), (0, 3, 4), (0, 4, 1), # sides (1, 2, 3, 4) # base ] obj = create_custom_mesh("Pyramid", pyramid_verts, pyramid_faces) print(f"Created: {obj.name}") ``` --- ### РЕДАКТИРОВАНИЕ СУЩЕСТВУЮЩЕГО (EDIT MODE) ```python import bpy import bmesh obj = bpy.data.objects.get("Target_Mesh") if not obj or obj.type != 'MESH': raise ValueError("Mesh object required") # Перейти в EDIT mode для BMesh bpy.context.view_layer.objects.active = obj bpy.ops.object.mode_set(mode='EDIT') bm = bmesh.from_edit_mesh(obj.data) bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() bm.faces.ensure_lookup_table() # === ОПЕРАЦИИ === # 1. SUBDIVIDE - подразделение рёбер edges_to_subdivide = [e for e in bm.edges if e.calc_length() > 0.5] bmesh.ops.subdivide_edges(bm, edges=edges_to_subdivide, cuts=2) # 2. BEVEL - фаска на рёбрах sharp_edges = [e for e in bm.edges if e.calc_face_angle() > 1.0] bmesh.ops.bevel(bm, geom=sharp_edges, offset=0.05, segments=3, profile=0.5) # 3. EXTRUDE - выдавливание граней top_faces = [f for f in bm.faces if f.normal.z > 0.9] extrude_result = bmesh.ops.extrude_face_region(bm, geom=top_faces) extruded_geom = extrude_result['geom'] # Переместить выдавленное new_faces = [g for g in extruded_geom if isinstance(g, bmesh.types.BMFace)] bmesh.ops.translate(bm, verts=[v for f in new_faces for v in f.verts], vec=(0, 0, 1.0)) # 4. INSET - врезание bmesh.ops.inset_region(bm, faces=list(bm.faces), thickness=0.1, depth=0.05) # 5. POKE - триангуляция через центр bmesh.ops.poke(bm, faces=list(bm.faces)) # 6. SMOOTH - сглаживание вершин bmesh.ops.smooth_vert(bm, verts=bm.verts, factor=0.3, iterations=5) # Применить изменения bmesh.update_edit_mesh(obj.data) bpy.ops.object.mode_set(mode='OBJECT') print(f"Edited: {obj.name}, verts={len(obj.data.vertices)}, faces={len(obj.data.polygons)}") ``` --- ## 📊 BMesh ОПЕРАЦИЙ ЧЕТ-ЛИСТ | Операция | Функция | Параметры | Use Case | |----------|---------|-----------|----------| | **Create Vert** | `bm.verts.new(Vector)` | coordinates | Базовая геометрия | | **Create Edge** | `bm.edges.new((v1, v2))` | two verts | Каркас | | **Create Face** | `bm.faces.new([v1,v2,v3...])` | vert list (3+) | Полигоны | | **Subdivide** | `bmesh.ops.subdivide_edges` | edges, cuts | Детализация | | **Bevel** | `bmesh.ops.bevel` | geom, offset, segments | Фаски | | **Extrude** | `bmesh.ops.extrude_face_region` | geom (faces) | Выдавливание | | **Inset** | `bmesh.ops.inset_region` | faces, thickness, depth | Врезки | | **Poke** | `bmesh.ops.poke` | faces | Триангуляция | | **Translate** | `bmesh.ops.translate` | verts, vec | Перемещение | | **Scale** | `bmesh.ops.scale` | verts, vec | Масштаб | | **Rotate** | `bmesh.ops.rotate` | verts, cent, matrix | Вращение | | **Smooth** | `bmesh.ops.smooth_vert` | verts, factor, iter | Релаксация | | **Remove Doubles** | `bmesh.ops.remove_doubles` | verts, dist | Очистка | | **Triangulate** | `bmesh.ops.triangulate` | faces | Триангуляция | | **Solidify** | `bmesh.ops.solidify` | faces, thickness | Толщина | --- ## 💡 ПРО ПАТТЕРНЫ ### Процедурный дом ( 바닥 + стены + крыша) ```python def create_house_bmesh(width=6, depth=8, height=4, wall_thick=0.2): bm = bmesh.new() # Пол (плоскость) floor = bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=1) # Масштабировать for v in floor['verts']: v.co.x *= width/2 v.co.y *= depth/2 v.co.z = 0 # Стены - экструдировать вверх wall_faces = [f for f in bm.faces if f.normal.z < 0.1] # не пол extrude = bmesh.ops.extrude_face_region(bm, geom=wall_faces) walls = [g for g in extrude['geom'] if isinstance(g, bmesh.types.BMFace)] bmesh.ops.translate(bm, verts=[v for f in walls for v in f.verts], vec=(0, 0, height)) # Толщина стен (Solidify) bmesh.ops.solidify(bm, geom=walls, thickness=wall_thick) # Крыша - пирамида на верху # ... roof logic mesh = bpy.data.meshes.new("House") bm.to_mesh(mesh) bm.free() return mesh ``` ### Модификация UV через BMesh ```python bm = bmesh.from_edit_mesh(obj.data) uv_layer = bm.loops.layers uv.verify() # или .new("UVMap") for face in bm.faces: for loop in face.loops: uv = loop[uv_layer] # Плановая проекция uv.uv = (loop.vert.co.x, loop.vert.co.y) bmesh.update_edit_mesh(obj.data) ``` --- ## ⚠️ ВАЖНЫЕ ПРАВИЛА | Правило | Почему | |---------|--------| | `bm.verts.ensure_lookup_table()` после изменений | Индексы сбиваются | | `bmesh.update_edit_mesh(obj.data)` перед выходом | Записать в меш | | `bpy.ops.object.mode_set(mode='OBJECT')` после | Вернуться в объектный режим | | `bm.free()` для новых BMesh | Утечка памяти | | Проверяй `if isinstance(g, BMFace)` после extrude | Результат смешанный | --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P03: MODIFIERS STACK](03_modifiers_stack.md), [P07: INSTANCING & ARRAYS](07_instancing_arrays.md), [P18: DRIVERS & CONSTRAINTS](18_drivers_constraints.md), [P23: GEOMETRY NODES BASICS](23_geometry_nodes_basics.md), [P25: PROCEDURAL BUILDING GENERATOR](25_procedural_building.md) - **По темам:** [P01: PRIMITIVE CREATION BASICS](01_geometry_basics.md), [P02: BOOLEAN OPERATIONS](02_boolean_operations.md), [P05: UV UNWRAPPING STRATEGIES](05_uv_unwrapping.md) - **Теги:** #bmesh, #edit, #edit-mode, #low, #low-level, #mesh, #mesh-manipulation, #modeling, #procedural, #prompt --- # P05: UV UNWRAPPING STRATEGIES Tags: #procedural #prompt #texturing #unwrapping #uv # P05: UV UNWRAPPING STRATEGIES **ID:** P05 | **Кластер:** Geometry Basics | **Сложность:** ⭐⭐ | **Теги:** #uv #unwrapping #texturing #procedural --- ## 🎯 ЦЕЛЬ Автоматические и полуавтоматические стратегии UV-развёртки для разных типов объектов --- ## 📝 ПРОМПТ (РУС) ``` Разверни UV для "[OBJECT_NAME]" методом [METHOD]: - SMART_PROJECT: автоматический, угол [LIMIT] - LIGHTMAP_PACK: для 베이ка, padding [PX] - CUBE_PROJECTION: кубическая проекция - SPHERE_PROJECTION: сферическая - MANUAL_SEAMS: по швам (нужны预先 отмеченные seams) ``` --- ## 📝 ПРОМПТ (ENG) ``` Unwrap UV for "[OBJECT_NAME]" using [METHOD]: - SMART_PROJECT: automatic, angle [LIMIT] - LIGHTMAP_PACK: for baking, padding [PX] - CUBE_PROJECTION: cubic projection - SPHERE_PROJECTION: spherical - MANUAL_SEAMS: by seams (pre-marked seams required) ``` --- ## 🔧 MCP: `execute_blender_code` ### SMART UV PROJECT (Универсальный) ```python import bpy obj = bpy.data.objects.get("Target") if not obj or obj.type != 'MESH': raise ValueError("Mesh object required") # Подготовка bpy.context.view_layer.objects.active = obj bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') # Smart UV Project bpy.ops.uv.smart_project( angle_limit=1.0472, # 66° - порог для нового острова island_margin=0.02, # отступ между островами area_weight=0.0, # вес площади (0-1) correct_aspect=True, # учитывать соотношение сторон scale_to_bounds=False, # масштабировать в границы ) bpy.ops.object.mode_set(mode='OBJECT') print(f"Smart UV Project done: {len(obj.data.uv_layers)} UV maps") ``` ### LIGHTMAP PACK (Для бейка) ```python bpy.ops.uv.lightmap_pack( PADDING=4, # пикселей между островами Px_MARGIN=4, # отступ в пикселях share_tex_space=False, # делить пространство ) ``` ### CUBE / SPHERE PROJECTION ```python # Кубическая (для архитектуры, коробок) bpy.ops.uv.cube_project( cube_size=1.0, clip_to_bounds=True, correct_aspect=True, ) # Сферическая (для окружений, неба) bpy.ops.uv.sphere_project( direction='Z', # ось проекции align='POLAR', # выравнивание ) ``` ### ПО ШВАМ (Ручное управление) ```python # 1. Отметить швы (в EDIT mode) bpy.ops.object.mode_set(mode='EDIT') bm = bmesh.from_edit_mesh(obj.data) bm.edges.ensure_lookup_table() # Авто-швы по острым рёбрам for e in bm.edges: if e.calc_face_angle() > 1.0472: # > 60° e.use_seam = True bmesh.update_edit_mesh(obj.data) # 2. Развернуть по швам bpy.ops.uv.unwrap( method='ANGLE_BASED', # 'CONFORMAL' для органики margin=0.001, fill_holes=True, correct_aspect=True, ) bpy.ops.object.mode_set(mode='OBJECT') ``` --- ## 📊 СРАВНЕНИЕ МЕТОДОВ | Метод | Скорость | Качество | Лучше для | Авто-швы | |-------|----------|----------|-----------|----------| | **Smart Project** | ⚡⚡⚡ | ⭐⭐ | Хард-сёрфейс, пропсы | Да | | **Lightmap Pack** | ⚡⚡ | ⭐⭐ | Бейкинг, игры | Да | | **Cube Project** | ⚡⚡⚡ | ⭐ | Архитектура, примитивы | Да | | **Sphere Project** | ⚡⚡⚡ | ⭐ | Сферы, небо | Да | | **Unwrap (Seams)** | ⚡ | ⭐⭐⭐ | Органика, персонажи | Нет (ручной) | | **Follow Active Quads** | ⚡ | ⭐⭐ | Прямоугольные полосы | Частично | --- ## 💡 СТРАТЕГИИ ПО ТИПАМ ОБЪЕКТОВ ### Архитектура (стены, пол, потолок) ```python # 1. Smart Project с большим margin bpy.ops.uv.smart_project(angle_limit=1.2, island_margin=0.05) # 2. Выровнять острова (опционально) bpy.ops.uv.align_rotation(method='AXIS_X') bpy.ops.uv.align_rotation(method='AXIS_Y') ``` ### Персонажи / Органика ```python # 1. Ручные швы по anatomiи # 2. Unwrap с CONFORMAL bpy.ops.uv.unwrap(method='CONFORMAL', margin=0.005) # 3. Pack islands bpy.ops.uv.pack_islands(margin=0.01) ``` ### Гейм-пропсы (Low-poly) ```python # 1. Smart Project агрессивно bpy.ops.uv.smart_project(angle_limit=1.5, island_margin=0.01) # 2. Упаковать максимально плотно bpy.ops.uv.pack_islands(margin=0.002, rotate=True) ``` ### Текстуры для бейка (Normal, AO, Curvature) ```python # Lightmap Pack с ненулевым padding bpy.ops.uv.lightmap_pack(PADDING=8, Px_MARGIN=8) ``` --- ## 🔧 ПРО: UV УТИЛИТЫ ### Проверить перекрытия ```python # В EDIT mode bpy.ops.uv.select_all(action='DESELECT') bpy.ops.uv.select_overlap() # Выделены перекрывающиеся острова ``` ### Выровнять тексель (Texas Density) ```python # Установить плотность текселей (пикселей на метр) bpy.ops.uv.texel_density_set(target=512) # 512 px/m ``` ### Копировать UV между объектами ```python src = bpy.data.objects["Source"] dst = bpy.data.objects["Target"] # Передать активный UV слой for i, (sf, df) in enumerate(zip(src.data.polygons, dst.data.polygons)): for sl, dl in zip(sf.loop_indices, df.loop_indices): dst.data.uv_layers.active.data[dl].uv = src.data.uv_layers.active.data[sl].uv ``` --- ## ⚠️ ЧАСТЫЕ ПРОБЛЕМЫ | Проблема | Причина | Решение | |----------|---------|---------| | Растянутые текстуры | Неправильный aspect | `correct_aspect=True` | | Перекрытия островов | Мало margin | Увеличь `island_margin` / `PADDING` | | Видимые швы | Нет швов / плохие | Отметь `use_seam` на острых рёбрах | | Неравномерная плотность | Разный масштаб | `Object > Apply > Scale` перед unwrap | | Инверсия UV | Отрицательный масштаб | Проверь `scale.x * scale.y * scale.z > 0` | --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P37: SEAMLESS TEXTURE WORKFLOW](37_seamless_textures.md), [P46: UDIM WORKFLOW](46_udim_workflow.md), [P03: MODIFIERS STACK](03_modifiers_stack.md), [P04: MESH EDITING (BMESH)](04_mesh_editing_bmesh.md), [P07: INSTANCING & ARRAYS](07_instancing_arrays.md) - **По темам:** [P10: NODE GROUP CREATION](10_node_groups.md), [P11: PROCEDURAL TEXTURES](11_procedural_textures.md), [P12: TEXTURE PAINTING](12_texture_painting.md) - **Теги:** #procedural, #prompt, #texturing, #unwrapping, #uv --- # P06: RETOPOLOGY WORKFLOW Tags: #game #game-ready #optimization #prompt #quads #retopo #retopology # P06: RETOPOLOGY WORKFLOW **ID:** P06 | **Cluster:** Geometry Basics | **Difficulty:** ⭐⭐⭐ | **Tags:** #retopology #retopo #game-ready #optimization #quads --- ## 🎯 GOAL Create clean, animation-ready topology from high-poly source using manual and automated retopology --- ## 📝 PROMPT (RUS) ``` Выполни ретопологию "[HIGHPOLY_OBJECT]" → "[LOWPOLY_OBJECT]": Метод: MANUAL (Snap to surface) / QUAD_REMESHER / INSTANT_MESHES Target tris: [NUMBER] Сохранить UV: YES/NO Сохранить вертекс-группы: YES/NO ``` --- ## 📝 PROMPT (ENG) ``` Retopologize "[HIGHPOLY_OBJECT]" → "[LOWPOLY_OBJECT]": Method: MANUAL (Snap to surface) / QUAD_REMESHER / INSTANT_MESHES Target tris: [NUMBER] Preserve UV: YES/NO Preserve vertex groups: YES/NO ``` --- ## 🔧 MCP: `execute_blender_code` ### QUAD REMESHER (Авто, лучшее качество) ```python import bpy highpoly = bpy.data.objects.get("HighPoly_Sculpt") if not highpoly: raise ValueError("Highpoly object not found") # Дубликат для ретопологии lowpoly = highpoly.copy() lowpoly.data = highpoly.data.copy() lowpoly.name = "LowPoly_Retopo" bpy.context.collection.objects.link(lowpoly) # Quad Remesher (требует аддон, встроен в Blender 3.5+) bpy.context.view_layer.objects.active = lowpoly bpy.ops.object.mode_set(mode='OBJECT') # Встроенный Quadriflow (Blender 3.5+) bpy.ops.object.quadriflow_remesh( quad_count=5000, # Целевое кол-во квадов adaptivity=0.0, # 0=равномерно, 1=адаптивно smoothness=1.0, # Сглаживание seed=42 # Для воспроизводимости ) print(f"Retopo done: {len(lowpoly.data.polygons)} quads, {len(lowpoly.data.vertices)} verts") ``` ### MANUAL RETOPO (Snap to Surface) ```python import bpy import bmesh highpoly = bpy.data.objects.get("HighPoly") lowpoly = bpy.data.objects.get("LowPoly_Base") if not highpoly or not lowpoly: raise ValueError("Objects not found") # Настроить Snap bpy.context.scene.tool_settings.snap_elements = {'FACE'} bpy.context.scene.tool_settings.use_snap = True bpy.context.scene.tool_settings.use_snap_project = True bpy.context.scene.tool_settings.snap_target = 'CLOSEST' # В EDIT mode на lowpoly bpy.context.view_layer.objects.active = lowpoly bpy.ops.object.mode_set(mode='EDIT') bm = bmesh.from_edit_mesh(lowpoly.data) bm.verts.ensure_lookup_table() # Переместить каждую вершину на поверхность highpoly import bpy_extras.view3d_utils import mathutils for v in bm.verts: if v.select: # Только выделенные world_co = lowpoly.matrix_world @ v.co # Raycast к highpoly result, location, normal, index = highpoly.ray_cast( highpoly.matrix_world.inverted() @ world_co, highpoly.matrix_world.inverted() @ (world_co + mathutils.Vector((0,0,-10))) ) if result: v.co = lowpoly.matrix_world.inverted() @ location bmesh.update_edit_mesh(lowpoly.data) bpy.ops.object.mode_set(mode='OBJECT') ``` ### INSTANT MESHES (Если установлен) ```bash # Внешний инструмент: https://github.com/wjakob/instant-meshes # CLI: instant-meshes -o output.obj input.obj -f 5000 # Потом импорт в Blender ``` --- ## 📊 СРАВНЕНИЕ МЕТОДОВ | Метод | Скорость | Качество | Контроль | Зависимости | |-------|----------|----------|----------|-------------| | **Quad Remesher** | ⚡⚡ | ⭐⭐⭐⭐ | Средний | Встроен (Blender 3.5+) | | **Instant Meshes** | ⚡⚡⚡ | ⭐⭐⭐ | Высокий | Внешний бинарь | | **Manual (Snap)** | ⚡ | ⭐⭐⭐⭐⭐ | Полный | Нет | | **Quad Remesher (аддон)** | ⚡⚡ | ⭐⭐⭐⭐⭐ | Высокий | Платный аддон | --- ## 💡 ПОСЛЕ РЕТОПОЛОГИИ ```python # 1. Проверить топологию bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.vert_connect_nonplanar() # Найти не-плоские bpy.ops.mesh.select_non_manifold() # Non-manifold bpy.ops.object.mode_set(mode='OBJECT') # 2. Перенос UV (Data Transfer) bpy.ops.object.mode_set(mode='OBJECT') bpy.context.view_layer.objects.active = highpoly highpoly.select_set(True) lowpoly.select_set(True) bpy.ops.object.join_uvs() # или Data Transfer modifier # 3. Перенос Vertex Groups (Data Transfer) dt = lowpoly.modifiers.new("DataTransfer", 'DATA_TRANSFER') dt.object = highpoly dt.use_vert_data = True dt.data_types_verts = {'VGROUP_WEIGHTS'} dt.vert_mapping = 'NEAREST_FACE_INTERPOLATED' bpy.ops.object.modifier_apply(modifier="DataTransfer") # 4. Bake Normal Map # См. P05 (UV) + WF 03 (Texture Pipeline) ``` --- ## ⚠️ ЧЕК-ЛИСТ ПЕРЕД ЭКСПОРТОМ - [ ] Только квады (0 треугольников) - [ ] Нет полюсов > 5 ребер - [ ] Edge flow следует анатомии / форме - [ ] UV развёрнуты без перекрытий - [ ] Нормали направлены наружу - [ ] Вертекс-группы перенесены - [ ] Normal Map запечён (High → Low) --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P30: OPTIMIZATION (SCENE & RENDER)](30_optimization.md), [P01: PRIMITIVE CREATION BASICS](01_geometry_basics.md), [P02: BOOLEAN OPERATIONS](02_boolean_operations.md), [P03: MODIFIERS STACK](03_modifiers_stack.md), [P04: MESH EDITING (BMESH)](04_mesh_editing_bmesh.md) - **По темам:** [P05: UV UNWRAPPING STRATEGIES](05_uv_unwrapping.md), [P07: INSTANCING & ARRAYS](07_instancing_arrays.md), [P08: SCULPTING BASICS](08_sculpting_basics.md) - **Теги:** #game, #game-ready, #optimization, #prompt, #quads, #retopo, #retopology --- # P09: PRINCIPLED BSDF SETUP Tags: #materials #pbr #principled #principled-bshf #prompt #shader #shader-nodes #shading # P09: PRINCIPLED BSDF SETUP **ID:** P09 | **Кластер:** Materials PBR | **Сложность:** ⭐ | **Теги:** #materials #pbr #principled-bshf #shader-nodes --- ## 🎯 ЦЕЛЬ Создание и настройка Principled BSDF материала с полным набором PBR карт --- ## 📝 ПРОМПТ (РУС) ``` Создай материал "[MAT_NAME]" типа Principled BSDF: Base Color: [R, G, B] или hex #RRGGBB Roughness: [0.0-1.0] Metallic: [0.0-1.0] [Optional] Transmission, IOR, Clearcoat, Sheen, Emission Присвой к объекту "[OBJECT_NAME]" ``` --- ## 📝 ПРОМПТ (ENG) ``` Create material "[MAT_NAME]" with Principled BSDF: Base Color: [R, G, B] or hex #RRGGBB Roughness: [0.0-1.0] Metallic: [0.0-1.0] [Optional] Transmission, IOR, Clearcoat, Sheen, Emission Assign to object "[OBJECT_NAME]" ``` --- ## 🔧 MCP: `set_material` (базовый) ```json { "type": "set_material", "params": { "object_name": "Cube", "material_name": "Mat_Red", "color": [1.0, 0.0, 0.0, 1.0] } } ``` --- ## 🔧 MCP: `execute_blender_code` (полный PBR) ```python import bpy from pathlib import Path def create_pbr_material(name, maps_dict, obj_name=None): """ maps_dict keys: 'base_color', 'roughness', 'metallic', 'normal', 'height', 'ao', 'emission' values: filepath или Color tuple (R,G,B,A) """ mat = bpy.data.materials.new(name) mat.use_nodes = True nodes = mat.node_tree.nodes links = mat.node_tree.links nodes.clear() # Principled BSDF principled = nodes.new('ShaderNodeBsdfPrincipled') principled.location = (0, 0) # Material Output output = nodes.new('ShaderNodeOutputMaterial') output.location = (400, 0) links.new(principled.outputs['BSDF'], output.inputs['Surface']) # Displacement disp = nodes.new('ShaderNodeDisplacement') disp.location = (200, -200) links.new(disp.outputs['Displacement'], output.inputs['Displacement']) mat.displacement_method = 'BOTH' # Texture Coordinate + Mapping tex_coord = nodes.new('ShaderNodeTexCoord') tex_coord.location = (-600, 0) mapping = nodes.new('ShaderNodeMapping') mapping.location = (-400, 0) links.new(tex_coord.outputs['UV'], mapping.inputs['Vector']) def add_texture(key, input_name, colorspace='sRGB', use_normal_map=False, use_disp=False): if key not in maps_dict: return val = maps_dict[key] if isinstance(val, (tuple, list)): # Color principled.inputs[input_name].default_value = val + (1.0,) if len(val)==3 else val return # Image file tex = nodes.new('ShaderNodeTexImage') tex.image = bpy.data.images.load(str(val)) tex.image.colorspace_settings.name = colorspace tex.location = (-200, 0) links.new(mapping.outputs['Vector'], tex.inputs['Vector']) if use_normal_map: nmap = nodes.new('ShaderNodeNormalMap') nmap.location = (0, -100) links.new(tex.outputs['Color'], nmap.inputs['Color']) links.new(nmap.outputs['Normal'], principled.inputs[input_name]) elif use_disp: links.new(tex.outputs['Color'], disp.inputs['Height']) else: links.new(tex.outputs['Color'], principled.inputs[input_name]) # Подключить карты add_texture('base_color', 'Base Color', 'sRGB') add_texture('roughness', 'Roughness', 'Non-Color') add_texture('metallic', 'Metallic', 'Non-Color') add_texture('normal', 'Normal', 'Non-Color', use_normal_map=True) add_texture('height', 'Height', 'Non-Color', use_disp=True) add_texture('emission', 'Emission Color', 'sRGB') # AO -> Mix с Base Color if 'ao' in maps_dict: ao_tex = nodes.new('ShaderNodeTexImage') ao_tex.image = bpy.data.images.load(str(maps_dict['ao'])) ao_tex.image.colorspace_settings.name = 'Non-Color' ao_tex.location = (-200, 200) links.new(mapping.outputs['Vector'], ao_tex.inputs['Vector']) mix = nodes.new('ShaderNodeMixRGB') mix.blend_type = 'MULTIPLY' mix.location = (0, 100) # Переподключить Base Color через Mix if principled.inputs['Base Color'].links: from_node = principled.inputs['Base Color'].links[0].from_node from_socket = principled.inputs['Base Color'].links[0].from_socket links.new(from_socket, mix.inputs[1]) links.new(ao_tex.outputs['Color'], mix.inputs[2]) links.new(mix.outputs['Color'], principled.inputs['Base Color']) # Назначить объекту if obj_name: obj = bpy.data.objects.get(obj_name) if obj: if obj.data.materials: obj.data.materials[0] = mat else: obj.data.materials.append(mat) return mat # === ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ === # 1. Простой цвет create_pbr_material("Mat_Red", { 'base_color': (1.0, 0.2, 0.2, 1.0), 'roughness': 0.5, 'metallic': 0.0 }, "Cube") # 2. Золото create_pbr_material("Mat_Gold", { 'base_color': (1.0, 0.843, 0.0, 1.0), 'roughness': 0.1, 'metallic': 1.0 }, "Ring") # 3. Стекло create_pbr_material("Mat_Glass", { 'base_color': (1.0, 1.0, 1.0, 1.0), 'roughness': 0.0, 'transmission': 1.0, # нужно добавить в функцию 'ior': 1.45 }, "Glass_Obj") # 4. Полный PBR из файлов create_pbr_material("Mat_Brick", { 'base_color': Path("//textures/brick_basecolor.png"), 'roughness': Path("//textures/brick_roughness.png"), 'normal': Path("//textures/brick_normal.png"), 'height': Path("//textures/brick_height.png"), 'ao': Path("//textures/brick_ao.png"), }, "Wall") ``` --- ## 📊 PBR ПАРАМЕТРЫ ШПАРГАЛКА | Материал | Base Color | Roughness | Metallic | Transmission | IOR | Clearcoat | |----------|------------|-----------|----------|--------------|-----|-----------| | **Пластик** | Цвет | 0.3-0.5 | 0.0 | 0.0 | 1.5 | 0.0 | | **Металл (полированный)** | Цвет металла | 0.05-0.15 | 1.0 | 0.0 | — | 0.0 | | **Металл (ржавый/матовый)** | Цвет металла | 0.6-0.9 | 1.0 | 0.0 | — | 0.0 | | **Стекло** | Белый | 0.0 | 0.0 | 1.0 | 1.45-1.5 | 0.0 | | **Вода** | Голубой | 0.05 | 0.0 | 1.0 | 1.33 | 0.0 | | **Кожа** | Кожаный цвет | 0.6-0.8 | 0.0 | 0.0 | 1.5 | 0.1-0.3 | | **Ткань** | Цвет ткани | 0.8-1.0 | 0.0 | 0.0 | 1.5 | 0.0 | | **Керамика** | Цвет | 0.1-0.3 | 0.0 | 0.0 | 1.5 | 0.2-0.5 | | **Камень/Бетон** | Серый/бежевый | 0.7-1.0 | 0.0 | 0.0 | 1.5 | 0.0 | | **Дерево** | Деревянный | 0.6-0.9 | 0.0 | 0.0 | 1.5 | 0.0 | --- ## 🎨 СПЕЦИАЛЬНЫЕ ЭФФЕКТЫ ### Emission (Свечение) ```python principled.inputs['Emission Color'].default_value = (1.0, 0.8, 0.2, 1.0) # Тёплый свет principled.inputs['Emission Strength'].default_value = 5.0 # Сила ``` ### Clearcoat (Лак/Керамика) ```python principled.inputs['Coat Weight'].default_value = 1.0 principled.inputs['Coat Roughness'].default_value = 0.03 ``` ### Sheen (Ткань/Велюр) ```python principled.inputs['Sheen Weight'].default_value = 0.5 principled.inputs['Sheen Roughness'].default_value = 0.3 principled.inputs['Sheen Tint'].default_value = 0.5 ``` ### Subsurface (Кожа, воск, мрамор) ```python principled.inputs['Subsurface Weight'].default_value = 0.3 principled.inputs['Subsurface Radius'].default_value = (1.0, 0.2, 0.1) # RGB радиус проникновения principled.inputs['Subsurface Color'].default_value = (1.0, 0.3, 0.1, 1.0) ``` ### Anisotropic (Поверхности с насечкой) ```python principled.inputs['Anisotropic'].default_value = 0.8 principled.inputs['Anisotropic Rotation'].default_value = 0.25 # 90° # Нужен Tangent вход (UV или Geometry > Tangent) ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P10: NODE GROUP CREATION](10_node_groups.md), [P11: PROCEDURAL TEXTURES](11_procedural_textures.md), [P12: TEXTURE PAINTING](12_texture_painting.md), [P41: DISPLACEMENT & MICRO-DETAIL](41_displacement_micro.md), [P46: UDIM WORKFLOW](46_udim_workflow.md) - **По темам:** [CASE 03: PRODUCT VISUALIZATION](../cases/case_03_product_viz.md), [P01: PRIMITIVE CREATION BASICS](01_geometry_basics.md), [P02: BOOLEAN OPERATIONS](02_boolean_operations.md) - **Теги:** #materials, #pbr, #principled, #principled-bshf, #prompt, #rrggbb, #shader, #shader-nodes, #shading --- # P16: KEYFRAME ANIMATION BASICS Tags: #animation #dope #dope-sheet #graph #graph-editor #interpolation #keyframes #prompt # P16: KEYFRAME ANIMATION BASICS **ID:** P16 | **Кластер:** Animation | **Сложность:** ⭐ | **Теги:** #animation #keyframes #interpolation #graph-editor #dope-sheet --- ## 🎯 ЦЕЛЬ Базовая анимация через ключевые кадры: location/rotation/scale, интерполяция, Graph Editor --- ## 📝 ПРОМПТ (РУС) ``` Анимируй "[OBJECT_NAME]" свойство [PROP: location/rotation_euler/scale]: Кадры: {frame: value, ...} Интерполяция: BEZIER / LINEAR / CONSTANT Пример: "Анимируй Cube location {1: (0,0,0), 50: (5,0,0), 100: (5,5,2)} BEZIER" ``` --- ## 📝 ПРОМПТ (ENG) ``` Animate "[OBJECT_NAME]" property [PROP]: Keyframes: {frame: value, ...} Interpolation: BEZIER / LINEAR / CONSTANT Example: "Animate Cube location {1: (0,0,0), 50: (5,0,0), 100: (5,5,2)} BEZIER" ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy def animate_property(obj_name, prop_path, keyframes, interpolation='BEZIER'): obj = bpy.data.objects.get(obj_name) if not obj: raise ValueError(f"Object {obj_name} not found") for frame, value in sorted(keyframes.items()): setattr(obj, prop_path, value) obj.keyframe_insert(data_path=prop_path, frame=frame) if obj.animation_data and obj.animation_data.action: for fc in obj.animation_data.action.fcurves: if fc.data_path == prop_path: for kp in fc.keyframe_points: kp.interpolation = interpolation return {"animated": obj_name, "prop": prop_path, "keys": len(keyframes)} # Пример: куб летит по дуге animate_property("Cube", "location", {1: (0,0,0), 50: (5,0,3), 100: (10,0,0)}, 'BEZIER') ``` --- ## 📊 ИНТЕРПОЛЯЦИЯ | Тип | Эффект | Когда | |-----|--------|-------| | BEZIER | Плавно, ease in/out | 90% случаев | | LINEAR | Равномерно | Механика | | CONSTANT | Резко, без перехода | Hold-позы, стоп-моушн | --- ## 💡 СОВЕТЫ - Frame 1 = Frame N+1 для лупа (см. Case 06) - Graph Editor → Key → Simplify для чистки - `fcurve.extrapolation = 'CYCLIC'` для бесконечного повтора --- ## ⚠️ ОШИБКИ | Ошибка | Решение | |--------|---------| | Ключи не вставляются | Проверь режим OBJECT, объект выбран | | Рывки | Смени BEZIER→LINEAR или почисти handles (V → Automatic) | --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P14: CAMERA & COMPOSITION](14_camera_composition.md), [P18: DRIVERS & CONSTRAINTS](18_drivers_constraints.md), [P19: NLA EDITOR WORKFLOW](19_nla_workflow.md), [P20: SHAPE KEYS & MORPHING](20_shape_keys.md), [P33: CAMERA PATH ANIMATION](33_camera_path.md) - **По темам:** [P34: CORRECTIVE SHAPE KEYS](34_corrective_shapes.md), [P43: CAMERA SWITCHING & MARKERS](43_camera_switching.md), [CASE 04: CHARACTER RIG & ANIMATION](../cases/case_04_character_rig.md) - **Теги:** #animation, #dope, #dope-sheet, #graph, #graph-editor, #interpolation, #keyframes, #prompt --- # P17: RIGIFY HUMANOID SETUP Tags: #armature #humanoid #prompt #rigging #rigify #weights # P17: RIGIFY HUMANOID SETUP **ID:** P17 | **Кластер:** Animation | **Сложность:** ⭐⭐ | **Теги:** #rigify #rigging #armature #weights #humanoid --- ## 🎯 ЦЕЛЬ Rigify-риг для humanoid-персонажа: meta-rig → generate → weights → тест поз --- ## 📝 ПРОМПТ (РУС) ``` Зариггай "[MESH_NAME]" через Rigify: Meta-rig: Human, масштаб под меш Generate → Parent (Automatic Weights) → Weight Paint чистка Тест: T-pose, A-pose, присед ``` --- ## 📝 ПРОМПТ (ENG) ``` Rig "[MESH_NAME]" with Rigify: Meta-rig: Human scaled to mesh Generate → Parent (Automatic Weights) → Weight cleanup Test: T-pose, A-pose, squat ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy def rigify_setup(mesh_name): mesh = bpy.data.objects.get(mesh_name) if not mesh or mesh.type != 'MESH': raise ValueError("Mesh required") # Meta-rig bpy.ops.object.armature_human_metarig_add() meta = bpy.context.active_object meta.name = "MetaRig" # Масштабировать под меш (вручную в UI или кодом) # Generate bpy.context.view_layer.objects.active = meta bpy.ops.object.armature_human_tweak_add() # опционально bpy.ops.pose.rigify_generate() rig = bpy.data.objects.get("rig") # Parent bpy.ops.object.select_all(action='DESELECT') mesh.select_set(True); rig.select_set(True) bpy.context.view_layer.objects.active = rig bpy.ops.object.parent_set(type='ARMATURE_AUTO') return {"rig": rig.name, "mesh": mesh.name} # rigify_setup("Character_Body") ``` --- ## 📊 ЧЕК-ЛИСТ РИГА - [ ] Meta-rig кости внутри меша - [ ] Generate без ошибок - [ ] Веса: голова=skull, плечи=градиент, пальцы=по фалангам - [ ] Экстремальные позы без разрывов --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P44: MOCAP RETARGETING](44_mocap_retarget.md), [P01: PRIMITIVE CREATION BASICS](01_geometry_basics.md), [P02: BOOLEAN OPERATIONS](02_boolean_operations.md), [P03: MODIFIERS STACK](03_modifiers_stack.md), [P04: MESH EDITING (BMESH)](04_mesh_editing_bmesh.md) - **По темам:** [CASE 04: CHARACTER RIG & ANIMATION](../cases/case_04_character_rig.md), [P05: UV UNWRAPPING STRATEGIES](05_uv_unwrapping.md), [P06: RETOPOLOGY WORKFLOW](06_retopology_workflow.md) - **Теги:** #armature, #humanoid, #prompt, #rigging, #rigify, #weights --- # P18: DRIVERS & CONSTRAINTS Tags: #animation #automation #constraints #drivers #modeling #procedural #prompt #rig # P18: DRIVERS & CONSTRAINTS **ID:** P18 | **Кластер:** Animation | **Сложность:** ⭐⭐ | **Теги:** #drivers #constraints #procedural #rig #automation --- ## 🎯 ЦЕЛЬ Процедурные связи: drivers (выражения) + constraints (Track To, Copy, Limit) --- ## 📝 ПРОМПТ (РУС) ``` Добавь драйвер: "[OBJ].[PROP]" ← "[TARGET].[PROP]" выражение "[EXPR]" Или constraint: [TYPE] на "[OBJ]" с целью "[TARGET]" Пример: "Гир B rotation.z = -gear_a_rot * 2" ``` --- ## 📝 ПРОМПТ (ENG) ``` Add driver: "[OBJ].[PROP]" ← "[TARGET].[PROP]" expression "[EXPR]" Or constraint: [TYPE] on "[OBJ]" targeting "[TARGET]" Example: "Gear B rotation.z = -gear_a_rot * 2" ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy def add_driver(obj_name, prop_path, target_name, target_prop, expr="var"): obj = bpy.data.objects.get(obj_name) tgt = bpy.data.objects.get(target_name) if not obj or not tgt: raise ValueError("Objects not found") drv = obj.driver_add(prop_path).driver drv.type = 'SCRIPTED' var = drv.variables.new() var.name = "var" var.targets[0].id = tgt var.targets[0].data_path = target_prop drv.expression = expr return {"driver": f"{obj_name}.{prop_path} = {expr}"} def add_track_to(obj_name, target_name): obj = bpy.data.objects.get(obj_name) tgt = bpy.data.objects.get(target_name) c = obj.constraints.new('TRACK_TO') c.target = tgt c.track_axis = 'TRACK_NEGATIVE_Z' c.up_axis = 'UP_Y' return {"constraint": "TRACK_TO"} # Примеры: # add_driver("Gear_B", "rotation_euler[2]", "Gear_A", "rotation_euler[2]", "-var * 2") # add_track_to("Camera", "Empty_Target") ``` --- ## 📊 CONSTRAINTS ШПАРГАЛКА | Тип | Назначение | |-----|------------| | TRACK_TO | Камера смотрит на цель | | COPY LOCATION/ROTATION/SCALE | Повторение трансформа | | LIMIT LOCATION/ROTATION | Ограничители (двери, суставы) | | FOLLOW PATH | Движение по кривой | | CHILD OF | Временное родительство (оружие в руке) | --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P34: CORRECTIVE SHAPE KEYS](34_corrective_shapes.md), [P03: MODIFIERS STACK](03_modifiers_stack.md), [P04: MESH EDITING (BMESH)](04_mesh_editing_bmesh.md), [P07: INSTANCING & ARRAYS](07_instancing_arrays.md), [P19: NLA EDITOR WORKFLOW](19_nla_workflow.md) - **По темам:** [P23: GEOMETRY NODES BASICS](23_geometry_nodes_basics.md), [P25: PROCEDURAL BUILDING GENERATOR](25_procedural_building.md), [P43: CAMERA SWITCHING & MARKERS](43_camera_switching.md) - **Теги:** #animation, #automation, #constraints, #drivers, #modeling, #procedural, #prompt, #rig --- # P20: SHAPE KEYS & MORPHING Tags: #animation #blendshapes #corrective #facial #morph #prompt #shape #shape-keys # P20: SHAPE KEYS & MORPHING **ID:** P20 | **Кластер:** Animation | **Сложность:** ⭐⭐ | **Теги:** #shape-keys #morph #facial #corrective #blendshapes --- ## 🎯 ЦЕЛЬ Shape keys для лица/коррективов: basis → keys → drivers → экспорт --- ## 📝 ПРОМПТ (РУС) ``` Создай shape keys для "[MESH]": [список: smile, blink_L, jaw_open...] Basis = нейтральное лицо. Значения 0..1. Драйверы от костей при нужде. ``` --- ## 📝 ПРОМПТ (ENG) ``` Create shape keys for "[MESH]": [list: smile, blink_L, jaw_open...] Basis = neutral. Range 0..1. Drive from bones if needed. ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy def add_shape_key(obj_name, key_name, from_mix=False): obj = bpy.data.objects.get(obj_name) if not obj or obj.type != 'MESH': raise ValueError("Mesh required") if not obj.data.shape_keys: obj.shape_key_add(name="Basis", from_mix=False) key = obj.shape_key_add(name=key_name, from_mix=from_mix) key.slider_min = 0.0 key.slider_max = 1.0 key.value = 0.0 return {"key": key.name} def drive_shape_from_bone(obj_name, key_name, rig_name, bone_name, axis='LOC_Z', scale=0.1): obj = bpy.data.objects.get(obj_name) key = obj.data.shape_keys.key_blocks.get(key_name) drv = key.driver_add("value").driver drv.type = 'SCRIPTED' var = drv.variables.new() var.name = "v" var.type = 'TRANSFORMS' var.targets[0].id = bpy.data.objects.get(rig_name) var.targets[0].bone_target = bone_name var.targets[0].transform_type = axis var.targets[0].transform_space = 'LOCAL_SPACE' drv.expression = f"v * {scale}" return {"driven": key_name} # add_shape_key("Head", "smile") # add_shape_key("Head", "blink_L") # drive_shape_from_bone("Head", "jaw_open", "Rig", "jaw", 'LOC_Z', 0.05) ``` --- ## 📊 ЛИЦЕВЫЕ НАБОРЫ | Набор | Ключи | |-------|-------| | Базовый (6) | smile, frown, blink_L/R, jaw_open, brow_up | | FACS-мини (15) | + lip_pucker, cheek_raise, nose_wrinkle, eye_squint... | | ARKit (52) | Полный набор для мокапа | --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P34: CORRECTIVE SHAPE KEYS](34_corrective_shapes.md), [P14: CAMERA & COMPOSITION](14_camera_composition.md), [P16: KEYFRAME ANIMATION BASICS](16_keyframe_animation.md), [P18: DRIVERS & CONSTRAINTS](18_drivers_constraints.md), [P19: NLA EDITOR WORKFLOW](19_nla_workflow.md) - **По темам:** [P33: CAMERA PATH ANIMATION](33_camera_path.md), [P43: CAMERA SWITCHING & MARKERS](43_camera_switching.md), [CASE 04: CHARACTER RIG & ANIMATION](../cases/case_04_character_rig.md) - **Теги:** #animation, #blendshapes, #corrective, #facial, #morph, #prompt, #shape, #shape-keys --- # P21: PHYSICS SIMULATION Tags: #cache #cloth #clothing #collision #fluid #physics #prompt #rigid #rigid-body #simulation # P21: PHYSICS SIMULATION **ID:** P21 | **Кластер:** Simulation | **Сложность:** ⭐⭐⭐ | **Теги:** #physics #rigid-body #cloth #fluid #collision #cache --- ## 🎯 ЦЕЛЬ Симуляции Rigid Body / Cloth / Fluid: настройка, бейк кэша, экспорт Alembic --- ## 📝 ПРОМПТ (РУС) ``` Настрой симуляцию [TYPE: RIGID/CLOTH/FLUID] для "[OBJECT]": Параметры: [mass/friction/damping/resolution] Кэш: DISK CACHE [start-end], запеки Экспорт: Alembic при нужде ``` --- ## 📝 ПРОМПТ (ENG) ``` Setup [TYPE: RIGID/CLOTH/FLUID] simulation for "[OBJECT]": Params: [mass/friction/damping/resolution] Cache: DISK CACHE [start-end], bake it Export: Alembic if needed ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy def setup_rigid_body(obj_name, body_type='ACTIVE', mass=1.0, friction=0.5, restitution=0.2): obj = bpy.data.objects.get(obj_name) if not obj: raise ValueError(f"Object {obj_name} not found") bpy.context.view_layer.objects.active = obj bpy.ops.rigidbody.object_add(type=body_type) rb = obj.rigid_body rb.mass = mass rb.friction = friction rb.restitution = restitution rb.collision_shape = 'CONVEX_HULL' return {"rigid_body": obj_name, "type": body_type} def setup_cloth(obj_name, pin_group=None, quality=5): obj = bpy.data.objects.get(obj_name) bpy.context.view_layer.objects.active = obj mod = obj.modifiers.new("Cloth", 'CLOTH') s = mod.settings s.quality = quality s.mass = 0.3 s.air_damping = 1.0 if pin_group: s.vertex_group_mass = pin_group # Collision mod.collision_settings.use_collision = True mod.collision_settings.thickness_outer = 0.01 return {"cloth": obj_name} def bake_cache(frame_start=1, frame_end=250, cache_type='ALL'): bpy.context.scene.frame_start = frame_start bpy.context.scene.frame_end = frame_end bpy.ops.ptcache.bake_all(bake=True) return {"baked": f"{frame_start}-{frame_end}"} def export_alembic(objects, filepath, start=1, end=250): bpy.ops.object.select_all(action='DESELECT') for o in objects: bpy.data.objects.get(o).select_set(True) bpy.ops.wm.alembic_export( filepath=filepath, selected=True, start=start, end=end, flatten=True, subsamples=4) return {"alembic": filepath} # setup_rigid_body("Crate", mass=2.0) # setup_cloth("Flag", pin_group="Pin", quality=6) # bake_cache(1, 120) ``` --- ## 📊 ТИПЫ СИМУЛЯЦИЙ | Тип | Модификатор | Кэш | Экспорт | |-----|-------------|-----|---------| | Rigid Body | RigidBody world | Point Cache | Alembic | | Cloth | CLOTH | Point Cache | Alembic / MDD | | Fluid (Mantaflow) | FLUID domain/flow | OpenVDB | VDB sequence | | Particles | PARTICLE_SYSTEM | Point Cache | Alembic | | Soft Body | SOFT_BODY | Point Cache | Alembic | --- ## ⚠️ ОШИБКИ | Ошибка | Решение | |--------|---------| | Объекты проваливаются | Увеличь Substeps (Scene → Rigid Body World → Steps), проверь Scale applied | | Cloth взрывается | Уменьши mass, увеличь quality, проверь self-collision distance | | Кэш не бейкится | Cache Type = All, путь записи доступен, Disk Cache включён | --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P45: GEOMETRY NODES SIMULATION ZONES](45_gn_simulation.md), [P47: ALEMBIC CACHE PIPELINE](47_alembic_cache.md), [P03: MODIFIERS STACK](03_modifiers_stack.md), [P22: PARTICLE SYSTEMS](22_particles.md), [P01: PRIMITIVE CREATION BASICS](01_geometry_basics.md) - **По темам:** [P02: BOOLEAN OPERATIONS](02_boolean_operations.md), [P04: MESH EDITING (BMESH)](04_mesh_editing_bmesh.md), [P05: UV UNWRAPPING STRATEGIES](05_uv_unwrapping.md) - **Теги:** #cache, #cloth, #clothing, #collision, #fluid, #physics, #prompt, #rigid, #rigid-body, #simulation --- # P22: PARTICLE SYSTEMS Tags: #cache #children #emitter #geometry-nodes #hair #modeling #particles #pipeline #prompt #render # P22: PARTICLE SYSTEMS **ID:** P22 | **Кластер:** Simulation | **Сложность:** ⭐⭐ | **Теги:** #particles #hair #emitter #children #cache --- ## 🎯 ЦЕЛЬ Системы частиц: emitter/hair, children, поля, кэш, рендер как объекты --- ## 📝 ПРОМПТ (РУС) ``` Создай particle system на "[EMITTER]": тип [EMITTER/HAIR], count [N], lifetime [F], velocity [V], render as [OBJECT/COLLECTION] ``` --- ## 📝 ПРОМПТ (ENG) ``` Create particle system on "[EMITTER]": type [EMITTER/HAIR], count [N], lifetime [F], velocity [V], render as [OBJECT/COLLECTION] ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy def create_emitter(obj_name, count=1000, lifetime=50, vel=2.0, render_obj=None): obj = bpy.data.objects.get(obj_name) if not obj: raise ValueError("Emitter not found") bpy.context.view_layer.objects.active = obj mod = obj.modifiers.new("Particles", 'PARTICLE_SYSTEM') psys = mod.particle_system s = psys.settings s.type = 'EMITTER' s.count = count s.lifetime = lifetime s.frame_start = 1 s.frame_end = 10 s.emit_from = 'FACE' s.normal_factor = vel s.render_type = 'OBJECT' if render_obj else 'HALO' if render_obj: s.instance_object = bpy.data.objects.get(render_obj) return {"particles": count, "emitter": obj_name} def create_hair(obj_name, count=500, length=0.3, children=10): obj = bpy.data.objects.get(obj_name) bpy.context.view_layer.objects.active = obj mod = obj.modifiers.new("Hair", 'PARTICLE_SYSTEM') s = mod.particle_system.settings s.type = 'HAIR' s.count = count s.hair_length = length s.child_nbr = children s.child_type = 'INTERPOLATED' return {"hair": count} # create_emitter("Emitter", count=2000, lifetime=60, vel=3.0, render_obj="Debris") # create_hair("Head", count=800, length=0.25) ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P45: GEOMETRY NODES SIMULATION ZONES](45_gn_simulation.md), [P47: ALEMBIC CACHE PIPELINE](47_alembic_cache.md), [P07: INSTANCING & ARRAYS](07_instancing_arrays.md), [P23: GEOMETRY NODES BASICS](23_geometry_nodes_basics.md), [P25: PROCEDURAL BUILDING GENERATOR](25_procedural_building.md) - **По темам:** [CASE 05: PROCEDURAL CITY](../cases/case_05_procedural_city.md), [P36: ASSET LICENSING (PIZZA/SKETCHFAB/HAVEN)](36_asset_licensing.md), [P43: CAMERA SWITCHING & MARKERS](43_camera_switching.md) - **Теги:** #cache, #children, #emitter, #geometry-nodes, #hair, #modeling, #particles, #pipeline, #prompt, #render --- # P24: GN SCATTER & DISTRIBUTION Tags: #density #geometry-nodes #lod #poisson #poisson-disk #prompt #scatter #weight #weight-paint # P24: GN SCATTER & DISTRIBUTION **ID:** P24 | **Кластер:** Geometry Nodes | **Сложность:** ⭐⭐ | **Теги:** #scatter #poisson-disk #density #weight-paint #lod --- ## 🎯 ЦЕЛЬ Продвинутое рассеивание: density attribute, weight paint, random pick, LOD по дистанции --- ## 📝 ПРОМПТ (РУС) ``` Рассей коллекцию "[COLL]" по "[TARGET]": Density: [attribute/значение], Count [N], Scale [min-max], Seed [S] ``` --- ## 📝 ПРОМПТ (ENG) ``` Scatter collection "[COLL]" on "[TARGET]": Density: [attribute/value], Count [N], Scale [min-max], Seed [S] ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy def gn_scatter_advanced(target, collection, density_attr="", count=1000, smin=0.8, smax=1.2, seed=42): obj = bpy.data.objects.get(target) coll = bpy.data.collections.get(collection) if not obj or not coll: raise ValueError("Target or collection not found") ng = bpy.data.node_groups.new("GN_ScatterAdv", 'GeometryNodeTree') nodes, links = ng.nodes, ng.links nodes.clear() inp = nodes.new('NodeGroupInput'); out = nodes.new('NodeGroupOutput') inp.location = (-500, 0); out.location = (500, 0) ng.interface.new_socket(name="Density", in_out='INPUT', socket_type='NodeSocketFloat') ng.interface.new_socket(name="Seed", in_out='INPUT', socket_type='NodeSocketInt') ng.interface.new_socket(name="Geometry", in_out='OUTPUT', socket_type='NodeSocketGeometry') dist = nodes.new('GeometryNodeDistributePointsOnFaces') dist.location = (-200, 0) dist.distribute_method = 'POISSON' if density_attr: dist.inputs['Density Attribute'].default_value = density_attr inst = nodes.new('GeometryNodeInstanceOnPoints') inst.location = (0, 0) rnd_scale = nodes.new('GeometryNodeRandomValue') rnd_scale.data_type = 'FLOAT_VECTOR' rnd_scale.location = (-200, -200) rnd_scale.outputs[0].default_value = (smin, smin, smin) rnd_scale.outputs[1].default_value = (smax, smax, smax) links.new(rnd_scale.outputs['Value'], inst.inputs['Scale']) real = nodes.new('GeometryNodeRealizeInstances') real.location = (300, 0) links.new(dist.outputs['Points'], inst.inputs['Points']) links.new(inst.outputs['Instances'], real.inputs['Geometry']) links.new(real.outputs['Geometry'], out.inputs['Geometry']) mod = obj.modifiers.new("GN_Scatter", 'NODES') mod.node_group = ng mod["Input_1"] = count mod["Input_2"] = seed return {"scatter": collection, "target": target} # gn_scatter_advanced("Ground", "Trees", density_attr="Density", count=2000) ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P07: INSTANCING & ARRAYS](07_instancing_arrays.md), [P22: PARTICLE SYSTEMS](22_particles.md), [P23: GEOMETRY NODES BASICS](23_geometry_nodes_basics.md), [P25: PROCEDURAL BUILDING GENERATOR](25_procedural_building.md), [P30: OPTIMIZATION (SCENE & RENDER)](30_optimization.md) - **По темам:** [P45: GEOMETRY NODES SIMULATION ZONES](45_gn_simulation.md), [CASE 05: PROCEDURAL CITY](../cases/case_05_procedural_city.md), [P01: PRIMITIVE CREATION BASICS](01_geometry_basics.md) - **Теги:** #density, #geometry-nodes, #lod, #poisson, #poisson-disk, #prompt, #scatter, #weight, #weight-paint --- # P25: PROCEDURAL BUILDING GENERATOR Tags: #architecture #array #building #geometry-nodes #modeling #procedural #prompt # P25: PROCEDURAL BUILDING GENERATOR **ID:** P25 | **Кластер:** Geometry Nodes | **Сложность:** ⭐⭐⭐ | **Теги:** #procedural #architecture #building #array #facade --- ## 🎯 ЦЕЛЬ Параметрическое здание: этажи, фасад с окнами, крыша, LOD --- ## 📝 ПРОМПТ (РУС) ``` Сгенерируй здание "[NAME]": W×D×H [w,d,h], этажей [N], оконная сетка [cols×rows], стиль [modern/classical], крыша [flat/pitched] ``` --- ## 📝 ПРОМПТ (ENG) ``` Generate building "[NAME]": W×D×H [w,d,h], floors [N], window grid [cols×rows], style [modern/classical], roof [flat/pitched] ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy def procedural_building(name, w=10, d=10, h=30, floors=10, win_cols=5, win_rows=8): mesh = bpy.data.meshes.new(name) obj = bpy.data.objects.new(name, mesh) bpy.context.collection.objects.link(obj) import bmesh bm = bmesh.new() # Корпус bmesh.ops.create_cube(bm, size=1.0) for v in bm.verts: v.co.x *= w / 2 v.co.y *= d / 2 v.co.z *= h / 2 v.co.z += h / 2 # base at z=0 bm.to_mesh(mesh) bm.free() # Этажи через Array arr = obj.modifiers.new("Floors", 'ARRAY') arr.count = 1 # корпус уже полный; окна отдельно # Окна: отдельные плоскости через массив bpy.ops.mesh.primitive_plane_add(size=1) win = bpy.context.active_object win.name = f"{name}_Windows" win.scale = (1.2, 0.05, 1.5) ax = win.modifiers.new("WinX", 'ARRAY') ax.count = win_cols ax.relative_offset_displace = (2.0, 0, 0) az = win.modifiers.new("WinZ", 'ARRAY') az.count = win_rows az.relative_offset_displace = (0, 0, 2.2) return {"building": name, "windows": win.name} # procedural_building("Tower_A", w=12, d=12, h=45, floors=12, win_cols=6, win_rows=10) ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P07: INSTANCING & ARRAYS](07_instancing_arrays.md), [P23: GEOMETRY NODES BASICS](23_geometry_nodes_basics.md), [P03: MODIFIERS STACK](03_modifiers_stack.md), [P04: MESH EDITING (BMESH)](04_mesh_editing_bmesh.md), [P18: DRIVERS & CONSTRAINTS](18_drivers_constraints.md) - **По темам:** [P22: PARTICLE SYSTEMS](22_particles.md), [P45: GEOMETRY NODES SIMULATION ZONES](45_gn_simulation.md), [CASE 05: PROCEDURAL CITY](../cases/case_05_procedural_city.md) - **Теги:** #architecture, #array, #building, #geometry-nodes, #modeling, #procedural, #prompt --- # P27: POLY HAVEN ASSET PIPELINE Tags: #assets #hdri #mcp #models #pipeline #polyhaven #prompt #textures # P27: POLY HAVEN ASSET PIPELINE **ID:** P27 | **Кластер:** MCP Integration | **Сложность:** ⭐ | **Теги:** #polyhaven #assets #hdri #textures #models #mcp --- ## 🎯 ЦЕЛЬ Поиск → скачивание → применение Poly Haven ассетов через MCP --- ## 📝 ПРОМПТ (РУС) ``` Найди на Poly Haven [тип: models/hdris/textures] "[запрос]", скачай [ID] [resolution] и [примени/поставь world] ``` --- ## 📝 ПРОМПТ (ENG) ``` Find on Poly Haven [type] "[query]", download [ID] [resolution] and [apply/set world] ``` --- ## 🔧 MCP TOOLS ```json // Поиск {"type": "search_polyhaven_assets", "params": {"asset_type": "hdris", "categories": "studio"}} // Скачивание HDRI {"type": "download_polyhaven_asset", "params": {"asset_id": "studio_small_01", "asset_type": "hdris", "resolution": "2k"}} // Скачивание текстуры + применение {"type": "download_polyhaven_asset", "params": {"asset_id": "brick_wall_01", "asset_type": "textures", "resolution": "2k"}} {"type": "set_texture", "params": {"object_name": "Wall", "texture_id": "brick_wall_01"}} ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P36: ASSET LICENSING (PIZZA/SKETCHFAB/HAVEN)](36_asset_licensing.md), [P13: LIGHTING & HDRI](13_lighting_hdri.md), [P11: PROCEDURAL TEXTURES](11_procedural_textures.md), [P22: PARTICLE SYSTEMS](22_particles.md), [P26: MCP BASICS (BLENDER-MCP COMMANDS)](26_mcp_basics.md) - **По темам:** [P28: CUSTOM MCP TOOLS DEVELOPMENT](28_custom_mcp_tools.md), [P35: MCP ERROR RECOVERY](35_mcp_error_recovery.md), [P38: HIGH→LOW BAKE PIPELINE](38_bake_pipeline.md) - **Теги:** #assets, #hdri, #mcp, #models, #pipeline, #polyhaven, #prompt, #textures --- # P28: CUSTOM MCP TOOLS DEVELOPMENT Tags: #addon #custom #custom-tools #fastmcp #mcp #prompt #python # P28: CUSTOM MCP TOOLS DEVELOPMENT **ID:** P28 | **Кластер:** MCP Integration | **Сложность:** ⭐⭐⭐ | **Теги:** #mcp #custom-tools #python #fastmcp #addon --- ## 🎯 ЦЕЛЬ Свои MCP-инструменты: серверная сторона (FastMCP) + Blender addon handler --- ## 📝 ПРОМПТ (РУС) ``` Создай MCP tool "[NAME]": параметры {...}, выполняет [действие bpy], возвращает {...}. Зарегистрируй в сервере и аддоне. ``` --- ## 📝 ПРОМПТ (ENG) ``` Create MCP tool "[NAME]": params {...}, runs [bpy action], returns {...}. Register in server and addon. ``` --- ## 🔧 ШАБЛОН ```python # Server (src/blender_mcp/tools/custom.py) from mcp.server.fastmcp import FastMCP mcp = FastMCP("Custom") @mcp.tool() async def my_tool(param: str) -> str: '''Description for LLM''' blender = get_blender_connection() return blender.send_command("my_command", {"param": param}) # Addon (addon.py) — в execute_command handlers: # def my_command_handler(self, param): ... ``` ```python # GenesisCore style (встроенный): # src/tools/my_tools.py from .common import ToolsPackageBase class MyTools(ToolsPackageBase): def my_tool(param: str) -> dict: '''Description for LLM''' import bpy return {"result": "success"} # + import в src/tools/__init__.py ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](26_mcp_basics.md), [P27: POLY HAVEN ASSET PIPELINE](27_polyhaven_pipeline.md), [P35: MCP ERROR RECOVERY](35_mcp_error_recovery.md), [P01: PRIMITIVE CREATION BASICS](01_geometry_basics.md), [P02: BOOLEAN OPERATIONS](02_boolean_operations.md) - **По темам:** [MCP ADVANCED: STREAMING & TOOL COMPOSITION](../mcp_advanced/streaming_tools.md), [P03: MODIFIERS STACK](03_modifiers_stack.md), [P04: MESH EDITING (BMESH)](04_mesh_editing_bmesh.md) - **Теги:** #addon, #custom, #custom-tools, #fastmcp, #mcp, #prompt, #python --- # P29: BATCH PROCESSING & CLI Tags: #automation #batch #cli #headless #prompt #render #render-farm # P29: BATCH PROCESSING & CLI **ID:** P29 | **Кластер:** Advanced | **Сложность:** ⭐⭐ | **Теги:** #batch #cli #headless #automation #render-farm --- ## 🎯 ЦЕЛЬ Пакетная обработка: `blender -b -P script.py`, батч-рендер, конвертация --- ## 📝 ПРОМПТ (РУС) ``` Запусти батч: blender -b [SCENE] -P [SCRIPT] -- [ARGS] Пример: "Отрендери 1-250 во всех .blend из папки" ``` --- ## 📝 ПРОМПТ (ENG) ``` Run batch: blender -b [SCENE] -P [SCRIPT] -- [ARGS] Example: "Render 1-250 for all .blend in folder" ``` --- ## 🔧 КОМАНДЫ ```bash # Один кадр blender -b scene.blend -E CYCLES -o //out/frame_#### -f 1 # Анимация blender -b scene.blend -E CYCLES -o //out/anim_#### -s 1 -e 250 -a # Скрипт blender -b scene.blend -P setup_render.py -o //out/ -f 1 # Пакет по папке for f in scenes/*.blend; do blender -b "$f" -P batch.py -- --out renders/; done ``` ```python # batch.py шаблон import bpy, sys argv = sys.argv[sys.argv.index("--")+1:] bpy.ops.wm.open_mainfile(filepath=argv[0]) bpy.context.scene.render.filepath = argv[1] bpy.ops.render.render(animation=True) ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P47: ALEMBIC CACHE PIPELINE](47_alembic_cache.md), [P49: RENDER FARM SUBMIT](49_farm_submit.md), [P14: CAMERA & COMPOSITION](14_camera_composition.md), [P18: DRIVERS & CONSTRAINTS](18_drivers_constraints.md), [P22: PARTICLE SYSTEMS](22_particles.md) - **По темам:** [P30: OPTIMIZATION (SCENE & RENDER)](30_optimization.md), [P31: RENDER PASSES & AOV](31_render_passes_aov.md), [P40: VERSION CONTROL & RENDER JOBS](40_version_control.md) - **Теги:** #automation, #batch, #cli, #headless, #prompt, #render, #render-farm --- # P30: OPTIMIZATION (SCENE & RENDER) Tags: #decimate #denoise #instances #lod #optimization #prompt #render #samples # P30: OPTIMIZATION (SCENE & RENDER) **ID:** P30 | **Кластер:** Advanced | **Сложность:** ⭐⭐ | **Теги:** #optimization #decimate #lod #instances #samples #denoise --- ## 🎯 ЦЕЛЬ Оптимизация сцены и рендера: полигоны, текстуры, сэмплы, инстансинг --- ## 📝 ПРОМПТ (РУС) ``` Оптимизируй сцену: decimate [ratio], LOD [уровни], текстуры [res], сэмплы [N] + denoise. Цель: [FPS/VRAM/время кадра] ``` --- ## 📝 ПРОМПТ (ENG) ``` Optimize scene: decimate [ratio], LOD [levels], textures [res], samples [N] + denoise. Target: [FPS/VRAM/frame time] ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy def optimize_scene(target_tris=500000, tex_res=2048, samples=128): # 1. Decimate heavy meshes for obj in bpy.data.objects: if obj.type == 'MESH': tris = sum(len(p.vertices)-2 for p in obj.data.polygons) if tris > 50000: mod = obj.modifiers.new("Decimate_Opt", 'DECIMATE') mod.ratio = max(0.1, target_tris / max(tris, 1) / len(bpy.data.objects)) bpy.context.view_layer.objects.active = obj bpy.ops.object.modifier_apply(modifier=mod.name) # 2. Render settings s = bpy.context.scene s.render.engine = 'CYCLES' s.cycles.samples = samples s.cycles.use_denoising = True s.cycles.use_adaptive_sampling = True s.cycles.adaptive_threshold = 0.01 return {"optimized": True, "samples": samples} # optimize_scene() ``` --- ## 📊 ЧЕК-ЛИСТ - [ ] Применены scale, удалены doubles - [ ] LOD0/1/2 для тяжёлых объектов - [ ] Текстуры ≤2k для фона, 4k только hero - [ ] Adaptive sampling + denoise - [ ] Instancing вместо дубликатов --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P06: RETOPOLOGY WORKFLOW](06_retopology_workflow.md), [P07: INSTANCING & ARRAYS](07_instancing_arrays.md), [P14: CAMERA & COMPOSITION](14_camera_composition.md), [P22: PARTICLE SYSTEMS](22_particles.md), [P23: GEOMETRY NODES BASICS](23_geometry_nodes_basics.md) - **По темам:** [P24: GN SCATTER & DISTRIBUTION](24_gn_scatter.md), [P29: BATCH PROCESSING & CLI](29_batch_cli.md), [P31: RENDER PASSES & AOV](31_render_passes_aov.md) - **Теги:** #decimate, #denoise, #instances, #lod, #optimization, #prompt, #render, #samples --- # P31: RENDER PASSES & AOV Tags: #aov #compositing #cryptomatte #exr #passes #prompt #render # P31: RENDER PASSES & AOV **ID:** P31 | **Кластер:** Rendering | **Сложность:** ⭐⭐ | **Теги:** #render #passes #aov #cryptomatte #exr #compositing --- ## 🎯 ЦЕЛЬ Включить нужные пассы, вывести Multilayer EXR, проверить в Compositor/Viewer --- ## 📝 ПРОМПТ (РУС) ``` Включи пассы для "[SCENE]": [Combined, Depth, Normal, Mist, AO, Emission, Shadow, Cryptomatte Object/Material]. Вывод: Multilayer EXR 32-bit в //renders/exr/. Проверь каждый пасс во Viewer. ``` --- ## 📝 ПРОМПТ (ENG) ``` Enable passes for "[SCENE]": [Combined, Depth, Normal, Mist, AO, Emission, Shadow, Cryptomatte Object/Material]. Output: Multilayer EXR 32-bit to //renders/exr/. Verify each pass in Viewer. ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy def enable_passes(): vl = bpy.context.view_layer vl.use_pass_combined = True vl.use_pass_z = True vl.use_pass_normal = True vl.use_pass_mist = True vl.use_pass_ambient_occlusion = True vl.use_pass_emission = True vl.use_pass_shadow = True vl.use_pass_diffuse_color = True vl.use_pass_glossy_color = True vl.use_pass_cryptomatte = True vl.cryptomatte_use_object = True vl.cryptomatte_use_material = True s = bpy.context.scene s.use_nodes = True s.render.image_settings.file_format = 'OPEN_EXR_MULTILAYER' s.render.image_settings.color_mode = 'RGBA' s.render.image_settings.color_depth = '32' s.render.image_settings.exr_codec = 'ZIP' s.render.filepath = "//renders/exr/pass_####" return {"passes": "enabled", "output": s.render.filepath} ``` --- ## 📊 ПАССЫ | Пасс | Зачем | |------|-------| | Depth | Туман, DOF в посте, маски по дистанции | | Normal | Рельеф, релайт | | Mist | Атмосфера | | AO | Контактные тени | | Cryptomatte | Маски объектов/материалов без ручной ротоскопии | --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P15: COMPOSITING](15_compositing.md), [P14: CAMERA & COMPOSITION](14_camera_composition.md), [P22: PARTICLE SYSTEMS](22_particles.md), [P29: BATCH PROCESSING & CLI](29_batch_cli.md), [P30: OPTIMIZATION (SCENE & RENDER)](30_optimization.md) - **По темам:** [CASE 03: PRODUCT VISUALIZATION](../cases/case_03_product_viz.md), [P40: VERSION CONTROL & RENDER JOBS](40_version_control.md), [P41: DISPLACEMENT & MICRO-DETAIL](41_displacement_micro.md) - **Теги:** #aov, #compositing, #cryptomatte, #exr, #passes, #prompt, #render --- # P32: LIGHT LINKING & SHADOWS Tags: #collections #cycles #eevee #light #light-linking #lighting #prompt #shadows # P32: LIGHT LINKING & SHADOWS **ID:** P32 | **Кластер:** Lighting | **Сложность:** ⭐⭐ | **Теги:** #light-linking #shadows #collections #cycles #eevee --- ## 🎯 ЦЕЛЬ Управлять какие объекты освещает каждый свет; настроить тени без артефактов --- ## 📝 ПРОМПТ (РУС) ``` Настрой light linking: свет "[LIGHT]" влияет только на коллекцию "[COLL]". Тени: soft size [R], bias авто, contact shadows для EEVEE. ``` --- ## 📝 ПРОМПТ (ENG) ``` Setup light linking: light "[LIGHT]" affects only collection "[COLL]". Shadows: soft size [R], auto bias, contact shadows for EEVEE. ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy def tune_shadows(light_name, soft_size=0.5, clip_start=0.1, clip_end=100.0): light = bpy.data.objects.get(light_name) if not light: raise ValueError("Light not found") ld = light.data if hasattr(ld, 'shadow_soft_size'): ld.shadow_soft_size = soft_size ld.shadow_buffer_clip_start = clip_start ld.shadow_buffer_clip_end = clip_end return {"shadows": "tuned", "light": light_name} ``` --- ## ⚠️ ОШИБКИ | Ошибка | Решение | |--------|---------| | Свет влияет на всё | Проверь коллекцию получателей, исключи фон (Light Linking, Blender 4.x) | | Полосы/акне на тенях | Увеличь bias, уменьши clip_end | --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P42: LIGHT PORTALS & CAUSTICS](42_portals_caustics.md), [P48: EEVEE VS CYCLES DECISION](48_eevee_vs_cycles.md), [P13: LIGHTING & HDRI](13_lighting_hdri.md), [P14: CAMERA & COMPOSITION](14_camera_composition.md), [P36: ASSET LICENSING (PIZZA/SKETCHFAB/HAVEN)](36_asset_licensing.md) - **По темам:** [P41: DISPLACEMENT & MICRO-DETAIL](41_displacement_micro.md), [CASE 03: PRODUCT VISUALIZATION](../cases/case_03_product_viz.md), [EEVEE NEXT (BLENDER 4.2+)](../realtime/eevee_next.md) - **Теги:** #collections, #cycles, #eevee, #light, #light-linking, #lighting, #prompt, #shadows --- # P33: CAMERA PATH ANIMATION Tags: #animation #camera #dof #follow #follow-path #prompt #rack #rack-focus #track #track-to # P33: CAMERA PATH ANIMATION **ID:** P33 | **Кластер:** Animation | **Сложность:** ⭐⭐ | **Теги:** #camera #follow-path #track-to #dof #rack-focus --- ## 🎯 ЦЕЛЬ Камера по кривой + Track To на цель + анимация фокуса (rack focus) --- ## 📝 ПРОМПТ (РУС) ``` Анимируй камеру "[CAM]" по кривой "[CURVE]" за [N] кадров, цель "[TARGET]". Rack focus: [OBJ_A] → [OBJ_B] на кадрах [F1-F2]. ``` --- ## 📝 ПРОМПТ (ENG) ``` Animate camera "[CAM]" along curve "[CURVE]" over [N] frames, target "[TARGET]". Rack focus: [OBJ_A] → [OBJ_B] at frames [F1-F2]. ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy def camera_follow_path(cam_name, curve_name, frames=250, target_name=None): cam = bpy.data.objects.get(cam_name) curve = bpy.data.objects.get(curve_name) if not cam or not curve: raise ValueError("Camera or curve not found") c = cam.constraints.new('FOLLOW_PATH') c.target = curve c.use_curve_follow = True c.use_fixed_location = True curve.data.path_duration = frames curve.data.eval_time = 1 curve.data.keyframe_insert("eval_time", frame=1) curve.data.eval_time = frames curve.data.keyframe_insert("eval_time", frame=frames) for fc in curve.animation_data.action.fcurves: for kp in fc.keyframe_points: kp.interpolation = 'LINEAR' if target_name: t = cam.constraints.new('TRACK_TO') t.target = bpy.data.objects.get(target_name) t.track_axis = 'TRACK_NEGATIVE_Z' t.up_axis = 'UP_Y' return {"camera": cam_name, "frames": frames} ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P14: CAMERA & COMPOSITION](14_camera_composition.md), [P43: CAMERA SWITCHING & MARKERS](43_camera_switching.md), [P16: KEYFRAME ANIMATION BASICS](16_keyframe_animation.md), [P18: DRIVERS & CONSTRAINTS](18_drivers_constraints.md), [P19: NLA EDITOR WORKFLOW](19_nla_workflow.md) - **По темам:** [P20: SHAPE KEYS & MORPHING](20_shape_keys.md), [P34: CORRECTIVE SHAPE KEYS](34_corrective_shapes.md), [CASE 04: CHARACTER RIG & ANIMATION](../cases/case_04_character_rig.md) - **Теги:** #animation, #camera, #dof, #follow, #follow-path, #prompt, #rack, #rack-focus, #track, #track-to --- # P34: CORRECTIVE SHAPE KEYS Tags: #animation #corrective #deform #drivers #prompt #rig #shape #shape-keys # P34: CORRECTIVE SHAPE KEYS **ID:** P34 | **Кластер:** Animation | **Сложность:** ⭐⭐⭐ | **Теги:** #corrective #shape-keys #drivers #rig #deform --- ## 🎯 ЦЕЛЬ Коррективы деформаций: shape key + driver от кости (локти, колени, плечи) --- ## 📝 ПРОМПТ (РУС) ``` Создай corrective "[KEY]" на "[MESH]" для кости "[BONE]". Драйвер: rotation → value 0..1 с плавным переходом. ``` --- ## 📝 ПРОМПТ (ENG) ``` Create corrective "[KEY]" on "[MESH]" for bone "[BONE]". Driver: rotation → value 0..1 with smooth mapping. ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy def corrective_key(mesh_name, key_name, rig_name, bone_name, lo=0.5, hi=1.2): mesh = bpy.data.objects.get(mesh_name) if not mesh.data.shape_keys: mesh.shape_key_add(name="Basis", from_mix=False) key = mesh.shape_key_add(name=key_name, from_mix=False) key.slider_min = 0.0 key.slider_max = 1.0 drv = key.driver_add("value").driver drv.type = 'SCRIPTED' var = drv.variables.new() var.name = "r" var.type = 'TRANSFORMS' var.targets[0].id = bpy.data.objects.get(rig_name) var.targets[0].bone_target = bone_name var.targets[0].transform_type = 'ROT_X' var.targets[0].transform_space = 'LOCAL_SPACE' drv.expression = f"min(max((r-({lo}))/(({hi})-({lo})),0),1)" return {"corrective": key_name} ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P20: SHAPE KEYS & MORPHING](20_shape_keys.md), [P18: DRIVERS & CONSTRAINTS](18_drivers_constraints.md), [P14: CAMERA & COMPOSITION](14_camera_composition.md), [P16: KEYFRAME ANIMATION BASICS](16_keyframe_animation.md), [P19: NLA EDITOR WORKFLOW](19_nla_workflow.md) - **По темам:** [P33: CAMERA PATH ANIMATION](33_camera_path.md), [P43: CAMERA SWITCHING & MARKERS](43_camera_switching.md), [CASE 04: CHARACTER RIG & ANIMATION](../cases/case_04_character_rig.md) - **Теги:** #animation, #corrective, #deform, #drivers, #prompt, #rig, #shape, #shape-keys --- # P35: MCP ERROR RECOVERY Tags: #errors #mcp #prompt #retry #scene #scene-check #validation # P35: MCP ERROR RECOVERY **ID:** P35 | **Кластер:** MCP Integration | **Сложность:** ⭐⭐ | **Теги:** #mcp #errors #retry #validation #scene-check --- ## 🎯 ЦЕЛЬ Стандартный протокол восстановления: проверить сцену → упростить → повторить → откатить --- ## 📝 ПРОМПТ (РУС) ``` Команда упала с "[ERROR]". Выполни recovery: 1) get_scene_info, 2) упрости запрос, 3) повтори, 4) при повторе — undo и доложи. ``` --- ## 📝 ПРОМПТ (ENG) ``` Command failed with "[ERROR]". Run recovery: 1) get_scene_info, 2) simplify, 3) retry, 4) on repeat failure — undo and report. ``` --- ## 🔧 MCP ПОСЛЕДОВАТЕЛЬНОСТЬ ```json {"type": "get_scene_info", "params": {}} {"type": "get_viewport_screenshot", "params": {"max_size": 800}} ``` ```python import bpy def safe_run(label, func): bpy.ops.ed.undo_push(message=label) try: out = func() bpy.context.view_layer.update() return {"success": True, "result": out} except Exception as e: bpy.ops.ed.undo() return {"success": False, "error": str(e)} ``` --- ## 📊 ТАБЛИЦА ОШИБОК | Ошибка | Действие | |--------|----------| | Timeout | Разбить на шаги по 1 объекту | | Connection refused | Проверить Start MCP Server | | Object not found | get_scene_info → точное имя | | Non-manifold после boolean | Recalculate normals → повторить | | OOM рендера | Samples/2, resolution/2, retry | --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](26_mcp_basics.md), [P27: POLY HAVEN ASSET PIPELINE](27_polyhaven_pipeline.md), [P28: CUSTOM MCP TOOLS DEVELOPMENT](28_custom_mcp_tools.md), [P01: PRIMITIVE CREATION BASICS](01_geometry_basics.md), [P02: BOOLEAN OPERATIONS](02_boolean_operations.md) - **По темам:** [MCP ADVANCED: STREAMING & TOOL COMPOSITION](../mcp_advanced/streaming_tools.md), [P03: MODIFIERS STACK](03_modifiers_stack.md), [P04: MESH EDITING (BMESH)](04_mesh_editing_bmesh.md) - **Теги:** #errors, #mcp, #prompt, #retry, #scene, #scene-check, #validation --- # P36: ASSET LICENSING (PIZZA/SKETCHFAB/HAVEN) Tags: #assets #cc #cc-by #licensing #lighting #modeling #pipeline #poly #poly-pizza #polyhaven #prompt #sketchfab # P36: ASSET LICENSING (PIZZA/SKETCHFAB/HAVEN) **ID:** P36 | **Кластер:** MCP Integration | **Сложность:** ⭐ | **Теги:** #licensing #poly-pizza #sketchfab #polyhaven #cc0 #cc-by --- ## 🎯 ЦЕЛЬ Правильно фильтровать лицензии: CC0 без атрибуции, CC-BY с кредитом --- ## 📝 ПРОМПТ (РУС) ``` Ищи только [CC0/CC-BY] ассеты "[запрос]". Для CC-BY: запиши автора в custom property и README сцены. ``` --- ## 📝 ПРОМПТ (ENG) ``` Search only [CC0/CC-BY] assets "[query]". For CC-BY: store author in custom property and scene README. ``` --- ## 🔧 MCP ```json {"type": "search_polypizza_models", "params": {"query": "chair", "licence": "CC0", "limit": 20}} {"type": "search_sketchfab_models", "params": {"query": "table", "count": 20, "downloadable": true}} ``` ```python import bpy def stamp_attribution(obj_name, author, source, licence): obj = bpy.data.objects.get(obj_name) obj["attribution_author"] = author obj["attribution_source"] = source obj["attribution_licence"] = licence return {"stamped": obj_name} ``` --- ## 📊 ЛИЦЕНЗИИ | Лицензия | Коммерция | Атрибуция | |----------|-----------|-----------| | CC0 | Да | Нет | | CC-BY | Да | Да | | CC-BY-SA | Да | Да, share-alike | --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P27: POLY HAVEN ASSET PIPELINE](27_polyhaven_pipeline.md), [P13: LIGHTING & HDRI](13_lighting_hdri.md), [P22: PARTICLE SYSTEMS](22_particles.md), [P01: PRIMITIVE CREATION BASICS](01_geometry_basics.md), [P02: BOOLEAN OPERATIONS](02_boolean_operations.md) - **По темам:** [CASE 03: PRODUCT VISUALIZATION](../cases/case_03_product_viz.md), [CASE 05: PROCEDURAL CITY](../cases/case_05_procedural_city.md), [P03: MODIFIERS STACK](03_modifiers_stack.md) - **Теги:** #assets, #cc, #cc-by, #licensing, #lighting, #modeling, #pipeline, #poly, #poly-pizza, #polyhaven --- # P37: SEAMLESS TEXTURE WORKFLOW Tags: #dream #dream-textures #inpaint #prompt #seamless #texturing #tiling #uv # P37: SEAMLESS TEXTURE WORKFLOW **ID:** P37 | **Кластер:** Materials | **Сложность:** ⭐⭐ | **Теги:** #seamless #tiling #inpaint #dream-textures #uv --- ## 🎯 ЦЕЛЬ Бесшовные тайлящиеся текстуры: генерация + проверка стыков + фикс inpaint --- ## 📝 ПРОМПТ (РУС) ``` Сгенерируй seamless "[материал]" [res], проверь стыки (offset 50%), замаскируй швы → inpaint. Примени с repeat [N]. ``` --- ## 📝 ПРОМПТ (ENG) ``` Generate seamless "[material]" [res], check seams (offset 50%), mask seams → inpaint. Apply with repeat [N]. ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy def tile_material(obj_name, scale=4.0): obj = bpy.data.objects.get(obj_name) mat = obj.active_material nodes = mat.node_tree.nodes mapping = next((n for n in nodes if n.type == 'MAPPING'), None) if not mapping: tex_coord = nodes.new('ShaderNodeTexCoord') mapping = nodes.new('ShaderNodeMapping') mat.node_tree.links.new(tex_coord.outputs['UV'], mapping.inputs['Vector']) for n in nodes: if n.type == 'TEX_IMAGE' and not n.inputs['Vector'].links: mat.node_tree.links.new(mapping.outputs['Vector'], n.inputs['Vector']) mapping.inputs['Scale'].default_value = (scale, scale, scale) return {"tiled": scale} ``` --- ## 📊 ПРОВЕРКА ШВОВ 1. Image Editor → View → Repeat 2. Или UV offset 0.5 → швы в центре 3. Inpaint masked seams (Dream Textures / A1111) --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P05: UV UNWRAPPING STRATEGIES](05_uv_unwrapping.md), [P46: UDIM WORKFLOW](46_udim_workflow.md), [P12: TEXTURE PAINTING](12_texture_painting.md), [P19: NLA EDITOR WORKFLOW](19_nla_workflow.md), [P01: PRIMITIVE CREATION BASICS](01_geometry_basics.md) - **По темам:** [CASE 03: PRODUCT VISUALIZATION](../cases/case_03_product_viz.md), [P02: BOOLEAN OPERATIONS](02_boolean_operations.md), [P03: MODIFIERS STACK](03_modifiers_stack.md) - **Теги:** #dream, #dream-textures, #inpaint, #prompt, #seamless, #texturing, #tiling, #uv --- # P39: EXPORT PRESETS (GLB/FBX/USD) Tags: #export #fbx #gltf #godot #pipeline #prompt #unity #unreal #usd #web # P39: EXPORT PRESETS (GLB/FBX/USD) **ID:** P39 | **Кластер:** Pipeline | **Сложность:** ⭐⭐ | **Теги:** #export #gltf #fbx #usd #unity #unreal #godot --- ## 🎯 ЦЕЛЬ Экспорт-пресеты под движки: GLB (Godot/Web), FBX (Unity/Unreal), USD (pipeline) --- ## 📝 ПРОМПТ (РУС) ``` Экспортируй "[OBJECTS]" в [GLB/FBX/USD] пресет [Godot/Unity/Unreal/Pipeline] в //exports/. Проверь масштаб и forward/up. ``` --- ## 📝 ПРОМПТ (ENG) ``` Export "[OBJECTS]" to [GLB/FBX/USD] preset [Godot/Unity/Unreal/Pipeline] to //exports/. Verify scale and forward/up. ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy def export_preset(objects, fmt='GLB', target='Godot', outdir="//exports/"): bpy.ops.object.select_all(action='DESELECT') for o in objects: bpy.data.objects.get(o).select_set(True) if fmt == 'GLB': bpy.ops.export_scene.gltf( filepath=f"{outdir}{objects[0]}.glb", export_format='GLB', use_selection=True, export_apply=True, export_yup=True, export_materials='EXPORT', export_animations=True) elif fmt == 'FBX': bpy.ops.export_scene.fbx( filepath=f"{outdir}{objects[0]}.fbx", use_selection=True, apply_unit_scale=True, apply_scale_options='FBX_SCALE_ALL', bake_space_transform=(target == 'Unity'), object_types={'ARMATURE', 'MESH'}, primary_bone_axis='Y_FORWARD', secondary_bone_axis='X_UP', bake_anim=True, bake_anim_use_nla_strips=True) elif fmt == 'USD': bpy.ops.wm.usd_export( filepath=f"{outdir}{objects[0]}.usda", selected_objects_only=True, export_materials=True, material_x=True, export_animation=True) return {"exported": fmt, "target": target} ``` --- ## 📊 ПРЕСЕТЫ | Target | Format | Scale | |--------|--------|-------| | Godot/Web | GLB | Meters, applied | | Unity | FBX | Meters, applied, bake transform | | Unreal | FBX | cm via unit scale | | Pipeline | USD + MaterialX | Meters | --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P22: PARTICLE SYSTEMS](22_particles.md), [P27: POLY HAVEN ASSET PIPELINE](27_polyhaven_pipeline.md), [P36: ASSET LICENSING (PIZZA/SKETCHFAB/HAVEN)](36_asset_licensing.md), [P38: HIGH→LOW BAKE PIPELINE](38_bake_pipeline.md), [P44: MOCAP RETARGETING](44_mocap_retarget.md) - **По темам:** [CASE 07: STYLIZED BARREL (HAND-PAINT)](../cases/case_07_stylized_barrel.md), [INTEGRATIONS: COMFYUI, UNREAL, USD, TOUCHDESIGNER](../integrations/comfyui_unreal_usd.md), [P46: UDIM WORKFLOW](46_udim_workflow.md) - **Теги:** #export, #fbx, #gltf, #godot, #pipeline, #prompt, #unity, #unreal, #usd, #web --- # P40: VERSION CONTROL & RENDER JOBS Tags: #backup #ci #flamenco #git #lfs #prompt #render #versioning # P40: VERSION CONTROL & RENDER JOBS **ID:** P40 | **Кластер:** Pipeline | **Сложность:** ⭐⭐ | **Теги:** #git #lfs #versioning #flamenco #ci #backup --- ## 🎯 ЦЕЛЬ Версионирование .blend (Git LFS + lock) и отправка рендер-джобов (Flamenco/CLI) --- ## 📝 ПРОМПТ (РУС) ``` Настрой версионирование: LFS track [маски], lock [мейн-сцены]. Отправь джобу: frames [A-B], samples [N], output [путь]. ``` --- ## 📝 ПРОМПТ (ENG) ``` Setup versioning: LFS track [masks], lock [main scenes]. Submit job: frames [A-B], samples [N], output [path]. ``` --- ## 🔧 КОМАНДЫ ```bash # .gitattributes # *.blend filter=lfs diff=lfs merge=lfs -text # *.exr filter=lfs diff=lfs merge=lfs -text # *.fbx filter=lfs diff=lfs merge=lfs -text # *.png filter=lfs diff=lfs merge=lfs -text ``` ```python # save versioned copy import bpy, datetime v = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") bpy.ops.wm.save_as_mainfile(filepath=f"//versions/scene_v{v}.blend") ``` ```bash # CLI render job # blender -b scenes/main.blend -E CYCLES -o //renders/anim_#### -s 1 -e 250 -a ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P49: RENDER FARM SUBMIT](49_farm_submit.md), [P14: CAMERA & COMPOSITION](14_camera_composition.md), [P22: PARTICLE SYSTEMS](22_particles.md), [P29: BATCH PROCESSING & CLI](29_batch_cli.md), [P30: OPTIMIZATION (SCENE & RENDER)](30_optimization.md) - **По темам:** [P31: RENDER PASSES & AOV](31_render_passes_aov.md), [P41: DISPLACEMENT & MICRO-DETAIL](41_displacement_micro.md), [P43: CAMERA SWITCHING & MARKERS](43_camera_switching.md) - **Теги:** #backup, #ci, #flamenco, #git, #lfs, #prompt, #render, #versioning --- # P41: DISPLACEMENT & MICRO-DETAIL Tags: #adaptive #adaptive-subdivision #cycles #displacement #micro #micro-detail #prompt #render #shading #true #true-displacement # P41: DISPLACEMENT & MICRO-DETAIL **ID:** P41 | **Кластер:** Materials/Render | **Сложность:** ⭐⭐⭐ | **Теги:** #displacement #adaptive-subdivision #micro-detail #true-displacement #cycles --- ## 🎯 ЦЕЛЬ Настоящий дисплейсмент: Adaptive Subdivision + 32-bit height + правильные настройки Cycles --- ## 📝 ПРОМПТ (РУС) ``` Настрой true displacement для "[OBJECT]": Height map [путь], scale [S], midlevel [M]. Adaptive Subdivision: dicing rate [R]. Проверь силуэт на крупном плане. ``` --- ## 📝 ПРОМПТ (ENG) ``` Setup true displacement for "[OBJECT]": Height map [path], scale [S], midlevel [M]. Adaptive Subdivision: dicing rate [R]. Verify silhouette in close-up. ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy def true_displacement(obj_name, height_img_path, scale=0.1, midlevel=0.5, dicing=1.0): obj = bpy.data.objects.get(obj_name) if not obj or obj.type != 'MESH': raise ValueError("Mesh object required") mat = obj.active_material if not mat: raise ValueError("Object has no material") mat.cycles.displacement_method = 'BOTH' nodes = mat.node_tree.nodes links = mat.node_tree.links tex = nodes.new('ShaderNodeTexImage') tex.image = bpy.data.images.load(height_img_path) tex.image.colorspace_settings.name = 'Non-Color' tex.location = (-400, -300) disp = nodes.new('ShaderNodeDisplacement') disp.location = (-100, -300) disp.inputs['Scale'].default_value = scale disp.inputs['Midlevel'].default_value = midlevel out = next(n for n in nodes if n.type == 'OUTPUT_MATERIAL') links.new(tex.outputs['Color'], disp.inputs['Height']) links.new(disp.outputs['Displacement'], out.inputs['Displacement']) sub = obj.modifiers.new("AdaptiveSubdiv", 'SUBSURF') sub.subdivision_type = 'CATMULL_CLARK' sub.use_adaptive_subdiv = True s = bpy.context.scene s.cycles.feature_set = 'EXPERIMENTAL' s.cycles.preview_dicing_rate = dicing s.cycles.dicing_rate = max(0.5, dicing / 2) return {"displacement": True, "scale": scale} # true_displacement("Ground", "//textures/ground_height_4k.exr", scale=0.15) ``` --- ## 📊 ПАРАМЕТРЫ | Параметр | Значение | Зачем | |----------|----------|-------| | Height map | 32-bit EXR | Без бандинга на плавных склонах | | Scale | 0.05–0.3 | Реальная высота рельефа в метрах | | Midlevel | 0.5 | Точка «ноль» (ниже — впадины, выше — выступы) | | Dicing rate | 1.0 preview / 0.5 render | Детализация сабдива | | Feature set | EXPERIMENTAL | Обязателен для adaptive | --- ## ⚠️ ОШИБКИ | Ошибка | Решение | |--------|---------| | Плоский силуэт | Displacement method = Both/True, а не Bump Only | | Взрыв геометрии | Scale слишком большой; проверь единицы midlevel | | Шум/артефакты | Height в Non-Color, 32-bit, dicing меньше | --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P48: EEVEE VS CYCLES DECISION](48_eevee_vs_cycles.md), [P09: PRINCIPLED BSDF SETUP](09_principled_bsdf.md), [P10: NODE GROUP CREATION](10_node_groups.md), [P11: PROCEDURAL TEXTURES](11_procedural_textures.md), [P14: CAMERA & COMPOSITION](14_camera_composition.md) - **По темам:** [CASE 03: PRODUCT VISUALIZATION](../cases/case_03_product_viz.md), [P22: PARTICLE SYSTEMS](22_particles.md), [P29: BATCH PROCESSING & CLI](29_batch_cli.md) - **Теги:** #adaptive, #adaptive-subdivision, #cycles, #displacement, #micro, #micro-detail, #prompt, #render, #shading, #true --- # P42: LIGHT PORTALS & CAUSTICS Tags: #caustics #cycles #daylight #glass #interior #lighting #portals #prompt # P42: LIGHT PORTALS & CAUSTICS **ID:** P42 | **Кластер:** Lighting | **Сложность:** ⭐⭐⭐ | **Теги:** #portals #caustics #interior #daylight #cycles #glass --- ## 🎯 ЦЕЛЬ Дневной интерьер без шума: порталы на окнах + каустика для стекла/воды --- ## 📝 ПРОМПТ (РУС) ``` Поставь порталы на все окна "[ROOM]", включи каустику (reflective/refractive), samples [N]. Проверь шум в углах. ``` --- ## 📝 ПРОМПТ (ENG) ``` Place portals on all windows of "[ROOM]", enable caustics (reflective/refractive), samples [N]. Check noise in corners. ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy def add_portals(window_objs, portal_size_pad=0.1): made = [] for wname in window_objs: w = bpy.data.objects.get(wname) if not w: continue dims = w.dimensions bpy.ops.mesh.primitive_plane_add(size=1) p = bpy.context.active_object p.name = f"Portal_{wname}" p.dimensions = (dims.x + portal_size_pad, dims.y + portal_size_pad, 0.01) p.location = w.location p.rotation_euler = w.rotation_euler # Портал — это Area light с portal enabled ld = bpy.data.lights.new(f"Portal_{wname}", 'AREA') p.data = ld ld.use_portal = True ld.energy = 0.0 made.append(p.name) return {"portals": made} def enable_caustics(samples=256): s = bpy.context.scene s.cycles.caustics_reflective = True s.cycles.caustics_refractive = True s.cycles.samples = samples s.cycles.use_denoising = True s.cycles.glossy_bounces = max(s.cycles.glossy_bounces, 8) s.cycles.transmission_bounces = max(s.cycles.transmission_bounces, 12) return {"caustics": True, "samples": samples} # add_portals(["Window_01", "Window_02"]) # enable_caustics(256) ``` --- ## 📊 КОГДА НУЖНО | Кейс | Порталы | Каустика | |------|---------|----------| | Дневной интерьер | Да (3–5x меньше шума) | Нет | | Бассейн/ванна | Нет | Да (refractive) | | Хром/зеркала | Нет | Да (reflective) | | Ночь/студия | Нет | Нет | --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P32: LIGHT LINKING & SHADOWS](32_light_linking.md), [P13: LIGHTING & HDRI](13_lighting_hdri.md), [P14: CAMERA & COMPOSITION](14_camera_composition.md), [P36: ASSET LICENSING (PIZZA/SKETCHFAB/HAVEN)](36_asset_licensing.md), [P41: DISPLACEMENT & MICRO-DETAIL](41_displacement_micro.md) - **По темам:** [P48: EEVEE VS CYCLES DECISION](48_eevee_vs_cycles.md), [CASE 03: PRODUCT VISUALIZATION](../cases/case_03_product_viz.md), [P01: PRIMITIVE CREATION BASICS](01_geometry_basics.md) - **Теги:** #caustics, #cycles, #daylight, #glass, #interior, #lighting, #portals, #prompt --- # P43: CAMERA SWITCHING & MARKERS Tags: #animation #bindings #camera #markers #modeling #multi #multi-cam #prompt #render #sequencer # P43: CAMERA SWITCHING & MARKERS **ID:** P43 | **Кластер:** Animation/Render | **Сложность:** ⭐⭐ | **Теги:** #camera #markers #bindings #sequencer #multi-cam --- ## 🎯 ЦЕЛЬ Несколько камер в одной сцене: маркеры, привязки, переключение для монтажа --- ## 📝 ПРОМПТ (РУС) ``` Настрой [N] камер: [имена] на кадрах [F1, F2...]. Привяжи маркеры (Ctrl+B), проверь переключение в viewport. ``` --- ## 📝 ПРОМПТ (ENG) ``` Setup [N] cameras: [names] at frames [F1, F2...]. Bind markers (Ctrl+B), verify switching in viewport. ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy def bind_cameras(bindings): """bindings: {frame: camera_name}""" s = bpy.context.scene for frame, cam_name in sorted(bindings.items()): cam = bpy.data.objects.get(cam_name) if not cam or cam.type != 'CAMERA': raise ValueError(f"Camera {cam_name} not found") marker = s.timeline_markers.new(f"Cam_{cam_name}", frame=frame) marker.camera_data = cam.data s.frame_start = min(bindings) return {"bound": len(bindings)} def camera_from_view(cam_name): bpy.ops.object.camera_add() cam = bpy.context.active_object cam.name = cam_name bpy.context.scene.camera = cam bpy.ops.view3d.camera_to_view() return {"camera": cam_name} # bind_cameras({1: "Cam_Wide", 60: "Cam_Medium", 120: "Cam_Close"}) ``` --- ## 📊 ПРИЁМЫ | Приём | Как | |-------|-----| | Жёсткая склейка | Маркеры на соседних кадрах | | Перебивка | Отдельная камера деталей на 10–15 кадров | | Облёт + склейка | Path-камера (см. P33), склейки маркерами | --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P14: CAMERA & COMPOSITION](14_camera_composition.md), [P18: DRIVERS & CONSTRAINTS](18_drivers_constraints.md), [P19: NLA EDITOR WORKFLOW](19_nla_workflow.md), [P22: PARTICLE SYSTEMS](22_particles.md), [P33: CAMERA PATH ANIMATION](33_camera_path.md) - **По темам:** [CASE 05: PROCEDURAL CITY](../cases/case_05_procedural_city.md), [CASE 13: HARD-SURFACE SCI-FI GUN](../cases/case_13_hard_surface_gun.md), [P01: PRIMITIVE CREATION BASICS](01_geometry_basics.md) - **Теги:** #animation, #bindings, #camera, #markers, #modeling, #multi, #multi-cam, #prompt, #render, #sequencer --- # P44: MOCAP RETARGETING Tags: #curves #fbx #mocap #prompt #retarget #rigify #rokoko # P44: MOCAP RETARGETING **ID:** P44 | **Кластер:** Animation | **Сложность:** ⭐⭐⭐ | **Теги:** #mocap #retarget #rokoko #fbx #rigify #curves --- ## 🎯 ЦЕЛЬ Перенести мокап (Rokoko/MoveAI FBX) на Rigify-риг: ретаргет, foot lock, чистка кривых, луп --- ## 📝 ПРОМПТ (РУС) ``` Ретаргеть мокап "[MOCAP_FBX]" на риг "[RIG]": маппинг костей, foot lock, despike, bake в action "[NAME]". ``` --- ## 📝 ПРОМПТ (ENG) ``` Retarget mocap "[MOCAP_FBX]" onto rig "[RIG]": bone mapping, foot lock, despike, bake to action "[NAME]". ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy BONE_MAP = { "Hips": "torso", "Spine": "spine_fk", "LeftUpLeg": "thigh_fk.L", "LeftLeg": "shin_fk.L", "LeftFoot": "foot_fk.L", "RightUpLeg": "thigh_fk.R", "RightLeg": "shin_fk.R", "RightFoot": "foot_fk.R", "LeftArm": "upper_arm_fk.L", "LeftForeArm": "forearm_fk.L", "LeftHand": "hand_fk.L", "RightArm": "upper_arm_fk.R", "RightForeArm": "forearm_fk.R", "RightHand": "hand_fk.R", "Head": "head", } def retarget_action(mocap_rig, target_rig, action_name="Mocap_Retarget"): src = bpy.data.objects.get(mocap_rig) dst = bpy.data.objects.get(target_rig) if not src or not dst: raise ValueError("Rigs not found") # Копируем world-матрицы костей покадрово через constraints (bake позже) bpy.ops.object.select_all(action='DESELECT') dst.select_set(True) bpy.context.view_layer.objects.active = dst bpy.ops.object.mode_set(mode='POSE') for sbone, dbone in BONE_MAP.items(): if sbone not in src.pose.bones or dbone not in dst.pose.bones: continue c = dst.pose.bones[dbone].constraints.new('COPY_ROTATION') c.target = src c.subtarget = sbone c.target_space = 'WORLD' c.owner_space = 'WORLD' bpy.ops.object.mode_set(mode='OBJECT') return {"mapped": len(BONE_MAP), "next": "bake action, удали constraints"} def despike_action(action_name, threshold=0.5): act = bpy.data.actions.get(action_name) if not act: raise ValueError("Action not found") cleaned = 0 for fc in act.fcurves: pts = fc.keyframe_points for i in range(1, len(pts) - 1): v0, v1, v2 = pts[i-1].co.y, pts[i].co.y, pts[i+1].co.y if abs(v1 - (v0 + v2) / 2) > threshold: pts[i].co.y = (v0 + v2) / 2 cleaned += 1 return {"despiked": cleaned} ``` --- ## 📊 ЧЕК-ЛИСТ - [ ] Масштаб мокапа = масштаб рига (иначе ноги «плывут») - [ ] Foot lock: IK-контроллеры стоп на контактных кадрах - [ ] Despike перед bake - [ ] Loop: первый кадр = последний +1 --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P17: RIGIFY HUMANOID SETUP](17_rigify_setup.md), [P39: EXPORT PRESETS (GLB/FBX/USD)](39_export_presets.md), [P01: PRIMITIVE CREATION BASICS](01_geometry_basics.md), [P02: BOOLEAN OPERATIONS](02_boolean_operations.md), [P03: MODIFIERS STACK](03_modifiers_stack.md) - **По темам:** [P04: MESH EDITING (BMESH)](04_mesh_editing_bmesh.md), [P05: UV UNWRAPPING STRATEGIES](05_uv_unwrapping.md), [P06: RETOPOLOGY WORKFLOW](06_retopology_workflow.md) - **Теги:** #curves, #fbx, #mocap, #prompt, #retarget, #rigify, #rokoko --- # P46: UDIM WORKFLOW Tags: #baking #blender #mari #pipeline #prompt #shading #texturing #udim #uv #uv-tiles # P46: UDIM WORKFLOW **ID:** P46 | **Кластер:** Materials | **Сложность:** ⭐⭐ | **Теги:** #udim #uv-tiles #mari #texturing #baking --- ## 🎯 ЦЕЛЬ UDIM-тайлы: раскладка 1001–10xx, нейминг, bake/export по тайлам --- ## 📝 ПРОМПТ (РУС) ``` Разложи UV "[OBJECT]" по UDIM: [назначение тайлов], нейминг `{asset}_{map}_.png`. Проверь перекрытия. ``` --- ## 📝 ПРОМПТ (ENG) ``` Lay out UV of "[OBJECT]" across UDIMs: [tile assignment], naming `{asset}_{map}_.png`. Check overlaps. ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy def udim_report(obj_name): obj = bpy.data.objects.get(obj_name) mesh = obj.data tiles = {} for poly in mesh.polygons: for li in poly.loop_indices: uv = mesh.uv_layers.active.data[li].uv tile = 1001 + int(uv.x) + int(uv.y) * 10 tiles.setdefault(tile, 0) tiles[tile] += 1 return {"tiles": sorted(tiles), "loops": sum(tiles.values())} def udim_fill_template(obj_name, tile=1001, color=(0.5, 0.5, 0.5, 1.0), res=2048): img = bpy.data.images.new(f"{obj_name}_{tile}", res, res, alpha=True) img.source = 'TILED' # tile_number: Blender API хранит номер тайла; создаём через оператор bpy.context.view_layer.objects.active = bpy.data.objects.get(obj_name) return {"tile_image": img.name} # udim_report("Hero_Body") ``` --- ## 📊 РАСКЛАДКА (пример персонаж) | UDIM | Часть | Res | |------|-------|-----| | 1001 | Голова/шея | 4k | | 1002 | Торс/руки | 2k | | 1003 | Ноги | 2k | | 1004 | Кисти/ступни/мелочи | 2k | --- ## 📊 НЕЙМИНГ `{asset}_{map}_{udim}.{ext}` → `hero_basecolor_1001.png`. `` плейсхолдер понимают Substance/Mari/Blender. --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P05: UV UNWRAPPING STRATEGIES](05_uv_unwrapping.md), [P12: TEXTURE PAINTING](12_texture_painting.md), [P37: SEAMLESS TEXTURE WORKFLOW](37_seamless_textures.md), [P38: HIGH→LOW BAKE PIPELINE](38_bake_pipeline.md), [P09: PRINCIPLED BSDF SETUP](09_principled_bsdf.md) - **По темам:** [CASE 03: PRODUCT VISUALIZATION](../cases/case_03_product_viz.md), [WF 17: STOP-MOTION FELT LOOK](../workflows/wf_17_stop_motion_felt.md), [P10: NODE GROUP CREATION](10_node_groups.md) - **Теги:** #baking, #blender, #mari, #pipeline, #prompt, #shading, #texturing, #udim, #uv, #uv-tiles --- # P47: ALEMBIC CACHE PIPELINE Tags: #alembic #cache #pipeline #prompt #render #render-farm #simulation # P47: ALEMBIC CACHE PIPELINE **ID:** P47 | **Кластер:** Pipeline | **Сложность:** ⭐⭐ | **Теги:** #alembic #cache #abc #simulation #render-farm #pipeline --- ## 🎯 ЦЕЛЬ Кэширование анимации/симуляций в Alembic: экспорт, velocity blur, импорт на ферме --- ## 📝 ПРОМПТ (РУС) ``` Запеки Alembic "[OBJECTS]" кадры [A-B]: flatten, subsamples [N], UV/normals ON. Проверь размер файла и motion blur. ``` --- ## 📝 ПРОМПТ (ENG) ``` Bake Alembic "[OBJECTS]" frames [A-B]: flatten, subsamples [N], UVs/normals ON. Verify file size and motion blur. ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy def export_alembic(objects, filepath, start=1, end=250, subsamples=4): bpy.ops.object.select_all(action='DESELECT') for o in objects: bpy.data.objects.get(o).select_set(True) bpy.ops.wm.alembic_export( filepath=filepath, selected=True, start=start, end=end, flatten=True, subsamples=subsamples, write_uvs=True, write_normals=True, write_vertex_colors=True, export_subdivision=True) import os size_mb = os.path.getsize(filepath) / 1024 / 1024 return {"alembic": filepath, "mb": round(size_mb, 1)} def import_alembic(filepath, as_background_job=False): bpy.ops.wm.alembic_import( filepath=filepath, as_background_job=as_background_job) return {"imported": filepath} # export_alembic(["Cloth_Shirt", "Hero_Body"], "//cache/shot_010.abc", 1, 120) ``` --- ## 📊 ПАРАМЕТРЫ | Параметр | Значение | |----------|----------| | Flatten | ON (чистая иерархия для фермы) | | Subsamples | 4 (motion blur), 1 (статика) | | UVs/Normals | ON (иначе shading сломан) | | Velocities | ON для motion blur | --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P22: PARTICLE SYSTEMS](22_particles.md), [P21: PHYSICS SIMULATION](21_physics_simulation.md), [P29: BATCH PROCESSING & CLI](29_batch_cli.md), [P49: RENDER FARM SUBMIT](49_farm_submit.md), [P14: CAMERA & COMPOSITION](14_camera_composition.md) - **По темам:** [CASE 03: PRODUCT VISUALIZATION](../cases/case_03_product_viz.md), [CASE 05: PROCEDURAL CITY](../cases/case_05_procedural_city.md), [CASE 07: STYLIZED BARREL (HAND-PAINT)](../cases/case_07_stylized_barrel.md), [CASE 08: SCI-FI CORRIDOR MODULAR](../cases/case_08_sci_fi_corridor.md) - **Теги:** #alembic, #cache, #pipeline, #prompt, #render, #render-farm, #simulation --- # P48: EEVEE VS CYCLES DECISION Tags: #cycles #decision #eevee #performance #presets #prompt #render #render-engine # P48: EEVEE VS CYCLES DECISION **ID:** P48 | **Кластер:** Rendering | **Сложность:** ⭐⭐ | **Теги:** #eevee #cycles #render-engine #decision #presets #performance --- ## 🎯 ЦЕЛЬ Выбрать движок под задачу: матрица решений + готовые пресеты настроек --- ## 📝 ПРОМПТ (РУС) ``` Выбери движок для "[задача]": [archviz/product/animation/stylized/vr]. Примени пресет, объясни 3 причины выбора. ``` --- ## 📝 ПРОМПТ (ENG) ``` Pick engine for "[task]": [archviz/product/animation/stylized/vr]. Apply preset, give 3 reasons. ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy def apply_engine_preset(preset='cycles_preview'): s = bpy.context.scene if preset == 'cycles_preview': s.render.engine = 'CYCLES' s.cycles.samples = 64 s.cycles.use_denoising = True s.cycles.use_adaptive_sampling = True elif preset == 'cycles_final': s.render.engine = 'CYCLES' s.cycles.samples = 256 s.cycles.use_denoising = True s.cycles.denoiser = 'OPTIX' s.cycles.use_adaptive_sampling = True s.cycles.adaptive_threshold = 0.01 elif preset == 'eevee_preview': s.render.engine = 'BLENDER_EEVEE_NEXT' s.eevee.taa_render_samples = 16 elif preset == 'eevee_final': s.render.engine = 'BLENDER_EEVEE_NEXT' s.eevee.taa_render_samples = 64 s.eevee.use_bloom = True s.eevee.use_ssr = True s.eevee.use_ssr_refraction = True return {"preset": preset, "engine": s.render.engine} # apply_engine_preset('cycles_final') ``` --- ## 📊 МАТРИЦА | Задача | Движок | Почему | |--------|--------|--------| | ArchViz фотореализм | Cycles | GI, каустика, дисплейсмент | | Product hero | Cycles | Точные отражения/преломления | | Анимация сериал | EEVEE Next | Скорость, стабильный вид | | Стилизация/NPR | EEVEE Next | Shader to RGB, lineart | | VR превью | EEVEE Next | FPS | | Текстуры bake | Cycles | Корректный бейк | --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P32: LIGHT LINKING & SHADOWS](32_light_linking.md), [P41: DISPLACEMENT & MICRO-DETAIL](41_displacement_micro.md), [P14: CAMERA & COMPOSITION](14_camera_composition.md), [P22: PARTICLE SYSTEMS](22_particles.md), [P29: BATCH PROCESSING & CLI](29_batch_cli.md) - **По темам:** [P30: OPTIMIZATION (SCENE & RENDER)](30_optimization.md), [P31: RENDER PASSES & AOV](31_render_passes_aov.md), [P40: VERSION CONTROL & RENDER JOBS](40_version_control.md) - **Теги:** #cycles, #decision, #eevee, #performance, #presets, #prompt, #render, #render-engine --- # P49: RENDER FARM SUBMIT Tags: #chunks #flamenco #jobs #monitor #prompt #render #render-farm # P49: RENDER FARM SUBMIT **ID:** P49 | **Кластер:** Pipeline | **Сложность:** ⭐⭐ | **Теги:** #render-farm #flamenco #jobs #chunks #monitor --- ## 🎯 ЦЕЛЬ Отправка джобы на ферму: чанки кадров, приоритеты, мониторинг, ретрай упавших --- ## 📝 ПРОМПТ (РУС) ``` Отправь джобу "[SCENE]": кадры [A-B] чанками по [N], samples [S], output [путь], приоритет [P]. Мониторь до конца. ``` --- ## 📝 ПРОМПТ (ENG) ``` Submit job "[SCENE]": frames [A-B] in chunks of [N], samples [S], output [path], priority [P]. Monitor to completion. ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy def prepare_farm_scene(samples=128, out="//renders/anim_####", fmt='OPEN_EXR_MULTILAYER'): s = bpy.context.scene s.render.engine = 'CYCLES' s.cycles.samples = samples s.cycles.use_denoising = True s.cycles.use_adaptive_sampling = True s.render.filepath = out s.render.image_settings.file_format = fmt s.render.image_settings.color_mode = 'RGBA' s.render.image_settings.color_depth = '16' # Все текстуры — packed или относительные пути for img in bpy.data.images: if img.source == 'FILE' and img.filepath.startswith('/'): img.filepath = '//textures/' + img.name bpy.ops.file.pack_all() bpy.ops.wm.save_mainfile() return {"prepared": True, "samples": samples} def chunk_frames(start, end, chunk=25): ranges = [] f = start while f <= end: ranges.append((f, min(f + chunk - 1, end))) f += chunk return {"chunks": ranges} # prepare_farm_scene(128) # chunk_frames(1, 250, 25) ``` --- ## 📊 ЧЕК-ЛИСТ ДЖОБЫ - [ ] Текстуры packed / относительные пути - [ ] Кэш симуляций запечён (не live) - [ ] Output путь существует на всех воркерах - [ ] Тестовый чанк 3 кадра прошёл - [ ] Ретрай только упавших чанков --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P29: BATCH PROCESSING & CLI](29_batch_cli.md), [P40: VERSION CONTROL & RENDER JOBS](40_version_control.md), [P47: ALEMBIC CACHE PIPELINE](47_alembic_cache.md), [P14: CAMERA & COMPOSITION](14_camera_composition.md), [P22: PARTICLE SYSTEMS](22_particles.md) - **По темам:** [P30: OPTIMIZATION (SCENE & RENDER)](30_optimization.md), [P31: RENDER PASSES & AOV](31_render_passes_aov.md), [P41: DISPLACEMENT & MICRO-DETAIL](41_displacement_micro.md) - **Теги:** #chunks, #flamenco, #jobs, #monitor, #prompt, #render, #render-farm --- # P50: PORTFOLIO TURNTABLE & QA Tags: #checklist #portfolio #presentation #product #prompt #qa #turntable # P50: PORTFOLIO TURNTABLE & QA **ID:** P50 | **Кластер:** Pipeline/Presentation | **Сложность:** ⭐ | **Теги:** #turntable #portfolio #qa #checklist #presentation --- ## 🎯 ЦЕЛЬ Финальная подача ассета: тёрнтейбл-риг, lighting ladder, QA-чеклист, экспорт превью --- ## 📝 ПРОМПТ (РУС) ``` Собери тёрнтейбл для "[ASSET]": 120 кадров, 3 ракурса света, wireframe+shaded+clay пассы. Прогони QA-чеклист, экспортируй MP4+PNG. ``` --- ## 📝 ПРОМПТ (ENG) ``` Build turntable for "[ASSET]": 120 frames, 3 light angles, wireframe+shaded+clay passes. Run QA checklist, export MP4+PNG. ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy import math def turntable_rig(target_name, radius=3.0, height=1.2, frames=120, lens=85): tgt = bpy.data.objects.get(target_name) if not tgt: raise ValueError("Target not found") # Пустышка-центр на объекте bpy.ops.object.empty_add(location=tgt.location) pivot = bpy.context.active_object pivot.name = f"Turntable_{target_name}" # Камера bpy.ops.object.camera_add(location=(radius, 0, height)) cam = bpy.context.active_object cam.name = f"Cam_Turntable_{target_name}" cam.data.lens = lens t = cam.constraints.new('TRACK_TO') t.target = pivot t.track_axis = 'TRACK_NEGATIVE_Z' t.up_axis = 'UP_Y' bpy.context.scene.camera = cam # Вращение пивота 360° за frames pivot.rotation_euler = (0, 0, 0) pivot.keyframe_insert("rotation_euler", frame=1) pivot.rotation_euler = (0, 0, math.radians(360)) pivot.keyframe_insert("rotation_euler", frame=frames) for fc in pivot.animation_data.action.fcurves: for kp in fc.keyframe_points: kp.interpolation = 'LINEAR' s = bpy.context.scene s.frame_start = 1 s.frame_end = frames return {"turntable": pivot.name, "camera": cam.name, "frames": frames} # turntable_rig("Hero", radius=2.5, frames=120) ``` --- ## 📊 QA-ЧЕКЛИСТ (финальный) - [ ] Scale applied, origin внизу/центре, имя по конвенции - [ ] Non-manifold = 0, UV без перекрытий, нормали наружу - [ ] Текстуры: BaseColor sRGB, остальные Non-Color, именование `{asset}_{map}_{res}` - [ ] LOD0/1/2 на месте, трисы в бюджете - [ ] Тёрнтейбл: shaded + clay + wireframe, 120f, MP4 - [ ] GLB/FBX открываются в чистом Blender --- ## 🔗 СВЯЗИ - **Прочие в категории «Промпты»:** [P01: PRIMITIVE CREATION BASICS](01_geometry_basics.md), [P02: BOOLEAN OPERATIONS](02_boolean_operations.md), [P03: MODIFIERS STACK](03_modifiers_stack.md), [P04: MESH EDITING (BMESH)](04_mesh_editing_bmesh.md), [P05: UV UNWRAPPING STRATEGIES](05_uv_unwrapping.md) - **По темам:** [CASE 10: SNEAKER PRODUCT HERO](../cases/case_10_sneaker_product.md), [CASE 14: FOOD RENDER (RAMEN)](../cases/case_14_food_r visualisation.md), [CASE 15: CAR STUDIO CINEMATIC](../cases/case_15_car_studio_cinematic.md) - **Теги:** #checklist, #portfolio, #presentation, #product, #prompt, #qa, #turntable --- # АДДОН BLENDER-MCP — ПОДРОБНЫЙ РАЗБОР Tags: #addon #blender #mcp #reference # АДДОН BLENDER-MCP — ПОДРОБНЫЙ РАЗБОР ## Обзор **Репозиторий:** https://github.com/ahujasid/blender-mcp **Версия:** Latest | **Blender:** 3.0+ | **Лицензия:** MIT **Звёзды:** 28.3k | **PyPI:** `blender-mcp` blender-mcp — это **референсная реализация MCP-сервера** для Blender. Он работает как отдельный Python-процесс (через `uvx`) и подключается к лёгкому аддону Blender через TCP-сокет (порт 9876). --- ## Архитектура ``` ┌────────────────────────────────────────────────────────────┐ │ MCP CLIENT (Claude/Cursor/Codex) │ │ uvx blender-mcp │ ├────────────────────────────────────────────────────────────┤ │ src/blender_mcp/ │ │ ├── server.py # FastMCP server (stdio transport) │ │ ├── socket_server.py# TCP server on :9876 │ │ └── tools/ # Tool definitions │ └────────────────────────────────────────────────────────────┘ │ TCP :9876 ▼ ┌────────────────────────────────────────────────────────────┐ │ BLENDER ADDON (addon.py) │ │ • Socket client connecting to :9876 │ │ • BlenderExecutor: Thread-safe command queue │ │ • Command handlers for each tool │ └────────────────────────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────┐ │ BLENDER │ └────────────────────────────────────────────────────────────┘ ``` --- ## Ключевые отличия от GenesisCore | Аспект | GenesisCore | blender-mcp | |--------|-------------|-------------| | **MCP-клиент** | Встроенный (несколько провайдеров) | Внешний (любой MCP-клиент) | | **MCP-сервер** | Встроенный (FastMCP на :45677) | Внешний процесс (uvx) | | **Транспорт** | SSE (внутренний) | stdio → TCP :9876 | | **Источники ассетов** | Только Poly Haven | Poly Haven + Poly Pizza + Sketchfab + Hyper3D + Hunyuan3D | | **Установка** | ZIP/Git в Blender | `uvx blender-mcp install-addon` | | **Конфигурация** | N-панель Blender | Конфиг MCP-клиента + настройки аддона Blender | --- ## Установка ### Предварительные требования ```powershell # Установить uv (Windows) powershell -c "irm https://astral.sh/uv/install.ps1 | iex" # Добавить в PATH (после этого перезапустить оболочку) $localBin = "$env:USERPROFILE\.local\bin" $userPath = [Environment]::GetEnvironmentVariable("Path", "User") [Environment]::SetEnvironmentVariable("Path", "$userPath;$localBin", "User") ``` ### Быстрая установка ```powershell # 1. Установить аддон Blender uvx blender-mcp install-addon # 2. Настроить MCP-клиент (см. ниже) # 3. В Blender: N-панель → MCP for Blender → Start MCP Server ``` ### Настройка MCP-клиента #### OpenCode ```json { "mcp": { "blender-mcp": { "type": "local", "command": ["uvx", "blender-mcp"], "enabled": true, "environment": { "BLENDER_HOST": "localhost", "BLENDER_PORT": "9876" } } } } ``` #### Claude Desktop ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["blender-mcp"] } } } ``` #### Cursor ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["blender-mcp"] } } } ``` --- ## Доступные инструменты (v2026) ### Базовые инструменты сцены | Инструмент | Описание | |------|-------------| | `get_scene_info` | Полная иерархия сцены | | `get_object_info` | Подробные данные объекта | | `create_object` | Примитивы: CUBE, SPHERE, CYLINDER, PLANE, CONE, TORUS, EMPTY, CAMERA, LIGHT | | `modify_object` | Трансформация + видимость | | `delete_object` | Удаление по имени | | `execute_blender_code` | **Выполнение произвольного Python-кода** | ### Инструменты материалов | Инструмент | Описание | |------|-------------| | `set_material` | Создание/применение Principled BSDF с цветом | ### Инструменты ассетов (нужны API-ключи) | Источник | Инструменты | Настройка | |--------|-------|-------| | **Poly Haven** | `search_polyhaven_assets`, `download_polyhaven_asset` | Бесплатно, без ключа | | **Poly Pizza** | `search_polypizza_models`, `download_polypizza_model` | Бесплатный API-ключ | | **Sketchfab** | `search_sketchfab_models`, `download_sketchfab_model` | API-ключ | | **Hyper3D Rodin** | `generate_rodin_model`, `get_rodin_generation_status` | API-ключ | | **Hunyuan3D** | `generate_hunyuan3d_model`, `get_hunyuan3d_status` | SecretId/SecretKey | --- ## Интеграция с Poly Pizza (уникально для blender-mcp) ```python # 1. Получить бесплатный API-ключ: https://poly.pizza/settings/api # 2. В Blender: N-панель → MCP for Blender → отметить "Use Poly Pizza" → вставить ключ # 3. Либо задать переменную окружения: BLENDERMCP_POLYPIZZA_API_KEY # Пример запроса к ИИ: # "Search Poly Pizza for a low-poly chair under CC0 license and import at 1 meter tall" ``` **Лицензирование:** ~69% CC-BY (требует атрибуции); фильтруйте через `licence="CC0"`, если атрибуция не нужна. --- ## Генерация 3D через ИИ (уникально для blender-mcp) ### Hyper3D Rodin ```python # Требуется API-ключ с https://hyper3d.ai/ # Переменная окружения: BLENDERMCP_HYPER3D_API_KEY # Запрос к ИИ: "Generate a 3D model of a garden gnome through Hyper3D" ``` ### Hunyuan3D (Tencent) ```python # Требуется SecretId/SecretKey с https://3d.hunyuan.tencent.com/ # Переменные окружения: BLENDERMCP_HUNYUAN3D_SECRET_ID / SECRET_KEY / API_URL # Запрос к ИИ: "Create a 3D model of a futuristic car using Hunyuan3D" ``` --- ## Безопасный режим ```bash # Включить безопасный режим (блокирует ввод-вывод файлов, сеть, подпроцессы, постоянный код) BLENDER_MCP_SAFE_MODE=1 uvx blender-mcp ``` Или в конфиге MCP: ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["blender-mcp"], "env": { "BLENDER_MCP_SAFE_MODE": "1" } } } } ``` --- ## Развёртывание в Docker ```bash # Сборка docker build -t blender-mcp . # Запуск (macOS/Windows с Docker Desktop) docker run -i --rm blender-mcp # Запуск (Linux — сеть хоста) docker run -i --rm --network=host -e BLENDER_HOST=localhost blender-mcp ``` Конфиг MCP: ```json { "mcpServers": { "blender": { "command": "docker", "args": ["run", "-i", "--rm", "blender-mcp"] } } } ``` --- ## Фиксация версии Python ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["--python", "3.11", "blender-mcp"], "env": { "UV_PYTHON_PREFERENCE": "only-managed" } } } } ``` --- ## Решение проблем | Проблема | Решение | |-------|-----| | `spawn uvx ENOENT` | Укажите полный путь к uvx: `which uvx` / `where uvx` | | Отказано в соединении | Аддон Blender не запущен; нажмите "Start MCP Server" | | Poly Pizza Cloudflare | Скачайте .glb вручную → File → Import → glTF | | Таймаут | Разбейте сложные запросы на шаги | | Конфликт версий | `uv cache clean blender-mcp && uvx --refresh blender-mcp` | --- ## Расположение файлов | Файл | Путь | |------|------| | Аддон (установлен) | `%APPDATA%\Blender Foundation\Blender\4.2\scripts\addons\blender_mcp.py` | | Аддон (исходники) | `addon.py` в репозитории | | MCP-сервер | `uvx blender-mcp` (запускается из кэша) | | Конфиг (настройки Blender) | Edit → Preferences → Add-ons → MCP for Blender | | Логи | Системная консоль Blender | --- ## Связанные страницы вики - [Архитектура MCP](../mcp/architecture.md) - [Сравнение с GenesisCore](genesiscore.md) - [Настройка MCP-клиента](../mcp/clients.md) - [Гайд по промптам](../guides/prompting.md) ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [INSTALLATION GUIDE - COMPLETE SETUP](../guides/installation.md), [TROUBLESHOOTING GUIDE](../guides/troubleshooting.md), [BLENDER-MCP - FULL DOCUMENTATION](blender-mcp-full.md), [GENESISCORE ADDON - DEEP DIVE](genesiscore.md), [MCP ARCHITECTURE - MODEL CONTEXT PROTOCOL FOR BLENDER](../mcp/architecture.md) - **По темам:** [TEXT-TO-3D WORKFLOWS](../workflows/text-to-3d.md), [BLENDER MATERIALS API REFERENCE](../api/materials.md), [BLENDER MESH OPERATIONS API REFERENCE](../api/mesh-operations.md) - **Теги:** #addon, #blender, #mcp, #reference --- # АДДОН DREAM TEXTURES — ПОДРОБНЫЙ РАЗБОР Tags: #addon #reference #stable-diffusion # АДДОН DREAM TEXTURES — ПОДРОБНЫЙ РАЗБОР ## Обзор **Репозиторий:** https://github.com/carson-katri/dream-textures **Версия:** Latest | **Blender:** 3.3+ | **Лицензия:** GPL-3.0 **Звёзды:** 8.2k | **Discord:** https://discord.gg/EmDJ8CaWZ7 Dream Textures встраивает **Stable Diffusion прямо в Blender** — работает локально через diffusers/PyTorch, внешние сервисы не нужны. Лучше всего подходит для генерации текстур, инпейнтинга, аутпейнтинга и проекции текстур. --- ## Архитектура ``` ┌────────────────────────────────────────────────────────────┐ │ DREAM TEXTURES ADDON │ ├────────────────────────────────────────────────────────────┤ │ .python_dependencies/ # Bundled PyTorch, diffusers │ │ builtin_presets/ # Prompt presets │ │ community_backends/ # Alternative backends │ │ engine/ # Core generation pipeline │ │ generator_process/ # Subprocess for model inference │ │ operators/ # Blender operators │ │ ui/ # N-panel + node editor │ │ sd_configs/ # Model configurations │ │ realtime_viewport.py # Live preview │ │ render_pass.py # Cycles render pass integration │ │ prompt_engineering.py # Prompt enhancement │ │ image_utils.py # Image processing │ └────────────────────────────────────────────────────────────┘ ``` --- ## Ключевые возможности ### 1. Текст в текстуру - Генерация наборов PBR-текстур (Base Color, Roughness, Normal, Metallic, Height) - Опция бесшовного тайлинга - Пресеты стилей: фотореализм, стилизация, фэнтези, sci-fi ### 2. Проекция текстур (Depth-to-Image) - Проекция текстур на 3D-модели с помощью карт глубины - Оператор "Project Dream Texture" - Работает на целых сценах ### 3. Инпейнтинг / аутпейнтинг - Исправление швов на текстурах - Расширение текстур за границы - Преобразование небесшовных текстур в бесшовные ### 4. Интеграция с render-pass - Использование рендера Cycles как входа для ControlNet - Ре-стилизация рендеров через img2img - Ре-стилизация анимации покадрово ### 5. Апскейл с помощью ИИ - Увеличение в 4 раза через ESRGAN/RealESRGAN - Встроено в систему нод ### 6. Система истории - Повторный вызов, экспорт и импорт генераций - Сохраняются промпт и параметры --- ## Установка ### Скачать релиз (рекомендуется) 1. Перейти на: https://github.com/carson-katri/dream-textures/releases/latest 2. Скачать `dream_textures-.zip` 3. Blender → Preferences → Add-ons → **Install from ZIP** 4. Включить **Dream Textures** ### Первый запуск - Аддон скачивает PyTorch + diffusers (~2-4 ГБ) в `.python_dependencies/` - Требуется **4 ГБ+ VRAM** (CUDA или Apple Silicon) - На macOS может потребоваться снятие карантина: ```bash xattr -r -d com.apple.quarantine ~/Library/Application\ Support/Blender/3.3/scripts/addons/dream_textures/.python_dependencies ``` ### Облачный запасной вариант (DreamStudio) - Если оборудование не поддерживается: укажите API-ключ DreamStudio в настройках - Обработка происходит в облаке, а не локально --- ## Требования к оборудованию | Компонент | Минимум | Рекомендуется | |-----------|---------|-------------| | **VRAM** | 4 ГБ | 8-12 ГБ | | **GPU** | CUDA 11.7+ / Apple Silicon | RTX 3080+ / M2 Max+ | | **RAM** | 8 ГБ | 16-32 ГБ | | **Диск** | 10 ГБ свободно | 20 ГБ+ | --- ## Рабочие сценарии ### Базовая генерация текстур ``` 1. Выберите объект → N-панель → Dream Textures 2. Откройте вкладку "Generate Texture" 3. Введите промпт: "weathered copper metal, patina, seamless" 4. Включите "Seamless" для тайлинга 5. Нажмите Generate 6. Примените к слотам материала ``` ### Проекция текстур ``` 1. Настройте камеру/вид для проекции 2. Выберите целевой объект(ы) 3. Выберите "Project Dream Texture" 4. Введите промпт: "ancient stone temple walls, moss, cracks" 5. Отрегулируйте влияние глубины 6. Project → создаёт текстуры с UV-развёрткой ``` ### Инпейнтинг (исправление швов) ``` 1. Выберите текстуру в Image Editor 2. Замаскируйте проблемный участок 3. Dream Textures → Inpaint 4. Промпт: "seamless continuation of stone texture" 5. Смешайте результат ``` ### Ре-стилизация рендера ``` 1. Настройте сцену с Cycles 2. Render pass → нода Dream Textures 3. Промпт: "cyberpunk neon aesthetic, volumetric lighting" 4. Перерендерите → ИИ трансформирует результат ``` --- ## Система нод (продвинутый уровень) Dream Textures добавляет собственные ноды в Shader Editor: - **Dream Texture Node** — генерация текстур прямо в графе нод - **ControlNet Node** — управление через Depth/Canny/Normal - **Upscale Node** — апскейл средствами ИИ внутри конвейера Позволяет строить сложные процедурные сценарии. --- ## Бэкенды моделей | Бэкенд | Модели | Примечания | |---------|--------|-------| | **diffusers** (по умолчанию) | SD 1.5, SDXL, SD 2.1, ControlNet | Локально, бесплатно | | **DreamStudio** | SDXL, SD 3 | Облако, платно | | **Community** | Flux, SD3, пользовательские | Экспериментально | --- ## Конфигурация ### Настройки (Edit → Preferences → Add-ons → Dream Textures) - **Device:** CUDA / MPS / CPU - **Precision:** fp16 (по умолчанию) / fp32 / bf16 - **Model Cache:** пользовательский путь к весам моделей - **DreamStudio API Key:** для облачной обработки - **Telemetry:** включение/отключение ### Пресеты Встроенные пресеты в `builtin_presets/`: - PBR-материалы (металл, ткань, дерево, камень) - Окружения (небо, земля, листва) - Стили (аниме, фотореализм, пиксель-арт) --- ## Решение проблем | Проблема | Решение | |-------|-----| | **Не хватает VRAM** | Снизьте разрешение; используйте fp16; закройте другие приложения | | **Модель не скачивается** | Проверьте интернет; задайте HF_TOKEN для моделей с ограниченным доступом | | **Карантин macOS** | Выполните команду xattr, приведённую выше | | **Медленная генерация** | Используйте меньше шагов (20-30); меньшее разрешение | | **Видны швы** | Включите "Seamless"; исправьте инпейнтингом | --- ## Интеграция с GenesisCore / blender-mcp Dream Textures работает **независимо** — интеграции с MCP нет. Варианты использования: 1. **Вручную** — через N-панель в Blender 2. **Python API** — вызов операторов из `execute_blender_code`: ```python bpy.ops.dream_texture.generate( prompt="brick wall, seamless", seamless=True, width=1024, height=1024 ) ``` 3. **Комбинирование сценариев:** - GenesisCore для геометрии/сцены - Dream Textures для материалов --- ## Расположение файлов | Файл | Путь | |------|------| | Аддон | `%APPDATA%\Blender Foundation\Blender\4.2\scripts\addons\dream_textures\` | | Модели | `%APPDATA%\Blender Foundation\Blender\4.2\scripts\addons\dream_textures\.python_dependencies\` | | Пресеты | `builtin_presets/` внутри аддона | | Вывод | Временная папка Blender / папка проекта | --- ## Связанные страницы вики - [Сценарий генерации текстур](../workflows/texture-generation.md) - [Связка GenesisCore + Dream Textures](../workflows/scene-building.md) - [API материалов Blender](../api/materials.md) ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [INSTALLATION GUIDE - COMPLETE SETUP](../guides/installation.md), [BLENDER-MCP ADDON - DEEP DIVE](blender-mcp.md), [DREAM TEXTURES - FULL WIKI DOCUMENTATION](dream-textures-full.md), [GENESISCORE ADDON - DEEP DIVE](genesiscore.md), [TROUBLESHOOTING GUIDE](../guides/troubleshooting.md) - **По темам:** [ANIMATION WORKFLOWS](../workflows/animation.md), [BLENDER MATERIALS API REFERENCE](../api/materials.md), [BLENDER MESH OPERATIONS API REFERENCE](../api/mesh-operations.md) - **Теги:** #addon, #reference, #stable-diffusion --- # АДДОН GENESISCORE — ПОДРОБНЫЙ РАЗБОР Tags: #addon #mcp #reference # АДДОН GENESISCORE — ПОДРОБНЫЙ РАЗБОР ## Обзор **Репозиторий:** https://github.com/AIGODLIKE/GenesisCore **Версия:** 0.0.1 | **Blender:** 4.0+ | **Лицензия:** Open Source **Звёзды:** 134 | **Язык:** Python GenesisCore — это **полноценный MCP-клиент + сервер**, встроенный прямо в Blender. В отличие от blender-mcp (только сервер), GenesisCore включает и LLM-клиент, поэтому вы общаетесь с ИИ *внутри Blender* через N-панель. --- ## Архитектура ``` ┌────────────────────────────────────────────────────────────┐ │ GENESISCORE ADDON │ ├────────────────────────────────────────────────────────────┤ │ src/ │ │ ├── client/ # MCP Clients (LLM providers) │ │ │ ├── base.py # MCPClientBase - core logic │ │ │ ├── openai.py # OpenAI compatible │ │ │ ├── openrouter.py # OpenRouter (extends OpenAI) │ │ │ ├── deepseek.py # DeepSeek API │ │ │ ├── claude.py # Anthropic Claude │ │ │ ├── ollama.py # Local Ollama │ │ │ └── siliconflow.py# SiliconFlow │ │ ├── server/ # MCP Server (FastMCP) │ │ │ ├── server.py # BlenderMCPServer on :45677 │ │ │ └── tools/ # Tool modules │ │ │ ├── common_tools.py # Scene info, code exec │ │ │ ├── object_tools.py # Create/modify/delete │ │ │ ├── material_tools.py # PBR materials │ │ │ ├── polyhaven_tools.py # Poly Haven assets │ │ │ └── asset_tools.py # Local assets │ │ ├── preference.py # Settings UI + config persistence │ │ ├── ui.py # N-panel interface │ │ ├── executor.py # Thread-safe bpy execution │ │ └── logger.py # Logging │ └────────────────────────────────────────────────────────────┘ ``` --- ## Ключевые возможности ### 1. Встроенный MCP-клиент (внешний хост не нужен) ```python # В base.py — подключается к локальному MCP-серверу на порту 45677 async def connect_to_server(self): headers = {"Content-Type": "application/json", "Accept": "application/json"} stdio_transport = await self.exit_stack.enter_async_context( sse_client(url="http://localhost:45677/sse", headers=headers) ) ``` ### 2. Поддержка нескольких провайдеров | Провайдер | Base URL | Модель по умолчанию | |----------|----------|---------------| | **OpenRouter** | `https://openrouter.ai/api` | `anthropic/claude-3.5-haiku` | | **DeepSeek** | `https://api.deepseek.com` | `deepseek-chat` | | **OpenAI** | `https://api.openai.com/v1` | `gpt-4o` | | **Anthropic** | `https://api.anthropic.com` | `claude-3-5-sonnet` | | **Ollama** | `http://localhost:11434/v1` | `llama3.2` | | **SiliconFlow** | `https://api.siliconflow.cn/v1` | `Qwen/Qwen2.5-Coder-32B` | ### 3. Модули инструментов (расширяемые) Каждый модуль регистрирует инструменты в MCP-сервере: ```python # common_tools.py — самый мощный инструмент def execute_blender_code(code: str) -> dict: """Выполнить произвольный код Blender Python API""" namespace = {"bpy": bpy} exec(code, namespace) return {"executed": True} ``` **Доступные инструменты:** | Модуль | Инструменты | |--------|-------| | `common_tools` | `get_scene_info`, `get_simple_info`, `execute_blender_code` | | `object_tools` | `create_object`, `modify_object`, `delete_object`, `get_object_info` | | `material_tools` | `set_material` (Principled BSDF) | | `polyhaven_tools` | `search_models`, `fetch_model_info`, `use_model_asset`, `search_hdris`, `use_hdri` | ### 4. Интеграция с Poly Haven - Кэширует в `%TEMP%\polyhaven_{models|hdris}` - Автоматически подгружает закэшированные ассеты - Поддерживает разрешения 1k/2k/4k/8k/16k ### 5. Подключение внешнего MCP-хоста ```json { "mcpServers": { "BlenderGenesis": { "url": "http://localhost:45677" } } } ``` Это позволяет Claude Desktop / Cursor / Codex подключаться к серверу GenesisCore. --- ## Установка ### Способ 1: установка из ZIP (рекомендуется) 1. Скачать: https://github.com/AIGODLIKE/GenesisCore/archive/refs/heads/main.zip 2. Blender → Edit → Preferences → Add-ons → **Install from ZIP** 3. Выбрать ZIP-файл 4. Включить **GenesisCore** (искать "GenesisCore" или "AI") ### Способ 2: git clone (с автообновлениями) ```powershell cd %APPDATA%\Blender Foundation\Blender\4.2\scripts\addons git clone https://github.com/AIGODLIKE/GenesisCore.git ``` Затем включить в настройках. --- ## Конфигурация ### Настройка OpenRouter (рекомендуется) 1. Получить API-ключ: https://openrouter.ai/keys 2. В N-панели Blender (нажать `N`) → вкладка **GenesisCore** 3. **Provider:** OpenRouter 4. **API Key:** вставить свой ключ 5. **Fetch Models** → выбрать: `deepseek/deepseek-chat` или `qwen/qwen-2.5-coder-32b` 6. **Tool Modules:** включить все (Shift для множественного выбора) 7. **Save Config** ### Рекомендации по моделям (2026) | Задача | Модель | Стоимость | |------|-------|------| | **Сложные рассуждения** | `anthropic/claude-3.5-sonnet` | $$$ | | **Код / Blender API** | `deepseek/deepseek-chat` | $ | | **Быстро и дёшево** | `qwen/qwen-2.5-coder-32b` | $ | | **Локально (бесплатно)** | `ollama:codellama:13b` | Бесплатно | --- ## Рабочий процесс ``` 1. Откройте N-панель (N) → GenesisCore 2. Выберите провайдера и модель 3. Включите модули инструментов (Common, Object, Material, Polyhaven) 4. Введите промпт в поле чата 5. Нажмите Enter / Execute 6. ИИ строит план → вызывает инструменты → выполняет в Blender 7. Результат появляется в чате, вьюпорт обновляется ``` --- ## Примеры промптов ### Базовая геометрия ``` "Create a low-poly castle with 4 towers, a central keep, and a moat. Use simple primitives." ``` ### Материалы и свет ``` "Create a studio lighting setup: 3-point lighting with key, fill, and rim lights. Make a chrome sphere in center." ``` ### Ассеты Poly Haven ``` "Search Poly Haven for a Victorian chair and an HDRI for indoor lighting. Import both and set up the scene." ``` ### Сложная сцена ``` "Build a cyberpunk alley scene: wet pavement (use HDRI), neon signs (emission materials), steam particles, and a hovering vehicle. Use Poly Haven assets where possible." ``` ### Прямое выполнение кода ``` "Execute this code to create a procedural city grid with random building heights" ``` → ИИ вызывает `execute_blender_code` с полным bpy-скриптом --- ## Продвинутое: разработка своих инструментов ### 1. Создание модуля инструментов ```python # src/tools/my_tools.py from .common import ToolsPackageBase class MyTools(ToolsPackageBase): def my_custom_tool(param: str) -> dict: """Описание для LLM""" # здесь операции bpy return {"result": "success"} ``` ### 2. Регистрация в `__init__.py` ```python # src/tools/__init__.py from .my_tools import MyTools # Порядок важен для отображения в интерфейсе ``` ### 3. Собственный LLM-клиент ```python # src/client/custom.py from .base import MCPClientBase class MCPClientCustom(MCPClientBase): @classmethod def info(cls): return {"name": "Custom", "description": "My Provider", "version": "0.1"} @classmethod def default_config(cls): return {"base_url": "https://api.custom.com", "model": "default"} ``` --- ## Решение проблем | Проблема | Решение | |-------|-----| | **"Connecting..." зависает** | Проверьте, что порт 45677 не заблокирован; перезапустите Blender | | **Список моделей пуст** | Проверьте API-ключ; проверьте сеть/прокси | | **Инструменты не появляются** | Включите модули в N-панели (Shift+клик для множественного выбора) | | **Poly Haven не работает** | Проверьте интернет; кэш лежит в `%TEMP%\polyhaven_*` | | **Ошибки выполнения кода** | Откройте системную консоль (Window → Toggle System Console) | --- ## Расположение файлов | Файл | Путь | |------|------| | Корень аддона | `%APPDATA%\Blender Foundation\Blender\4.2\scripts\addons\GenesisCore\` | | Конфиг | `%APPDATA%\Blender Foundation\Blender\4.2\config\genesiscore_config.json` | | Кэш Poly Haven | `%TEMP%\polyhaven_models\` / `polyhaven_hdris\` | | Логи | Системная консоль Blender | --- ## Связанные страницы вики - [Архитектура MCP](../mcp/architecture.md) - [Паттерны работы с API Blender](../api/scripting-patterns.md) - [Гайд по промптам](../guides/prompting.md) - [Сравнение с blender-mcp](blender-mcp.md) ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [BLENDER-MCP ADDON - DEEP DIVE](blender-mcp.md), [INSTALLATION GUIDE - COMPLETE SETUP](../guides/installation.md), [TROUBLESHOOTING GUIDE](../guides/troubleshooting.md), [BLENDER-MCP - FULL DOCUMENTATION](blender-mcp-full.md), [DREAM TEXTURES ADDON - DEEP DIVE](dream-textures.md) - **По темам:** [MCP ARCHITECTURE - MODEL CONTEXT PROTOCOL FOR BLENDER](../mcp/architecture.md), [MCP CLIENT CONFIGURATIONS](../mcp/clients.md), [MCP SERVER API REFERENCE](../mcp/server-api.md) - **Теги:** #addon, #mcp, #reference --- # АРХИТЕКТУРА MCP — ПРОТОКОЛ MODEL CONTEXT ДЛЯ BLENDER Tags: #blender #mcp #reference # АРХИТЕКТУРА MCP — ПРОТОКОЛ MODEL CONTEXT ДЛЯ BLENDER ## Обзор MCP (Model Context Protocol) — открытый стандарт для подключения ИИ-ассистентов к внешним инструментам и данным. В контексте Blender он позволяет LLM напрямую управлять Blender через структурированные вызовы инструментов. --- ## Спецификация протокола ### Варианты транспорта | Транспорт | Сценарий использования | Пример | |-----------|------------------------|---------| | **stdio** | Локальный подпроцесс | `uvx blender-mcp` | | **SSE** | Долгий опрос по HTTP (long-polling) | GenesisCore на :45677 | | **WebSocket** | Двунаправленный | В будущем | ### Формат сообщений ```json // Запрос (клиент → сервер) { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "create_object", "arguments": {"entity_type": "CUBE", "name": "MyCube"} } } // Ответ (сервер → клиент) { "jsonrpc": "2.0", "id": 1, "result": { "name": "MyCube", "type": "MESH", "location": [0, 0, 0] } } ``` --- ## Реализации MCP для Blender ### 1. blender-mcp (эталонный сервер) - **Архитектура:** внешний MCP-сервер (uvx) + аддон Blender (TCP :9876) - **Транспорт:** stdio → TCP - **Инструменты:** 28+ (сцена, объекты, материалы, 5 источников ассетов, генерация 3D по ИИ) - **Клиенты:** любой совместимый с MCP (Claude, Cursor, Codex, OpenCode и др.) ### 2. GenesisCore (всё в одном) - **Архитектура:** аддон Blender со встроенными MCP-клиентом и MCP-сервером - **Транспорт:** внутренний SSE (:45677) + внешний SSE для других клиентов - **Провайдеры:** OpenRouter, DeepSeek, OpenAI, Anthropic, Ollama, SiliconFlow - **UI:** встроенный чат в N-панели ### Сравнение | Аспект | blender-mcp | GenesisCore | |--------|-------------|-------------| | MCP-сервер | Внешний процесс | Встроенный (FastMCP) | | MCP-клиент | Внешний (любой) | Встроенный (несколько провайдеров) | | Транспорт | stdio → TCP :9876 | Внутренний SSE :45677 | | LLM-провайдеры | Любой (через клиент) | 6 встроенных | | Источники ассетов | 5 (PH, Sketchfab, PP, Rodin, Hunyuan) | 1 (Poly Haven) | | UI | N-панель (управление сервером) | N-панель (чат + конфигурация) | | Выполнение кода | `execute_blender_code` | `execute_blender_code` | --- ## Схема вызова инструментов ``` User Prompt │ ▼ LLM analyzes → selects tool(s) │ ▼ MCP Client formats tool call │ ▼ ┌─────────────────────────────┐ │ Transport Layer │ │ stdio / SSE / WebSocket │ └─────────────────────────────┘ │ ▼ MCP Server receives → validates │ ▼ Tool handler executes │ ├─ blender-mcp: TCP → Blender addon → bpy └─ GenesisCore: Direct bpy (same process) │ ▼ Result → MCP response → LLM │ ▼ LLM continues or responds to user ``` --- ## Паттерны интеграции аддона в Blender ### Паттерн 1: сокетный сервер (blender-mcp) ```python # addon.py — сокетный сервер в фоновом потоке class BlenderMCPServer: def __init__(self, host='localhost', port=9876): self.socket = socket.socket() self.command_queue = queue.Queue() def _server_loop(self): # Поток while running: client, _ = self.socket.accept() threading.Thread(target=self._handle_client, args=(client,)).start() def _drain_command_queue(self): # bpy.app.timer (главный поток) while queue: cmd, client = queue.get() result = self.execute_command(cmd) client.send(json.dumps(result)) def execute_command(self, cmd): handler = HANDLERS[cmd['type']] return handler(**cmd['params']) ``` ### Паттерн 2: встроенный MCP-сервер (GenesisCore) ```python # src/server/server.py — FastMCP внутри процесса Blender class BlenderMCPServer(FastMCP): def __init__(self): super().__init__(name="BlenderMCPServer", host="localhost", port=45677) @self.tool() def create_object(self, entity_type: str, ...) -> dict: # Прямые операции bpy — IPC не требуется bpy.ops.mesh.primitive_cube_add(...) return {"name": obj.name, ...} # Выполняется в потоке-демоне Server.run() # Thread(target=Server.main).start() ``` ### Паттерн 3: MCP-клиент внутри Blender (GenesisCore) ```python # src/client/base.py — подключается к локальному MCP-серверу class MCPClientBase: async def connect_to_server(self): async with sse_client(url="http://localhost:45677/sse") as (read, write): self.session = ClientSession(read, write) await self.session.initialize() async def call_tool(self, fn_name, arguments): return await self.session.call_tool(fn_name, arguments) ``` --- ## Вопросы безопасности ### Безопасный режим (blender-mcp) ```bash BLENDER_MCP_SAFE_MODE=1 uvx blender-mcp ``` **Блокирует:** - `import os, subprocess, sys, socket, requests, urllib` - `open()`, `eval()`, `exec()`, `compile()` - `bpy.app.handlers`, `bpy.app.timers`, `bpy.app.driver_namespace` - Регистрацию классов/свойств (`bpy.utils.register_class`) - Загрузку внешних файлов (`bpy.data.libraries.load`) **Разрешает:** - Все `bpy.ops.*` (моделирование, материалы, рендер, сохранение, импорт/экспорт) - Чтение/запись `bpy.data.*` (объекты, меши, материалы, сцены) - `mathutils`, `math`, `random`, `json`, `uuid` (stdlib) ### Риск выполнения кода Обе реализации предоставляют `execute_blender_code`: ```python # ОПАСНО — полный доступ к Python bpy.ops.wm.open_mainfile(filepath="/etc/passwd") # Блокируется в безопасном режиме os.system("rm -rf /") # Блокируется в безопасном режиме # БЕЗОПАСНО — только операции Blender bpy.ops.mesh.primitive_cube_add() mat = bpy.data.materials.new("Red") mat.use_nodes = True mat.node_tree.nodes["Principled BSDF"].inputs[0].default_value = (1,0,0,1) ``` **Лучшая практика:** всегда используйте конкретные инструменты (`create_object`, `set_material`) вместо прямого выполнения кода. --- ## Переменные окружения ### blender-mcp | Переменная | По умолчанию | Описание | |------------|---------------|----------| | `BLENDER_HOST` | `localhost` | Хост аддона Blender | | `BLENDER_PORT` | `9876` | Порт аддона Blender | | `BLENDER_MCP_SAFE_MODE` | `0` | Включить безопасный режим | | `DISABLE_TELEMETRY` | `false` | Отключить телеметрию | | `UV_PYTHON_PREFERENCE` | - | `only-managed`, чтобы избежать conda/pyenv | ### GenesisCore | Переменная | По умолчанию | Описание | |------------|---------------|----------| | `OPENROUTER_API_KEY` | - | API-ключ OpenRouter | | `DEEPSEEK_API_KEY` | - | API-ключ DeepSeek | | `OPENAI_API_KEY` | - | API-ключ OpenAI | | `ANTHROPIC_API_KEY` | - | API-ключ Anthropic | | `OLLAMA_HOST` | `http://localhost:11434` | Эндпоинт Ollama | --- ## Добавление своих инструментов ### blender-mcp (на стороне сервера) ```python # src/blender_mcp/tools/my_tool.py from mcp.server.fastmcp import FastMCP @mcp.tool() async def my_custom_tool(param: str) -> str: """Description for LLM""" blender = get_blender_connection() return blender.send_command("my_custom_command", {"param": param}) ``` ### GenesisCore (на стороне аддона) ```python # src/tools/my_tools.py from .common import ToolsPackageBase class MyTools(ToolsPackageBase): def my_custom_tool(param: str) -> dict: """Description for LLM""" # Прямой доступ к bpy return {"result": "success"} # src/tools/__init__.py from .my_tools import MyTools # Порядок влияет на отображение в UI ``` ### GenesisCore: собственный клиент ```python # src/client/custom.py from .base import MCPClientBase class MCPClientCustom(MCPClientBase): @classmethod def info(cls): return {"name": "Custom", "description": "My Provider", "version": "0.1"} @classmethod def default_config(cls): return {"base_url": "https://api.custom.com", "model": "default"} ``` --- ## Отладка ### Системная консоль Blender - **Windows:** Window → Toggle System Console - **macOS:** `/Applications/Blender.app/Contents/MacOS/Blender` в терминале - **Linux:** запустите Blender из терминала ### Типичные проблемы | Симптом | Причина | Решение | |---------|---------|---------| | "Connection refused" | Аддон не запущен | N-панель → Start MCP Server | | "Timeout" | Сложная операция | Разбить на более мелкие шаги | | "Safe mode rejected" | Запрещённый импорт | Переписать без os/subprocess | | Список моделей пуст | Неверный API-ключ | Проверить ключ и сеть | | Poly Pizza 403 | Проверка Cloudflare на бота | Домашний IP или ручная загрузка | --- ## Совместимость версий | Blender | blender-mcp | GenesisCore | Спецификация MCP | |---------|-------------|-------------|----------| | 3.0+ | ✅ | ❌ | 2024-11-05 | | 4.0+ | ✅ | ✅ | 2024-11-05 | | 4.2+ | ✅ | ✅ | 2025-06-18 | --- ## Ресурсы - [MCP Specification](https://modelcontextprotocol.io/) - [blender-mcp Repo](https://github.com/ahujasid/blender-mcp) - [GenesisCore Repo](https://github.com/AIGODLIKE/GenesisCore) - [FastMCP Python SDK](https://github.com/jlowin/fastmcp) --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [BLENDER-MCP - FULL DOCUMENTATION](../addons/blender-mcp-full.md), [BLENDER-MCP ADDON - DEEP DIVE](../addons/blender-mcp.md), [INSTALLATION GUIDE - COMPLETE SETUP](../guides/installation.md), [TEXT-TO-3D WORKFLOWS](../workflows/text-to-3d.md), [TROUBLESHOOTING GUIDE](../guides/troubleshooting.md) - **По темам:** [BLENDER MATERIALS API REFERENCE](../api/materials.md), [BLENDER MESH OPERATIONS API REFERENCE](../api/mesh-operations.md), [BLENDER PYTHON API REFERENCE (4.x)](../api/blender-api.md) - **Теги:** #blender, #mcp, #reference --- ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- # ВОРКФЛОУ — ПРОИЗВОДСТВЕННЫЕ ПАЙПЛАЙНЫ Tags: #ai-3d #animation #assets #automation #blender #geometry-nodes #mcp #modeling # ВОРКФЛОУ — ПРОИЗВОДСТВЕННЫЕ ПАЙПЛАЙНЫ ## Воркфлоу 1: Генерация сцены из текста ### Через GenesisCore (чат внутри Blender) ``` 1. Откройте Blender → N-панель → GenesisCore 2. Настройте OpenRouter + модель (DeepSeek/Qwen) 3. Включите инструменты: Common, Object, Material, Polyhaven 4. Последовательность промптов: "Create a cyberpunk alley scene: - Wet pavement plane 20x20m - Neon signs using emission materials (cyan, magenta, yellow) - Steam particles from grates - Hovering vehicle (simple primitive shapes) - Search Poly Haven for 'urban' HDRI at 2k, set as world - Camera: low angle looking down alley, 35mm lens" ``` ### Через blender-mcp (внешний ИИ) ``` Промпт для OpenCode/Claude: "Build a cyberpunk alley scene in Blender: 1. Create 20x20m plane for wet pavement at z=0 2. Add emission materials for neon signs (cyan, magenta, yellow) on cubes 3. Create particle system for steam on plane areas 4. Build simple hovering vehicle from primitives 5. Search Poly Haven for 'urban' HDRI, download 2k, set as world 6. Add camera at (-5, -10, 3) looking at (0,0,1), 35mm lens 7. Set render to Cycles, 256 samples, denoising on" ``` --- ## Воркфлоу 2: Пайплайн генерации текстур ### Связка GenesisCore + Dream Textures ``` Фаза 1 — Геометрия (GenesisCore): "Create a medieval castle wall section: - 10x5m stone wall with crenellations - Wooden door with iron hinges - Arrow slits as boolean cutouts - Stone floor tiles" Фаза 2 — Материалы (Dream Textures): 1. Выберите объект стены → N-панель → Dream Textures 2. Сгенерировать: "weathered medieval stone wall, moss in cracks, seamless" → Check Seamless → Generate → Apply 3. Выберите дверь → "aged oak wood, iron studs, seamless" → Apply 4. Выберите пол → "cobblestone pathway, worn, seamless" → Apply Фаза 3 — Полировка (GenesisCore): "Add vertex paint for moss variation on wall Add geometry nodes for procedural ivy on top edges Set up 3-point lighting for texture showcase" ``` ### Только Dream Textures (проекция текстур) ``` 1. Смоделируйте базовую геометрию (стена, пол, реквизит) 2. Выделите все грани под текстурирование 3. Войдите в Local View (/) 4. Dream Textures → Project Dream Texture 5. Промпт: "ancient dungeon corridor, stone bricks, torch lighting, moss" 6. Модель: stabilityai/stable-diffusion-2-depth 7. Нажмите Project Dream Texture 8. UV корректируются автоматически ``` --- ## Воркфлоу 3: Сборка сцены из готовых ассетов ### Poly Haven + Poly Pizza + Sketchfab ``` "Create a cozy library scene using free assets: 1. Poly Haven: - Search models: 'bookshelf', 'wooden chair', 'desk lamp' - Search HDRI: 'library', 'indoor', 2k - Download all at 2k 2. Poly Pizza (low-poly fill): - Search: 'book', 'scroll', 'candle', 'potion bottle' - Licence: CC0 (no attribution) - Download, normalize_size=1.0, target_size=0.15 3. Sketchfab (hero pieces): - Search: 'ornate desk', 'grandfather clock' - Get preview, download at target_size=1.5m 4. Assembly: - Position bookshelves along walls - Scatter Poly Pizza books on shelves (random rotation) - Place hero desk center, chair beside - Add HDRI as world environment - Add area lights for desk lamp, candle glow - Camera: wide angle from doorway" ``` --- ## Воркфлоу 4: Генерация 3D средствами ИИ + чистка ### Hyper3D Rodin / Hunyuan3D ``` "Generate a fantasy creature concept: 1. Create Hyper3D job: 'dragon whelp, cute, stylized, low poly' 2. Poll status every 30s until complete 3. Import result 4. Cleanup: - Decimate modifier (ratio 0.3) for game-ready - Separate loose parts (P → By Loose Parts) - Create materials for each part (scales, eyes, horns) - UV unwrap (Smart UV Project) - Bake normal map from high-poly (if needed) 5. Rig: Add armature, auto-weight, basic pose" ``` --- ## Воркфлоу 5: Процедурная генерация на Geometry Nodes ### Генератор городского квартала ``` "Create Geometry Nodes setup for procedural city block: 1. Base grid: 10x10, 20m spacing 2. Building height: random 10-50m (seed controlled) 3. Building shape: extruded random polygons 4. Windows: array on faces, random offset 5. Roofs: flat/hipped/gabled (weighted random) 6. Street level: sidewalk, road markings, lamps 7. Variations: - Park lots (empty with trees) - Plaza (fountain + benches) - Alleyways (dumpsters, fire escapes) Controls exposed in modifier panel: - Seed, Block Size, Density, Height Range, Style Mix" ``` ### Blender Python для Geometry Nodes ```python # Выполнять через execute_blender_code import bpy ng = bpy.data.node_groups.new("CityBlock", 'GeometryNodeTree') # ... программная сборка дерева нод ... # API Geometry Nodes см. в api/blender-api.md ``` --- ## Воркфлоу 6: Анимация и риггинг ### Пайплайн анимации персонажа ``` 1. Импорт/генерация персонажа (Mixamo, Hyper3D, вручную) 2. Rigify: - Добавить мета-риг Rigify (Human) - Подогнать кости под меш - Сгенерировать риг - Parenting с автоматическими весами 3. Анимация: - Импорт motion capture (BVH/FBX) - Ретargeting на Rigify - Чистка: NLA-стрипы, слои action 4. Экспорт: - FBX для игровых движков - GLTF для веба - Alembic для кэша симуляции ``` ### Анимация камеры ``` "Create cinematic camera path: 1. Add Bezier curve for dolly path 2. Add camera, Follow Path constraint 3. Add Empty for target, Track To constraint 4. Animate curve evaluation (0-1 over frames) 5. Add DOF: focus on target Empty, f-stop 1.8 6. Motion blur: Cycles → 0.5 shutter, 16 samples" ``` --- ## Воркфлоу 7: Оптимизация под real-time ### Пайплайн игрового ассета ``` Вход: high-poly модель (Sketchfab/Hyper3D/вручную) 1. Ретопология: - Instant Meshes / Quad Remesher - Цель: 1-5k трисов (пропсы), 10-30k (персонажи) 2. UV Unwrap: - Швы на жёстких рёбрах - Упаковка островов (margin 0.005) - UDIM при текстурах >4k 3. Запекание (Cycles): - Normal (Tangent Space, 4k) - AO (16 samples, 4k) - Curvature (4k) - Position (для масок) - Material ID 4. Текстурирование (Substance/Blender): - PBR-воркфлоу metal/rough - Смарт-материалы + кастомные детали 5. Экспорт: - GLTF: раздельные текстуры, сжатие Draco - Проверка: gltf-transform inspect - LOD: 100%, 50%, 25%, 10% от числа полигонов ``` --- ## Воркфлоу 8: Пакетная обработка / автоматизация ### Настройка рендер-фермы ``` # Рендер через Blender CLI blender -b scene.blend -E CYCLES -o //render/frame_#### -f 1..250 # Распределённый рендер (Flamenco или своя реализация) # 1. Раздать кадры по машинам # 2. Синхронизировать ассеты через общее хранилище # 3. Собрать результат ``` ### Шаблон пакетного скрипта на Python ```python # Запуск: blender -b -P batch_script.py import bpy import sys def process_scene(scene_path, output_dir): bpy.ops.wm.open_mainfile(filepath=scene_path) scene = bpy.context.scene scene.render.filepath = output_dir scene.render.engine = 'CYCLES' scene.cycles.samples = 128 bpy.ops.render.render(animation=True, write_still=False) bpy.ops.wm.quit_blender() if __name__ == "__main__": # Аргументы после -- argv = sys.argv[sys.argv.index("--")+1:] process_scene(argv[0], argv[1]) ``` --- ## Воркфлоу 9: Контроль версий для .blend ### Настройка Git LFS ```bash # .gitattributes *.blend filter=lfs diff=lfs merge=lfs -text *.png filter=lfs diff=lfs merge=lfs -text *.jpg filter=lfs diff=lfs merge=lfs -text *.hdr filter=lfs diff=lfs merge=lfs -text *.exr filter=lfs diff=lfs merge=lfs -text *.glb filter=lfs diff=lfs merge=lfs -text *.gltf filter=lfs diff=lfs merge=lfs -text # Инициализация git lfs install git lfs track "*.blend" "*.png" "*.hdr" "*.exr" "*.glb" git add .gitattributes ``` ### Diff .blend-файлов ```bash # Используйте blendfile-utils или собственный скрипт # Для осмысленного diff экспортируйте в обменный формат: blender -b scene.blend -P export_gltf.py # Затем diff по .gltf (JSON) ``` --- ## Матрица решений: какой инструмент когда? | Задача | Основной инструмент | Запасной вариант | |--------|---------------------|------------------| | Быстрая геометрия | Чат GenesisCore | blender-mcp | | Сложная логика сцены | blender-mcp + Codex/Claude | GenesisCore | | Генерация текстур | Dream Textures | Текстуры Poly Haven | | Ключевые ассеты | Sketchfab / Hyper3D | Ручное моделирование | | Фоновый реквизит | Poly Pizza | Модели Poly Haven | | HDRI-освещение | Poly Haven (оба) | Poly Haven | | Автоматизация кодом | execute_blender_code | Geometry Nodes | | Игровые ассеты | Dream Textures + Retopo | Substance Painter | | Анимация | Rigify + NLA | Ручные ключи | | Пакетный рендер | CLI + Flamenco | Вручную | --- ## Советы по надёжной автоматизации 1. **Всегда проверяйте перед действием:** `get_scene_info` → план → выполнение 2. **Используйте именованные объекты:** явные имена > «куб, который я только что сделал» 3. **Проверяйте режим:** перед трансформациями убедитесь, что включён режим `OBJECT` 4. **Применяйте масштаб:** `Ctrl+A → Scale` до bevel/array/модификаторов 5. **Уникальные данные:** при дублировании мешей используйте `.copy()` 6. **Сохраняйтесь инкрементально:** `bpy.ops.wm.save_as_mainfile(filepath="scene_v001.blend")` 7. **Используйте коллекции:** группируйте по типу (Geometry, Lights, Cameras, FX) --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [TEXT-TO-3D WORKFLOWS](text-to-3d.md), [BLENDER MESH OPERATIONS API REFERENCE](../api/mesh-operations.md), [BLENDER-MCP - FULL DOCUMENTATION](../addons/blender-mcp-full.md), [BLENDER-MCP ADDON - DEEP DIVE](../addons/blender-mcp.md), [INSTALLATION GUIDE - COMPLETE SETUP](../guides/installation.md) - **По темам:** [P18: DRIVERS & CONSTRAINTS](../../../prompts/18_drivers_constraints.md), [P45: GEOMETRY NODES SIMULATION ZONES](../../../prompts/45_gn_simulation.md), [SETUP 35: READY PLAYER ME AVATARS](../../../setups/setup_35_readyplayerme.md), [SETUP 45: HUNYUAN3D 2.1 LOCAL](../../../setups/setup_45_hunyuan3d_21.md) - **Теги:** #ai-3d, #animation, #assets, #automation, #blender, #geometry-nodes, #mcp, #modeling --- ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- # ВОРКФЛОУ АНИМАЦИИ Tags: #animation #frame #reference # ВОРКФЛОУ АНИМАЦИИ ## Обзор Пайплайны риггинга, анимации и рендера в Blender. --- ## Риггинг ### Rigify (гуманоид) ``` 1. Импортировать меш персонажа (T-pose или A-pose) 2. Добавить Armature → Human (Meta-Rig) 3. Отмасштабировать/сдвинуть мета-риг под меш 4. Сгенерировать риг (кнопки Rigify → Generate) 5. Parenting: Mesh → Armature (Automatic Weights) 6. При необходимости — чистка весов (weight paint) ``` ### Auto-Rig Pro / Rigify Pro (продвинутый уровень) - Лучше работают лицевые риги - Переключение IK/FK - Собственные формы костей ### Простой риг (реквизит/механика) ``` 1. Добавить кости под каждую подвижную часть 2. Подчинить кости мешу (Bone Heat / Envelope) 3. Добавить ограничения: Copy Location/Rotation/Scale 4. Добавить драйверы под механические связи ``` --- ## Анимация по ключам ### Базовый воркфлоу ``` 1. Задать диапазон кадров: Scene → Frame Start/End 2. Поза на кадре 1 → вставить ключи (I → Location/Rotation/Scale) 3. Перейти на кадр 50 → Поза → вставить ключи 4. Graph Editor: почистить кривые, задать интерполяцию 5. Dope Sheet: подстроить тайминг ``` ### Типы интерполяции | Тип | Применение | |-----|------------| | Bezier | Плавное, естественное движение (по умолчанию) | | Linear | Механическое движение, постоянная скорость | | Constant | Удержание позы (ступенчато) | | Elastic/Bounce | Мультяшные эффекты | ### Советы по Graph Editor ``` - T: меню интерполяции - V: тип хендла (Auto, Vector, Aligned, Free) - Shift+E: автоматическая подрезка хендлов - Period (.) на нампаде: Frame selected - Ctrl+Click: добавить ключ в позиции курсора ``` --- ## NLA (нелинейная анимация) ### Action-стрипы ``` 1. Создать actions: Walk, Run, Idle, Attack 2. NLA Editor → Push Down (stash) для каждого action 3. Смешивать стрипы: наложение для переходов 4. Extrapolation: Hold / Nothing / Repeat 5. Слоистые стрипы: аддитивные слои для деталей ``` ### Воркфлоу ``` Base Layer: цикл ходьбы (repeat) Layer 1: вариация взмаха рук (additive) Layer 2: поворот головы (override) Layer 3: мимика (override) ``` --- ## Motion Capture / ретаргетинг ### Импорт BVH/FBX ``` 1. File → Import → BVH / FBX 2. Создаётся арматура с анимацией 3. Ретаргетинг на ваш риг: - Добавить ограничения: Copy Rotation (цель — кость mocap) - Либо использовать аддон Retargeting (в Rigify он встроен) ``` ### Воркфлоу ретаргетинга (Rigify) ``` 1. Импортировать mocap → создаётся арматура "Mocap" 2. Выбрать свой риг Rigify 3. Bone Constraints → Copy Rotation → Target: кость Mocap 4. Bake Action (Pose → Animation → Bake Action) 5. Чистка: убрать ограничения, упростить ключи ``` --- ## Процедурная анимация ### Драйверы ```python # Драйвер: вращение шестерёнки от другой шестерёнки # Выражение драйвера: -var * (teeth_a / teeth_b) # Настройка через Python driver = gear_b.driver_add('rotation_euler', 2).driver driver.type = 'SCRIPTED' var = driver.variables.new() var.name = 'gear_a_rot' var.targets[0].id = gear_a var.targets[0].data_path = 'rotation_euler[2]' driver.expression = '-gear_a_rot * 0.5' # передаточное число 2:1 ``` ### Анимация в Geometry Nodes ``` 1. Geometry Nodes → Object Info → взять анимируемый объект 2. Использовать номер кадра: #frame 3. Math-ноды для процедурного движения 4. Instance on points со смещением по анимации ``` ### Симуляция (физика) ``` - Rigid Body: падающие объекты, разрушения - Cloth: флаги, плащи, одежда - Fluid: вода, дым, огонь - Particles: магические эффекты, обломки, толпа - Soft Body: желе, деформация плоти ``` --- ## Анимация камеры ### Долли/кран ``` 1. Добавить кривую (Bezier) под путь движения 2. Камера → ограничение Follow Path 3. Empty-цель → ограничение Track To 4. Анимировать Curve Evaluation (0→1 по кадрам) ``` ### Тряска от руки ``` 1. Камера → добавить модификатор Noise (Graph Editor) 2. Scale: 0.01-0.05, Strength: 0.1-0.5 3. Либо: Python-скрипт для процедурной тряски ``` ### DOF (глубина резкости) ``` 1. Камера → Depth of Field → Focus Object: целевой Empty 2. F-Stop: 1.4-2.8 (малая глубина) / 8-16 (большая) 3. Анимировать Focus Distance для перевода фокуса ``` --- ## Рендер анимации ### Настройки ```python scene = bpy.context.scene scene.frame_start = 1 scene.frame_end = 250 scene.render.fps = 24 scene.render.resolution_x = 1920 scene.render.resolution_y = 1080 # Вывод scene.render.filepath = "//renders/anim_####.png" scene.render.image_settings.file_format = 'PNG' scene.render.image_settings.color_mode = 'RGBA' scene.render.image_settings.compression = 15 scene.render.use_motion_blur = True scene.render.motion_blur_shutter = 0.5 scene.render.motion_blur_samples = 16 ``` ### Motion Blur ``` Cycles: - Render → Motion Blur → Position: Center/Start/End - Shutter: 0.5 (шторка 180°) - Samples: 16+ для качества EEVEE: - Render → Motion Blur → включить - Steps: 8-16 ``` ### Сборка кадров в видео ```bash # Отрендерить кадры blender -b scene.blend -E CYCLES -o //renders/frame_#### -f 1..250 # Скодировать в видео (ffmpeg) ffmpeg -framerate 24 -i //renders/frame_%04d.png -c:v libx264 -pix_fmt yuv420p output.mp4 # Либо в Blender: Video Sequencer → Add → Image Sequence → Export FFmpeg ``` --- ## Alembic-кэш (пайплайн) ### Экспорт ```python bpy.ops.wm.alembic_export( filepath="animation.abc", start=1, end=250, selected=True, flatten=True, subsamples=4 ) ``` ### Импорт ```python bpy.ops.wm.alembic_import( filepath="animation.abc", as_background_job=False ) ``` --- ## Анимация толпы ### Инстансинг частицами ``` 1. Плоскость-эмиттер → Particle System (Hair) 2. Render → Object → Collection (выбрать коллекцию с персонажем) 3. Advanced → Rotation: Velocity/Random 4. Scale: Random 0.9-1.1 5. Использовать инстансы коллекции для разнообразия ``` ### Толпа на Geometry Nodes ``` 1. Сетка точек 2. Случайно выбрать персонажа из коллекции 3. Инстансировать со случайным смещением по анимации 4. Look-at цель для направления ``` --- ## Синхронизация губ ### Papagayo / Rhubarb ``` 1. Аудио → Rhubarb Lip Sync → .json/.tsv 2. Импорт в Blender (аддон Papagayo или свой скрипт) 3. Сопоставить фонемы с shape keys 4. Запечь в анимацию shape keys ``` ### Настройка Shape Keys ``` 1. Создать basis + фонемные формы (A, E, I, O, U, M и т. д.) 2. Управлять каждым shape key с дорожки фонем 3. Сгладить в Graph Editor ``` --- ## Экспорт для игровых движков ### Unity/Unreal (FBX) ```python bpy.ops.export_scene.fbx( filepath="character.fbx", use_selection=True, apply_unit_scale=True, apply_scale_options='FBX_SCALE_ALL', bake_space_transform=True, object_types={'ARMATURE', 'MESH'}, use_armature_deform_only=True, add_leaf_bones=False, primary_bone_axis='Y_FORWARD', secondary_bone_axis='X_UP' ) ``` ### Веб (GLTF) ```python bpy.ops.export_scene.gltf( filepath="character.glb", export_format='GLB', use_selection=True, export_animations=True, export_frame_range=True, export_frame_step=1, export_force_sampling=True, export_skins=True, export_morph=True ) ``` --- ## Советы по производительности ### Оптимизация анимации ``` - Меньше ключей: упрощать кривые (Graph Editor → Key → Simplify) - Использовать actions для переиспользуемых циклов - Запекать физику в ключи для рендер-фермы - Прокси/low-res меши для анимации во вьюпорте - Скрывать тяжёлые коллекции на время анимации ``` ### Рендер-ферма ``` 1. Раздать кадры по машинам 2. Использовать Flamenco (рендер-ферма Blender) или Deadline 3. Общее хранилище для ассетов 4. Alembic-кэши для данных симуляции 5. Собрать → скодировать → проверить ``` --- ## Анимация с ИИ (через MCP) ### Примеры промптов ``` "Animate camera: 360° orbit around object over 120 frames, ease in/out" "Create walk cycle for rigged character: 24 frames, loop seamlessly" "Add noise modifier to camera rotation for handheld feel" "Set up DOF rack focus from object A to B over 60 frames" "Create particle explosion at frame 50, 200 particles, 2 second life" "Bake rigid body simulation for 100 falling boxes to keyframes" ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [BLENDER MATERIALS API REFERENCE](../api/materials.md), [BLENDER MESH OPERATIONS API REFERENCE](../api/mesh-operations.md), [BLENDER PYTHON API REFERENCE (4.x)](../api/blender-api.md), [BLENDER SCRIPTING PATTERNS - SAFE & EFFECTIVE](../api/scripting-patterns.md), [BLENDER-MCP - FULL DOCUMENTATION](../addons/blender-mcp-full.md) - **По темам:** [BLENDER-MCP ADDON - DEEP DIVE](../addons/blender-mcp.md), [CASE 04: CHARACTER RIG & ANIMATION](../../../cases/case_04_character_rig.md), [CASE 06: ANIMATION LOOP](../../../cases/case_06_animation_loop.md) - **Теги:** #animation, #frame, #reference --- ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- # ВОРКФЛОУ ГЕНЕРАЦИИ ТЕКСТУР Tags: #reference #texturing # ВОРКФЛОУ ГЕНЕРАЦИИ ТЕКСТУР ## Обзор Несколько подходов к созданию PBR-текстур и материалов. --- ## Метод 1: Dream Textures (локальный Stable Diffusion) **Лучше всего для:** собственных текстур, бесшовного тайлинга, инпейнтинга, проекции **Стоимость:** бесплатно (локальный GPU) **Качество:** высокое, полный контроль ### Базовая генерация текстуры ``` 1. Выберите объект → N-панель → Dream Textures 2. Вкладка: "Generate Texture" 3. Промпт: "weathered brick wall, moss in cracks, seamless" 4. Отметить: "Seamless" (для тайлинга) 5. Модель: stabilityai/stable-diffusion-2-1-base 6. Размер: 512x512 (или 1024x1024, если хватает VRAM) 7. Нажать: Generate 8. Нажать: "Apply to Material" или перетащить в ноды шейдера ``` ### Проекция текстуры (Depth-to-Image) ``` 1. Смоделируйте геометрию (стена, пол, персонаж) 2. Выделите грани под текстурирование (Edit Mode → Select faces) 3. Local View (/) — изолировать объект 4. Dream Textures → "Project Dream Texture" 5. Модель: stabilityai/stable-diffusion-2-depth (REQUIRED) 6. Промпт: "ancient stone temple, moss, cracks, torch lighting" 7. Data Source: "Depth" или "Depth and Color" 8. Нажать: Project Dream Texture 9. Автоматически создаёт материал + UV ``` ### Инпейнтинг (исправление швов / правка) ``` 1. Откройте сгенерированную текстуру в Image Editor 2. Замаскируйте проблемную область (brush tool) 3. Dream Textures → Inpaint 4. Промпт: "seamless continuation of stone texture" 5. Генерирует только замаскированную область ``` ### Аутпейнтинг (расширение текстуры) ``` 1. Image Editor → выберите текстуру 2. Dream Textures → Outpaint 3. Выберите начало отсчёта (центр, край, угол) 3. Промпт: "extend brick wall pattern" 4. Увеличивает размер холста ``` ### Ре-стилизация через render pass ``` 1. Настройте сцену Cycles с базовым освещением 2. Добавьте ноду "Dream Texture" в Compositor 3. Соедините Render Layers → Dream Texture → Composite 4. Промпт: "cyberpunk neon, volumetric fog, photorealistic" 5. Повторный рендер → ИИ преобразует каждый кадр ``` ### Апскейл средствами ИИ ``` 1. Текстура в низком разрешении (512x512) 2. Dream Textures → Upscale 3. Модель: stabilityai/stable-diffusion-x4-upscaler 4. Выход: 2048x2048 ``` --- ## Метод 2: Poly Haven Textures (бесплатно, отобрано вручную) **Лучше всего для:** готовых к продакшену PBR, стабильного качества **Стоимость:** бесплатно (CC0) **Качество:** профессиональное, 1k-8k ### Воркфлоу (через blender-mcp / GenesisCore) ```bash # Через ИИ: "Search Poly Haven textures for 'metal' category 'rusted' at 2k resolution Download 'RustedIron' and apply to object 'Pipe'" # Шаги: 1. search_polyhaven_assets(asset_type="textures", categories="metal") 2. download_polyhaven_asset(asset_id="RustedIron", asset_type="textures", resolution="2k") 3. set_texture(object_name="Pipe", texture_id="RustedIron") ``` ### Какие карты входят в набор - Base Color (Diffuse) — базовый цвет - Roughness — шероховатость - Normal (OpenGL) — нормали - Metallic — металличность - AO (Ambient Occlusion) — затенение окружения - Height/Displacement — высота/смещение ### Варианты разрешения | Разрешение | Применение | |------------|------------| | 1k | Превью, мобильные устройства, фон | | 2k | Стандартный продакшен | | 4k | Ключевые ассеты, крупные планы | | 8k | Предельная детализация, кино | --- ## Метод 3: Процедурный / нодовый (средства самого Blender) **Лучше всего для:** бесконечного разнообразия, параметричности, GPU не требуется **Стоимость:** бесплатно **Качество:** математическая точность ### Паттерн на шуме Вороного ```python # Выполнять через execute_blender_code import bpy mat = bpy.data.materials.new("ProceduralMetal") mat.use_nodes = True nodes = mat.node_tree.nodes links = mat.node_tree.links nodes.clear() # Ноды output = nodes.new('ShaderNodeOutputMaterial') principled = nodes.new('ShaderNodeBsdfPrincipled') voronoi = nodes.new('ShaderNodeTexVoronoi') color_ramp = nodes.new('ShaderNodeValToRGB') mapping = nodes.new('ShaderNodeMapping') tex_coord = nodes.new('ShaderNodeTexCoord') # Раскладка tex_coord.location = (-600, 0) mapping.location = (-400, 0) voronoi.location = (-200, 0) color_ramp.location = (0, 0) principled.location = (200, 0) output.location = (400, 0) # Настройка voronoi.feature = 'SMOOTH_F1' voronoi.metric = 'MANHATTAN' color_ramp.color_ramp.elements[0].position = 0.3 color_ramp.color_ramp.elements[0].color = (0.1, 0.1, 0.1, 1) color_ramp.color_ramp.elements[1].position = 0.7 color_ramp.color_ramp.elements[1].color = (0.8, 0.7, 0.6, 1) # Связи links.new(tex_coord.outputs['Object'], mapping.inputs['Vector']) links.new(mapping.outputs['Vector'], voronoi.inputs['Vector']) links.new(voronoi.outputs['Color'], color_ramp.inputs['Fac']) links.new(color_ramp.outputs['Color'], principled.inputs['Base Color']) links.new(voronoi.outputs['Distance'], principled.inputs['Roughness']) links.new(principled.outputs['BSDF'], output.inputs['Surface']) # Анимировать масштаб mapping mapping.inputs['Scale'].default_value = (5, 5, 5) ``` ### Генератор кирпича (Geometry Nodes → текстура) ```python # Создаёт процедурный кирпичный паттерн с раствором # Через Geometry Nodes генерируется меш → запекается в текстуру ``` --- ## Метод 4: Dream Textures + Poly Haven (гибрид) **Лучше всего для:** ключевых ассетов с собственными деталями на отобранной базе ### Воркфлоу ``` 1. База: скачать Poly Haven "ConcreteFloor01" (набор PBR 2k) 2. Доработка: Dream Textures Inpaint - Замаскировать области под "cracks with glowing moss" - "painted graffiti symbols" - "water puddles with reflections" 3. Запекание: объединить в единый набор текстур 4. Применение: один материал со всеми картами ``` --- ## Метод 5: Substance / внешние инструменты **Лучше всего для:** AAA-продакшена, командных пайплайнов **Стоимость:** подписка **Интеграция:** экспорт из Blender → Substance → импорт ### Пайплайн ``` 1. Blender: моделирование + UV unwrap 2. Экспорт: GLTF/FBX с UV 3. Substance Painter: смарт-материалы + кастом 4. Экспорт: PBR-карты (4k) 5. Blender: импорт карт → настройка Principled BSDF ``` --- ## Настройка PBR-материала (стандарт) ### Соединения Principled BSDF ``` Image Texture (Base Color) ──────────────────→ Base Color Image Texture (Roughness) ──────────────────→ Roughness (Color Space: Non-Color) Image Texture (Normal) → Normal Map ────────→ Normal Image Texture (Metallic) ───────────────────→ Metallic (Non-Color) Image Texture (Height) → Displacement ──────→ Displacement (Material Settings) Image Texture (AO) ─────────────────────────→ Mix with Base Color (Multiply) ``` ### Скрипт для сборки нод ```python # Выполнять через execute_blender_code import bpy def setup_pbr_material(obj, name, maps_dict): """maps_dict: {'base_color': img, 'roughness': img, 'normal': img, ...}""" mat = bpy.data.materials.new(name) mat.use_nodes = True nodes = mat.node_tree.nodes links = mat.node_tree.links nodes.clear() principled = nodes.new('ShaderNodeBsdfPrincipled') output = nodes.new('ShaderNodeOutputMaterial') links.new(principled.outputs['BSDF'], output.inputs['Surface']) # Base Color if 'base_color' in maps_dict: tex = nodes.new('ShaderNodeTexImage') tex.image = maps_dict['base_color'] tex.image.colorspace_settings.name = 'sRGB' links.new(tex.outputs['Color'], principled.inputs['Base Color']) # Roughness if 'roughness' in maps_dict: tex = nodes.new('ShaderNodeTexImage') tex.image = maps_dict['roughness'] tex.image.colorspace_settings.name = 'Non-Color' links.new(tex.outputs['Color'], principled.inputs['Roughness']) # Normal if 'normal' in maps_dict: tex = nodes.new('ShaderNodeTexImage') tex.image = maps_dict['normal'] tex.image.colorspace_settings.name = 'Non-Color' normal_map = nodes.new('ShaderNodeNormalMap') links.new(tex.outputs['Color'], normal_map.inputs['Color']) links.new(normal_map.outputs['Normal'], principled.inputs['Normal']) # Metallic if 'metallic' in maps_dict: tex = nodes.new('ShaderNodeTexImage') tex.image = maps_dict['metallic'] tex.image.colorspace_settings.name = 'Non-Color' links.new(tex.outputs['Color'], principled.inputs['Metallic']) # Height/Displacement if 'height' in maps_dict: tex = nodes.new('ShaderNodeTexImage') tex.image = maps_dict['height'] tex.image.colorspace_settings.name = 'Non-Color' disp = nodes.new('ShaderNodeDisplacement') links.new(tex.outputs['Color'], disp.inputs['Height']) links.new(disp.outputs['Displacement'], output.inputs['Displacement']) # Включить в Material Settings mat.displacement_method = 'BOTH' # Назначение материала if obj.data.materials: obj.data.materials[0] = mat else: obj.data.materials.append(mat) return mat ``` --- ## Рекомендации по разрешению текстур | Тип ассета | Дистанция просмотра | Разрешение | |------------|----------------------|------------| | Ключевой пропс (hero) | Крупный план | 4k-8k | | Персонаж | Средняя | 2k-4k | | Окружение | Средняя | 2k | | Фон | Дальняя | 1k-2k | | Мобайл/веб | Любая | 1k-2k | | VR/AR | Ближняя | 2k-4k | --- ## Советы по оптимизации ### Управление VRAM (Dream Textures) ```bash # Preferences → Dream Textures: - Precision: fp16 (по умолчанию) - Speed: Half Precision - Memory: Attention Slicing (Auto) - Tile Size: 512 (для апскейла) ``` ### Пакетная генерация текстур ```python # Сгенерировать несколько вариантов for i, prompt in enumerate(prompts): bpy.ops.dream_texture.generate( prompt=prompt, seed=i * 1000, width=512, height=512, seamless=True ) # Сохранить каждый img = bpy.data.images[-1] img.filepath_raw = f"//textures/tex_{i:03d}.png" img.save() ``` ### Соглашение по именам ``` {asset}_{map}_{resolution}.{ext} Примеры: - wall_basecolor_2k.png - wall_roughness_2k.png - wall_normal_2k.exr - wall_height_2k.png - wall_metallic_2k.png - wall_ao_2k.png ``` --- ## Решение проблем | Проблема | Решение | |----------|---------| | Видны швы | включите "Seamless" в Dream Textures; прогоните Inpaint по краям | | Размытая текстура | увеличьте разрешение; проверьте масштаб UV; возьмите модель качественнее | | Неверные цвета | проверьте Color Space (sRGB vs Non-Color) у каждой карты | | Перевёрнутые нормали | инвертируйте Y в ноде Normal Map; проверьте OpenGL vs DirectX | | Displacement не работает | Material Settings → Displacement: "BOTH" или "Displacement Only" | | OOM | снизьте разрешение; включите Attention Slicing; закройте другие программы | ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [ANIMATION WORKFLOWS](animation.md), [BLENDER MATERIALS API REFERENCE](../api/materials.md), [BLENDER MESH OPERATIONS API REFERENCE](../api/mesh-operations.md), [BLENDER PYTHON API REFERENCE (4.x)](../api/blender-api.md), [BLENDER SCRIPTING PATTERNS - SAFE & EFFECTIVE](../api/scripting-patterns.md) - **По темам:** [BLENDER-MCP - FULL DOCUMENTATION](../addons/blender-mcp-full.md), [BLENDER-MCP ADDON - DEEP DIVE](../addons/blender-mcp.md), [CASE 03: PRODUCT VISUALIZATION](../../../cases/case_03_product_viz.md) - **Теги:** #reference, #texturing --- # ВОРКФЛОУ TEXT-TO-3D Tags: #ai-3d #blender #mcp #reference # ВОРКФЛОУ TEXT-TO-3D ## Обзор Несколько подходов к генерации 3D-моделей из текстовых промптов. --- ## Метод 1: Hyper3D Rodin (через blender-mcp) **Лучше всего для:** быстрых концепт-моделей, игровых ассетов **Стоимость:** кредиты API (есть бесплатный тариф) **Качество:** среднее-высокое, чистая топология ### Воркфлоу ```bash # 1. Включить в Blender N-panel → MCP for Blender → отметить "Use Hyper3D" → добавить API-ключ (Preferences) # 2. Через ИИ (OpenCode/Claude): "Generate a 3D model of a medieval fantasy sword using Hyper3D Rodin" # → Создаёт задачу → опрашивает статус → импортирует результат # 3. Постобработка: "Decimate the sword to 2000 tris, add bevel, create metal material" ``` ### Параметры - `prompt`: текстовое описание - `seed`: для воспроизводимости - `generate_textures`: необязательная генерация PBR --- ## Метод 2: Hunyuan3D (через blender-mcp) **Лучше всего для:** ассетов высокого качества, китайских/азиатских тем **Стоимость:** требуются SecretId/SecretKey **Качество:** высокое, годится для продакшена ### Воркфлоу ```bash # 1. Получить учётные данные: https://3d.hunyuan.tencent.com/ # 2. Включить в Blender: отметить "Use Hunyuan3D" → добавить SecretId/SecretKey/API URL # 3. Через ИИ: "Create a 3D model of a Chinese dragon using Hunyuan3D" # → Задача → опрос → импорт # 4. Чистка: "Separate loose parts, apply scale, create materials for each part" ``` --- ## Метод 3: TripoSR (локально, бесплатно) **Лучше всего для:** приватности, офлайна, экспериментов **Стоимость:** бесплатно (работает локально) **Качество:** ниже, годится для концептов **Железо:** рекомендуется 8GB+ VRAM ### Установка ```bash # Вариант A: pip pip install triposr # Вариант B: клонировать репозиторий git clone https://github.com/VAST-AI-Research/TripoSR cd TripoSR pip install -r requirements.txt # Скачать веса (автоматически при первом запуске) ``` ### Использование из Python ```python # Выполнять через execute_blender_code import bpy from triposr import TripoSR model = TripoSR.from_pretrained("VAST-AI/TripoSR") output = model.generate("a cute robot", num_inference_steps=50) # output — словарь с мешем → импортировать в Blender ``` ### Интеграция с Blender ```python # Сохранить как .obj/.glb, затем импортировать import bpy bpy.ops.wm.obj_import(filepath="output.obj") # или bpy.ops.import_scene.gltf(filepath="output.glb") ``` --- ## Метод 4: MeshGPT / Shap-E (локально) **Лучше всего для:** исследований, собственного обучения **Стоимость:** бесплатно **Качество:** экспериментальное ### Shap-E (OpenAI) ```bash git clone https://github.com/openai/shap-e cd shap-e pip install -e . # Скачать веса ``` ```python from shap_e.diffusion.sample import sample_latents from shap_e.diffusion.gaussian_diffusion import diffusion_from_config from shap_e.models.download import load_model, load_config from shap_e.util.notebooks import decode_latent_mesh xm = load_model('transmitter', device='cuda') model = load_model('text300M', device='cuda') diffusion = diffusion_from_config(load_config('diffusion')) latents = sample_latents( batch_size=1, model=model, diffusion=diffusion, prompt="a red sports car", guidance_scale=15.0, device='cuda' ) for i, latent in enumerate(latents): mesh = decode_latent_mesh(xm, latent).tri_mesh() with open(f'car_{i}.obj', 'w') as f: mesh.write_obj(f) ``` --- ## Метод 5: LGM (Large Gaussian Model) **Лучше всего для:** высокого качества и скорости (Gaussian splatting) **Стоимость:** бесплатно (локально) ```bash git clone https://github.com/ashawkey/LGM cd LGM pip install -r requirements.txt ``` --- ## Сравнительная матрица | Метод | Локально/облако | Стоимость | Качество | Скорость | Топология | UV | |-------|-----------------|-----------|----------|----------|-----------|----| | **Hyper3D** | Облако | $ | ⭐⭐⭐⭐ | Быстро | Хорошая | Есть | | **Hunyuan3D** | Облако | $ | ⭐⭐⭐⭐⭐ | Средне | Отличная | Есть | | **TripoSR** | Локально | Бесплатно | ⭐⭐⭐ | Быстро | Базовая | Базовые | | **Shap-E** | Локально | Бесплатно | ⭐⭐ | Медленно | Базовая | Нет | | **LGM** | Локально | Бесплатно | ⭐⭐⭐⭐ | Быстро | Н/П (сплаты) | Н/П | --- ## Рекомендуемый пайплайн (2026) ### Для продакшен-ассетов ``` 1. Сгенерировать базу: Hyper3D или Hunyuan3D (облако) 2. Импорт → Blender 3. Чистка: Decimate, Separate, Scale, Origin 4. Ретопология: Quad Remesher / Instant Meshes 5. UV: Smart UV Project / вручную 6. Запекание: Normal, AO, Curvature из high-poly 7. Текстуры: Substance Painter / Dream Textures 6. Экспорт: GLTF (игра) / USD (пайплайн) ``` ### Для быстрого прототипа ``` 1. TripoSR (локально, бесплатно) → быстрый концепт 2. Если перспективно → Hyper3D ради качества 3. GenesisCore для сборки сцены 4. Dream Textures для материалов ``` ### Для приватности / local-first ``` 1. Ollama (llama3.2/codellama) через GenesisCore 2. TripoSR для генерации 3D 3. Dream Textures (локальный SD) для текстур 4. Всё офлайн, без API-ключей ``` --- ## Шаблон скрипта автоматизации ```python # Выполнять через execute_blender_code import bpy import requests import time def generate_hyper3d(prompt, api_key): # Отправить задачу resp = requests.post( "https://api.hyper3d.ai/v1/jobs", headers={"Authorization": f"Bearer {api_key}"}, json={"prompt": prompt, "generate_textures": True} ) job_id = resp.json()["job_id"] # Опрашивать статус while True: time.sleep(10) status = requests.get( f"https://api.hyper3d.ai/v1/jobs/{job_id}", headers={"Authorization": f"Bearer {api_key}"} ).json() if status["status"] == "completed": return status["result_url"] elif status["status"] == "failed": raise Exception(status["error"]) # Использование url = generate_hyper3d("futuristic motorcycle", "YOUR_KEY") # Скачать и импортировать import urllib.request urllib.request.urlretrieve(url, "/tmp/model.glb") bpy.ops.import_scene.gltf(filepath="/tmp/model.glb") ``` --- ## Чек-лист после генерации - [ ] **Scale:** применить масштаб (Ctrl+A → Scale), попасть в реальный размер - [ ] **Origin:** выставить в центр геометрии (Right-click → Set Origin → Origin to Geometry) - [ ] **Topology:** проверить трисы/квады, при необходимости decimate - [ ] **Separation:** разделить Loose Parts (P → By Loose Parts) под материалы - [ ] **Naming:** переименовать объекты осмысленно - [ ] **Materials:** назначить/создать PBR-материалы - [ ] **UVs:** развернуть (Smart UV Project), проверить наложения - [ ] **LODs:** создать LOD-версии (50%, 25%, 10%) - [ ] **Export:** GLTF для веба/игры, USD для пайплайна --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [BLENDER-MCP - FULL DOCUMENTATION](../addons/blender-mcp-full.md), [BLENDER-MCP ADDON - DEEP DIVE](../addons/blender-mcp.md), [INSTALLATION GUIDE - COMPLETE SETUP](../guides/installation.md), [MCP ARCHITECTURE - MODEL CONTEXT PROTOCOL FOR BLENDER](../mcp/architecture.md), [TROUBLESHOOTING GUIDE](../guides/troubleshooting.md) - **По темам:** [SETUP 45: HUNYUAN3D 2.1 LOCAL](../../../setups/setup_45_hunyuan3d_21.md), [WORKFLOWS - PRODUCTION PIPELINES](scene-building.md), [BLENDER MATERIALS API REFERENCE](../api/materials.md) - **Теги:** #ai-3d, #blender, #mcp, #reference --- ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность ⭐⭐⭐⭐. --- # ГАЙД ПО ПРОМПТАМ — ЭФФЕКТИВНЫЕ КОМАНДЫ ДЛЯ BLENDER Tags: #blender #reference # ГАЙД ПО ПРОМПТАМ — ЭФФЕКТИВНЫЕ КОМАНДЫ ДЛЯ BLENDER ## Основные принципы ### 1. Конкретизируйте геометрию ``` ❌ "Make a house" ✅ "Create a low-poly house: 6x8m base, 4m walls, gabled roof 3m high, 1x2m door centered front, two 1x1m windows on each side" ``` ### 2. Ссылайтесь на понятия Blender ``` ✅ "Add a Subdivision Surface modifier (levels=2) to the cube" ✅ "Create a Principled BSDF material: Base Color #8B4513, Roughness 0.8" ✅ "Set object origin to geometry center (Right-click → Set Origin → Origin to Geometry)" ``` ### 3. Разбивайте сложные задачи на шаги ``` Step 1: "Create the base geometry for a medieval tower" Step 2: "Add window openings using boolean difference with cylinders" Step 3: "Apply stone material with moss in crevices" Step 4: "Add crenellations on top using array modifier" Step 5: "Set up 3-point lighting for dramatic rendering" ``` --- ## Шаблоны промптов по задачам ### Создание объектов ``` "Create a [PRIMITIVE] named [NAME] at location [X,Y,Z] with scale [X,Y,Z] rotation [X,Y,Z] (radians). Primitives: CUBE, SPHERE, CYLINDER, PLANE, CONE, TORUS, EMPTY, CAMERA, LIGHT" Example: "Create a CYLINDER named 'Column' at (0,0,0) scale (0.3,0.3,5) rotation (0,0,0)" ``` ### Назначение материалов ``` "Create a Principled BSDF material named [NAME] with: Base Color: [R,G,B] or hex #RRGGBB Roughness: [0-1] Metallic: [0-1] [Optional: Transmission, IOR, Clearcoat, Emission] Apply to object [OBJECT_NAME]" Example: "Create material 'Gold' with Base Color #FFD700, Roughness 0.1, Metallic 1.0, Apply to 'Ring'" ``` ### Схемы освещения ``` "Create [LIGHT_TYPE] light named [NAME] at [LOCATION] with: Energy: [WATTS] Color: [R,G,B] or temperature [K] [For AREA: size, spread] [For SUN: angle]" Common setups: - "3-point studio lighting: Key (1000W, 5600K, 45°), Fill (300W, 6500K, -30°), Rim (500W, 5600K, 180°)" - "HDRI lighting: Search Poly Haven for 'studio' HDRI, 2k resolution, set as world" - "Volumetric fog: Add cube domain, Principled Volume density 0.1, Sun light through" ``` ### Настройка камеры ``` "Add CAMERA named [NAME] at [LOCATION] rotation [EULER] Set as active camera: bpy.context.scene.camera = bpy.data.objects['NAME'] [Optional: Lens 50mm, Sensor 36mm, DOF focus on object]" ``` ### Ассеты Poly Haven ``` "Search Poly Haven for [ASSET_TYPE: models/hdris/textures] with [categories: ['category1', 'category2']] [tags: ['tag1']] Download [ASSET_ID] at [RESOLUTION: 1k/2k/4k/8k] [For textures: apply to object [NAME]]" Examples: "Search Poly Haven models categories ['furniture', 'seating'] download 'ArmChair_01' at 2k" "Search Poly Haven hdris categories ['indoor'] download 'abandoned_bakery' at 2k set as world" "Search Poly Haven textures categories ['metal'] download 'RustedIron' at 2k apply to 'Pipe'" ``` ### Poly Pizza (low-poly) ``` "Search Poly Pizza for [QUERY] category [CATEGORY_NAME] licence [CC0/CC-BY] animated [true/false] limit [N] Download [MODEL_ID] normalize_size true target_size [METERS]" Categories: Food & Drink, Clutter, Weapons, Transport, Furniture & Decor, Objects, Nature, Animals, Buildings, People & Characters, Scenes & Levels, Other ``` ### Sketchfab (высокое качество) ``` "Search Sketchfab for [QUERY] categories [CATEGORY] count [N] downloadable true Get preview for [UID] Download [UID] target_size [METERS]" ``` ### Генерация 3D через ИИ (Hyper3D/Hunyuan3D) ``` "Generate 3D model of [DESCRIPTION] using Hyper3D Rodin [or Hunyuan3D] Poll status until complete Import result" ``` ### Сборка сцены ``` "Create a [SCENE_TYPE] scene: - [LIST OF OBJECTS WITH SPECS] - Lighting: [SETUP] - Camera: [POSITION/TYPE] - Render settings: Cycles/EEVEE, samples, resolution" ``` --- ## Продвинутые приёмы составления промптов ### Итеративное уточнение ``` 1. "Create a basic chair shape with primitives" 2. "Add subdivision surface and bevel to smooth edges" 3. "Create fabric material for seat, wood for legs" 4. "Add stitching detail using geometry nodes or displacement" 5. "Set up turntable animation 360° over 120 frames" ``` ### Условная логика ``` "If the object 'Wall' exists, add a window opening at height 1.2m. Otherwise, create a wall first then add window." ``` ### Пакетные операции ``` "For each selected object: 1. Apply scale (Ctrl+A → Scale) 2. Add bevel modifier (width=0.02, segments=3) 3. Shade smooth 4. Create unique material with random hue" ``` ### Выполнение кода (когда инструментов не хватает) ``` "Execute this Blender Python code: ``` import bpy for obj in bpy.context.selected_objects: obj.modifiers.new('Bevel', 'BEVEL') obj.modifiers['Bevel'].width = 0.02 obj.modifiers['Bevel'].segments = 3 bpy.ops.object.shade_smooth() ```" ``` --- ## Советы по провайдерам ### OpenRouter (GenesisCore) - **Лучше всего для кода:** `deepseek/deepseek-chat`, `qwen/qwen-2.5-coder-32b` - **Лучше всего для рассуждений:** `anthropic/claude-3.5-sonnet` - **Самый дешёвый:** `meta-llama/llama-3.1-8b-instruct` (простые задачи) - Всегда указывайте модель в настройках GenesisCore ### blender-mcp (любой клиент) - Работает с любым MCP-клиентом (Claude, Cursor, Codex, OpenCode) - Инструменты самодокументируемые — LLM знает их параметры - Используйте `execute_blender_code` для произвольных операций --- ## Шпаргалка по типовым паттернам | Задача | Шаблон промпта | |------|----------------| | **Примитив** | "Create [TYPE] named [NAME] at [LOC] scale [SCALE]" | | **Материал** | "Create Principled BSDF [NAME]: color [RGB], roughness [0-1], metallic [0-1] apply to [OBJ]" | | **Модификатор** | "Add [MODIFIER_TYPE] to [OBJ] with [PARAMS]" | | **Bool** | "Boolean [UNION/DIFFERENCE/INTERSECT] [OBJ_A] with [OBJ_B]" | | **Array** | "Add Array modifier to [OBJ]: count [N], offset [X,Y,Z]" | | **Кривая** | "Create Bezier curve through points [LIST], bevel depth [RADIUS]" | | **Частицы** | "Add Particle System to [OBJ]: type HAIR, count [N], length [L]" | | **Rigid Body** | "Add Rigid Body to [OBJ]: type ACTIVE/PASSIVE, mass [KG]" | | **Анимация** | "Animate [OBJ] location from [START] to [END] over [FRAMES] frames" | | **Driver** | "Add driver to [OBJ].[PROP] driven by [TARGET].[PROP] with expression [EXPR]" | | **Geo Nodes** | "Create Geometry Nodes tree on [OBJ]: [NODE DESCRIPTION]" | --- ## Восстановление после ошибок ### Если ИИ застрял ``` "Reset: Delete all objects created in this session. Start fresh with: [CLEAR SPECIFICATION]" ``` ### Если инструмент не сработал ``` "The previous command failed with [ERROR]. Try alternative approach: [SUGGESTION]" ``` ### Если результат неверен ``` "Undo the last operation. The issue is [DESCRIPTION]. Instead: [CORRECTED INSTRUCTION]" ``` --- ## Оптимизация токенов ### Грамотно используйте историю - **GenesisCore:** выключайте "Use History" для независимых команд - **blender-mcp:** каждый вызов инструмента независим, контекст не нужен ### Краткие промпты ``` ❌ "I would like you to please create a cube that is red in color and put it at the origin" ✅ "Create CUBE named 'RedCube' at (0,0,0) material red" ``` ### Объединяйте связанные операции ``` ✅ Один промпт: "Create 5 cubes in a row at x=-2,-1,0,1,2 with materials red,blue,green,yellow,purple" ``` --- ## Отладочные промпты ### Инспекция сцены ``` "Get full scene info including all objects, materials, lights, camera" "Get object info for [NAME] - show vertices, faces, materials, modifiers" "Take viewport screenshot" ``` ### Проверка статуса аддона ``` "Get addon info - show version, capabilities, protocol version" "Check Poly Haven status" "Check Poly Pizza status" ``` --- ## Промпты под конкретные модели ### DeepSeek / Qwen (упор на код) ``` "Write Blender Python code to: [ALGORITHMIC DESCRIPTION] Use bpy.data, bpy.ops, mathutils. No external imports." ``` ### Claude (упор на рассуждения) ``` "Plan the steps to create [COMPLEX SCENE], then execute step by step. Explain each step before executing." ``` ### GPT-4o (сбалансированно) ``` "Create [SCENE] following best practices for [STYLE/TECHNIQUE]. Optimize for render time / poly count / editability." ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [BLENDER MATERIALS API REFERENCE](../api/materials.md), [BLENDER MESH OPERATIONS API REFERENCE](../api/mesh-operations.md), [BLENDER PYTHON API REFERENCE (4.x)](../api/blender-api.md), [BLENDER SCRIPTING PATTERNS - SAFE & EFFECTIVE](../api/scripting-patterns.md), [BLENDER-MCP - FULL DOCUMENTATION](../addons/blender-mcp-full.md) - **По темам:** [BLENDER-MCP ADDON - DEEP DIVE](../addons/blender-mcp.md), [INSTALLATION GUIDE - COMPLETE SETUP](installation.md), [MCP ARCHITECTURE - MODEL CONTEXT PROTOCOL FOR BLENDER](../mcp/architecture.md) - **Теги:** #blender, #reference, #rrggbb --- ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- # ГАЙД ПО РЕШЕНИЮ ПРОБЛЕМ Tags: #addon #blender #mcp #reference # ГАЙД ПО РЕШЕНИЮ ПРОБЛЕМ ## Проблемы с установкой Blender ### Сбой установки MSI (код возврата 1620) ```powershell # Проверить лог установки Get-Content C:\BLENDER ai\install.log -Tail 50 # Типичные причины: # - Требуется перезагрузка # - Уже запущен другой процесс установки MSI # - Недостаточно прав (запускать от имени администратора) # - Повреждённая загрузка # Решение: скачать заново, запустить от имени администратора Start-Process msiexec.exe -ArgumentList "/i C:\BLENDER ai\blender-4.2.3-windows-x64.msi /qb" -Verb RunAs -Wait ``` ### Blender не найден после установки ```powershell # Проверить типовые расположения Test-Path "C:\Program Files\Blender Foundation\Blender 4.2\blender.exe" Test-Path "C:\Program Files\Blender Foundation\Blender 4.1\blender.exe" Get-ChildItem "$env:LOCALAPPDATA\Programs\Blender Foundation\" -Recurse -Filter blender.exe Get-ChildItem "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\" -Recurse -Filter "*lender*" # Используйте portable-версию # Распакуйте ZIP в C:\BLENDER ai\Blender\ и запустите blender.exe напрямую ``` --- ## Проблемы с uv / uvx ### `uvx` не найден / `spawn uvx ENOENT` ```powershell # 1. Полностью перезапустите PowerShell/терминал # 2. Проверьте PATH $env:PATH -split ";" | Where-Object { $_ -like "*\.local\bin*" } # 3. Добавьте вручную, если пути нет $localBin = "$env:USERPROFILE\.local\bin" $userPath = [Environment]::GetEnvironmentVariable("Path", "User") [Environment]::SetEnvironmentVariable("Path", "$userPath;$localBin", "User") # 4. Укажите полный путь в конфиге MCP where uvx # → C:\Users\\.local\bin\uvx.exe ``` ### Конфликты версий / проблемы с кэшем ```bash uv cache clean blender-mcp uv cache clean --all uvx --refresh blender-mcp ``` ### Проблемы с версией Python ```json // В конфиге MCP зафиксируйте Python 3.11 { "args": ["--python", "3.11", "blender-mcp"], "env": { "UV_PYTHON_PREFERENCE": "only-managed" } } ``` --- ## Проблемы с подключением MCP ### "Connection Refused" / "Cannot Connect to Blender" 1. **Аддон в Blender не запущен:** - Откройте Blender → N-панель → **MCP for Blender** → нажмите **Start MCP Server** - Либо для GenesisCore: N-панель → **GenesisCore** → нажмите **Connect** 2. **Неверный порт:** - blender-mcp: порт 9876 (настраивается через `BLENDER_PORT`) - GenesisCore: порт 45677 (внутренний) 3. **Блокировка брандмауэром/антивирусом:** - Разрешите `python.exe` / `blender.exe` в брандмауэре Windows - Проверьте профиль частной сети ### Таймаут на командах ```bash # Разбейте сложные запросы на меньшие шаги # Вместо: "Build entire city with 50 buildings" # Делайте: "Create one building template" → "Array it 10x" → "Add variations" # При необходимости увеличьте таймаут в конфиге клиента ``` ### Инструменты не появляются в AI-клиенте ```bash # 1. Полностью перезапустите AI-клиент (не только окно) # - Windows: выход из системного трея # - macOS: Cmd+Q # 2. Проверьте синтаксис конфига (валидные JSON/TOML) # 3. Убедитесь, что uvx работает в терминале: uvx blender-mcp --help ``` --- ## Специфика GenesisCore ### "Connecting..." зависает навсегда ```bash # 1. Проверьте, что порт 45677 не занят netstat -an | findstr 45677 # 2. Полностью перезапустите Blender # 3. Загляните в System Console (Window → Toggle System Console) на предмет ошибок # 4. Отключите другие аддоны, которые могут конфликтовать ``` ### Список моделей пуст / "Failed to Fetch Models" ```bash # 1. Убедитесь, что API-ключ верный # 2. Проверьте интернет-соединение / прокси # 3. Попробуйте другого провайдера (DeepSeek, OpenRouter, Ollama) # 4. Для Ollama: убедитесь, что ollama serve запущен, а модель скачана (ollama pull deepseek-coder) ``` ### Инструменты не выполняются ```bash # 1. Включите модули инструментов в N-панели (Shift+клик для множественного выбора) # Обязательные: Common Tools, Object Tools, Material Tools # Опциональные: Polyhaven Tools, Asset Tools # 2. Проверьте System Console на ошибки Python # 3. Некоторым инструментам нужен определённый контекст (например, активный объект) ``` --- ## Проблемы Dream Textures ### Не хватает VRAM / CUDA OOM ```bash # Снизьте разрешение: максимум 512x512 для 4 ГБ VRAM # Используйте fp16 (half precision) — значение по умолчанию # Закройте другие программы, нагружающие GPU # Включите Attention Slicing (Auto) в настройках # Уменьшите число шагов: 20-30 вместо 50 ``` ### Сбой скачивания модели ```bash # 1. Проверьте интернет # 2. Для моделей с ограниченным доступом: задайте HF_TOKEN в настройках # 3. Попробуйте другую модель: stabilityai/stable-diffusion-2-1-base # 4. Ручная загрузка: huggingface.co → download → Import Checkpoint ``` ### Карантин macOS (не Windows) ```bash xattr -r -d com.apple.quarantine ~/Library/Application\ Support/Blender/3.3/scripts/addons/dream_textures/.python_dependencies ``` ### Медленная генерация ```bash # Используйте меньше шагов (20-30) # Меньшее разрешение (512x512) # Scheduler: DPM++ 2M Karras (быстрее) # Включите Speed Optimizations: Half Precision ``` --- ## Специфика blender-mcp ### Сбой загрузки Poly Pizza (Cloudflare 403) ``` Error: "Poly Pizza's CDN returned a Cloudflare bot-protection challenge" Исправление: 1. Скачайте .glb вручную с https://poly.pizza 2. File → Import → glTF 2.0 3. Либо используйте домашний IP (не VPN и не адрес дата-центра) ``` ### Команды не выполняются в headless Blender (`blender -b`) ``` blender-mcp требует GUI Blender для выполнения команд. Используйте: blender (с GUI) или xvfb-run -a blender (headless в Linux) ``` ### Safe Mode блокирует скрипт ``` Error: "Rejected by safe mode - ..." Исправление: 1. Перепишите без: os, subprocess, open(), eval(), exec() 2. Используйте только: bpy, bmesh, mathutils, math, random, json, uuid 3. Либо отключите: BLENDER_MCP_SAFE_MODE=0 (по умолчанию) ``` --- ## Проблемы интеграции ассетов ### Ассеты Poly Haven не появляются ```bash # 1. Включите "Use Poly Haven" в N-панели Blender → MCP for Blender # 2. Проверьте кэш: %TEMP%\polyhaven_models\ / polyhaven_hdris\ # 3. Очистите кэш и повторите ``` ### Sketchfab требует API-ключа ```bash # 1. Получите ключ: https://sketchfab.com/developers/api # 2. Blender Preferences → Add-ons → MCP for Blender → Sketchfab API Key # 3. Или через env: BLENDERMCP_SKETCHFAB_API_KEY ``` ### Hyper3D / Hunyuan3D не работают ```bash # 1. Включите в N-панели # 2. Добавьте API-ключи в Preferences # 3. Проверьте баланс/квоту в панели провайдера # 4. На бесплатном тарифе могут быть ограничения ``` --- ## Проблемы с производительностью ### Blender зависает / не отвечает ```bash # 1. Ищите бесконечные циклы в System Console # 2. Сократите количество объектов в сцене # 3. Отключите тяжёлые модификаторы (Subsurf с высоким числом уровней) # 4. Используйте более простую геометрию для ИИ-генерации ``` ### Большие .blend-файлы ```bash # File → Clean Up → Unused Data Blocks # File → External Data → Automatically Pack Resources (или распаковать) # Удалите неиспользуемые материалы, текстуры, меши # Используйте Linked libraries для повторяющихся ассетов ``` --- ## Отладочные инструменты ### Системная консоль Blender ``` Windows: Window → Toggle System Console macOS: /Applications/Blender.app/Contents/MacOS/Blender (в терминале) Linux: запустите blender из терминала ``` ### Логи MCP-сервера ```bash # blender-mcp: работает в том терминале, где вы запустили uvx # GenesisCore: смотрите в системную консоль Blender ``` ### Включите подробное логирование ```python # В консоли Python в Blender: import logging logging.basicConfig(level=logging.DEBUG) ``` --- ## Типовые сообщения об ошибках | Ошибка | Причина | Исправление | |-------|-------|-------| | `KeyError: 'object_name'` | Объект не существует | Проверьте написание, используйте `bpy.data.objects.get()` | | `AttributeError: 'NoneType'` | Контекст равен None (background mode) | Запустите Blender с GUI | | `RuntimeError: Operator not available` | Неверный режим (EDIT вместо OBJECT) | `bpy.ops.object.mode_set(mode='OBJECT')` | | `MemoryError` | Не хватает RAM/VRAM | Снизьте разрешение, закройте приложения | | `ModuleNotFoundError` | Отсутствует Python-пакет | Зависимости подтягивает uvx; проверьте .python_dependencies | --- ## Где искать помощь 1. **Системная консоль Blender** — первое, куда стоит заглянуть 2. **GitHub Issues:** - GenesisCore: https://github.com/AIGODLIKE/GenesisCore/issues - blender-mcp: https://github.com/ahujasid/blender-mcp/issues - Dream Textures: https://github.com/carson-katri/dream-textures/issues 3. **Discord:** - blender-mcp: https://discord.gg/SNqPn4TcKQ - Dream Textures: https://discord.gg/EmDJ8CaWZ7 4. **Документация:** эта вики + официальные руководства ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [BLENDER-MCP ADDON - DEEP DIVE](../addons/blender-mcp.md), [INSTALLATION GUIDE - COMPLETE SETUP](installation.md), [BLENDER-MCP - FULL DOCUMENTATION](../addons/blender-mcp-full.md), [GENESISCORE ADDON - DEEP DIVE](../addons/genesiscore.md), [MCP ARCHITECTURE - MODEL CONTEXT PROTOCOL FOR BLENDER](../mcp/architecture.md) - **По темам:** [TEXT-TO-3D WORKFLOWS](../workflows/text-to-3d.md), [BLENDER MATERIALS API REFERENCE](../api/materials.md), [BLENDER MESH OPERATIONS API REFERENCE](../api/mesh-operations.md) - **Теги:** #addon, #blender, #mcp, #reference --- # КОНФИГУРАЦИИ MCP-КЛИЕНТОВ Tags: #mcp #reference # КОНФИГУРАЦИИ MCP-КЛИЕНТОВ ## OpenCode **Конфиг:** `~/.config/opencode/opencode.json` ```json { "mcp": { "blender-mcp": { "type": "local", "command": ["uvx", "blender-mcp"], "enabled": true, "environment": { "BLENDER_HOST": "localhost", "BLENDER_PORT": "9876" } } } } ``` ## Claude Desktop **Конфиг:** `claude_desktop_config.json` - macOS: `~/Library/Application Support/Claude/` - Windows: `%APPDATA%\Claude\` - Linux: `~/.config/claude/` ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["blender-mcp"] } } } ``` **Claude Code CLI:** ```bash claude mcp add blender uvx blender-mcp ``` ## Cursor **Settings → MCP → Add Server** ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["blender-mcp"] } } } ``` **Windows:** используйте `"command": "cmd", "args": ["/c", "uvx", "blender-mcp"]` ## VS Code (с MCP-расширением) **Settings → Extensions → MCP → Add Server** ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["blender-mcp"] } } } ``` ## Codex **CLI:** ```bash codex mcp add blender -- uvx blender-mcp ``` **Конфиг:** `~/.codex/config.toml` ```toml [mcp_servers.blender] command = "uvx" args = ["blender-mcp"] env = { BLENDER_HOST = "localhost", BLENDER_PORT = "9876" } ``` **Десктопное приложение:** Settings → MCP Servers → Add Server → name: `blender`, STDIO, command: `uvx blender-mcp` ## Antigravity ```json { "mcpServers": { "blender-mcp": { "command": "uvx", "args": ["blender-mcp"], "env": { "BLENDER_HOST": "localhost", "BLENDER_PORT": "9876" } } } } ``` ## Docker (альтернатива) ```json { "mcpServers": { "blender": { "command": "docker", "args": ["run", "-i", "--rm", "blender-mcp"] } } } ``` **Linux:** добавьте `"--network=host", "-e", "BLENDER_HOST=localhost"` ## Переменные окружения (все клиенты) ```json "environment": { "BLENDER_HOST": "localhost", "BLENDER_PORT": "9876", "BLENDER_MCP_SAFE_MODE": "0", "DISABLE_TELEMETRY": "false", "UV_PYTHON_PREFERENCE": "only-managed" } ``` ## Фиксация версии Python (рекомендуется) ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["--python", "3.11", "blender-mcp"], "env": { "UV_PYTHON_PREFERENCE": "only-managed" } } } } ``` ## Диагностика подключений клиентов | Проблема | Решение | |-------|-----| | `spawn uvx ENOENT` | Укажите полный путь: `which uvx` / `where uvx` → впишите в `command` | | Connection refused | Аддон Blender не запущен; нажмите "Start MCP Server" | | Инструменты не появляются | Перезапустите клиент после правки конфига (полный выход) | | Таймаут при первом вызове | Это нормально; повторите один раз | ## GenesisCore (встроенный клиент) Внешний MCP-клиент не нужен. Настройте в N-панели Blender: 1. Провайдер: OpenRouter / DeepSeek / OpenAI / Anthropic / Ollama / SiliconFlow 2. API-ключ 3. Выбор модели 4. Модули инструментов (Common, Object, Material, Polyhaven, Asset) 5. Подключение (использует внутренний SSE на :45677) **Внешние клиенты могут подключаться к GenesisCore:** ```json { "mcpServers": { "BlenderGenesis": { "url": "http://localhost:45677" } } } ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [BLENDER-MCP - FULL DOCUMENTATION](../addons/blender-mcp-full.md), [BLENDER-MCP ADDON - DEEP DIVE](../addons/blender-mcp.md), [GENESISCORE ADDON - DEEP DIVE](../addons/genesiscore.md), [INSTALLATION GUIDE - COMPLETE SETUP](../guides/installation.md), [MCP ARCHITECTURE - MODEL CONTEXT PROTOCOL FOR BLENDER](architecture.md) - **По темам:** [MCP SERVER API REFERENCE](server-api.md), [TEXT-TO-3D WORKFLOWS](../workflows/text-to-3d.md), [TROUBLESHOOTING GUIDE](../guides/troubleshooting.md) - **Теги:** #mcp, #reference --- ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- # ПАТТЕРНЫ СКРИПТИНГА В BLENDER — БЕЗОПАСНО И ЭФФЕКТИВНО Tags: #blender #reference # ПАТТЕРНЫ СКРИПТИНГА В BLENDER — БЕЗОПАСНО И ЭФФЕКТИВНО ## Базовые принципы ### 1. Всегда проверяйте существование ```python # ПЛОХО obj = bpy.data.objects["Cube"] obj.location.x = 1 # ХОРОШО obj = bpy.data.objects.get("Cube") if obj: obj.location.x = 1 else: print("Object not found") ``` ### 2. Явный контекст важнее неявного ```python # ПЛОХО (полагается на bpy.context.active_object) bpy.ops.object.shade_smooth() # ХОРОШО (явно) obj = bpy.data.objects["Cube"] bpy.context.view_layer.objects.active = obj obj.select_set(True) bpy.ops.object.shade_smooth() ``` ### 3. Учитывайте режим ```python # Перед операциями убедиться, что задан правильный режим def ensure_mode(mode='OBJECT'): if bpy.context.mode != mode: bpy.ops.object.mode_set(mode=mode) # Использование ensure_mode('OBJECT') # ... операции с объектом ... ensure_mode('EDIT') bm = bmesh.from_edit_mesh(obj.data) # ... операции bmesh ... bmesh.update_edit_mesh(obj.data) ensure_mode('OBJECT') ``` --- ## Шаблон безопасного выполнения ### Для MCP `execute_blender_code` ```python def safe_execute(operation_name, func): """ Обёртка для безопасного выполнения кода в MCP. Возвращает dict с информацией об успехе/ошибке. """ try: # Убедиться, что мы в режиме объекта if bpy.context.mode != 'OBJECT': bpy.ops.object.mode_set(mode='OBJECT') result = func() # Принудительное обновление bpy.context.view_layer.update() return { "success": True, "result": result, "operation": operation_name } except Exception as e: import traceback traceback.print_exc() return { "success": False, "error": str(e), "operation": operation_name, "traceback": traceback.format_exc() } # Использование def create_scene(): # ... ваш код ... return {"created": ["Cube", "Light", "Camera"]} result = safe_execute("create_scene", create_scene) ``` --- ## Общие паттерны ### Паттерн 1: создание объекта с материалом ```python def create_object_with_material(name, primitive_type, location, material_name, color): """Создать примитив, добавить материал, назначить""" # Создать меш if primitive_type == 'CUBE': bpy.ops.mesh.primitive_cube_add(location=location) elif primitive_type == 'SPHERE': bpy.ops.mesh.primitive_uv_sphere_add(location=location) # ... другие типы obj = bpy.context.active_object obj.name = name # Создать материал mat = bpy.data.materials.new(material_name) mat.use_nodes = True nodes = mat.node_tree.nodes links = mat.node_tree.links nodes.clear() principled = nodes.new('ShaderNodeBsdfPrincipled') output = nodes.new('ShaderNodeOutputMaterial') links.new(principled.outputs['BSDF'], output.inputs['Surface']) principled.inputs['Base Color'].default_value = color + (1.0,) if len(color) == 3 else color principled.inputs['Roughness'].default_value = 0.5 principled.inputs['Metallic'].default_value = 0.0 # Назначить if obj.data.materials: obj.data.materials[0] = mat else: obj.data.materials.append(mat) return obj ``` ### Паттерн 2: пакетное создание с вариациями ```python def create_variations(base_name, count, position_func, variation_func): """Создать несколько объектов с вариациями""" objects = [] for i in range(count): loc = position_func(i) obj = create_object_with_material( f"{base_name}_{i:03d}", 'CUBE', loc, f"Mat_{base_name}_{i:03d}", variation_func(i) ) objects.append(obj) return objects # Использование def grid_position(i, cols=5): row = i // cols col = i % cols return (col * 3, row * 3, 0) def color_variation(i): import colorsys hue = (i * 0.618) % 1.0 # распределение по золотому сечению r, g, b = colorsys.hsv_to_rgb(hue, 0.7, 0.9) return (r, g, b) objs = create_variations("Brick", 20, grid_position, color_variation) ``` ### Паттерн 3: иерархическая структура сцены ```python def setup_scene_hierarchy(): """Создать организованную иерархию коллекций""" # Корневая коллекция root = bpy.data.collections.new("Scene_Root") bpy.context.scene.collection.children.link(root) # Вложенные коллекции collections = {} for name in ["Geometry", "Lights", "Cameras", "FX", "References"]: col = bpy.data.collections.new(name) root.children.link(col) collections[name] = col return collections def move_to_collection(obj, collection_name, collections): """Переместить объект в коллекцию по имени""" # Отвязать от всех for col in obj.users_collection: col.objects.unlink(obj) # Привязать к целевой collections[collection_name].objects.link(obj) ``` ### Паттерн 4: ключевые кадры анимации ```python def animate_property(obj, prop_path, keyframes, interpolation='BEZIER'): """ keyframes: {frame: value, ...} prop_path: например, "location", "rotation_euler", "scale" """ for frame, value in keyframes.items(): setattr(obj, prop_path, value) obj.keyframe_insert(data_path=prop_path, frame=frame) # Задать интерполяцию if obj.animation_data and obj.animation_data.action: for fcurve in obj.animation_data.action.fcurves: if fcurve.data_path == prop_path: for kp in fcurve.keyframe_points: kp.interpolation = interpolation # Использование animate_property(obj, "location", { 1: (0, 0, 0), 50: (10, 0, 0), 100: (10, 10, 0), 150: (0, 10, 0), 200: (0, 0, 0) }) ``` ### Паттерн 5: настройка драйвера ```python def add_driver(obj, prop_path, target_obj, target_prop, expression="var"): """Добавить драйвер к obj.prop_path, управляемый target_obj.target_prop""" driver = obj.driver_add(prop_path).driver driver.type = 'SCRIPTED' var = driver.variables.new() var.name = "var" var.targets[0].id = target_obj var.targets[0].data_path = target_prop driver.expression = expression return driver # Использование # Вращать gear_b по вращению gear_a (соотношение 2:1) add_driver(gear_b, "rotation_euler[2]", gear_a, "rotation_euler[2]", "-var * 2") ``` --- ## Паттерны обработки ошибок ### Контекстный менеджер для смены режимов ```python from contextlib import contextmanager @contextmanager def temp_mode(mode='OBJECT'): """Временно переключить режим, затем восстановить""" original_mode = bpy.context.mode if original_mode != mode: bpy.ops.object.mode_set(mode=mode) try: yield finally: if original_mode != mode: bpy.ops.object.mode_set(mode=original_mode) # Использование with temp_mode('EDIT'): bm = bmesh.from_edit_mesh(obj.data) # ... операции ... bmesh.update_edit_mesh(obj.data) # Автоматический возврат к исходному режиму ``` ### Контекстный менеджер для выделения ```python @contextmanager def temp_selection(objects, active=None): """Временно выделить объекты, затем восстановить выделение""" # Сохранить текущее prev_active = bpy.context.view_layer.objects.active prev_selected = bpy.context.selected_objects[:] # Задать новое bpy.ops.object.select_all(action='DESELECT') for obj in objects: obj.select_set(True) if active: bpy.context.view_layer.objects.active = active elif objects: bpy.context.view_layer.objects.active = objects[0] try: yield finally: # Восстановить bpy.ops.object.select_all(action='DESELECT') for obj in prev_selected: obj.select_set(True) bpy.context.view_layer.objects.active = prev_active # Использование with temp_selection([obj1, obj2, obj3], active=obj1): bpy.ops.object.join() ``` ### Группировка в Undo ```python def with_undo_step(name, func): """Обернуть операцию в шаг Undo""" bpy.ops.ed.undo_push(message=name) try: return func() except Exception: bpy.ops.ed.undo() raise # Использование with_undo_step("Create Building", lambda: create_building()) ``` --- ## Паттерны производительности ### Пакетная обработка ```python def process_objects_batch(objects, process_func, batch_size=100): """Обрабатывать объекты пачками, чтобы избежать проблем с памятью""" results = [] for i in range(0, len(objects), batch_size): batch = objects[i:i+batch_size] with bpy.context.temp_override(selected_objects=batch): batch_results = [process_func(obj) for obj in batch] results.extend(batch_results) # Опционально: принудительный сбор мусора import gc gc.collect() return results ``` ### Отложенные дорогие операции ```python def defer_to_next_frame(func): """Запустить функцию на следующем кадре (полезно для обновления UI)""" def wrapper(): func() return None # Не повторять bpy.app.timers.register(wrapper, first_interval=0.01) # Использование defer_to_next_frame(lambda: heavy_computation()) ``` --- ## Вспомогательные функции для отладки ### Печать сводки по сцене ```python def print_scene_summary(): scene = bpy.context.scene print(f"Scene: {scene.name}") print(f"Objects: {len(scene.objects)}") print(f"Materials: {len(bpy.data.materials)}") print(f"Meshes: {len(bpy.data.meshes)}") print(f"Collections: {len(bpy.data.collections)}") for obj in scene.objects: print(f" {obj.name} ({obj.type}) - {len(obj.data.vertices) if obj.type=='MESH' else 'N/A'} verts") # Быстрый вызов print_scene_summary() ``` ### Поиск проблем ```python def find_mesh_issues(): """Проверить типичные проблемы меша""" issues = [] for obj in bpy.context.scene.objects: if obj.type != 'MESH': continue mesh = obj.data # Неманфолдные рёбра bm = bmesh.new() bm.from_mesh(mesh) non_manifold = [e for e in bm.edges if not e.is_manifold] if non_manifold: issues.append(f"{obj.name}: {len(non_manifold)} non-manifold edges") # Грани нулевой площади zero_area = [f for f in bm.faces if f.calc_area() < 0.0001] if zero_area: issues.append(f"{obj.name}: {len(zero_area)} zero-area faces") # Дублирующиеся вершины coords = [v.co for v in bm.verts] if len(coords) != len(set(str(c) for c in coords)): issues.append(f"{obj.name}: duplicate vertices") bm.free() return issues # Запуск for issue in find_mesh_issues(): print(f"ISSUE: {issue}") ``` --- ## Соответствие safe mode (для blender-mcp) ### Только разрешённые импорты ```python # ✅ РАЗРЕШЕНО import bpy import bmesh import math import mathutils import random import json import uuid import itertools import collections import functools import fractions import decimal import statistics import typing import dataclasses import enum import hashlib import base64 import datetime import time import re import string import textwrap import copy import pprint import numbers import fractions import pathlib import os.path import sys # ❌ ЗАБЛОКИРОВАНО (safe mode) import os # файловый ввод-вывод import subprocess # запуск процессов import sys # доступ к системе import socket # сеть import requests # HTTP import urllib # работа с URL import threading # потоки import multiprocessing import asyncio import importlib # динамические импорты import pkgutil import inspect ``` ### Шаблон безопасного кода ```python # Этот шаблон проходит проверку safe mode import bpy import bmesh import math import mathutils import random import json def main(): # Здесь ваши операции в Blender # Используйте только: bpy.data, bpy.ops, bmesh, mathutils, math, random, json # Пример: процедурное здание for i in range(5): bpy.ops.mesh.primitive_cube_add( location=(i * 3, 0, 0), scale=(1, 1, random.uniform(2, 5)) ) obj = bpy.context.active_object obj.name = f"Building_{i}" return {"created": 5} if __name__ == "__main__": result = main() print(json.dumps(result)) ``` --- ## Совместимость версий ### Проверки версий ```python # Версия Blender if bpy.app.version >= (4, 0, 0): # Новый API bpy.ops.mesh.primitive_cube_add(size=2) else: # Старый API bpy.ops.mesh.primitive_cube_add(radius=1) # Определение наличия возможности has_asset_browser = hasattr(bpy.ops, 'asset') has_geometry_nodes = 'GeometryNodeTree' in dir(bpy.types) # Проверка существования свойства if hasattr(bpy.types.Object, "new_property"): obj.new_property = value ``` --- ## Паттерны тестирования ### Шаблон юнит-теста ```python import unittest import bpy import bmesh class TestMeshOperations(unittest.TestCase): def setUp(self): # Очистить сцену bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete() def test_create_cube(self): obj = create_object_with_material("TestCube", 'CUBE', (0,0,0), "TestMat", (1,0,0)) self.assertEqual(obj.name, "TestCube") self.assertEqual(obj.type, 'MESH') self.assertEqual(len(obj.data.materials), 1) def test_boolean_difference(self): cube = create_object_with_material("Cube", 'CUBE', (0,0,0), "Mat", (1,0,0)) sphere = create_object_with_material("Sphere", 'SPHERE', (0.5,0,0), "Mat", (0,1,0)) result = boolean_difference(cube, sphere) self.assertEqual(result.name, "Cube") # Сфера должна быть удалена self.assertNotIn("Sphere", bpy.data.objects) if __name__ == '__main__': unittest.main() ``` --- ## Лучшие практики интеграции с MCP ### Пошагово для сложных задач ```python # Вместо одной гигантской операции разбейте задачу на шаги: # Шаг 1: подготовка result = execute("Создать коллекции для организации") # Шаг 2: базовая геометрия result = execute("Создать плоскость пола 20x20м в начале координат") # Шаг 3: стены result = execute("Создать 4 стены по периметру пола, высота 3м") # Шаг 4: проёмы result = execute("Булевым вычитанием вырезать дверной проём 1x2м в передней стене") # Шаг 5: крыша result = execute("Создать двускатную крышу над стенами") # Шаг 6: материалы result = execute("Назначить каменный материал стенам, деревянный полу, плиточный крыше") # Шаг 7: освещение result = execute("Добавить трёхточечное освещение: key, fill, rim") # Шаг 8: камера result = execute("Добавить камеру в (-10, -15, 5) с направлением на центр, 35мм") # Шаг 9: настройки рендера result = execute("Настроить Cycles, 128 сэмплов, 1920x1080, вывод в //renders/") ``` ### Проверка после каждого шага ```python def verify_step(step_name, verification_func): """Выполнить шаг и проверить результат""" result = execute(step_name) if not result.get("success"): return {"success": False, "error": f"{step_name} failed: {result.get('error')}"} # Проверка try: verification_func() return {"success": True, "result": result} except AssertionError as e: return {"success": False, "error": f"{step_name} verification failed: {e}"} # Использование def verify_floor_exists(): obj = bpy.data.objects.get("Floor") assert obj and obj.type == 'MESH' assert abs(obj.dimensions.x - 20) < 0.01 assert abs(obj.dimensions.y - 20) < 0.01 result = verify_step("Create floor", verify_floor_exists) ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [BLENDER MATERIALS API REFERENCE](materials.md), [BLENDER MESH OPERATIONS API REFERENCE](mesh-operations.md), [BLENDER PYTHON API REFERENCE (4.x)](blender-api.md), [BLENDER-MCP - FULL DOCUMENTATION](../addons/blender-mcp-full.md), [BLENDER-MCP ADDON - DEEP DIVE](../addons/blender-mcp.md) - **По темам:** [INSTALLATION GUIDE - COMPLETE SETUP](../guides/installation.md), [MCP ARCHITECTURE - MODEL CONTEXT PROTOCOL FOR BLENDER](../mcp/architecture.md), [PROMPTING GUIDE - EFFECTIVE AI COMMANDS FOR BLENDER](../guides/prompting.md) - **Теги:** #blender, #reference --- ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- # РУКОВОДСТВО ПО УСТАНОВКЕ — ПОЛНАЯ НАСТРОЙКА Tags: #addon #blender #mcp #reference #stable-diffusion # РУКОВОДСТВО ПО УСТАНОВКЕ — ПОЛНАЯ НАСТРОЙКА ## Предварительные требования - Windows 10/11 (это руководство) - Blender 4.2+ - Python 3.10+ (входит в поставку Blender) - Интернет-соединение для скачивания моделей --- ## Шаг 1: Установка uv (обязательно) ```powershell # Установить uv powershell -c "irm https://astral.sh/uv/install.ps1 | iex" # Добавить в PATH (ОБЯЗАТЕЛЬНО — после этого перезапустите PowerShell) $localBin = "$env:USERPROFILE\.local\bin" $userPath = [Environment]::GetEnvironmentVariable("Path", "User") [Environment]::SetEnvironmentVariable("Path", "$userPath;$localBin", "User") # Проверка uv --version uvx --version ``` --- ## Шаг 2: Установка Blender ### Вариант A: MSI-установщик (рекомендуется) ```powershell # Файл уже скачан: C:\BLENDER ai\blender-4.2.3-windows-x64.msi msiexec /i "C:\BLENDER ai\blender-4.2.3-windows-x64.msi" /quiet /norestart ``` ### Вариант B: Portable ZIP ```powershell # Распаковать в C:\BLENDER ai\Blender # Запустить: C:\BLENDER ai\Blender\blender.exe ``` ### Проверка ```powershell # Найти путь к Blender where blender # Или проверьте: C:\Program Files\Blender Foundation\Blender 4.2\blender.exe ``` --- ## Шаг 3: Установка blender-mcp (MCP-сервер + аддон) ```powershell # Установить аддон в Blender uvx blender-mcp install-addon # Проверить путь к аддону uvx blender-mcp addon-paths ``` --- ## Шаг 4: Установка аддона GenesisCore ```powershell # Уже скачан: C:\BLENDER ai\addons\GenesisCore-main.zip # В Blender: # 1. Edit → Preferences → Add-ons → Install from ZIP # 2. Выбрать: C:\BLENDER ai\addons\GenesisCore-main.zip # 3. Включить: Search "GenesisCore" → Check box ``` --- ## Шаг 5: Установка аддона Dream Textures ```powershell # Скачать свежий релиз: # https://github.com/carson-katri/dream-textures/releases/latest # Скачать: dream_textures-.zip # В Blender: # 1. Edit → Preferences → Add-ons → Install from ZIP # 2. Выбрать скачанный ZIP # 3. Включить: Dream Textures # Первый запуск: скачивает PyTorch + diffusers (~2-4 ГБ) # Preferences → Dream Textures → Download model: stabilityai/stable-diffusion-2-1-base ``` --- ## Шаг 6: Настройка OpenCode MCP Создайте/откройте: `C:\Users\\.config\opencode\opencode.json` ```json { "mcp": { "blender-mcp": { "type": "local", "command": ["uvx", "blender-mcp"], "enabled": true, "environment": { "BLENDER_HOST": "localhost", "BLENDER_PORT": "9876" } } } } ``` --- ## Шаг 7: Настройка GenesisCore (OpenRouter) 1. Получите API-ключ: https://openrouter.ai/keys 2. В Blender: нажмите `N` → вкладка **GenesisCore** 3. **Provider:** OpenRouter 4. **API Key:** вставьте ключ 5. **Fetch Models** → выберите: `deepseek/deepseek-chat` или `qwen/qwen-2.5-coder-32b` 6. **Tool Modules:** включите все (Shift+клик для множественного выбора) 7. **Save Config** --- ## Шаг 8: Запуск серверов ### blender-mcp (для OpenCode/Claude/Cursor) ```powershell # Терминал 1: запустить MCP-сервер uvx blender-mcp # Держать процесс запущенным ``` ### В Blender (для обоих) 1. Нажмите `N` → вкладка **MCP for Blender** 2. Отметьте: Poly Haven, Poly Pizza, Sketchfab и т. д. 3. Введите API-ключи в Preferences → Add-ons → MCP for Blender 4. Нажмите **Start MCP Server** ### GenesisCore (для чата внутри Blender) 1. Нажмите `N` → вкладка **GenesisCore** 2. Нажмите **Connect** (подключается к внутреннему серверу на :45677) 3. Общайтесь с ИИ прямо в Blender --- ## Шаг 9: Проверка всего ### Тест blender-mcp (через OpenCode) ```bash # В чате OpenCode: "Get the current Blender scene info" "Create a cube at location (1, 2, 3)" "Search Poly Haven for a concrete texture and apply to cube" ``` ### Тест GenesisCore (в N-панели Blender) ``` Prompt: "Create a low-poly house with a roof, door, and windows" Prompt: "Add studio lighting: key, fill, rim lights" Prompt: "Search Poly Haven for an HDRI and set as world environment" ``` ### Тест Dream Textures 1. Выберите куб → N-панель → **Dream Textures** 2. Prompt: "weathered brick wall, seamless" 3. Отметьте **Seamless** → Generate 4. Примените к материалу --- ## Необходимые API-ключи | Сервис | Ключ | Где получить | Где ввести | |---------|-----|--------------|----------------| | **OpenRouter** | Обязательно | https://openrouter.ai/keys | N-панель GenesisCore | | **Poly Pizza** | Бесплатный | https://poly.pizza/settings/api | Blender Preferences → MCP for Blender | | **Sketchfab** | Обязательно | https://sketchfab.com/developers/api | Blender Preferences → MCP for Blender | | **Hyper3D** | Обязательно | https://hyper3d.ai/ | Blender Preferences → MCP for Blender | | **Hunyuan3D** | SecretId/Key | https://3d.hunyuan.tencent.com/ | Blender Preferences → MCP for Blender | | **DreamStudio** | Опционально | https://beta.dreamstudio.ai/ | Blender Preferences → Dream Textures | --- ## Итоговая структура каталогов ``` C:\BLENDER ai\ ├── blender-4.2.3-windows-x64.msi ├── blender-4.2.3-windows-x64.zip ├── CHANGELOG.md ├── INDEX.md ├── addons\ │ ├── GenesisCore-main.zip │ ├── blender-mcp-full.md │ ├── blender-mcp.md │ ├── dream-textures-full.md │ ├── dream-textures.md │ └── genesiscore.md ├── mcp\ │ ├── architecture.md │ ├── clients.md │ ├── server-api.md │ └── setup-guide.md ├── guides\ │ ├── installation.md │ ├── prompting.md │ ├── troubleshooting.md │ └── workflows.md ├── api\ │ ├── blender-api.md │ ├── materials.md │ ├── mesh-operations.md │ └── scripting-patterns.md ├── workflows\ │ ├── animation.md │ ├── scene-building.md │ ├── text-to-3d.md │ └── texture-generation.md └── references\ └── (additional docs) ``` --- ## Краткая сводка по решению проблем | Проблема | Решение | |---------|----------| | `uvx` не найден | Перезапустите PowerShell; проверьте PATH | | Blender не найден | Выполните `uvx blender-mcp addon-paths` | | Аддон не отображается | Перезапустите Blender; проверьте папку нужной версии | | GenesisCore «Connecting...» | Порт 45677 заблокирован; перезапустите Blender | | Dream Textures OOM | Снизьте разрешение; используйте fp16 | | Poly Pizza 403 | Ручная загрузка + File → Import → glTF | --- ## Следующие шаги 1. Прочитайте [Гайд по промптам](../guides/prompting.md) 2. Прочитайте [Воркфлоу](../workflows/scene-building.md) 3. Изучите [Паттерны работы с API Blender](../api/scripting-patterns.md) 4. Проверьте [Архитектуру MCP](../mcp/architecture.md) ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [BLENDER-MCP ADDON - DEEP DIVE](../addons/blender-mcp.md), [TROUBLESHOOTING GUIDE](troubleshooting.md), [BLENDER-MCP - FULL DOCUMENTATION](../addons/blender-mcp-full.md), [DREAM TEXTURES ADDON - DEEP DIVE](../addons/dream-textures.md), [GENESISCORE ADDON - DEEP DIVE](../addons/genesiscore.md) - **По темам:** [MCP ARCHITECTURE - MODEL CONTEXT PROTOCOL FOR BLENDER](../mcp/architecture.md), [SETUP 35: READY PLAYER ME AVATARS](../../../setups/setup_35_readyplayerme.md), [TEXT-TO-3D WORKFLOWS](../workflows/text-to-3d.md) - **Теги:** #addon, #blender, #mcp, #reference, #stable-diffusion --- # СПРАВОЧНИК ПО API МАТЕРИАЛОВ BLENDER Tags: #api #blender #reference #shading # СПРАВОЧНИК ПО API МАТЕРИАЛОВ BLENDER ## Principled BSDF (стандартный PBR) ### Входные сокеты ```python principled = nodes["Principled BSDF"] # Базовый слой principled.inputs['Base Color'].default_value # RGBA (0-1) principled.inputs['Subsurface'].default_value # Float (0-1) principled.inputs['Subsurface Radius'].default_value # RGB principled.inputs['Subsurface Color'].default_value # RGBA principled.inputs['Subsurface IOR'].default_value # Float principled.inputs['Subsurface Anisotropy'].default_value # Float # Металлик principled.inputs['Metallic'].default_value # Float (0-1) principled.inputs['Specular'].default_value # Float (0-1) principled.inputs['Specular Tint'].default_value # Float (0-1) # Шероховатость principled.inputs['Roughness'].default_value # Float (0-1) principled.inputs['Anisotropic'].default_value # Float (0-1) principled.inputs['Anisotropic Rotation'].default_value # Float (0-1) # Transmission (стекло) principled.inputs['Transmission'].default_value # Float (0-1) principled.inputs['Transmission Roughness'].default_value # Float # Coat (лаковое покрытие) principled.inputs['Coat Weight'].default_value # Float (0-1) principled.inputs['Coat Roughness'].default_value # Float (0-1) # Sheen (ткань) principled.inputs['Sheen Weight'].default_value # Float (0-1) principled.inputs['Sheen Roughness'].default_value # Float (0-1) principled.inputs['Sheen Tint'].default_value # Float (0-1) # Излучение principled.inputs['Emission Color'].default_value # RGBA principled.inputs['Emission Strength'].default_value # Float # Alpha principled.inputs['Alpha'].default_value # Float (0-1) # Normal principled.inputs['Normal'].default_value # Vector (из Normal Map) principled.inputs['Clearcoat Normal'].default_value # Vector # Tangent principled.inputs['Tangent'].default_value # Vector (для Anisotropic) ``` ### Выходные сокеты ```python principled.outputs['BSDF'] # Shader (в Material Output) principled.outputs['Alpha'] # Float (для прозрачности) ``` --- ## Полная настройка PBR-материала ### По картам текстур ```python def create_pbr_material(name, maps, obj=None): """maps: словарь с ключами: base_color, roughness, normal, metallic, height, ao, emission""" mat = bpy.data.materials.new(name) mat.use_nodes = True nodes = mat.node_tree.nodes links = mat.node_tree.links nodes.clear() # Principled BSDF principled = nodes.new('ShaderNodeBsdfPrincipled') principled.location = (0, 0) # Material Output output = nodes.new('ShaderNodeOutputMaterial') output.location = (400, 0) links.new(principled.outputs['BSDF'], output.inputs['Surface']) # Displacement output (для height) disp = nodes.new('ShaderNodeDisplacement') disp.location = (200, -200) links.new(disp.outputs['Displacement'], output.inputs['Displacement']) mat.displacement_method = 'BOTH' # Texture Coordinate + Mapping (для всех текстур) tex_coord = nodes.new('ShaderNodeTexCoord') tex_coord.location = (-600, 0) mapping = nodes.new('ShaderNodeMapping') mapping.location = (-400, 0) links.new(tex_coord.outputs['UV'], mapping.inputs['Vector']) def add_texture(map_key, input_name, colorspace='sRGB', use_normal_map=False, use_disp=False): if map_key not in maps: return img = maps[map_key] tex = nodes.new('ShaderNodeTexImage') tex.image = img tex.image.colorspace_settings.name = colorspace tex.location = (-200, 0) links.new(mapping.outputs['Vector'], tex.inputs['Vector']) if use_normal_map: nmap = nodes.new('ShaderNodeNormalMap') nmap.location = (0, -100) links.new(tex.outputs['Color'], nmap.inputs['Color']) links.new(nmap.outputs['Normal'], principled.inputs[input_name]) elif use_disp: links.new(tex.outputs['Color'], disp.inputs['Height']) else: links.new(tex.outputs['Color'], principled.inputs[input_name]) # Подключить карты add_texture('base_color', 'Base Color', 'sRGB') add_texture('roughness', 'Roughness', 'Non-Color') add_texture('metallic', 'Metallic', 'Non-Color') add_texture('normal', 'Normal', 'Non-Color', use_normal_map=True) add_texture('height', 'Height', 'Non-Color', use_disp=True) add_texture('ao', 'Base Color', 'Non-Color') # AO смешивается отдельно add_texture('emission', 'Emission Color', 'sRGB') # Смешивание AO (умножение с Base Color) if 'ao' in maps: ao_tex = nodes.new('ShaderNodeTexImage') ao_tex.image = maps['ao'] ao_tex.image.colorspace_settings.name = 'Non-Color' ao_tex.location = (-200, 200) links.new(mapping.outputs['Vector'], ao_tex.inputs['Vector']) mix = nodes.new('ShaderNodeMixRGB') mix.blend_type = 'MULTIPLY' mix.location = (0, 100) links.new(principled.inputs['Base Color'].links[0].from_node.outputs['Color'], mix.inputs[1]) links.new(ao_tex.outputs['Color'], mix.inputs[2]) links.new(mix.outputs['Color'], principled.inputs['Base Color']) # Назначить объекту if obj: if obj.data.materials: obj.data.materials[0] = mat else: obj.data.materials.append(mat) return mat ``` --- ## Справочник типов нод ### Текстурные ноды ```python # Image Texture tex = nodes.new('ShaderNodeTexImage') tex.image = bpy.data.images.load("path.png") tex.projection = 'FLAT' # 'BOX', 'SPHERE', 'TUBE' tex.interpolation = 'Linear' # 'Closest', 'Cubic', 'Smart' # Environment Texture (HDRI) env = nodes.new('ShaderNodeTexEnvironment') env.image = bpy.data.images.load("hdri.exr") env.projection = 'EQUIRECTANGULAR' # Процедурные текстуры noise = nodes.new('ShaderNodeTexNoise') noise.noise_dimensions = '4D' noise.noise_type = 'FBM' voronoi = nodes.new('ShaderNodeTexVoronoi') voronoi.feature = 'SMOOTH_F1' # 'F1', 'F2', 'SMOOTH_F1', 'DISTANCE_TO_EDGE' voronoi.metric = 'MANHATTAN' # 'EUCLIDEAN', 'MANHATTAN', 'CHEBYCHEV', 'MINKOWSKI' musgrave = nodes.new('ShaderNodeTexMusgrave') musgrave.musgrave_dimensions = '3D' musgrave.musgrave_type = 'MULTIFRACTAL' magic = nodes.new('ShaderNodeTexMagic') wave = nodes.new('ShaderNodeTexWave') wave.wave_type = 'SINE' # 'SAW', 'TRI' brick = nodes.new('ShaderNodeTexBrick') checker = nodes.new('ShaderNodeTexChecker') gradient = nodes.new('ShaderNodeTexGradient') gradient.gradient_type = 'LINEAR' # 'QUADRATIC', 'EASING', 'DIAGONAL', 'RADIAL', 'QUADRATIC_SPHERE' ``` ### Цветовые ноды ```python # Mix RGB mix = nodes.new('ShaderNodeMixRGB') mix.blend_type = 'MIX' # 'MULTIPLY', 'SCREEN', 'OVERLAY', 'SOFT_LIGHT', 'HARD_LIGHT', 'DIFFERENCE', 'EXCLUSION', 'HUE', 'SATURATION', 'VALUE', 'COLOR' # Color Ramp ramp = nodes.new('ShaderNodeValToRGB') ramp.color_ramp.interpolation = 'LINEAR' # 'CONSTANT', 'EASE', 'CARDINAL', 'B_SPLINE' # ramp.color_ramp.elements[0].position = 0.0 # ramp.color_ramp.elements[0].color = (0,0,0,1) # RGB Curves curves = nodes.new('ShaderNodeRGBCurve') curves.mapping.curves[0].points[0].location = (0, 0) curves.mapping.curves[0].points[1].location = (1, 1) # Hue/Saturation/Value hsv = nodes.new('ShaderNodeHueSaturation') hsv.hue = 0.5 hsv.saturation = 1.0 hsv.value = 1.0 hsv.fac = 1.0 # Gamma gamma = nodes.new('ShaderNodeGamma') gamma.gamma = 2.2 # Brightness/Contrast bc = nodes.new('ShaderNodeBrightContrast') bc.bright = 0.0 bc.contrast = 0.0 # Invert invert = nodes.new('ShaderNodeInvert') invert.fac = 1.0 ``` ### Векторные и математические ноды ```python # Mapping mapping = nodes.new('ShaderNodeMapping') mapping.vector_type = 'POINT' # 'TEXTURE', 'VECTOR', 'NORMAL' # mapping.inputs['Location'].default_value = (0,0,0) # mapping.inputs['Rotation'].default_value = (0,0,0) # mapping.inputs['Scale'].default_value = (1,1,1) # Texture Coordinate tex_coord = nodes.new('ShaderNodeTexCoord') # Выходы: Generated, Normal, UV, Object, Camera, Window, Reflection # Vector Math vmath = nodes.new('ShaderNodeVectorMath') vmath.operation = 'ADD' # 'SUBTRACT', 'MULTIPLY', 'DIVIDE', 'CROSS_PRODUCT', 'PROJECT', 'REFLECT', 'DOT_PRODUCT', 'DISTANCE', 'LENGTH', 'SCALE', 'NORMALIZE', 'ABSOLUTE', 'FLOOR', 'CEIL', 'MODULO', 'FRACTION', 'MINIMUM', 'MAXIMUM', 'SNAP', 'WRAP', 'PINGPONG', 'SINE', 'COSINE', 'TANGENT' # Math math = nodes.new('ShaderNodeMath') math.operation = 'ADD' # 'SUBTRACT', 'MULTIPLY', 'DIVIDE', 'SINE', 'COSINE', 'TANGENT', 'ARCSINE', 'ARCCOSINE', 'ARCTANGENT', 'ARCTAN2', 'POWER', 'LOGARITHM', 'MINIMUM', 'MAXIMUM', 'ROUND', 'LESS_THAN', 'GREATER_THAN', 'MODULO', 'ABSOLUTE', 'EXPONENT', 'SIGN', 'COMPARE', 'SMOOTH_MIN', 'SMOOTH_MAX', 'ROUND', 'TRUNC', 'FRACT', 'FLOOR', 'CEIL', 'FRACTION', 'PINGPONG', 'SNAP', 'WRAP' math.use_clamp = False ``` ### Шейдерные ноды ```python # Glossy BSDF glossy = nodes.new('ShaderNodeBsdfGlossy') glossy.distribution = 'GGX' # 'ASHIKHMIN', 'BECKMANN', 'MULTI_GGX' # Glass BSDF glass = nodes.new('ShaderNodeBsdfGlass') glass.distribution = 'GGX' # Transparent BSDF transp = nodes.new('ShaderNodeBsdfTransparent') # Излучение emission = nodes.new('ShaderNodeEmission') emission.inputs['Color'].default_value = (1,1,1,1) emission.inputs['Strength'].default_value = 1.0 # Объём volume = nodes.new('ShaderNodeVolumePrincipled') volume_abs = nodes.new('ShaderNodeVolumeAbsorption') volume_scat = nodes.new('ShaderNodeVolumeScatter') # Mix Shader mix_shader = nodes.new('ShaderNodeMixShader') # Factor 0-1, Shader1, Shader2 # Add Shader add_shader = nodes.new('ShaderNodeAddShader') ``` ### Выходные ноды ```python # Material Output output = nodes.new('ShaderNodeOutputMaterial') # Входы: Surface, Volume, Displacement, Thickness # World Output world_out = nodes.new('ShaderNodeOutputWorld') # Входы: Surface, Volume ``` --- ## Вспомогательные функции ### Поиск и создание нод ```python def get_or_create_node(nodes, type_name, name=None): """Найти существующую ноду или создать новую""" for node in nodes: if node.bl_idname == type_name: return node node = nodes.new(type_name) if name: node.name = name return node def clear_node_tree(node_tree): """Удалить все ноды""" for node in list(node_tree.nodes): node_tree.nodes.remove(node) ``` ### Автоматическая раскладка ```python def auto_layout_nodes(nodes, start_x=-600, y_step=200): """Простая горизонтальная раскладка""" x = start_x for i, node in enumerate(nodes): node.location.x = x node.location.y = i * y_step x += 300 ``` ### Запекание карт текстур ```python def bake_texture(obj, map_type, output_image, margin=16): """Запечь map_type в output_image""" # map_type: 'DIFFUSE', 'NORMAL', 'ROUGHNESS', 'EMIT', 'AO', 'COMBINED' bpy.context.view_layer.objects.active = obj obj.select_set(True) # Создать bake-ноду mat = obj.active_material nodes = mat.node_tree.nodes bake_node = nodes.new('ShaderNodeTexImage') bake_node.image = output_image bake_node.select = True nodes.active = bake_node # Запечь bpy.ops.object.bake( type=map_type, use_selected_to_active=False, margin=margin ) nodes.remove(bake_node) output_image.save() ``` --- ## Типовые пресеты материалов ### Металл (золото) ```python principled.inputs['Base Color'].default_value = (1.0, 0.843, 0.0, 1.0) # Золото principled.inputs['Metallic'].default_value = 1.0 principled.inputs['Roughness'].default_value = 0.1 ``` ### Металл (хром) ```python principled.inputs['Base Color'].default_value = (1.0, 1.0, 1.0, 1.0) principled.inputs['Metallic'].default_value = 1.0 principled.inputs['Roughness'].default_value = 0.0 ``` ### Стекло ```python principled.inputs['Transmission'].default_value = 1.0 principled.inputs['Roughness'].default_value = 0.0 principled.inputs['IOR'].default_value = 1.45 ``` ### Излучение (неон) ```python principled.inputs['Emission Color'].default_value = (0.0, 1.0, 1.0, 1.0) # Циан principled.inputs['Emission Strength'].default_value = 10.0 ``` ### Подповерхностное рассеяние (кожа/воск) ```python principled.inputs['Subsurface'].default_value = 0.5 principled.inputs['Subsurface Radius'].default_value = (1.0, 0.2, 0.1) principled.inputs['Subsurface Color'].default_value = (1.0, 0.3, 0.1, 1.0) ``` ### Ткань (Sheen) ```python principled.inputs['Sheen Weight'].default_value = 0.5 principled.inputs['Sheen Roughness'].default_value = 0.3 principled.inputs['Sheen Tint'].default_value = 0.5 ``` ### Автоэмаль (Coat) ```python principled.inputs['Base Color'].default_value = (0.8, 0.0, 0.0, 1.0) principled.inputs['Metallic'].default_value = 0.8 principled.inputs['Roughness'].default_value = 0.2 principled.inputs['Coat Weight'].default_value = 1.0 principled.inputs['Coat Roughness'].default_value = 0.03 ``` --- ## Утилиты для материалов ### Копирование материала ```python new_mat = mat.copy() new_mat.name = "NewMaterial" ``` ### Link и Append ```python # Link (ссылка на оригинал) with bpy.data.libraries.load("file.blend", link=True) as (data_from, data_to): data_to.materials = ["MaterialName"] # Append (копия внутрь blend) with bpy.data.libraries.load("file.blend", link=False) as (data_from, data_to): data_to.materials = ["MaterialName"] ``` ### Удаление неиспользуемых ```python # Удалить неиспользуемые материалы for mat in list(bpy.data.materials): if mat.users == 0: bpy.data.materials.remove(mat) # Или: File → Clean Up → Unused Data Blocks ``` ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [BLENDER MESH OPERATIONS API REFERENCE](mesh-operations.md), [BLENDER PYTHON API REFERENCE (4.x)](blender-api.md), [BLENDER SCRIPTING PATTERNS - SAFE & EFFECTIVE](scripting-patterns.md), [BLENDER-MCP - FULL DOCUMENTATION](../addons/blender-mcp-full.md), [BLENDER-MCP ADDON - DEEP DIVE](../addons/blender-mcp.md) - **По темам:** [INSTALLATION GUIDE - COMPLETE SETUP](../guides/installation.md), [MCP ARCHITECTURE - MODEL CONTEXT PROTOCOL FOR BLENDER](../mcp/architecture.md), [MCP SERVER API REFERENCE](../mcp/server-api.md) - **Теги:** #api, #blender, #reference, #shading --- # СПРАВОЧНИК ПО API ОПЕРАЦИЙ С МЕШАМИ BLENDER Tags: #api #blender #modeling #reference # СПРАВОЧНИК ПО API ОПЕРАЦИЙ С МЕШАМИ BLENDER ## Создание примитивов ### bpy.ops (зависит от контекста) ```python # Требуется корректный контекст (view_layer, активный объект) bpy.ops.mesh.primitive_cube_add(size=2, location=(0,0,0), rotation=(0,0,0)) bpy.ops.mesh.primitive_uv_sphere_add(radius=1, segments=32, ring_count=16) bpy.ops.mesh.primitive_ico_sphere_add(radius=1, subdivisions=2) bpy.ops.mesh.primitive_cylinder_add(radius=1, depth=2, vertices=32, cap_ends=True) bpy.ops.mesh.primitive_cone_add(radius1=1, radius2=0, depth=2, vertices=32) bpy.ops.mesh.primitive_torus_add( major_radius=1, minor_radius=0.3, major_segments=48, minor_segments=12 ) bpy.ops.mesh.primitive_plane_add(size=2) bpy.ops.mesh.primitive_grid_add(x_subdivisions=10, y_subdivisions=10, size=2) bpy.ops.mesh.primitive_monkey_add(size=1) # Suzanne ``` ### Низкоуровневый способ (без контекста) — ПРЕДПОЧТИТЕЛЬНО ```python import bpy import bmesh from mathutils import Vector def create_cube(name="Cube", size=2, location=(0,0,0)): mesh = bpy.data.meshes.new(name) obj = bpy.data.objects.new(name, mesh) bpy.context.collection.objects.link(obj) obj.location = location bm = bmesh.new() bmesh.ops.create_cube(bm, size=size) bm.to_mesh(mesh) bm.free() return obj def create_uv_sphere(name="Sphere", radius=1, segments=32, ring_count=16): mesh = bpy.data.meshes.new(name) obj = bpy.data.objects.new(name, mesh) bpy.context.collection.objects.link(obj) bm = bmesh.new() bmesh.ops.create_uvsphere(bm, radius=radius, u_segments=segments, v_segments=ring_count) bm.to_mesh(mesh) bm.free() return obj def create_cylinder(name="Cylinder", radius=1, depth=2, vertices=32): mesh = bpy.data.meshes.new(name) obj = bpy.data.objects.new(name, mesh) bpy.context.collection.objects.link(obj) bm = bmesh.new() bmesh.ops.create_cone(bm, radius1=radius, radius2=radius, depth=depth, segments=vertices) bm.to_mesh(mesh) bm.free() return obj ``` --- ## Операции BMesh (эквивалент режима редактирования) ### Подготовка ```python import bmesh # BMesh в режиме объекта (создаёт новый меш) bm = bmesh.new() # ... операции ... bm.to_mesh(mesh) bm.free() # BMesh в режиме редактирования (изменяет существующий) bm = bmesh.from_edit_mesh(obj.data) # ... операции ... bmesh.update_edit_mesh(obj.data) ``` ### Операции с вершинами ```python # Создать вершины v1 = bm.verts.new((0, 0, 0)) v2 = bm.verts.new((1, 0, 0)) v3 = bm.verts.new((0, 1, 0)) # Выделить / снять выделение v1.select = True bm.verts.ensure_lookup_table() # Трансформация v1.co = (2, 0, 0) v1.co += Vector((0.1, 0, 0)) # Удаление bm.verts.remove(v1) ``` ### Операции с рёбрами ```python # Создать ребро e = bm.edges.new((v1, v2)) # Циклы рёбер bmesh.ops.edge_loops(bm, edges=[e]) # Разделить рёбра bmesh.ops.subdivide_edges(bm, edges=bm.edges, cuts=2) # Скруглить рёбра bmesh.ops.bevel(bm, geom=bm.edges, offset=0.1, segments=3, profile=0.5) # Растворить рёбра bmesh.ops.dissolve_edges(bm, edges=[e]) ``` ### Операции с гранями ```python # Создать грань f = bm.faces.new((v1, v2, v3)) # Треугольник f = bm.faces.new((v1, v2, v3, v4)) # Четырёхугольник # Свойства грани f.select = True f.material_index = 0 f.normal # mathutils.Vector # Экструзия result = bmesh.ops.extrude_face_region(bm, geom=[f]) extruded_geom = result['geom'] # Переместить экструдированные грани for elem in extruded_geom: if isinstance(elem, bmesh.types.BMFace): bmesh.ops.translate(bm, verts=elem.verts, vec=(0, 0, 1)) # Inset bmesh.ops.inset_region(bm, faces=[f], thickness=0.1, depth=0.1) # Poke (веер из треугольников) bmesh.ops.poke(bm, faces=[f]) # Триангуляция bmesh.ops.triangulate(bm, faces=bm.faces) # Разделить грани bmesh.ops.subdivide_edges(bm, edges=[e for f in bm.faces for e in f.edges], cuts=1) ``` ### Выделение и запросы ```python # Выделить всё for v in bm.verts: v.select = True for e in bm.edges: e.select = True for f in bm.faces: f.select = True # Получить выделенное selected_verts = [v for v in bm.verts if v.select] selected_edges = [e for e in bm.edges if e.select] selected_faces = [f for f in bm.faces if f.select] # По индексу v = bm.verts[0] e = bm.edges[0] f = bm.faces[0] # Таблицы поиска (обязательны после изменения топологии) bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() bm.faces.ensure_lookup_table() ``` ### Модификаторы меша (BMesh) ```python # Remesh (воксельный) bmesh.ops.remesh(bm, voxel_size=0.1, adaptivity=0.0) # Сглаживание bmesh.ops.smooth_vert(bm, verts=bm.verts, factor=0.5, iterations=10) # Decimate # Напрямую в bmesh нет — используйте модификатор или bpy.ops # Удалить оторванные элементы bmesh.ops.delete_loose(bm, dist=0.001) ``` --- ## Доступ к данным меша (bpy.data.meshes) ### Чтение данных меша ```python mesh = obj.data # Вершины for v in mesh.vertices: v.co # локальные координаты v.normal # нормаль вершины v.groups # группы вершин v.index # индекс # Рёбра for e in mesh.edges: e.vertices # (v1_idx, v2_idx) e.use_seam # шов UV e.use_sharp # острое ребро e.bevel_weight # вес Bevel # Полигоны (грани) for poly in mesh.polygons: poly.vertices # индексы вершин poly.loop_indices # индексы лупов poly.normal # нормаль грани poly.center # центр грани poly.area # площадь грани poly.material_index # индекс слота материала # Лупы (данные на каждую пару «вершина-грань») for loop in mesh.loops: loop.vertex_index # индекс вершины loop.normal # нормаль лупа (для гладкого затенения) # Слои UV for uv_layer in mesh.uv_layers: for uv in uv_layer.data: uv.uv # Vector (u, v) # Цвета вершин for vc_layer in mesh.vertex_colors: for vc in vc_layer.data: vc.color # RGBA (0-1) # Shape Keys if mesh.shape_keys: for key in mesh.shape_keys.key_blocks: key.value = 0.5 ``` --- ## Модификаторы (через API) ### Добавление модификатора ```python mod = obj.modifiers.new("Subdivision", 'SUBSURF') mod.levels = 2 mod.render_levels = 3 mod.subdivision_type = 'CATMULL_CLARK' # 'SIMPLE' mod = obj.modifiers.new("Bevel", 'BEVEL') mod.width = 0.02 mod.segments = 3 mod.profile = 0.5 mod.limit_method = 'ANGLE' # 'WEIGHT', 'VGROUP', 'NONE' mod.angle_limit = 1.0472 # 60 градусов mod = obj.modifiers.new("Array", 'ARRAY') mod.count = 5 mod.use_relative_offset = True mod.relative_offset_displace = (1.5, 0, 0) mod = obj.modifiers.new("Mirror", 'MIRROR') mod.use_axis = (True, False, False) mod.use_bisect_axis = (True, False, False) mod = obj.modifiers.new("Boolean", 'BOOLEAN') mod.operation = 'DIFFERENCE' # 'UNION', 'INTERSECT' mod.object = other_obj mod.solver = 'EXACT' # 'FAST' mod = obj.modifiers.new("Solidify", 'SOLIDIFY') mod.thickness = 0.1 mod.offset = 0 # от -1 до 1 mod.use_even_offset = True mod = obj.modifiers.new("Decimate", 'DECIMATE') mod.ratio = 0.5 # 0.0-1.0 mod.decimate_type = 'COLLAPSE' # 'UNSUBDIV', 'PLANNAR' mod = obj.modifiers.new("Remesh", 'REMESH') mod.mode = 'VOXEL' # 'SMOOTH', 'SHARP' mod.voxel_size = 0.1 mod.adaptivity = 0.0 ``` ### Применение модификатора ```python # Один bpy.ops.object.modifier_apply(modifier="Subdivision") # Все for mod in list(obj.modifiers): bpy.ops.object.modifier_apply(modifier=mod.name) # Через Python (без контекста) bpy.context.view_layer.objects.active = obj bpy.ops.object.modifier_apply(modifier=mod.name) ``` --- ## Булевы операции ### Через модификатор (рекомендуется) ```python def boolean_union(obj_a, obj_b): mod = obj_a.modifiers.new("Bool", 'BOOLEAN') mod.operation = 'UNION' mod.object = obj_b mod.solver = 'EXACT' bpy.context.view_layer.objects.active = obj_a bpy.ops.object.modifier_apply(modifier="Bool") bpy.data.objects.remove(obj_b, do_unlink=True) return obj_a def boolean_difference(obj_a, obj_b): mod = obj_a.modifiers.new("Bool", 'BOOLEAN') mod.operation = 'DIFFERENCE' mod.object = obj_b mod.solver = 'EXACT' bpy.context.view_layer.objects.active = obj_a bpy.ops.object.modifier_apply(modifier="Bool") bpy.data.objects.remove(obj_b, do_unlink=True) return obj_a def boolean_intersect(obj_a, obj_b): mod = obj_a.modifiers.new("Bool", 'BOOLEAN') mod.operation = 'INTERSECT' mod.object = obj_b mod.solver = 'EXACT' bpy.context.view_layer.objects.active = obj_a bpy.ops.object.modifier_apply(modifier="Bool") bpy.data.objects.remove(obj_b, do_unlink=True) return obj_a ``` --- ## Развёртка UV ### Базовая развёртка ```python # Smart UV Project (лучше для hard surface) bpy.context.view_layer.objects.active = obj bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.uv.smart_project(angle_limit=1.0472, island_margin=0.02) bpy.ops.object.mode_set(mode='OBJECT') # Lightmap Pack (для запекания) bpy.ops.uv.lightmap_pack(PADDING=4, Px_MARGIN=4) # Cube Projection bpy.ops.uv.cube_project() # Sphere Projection bpy.ops.uv.sphere_project() ``` ### Развёртка по швам ```python bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='DESELECT') # Пометить швы for e in bmesh.from_edit_mesh(obj.data).edges: if e.calc_face_angle() > 1.0472: # 60 градусов e.use_seam = True bpy.ops.mesh.select_all(action='SELECT') bpy.ops.uv.unwrap(method='ANGLE_BASED', margin=0.001) bpy.ops.object.mode_set(mode='OBJECT') ``` ### Манипуляция с UV ```python # Масштабировать UV uv_layer = mesh.uv_layers.active for uv in uv_layer.data: uv.uv *= 2.0 # Сдвинуть UV for uv in uv_layer.data: uv.uv += (0.5, 0.5) # Повернуть UV import math angle = math.pi / 4 for uv in uv_layer.data: u, v = uv.uv uv.uv = (u * math.cos(angle) - v * math.sin(angle), u * math.sin(angle) + v * math.cos(angle)) ``` --- ## Группы вершин и веса ### Создание и назначение ```python vg = obj.vertex_groups.new(name="GroupName") # Назначить вершины с весом vg.add([0, 1, 2, 3], 1.0, 'REPLACE') # индексы вершин, вес, тип # Получить вес weight = vg.weight(vertex_index) # Удалить vg.remove([0, 1, 2]) ``` ### Операции с группами вершин ```python # Нормализовать все группы для вершины vg.normalize() # Очистить (убрать нулевые веса) vg.clean(0.001, 0.0, 0.0) # Зеркалить (для симметричных ригов) vg.mirror("GroupName.R", "GroupName.L", True) # Копировать new_vg = obj.vertex_groups.new(name="Copy") for v in obj.data.vertices: try: w = vg.weight(v.index) new_vg.add([v.index], w, 'REPLACE') except RuntimeError: pass # Вершина не входит в группу ``` --- ## Нормали и затенение ### Автосглаживание ```python obj.data.use_auto_smooth = True obj.data.auto_smooth_angle = 1.0472 # 60 градусов ``` ### Пользовательские нормали ```python # Задать пользовательские нормали mesh = obj.data mesh.use_auto_smooth = True normals = [] for poly in mesh.polygons: for loop_idx in poly.loop_indices: normals.append(poly.normal) mesh.normals_split_custom_set(normals) mesh.use_auto_smooth = True # Или по вершинам mesh.normals_split_custom_set_from_vertices([v.normal for v in mesh.vertices]) ``` ### Острые рёбра ```python # Пометить острыми for e in mesh.edges: e.use_sharp = True # Для автосглаживания # Вес Bevel for e in mesh.edges: e.bevel_weight = 1.0 ``` --- ## Цвета вершин ### Рисование через Python ```python # Создать слой vc_layer = mesh.vertex_colors.new(name="Paint") # Красить по граням for poly in mesh.polygons: for loop_idx in poly.loop_indices: loop = mesh.loops[loop_idx] vc_layer.data[loop_idx].color = (1.0, 0.0, 0.0, 1.0) # Красный # Градиент по высоте for poly in mesh.polygons: for loop_idx in poly.loop_indices: v = mesh.vertices[mesh.loops[loop_idx].vertex_index] h = (v.co.z - min_z) / (max_z - min_z) vc_layer.data[loop_idx].color = (h, h, h, 1.0) ``` --- ## Советы по производительности ### Пакетные операции ```python # Использовать foreach для массового доступа к атрибутам co_list = [0.0] * (len(mesh.vertices) * 3) mesh.vertices.foreach_get("co", co_list) # Изменить for i in range(0, len(co_list), 3): co_list[i+2] += 0.1 # Поднять Z # Записать обратно mesh.vertices.foreach_set("co", co_list) mesh.update() ``` ### Эффективное выделение ```python # Вместо цикла: # for v in bm.verts: v.select = True # Использовать: bm.verts.ensure_lookup_table() for v in bm.verts: v.select_set(True) # или v.select = True ``` ### Избегайте bpy.ops в циклах ```python # ПЛОХО for obj in objects: bpy.ops.object.modifier_apply(modifier="Subdivision") # ХОРОШО — один override контекста with bpy.context.temp_override(selected_objects=objects): bpy.ops.object.modifier_apply(modifier="Subdivision") ``` --- ## Общие паттерны ### Создание меша по координатам ```python def create_mesh_from_vertices(name, vertices, faces): """vertices: [(x,y,z), ...], faces: [(v1,v2,v3), ...]""" mesh = bpy.data.meshes.new(name) mesh.from_pydata(vertices, [], faces) mesh.update() obj = bpy.data.objects.new(name, mesh) bpy.context.collection.objects.link(obj) return obj # Использование verts = [(0,0,0), (1,0,0), (0,1,0), (1,1,0)] faces = [(0,1,3,2)] # Четырёхугольник obj = create_mesh_from_vertices("Quad", verts, faces) ``` ### Дублирование и разделение ```python # Дублировать объект + меш new_obj = obj.copy() new_obj.data = obj.data.copy() bpy.context.collection.objects.link(new_obj) # Отделить оторванные части (режим редактирования) bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.separate(type='LOOSE') bpy.ops.object.mode_set(mode='OBJECT') ``` ### Преобразование в меш ```python # Преобразовать кривую/текст/мета-объект в меш bpy.context.view_layer.objects.active = obj bpy.ops.object.convert(target='MESH') ``` ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [BLENDER MATERIALS API REFERENCE](materials.md), [BLENDER PYTHON API REFERENCE (4.x)](blender-api.md), [BLENDER SCRIPTING PATTERNS - SAFE & EFFECTIVE](scripting-patterns.md), [BLENDER-MCP - FULL DOCUMENTATION](../addons/blender-mcp-full.md), [BLENDER-MCP ADDON - DEEP DIVE](../addons/blender-mcp.md) - **По темам:** [INSTALLATION GUIDE - COMPLETE SETUP](../guides/installation.md), [MCP ARCHITECTURE - MODEL CONTEXT PROTOCOL FOR BLENDER](../mcp/architecture.md), [MCP SERVER API REFERENCE](../mcp/server-api.md) - **Теги:** #api, #blender, #modeling, #reference --- # СПРАВОЧНИК ПО API MCP-СЕРВЕРА Tags: #api #mcp #reference # СПРАВОЧНИК ПО API MCP-СЕРВЕРА ## Инструменты blender-mcp (28+) ### Базовые инструменты работы со сценой #### `get_scene_info` ```json // Возвращает упрощённые данные сцены { "name": "Scene", "object_count": 15, "objects": [ {"name": "Cube", "type": "MESH", "location": [0,0,0]} ], "materials_count": 5 } ``` #### `get_object_info` ```json // Параметры {"name": "Cube"} // Возвращает { "name": "Cube", "type": "MESH", "location": [0,0,0], "rotation": [0,0,0], "scale": [1,1,1], "visible": true, "bound_box": [[...], ...], "materials": ["Material.001"], "mesh": {"vertices": 8, "edges": 12, "polygons": 6} } ``` #### `get_world_state_snapshot` ```json // Компактный снимок траектории (лимит — 4000 объектов) { "name": "Scene", "object_count": 15, "objects_listed": 15, "objects_truncated": false, "selected": ["Cube"], "selected_count": 1, "frame_current": 1, "fps": 24, "objects": [...], "active_camera": "Camera", "camera": {...}, "lights": [...], "materials_count": 5, "material_fps": {"Material": "a1b2c3d4"}, "world_fp": "e5f6g7h8", "project_id": "abc123def456", "blender_version": "4.2.3", "snapshot_source": "native" } ``` #### `get_viewport_screenshot` ```json // Параметры {"max_size": 800, "filepath": "/tmp/screenshot.png", "format": "png"} // Возвращает: Image (тип Image в MCP) ``` #### `execute_blender_code` ```json // Параметры {"code": "import bpy\nbpy.ops.mesh.primitive_cube_add(location=(1,2,3))"} // Возвращает {"result": "Code executed successfully: ..."} ``` #### `get_addon_info` ```json // Данные рукопожатия { "name": "MCP for Blender", "addon_version": [1, 6], "protocol_version": 5, "capabilities": ["get_scene_info", "get_object_info", ...], "blender_version": "Blender 4.2.3" } ``` ### Инструменты материалов #### `set_texture` ```json // Параметры {"object_name": "Cube", "texture_id": "RustedIron"} // Применяет ранее скачанную текстуру Poly Haven ``` ### Инструменты Poly Haven (требуют включения в Blender) #### `get_polyhaven_categories` ```json // Параметры {"asset_type": "hdris"} // "hdris" | "textures" | "models" | "all" // Возвращает отформатированный список категорий с количеством элементов ``` #### `search_polyhaven_assets` ```json // Параметры {"asset_type": "models", "categories": "architecture,buildings"} // Возвращает { "total_count": 45, "returned_count": 20, "assets": {...} } ``` #### `download_polyhaven_asset` ```json // Параметры {"asset_id": "abandoned_bakery", "asset_type": "hdris", "resolution": "2k", "file_format": "exr"} // asset_type: "hdris" | "textures" | "models" // resolution: "1k" | "2k" | "4k" | "8k" | "16k" // HDRI → становится окружением мира // Texture → создаёт PBR-материал // Model → импортирует .blend в сцену ``` #### `get_polyhaven_status` ```json // Возвращает статус включения и сообщение ``` ### Инструменты Poly Pizza (требуют API-ключ и включения) #### `get_polypizza_status` ```json // Возвращает статус включения ``` #### `search_polypizza_models` ```json // Параметры {"query": "chair", "category": "Furniture & Decor", "licence": "CC0", "animated": false, "limit": 20} // Categories: Food & Drink, Clutter, Weapons, Transport, Furniture & Decor, // Objects, Nature, Animals, Buildings, People & Characters, Scenes & Levels, Other // Licence: "CC0" (без указания авторства) | "CC-BY" (атрибуция обязательна) // Возвращает отформатированный список с лицензией и числом треугольников ``` #### `download_polypizza_model` ```json // Параметры {"model_id": "chair_01", "normalize_size": true, "target_size": 1.0} // target_size в метрах ``` ### Инструменты Sketchfab (требуют API-ключ и включения) #### `get_sketchfab_status` #### `search_sketchfab_models` ```json // Параметры {"query": "car", "categories": "vehicles", "count": 20, "downloadable": true} ``` #### `get_sketchfab_model_preview` ```json // Параметры {"uid": "abc123"} // Возвращает: Image (миниатюра) ``` #### `download_sketchfab_model` ```json // Параметры {"uid": "abc123", "target_size": 4.5} // target_size ОБЯЗАТЕЛЬНЫЙ (метры, наибольшая сторона) // normalize_size всегда true ``` ### Инструменты Hyper3D Rodin (требуют API-ключ и включения) #### `get_hyper3d_status` #### `create_rodin_job` ```json // Параметры {"prompt": "garden gnome", "seed": 12345} // Возвращает job_id ``` #### `poll_rodin_job_status` ```json // Параметры {"job_id": "job_abc123"} // Возвращает status, progress, result_url ``` #### `import_generated_asset` ```json // Параметры {"job_id": "job_abc123", "result_url": "https://..."} // Импортирует в сцену ``` ### Инструменты Hunyuan3D (требуют SecretId/SecretKey и включения) #### `get_hunyuan3d_status` #### `create_hunyuan_job` #### `poll_hunyuan_job_status` #### `import_generated_asset_hunyuan` ### Инструменты телеметрии #### `get_telemetry_consent` ```json // Возвращает {"consent": true|false|null} ``` #### `set_telemetry_consent` ```json // Параметры {"consent": false} // Выключает сбор данных (через инструмент включить обратно нельзя) ``` #### `drain_human_activity` ```json // Возвращает буферизованные события отмены/повтора/операторов {"events": [{"kind": "operator", "bl_idname": "mesh.primitive_cube_add", ...}]} ``` --- ## Инструменты GenesisCore (внутренние) GenesisCore использует те же определения инструментов, но выполняет их напрямую в процессе Blender (без TCP). ### Модули инструментов (src/tools/) #### `CommonTools` - `get_simple_info()` → базовая статистика сцены - `get_scene_info()` → полная сцена - `execute_blender_code(code)` → произвольный Python #### `ObjectTools` - `create_object(entity_type, name, location, rotation, scale, ...)` → CUBE, SPHERE, CYLINDER, PLANE, CONE, TORUS, EMPTY, CAMERA, LIGHT - `modify_object(name, location, rotation, scale, visible)` - `delete_object(object_name)` - `get_object_info(object_name)` #### `MaterialTools` - `set_material(object_name, material_name, color)` → Principled BSDF #### `PolyhavenTools` - `polyhaven_search_models(names, tags, categories)` - `polyhaven_fetch_model_info(asset_id)` - `polyhaven_use_model_asset(asset_id, expected_resolution)` - `polyhaven_search_hdris(names, tags, categories)` - `polyhaven_fetch_hdri_info(asset_id)` - `polyhaven_use_hdri(asset_id, expected_resolution)` --- ## Протокол JSON-RPC (справочно) ### Запрос ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "tool_name", "arguments": {...} } } ``` ### Успешный ответ ```json { "jsonrpc": "2.0", "id": 1, "result": {...} } ``` ### Ответ с ошибкой ```json { "jsonrpc": "2.0", "id": 1, "error": { "code": -32603, "message": "Internal error: ..." } } ``` --- ## Добавление своих инструментов ### blender-mcp (на стороне сервера) ```python # src/blender_mcp/tools/custom.py from mcp.server.fastmcp import FastMCP @mcp.tool() async def my_tool(param: str) -> str: """Description for LLM""" blender = get_blender_connection() return blender.send_command("my_command", {"param": param}) ``` ### GenesisCore (на стороне аддона) ```python # src/tools/my_tools.py from .common import ToolsPackageBase class MyTools(ToolsPackageBase): def my_tool(param: str) -> dict: """Description for LLM""" # Прямой доступ к bpy return {"result": "success"} # src/tools/__init__.py from .my_tools import MyTools # Порядок = порядок отображения в UI ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [BLENDER MATERIALS API REFERENCE](../api/materials.md), [BLENDER MESH OPERATIONS API REFERENCE](../api/mesh-operations.md), [BLENDER PYTHON API REFERENCE (4.x)](../api/blender-api.md), [BLENDER-MCP - FULL DOCUMENTATION](../addons/blender-mcp-full.md), [BLENDER-MCP ADDON - DEEP DIVE](../addons/blender-mcp.md) - **По темам:** [GENESISCORE ADDON - DEEP DIVE](../addons/genesiscore.md), [INSTALLATION GUIDE - COMPLETE SETUP](../guides/installation.md), [MCP ARCHITECTURE - MODEL CONTEXT PROTOCOL FOR BLENDER](architecture.md) - **Теги:** #api, #mcp, #reference --- ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- # СПРАВОЧНИК ПО BLENDER PYTHON API (4.x) Tags: #api #blender #reference # СПРАВОЧНИК ПО BLENDER PYTHON API (4.x) ## Быстрая справка ### Контекст и доступ к данным ```python import bpy # Контекст (в фоновом режиме только для чтения) bpy.context.scene # Текущая сцена bpy.context.view_layer # Текущий view layer bpy.context.active_object # Активный объект (может быть None) bpy.context.selected_objects # Список выделенных объектов bpy.context.object # То же, что active_object bpy.context.mode # Текущий режим ('OBJECT', 'EDIT' и т.д.) # Данные (доступны всегда) bpy.data.objects # Все объекты bpy.data.meshes # Все меши bpy.data.materials # Все материалы bpy.data.collections # Все коллекции bpy.data.scenes # Все сцены bpy.data.images # Все изображения bpy.data.node_groups # Все группы нод ``` ### Создание объектов ```python # Примитивы (bpy.ops — требует контекста) bpy.ops.mesh.primitive_cube_add(size=2, location=(0,0,0), rotation=(0,0,0)) bpy.ops.mesh.primitive_uv_sphere_add(radius=1, segments=32, ring_count=16) bpy.ops.mesh.primitive_cylinder_add(radius=1, depth=2, vertices=32) bpy.ops.mesh.primitive_plane_add(size=2) bpy.ops.mesh.primitive_cone_add(radius1=1, radius2=0, depth=2) bpy.ops.mesh.primitive_torus_add(major_radius=1, minor_radius=0.3) # Низкоуровневый способ (контекст не нужен) — ПРЕДПОЧТИТЕЛЬНО mesh = bpy.data.meshes.new("MyMesh") obj = bpy.data.objects.new("MyObject", mesh) bpy.context.collection.objects.link(obj) # Создание данных меша bpy.ops.object.mode_set(mode='EDIT') bm = bmesh.from_edit_mesh(mesh) # ... операции bmesh ... bmesh.update_edit_mesh(mesh) bpy.ops.object.mode_set(mode='OBJECT') ``` ### Операции трансформации ```python obj = bpy.data.objects["Cube"] # Location / Rotation / Scale obj.location = (1.0, 2.0, 3.0) obj.rotation_euler = (0, 0, 1.5708) # Радианы obj.scale = (2.0, 2.0, 2.0) # Матрица (мировое преобразование) obj.matrix_world = mathutils.Matrix.Translation((1,2,3)) @ \ mathutils.Matrix.Rotation(1.57, 4, 'Z') # Применить преобразования (Ctrl+A) bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) # Origin (начало координат) bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS') bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_MASS') ``` ### Редактирование меша (bmesh) ```python import bmesh # bmesh в режиме редактирования bm = bmesh.from_edit_mesh(obj.data) # Создание геометрии v1 = bm.verts.new((0,0,0)) v2 = bm.verts.new((1,0,0)) v3 = bm.verts.new((0,1,0)) bm.faces.new((v1, v2, v3)) # Операции bmesh вместо модификаторов bmesh.ops.subdivide_edges(bm, edges=bm.edges, cuts=2) bmesh.ops.bevel(bm, geom=bm.edges, offset=0.1, segments=3) bmesh.ops.extrude_face_region(bm, geom=[f for f in bm.faces]) # Обновление bmesh.update_edit_mesh(obj.data) # bmesh в режиме объекта (новый меш) bm = bmesh.new() bmesh.ops.create_cube(bm, size=1.0) bm.to_mesh(mesh) bm.free() ``` ### Материалы и ноды ```python # Создать материал mat = bpy.data.materials.new("MyMaterial") mat.use_nodes = True nodes = mat.node_tree.nodes links = mat.node_tree.links # Очистить ноды по умолчанию nodes.clear() # Principled BSDF (стандартный) principled = nodes.new('ShaderNodeBsdfPrincipled') principled.location = (0, 0) output = nodes.new('ShaderNodeOutputMaterial') output.location = (400, 0) links.new(principled.outputs['BSDF'], output.inputs['Surface']) # Задать параметры principled.inputs['Base Color'].default_value = (1.0, 0.0, 0.0, 1.0) # RGBA principled.inputs['Roughness'].default_value = 0.5 principled.inputs['Metallic'].default_value = 0.0 principled.inputs['Transmission'].default_value = 0.0 principled.inputs['Emission Strength'].default_value = 0.0 # Текстурные ноды tex_image = nodes.new('ShaderNodeTexImage') tex_image.image = bpy.data.images.load("path/to/texture.png") tex_coord = nodes.new('ShaderNodeTexCoord') mapping = nodes.new('ShaderNodeMapping') links.new(tex_coord.outputs['UV'], mapping.inputs['Vector']) links.new(mapping.outputs['Vector'], tex_image.inputs['Vector']) links.new(tex_image.outputs['Color'], principled.inputs['Base Color']) # Назначить объекту if obj.data.materials: obj.data.materials[0] = mat else: obj.data.materials.append(mat) ``` ### Модификаторы ```python mod = obj.modifiers.new("Subdivision", 'SUBSURF') mod.levels = 2 mod.render_levels = 3 mod = obj.modifiers.new("Bevel", 'BEVEL') mod.width = 0.02 mod.segments = 3 mod.profile = 0.5 mod = obj.modifiers.new("Array", 'ARRAY') mod.count = 5 mod.use_relative_offset = True mod.relative_offset_displace = (1.5, 0, 0) mod = obj.modifiers.new("Boolean", 'BOOLEAN') mod.operation = 'DIFFERENCE' # UNION, INTERSECT mod.object = other_obj mod.solver = 'EXACT' # 'FAST' для простых случаев # Применить модификатор bpy.ops.object.modifier_apply(modifier="Subdivision") ``` ### Коллекции и иерархия ```python # Создать коллекцию col = bpy.data.collections.new("MyCollection") bpy.context.scene.collection.children.link(col) # Переместить объект в коллекцию col.objects.link(obj) bpy.context.scene.collection.objects.unlink(obj) # Родитель child.parent = parent child.parent_type = 'OBJECT' # 'BONE', 'VERTEX' # Коллекции для организации bpy.context.view_layer.active_layer_collection = \ bpy.context.view_layer.layer_collection.children["MyCollection"] ``` ### Анимация ```python # Ключевые кадры obj.location = (0, 0, 0) obj.keyframe_insert(data_path="location", frame=1) obj.location = (10, 0, 0) obj.keyframe_insert(data_path="location", frame=250) # Интерполяция for fcurve in obj.animation_data.action.fcurves: for kp in fcurve.keyframe_points: kp.interpolation = 'BEZIER' # 'LINEAR', 'CONSTANT' # Драйверы driver = obj.driver_add("location", 0).driver driver.type = 'SCRIPTED' var = driver.variables.new() var.name = "target_x" var.targets[0].id = target_obj var.targets[0].data_path = "location[0]" driver.expression = "target_x * 2" # NLA track = obj.animation_data.nla_tracks.new() strip = track.strips.new("ActionName", 1, obj.animation_data.action) strip.scale = 2.0 # Масштаб времени ``` ### Geometry Nodes ```python # Создать группу нод ng = bpy.data.node_groups.new("MyGeoNodes", 'GeometryNodeTree') nodes = ng.nodes links = ng.links # Вход / выход input_node = nodes.new('NodeGroupInput') output_node = nodes.new('NodeGroupOutput') # Добавить сокеты ng.interface.new_socket(name="Count", in_out='INPUT', socket_type='NodeSocketInt') ng.interface.new_socket(name="Geometry", in_out='OUTPUT', socket_type='NodeSocketGeometry') # Ноды mesh_line = nodes.new('GeometryNodeMeshLine') mesh_line.location = (-200, 0) mesh_line.mode = 'POINTS' links.new(input_node.outputs['Count'], mesh_line.inputs['Count']) instance = nodes.new('GeometryNodeInstanceOnPoints') instance.location = (200, 0) links.new(mesh_line.outputs['Mesh'], instance.inputs['Points']) links.new(input_node.outputs['Geometry'], instance.inputs['Instance']) links.new(instance.outputs['Instances'], output_node.inputs['Geometry']) # Применить к объекту mod = obj.modifiers.new("GeoNodes", 'NODES') mod.node_group = ng ``` ### Рендеринг ```python scene = bpy.context.scene # Движок scene.render.engine = 'CYCLES' # 'BLENDER_EEVEE', 'BLENDER_WORKBENCH' # Настройки Cycles scene.cycles.samples = 256 scene.cycles.use_denoising = True scene.cycles.device = 'GPU' # 'CPU', 'GPU' # Настройки EEVEE scene.eevee.taa_render_samples = 64 scene.eevee.use_bloom = True scene.eevee.use_ssr = True scene.eevee.use_ssr_refraction = True # Вывод scene.render.resolution_x = 1920 scene.render.resolution_y = 1080 scene.render.resolution_percentage = 100 scene.render.filepath = "//render_" scene.render.image_settings.file_format = 'PNG' scene.render.image_settings.color_mode = 'RGBA' scene.render.film_transparent = True # Рендер bpy.ops.render.render(write_still=True) bpy.ops.render.render(animation=True) ``` ### Импорт и экспорт ```python # Импорт bpy.ops.import_scene.gltf(filepath="model.glb") bpy.ops.import_scene.fbx(filepath="model.fbx") bpy.ops.import_mesh.stl(filepath="model.stl") bpy.ops.wm.obj_import(filepath="model.obj") bpy.ops.wm.ply_import(filepath="model.ply") # Экспорт bpy.ops.export_scene.gltf(filepath="model.glb", export_format='GLB') bpy.ops.export_scene.fbx(filepath="model.fbx") bpy.ops.export_mesh.stl(filepath="model.stl") bpy.ops.wm.obj_export(filepath="model.obj") bpy.ops.wm.ply_export(filepath="model.ply") ``` ### Утилиты ```python # Выделение bpy.ops.object.select_all(action='DESELECT') obj.select_set(True) bpy.context.view_layer.objects.active = obj # Переключение режимов bpy.ops.object.mode_set(mode='EDIT') bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.mode_set(mode='SCULPT') # Удаление bpy.data.objects.remove(obj, do_unlink=True) bpy.data.meshes.remove(mesh, do_unlink=True) # Дублирование new_obj = obj.copy() new_obj.data = obj.data.copy() # Уникальный меш bpy.context.collection.objects.link(new_obj) # Преобразовать в меш bpy.ops.object.convert(target='MESH') # Затенение bpy.ops.object.shade_smooth() bpy.ops.object.shade_flat() bpy.ops.object.shade_auto() # Единицы измерения scene = bpy.context.scene scene.unit_settings.system = 'METRIC' scene.unit_settings.scale_length = 1.0 ``` ### Советы по производительности ```python # Пакетные операции with bpy.context.temp_override(selected_objects=objects): bpy.ops.object.shade_smooth() # Отключить обновления во время пакетной обработки depsgraph = bpy.context.evaluated_depsgraph_get() depsgraph.update() # Использовать foreach для массового задания свойств mesh.vertices.foreach_set("co", flat_coordinate_list) # Профилирование import cProfile, pstats profiler = cProfile.Profile() profiler.enable() # ... код ... profiler.disable() stats = pstats.Stats(profiler).sort_stats('cumulative') stats.print_stats(20) ``` --- ## Общие паттерны для автоматизации с помощью ИИ ### Безопасный доступ к объектам ```python # ВСЕГДА проверять существование obj = bpy.data.objects.get("Name") if obj is None: raise ValueError("Object not found") # ВСЕГДА использовать явные ссылки obj = bpy.data.objects["Name"] # Бросает KeyError, если объекта нет ``` ### Потокобезопасное выполнение (для MCP) ```python # В аддоне blender-mcp: команды выполняются в главном потоке через timer # В GenesisCore: прямой доступ к bpy (тот же процесс) # Для фоновых скриптов: def run_in_main_thread(func): def wrapper(): return func() bpy.app.timers.register(wrapper, first_interval=0.01) ``` ### Шаблон обработки ошибок ```python def safe_execute(code_func): try: result = code_func() return {"success": True, "result": result} except Exception as e: import traceback traceback.print_exc() return {"success": False, "error": str(e)} # Использование result = safe_execute(lambda: create_complex_scene()) ``` ### Совместимость версий ```python # Проверка версии Blender if bpy.app.version >= (4, 0, 0): # Новый API bpy.ops.mesh.primitive_cube_add(size=2) else: # Старый API bpy.ops.mesh.primitive_cube_add(radius=1) # Определение наличия возможности has_asset_browser = hasattr(bpy.ops, 'asset') ``` --- ## Ссылки на документацию - [Документация Blender Python API](https://docs.blender.org/api/current/) - [Документация bpy.types](https://docs.blender.org/api/current/bpy.types.html) - [Документация bmesh](https://docs.blender.org/api/current/bmesh.html) - [Документация mathutils](https://docs.blender.org/api/current/mathutils.html) **Теги:** #api, #blender, #reference ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- # BLENDER-MCP — ПОЛНАЯ ДОКУМЕНТАЦИЯ Tags: #blender #mcp #reference # BLENDER-MCP — ПОЛНАЯ ДОКУМЕНТАЦИЯ ## Обзор **Репозиторий:** https://github.com/ahujasid/blender-mcp **Сайт:** https://mcp-for-blender.com/ **Версия:** 1.6+ | **Blender:** 3.0+ | **Лицензия:** MIT **PyPI:** `blender-mcp` | **Discord:** https://discord.gg/SNqPn4TcKQ --- ## Архитектура ``` ┌─────────────────────────────────────────────────────────────────┐ │ MCP CLIENTS │ │ Claude Desktop │ Cursor │ VS Code │ Codex │ OpenCode │ ... │ └─────────────────────────────────────────────────────────────────┘ │ stdio (uvx blender-mcp) ▼ ┌─────────────────────────────────────────────────────────────────┐ │ MCP SERVER (src/blender_mcp/server.py) │ │ • FastMCP server with stdio transport │ │ • Tool definitions (28+ tools) │ │ • Telemetry, trajectory capture, safe mode │ │ • Persistent TCP connection to Blender addon (port 9876) │ └─────────────────────────────────────────────────────────────────┘ │ TCP :9876 (JSON protocol) ▼ ┌─────────────────────────────────────────────────────────────────┐ │ BLENDER ADDON (addon.py) │ │ • Socket server on :9876 │ │ • Command queue → main thread execution (bpy.app.timers) │ │ • Handlers for all 28+ command types │ │ • Poly Haven, Sketchfab, Poly Pizza, Hyper3D, Hunyuan3D │ │ • Manual edit capture (undo/redo/operators) │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ BLENDER 3.0+ │ │ • bpy.data / bpy.ops / bpy.context │ │ • GPU offscreen rendering for screenshots │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## Установка ### Предварительные требования - Blender 3.0+ - Python 3.10+ - Пакетный менеджер **uv** (обязателен) ### Установка uv ```powershell # Windows powershell -c "irm https://astral.sh/uv/install.ps1 | iex" # Добавить в PATH (после этого перезапустить оболочку) $localBin = "$env:USERPROFILE\.local\bin" $userPath = [Environment]::GetEnvironmentVariable("Path", "User") [Environment]::SetEnvironmentVariable("Path", "$userPath;$localBin", "User") ``` ### Быстрая установка ```bash # 1. Установить аддон Blender uvx blender-mcp install-addon # 2. Настроить MCP-клиент (см. ниже) # 3. В Blender: N-панель → MCP for Blender → Start MCP Server ``` --- ## Настройка MCP-клиента ### OpenCode ```json { "mcp": { "blender-mcp": { "type": "local", "command": ["uvx", "blender-mcp"], "enabled": true, "environment": { "BLENDER_HOST": "localhost", "BLENDER_PORT": "9876" } } } } ``` ### Claude Desktop ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["blender-mcp"] } } } ``` Конфиг: `claude_desktop_config.json` (Settings → Developer → Edit Config) ### Cursor ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["blender-mcp"] } } } ``` Settings → MCP → Add Server (Windows: используйте `"command": "cmd", "args": ["/c", "uvx", "blender-mcp"]`) ### Codex ```bash codex mcp add blender -- uvx blender-mcp ``` Или `~/.codex/config.toml`: ```toml [mcp_servers.blender] command = "uvx" args = ["blender-mcp"] env = { BLENDER_HOST = "localhost", BLENDER_PORT = "9876" } ``` --- ## Доступные инструменты (28+) ### Базовые инструменты сцены | Инструмент | Описание | |------|-------------| | `get_scene_info` | Полная иерархия сцены (ограничено 10 объектами) | | `get_object_info` | Подробные данные объекта (трансформация, меш, материалы) | | `get_world_state_snapshot` | Компактный снимок для траектории (потолок 4000 объектов) | | `get_viewport_screenshot` | GPU-рендер offscreen → Image | | `execute_blender_code` | **Выполнение произвольного Python-кода** (пошагово!) | | `get_addon_info` | Рукопожатие версии/возможностей | ### Инструменты материалов | Инструмент | Описание | |------|-------------| | `set_texture` | Применение загруженной текстуры Poly Haven к объекту | ### Poly Haven (бесплатно, без ключа) | Инструмент | Описание | |------|-------------| | `get_polyhaven_categories` | Список категорий для hdris/textures/models | | `search_polyhaven_assets` | Поиск с фильтрами по типу и категории | | `download_polyhaven_asset` | Скачивание + импорт (HDRI→world, Texture→material, Model→scene) | | `get_polyhaven_status` | Проверка, включён ли в Blender | ### Poly Pizza (бесплатный API-ключ) | Инструмент | Описание | |------|-------------| | `get_polypizza_status` | Проверка, включён ли | | `search_polypizza_models` | Запрос, категория (0-11), лицензия (0=CC-BY,1=CC0), animated, limit | | `download_polypizza_model` | Импорт GLB, normalize_size, target_size (метры) | **Категории:** 0=Food&Drink, 1=Clutter, 2=Weapons, 3=Transport, 4=Furniture&Decor, 5=Objects, 6=Nature, 7=Animals, 8=Buildings, 9=People&Characters, 10=Scenes&Levels, 11=Other **Лицензия:** 0=CC-BY (нужен кредит), 1=CC0 (кредит не нужен) ### Sketchfab (API-ключ) | Инструмент | Описание | |------|-------------| | `get_sketchfab_status` | Проверка, включён ли | | `search_sketchfab_models` | Запрос, категории, количество, downloadable | | `get_sketchfab_model_preview` | Миниатюра Image для визуальной проверки | | `download_sketchfab_model` | Импорт, normalize_size, **target_size обязателен** (метры) | ### Hyper3D Rodin (API-ключ) | Инструмент | Описание | |------|-------------| | `get_hyper3d_status` | Проверка, включён ли | | `create_rodin_job` | Отправка задания генерации | | `poll_rodin_job_status` | Проверка статуса задания | | `import_generated_asset` | Импорт готовой модели | ### Hunyuan3D (SecretId/SecretKey) | Инструмент | Описание | |------|-------------| | `get_hunyuan3d_status` | Проверка, включён ли | | `create_hunyuan_job` | Отправка задания генерации | | `poll_hunyuan_job_status` | Проверка статуса задания | | `import_generated_asset_hunyuan` | Импорт готовой модели | --- ## Постоянные учётные данные API ### Настройки Blender (переживают перезапуск) Edit → Preferences → Add-ons → MCP for Blender: - Sketchfab API Key - Poly Pizza API Key - Hyper3D API Key - Hunyuan3D SecretId / SecretKey / API URL ### Переменные окружения (headless/CI) ```bash BLENDERMCP_SKETCHFAB_API_KEY BLENDERMCP_POLYPIZZA_API_KEY BLENDERMCP_HYPER3D_API_KEY BLENDERMCP_HUNYUAN3D_SECRET_ID BLENDERMCP_HUNYUAN3D_SECRET_KEY BLENDERMCP_HUNYUAN3D_API_URL ``` --- ## Безопасный режим Блокирует рискованные операции в `execute_blender_code`: - Ввод-вывод файлов, сеть, подпроцессы - Хендлеры/таймеры/драйверы - Регистрация классов/свойств - Загрузка внешних .blend **Включение:** ```bash BLENDER_MCP_SAFE_MODE=1 uvx blender-mcp ``` Или в конфиге MCP: ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["blender-mcp"], "env": { "BLENDER_MCP_SAFE_MODE": "1" } } } } ``` **Разрешено:** операторы Blender (моделирование, материалы, рендер, сохранение, импорт/экспорт) --- ## Развёртывание в Docker ```bash # Сборка docker build -t blender-mcp . # macOS/Windows (Docker Desktop) docker run -i --rm blender-mcp # Linux (сеть хоста) docker run -i --rm --network=host -e BLENDER_HOST=localhost blender-mcp ``` Конфиг MCP: ```json { "mcpServers": { "blender": { "command": "docker", "args": ["run", "-i", "--rm", "blender-mcp"] } } } ``` --- ## Фиксация версии Python ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["--python", "3.11", "blender-mcp"], "env": { "UV_PYTHON_PREFERENCE": "only-managed" } } } } ``` --- ## Решение проблем | Проблема | Решение | |-------|-----| | `spawn uvx ENOENT` | Укажите полный путь: `which uvx` / `where uvx` | | Отказано в соединении | Аддон Blender не запущен; нажмите "Start MCP Server" | | Poly Pizza Cloudflare 403 | Скачайте .glb вручную → File → Import → glTF | | Таймаут | Разбейте сложные запросы на шаги | | Конфликт версий | `uv cache clean blender-mcp && uvx --refresh blender-mcp` | | Headless Blender (`-b`) | Команды не выполняются; используйте GUI или `xvfb-run -a blender` | --- ## Расположение файлов | Файл | Путь | |------|------| | Аддон (установлен) | `%APPDATA%\Blender Foundation\Blender\4.2\scripts\addons\blender_mcp.py` | | Аддон (исходники) | `addon.py` в репозитории | | MCP-сервер | `uvx blender-mcp` (из кэша) | | Конфиг (настройки Blender) | Edit → Preferences → Add-ons → MCP for Blender | | Логи | Системная консоль Blender (Window → Toggle System Console) | --- ## Телеметрия - **По умолчанию:** ВКЛ (анонимные данные об использовании) - **Отключение в Blender:** Preferences → Add-ons → MCP for Blender → снять галку с телеметрии - **Отключение через env:** `DISABLE_TELEMETRY=true uvx blender-mcp` - **Данные:** имя инструмента, успех, длительность, промпты, код, скриншоты, данные сцены - **Условия:** см. `TERMS_AND_CONDITIONS.md` в репозитории --- ## Исходный код аддона (addon.py) — ключевые классы ### BlenderMCPServer ```python class BlenderMCPServer: def __init__(self, host='localhost', port=9876) def start() # Binds socket, starts thread, registers timer def stop() # Closes socket, unregisters timer, cleans up def _server_loop() # Accepts connections in daemon thread def _drain_command_queue() # Timer callback (0.05s) - executes on main thread def _handle_client() # Per-client handler thread def execute_command() # Dispatches to handlers ``` ### Ключевые обработчики - `get_scene_info()` — Упрощённые данные сцены - `get_world_state_snapshot()` — Компактный снимок траектории - `get_object_info(name)` — Полные детали объекта - `get_viewport_screenshot()` — GPU-рендер offscreen - `execute_code(code)` — **Произвольный Python** (с проверкой безопасного режима) - `drain_human_activity()` — Захват undo/redo/операторов - Обработчики Poly Haven / Sketchfab / Poly Pizza / Hyper3D / Hunyuan3D ### Захват ручных правок - Записывает операторы пользователя (кроме игнорируемых) во время MCP-сессии - Хендлеры `undo_post` / `redo_post` / `depsgraph_update_post` - Работает только при согласии на телеметрию и запущенном сервере - Сбрасывается инструментом `drain_human_activity()` --- ## Протокол - **Транспорт:** TCP-сокет (JSON поверх raw-сокета) - **Порт:** 9876 (настраивается через `BLENDER_PORT`) - **Команда:** `{"type": "cmd_name", "params": {...}}` - **Ответ:** `{"status": "success|error", "result": {...}}` или `{"status": "error", "message": "..."}` - **Блокировки:** однопоточная очередь команд в главном потоке Blender ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [BLENDER-MCP ADDON - DEEP DIVE](blender-mcp.md), [INSTALLATION GUIDE - COMPLETE SETUP](../guides/installation.md), [MCP ARCHITECTURE - MODEL CONTEXT PROTOCOL FOR BLENDER](../mcp/architecture.md), [TEXT-TO-3D WORKFLOWS](../workflows/text-to-3d.md), [TROUBLESHOOTING GUIDE](../guides/troubleshooting.md) - **По темам:** [BLENDER MATERIALS API REFERENCE](../api/materials.md), [BLENDER MESH OPERATIONS API REFERENCE](../api/mesh-operations.md), [BLENDER PYTHON API REFERENCE (4.x)](../api/blender-api.md) - **Теги:** #blender, #mcp, #reference --- # DREAM TEXTURES — ПОЛНАЯ ДОКУМЕНТАЦИЯ Tags: #reference #stable-diffusion # DREAM TEXTURES — ПОЛНАЯ ДОКУМЕНТАЦИЯ ## Обзор **Репозиторий:** https://github.com/carson-katri/dream-textures **Wiki:** https://github.com/carson-katri/dream-textures/wiki **Версия:** Latest | **Blender:** 3.3+ | **Лицензия:** GPL-3.0 --- ## Руководство по настройке (из Wiki) ### Установка 1. Скачать из [последнего релиза](https://github.com/carson-katri/dream-textures/releases/latest) 2. Windows: распаковать архив 7-Zip → получить `.zip` → установить в Blender Preferences → Add-ons 3. Включить **Dream Textures** в аддонах 4. **НЕ** устанавливать зависимости вручную — использовать только готовую сборку ### Скачивание моделей В настройках аддона найти и скачать: - `stabilityai/stable-diffusion-2-1-base` — основная модель 512x512 - `stabilityai/stable-diffusion-2-inpainting` — для инпейнтинга/аутпейнтинга - `stabilityai/stable-diffusion-2-depth` — для проекции текстур, depth-to-image - `stabilityai/stable-diffusion-x4-upscaler` — для 4-кратного апскейла средствами ИИ - `stabilityai/stable-diffusion-2-1` — дообучена под 768x768 ### Приватные модели - Сгенерировать токен Hugging Face на https://huggingface.co/settings/tokens - Вставить его в поле "Token" в настройках ### Импорт чекпойнтов (.ckpt) - Нажать "Import Checkpoint File" в настройках - Выбрать файл → указать Model Config: - `v1` — оригинальные модели CompVis (SD 1.x) - `v2 (512, epsilon)` — модели 512x512 с prediction_type="epsilon" - `v2 (768, v_prediction)` — модели 768x768 с prediction_type="v_prediction" ### DreamStudio (облако) - Ввести API-ключ с https://beta.dreamstudio.ai/membership?tab=apiKeys - Обработка в облаке, если локального железа не хватает --- ## Генерация изображений (из Wiki) ### Доступ - Открыть Image Editor или Shader Editor - Нажать `N` для боковой панели → панель **Dream** ### Параметры | Параметр | Описание | |--------|-------------| | **Pipeline** | Stable Diffusion (локально) или DreamStudio (облако) | | **Model** | Выбор установленной модели (части функций требуют конкретных моделей) | | **Prompt** | Текстовый промпт. Доступны пресеты (например, "Texture" добавляет суффикс "texture") | | **Seamless** | Циклическая свёртка для идеального тайлинга. Можно указать оси | | **Negative** | Чего избегать (например, "building", чтобы убрать здания) | | **Size** | Целевые размеры (кратны 64). Лучше всего 512x512 | | **Source Image** | Файл или Open Image для img2img | #### Режимы исходного изображения 1. **Color** — смешивание изображения с шумом 2. **Color + Generated Depth** — глубина MiDaS + цвет (требуется модель глубины) 3. **Color + Depth Map** — пользовательское изображение-карта глубины 3. **Depth** — трактовать изображение только как карту глубины #### Продвинутые параметры | Параметр | Описание | |-----------|-------------| | **Random Seed** | Автогенерация или ручной сид | | **Steps** | Шаги сэмплера (больше — лучше сходимость) | | **CFG Scale** | Сила следования промпту | | **Scheduler** | Разные скорости сходимости | | **Step Preview** | "Fast" (превью латента) или "Accurate" (декодирование VAE на каждом шаге) | | **Speed Optimizations** | Рекомендуется Half Precision | | **Memory Optimizations** | Рекомендуется Attention Slicing (Auto) | --- ## Проекция текстур (из Wiki) ### Рабочий процесс 1. Выбрать объекты в 3D Viewport 2. Войти в Edit Mode → выбрать целевые полигоны 3. Local View (`/`) чтобы ограничить карту глубины выделением 4. **Совет:** если проекция искажается, разбейте большие полигоны ### Настройки - Выбрать **модель глубины** (например, `stable-diffusion-2-depth`) - Ввести промпт - **Источник данных:** "Depth" (по умолчанию) или "Depth and Color" (использует затенение вьюпорта) - Подобрать размер изображения под соотношение сторон вьюпорта ### Выполнение - Нажать **Project Dream Texture** - Создаётся новый материал с именем-сидом - UV обновляются под угол проекции - Живое превью в режиме Viewport Shading --- ## Инпейнт и аутпейнт (из Wiki) ### Инпейнтинг - Замаскировать участок в Image Editor - Требуется модель `stable-diffusion-2-inpainting` - Промпт: "seamless continuation of [texture]" - **Как сделать текстуры бесшовными:** инпейнтить края, чтобы смешать их ### Аутпейнтинг - Расширить изображение за границы - Выбрать точку отсчёта (центр, углы, края) - Генерирует новое содержимое в том же стиле --- ## Render-движок / система нод (из Wiki) ### Добавленные ноды - **Dream Texture Node** — генерация текстур в графе шейдеров - **ControlNet Node** — управление через Depth/Canny/Normal - **Upscale Node** — апскейл средствами ИИ внутри конвейера ### Интеграция с render-pass - Использовать рендер Cycles как вход для ControlNet - Ре-стилизация через img2img - Анимация: ре-стилизация покадрово #### Управляющие параметры | Параметр | Эффект | |-----------|--------| | **Depth** | Использовать depth-pass для структуры | | **Noise Strength** | Отклонение от рендера (0-1) | | **Seed** | Воспроизводимость | --- ## Апскейл средствами ИИ (из Wiki) - Увеличение в 4 раза через ESRGAN/RealESRGAN - Опция **Tile Size** для больших изображений --- ## История (из Wiki) - Повторный вызов, экспорт и импорт генераций - **Export:** сохранить промпт + параметры + сид - **Import:** восстановить настройки предыдущей генерации --- ## Настройка для разработки (из Wiki) ```bash git clone https://github.com/carson-katri/dream-textures cd dream-textures # Установить в Blender # Установить зависимости: pip install -r requirements.txt ``` --- ## Требования к оборудованию | Компонент | Минимум | Рекомендуется | |-----------|---------|-------------| | **VRAM** | 4 ГБ | 8-12 ГБ | | **GPU** | CUDA 11.7+ / Apple Silicon | RTX 3080+ / M2 Max+ | | **RAM** | 8 ГБ | 16-32 ГБ | | **Диск** | 10 ГБ | 20 ГБ+ | --- ## Решение проблем | Проблема | Решение | |-------|-----| | OOM / CUDA OOM | Снизьте разрешение; fp16; закройте приложения | | Модель не скачивается | Проверьте интернет; HF_TOKEN для моделей с ограниченным доступом | | Карантин macOS | `xattr -r -d com.apple.quarantine /.python_dependencies` | | Медленная генерация | Меньше шагов (20-30); меньшее разрешение | | Видны швы | Включите Seamless; инпейнтить края | --- ## Интеграция с MCP Dream Textures — **самостоятельный** аддон, интеграции с MCP нет. Варианты использования: 1. Вручную через N-панель 2. Python API через `execute_blender_code`: ```python bpy.ops.dream_texture.generate( prompt="brick wall, seamless", seamless=True, width=1024, height=1024 ) ``` 3. Комбинированный сценарий: GenesisCore для геометрии → Dream Textures для материалов ## 🎯 ЦЕЛЬ Референс категории «Референсы», сложность —. --- ## 🔗 СВЯЗИ - **Прочие в категории «Референсы»:** [DREAM TEXTURES ADDON - DEEP DIVE](dream-textures.md), [INSTALLATION GUIDE - COMPLETE SETUP](../guides/installation.md), [ANIMATION WORKFLOWS](../workflows/animation.md), [BLENDER MATERIALS API REFERENCE](../api/materials.md), [BLENDER MESH OPERATIONS API REFERENCE](../api/mesh-operations.md) - **По темам:** [BLENDER PYTHON API REFERENCE (4.x)](../api/blender-api.md), [BLENDER SCRIPTING PATTERNS - SAFE & EFFECTIVE](../api/scripting-patterns.md), [BLENDER-MCP - FULL DOCUMENTATION](blender-mcp-full.md) - **Теги:** #reference, #stable-diffusion --- # SETUP 01: BLENDER-MCP BASIC Tags: #blender #mcp #setup # SETUP 01: BLENDER-MCP BASIC **ID:** SETUP-01 | **Тип:** Core Infrastructure | **Сложность:** ⭐ | **Время:** 10-15 мин --- ## 🎯 ЦЕЛЬ Установка и настройка blender-mcp для работы с IDE агентами (OpenCode, Cursor, VS Code, Codex, Claude Desktop) --- ## 📋 ПРЕДВАРИТЕЛЬНЫЕ ТРЕБОВАНИЯ | Компонент | Версия | Статус | |-----------|--------|--------| | **Blender** | 4.0+ | ✅ Обязательно | | **Python** | 3.10+ | ✅ Встроен в Blender | | **uv** | latest | ✅ Обязательно | | **Git** | latest | ⚠️ Опционально | --- ## 🪟 WINDOWS УСТАНОВКА (ПОШАГОВО) ### 1. Установка uv ```powershell # PowerShell от администратора irm https://astral.sh/uv/install.ps1 | iex # Перезапустить терминал # Проверить uv --version uvx --version ``` ### 2. Установка blender-mcp аддона ```powershell # Автоматическая установка в Blender uvx blender-mcp install-addon # Проверить путь uvx blender-mcp addon-paths # Должен показать: C:\Users\\AppData\Roaming\Blender Foundation\Blender\4.x\scripts\addons\blender_mcp.py ``` ### 3. Настройка PATH (если uvx не найден) ```powershell # Найти uvx where uvx # Обычно: C:\Users\\.local\bin\uvx.exe # Добавить в User PATH $localBin = "$env:USERPROFILE\.local\bin" $userPath = [Environment]::GetEnvironmentVariable("Path", "User") [Environment]::SetEnvironmentVariable("Path", "$userPath;$localBin", "User") # Перезапустить терминал/IDE ``` --- ## 🔧 BLENDER НАСТРОЙКА ### 1. Включить аддон ``` Edit → Preferences → Add-ons Search: "MCP for Blender" ☑ Interface: MCP for Blender ``` ### 2. Запустить MCP Server ``` N-Panel (клавиша N) → вкладка "MCP for Blender" ☑ Poly Haven ☑ Poly Pizza (вставить API ключ) ☐ Sketchfab (опционально) ☐ Hyper3D / Hunyuan3D (опционально) 🟢 Кнопка: "Start MCP Server" ``` ### 3. Проверить в System Console ``` Window → Toggle System Console Должно появиться: "BlenderMCP server started on localhost:9876" ``` --- ## 🤖 IDE АГЕНТЫ КОНФИГУРАЦИЯ ### OpenCode (`~/.config/opencode/opencode.jsonc`) ```jsonc { "mcp": { "blender-mcp": { "type": "local", "command": ["uvx", "blender-mcp"], "enabled": true, "environment": { "BLENDER_HOST": "localhost", "BLENDER_PORT": "9876" } } } } ``` ### Cursor (Settings → MCP → Add Server) ```json { "mcpServers": { "blender": { "command": "cmd", "args": ["/c", "uvx", "blender-mcp"] } } } ``` ### VS Code (MCP Extension) ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["blender-mcp"] } } } ``` ### Codex ```bash codex mcp add blender -- uvx blender-mcp ``` ### Claude Desktop (`claude_desktop_config.json`) ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["blender-mcp"] } } } ``` --- ## ✅ ТЕСТ ПОДКЛЮЧЕНИЯ ### В OpenCode / терминале: ```bash # Простой тест "Получи информацию о текущей сцене" ``` ### Ожидаемый ответ: ```json { "name": "Scene", "object_count": 2, "objects": [ {"name": "Cube", "type": "MESH", "location": [0, 0, 0]}, {"name": "Light", "type": "LIGHT", "location": [0, 0, 5]} ], "materials_count": 1 } ``` ### Тест создания объекта: ```bash "Создай куб 'TestCube' в позиции (2, 0, 0) с масштабом (1, 2, 1)" ``` --- ## 🔑 API КЛЮЧИ (ОПЦИОНАЛЬНО) | Сервис | Где получить | Куда вставить | |--------|--------------|---------------| | **Poly Pizza** | https://poly.pizza/settings/api | Blender Preferences → Add-ons → MCP for Blender → Poly Pizza API Key | | **Sketchfab** | https://sketchfab.com/developers/api | Blender Preferences → Add-ons → MCP for Blender → Sketchfab API Key | | **Hyper3D** | https://hyper3d.ai/ | Blender Preferences → Add-ons → MCP for Blender → Hyper3D API Key | | **Hunyuan3D** | https://3d.hunyuan.tencent.com/ | Blender Preferences → Add-ons → MCP for Blender → SecretId/SecretKey | --- ## ⚠️ ЧАСТЫЕ ПРОБЛЕМЫ | Проблема | Решение | |----------|---------| | `spawn uvx ENOENT` | Добавить `~/.local/bin` в PATH, перезапустить IDE | | `Connection refused` | Blender не запущен или MCP Server не стартовал (N-panel → Start) | | `Timeout` | Слишком сложный запрос → разбить на шаги | | `Poly Pizza 403` | Cloudflare блок → скачать .glb вручную → File → Import → glTF | | `ModuleNotFoundError: mcp` | Запустить Blender от админа один раз для автоустановки deps | --- ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 09: BLENDERBOT / AI CODING AGENT](setup_09_blenderbot_ai.md), [SETUP 35: READY PLAYER ME AVATARS](setup_35_readyplayerme.md), [SETUP 45: HUNYUAN3D 2.1 LOCAL](setup_45_hunyuan3d_21.md), [SETUP 02: GENESISCORE LOCAL (OLLAMA)](setup_02_genesiscore_local.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 06: CUSTOM MCP TOOLS](setup_06_custom_mcp_tools.md), [SETUP 07: CI/CD INTEGRATION](setup_07_ci_cd.md) - **Теги:** #blender, #mcp, #setup --- # SETUP 02: GENESISCORE LOCAL (OLLAMA) Tags: #llm #mcp #setup # SETUP 02: GENESISCORE LOCAL (OLLAMA) **ID:** SETUP-02 | **Type:** Local LLM Integration | **Difficulty:** ⭐⭐ | **Time:** 15-30 min --- ## 🎯 ЦЕЛЬ Настроить GenesisCore для работы с локальными LLM через Ollama (приватность, бесплатно, офлайн) --- ## 📋 ПРЕДВАРИТЕЛЬНЫЕ ТРЕБОВАНИЯ | Компонент | Версия | Статус | |-----------|--------|--------| | **Blender** | 4.0+ | ✅ Обязательно | | **Ollama** | latest | ✅ Обязательно | | **RAM** | 16GB+ | 🟡 Рекомендуется | | **VRAM** | 8GB+ | 🟡 Для 7B+ моделей | --- ## 🪟 WINDOWS УСТАНОВКА ### 1. Установка Ollama ```powershell # Скачать и установить winget install Ollama.Ollama # Или с официального сайта # https://ollama.com/download/windows # Проверить ollama --version ollama list ``` ### 2. Загрузка моделей для кодинга/блендера ```powershell # Лучшие модели для Blender Python (по приоритету) ollama pull codellama:13b # Лучшая для кода, 7.3GB ollama pull deepseek-coder:6.7b # Отличная для Python, 3.8GB ollama pull qwen2.5-coder:7b # Новая, быстрая, 4.7GB ollama pull llama3.2:3b # Легкая, быстрая, 2.0GB # Проверить ollama list ``` ### 3. Запуск Ollama сервера ```powershell # Должен работать в фоне (автозапуск при установке) # Проверить: curl http://localhost:11434/api/tags # Если не работает: ollama serve ``` --- ## 🔧 НАСТРОЙКА GENESISCORE ### 1. Установка аддона (если ещё не) ```bash # В Blender: Edit → Preferences → Add-ons → Install from ZIP # Выбрать: GenesisCore-main.zip # Включить: GenesisCore ``` ### 2. Настройка провайдера ``` N-Panel → GenesisCore вкладка: 1. Provider: ▼ Ollama 2. Base URL: http://localhost:11434/v1 (важно: /v1 на конце!) 3. API Key: (оставить пустым для Ollama) 4. Нажать "Fetch Models" 4. Выбрать модель: codellama:13b / deepseek-coder:6.7b / qwen2.5-coder:7b 5. Tool Modules: включить все (Shift+Click) 6. Save Config 7. Connect ``` ### 3. Настройки модели (рекомендуемые) ```json { "provider": "Ollama", "base_url": "http://localhost:11434/v1", "model": "codellama:13b", "temperature": 0.1, "top_p": 0.9, "max_tokens": 4096, "use_history": true } ``` --- ## 🔧 ОПТИМИЗАЦИЯ ПРОИЗВОДИТЕЛЬНОСТИ ### GPU Offloading (NVIDIA) ```powershell # Ollama автоматически использует GPU если есть CUDA # Проверить: ollama run codellama:13b "test" # Должно показать: "using GPU" в логах # Принудительно включить GPU layers: # В переменных окружения: OLLAMA_NUM_GPU=999 OLLAMA_GPU_LAYERS=999 ``` ### Оптимизация памяти ```yaml # В настройках GenesisCore: use_history: false # Отключить историю для экономии токенов max_tokens: 2048 # Ограничить длину ответа temperature: 0.1 # Детерминированные ответы ``` ### Квантование (если мало VRAM) ```powershell # Использовать квантованные версии: ollama pull codellama:13b-q4_K_M # 4-bit, 7.3GB → 4.7GB ollama pull deepseek-coder:6.7b-q4_K_M # 3.8GB → 2.5GB ``` --- ## 🧪 ТЕСТИРОВАНИЕ ### Быстрый тест в GenesisCore чате ``` "Создай куб 'TestCube' в позиции (1, 2, 3) с материалом красного цвета" ``` ### Ожидаемый результат 1. Агент анализирует запрос 2. Вызывает `create_object` + `set_material` 3. В Blender появляется куб в (1, 2, 3) с красным материалом --- ## 🔧 УСТРАНЕНИЕ НЕИСПРАВНОСТЕЙ | Проблема | Решение | |----------|---------| | `Connection refused` | Ollama не запущен → `ollama serve` | | `Model not found` | `ollama pull ` | | `Out of memory` | Использовать меньшую модель (3b/7b) или q4 квантование | | `Slow response` | Включить GPU offloading, уменьшить `max_tokens` | | `Base URL error` | Проверить `http://localhost:11434/v1` (с /v1!) | --- ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 35: READY PLAYER ME AVATARS](setup_35_readyplayerme.md), [SETUP 45: HUNYUAN3D 2.1 LOCAL](setup_45_hunyuan3d_21.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 06: CUSTOM MCP TOOLS](setup_06_custom_mcp_tools.md), [SETUP 09: BLENDERBOT / AI CODING AGENT](setup_09_blenderbot_ai.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [MCP ADVANCED: STREAMING & TOOL COMPOSITION](../mcp_advanced/streaming_tools.md), [P27: POLY HAVEN ASSET PIPELINE](../prompts/27_polyhaven_pipeline.md) - **Теги:** #llm, #mcp, #setup --- # SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER) Tags: #blender #setup #stable-diffusion # SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER) **ID:** SETUP-03 | **Type:** Local AI Texturing | **Difficulty:** ⭐⭐ | **Time:** 20-40 min (зависит от загрузки моделей) --- ## 🎯 ЦЕЛЬ Установить и настроить Dream Textures для генерации PBR текстур, инпейнтинга, аутпейнтинга и проекции текстур прямо в Blender --- ## 📋 ПРЕДВАРИТЕЛЬНЫЕ ТРЕБОВАНИЯ | Компонент | Минимум | Рекомендуется | |-----------|---------|---------------| | **Blender** | 3.3+ | 4.0+ | | **GPU** | 4GB VRAM | 8-12GB VRAM | | **RAM** | 8GB | 16-32GB | | **Диск** | 10GB | 20GB+ | | **CUDA** | 11.7+ | 12.1+ | | **Python** | 3.10+ | 3.10+ (встроен в Blender) | --- ## 🪟 УСТАНОВКА ### 1. Скачать релиз ```bash # Последний релиз: # https://github.com/carson-katri/dream-textures/releases/latest # Скачать: dream_textures-.zip # НЕ распаковывать! Устанавливать как ZIP. ``` ### 2. Установка в Blender ``` Edit → Preferences → Add-ons → Install from ZIP Выбрать: dream_textures-.zip ☑ Dream Textures ``` ### 3. Первая загрузка (автоматическая) ``` N-Panel → Dream Textures вкладка → Preferences → Download Models ``` **Скачиваются автоматически (~2-4 GB):** - PyTorch + TorchVision - Diffusers + Transformers - Safetensors + Accelerate --- ## 🤖 ВЫБОР МОДЕЛЕЙ (Preferences → Models) ### ОБЯЗАТЕЛЬНЫЕ (базовые) | Модель | Назначение | Размер | |--------|------------|--------| | `stabilityai/stable-diffusion-2-1-base` | Основная генерация 512x512 | ~2.1 GB | | `stabilityai/stable-diffusion-2-inpainting` | Инпейтинг/аутпейтинг | ~2.1 GB | ### РЕКОМЕНДУЕМЫЕ (дополнительные) | Модель | Назначение | Размер | |--------|------------|--------| | `stabilityai/stable-diffusion-2-depth` | Texture Projection, Depth-to-Image | ~2.1 GB | | `stabilityai/stable-diffusion-x4-upscaler` | AI Upscaling 4x | ~0.5 GB | | `stabilityai/stable-diffusion-2-1` | Генерация 768x768 | ~2.1 GB | ### ПРОДВИНУТЫЕ (опционально) | Модель | Назначение | |--------|------------| | `stabilityai/sdxl-base-1.0` | SDXL качество (требует 8GB+ VRAM) | | `runwayml/stable-diffusion-v1-5` | SD 1.5 стиль | | Custom `.ckpt` / `.safetensors` | Любые файн-тюны (Import Checkpoint) | --- ## ⚙️ НАСТРОЙКИ ПРЕФЕРЕНСЕС ### Device & Precision ``` Preferences → Dream Textures: Device: CUDA (или MPS для Apple Silicon) Precision: fp16 (Half) - рекомендуется Speed Optimizations: Half Precision ✓ Memory Optimizations: Attention Slicing (Auto) ✓ ``` ### Model Cache ``` Model Cache Directory: (можно изменить на другой диск) # По умолчанию: %APPDATA%\Blender Foundation\Blender\4.x\scripts\addons\dream_textures\.python_dependencies ``` ### DreamStudio (облачный фоллбэк) ``` API Key: (опционально, если GPU слабый) # https://beta.dreamstudio.ai/membership?tab=apiKeys ``` --- ## 🎨 БЫСТРЫЙ СТАРТ (3 ШАГА) ### 1. Генерация текстуры ``` 1. Выбери объект → N-Panel → Dream Textures → Generate 2. Prompt: "weathered brick wall, moss in cracks, seamless, 8k" 3. ✓ Seamless 4. Model: stable-diffusion-2-1-base 5. Size: 512x512 (или 1024x1024 если 8GB+ VRAM) 6. Generate → Apply to Material ``` ### 2. Texture Projection (на модель) ``` 1. Tab → Edit Mode → Select faces → Numpad / (Local View) 2. Dream Textures → Project Dream Texture 3. Model: stable-diffusion-2-depth (ОБЯЗАТЕЛЬНО!) 4. Prompt: "ancient stone wall, moss, cracks, torch light" 5. Project → Авто UV + Материал ``` ### 3. Inpainting (исправление швов) ``` 1. Image Editor → Открыть текстуру 2. Mask проблемную область (RMB drag) 5. Dream → Inpaint 6. Prompt: "seamless continuation of stone texture" 7. Generate ``` --- ## ⚙️ ОПТИМИЗАЦИЯ VRAM | Настройка | 4GB VRAM | 8GB VRAM | 12GB+ VRAM | |-----------|----------|----------|------------| | **Resolution** | 512x512 | 512-768 | 768-1024 | | **Precision** | fp16 | fp16 | fp16/bf16 | | **Attention Slicing** | Auto | Auto | Auto | | **Model Offloading** | Sequential | Auto | None | | **Steps** | 20-25 | 25-30 | 30-50 | ### При нехватке памяти: ```python # В Blender Python Console: import torch torch.cuda.empty_cache() # Или перезапустить Blender ``` --- ## 🌐 MACOS СПЕЦИФИКА ```bash # Если ошибка "quarantine": xattr -r -d com.apple.quarantine ~/Library/Application\ Support/Blender/4.x/scripts/addons/dream_textures/.python_dependencies # Запуск Blender для логов: /Applications/Blender.app/Contents/MacOS/Blender ``` --- ## 🔧 УСТРАНЕНИЕ НЕИСПРАВНОСТЕЙ | Ошибка | Решение | |--------|---------| | `CUDA out of memory` | Уменьшить Resolution, Steps; закрыть другие приложения | | `Model download failed` | Проверить интернет; HF_TOKEN для гейтнутых моделей | | `ModuleNotFoundError: torch` | Переустановить аддон; проверить `.python_dependencies` | | `macOS quarantine` | Команда `xattr` выше | | `Slow generation` | fp16, Attention Slicing, меньше steps | --- ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 15: BLENDER + COMFYUI SDXL](setup_15_comfyui_sdxl.md), [SETUP 16: BLENDER + AUTOMATIC1111 API](setup_16_automatic1111_api.md), [SETUP 35: READY PLAYER ME AVATARS](setup_35_readyplayerme.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 07: CI/CD INTEGRATION](setup_07_ci_cd.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 09: BLENDERBOT / AI CODING AGENT](setup_09_blenderbot_ai.md), [SETUP 10: BLENDER + COMPOSITING (NUKE/AFTER EFFECTS ROUNDTRIP)](setup_10_compositing_roundtrip.md) - **Теги:** #blender, #setup, #stable-diffusion --- # SETUP 04: HEADLESS RENDER (CLI / CI/CD) Tags: #automation #ci #render #setup # SETUP 04: HEADLESS RENDER (CLI / CI/CD) **ID:** SETUP-04 | **Type:** Automated Rendering | **Difficulty:** ⭐⭐ | **Time:** 15-30 min --- ## 🎯 ЦЕЛЬ Настроить рендеринг без GUI для CI/CD, серверов, батч-обработки --- ## 📋 ПРЕДВАРИТЕЛЬНЫЕ ТРЕБОВАНИЯ | Компонент | Linux | Windows (WSL2) | macOS | |-----------|-------|----------------|-------| | **Blender** | 4.0+ | 4.0+ | 4.0+ | | **GPU** | NVIDIA + CUDA | NVIDIA + CUDA | Apple Silicon / AMD | | **Display** | Virtual (Xvfb) | Virtual (Xvfb) | Native | | **Python** | 3.10+ | 3.10+ | 3.10+ | --- ## 🐧 LINUX УСТАНОВКА ### 1. Установка зависимостей ```bash # Ubuntu/Debian sudo apt update && sudo apt install -y \ blender \ xvfb \ libgl1-mesa-glx \ libglib2.0-0 \ libsm6 \ libxext6 \ libxrender1 \ libxfixes3 \ libxi6 \ libxrandr2 \ libxcb1 \ libx11-6 \ libglu1-mesa \ libegl1 \ libgles2 # Для NVIDIA GPU: sudo apt install -y nvidia-driver-535 nvidia-cuda-toolkit ``` ### 2. Проверка Blender ```bash # GUI режим blender --version # Headless (background) режим blender -b --version # С GPU поддержкой blender -b -E CYCLES --cycles-device CUDA --version ``` --- ## 🪟 WINDOWS (WSL2) ### 1. Включить WSL2 ```powershell wsl --install wsl --set-default-version 2 ``` ### 2. В WSL2 (Ubuntu) ```bash # Тот же процесс что и Linux # GPU passthrough работает автоматически в WSL2 с драйверами Windows ``` --- ## 🍎 MACOS ### 1. Установка ```bash # Homebrew brew install --cask blender # Или скачать с blender.org ``` ### 2. Headless работает нативно ```bash /Applications/Blender.app/Contents/MacOS/Blender -b --version ``` --- ## 🎬 БАЗОВЫЕ КОМАНДЫ РЕНДЕРА ### Одиночный кадр ```bash blender -b scene.blend \ -E CYCLES \ -o //renders/frame_#### \ -f 1 ``` ### Анимация (диапазон кадров) ```bash blender -b scene.blend \ -E CYCLES \ -o //renders/anim_#### \ -s 1 -e 250 -j 1 \ -a ``` ### С кастомными настройками ```bash blender -b scene.blend \ -E CYCLES \ -o //renders/ \ -f 1 \ -P render_settings.py ``` --- ## ⚙️ PYTHON СКРИПТЫ ДЛЯ НАСТРОЕК (`-P script.py`) ### render_settings.py ```python import bpy scene = bpy.context.scene # Engine scene.render.engine = 'CYCLES' scene.cycles.device = 'GPU' scene.cycles.samples = 256 scene.cycles.use_denoising = True scene.cycles.denoiser = 'OPTIX' # Resolution scene.render.resolution_x = 3840 scene.render.resolution_y = 2160 scene.render.resolution_percentage = 100 # Output scene.render.filepath = "//renders/render_" scene.render.image_settings.file_format = 'OPEN_EXR_MULTILAYER' scene.render.image_settings.color_mode = 'RGBA' scene.render.image_settings.color_depth = '32' scene.render.image_settings.exr_codec = 'ZIP' scene.render.image_settings.use_preview = True # Color Management scene.view_settings.view_transform = 'Filmic' scene.view_settings.look = 'Medium High Contrast' # Performance scene.cycles.use_adaptive_sampling = True scene.cycles.adaptive_threshold = 0.01 scene.cycles.adaptive_min_samples = 32 # Save bpy.ops.wm.save_mainfile(filepath="scene_render_ready.blend") ``` ### Запуск с настройками ```bash blender -b scene.blend -P render_settings.py -o //renders/ -f 1 ``` --- ## 🎞️ БАТЧ РЕНДЕР (МНОГО ФАЙЛОВ) ### batch_render.py ```python import bpy import sys import os # Аргументы после -- argv = sys.argv[sys.argv.index("--") + 1:] input_dir = argv[0] output_dir = argv[1] # Найти все .blend файлы blend_files = [f for f in os.listdir(input_dir) if f.endswith('.blend')] for blend_file in blend_files: filepath = os.path.join(input_dir, blend_file) name = os.path.splitext(blend_file)[0] # Открыть bpy.ops.wm.open_mainfile(filepath=filepath) # Настроить вывод scene = bpy.context.scene scene.render.filepath = os.path.join(output_dir, f"{name}_####") # Рендер анимации bpy.ops.render.render(animation=True, write_still=False) print(f"Rendered: {name}") # Выход bpy.ops.wm.quit_blender() ``` ### Запуск батча ```bash blender -b -P batch_render.py -- /path/to/scenes /path/to/output ``` --- ## 🐳 DOCKER (ПОЛНАЯ ИЗОЛЯЦИЯ) ### Dockerfile ```dockerfile FROM nvidia/cuda:12.1-devel-ubuntu22.04 # Install dependencies RUN apt-get update && apt-get install -y \ blender \ xvfb \ python3-pip \ && rm -rf /var/lib/apt/lists/* # Python deps RUN pip3 install bpy-sdk # Working dir WORKDIR /workspace # Entry point ENTRYPOINT ["xvfb-run", "-a", "blender", "-b"] CMD ["--help"] ``` ### Сборка и запуск ```bash docker build -t blender-render . docker run --gpus all -v $(pwd)/scenes:/workspace -v $(pwd)/output:/output blender-render -b /workspace/scene.blend -o /output/render_ -f 1 ``` --- ## 🔄 CI/CD ИНТЕГРАЦИЯ ### GitHub Actions (.github/workflows/render.yml) ```yaml name: Render Animation on: push: paths: ['scenes/**/*.blend'] jobs: render: runs-on: ubuntu-latest container: image: nvidia/cuda:12.1-devel-ubuntu22.04 options: --gpus all steps: - uses: actions/checkout@v4 - name: Install Blender run: | apt-get update && apt-get install -y blender xvfb - name: Render run: | xvfb-run -a blender -b scenes/animation.blend \ -E CYCLES -o //renders/anim_#### -a - name: Upload Artifacts uses: actions/upload-artifact@v4 with: name: rendered-frames path: renders/ retention-days: 7 ``` ### GitLab CI (.gitlab-ci.yml) ```yaml render: image: nvidia/cuda:12.1-devel-ubuntu22.04 services: - name: xvfb variables: NVIDIA_VISIBLE_DEVICES: all script: - apt-get update && apt-get install -y blender - xvfb-run -a blender -b $SCENE_FILE -E CYCLES -o //renders/ -a artifacts: paths: - renders/ expire_in: 1 week ``` --- ## 📊 МОНИТОРИНГ И ЛОГИ ### Прогресс рендера ```bash # В логах ищите: # "Fra: 0042 Time: 00:15.23" # Python callback для прогресса import bpy def render_progress(scene): frame = scene.frame_current total = scene.frame_end - scene.frame_start + 1 percent = (frame / total) * 100 print(f"PROGRESS: {percent:.1f}% ({frame}/{total})") bpy.app.handlers.render_post.append(render_progress) ``` ### Таймауты и ретраи ```bash # Таймаут на кадр (секунды) timeout 300 blender -b scene.blend -f 1 # Ретраи при ошибке for i in {1..3}; do blender -b scene.blend -f $FRAME && break || sleep 10 done ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 05: RENDER FARM (FLAMENCO)](setup_05_render_farm.md), [SETUP 07: CI/CD INTEGRATION](setup_07_ci_cd.md), [SETUP 35: READY PLAYER ME AVATARS](setup_35_readyplayerme.md), [SETUP 40: AMBIENTCG + POLYHAVEN BATCH](setup_40_ambientcg_polyhaven.md), [SETUP 54: CLOUD RENDER AWS/GCP](setup_54_cloud_render_aws.md) - **По темам:** [P29: BATCH PROCESSING & CLI](../prompts/29_batch_cli.md), [P40: VERSION CONTROL & RENDER JOBS](../prompts/40_version_control.md), [CASE 03: PRODUCT VISUALIZATION](../cases/case_03_product_viz.md) - **Теги:** #automation, #ci, #render, #setup --- # SETUP 05: RENDER FARM (FLAMENCO) Tags: #render #render-farm #setup # SETUP 05: RENDER FARM (FLAMENCO) **ID:** SETUP-05 | **Type:** Distributed Rendering | **Difficulty:** ⭐⭐⭐ | **Time:** 30-60 min --- ## 🎯 ЦЕЛЬ Настроить Flamenco (официальный рендер-ферма Blender) для распределённого рендеринга на кластере машин --- ## 📋 АРХИТЕКТУРА FLAMENCO ``` ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ MANAGER │────▶│ WORKER 1 │ │ WORKER N │ │ (Server) │ │ (Render) │ │ (Render) │ │ Port 8080 │ │ Port 8081 │ │ Port 8081 │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ ▼ ▼ ▼ ┌─────────────────────────────────────────────────────┐ │ SHARED STORAGE (NFS/SMB) │ │ .blend files | Textures | Output | Logs │ └─────────────────────────────────────────────────────┘ ``` --- ## 🖥️ ТРЕБОВАНИЯ | Компонент | Manager | Worker | |-----------|---------|--------| | **OS** | Linux (Ubuntu 22.04+) | Linux / Windows / macOS | | **CPU** | 4+ cores | 8+ cores (по ядру на таск) | | **RAM** | 8 GB | 16-32 GB | | **GPU** | Не обязательно | NVIDIA RTX (CUDA) | | **Storage** | 100 GB+ (SSD) | 50 GB+ | | **Network** | 1 Gbps+ | 1 Gbps+ | | **Blender** | 4.0+ | 4.0+ (та же версия) | --- ## 📦 УСТАНОВКА MANAGER (SERVER) ### 1. Скачать Flamenco ```bash # Последний релиз: # https://github.com/blender/flamenco/releases/latest # Скачать flamenco-manager-linux.tar.gz wget https://github.com/blender/flamenco/releases/download/v3.6/flamenco-manager-linux.tar.gz tar -xzf flamenco-manager-linux.tar.gz cd flamenco-manager ``` ### 2. Конфигурация (flamenco-manager.yaml) ```yaml # flamenco-manager.yaml manager: listen_addr: "0.0.0.0:8080" auth: jwt_secret: "your-super-secret-jwt-key-change-me" allow_anonymous: false storage: local: path: "/mnt/flamenco-storage" # или NFS: "nfs://server:/path" database: sqlite: path: "/var/lib/flamenco/flamenco.sqlite" blender: executable: "/usr/bin/blender" # или путь к конкретной версии log: level: "info" json: false ``` ### 3. Запуск Manager ```bash # Создать storage sudo mkdir -p /mnt/flamenco-storage/{jobs,output,logs} sudo chown -R $USER:$USER /mnt/flamenco-storage # Запуск ./flamenco-manager -config flamenco-manager.yaml # Как systemd сервис: sudo cp flamenco-manager.service /etc/systemd/system/ sudo systemctl enable flamenco-manager sudo systemctl start flamenco-manager ``` ### 4. Проверка ```bash # Web UI: http://server-ip:8080 # API: http://server-ip:8080/api curl http://localhost:8080/api/health ``` --- ## 🖥️ УСТАНОВКА WORKER (RENDER NODES) ### 1. Установка Worker ```bash # На каждой рендер-машине: wget https://github.com/blender/flamenco/releases/download/v3.6/flamenco-worker-linux.tar.gz tar -xzf flamenco-worker-linux.tar.gz cd flamenco-worker ``` ### 2. Конфигурация Worker (flamenco-worker.yaml) ```yaml worker: manager_url: "http://<АДРЕС-МЕНЕДЖЕРА>:8080" worker_name: "worker-gpu-01" # уникальное имя platform: "linux" # linux/windows/macos # GPU настройки gpu_devices: ["cuda:0", "cuda:1"] # или ["all"] # Blender blender_executable: "/usr/bin/blender" blender_args: ["--cycles-device", "CUDA"] # Ресурсы max_concurrent_tasks: 2 # по одному на GPU cpu_affinity: "auto" # Storage (shared с Manager) storage: local: path: "/mnt/flamenco-storage" # Logs log_level: "info" ``` ### 3. GPU Настройка (NVIDIA) ```bash # Установить драйверы sudo apt install -y nvidia-driver-535 nvidia-cuda-toolkit # Проверить nvidia-smi nvcc --version # В worker конфиге указать GPU # gpu_devices: ["cuda:0"] # для одного GPU # gpu_devices: ["cuda:0", "cuda:1"] # для двух GPU ``` ### 4. Запуск Worker ```bash # Тестовый запуск ./flamenco-worker -config flamenco-worker.yaml # Как systemd сервис (на каждой машине): sudo cp flamenco-worker.service /etc/systemd/system/ sudo systemctl enable flamenco-worker sudo systemctl start flamenco-worker # Проверить статус sudo systemctl status flamenco-worker ``` --- ## 🌐 НАСТРОЙКА СТОРАДЖ (SHARED STORAGE) ### Вариант А: NFS (Linux) ```bash # На Manager (Server) sudo apt install nfs-kernel-server echo "/mnt/flamenco-storage *(rw,sync,no_subtree_check,no_root_squash)" >> /etc/exports sudo exportfs -ra # На Workers (Client) sudo apt install nfs-common sudo mkdir -p /mnt/flamenco-storage echo "<АДРЕС-МЕНЕДЖЕРА>:/mnt/flamenco-storage /mnt/flamenco-storage nfs defaults 0 0" >> /etc/fstab sudo mount -a ``` ### Вариант Б: SMB (Cross-platform) ```bash # На Manager sudo apt install samba # smb.conf: # [flamenco] # path = /mnt/flamenco-storage # read only = no # guest ok = yes # На Windows Worker net use Z: \\<АДРЕС-МЕНЕДЖЕРА>\flamenco /persistent:yes ``` --- ## 🚀 ИСПОЛЬЗОВАНИЕ ### 1. Web UI (http://<АДРЕС-МЕНЕДЖЕРА>:8080) ``` 1. Login (admin / пароль из конфига) 2. Jobs → Submit Job 3. Выбрать .blend файл 4. Настроить: Frames, Resolution, Samples, Output 5. Submit ``` ### 2. CLI Submission ```bash # flamenco-cli pip install flamenco-client # Submit job flamenco-cli job submit \ --manager http://<АДРЕС-МЕНЕДЖЕРА>:8080 \ --blend-file scene.blend \ --frame-start 1 --frame-end 250 \ --output //renders/anim_#### \ --engine CYCLES --samples 256 ``` ### 3. Python API ```python from flamenco_client import FlamencoClient client = FlamencoClient("http://<АДРЕС-МЕНЕДЖЕРА>:8080", token="YOUR_JWT") job = client.submit_job( blend_file="scene.blend", frame_range=(1, 250), output_path="//renders/anim_####", render_settings={ "engine": "CYCLES", "samples": 256, "resolution": (3840, 2160), } ) # Poll status while True: status = client.get_job_status(job.id) print(f"Progress: {status.progress}%") if status.state in ["finished", "failed"]: break time.sleep(30) ``` --- ## 📊 МОНИТОРИНГ И ОПТИМИЗАЦИЯ ### Мониторинг (Prometheus + Grafana) ```yaml # prometheus.yml scrape_configs: - job_name: 'flamenco' static_configs: - targets: ['<АДРЕС-МЕНЕДЖЕРА>:8080', 'WORKER1:8081', 'WORKER2:8081'] ``` ### Ключевые метрики | Метрика | Норма | Action | |---------|-------|--------| | **Queue Length** | < 10 | Добавить workers | | **Worker Utilization** | 80-95% | Баланс нагрузки | | **Frame Time** | < 5 мин | Оптимизировать сцену | | **VRAM Usage** | < 90% | Уменьшить samples/textures | | **Failed Frames** | < 1% | Проверить логи | --- ## 🔧 ОПТИМИЗАЦИЯ ПРОИЗВОДИТЕЛЬНОСТИ ### Worker Tuning ```yaml worker: max_concurrent_tasks: 2 # 1 на GPU blender_args: [ "--cycles-device", "CUDA", "--cycles-feature-set", "SUPPORTED" ] # Для больших сцен memory_limit_gb: 20 # лимит RAM на таск ``` ### Job Splitting (для больших анимаций) ```yaml # Разбить 1000 кадров на чанки по 50 frame_chunks: 50 # Flamenco сам распределит между workers ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 04: HEADLESS RENDER (CLI / CI/CD)](setup_04_headless_render.md), [SETUP 53: FLAMENCO FARM CLUSTER](setup_53_flamenco_farm.md), [SETUP 54: CLOUD RENDER AWS/GCP](setup_54_cloud_render_aws.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 02: GENESISCORE LOCAL (OLLAMA)](setup_02_genesiscore_local.md) - **По темам:** [P29: BATCH PROCESSING & CLI](../prompts/29_batch_cli.md), [P47: ALEMBIC CACHE PIPELINE](../prompts/47_alembic_cache.md), [P49: RENDER FARM SUBMIT](../prompts/49_farm_submit.md), [CASE 03: PRODUCT VISUALIZATION](../cases/case_03_product_viz.md) - **Теги:** #render, #render-farm, #setup --- # SETUP 06: CUSTOM MCP TOOLS Tags: #mcp #setup # SETUP 06: CUSTOM MCP TOOLS **ID:** SETUP-06 | **Type:** Extending Blender-MCP / GenesisCore | **Difficulty:** ⭐⭐⭐ | **Time:** 30-60 min --- ## 🎯 ЦЕЛЬ Создать собственные MCP инструменты для специфических задач: кастомные примитивы, экспорт в форматы, интеграция с внешними API --- ## 📋 АРХИТЕКТУРА MCP TOOLS ### Blender-MCP (External Server) ``` ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ │ MCP Client │────▶│ MCP Server │────▶│ Blender Addon │ │ (OpenCode/ │ │ (Python/FastMCP)│ │ (TCP :9876) │ │ Cursor/etc) │ │ Custom Tools │ │ bpy execution │ └─────────────────┘ └──────────────────┘ └─────────────────┘ ``` ### GenesisCore (Built-in) ``` ┌─────────────────┐ ┌──────────────────┐ │ GenesisCore │────▶│ Blender Process │ │ (Built-in │ │ Custom Tools │ │ MCP Client) │ │ Direct bpy │ └─────────────────┘ └──────────────────┘ ``` --- ## 🔧 BLANDER-MCP: СОЗДАНИЕ CUSTOM TOOLS ### 1. Структура проекта ``` my_blender_mcp/ ├── pyproject.toml ├── src/ │ └── my_blender_mcp/ │ ├── __init__.py │ ├── server.py │ └── tools/ │ ├── __init__.py │ ├── export_tools.py │ └── geometry_tools.py └── tests/ ``` ### 2. pyproject.toml ```toml [project] name = "my-blender-mcp" version = "0.1.0" description = "Custom MCP tools for Blender" requires-python = ">=3.10" dependencies = [ "blender-mcp>=1.6", "mcp>=1.0", "fastmcp>=2.0", ] [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project.entry-points.mcp] my_tools = "my_blender_mcp.server:register_tools" ``` ### 3. Custom Tool: Export to glTF with Settings ```python # src/my_blender_mcp/tools/export_tools.py from mcp.server.fastmcp import FastMCP from typing import Optional import json def register_export_tools(mcp: FastMCP): @mcp.tool() async def export_gltf_advanced( filepath: str, export_format: str = "GLB", export_animations: bool = True, export_materials: bool = True, export_tangents: bool = True, export_cameras: bool = False, export_lights: bool = False, apply_modifiers: bool = True, export_selected_only: bool = False, ) -> str: """ Export scene to glTF/GLB with advanced options. Args: filepath: Output path (relative to .blend or absolute) export_format: "GLB" or "GLTF_EMBEDDED" or "GLTF_SEPARATE" export_animations: Include animations export_materials: Include PBR materials export_tangents: Include tangent space for normal maps export_cameras: Include cameras export_lights: Include lights apply_modifiers: Apply modifiers before export export_selected_only: Export only selected objects """ # This will be executed in Blender via the addon return json.dumps({ "type": "export_gltf_advanced", "params": { "filepath": filepath, "export_format": export_format, "export_animations": export_animations, "export_materials": export_materials, "export_tangents": export_tangents, "export_cameras": export_cameras, "export_lights": export_lights, "apply_modifiers": apply_modifiers, "export_selected_only": export_selected_only, } }) @mcp.tool() async def export_fbx_game( filepath: str, apply_unit_scale: bool = True, bake_space_transform: bool = True, object_types: list = None, primary_bone_axis: str = "Y_FORWARD", secondary_bone_axis: str = "X_UP", ) -> str: """ Export to FBX optimized for game engines (Unity/Unreal/Godot). """ return json.dumps({ "type": "export_fbx_game", "params": { "filepath": filepath, "apply_unit_scale": apply_unit_scale, "bake_space_transform": bake_space_transform, "object_types": object_types or ["ARMATURE", "MESH"], "primary_bone_axis": primary_bone_axis, "secondary_bone_axis": secondary_bone_axis, } }) @mcp.tool() async def export_usd_pipeline( filepath: str, export_animation: bool = True, export_materials: bool = True, export_uv_maps: bool = True, purpose: str = "default", # "default", "render", "proxy", "guide" ) -> str: """ Export to USD for pipeline (Houdini, Maya, Katana, USDZ). """ return json.dumps({ "type": "export_usd_pipeline", "params": { "filepath": filepath, "export_animation": export_animation, "export_materials": export_materials, "export_uv_maps": export_uv_maps, "purpose": purpose, } }) ``` ### 4. Custom Tool: Geometry Generation ```python # src/my_blender_mcp/tools/geometry_tools.py import json from mcp.server.fastmcp import FastMCP from typing import List, Optional def register_geometry_tools(mcp: FastMCP): @mcp.tool() async def create_procedural_building( name: str, width: float = 10.0, depth: float = 10.0, height: float = 30.0, floors: int = 10, window_density: float = 0.7, style: str = "modern", # "modern", "classical", "industrial" location: List[float] = None, ) -> str: """ Generate a procedural building with floors, windows, roof. """ return json.dumps({ "type": "create_procedural_building", "params": { "name": name, "width": width, "depth": depth, "height": height, "floors": floors, "window_density": window_density, "style": style, "location": location or [0, 0, 0], } }) @mcp.tool() async def create_road_network( name: str, curves: List[dict], # [{points: [[x,y,z],...], width: 8, type: "main"}] sidewalk_width: float = 2.0, curb_height: float = 0.15, markings: bool = True, streetlights: bool = True, light_spacing: float = 20.0, ) -> str: """ Generate road network with sidewalks, curbs, markings, streetlights. """ return json.dumps({ "type": "create_road_network", "params": { "name": name, "curves": curves, "sidewalk_width": sidewalk_width, "curb_height": curb_height, "markings": markings, "streetlights": streetlights, "light_spacing": light_spacing, } }) @mcp.tool() async def scatter_on_surface( target_object: str, source_collection: str, density: float = 0.1, scale_min: float = 0.8, scale_max: float = 1.2, rotation_random: bool = True, align_to_normal: bool = True, seed: int = 42, ) -> str: """ Scatter objects from collection on target surface (Geometry Nodes style). """ return json.dumps({ "type": "scatter_on_surface", "params": { "target_object": target_object, "source_collection": source_collection, "density": density, "scale_min": scale_min, "scale_max": scale_max, "rotation_random": rotation_random, "align_to_normal": align_to_normal, "seed": seed, } }) ``` ### 5. Регистрация в Server ```python # src/my_blender_mcp/server.py from mcp.server.fastmcp import FastMCP from .tools.export_tools import register_export_tools from .tools.geometry_tools import register_geometry_tools mcp = FastMCP("MyBlenderMCP") # Регистрировать все кастомные инструменты register_export_tools(mcp) register_geometry_tools(mcp) def register_tools(): """Entry point для pyproject.toml""" return mcp ``` --- ## 🔧 GENESISCORE: CUSTOM TOOLS (Built-in) ### 1. Структура в аддоне ``` GenesisCore/ ├── src/ │ └── tools/ │ ├── __init__.py │ ├── my_custom_tools.py │ └── ... ``` ### 2. Custom Tool для GenesisCore ```python # src/tools/my_custom_tools.py from .common import ToolsPackageBase import bpy class MyCustomTools(ToolsPackageBase): """ Custom tools for specific workflows. """ def create_shot_sequence( shot_name: str, camera_name: str, start_frame: int, end_frame: int, resolution: list = None, ) -> dict: """ Create a complete shot setup: camera, frames, render settings. """ resolution = resolution or [1920, 1080] cam = bpy.data.objects.get(camera_name) if not cam: raise ValueError(f"Camera {camera_name} not found") scene = bpy.context.scene scene.frame_start = start_frame scene.frame_end = end_frame scene.render.resolution_x = resolution[0] scene.render.resolution_y = resolution[1] scene.camera = cam # Create marker marker = scene.timeline_markers.new(shot_name, frame=start_frame) marker.camera = cam return { "shot": shot_name, "camera": camera_name, "frames": f"{start_frame}-{end_frame}", "resolution": resolution, } def batch_rename_objects( pattern: str, replacement: str, selection_only: bool = True, ) -> dict: """ Batch rename objects using regex pattern. """ import re objects = bpy.context.selected_objects if selection_only else bpy.data.objects renamed = 0 for obj in objects: new_name = re.sub(pattern, replacement, obj.name) if new_name != obj.name: obj.name = new_name renamed += 1 return {"renamed": renamed, "total": len(objects)} def create_lod_hierarchy( base_object: str, lod_levels: int = 3, decimate_ratios: list = None, ) -> dict: """ Create LOD hierarchy for game export. """ decimate_ratios = decimate_ratios or [0.5, 0.25, 0.1] base = bpy.data.objects.get(base_object) if not base: raise ValueError(f"Object {base_object} not found") lod_objects = [base] for i, ratio in enumerate(decimate_ratios): lod = base.copy() lod.data = base.data.copy() lod.name = f"{base_object}_LOD{i+1}" bpy.context.collection.objects.link(lod) mod = lod.modifiers.new(f"Decimate_LOD{i+1}", 'DECIMATE') mod.ratio = ratio bpy.context.view_layer.objects.active = lod bpy.ops.object.modifier_apply(modifier=mod.name) lod_objects.append(lod) return {"base": base_object, "lods": [o.name for o in lod_objects[1:]]} def register(): from .common import ToolsPackageBase ToolsPackageBase.register_tool_package(MyCustomTools) def unregister(): from .common import ToolsPackageBase ToolsPackageBase.unregister_tool_package(MyCustomTools) ``` ### 3. Регистрация в GenesisCore ```python # src/tools/__init__.py from .common import ToolsPackageBase from .common_tools import CommonTools from .object_tools import ObjectTools from .material_tools import MaterialTools from .polyhaven_tools import PolyhavenTools from .asset_tools import AssetTools # Добавьте свои: from .my_custom_tools import MyCustomTools def register(): ToolsPackageBase.register() def unregister(): ToolsPackageBase.unregister() ``` --- ## 📦 УПАКОВКА И УСТАНОВКА ### Blender-MCP Package ```bash # Сборка pip install build python -m build # Установка в development mode pip install -e . # Тест uvx my-blender-mcp --help ``` ### GenesisCore (просто перезапуск Blender) ```bash # После добавления файлов в src/tools/ # Перезапустить Blender # GenesisCore автоматически загрузит новые инструменты ``` --- ## 🔗 РЕГИСТРАЦИЯ В MCP CLIENTS ### OpenCode (my-blender-mcp) ```json { "mcp": { "my-blender-mcp": { "type": "local", "command": ["uvx", "my-blender-mcp"], "enabled": true } } } ``` ### GenesisCore (встроено) ``` # Никакой дополнительной настройки - инструменты доступны сразу в чате ``` --- ## 🧪 ТЕСТИРОВАНИЕ ### Blender-MCP Test ```bash # В терминале с запущенным blender-mcp echo '{"type":"create_procedural_building","params":{"name":"TestTower","width":10,"height":50,"floors":10}}' | nc localhost 9876 ``` ### GenesisCore Test ``` # В чате GenesisCore: "Создай процедурную башню 'TestTower' шириной 10м, высотой 50м, 10 этажей" ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 02: GENESISCORE LOCAL (OLLAMA)](setup_02_genesiscore_local.md), [SETUP 09: BLENDERBOT / AI CODING AGENT](setup_09_blenderbot_ai.md), [SETUP 35: READY PLAYER ME AVATARS](setup_35_readyplayerme.md), [SETUP 45: HUNYUAN3D 2.1 LOCAL](setup_45_hunyuan3d_21.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [MCP ADVANCED: STREAMING & TOOL COMPOSITION](../mcp_advanced/streaming_tools.md), [P27: POLY HAVEN ASSET PIPELINE](../prompts/27_polyhaven_pipeline.md) - **Теги:** #mcp, #setup --- # SETUP 07: CI/CD INTEGRATION Tags: #blender #ci #setup # SETUP 07: CI/CD INTEGRATION **ID:** SETUP-07 | **Type:** Automated Pipeline | **Difficulty:** ⭐⭐ | **Time:** 20-40 min --- > Когда применять: когда сцену или набор ассетов нужно регулярно рендерить и экспортировать, а ручной прогон перестал быть воспроизводимым: у кого-то другой Blender, у кого-то другие текстуры, и «у меня работает» больше не аргумент. ## 🎯 ЦЕЛЬ Собрать пайплайн, где Blender запускается в фоновом режиме из командной строки, выполняет заранее записанные в репозитории скрипты рендера и экспорта, отдаёт артефакты и при этом **ломает сборку**, если сцена невалидна. Настраиваем три вещи. Первая — разделение данных и логики: скрипты живут в git, а `.blend` считается входным артефактом, версия которого тоже в репозитории. Вторая — окружение: зафиксированная версия Blender и headless-драйверы, чтобы GPU-рендер и CPU-рендер не были разными сценами. Третья — уровни проверок: дешёвая валидация на каждом pull request и полный рендер по расписанию или тегу. Принцип, из которого следует всё остальное: **CI не должен уметь рендерить красиво — он должен уметь повторяться**. Любой параметр, влияющий на результат, задаётся скриптом. Значение, набранное в окне настроек и сохранённое в `.blend`, воспроизвести невозможно: другой человек откроет файл, увидит то же самое и не догадается, что это было решением, а не дефолтом. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия / состояние | Зачем | |---|---|---| | Blender | зафиксированная версия, распакованная из tar-архива дистрибутива | рендер должен идти тем же билдом, что и локально | | Python из поставки Blender | bundled | системный Python не видит модуль `bpy` | | `xvfb` | пакет Ubuntu | виртуальный X-сервер для EEVEE; Cycles в `-b` обычно обходится без него | | `ffmpeg` | системный или статический | сборка превью из отрендеренных кадров | | Раннер с GPU | опционально, отдельный пул | Cycles на CPU считает, но на порядок медленнее | | Хранилище артефактов | встроенное у CI-провайдера | выгрузка превью, кадров и экспортированных ассетов | ## 📋 CI/CD СЦЕНАРИИ | Сценарий | Триггер | Действия | |----------|---------|----------| | **PR Validation** | Pull Request | линт скриптов, проверка сцены, быстрый рендер одного кадра | | **Nightly Build** | расписание (cron) | полный рендер, регрессионные тесты, метрики | | **Release** | push тега | финальный рендер, экспорт ассетов, публикация | | **Asset Update** | изменение текстур или скриптов | пересборка только зависимых сцен | ## 🛠️ НАСТРОЙКА ### Шаг 1. Зафиксировать версию Blender Версия и URL архива задаются переменными окружения, а не зашиты в команды. Сам список релизов лежит на `https://download.blender.org/release/` — там же лежат файлы `.tar.xz` для Linux и `.zip` для Windows. ```yaml env: BLENDER_VERSION: "4.2" # Полный список релизов: https://download.blender.org/release/ BLENDER_URL: "https://download.blender.org/release/Blender4.2/blender-4.2.3-linux-x64.tar.xz" ``` > [!NOTE] > Проверь конкретный патч-номер по списку релизов перед фиксацией в YAML. Сборка, > которая работала месяц, падает через месяц только потому, что файл перестал > существовать или его переименовали. Если команда не готова обновляться > намеренно, хранить стоит не «последнюю», а конкретную проверенную версию — > и отдельным шагом описывать процедуру апгрейда. ### Шаг 2. Композитное действие для установки Отдельное composite-действие переиспользуется всеми джобами: иначе блок установки копируется в три места и через месяц разъезжается. ```yaml # .github/actions/setup-blender/action.yml name: "Setup Blender" description: "Install and configure Blender for CI" inputs: version: description: "Blender version" required: false default: "4.2" runs: using: "composite" steps: - name: Download Blender run: | VERSION="${{ inputs.version }}" URL="https://download.blender.org/release/Blender${VERSION%.*}/blender-${VERSION}-linux-x64.tar.xz" wget -q "$URL" -O blender.tar.xz tar -xf blender.tar.xz echo "$(pwd)/blender-${VERSION}-linux-x64/blender" >> $GITHUB_PATH shell: bash - name: Install xvfb run: sudo apt-get update && sudo apt-get install -y xvfb shell: bash - name: Verify Blender run: | blender --version shell: bash ``` ### Шаг 3. Скрипт валидации сцены Это самый дешёвый и самый полезный скрипт в пайплайне: он ловит битые пути к текстурам, пустые материалы, объекты вне кадра и несохранённые изменения — раньше, чем потрачены часы рендера. ```python # scripts/validate_scene.py — запускается как: blender -b scene.blend -P scripts/validate_scene.py import bpy, os, sys errors = [] report = [] # 1. Разрешить //-пути относительно самого blend-файла, а не рабочего каталога bpy.ops.file.make_paths_absolute() # 2. Пропавшие текстуры for img in bpy.data.images: if img.source == 'FILE' and not os.path.exists(bpy.path.abspath(img.filepath)): errors.append(f"missing image: {img.name} -> {img.filepath}") # 3. Объекты с материалом, но без геометрии / с пустым мешем for ob in bpy.data.objects: if ob.type == 'MESH' and len(ob.data.polygons) == 0: errors.append(f"empty mesh: {ob.name}") # 4. Объекты без материала — почти всегда недосделанная работа for ob in bpy.data.objects: if ob.type == 'MESH' and len(ob.data.materials) == 0: report.append(f"no material (warn): {ob.name}") # 5. Камера и рендер-движок if not bpy.context.scene.camera: errors.append("scene has no camera") for line in report: print("WARN:", line) for line in errors: print("ERROR:", line) print(f"validate_scene: {len(errors)} errors, {len(report)} warnings") if errors: # В фоновом режиме SystemExit прерывает Blender, и процесс возвращает # ненулевой код выхода — именно его читает CI. sys.exit(1) ``` > [!NOTE] > Проверь фактический код возврата в своей версии Blender одной командой: > запусти скрипт с `sys.exit(1)` вручную и посмотри на `$?` (в PowerShell — на > `$LASTEXITCODE`). Если ненулевой код не доходит до CI, замени завершение на > `bpy.ops.wm.quit_blender()` в конце скрипта и проверь ещё раз. ### Шаг 4. Скрипт превью ```python # scripts/render_preview.py import bpy import sys argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] scene = bpy.context.scene scene.render.engine = 'CYCLES' scene.cycles.samples = 32 # превью: дёшево и шумно, это нормально scene.cycles.use_denoising = True scene.render.resolution_x = 1280 scene.render.resolution_y = 720 scene.render.resolution_percentage = 50 # превью не обязано быть полным размером scene.render.filepath = "//preview/preview_" scene.render.image_settings.file_format = 'PNG' scene.render.image_settings.color_mode = 'RGBA' bpy.ops.render.render(write_still=True) print("preview rendered:", scene.render.filepath) ``` ### Шаг 5. Скрипт полного рендера ```python # scripts/render_full.py import bpy, sys, argparse argv = sys.argv[sys.argv.index("--") + 1:] parser = argparse.ArgumentParser() parser.add_argument("--resolution", choices=["1080p", "4k"], default="1080p") parser.add_argument("--samples", type=int, default=256) parser.add_argument("--output", default="//renders/") parser.add_argument("--device", choices=["CPU", "GPU"], default="CPU") args = parser.parse_args(argv) scene = bpy.context.scene scene.render.engine = 'CYCLES' scene.cycles.device = args.device scene.cycles.samples = args.samples scene.cycles.use_adaptive_sampling = True scene.cycles.adaptive_threshold = 0.01 scene.cycles.use_denoising = True res_map = {"1080p": (1920, 1080), "4k": (3840, 2160)} scene.render.resolution_x, scene.render.resolution_y = res_map[args.resolution] scene.render.resolution_percentage = 100 scene.render.filepath = args.output scene.render.image_settings.file_format = 'OPEN_EXR_MULTILAYER' scene.render.image_settings.color_mode = 'RGBA' scene.render.image_settings.color_depth = '32' bpy.ops.render.render(animation=True) print("full render complete:", args.resolution, "@", args.samples, "samples") ``` > [!NOTE] > Про денойзер: в Cycles доступно несколько реализаций (`OPTIX`, `OPENIMAGEDENOISE`, > а в части сборок ещё один). `OPTIX` требует NVIDIA-карту и на CPU-раннере упадёт, > поэтому в общем пайплайне оставляй денойзер по умолчанию и включай конкретный > только в джобе с GPU. Полный список значений этого поля смотри в > `Render Properties → Sampling → Denoiser` своей версии. ### Шаг 6. Скрипт экспорта ```python # scripts/export_assets.py import bpy, os, sys, argparse argv = sys.argv[sys.argv.index("--") + 1:] parser = argparse.ArgumentParser() parser.add_argument("--output-dir", default="exports/") parser.add_argument("--formats", nargs="+", default=["GLB"]) args = parser.parse_args(argv) os.makedirs(args.output_dir, exist_ok=True) for obj in bpy.data.objects: if obj.type != 'MESH' or not obj.get("export", True): continue for fmt in args.formats: fmt_dir = os.path.join(args.output_dir, fmt.lower()) os.makedirs(fmt_dir, exist_ok=True) filepath = os.path.join(fmt_dir, obj.name.replace(" ", "_") + "." + fmt.lower()) bpy.ops.object.select_all(action='DESELECT') obj.select_set(True) bpy.context.view_layer.objects.active = obj if fmt == "GLB": bpy.ops.export_scene.gltf(filepath=filepath, export_format='GLB', use_selection=True, export_apply=True) elif fmt == "FBX": bpy.ops.export_scene.fbx(filepath=filepath, use_selection=True, apply_unit_scale=True, object_types={'ARMATURE', 'MESH'}) print("export done ->", args.output_dir) ``` > [!NOTE] > Набор аргументов у операторов экспорта разный и менялся между версиями > (`bpy.ops.export_scene.gltf`, `bpy.ops.export_scene.fbx`, экспорт в USD > вынесен в отдельную группу операторов). Не переноси флаги из одной версии в > другую списком — вызови оператор один раз, открой его параметры через > `bpy.ops.export_scene.gltf.get_rna_type().properties`, и сверяйся с тем, что > реально есть. Нераспознанный именованный аргумент в Python — это `TypeError`, > и пайплайн упадёт целиком, а не «предупредит и продолжит». ### Шаг 7. Workflow ```yaml # .github/workflows/blender.yml name: Blender CI/CD on: push: branches: [main] paths: - 'scenes/**/*.blend' - 'scripts/**/*.py' - '.github/workflows/blender.yml' pull_request: branches: [main] paths: - 'scenes/**/*.blend' - 'scripts/**/*.py' schedule: - cron: '0 2 * * *' workflow_dispatch: jobs: validate: name: Validate scene runs-on: ubuntu-latest timeout-minutes: 15 steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup-blender - name: Validate run: | xvfb-run -a blender -b scenes/main.blend -P scripts/validate_scene.py - name: Lint render scripts run: | python -m pip install --upgrade pip flake8 flake8 scripts/ --max-line-length=120 --ignore=E501,W503 render-preview: name: Render preview (PR) needs: validate runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup-blender - name: Preview run: | xvfb-run -a blender -b scenes/main.blend -P scripts/render_preview.py - uses: actions/upload-artifact@v4 with: name: render-preview path: preview/ retention-days: 7 render-full: name: Full render needs: validate if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest timeout-minutes: 240 steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup-blender - name: Full render run: | xvfb-run -a blender -b scenes/main.blend -P scripts/render_full.py -- \ --resolution 1080p --samples 256 --device CPU --output //renders/ - uses: actions/upload-artifact@v4 with: name: renders path: renders/ retention-days: 30 ``` ### Шаг 8. Порядок аргументов командной строки Это место, где чаще всего теряется час: `-f 1`, `-a` и `-P` выполняются именно в том порядке, в котором написаны. ```bash # НЕПРАВИЛЬНО: кадр 1 отрендерится раньше, чем скрипт поменяет настройки, # и на выходе будет кадр из дефолтов сцены blender -b scene.blend -f 1 -P scripts/render_preview.py # ПРАВИЛЬНО: сначала выполняется скрипт, затем рендерится кадр blender -b scene.blend -P scripts/render_preview.py -f 1 # Аргументы после "--" уходят в sys.argv скрипта, а не в сам Blender blender -b scene.blend -P scripts/render_full.py -- --samples 512 ``` ### Шаг 9. GitLab CI Тот же пайплайн в терминах GitLab — стадии вместо джоб, `artifacts` вместо `upload-artifact`. ```yaml stages: [test, render, export, deploy] variables: BLENDER_VERSION: "4.2" before_script: - apt-get update -qq && apt-get install -y -qq blender xvfb ffmpeg validate: stage: test script: - xvfb-run -a blender -b scenes/main.blend -P scripts/validate_scene.py render_preview: stage: render script: - xvfb-run -a blender -b scenes/main.blend -P scripts/render_preview.py artifacts: paths: [preview/] expire_in: 1 week deploy: stage: deploy script: - aws s3 sync exports/ "$ASSET_BUCKET/latest/" --delete rules: - if: $CI_COMMIT_BRANCH == "main" when: manual ``` > [!NOTE] > Дистрибутивный пакет `blender` в репозиториях Ubuntu отстаёт от официального > релиза на несколько версий. Если важна именно новая версия, ставь её из > архива дистрибутива (Шаг 2), а не из `apt`. ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `cycles.samples` (превью) | 24–64 | превью нужно для оценки композиции, а не качества | | `cycles.samples` (финал) | 128–1024 | ниже 128 сцена с SSS или стеклом будет шумной | | `resolution_percentage` | 50 для превью | вдвое меньше линейного разрешения вчетверо дешевле по времени | | `render.image_settings.file_format` | `OPEN_EXR_MULTILAYER` для финала, `PNG` для превью | EXR нужен, чтобы пересобирать грейд без повторного рендера | | `cycles.device` | `CPU` по умолчанию | `GPU` ставится отдельной джобе, где гарантированно есть карта | | `timeout-minutes` | 15 для валидации, 240 для полного рендера | слишком маленький таймаут убивает длинный рендер на середине | | `retention-days` | 7 для превью, 30 для релизов | артефакты съедают место квотированного хранилища | | `BLENDER_VERSION` | одна зафиксированная версия на все джобы | рассинхрон версий даёт артефакты, которые не воспроизводятся локально | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | CI зелёный, картинка неправильная | скрипт выполнялся после `-f 1` и не успел поменять настройки | `-P script.py` всегда раньше `-f`/`-a` | | `TypeError` на неизвестном аргументе экспорта | флаги скопированы из другой версии Blender | сверяться с `get_rna_type().properties` оператора в своей версии | | Текстуры «пропали» в CI, локально есть | `//`-пути не разрешились: рабочий каталог раннера другой | `bpy.ops.file.make_paths_absolute()` перед экспортом и рендером | | Денойзер падает на CPU-раннере | выбран GPU-специфичный денойзер | оставить денойзер по умолчанию, GPU-настройки — в отдельной джобе | | Пайплайн зелёный, артефакта нет | скрипт вернул 0, но ничего не отрендерил | проверять ненулевой код возврата и наличие файлов в `preview/` | | Рендер в 20 раз медленнее локального | раннер без GPU при `device = 'GPU'` | явный `CPU` на дешёвых раннерах, GPU — на отдельном пуле | | Полный рендер съедает квоту и падает | нет `paths`-фильтра, тянутся все файлы сцены | сузить `paths` в триггере до `scenes/**` и `scripts/**` | | Уведомление всегда «зелёное» | в job уведомлений `${{ job.status }}` — статус самого этого джоба | прокидывать статусы через `needs.*.result` или отдельную сводку | | Windows-локально работает, Linux-раннер падает | в пути есть русские буквы или пробелы | держать в репозитории только ASCII-пути | ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 04: HEADLESS RENDER (CLI / CI/CD)](setup_04_headless_render.md), [SETUP 09: BLENDERBOT / AI CODING AGENT](setup_09_blenderbot_ai.md), [SETUP 10: BLENDER + COMPOSITING (NUKE/AFTER EFFECTS ROUNDTRIP)](setup_10_compositing_roundtrip.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 11: BLENDER + TOUCHDESIGNER REALTIME VISUALS](setup_11_touchdesigner.md), [SETUP 12: BLENDER + UNREAL ENGINE 5 LIVE LINK](setup_12_unreal_livelink.md) - **Теги:** #blender, #ci, #setup --- # SETUP 08: ASSET PIPELINE Tags: #list #none #pipeline #setup # SETUP 08: ASSET PIPELINE **ID:** SETUP-08 | **Type:** Asset Management & Pipeline | **Difficulty:** ⭐⭐⭐ | **Time:** 40-60 min --- ## 🎯 ЦЕЛЬ Полный пайплайн управления ассетами: организация, версионирование, валидация, экспорт, деплой --- ## 📋 АРХИТЕКТУРА ПАЙПЛАЙНА ``` ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ CREATE │───▶│ VALIDATE │───▶│ VERSION │───▶│ EXPORT │ │ (Model/ │ │ (Auto QA) │ │ (Git LFS) │ │ (Multi-fmt)│ │ Texture) │ │ │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ ▼ ▼ ▼ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ ASSET REGISTRY (JSON/DB) │ │ Metadata | Dependencies | Tags | LODs | Preview │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ DEPLOY TARGETS │ │ Game Engine | Web/Three.js | USD Pipeline | Render Farm │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## 📁 СТРУКТУРА АССЕТОВ ``` assets/ ├── models/ │ ├── characters/ │ │ ├── hero/ │ │ │ ├── v001/ │ │ │ │ ├── hero_v001.blend │ │ │ │ ├── hero_v001.fbx │ │ │ │ ├── hero_v001.gltf │ │ │ │ ├── hero_v001.usdz │ │ │ │ ├── textures/ │ │ │ │ │ ├── hero_basecolor_2k.png │ │ │ │ │ ├── hero_roughness_2k.png │ │ │ │ │ ├── hero_normal_2k.exr │ │ │ │ │ └── hero_metallic_2k.png │ │ │ │ ├── lods/ │ │ │ │ │ ├── hero_LOD0.blend │ │ │ │ │ ├── hero_LOD1.blend │ │ │ │ │ └── hero_LOD2.blend │ │ │ │ ├── metadata.json │ │ │ │ └── preview.png │ │ │ └── latest -> v001/ │ │ └── ... │ ├── props/ │ ├── environment/ │ └── vehicles/ ├── materials/ │ ├── library/ │ │ ├── metal/ │ │ ├── wood/ │ │ ├── fabric/ │ │ └── ... │ └── substance/ ├── textures/ │ ├── pbr/ │ ├── hdri/ │ └── masks/ ├── hdri/ ├── scripts/ │ ├── validators/ │ ├── exporters/ │ └── pipeline/ ├── registry/ │ ├── assets.json # Главный реестр │ ├── index.db # SQLite для поиска │ └── tags.yaml # Теговые категории └── pipeline/ ├── config.yaml ├── validators.yaml └── export_presets.yaml ``` --- ## 📄 МЕТАДАННЫЕ АССЕТА (metadata.json) ```json { "asset": { "id": "hero_main", "name": "Hero Character", "type": "character", "category": "characters", "version": "v001", "status": "approved", // "wip", "review", "approved", "deprecated" "created": "2026-01-15T10:30:00Z", "updated": "2026-01-15T14:22:00Z", "author": "artist_name", "tags": ["hero", "human", "rigged", "game-ready", "lod"], "description": "Main protagonist character with full rig" }, "technical": { "blender_version": "4.2.3", "triangles": 24500, "vertices": 12800, "uv_tiles": 2, "materials_count": 4, "bones_count": 150, "shape_keys": 28, "uv_maps": ["UVMap", "Lightmap"], "vertex_groups": 45 }, "lod": { "levels": 3, "LOD0": {"tris": 24500, "distance": 0, "file": "hero_LOD0.blend"}, "LOD1": {"tris": 8200, "distance": 15, "file": "hero_LOD1.blend"}, "LOD2": {"tris": 2100, "distance": 50, "file": "hero_LOD2.blend"} }, "materials": [ {"name": "Mat_Skin", "type": "skin", "textures": ["basecolor", "roughness", "normal", "subsurface"]}, {"name": "Mat_Hair", "type": "hair", "textures": ["basecolor", "roughness", "normal", "alpha"]}, {"name": "Mat_Clothing", "type": "fabric", "textures": ["basecolor", "roughness", "normal", "height"]}, {"name": "Mat_Eyes", "type": "eye", "textures": ["basecolor", "normal", "emission"]} ], "rig": { "type": "rigify", "bones": 150, "face_bones": 52, "ik_fk": true, "twist_bones": true, "corrective_shapes": 24 }, "exports": { "gltf": {"file": "hero_v001.gltf", "size_mb": 12.4, "version": "2.0"}, "fbx": {"file": "hero_v001.fbx", "size_mb": 15.2, "version": "7.4"}, "usdz": {"file": "hero_v001.usdz", "size_mb": 18.7, "version": "1.0"}, "usd": {"file": "hero_v001.usd", "size_mb": 22.1, "purpose": "default"} }, "preview": { "thumbnail": "preview.png", "turntable": "turntable.mp4", "wireframe": "wireframe.png" }, "dependencies": [ "assets/materials/library/skin_base.mat", "assets/materials/library/hair_shader.mat" ], "pipeline": { "validated": true, "validator_version": "1.2.0", "last_validation": "2026-01-15T14:22:00Z", "warnings": [], "errors": [] } } ``` --- ## ✅ АВТОВАЛИДАЦИЯ (QA) ### validators/geometry_validator.py ```python import bpy import bmesh from typing import List, Dict, Any class GeometryValidator: """Валидация геометрии ассета""" def __init__(self, obj: bpy.types.Object): self.obj = obj self.errors = [] self.warnings = [] self.info = [] def validate_all(self) -> Dict[str, Any]: self.check_non_manifold() self.check_loose_geometry() self.check_uv_overlaps() self.check_vertex_groups() self.check_face_orientation() self.check_sharp_edges() self.check_triangulation() self.check_scale() return { "valid": len(self.errors) == 0, "errors": self.errors, "warnings": self.warnings, "info": self.info, "stats": self.get_stats() } def check_non_manifold(self): bm = bmesh.new() bm.from_mesh(self.obj.data) non_manifold = [e for e in bm.edges if not e.is_manifold] if non_manifold: self.errors.append(f"Non-manifold edges: {len(non_manifold)}") bm.free() def check_uv_overlaps(self): if not self.obj.data.uv_layers: self.warnings.append("No UV maps found") return bm = bmesh.new() bm.from_mesh(self.obj.data) uv_layer = bm.loops.layers.uv.active # Упрощенная проверка uv_coords = [] for face in bm.faces: for loop in face.loops: uv = loop[uv_layer].uv uv_coords.append((round(uv.x, 4), round(uv.y, 4))) if len(uv_coords) != len(set(uv_coords)): self.warnings.append("Possible UV overlaps detected") bm.free() def check_scale(self): scale = self.obj.scale if any(abs(s - 1.0) > 0.001 for s in scale): self.warnings.append(f"Object scale not applied: {scale}") def get_stats(self) -> Dict: mesh = self.obj.data return { "vertices": len(mesh.vertices), "edges": len(mesh.edges), "faces": len(mesh.polygons), "triangles": sum(len(p.vertices) - 2 for p in mesh.polygons), "materials": len(mesh.materials), "uv_maps": len(mesh.uv_layers), "vertex_groups": len(self.obj.vertex_groups), "shape_keys": len(self.obj.data.shape_keys.key_blocks) if self.obj.data.shape_keys else 0, } # CLI usage def validate_asset(blend_path: str) -> Dict: import bpy bpy.ops.wm.open_mainfile(filepath=blend_path) results = {} for obj in bpy.data.objects: if obj.type == 'MESH': validator = GeometryValidator(obj) results[obj.name] = validator.validate_all() return results ``` ### validators/material_validator.py ```python class MaterialValidator: """Валидация PBR материалов""" REQUIRED_PBR_MAPS = ["base_color", "roughness", "normal", "metallic"] OPTIONAL_MAPS = ["height", "ao", "emission", "subsurface"] def validate_material(self, mat: bpy.types.Material) -> Dict: errors = [] warnings = [] if not mat.use_nodes: errors.append("Material doesn't use nodes") return {"valid": False, "errors": errors, "warnings": warnings} nodes = mat.node_tree.nodes principled = None for node in nodes: if node.type == 'BSDF_PRINCIPLED': principled = node break if not principled: errors.append("No Principled BSDF found") return {"valid": False, "errors": errors, "warnings": warnings} # Проверка текстур for map_name in self.REQUIRED_PBR_MAPS: input_socket = principled.inputs.get(map_name.title().replace('_', ' ')) if input_socket and not input_socket.links: warnings.append(f"Missing {map_name} texture") # Проверка цветовых пространств for node in nodes: if node.type == 'TEX_IMAGE' and node.image: if node.image.colorspace_settings.name == 'sRGB' and 'roughness' in node.label.lower(): warnings.append(f"Roughness map '{node.image.name}' uses sRGB instead of Non-Color") if node.image.colorspace_settings.name == 'sRGB' and 'normal' in node.label.lower(): warnings.append(f"Normal map '{node.image.name}' uses sRGB instead of Non-Color") return {"valid": len(errors) == 0, "errors": errors, "warnings": warnings} ``` --- ## 📦 ЭКСПОРТЕРЫ (МУЛЬТИ-ФОРМАТ) ### exporters/multi_exporter.py ```python import bpy import os import json from pathlib import Path from typing import List, Dict class MultiExporter: """Экспорт в все необходимые форматы""" def __init__(self, output_dir: str, asset_id: str, version: str): self.output_dir = Path(output_dir) self.asset_id = asset_id self.version = version self.results = {} def export_all(self, objects: List[bpy.types.Object], formats: List[str] = None) -> Dict: formats = formats or ["GLTF", "FBX", "USDZ", "USD"] for fmt in formats: if fmt == "GLTF": self.results["gltf"] = self.export_gltf(objects) elif fmt == "FBX": self.results["fbx"] = self.export_fbx(objects) elif fmt == "USDZ": self.results["usdz"] = self.export_usdz(objects) elif fmt == "USD": self.results["usd"] = self.export_usd(objects) return self.results def export_gltf(self, objects) -> Dict: self.output_dir.joinpath("gltf").mkdir(parents=True, exist_ok=True) results = {} for obj in objects: filepath = self.output_dir / "gltf" / f"{obj.name}.glb" bpy.ops.object.select_all(action='DESELECT') obj.select_set(True) bpy.context.view_layer.objects.active = obj bpy.ops.export_scene.gltf( filepath=str(filepath), export_format='GLB', use_selection=True, export_apply=True, export_tangents=True, export_materials='EXPORT', export_colors=True, export_animations=True, export_morph=True, export_skins=True, export_extras=True, ) size = filepath.stat().st_size results[obj.name] = {"file": str(filepath), "size_mb": round(size/1024/1024, 2)} return results def export_fbx(self, objects) -> Dict: self.output_dir.joinpath("fbx").mkdir(parents=True, exist_ok=True) results = {} for obj in objects: filepath = self.output_dir / "fbx" / f"{obj.name}.fbx" bpy.ops.object.select_all(action='DESELECT') obj.select_set(True) bpy.context.view_layer.objects.active = obj bpy.ops.export_scene.fbx( filepath=str(filepath), use_selection=True, apply_unit_scale=True, apply_scale_options='FBX_SCALE_ALL', bake_space_transform=True, object_types={'ARMATURE', 'MESH'}, use_armature_deform_only=True, add_leaf_bones=False, primary_bone_axis='Y_FORWARD', secondary_bone_axis='X_UP', ) size = filepath.stat().st_size results[obj.name] = {"file": str(filepath), "size_mb": round(size/1024/1024, 2)} return results def export_usdz(self, objects) -> Dict: self.output_dir.joinpath("usdz").mkdir(parents=True, exist_ok=True) results = {} for obj in objects: filepath = self.output_dir / "usdz" / f"{obj.name}.usdz" bpy.ops.object.select_all(action='DESELECT') obj.select_set(True) bpy.context.view_layer.objects.active = obj bpy.ops.wm.usd_export( filepath=str(filepath), export_selected=True, export_animation=True, export_materials=True, ) size = filepath.stat().st_size results[obj.name] = {"file": str(filepath), "size_mb": round(size/1024/1024, 2)} return results def export_usd(self, objects) -> Dict: self.output_dir.joinpath("usd").mkdir(parents=True, exist_ok=True) results = {} filepath = self.output_dir / "usd" / f"{self.asset_id}.usd" bpy.ops.object.select_all(action='DESELECT') for obj in objects: obj.select_set(True) bpy.context.view_layer.objects.active = objects[0] bpy.ops.wm.usd_export( filepath=str(filepath), export_selected=True, export_animation=True, export_materials=True, export_uvmaps=True, ) size = filepath.stat().st_size return {"file": str(filepath), "size_mb": round(size/1024/1024, 2)} ``` --- ## 🏷️ АССЕТ РЕЕСТР (REGISTRY) ### registry/assets.json ```json { "version": "1.0", "updated": "2026-01-15T15:00:00Z", "assets": { "hero_main": { "path": "assets/models/characters/hero/latest/", "version": "v001", "status": "approved", "tags": ["character", "hero", "rigged", "lod"], "dependencies": ["skin_material", "hair_shader"], "exports": ["gltf", "fbx", "usdz", "usd"], "lod": true, "rigged": true }, "prop_crate_wooden": { "path": "assets/models/props/crate_wooden/latest/", "version": "v003", "status": "approved", "tags": ["prop", "crate", "wood", "game-ready"], "exports": ["gltf", "fbx"], "lod": true, "rigged": false } }, "tags": { "character": ["hero_main", "npc_villager"], "prop": ["prop_crate_wooden", "prop_barrel"], "environment": ["building_house_01", "tree_oak"], "vehicle": ["car_sedan", "truck"], "game-ready": ["hero_main", "prop_crate_wooden"], "rigged": ["hero_main"], "lod": ["hero_main", "prop_crate_wooden"] } } ``` ### registry/search.py ```python import json from pathlib import Path from typing import List, Dict, Optional class AssetRegistry: def __init__(self, registry_path: str): self.path = Path(registry_path) self.data = self.load() def load(self) -> Dict: if self.path.exists(): return json.loads(self.path.read_text()) return {"version": "1.0", "assets": {}, "tags": {}} def save(self): self.path.write_text(json.dumps(self.data, indent=2, ensure_ascii=False)) def register_asset(self, asset_id: str, metadata: Dict): self.data["assets"][asset_id] = metadata # Обновить теги for tag in metadata.get("tags", []): if tag not in self.data["tags"]: self.data["tags"][tag] = [] if asset_id not in self.data["tags"][tag]: self.data["tags"][tag].append(asset_id) self.save() def search(self, query: str = None, tags: List[str] = None, type_filter: str = None, status: str = None) -> List[Dict]: results = [] for asset_id, asset in self.data["assets"].items(): if query and query.lower() not in asset_id.lower(): continue if tags and not all(t in asset.get("tags", []) for t in tags): continue if type_filter and asset.get("type") != type_filter: continue if status and asset.get("status") != status: continue results.append({"id": asset_id, **asset}) return results def get_dependencies(self, asset_id: str) -> List[str]: return self.data["assets"].get(asset_id, {}).get("dependencies", []) def get_dependents(self, asset_id: str) -> List[str]: dependents = [] for aid, asset in self.data["assets"].items(): if asset_id in asset.get("dependencies", []): dependents.append(aid) return dependents ``` --- ## 🔄 CI/CD ИНТЕГРАЦИЯ ### pipeline/validate_and_export.py ```python #!/usr/bin/env python3 """ Полный пайплайн: Валидация -> Экспорт -> Реестр -> Деплой """ import sys import argparse from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent / "scripts")) from scripts.validators.geometry_validator import GeometryValidator from scripts.validators.material_validator import MaterialValidator from scripts.exporters.multi_exporter import MultiExporter from scripts.registry.search import AssetRegistry def main(): parser = argparse.ArgumentParser(description="Asset Pipeline") parser.add_argument("asset_path", help="Path to .blend file") parser.add_argument("--asset-id", required=True, help="Asset ID") parser.add_argument("--version", required=True, help="Version (e.g., v001)") parser.add_argument("--output", default="exports/", help="Output directory") parser.add_argument("--formats", nargs="+", default=["GLTF", "FBX", "USDZ"]) parser.add_argument("--registry", default="registry/assets.json") parser.add_argument("--skip-validation", action="store_true") parser.add_argument("--deploy", action="store_true") args = parser.parse_args() # 1. Load Blender file import bpy bpy.ops.wm.open_mainfile(filepath=args.asset_path) # 2. Validation if not args.skip_validation: print("🔍 Validating geometry...") for obj in bpy.data.objects: if obj.type == 'MESH': validator = GeometryValidator(obj) result = validator.validate_all() if not result["valid"]: print(f"❌ Validation failed for {obj.name}: {result['errors']}") sys.exit(1) if result["warnings"]: print(f"⚠️ Warnings for {obj.name}: {result['warnings']}") print("🔍 Validating materials...") for mat in bpy.data.materials: validator = MaterialValidator() result = validator.validate_material(mat) if not result["valid"]: print(f"❌ Material validation failed: {result['errors']}") sys.exit(1) if result["warnings"]: print(f"⚠️ Material warnings: {result['warnings']}") print("✅ Validation passed") # 3. Export print("📦 Exporting...") exporter = MultiExporter(args.output, args.asset_id, args.version) objects = [obj for obj in bpy.data.objects if obj.get("export", True)] results = exporter.export_all(objects, args.formats) print(f"✅ Exported: {json.dumps(results, indent=2)}") # 4. Update Registry registry = AssetRegistry(args.registry) # ... обновить реестр с новой версией # 5. Deploy (если указано) if args.deploy: print("🚀 Deploying...") # aws s3 sync / rsync / etc. if __name__ == "__main__": main() ``` --- ## 📦 ДЕПЛОЙ ТАРГЕТЫ ### deploy/targets.yaml ```yaml targets: unreal_engine: path: "../UE_Project/Content/Assets/" formats: ["FBX", "USD"] naming: "{asset_id}_v{version}" post_process: - "python scripts/ue_import_helper.py" unity: path: "../Unity_Project/Assets/Imported/" formats: ["FBX", "GLTF"] naming: "{asset_id}" import_settings: scale_factor: 1.0 generate_colliders: true godot: path: "../Godot_Project/assets/models/" formats: ["GLTF", "GLB"] naming: "{asset_id}.glb" threejs_web: path: "../Web_Project/public/assets/models/" formats: ["GLTF", "GLB"] compression: "draco" texture_compression: "basis" usd_pipeline: path: "../USD_Pipeline/assets/" formats: ["USD", "USDA"] purpose: "default" layering: true render_farm: path: "/mnt/flamenco-storage/assets/" formats: ["BLEND", "USD"] include_lods: true cdn: bucket: "s3://my-game-assets/" formats: ["GLTF", "GLB"] cdn_url: "https://cdn.mygame.com/assets/" cache_control: "max-age=31536000" ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 14: BLENDER + GODOT PIPELINE](setup_14_godot_pipeline.md), [SETUP 38: FAB.COM (EPIC) PIPELINE](setup_38_fab_epic.md), [SETUP 45: HUNYUAN3D 2.1 LOCAL](setup_45_hunyuan3d_21.md), [SETUP 49: FFMPEG + VSE PIPELINE](setup_49_ffmpeg_vse_pipeline.md), [SETUP 55: AI UPSCALE PIPELINE](setup_55_ai_upscale_pipeline.md) - **По темам:** [SETUP 59: WEB VIEWER EXPORT](setup_59_web_viewer_export.md), [CASE 02: GAME PROPS PIPELINE](../cases/case_02_game_props.md), [CASE 03: PRODUCT VISUALIZATION](../cases/case_03_product_viz.md) - **Теги:** #list, #none, #pipeline, #setup --- # SETUP 09: BLENDERBOT / AI CODING AGENT Tags: #blender #mcp #setup # SETUP 09: BLENDERBOT / AI CODING AGENT **ID:** SETUP-09 | **Type:** AI Coding Assistant for Blender | **Difficulty:** ⭐⭐ | **Time:** 20-30 min --- ## 🎯 ЦЕЛЬ Настроить BlenderBot / AI Coding Agent для генерации Blender Python кода через LLM --- ## 📋 АРХИТЕКТУРА ``` ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ LLM (GPT-4/ │────▶│ BlenderBot │────▶│ Blender │ │ Claude/Codex) │ │ (MCP Server) │ │ (bpy API) │ └─────────────────┘ └─────────────────┘ └─────────────────┘ ``` --- ## 🛠️ УСТАНОВКА ### Вариант 1: BlenderGPT (Open Source) ```bash # GitHub: https://github.com/BlenderGPT/BlenderGPT git clone https://github.com/BlenderGPT/BlenderGPT cd BlenderGPT pip install -r requirements.txt # Настройка API ключей cp .env.example .env # Редактируй .env с твоими API ключами ``` ### Вариант 2: Blender-Copilot (VS Code Extension) ```bash # Установка через VS Code Marketplace # Поиск: "Blender Copilot" или "Blender Python" # Или через pip pip install blender-copilot ``` ### Вариант 3: Custom MCP Agent (Рекомендуемый) ```python # custom_blender_agent.py import bpy import bmesh import mathutils import json from typing import Dict, Any, List from mcp.server.fastmcp import FastMCP mcp = FastMCP("BlenderCodingAgent") @mcp.tool() async def generate_blender_script( task: str, context: str = "", style: str = "clean" # "clean", "verbose", "minimal" ) -> str: """ Генерация Blender Python скрипта по описанию задачи. Args: task: Описание задачи (например: "Создай процедурный дом") context: Дополнительный контекст (сцена, объекты, настройки) style: Стиль кода """ system_prompt = f""" Ты — эксперт по Blender Python API (bpy). Стиль кода: {style}. Правила: 1. Всегда используй bpy.data, bpy.ops, bpy.context, bmesh, mathutils 2. Проверяй существование объектов перед обращением 3. Используй context managers для режимов (EDIT/OBJECT) 4. Добавляй комментарии на русском/английском 5. Обрабатывай ошибки try/except 6. Возвращай готовый к выполнению код Контекст: {context} """ # Здесь был бы вызов LLM API # Возвращаем готовый скрипт return generate_code_via_llm(task, system_prompt) @mcp.tool() async def fix_blender_script( script: str, error: str, context: str = "" ) -> str: """Исправление Blender скрипта по ошибке""" return fix_code_via_llm(script, error, context) @mcp.tool() async def explain_blender_api( topic: str, depth: str = "medium" # "basic", "medium", "deep" ) -> str: """Объяснение Blender API темы""" return explain_api_via_llm(topic, depth) if __name__ == "__main__": mcp.run() ``` --- ## 🔧 КОНФИГУРАЦИЯ MCP ### OpenCode Config ```json { "mcp": { "blender-coding-agent": { "type": "local", "command": ["python", "-m", "custom_blender_agent"], "enabled": true, "environment": { "BLENDER_MCP_SAFE_MODE": "0", "OPENAI_API_KEY": "${OPENAI_API_KEY}", "ANTHROPIC_API_KEY": "${ANTHROPIC_API_KEY}" } } } } ``` ### Claude Desktop Config ```json { "mcpServers": { "blender-coding-agent": { "command": "python", "args": ["-m", "custom_blender_agent"], "env": { "OPENAI_API_KEY": "your-key", "ANTHROPIC_API_KEY": "your-key" } } } } ``` --- ## 🎯 ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ ### Пример 1: Генерация процедурного здания ``` User: "Создай процедурный небоскреб 50 этажей с остеклением" Agent генерирует: - GN tree для небоскреба - Параметры: этажи, оконные ряды, стиль - Материалы: стекло, бетон, металл - LODs для производительности ``` ### Пример 2: Исправление ошибки ``` User: "Ошибка: 'bpy.data.objects' has no attribute 'my_obj'" Agent: Исправляет на bpy.data.objects.get("my_obj") с проверкой None ``` ### Пример 3: Объяснение API ``` User: "Объясни bpy.ops.mesh.boolean_modifier" Agent: Дает подробное объяснение с примерами использования ``` --- ## 🔧 РАСШИРЕНИЕ: CUSTOM TOOLS ### Дополнительные MCP Tools ```python @mcp.tool() async def analyze_blender_scene() -> dict: """Анализ текущей сцены для контекста""" return { "objects_count": len(bpy.data.objects), "materials_count": len(bpy.data.materials), "collections": [c.name for c in bpy.data.collections], "current_frame": bpy.context.scene.frame_current, "render_engine": bpy.context.scene.render.engine, } @mcp.tool() async def suggest_optimization(scene_data: dict) -> list: """Предложения по оптимизации сцены""" suggestions = [] # Анализ и предложения return suggestions @mcp.tool() async def generate_boilerplate(template: str) -> str: """Генерация boilerplate кода""" templates = { "operator": OPERATOR_TEMPLATE, "panel": PANEL_TEMPLATE, "property_group": PROP_GROUP_TEMPLATE, "addon": ADDON_TEMPLATE, } return templates.get(template, "") ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 35: READY PLAYER ME AVATARS](setup_35_readyplayerme.md), [SETUP 45: HUNYUAN3D 2.1 LOCAL](setup_45_hunyuan3d_21.md), [SETUP 02: GENESISCORE LOCAL (OLLAMA)](setup_02_genesiscore_local.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 06: CUSTOM MCP TOOLS](setup_06_custom_mcp_tools.md), [SETUP 07: CI/CD INTEGRATION](setup_07_ci_cd.md) - **Теги:** #blender, #mcp, #setup --- # SETUP 10: BLENDER + COMPOSITING (NUKE/AFTER EFFECTS ROUNDTRIP) Tags: #blender #compositing #setup # SETUP 10: BLENDER + COMPOSITING (NUKE/AFTER EFFECTS ROUNDTRIP) **ID:** SETUP-10 | **Type:** Compositing Pipeline | **Difficulty:** ⭐⭐⭐ | **Time:** 30-45 min --- ## 🎯 ЦЕЛЬ Настроить полноценный пайплайн Blender ↔ Nuke / After Effects для профессионального композитинга --- ## 🎯 АРХИТЕКТУРА ПАЙПЛАЙНА ``` Blender (3D) OpenEXR/ACES Nuke / AE (Comp) ┌─────────────┐ Multi-layer ┌─────────────┐ │ 3D Scene │ ──────────────────────▶ │ Compositing│ │ - Models │ EXR Multi-layer │ - Grade │ │ - Lights │ Cryptomatte │ - Effects │ │ - Cameras │ Deep Data │ - Track │ └─────────────┘ └─────────────┘ │ │ ▼ ▼ ┌─────────────────┐ ┌─────────────┐ │ Deliverables │ │ Final Out │ │ - EXR seq │ │ - ProRes │ │ - LUTs/CDL │ │ - H264/HEVC│ │ - LUTs/3D LUT │ │ - DPX/EXR │ └─────────────────┘ └─────────────┘ ``` --- ## 📋 НАСТРОЙКА EXR MULTI-LAYER EXPORT ### Blender EXR Multi-layer Setup ```python # exr_multilayer_setup.py import bpy def setup_exr_multilayer(): scene = bpy.context.scene scene.use_nodes = True tree = scene.node_tree nodes = tree.nodes links = tree.links nodes.clear() # Render Layers rl = nodes.new('CompositorNodeRLayers') rl.location = (-400, 0) rl.layer = "ViewLayer" # Enable all passes view_layer = bpy.context.view_layer view_layer.use_pass_combined = True view_layer.use_pass_z = True # Depth view_layer.use_pass_normal = True # Normal view_layer.use_pass_mist = True # Mist view_layer.use_pass_ambient_occlusion = True # AO view_layer.use_pass_emission = True # Emission view_layer.use_pass_shadow = True # Shadow view_layer.use_pass_diffuse_color = True # DiffCol view_layer.use_pass_glossy_color = True # GlossCol view_layer.use_pass_transmission_color = True # TransCol view_layer.use_pass_subsurface_color = True # SubsurfCol view_layer.use_pass_volume = True # Volume # Cryptomatte view_layer.use_pass_cryptomatte = True view_layer.cryptomatte_use_object = True view_layer.cryptomatte_use_material = True view_layer.cryptomatte_use_asset = True # Freestyle bpy.context.scene.render.use_freestyle = True view_layer.use_freestyle = True # Cryptomatte node crypto = nodes.new('CompositorNodeCryptomatte') crypto.location = (0, -400) links.new(rl.outputs['Cryptomatte00'], crypto.inputs['Image']) # File Output - MultiLayer EXR file_out = nodes.new('CompositorNodeOutputFile') file_out.location = (800, 0) file_out.format.file_format = 'OPEN_EXR_MULTILAYER' file_out.format.color_mode = 'RGBA' file_out.format.color_depth = '32' file_out.format.exr_codec = 'ZIP' file_out.base_path = "//renders/exr/" # Connect all passes passes = [ 'Image', 'Depth', 'Normal', 'Mist', 'AO', 'Emission', 'Shadow', 'DiffCol', 'GlossCol', 'TransCol', 'SubsurfCol', 'Volume', 'Cryptomatte00', 'Cryptomatte10', 'Cryptomatte20' ] for i, pass_name in enumerate(passes): if pass_name in rl.outputs: file_out.layer_slots.new(pass_name) links.new(rl.outputs[pass_name], file_out.inputs[i]) file_out.layer_slots[i].path = pass_name # Composite output comp = nodes.new('CompositorNodeComposite') comp.location = (1000, 0) links.new(nodes['Render Layers'].outputs['Image'], comp.inputs['Image']) print("EXR Multi-layer setup complete!") setup_exr_multilayer() ``` --- ## 🎬 NUKE ROUNDTRIP ### Nuke Import Script ```python # nuke_import.py (запускать в Nuke Script Editor) import nuke import os def import_blender_exr(exr_path): """Импорт Blender EXR в Nuke""" read = nuke.createNode('Read') read['file'].setValue(exr_path) read['colorspace'].setValue('linear') return read def setup_cryptomatte(read_node): """Настройка Cryptomatte""" crypto = nuke.createNode('Cryptomatte') crypto.setInput(0, read_node) crypto['cryptoLayer'].setValue('crypto_object') # или crypto_material return crypto def setup_deep_compositing(read_node): """Deep compositing setup""" deep = nuke.createNode('DeepRead') deep['file'].setValue(read_node['file'].value()) return deep # Пример полного пайплайна def setup_blender_shot(shot_name, exr_path): read = import_blender_exr(exr_path) crypto = setup_cryptomatte(read) # Color correction grade = nuke.createNode('Grade') grade.setInput(0, crypto) # Output write = nuke.createNode('Write') write['file'].setValue(f'/renders/final/{shot_name}_v001.%04d.exr') write['file_type'].setValue('exr') write['colorspace'].setValue('linear') write.setInput(0, grade) return write ``` --- ## 🎨 AFTER EFFECTS ROUNDTRIP ### AE Import Script (JSX) ```javascript // ae_import_blender.jsx (запуск в After Effects) var exrPath = "/path/to/renders/shot001/beauty.####.exr"; var compName = "Blender_Shot_001"; // Создание компа var comp = app.project.items.addComp( "Blender_Shot_001", 1920, 1080, 1, 250, 24 ); // Импорт EXR последовательности var importOptions = new ImportOptions(File(exrPath)); importOptions.sequence = true; importOptions.forceAlphabetical = true; var footage = app.project.importFile(importOptions); // Добавление в комп var layer = comp.layers.add(footage); layer.adjustmentLayer = false; // Cryptomatte эффект var crypto = layer.Effects.addProperty("Cryptomatte"); crypto.property("Crypto Layer").setValue("crypto_object"); // Color Correction var cc = layer.Effects.addProperty("Color Correction"); cc.property("Master Saturation").setValue(110); // Export var renderQueue = app.project.renderQueue; var rqItem = renderQueue.items.add(comp); rqItem.outputModule(1).file = new File("/renders/final/shot001_final.mov"); rqItem.outputModule(1).applyTemplate("Lossless with Alpha"); ``` ### OpenColorIO в AE ```javascript // OCIO Setup для ACES в AE app.project.colorSettings.colorManagementEnabled = true; app.project.colorSettings.workingSpace = "ACEScg"; app.project.colorSettings.displayColorSpace = "sRGB"; app.project.colorSettings.viewTransform = "ACES 1.0 SDR-video"; ``` --- ## 🔄 ROUNDTRIP WORKFLOW ### Blender → Nuke → Blender ```python # blender_nuke_roundtrip.py import bpy import subprocess from pathlib import Path def blender_to_nuke_to_blender(shot_name): """ Полный цикл: Blender → Nuke → Blender """ # 1. Blender: Render EXR Multi-layer exr_path = f"//renders/{shot_name}/beauty.####.exr" bpy.context.scene.render.filepath = exr_path bpy.context.scene.render.image_settings.file_format = 'OPEN_EXR_MULTILAYER' bpy.context.scene.render.image_settings.color_mode = 'RGBA' bpy.context.scene.render.image_settings.color_depth = '32' bpy.context.scene.render.image_settings.exr_codec = 'ZIP' bpy.ops.render.render(animation=True, write_still=False) # 2. Nuke Processing (внешний скрипт) nuke_script = f""" import nuke read = nuke.createNode('Read') read['file'].setValue('{exr_path}') # ... compositing ... write = nuke.createNode('Write') write['file'].setValue('//renders/{shot_name}_comp.####.exr') write['file_type'].setValue('exr') nuke.execute(write, 1, 250) """ # Запуск Nuke subprocess.run(['nuke', '-t', '-x', nuke_script], check=True) # 3. Blender: Импорт результата comp_path = f"//renders/{shot_name}_comp.####.exr" # Импорт как Image Sequence или Movie Clip bpy.ops.clip.open(filepath=comp_path) return "Roundtrip complete" ``` --- ## 🎯 COLOR MANAGEMENT (ACES/OCIO) ### Unified OCIO Config ```yaml # config.ocio (общий для Blender/Nuke/AE) ocio_profile_version: 2 color_spaces: - ! name: ACEScg bit_depth: 32f is_data: false to_reference: ! children: - ! {matrix: [0.662454, 0.134004, 0.156187, 0, 0.272228, 0.674081, 0.053691, 0, -0.005574, -0.004058, 1.009632, 0, 0, 0, 0, 1]} - name: sRGB bit_depth: 8ui to_reference: ! {src: sRGB, dst: ACEScg} viewing_rules: - ! {name: "sRGB", colorspace: "sRGB", encodings: ["sRGB"]} views: - ! {name: "ACES 1.3 SDR-video", colorspace: "sRGB", looks: ["ACES_sRGB"]} looks: - ! {name: "ACES_sRGB", process_space: "ACEScg", transform: ! {children: [{! {src: ACEScg, dst: sRGB}}]}} ``` ### Nuke OCIO Setup ```python # nuke_ocio_setup.py nuke.root()['colorManagement'].setValue('OCIO') nuke.root()['OCIO_config'].setValue('custom') nuke.root()['OCIO_config_file'].setValue('/path/to/config.ocio') nuke.root()['colorManagement'].setValue('OCIO') nuke.root()['OCIO_viewTransform'].setValue('ACES 1.3 SDR-video') ``` --- ## 📦 DELIVERY SPECS | Deliverable | Format | Color Space | Bit Depth | Use Case | |-------------|--------|-------------|-----------|----------| | **Master** | EXR MultiLayer | ACEScg | 32-bit float | Archive | | **Comp Source** | EXR MultiLayer | ACEScg | 32-bit float | Nuke/AE | | **Review** | MP4/H.264 | sRGB | 8-bit | Client Review | | **Broadcast** | ProRes 422 HQ | Rec.709 | 10-bit | TV Delivery | | **DCI-P3** | ProRes 4444 | P3-D65 | 12-bit | Cinema DCP | | **HDR (PQ)** | ProRes 4444 | Rec.2020 ST2084 | 16-bit | HDR Delivery | | **HDR (HLG)** | ProRes 4444 | Rec.2020 HLG | 10-bit | Broadcast HDR | | **Web/Streaming** | H.265/HEVC | sRGB/P3 | 10-bit | YouTube/Vimeo | --- ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 07: CI/CD INTEGRATION](setup_07_ci_cd.md), [SETUP 09: BLENDERBOT / AI CODING AGENT](setup_09_blenderbot_ai.md), [SETUP 11: BLENDER + TOUCHDESIGNER REALTIME VISUALS](setup_11_touchdesigner.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 12: BLENDER + UNREAL ENGINE 5 LIVE LINK](setup_12_unreal_livelink.md), [SETUP 13: BLENDER + UNITY ROUNDTRIP](setup_13_unity_roundtrip.md) - **Теги:** #blender, #compositing, #setup --- # SETUP 11: BLENDER + TOUCHDESIGNER REALTIME VISUALS Tags: #blender #realtime #setup # SETUP 11: BLENDER + TOUCHDESIGNER REALTIME VISUALS **ID:** SETUP-11 | **Type:** Realtime Visuals / Interactive | **Difficulty:** ⭐⭐⭐ | **Time:** 60-90 min > Когда применять: когда рендер Blender должен попадать в TouchDesigner вживую — VJ-сет, инсталляция, генеративная графика с реакцией на OSC-параметры. --- ## 🎯 ЦЕЛЬ Собрать рабочий канал Blender → TouchDesigner и обратный канал управления параметрами. Отдельно настроить два независимых потока: поток картинки (видео, а не данные) и поток чисел (параметры, триггеры, OSC). На выходе — TouchDesigner, в котором видно рендер Blender и который управляет им по сети. ## 🎯 АРХИТЕКТУРА ``` Blender (Content) OSC/NDI/Spout TouchDesigner (Realtime) ┌─────────────────┐ OSC/UDP/TCP ┌─────────────────┐ │ Models/Anim │ ───────────────────────▶ │ Real-time │ │ Cameras/Lights │ NDI/Spout (Video) │ Render/Comp │ │ Geometry Nodes │ Shared Memory │ - VJ │ │ Shaders │ OSC (Params) │ - Interactive│ └─────────────────┘ └─────────────────┘ ``` ## 🔧 ОСНОВНЫЕ ПРОТОКОЛЫ | Протокол | Задержка | Пропускная способность | Use Case | |----------|----------|------------------------|----------| | **OSC** | ~1-5ms | Низкая | Параметры, триггеры, синк | | **NDI** | ~1-2 кадра | Высокая (4K60) | Видео поток | | **Spout** | ~0-1 кадр | Высокая (GPU-direct) | Windows GPU sharing | | **Shared Memory** | ~0ms | Очень высокая | Текстуры/геометрия | | **WebSocket** | ~5-10ms | Средняя | Web/Remote | | **ZeroMQ** | ~1-2ms | Высокая | Высоконагруженные | ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |-----------|--------|-------| | Blender | 4.2 LTS или новее | источник картинки и геометрии | | TouchDesigner | 2023.x и новее | приём видео и OSC, композитинг в реальном времени | | Python-пакет `pyosc` | любая совместимая с Python Blender | отправка OSC из Blender без ручной сборки пакетов | | NDI-аддон для Blender (structify) | — | видеопоток NDI из окна Blender | | NDI Tools (NDI 6 Tools, Test Patterns) | 6.x | проверка, что поток вообще идёт, без TouchDesigner | | Windows | 10/11 | Spout и NDI существуют в основном под Windows; на Linux альтернатива — Pipe/Spout через wine отсутствует | > [!NOTE] > Список аддонов NDI/Spout для Blender меняется: проекты сторонние, не все поддерживают свежие версии Blender. Проверь в `Edit → Preferences → Add-ons`, что аддон совместим с твоей версией, прежде чем планировать пайплайн. Если аддон не собирается — рабочий запасной вариант описан в шаге 4 (кадры на диск + `Video File In TOP`). ## 🛠️ НАСТРОЙКА ### 1. Выбрать, что именно «живёт» в Blender Режим рендера — первое, что определяет всю дальнейшую настройку: ```python # Blender: Python Console или Text Editor import bpy scene = bpy.context.scene # EEVEE — единственный вариант, дающий realtime-сток для видеопотока scene.render.engine = 'BLENDER_EEVEE_NEXT' # в Blender 4.2+ # В более старых версиях идентификатор движка — 'BLENDER_EEVEE' # Разрешение потока под целевой выход TouchDesigner scene.render.resolution_x = 1920 scene.render.resolution_y = 1080 scene.render.resolution_percentage = 100 scene.render.fps = 30 # Кадры рендера пишем в PNG — NDI/Spout-аддоны берут кадр из Render Result, # а не с диска, поэтому формат влияет только на отладочные рендеры scene.render.image_settings.file_format = 'PNG' ``` > [!NOTE] > Идентификатор движка рендера (`BLENDER_EEVEE_NEXT` против `BLENDER_EEVEE`) менялся между версиями Blender. Не хардкодь его в скриптах: читай значение из `bpy.context.scene.render.engine` и меняй в UI. Точный список доступных движков — `bpy.types.RenderSettings.bl_rna.properties["engine"].enum_items`. ### 2. Отправка параметров в TouchDesigner по OSC Это самый надёжный канал: маленькие сообщения, никакой зависимости от версий видео-аддонов. ```bash # Установка pyosc в Python, которым пользуется Blender, а не в системный Python. # На Windows путь к нему примерно такой: # %APPDATA%\Blender Foundation\Blender\4.2\scripts\python\Lib\site-packages # Точную версию подставь вместо 4.2 — она равна твоей версии Blender. # Либо поставь пакет в отдельный venv и подключи его через # Blender → Edit → Preferences → System → Python Paths. pip install python-osc ``` ```python # blender_osc_bridge.py — запускать из Blender: Text Editor → Run Script import bpy from pyosc.udp_client import SimpleUDPClient TD_IP = "127.0.0.1" TD_PORT = 3333 # порт, на котором в TouchDesigner слушает OSC In CHOP client = SimpleUDPClient(TD_IP, TD_PORT) def _send(scene): """Каждый кадр шлём в TouchDesigner то, что он не может узнать сам.""" # Позиция камеры — TouchDesigner двигает ею свой 3D-слой cam = scene.camera if cam: client.send_message("/blender/cam/pos", list(cam.matrix_world.translation)) client.send_message("/blender/cam/rot", list(cam.matrix_world.to_euler())) # Текущий кадр — для синхронизации с запуском аудио/видео в TD client.send_message("/blender/frame", scene.frame_current) def start(): if _send not in bpy.app.handlers.frame_change_post: bpy.app.handlers.frame_change_post.append(_send) def stop(): if _send in bpy.app.handlers.frame_change_post: bpy.app.handlers.frame_change_post.remove(_send) if __name__ == "__main__": start() ``` На стороне TouchDesigner: `OSC In CHOP` (внутренний путь `tosi/in`) на порт 3333, на выходе — таблица каналов `/blender/cam/pos[0..2]` и `/blender/frame`. Дальше `Parameter CHOP` или `Math CHOP` превращают их в параметры. ### 3. Приём OSC обратно: ручки TouchDesigner управляют Blender ```python # В Blender отдельным скриптом: поднимаем OSC-сервер и пишем значения в сцену import bpy import threading from pyosc.osc_server import OSCServer scene = bpy.context.scene def handler(address, *args): """pyosc зовёт хендлер как handler('/address', value1, value2, ...)""" if address == "/td/mood" and args: # Один параметр на все материалы сцены — грубо, но наглядно scene.view_settings.exposure = float(args[0]) if address == "/td/worley_scale" and args: node = bpy.data.node_groups.get("TDScratch") if node and "Scale" in node.inputs: node.inputs["Scale"].default_value = float(args[0]) server = OSCServer("127.0.0.1", 3334) # отдельный порт, чтобы не конфликтовать с TX server.bind(handler) # OSCServer.listen() блокирующий — поднимаем в отдельном потоке, # иначе скрипт запуска не вернёт управление в Blender threading.Thread(target=server.listen, daemon=True).start() print("OSC server on 3334") ``` ### 4. Видеопоток: три пути, от простого к сложному Путь A — самый предсказуемый: Blender пишет кадры, TouchDesigner читает их как видеофайл. Годится для проверки пайплайна и для выставочного проекта, где задержка в кадр не важна. ```python # Blender: рендер последовательности в папку, которую смотрит TouchDesigner import bpy, os scene = bpy.context.scene out_dir = os.path.join(os.path.expanduser("~"), "td_stream") os.makedirs(out_dir, exist_ok=True) scene.render.filepath = os.path.join(out_dir, "frame_") # Blender допишет 0001.png scene.render.image_settings.file_format = 'PNG' bpy.ops.render.render(animation=True) # блокирует UI на всё время секции ``` В TouchDesigner: `Video File In TOP` с этим путём → `Movie Out TOP` или в Composite. Это битмап по кадрам, не видеоданные, поэтому задержка и цвет ведут себя предсказуемо. Путь B — NDI. Аддон для Blender отправляет кадр в NDI-имя, в TouchDesigner `Video In TOP` с Source = NDI читает его. Проверяется без Blender: сначала `NDI Test Patterns` в NDI Tools, потом `NDI Studio Monitor` — если картинка есть, Blender уже не при чём. Путь C — Spout. Оба приложения на одной видеокарте, кадр не покидает GPU. Аддон Blender отдаёт кадр в Spout-шину, `Spout In TOP` в TouchDesigner его забирает. Проще всего для быстрых визуалов, но Spout — это Windows-only и требует, чтобы Blender не был в модальном диалоге. Быстрый снимок одного кадра (для отладки, не для потока): ```python # Снимок 3D-вьюпорта без рендера — быстро, но без EEVEE-освещения bpy.ops.render.opengl(write_still=True, view_context=True) bpy.data.images["Render Result"].save_render(filepath=r"C:\tmp\td_debug.png") ``` ### 5. Проверка сквозного канала ``` 1. Blender: запусти скрипт OSC-моста, поставь frame_current = 1 2. TouchDesigner: OSC In CHOP читает /blender/frame — значение должно расти 3. TouchDesigner: добавь в сеть Video In TOP (NDI/Spout) или Video File In TOP 4. Blender: в NDI Tools убедись, что поток называется так, как его ищет TD 5. Двигай камеру в Blender — картинка в TD обязана ехать без перезапуска ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```python # Диагностика одним куском — вставить в Blender Python Console import bpy, socket, sys print("engine :", bpy.context.scene.render.engine) print("fps :", bpy.context.scene.render.fps) print("handlers :", [f.__name__ for f in bpy.app.handlers.frame_change_post]) print("osc client :", "pyosc ok" if "pyosc" in sys.modules else "pyosc NOT imported") # Проверяем, что порт, который слушает TD, действительно живой на этой машине probe = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) probe.settimeout(0.5) try: probe.sendto(b"\x00", ("127.0.0.1", 3333)) print("udp 3333 : отправлено без ошибки (это не доказывает, что слушатель есть)") except OSError as e: print("udp 3333 :", e) finally: probe.close() ``` | Параметр | Значение | Когда менять | |----------|----------|--------------| | `TD_PORT` (OSC out) | 3333 | если порт занят — смени и в `SimpleUDPClient`, и в OSC In CHOP | | `TD_PORT` (OSC in) | 3334 | разведи входящий и исходящий OSC по разным портам, иначе сообщения зацикливаются | | `frame_change_post` | список хендлеров | всегда проверяй перед `remove()`, иначе `ValueError` при повторном запуске | | `scene.render.fps` | 30 | под audio-такт в TouchDesigner ставь целое число | | `resolution_x/y` | 1920×1080 | NDI-аддоны часто умеют меньше — сверься с документацией аддона | | Видеопротокол | NDI / Spout / файл | NDI для передачи по сети, Spout для одной GPU, файл для отладки | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |--------|---------|---------| | В TouchDesigner нет ни одного канала OSC In | Blender-скрипт не запущен, либо `pyosc` не импортировался в Python Blender | Проверь `sys.modules` в консоли Blender; пакет ставится в Python Blender, не в системный Python | | Значения приходят, но не меняются | Хендлер навешен на `frame_change_pre`, а сцена не пересчитывается | Используй `frame_change_post`; для параметров сцены этого достаточно | | `ValueError: not found in list` при повторном запуске | Хендлер добавился дважды и `remove()` снимает несуществующий | Перед добавлением проверяй `if _send not in bpy.app.handlers.frame_change_post` | | Видеопоток в TD чёрный, хотя OSC работает | Аддон отправляет кадр из окна, а окно Blender свёрнуто или перекрыто | Разверни окно с 3D-вьюпортом; в настройках аддона выбери источник — Viewport или Camera | | Кадр в TD отстаёт на 2-3 кадра | Кодирование/декодирование видео по сети | Переходи на Spout (общая GPU) либо снижай `resolution_percentage` | | После рендера `python` падает с `AttributeError` на `file_format` | Оператор рендера вернул `CANCELLED`, результата нет | Проверяй результат оператора перед `save_render` | | OSC-сервер вешает интерфейс Blender | `OSCServer.listen()` блокирующий, вызван из Text Editor | Запускай в `threading.Thread(..., daemon=True)` | | Цвета в TD и рендере Blender не совпадают | Разные view transform в двух приложениях | Приводи оба к одному view transform; TouchDesigner работает в линейном пространстве | ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 07: CI/CD INTEGRATION](setup_07_ci_cd.md), [SETUP 09: BLENDERBOT / AI CODING AGENT](setup_09_blenderbot_ai.md), [SETUP 10: BLENDER + COMPOSITING (NUKE/AFTER EFFECTS ROUNDTRIP)](setup_10_compositing_roundtrip.md) - **По темам:** [EEVEE NEXT (BLENDER 4.2+)](../realtime/eevee_next.md), [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 12: BLENDER + UNREAL ENGINE 5 LIVE LINK](setup_12_unreal_livelink.md) - **Теги:** #blender, #realtime, #setup --- # SETUP 12: BLENDER + UNREAL ENGINE 5 LIVE LINK Tags: #blender #setup #unreal # SETUP 12: BLENDER + UNREAL ENGINE 5 LIVE LINK **ID:** SETUP-12 | **Type:** Game Engine Integration | **Difficulty:** ⭐⭐⭐ | **Time:** 30-60 min --- ## 🎯 ЦЕЛЬ Настроить бесшовную интеграцию Blender ↔ Unreal Engine 5 через Live Link, Datasmith, USD --- ## 🎯 АРХИТЕКТУРА ``` Blender (Authoring) Live Link / USD Unreal Engine 5 (Runtime) ┌─────────────────┐ Live Link / USD ┌─────────────────┐ │ Modeling │ ──────────────────────────────▶ │ Level Design │ │ Rigging/Anim │ Datasmith / Live Link │ Blueprint │ │ Materials │ Alembic / FBX / USD │ Niagara FX │ │ Cameras │ Live Link Face/VCam │ Sequencer │ └─────────────────┘ └─────────────────┘ ``` --- ## 🔗 MЕТОДЫ ИНТЕГРАЦИИ | Метод | Качество | Сложность | Use Case | |-------|----------|-----------|----------| | **Live Link** | Высокая | Средняя | Realtime камеры, 얼굴, камеры | | **Datasmith** | Высокая | Низкая | Архитектура, статические сцены | | **USD** | Максимальная | Высокая | Пайплайн, варианты, LOD | | **Alembic** | Высокая | Низкая | Анимация, кэш | | **FBX** | Средняя | Низкая | Быстрый экспорт/импорт | | **Live Link Face/VCam** | Высокая | Низкая | Мокап, виртуальная камера | --- ## 🔴 LIVE LINK SETUP ### 1. Blender: Live Link Plugin ```python # blender_livelink.py # Требует: Unreal Live Link Plugin в Blender (Experimental) # или аддон: https://github.com/EpicGames/UnrealEngine/tree/release/Engine/Plugins/Animation/LiveLink import bpy import asyncio import websockets import json class BlenderLiveLink: def __init__(self, host="127.0.0.1", port=9876): self.uri = f"ws://{host}:{port}" self.websocket = None self.running = False async def connect(self): self.websocket = await websockets.connect(self.uri) await self.register_subject() async def register_subject(self): """Регистрация субъекта в Unreal""" msg = { "MessageType": "SubjectRegistration", "SubjectName": "Blender_Camera", "SubjectType": "Camera" } await self.websocket.send(json.dumps(msg)) async def send_camera_frame(self, camera_data): """Отправка данных камеры каждый кадр""" frame_data = { "MessageType": "FrameData", "SubjectName": "Blender_Camera", "FrameData": { "WorldTransform": { "Translation": list(camera_data.location), "Rotation": list(camera_data.rotation_quaternion), "Scale": [1,1,1] }, "FieldOfView": camera_data.fov, "AspectRatio": camera_data.aspect_ratio, "NearClip": camera_data.clip_start, "FarClip": camera_data.clip_end } } await self.websocket.send(json.dumps(frame_data)) # Авто-отправка каждый кадр def setup_livelink_camera(): bpy.app.handlers.frame_change_post.append(send_camera_data) def send_camera_data(scene): cam = bpy.context.scene.camera if cam and livelink_client: asyncio.create_task(livelink_client.send_camera_frame(cam.data)) ``` ### Unreal Live Link Setup ``` 1. Unreal: Edit → Plugins → Live Link → Enable 2. Window → Live Link → Add Source → "Blender" (WebSocket) 3. Source: ws://localhost:9876 4. Subject: "Blender_Camera" → Add to Level → CineCameraActor → Live Link Controller ``` --- ## 📦 DATASMITH (Архитектура/Статика) ### Blender → Datasmith Export ```python # blender_datasmith_export.py import bpy import os from pathlib import Path def export_datasmith(output_path, selection_only=True): """ Экспорт через Datasmith (требует Blender Datasmith Exporter) """ # В Blender 4.0+ есть встроенный экспорт Datasmith # File → Export → Datasmith (.udatasmith) # Настройки экспорта: export_settings = { "export_selected": True, "export_hierarchy": True, "export_materials": True, "export_textures": True, "export_lights": True, "export_cameras": True, "convert_units": True, # Blender meters → Unreal cm "collision": "simple", # none/simple/complex "generate_lightmap_uvs": True, "lightmap_resolution": 64, } # Экспорт через Python API (если доступно) # bpy.ops.export_scene.datasmith(filepath=output_path, **export_settings) print("Use File → Export → Datasmith (.udatasmith) for best results") # Настройки материалов для Datasmith def setup_datasmith_materials(): """Подготовка материалов для Datasmith""" for mat in bpy.data.materials: if mat.use_nodes: # Найти Principled BSDF for node in mat.node_tree.nodes: if node.type == 'BSDF_PRINCIPLED': # Datasmith понимает стандартные PBR входы # Base Color, Roughness, Metallic, Normal, Emission # Дополнительные: Clearcoat, Anisotropy, Sheen, Transmission pass # Именование для Datasmith def naming_convention(): """ Datasmith Naming Rules: - SM_StaticMeshName (Static Mesh) - SK_SkeletalMeshName (Skeletal Mesh) - M_MaterialName (Material) - T_TextureName_D (BaseColor) - T_TextureName_N (Normal) - T_TextureName_R (Roughness) - T_TextureName_M (Metallic) - T_TextureName_AO (Ambient Occlusion) - T_TextureName_H (Height) - MI_MaterialInstance (Material Instance) """ ``` --- ## 💎 USD PIPELINE (Продвинутый) ### USD Export для Unreal ```python # usd_unreal_export.py import bpy from pathlib import Path def export_usd_unreal(filepath, selection_only=True): """Экспорт USD оптимизированный для Unreal""" Path(filepath).parent.mkdir(parents=True, exist_ok=True) bpy.ops.wm.usd_export( filepath=str(Path(filepath).absolute()), selection_only=selection_only, export_materials=True, export_uvmaps=True, export_animation=True, export_frame_range=True, export_frame_step=1, # MaterialX для Unreal material_x=True, shading_mode='USE_MATERIAL_X', # Purpose для Unreal purpose='default', # default, render, proxy, guide # Instancing export_instancing=True, # Animation export_frame_range=True, export_frame_step=1, # Custom properties export_custom_properties=True, export_custom_attributes=True, ) # USD Variants для Unreal def create_usd_variants(filepath, variants): """ variants = { "modeling": ["high", "low", "proxy"], "material": ["clean", "damaged", "rusty"], "lod": ["LOD0", "LOD1", "LOD2", "LOD3"] } """ from pxr import Usd, UsdGeom, Sdf stage = Usd.Stage.CreateNew(filepath) root = stage.GetRootPrim() for set_name, variants in variants.items(): variant_set = root.GetVariantSets().AddVariantSet(set_name) for variant in variants: variant_set.AddVariant(variant) variant_set.SetVariantSelection(variant) with variant_set.GetVariantEditContext(): # Добавить variant-specific контент pass stage.GetRootLayer().Save() # LOD Variants def create_lod_variants_usd(asset_name, lod_meshes): """ lod_meshes = { "LOD0": mesh_obj_high, "LOD1": mesh_obj_med, "LOD2": mesh_obj_low, "LOD3": mesh_obj_imp } """ from pxr import Usd, UsdGeom stage = Usd.Stage.CreateNew(f"{asset_name}_LODs.usda") root = stage.DefinePrim(f"/{asset_name}", "Xform") for lod_name, mesh_obj in lod_meshes.items(): lod_prim = stage.DefinePrim(f"/{asset_name}/{lod_name}", "Xform") # Экспорт меша в USD prim # ... mesh export logic stage.GetRootLayer().Save() ``` --- ## 🎬 ANIMATION PIPELINE ### Alembic Export/Import ```python # alembic_pipeline.py import bpy def export_alembic_animation(filepath, objects, start_frame, end_frame): """Экспорт анимации в Alembic для Unreal""" bpy.ops.wm.alembic_export( filepath=str(Path(filepath).absolute()), selected=True, selected_objects=[o.name for o in objects], start=start_frame, end=end_frame, step=1.0, flatten=True, subdiv_schema=True, write_uvs=True, write_normals=True, write_vertex_colors=True, export_subdivision=True, subdiv_schema=True, ) # Unreal Alembic Import """ Unreal: File → Import → Alembic Cache (.abc) Settings: - Import Type: Skeletal / Geometry Cache - Import Type: Skeletal (для скелетной анимации) - Create Skeleton: Yes - Import Materials: True - Create Physics Asset: No """ # Geometry Cache (для сложной анимации) def export_geometry_cache(objects, output_dir): """ Geometry Cache (.abc) для сложных деформаций Unreal: Geometry Cache Component """ for obj in objects: bpy.ops.wm.alembic_export( filepath=f"{output_dir}/{obj.name}.abc", selected=True, start=bpy.context.scene.frame_start, end=bpy.context.scene.frame_end, ) ``` --- ## 🎥 LIVE LINK FACE / VCAM ### Live Link Face (iOS) ```python # blender_livelink_face.py """ Live Link Face (iOS) → Blender → Unreal 1. iPhone: Live Link Face app → IP Blender/Unreal 2. Blender: Прием morph targets 3. Unreal: Live Link Face → MetaHuman """ # Blender: Прием ARKit blendshapes class LiveLinkFaceReceiver: def __init__(self, port=11111): self.server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.server.bind(('0.0.0.0', port)) def receive_blendshapes(self): data, addr = self.sock.recvfrom(65536) # ARKit blendshapes: 52 blendshapes # Применить к Shape Keys меша data = json.loads(data) for name, value in data['blendshapes'].items(): if name in bpy.context.object.data.shape_keys.key_blocks: bpy.context.object.data.shape_keys.key_blocks[name].value = value # Unreal Live Link Face Setup: """ 1. Unreal: Window → Live Link → Add Source → Live Link Face 2. iPhone: Live Link Face App → Connect to Unreal IP 3. Blueprint: Live Link Controller → Set Subject → Face 3. MetaHuman: Assign to Face Mesh """ ``` ### Virtual Camera (VCam) ```python # blender_virtual_camera.py """ Virtual Camera: Blender Camera → Unreal VCam 1. Blender: Camera + Live Link 2. Unreal: VCam Plugin → Live Link Controller 3. iPad/iPhone: Unreal VCam App → Control Camera """ # Blender: Virtual Camera Setup def setup_virtual_camera(): cam = bpy.data.objects.new("VCam", bpy.data.cameras.new("VCam")) bpy.context.collection.objects.link(cam) # Live Link properties cam["LiveLinkSubject"] = "VCam" cam["LiveLinkSource"] = "Blender" # Constraints для smooth movement constraint = cam.constraints.new('COPY_TRANSFORMS') constraint.target = bpy.data.objects.get("VCam_Controller") return cam ``` --- ## 📦 UNREAL IMPORT SETTINGS ### FBX Import Settings ```python # unreal_fbx_import.py (Unreal Python) import unreal def import_fbx_optimized(fbx_path, destination_path="/Game/Assets/"): """Оптимизированный импорт FBX в Unreal""" import_options = unreal.FbxImportUI() import_options.import_mesh = True import_options.import_as_skeletal = False import_options.mesh_type_to_import = unreal.FBXImportType.FBXIT_STATIC_MESH # Static Mesh Settings import_options.static_mesh_import_data.combine_meshes = True import_options.static_mesh_import_data.auto_generate_collision = True import_options.static_mesh_import_data.collision_type = unreal.FBXCollisionType.FBXCTF_Box import_options.static_mesh_import_data.auto_compute_lod_distances = True import_options.static_mesh_import_data.lod_count = 4 import_options.static_mesh_import_data.lod_distance_ratios = [0.0, 0.3, 0.6, 0.9] # Materials import_options.material_import_method = unreal.FBXMaterialImportMethod.FBXMI_FIND_OR_CREATE import_options.texture_import_data.material_search_path = "/Game/Materials" # Import task = unreal.AssetImportTask() task.filename = fbx_path task.destination_path = destination_path task.replace_existing = True task.automated = True task.save = True task.options = import_options unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) ``` ### Geometry Cache Import ```python # Geometry Cache для сложных деформаций def import_geometry_cache(abc_path, destination="/Game/Animations/"): import_options = unreal.GeometryCacheImportOptions() import_options.import_type = unreal.GeometryCacheImportType.ALEMBIC import_options.flip_u = False import_options.flip_v = False task = unreal.AssetImportTask() task.filename = abc_path task.destination_path = destination task.options = import_options task.automated = True task.replace_existing = True unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) ``` --- ## 🎮 VIRTUAL CAMERA (VCAM) ### Blender → Unreal VCam ```python # blender_virtual_camera.py """ Blender Camera → Unreal Virtual Camera 1. Blender: Camera + Live Link 2. Unreal: VCam Plugin → Live Link Controller 3. iPad/iPhone: Unreal VCam App """ class VirtualCameraSetup: def __init__(self): self.vcam = None def setup_vcam(self, camera_name="VCam"): # Создать камеру cam_data = bpy.data.cameras.new("VCam") cam_obj = bpy.data.objects.new("VCam", cam_data) bpy.context.collection.objects.link(cam_obj) # Настройки для VCam cam_data.lens = 35 # mm cam_data.sensor_width = 36 cam_data.sensor_height = 24 # Live Link properties bpy.context.object["LiveLinkSubject"] = "VCam" bpy.context.object["LiveLinkRole"] = "VirtualCamera" # Constraints для VCam controller self.setup_vcam_constraints() def setup_vcam_constraints(self): # VCam Controller Empty controller = bpy.data.objects.new("VCam_Controller", None) bpy.context.collection.objects.link(controller) controller.empty_display_type = 'ARROWS' controller.empty_display_size = 0.5 # Camera follows controller cam = bpy.data.objects["VCam"] constraint = cam.constraints.new('COPY_TRANSFORMS') constraint.target = bpy.data.objects["VCam_Controller"] constraint.influence = 1.0 # VCam specific properties controller["VCam_Speed"] = 10.0 controller["VCam_Sensitivity"] = 1.0 ``` ### Unreal VCam Setup ```python # Unreal Python (в Unreal Editor) import unreal def setup_unreal_vcam(): # Enable VCam Plugin # Edit → Plugins → Virtual Camera → Enable → Restart # Create VCam Component vcam = unreal.VirtualCameraComponent() vcam.set_editor_property("LiveLinkSubject", "VCam") # VCam Settings vcam.set_editor_property("FieldOfView", 90.0) vcam.set_editor_property("AspectRatio", 16.0/9.0) # Input Settings input_settings = unreal.get_default_object(unreal.InputSettings) input_settings.add_axis_mapping(unreal.InputAxisKeyMapping( axis_name="VCam_MoveForward", key=unreal.Key("W"), scale=1.0 )) # ... другие маппинги ``` --- ## 📦 PIPELINE AUTOMATION ### Automated Export/Import ```python # pipeline_automation.py import bpy import subprocess from pathlib import Path class PipelineAutomation: def __init__(self, project_root): self.project_root = Path(project_root) self.exports_dir = self.project_root / "exports" self.imports_dir = self.project_root / "imports" def export_all_for_unreal(self): """Экспорт всех ассетов для Unreal""" collections = { "Characters": "Characters", "Props": "Props", "Environment": "Environment", "Vehicles": "Vehicles" } for coll_name, export_subdir in collections.items(): coll = bpy.data.collections.get(coll_name) if coll: self.export_collection(coll, export_subdir) def export_collection(self, collection, subdir): """Экспорт коллекции как отдельные FBX/USD""" output_dir = self.exports_dir / subdir output_dir.mkdir(parents=True, exist_ok=True) for obj in collection.all_objects: if obj.type == 'MESH': # FBX Export fbx_path = output_dir / f"{obj.name}.fbx" bpy.ops.object.select_all(action='DESELECT') obj.select_set(True) bpy.context.view_layer.objects.active = obj bpy.ops.export_scene.fbx( filepath=str(fbx_path), use_selection=True, apply_unit_scale=True, apply_scale_options='FBX_SCALE_ALL', bake_space_transform=True, object_types={'MESH', 'ARMATURE'}, use_armature_deform_only=True, add_leaf_bones=False, primary_bone_axis='Y_FORWARD', secondary_bone_axis='X_UP' ) # USD Export usd_path = output_dir / f"{obj.name}.usda" bpy.ops.wm.usd_export( filepath=str(output_dir / f"{obj.name}.usda"), selected_objects_only=True, export_materials=True, material_x=True, export_animation=False ) # Usage auto = PipelineAutomation("/project") auto.export_all_for_unreal() ``` --- ## 🎯 UNREAL BLUEPRINT INTEGRATION ### Live Link Blueprint ```cpp // Unreal C++ / Blueprint // LiveLinkController Component UCLASS() class MYGAME_API AMyCharacter : public ACharacter { GENERATED_BODY() UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "LiveLink") ULiveLinkControllerComponent* LiveLinkController; UFUNCTION(BlueprintCallable) void SetLiveLinkSubject(FName SubjectName) { if (LiveLinkController) { LiveLinkController->SetSubject(SubjectName); } } UFUNCTION(BlueprintCallable) void SetLiveLinkRetargetAsset(ULiveLinkRetargetAsset* Asset) { if (LiveLinkController) { LiveLinkController->SetRetargetAsset(Asset); } } }; // Blueprint Usage: // 1. Add LiveLinkController Component to Character // 2. Set Subject Name: "Blender_Camera" или "VCam" // 3. Set Retarget Asset: ваш Retarget Asset // 4. В Tick: Get Live Link Pose → Apply to Mesh ``` --- ## 📊 PERFORMANCE TIPS | Optimization | Impact | |--------------|--------| | **Reduce Update Rate** | Live Link: 30Hz вместо 60Hz для далеко объектов | | **Compress Transforms** | Отправлять только измененные кости | | **Batch Updates** | Группировать обновления в один пакет | | **LOD для Live Link** | Упрощенные скелеты для дальних объектов | | **Async Updates** | Не блокировать Game Thread | --- ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 07: CI/CD INTEGRATION](setup_07_ci_cd.md), [SETUP 09: BLENDERBOT / AI CODING AGENT](setup_09_blenderbot_ai.md), [SETUP 10: BLENDER + COMPOSITING (NUKE/AFTER EFFECTS ROUNDTRIP)](setup_10_compositing_roundtrip.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 11: BLENDER + TOUCHDESIGNER REALTIME VISUALS](setup_11_touchdesigner.md), [SETUP 13: BLENDER + UNITY ROUNDTRIP](setup_13_unity_roundtrip.md) - **Теги:** #blender, #setup, #unreal --- # SETUP 13: BLENDER + UNITY ROUNDTRIP Tags: #blender #setup #unity # SETUP 13: BLENDER + UNITY ROUNDTRIP **ID:** SETUP-13 | **Type:** Game Engine | **Difficulty:** ⭐⭐ | **Time:** 20-30 min **RU/EN:** Двуязычный файл / Bilingual --- > Когда применять: когда ассеты делаются в Blender, а собираются и рендерятся в Unity — и нужно, чтобы импорт был предсказуемым, а не «повезло с осями и масштабом». ## 🎯 ЦЕЛЬ Настроить двусторонний обмен Blender ↔ Unity так, чтобы геометрия приходила в правильном масштабе, с правильными нормалями и материалами, а мелкие правки из Unity возвращались обратно без разрушения исходника. Главное в этом обмене — не формат файла, а **договор о состоянии сцены перед экспортом**: применённый масштаб, единицы, отсутствие модификаторов, которые ещё не применены, и согласованная система имён. Формат — это деталь; состояние — это то, из-за чего импорт ломается. Отдельно стоит понимать разницу двух мостов: FBX (классический, с историей и багами) и glTF 2.0 (современный, с материалом и текстурами внутри одного файла). В Unity оба поддерживаются, выбор влияет на то, что переживает экспорт, а что нет. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия / состояние | Зачем | |---|---|---| | Blender | любая актуальная, экспортёр — в комплекте | FBX и glTF экспортируются штатно, плагины не нужны | | Unity | актуальный LTS | импортёр FBX и glTF встроены в редактор | | Пакет экспорта FBX из Unity | проверь, установлен ли | нужен только для обратного пути Unity → Blender | | Текстуры | отдельными файлами либо вшитыми в GLB | для FBX — только отдельными файлами | | Нейминг-конвенция | одна на проект | без неё префабы собираются вручную | ## 🛠️ НАСТРОЙКА ### Шаг 1. Единицы, масштаб и оси Blender и Unity оба считают в метрах, поэтому масштабировать сцену для переноса не нужно. Нужно сделать так, чтобы Blender не «помнил» нестандартный масштаб объектов. ```python import bpy # Применить масштаб ко всем мешам: экспортировать объект с scale = (1, 1, 1) def apply_scale_and_fix_rotation(objects=None): objects = objects or [o for o in bpy.context.scene.objects if o.type == 'MESH'] bpy.ops.object.select_all(action='DESELECT') for o in objects: o.select_set(True) bpy.context.view_layer.objects.active = objects[0] bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) apply_scale_and_fix_rotation() ``` Про оси: Blender — Z-up, Unity — Y-up. Это **не** требует ручного поворота объектов. Ориентация оси — свойство формата, и конвертер FBX/glTF переводит её сам. Поворот всего на 90° «на всякий случай» — самая частая причина ассетов, лежащих на боку. > [!NOTE] > Набор флагов оси у экспортёров (`axis_forward`, `axis_up` у FBX и `export_yup` > у glTF) менялся между версиями Blender. Значения по умолчанию рассчитаны на > Y-up-движки и менять их обычно не нужно. Если пришлось переопределять — сначала > проверь, что не сломана сама сцена, и только потом крути флаги. ### Шаг 2. Применить модификаторы перед экспортом Модификаторы вроде `Subdivision Surface`, `Mirror`, `Array` живут в файле и **не** переносятся в Unity: FBX и glTF несут только итоговую сетку. Дублировать их в движке — ручная работа, которая разъедется с исходником. ```python import bpy def apply_modifiers(objects=None): objects = objects or [o for o in bpy.context.scene.objects if o.type == 'MESH'] for ob in objects: bpy.context.view_layer.objects.active = ob for mod in list(ob.modifiers): # Для неразрушающих (Bevel, Solidify) применение не обязательно: # решает задача, а не инструмент. Здесь применяем всё геометрическое. bpy.ops.object.modifier_apply(modifier=mod.name) apply_modifiers() ``` ### Шаг 3. Экспорт FBX ```python import bpy bpy.ops.export_scene.fbx( filepath="//exports/unity/prop.fbx", use_selection=True, apply_unit_scale=True, object_types={'ARMATURE', 'MESH'}, # Только кости деформации: служебные кости в Unity мешают и раздувают скелет use_armature_deform_only=True, add_leaf_bones=False, bake_anim=True, # Кадр 1 = текущий кадр сцены. Без этого скелет уезжает в дефолтный позу bake_anim_use_nla_strips=True, ) ``` `path_mode` стоит выставить явно: `'COPY'` копирует текстуры рядом с `.fbx`, `'RELATIVE'` оставляет ссылку на исходный путь. `'AUTO'` по умолчанию ведёт себя по-разному в зависимости от того, запущен ли Blender из интерфейса или в фоне — а это ровно тот случай, когда локально работает, а в CI нет. ```python bpy.ops.export_scene.fbx( filepath="//exports/unity/prop.fbx", use_selection=True, path_mode='COPY', embed_textures=False, # True упакует текстуры внутрь FBX — крупный файл ) ``` > [!NOTE] > Полный список допустимых значений `path_mode` и `mesh_smooth_type` смотри > в `File → Export → FBX` или через > `bpy.ops.export_scene.fbx.get_rna_type().properties["path_mode"].enum_items`. > Значения `path_mode` и состав свойств менялись между версиями Blender. ### Шаг 4. Альтернатива: glTF 2.0 Если проект не привязан к FBX, glTF обычно предпочтительнее: материал, текстуры и анимация лежат в одном файле, а не рядом пачкой, и не нужно гадать, куда экспортировались текстуры. ```python bpy.ops.export_scene.gltf( filepath="//exports/unity/prop.glb", export_format='GLB', # GLB = один файл; GLTF_SEPARATE = .gltf + .bin + текстуры use_selection=True, export_apply=True, # применить модификаторы на лету export_yup=True, # glTF по спецификации Y-up export_materials='EXPORT', export_texcoords=True, export_normals=True, export_tangents=True, export_animations=True, export_frame_range=True, export_skins=True, ) ``` Сжатие Draco в glTF даёт сильно меньший файл, но требует поддержки на стороне импорта: в Unity и в Godot Draco воспринимается по-разному в зависимости от импортёра и версии. Если размер не критичен, держите сжатие выключенным — сэкономите много времени на отладке. ### Шаг 5. Текстуры: цвет против данных Цветовая текстура и карта данных — это разные миры, и путаница между ними даёт классические артефакты: слишком тёмное зерно, «вывернутые» блики, странные тени. | Blender | Тип в Unity | Что произойдёт при ошибке | |---|---|---| | Base Color | sRGB | корректно | | Metallic | Non-Color (Raw) | каналы пересчитаются, металл станет грязным | | Roughness | Non-Color | зеркальность «поплывёт» по тону | | Normal map (OpenGL) | Normal map | без флага Flip Y свет будет инвертирован по вертикали | | Ambient Occlusion | Non-Color | тени слишком тёмные | | Emission | sRGB | свечение темнее ожидаемого | Нормали из Blender используют соглашение OpenGL: ось Y вниз. Unity ожидает ось Y вверх. Если рельеф выглядит «вдавленным» вместо выпуклого — это переворот, и лечится флагом **Flip Green Channel** в импортёре, а не перевёрнутой текстурой в редакторе. ### Шаг 6. Метаданные, по которым Unity собирает префабы Unity не знает, что такое Blender-объект. Всё, что не выражено геометрией и именем, переносится через пользовательские свойства (custom properties) или через соглашение об именах. ```python import bpy # Соглашение об именах + метаданные, которые переживают экспорт. # Unity собирает префаб по префиксу и читает поля как строки. obj = bpy.data.objects["Barrel_Mesh"] obj["ucx_enabled"] = True # коллайдер: бокс по габаритам obj["ucx_width"] = 0.65 obj["ucx_depth"] = 0.45 obj["ucx_height"] = 0.95 obj["lod1_screen"] = 0.45 # порог переключения LOD в долях экрана obj["material_guide"] = "wood_painted" # Переименовать коллайдеры так, чтобы Unity их подхватил collider = bpy.data.objects.get("Col_Barrel") if collider: collider.name = "UCX_Barrel_00" ``` > [!NOTE] > Экспорт пользовательских свойств в FBX зависит от версии Blender и от флага > экспорта свойств объекта — в разных сборках он то включается, то требует > явного флага. Проверь, доезжают ли свойства, простым тестом: экспортируй один > объект, открой `.fbx` текстовым редактором и поищи своё имя. Если его нет — > не полагайся на этот канал и переходи на соглашение об именах. ### Шаг 7. Обратный путь: из Unity в Blender Импорт из Unity нужен, когда асет уехал в движок и вернулся с правками рига или формы. Шаги обратные, но ловушки те же. ```bash # Экспорт из Unity: в редакторе выделить модель, # Assets → Export → FBX..., выбрать, что включать в файл, и нажать Export. # Проверь, что пакет экспорта FBX установлен в твоей версии редактора. # Импорт результата в Blender blender -b --python-expr "import bpy; bpy.ops.import_scene.fbx(filepath='D:/exports/unity/Barrel.fbx', automatic_bone_orientation=True)" ``` Обратный импорт почти всегда приносит масштаб, отличный от ожидаемого, и переименованные кости. Перед записью обратно в исходный файл результат надо проверить и, если нужно, привести трансформации к тому же состоянию, что было до экспорта. ### Шаг 8. Пакетная выгрузка каталога ```python # scripts/export_for_unity.py import bpy, os, sys, argparse argv = sys.argv[sys.argv.index("--") + 1:] p = argparse.ArgumentParser() p.add_argument("--out", default="//exports/unity/") p.add_argument("--format", choices=["FBX", "GLB"], default="GLB") args = p.parse_args(argv) os.makedirs(bpy.path.abspath(args.out), exist_ok=True) for ob in [o for o in bpy.context.scene.objects if o.type == 'MESH']: bpy.ops.object.select_all(action='DESELECT') ob.select_set(True) bpy.context.view_layer.objects.active = ob path = os.path.join(bpy.path.abspath(args.out), ob.name + "." + args.format.lower()) if args.format == "FBX": bpy.ops.export_scene.fbx(filepath=path, use_selection=True, path_mode='COPY') else: bpy.ops.export_scene.gltf(filepath=path, export_format='GLB', use_selection=True, export_apply=True) print("exported:", path) ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `apply_unit_scale` | `True` | иначе Unity применит свой `Scale Factor` поверх | | `use_selection` | `True` в скриптах | в интерактивном режиме выбор делает пользователь | | `use_armature_deform_only` | `True` | кости управления нужны только если их импортируют обратно | | `path_mode` (FBX) | `COPY` | `RELATIVE` ломается при переносе проекта, `AUTO` ведёт себя по-разному в фоне | | `export_apply` (glTF) | `True` | применение модификаторов на лету вместо ручного | | `embed_textures` | `False` | `True` даёт один большой файл, который неудобно версионировать | | Flip Green Channel (Unity) | `True` для нормалей из Blender | выключать только если в проекте используется DirectX-конвенция | | Texture Type (Unity) | sRGB для Base Color, Non-Color для карт данных | ошибка здесь видна как «грязный» металл и проваленные тени | | масштаб объекта перед экспортом | строго `1.0` | применить один раз в скрипте, а не руками | | система координат вручную | не трогать | Unity сам приводит ориентацию при импорте | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Ассет в Unity в 100 раз больше | не применён масштаб объекта | `transform_apply(scale=True)` перед экспортом | | Ассет лежит на боку | ручной поворот сцены «под движок» поверх встроенной конвертации | вернуть объекты в исходную ориентацию, оставить `export_yup` | | Кадр анимации импортируется в позу «T» | `bake_anim` без кадра | указать кадр диапазона, проверить результат в редакторе | | Нормали вдавленные | соглашение OpenGL против Unity | Flip Green Channel в импортёре | | Металл выглядит грязным | карта Metallic импортирована как sRGB | сменить Texture Type на Normal/Single channel | | Текстуры потерялись при переносе папки | `path_mode='RELATIVE'` или `'AUTO'` | явно `path_mode='COPY'` | | Превью в Unity темнее, чем рендер | сцена без HDRI и без правильных light probes | это не ошибка экспорта; настрой освещение в движке | | Кастомные свойства не доехали | FBX-экспорт свойств зависит от версии | перейти на соглашение об именах и префиксы | | Ошибка `TypeError` на флаге экспорта | флаг из другой версии Blender | сверить список через `get_rna_type().properties` | ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 07: CI/CD INTEGRATION](setup_07_ci_cd.md), [SETUP 09: BLENDERBOT / AI CODING AGENT](setup_09_blenderbot_ai.md), [SETUP 10: BLENDER + COMPOSITING (NUKE/AFTER EFFECTS ROUNDTRIP)](setup_10_compositing_roundtrip.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 11: BLENDER + TOUCHDESIGNER REALTIME VISUALS](setup_11_touchdesigner.md), [SETUP 12: BLENDER + UNREAL ENGINE 5 LIVE LINK](setup_12_unreal_livelink.md) - **Теги:** #blender, #setup, #unity --- # SETUP 14: BLENDER + GODOT PIPELINE Tags: #blender #godot #pipeline #setup # SETUP 14: BLENDER + GODOT PIPELINE **ID:** SETUP-14 | **Type:** Open-source Engine | **Difficulty:** ⭐⭐ | **Time:** 20 min **RU/EN Bilingual** --- > Когда применять: когда ассеты из Blender едут в Godot — а именно туда чаще всего, потому что glTF у Godot единственный нативный формат импорта. ## 🎯 ЦЕЛЬ Собрать повторяемый путь `Blender → .glb → проект Godot`, на котором геометрия приезжает с правильным масштабом и нормалями, текстуры — с правильным цветовым пространством, а анимация и коллизии не требуют ручной доводки. Принцип простой: **glTF — это контракт, а не формат**. Всё, что не выражено в контракте (материал Godot, шейдер, физика, LOD-конфигурация), живёт в движке и настраивается уже там. Из Blender в Godot едет геометрия, UV, материал в формате glTF и анимация. Всё остальное — на стороне проекта. Вторая половина работы — вторая. Экспорт занимает минуту; настройка импорта в редакторе Godot (LOD, окклюзия, коллайдеры, сжатие текстур) занимает часы, и именно она определяет, сколько кадров в секунду получит проект. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия / состояние | Зачем | |---|---|---| | Blender | актуальная, со встроенным glTF-экспортёром | экспорт в GLB без плагинов | | Godot | 4.x | нативный импорт glTF, `MeshInstance3D`, `AnimationPlayer` | | Расширение текстур KTX2 / Basis | проверь, включено ли в проекте | в разы меньший VRAM, чем у PNG/JPEG | | Соглашение об именах | одна на проект | без неё коллайдеры и LOD не подхватятся автоматически | | Модуль Draco в Godot | встроенный | чтение геометрии со сжатием, если включишь его на экспорте | ## 🛠️ НАСТРОЙКА ### Шаг 1. Подготовка сцены перед экспортом ```python import bpy def prepare_for_godot(objects=None): objects = objects or [o for o in bpy.context.scene.objects if o.type == 'MESH'] bpy.ops.object.select_all(action='DESELECT') for o in objects: o.select_set(True) bpy.context.view_layer.objects.active = objects[0] # Единый масштаб: без этого Godot применит свой корректирующий коэффициент bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) for mod in list(o.modifiers): bpy.ops.object.modifier_apply(modifier=mod.name) # Перевести оси на Y-up заранее, если в проекте это принято: # так геометрия в .glb читается без сюрпризов o.rotation_mode = 'XYZ' prepare_for_godot() ``` ### Шаг 2. Экспорт GLB ```python bpy.ops.export_scene.gltf( filepath="//exports/godot/prop.glb", export_format='GLB', # один файл: геометрия + текстуры + анимация use_selection=True, export_apply=True, # применить модификаторы на лету export_yup=True, # glTF и Godot оба Y-up, конвертация не нужна export_materials='EXPORT', export_image_format='AUTO', # оставить исходный формат текстур export_texcoords=True, export_normals=True, export_tangents=True, # без тангентов не будет нормалей в шейдере export_animations=True, export_skins=True, export_morph=True, export_extras=True, # custom properties -> метаданные в Godot ) ``` Нормали здесь не требуют переворота: glTF и Blender используют одно соглашение, и Godot тоже. Если в проекте вручную делают «Flip Green Channel», почти всегда виноват не формат, а один вручную сохранённый Normal map в стороннем редакторе. > [!NOTE] > Параметр режима экспорта анимации (`export_animation_mode`) принимает разные > значения в разных версиях Blender — набор вариантов для NLA-дорожек, активного > экшена и всей сцены менялся. Если сомневаешься, оставь значение по умолчанию > и проверь результат: сколько клипов появилось в `AnimationPlayer` после > импорта. Список допустимых значений — через > `bpy.ops.export_scene.gltf.get_rna_type().properties`. ### Шаг 3. Сжатие: Draco и текстуры ```python # Геометрия со сжатием Draco: файл меньше в разы, импорт чуть медленнее bpy.ops.export_scene.gltf( filepath="//exports/godot/prop_draco.glb", export_format='GLB', export_draco_mesh_compression_enable=True, export_draco_mesh_compression_level=6, # выше = меньше файл и дольше упаковка ) ``` Godot читает сжатую геометрию, но профиль совместимости зависит от версии редактора и наличия модуля Draco. Если проект должен открываться в чужих сборках, разумнее держать несжатый экспорт, а сжатие включать точечно для дальних ассетов. Текстуры — отдельная история. PNG и JPEG распаковываются в VRAM как есть, и 4k-текстура весит там больше, чем на диске. Переключение проекта на импорт текстур KTX2 (внутри — Basis) даёт заметную экономию памяти, но качество при агрессивном сжатии страдает. | Источник в Blender | Что выбрать в Godot | Комментарий | |---|---|---| | PNG / JPEG, цвет | Lossless или Lossy, зависит от канала | цветовые карты сжимать нельзя | | Карта нормалей | Нормали сжаты отдельно, свой режим | сжатие карт нормалей даёт артефакты | | Roughness / Metallic / AO | Каналы упаковываются в один файл | экономит сэмплеров и памяти | > [!NOTE] > Точные названия опций импорта текстур в Godot 4 менялись между релизами, а часть > из них доступна только при включённом модуле KTX2. Список актуальных ключей > импорта смотри в панели Import напротив файла и в документации Godot > (`docs.godotengine.org`, раздел про импорт 3D-ассетов). Не переноси настройки > из старого проекта вслепую. ### Шаг 4. Материалы | Blender Principled | Godot `StandardMaterial3D` | Комментарий | |---|---|---| | Base Color | Albedo | совпадает напрямую | | Metallic | Metallic | совпадает напрямую | | Roughness | Roughness | совпадает напрямую | | Normal | Normal Map (OpenGL) | переворот не нужен | | Emission + Emission Strength | Emission / Emission Energy | в glTF интенсивность едет отдельным множителем | | Alpha | Transparency, порог и режим | в glTF альфа есть, но режим смешивания — решение Godot | | Transmission | не переносится | стекло придётся настраивать вручную | Главное расхождение: **Blender и glTF не описывают прозрачность как «режим»**. Материал с `Alpha < 1` приедет в Godot непрозрачным, пока в настройках материала вручную не выставлен режим прозрачности и отключена отсечка по глубине. Для стекла и витражей это отдельная ручная работа на каждый материал. ### Шаг 5. Импорт в Godot и проверка После копирования `.glb` в папку проекта Godot сам создаёт импортированный ресурс. Дальше — панель **Import** напротив файла: там задаются дополнительные параметры. ```bash # Принудительный переимпорт без открытия редактора (для CI-скриптов): godot --headless --path ./project --import # Точный набор флагов и порядок аргументов сверяй по `godot --help` # в своей версии: они отличаются между 3.x и 4.x. ``` Проверять после импорта стоит четыре вещи, и все четыре ловятся на первом кадре: 1. Масштаб — знакомый объект рядом с импортированным совпал по размеру. 2. Нормали — рельеф выпуклый, а не вдавленный; тени не «поплыли». 3. Материалы — количество материалов совпадает с числом в Blender, текстуры не серые. 4. Анимация — клипы появились в `AnimationPlayer` и проигрываются без рывков. ### Шаг 6. Коллизии через суффиксы имён Godot умеет брать коллайдеры из тех же файлов. Объект, названный `Barrel-convex_col`, станет выпуклым коллайдером для `Barrel`. ```python import bpy # Обвязать главный объект коллайдером, который Godot подхватит сам col = bpy.data.objects.get("Col_Barrel") if col: col.name = "Barrel-convex_col" # выпуклая оболочка, дёшево col.display_type = 'WIRE' col.hide_render = True # Трюк с LOD: имена с суффиксом -lodN читаются как уровни детализации for i, ratio in enumerate((1.0, 0.45, 0.15)): name = f"Barrel-lod{i}" if i else "Barrel" ob = bpy.data.objects.get(name) if ob: print(f"LOD{i}: {len(ob.data.polygons)} polys") ``` > [!NOTE] > Набор суффиксов имён, которые Godot распознаёт при импорте, шире, чем два > примера выше, и дополнялся между версиями. Полный актуальный список — в > документации Godot, раздел про соглашения имён при импорте сцен. Не > рассчитывай, что незнакомый суффикс просто проигнорируют: может вылететь > ошибка импорта. ### Шаг 7. LOD и окклюзия в движке Импортированная модель — это `MeshInstance3D`. LOD в Godot не декоративный: дальний объект действительно перестаёт отправляться на отрисовку. ```gdscript # scripts/lod_setup.gd — повесь на корневой узел импортированной сцены extends Node3D ## Дальность, с которой ближний уровень перестаёт быть виден @export var lod_begin: float = 60.0 ## Дальность, с которой исчезает самый дальний уровень @export var lod_end: float = 160.0 func _ready() -> void: # Уровни приходят в порядке имён: Barrel-lod0 самый близкий var levels: Array[MeshInstance3D] = [] for child in get_children(): if child is MeshInstance3D: levels.append(child) levels.sort_custom(func(a: MeshInstance3D, b: MeshInstance3D) -> bool: return a.name.length() < b.name.length()) for i in range(levels.size()): var mesh := levels[i] mesh.visibility_range_begin = 0.0 if i == 0 else lod_begin * float(i) mesh.visibility_range_end = lod_end mesh.visibility_range_fade_mode = GeometryInstance3D.VISIBILITY_RANGE_FADE_SELF print(mesh.name, ": ", mesh.mesh.get_faces().size() / 3, " tris") ``` > [!NOTE] > Имена полей `visibility_range_*` и значения перечисления режима затухания > (`VISIBILITY_RANGE_FADE_*`) берутся из класса `GeometryInstance3D` и дополнялись > между версиями Godot. Если редактор ругается на неизвестное поле — открой > документацию класса в своей сборке (`GeometryInstance3D` в поиске редактора > покажет реальный список свойств) и поправь имена. Отдельная тема — **окклюзия**. Дальний LOD не помогает, если объект всё равно проходит через frustum и отсечение по расстоянию. Реальную экономию дают `OccluderInstance3D` вокруг крупных непрозрачных форм: движок перестаёт отправлять на рендер всё, что за ними. ### Шаг 8. Материал и шейдер — только в Godot ```gdscript # scripts/one_material.gd — один материал на всё дерево вместо материалов на мешах extends Node3D ## Материал, который подставляется всем потомкам @export var shared_material: Material func _ready() -> void: for child in get_children(): if child is GeometryInstance3D: # material_override сильнее материалов, заданных на самом меше, # и рисуется одним вызовом на всё дерево child.material_override = shared_material ``` ### Шаг 9. Проверка `.glb` без движка GLB — обычный файл, его можно открыть в Blender обратно и проверить, не запуская Godot. Это дешёвый способ поймать сломанный экспорт до того, как файл уедет в репозиторий. ```python # scripts/check_glb.py — blender -b --python scripts/check_glb.py -- ./exports/godot/prop.glb import bpy, sys, os argv = sys.argv[sys.argv.index("--") + 1:] problems = [] for path in argv: if not os.path.exists(path): problems.append(f"missing file: {path}") continue bpy.ops.wm.read_factory_settings(use_empty=True) bpy.ops.import_scene.gltf(filepath=path) meshes = [o for o in bpy.context.scene.objects if o.type == 'MESH'] tris = sum(len(o.data.loop_triangles) for o in meshes) mats = sum(len(o.data.materials) for o in meshes) print(f"{path}: {len(meshes)} meshes, {tris} tris, {mats} material slots") if tris == 0: problems.append(f"empty geometry in {path}") if mats == 0: problems.append(f"no materials in {path}") if problems: for p in problems: print("ERROR:", p) sys.exit(1) print("check_glb: OK") ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---| | `export_format` | `GLB` для одного файла, `GLTF_SEPARATE` для инспекции | `GLTF_SEPARATE` удобен при отладке: видно, что лежит внутри | | `export_yup` | `True` | Godot и glTF оба Y-up, отключать не нужно | | `export_tangents` | `True` | без тангентов не будет корректных нормалей в шейдере | | `export_apply` | `True` | иначе модификаторы в `.glb` не попадут | | `export_extras` | `True` | переносятся пользовательские свойства в метаданные | | Draco | выключен по умолчанию | включать для дальних ассетов после проверки импорта | | текстуры | KTX2 / Basis | экономия VRAM; сжатые карты нормалей проверять отдельно | | суффикс `-convex_col` | для выпуклых коллайдеров | сложную геометрию коллайдера лучше делать в Godot вручную | | `visibility_range_begin/end` | по дистанции LOD | подбирается замером в реальной сцене, а не на глаз | | `material_override` | один материал на дерево | меньше вызовов отрисовки, но теряются материалы отдельных мешей | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Модель в 100 раз больше | не применён масштаб | `transform_apply(scale=True)` перед экспортом | | Материал серый | текстура не на месте в момент импорта | импортировать файл заново после копирования текстур | | Стекло выглядит как непрозрачный пластик | glTF не несёт режим прозрачности | выставить Transparency в `StandardMaterial3D` вручную | | Анимация не появилась | не совпал режим экспорта анимации | сверить число клипов с Blender, попробовать другой режим | | Нормали вдавленные | вручную переворачивали карту нормалей | вернуть OpenGL-конвенцию, убрать ручной флип | | Коллайдера нет в сцене | суффикс имени не распознан | сверить суффикс со списком соглашений имён Godot | | VRAM растёт быстрее, чем ожидалось | текстуры импортированы как PNG/JPEG | переключить импорт на KTX2 и переимпортировать проект | | LOD не переключается | границы заданы только на корневом узле | задать `visibility_range_*` на каждом `MeshInstance3D` | | Тормозит вдали от игрока | дальние объекты всё ещё в кадре | добавить окклюзию, а не только LOD | ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 45: HUNYUAN3D 2.1 LOCAL](setup_45_hunyuan3d_21.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 07: CI/CD INTEGRATION](setup_07_ci_cd.md), [SETUP 08: ASSET PIPELINE](setup_08_asset_pipeline.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [P39: EXPORT PRESETS (GLB/FBX/USD)](../prompts/39_export_presets.md), [P46: UDIM WORKFLOW](../prompts/46_udim_workflow.md) - **Теги:** #blender, #godot, #pipeline, #setup --- # SETUP 15: BLENDER + COMFYUI SDXL Tags: #blender #setup #stable-diffusion # SETUP 15: BLENDER + COMFYUI SDXL **ID:** SETUP-15 | **Type:** AI Texturing | **Difficulty:** ⭐⭐ | **Time:** 45-75 min **RU/EN Bilingual** > Когда применять: когда текстуры нужны быстро, вариативно и в большом количестве — blackbase, concept art, заполнение невидимых поверхностей. Для финальных PBR-карт нужен отдельный пайплайн. ## 🎯 ЦЕЛЬ Поднять ComfyUI локально как HTTP-сервис генерации картинок, научить его принимать карту глубины из Blender как управляющий сигнал (ControlNet) и забирать результат обратно в Blender как текстуру. На выходе — скрипт, который по кнопке кладёт сгенерированное изображение в Image Editor поверх материала. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |-----------|--------|-------| | ComfyUI | текущий релиз | локальный генератор с HTTP API и очереди | | Python | 3.10–3.12 (проверь `README` ComfyUI для своей версии) | рантайм ComfyUI | | SDXL base | `sd_xl_base_1.0.safetensors` (~7 ГБ) | базовая модель, нативное разрешение 1024×1024 | | SDXL refiner | `sd_xl_refiner_1.0.safetensors` (опционально) | доработка деталей на втором проходе | | ControlNet для SDXL | например Canny/Depth варианты под SDXL | привязка генерации к геометрии из Blender | | GPU | 8 ГБ VRAM минимум для SDXL в fp16; 12 ГБ комфортнее | инференс | | Blender | 4.2 LTS+ | источник depth/normal map и приём результата | > [!NOTE] > Точный список моделей, который лежит в `models/controlnet/`, и их имена файлов меняются с выходом новых моделей. Смотри вкладку Files на huggingface.co у конкретной модели и папку `ComfyUI/models/controlnet/` — ComfyUI показывает в списке имя файла, а не «человеческое» имя модели. ## 🛠️ НАСТРОЙКА ### 1. Установить ComfyUI ```bash # Вариант на Windows без возни с окружением: portable-сборка с NVIDIA # качай с github.com/comfyanonymous/ComfyUI/releases, запускай run_nvidia_gpu.bat # Вариант из исходников (работает везде, где есть CUDA/ROCm/Metal) git clone https://github.com/comfyanonymous/ComfyUI cd ComfyUI python -m pip install -r requirements.txt # Старт сервера. Порт по умолчанию 8188, адрес по умолчанию только localhost python main.py # Полезные флаги запуска: # --listen 0.0.0.0 открыть наружу (осторожно: это открытый генератор) # --port 8189 другой порт, если 8188 занят # --disable-auto-launch не открывать браузер при старте ``` Проверь, что сервис жив, до того как писать скрипты: ```bash # Список нод, которые сервер реально знает — источник правды о версии curl -s http://127.0.0.1:8188/object_info | python -c "import json,sys; print(len(json.load(sys.stdin)), 'node types')" # Статистика: видно, какой backend (GPU/CPU) подхватился curl -s http://127.0.0.1:8188/system_stats ``` ### 2. Положить модели ``` ComfyUI/models/ ├── checkpoints/ sd_xl_base_1.0.safetensors ├── controlnet/ controlnet-canny-sdxl-1.0.safetensors │ controlnet-depth-sdxl-1.0.safetensors └── vae/ (необязательно, только если вынес VAE из модели) ``` Собственный VAE для SDXL имеет смысл выносить: fp16-вариант VAE от SDXL даёт чёрный NaN-шум, и в этом виновата не картинка, а переполнение в полуточном формате. ### 3. Отдать из Blender карту глубины ControlNet в ComfyUI ждёт **обычное 8/16-битное изображение**, а не EXR. Поэтому глубину нормализуем в 0..1 и сохраняем в PNG. ```python # blender_depth_pass.py — Blender: Text Editor → Run Script import bpy, os scene = bpy.context.scene out_dir = os.path.join(os.path.expanduser("~"), "comfy_in") os.makedirs(out_dir, exist_ok=True) # 1) Включаем слои рендера, которые нам нужны vl = scene.view_layers[0] vl.use_pass_z = True # Depth vl.use_pass_normal = True # Normal (пригодится для canny/lineart-пайплайнов) # 2) Рендерим один кадр во внетактовый EXR — там глубина линейная и 32-битная scene.render.image_settings.file_format = 'OPEN_EXR' scene.render.image_settings.color_depth = '32' scene.render.filepath = os.path.join(out_dir, "depth.exr") bpy.ops.render.render(write_still=True) # 3) Нормализуем EXR в PNG через Compositor: Color Ramp растягивает # линейную глубину на 0..1, Save Image Operator пишет 8-битный PNG scene.use_nodes = True tree = scene.node_tree tree.nodes.clear() rl = tree.nodes.new("CompositorNodeRLayers") ramp = tree.nodes.new("CompositorNodeColorRamp") # Подставь свои near/far: near должен быть чуть ближе ближайшей геометрии, # far — чуть дальше самой дальней, иначе весь кадр уедет в один конец градиента ramp.color_ramp.elements[0].position = 0.0 ramp.color_ramp.elements[1].position = 1.0 ramp.color_ramp.interpolation = 'EASE' save = tree.nodes.new("CompositorNodeOutputFile") save.base_path = out_dir save.format.file_format = 'PNG' save.format.color_mode = 'BW' save.file_slots[0].path = "depth_norm" tree.links.new(rl.outputs["Depth"], ramp.inputs["Fac"]) tree.links.new(ramp.outputs["Color"], save.inputs[0]) bpy.ops.render.render(write_still=True) print("Готово:", out_dir) # файл будет depth_norm0001.png — с номером кадра ``` ### 4. Отправить workflow в ComfyUI Граф — это обычный JSON: ключ словаря = id ноды, `inputs` ссылаются на выходы других нод как `[id_ноды, индекс_выхода]`. ```json { "3": { "class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": "sd_xl_base_1.0.safetensors"} }, "5": { "class_type": "EmptyLatentImage", "inputs": {"width": 1024, "height": 1024, "batch_size": 1} }, "6": { "class_type": "CLIPTextEncode", "inputs": {"text": "weathered concrete wall, seamless, top-down lighting", "clip": ["3", 1]} }, "7": { "class_type": "CLIPTextEncode", "inputs": {"text": "blurry, watermark, text, frame", "clip": ["3", 1]} }, "4": { "class_type": "KSampler", "inputs": { "seed": 0, "steps": 30, "cfg": 7.0, "sampler_name": "dpmpp_2m", "scheduler": "karras", "denoise": 1.0, "model": ["3", 0], "positive": ["6", 0], "negative": ["7", 0], "latent_image": ["5", 0] } }, "8": { "class_type": "VAEDecode", "inputs": {"samples": ["4", 0], "vae": ["3", 2]} }, "9": { "class_type": "SaveImage", "inputs": {"filename_prefix": "blender/base", "images": ["8", 0]} } } ``` > [!NOTE] > `class_type` — это идентификаторы нод из конкретной сборки ComfyUI. В ванильном это `CheckpointLoaderSimple` / `EmptyLatentImage` / `KSampler` / `VAEDecode` / `SaveImage`, но любой установленный custom-пакет может их переопределить. Сверяйся с выводом `/object_info` — это единственный источник правды о том, что сервер действительно умеет. ### 5. Полный цикл из Blender: submit → poll → забрать в материал ```python # blender_comfy_bridge.py — запускать из Blender import bpy, json, os, time, uuid, urllib.request, urllib.parse from pathlib import Path COMFY = "http://127.0.0.1:8188" IN_DIR = Path.home() / "comfy_in" OUT_DIR = Path.home() / "comfy_out" OUT_DIR.mkdir(parents=True, exist_ok=True) def _get(url): with urllib.request.urlopen(url, timeout=30) as r: return r.read() def _post_json(url, payload): req = urllib.request.Request( url, data=json.dumps(payload).encode("utf-8"), headers={"Content-Type": "application/json"}, ) with urllib.request.urlopen(req, timeout=30) as r: return json.loads(r.read()) def upload_image(path: Path) -> str: """Отдаём карту глубины серверу. Возвращает имя файла, как его видит ComfyUI.""" boundary = uuid.uuid4().hex name = path.name body = b"".join([ f"--{boundary}\r\nContent-Disposition: form-data; name=\"image\"; filename=\"{name}\"\r\n".encode(), b"Content-Type: image/png\r\n\r\n", path.read_bytes(), b"\r\n", f"--{boundary}\r\nContent-Disposition: form-data; name=\"overwrite\"\r\n\r\ntrue\r\n".encode(), f"--{boundary}--\r\n".encode(), ]) req = urllib.request.Request( COMFY + "/upload/image", data=body, headers={"Content-Type": f"multipart/form-data; boundary={boundary}"}, ) with urllib.request.urlopen(req, timeout=60) as r: info = json.loads(r.read()) return info["name"] + ((" [input]" + info.get("subfolder", "")) if info.get("subfolder") else "") def submit(graph: dict) -> str: r = _post_json(COMFY + "/prompt", {"prompt": graph, "client_id": "blender"}) if r.get("node_errors"): raise RuntimeError("ComfyUI отклонил граф: " + json.dumps(r["node_errors"])[:400]) return r["prompt_id"] def wait_done(prompt_id: str, timeout=600): """Поллинг /history вместо WebSocket — проще, а для батча разницы нет.""" t0 = time.time() while time.time() - t0 < timeout: hist = json.loads(_get(f"{COMFY}/history/{prompt_id}")) if prompt_id in hist: return hist[prompt_id] time.sleep(1.0) raise TimeoutError("ComfyUI не закончил за отведённое время") def fetch_output(node_out: dict): """Скачиваем первый файл из outputs SaveImage-ноды и кладём его на диск.""" img = node_out["images"][0] q = urllib.parse.urlencode({ "filename": img["filename"], "subfolder": img.get("subfolder", ""), "type": img["type"], }) dest = OUT_DIR / img["filename"] dest.write_bytes(_get(f"{COMFY}/view?{q}")) return dest def load_into_material(png_path: Path, mat_name="M_AI_BaseColor"): """Заменяем текстуру в существующем материале — не создаём новый.""" mat = bpy.data.materials.get(mat_name) if not mat or not mat.use_nodes: print("Материал не найден или без нод:", mat_name); return tree = mat.node_tree tex = next((n for n in tree.nodes if n.type == 'TEX_IMAGE'), None) if not tex: print("В материале нет Image Texture"); return if tex.image is None: tex.image = bpy.data.images.load(str(png_path), check_existing=False) else: tex.image.filepath = str(png_path) tex.image.reload() # Генерированная картинка в sRGB, и её нужно интерпретировать как sRGB tex.image.colorspace_settings.name = 'sRGB' print("Текстура обновлена:", png_path.name) ``` ### 6. ControlNet: привязка к геометрии Для того чтобы картинка легла на конкретную форму, в граф добавляются три ноды: загрузчик ControlNet, применение (условное и безусловное) и правильная подмена входа. У SDXL ControlNet применяется **дважды** — к positive и к negative, иначе результат «плывёт». ```text ControlNetLoader → ControlNetApplyAdvanced (positive) ─┐ ControlNetLoader → ControlNetApplyAdvanced (negative) ─┤→ KSampler LoadImage(depth_norm0001.png) → image ───────────────┘ ``` Практические значения: `strength` 0.4–0.8 для depth (выше — начинает копировать артефакты карты), `start_percent` 0.0–0.4, `end_percent` 0.8–1.0. Для canny `strength` обычно 0.3–0.6. > [!NOTE] > Имена входов у `ControlNetApplyAdvanced` (`image`, `strength`, `start_percent`, `end_percent`) и способ подмены входов у `KSampler` в разных сборках различаются: в некоторых виртуальных нодах `ControlNet` подключается отдельной нодой `SetLatentNoiseMask`/`LatentComposite`, в других — через `control_net_name` в `KSampler`. Смотри JSON, который ComfyUI сохраняет при сохранении графа в UI: это точная схема твоей сборки. ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```bash # Очередь: посмотреть, что в работе и что ждёт curl -s http://127.0.0.1:8188/queue # Прервать текущую генерацию (из Blender — когда пользователь нажал Esc) curl -s -X POST http://127.0.0.1:8188/interrupt # Сбросить очередь целиком curl -s -X POST http://127.0.0.1:8188/queue -H "Content-Type: application/json" -d '{"clear": true}' ``` | Параметр | Значение | Когда менять | |----------|----------|--------------| | `width` / `height` | 1024×1024 для SDXL | ниже 768 качество падает, выше — растёт время и VRAM | | `steps` | 25–35 | меньше 20 — каша, больше 50 — почти без выигрыша | | `cfg` | 5.5–8 | выше 10 текст превращается в мешанину | | `sampler_name` / `scheduler` | `dpmpp_2m` + `karras` | рабочая связка по умолчанию, `euler`/`dpmpp_sdekarras` для другой стилистики | | `denoise` | 1.0 для txt2img, 0.3–0.6 для img2img | ниже 0.3 результат слишком близок к исходнику | | `seed` | любое целое, `-1` = случайный | **фиксируй seed**: без него невозможно воспроизвести удачный результат | | ControlNet `strength` | 0.4–0.8 (depth) | понижай, если на картинке видны артефакты геометрии | | `filename_prefix` | `blender/base` | по нему ты находишь результат в `ComfyUI/output/` | ### Про «PBR из ComfyUI» Стоит сказать прямо: **ComfyUI не выдаёт корректный PBR-набор**. `basecolor` получается осмысленно, `roughness`/`metallic`/`normal` — нет. Варианты, которые реально работают: - **basecolor + вручную**: генерация цвета, roughness/height из процедурных нод Blender или из базовых карт с запеканием в Blender; - **кастом-пайплайн**: готовые наборы нод (normal/roughness из картинки через depth→normal) — они сторонние, имена нод надо брать из `/object_info`; - **gbexr/texture-экспорт из Substance Designer**, а не из ComfyUI — см. SETUP 19. ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |--------|---------|---------| | `Connection refused` при `urlopen` | ComfyUI не запущен или слушает не на 8188 | Проверь `python main.py` в консоли и флаг `--port` | | `HTTP 400` на `/prompt` | JSON графа невалиден или нет такого файла модели | Тело ответа содержит `node_errors` — печатай его целиком, а не первые 400 символов | | Картинка с чёрными квадратами / NaN-шумом | fp16 VAE, вынесенный из SDXL без фикса | Поставь fp32 VAE или убери отдельный VAE совсем | | ControlNet ничего не делает | Подключён только к positive, либо `strength` слишком низкий | Примени к обоим conditioner'ам, подними `strength` до 0.6 для проверки | | Результат «не налезает» на форму | Карта глубины пустая или инвертированная | Проверь, что `Depth` идёт в Color Ramp с монотонным ростом, а не инверсией | | Текстура в Blender не обновилась | Image datablock остался с кэшем | `img.reload()`, плюс `img.filepath = str(path)` перед reload | | Текстура в Blender темнее, чем на картинке | colorspace стоит `Non-Color` | Для basecolor должно быть `sRGB`; `Non-Color` — только для normal/roughness/height | | OOM на 8 ГБ VRAM | SDXL целиком + ControlNet | Понизь batch_size до 1, отключи превью в UI ComfyUI, используй fp16-веса | | Очередь растёт, интерфейс встал | Клиент шлёт запросы без ограничения rate | Перед `submit` проверяй `/queue`; в UI у ComfyUI есть лимит на размер очереди | ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 16: BLENDER + AUTOMATIC1111 API](setup_16_automatic1111_api.md), [SETUP 35: READY PLAYER ME AVATARS](setup_35_readyplayerme.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 07: CI/CD INTEGRATION](setup_07_ci_cd.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 09: BLENDERBOT / AI CODING AGENT](setup_09_blenderbot_ai.md), [SETUP 10: BLENDER + COMPOSITING (NUKE/AFTER EFFECTS ROUNDTRIP)](setup_10_compositing_roundtrip.md) - **Теги:** #blender, #setup, #stable-diffusion --- # SETUP 16: BLENDER + AUTOMATIC1111 API Tags: #api #blender #setup #stable-diffusion # SETUP 16: BLENDER + AUTOMATIC1111 API **ID:** SETUP-16 | **Type:** AI Texturing | **Difficulty:** ⭐⭐ | **Time:** 30-45 min **RU/EN Bilingual** > Когда применять: когда в проекте уже есть A1111, нужна одна картинка здесь и там, а node-график ComfyUI избыточен. API A1111 — самый простой способ дёрнуть генерацию из Blender-скрипта. ## 🎯 ЦЕЛЬ Поднять A1111 в режиме HTTP API и написать на Python клиента, который: делает txt2img по параметрам, гонит img2img по картинке из Blender, шлёт inpaint по маске и кладёт результат в Image Texture ноду. На выходе — один модуль с тремя функциями, который можно звать из любого хендлера Blender. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |-----------|--------|-------| | Stable Diffusion web UI (A1111) | 1.7.x / 1.10.x — ветка v1 | генератор; API-эндпоинты живут именно в v1, в v2 (Forge/развилки) пути меняются | | Python-пакеты из `requirements.txt` A1111 | из репозитория | torch и зависимости | | Модель-чекпойнт | SD 1.5 ~2 ГБ или SDXL ~7 ГБ | сам генератор; SDXL требует больше VRAM | | Blender | 4.2 LTS+ | источник картинки/маски и приём результата | | HTTP-клиент | стандартная библиотека (`urllib`) | внешние библиотеки не нужны, достаточно стандартного модуля | > [!NOTE] > Если ты на **v2/SD.Next или Forge**, не переноси флаги из этой статьи один в один: путь эндпоинтов, ключи запросов и часть флагов запуска там другие. Проверь свой `launch.py --help` и, если есть, `/sdapi/v1/options` — самый дешёвый способ убедиться, что API поднялся. ## 🛠️ НАСТРОЙКА ### 1. Запустить A1111 с API ```bash # Вариант "в один файл" (portable-сборка): допиши флаги в webui-user.bat # .\webui-user.bat --api --port 7860 # Обычная установка: python launch.py --api --port 7860 # Полезные флаги рядом с --api # --ckpt D:\models\sd15\v1-5-pruned.safetensors свой чекпойнт без копирования в models/Stable-diffusion # --vae D:\models\sd15\vae-ft-mse.safetensors отдельный VAE # --xformers меньше VRAM, быстрее (нужен xformers в окружении) # --api-auth user:pass базовая аутентификация для API # --ckpt-safe-dir ... путь к моделям с пробелами/кириллицей # Проверка: сервер должен подняться на 127.0.0.1:7860 curl -s http://127.0.0.1:7860/sdapi/v1/sd-models curl -s http://127.0.0.1:7860/sdapi/v1/samplers | python -c "import json,sys; print(json.load(sys.stdin)[:8])" ``` > [!NOTE] > Наличие флага `--api-auth` и его точное написание проверь в своей сборке: `python launch.py --help`. Логика та же — A1111 начинает требовать Basic-аутентификацию для API, и клиент должен слать заголовок `Authorization`. ### 2. Минимальный клиент: транспорт ```python # a1111_client.py — положи в Text Editor Blender или в свой пакет import base64, json, urllib.request, urllib.error BASE = "http://127.0.0.1:7860" AUTH = None # ("user", "pass") если запускал с --api-auth def _url(path: str) -> str: return BASE + "/sdapi/v1/" + path.lstrip("/") def api(path: str, payload: dict | None = None, method: str | None = None): """Единая точка входа. GET, если payload не передан, иначе POST с JSON.""" headers = {"Content-Type": "application/json"} if AUTH: token = base64.b64encode(f"{AUTH[0]}:{AUTH[1]}".encode()).decode() headers["Authorization"] = "Basic " + token data = json.dumps(payload).encode("utf-8") if payload is not None else None m = method or ("POST" if data else "GET") req = urllib.request.Request(_url(path), data=data, headers=headers, method=m) with urllib.request.urlopen(req, timeout=300) as r: # таймаут большой: генерация бывает долгой return json.loads(r.read()) def save_first(result: dict, path: str): """Ответ приходит как {"images": ["", ...]} — это PNG.""" if not result.get("images"): raise RuntimeError("A1111 вернул пустой images: " + str(result)[:300]) raw = base64.b64decode(result["images"][0]) with open(path, "wb") as f: f.write(raw) return path ``` ### 3. txt2img с параметрами, которые реально что-то значат ```python def txt2img(prompt, negative="lowres, watermark, text", steps=28, cfg=7.0, width=768, height=768, seed=-1, sampler="DPM++ 2M Karras", ckpt=None): payload = { "prompt": prompt, "negative_prompt": negative, "steps": steps, "cfg_scale": cfg, "sampler_name": sampler, # точное имя бери из /sdapi/v1/samplers "width": width, "height": height, "seed": seed, # -1 = случайный; фиксируй, если нужен повтор "batch_size": 1, "n_iter": 1, "tiling": True, # seamless-тайлинг: чекбокс «Tiling» во вкладке txt2img "save_images": False, "send_images": True, "override_settings": {}, # сюда кладём смену модели/сэмплера "override_filter_rgba": True, } if ckpt: payload["override_settings"]["sd_model_checkpoint"] = ckpt return api("txt2img", payload) # Пример: три варианта одной стены с разным зерном for i in range(3): r = txt2img("weathered concrete wall, seamless, top-down", seed=1000 + i) save_first(r, rf"C:\tmp\wall_{i}.png") print(r["parameters"]["seed"], "→", r["info"].splitlines()[0]) ``` ### 4. img2img: картинка из Blender уходит в A1111 Данные в `img2img` передаются как base64-PNG, поэтому из Blender мы сначала пишем временный файл и читаем его байты — `bpy.types.Image.pixels` отдаёт float-массив, а не PNG. ```python import bpy, tempfile, os, base64 def _image_to_png_b64(img: bpy.types.Image) -> str: tmp = os.path.join(tempfile.gettempdir(), "_a1111_src.png") # save_render применяет color management и пишет валидный PNG img.save_render(filepath=tmp) with open(tmp, "rb") as f: return base64.b64encode(f.read()).decode() def img2img(img: bpy.types.Image, prompt, strength=0.55, steps=30, cfg=7.0, seed=-1): payload = { "init_images": [_image_to_png_b64(img)], # список, не строка "prompt": prompt, "negative_prompt": "blurry", "denoising_strength": strength, # главный параметр img2img "steps": steps, "cfg_scale": cfg, "sampler_name": "DPM++ 2M Karras", "seed": seed, "width": img.size[0], "height": img.size[1], "tiling": True, "send_images": True, } return api("img2img", payload) # Пример: взять Color из материала и «перерисовать» его в basecolor mat = bpy.data.materials.get("M_Wall") tex = next(n for n in mat.node_tree.nodes if n.type == 'TEX_IMAGE') res = img2img(tex.image, "seamless concrete, flat lighting, no shadows", strength=0.45) save_first(res, os.path.join(bpy.app.tempdir, "wall_ai.png")) ``` ### 5. inpaint по маске из Blender Маска — это картинка того же размера, где белым чинить, чёрным оставить. `inpainting_fill_in`: 0 — сохранять фон, 1 — рисовать фон, 2 — не трогать. ```python def inpaint(src: bpy.types.Image, mask: bpy.types.Image, prompt, mask_blur=4, full_res=True, padding=32, fill_in=1): payload = { "init_images": [_image_to_png_b64(src)], "mask": _image_to_png_b64(mask), "prompt": prompt, "negative_prompt": "", "denoising_strength": 0.75, "inpainting_mask_invert": False, # False = белое в маске чинится "mask_blur": mask_blur, "inpainting_full_res": full_res, "inpainting_full_res_padding": padding, "inpainting_fill_in": fill_in, "steps": 30, "cfg_scale": 7.5, "sampler_name": "DPM++ 2M Karras", "send_images": True, } return api("inpaint", payload) # Маску в Blender рисуют кистью по Render Result / Image Editor и сохраняют PNG; # из Blender она уходит тем же save_render-путём, что и исходник. ``` ### 6. Обратный ход: достать параметры из чужой картинки Если картинка уже отрисована и вшитые в неё параметры важны (для батча, для отката, для документации), `png-info` разбирает метаданные обратно в словарь. ```python def read_png_info(png_path: str) -> dict: with open(png_path, "rb") as f: b64 = base64.b64encode(f.read()).decode() info = api("png-info", {"image": b64}) # {"info": "job: ...\nStep: 30, Sampler: ..., CFG scale: 7, Seed: 123, ...", # "json": {...}, "items": {...}} return info.get("json") or info.get("items") or {} meta = read_png_info(r"C:\tmp\wall_0.png") print("seed:", meta.get("Seed"), "| model:", (meta.get("Model") or "")[:40]) ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```bash # Список моделей, доступных серверу (нужно для override_settings) curl -s http://127.0.0.1:7860/sdapi/v1/sd-models # Точное имя сэмплера — строкой нужно то, что вернул сервер, а не то что в UI curl -s http://127.0.0.1:7860/sdapi/v1/samplers | python -c "import json,sys;[print(s) for s in json.load(sys.stdin)]" # Список встроенных VAE curl -s http://127.0.0.1:7860/sdapi/v1/sd-vae # Текущие опции (можно поменять через POST /options, например переключить UI-тему) curl -s http://127.0.0.1:7860/sdapi/v1/options | python -c "import json,sys;d=json.load(sys.stdin);print(d['sd_model_checkpoint'], d['show_progress_in_title'])" ``` | Параметр | Значение | Когда менять | |----------|----------|--------------| | `denoising_strength` | 0.3–0.5 для img2img, 0.7–0.9 для inpaint | выше 0.9 картинка уходит от исходника полностью | | `cfg_scale` | 4–9 для SD 1.5, 5–8 для SDXL | выше 12 — артефакты и «пережаренный» результат | | `steps` | 25–35 | 20 достаточно для черновика, 40+ для финальной | | `sampler_name` | значение из `/sdapi/v1/samplers` | никогда не пиши руками — строки менялись между версиями | | `seed` | `-1` = случайно | фиксируй для воспроизводимого батча | | `mask_blur` | 2–8 | слишком большое размытие съедает детали на границе патча | | `inpainting_full_res_padding` | 16–64 | поднимай, если по краю маски видно ореол от перехода | | `tiling` | `True` | для бесшовных текстур; для одиночной картинки выключай | | `override_settings` | `{"sd_model_checkpoint": ...}` | удобнее, чем переключать чекпойнт в UI перед каждой генерацией | ### Очередь и отмена A1111 обрабатывает запросы последовательно, поэтому клиент должен быть терпеливым и уметь отмену: ```python import urllib.error def interrupt(): """Остановить текущую генерацию — например, по Escape в Blender. Внимание: этот обработчик живёт в корне сервера, а не под /sdapi/v1, поэтому здесь BASE собирается вручную.""" with urllib.request.urlopen(BASE + "/interrupt", timeout=5) as r: return r.status # Обработка занятого/ошибочного сервера try: r = txt2img("test") except urllib.error.HTTPError as e: body = e.read().decode("utf-8", "replace") if e.code == 400: print("Сервер отверг запрос — скорее всего неверное имя сэмплера или модели:\n", body[:400]) else: print(e.code, body[:400]) ``` > [!NOTE] > Путь отмены генерации (`/interrupt`) — единственный, который лежит не под `/sdapi/v1`, и он менялся между версиями A1111. Если получаешь 404, посмотри вкладку Network в браузере, нажав Stop в UI: там будет точный путь, который зовёт сама страница. ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |--------|---------|---------| | `404` на любой `/sdapi/v1/...` | Запущено без `--api` | Дописать `--api` и перезапустить | | `HTTP 401` | Включён `--api-auth`, клиент не шлёт `Authorization` | Добавь Basic-заголовок в `api()` | | `HTTP 400` и текст про `Invalid sampler_name` | Сэмплер назван как в UI, а не как в API | Возьми точную строку из `/sdapi/v1/samplers` | | `Connection refused` | Сервер на другом порту или не запущен | Проверь флаг `--port`; по умолчанию 7860 | | Ответ приходит через несколько минут, клиент падает | Таймаут `urlopen` меньше времени генерации | Подними таймаут (в коде 300 с) | | Картинка в Blender темнее/светлее оригинала | `save_render` применил view transform повторно | Читай PNG-файл и грузи через `bpy.data.images.load()`, а не конвертируй float-пиксели | | Маска инвертирована: чинится ровно то, что не нужно | `inpainting_mask_invert=True` | Поставь `False` и убери альфу из маски | | Текстура обновилась, но в рендере старая | Image datablock не перечитал файл | `img.filepath = path; img.reload()`, затем `img.update()` | | В логе A1111 «Bad request» без деталей | Ошибка валится в stderr GUI, а не в HTTP-ответ | Смотри консоль, где запущен `launch.py` | | Батч из 20 картинок занимает полчаса и кажется «зависшим» | Очередь обрабатывается последовательно | Отправляй по одной и делай `interrupt()` для отмены остатка | ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 15: BLENDER + COMFYUI SDXL](setup_15_comfyui_sdxl.md), [SETUP 35: READY PLAYER ME AVATARS](setup_35_readyplayerme.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 07: CI/CD INTEGRATION](setup_07_ci_cd.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 09: BLENDERBOT / AI CODING AGENT](setup_09_blenderbot_ai.md), [SETUP 10: BLENDER + COMPOSITING (NUKE/AFTER EFFECTS ROUNDTRIP)](setup_10_compositing_roundtrip.md) - **Теги:** #api, #blender, #setup, #stable-diffusion --- # SETUP 17: BLENDER + KRITA AI DIFFUSION Tags: #blender #setup # SETUP 17: BLENDER + KRITA AI DIFFUSION **ID:** SETUP-17 | **Type:** 2D/3D Paint | **Difficulty:** ⭐ | **Time:** 30-45 min **RU/EN Bilingual** > Когда применять: когда AI-генерация нужна не «весь материал», а конкретный кусок — замазать дыру в текстуре, дорисовать латунную накладку, развернуть бесшовный участок. Blender тут нужен только как владелец текстуры, а рисовать удобнее в 2D. ## 🎯 ЦЕЛЬ Собрать связку из трёх ролей: **Krita** — маски, подчистка и ручная дорисовка; **ComfyUI** — генерация (его поднимает плагин AI Diffusion); **Blender** — владелец конечной текстуры и место, где всё собирается в материал. На выходе — кнопка «почини вот здесь», после которой текстура в Blender обновилась сама. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |-----------|--------|-------| | Krita | 5.x (плагин рассчитан на современные сборки) | 2D-редактор с масками и слоями | | Плагин **Krita AI Diffusion** | текущий релиз | генерация прямо в Krita | | ComfyUI | см. SETUP 15 | бэкенд генерации, поднимается плагином или ставится вручную | | Модель и (для inpaint/outpaint) inpainted-модель | SDXL или SD 1.5 inpainting | качество inpaint у обычной модели заметно ниже | | Blender | 4.2 LTS+ | приём результата и финальный материал | | Общая папка-обменник на диске | любая, лучше ASCII-путь | канал между приложениями | > [!NOTE] > Требования плагина к версии Krita и Python менялись с каждым релизом, а часть сборок приходит без работающего Python-плагинов. Проверяй совместимость по странице/репозиторию плагина перед установкой: несовместимость проявляется так, что панель AI Diffusion просто не появляется в списке докеров. ## 🛠️ НАСТРОЙКА ### 1. Поставить ComfyUI Плагин умеет ставить ComfyUI сам (кнопка в настройках), но надёжнее сделать это руками — тогда же и версию выбираешь ты: ```bash git clone https://github.com/comfyanonymous/ComfyUI cd ComfyUI python -m pip install -r requirements.txt python main.py --port 8188 ``` ### 2. Установить плагин в Krita Проще всего через встроенную установку: Krita умеет ставить плагины из репозитория, и это безопаснее ручного копирования. ```text Krita → Settings → Manage Python Plugins → Install from URL… → вставь ссылку на репозиторий плагина (адрес бери из его README) Если URL-установщика недоступен — вручную: 1. Скачай zip плагина (страница Releases) 2. Распакуй так, чтобы получилось: /plug-ins/krita_ai_diffusion/ (в Windows это %APPDATA%\krita\5.x\scripts\plug-ins\) 3. Перезапусти Krita 4. Window → Dockers → найди «AI Diffusion» ``` > [!NOTE] > Точный путь к папке `plug-ins` зависит от того, portable это Krita или установленная в систему, и от номера версии в пути. Не выдумывай его — посмотри `Settings → Krita → System Resources`, там указан реальный каталог ресурсов, и клади плагин рядом с существующими папками плагинов. ### 3. Связать плагин с ComfyUI и скачать модели > [!NOTE] > Плагин не просто рисует картинку — он поднимает собственный небольшой сервер и уже через него говорит с ComfyUI. Отсюда два практических следствия: порт плагина (по умолчанию `127.0.0.1:8008`) не должен совпадать с 8188, который занят ComfyUI; и после перезапуска ComfyUI плагин переподключают кнопкой в настройках либо перезапуском Krita. Точный порт смотри в `AI Diffusion → Settings`, а не по памяти. ```text AI Diffusion → Settings: • Backend → ComfyUI • ComfyUI server URL → http://127.0.0.1:8188 • (кнопка Test/Start server) — проверка связи; без неё плагин молча не работает • Resources → выбрать base-модель и, если нужен inpaint, inpainted-модель и скачать их прямо из плагина ``` Модель — это `.safetensors`, который кладётся в `ComfyUI/models/checkpoints/`. Плагин качает их сам, но знает не все модели: если твоей нет в списке, скачай вручную и перезапусти ComfyUI. ### 4. Раскладка работы: кто что делает ```text Blender Krita ComfyUI ──────── ───── ─────── Export Texture ▸ base.png ──▶ открыть как слой нарисовать маску (белое = чинить) Export ▸ mask.png ──────────▶ Save Layer/Selection as PNG AI Diffusion → Inpaint, prompt результат появляется слоем Save Layer ▸ out.png ◀── Reload Texture ─── out.png на диске ◀───────── SaveImage ``` Маска из Krita и промпт — единственное, что реально нужно от 2D-редактора. Всё остальное (цвет, стиль, seamless) разумнее отдавать генератору. ### 5. Blender: текстура входит в материал ```python # blender_krita_bridge.py — импорт текстуры и стандартный PBR-сетап import bpy, os TEXTURE_DIR = os.path.join(os.path.expanduser("~"), "krita_bridge") # ASCII-путь os.makedirs(TEXTURE_DIR, exist_ok=True) MAPS = { # Blender images → файлы, которые приходят из Krita "basecolor": "base.png", "roughness": "rough.png", "normal": "normal.png", # normal НЕ должен быть flipped по каналам "height": "height.png", } def setup_material(name="M_Krita"): mat = bpy.data.materials.get(name) or bpy.data.materials.new(name) mat.use_nodes = True tree = mat.node_tree bsdf = next(n for n in tree.nodes if n.type == "BSDF_PRINCIPLED") nodes, links = tree.nodes, tree.links for key, filename in MAPS.items(): path = os.path.join(TEXTURE_DIR, filename) if not os.path.exists(path): continue tex = nodes.new("ShaderNodeTexImage") tex.image = bpy.data.images.load(path, check_existing=False) tex.image.colorspace_settings.name = { # цвет читается как sRGB, а карты — как линейные данные "basecolor": "sRGB", "roughness": "Non-Color", "normal": "Non-Color", "height": "Non-Color", }[key] links.new(tex.outputs["Color"], bsdf.inputs[key]) return mat ``` ### 6. Автоперезагрузка: Blender следит за папкой ```python # Сторож за файлами. Работает и без внешних библиотек. import bpy, os TEXTURE_DIR = os.path.join(os.path.expanduser("~"), "krita_bridge") _seen = {} # путь → mtime, чтобы неReload'ить без изменений def _poll(): """Вызывается из bpy.app.timers. Возвращает None = снять таймер, любое число = интервал до следующего вызова.""" for filename in os.listdir(TEXTURE_DIR) if os.path.isdir(TEXTURE_DIR) else []: if not filename.lower().endswith((".png", ".exr")): continue path = os.path.join(TEXTURE_DIR, filename) mtime = os.path.getmtime(path) if _seen.get(path) == mtime: continue _seen[path] = mtime for img in bpy.data.images: if img.filepath_raw and os.path.abspath(img.filepath_raw) == os.path.abspath(path): img.reload() # главный шаг: перечитать файл с диска img.update() print("reloaded:", filename) return 1.0 # проверка раз в секунду def start_watching(): if _poll not in bpy.app.timers: bpy.app.timers.register(_poll, first_interval=1.0) def stop_watching(): if _poll in bpy.app.timers: bpy.app.timers.unregister(_poll) ``` > [!NOTE] > `bpy.app.timers.register` в Blender 4.x имеет переименованные параметры (`first_interval` вместо `sleep` в некоторых сборках). Если увидишь `TypeError`, посмотри подпись функции в своей версии: `help(bpy.app.timers.register)`. ### 7. Отправка в Krita: маска наружу Со стороны Krita, если нужно автоматизировать (Python Console: `Settings → Dockers → Python Console`): ```python # ВЫПОЛНЯЕТСЯ ВНУТРИ Krita. Модуль krita доступен только в её процессе — # из Blender `import krita` не сработает, это принципиальное ограничение. from krita import Krita k = Krita.instance() doc = k.activeDocument() layer = doc.activeLayer() print(doc.width(), doc.height(), layer.name()) # Положить активный слой отдельным PNG-файлом — так его и забирает Blender out_url = doc.filePath() # пока ничего не сохранено — будет пусто print("doc:", out_url) ``` Из Blender наружу отдаётся тот же файл через `save_render` / экспорт, а забрать результат — через `img.reload()` (шаг 6). Обмен файлами здесь не костыль, а единственный надёжный канал: прямой IPC между Krita и Blender не существует. ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```bash # Проверить, что ComfyUI жив и что плагин увидит те же ноды curl -s http://127.0.0.1:8188/system_stats # Забрать последний результат из папки ComfyUI вручную (обменная папка) copy /Y "C:\ComfyUI\output\blender\inpaint_00001_.png" "C:\Users\me\krita_bridge\base.png" ``` ```python # Что вообще лежит в обменной папке и какого размера — полезно при отладке import os d = os.path.join(os.path.expanduser("~"), "krita_bridge") for f in sorted(os.listdir(d)): p = os.path.join(d, f) print(f"{f:28s} {os.path.getsize(p)/1024:8.1f} KB {int(os.path.getmtime(p))}") ``` | Параметр | Значение | Когда менять | |----------|----------|--------------| | Обменная папка | ASCII-путь без пробелов и кириллицы | если плагин не видит файл — сначала подозревай путь | | `Denoise` (inpaint) | 0.4–0.7 | выше 0.9 замазанное место перестаёт вписываться в окружение | | `Sampling steps` | 20–30 | 15 хватает для черновика | | `Guidance scale` | 5–8 | ниже 4 — «мягкая» картинка, выше 10 — пережаренная | | Размер маски | совпадать с исходником | Krita экспортирует маску в своём размере — приводи к размеру текстуры | | Mask blur/ feather | 8–32 px | маленький feather даёт видимый шов на границе патча | | Формат масок | 8-bit grayscale PNG | RGB-маски Krita плагин интерпретирует по первому каналу, это источник «инверта» | | Тип операции | Inpaint / Outpaint / Fill / Control | Fill требует заливки всей маски, иначе плагин предложит Inpaint | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |--------|---------|---------| | В списке докеров нет AI Diffusion | Плагин не установлен или несовместим с версией Krita | Проверь `Manage Python Plugins`, переустанови, сверь версии | | `Connection refused` в панели плагина | ComfyUI не запущен или на другом порту | Запусти `python main.py`, проверь URL и порт в настройках плагина | | Генерация идёт минуты, картинки нет | ComfyUI считает, но `SaveImage` не отдаёт файл | Смотри консоль ComfyUI; проверь права на запись в `ComfyUI/output/` | | Маска инвертирована — чинится не то место | Маска в RGB, плагин берёт один канал; либо инверт в самом плагине | Экспортируй маску как 8-bit grayscale (в Krita: Image → Desaturate → Save) | | Текстура в Blender не обновляется | `filepath_raw` указывает не на тот файл | Сверь абсолютные пути; перезагрузи вручную `img.reload()` | | Таймер не вызывается | `bpy.app.timers` живёт только пока активен цикл событий | Не запускай из headless-рендера: для CI используй setup_04 (headless) | | Результат 512×512, а нужно 2048 | Плагин не смог найти в Krita нити для генерации | Генерируй на 512 и апскейль, либо иди через ComfyUI напрямую (SETUP 15) | | Кириллица в имени файла → плагин его не видит | Кодировка пути | Держи обменную папку и файлы в ASCII | | Нормаль выглядит «вдавленной» | Перепутаны green-канал или flip по Y | Нормали всегда экспортируй без flip; сверяйся с тестовой сферой | ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 07: CI/CD INTEGRATION](setup_07_ci_cd.md), [SETUP 09: BLENDERBOT / AI CODING AGENT](setup_09_blenderbot_ai.md), [SETUP 10: BLENDER + COMPOSITING (NUKE/AFTER EFFECTS ROUNDTRIP)](setup_10_compositing_roundtrip.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 11: BLENDER + TOUCHDESIGNER REALTIME VISUALS](setup_11_touchdesigner.md), [SETUP 12: BLENDER + UNREAL ENGINE 5 LIVE LINK](setup_12_unreal_livelink.md) - **Теги:** #blender, #setup --- # SETUP 18: SUBSTANCE PAINTER LIVE-LINK Tags: #setup #substance # SETUP 18: SUBSTANCE PAINTER LIVE-LINK **ID:** SETUP-18 | **Type:** Texturing Pro | **Difficulty:** ⭐⭐ | **Time:** 40-60 min **RU/EN Bilingual** > Когда применять: когда сцена уже в Blender, а текстуры пишет художник в Substance 3D Painter и результат должен возвращаться в материал без ручного «экспорт-импорт-потерялся». Для однократного прогона достаточно обычного экспорта; здесь — схема, где цикл можно повторять. ## 🎯 ЦЕЛЬ Организовать повторяемый цикл Blender ↔ Substance 3D Painter: вытащить из Blender меш с корректной геометрией (для карт ID/curvature/curvature/AO), забрать из Painter готовые PBR-карты и автоматически подставить их в материал Blender, включая UDIM-тайлы. Отдельная цель — сделать так, чтобы повторный прогон после правок в Painter не требовал ручных кликов в Blender. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |-----------|--------|-------| | Substance 3D Painter | 9.x / 10.x (ранее Substance Painter) | текстурирование, smart materials, экспорт карт | | Substance 3D Integration для Blender | см. примечание ниже | живая связь, если она есть для твоей версии | | Blender | 4.2 LTS+ | сборка материала и приём карт | | Разрешение текстур | 2K на тест, 4K в продакшене | Painter жрёт память пропорционально размеру карт | | Расшаренная папка обмена | ASCII-путь | канал между Painter и Blender | > [!NOTE] > Официальные плагины Substance 3D Integration заявлены для 3ds Max, Maya и Unreal, и набор поддерживаемых DCC менялся от версии к версии. **Проверь на сайте Adobe, есть ли поддержка Blender в твоей версии Substance 3D** — если её нет, статья ниже описывает файловый roundtrip с автоподхватом, который работает всегда. Не выдумывай имя аддона: сначала посмотри, что предлагает Adobe. ## 🛠️ НАСТРОЙКА ### 1. Из Blender в Painter: меш, на котором считаются карты ```bash # Batch-экспорт из Blender без GUI — удобно, когда скрипт гоняется по расписанию blender --background scene.blend \ --python-expr "import bpy; bpy.ops.object.select_all(action='SELECT'); \ bpy.ops.export_scene.fbx(filepath='C:/sp/asset.fbx', use_selection=True, \ path_mode='COPY', embed_textures=False, mesh_smooth_type='FACE')" ``` Ключевое: `mesh_smooth_type='FACE'` и развёртка. Painter строит карты ID/curvature/AO из геометрии; если в FBX приедут усреднённые нормали, карта curvature станет плоской и запечется пустой. Внутри Blender, чтобы не собирать длинную строку: ```python # export_for_painter.py — Blender: Text Editor → Run Script import bpy, os OUT = r"C:\sp" os.makedirs(OUT, exist_ok=True) bpy.ops.object.select_all(action='DESELECT') for ob in bpy.context.scene.objects: if ob.type == 'MESH': ob.select_set(True) bpy.context.view_layer.objects.active = bpy.context.scene.objects[0] bpy.ops.export_scene.fbx( filepath=os.path.join(OUT, "asset.fbx"), use_selection=True, path_mode='COPY', object_types={'MESH'}, mesh_smooth_type='FACE', # усреднённые нормали убивают карту curvature use_mesh_modifiers=True, # subdivision должен быть применён ДО экспорта apply_unit_scale=True, axis_forward='-Z', axis_up='Y', # согласование осей с Painter — главный источник «перевёрнутого» меша ) print("FBX:", os.path.join(OUT, "asset.fbx")) ``` ### 2. В Painter: export preset под Blender Готовый пресет — это JSON-файл с расширением `.spexp`. Надёжный путь: в UI `File → Export Texture Preset → Export`, потом правишь полученный файл и закидываешь обратно в `Documents/Adobe/Adobe Substance 3D Painter/*/resources/presets/`. ```json { "exportPresets": [ { "name": "Blender_PBR_UDIM", "maps": [ { "fileName": "$textureSet_BaseColor", "channels": [ { "destChannel": "R", "srcChannel": "R" }, { "destChannel": "G", "srcChannel": "G" }, { "destChannel": "B", "srcChannel": "B" } ], "parameters": { "fileFormat": "png", "bitDepth": "8", "dithering": false, "paddingAlgorithm": "infinite" } }, { "fileName": "$textureSet_Metallic", "channels": [ { "destChannel": "R", "srcChannel": "L" } ], "parameters": { "fileFormat": "png", "bitDepth": "8", "dithering": false, "paddingAlgorithm": "infinite" } }, { "fileName": "$textureSet_Roughness", "channels": [ { "destChannel": "R", "srcChannel": "R" } ], "parameters": { "fileFormat": "png", "bitDepth": "8", "dithering": false, "paddingAlgorithm": "infinite" } }, { "fileName": "$textureSet_Normal", "channels": [ { "destChannel": "RGB", "srcChannel": "Normal" } ], "parameters": { "fileFormat": "png", "bitDepth": "16", "dithering": false, "ditheringIntensity": 0 } }, { "fileName": "$textureSet_AO", "channels": [ { "destChannel": "R", "srcChannel": "AO" } ], "parameters": { "fileFormat": "png", "bitDepth": "8", "dithering": false, "paddingAlgorithm": "infinite" } }, { "fileName": "$textureSet_Height", "channels": [ { "destChannel": "R", "srcChannel": "Height" } ], "parameters": { "fileFormat": "exr", "bitDepth": "32" } } ], "exportList": [ { "rootPath": "$textureSet" } ], "exportPresets": [ { "name": "Default" } ], "parameters": { "paddingAlgorithm": "infinite", "dithering": true, "sizeLog2": 11, "fileFormat": "png" } } ] } ``` > [!NOTE] > Схема `.spexp` — это то, что Painter сохраняет сам. Не собирай пресет вручную по памяти: выгрузи пресет из UI, открой его в текстовом редакторе и правь только те поля, которые реально нужно поменять (размер, формат, `paddingAlgorithm`). Точные имена токенов `$textureSet_BaseColor` у каждой карты свои — сверяйся с выгрузкой. Три решения, которые чаще всего отличаются от «ожидаемого»: - **Metallic в R, Roughness в G** — как ожидает Principled BSDF; отдельная карта metallic только в R, roughness только в R, если используешь отдельные ноды. - **Normal в PNG 16-bit** — 8 бит на нормали дают заметный бандинг на гладких поверхностях. - **Height в 32-bit EXR** — 8-битный height ломает Bump/Displace при сильном смещении. ### 3. В Painter: батч-экспорт по всем texture set ```python # substance_export.py — запускается ВНУТРИ Painter: Python → Run Script, # либо из меню Python в панели Scripting. Не из командной строки ОС. import substance_painter.export import substance_painter.project import substance_painter.textures CONFIG = substance_painter.export.ExportUASConfig( export_project_path="C:/sp/export", # куда класть результат export_list=[substance_painter.export.ExportOutputConfig( output_name="Blender_PBR_UDIM", export_shader_params={} )], ) def do_export(): substance_painter.export.export_project_textures(CONFIG) print("exported", len(substance_painter.textures.all_texture_sets()), "texture sets") # Экспорт нельзя запускать, пока Painter занят — отсюда обёртка substance_painter.project.execute_when_not_busy(do_export) ``` > [!NOTE] > Имена классов конфигурации экспорта в Painter менялись между мажорными версиями (например, `ExportUASConfig`/`ExportOutputConfig` против более ранних конструкторов). Сверься с `help(substance_painter.export)` в Python-консоли Painter — это надёжнее любой статьи. ### 4. В Blender: собрать материал из карт ```python # build_pbr_material.py — Blender: Text Editor → Run Script import bpy, os EXPORT_DIR = r"C:\sp\export" def _clear_and_build(mat, basecolor="BaseColor.png", metallic="Metallic.png", roughness="Roughness.png", normal="Normal.png", ao="AO.png", height="Height.exr"): mat.use_nodes = True tree = mat.node_tree tree.nodes.clear() nodes, links = tree.nodes, tree.links out = nodes.new("ShaderNodeOutputMaterial"); out.location = (700, 0) bsdf = nodes.new("ShaderNodeBsdfPrincipled"); bsdf.location = (400, 0) def tex(filename, non_color=False): n = nodes.new("ShaderNodeTexImage"); n.location = (0, 0) path = os.path.join(EXPORT_DIR, filename) if not os.path.exists(path): print("нет карты:", filename); return None n.image = bpy.data.images.load(path, check_existing=False) n.image.colorspace_settings.name = 'Non-Color' if non_color else 'sRGB' return n # sRGB только у BaseColor; Metallic/Roughness/AO — это данные for filename, key, non_color in [ (basecolor, "Base Color", False), (metallic, "Metallic", True), (roughness, "Roughness", True), ]: n = tex(filename, non_color) if n: links.new(n.outputs["Color"], bsdf.inputs[key]) n = tex(normal, non_color=True) if n: nm = nodes.new("ShaderNodeNormalMap"); nm.location = (200, -300) nm.inputs["Strength"].default_value = 1.0 links.new(n.outputs["Color"], nm.inputs["Color"]) links.new(nm.outputs["Normal"], bsdf.inputs["Normal"]) n = tex(ao, non_color=True) if n: # Мультипликативное наложение AO на Base Color. mix = nodes.new("ShaderNodeMixRGB"); mix.location = (200, 250) mix.blend_type = 'MULTIPLY'; mix.inputs["Fac"].default_value = 1.0 links.new(n.outputs["Color"], mix.inputs["Color2"]) base = tex(basecolor) if base: links.new(base.outputs["Color"], mix.inputs["Color1"]) links.new(mix.outputs["Color"], bsdf.inputs["Base Color"]) links.new(bsdf.outputs["BSDF"], out.inputs["Surface"]) return mat _clear_and_build(bpy.data.materials.get("M_Asset") or bpy.data.materials.new("M_Asset")) ``` #> [!NOTE] > `ShaderNodeMixRGB` — это legacy-идентификатор: в Blender 4.x нода Mix в меню Add переехала в `ShaderNodeMix` с полем `data_type`, и старый тип оставлен для совместимости скриптов. Если в твоей версии `nodes.new("ShaderNodeMixRGB")` бросает `TypeError`, замени на `ShaderNodeMix` с `data_type='RGBA'` и `blend_type='MULTIPLY'`; входы у новой ноды продублированы по типам, так что обращайся к ним по индексам, а не по именам. ## 5. UDIM: импорт тайлов в Blender Painter пишет тайлы отдельными файлами (`_1001.png` и далее). Blender читает их как **одну** текстуру, если при импорте указать номера тайлов. ```text Blender → Image Editor → Image → Open → в файловом диалоге выбрать НЕСКОЛЬКО файлов с именами вида 1001.png, 1002.png … → Blender соберёт их в один tile-датаблок → в ноде Image Texture ничего менять не нужно: проекция уже UDIM ``` Программно через `bpy.data`: ```python import bpy, os EXPORT_DIR = r"C:\sp\export" tiles = sorted( f for f in os.listdir(EXPORT_DIR) if f.endswith(".png") and f.rsplit("_", 1)[-1].split(".")[0].isdigit() ) print("тайлов найдено:", len(tiles), tiles[:4]) # Важно: bpy.data.images.load() на каждый файл по отдельности НЕ собирает # тайлы — получится N отдельных датаблоков. Тайлы собирает оператор # image.open, когда ему передают список файлов. bpy.ops.image.open( directory=EXPORT_DIR + os.sep, files=[bpy.types.FileSelectEntry(name=n) for n in tiles], ) # Проверка: у собра изображения должен появиться список тайлов for img in bpy.data.images: if getattr(img, "source", None) == 'TILED': print("UDIM-датаблок:", img.name, "тайлов:", len(img.tiles)) ``` Через UI это делается ровно так же: `Image Editor → Image → Open`, выбрать в диалоге все файлы `*_1001.png … *_1004.png` сразу — и Blender соберёт их в один датаблок. Отдельно тайлы раскладываются сеткой оператором UV-редактора: ```python # Разложить UV по сетке тайлов. Оператор живёт в контексте UV-редактора, # поэтому включи контекст явно — из Text Editor он может не сработать. bpy.ops.uv.smart_project(angle_limit=1.15, island_margin=0.02) # на случай, если развёртки нет with bpy.context.temp_override(area=bpy.context.screen.areas[0]): bpy.ops.uv.uv_layout_generate_tile_grid(image=bpy.data.images["BaseColor"], grid_x=4, grid_y=4) ``` > [!NOTE] > `uv_layout_generate_tile_grid` — оператор UV-редактора, и его сигнатура (а также то, обязателен ли для него активный объект и открытый UV-редактор) менялась между версиями Blender. Проверь `bpy.ops.uv.uv_layout_generate_tile_grid.get_rna_type().properties` в своей версии; если оператора нет — разложи тайлы вручную через Image Editor. ### 6. «Живой» цикл: Blender сам подхватывает новые файлы ```python # watcher.py — следит за папкой экспорта и перезагружает текстуры import bpy, os EXPORT_DIR = r"C:\sp\export" _seen = {} def _poll(): if not os.path.isdir(EXPORT_DIR): return 2.0 for img in list(bpy.data.images): if not img.filepath_raw or not os.path.isabs(bpy.path.abspath(img.filepath_raw)): continue path = bpy.path.abspath(img.filepath_raw) if os.path.dirname(path).lower() != os.path.normcase(EXPORT_DIR).lower(): continue mtime = os.path.getmtime(path) if _seen.get(img.name) != mtime: _seen[img.name] = mtime img.reload() img.update() print("[painter] обновлено:", os.path.basename(path)) return 1.5 if _poll not in bpy.app.timers: bpy.app.timers.register(_poll, first_interval=1.5) ``` Чтобы цикл не ждал перерисовки вручную, добавь перерисовку вьюпорта: ```python for area in bpy.context.screen.areas: if area.type == 'VIEW_3D': area.tag_redraw() ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```bash # Painter: повторный батч-экспорт всех texture set с одним пресетом # (через встроенную Python-консоль Painter, не через системный Python) # substance_painter.project.execute_when_not_busy(do_export) # Blender: headless-пересборка материалов после экспорта — для CI blender --background scene.blend --python build_pbr_material.py -- --out C:/sp/export ``` ```python # Проверка, что все карты на месте, до того как начнётся сборка нод import os EXPORT_DIR = r"C:\sp\export" required = ["BaseColor.png", "Metallic.png", "Roughness.png", "Normal.png", "Height.exr"] for f in required: p = os.path.join(EXPORT_DIR, f) print(f"{'OK ' if os.path.exists(p) else 'MISS'} {f}" + (f" {os.path.getsize(p)//1024} KB" if os.path.exists(p) else "")) ``` | Параметр | Значение | Когда менять | |----------|----------|--------------| | `sizeLog2` / размер карт | 11 = 2K, 12 = 4K | 4K на 4GB VRAM — уже тесно, считай окно Painter | | Normal: bitDepth | 16 | 8 бит даёт бандинг на больших гладких поверхностях | | Height: fileFormat | `exr`, 32 bit | если используешь Displace, а не Bump | | `paddingAlgorithm` | `infinite` для тайлов, `finite` для отдельных карт | UDIM требует бесшовности по краю | | `dithering` | 8-бит карты — да, 16-бит normal — нет | dithering ломает карты, считываемые как данные | | `use_mesh_modifiers` при экспорте FBX | `True` | выключи, если не хочешь применять модификаторы | | Таймер опроса | 1.0–2.0 с | меньше 0.5 — нагрузка на диск без пользы | | colorspace карты | BaseColor `sRGB`, остальные `Non-Color` | ошибка здесь не видна в viewport, но видна в рендере | ### Про smart materials и экспорт Smart materials (и вообще весь процедурный слой Painter) **не экспортируются**. Выход — только разрешённые каналы: Base Color, Metallic, Roughness, Normal, Height, AO, Opacity, Emissive. Если в сцене нужен процедурный слой, вариантов два: оставить Artist Tool поверх карты в Blender, или генерировать процедурный слой в Substance Designer (см. SETUP 19). ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |--------|---------|---------| | Текстура плоская, curvature/AO пустые | В FBX усреднённые нормали или не применён Subdivision | `mesh_smooth_type='FACE'`, `use_mesh_modifiers=True`; в Painter проверь карту curvature на глаз | | Меш в Painter лежит/повёрнут не так | Несовпадение систем осей | Правь `axis_forward`/`axis_up` в экспорте; для FBX в Painter есть переключатель оси | | Меш в Blender после импорта вверх ногами | Обратная задача | Импортируй FBX с теми же `axis_forward`/`axis_up`, что и экспортировал | | Карты с чёрными рамками по краям | Не задан `paddingAlgorithm` | Для UDIM — `infinite`; переэкспортируй | | Нормали «пластиковые» / полосатые | 8-битный PNG для normal | 16 бит, и dithering выключить | | Металл выглядит как пластик | Metallic/Roughness подключены как sRGB или AO перемножен в Base Color с инверсией | Обе карты — `Non-Color`; AO только MULTIPLY, Fac = 1.0 | | UDIM-текстура не занимает весь меш | При импорте указаны не все тайлы | Переимпортируй, выбрав все файлы `*_10xx.png` разом | | Новые файлы из Painter не подхватываются | Таймер не зарегистрирован или сцена открыта в background | Проверь `bpy.app.timers.is_registered(_poll)`; в headless это не сработает | | Painter молча ничего не экспортирует | Экспорт вызвался в занятом состоянии | Оборачивай в `execute_when_not_busy` | | Долгая пауза перед появлением карты | Painter считает карты и dithering одновременно | Считай по одной карте, dithering — только на цветовых | ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 19: SUBSTANCE DESIGNER SBSAR](setup_19_substance_designer_sbsar.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 02: GENESISCORE LOCAL (OLLAMA)](setup_02_genesiscore_local.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 04: HEADLESS RENDER (CLI / CI/CD)](setup_04_headless_render.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 05: RENDER FARM (FLAMENCO)](setup_05_render_farm.md), [SETUP 06: CUSTOM MCP TOOLS](setup_06_custom_mcp_tools.md) - **Теги:** #setup, #substance --- # SETUP 19: SUBSTANCE DESIGNER SBSAR Tags: #setup #shading #substance # SETUP 19: SUBSTANCE DESIGNER SBSAR **ID:** SETUP-19 | **Type:** Procedural | **Difficulty:** ⭐⭐ | **Time:** 40-70 min **RU/EN Bilingual** > Когда применять: когда материал должен быть параметрическим — один граф даёт десятки вариантов (разная ширина шва, разный износ, разная толщина), и править его руками в текстуре дороже, чем двигать ручку. Классика: кирпич, бетон, ткань, металл с паттерном ржавчины. ## 🎯 ЦЕЛЬ Собрать из графа Substance 3D Designer переиспользуемый материал и подключить его к Blender так, чтобы в сцене появились ноды, которыми можно крутить параметры. Основной путь — готовые аддоны-конвертеры `.sbsar` в нодовый граф Blender; запасной, работающий всегда — запечь текстуры через `sbscooker`/`sbsrender` и собрать Principled BSDF вручную. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |-----------|--------|-------| | Substance 3D Designer | 12.x / 13.x (ранее Substance Designer) | авторство графа и экспорт `.sbsar` | | Substance 3D Player | та же мажорная версия | бесплатный просмотр `.sbsar`, проверка до Blender | | Blender | 4.2 LTS+ | приём материала | | Аддон-конвертер `.sbsar` → ноды Blender | см. примечание | превращает граф в ShaderNode-дерево | | `sbscooker` (входит в поставку Designer) | — | запекание текстур из `.sbsar` без GUI | | Готовый `.sbsar` | любой из Substance Share или свой | входные данные | > [!NOTE] > Официального аддона от Adobe для Blender нет: конвертеры `.sbsar` — сторонние проекты с разной степенью поддержки версий Blender. Перед тем как строить на них пайплайн, открой README конкретного аддона и проверь две вещи: на какую версию Blender он рассчитан и умеет ли он переносить exposed-параметры как отдельные ноды с дефолтами. Если не умеет — переходи на запекание через `sbscooker`, оно верно во всех версиях. ## 🛠️ НАСТРОЙКА ### 1. Экспорт `.sbsar` из Designer `.sbsar` — это уже готовый, параметрический пакет: граф плюс список входов, которые он объявил как «exposed». Именно эта функция и делает файл ценным. ```text Substance 3D Designer: 1. Открой свой граф (или собери: Uniform Color → Bitmap/Scan → Blend → Output) 2. Выходной нод (Output) должен вести в группу с включённым Output 3. File → Export → Export SBSAR → my_material.sbsar Проверка без Blender: Substance 3D Player → File → Open → my_material.sbsar В панели Parameters справа должны появиться exposed-ручки. Их нет → параметры не были экспонированы, и конвертер их не подхватит. ``` Что должно быть на выходе графа (это то, что потом увидит Blender): ```text Output-узел со следующими каналами: baseColor (rgb) → Base Color metallic (scalar)→ Metallic roughness (scalar)→ Roughness normal (normal)→ Normal height (scalar)→ Height (опционально, нужен для бампа) ambient (rgb) → AO (опционально) opacity (scalar)→ Alpha (опционально) emissive (rgb) → Emission (опционально) ``` ### 2. Запекание через `sbscooker` — путь, который работает всегда `sbscooker` берёт `.sbsar` и список входов в `.sbsxml` и раскладывает текстуры в папку. Это ровно то, что делает аддон-конвертер, но без конвертации в ноды. ```bash # Структура запуска (путь зависит от установки Designer, см. ниже) "C:\Program Files\Adobe\Adobe Substance 3D Designer\...\sbscooker.exe" \ C:\mat\brick.sbsar C:\mat\brick_out \ --inputs C:\mat\brick_inputs.sbsxml # Список доступных флагов — сам надёжный источник правды: sbscooker.exe --help ``` Входы описываются в `.sbsxml` — это обычный XML с идентификаторами параметров из графа: ```xml 0.012 0.35 4.0 ``` > [!NOTE] > Набор флагов `sbscooker` менялся между версиями Designer (в частности, формат входных параметров). Не переноси команды из интернета один в один: запусти `sbscooker.exe --help` из своей установки и сверь имена. Точный путь к бинарнику смотри в директории установки Designer, там же лежит `sbsrender.exe` — она делает то же самое, но в PNG-превью. ### 3. Blender: сборка Principled BSDF из запечённых карт ```python # sbsar_to_material.py — Blender: Text Editor → Run Script import bpy, os, glob MAT_DIR = r"C:\mat\brick_out" # куда положил sbscooker MAT_NAME = "M_Brick_SBSAR" def _find(pattern): hits = glob.glob(os.path.join(MAT_DIR, pattern)) return hits[0] if hits else None def build(mat_name=MAT_NAME): mat = bpy.data.materials.get(mat_name) or bpy.data.materials.new(mat_name) mat.use_nodes = True tree = mat.node_tree tree.nodes.clear() nodes, links = tree.nodes, tree.links out = nodes.new("ShaderNodeOutputMaterial"); out.location = (800, 0) bsdf = nodes.new("ShaderNodeBsdfPrincipled"); bsdf.location = (500, 0) def tex(label, srgb=False, y=0): path = _find(f"*{label}*") if not path: print("нет карты:", label); return None n = nodes.new("ShaderNodeTexImage") n.location = (0, y) n.image = bpy.data.images.load(path, check_existing=False) n.image.colorspace_settings.name = 'sRGB' if srgb else 'Non-Color' return n t = tex("basecolor", srgb=True, y=400) if t: links.new(t.outputs["Color"], bsdf.inputs["Base Color"]) t = tex("roughness", y=200) if t: links.new(t.outputs["Color"], bsdf.inputs["Roughness"]) t = tex("metallic", y=0) if t: links.new(t.outputs["Color"], bsdf.inputs["Metallic"]) t = tex("normal", y=-200) if t: nm = nodes.new("ShaderNodeNormalMap"); nm.location = (250, -200) nm.inputs["Strength"].default_value = 1.0 links.new(t.outputs["Color"], nm.inputs["Color"]) links.new(nm.outputs["Normal"], bsdf.inputs["Normal"]) # Height в Bump, а не в Displace: displacement требует достаточной # плотности меша и ломается на тайлах UDIM t = tex("height", y=-400) if t: bump = nodes.new("ShaderNodeBump"); bump.location = (250, -400) bump.inputs["Strength"].default_value = 0.2 bump.inputs["Distance"].default_value = 0.02 links.new(t.outputs["Color"], bump.inputs["Height"]) links.new(bump.outputs["Normal"], bsdf.inputs["Normal"]) # Мировые масштабы: узор SBSAR рассчитан на свои единицы, в Blender он # почти всегда «поедет» — задавай явно mapping = nodes.new("ShaderNodeMapping"); mapping.location = (-250, 400) mapping.inputs["Scale"].default_value = (1.0, 1.0, 1.0) coord = nodes.new("ShaderNodeTexCoord"); coord.location = (-450, 400) links.new(coord.outputs["Object"], mapping.inputs["Vector"]) links.new(bsdf.outputs["BSDF"], out.inputs["Surface"]) return mat m = build() m.use_fake_user = True print("материал:", m.name, "| нод:", len(m.node_tree.nodes)) ``` ### 4. Путь «живых параметров»: аддон-конвертер ```text 1. Установить аддон через Edit → Preferences → Add-ons → Install… (проверь совместимость версий по README до установки) 2. Открыть панель аддона (обычно в Properties → Material или отдельный панель) 3. Выбрать .sbsar → аддон конвертирует граф в ноды 4. Exposed-параметры появляются как отдельные ноды Value/RGB с дефолтами 5. Изменить параметр → пересчитать (Recompile / Refresh) → текстура обновится ``` Что важно понимать про этот путь: | Свойство | Значение | |----------|----------| | Что становится нодой | Одна нода на input графа, значение в `default_value` | | Что НЕ становится нодой | Внутренняя логика графа — она «запекается» в текстуру | | Скорость пересчёта | Секунды для 1K, десятки секунд для 4K — это рендер, не live-обновление | | Разрешение | Задаётся в настройках аддона или в Output-группе графа | | Зависимость от Adobe | Нет: `.sbsar` — самостоятельный файл со встроенным движком | ### 5. Сравнение подходов | | Конвертер в ноды | `sbscooker` + ручная сборка | |---|---|---| | Параметры | крутятся прямо в Blender | только перезапуск `sbscooker` с новым `.sbsxml` | | Зависимость от аддона | да | нет, только бинарник Designer | | Автоматизация/CI | плохо (нужен интерактивный UI) | отлично (скрипт + аргументы) | | Что выбрать | интерактивная работа, один материал | пайплайн, много вариантов, рендер-ферма | ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```bash # Пакетная генерация N вариантов одного материала: меняем только .sbsxml for w in 0.006 0.012 0.024; do sed "s|0.012|$w|" inputs.sbsxml > in_$w.sbsxml sbscooker.exe brick.sbsar out_$w --inputs in_$w.sbsxml done ``` ```python # Blender: прогнать все варианты в сцене как отдельные объекты — быстрый A/B import bpy, glob, os for d in sorted(glob.glob(r"C:\mat\out_*")): mat = bpy.data.materials.get(os.path.basename(d)) if not mat: print("пропущен (нет материала):", d); continue ob = bpy.data.objects.new(os.path.basename(d), bpy.data.meshes.new(os.path.basename(d))) ob.data.materials.append(mat) bpy.context.collection.objects.link(ob) ob.location = (len(bpy.context.collection.objects) * 1.5, 0, 0) ``` | Параметр | Значение | Когда менять | |----------|----------|--------------| | Exposure (Output-группа) | `default`, `parent`, `0-1` | `default` — гамма 2.2, sRGB-текстуры; `0-1` — линейные данные | | Разрешение выхода | 1K / 2K / 4K / 8K | 8K — это ~800 МБ на канал, для Cycles и GPU не всегда полезно | | `tile_scale` (через mapping) | 1.0 | узор из SBSAR масштабируется в мировых единицах Blender, а не в UV | | Bump `Distance` | 0.01–0.05 | слишком большое значение даёт «жестянку» на мелких деталях | | Bump `Strength` | 0.1–0.4 | выше 0.5 высоты перестают читаться как рельеф | | Входные параметры графа | float / color / integer | тип влияет на формат XML в `.sbsxml` | ### Про лицензию и зависимости `sbsar` — открытый формат, но рендер идёт через движок, который **поставляется вместе с Substance 3D Designer или Player**. Player бесплатный и умеет открывать `.sbsar`, поэтому чаще всего достаточно его. Аддон-конвертер, который работает внутри Blender, зависит от того, что аддон сам умеет считать граф — проверяй в его README, нужна ли ему установленная Adobe. ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |--------|---------|---------| | В Player нет ручек Parameters | Параметры не экспонированы в графе | В Designer отметь нужные входы как exposed перед экспортом | | Аддон не находит граф | Несовместимость версий аддона и Blender | Проверь README аддона; запасной путь — `sbscooker` | | `sbscooker` не запускается | Путь к бинарнику отличается от ожидаемого | Найди `sbscooker.exe` в директории установки Designer и вызови по полному пути | | Материал чёрный | Exposure Output-группы не тот, либо карта не нашлась | Проверь путь/имена файлов в `MAT_DIR`, сверь exposure в Player | | Текстура мылится вдали | Нет мирового масштаба: узор считался в UV | Добавь Mapping + TexCoord Object и подбери `Scale` | | Тайлы UDIM из SBSAR не подхватаются | Blender не собрал тайлы в один датаблок | Открой все файлы `1001.png…` разом через Image Editor → Open | | Рельеф выглядит как «жесть» | Displace вместо Bump, либо слишком большой Distance | Используй Bump ноду; Distancе держи в пределах 0.01–0.05 | | Нормали инвертированы | Каналы перепутаны или.flip по Y | Сравни с любой готовой картой normal того же формата; в Substance — «direct» против «OpenGL» | | Пересчёт параметра занимает минуты | Рендер графа на высоком разрешении | Опусти разрешение на этапе подбора параметров, подними на финальном | | Материал пропал после перезагрузки файла | У датаблока не стоял пользователь | `mat.use_fake_user = True` | ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 18: SUBSTANCE PAINTER LIVE-LINK](setup_18_substance_painter_live.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 02: GENESISCORE LOCAL (OLLAMA)](setup_02_genesiscore_local.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 04: HEADLESS RENDER (CLI / CI/CD)](setup_04_headless_render.md) - **По темам:** [CASE 03: PRODUCT VISUALIZATION](../cases/case_03_product_viz.md), [P09: PRINCIPLED BSDF SETUP](../prompts/09_principled_bsdf.md), [P10: NODE GROUP CREATION](../prompts/10_node_groups.md), [P11: PROCEDURAL TEXTURES](../prompts/11_procedural_textures.md) - **Теги:** #setup, #shading, #substance --- # SETUP 20: ZBRUSH GOZ BRIDGE Tags: #sculpt #setup # SETUP 20: ZBRUSH GOZ BRIDGE **ID:** SETUP-20 | **Type:** Sculpt | **Difficulty:** ⭐⭐ | **Time:** 30-50 min **RU/EN Bilingual** > Когда применять: когда детализация делается в ZBrush (поры, складки, разрушения), а сцена живёт в Blender. GoZ — это push-мост: ZBrush сам отправляет выбранный сабтул в активную программу, без ручного экспорта. ## 🎯 ЦЕЛЬ Настроить двусторонний обмен между ZBrush и Blender через GoZ (GoZBrush), научиться передавать меш на нужном уровне subdivision, полипейнт как vertex color и карту смещения как displacement. На выходе — цикл «лепим в ZBrush → го в Blender → го обратно», в котором Blender остаётся источником формы, а ZBrush — источником деталей. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |-----------|--------|-------| | ZBrush | 2022.0.5 и новее (ZBrush 2024 / 2025) | sculpt + отправитель GoZ | | GoZ-плагин для Blender | версия, совместимая с твоим ZBrush и Blender | приёмник в Blender | | ZBrushCentral / ZBrush 2024+ лицензия | — | сам ZBrush платный; **GoZ при этом бесплатен и open source** | | Blender | 4.2 LTS+ | приём, vertex color, displacement | | Карта смещения | 16- или 32-битная | true displacement | | Одно из двух | Quad Remesher или ручная ретопология | игровая/рендерная топология | > [!NOTE] > GoZ — одна из немногих вещей в этом пайплайне, где не надо гадать: протокол и его реализации публичны (репозиторий GoZBrush), и в ZBrush он встроен. Проверь, что плагин Blender разрешает входящие подключения — иначе ZBrush будет писать в пустоту, молча. ## 🛠️ НАСТРОЙКА ### 1. Порядок запуска важен ```text 1. Blender запустить ПЕРВЫМ и открыть нужную сцену 2. В ZBrush нажать GoZ (кнопка в панели Tool, справа внизу) 3. ZBrush подключится к Blender как к получателю 4. Дальше нажатие GoZ отправляет текущий сабтул в Blender ``` Если ZBrush запущен раньше Blender — подключения не будет, потому что ZBrush ищет получателя в момент нажатия GoZ, а не слушает постоянно. ### 2. Отправка меша и управление уровнем subdivision ```text Уровни детализации в ZBrush настраиваются ДО нажатия GoZ: • В панели Tool справа внизу — 5 кнопок уровней SDiv (1–5) • Отмеченные уровни отправляются в Blender • Higher Resolution: если включить, ZBrush сама досчитает недостающие уровни перед отправкой (долго, зато не нужно ждать вручную) Практика: отправлять на том уровне, где реально есть работа. Уровень 5 на недовылепленном объекте — это миллионы пустых полигонов. ``` Что GoZ умеет передавать (перечень закрытый — что не входит, тем придётся обмениваться файлами): | Что | Передаётся через GoZ | Файл вручную | |-----|---------------------|--------------| | Меш (выбранный уровень SDiv) | да | FBX / OBJ | | Normals | да | — | | UV | да (как есть в ZBrush) | FBX | | Полипейнт → vertex color | да | OBJ/FBX + vertex color | | Displacement (карта смещения) | да | TIFF/EXR | | Mask | да | — | | SubTools как отдельные объекты | зависит от настроек плагина | FBX | | Материалы/шейдеры | нет | нет | ### 3. Polypaint → vertex color в Blender ```python # goz_setup.py — Blender: Text Editor → Run Script import bpy def ensure_color_attribute(obj: bpy.types.Object, name="Col", domain='CORNER'): """ GoZ обычно приносит vertex color под фиксированным именем. Создаём атрибут заранее, чтобы потом просто перезаполнять его. """ if name in obj.data.color_attributes: return obj.data.color_attributes[name] return obj.data.color_attributes.new(name=name, type='BYTE_COLOR', domain=domain) def setup_polypaint_material(obj: bpy.types.Object, mat_name="M_Polypaint"): """Vertex Color в Principled BSDF. В Blender 4.x нода Vertex Color ушла в Attribute (GEOMETRY) — имя атрибута задаётся вручную.""" mat = bpy.data.materials.get(mat_name) or bpy.data.materials.new(mat_name) mat.use_nodes = True tree = mat.node_tree bsdf = next(n for n in tree.nodes if n.type == 'BSDF_PRINCIPLED') attr = tree.nodes.new("ShaderNodeAttribute") attr.attribute_type = 'GEOMETRY' attr.attribute_name = "Col" attr.location = (-250, 100) tree.links.new(attr.outputs["Color"], bsdf.inputs["Base Color"]) mat.use_fake_user = True return mat def apply_polypaint(obj_name): obj = bpy.data.objects.get(obj_name) if not obj: print("объект не найден:", obj_name); return ensure_color_attribute(obj) setup_polypaint_material(obj) # GoZ обновляет атрибут на месте; если переименовал — поправь в атрибуте print("vertex color подключён:", obj.name) apply_polypaint("Cube") ``` Проверка, что цвет реально пришёл (должно быть видно на модели, а не серое): ```python # Диагностика: смотрим реальные значения в атрибуте obj = bpy.data.objects.get("Cube") if obj: ca = obj.data.color_attributes.get("Col") if ca: n = len(ca.data) nonzero = sum(1 for d in ca.data if d.color[0] + d.color[1] + d.color[2] > 0.01) print(f"{ca.name}: {n} значений, ненулевых {nonzero} ({100*nonzero/max(n,1):.1f}%)") else: print("атрибута Col нет — GoZ не прислал полипейнт") ``` ### 4. Displacement: карта смещения как настоящее смещение ```python # displacement_setup.py — Blender: Text Editor → Run Script import bpy, os DISP_DIR = r"C:\zb" # куда ZBrush сохранил карту NAME = "disp.tiff" # или disp.exr — зависит от того, что ты экспортировал def setup_true_displacement(obj_name, disp_filename=NAME, scale=1.0): obj = bpy.data.objects.get(obj_name) if not obj: print("нет объекта:", obj_name); return path = os.path.join(DISP_DIR, disp_filename) if not os.path.exists(path): print("нет карты смещения:", path); return img = bpy.data.images.load(path, check_existing=False) # Карта смещения — данные, не цвет: Non-Color обязателен img.colorspace_settings.name = 'Non-Color' disp = obj.modifiers.new("ZBrush Displace", 'DISPLACE') disp.texture = img disp.texture_coords = 'LOCAL' # LOCAL = по системе координат объекта disp.mid_level = 0.5 # соответствует нулевому смещению disp.direction = 'NORMAL' disp.strength = scale # Порядок модификаторов критичен: смещение должно идти ПЕРЕД сабдивом subsurf = next((m for m in obj.modifiers if m.type == 'SUBSURF'), None) if subsurf: while obj.modifiers.find(subsurf.name) < obj.modifiers.find(disp.name): bpy.ops.object.modifier_move_up(modifier=subsurf.name) return disp # Проверить, что карта не пустая img = bpy.data.images.get("disp.tiff") if img: px = list(img.pixels[:4096]) mn, mx = min(px), max(px) print(f"диапазон карты: {mn:.3f}..{mx:.3f}", "— карта пустая" if mx - mn < 0.01 else "— карта информативна") ``` > [!NOTE] > Экспорт карты смещения из ZBrush: `Tool → Displacement → Displacement Maps → Create` (предпросмотр), затем `Export`. Формат выбирай **16-битный TIFF или 32-битный EXR**: 8-битных карт не хватает на больших смещениях, шаг квантования становится виден. Точные пункты меню проверь в своей версии ZBrush — интерфейс заметно менялся между 2020 и 2025. ### 5. Ретопология: high → low Главная ошибка — приносить high-poly в Blender и там разбираться. Правильная схема: ```python # retopo_plan.py — Blender: Text Editor → Run Script import bpy def setup_retopo_workflow(high_name, low_name="Cube_Low"): """ Высокая детальность живёт на отдельном объекте, рядом с игровым. В сцене они должны занимать одно и то же место. """ high = bpy.data.objects.get(high_name) low = bpy.data.objects.get(low_name) if not high or not low: print("нужны оба объекта:", high_name, low_name); return # Временная папка для проекции for ob in (high, low): mod = ob.modifiers.new("ProjectFromSurface", 'SURFACE_DEFORM') try: mod.target = high mod.falloff = 4.0 except Exception as e: print("Surface Deform:", e) return print("Surface Deform настроен. Запеки через Object → Apply, иначе привязка живёт только в текущей сессии.") ``` ```text Если в проекте доступен Quad Remesher (аддон от Blender Studio / отдельная лицензия): Blender → Quad Remesher → Target = High, Target Preset = Quad, выбрать Target Surface → получаешь quad-меш на месте высокого, готовый к UV и запеканию. Это на порядок быстрее ручной ретопологии на органике. ``` > [!NOTE] > Quad Remesher распространяется как отдельный аддон (Extension) с собственной лицензией и подпиской, и доступность/условия менялись. Проверь актуальный статус в списке Extensions в своей версии Blender, прежде чем строить на нём пайплайн. Альтернативы: Remesh → QuadriFlow (встроен в новые версии Blender как отдельный аддон), либо ручная ретопология. ### 6. Проверка сквозного цикла ```text 1. Запустить Blender, затем ZBrush 2. ZBrush → GoZ (подключение) 3. Выбрать Tool → нажать GoZ → в Blender появляется/обновляется объект 4. Перерисовать Polypaint в ZBrush → GoZ → vertex color в Blender обновился 5. Экспортировать displacement → подключить Displace в Blender 6. Surface Deform для проекции деталей на ретопологию ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```python # Полная диагностика GoZ-сцены: что пришло, с чем, и где проблема import bpy def goz_report(): print("=" * 50) print("Blender:", bpy.app.version_string) meshes = [o for o in bpy.context.scene.objects if o.type == 'MESH'] print(f"мешей в сцене: {len(meshes)}") for ob in meshes: attrs = [a.name for a in ob.data.color_attributes] mods = [f"{m.type}" for m in ob.modifiers] print(f" {ob.name:20s} v={len(ob.data.vertices):>9,} " f"color={attrs or '—'} mods={mods or '—'}") # Проверка карт смещения for img in bpy.data.images: if img.source == 'FILE' and img.name.lower().startswith(("disp", "normal")): print(f" {img.name}: {img.size[0]}×{img.size[1]}, " f"colorspace={img.colorspace_settings.name}, depth={img.depth}") print("=" * 50) goz_report() ``` | Параметр | Значение | Когда менять | |----------|----------|--------------| | Уровень SDiv на GoZ | 1–5, активный обычно 3–5 | 5 на грубом объекте = миллионы ненужных полигонов | | Higher Resolution | вкл/выкл | включай, когда не хочешь ждать досчёта вручную | | `mid_level` (Displace) | 0.5 | всегда 0.5, если ZBrush карта нормализована в 0..1 | | `texture_coords` (Displace) | `LOCAL` | `GLOBAL` — если объект двигается в мире и смещение «поехало» | | `strength` (Displace) | 0.01–0.05 м | слишком большое значение рвёт низкополигональную сетку | | Формат карты смещения | 16-bit TIFF / 32-bit EXR | 8 бит = видимые ступеньки на больших смещениях | | Attribute name (vertex color) | `Col` | имя задаёт плагин; если не совпадает — поправь в ноде Attribute | | `BYTE_COLOR` vs `FLOAT_COLOR` | BYTE_COLOR | точность 8 бит на канал хватает для полипейнта | | Subdivision после Displace | 1–2 уровня | смещение требует плотности; считай полигоны | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |--------|---------|---------| | ZBrush не видит Blender | ZBrush запущен раньше Blender | Перезапусти: сначала Blender, потом ZBrush, потом GoZ | | Нажал GoZ, в Blender ничего | Плагин GoZ выключен или не установлен | `Edit → Preferences → Add-ons`, найти GoZ и включить | | Пришёл объект с 20 млн полигонов | Отправлен SDiv 5 без Higher Resolution | Отправляй на том уровне, где есть работа; проверь на скриншоте | | Vertex color в Blender серый | Атрибут называется не `Col`, либо перезаписался при GoZ | Назови в ноде Attribute точное имя атрибута из Mesh Properties → Color Attributes | | Polypaint цвета «уехали» | Полипейнт в ZBrush в линейном пространстве, а в Blender интерпретируется как sRGB | Держи одинаковый colorspace в ZBrush и в Blender; проверь на градиенте | | Displacement рвёт меш | Не хватает плотности сетки или `strength` слишком большой | Добавь Subdivision до Displace в порядке модификаторов, уменьши strength | | Displacement смещается при движении объекта | `texture_coords='GLOBAL'` | Поставь `LOCAL` | | Поверхность «плывёт» после запекания Surface Deform | Модификатор не применён | `Object → Apply`, иначе привязка не переживёт перезагрузку файла | | Материал из ZBrush не пришёл | GoZ не передаёт материалы | Собирай материал в Blender вручную: это 6 нод, а не проблема пайплайна | | Экспорт карты смещения даёт серое | Включён Grayscale/Monochrome, карта без данных | В ZBrush сначала `Create`, потом `Export`; проверь диапазон пикселей скриптом выше | ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 02: GENESISCORE LOCAL (OLLAMA)](setup_02_genesiscore_local.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 04: HEADLESS RENDER (CLI / CI/CD)](setup_04_headless_render.md), [SETUP 05: RENDER FARM (FLAMENCO)](setup_05_render_farm.md) - **По темам:** [P08: SCULPTING BASICS](../prompts/08_sculpting_basics.md), [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 06: CUSTOM MCP TOOLS](setup_06_custom_mcp_tools.md) - **Теги:** #sculpt, #setup --- # SETUP 21: MARVELOUS / CLO3D CLOTH Tags: #clothing #setup # SETUP 21: MARVELOUS / CLO3D CLOTH **ID:** SETUP-21 | **Type:** Cloth | **Difficulty:** ⭐⭐⭐ | **Time:** 60-120 min **RU/EN Bilingual** > Когда применять: когда одежду нельзя сделать в Blender. Marvelous Designer / CLO 3D дают симуляцию ткани с настоящей вышивкой, швами и заломами, которые руками в нодах не воспроизвести. Blender принимает результат и отвечает за материал, рендер и общее оформление. ## 🎯 ЦЕЛЬ Прогнать цикл «аватар → гардероб → симуляция → экспорт → Blender», где симуляция ткани считается в Marvelous/CLO, а геометрия приезжает в Blender кэшем Alembic. Дополнительно настроить толщину (Solidify), коллизию с телом и запекание нормалей, чтобы одежда корректно выглядела в рендере. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |-----------|--------|-------| | Marvelous Designer | 12/13 (или новее) | моделирование и симуляция одежды | | CLO 3D | 7/8 (альтернатива, не оба сразу) | то же самое плюс симуляция тела | | Blender | 4.2 LTS+ | приём Alembic, материал, рендер | | Alembic (.abc) | любой | основной формат обмена | | Collada / FBX / OBJ | как запасной путь | если Alembic несёт слишком тяжёлую геометрию | | Avatar в A-pose или T-pose | — | поза, из которой симуляция стартует | > [!NOTE] > Главная техническая оговорка: **прямой живой линк между Marvelous/CLO и Blender отсутствует** как штатная функция. CLO продвигает свою экосистему (CLO LiveSync идёт в сторону Unity/Unreal и в сторону Blender отдельными аддонами сторонних авторов). Статья ниже описывает схему, которая работает всегда: симуляция в Marvelous → Alembic/OBJ в Blender. Если нужен именно live-линк, ищи сторонний аддон и проверяй его дату поддержки версий. ## 🛠️ НАСТРОЙКА ### 1. Позы и единицы — проверь до всего остального ```text Marvelous Designer / CLO: • Аватар должен быть в A-pose (руки вниз под ~45°) или T-pose • Обе программы считают в сантиметрах по умолчанию • Blender — в метрах по умолчанию Это главный источник ошибки «плащ размером с комнату». Решение: при импорте в Blender выставить масштаб 0.01 (см / м), или заранее сменить единицы в настройках сцены. ``` ### 2. Экспорт из Marvelous Designer ```text File → Export → Object Формат: Alembic (.abc) ← основной выбор Что включить: ✓ Geometry / Mesh ✓ UV ✓ Normals (Smooth shading) ✓ Material (если есть .mtl — но текстуры всё равно придётся пересобирать) Отдельно, если нужен кэш по кадрам: File → Export → Simulation Cache → папка с .obj по кадрам Варианты форматов и когда что: .abc — сохраняет UV и деформацию по кадрам в одном файле. Основной выбор. .fbx — сохраняет скелет, но UV и швы теряются частично. .obj — простой, надёжный, но без анимации: только статичный кадр. ``` > [!NOTE] > Опции экспорта и их названия в меню менялись между мажорными версиями Marvelous Designer и CLO 3D (в частности, набор галочек в Object Export). Сверяйся с диалогом экспорта в своей версии, а не с этим списком: выбор здесь буквально «потерять UV» или «не потерять». ### 3. Импорт Alembic в Blender ```python # import_cloth.py — Blender: Text Editor → Run Script import bpy, os ABC = r"C:\clo\jacket.abc" # путь к кэшу из Marvelous SCALE = 0.01 # сантиметры → метры def import_garment(filepath=ABC, scale=SCALE, start=1, end=250): if not os.path.exists(filepath): print("нет файла:", filepath); return None before = set(bpy.data.objects) # ВАЖНО: оператор создаёт объект по контексту. Из Text Editor он # работает, но в background-режиме нужно переключить контекст. with bpy.context.temp_override(): bpy.ops.wm.alembic_import( filepath=filepath, scale=scale, start=start, end=end, validate_meshes=True, is_shadow=False, ) new = set(bpy.data.objects) - before print("импортировано объектов:", len(new)) for ob in sorted(new, key=lambda o: o.name): print(f" {ob.name:30s} v={len(ob.data.vertices):>9,} " f"frames={getattr(ob.data, 'use_shape_key_edit_mode', 'n/a')}") return new objs = import_garment() ``` > [!NOTE] > Набор аргументов оператора `bpy.ops.wm.alembic_import` менялся между версиями Blender (часть флагов переименовывалась, часть добавлялась). Перед использованием вызови в консоли Blender `bpy.ops.wm.alembic_import.get_rna_type().properties.keys()` и собери вызов по фактическим именам — это надёжнее, чем доверять любому списку из интернета, включая этот. ### 4. Alembic без анимации: кэш OBJ по кадрам Если Alembic весит сотни мегабайт, а нужна статика — импортируй кадр как OBJ: ```python import bpy, os, glob CACHE_DIR = r"C:\clo\cache" # папка с jacket_0001.obj, jacket_0002.obj, ... def import_single_frame(cache_dir=CACHE_DIR, frame=1): files = sorted(glob.glob(os.path.join(cache_dir, "*.obj"))) if not files: print("кэш пуст:", cache_dir); return idx = min(max(frame - 1, 0), len(files) - 1) print("берём:", files[idx]) # Импортируем один кадр — это просто OBJ with bpy.context.temp_override(): bpy.ops.wm.obj_import( filepath=files[idx], forward_axis='NEGATIVE_Z', up_axis='Y', # оси Marvelous → Blender global_scale=0.01, ) ``` ### 5. Толщина: Solidify ```python # thickness.py — Blender: Text Editor → Run Script import bpy def add_thickness(obj_name, thickness_m=0.002, material_offset=0): """ 0.002 м = 2 мм. Реальные значения: джинса/кожа 1.5–3 мм хлопковая рубашка 0.3–0.6 мм тонкий шёлк 0.1–0.2 мм куртка-пуховик 8–20 мм (тут Solidify уже не подходит) """ ob = bpy.data.objects.get(obj_name) if not ob: print("нет объекта:", obj_name); return sol = ob.modifiers.new("Thickness", 'SOLIDIFY') sol.thickness = thickness_m sol.offset = 0.0 # 0 = растёт наружу и внутрь; -1 = только внутрь sol.use_even_offset = True sol.use_quality_normals = True sol.material_offset = material_offset # материал для внутренней стороны sol.material_offset_rim = material_offset # материал для кромки # Порядок: сначала толщина, потом сглаживание sm = ob.modifiers.new("Smooth", 'SMOOTH') sm.factor = 0.5 sm.iterations = 2 return sol add_thickness("jacket", 0.002) ``` > [!NOTE] > Solidify с невысокой плотностью сетки даёт «жестяной» вид на сгибах: вершины раздвигаются неравномерно. Если количество полигонов невелико, добавь Subdivision **перед** Solidify в порядке модификаторов, либо разбей ретопологию на сегменты по 2–3 см. ### 6. Коллизия и сшивка с телом ```python # collision.py — Blender: Text Editor → Run Script import bpy def setup_collision(body_name="Body", garment_name="jacket"): body = bpy.data.objects.get(body_name) garment = bpy.data.objects.get(garment_name) if not body or not garment: print("нужны оба объекта"); return # На ТЕЛЕ — Collision: тело становится препятствием if not any(m.type == 'COLLISION' for m in body.modifiers): col = body.modifiers.new("Collision", 'COLLISION') # Плоский коллайдер (convex hull) работает стабильнее, чем mesh col.collision_shape = 'CONVEX_HULL' body.collision.thickness_outer = 0.01 body.collision.damping = 0.3 body.collision.friction_factor = 0.5 # На ОДЕЖДЕ — Shrinkwrap, если она сидит слишком свободно sw = garment.modifiers.new("Wrap to body", 'SHRINKWRAP') sw.wrap_method = 'NEAREST_SURFACEPOINT' sw.target = body sw.offset = 0.01 # Порядок: Shrinkwrap ДО Solidify, иначе толщина ляжет на уже сжатую форму setup_collision() ``` Если в Blender нужна **собственная** симуляция одежды (а не кэш из Marvelous), цепочка такая: ```python # Настоящая cloth-симуляция в Blender — только для простых случаев. # Струна/лента/юбка получаются хорошо, куртка с капюшоном — нет. def setup_cloth(body_name="Body", garment_name="jacket", quality=5): body = bpy.data.objects[body_name] cloth = bpy.data.objects[garment_name] if not any(m.type == 'COLLISION' for m in body.modifiers): body.modifiers.new("Collision", 'COLLISION') m = cloth.modifiers.new("Cloth", 'CLOTH') s = m.settings s.quality = quality # 5 достаточно; выше 10 — минуты на кадр s.mass = 0.3 # kg/vertex; меньше → легче, «шёлковая» ткань s.tension_stiffness = 15 s.compression_stiffness = 15 s.shear_stiffness = 5 s.bending_stiffness = 0.5 s.air_damping = 1.0 # Pin-группа: зафиксировать верх (плечи, талия) иначе одежда улетает if not cloth.modifiers.get("Pin"): pin = cloth.vertex_groups.new(name="Pin") top = max((v.co.z for v in cloth.data.vertices), default=0.0) ids = [v.index for v in cloth.data.vertices if v.co.z > top * 0.9] pin.add(ids, 1.0, 'REPLACE') m.settings.vertex_group_mass = "Pin" return m ``` ### 7. Запекание нормалей на одежду ```text Только в Cycles (EEVEE запекание не поддерживает): 1. Выделить одежду, сделать активной 2. Добавить в материал Image Texture с ACTIVE UV (имя слоя = имя UV-карты) 3. Object Data → UV → выбрать развёртку одежды как активную 4. Baking → Type: Normal → Save As: C:\clo\jacket_normal.exr Важно: перед запеканием разверни одежду (UV → Smart UV Project), иначе запечётся каша из перекрывающихся островов. ``` ```python # Проверка готовности к запеканию import bpy def check_bake_ready(obj_name="jacket"): ob = bpy.data.objects.get(obj_name) if not ob: print("нет объекта"); return uv_layers = [uv.name for uv in ob.data.uv_layers] print("UV-слои:", uv_layers or "НЕТ — запекание невозможно") print("активный движок:", bpy.context.scene.render.engine) mats = [m.name for m in ob.data.materials if m and m.use_nodes] for m in mats: imgs = [n.image.name for n in bpy.data.materials[m].node_tree.nodes if n.type == 'TEX_IMAGE'] print(f" {m}: текстуры {imgs or 'НЕТ'}") if bpy.context.scene.render.engine != 'CYCLES': print("внимание: EEVEE не умеет запекать. Переключись на Cycles.") check_bake_ready() ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```bash # Запекание симуляции одежды не требует GUI при экспорте кэша: # в Marvelous Designer File → Export → Simulation Cache выбираешь # папку, диапазон кадров и формат (obj/abc). Дальше — обычная папка с файлами. # Импорт кэша в Blender в batch-режиме (CI, без открытой сцены): blender --background --python import_cloth.py -- /c/clo/cache /c/clo/out.blend ``` ```python # Сводка по всей одежде: толщина, коллизия, порядок модификаторов import bpy for ob in bpy.context.scene.objects: if ob.type != 'MESH': continue if not ob.modifiers: continue chain = " → ".join(m.type for m in ob.modifiers) sol = next((m for m in ob.modifiers if m.type == 'SOLIDIFY'), None) thick = f"{sol.thickness*1000:.1f} мм" if sol else "—" print(f"{ob.name:22s} {len(ob.data.vertices):>8,} v {thick:>8s} {chain}") ``` | Параметр | Значение | Когда менять | |----------|----------|--------------| | `scale` при Alembic-импорте | 0.01 | если импортируешь FBX/OBJ и он уже в метрах — 1.0 | | `solidify.thickness` | 0.0002–0.003 м | 0.002 м (2 мм) — универсальное значение для ткани | | `solidify.offset` | 0.0 или -1.0 | 0 — наружу и внутрь, -1 — только внутрь (для облегающего) | | `use_even_offset` | `True` | без него толщина густеет на острых углах | | `collision.collision_shape` | `CONVEX_HULL` | `MESH` точнее, но в разы медленнее и капризнее | | `cloth.quality` | 5 | выше 8 — минуты вместо секунд, без заметного выигрыша | | `cloth.mass` | 0.3 kg/vertex | меньше → ткань «летит», больше → «ватная» | | `shrinkwrap.offset` | 0.005–0.01 м | меньше значения → z-fighting с телом | | Bump/AO на одежде | запекать в 2K | мелкий шов виден только вблизи — нужен большой бюджет карт | ### Порядок модификаторов (то, что ломается чаще всего) ```text Правильно: Shrinkwrap / Surface Deform → Solidify → Subdivision → Smooth → Weighted Normal Неправильно: Solidify → Shrinkwrap # толщина уже запечатана, потом её мнёт Solidify → Subdivision # сабдив сгладит кромку, край станет скруглённым ``` ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |--------|---------|---------| | Одежда в 100 раз больше тела | Сантиметры против метров | `scale=0.01` при импорте, либо сменить единицы сцены | | Меш вверх ногами / повёрнут | Несовпадение осей OBJ | Для OBJ задай `forward_axis='NEGATIVE_Z', up_axis='Y'` | | Импорт Alembic падает с `TypeError` | Список аргументов оператора изменился | `bpy.ops.wm.alembic_import.get_rna_type().properties.keys()` | | Одежда «протекает» сквозь тело при рендере | Нет Collision на теле | Добавить Collision на тело, проверить `thickness_outer` | | Ткань выглядит как картон | Толщина слишком большая или нечётная сетка | Уменьши thickness, включи `use_even_offset`, добавь Subdivision перед Solidify | | Швы и строчки потерялись | Экспорт без UV | Включи UV в экспорте; проверь наличие UV-слоя после импорта | | Нормали запечёлись кашей | Развёртка не сделана или острова перекрыты | Smart UV Project с margin 0.02, залей до запекания | | Запекание не запускается | Активен EEVEE | Только Cycles | | Alembic весит 400 МБ и едет | Полная симуляция по кадрам | Экспортируй кэш только на нужный диапазон кадров либо один статичный кадр | | В Blender нет нормалей на одежде | Импорт без smooth normals | Включи Normals при экспорте; проверь `obj.data.shade_smooth()` | | Zipper/пуговицы смещаются при анимации тела | Alembic не несёт привязки к скелету | Для анимации используй Cloth (в Blender) или привяжи через Surface Deform к деформированной сетке | ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 02: GENESISCORE LOCAL (OLLAMA)](setup_02_genesiscore_local.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 04: HEADLESS RENDER (CLI / CI/CD)](setup_04_headless_render.md), [SETUP 05: RENDER FARM (FLAMENCO)](setup_05_render_farm.md) - **По темам:** [P03: MODIFIERS STACK](../prompts/03_modifiers_stack.md), [P21: PHYSICS SIMULATION](../prompts/21_physics_simulation.md), [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md) - **Теги:** #clothing, #setup --- # SETUP 22: RIZOMUV BRIDGE Tags: #setup #topology # SETUP 22: RIZOMUV BRIDGE **ID:** SETUP-22 | **Type:** UV Pro | **Difficulty:** ⭐⭐ | **Time:** 30-60 min **RU/EN Bilingual** > Когда применять: когда развёртка сделана, но разложена плохо — острова разного размера, текстура растянута, швы не там. RizomUV даёт честный контроль над площадью островов и предсказуемые швы, чего не дают встроенные инструменты Blender. ## 🎯 ЦЕЛЬ Настроить обмен UV между Blender и RizomUV так, чтобы развёртка приходила обратно в тот же объект, в правильном масштабе и с заданными швами. Научиться делать три вещи: авто-швы по углу, пакинг с контролем минимальной длины текстуры и раскладку по сетке UDIM для больших текстур. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |-----------|--------|-------| | Blender | 4.2 LTS+ | источник геометрии и приём UV | | RizomUV Bridge / RizomUV | 2020.3+ | работа с UV | | Лицензия RizomUV | на момент написания коммерческая, есть пробный период | сам инструмент | | Модель с развёрткой либо без | — | если развёртки нет, её надо построить до обмена | > [!NOTE] > У RizomUV сложная история с лицензиями и режимами (Bridge / Islands / UV Pack), и состав функций менялся между крупными релизами. Проверь, что в твоей версии есть именно те инструменты, что описаны ниже (Auto Seams, Scaling, Layout), прежде чем строить пайплайн на них. Ничего из перечисленного ниже не является бесплатным и гарантированным — это коммерческий продукт. ## 🛠️ НАСТРОЙКА ### 1. Передать объект в RizomUV ```text RizomUV → Preferences → Miscellaneous • Subdivision level: выбери уровень (обычно 1 — RizomUV не нужен full-res) • Risky mode / LOD: для больших мешей Blender → File → Export → FBX (.fbx) с выбором только нужного объекта В настройках экспорта: ✓ Apply Modifiers — обязательно, иначе RizomUV увидит «голый» меш Path Mode: Copy Limit to: Selected Objects Альтернатива без файла: буфер обмена RizomUV умеет читать геометрию из clipboard (формат OBJ). В Blender: выделить объект → Ctrl+C в контексте, где копируется выделение как OBJ (обычно Edit → Copy умеет), затем в RizomUV вставить. Формат обмена через clipboard менялся — проверь свою версию. ``` > [!NOTE] > Простое и надёжное правило: **FBX через файл работает всегда**, clipboard — удобство, которое может не сработать. Для CI и повторяемых прогонов пиши только файловый путь. ### 2. Авто-швы по углу ```text В RizomUV: Toolbars → UV Seams / Seams Auto Seams (по углу): • Enable Auto Seams — включить • Angle: значение в градусах Как выбрать угол: • 30° — много мелких деталей, ювелирка, заклёпки • 66° — универсальное значение для большинства объектов • 80° — только жёсткие рёбра (куб, панель) • 90° — фактически отключает: рёбра под прямым углом не режутся ``` Ручной контроль поверх авто-швов: ```python # Упрощённая эталонная развёртка в Blender — то, что RizomUV делает, # но похуже. Полезно как запасной вариант и как проверка: если результат # Blender приемлем, возможно RizomUV не нужен. import bpy, math def unwrap_angle_based(obj_name="Body", angle_deg=66.0, margin=0.02): """ angle_based режет по рёбрам, острые нежели angle_limit. Углы задаются в радианах. """ ob = bpy.data.objects.get(obj_name) if not ob: print("нет объекта:", obj_name); return bpy.ops.object.select_all(action='DESELECT') ob.select_set(True) bpy.context.view_layer.objects.active = ob # Гладкие полигоны НЕ должны резаться — иначе развёртка распадается if ob.data.use_auto_smooth if hasattr(ob.data, "use_auto_smooth") else False: for p in ob.data.polygons: p.use_smooth = True bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.uv.unwrap(method='ANGLE_BASED', angle_limit=math.radians(angle_deg)) bpy.ops.uv.pack_islands(margin=margin, rotate=True) bpy.ops.object.mode_set(mode='OBJECT') print("unwrap:", ob.name, "| UV-слоёв:", len(ob.data.uv_layers)) ``` ### 3. Пакинг: контроль минимальной длины текстуры Главная метрика пакинга — **texel density** (пиксели на единицу поверхности). В RizomUV она выравнивается автоматически, и это то, ради чего инструмент и нужен. ```text RizomUV → Toolbars → Layout / Scaling: • Normalize / Scale Island — выровнять размер островов • Scale — подвигать вручную отдельный остров • Target Texture Size (или Average Island Scale) — целевая плотность Настройки пакинга, которые стоит зафиксировать: • Padding / Margin: 4–16 px (при 2K). 2 px — экономия памяти, но при бейкe видна «расфокусировка» на швах. • Rotate/Flip islands: включить, иначе упакует непредсказуемо • Метрика расстояния: Straightener Distance — лучше сохраняет форму ``` ```python # Измерение texel density в Blender — способ проверить, выровнял ли RizomUV import bpy from mathutils import Vector def texel_density_report(obj_name="Body", texture_size=2048): ob = bpy.data.objects.get(obj_name) if not ob or not ob.data.uv_layers: print("нет UV"); return uv = ob.data.uv_layers.active.data u_extent = max(d.uv.x for d in uv) - min(d.uv.x for d in uv) # Средняя площадь островов в UV-пространстве tris = sum(1 for p in ob.data.polygons) * 2 uv_area = sum(abs((d.uv[i].x - d.uv[j].x) * (d.uv[k].y - d.uv[j].y)) for poly in ob.data.polygons for i, j, k in zip(poly.loop_indices, poly.loop_indices[1:] + poly.loop_indices[:1], poly.loop_indices[2:] + poly.loop_indices[:1])) if uv_area <= 0: print("нулевая UV-площадь — развёртка сломана"); return px_per_uv = texture_size / max(u_extent, 1e-6) print(f"объект: {ob.name}") print(f"UV-протяжённость: {u_extent:.3f}") print(f"треугольников: {tris:,}") print(f"пикселей на единицу UV: {px_per_uv:.0f}") print(f"средняя площадь острова: {uv_area/tris*1e4:.2f} (в 10⁻⁴ долях UV)") texel_density_report() ``` ### 4. UDIM: раскладка по сетке тайлов ```text RizomUV: • Выделить все острова → Tools → Layout • В настройках раскладки выбрать UV Tile Grid (4×4, 8×8 …) • Острова разложатся по тайлам 1001, 1002, … с равномерной плотностью В Blender: Image Editor → Image → Open → выбрать ВСЕ файлы 1001.png, 1002.png … Blender соберёт их в один датаблок с source='TILED' Нода Image Texture работает с ними как с одной текстурой ``` Проверка, что тайлы действительно собрались: ```python import bpy for img in bpy.data.images: if getattr(img, "source", None) == 'TILED': print(f"{img.name}: тайлов {len(img.tiles)}, " f"размер тайла {img.tiles[0].size[0]}×{img.tiles[0].size[1]}") ``` ### 5. Вернуть UV в Blender ```text Вариант A (основной): FBX обратно RizomUV → File → Export FBX → в Blender File → Import → FBX (.fbx) В настройках импорта: ✓ UVs — обязательно ✓ Normals Осторожно: импорт FBX создаёт НОВЫЙ объект. Старый удали, иначе в сцене будет дубль. Вариант B (точнее): только UV-карта RizomUV умеет экспортировать UV-карты отдельно (формат .uv / .json — зависит от версии). В Blender: Image Editor → выбрать объект → Image → Open → выбрать этот файл Он наложится на существующий UV-слой. Вариант C (для CI): встроенный формат Blender Blender умеет читать и писать свой формат раскладки: bpy.ops.uv.export_layout(filepath="layout.uv") # экспорт раскладки bpy.ops.uv.import_layout(filepath="layout.uv") # импорт обратно Это НЕ совместимо с RizomUV — только для внутреннего обмена/бэкапа. ``` > [!NOTE] > Набор операторов `uv.import_layout` / `uv.export_layout` и их аргументы менялись между версиями Blender. Проверь `bpy.ops.uv.export_layout.get_rna_type().properties.keys()` перед использованием. Формат `.uv` — внутренний формат Blender, RizomUV его не читает. ### 6. Ручные швы и проверка результата Авто-швы — это отправная точка, а не ответ. Два места почти всегда требуют рук: ```text • Сгибы, которые не должны разворачиваться (жёсткие рёбра мебели, складки, заклёпки) — им назначают UV Seam поверх грани • Скрытые зоны (внутренняя сторона дна, зона под юбкой) — им назначают Project from View, чтобы упаковать туда мелкий островок, а не тратить на них пол-атласа ``` ```python # Ручные швы в Blender: маркировка рёбер и повторная проверка плотности import bpy, math def mark_seams_by_angle(obj_name="Body", angle_deg=66.0): """Отмечает швами рёбра, острее порога. Основа для ручной правки.""" ob = bpy.data.objects[obj_name] bpy.ops.object.select_all(action='DESELECT') ob.select_set(True); bpy.context.view_layer.objects.active = ob # Уже существующие швы снимаем, чтобы не копить мусор bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.uv.seams_from_islands(mark_seams=True, mark_sharp=False) bpy.ops.object.mode_set(mode='OBJECT') print("швы с островов проставлены — дальше рисуй руками в UV-редакторе") def pack_report(obj_name="Body", texture_size=2048): """Проверка после пакинга: сколько места занято и какая плотность.""" ob = bpy.data.objects[obj_name] uv = ob.data.uv_layers.active.data xs = [d.uv.x for d in uv]; ys = [d.uv.y for d in uv] w = max(xs) - min(xs); h = max(ys) - min(ys) print(f"UV-протяжённость: {w:.3f} × {h:.3f}") print(f"пикселей по ширине: {texture_size * w:.0f} из {texture_size}") if w < 0.9 or h < 0.9: print("внимание: атлас занят слабо — есть смысл поднять разрешение тайла") ``` Признаки плохой развёртки, которые видно сразу: | Симптом | Причина | Что делать | |---------|---------|-----------| | Светлая полоса поперёк текстуры | Перекрывающиеся острова | Проверить острова на перекрытие, распаковать заново | | Артефакты бейка вокруг каждого острова | Padding мал для фильтра | Поднять padding, перезапечь | | Одна часть текстуры резче другой | Texel density не выровнен | RizomUV: Normalize Island Scale; либо распаковать острова по площади | | Полоса «размытия» на сгибе | Остров пересекает острый угол | Добавить шов на ребре | | Текстура «едет» при анимации | Развёртка деформируется вместе с мешем | Проверить, что UV не лежит на перемещаемых вершинах | ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```python # Диагностика развёртки в Blender: перекрытия, острова, плотность import bpy def uv_health(obj_name="Body", texture_size=2048): ob = bpy.data.objects.get(obj_name) if not ob or not ob.data.uv_layers: print("нет UV-слоя"); return uv = ob.data.uv_layers.active.data uv_data = [d.uv.copy() for d in uv] # Острова по связанности рёбер mesh = ob.data edge_faces = {} for poly in mesh.polygons: ls = list(poly.loop_indices) for a, b in zip(ls, ls[1:] + ls[:1]): key = tuple(sorted((a, b))) edge_faces.setdefault(key, []).append(poly.index) seen, islands = set(), 0 for start in range(len(mesh.loops)): if start in seen: continue islands += 1 seen.add(start) stack = [start] while stack: cur = stack.pop() for a, b in ((cur, (cur + 1) % len(mesh.loops)), ((cur - 1) % len(mesh.loops), cur)): for f in edge_faces.get(tuple(sorted((a, b))), []): for li in mesh.polygons[f].loop_indices: if li not in seen: seen.add(li); stack.append(li) xs = [v.x for v in uv_data]; ys = [v.y for v in uv_data] print(f"объект : {ob.name}") print(f"островов : {islands}") print(f"диапазон UV : X {min(xs):.3f}..{max(xs):.3f} Y {min(ys):.3f}..{max(ys):.3f}") print(f"покрытие : {texture_size * (max(xs)-min(xs)):.0f} px по ширине") print("вне 0..1? :", "ДА — развёртка вылезла за тайл" if max(xs) > 1.01 or min(xs) < -0.01 or max(ys) > 1.01 else "нет") uv_health() ``` ```python # Раскладка по сетке тайлов средствами Blender — запасной путь, если # RizomUV под рукой нет import bpy def tile_uv(obj_name="Body", grid=4): ob = bpy.data.objects[obj_name] bpy.ops.object.select_all(action='DESELECT') ob.select_set(True); bpy.context.view_layer.objects.active = ob bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.uv.unwrap(method='CONFORMAL') # минимизирует искажения формы bpy.ops.uv.pack_islands(margin=0.005) bpy.ops.object.mode_set(mode='OBJECT') print("раскладка готова. Оператор раскладки по сетке тайлов —") print("UV → Image → New Image (или Image Editor → Image), выбери UV Tile Grid.") ``` | Параметр | Значение | Когда менять | |----------|----------|--------------| | Auto Seams angle | 66° | 30° для мелочи, 80° для кубических форм | | FBX `Apply Modifiers` | включён | выключи, если рисуешь уже с применённым сабдивом | | RizomUV Subdivision Level | 1 | 2 для мелких деталей, выше 3 — уже тяжело | | Padding/Margin | 4–16 px @ 2K | 2 px при 8K экономит память, но ореол на швах | | Rotate islands | включён | без него пакинг неоптимален по форме | | UDIM grid | 4×4 для одной 2K-«страницы» | 8×8 при текстуре 16K | | `angle_limit` для `ANGLE_BASED` | 66° = 1.15 рад | значение по умолчанию в Blender — 66°, совпадает с привычкой | | `pack_islands(margin=)` | 0.005–0.02 | для текстуры с бейком ставь больше, чем для цветной | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |--------|---------|---------| | RizomUV открыл объект без детализации | FBX без `Apply Modifiers` | Переэкспортируй с галочкой Apply Modifiers | | В сцене два одинаковых объекта | Импорт FBX создал новый объект | Удали старый, переименуй импортированный | | Импорт FBX потерял UV | В настройках импорта снята галочка UVs | Импортируй заново с UVs | | Острова в Blender «поплыли» после обратного импорта | Разный масштаб UV между программами | Проверь диапазон UV: должен лежать в 0..1 (или в сетке тайлов) | | Текстура «мылится» в одном месте | Texel density не выровнен | RizomUV → Normalize Island Scale; либо в Blender распаковать острова по площади | | Бейк даёт чёрные швы | Padding меньше ширины фильтра бейка | Увеличь padding до 8–16 px и перезапеки | | UDIM-текстура не занимает весь меш | В Blender выбран один тайл вместо всех | Открой все файлы 1001… разом через Image Editor → Open | | Развёртка в одном месте вырождена (нулевая площадь) | Вырожденные UV из-за вырожденных фейсов | Удали дублирующиеся вершины: Mesh → Merge by Distance | | `TypeError` на `bpy.ops.uv.import_layout` | Аргументы оператора изменились | `get_rna_type().properties.keys()` | | Швы в RizomUV не совпадают с тем, что в Blender | Разная интерпретация flip островов | После импорта проверь острова на перевёрнутость: выдели остров и посмотри на нормали | ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 26: TOPOGUN / QUAD DRAW RETOPO](setup_26_topogun_quad.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 02: GENESISCORE LOCAL (OLLAMA)](setup_02_genesiscore_local.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 04: HEADLESS RENDER (CLI / CI/CD)](setup_04_headless_render.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 05: RENDER FARM (FLAMENCO)](setup_05_render_farm.md), [SETUP 06: CUSTOM MCP TOOLS](setup_06_custom_mcp_tools.md) - **Теги:** #setup, #topology --- # SETUP 23: REALITYCAPTURE SCAN TO BLENDER Tags: #blender #photogrammetry #setup # SETUP 23: REALITYCAPTURE SCAN TO BLENDER **ID:** SETUP-23 | **Type:** Photogrammetry | **Difficulty:** ⭐⭐⭐ | **Time:** 90-180 min **RU/EN Bilingual** > Когда применять: когда объект существует физически и его нужно «оцифровать»: ремонтный участок, артефакт, помещение, референс для пропов. Фотограмметрия даёт точную форму и цвет, но не даёт ни чистую топологию, ни пригодных для рендера карт — эти два шага делаются в Blender. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |-----------|--------|-------| | RealityCapture | 1.3 / 1.4 (для 1.4 — редакция и лицензия Epic) | выравнивание фото, построение модели и текстуры | | Камера с ручным фокусом | любая | съёмка с перекрытием кадров | | Мишени (кодовые метки) | опционально, но сильно помогают | ускоряют выравнивание | | Blender | 4.2 LTS+ | ретопология, запекание карт, подготовка к экспорту | | Quad Remesher или QuadriFlow | см. примечание | превращение треугольников в квады | | Достаточно VRAM | 8–12 ГБ для текстурирования 8K | текстура считается на GPU | > [!NOTE] > RealityCapture — коммерческий продукт, и условия использования менялись (после перехода к Epic). Проверь актуальную лицензию у Epic/Capturing Reality перед использованием в коммерческом проекте. Команды `rc-cli` в 1.3 и в 1.4 отличаются — ниже дана логика команд, а точный синтаксис стоит проверить через `rc-cli --help`. ## 🎯 ЦЕЛЬ Получить из фотосъёмки геометрию и цвет, которые можно поставить в сцену Blender: отфильтровать мусор, построить ретопологию для рендера, перенести фототекстуру на новую сетку через запекание и подготовить экспорт в игру или в рендер-ферму. ## 🛠️ НАСТРОЙКА ### 1. Съёмка: требования, которые решают всё ```text • Перекрытие между кадрами: 60–70% сбоку, 70–80% снизу вверх • Текстура не должна быть однотонной (проблема белых стен): либо метки, либо временная текстура изолентой / краской-спреем • Не снимать при прямом солнце: блики убивают текстурирование • Не двигать объект между кадрами • Снимать с расстояния, при котором объект занимает 60–80% кадра • Для мелких деталей — отдельная серия с макросъёмкой ``` ### 2. Обработка в RealityCapture: CLI-пайплайн ```text RealityCapture умеет работать без GUI через rc-cli. Логика команд (названия именно так и выглядят в ветке 1.x, но набор флагов проверяй через rc-cli --help): import — добавить фотографии в проект align — сопоставить кадры между собой (alignCameras в GUI) reconstruct — построить поверхность texture — развернуть текстуру calculate normals export — выгрузить модель (obj/fbx/ply) cleanup — удалить лишние части (scale-aware) Типичный порядок: rc-cli import --inputPath=photos/ --project=scan.rcp rc-cli align rc-cli reconstruct rc-cli texture rc-cli export --outputFile=scan.obj ``` В GUI тот же пайплайн выглядит так: `Align Images` → `Reconstruct` → `Texture` → `Export`. **Что важно выставить в GUI до текстурирования:** ```text Reconstruction → Model Quality: Medium/High Reconstruction → Decimation: 0.5–0.8 (меньше = грубее) Texturing → Texturing Mode: Basic — быстро, только цвет Consistent — средне, стабильнее на больших поверхностях Detail — долго, лучший результат Texturing → Texture Size: 4096 / 8192 Project → Metric Scale: задать по измерению — иначе модель будет произвольного масштаба ``` > [!NOTE] > Точные названия опций и их значения (в том числе `Max Decimation` и режимы текстурирования) менялись между версиями RealityCapture. Сверяйся с панелью своей версии и с `rc-cli --help`, а не с этим списком. ### 3. Импорт в Blender и оценка качества ```python # scan_import.py — Blender: Text Editor → Run Script import bpy, os SCAN = r"C:\scans\scan.obj" def import_scan(filepath=SCAN, target_size_m=None): """ Сразу фиксируем масштаб: фотограмметрия не знает реального размера, а сцена — знает. Ориентир: человек ~1.75 м, дверь ~2.0 м, автомобиль ~4.5 м. """ if not os.path.exists(filepath): print("нет файла:", filepath); return with bpy.context.temp_override(): bpy.ops.wm.obj_import( filepath=filepath, forward_axis='NEGATIVE_Z', up_axis='Y', global_scale=1.0, ) ob = bpy.context.view_layer.objects.active if not ob: print("импорт не дал активного объекта"); return print(f"{ob.name}: {len(ob.data.vertices):,} вершин, " f"{len(ob.data.polygons):,} полигонов") if target_size_m: import mathutils dims = ob.dimensions biggest = max(dims) factor = target_size_m / max(biggest, 1e-6) ob.scale = (factor, factor, factor) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) print(f"масштабировано до {target_size_m} м по наибольшей стороне, " f"коэффициент {factor:.4f}") return ob ob = import_scan(target_size_m=1.75) ``` ```python # Диагностика скана: размеры, материалы, состояние UV import bpy def scan_report(ob): d = ob.dimensions print("размеры (м):", f"{d.x:.2f} × {d.y:.2f} × {d.z:.2f}") print("UV-слои :", [uv.name for uv in ob.data.uv_layers] or "НЕТ") print("материалы :", [m.name if m else None for m in ob.data.materials] or "НЕТ") print("шейдинг :", "smooth" if any(p.use_smooth for p in ob.data.polygons) else "flat") # Отдельные острова-обломки обычно не примыкают к основной поверхности import bmesh bm = bmesh.new(); bm.from_mesh(ob.data) loose = sum(1 for v in bm.verts if not v.link_faces) print(f"висячих вершин: {loose:,}") bm.free() scan_report(ob) ``` ### 4. Очистка геометрии ```python # cleanup.py — Blender: Text Editor → Run Script import bpy, bmesh def cleanup_scan(obj_name="scan", merge_threshold=0.001, decimate_ratio=0.25): ob = bpy.data.objects.get(obj_name) if not ob: print("нет объекта"); return bpy.ops.object.select_all(action='DESELECT') ob.select_set(True) bpy.context.view_layer.objects.active = ob bpy.ops.object.mode_set(mode='EDIT') # 1) Слить почти совпадающие вершины — убирает дырявую сетку bpy.ops.mesh.remove_doubles(threshold=merge_threshold) # 2) Удалить висячие вершины bpy.ops.mesh.delete(type='VERT') # 3) Удалить мелкие грани-обломки try: bpy.ops.mesh.dissolve_degenerate(dist=merge_threshold, edges=True) except RuntimeError: pass bpy.ops.object.mode_set(mode='OBJECT') before = len(ob.data.polygons) # 4) Прореживание: у фотограмметрии бывает 5–50 млн полигонов if decimate_ratio < 1.0: bpy.ops.object.modifier_add(type='DECIMATE') dec = ob.context.view_layer.objects.active.modifiers["Decimate"] dec.decimate_type = 'COLLAPSE' # для треугольников dec.ratio = decimate_ratio dec.use_collapse_triangulate = True bpy.ops.object.modifier_apply(modifier=dec.name) print(f"полигонов: {before:,} → {len(ob.data.polygons):,}") cleanup_scan() ``` > [!NOTE] > `dissolve_degenerate` в некоторых версиях требует явного выделения рёбер и падает с `RuntimeError` при пустом выделении. Если так — пропусти этот шаг и сделай только слияние вершин: `remove_doubles` убирает 90% артефактов фотограмметрии. ### 5. Ретопология для рендера ```text Фотограмметрическая сетка годится только как референс формы и как источник запекания. Для рендера нужна quadrongon. Вариант A — Quad Remesher (аддон, платный/подписочный): Blender → Quad Remesher → Target = (очищенный скан или его проекция) Target Preset = Quad, Target Surface = Scan Получаем квадратную сетку на месте скана. Вариант B — QuadriFlow (аддон от Blender): Работает как «Remesh» в режиме Quad с направляющими кривыми. Вариант C — встроенный Voxel Remesh (бесплатно, но теряет детали): Object → Remesh → Octree Depth 7–8, Scale 0.9 Хорош как промежуточный этап перед бейком, плох как финал. ``` ```python # Промежуточный этап: воксельная ремеш-сетка, чтобы потом запечь на неё def voxel_remesh(obj_name="scan", voxel_size=0.01, adaptivity=0.0): ob = bpy.data.objects[obj_name] bpy.ops.object.select_all(action='DESELECT') ob.select_set(True); bpy.context.view_layer.objects.active = ob bpy.ops.object.voxel_remesh() # использует настройки ниже remesh = ob.data.remesh_voxel_size ob.data.remesh_voxel_size = voxel_size ob.data.remesh_voxel_adaptivity = adaptivity print("воксели:", voxel_size, "| адаптивность:", adaptivity) voxel_remesh() ``` ### 6. Запекание фототекстуры на новую сетку ```text Только в Cycles. Порядок: 1. Ретопология (low) и скан (high) в одной точке, рядом. 2. На low-меше — Image Texture с пустым изображением 2048×2048, НЕ Non-Color, привязанный к активной UV-карте. 3. Выделить low-меш (active) → выделить скан (в Shift выделения). 4. Baking → Type: Diffuse → нажать Bake. 5. Проверить результат: растянутые участки должны быть у краёв островов. ``` ```python # bake_setup.py — Blender: Text Editor → Run Script import bpy def setup_bake(low_name="scan_low", high_name="scan", bake_type='DIFFUSE', size=2048): scene = bpy.context.scene if scene.render.engine != 'CYCLES': print("внимание: активен", scene.render.engine, "— запекание только в Cycles") return low = bpy.data.objects[low_name] if not low: print("нет low-меша:", low_name); return # Пустое изображение — цель запекания img = bpy.data.images.get("BakeTarget") if img is None: img = bpy.data.images.new("BakeTarget", width=size, height=size) img.colorspace_settings.name = 'sRGB' # для DIFFUSE; для NORMAL — 'Non-Color' mat = low.data.materials[0] if low.data.materials else low.data.materials.new("M_Bake") if not mat.use_nodes: mat.use_nodes = True tree = mat.node_tree tree.nodes.clear() tex = tree.nodes.new("ShaderNodeTexImage") tex.image = img tex.uv_map = low.data.uv_layers.active.name if low.data.uv_layers else "" out = tree.nodes.new("ShaderNodeOutputMaterial") tree.links.new(tex.outputs["Color"], out.inputs["Surface"]) mat.use_fake_user = True bpy.ops.object.select_all(action='DESELECT') low.select_set(True) bpy.context.view_layer.objects.active = low high = bpy.data.objects[high_name] if high: high.select_set(True) # Активным остаётся low — в high выбранном запекается цвет из его текстур print("high:", high.name, "| материалы:", [m.name for m in high.data.materials if m]) print("готово к запеканию:", bake_type, "→", img.name, size, "px") print("теперь Baking → Bake") setup_bake() ``` ### 7. Масштаб: как не ошибиться с размером Фотограмметрия без Metric Scale даёт модель произвольного размера. Три способа это починить, от лучшего к худшему: ```text 1. Scale bar в проекте RC (точный путь): Панель Measurements → Add Scale Bar → выбрать 2 точки → вписать расстояние. Точки ставятся на грани, между которыми ты знаешь реальное расстояние. 2. Известный объект в кадре: В кадр кладётся предмет известного размера (плинтус, монета, кубик калибровочный). По нему ставится scale bar. Удобно, когда объект мелкий и точный масштаб не критичен. 3. Масштабирование в Blender по измеренной оси (шаг 3): Измерить в кадре высоту спинки стула и подогнать. Ошибка 10–20% — приемлемо для большинства задач. ``` ```python # Масштабирование по оси с измерением: тот же fit_by_measured_dimension, # но в применении к скану def fit_by_reference_object(scan_name="scan", reference_name="chair_ref", reference_height_m=0.92): """ Если в сцене есть эталонный объект известного размера — масштабируем скан под него. """ scan = bpy.data.objects.get(scan_name) ref = bpy.data.objects.get(reference_name) if not scan or not ref: print("нужны оба объекта"); return factor = reference_height_m / max(ref.dimensions.z, 1e-6) scan.scale = (factor, factor, factor) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) print(f"эталон {ref.name}: {ref.dimensions.z:.3f} м → коэффициент {factor:.4f}") print(f"скан {scan.name}: {tuple(round(d, 3) for d in scan.dimensions)}") ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```bash # Реалистичный ориентир по размеру сцены: # фото 24 МП, средняя дистанция, объект 1 м: # ~600–1200 снимков → 1–4 млн полигонов → текстура 4K # фото 45 МП, тот же объект: # ~300–600 снимков → 2–8 млн полигонов # Больше снимков лучше по качеству, но не линейно: 400 снимков # обычно дают 90% результата, 1200 — оставшиеся 10%. # Порядок команд: сначала выравнивание, потом всё остальное. # Если выравнивание плохое, остальные команды тратят время впустую. ``` ```python # Считаем «качество» скана до запекания — быстрый чек-лист import bpy def scan_checklist(obj_name="scan"): ob = bpy.data.objects.get(obj_name) checks = { "UV-слой есть": bool(ob.data.uv_layers), "Материал с текстурой": any(m and m.use_nodes for m in ob.data.materials), "Сглаженные нормали": any(p.use_smooth for p in ob.data.polygons), "Нет незамкнутых краёв": not any(len(p.vertices) < 3 for p in ob.data.polygons), "Разумный размер (0.1–100 м)": 0.1 < max(ob.dimensions) < 100, } for k, v in checks.items(): print(f"[{'OK ' if v else 'НЕТ'}] {k}") return all(checks.values()) scan_checklist() ``` | Параметр | Значение | Когда менять | |----------|----------|--------------| | Перекрытие кадров | 60–80% | ниже 50% — дыры в геометрии | | Reconstruction Decimation | 0.5–0.8 | 1.0 = максимум деталей и максимум шума | | Texturing Mode | Consistent / Detail | Basic — только для проверки, не для продакшена | | Texture Size | 4096 / 8192 | 8192 на 12 ГБ VRAM — предел для большинства карт | | `remove_doubles` threshold | 0.001 м (1 мм) | для крупных объектов 0.005–0.01 | | Decimate ratio | 0.2–0.4 | ретопология делается дальше, тут только чистка | | Voxel size | 0.005–0.02 м | мельче → больше полигонов и меньше шума | | Bake size | 2048 на остров | 4096, если сцена крупная и память позволяет | ### Про цвета и color management Фототекстура — это sRGB-данные из JPEG. Если запечь её в линейное пространство или наоборот, получится «грязь». Отдельно: RealityCapture и Blender по-разному трактуют белый баланс, поэтому сцена почти всегда требует ручной правки через Hue/Saturation/Color Balance в композитной ноде. ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |--------|---------|---------| | Align находит мало кадров | Мало перекрытия, нет текстуры, плохая освещённость | Добавь метки, сними с другим светом, уменьши шаг съёмки | | Модель в «пузырях» | Слишком высокий decimation или шумная текстура | Decimation 0.6, текстурирование Consistent | | Дыры в геометрии | Не хватило ракурсов с нижней стороны | Доснять низ и перевыровнять (Align Increment) | | Текстура мыльная | Texturing Mode = Basic или слишком мало снимков | Consistent/Detail + пересчёт на полном наборе | | Модель произвольного размера | Не задан Metric Scale | Задать в проекте RC и пересчитать, либо масштабировать в Blender | | Blender зависает на импорте | 20 млн полигонов OBJ | Импортируй в фоне с прореживанием, либо экспортируй из RC сразу прорежённым | | `remove_doubles` не помогает | Порог меньше реального расстояния между вершинами | Увеличь threshold до 0.005 и повтори | | Запекание не стартует | Активен EEVEE или не выделен high-объект | Cycles + два объекта в выделении, active = low | | Запечённая текстура в одном цвете | На high-объекте нет материала с текстурой | Проверь `high.data.materials` — там должен быть результат RC | | После ретопологии форма «поплыла» | Ретопология построена не поверх скана | Surface Deform или Quad Remesher с целью = скан | | Импорт OBJ вверх ногами | Не заданы оси | `forward_axis='NEGATIVE_Z', up_axis='Y'` | ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 07: CI/CD INTEGRATION](setup_07_ci_cd.md), [SETUP 09: BLENDERBOT / AI CODING AGENT](setup_09_blenderbot_ai.md), [SETUP 10: BLENDER + COMPOSITING (NUKE/AFTER EFFECTS ROUNDTRIP)](setup_10_compositing_roundtrip.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 11: BLENDER + TOUCHDESIGNER REALTIME VISUALS](setup_11_touchdesigner.md), [SETUP 12: BLENDER + UNREAL ENGINE 5 LIVE LINK](setup_12_unreal_livelink.md) - **Теги:** #blender, #photogrammetry, #setup --- # SETUP 24: AGISOFT METASHAPE Tags: #photogrammetry #setup # SETUP 24: AGISOFT METASHAPE **ID:** SETUP-24 | **Type:** Photogrammetry | **Difficulty:** ⭐⭐⭐ | **Time:** 60-150 min **RU/EN Bilingual** > Когда применять: когда фотограмметрию нужно повторить, а не сделать один раз: серия однотипных измерений, регулярный контроль объекта, батч из сотни объектов. Главное отличие от RealityCapture — у Metashape полноценная среда автоматизации: скрипты на Python, батч-обработка и повторяемость параметров. ## 🎯 ЦЕЛЬ Описать пайплайн Metashape как воспроизводимый скрипт: выравнивание → разреженное и плотное облако → модель → UV → текстура → экспорт в OBJ/FBX. На выходе — `.py`-скрипт, который прогоняет съёмку от начала до конца без GUI, и понимание того, где в Blender продолжать: ретопология, запекание карт, экспорт. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |-----------|--------|-------| | Agisoft Metashape | 2.1 / 2.2 (Python 3) либо 1.9 (Python 2 — отдельно) | обработка | | Лицензия | Standard/Pro/Education — влияет на число CPU-ядер и на экспорт | скорость | | Python, совместимый с Metashape | версия зависит от мажорной версии Metashape | скрипты и батч | | Blender | 4.2 LTS+ | приём результата | | Scale bars либо референсные метки | для масштаба | реальный размер модели | | Quad Remesher / QuadriFlow | см. примечание | ретопология для рендера | > [!NOTE] > Metashape 2.x использует Python 3, Metashape 1.9 — Python 2. Скрипты для них **не взаимозаменяемы**. Кроме того, набор методов `chunk` между версиями расширялся. Перед использованием скрипта проверь `dir(chunk)` в консоли Metashape — это надёжнее любого списка в статье. ## 🛠️ НАСТРОЙКА ### 1. Порядок и параметры шагов ```text Классический порядок: 1. Загрузка фото + Reference-панель (опционально) 2. Alignment (сопоставление кадров) → разреженное облако 3. Оценка точности, добавление scale bar 4. Dense cloud (плотное облако) → XYZ-цвет 5. Model (поверхность) 6. Texture coordinates (UV) 7. Texture (натяжка текстуры) 8. Export (OBJ / FBX / PLY) ``` Где менять качество (в GUI и в скрипте параметры называются по-разному, но смысл один): | Шаг | Быстро | Средне | Качественно | |-----|--------|--------|-------------| | Alignment | `downscale=4`, `generic_preselection=False` | `downscale=2`, `generic_preselection=True` | `downscale=1` | | Dense cloud | `downscale=4` | `downscale=2` | `downscale=1`, `filter_stationary_points` | | Model | `face_count=Low` | `face_count=Medium` | `face_count=High`, `face_count_quality` | | Texture | 2048 | 4096 | 8192, `texture_size`, `ghosting_filter`, `white_balance` | ### 2. Скрипт: полный проход ```python # pipeline.py — запускается в metashape.exe --run pipeline.py import Metashape, os, sys chunk = Metashape.app.document.chunk # Каталоги. Используй os.path.join: обратные слэши в строках-константах # ломаются при переносе скрипта на другую ОС. PHOTOS = r"C:\scans\chair\images" OUT = r"C:\scans\chair" DOWNSCALE = 2 # alignment DENSE_DOWNSCALE= 2 FACE_COUNT = Metashape.Medium TEXTURE_SIZE = 4096 # 1. Список фото: расширения верхнего регистра — разные на камерах exts = ('.jpg', '.jpeg', '.png', '.tif', '.tiff') photos = [] for name in os.listdir(PHOTOS): if name.lower().endswith(exts): photos.append(os.path.join(PHOTOS, name)) photos.sort() # порядок имён влияет на нумерацию кадров и на стабильность print("фотографий:", len(photos)) # 2. Загрузка. chunk.addPhotos умеет брать список либо снимать кадры с камеры chunk.addPhotos(photos) # 3. Ограничить перекрытие между кадрами (сильно ускоряет, если снимали # с избыточностью) chunk.matchPhotos(downscale=DOWNSCALE, generic_preselection=True, overlap=0.75) # 4. Выравнивание + разреженное облако chunk.alignCameras() print("камер:", len(chunk.cameras), "| точек:", len(chunk.tie_points)) # 5. Фильтрация: выкидываем точки с высокой ошибкой и слабой связностью chunk.filterTiePoints(min_viewing_angle=10, reprojection_error=0.5, reconstruction_error=0.5) # 6. Плотное облако chunk.buildDepthMaps(downscale=DENSE_DOWNSCALE, filter_mode='blur') chunk.buildPointCloud() # 7. Модель chunk.buildModel(source_data=Metashape.PointCloudData, face_count=FACE_COUNT, face_count_quality=50) # 8. Развёртка и текстура chunk.buildUV(page_count=1, texture_size=TEXTURE_SIZE) chunk.buildTexture(texture_size=TEXTURE_SIZE, ghosting_filter=True, white_balance=False) # 9. Экспорт. save_as=False — не трогаем .psx-проект на диске chunk.exportModel(path=os.path.join(OUT, "model.obj"), texture_format=Metashape.ImageFormatJPEG, texture_size=TEXTURE_SIZE, save_as=False, export_format=Metashape.ModelFormatOBJ) print("готово:", os.path.join(OUT, "model.obj")) ``` > [!NOTE] > Имена аргументов в Metashape менялись между версиями (например, параметры фильтрации и `save_as`). Если скрипт падает на `TypeError`, сначала проверь: `help(chunk.buildModel)`, `help(chunk.buildTexture)`, `dir(chunk.filterTiePoints)`. Это 10 секунд и точнее, чем любая статья. ### 3. Масштаб: scale bar и reference Без этого модель произвольного размера, и в Blender всё придётся угадывать. ```text Вариант A — Scale Bar (две точки + расстояние): 1. Взять известное расстояние (длина стола, рост человека) 2. В Metashape: правый клик на 2 точки → Add Scale Bar → вписать длину 3. Либо скриптом: chunk.addScaleBar(point1_id, point2_id, distance) Точки берутся из tie_points: chunk.tie_points.items() Вариант B — Reference-панель (координаты известны): 1. Снимки с известными GPS-координатами грузятся с выравниванием по координатам, потом: chunk.addMarker(point_id) marker.reference.location = Metashape.Vector([lon, lat, alt]) marker.reference.altitude = ... marker.reference.coordinate_system = "EPSG:4326" 2. Либо CRS всего проекта: chunk.crs = Metashape.CoordinateSystem("EPSG:4326") chunk.crs.estimate_scale() # масштаб выводится из GPS ``` ```python # Масштаб по GPS — полезно для съёмки с дрона и для больших объектов def apply_geographic_scale(chunk): chunk.crs = Metashape.CoordinateSystem("EPSG:4326") ok = chunk.crs.estimate_scale() print("масштаб вычислен:", ok) if ok: print("координаты центра:", chunk.center) return ok ``` ### 4. Батч: серия объектов ```python # batch.py — цикл по всем съёмкам. Запускать на отдельной машине # или в фоне: Metashape держит память между проектами. import Metashape, os, glob, sys BASE = r"C:\scans" for proj in sorted(glob.glob(os.path.join(BASE, "*"))): images = os.path.join(proj, "images") if not os.path.isdir(images): continue doc = Metashape.Document() chunk = doc.chunk photos = sorted( os.path.join(images, n) for n in os.listdir(images) if n.lower().endswith(('.jpg', '.jpeg', '.png')) ) print("=" * 40, proj, len(photos), "фото") chunk.addPhotos(photos) chunk.matchPhotos(downscale=2, generic_preselection=True, overlap=0.75) chunk.alignCameras() chunk.filterTiePoints(reprojection_error=0.5) chunk.buildDepthMaps(downscale=2, filter_mode='blur') chunk.buildPointCloud() chunk.buildModel(source_data=Metashape.PointCloudData, face_count=Metashape.Medium) chunk.buildUV(page_count=1, texture_size=4096) chunk.buildTexture(texture_size=4096, ghosting_filter=True) doc.save(os.path.join(proj, "project.psx")) chunk.exportModel(path=os.path.join(proj, "model.obj"), save_as=False) print("OK", proj) ``` ```bash # Запуск headless на Windows (metashape.exe вместо .sh на Linux/macOS) "C:\Program Files\Agisoft\Metashape Pro\metashape.exe" \ -r pipeline.py # -r = запустить скрипт -platform offscreen # без окна; полезно для сервера ``` ### 5. Blender: приём и продолжение работы ```python # metashape_import.py — Blender: Text Editor → Run Script import bpy, os MODEL = r"C:\scans\chair\model.obj" def import_metashape(filepath=MODEL): if not os.path.exists(filepath): print("нет файла:", filepath); return with bpy.context.temp_override(): bpy.ops.wm.obj_import(filepath=filepath, forward_axis='NEGATIVE_Z', up_axis='Y') ob = bpy.context.view_layer.objects.active print(f"{ob.name}: {len(ob.data.vertices):,} v, {len(ob.data.polygons):,} f, " f"размер {tuple(round(d, 3) for d in ob.dimensions)}") return ob ob = import_metashape() # То же для облака точек (PLY). Расширение импорта зависит от версии # Blender — в 4.x это wm.point_cloud_import, в 3.x был import_mesh.ply # bpy.ops.wm.point_cloud_import(filepath=r"C:\scans\chair\cloud.ply") ``` ```python # Проверка масштаба: если модель «примерно 1 м» — можно работать, # если 0.01 или 100 — возвращаться в Metashape за scale bar import bpy ob = bpy.context.view_layer.objects.active if ob: big = max(ob.dimensions) verdict = ("подозрительно мало" if big < 0.2 else "подозрительно много" if big > 50 else "правдоподобно") print(f"наибольшая сторона: {big:.3f} м — {verdict}") print("ориентиры: стул ~0.6, стол ~0.8, дверь 2.0, человек 1.75, авто 4.5") ``` ### 5. Лидар и другие входные данные Фотограмметрия — не единственный вход. Metashape умеет работать и с другой геометрией, и это меняет весь пайплайн: ```text Вход Что получается Особенности ───────────────────────── ───────────────────────────── ────────────────────── Фотографии с камеры Сетка + цвет, произвольный Стандартный случай масштаб Лидар (DJI, REVO, Leica) Точная форма, слабый цвет Лидарные точки импортируются отдельным шагом; для текстуры всё равно нужны фотографии Скан из мобильного Сетка среднего качества Проблемы с масштабом, см. приложения с известными размерами SETUP 25 Одиночные размеры Один вымеренный объект Нужен для VR-калибровки Изображения с EXIF GPS Реальный масштаб из координат crs.estimate_scale() ``` ```python # Импорт лидарных точек: путь зависит от версии Metashape и типа файла # (форматы: LAS/LAZ от DJI и Leica, proprietary — от Matterport и др.) def import_lidar(chunk, path): chunk.importPointCloud(path=r"C:\scans\dronelidar\points.ply", format=Metashape.PointCloudFormatPLY) print("точек после импорта лидара:", len(chunk.tie_points)) # Дальше: buildPointCloud / buildModel с source_data=PointCloudData ``` > [!NOTE] > Имена констант форматов и сигнатура `importPointCloud` различаются между версиями Metashape 1.x и 2.x. Проверь `help(chunk.importPointCloud)` в консоли Metashape перед использованием. Также: лидарные форматы — LAS/LAZ/PLY, а не proprietary-файлы производителей; конвертация нужна отдельным инструментом. ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```text Ориентиры по объёму данных (средняя дистанция, объект ~1 м): фото 24 МП 400–800 снимков → модель 1–3 млн полигонов фото 45 МП 200–500 снимков → модель 2–6 млн полигонов Лидар (DJI/REVO) + текстуры → модель 5–20 млн, резче форма Время (8 ядер CPU, без GPU): Alignment 400 фото ~5–15 мин Dense cloud downscale=2 ~20–60 мин Model (High) ~10–30 мин Texture 4K ~10–40 мин Итого: 45–150 мин на объект ``` ```python # Инструмент, который экономит часы: логирование прогресса в файл import Metashape, time def timed(label, fn, *args, **kwargs): t0 = time.time() result = fn(*args, **kwargs) dt = time.time() - t0 with open(r"C:\scans\timing.log", "a", encoding="utf-8") as f: f.write(f"{label:24s} {dt:8.1f}s\n") print(f"{label:24s} {dt:8.1f}s") return result chunk = Metashape.app.document.chunk timed("alignCameras", chunk.alignCameras) timed("buildDepthMaps", chunk.buildDepthMaps, downscale=2) timed("buildPointCloud", chunk.buildPointCloud) timed("buildModel", chunk.buildModel, source_data=Metashape.PointCloudData, face_count=Metashape.Medium) ``` | Параметр | Значение | Когда менять | |----------|----------|--------------| | `matchPhotos.downscale` | 4 черновик, 2 рабочий, 1 финал | меньше = точнее и медленнее | | `overlap` | 0.75 | выше 0.8, если кадры сняты с запасом | | `reprojection_error` | 0.3–0.6 | ниже 0.3 отсекает слишком много точек | | `filter_mode` в buildDepthMaps | `blur` | попробуй `none`, если `blur` теряет детали | | `face_count` | Low/Medium/High | High = в разы больше времени | | `page_count` в buildUV | 1 | 2 и больше — раскладка на несколько тайлов | | `texture_size` | 4096 | 8192 требует много RAM | | `ghosting_filter` | `True` | обязателен при движущемся объекте/людях в кадре | | `white_balance` | `True` | выравнивает цвет между кадрами; иногда портит задуманный оттенок | | `save_as=False` при экспорте | всегда | иначе перезапишет .psx-проект | ### Про экспорт: OBJ против FBX ```text OBJ + отдельный текстурный атлас: + простой, читается везде, текстура одним файлом − вершины раскрыты (дублирование на швах): файл в разы больше FBX: + общая топология (shared vertices), компактнее − UV и материалы интерпретируются по-разному в разных программах PLY: + для облака точек, большой XYZ-цвет − как меш бесполезен (нет граней) ``` ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |--------|---------|---------| | `alignCameras` сопоставил 10 из 400 кадров | Мало перекрытия, нет текстуры, блики | Метки/текстура, другой свет, переснять | | `TypeError` на любом методе скрипта | Версия Metashape не та, что ожидал скрипт | `help(chunk.buildModel)` в консоли, адаптируй вызов | | Модель размером 0.003 м | Нет scale bar | `addScaleBar` или `crs.estimate_scale()` | | Текстура размытая на краях | Мало ракурсов или `face_count` слишком низкий | Переснять основание, `face_count=High` | | Чёрные «призраки» в текстуре | Люди или движение в кадрах | `ghosting_filter=True`, переснять без людей в кадре | | Текстура в одном тоне, полосы | Пересвет на части кадров | Изменить экспозицию при съёмке, уменьшить диафрагму | | `buildModel` ест всю RAM | `face_count=High` + большое облако | Понизить face_count, увеличить downscale в dense cloud | | Экспорт OBJ весит больше, чем входные фото | Развёрнутая топология | Перейти на FBX или на экспорт с `save_as=False` | | Blender падает на импорте | 15 млн полигонов OBJ | Прорежить в Metashape на этапе Model, а не в Blender | | Импорт OBJ вверх ногами | Не заданы оси | `forward_axis='NEGATIVE_Z', up_axis='Y'` | | Батч упал на середине и потерял 3 часа | Нет промежуточного сохранения | `doc.save()` после каждого объекта; устойчивость важнее красоты | | `.psx` не сохраняется | Нет прав на каталог или проект read-only | Проверь путь; сохраняй в ASCII-каталог | ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 23: REALITYCAPTURE SCAN TO BLENDER](setup_23_realitycapture_scan.md), [SETUP 25: POLYCAM / LUMA / KIRI SCAN](setup_25_polycam_luma.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 02: GENESISCORE LOCAL (OLLAMA)](setup_02_genesiscore_local.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 04: HEADLESS RENDER (CLI / CI/CD)](setup_04_headless_render.md), [SETUP 05: RENDER FARM (FLAMENCO)](setup_05_render_farm.md) - **Теги:** #photogrammetry, #setup --- # SETUP 25: POLYCAM / LUMA / KIRI SCAN Tags: #photogrammetry #setup # SETUP 25: POLYCAM / LUMA / KIRI SCAN **ID:** SETUP-25 | **Type:** Mobile Scan | **Difficulty:** ⭐ | **Time:** 30-60 min **RU/EN Bilingual** > Когда применять: когда нужно быстро «оцифровать» объект на телефоне: референс пропов, ас sets-скан, замер помещения, прототип. Скорость съёмки измеряется минутами, качество — заметно ниже, чем у десктопной фотограмметрии, и почти всегда требует чистки в Blender. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |-----------|--------|-------| | Polycam или Luma AI или KIRI 3D Scan | актуальная из App Store / Google Play | съёмка и обработка на телефоне | | Телефон с LiDAR (iPhone Pro / iPad Pro) | — | качественно меняет результат: даёт глубину, а не только цвет | | Blender | 4.2 LTS+ | импорт, чистка, ретопология | | Quad Remesher / QuadriFlow | см. примечание | квадры для рендера | | Приложение для конвертации splat → mesh, если нужен меш | см. примечание | для Gaussian splatting | > [!NOTE] > Экосистема мобильного сканирования меняется быстрее всего во всём пайплайне: приложения добавляют и убирают режимы (NeRF, Gaussian splatting, LiDAR-фото), а экспорт меняет набор поддерживаемых форматов. Прежде чем строить на чём-то пайплайн, **сделай один тестовый экспорт и посмотри, что реально предлагает твоя версия приложения** — это 10 минут и снимает половину вопросов. ## 🎯 ЦЕЛЬ Получить из мобильного скана пригодный объект: разобраться в форматах экспорта (GLB, OBJ+текстуры, PLY, splat), понять, что Blender умеет импортировать, починить масштаб, убрать мусор и довести геометрию до состояния, в котором её можно ретополозировать и запечь. ## 🛠️ НАСТРОЙКА ### 1. Съёмка: правила, которые экономят пересъёмку ```text • Двигайся вокруг объекта, не останавливаясь на 5 секунд — приложениям нужен поток кадров, а не одиночные снимки • Сначала пройди на средней дистанции, потом вернись ближе для деталей: приложение само склеит два прохода • Не закрывай объект рукой в кадре — получишь дыру • Для помещений: сначала круг по периметру, потом по вертикали • Для мелких объектов: рента с расстояния 30–60 см и высоким ISO • Свет: равномерный. Жёсткая тень даёт «запечатанную» ошибку, которую склеить нельзя ``` ### 2. Форматы экспорта и что с ними делать | Формат | Что внутри | Blender | Когда выбирать | |---------|-----------|---------|----------------| | **GLB / glTF** | меш + встроенная текстура, PBR-материал | импортируется штатно | лучший выбор: один файл, всё внутри | | **OBJ + PNG** | меш + карты, развёртка | импортируется штатно | если GLB не получается | | **PLY** | облако точек (иногда с цветом) | импортируется в новых версиях | когда нужна геометрия без текстуры | | **Gaussian splat** (`.splat`, `.ply`, `.spz`) | набор «брызг» с параметрами | **напрямую не меш** | только для рендера в specialized-плеерах | > [!NOTE] > **Gaussian splatting не является мешем.** В Blender его нельзя импортировать как геометрию штатными средствами — это другой тип данных (анизотропные эллипсоиды с коэффициентами). Варианты: конвертация в меш сторонними инструментами (с потерей качества и увеличением размера) либо рендер сплата в отдельном плеерере и композитинг. Прежде чем строить пайплайн, убедись, что экспорт splat вообще доступен в твоей версии приложения и понимай, куда ты его понесёшь. ### 3. Импорт GLB в Blender ```python # mobile_import.py — Blender: Text Editor → Run Script import bpy, os def import_gltf(filepath=r"C:\scans\chair.glb"): if not os.path.exists(filepath): print("нет файла:", filepath); return before = set(bpy.data.objects) with bpy.context.temp_override(): bpy.ops.import_scene.gltf(filepath=filepath) new = set(bpy.data.objects) - before print("импортировано:", len(new)) for ob in sorted(new, key=lambda o: o.name): d = ob.dimensions print(f" {ob.name:24s} v={len(ob.data.vertices):>7,} " f"dims={d.x:.3f}×{d.y:.3f}×{d.z:.3f}") return new objs = import_gltf() ``` ```python # Диагностика после импорта: масштаб, UV, материалы, версиии import bpy def scan_report(): for ob in bpy.context.scene.objects: if ob.type != 'MESH': continue d = ob.dimensions big = max(d) verdict = ("подозрительно мало" if big < 0.2 else "подозрительно много" if big > 50 else "правдоподобно") print(f"{ob.name:22s} {big:8.3f} м — {verdict}") print(f" UV: {[u.name for u in ob.data.uv_layers] or 'НЕТ'}") print(f" материалы: {[m.name for m in ob.data.materials if m] or 'НЕТ'}") print("ориентиры: стул ~0.6, стол ~0.8, дверь 2.0, человек 1.75, авто 4.5") scan_report() ``` ### 4. Починка масштаба Мобильные приложения часто нормализуют объект «в куб 1×1×1» или считают в сантиметрах. Два способа: ```python # scale_fix.py — Blender: Text Editor → Run Script import bpy def fit_to_height(obj_name, target_m=0.45): """ Подгоняет объект под известный размер. Примеры: стул по высоте спинки 0.9 м, дверь 2.0 м, кружка по высоте 0.09 м, человек 1.75 м. """ ob = bpy.data.objects.get(obj_name) if not ob: print("нет объекта:", obj_name); return current = ob.dimensions.z # работаем по Z: приложения обычно # ориентируют объект вертикально if current < 1e-6: print("нулевая высота — объект вырожден"); return factor = target_m / current ob.scale = (factor, factor, factor) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) print(f"коэффициент {factor:.4f} → высота {ob.dimensions.z:.3f} м") def fit_by_measured_dimension(obj_name, axis, measured_m): """ axis: 0 = X (ширина), 1 = Y (глубина), 2 = Z (высота). measured_m — реальная замеренная величина. """ ob = bpy.data.objects[obj_name] current = ob.dimensions[axis] factor = measured_m / max(current, 1e-6) ob.scale = (factor, factor, factor) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) print(f"ось {axis}: {current:.4f} → {measured_m} м (×{factor:.4f})") # Пример: стул, высота спинки 0.9 м fit_to_height("Chair", 0.9) # Пример: измеренная ширина стола 1.4 м fit_by_measured_dimension("Table", 0, 1.4) ``` ### 5. Чистка мусора Типовой мусор мобильного скана: «пузыри» на фоне, «tentacles» от движения, дыры. ```python # cleanup.py — Blender: Text Editor → Run Script import bpy, bmesh from mathutils import Vector def cleanup_scan(obj_name="scan", merge_threshold=0.002, # 2 мм — сливает двойные вершины outlier_scale=0.02, # порог отбрасывания дальних точек smooth_iterations=2): ob = bpy.data.objects.get(obj_name) if not ob: print("нет объекта"); return bpy.ops.object.select_all(action='DESELECT') ob.select_set(True); bpy.context.view_layer.objects.active = ob # 1) Обрезка «пузырей» — геометрия, отстоящая дальше порога от центра bm = bmesh.new() bm.from_mesh(ob.data) coords = [v.co.copy() for v in bm.verts] cx = sum(c.x for c in coords) / len(coords) cy = sum(c.y for c in coords) / len(coords) cz = sum(c.z for c in coords) / len(coords) center = Vector((cx, cy, cz)) diag = max(ob.dimensions) limit = outlier_scale * diag loose = [v for v in bm.verts if (v.co - center).length > limit] bmesh.ops.delete(bm, geom=loose, context='VERTS') bm.to_mesh(ob.data) bm.free() print("удалено далёких вершин:", len(loose)) # 2) Слияние и удаление висячих вершин bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.remove_doubles(threshold=merge_threshold) bpy.ops.mesh.delete(type='VERT') bpy.ops.object.mode_set(mode='OBJECT') # 3) Сглаживание — убирает шум и ступеньки сканирования sm = ob.modifiers.new("Clean", 'SMOOTH') sm.factor = 0.5 sm.iterations = smooth_iterations print(f"осталось {len(ob.data.vertices):,} вершин") ``` ### 6. Текстура мобильного скана: чего ожидать ```text Что обычно приезжает: • Одна текстура 1–2K на весь объект (атлас), иногда без нормалей • Нормали часто нет вообще — только цвет • Качество ниже, чем у десктопной фотограмметрии: шум, «замыленность» • Иногда цвет в вершинных атрибутах вместо UV-текстуры Что с этим делать: 1. Для рендера: ретопология → своя развёртка → запечь карты со скана 2. Для быстрого превью: оставить как есть, но поднять разрешение текстуры или перевзвесить через шумоподавление в композитной ноде 3. Если текстуры нет вообще: только форма, материалы придётся делать процедурно или красить заново ``` ```python # Восстановить цвет из вершинных атрибутов в текстуру, если приехало именно так def vertex_color_to_attribute(ob): """Если цвет лежит в vertex colors — переносим в color attribute, из которого его читает нода Attribute в материале.""" if "Col" in ob.data.vertex_colors: ob.data.vertex_colors.remove(ob.data.vertex_colors["Col"]) ca = ob.data.color_attributes.new(name="Col", type='BYTE_COLOR', domain='POINT') src = ob.data.vertex_colors.active for li, loop in enumerate(ob.data.loops): ca.data[loop.vertex_index].color = src.data[li].color print("перенесено в color attribute 'Col':", len(ca.data), "значений") ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```bash # Проверка содержимого GLB без Blender — быстрый взгляд на состав python -c "import struct,sys;d=open(r'C:\scans\chair.glb','rb').read(); \ print('magic:',d[:4]); print('version:',struct.unpack('= 0 or "nrm" in n.image.name.lower(): has_normal = True print(f"вершин {total_v:,} | полигонов {total_f:,}") print(f"UV: {'есть' if has_uv else 'НЕТ'} | текстура: {'есть' if has_tex else 'НЕТ'} " f"| карта нормалей: {'есть' if has_normal else 'НЕТ'}") print("нет UV или текстуры → ретопология обязательна, " "запекать будет нечего") inventory() ``` | Параметр | Значение | Когда менять | |----------|----------|--------------| | Формат экспорта | GLB | GLB несёт PBR внутри и импортируется без настроек | | `merge_threshold` | 0.001–0.005 м | 5 мм для крупных объектов, 1 мм для мелких | | `outlier_scale` | 0.02 от диагонали | ниже 0.01 — срежет края; выше 0.05 — мусор останется | | Smooth iterations | 2–4 | больше 4 — теряются мелкие детали | | Известный размер | всегда задавать | без него сцена «повисает» на неизвестном масштабе | | Текстура скана | 1–2K атлас | для рендера лучше перевзвесить на своей развёртке | | ЛиDAR vs фото-режим | LiDAR лучше по форме | фото-режим лучше по цвету | ### Про NeRF и Gaussian splatting: когда это полезно ```text Скан-тип Что это Когда брать вместо меша ───────────────── ────────────────────── ───────────────────────────── Фотограмметрия сетка + текстура финальная геометрия для рендера LiDAR-фото сетка + цвет точная форма, средний цвет NeRF (нейронное) объёмное представление просмотр, прототип; не для экспорта Gaussian splatting «брызги» с параметрами фотореалистичный просмотр с любого ракурса; тяжёлые файлы ``` Практическое правило: если результат пойдёт дальше в рендер или в экспорт — бери сетку (фотограмметрия или LiDAR-фото). NeRF и splatting хороши как быстрый просмотр идеи, но из них не получится ни ретопология, ни запекание, ни физика. ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |--------|---------|---------| | Модель размером 1×1×1 | Приложение нормализовало геометрию | `fit_to_height` или `fit_by_measured_dimension` | | Модель размером 0.01 м | Приложение считает в сантиметрах | Та же функция, подставить реальные метры | | Объект перевёрнут | Разные соглашения по Y-up/Z-up | Ручной поворот на 180° вокруг X; при экспорте через glTF Blender обычно справляется сам | | Импорт GLB не дал текстуры | Текстура внутри отдельного буфера, версия glTF-экспортёра старая | Проверь содержимое файла (блок выше); попроси экспорт в OBJ + PNG | | Сплошной цвет без деталей | Нормалей нет, освещение плоское | Перевзвесить карты скана на своей развёртке — запечь AO/curvature с ретопологии | | «Пузыри» и «щупальца» | Мусор в облаке точек, движение при съёмке | `cleanup_scan` с outlier-отсечением; при сильном мусоре — переснять | | Дыры на скан | Закрытые поверхности (низ, ручки) | Обойти объект снизу; заполнить дыры в Blender (`Mesh → Fill Holes`) | | Splat не открывается в Blender | Splat — не меш | Конвертировать в меш сторонним инструментом или рендерить в отдельном плеерере | | Экспорт недоступен в приложении | Формат заблокирован за подпиской / не поддерживается | Проверь тариф и список форматов; для GLB обычно нет ограничений | | Blender зависает на импорте | 2–5 млн полигонов PLY | Импортируй фоновым режимом и прореживай сразу | | Текстура «мылится» после рендера | Разрешение атласа ниже, чем нужно для кадра | Ретопология + своя развёртка + запекание 2K на остров | ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 23: REALITYCAPTURE SCAN TO BLENDER](setup_23_realitycapture_scan.md), [SETUP 24: AGISOFT METASHAPE](setup_24_metashape_scan.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 02: GENESISCORE LOCAL (OLLAMA)](setup_02_genesiscore_local.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 04: HEADLESS RENDER (CLI / CI/CD)](setup_04_headless_render.md), [SETUP 05: RENDER FARM (FLAMENCO)](setup_05_render_farm.md) - **Теги:** #photogrammetry, #setup --- # SETUP 26: TOPOGUN / QUAD DRAW RETOPO Tags: #setup #topology # SETUP 26: TOPOGUN / QUAD DRAW RETOPO **ID:** SETUP-26 | **Type:** Retopo | **Difficulty:** ⭐⭐ | **Time:** 35 min **RU/EN Bilingual** > Когда применять: тебе нужна лоу-поли сетка для анимации, и ручная лепка квадов в Edit Mode стала медленной. ## 🎯 ЦЕЛЬ Собрать рабочий ретопо-пайплайн: подготовить исходный хиг-полис меш, построить по нему квадратную сетку и прижать её к форме так, чтобы при деформации не появлялись заломы. Два рабочих пути: встроенный в Blender инструмент Quad Draw — для лица и точечных правок, и отдельное приложение TopoGun — когда меш большой и квады идут сотнями в минуту. В конце — перенос весов с исходника на ретопо и проверка результата на экстремальной позе. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Blender | 4.x; Quad Draw появился в 2.90 | Edit Mode, Shrinkwrap, Data Transfer | | TopoGun | актуальный релиз | отдельный квад-дри для больших мешей | | FBX-плагин | встроенный в Blender | обмен геометрией с TopoGun | | Исходный меш | от ~30k до ~200k полигонов | «форма», к которой прижимается сетка | > [!NOTE] > Проверь доступность Quad Draw в своей сборке: `Edit Mode → Tool Shelf` или пицца-меню по `Shift+Q` (в списке есть Quad Draw). Точная клавиша менялась между релизами — сверься со своей раскладкой в `Preferences → Keymap`. ## 🛠️ НАСТРОЙКА ### 1. Подготовка «формы» Ретопо строится поверх отдельной копии исходника. Копию сразу прячем и отключаем от рендера — иначе она будет мешать в превью и в весах. ```python import bpy src = bpy.context.view_layer.objects.active src.name = "SRC_high" # Копия геометрии — то, по чему рисуем ретопо me = src.data.copy() ret = bpy.data.objects.new("RET_low", me) bpy.context.collection.objects.link(ret) ret.parent = src # едет вместе с исходником, сохраняя позицию ret.matrix_parent_inverse = src.matrix_world.inverted() src.hide_render = True src.hide_viewport = True ``` ### 2. Чистка исходника перед снятием формы Скругления, на которых ретопо всё равно не ляжет ровно, мешают. Проще всего снять их одним Data Transfer с включённым Smooth. ```python smooth = src.modifiers.new("Smooth_low", 'SMOOTH') smooth.factor = 0.5 smooth.iterations = 3 # Нормали пересчитать, иначе Shrinkwrap будет цепляться к старым нормалям for poly in src.data.polygons: poly.use_smooth = True bpy.ops.object.select_all(action='DESELECT') bpy.context.view_layer.objects.active = src bpy.ops.object.modifier_apply(modifier="Smooth_low") ``` ### 3. Рисуем квады **Путь A — Quad Draw в Blender.** Перейди в `Edit Mode` на объекте `RET_low`, возьми Quad Draw и рисуй четырёхугольники кликом с зажатым `Shift`. Раскладку отключи (`Ribbon / Pie` выключены), иначе каждый клик открывает меню. Клавиша удаления/отмены — `Ctrl+Z` внутри Edit Mode работает штатно. **Путь B — TopoGun.** Выгружаем форму в FBX с нулевой анимацией, рисуем в TopoGun, выгружаем обратно. ```python # Экспорт формы наружу: только меш, без костей bpy.ops.object.select_all(action='DESELECT') ret.select_set(True) bpy.context.view_layer.objects.active = ret ret.hide_viewport = False bpy.ops.export_scene.fbx( filepath=r"C:\work\retopo\src.fbx", use_selection=True, object_types={'MESH'}, use_mesh_modifiers=True, mesh_smooth_type='FACE', # OFF / EDGE / FACE — экспорт сглаживания path_mode='AUTO', ) ``` > [!NOTE] > Ссылку на репозиторий TopoGun (`github.com/topogun/topogun`) проверь перед установкой — проекты с таким именем нередко переезжают и меняют владельца. Надёжный признак актуальности: в репозитории есть готовые сборки под твою ОС. Если репозиторий недоступен, Quad Draw в Blender закрывает те же задачи, просто медленнее. ### 4. Прижимаем сетку к форме Shrinkwrap с офсетом порядка 0.001 единицы — рабочий диапазон: меньше — сетка тонет в исходнике, больше — появляется видимый зазор. ```python sw = ret.modifiers.new("Shrinkwrap", 'SHRINKWRAP') sw.target = src sw.wrap_method = 'NEAREST_SURFACEPOINT' sw.wrap_mode = 'ON_SURFACE' sw.offset = 0.001 # Смещение в нормальном направлении даёт одинаковый зазор на всех сторонах sw.use_project_y = False ``` ### 5. Размечаем рёбра деформации Кромки вдоль позвоночника, в локтях и подмышках ломаются первыми. Помечай их как sharp или crease — тогда при изгибе сетка не «тянет» за собой гладкие участки. ```python me = ret.data # 1.0 — максимум crease по всем рёбрам; точечно лучше выставлять в UI me.edges.foreach_set("crease", [0.0] * len(me.edges)) # Порог нормалей: угол выше него считается жёсткой кромкой for p in me.polygons: p.use_smooth = True bpy.context.view_layer.objects.active = ret bpy.ops.object.shade_smooth_by_angle(angle=0.523599) # 30° ``` > [!NOTE] > `shade_smooth_by_angle` существует в Blender 4.1+. В 4.0 и раньше это была галочка `Mesh → Shade Auto Smooth` (свойства `mesh.use_auto_smooth` и `mesh.auto_smooth_angle`). Проверь, что есть в твоей версии. ### 6. Переносим веса с исходника Ретопо — отдельная геометрия, весов у неё нет. Data Transfer копирует vertex groups с исходника. ```python dt = ret.modifiers.new("DataTransfer", 'DATA_TRANSFER') dt.object = src dt.mix_mode = 'REPLACE' dt.mix_factor = 1.0 dt.use_max_face_angle = False # Дальше через UI: Modifier → Data Transfer → Vertex Data / Mesh Data, # переключатель "All Layers", затем Apply ``` > [!NOTE] > Имена свойств модификатора Data Transfer отличаются между версиями: включение переноса вершин называлось `use_mesh_data`, а в новых сборках — `use_vert_data`. Уточни точные поля в `DataTransferModifier` через поиск по `Help → Manual → API`, и не полагайся на приведённый выше список как на полный. ### 7. Проверяем на позе Гоняй крайние позы, а не нейтральную: именно на них видно заломы, которые не видны в T-pose. ```python sc = bpy.context.scene sc.frame_set(1) # Удобный способ: поставить кадр и посмотреть, не проткнула ли сетка форму for f in (0, 25, 50, 75): sc.frame_set(f) print(f"frame {f}: retopo verts={len(ret.data.vertices)}") ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```python # Замерить результат — до/после должно быть видно в консоли def tri_count(ob): return sum(len(p.vertices) - 2 for p in ob.data.polygons) print("src tris:", tri_count(src)) print("ret tris:", tri_count(ret)) ``` | Параметр | Значение | Когда менять | |---|---|---| | `Shrinkwrap.offset` | `0.001` | больше — если сетка тонет в исходнике при сгибе | | `Shrinkwrap.wrap_method` | `NEAREST_SURFACEPOINT` | `PROJECT` — когда есть направление проекции | | `Shrinkwrap.wrap_mode` | `ON_SURFACE` | `ABOVE_SURFACE` — чтобы сетка не проваливалась внутрь | | `DataTransfer.mix_mode` | `REPLACE` | `ADD` — когда переносишь поверх уже готовых весов | | `DataTransfer.mix_factor` | `1.0` | `0.5` — для частичного переноса при ручной правке | | crease рёбер | `0.0 – 1.0` | `1.0` на позвоночнике и в сгибах | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Retopo проваливается внутрь формы | `wrap_mode` не задан или `offset` отрицательный | Поставь `ON_SURFACE` и положительный `offset` | | Сетка «плывёт» при движении | ретопо не в иерархии исходника | `ret.parent = src` + `matrix_parent_inverse` | | Данные Data Transfer не переносятся | несовпадение расстояния между сетками | Сначала Shrinkwrap, потом Transfer; либо поднять `max_angle` | | Квады не создаются, вместо них треугольники | активен не Quad Draw, а обычное выделение | Взять инструмент Quad Draw в Tool Shelf, рисовать с `Shift` | | Залом на локте после Apply | не выставлен crease на рёбрах сгиба | Выделить рёбра → `Edge → Crease`, значение 0.5–1.0 | | Ретопо тормозит при рисовании | в сцене виден исходник в 200k полигонов | `src.hide_viewport = True` во время рисования | | Текстура «поехала» после Apply | UV разъехались после Shrinkwrap | Перенеси UV через Data Transfer (UV Data) до применения | ## 🖥 MCP-ПРОМПТ ```text RU: "Сделай копию SRC как RET_low, навесь Shrinkwrap 0.001 к SRC, перенеси веса Data Transfer, проверь на кадрах 1 и 50" / EN: "Duplicate SRC as RET_low, Shrinkwrap 0.001 onto SRC, Data Transfer weights, check frames 1 and 50" ``` ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 22: RIZOMUV BRIDGE](setup_22_rizomuv_bridge.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 02: GENESISCORE LOCAL (OLLAMA)](setup_02_genesiscore_local.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 04: HEADLESS RENDER (CLI / CI/CD)](setup_04_headless_render.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 05: RENDER FARM (FLAMENCO)](setup_05_render_farm.md), [SETUP 06: CUSTOM MCP TOOLS](setup_06_custom_mcp_tools.md) - **Теги:** #setup, #topology --- # SETUP 27: MIXAMO AUTO-RIG Tags: #rigging #setup # SETUP 27: MIXAMO AUTO-RIG **ID:** SETUP-27 | **Type:** Auto Rig | **Difficulty:** ⭐ | **Time:** 15 min **RU/EN Bilingual** > Когда применять: нужен рабочий гуманоидный риг с библиотекой анимаций за один сеанс, без ручной сборки скелета. ## 🎯 ЦЕЛЬ Провести персонажа из Blender через веб-авторинг Mixamo, получить скелет и анимации обратно в сцену и починить то, что ломается при импорте FBX. Отдельно разбираем главное ограничение Mixamo: в скелете нет костей пальцев — если пальцы нужны, риг собирается иначе. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Blender | 4.x | FBX-импорт, проверка скелета | | Adobe Mixamo | актуальная версия, браузер | авторинг и библиотека motion capture | | Аккаунт Adobe | бесплатного тарифа хватает | загрузка персонажа и скачивание клипов | | FBX-экспорт из Blender | встроенный | только меш, без уже существующего рига | ## 🛠️ НАСТРОЙКА ### 1. Готовим персонажа к загрузке Mixamo анализирует силуэт, поэтому сцена не должна быть заслонена. Проверь три вещи: ориентация (лицом по `+Y`, носом по `+Z`), отсутствие лишних частей (волосы, одежда мешают автоопределению суставов) и пропорции. ```python import bpy ob = bpy.context.view_layer.objects.active ob.name = "CHAR" # Единицы: метры, без применённого масштаба — FBX переносит масштаб как есть bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) # Сохраняем только видимую геометрию bpy.ops.object.select_all(action='DESELECT') ob.select_set(True) bpy.context.view_layer.objects.active = ob ``` Позу перед загрузкой держи в **T-pose** или **A-pose** — руки вдоль тела либо строго в стороны. В T-pose плечи и локти определяются увереннее, в A-pose меньше риск, что авторинг притаит руки к торсу. ### 2. Экспорт в Mixamo ```python bpy.ops.export_scene.fbx( filepath=r"C:\work\mixamo\char.fbx", use_selection=True, object_types={'MESH'}, # только меш: старый риг Mixamo перезапишет use_mesh_modifiers=True, mesh_smooth_type='FACE', add_leaf_bones=False, # висячие кости в игре ломают маппинг path_mode='AUTO', ) ``` На стороне сайта Mixamo: загрузи FBX, пройди три шага авторинга (постановка скелета → проверка → выбор модели), на этапе выбора модели подтверди, что поза соответствует указанной тобой. Скачай **with skin**, если нужен готовый персонаж, и **without skin**, если анимация нужна для твоего собственного рига. ### 3. Импорт обратно и разбор результата ```python bpy.ops.import_scene.fbx( filepath=r"C:\work\mixamo\char_with_skin.fbx", automatic_bone_orientation=True, global_scale=1.0, use_anim=True, ) ``` После импорта в сцене два объекта: меш и арматура. Все кости называются с префиксом `mixamorig:` — это не баг, а способ Mixamo избежать конфликтов имён при объединении нескольких клипов. ```python arm = next(o for o in bpy.data.objects if o.type == 'ARMATURE') arm.name = "mixamorig" print("костей:", len(arm.data.bones)) print("есть ли пальцы:", any(b.name.startswith("mixamorig:LeftHandIndex") for b in arm.data.bones)) ``` ### 4. Приводим меш в порядок Сглаживание нормалей ломается на стыках импортированного FBX. Пересчитываем по углу, иначе в движении появляются тёмные полосы на плечах и бёдрах. ```python mesh_ob = next(o for o in bpy.data.objects if o.type == 'MESH') bpy.ops.object.select_all(action='DESELECT') bpy.context.view_layer.objects.active = mesh_ob mesh_ob.select_set(True) try: bpy.ops.object.shade_smooth_by_angle(angle=0.523599) # 30°, Blender 4.1+ except RuntimeError: mesh_ob.data.use_auto_smooth = True # Blender 4.0 и раньше mesh_ob.data.auto_smooth_angle = 0.523599 ``` > [!NOTE] > Симметрию нормалей проверяй вручную: у FBX из Mixamo она обычно корректна, но после любых ручных правок — нет. Отдельный оператор `normals_make_consistent` (`bpy.ops.mesh.normals_make_consistent(inside=False)`) существует только в Edit Mode. ### 5. Снимаем локти и плечи Классическая проблема: объём предплечья уезжает, плечо проваливается. Лечится не ригом, а весами и корректирующими формами. ```python # Проверяем, что каждая кость реально управляет чем-то: # пустая группа = веса не перенеслись, меш не привязан def weights_for(arm, mesh, bone_name): vg = arm.vertex_groups.get(bone_name) or arm.vertex_groups.get( "mixamorig:" + bone_name) if vg is None: return "нет группы" n = sum(1 for v in mesh.data.vertices for g in v.groups if g.group == vg.index) return f"{n} вершин" for bn in ("Hips", "Spine", "LeftArm", "RightArm"): print(bn, "->", weights_for(arm, mesh_ob, bn)) ``` > [!NOTE] > У Mesh-объекта группы весов лежат в `mesh.vertices[i].groups` (записи с полями `group` и `weight`), а список самых групп — в `armature.vertex_groups`. Имя группы у Mixamo совпадает с полным именем кости, то есть с префиксом. Путаница между этими двумя списками — самая частая причина «весов нет нигде». ### 6. Склейка клипов на одном риге Скачанные клипы приходят отдельными файлами с копиями арматуры. Дальше их нужно свести к одному скелету: иначе в сцене будет пять одинаковых персонажей. ```python import os # 1. Импортировать клип как Action в существующую арматуру. # Оператор bpy.ops.nla.* работает по выделенному объекту, поэтому # сначала делаем активной именно ту арматуру, к которой хотим привязать клип. rig = bpy.data.objects["mixamorig"] bpy.context.view_layer.objects.active = rig bpy.ops.object.select_all(action='DESELECT') rig.select_set(True) clip = r"C:\work\mixamo\clips\walk.fbx" bpy.ops.import_scene.fbx(filepath=clip, use_anim=True) # 2. В NLA-редакторе (Animation workspace → NLA Editor) действие # появится как новый Action; перетащи его на канал сдвига. # Проверить, что он действительно появился: for act in bpy.data.actions: print(act.name, "→", len(act.fcurves), "кривых") ``` > [!NOTE] > Массовое заполнение NLA через bpy возможно, но оно же и хрупко: нужно вручную выставлять диапазон strip'а и слои. Для пяти клипов быстрее сделать это мышью, чем писать код. Если клипов больше тридцати — тогда пиши скрипт. ### 7. Массовая выгрузка клипов ```python import os OUT = r"C:\work\mixamo\clips" os.makedirs(OUT, exist_ok=True) CLIPS = ["idle", "walk", "run", "jump", "wave"] for name in CLIPS: bpy.ops.import_scene.fbx( filepath=os.path.join(OUT, f"{name}.fbx"), automatic_bone_orientation=True, use_anim=True, ) ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```python # Как выглядит результат: сколько костей, какие action'ы, сколько ключей for act in bpy.data.actions: print(act.name, "→", len(act.fcurves), "кривых") ``` | Параметр | Значение | Когда менять | |---|---|---| | `object_types` | `{'MESH'}` | `{'ARMATURE','MESH'}` — если везёшь свой риг в Mixamo | | `add_leaf_bones` | `False` | `True` — если движок требует хвосты у листьев | | `automatic_bone_orientation` | `True` | `False` — если скелет ложится боком | | `mesh_smooth_type` | `FACE` | `EDGE` — когда нужны хард-нормали на рёбрах | | поза перед загрузкой | T-pose / A-pose | менять под конкретный меш, авторинг не всегда угадывает | | `use_anim` при импорте | `True` | `False` — когда нужна только геометрия | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Авторинг «зацепил» кисти за торс | поза не T/A или пальцы прижаты | Повторить в чистой T-pose, без одежды и волос | | Кости называются `mixamorig:Hips` | это штатное имя из FBX | Не переименовывать: клипы привязаны к этим именам | | Нет костей пальцев | в риге Mixamo их нет в принципе | Добавить вручную либо собрать риг через Rigify/ARP | | Рука проходит сквозь бедро | веса не перенеслись, меш не привязан | Проверить модификатор Armature и его цель | | Тёмные полосы на сгибах | нормали не пересчитаны после импорта | `shade_smooth_by_angle` | | Анимация проиграется рывками | снят `use_anim` или клип без ключей | Импортировать с `use_anim=True` | | Персонаж после импорта лежит | `automatic_bone_orientation` справился не со всеми костями | Ручная ориентация в Pose Mode → `R` / выравнивание по костям | ## 🖥 MCP-ПРОМПТ ```text RU: "Экспортируй CHAR в FBX только мешем, без leaf bones, для Mixamo" / EN: "Export CHAR to FBX, mesh only, no leaf bones, for Mixamo" ``` ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 28: ACCURIG / ACTORCORE RIG](setup_28_accurig_cc.md), [SETUP 29: AUTO-RIG PRO](setup_29_autorignpro.md), [SETUP 30: RIGIFY ADVANCED + FACE](setup_30_rigify_advanced.md), [SETUP 35: READY PLAYER ME AVATARS](setup_35_readyplayerme.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md) - **По темам:** [CASE 04: CHARACTER RIG & ANIMATION](../cases/case_04_character_rig.md), [P17: RIGIFY HUMANOID SETUP](../prompts/17_rigify_setup.md), [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md) - **Теги:** #rigging, #setup --- # SETUP 28: ACCURIG / ACTORCORE RIG Tags: #rigging #setup # SETUP 28: ACCURIG / ACTORCORE RIG **ID:** SETUP-28 | **Type:** Auto Rig | **Difficulty:** ⭐⭐ | **Time:** 25 min **RU/EN Bilingual** > Когда применять: нужен риг на персонаже, для которого Reallusion-сообщество уже насделало качественные анимации — то есть важна совместимость скелета, а не скорость авторинга. ## 🎯 ЦЕЛЬ Собрать в Blender риг, совместимый с анимациями ActorCore от Reallusion, и убедиться, что веса и оси костей не разъедутся. Разбираем путь через браузерный AccuRig (генерация скелета по фото-проекциям), установку плагина ActorCore в Blender и честное ограничение: универсального авто-ретаргета в Blender нет, поэтому скелепы приводятся к единому виду вручную или констрейнтами. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Blender | 4.x | целевая сцена | | AccuRig (браузер) | актуальная, `accurig.com` | генерация скелета и автовеса | | ActorCore Plugin for Blender | версия под свою Blender | импорт анимаций, конвертация скелета | | Исходный меш | гуманоид, T-pose или A-pose | вход для AccuRig | > [!NOTE] > Точные пункты меню и название плагина менялись вместе с переездом Reallusion на новый сайт. Ориентируйся на поиск в самом Blender: `Preferences → Add-ons → Install`, архив плагина берётся с официальной страницы Reallusion. Не качай плагин со сторонних зеркал — они регулярно ломаются после обновлений. ## 🛠️ НАСТРОЙКА ### 1. Подготовка меша AccuRig определяет суставы по силуэту, поэтому лишняя геометрия (волосы, рюкзак, массивная одежда) резко ухудшает результат. Импортируй тело без головы и без волос — голову добавишь отдельно. ```python import bpy ob = bpy.context.view_layer.objects.active ob.name = "BODY" # Размер в метрах, масштаб применён: AccuRig работает с абсолютными пропорциями bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) # Проверить, что меш реально в позе, а не в A-stand с руками вдоль тела print("рост по Z:", max(v.co.z for v in ob.data.vertices) - min(v.co.z for v in ob.data.vertices)) ``` ### 2. Генерация скелета в AccuRig Браузерный шаг: загрузи FBX → расставь ключевые точки суставов (таз, плечи, локти, запястья, бёдра, колени, стопы) → подтверди, что скелеп в нужной позе → скачай экспорт. ```python # Экспорт входного FBX: без скелета, только меш bpy.ops.object.select_all(action='DESELECT') ob.select_set(True) bpy.context.view_layer.objects.active = ob bpy.ops.export_scene.fbx( filepath=r"C:\work\accurig\body.fbx", use_selection=True, object_types={'MESH'}, use_mesh_modifiers=True, mesh_smooth_type='FACE', path_mode='AUTO', ) ``` Что выбрать в самом AccuRig при экспорте, зависит от цели: | Вариант экспорта в AccuRig | Что получается | Когда брать | |---|---|---| | Только скелет (skeleton) | чистая костная структура | свой риг на своём меше | | Скелет + автовеса | FBX с готовыми весами | быстрый старт без ручного веса | | С ActorCore-совместимой структурой | имена костей под анимации Reallusion | нужна библиотека ActorCore | ### 3. Импорт и привязка в Blender ```python bpy.ops.import_scene.fbx( filepath=r"C:\work\accurig\body_skeleton.fbx", automatic_bone_orientation=True, global_scale=1.0, ) arm = next(o for o in bpy.data.objects if o.type == 'ARMATURE') arm.name = "AccuRig" # Классическая автопривязка: распределяет веса по прогреву костей bpy.ops.object.select_all(action='DESELECT') ob.select_set(True) arm.select_set(True) bpy.context.view_layer.objects.active = arm bpy.ops.object.parent_set(type='ARMATURE_AUTO') ``` Типы у оператора реально такие: `ARMATURE_AUTO`, `ARMATURE_NAME`, `ARMATURE_ENVELOPE`, `ARMATURE_WITH_EMPTY_GROUP`. ### 4. Оси костей AccuRig и ActorCore используют Y как костную ось и X как ось наклона. Blender по умолчанию Y-up в костях, поэтому при первой генерации рига оси надо задать явно — иначе анимации будут вставать «на бок». ```python # Пересчитать ролл костей под свои оси (Edit Mode, выделенные кости). # Доступные значения type: POSITIVE_X / NEGATIVE_X / POSITIVE_Z / NEGATIVE_Z bpy.ops.object.mode_set(mode='EDIT') bpy.ops.armature.calculate_roll(type='POSITIVE_X') bpy.ops.object.mode_set(mode='OBJECT') # Прочитать, что получилось, и сверить с ожидаемым: # у всех костей, кроме корневой, Y должен смотреть вдоль кости for b in arm.pose.bones[:5]: print(b.name, "→ head", tuple(round(c, 3) for c in b.head), "tail", tuple(round(c, 3) for c in b.tail)) ``` > [!NOTE] > Автонаправление костей (`Bone envelopes`, `Inverse matrices`, `Automatic align rolls`) в Rigify и в авто-ригах задаётся по-разному. Универсального способа «выровнять всё» в bpy нет — у каждого рига своя процедура. Сверяйся с документацией конкретного аддона, а не с общим описанием Blender. ### 5. Сохранение объёма при деформации Тонкие участки — запястья, шея, щиколотка — «схлопываются» при линейных весах. Двойные кватернионы это чинят. ```python for pb in arm.pose.bones: pb.use_deform_preserve_volume = True # Soft/dual-quaternion skinning ``` Если этого мало, помогает классическая связка: автоматические веса + контрольный меш (в ARP это Clean Plate, в других ригах — дубликат поверхности). ### 6. Импорт анимаций ActorCore Открываешь `.fbx` или `.actor` из библиотеки ActorCore через плагин. Главный нюанс: имена костей должны совпасть с уже привязанным ригом. Если AccuRig дал другие имена — скелет надо переименовать или привести к общему виду до импорта. ```python # Сверка имён: что пришло из рига и что ожидает библиотека arm_bones = {b.name for b in arm.data.bones} print(sorted(arm_bones)) ``` ### 7. Ретаргет, если скелеты не совпадают Универсального авто-ретаргета в Blender нет — это факт, а не недоработка: встроенный стек работает с BVH и с одинаковыми именами костей. Когда источник и приёмник ригов различаются, рабочий путь — констрейнты на уровне костей. ```python # Ретаргет через констрейнт: копия поворота с кости-источника на кость-приёмник src_arm = bpy.data.objects["ImportedAnimation"] # арматура-источник dst = arm.pose.bones["upperarm.R"] # имя кости уточни в своём риге src = src_arm.pose.bones["RightArm"] # кость-источник con = dst.constraints.new('COPY_ROTATION') con.target = src_arm con.subtarget = src.name con.target_space = 'WORLD' con.owner_space = 'WORLD' con.mix_mode = 'BEFORE' con.influence = 1.0 # Прогнать кадр, чтобы констрейнты пересчитались bpy.context.scene.frame_set(bpy.context.scene.frame_current) ``` > [!NOTE] > Констрейнт «копия поворота» не учитывает разницу в длине костей и в оценочной позе, поэтому для серьёзного продакшна ретаргет дописывают руками. Для быстрой проверки «анимация вообще похожа» — способ рабочий. ### 8. Диагностика перед сдачей ```python # Свести всё в одну таблицу: что приехало и в каком состоянии for o in bpy.data.objects: if o.type == 'MESH': tris = sum(len(p.vertices) - 2 for p in o.data.polygons) print(f"{o.name}: tris={tris} mods={[m.type for m in o.modifiers]}") elif o.type == 'ARMATURE': print(f"{o.name}: костей={len(o.data.bones)} " f"групп={len(o.vertex_groups)}") ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```python # Применить позу как новую оценочную — после импорта анимации bpy.ops.object.mode_set(mode='POSE') bpy.ops.pose.armature_apply() bpy.ops.object.mode_set(mode='OBJECT') ``` | Параметр | Значение | Когда менять | |---|---|---| | `parent_set(type=)` | `ARMATURE_AUTO` | `ARMATURE_WITH_EMPTY_GROUP` — когда автовес даёт мусор | | `pb.use_deform_preserve_volume` | `True` | `False` — если анимации сжимают суставы сильнее, чем «линейно» | | `automatic_bone_orientation` | `True` | `False` — когда импорт кладёт кости боком | | `global_scale` | `1.0` | подбирается под размер персонажа; FBX хранит его в самом файле | | `mesh_smooth_type` | `FACE` | `EDGE` — для хард-нормалей на рёбрах | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | AccuRig ставит суставы в сторону | на меш попали волосы, одежда или дубликат | Экспортировать голое тело, повторить | | Автовеса не распределились | пересекающиеся кости, невыпуклая топология | Переделать сетку в районе таза/плеч, весать вручную | | Анимация из ActorCore не цепляется | имена костей не совпадают | Сверить `arm.data.bones` с именами в библиотеке | | Кости повёрнуты на 90° | не задан bone orientation при импорте | Переимпортировать с `automatic_bone_orientation=True` | | Локти и колени «схлопываются» | линейные веса | Включить `use_deform_preserve_volume` | | Плагин не появился в списке аддонов | несовместимая версия Blender | Проверить требования плагина к версии Blender | | Персонаж сместился после Apply | Apply Pose с неприменённым масштабом | Заранее `transform_apply` | ## 🖥 MCP-ПРОМПТ ```text RU: "AccuRig: экспорт скелета тела, импорт FBX, parent_set ARMATURE_AUTO, preserve volume на всех костях, сверка имён с библиотекой ActorCore" / EN: "AccuRig: export body skeleton, import FBX, ARMATURE_AUTO parenting, preserve volume on all bones, compare names with ActorCore library" ``` ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 27: MIXAMO AUTO-RIG](setup_27_mixamo_autorig.md), [SETUP 29: AUTO-RIG PRO](setup_29_autorignpro.md), [SETUP 30: RIGIFY ADVANCED + FACE](setup_30_rigify_advanced.md), [SETUP 35: READY PLAYER ME AVATARS](setup_35_readyplayerme.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md) - **По темам:** [CASE 04: CHARACTER RIG & ANIMATION](../cases/case_04_character_rig.md), [P17: RIGIFY HUMANOID SETUP](../prompts/17_rigify_setup.md), [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md) - **Теги:** #rigging, #setup --- # SETUP 29: AUTO-RIG PRO Tags: #rigging #setup # SETUP 29: AUTO-RIG PRO **ID:** SETUP-29 | **Type:** Rig Pro | **Difficulty:** ⭐⭐⭐ | **Time:** 60 min **RU/EN Bilingual** > Когда применять: риг делается под продакшн — нужны пальцы, корректные скручивания конечностей и готовый экспорт под игровой движок, а не разовая анимация. ## 🎯 ЦЕЛЬ Собрать в ARP (Auto-Rig Pro) риг, который переживёт передачу другой аниматору: с полными пальцами, раздельными IK/FK для рук и ног, рабочими корректирующими формами на сгибах и предсказуемым экспортом в FBX для Unity или Unreal. Настраиваем четыре вещи: генерацию рига из меша, распределение весов, систему управления и экспорт под движок. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Blender | 4.x (совместимость проверяй на сайте автора) | хост | | Auto-Rig Pro | платный аддон, есть триал | сам риг и экспорт | | Лицензия / триал | активируется в Preferences аддона | разблокировка экспорта | | Исходный меш | гуманоид, без лишней геометрии | вход Rig Builder | > [!NOTE] > Официальная страница аддона — `lucky3d.fr/arp/`. Там же указаны поддерживаемые версии Blender; ARP регулярно отстаёт по совместимости, поэтому проверь список до установки, а не после. ## 🛠️ НАСТРОЙКА ### 1. Установка аддона Скачай zip аддона → `Edit → Preferences → Add-ons → Install…` → выбери архив → в списке включи **Auto-Rig Pro** → `Save Preferences`. В N-панели (Sidebar) появится вкладка аддона. ```python import bpy # Проверка, что аддон загрузился: его операторы попадают в bpy.ops.<пространство> print("дополнения с 'rig' в имени:", [k for k in bpy.context.preferences.addons.keys() if "rig" in k.lower()]) print("версия Blender:", bpy.app.version_string) ``` > [!NOTE] > Имена операторов аддона начинаются с префикса `arp_` и живут в отдельном пространстве имён внутри `bpy.ops`, но конкретные названия (`arp_rigify`, `arp_rig_build`, `arp_export`) не являются публичным API — они меняются между версиями. Надёжный способ узнать точные имена в своей сборке: навести курсор на кнопку в UI и посмотреть подсказку с `bpy.ops.имя.оператора()`. ### 2. Подготовка меша ```python import bpy ob = bpy.context.view_layer.objects.active ob.name = "CHAR" # Применить трансформации: ARP опирается на мировые координаты вершин bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) # Отделить всё, что не участвует в риге (волосы, одежда, пропсы) bpy.ops.object.select_all(action='SELECT') bpy.ops.object.join() me = ob.data # Сбросить группы весов, если меш пришёл из другого рига ob.vertex_groups.clear() ``` ### 3. Rig Builder: сборка рига В Rig Builder расставляешь части тела (голова, шея, позвоночник, ключицы, руки, кисти, пальцы, ноги, стопы, хвост/грудь при наличии), а затем жмёшь **Rig**. Что важно задать до генерации: | Параметр Rig Builder | Значение | Когда менять | |---|---|---| | Число сегментов позвоночника | 3–5 | больше — для длинной спины, меньше — для короткой и быстрой анимации | | Пальцы | полный набор (5 лучей) | без пальцев — если персонаж в перчатках, экономит вес и время | | Скручивание (twist) конечностей | включено | выключить, если кисти и стопы не деформируются | | IK для рук / ног | по необходимости | без рук IK анимация идёт быстрее, но теряется управление кистью | | Плечи (shoulders) | включены | когда нужна дельта-деформация плечевого сустава | ### 4. Веса Основной путь — автоматический, с последующей чисткой. ```python bpy.ops.object.select_all(action='DESELECT') ob.select_set(True) arm = next(o for o in bpy.data.objects if o.type == 'ARMATURE') arm.select_set(True) bpy.context.view_layer.objects.active = arm # Тепловой метод: Blender распределяет веса по прогреву кости bpy.ops.object.parent_set(type='ARMATURE_AUTO') # Dual quaternion — обязателен для суставов, иначе колени «схлопываются» for pb in arm.pose.bones: pb.use_deform_preserve_volume = True ``` Автовес почти всегда требует ручной чистки в трёх зонах: шея, запястье, щиколотка. Там, где ARP-веса «съехали», помогает контрольный меш (в самом аддоне — инструмент очистки весов в панели Rig Tools; точный пункт меню сверяй по документации версии). ```python # Диагностика: какие группы весов пустые или почти пустые report = [(g.name, len(g.name)) for g in ob.vertex_groups] for g in ob.vertex_groups: if not any(x.group == g.index for v in ob.data.vertices for x in v.groups): print("пустая группа:", g.name) ``` ### 5. Корректирующие формы Ригируемые плечи и локти деформируются неидеально — появляется провал. ARP чинит это либо корректирующими формами (corrective shapes, доступны для части суставов), либо через дельту на управляющих костях. ```python # Ручная корректирующая форма: дельта кости -> ключ формы arm = bpy.data.objects["rig"] print("костей управления:", [b.name for b in arm.pose.bones][:12]) sk = ob.shape_key_add(name="Basis") corr = ob.shape_key_add(name="ShoulderFix") corr.slider_min = 0.0 corr.slider_max = 1.0 drv = corr.driver_add("value").driver drv.expression = "max(0.0, 1.0 - shoulder_ctrl)" # имя контроллера уточни ``` > [!NOTE] > Конкретные имена управляющих костей (`.L` / `_L`, `upper_arm`, `ForeArm`) задаются самим аддоном и различаются между версиями и настройками. Не копируй имя из документации другого аддона — выведи `arm.pose.bones` и найди нужное. ### 6. Экспорт под игровой движок Раздел **Game Tools** собирает чистый риг: только деформационные кости, без control-костей и без вспомогательных целей. Это то, что увидит движок. ```python bpy.ops.object.select_all(action='DESELECT') ob.select_set(True) arm.select_set(True) bpy.context.view_layer.objects.active = arm bpy.ops.export_scene.fbx( filepath=r"C:\work\export\char_game.fbx", use_selection=True, object_types={'ARMATURE', 'MESH'}, apply_unit_scale=True, global_scale=1.0, add_leaf_bones=False, path_mode='AUTO', mesh_smooth_type='FACE', ) ``` > [!NOTE] > Список игровых пресетов (Unity Standard / Unity Mecanim / Unreal / Godot и другие) менялся между версиями аддона. Открывай панель Game Tools в своей сборке и выбирай оттуда — не по памяти и не по скриншоту из другой статьи. ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```python # Что уедет в движок: перечень костей деформации и их веса deform = [b.name for b in arm.data.bones if b.use_deform] print("деформационных костей:", len(deform)) # Проверка рига в предельных позах — обязательный шаг перед экспортом for name in ("rig", "CHAR"): o = bpy.data.objects.get(name) if o and o.type == 'ARMATURE': print(name, "→", len(o.pose.bones), "костей управления") ``` | Параметр | Значение | Когда менять | |---|---|---| | `parent_set(type=)` | `ARMATURE_AUTO` | `ARMATURE_WITH_EMPTY_GROUP` — когда тепловой вес не справляется | | `use_deform_preserve_volume` | `True` | `False` — если аниматор жалуется на «резину» в суставах | | `add_leaf_bones` | `False` | `True` — если движок требует висячие кости | | `object_types` | `{'ARMATURE','MESH'}` | `{'MESH'}` — когда риг уже вшит в движке | | `mesh_smooth_type` | `FACE` | `EDGE` — для хард-нормалей | | число сегментов позвоночника | 3–5 | больше — длинная спина, меньше — скорость | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Экспорт в FBX пустой | не выделен риг, либо не сделан Game Tools export | Выделить меш + риг, повторить пресет в Game Tools | | В Unity анимация не распознаётся | нет пресета под движок либо лишние кости | Использовать пресет Unity, а не «просто FBX» | | Суставы «сжимаются» при сгибе | линейные веса | `use_deform_preserve_volume = True` | | Плечи проваливаются в прыжке | нет корректирующих форм | Добавить shape key + драйвер, либо включить shoulders | | Кисть отрывается от предплечья | автовес не захватил зону запястья | Весать вручную, проверить группу кисти | | Аддон не появился в Add-ons | архив не того формата или несовместимая версия | Распаковать zip, проверить требования на `lucky3d.fr/arp/` | | Импорт FBX кладёт кости боком | не задан bone orientation | Переимпортировать с `automatic_bone_orientation=True` | ## 🖥 MCP-ПРОМПТ ```text RU: "Auto-Rig Pro Rig Builder: spine 5, полные пальцы, twist, shoulders on, ARMATURE_AUTO, preserve volume, экспорт пресетом Unity" / EN: "ARP Rig Builder: spine 5, full fingers, twist, shoulders on, ARMATURE_AUTO, preserve volume, export with the Unity preset" ``` ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 27: MIXAMO AUTO-RIG](setup_27_mixamo_autorig.md), [SETUP 28: ACCURIG / ACTORCORE RIG](setup_28_accurig_cc.md), [SETUP 30: RIGIFY ADVANCED + FACE](setup_30_rigify_advanced.md), [SETUP 35: READY PLAYER ME AVATARS](setup_35_readyplayerme.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md) - **По темам:** [CASE 04: CHARACTER RIG & ANIMATION](../cases/case_04_character_rig.md), [P17: RIGIFY HUMANOID SETUP](../prompts/17_rigify_setup.md), [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md) - **Теги:** #rigging, #setup --- # SETUP 30: RIGIFY ADVANCED + FACE Tags: #rigging #setup # SETUP 30: RIGIFY ADVANCED + FACE **ID:** SETUP-30 | **Type:** Rig | **Difficulty:** ⭐⭐⭐ | **Time:** 90 min **RU/EN Bilingual** > Когда применять: нужен риг, который можно передать аниматору и который не сломается при добавлении корректирующих форм на колени и локти. ## 🎯 ЦЕЛЬ Пройти полный цикл Rigify: от мета-рига и генерации до кастомной части тела, добавить мимику и корректирующие формы на сгибах, и понять, что именно уходит в игровой движок. Ключевая честная оговорка: Rigify делает тело, а лицо — отдельная работа на shape keys и контроллерах. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Blender | 4.x | генерация и правка рига | | Rigify | входит в поставку Blender, включается в Preferences → Add-ons | генерация рига из мета-рига | | Мета-риг | из пресетов аддона, шаблон `rigify/presets` | базовая форма скелета | | Меш персонажа | гуманоид, деформационная топология без пересечений | веса и коррективы | > [!NOTE] > Rigify пережил крупное обновление в ветке 4.x: пресеты и названия «типов частей тела» (limb type) менялись. Перед генерацией открой список пресетов в самом аддоне и сверься с `Help → Manual` твоей версии — копировать имена из обзоров 3.x нельзя. ## 🛠️ НАСТРОЙКА ### 1. Включение аддона и осмотр пресетов ```python import bpy # Включить из кода (в UI: Preferences → Add-ons → Rigify) if "rigify" not in bpy.context.preferences.addons: bpy.ops.preferences.addon_enable(module="rigify") # Проверить, что оператор генерации зарегистрирован. # bpy.ops.<путь> — не значение: getattr создаёт заглушку, поэтому # `bool(bpy.ops.pose.rigify_generate)` всегда истинно и не проверяет ничего. # dir() же перечисляет реально зарегистрированные операторы. print("rigify_generate доступен:", "rigify_generate" in dir(bpy.ops.pose)) print("blender:", bpy.app.version_string, "| python:", bpy.app.version) ``` Пресеты мета-ригов лежат в каталоге аддона; путь виден в `Preferences → Add-ons → Rigify`. Там есть `basic`, `human`, `human-no-face` (или близкие по смыслу) — выбирай тот, чья развёртка пропорций совпадает с твоим мешем, иначе осевая сетка уедет. ### 2. Установка пропорций мета-рига Пропорции выставляются прямо на костях в Edit Mode. Плечи — шире таза, длина бедра — по канону персонажа. Именно здесь экономится время: после генерации мета-риг больше не редактируется. ```python # Пересчитать длины костей мета-рига вниз по иерархии bpy.ops.object.mode_set(mode='EDIT') bpy.ops.armature.calculate_roll(type='POSITIVE_X') bpy.ops.object.mode_set(mode='OBJECT') ``` ### 3. Генерация рига ```python arm = bpy.context.view_layer.objects.active bpy.context.view_layer.objects.active = arm # Оператор генерации; перед вызовом мета-риг уже размечен bpy.ops.pose.rigify_generate() ``` После генерации риг распадается на коллекции `rig` (управляющие кости, DEF-заменители) и `deform` (кости, к которым реально цепляются веса и коррективы). Мета-риг уезжает в коллекцию `metarig` и не участвует в анимации — его можно скрыть или удалить. ```python print("коллекции:", [c.name for c in bpy.data.armatures[0].collections]) ``` ### 4. Кастомная часть тела Rigify умеет добавлять части: например, хвост или шею животного. Процедура: добавить кости в мета-риг с правильными префиксами (`DEF-`, `ORG-`), задать им тип деформации, добавить пользовательский пресет в `rigify/presets`, после чего он появится в списке при следующем открытии аддона. ```python # Префиксы имён — то, на что Rigify опирается при генерации. # DEF- деформируемые, ORG- «органы» (например, вспомогательные), WGT- веса. arm = bpy.data.armatures[0] print("DEF-костей:", len([b for b in arm.bones if b.name.startswith("DEF-")])) ``` ### 5. Веса ```python ob = next(o for o in bpy.context.selected_objects if o.type == 'MESH') rig = next(o for o in bpy.context.scene.objects if o.type == 'ARMATURE' and o.data.collections.get("deform")) bpy.ops.object.select_all(action='DESELECT') ob.select_set(True) rig.select_set(True) bpy.context.view_layer.objects.active = rig bpy.ops.object.parent_set(type='ARMATURE_AUTO') ``` Ручная правка весов в Rigify идёт по коллекции `WGT-` через инструмент Weight Paint на деформационных костях. Типичные зоны ручной правки: таз (перекрытие бёдер), подмышка, шея. ```python # Диагностика «съехавших» весов на стыке суставов for z in ("pelvis.L", "thigh.L", "DEF-spine.03"): vg = rig.vertex_groups.get(z) if vg: w = [g.weight for v in ob.data.vertices for g in v.groups if g.group == vg.index] if w: print(z, "min", min(w), "max", max(w)) ``` ### 6. Корректирующие формы Rigify не имеет встроенной системы коррективов — их делают shape keys с драйверами. Схема: ставишь деформацию в предельную позу, добавляешь shape key, цепляешь к нему драйвер от контроллера, возвращаешь позу в ноль. ```python bpy.ops.object.select_all(action='DESELECT') ob.select_set(True) bpy.context.view_layer.objects.active = ob sk = ob.shape_key_add(name="Basis") knee = ob.shape_key_add(name="KneeFix") knee.slider_min, knee.slider_max = 0.0, 1.0 # Драйвер: значение ключа = положение контроллера drv = knee.driver_add("value").driver drv.type = 'SCRIPTED' drv.expression = "min(1.0, max(0.0, ctrl_angle))" ``` Затем, чтобы ключи не накапливали деформацию при последовательных правках: `bpy.ops.object.shape_key_retime()` в Object Mode. > [!NOTE] > Имена контроллеров Rigify стабильны для дефолтных пресетов (например `DEF-spine.03`), но для кастомных частей тела ты задаёшь их сам. Имя в выражении драйвера должно точно совпадать с именем кости или свойства, которое ты используешь — иначе драйвер молча даёт 0. ### 7. Лицо Rigify лицо не рисует. Рабочий минимум для продакшна: | Задача | Инструмент | Комментарий | |---|---|---| | Базовые эмоции | shape keys на голове | 10–15 базовых форм достаточно для большинства проектов | | Произвольная мимика | лицевой риг на костях + dельты | существенно дороже, обычно отдельная задача | | Липсинк | отдельная артикуляционная раскладка | либо готовый набор, либо покупной | ```python # Проверка: сколько ключей формы на лице и в каком они диапазоне face_keys = ob.data.shape_keys.key_blocks if ob.data.shape_keys else [] print("ключей формы:", len(face_keys)) for k in face_keys: print(f" {k.name}: {k.value:.2f} (min {k.slider_min}, max {k.slider_max})") ``` ### 8. Экспорт ```python bpy.ops.object.select_all(action='DESELECT') ob.select_set(True) rig.select_set(True) bpy.context.view_layer.objects.active = rig bpy.ops.export_scene.fbx( filepath=r"C:\work\export\char_rigify.fbx", use_selection=True, object_types={'ARMATURE', 'MESH'}, add_leaf_bones=False, path_mode='AUTO', bake_anim=False, ) ``` Unity и Unreal не понимают скелет Rigify напрямую: ни одна из этих платформ не имеет маппинга под DEF/ORG-кости. Варианты — ретаргет готовых анимаций через общего родителя (Hierarchy Bone) либо экспорт в стандартный гуманоидный скелет через промежуточный риг. ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```python # Сводка по ригу: чего сколько, чтобы понять, что сгенерировалось rig = bpy.data.objects["rig"] bpy.ops.object.mode_set(mode='POSE') print("управляющих костей:", len(rig.pose.bones)) print("деформационных:", len([b for b in rig.data.bones if b.use_deform])) bpy.ops.object.mode_set(mode='OBJECT') ``` | Параметр | Значение | Когда менять | |---|---|---| | пресет мета-рига | `human` | `basic` — когда детальные пропорции не нужны | | сегменты позвоночника | 3–5 | больше — для длинной спины, меньше — для скорости | | `bpy.ops.pose.rigify_generate()` | вызов из Pose Mode или Object Mode с активной арматурой | повторный вызов пересобирает риг заново | | `shape_key_retime()` | Object Mode | после каждой серии правок ключей формы | | `add_leaf_bones` | `False` | `True` — если движок требует хвосты у листьев | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | `rigify_generate` не появляется в меню | аддон не включён либо активна не та арматура | Включить Rigify, выделить именно мета-риг | | Кости называются `DEF-spine` вместо ожидаемого имени | кастомный пресет не подхватился | Проверить префиксы `DEF-` и папку `rigify/presets` | | Драйвер ключа всегда 0 | имя в выражении не совпадает с реальным | Распечатать `rig.pose.bones` и сверить посимвольно | | Веса съехали на тазу | автовес не справился с пересечением | Ручная правка весов по зоне, либо контрольный меш | | Лимб не деформируется | кость не попала в коллекцию `deform` | Проверить коллекции арматуры | | В движке нет анимаций | экспортирован DEF-скелет, а движок ждёт гуманоид | Ретаргет через общего родителя либо промежуточный риг | | После генерации форма деформировалась | мета-риг редактировали после генерации | Правки вносятся в мета-риг, затем генерация повторяется | ## 🖥 MCP-ПРОМПТ ```text RU: "Rigify: human-пресет, spine 5, generate rig, ARMATURE_AUTO, shape key KneeFix + драйвер, shape_key_retime, экспорт FBX" / EN: "Rigify: human preset, spine 5, generate rig, ARMATURE_AUTO, KneeFix shape key + driver, shape_key_retime, FBX export" ``` ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 27: MIXAMO AUTO-RIG](setup_27_mixamo_autorig.md), [SETUP 28: ACCURIG / ACTORCORE RIG](setup_28_accurig_cc.md), [SETUP 29: AUTO-RIG PRO](setup_29_autorignpro.md), [SETUP 35: READY PLAYER ME AVATARS](setup_35_readyplayerme.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md) - **По темам:** [CASE 04: CHARACTER RIG & ANIMATION](../cases/case_04_character_rig.md), [P17: RIGIFY HUMANOID SETUP](../prompts/17_rigify_setup.md), [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md) - **Теги:** #rigging, #setup --- # SETUP 31: MAKEHUMAN / MPFB Tags: #character #setup # SETUP 31: MAKEHUMAN / MPFB **ID:** SETUP-31 | **Type:** Base Human | **Difficulty:** ⭐ | **Time:** 20 min **RU/EN Bilingual** > Когда применять: нужен параметрический человек как отправная точка — для прототипа, для стокового персонажа или чтобы не моделить гуманоида с нуля. ## 🎯 ЦЕЛЬ Получить в Blender параметрически управляемого человека через MPFB (MakeHuman Project for Blender), разобраться, как устроены таргеты (morphs), одежда и скелет, и выгрузить результат в FBX для рига или движка. Отдельно: MPFB — это не MakeHuman 1.x, это переписанный аддон, и старые инструкции к нему неприменимы. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Blender | 4.x | хост аддона | | MPFB | релиз под свою версию Blender | генерация человека, таргеты, одежда | | MPFB-ресурсы | пакеты с одеждой и материалами | обход голого тела | | Старый MakeHuman 1.x | не нужен | можно держать, но в Blender он больше не интегрирован | > [!NOTE] > Официальная точка входа по MPFB — сообщество makehumancommunity, репозиторий проекта ищется там. Не указываю конкретный URL: аддон регулярно меняет имя пакета и способ установки между релизами. Смотри `Help → Manual → Add-ons` в своей сборке или README актуального релиза — там указан способ установки для твоей версии. ## 🛠️ НАСТРОЙКА ### 1. Установка аддона ```bash # Установка из исходников в папку аддонов Blender git clone <репозиторий-MPFB> mpfb # затем: Edit → Preferences → Add-ons → Install… → выбрать zip-пакет аддона # или положить папку mpfb в %APPDATA%\Blender Foundation\Blender\<версия>\scripts\addons ``` После включения аддона в 3D-вьюпорте появляется отдельная вкладка с панелью MPFB, в N-панели — меню операций. ```python import bpy print("blender:", bpy.app.version_string) print("дополнения:", [k for k in bpy.context.preferences.addons.keys() if "mpfb" in k.lower() or "makehuman" in k.lower()]) ``` ### 2. Создание человека Панель MPFB даёт набор базовых параметров: пол, возраст, рост, телосложение (мускулистость, полнота), отдельные блоки под лицо и под тело. Параметры — это не ползунки в сцене, а **таргеты**: контрольные вершины, которые по умолчанию скрыты. ```python import bpy human = next((o for o in bpy.data.objects if o.type == 'MESH'), None) if human: print("объект:", human.name) print("вершин:", len(human.data.vertices)) if human.data.shape_keys: keys = human.data.shape_keys.key_blocks print("ключей формы:", len(keys)) for k in list(keys)[:10]: print(f" {k.name}: value={k.value:.3f} " f"(slider {k.slider_min}..{k.slider_max})") ``` ### 3. Работа с таргетами Таргеты — это shape keys с нулевым значением, которые можно поднимать и опускать. ```python def set_target(obj, name_part, value): """Найти таргет по подстроке имени и выставить значение.""" sk = obj.data.shape_keys if not sk: return None for k in sk.key_blocks: if name_part.lower() in k.name.lower(): k.value = max(k.slider_min, min(k.slider_max, value)) return k return None human = next(o for o in bpy.data.objects if o.type == 'MESH') # Не угадывай имена — выведи их: print([k.name for k in human.data.shape_keys.key_blocks][:20]) ``` > [!NOTE] > Имена таргетов заданы сообществом MakeHuman и включают идиомы вроде `macro`, `detail`, `male`/`female` и коды регионов. Не переводи и не переименовывай их: одежда и пресеты из пакетов рассчитаны на эти имена. Просто выведи список — он сам покажет структуру. ### 4. Скелет MPFB умеет добавить к человеку скелет. Это отдельное действие, и после него меш получает арматуру и веса. ```python rig = next((o for o in bpy.data.objects if o.type == 'ARMATURE'), None) if rig: print("костей:", len(rig.data.bones)) for pb in rig.pose.bones: pb.use_deform_preserve_volume = True ``` Проверка привязки — та же, что и для любого рига: у каждой кости должна быть непустая группа весов. ```python def bone_weight_report(rig, mesh): for b in rig.data.bones[:8]: vg = rig.vertex_groups.get(b.name) if vg is None: print(b.name, "-> нет группы") continue n = sum(1 for v in mesh.data.vertices for g in v.groups if g.group == vg.index) print(b.name, "->", n, "вершин") if rig and human: bone_weight_report(rig, human) ``` ### 5. Одежда Одежда приходит отдельными объектами (прокси в терминологии MakeHuman). Каждый вариант — отдельный объект в сцене, и одежда привязана к телу: при смене параметров таргета она растягивается вместе с ним. ```python bpy.ops.object.select_all(action='DESELECT') cloth = next((o for o in bpy.data.objects if o.type == 'MESH' and o.name != human.name), None) if cloth: cloth.select_set(True) bpy.context.view_layer.objects.active = cloth print("одежда:", cloth.name, "модификаторов:", [m.type for m in cloth.modifiers]) ``` Одежда зачастую приходит с модификаторами (Subdivision, Shrinkwrap или Data Transfer) — не применяй их до экспорта, иначе она перестанет реагировать на таргеты. ### 6. Сохранение и повторное использование Готового человека имеет смысл сохранить отдельным blend-файлом: иначе параметры таргетов придётся подбирать заново. ```python # Отдельный blend под библиотеку персонажей bpy.ops.wm.save_as_mainfile( filepath=r"C:\work\mphuman\humans.fuse.blend", copy=True, # copy=True — текущий файл останется прежним ) # Ассеты из пакетов подключаются через Append, а не Link: # link тянет внешний файл и ломается при его перемещении bpy.ops.wm.append( filepath=r"C:\Users\\AppData\Roaming\Blender Foundation\Blender\4.x\scripts\addons\\assets.human.female.json", directory=r"C:\Users\\AppData\Roaming\Blender Foundation\Blender\4.x\scripts\addons\\", filename="<файл-ресурса>", link=False, ) ``` > [!NOTE] > Путь к пакетам ресурсов зависит от способа установки аддона (архивом в addons или через extensions) и от версии Blender. Точный путь смотри в `Preferences → Add-ons` и в самом аддоне — в текстовом поле ресурсов, если оно есть в панели. ### 7. Экспорт ```python bpy.ops.object.select_all(action='DESELECT') human.select_set(True) if rig: rig.select_set(True) bpy.context.view_layer.objects.active = human bpy.ops.export_scene.fbx( filepath=r"C:\work\mphuman\base_human.fbx", use_selection=True, object_types={'MESH', 'ARMATURE'}, apply_unit_scale=True, add_leaf_bones=False, mesh_smooth_type='FACE', path_mode='AUTO', ) ``` Таргеты в FBX не попадают: морфы превращаются в shape keys только если экспортировать их как Blend Shapes через настройки FBX, и не все движки это читают. Для универсальности применяют таргеты перед экспортом. ```python # Применить все таргеты разом перед экспортом, если движок не понимает морфы sk = human.data.shape_keys if sk: for k in sk.key_blocks: k.value = 0.0 # reset -> значения из вкладки MPFB надо выставить заранее ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```python # Итоговая сводка по сцене: что получилось for o in bpy.data.objects: tris = sum(len(p.vertices) - 2 for p in o.data.polygons) \ if o.type == 'MESH' else 0 print(f"{o.name:24s} {o.type:9s} tris={tris:6d} mods={[m.type for m in o.modifiers]}") ``` | Параметр | Значение | Когда менять | |---|---|---| | `shape_key.value` | `0.0 – 1.0` | диапазон ограничен `slider_min`/`slider_max` конкретного таргета | | `use_deform_preserve_volume` | `True` | `False` — если части тела «схлопываются» на сгибах | | `object_types` | `{'MESH','ARMATURE'}` | `{'MESH'}` — когда риг будет сделан позже (Mixamo, ARP) | | `add_leaf_bones` | `False` | `True` — если движок требует хвосты | | Применять модификаторы одежды | нет | только перед экспортом, если дальше не нужны таргеты | Порядок действий, который экономит время: сначала выставить все таргеты, потом надеть одежду, потом добавить скелет, и только в конце — экспорт. Любая из этих операций в обратном порядке заставит переделывать предыдущую. ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Аддон не появился в списке | не та версия Blender или не распакован архив | Проверить требования релиза, ставить через Install… | | Таргет не находится по имени | имя на языке сообщества, а не английское | Вывести `key_blocks` и искать по подстроке | | Одежда «отстала» от тела | таргет тела изменён после надевания одежды | Настроить таргеты до надевания, либо обновить прокси | | Морфы пропали при экспорте | FBX не выгружает их как Blend Shapes | Применить таргеты перед экспортом | | Персонаж в 10 раз меньше роста | единицы сцены не совпадают с единицами аддона | Проверить `Scene Unit Settings`, применить трансформации | | Скелет не появился | скелет добавляется отдельным действием, не автоматически | Добавить скелет из панели MPFB, затем перепривязать веса | ## 🖥 MCP-ПРОМПТ ```text RU: "MPFB: создать человека, вывести список таргетов, сбросить значения, добавить скелет, применить preserve volume, экспорт FBX" / EN: "MPFB: create human, list shape-key targets, reset values, add skeleton, preserve volume, FBX export" ``` ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 32: DAZ → BLENDER (DIFFEOMORPHIC)](setup_32_daz_diffeomorphic.md), [SETUP 34: METAHUMAN → BLENDER](setup_34_metahuman_blender.md), [SETUP 36: VROID STUDIO → BLENDER](setup_36_vroid_studio.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 02: GENESISCORE LOCAL (OLLAMA)](setup_02_genesiscore_local.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 04: HEADLESS RENDER (CLI / CI/CD)](setup_04_headless_render.md) - **Теги:** #character, #setup --- # SETUP 32: DAZ → BLENDER (DIFFEOMORPHIC) Tags: #blender #character #setup # SETUP 32: DAZ → BLENDER (DIFFEOMORPHIC) **ID:** SETUP-32 | **Type:** Daz Bridge | **Difficulty:** ⭐⭐ | **Time:** 45 min **RU/EN Bilingual** > Когда применять: работаешь с Daz Studio и хочешь переносить персонажа в Blender без потери морфов, JCM и волос. ## 🎯 ЦЕЛЬ Настроить связку Daz Studio + аддон Diffeomorphic в Blender и провести персонажа из Daz в сцену: тело с морфами, коррекции суставов (JCM), волосы и одежда. Разбираем, где Diffeomorphic ускоряет работу, а где всё равно приходится доводить руками, потому что его цель — не «идеальный импорт», а рабочая связка двух программ. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Daz Studio | 4.x | источник персонажа и контент | | Контент Daz | установленный через Daz Central / Install Manager | нужные гены, одежда, пресеты | | Diffeomorphic | релиз под свою пару версий | аддон-мост в Blender | | Blender | версия, поддержанная Diffeomorphic | цель импорта | | Модели Daz | бесплатные стартовые модели | с чего начинать, чтобы проверить связку | > [!NOTE] > Diffeomorphic живёт в репозитории организации Diffeomorphic на GitHub и распространяется в виде скрипта-установщика, который запускается **из Daz Studio** (раздел скриптинга), а не копируется вручную в папку аддонов Blender. Точный способ запуска и требуемые версии — в README актуального релиза: связка версионно-чувствительная, и это её главная хрупкость. ## 🛠️ НАСТРОЙКА ### 1. Порядок установки Порядок жёсткий, и нарушение даёт «аддон включился, но ничего не происходит». 1. Установить Daz Studio и хотя бы один пакет контента через Daz Central. 2. Убедиться, что персонаж открывается в Daz и выглядит нормально. 3. Запустить установщик Diffeomorphic **из Daz Studio**. 4. В Blender: `Edit → Preferences → Add-ons`, найти Diffeomorphic и включить. 5. Перезапустить Blender. ```python import bpy print("blender:", bpy.app.version_string) print("daz/diff аддоны:", [k for k in bpy.context.preferences.addons.keys() if "daz" in k.lower() or "diffeo" in k.lower()]) ``` ### 2. Первичный экспорт из Daz В Daz сохраняем сцену персонажа. Важно, что имя сцены и имя корня в Daz станут именами объектов в Blender — это единственный мост между ними. ```python # Сцены Daz импортируются как отдельные коллекции в Blender for c in bpy.data.collections: print(c.name, "->", len(c.objects), "объектов") ``` ### 3. Импорт персонажа Через панель Diffeomorphic в Blender выбирается нужная сцена из Daz. Основные переключатели, которые имеет смысл выставить сразу: | Переключатель | Состояние | Смысл | |---|---|---| | Import face / body morphs | вкл | тянуть активные морфы лица и тела | | Joint correction morphs (JCM) | вкл | смягчить «слом» на локтях и коленях | | Shape keys вместо поз | как удобнее | иногда JCM проще приехать готовыми ключами | | Materials | по необходимости | тянуть ли материалы UberSurface | > [!NOTE] > Конкретные названия чекбоксов в панели Diffeomorphic отличаются от перечисленных — они зависят от версии аддона. Открывай панель и читай подписи на месте; список выше дан по смыслу, а не как цитата из UI. ### 4. Проверка того, что пришло ```python # Что должно быть в сцене после импорта for o in bpy.data.objects: if o.type == 'MESH': keys = len(o.data.shape_keys.key_blocks) if o.data.shape_keys else 0 print(f"{o.name}: вершин={len(o.data.vertices)} ключей={keys}") elif o.type == 'ARMATURE': print(f"{o.name}: костей={len(o.data.bones)}") # Модификатор Armature на меше — признак того, что деформация подключена for o in bpy.data.objects: if o.type == 'MESH': print(o.name, [m.type for m in o.modifiers]) ``` ### 5. Ручная доводка деформации Даже с JCM на сгибах остаётся провал. Лечится либо весами, либо дополнительным модификатором. ```python arm = next(o for o in bpy.data.objects if o.type == 'ARMATURE') # Dual quaternion: без него плечо и колено заметно «сжимаются» for pb in arm.pose.bones: pb.use_deform_preserve_volume = True # Плавная деформация поверх основной — классический приём для Daz-импорта mesh_ob = next(o for o in bpy.data.objects if o.type == 'MESH' and len(o.data.vertices) > 5000) sub = mesh_ob.modifiers.new("Subdiv_deform", 'SUBSURF') sub.levels = 1 sub.render_levels = 2 ``` ### 6. Волосы Волосы в Daz — генетический ассет, в Blender их надо конвертировать. Два направления: ```python # Проверить, чем представлены волосы после импорта for o in bpy.data.objects: if o.type in {'CURVE', 'SURFACE', 'MESH'}: mods = [m.type for m in o.modifiers] if 'PARTICLE_SYSTEM' in mods or 'CURVES' in mods: print(o.name, "→", mods) ``` Конвертация выполняется из панели Diffeomorphic: волосы переводятся либо в систему частиц, либо в кривые (curves). Выбор влияет на то, чем ты будешь их дальше красить и рендерить. > [!NOTE] > Точные названия пунктов конвертации волос («Particles» / «Curves» / «Hair Curves») менялись между версиями аддона вместе с переходом Blender на новую систему кривых. Открывай панель Diffeomorphic в своей сборке и выбирай там; в коде через bpy эти пункты вызывать не стоит — они часть UI аддона, а не стабильный API. ### 7. Одежда ```python # Одежда в Daz — отдельные объекты со своей физикой; в Blender # симуляцию придётся перенастраивать, веса из Daz не переносятся cloth_like = [o.name for o in bpy.data.objects if o.type == 'MESH' and any( m.type == 'CLOTH' for m in o.modifiers)] print("объекты с физикой:", cloth_like) ``` ### 8. Экспорт для движка ```python bpy.ops.object.select_all(action='DESELECT') for o in bpy.context.scene.objects: if o.type in {'MESH', 'ARMATURE'}: o.select_set(True) bpy.context.view_layer.objects.active = arm bpy.ops.export_scene.fbx( filepath=r"C:\work\export\daz_char.fbx", use_selection=True, object_types={'MESH', 'ARMATURE'}, apply_unit_scale=True, add_leaf_bones=False, mesh_smooth_type='FACE', path_mode='AUTO', ) ``` Морфы в FBX: если движку нужны blendshape-анимации лица, перенеси ключи формы как Blend Shapes через настройки экспорта и проверь, что целевой движок читает именно этот набор. ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```python # Итоговая проверка перед сдачей: полигоны, ключи формы, модификаторы for o in bpy.data.objects: if o.type != 'MESH': continue tris = sum(len(p.vertices) - 2 for p in o.data.polygons) keys = len(o.data.shape_keys.key_blocks) if o.data.shape_keys else 0 print(f"{o.name:30s} tris={tris:7d} shapekeys={keys:3d} " f"mods={[m.type for m in o.modifiers]}") ``` | Параметр | Значение | Когда менять | |---|---|---| | JCM | вкл | выкл — когда морфы конфликтуют с ручными правками | | `use_deform_preserve_volume` | `True` | `False` — если ключи деформируются неожиданно | | `Subsurf.levels` | `1` | `2` — когда сетка от Daz слишком грубая для крупных планов | | `object_types` | `{'MESH','ARMATURE'}` | `{'MESH'}` — если риг будет свой | | `add_leaf_bones` | `False` | `True` — под требования движка | | `mesh_smooth_type` | `FACE` | `EDGE` — для хард-нормалей | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Аддон включился, панели нет | ставился в папку аддонов вручную вместо установщика из Daz | Ставить установщиком из Daz Studio, потом перезапустить Blender | | Импортируется пустая сцена | сцена в Daz не сохранена под тем же именем | Сохрани сцену в Daz, проверь имя сцены | | Пропали морфы лица | переключатель импорта морфов выключен | Включить импорт face/body morphs и переимпортировать | | JCM ломает позу вручную | JCM-морфы конфликтуют с твоими правками | Выключить JCM, оставить только форму и веса | | Колено проваливается | линейные веса на тонкой сгибной зоне | `use_deform_preserve_volume = True` | | Волосы невидимы в рендере | частицы без рендерера или кривые без материала | Назначь материал и проверь настройки рендера волос | | Одежда «протекает» сквозь тело | физика Daz не перенесена, весов нет | Перенастроить Cloth/Sim либо сделать одежду статичной | | Импорт занимает десятки минут | сцена с тяжёлым контентом и Subdiv | Отключай невидимые объекты до импорта, импортируй по частям | ## 🖥 MCP-ПРОМПТ ```text RU: "После импорта Diffeomorphic: включи preserve volume на всех костях, добавь Subsurf 1 на основной меш, перечисли ключи формы, проверь модификаторы и выгрузи FBX" / EN: "After Diffeomorphic import: enable preserve volume on all bones, add Subsurf 1 to the body mesh, list shape keys, audit modifiers and export FBX" ``` ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 34: METAHUMAN → BLENDER](setup_34_metahuman_blender.md), [SETUP 36: VROID STUDIO → BLENDER](setup_36_vroid_studio.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 07: CI/CD INTEGRATION](setup_07_ci_cd.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 09: BLENDERBOT / AI CODING AGENT](setup_09_blenderbot_ai.md), [SETUP 10: BLENDER + COMPOSITING (NUKE/AFTER EFFECTS ROUNDTRIP)](setup_10_compositing_roundtrip.md) - **Теги:** #blender, #character, #setup --- # SETUP 33: CHARACTER CREATOR 4 → BLENDER Tags: #blender #setup # SETUP 33: CHARACTER CREATOR 4 → BLENDER **ID:** SETUP-33 | **Type:** CC Pipeline | **Difficulty:** ⭐⭐ | **Time:** 35 min **RU/EN Bilingual** > Когда применять: персонаж сделан в Reallusion Character Creator 4 и нужно либо живую связь с Blender, либо честный FBX-экспорт с shape keys, LOD и нормалями. ## 🎯 ЦЕЛЬ Настроить пайплайн Reallusion Character Creator 4 → Blender: выбрать правильный режим выгрузки (живой пайплайн против файлового экспорта), корректно перенести материалы, морфы лица и LOD, и проверить, что нормали и микрокарты не развалились. Разбираем разницу между двумя подходами и её последствия для анимации. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Character Creator 4 | подписка (есть триал) | источник персонажа | | Blender Pipeline Plugin for Blender | ставится из самого CC4 | живая связь CC4 → Blender | | Blender | версия, поддержанная плагином | приёмник | | CC-ресурсы | одежда, волосы, детали лица | сам персонаж | > [!NOTE] > Blender-пайплайн — не сторонний аддон: его файлы генерирует сама CC4. Устанавливается через `Edit → Preferences → Add-ons → Install` в Blender, при этом путь к файлам плагина задаётся в самой CC4 (`Pipeline → Blender`). Проверь, какая версия Blender поддерживается твоей сборкой CC4 — плагин версионно-чувствителен. ## 🛠️ НАСТРОЙКА ### 1. Сборка персонажа в CC4 Сначала собираем персонажа в самом CC4: тело (Base), одежда, детали лица, материалы. Принцип: всё, что нужно в Blender, должно быть применено **до** выгрузки — на стороне Blender часть параметров уже недоступна. | Что задать в CC4 | Зачем | Типичная ошибка | |---|---|---| | Material Type (Standard / HD) | определяет, какие карты вообще будут выгружены | оставить Standard, потом удивляться отсутствию roughness | | Sub-division level | влияет на плотность сетки и размер FBX | выставить слишком высокое и получить 3 млн полигонов | | Morph targets лица | они станут shape keys в Blender | не отметить и потерять мимику | | Auto-LOD | генерация упрощённых уровней для движка | не включить и грузить LOD0 везде | ### 2. Подготовка Blender ```python import bpy # Единицы: CC4 работает в сантиметрах, Blender — в метрах. # Неверная настройка = персонаж ростом 3 метра или 3 сантиметра. us = bpy.context.scene.unit_settings print("scale_length:", us.scale_length, "system:", us.system) # Чистая сцена — иначе плагин подцепит чужие объекты bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete(use_global=False) ``` ### 3. Вариант A — живой пайплайн CC4 В CC4 открываешь `Pipeline → Blender`, запускаешь соединение, в Blender в появившейся панели выбираешь сцену и нажимаешь импорт. Меш появляется в сцене, и дальше работаешь как с обычной геометрией. Плюсы: можно перезалить после правок в CC4 без ручного экспорта. Минусы: связь хрупкая — падение коннекта оставляет сцену в промежуточном состоянии, и перед любой тяжёлой операцией (рендер фермы, экспорт) лучше запечь результат в обычные объекты. ```python # Что реально приехало в сцену for o in bpy.data.objects: if o.type == 'MESH': keys = len(o.data.shape_keys.key_blocks) if o.data.shape_keys else 0 print(f"{o.name}: вершин={len(o.data.vertices)} shape_keys={keys}") ``` ### 4. Вариант B — файловый экспорт FBX Когда плагина нет или нужна воспроизводимая выгрузка. В CC4: `Pipeline → Export Settings`, формат FBX, затем экспорт в файл; в Blender — импорт. ```python bpy.ops.import_scene.fbx( filepath=r"C:\work\cc4\char.fbx", automatic_bone_orientation=True, use_anim=True, mesh_smooth_type='FACE', ) ``` В настройках импорта FBX решающими являются `mesh_smooth_type` (как сглаживание выгружено) и `use_custom_normals`, если он доступен в твоей версии. > [!NOTE] > `use_custom_normals` был удалён из импортёра FBX в ветке 4.x, где нормали считаются через атрибут mesh. Если параметра нет в списке — это не ошибка установки, а изменение API. Ориентируйся на `mesh.use_auto_smooth` (до 4.1) либо на операторы `shade_smooth_by_angle` / `shade_auto_smooth` в 4.1+. ### 5. Материалы Материалы CC4 приезжают как набор карт, а не как готовый шейдер Blender. Собираем Principled вручную или через скрипт. ```python import bpy mat = bpy.data.materials.get("CC4_Skin") or bpy.data.materials.new("CC4_Skin") mat.use_nodes = True nt = mat.node_tree bsdf = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED') # Имена сокетов Principled менялись в 4.0 — печатаем, а не угадываем print("сокеты:", [s.name for s in bsdf.inputs]) for path in ("diffuse", "normal", "roughness", "metallic", "opacity"): img = bpy.data.images.load(rf"C:\work\cc4\textures\skin_{path}.jpg", check_existing=True) tex = nt.nodes.new('ShaderNodeTexImage') tex.image = img tex.location = (-600, 200) nt.links.new(tex.outputs['Color'], bsdf.inputs['Base Color']) ``` > [!NOTE] > В Blender 4.0 сокеты Principled BSDF были переименованы (например, `Specular` → `Specular IOR Level`, `Subsurface` → `Subsurface Weight`, `Emission` → `Emission Color`). Сопоставляй по выведенному списку сокетов, а не по памяти. ### 6. Микрокарты и нормали Отдельная боль CC4: мелкие детали (поры, складки) живут в картах. Без них персонаж выглядит резиновым. ```python # Проверить, что карты действительно подключены, а не просто лежат в файле nt = bpy.data.materials["CC4_Skin"].node_tree for n in nt.nodes: if n.type == 'TEX_IMAGE': print(n.image.filepath, "| size:", tuple(n.image.size)) ``` ### 7. LOD для движка ```python # Считаем бюджет: во сколько раз должен упасть LOD1 относительно LOD0 def tris(ob): return sum(len(p.vertices) - 2 for p in ob.data.polygons) for o in bpy.data.objects: if o.type == 'MESH': print(f"{o.name}: {tris(o)} треугольников") ``` LOD-объекты в CC4 приходят отдельными мешами с готовыми упрощениями; в Blender их нужно либо оставить отдельными объектами, либо вручную применить Decimate. ```python lod0 = bpy.data.objects.get("char_lod0") if lod0: dec = lod0.modifiers.new("Decimate", 'DECIMATE') dec.ratio = 0.5 dec.use_collapse_triangulate = True ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```python # Сводка перед экспортом в движок sc = bpy.context.scene print("объектов:", len(sc.objects)) print("материалов:", len(bpy.data.materials)) print("изображений:", len(bpy.data.images)) print("unit scale:", sc.unit_settings.scale_length) ``` | Параметр | Значение | Когда менять | |---|---|---| | `scene.unit_settings.scale_length` | соответствует единицам CC4 | расхождение размера в 100 раз — классика | | `mesh_smooth_type` | `FACE` | `EDGE` — когда нужны хард-нормали на рёбрах | | `Decimate.ratio` | `0.5` | меньше — для агрессивного LOD, больше — почти не заметно | | Material Type в CC4 | `HD` для крупных планов | `Standard` — для игровых ассетов | | Sub-division level в CC4 | по бюджету полигонов | выше — студийный рендер, ниже — реальное время | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Персонаж в 100 раз больше/меньше | не совпали единицы сцены | Проверить `unit_settings.scale_length` и применить трансформации | | Плагин Blender в CC4 не находит сцену | не задан путь к Blender в настройках пайплайна | Указать путь в `Pipeline → Blender` и перезапустить CC4 | | Мимица не переехала | в CC4 не включён экспорт морфов | Отметить Morph targets и перевыгрузить | | Кожа «резиновая» | не приехали карты деталей | Проверить наличие карт в `bpy.data.images` и подключить к материалу | | Чёрные полосы на сгибах | нормали не импортированы | `shade_smooth_by_angle` либо настройки сглаживания в CC4 | | Потеряны RL-специфичные данные | в FBX нет формата для этих данных | Экспортировать в glTF/GLB вместо FBX, если приёмник умеет | | Экспорт не повторяется из CC4 | ручные правки в Blender не попали обратно | Живой пайплайн — только в одну сторону, правки держи в CC4 | ## 🖥 MCP-ПРОМПТ ```text RU: "Проверь unit scale, собери Principled из карт CC4, выведи список shape keys и LOD-объектов с числом треугольников" / EN: "Check unit scale, build a Principled shader from CC4 maps, list shape keys and LOD objects with triangle counts" ``` ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 07: CI/CD INTEGRATION](setup_07_ci_cd.md), [SETUP 09: BLENDERBOT / AI CODING AGENT](setup_09_blenderbot_ai.md), [SETUP 10: BLENDER + COMPOSITING (NUKE/AFTER EFFECTS ROUNDTRIP)](setup_10_compositing_roundtrip.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 11: BLENDER + TOUCHDESIGNER REALTIME VISUALS](setup_11_touchdesigner.md), [SETUP 12: BLENDER + UNREAL ENGINE 5 LIVE LINK](setup_12_unreal_livelink.md) - **Теги:** #blender, #setup --- # SETUP 34: METAHUMAN → BLENDER Tags: #blender #character #setup # SETUP 34: METAHUMAN → BLENDER **ID:** SETUP-34 | **Type:** MetaHuman | **Difficulty:** ⭐⭐⭐ | **Time:** 60 min **RU/EN Bilingual** > Когда применять: персонаж MetaHuman сделан в MetaHuman Creator и нужно отрендерить его в Blender или вытащить в другой движок. ## 🎯 ЦЕЛЬ Провести MetaHuman в Blender по официальному пути (MetaHuman Creator → Unreal Engine → Quixel Bridge → FBX) и собрать рабочий материал в Principled BSDF. Отдельно разбираем то, что Epic не поддерживает официально: волосы (groom), материалы и LOD — там нужен ручной обход, и честное описание этого обхода полезнее обещаний «одной кнопкой». ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | MetaHuman Creator | в браузере, актуальный | создание персонажа | | Unreal Engine | 5.x | обязательный посредник: MetaHuman живёт в UE | | Quixel Bridge | плагин для UE | импорт MetaHuman и экспорт в Blender | | Blender | 4.x | сборка материалов и рендер | > [!NOTE] > Официального аддона Epic для Blender не существует: MetaHuman — актив внутри Unreal Engine, а не формат. Всё, что ниже, — это связка UE + Bridge + FBX, а не «импорт MetaHuman в Blender». Если где-то обещают прямой импорт, стоит уточнить, чей это код и какой у него статус поддержки. ## 🛠️ НАСТРОЙКА ### 1. Создание персонажа MetaHuman Creator работает в браузере: собираешь лицо и тело, применяешь материал из библиотеки, выбираешь уровень детализации. На выходе — акты в Content Browser проекта Unreal. ```text 1. MetaHuman Creator в браузере → собрать лицо/тело 2. Export to Unreal Engine → выбрать проект UE и качество (Nanite / нет) 3. В UE: Content → готовые ассеты персонажа (Body, Face, Eyelashes, Hair) 4. Для каждого ассета: LOD-уровни создаются автоматически ``` ### 2. Установка Bridge для UE ```text 1. Quixel Bridge ставится из Epic Games Launcher как плагин к вашему UE 2. В UE: Window → Quixel Bridge → Launch → вкладка сцены 3. Авторизация через аккаунт Epic ``` > [!NOTE] > Quixel Bridge периодически переписывается, а часть функций переезжала между плагинами и встроенными инструментами UE. Перед тем как строить на нём пайплайн, открой его в своей версии UE и посмотри, есть ли вкладка экспорта в Blender: если её нет, работай через обычный FBX-экспорт из UE. ### 3. Экспорт из UE в FBX ```text 1. В Content Browser выбрать ассет персонажа 2. Bridge → Export → выбрать целевой путь и набор ассетов (Body LOD0..LOD4, Face, Eyelashes) 3. Формат: FBX, Include → Meshes / Skeletal Meshes 4. Настройки: Import into Scene выключить, если работаешь через файл ``` Если Bridge не даёт экспорт — тот же результат даёт стандартный экспорт UE: выделить Static/Skeletal Mesh → Asset Actions → Export → FBX. ### 4. Импорт в Blender и базовая проверка ```python import bpy bpy.ops.import_scene.fbx( filepath=r"C:\work\metahuman\mh_body_LOD0.fbx", automatic_bone_orientation=True, use_anim=False, mesh_smooth_type='FACE', ) for o in bpy.data.objects: if o.type == 'MESH': tris = sum(len(p.vertices) - 2 for p in o.data.polygons) print(f"{o.name}: вершин={len(o.data.vertices)} треугольников={tris}") elif o.type == 'ARMATURE': print(f"{o.name}: костей={len(o.data.bones)}") ``` ### 5. Материалы: главный объём работы MetaHuman использует собственный шейдер на текстурах, в Blender его нет. Карты (albedo, normal, roughness, RMA) приезжают файлами, шейдер собирается вручную. ```python import bpy def build_skin_material(name, base_color_path, normal_path, rma_path): """Собрать кожу MetaHuman из карт. RMA = R:ao G:roughness B:metallic.""" mat = bpy.data.materials.get(name) or bpy.data.materials.new(name) mat.use_nodes = True nt = mat.node_tree bsdf = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED') def tex(path): n = nt.nodes.new('ShaderNodeTexImage') n.image = bpy.data.images.load(path, check_existing=True) n.location = (-800, 0) return n nt.links.new(tex(base_color_path).outputs['Color'], bsdf.inputs['Base Color']) nt.links.new(tex(normal_path).outputs['Color'], bsdf.inputs['Normal']) # RMA -> отдельные каналы sep = nt.nodes.new('ShaderNodeSeparateColor') sep.location = (-500, -200) nt.links.new(tex(rma_path).outputs['Color'], sep.inputs['Color']) nt.links.new(sep.outputs['Green'], bsdf.inputs['Roughness']) # G = roughness nt.links.new(sep.outputs['Blue'], bsdf.inputs['Metallic']) # B = metallic return mat ``` > [!NOTE] > Сокеты Principled BSDF переименовывались в Blender 4.x (`Subsurface` → `Subsurface Weight`, `Specular` → `Specular IOR Level`, `Emission` → `Emission Color`). Прежде чем соединять ноды кодом, выведи список: `print([s.name for s in bsdf.inputs])`. ### 6. Нормали: проверка обязательна MetaHuman запекает нормали в карты с перевёрнутым зелёным каналом под DirectX. В Blender по умолчанию OpenGL, поэтому зелёный канал надо инвертировать — иначе всё будет «в пузырях». ```python n = bpy.data.images.load(r"C:\work\metahuman\textures\body_nrm.png", check_existing=True) n.colorspace_settings.name = 'Non-Color' print("colorspace:", n.colorspace_settings.name, "| alpha_mode:", n.alpha_mode) ``` Инверсия делается нодой **RGB to BW → Invert** либо переключателем цветового пространства; какой именно путь выбрал твой пайплайн — вопрос вкуса, результат один. ### 7. Волосы (groom) Самый болезненный пункт. Волосы MetaHuman — groom-ассеты UE, в Blender их нет. | Вариант | Что получится | Цена | |---|---|---| | Оставить без волос | лысый персонаж | 0 | | Конвертировать groom в кривые в UE и выгрузить | отдельный объект кривых, без физ-привязки к голове | высокая, геометрия тяжёлая | | Взять готовую причёску из стока | согласованный вид, но не «тот самой» MetaHuman | средняя | > [!NOTE] > Переход «Groom → Curves» есть в Unreal, и получившиеся кривые можно выгрузить через FBX. Но качество сильно зависит от настроек конвертации, и результат не эквивалентен groom-ассету UE. Не рассчитывай на round-trip. ### 8. LOD ```python def tris(ob): return sum(len(p.vertices) - 2 for p in ob.data.polygons) for name in ("mh_body_LOD0", "mh_body_LOD1", "mh_body_LOD2"): o = bpy.data.objects.get(name) print(name, "→", tris(o) if o else "нет объекта") ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```python # Итог перед рендером: материалы, текстуры, бюджет геометрии print("материалов:", len(bpy.data.materials)) print("изображений:", len(bpy.data.images)) for img in bpy.data.images: if img.source == 'FILE': print(f" {img.name} {tuple(img.size)} {img.colorspace_settings.name}") ``` | Параметр | Значение | Когда менять | |---|---|---| | `image.colorspace_settings.name` | `Non-Color` для карт данных | `sRGB` только для цветных карт (albedo) | | LOD | `LOD0` для рендера | `LOD2`/`LOD3` — для превью и реального времени | | `mesh_smooth_type` | `FACE` | `EDGE` — когда нужны хард-нормали | | RMA-разделение | `R`=AO, `G`=roughness, `B`=metallic | у MetaHuman раскладка именно такая | | Принципиал 4.x | сокеты с новыми именами | сверяться по выводу `bsdf.inputs` | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Экспорт в Blender недоступен в Bridge | функция переехала или изменился плагин | Экспортировать FBX стандартными средствами UE | | Нет волос | groom — формат UE, не Blender | Конвертировать в кривые в UE либо взять стоковую причёску | | Кожа в «пузырях» | зелёный канал нормалей не инвертирован | Инвертировать G или включить DirectX-трактовку | | Металлический или стеклянный вид | каналы RMA перепутаны | Проверить раскладку: G=roughness, B=metallic | | `KeyError` при обращении к сокету | сокет переименован в 4.x | Вывести `[s.name for s in bsdf.inputs]` и соединять по факту | | Текстуры фиолетовые | для карт данных стоит sRGB | Поставить `Non-Color` | | Анимация не едет | FBX выгружен без скелета | Экспортировать Skeletal Mesh, а не Static Mesh | | Файл сцены весит сотни мегабайнут | втянуты все LOD и микротекстуры | Оставить LOD0–LOD2, крупные карты вынести наружу | ## 🖥 MCP-ПРОМПТ ```text RU: "Собери материал кожи MetaHuman из albedo/normal/RMA, поставь Non-Color на карты данных, выведи LOD-бюджет треугольников" / EN: "Build a MetaHuman skin material from albedo/normal/RMA, set Non-Color on data maps, print LOD triangle budgets" ``` ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 32: DAZ → BLENDER (DIFFEOMORPHIC)](setup_32_daz_diffeomorphic.md), [SETUP 36: VROID STUDIO → BLENDER](setup_36_vroid_studio.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 07: CI/CD INTEGRATION](setup_07_ci_cd.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 09: BLENDERBOT / AI CODING AGENT](setup_09_blenderbot_ai.md), [SETUP 10: BLENDER + COMPOSITING (NUKE/AFTER EFFECTS ROUNDTRIP)](setup_10_compositing_roundtrip.md) - **Теги:** #blender, #character, #setup --- # SETUP 35: READY PLAYER ME AVATARS Tags: #automation #blender #llm #mcp #rigging #setup #stable-diffusion # SETUP 35: READY PLAYER ME AVATARS **ID:** SETUP-35 | **Type:** Avatar | **Difficulty:** ⭐ | **Time:** 15 min **RU/EN Bilingual** > Когда применять: нужны десятки аватаров с готовым гуманоидным ригом для Web3, VR или быстрого прототипа — без моделирования и без риггинга. ## 🎯 ЦЕЛЬ Настроить связку Ready Player Me → Blender: получить GLB по ссылке на аватар, импортировать его вместе с текстурой, проверить скелет и собрать коллекцию из нескольких аватаров одним проходом. Отдельный акцент — на пакетной обработке: ручная загрузка двадцати аватаров не масштабируется. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Ready Player Me | облачный сервис, `readyplayer.me` | генерация аватаров и выдача GLB | | Аккаунт | регистрация на сайте | собственные аватары | | Blender | 4.x | импорт glTF | | glTF-импортёр | встроенный в Blender | формат GLB | > [!NOTE] > Ready Player Me выдаёт две вещи: 3D-модель (GLB) и 2D-фото аватара (PNG). Ссылки вида `https://models.readyplayer.me/.glb` и `https://models.readyplayer.me/.png` открываются без ключа — это публичные ссылки конкретного аватара. Полноценный REST API с ключом и созданием аватаров программно требует подписки, поэтому массовая генерация «по описанию» и массовая загрузка готовых ID — разные задачи с разными требованиями. ## 🛠️ НАСТРОЙКА ### 1. Получение ссылки Создаёшь аватар на сайте, копируешь его ID (последний сегмент ссылки на аватар) и подставляешь в шаблон. ```python import urllib.request AVATAR_ID = "" BASE = "https://models.readyplayer.me" def fetch(url, out): """Скачать бинарный файл по ссылке. Returns (путь, размер в байтах).""" req = urllib.request.Request(url, headers={"User-Agent": "blender-script"}) with urllib.request.urlopen(req, timeout=60) as r: data = r.read() with open(out, "wb") as f: f.write(data) return out, len(data) glb = f"{BASE}/{AVATAR_ID}.glb" png = f"{BASE}/{AVATAR_ID}.png?size=512" print("3D:", glb) print("2D:", png) ``` > [!NOTE] > Параметры query-строки у ссылок RPM (например `size` для PNG или параметры текстурного атласа для GLB) менялись по мере развития сервиса. Базовые ссылки выше стабильны; точный список допустимых параметров — в документации RPM на readyplayer.me. ### 2. Импорт GLB ```python import bpy bpy.ops.import_scene.gltf( filepath=r"C:\work\rpm\avatar.glb", merge_vertices=True, import_pack_images=True, ) ``` `merge_vertices` важен для двух вещей: меньше мусора в топологии и корректные нормали. `import_pack_images` упаковывает текстуры внутрь blend-файла — для батча это удобно, для тяжёлых аватаров избыточно. ### 3. Проверка скелета Аватары RPM приезжают с гуманоидным ригом, совместимым с Mixamo, и с костями глаз. ```python arm = next((o for o in bpy.data.objects if o.type == 'ARMATURE'), None) if arm: print("костей:", len(arm.data.bones)) print("есть кисть:", "LeftHand" in arm.data.bones or any("Hand" in b.name for b in arm.data.bones)) print("есть пальцы:", any("Index" in b.name for b in arm.data.bones)) ``` Пальцев в стандартном риге RPM нет — как и у Mixamo. Если персонаж должен жестикулировать пальцами, риг дописывается вручную. ### 4. Разбор структуры объектов ```python for o in bpy.context.scene.objects: if o.type == 'MESH': tris = sum(len(p.vertices) - 2 for p in o.data.polygons) print(f"{o.name}: tris={tris} mods={[m.type for m in o.modifiers]} " f"mats={[m.name for m in o.data.materials]}") else: print(f"{o.name}: {o.type}") ``` RPM часто привозит несколько мешей (тело, волосы, одежда, глаза). Это нормально: они разделяются намеренно, чтобы волосы можно было заменить без перезагрузки всего аватара. ### 5. Что лежит внутри GLB Формат glTF упаковывает в себя геометрию, текстуры и скелет, поэтому один файл может содержать сразу всё. Стоит знать заранее, что ожидать: | Внутри | Как выглядит в Blender | На что смотреть | |---|---|---| | Тело и одежда | отдельные Mesh-объекты | количество объектов, лишние дубли | | Текстурный атлас | один Image + UV-слой | размер атласа, не растянут ли | | Вершинные цвета | Color Attribute на меше | без них аватар станет серым | | Скелет | Armature + Skin-модификатор | имена костей и порядок | | Выражения лица | Shape Keys на голове | набор ключей, нумерация | ```python # Разбор одного GLB: скелет, ключи лица, UV и вершинные цвета mesh = next(o for o in bpy.data.objects if o.type == 'MESH' and o.name != "Armature") print("UV-слоёв:", len(mesh.data.uv_layers)) print("вершинных цветов:", len(mesh.data.color_attributes)) print("шейп-ключей:", [k.name for k in mesh.data.shape_keys.key_blocks][:12] if mesh.data.shape_keys else "нет") print("модификаторы:", [(m.name, m.type) for m in mesh.modifiers]) ``` ### 6. Батч: коллекция из N аватаров ```python import os OUT = r"C:\work\rpm" AVATARS = ["", "", "", "", ""] os.makedirs(OUT, exist_ok=True) def import_avatar(avatar_id, collection_name): path = os.path.join(OUT, f"{avatar_id}.glb") if not os.path.exists(path): fetch(f"{BASE}/{avatar_id}.glb", path) before = set(bpy.data.objects) bpy.ops.import_scene.gltf(filepath=path, merge_vertices=True) new = [o for o in bpy.data.objects if o not in before] coll = bpy.data.collections.get(collection_name) or \ bpy.data.collections.new(collection_name) if coll.name not in bpy.context.scene.collection.children: bpy.context.scene.collection.children.link(coll) for o in new: for c in list(o.users_collection): c.objects.unlink(o) coll.objects.link(o) return coll, new for aid in AVATARS: coll, objs = import_avatar(aid, "RPM_batch") print(aid, "->", len(objs), "объектов") ``` ### 7. Расстановка по сцене ```python # Аватары одного батча перекрываются: разложить в ряд colls = [c for c in bpy.data.collections if c.name == "RPM_batch"] for i, coll in enumerate(colls): for o in coll.objects: o.location.x = i * 1.0 ``` ### 8. Проверка материалов и вершинных цветов Аватары RPM используют один текстурный атлас с вершинными цветами: цвет волос и кожи часто приезжает именно вершинными цветами, а не отдельными картами. Если их не сохранить при экспорте, персонаж станет серым. ```python mesh = next(o for o in bpy.data.objects if o.type == 'MESH') print("vertex colors:", len(mesh.data.color_attributes)) for ca in mesh.data.color_attributes: print(" слой:", ca.name, "| домен:", ca.domain, "| data_type:", ca.data_type) # Цвета атрибута -> Color Attribute нода в шейдере материала mat = next((m for m in bpy.data.materials if m and m.use_nodes), None) if mat: nt = mat.node_tree attr = nt.nodes.new('ShaderNodeAttribute') attr.attribute_type = 'GEOMETRY' attr.attribute_name = mesh.data.color_attributes[0].name attr.location = (-600, -300) bsdf = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED') nt.links.new(attr.outputs['Color'], bsdf.inputs['Base Color']) print("подключено:", attr.attribute_name) ``` ### 9. 2D-портрет Фото аватара используется как референс при текстурировании вручную либо как плоский «аватар» для тех, кому нужен 2D-стиккер. ```python fetch(f"{BASE}/{AVATARS[0]}.png?size=512", r"C:\work\rpm\portrait.png") img = bpy.data.images.load(r"C:\work\rpm\portrait.png", check_existing=True) print("портрет:", tuple(img.size), "| colorspace:", img.colorspace_settings.name) ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```python # Сводка по батчу: сколько аватаров, сколько полигонов суммарно total = 0 for o in bpy.data.objects: if o.type == 'MESH': total += sum(len(p.vertices) - 2 for p in o.data.polygons) print("всего треугольников:", total) ``` | Параметр | Значение | Когда менять | |---|---|---| | `merge_vertices` | `True` | `False` — когда важно сохранить исходные UV-швы | | `import_pack_images` | `True` | `False` — при большом батче, чтобы не раздувать blend-файл | | PNG `size` | `512` | `256` для превью, `1024` для качественной текстуры | | Коллекция | отдельная на каждый аватар или общая на батч | отдельная — когда аватары редактируются независимо | | Кости глаз | оставить | удалить, если анимация лица делается морфами | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | 404 на ссылку `.glb` | неверный ID или приватный аватар | Проверить ID на странице аватара, убрать пробелы | | Текстуры не подтянулись | атлас не скачался | `import_pack_images=True`, при необходимости скачать вручную | | Модель маленькая | аватар нормализован по своему росту | Масштабировать вручную, единицы сцены не при чём | | Кости не на месте | импорт разложил арматуру в сторону | Переимпортировать с `merge_vertices` и проверить `use_transform` | | Все аватары в одной точке | импорт не сдвигает объекты | Расставить по X вручную, скриптом | | Нет нормалей | GLB без нормалей | `shade_smooth_by_angle` либо перегенерировать | | Пальцев нет | в риге RPM их нет по умолчанию | Дописать вручную или взять риг с пальцами (ARP, Rigify) | ## 🖥 MCP-ПРОМПТ ```text RU: "Скачай GLB по ID, импортируй в отдельную коллекцию, merge_vertices=True, выведи треугольники и список костей, расставь 10 аватаров по X с шагом 1.0" / EN: "Download GLB by ID, import into its own collection, merge_vertices=True, print triangle count and bone list, lay out 10 avatars along X with 1.0 spacing" ``` ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 45: HUNYUAN3D 2.1 LOCAL](setup_45_hunyuan3d_21.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 02: GENESISCORE LOCAL (OLLAMA)](setup_02_genesiscore_local.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 09: BLENDERBOT / AI CODING AGENT](setup_09_blenderbot_ai.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 15: BLENDER + COMFYUI SDXL](setup_15_comfyui_sdxl.md), [SETUP 16: BLENDER + AUTOMATIC1111 API](setup_16_automatic1111_api.md) - **Теги:** #automation, #blender, #llm, #mcp, #rigging, #setup, #stable-diffusion --- # SETUP 36: VROID STUDIO → BLENDER Tags: #blender #character #setup # SETUP 36: VROID STUDIO → BLENDER **ID:** SETUP-36 | **Type:** Anime | **Difficulty:** ⭐⭐ | **Time:** 25 min **RU/EN Bilingual** > Когда применять: делаешь аниме-аватара в VRoid Studio и нужно довести его до рендера или до VRM-сцены в Blender. ## 🎯 ЦЕЛЬ Импортировать VRM 1.0 в Blender, разобраться, что делать с материалами MToon и пружинными костями волос, и честно оценить, что из правок Blender переживёт обратный экспорт в VRoid Studio. Последний пункт важнее остальных: формат VRM проектировался для «модель + риг + выражения лица», и любая ручная правка геометрии в Blender за пределами допустимого почти всегда ломает обратный путь. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | VRoid Studio | актуальная | создание модели и выгрузка VRM | | VRM 1.0 | спецификация, `vrM.dev` | формат обмена | | Blender | 4.x | импорт, рендер, доработка | | VRM-аддон для Blender | входит в поставку Blender, включается в Preferences → Add-ons | импорт и экспорт VRM | > [!NOTE] > Аддон импорта VRM есть в поставке Blender, но в разных версиях он называется и лежит по-разному («VRM Add-on for Blender» / включён через Add-ons / вынесен в расширения). Плюс существует популярная одноимённая версия аддона от сообщества — с куда большим числом настроек. Проверь в `Preferences → Add-ons`, что именно у тебя включено, до того как писать пайплайн: у них разные возможности экспорта. ## 🛠️ НАСТРОЙКА ### 1. Подготовка модели в VRoid Studio Модель настраивается до выгрузки: поза, материалы, набор выражений лица, шейдер. Важное правило — VRM хранит только то, что описано стандартом; всё остальное при обратном импорте теряется. ```text 1. VRoid Studio → собрать модель (тело, одежда, причёска) 2. Настроить материалы: MToon, параметры (outline, rim, matcap) 3. Задать набор выражений лица (A / I / U / E / O, blink и т.д.) 4. Pose → выбрать позу (обычно A-Pose) → File → Export → VRM 1.0 ``` ### 2. Импорт в Blender ```python import bpy print("blender:", bpy.app.version_string) print("vrm-аддоны:", [k for k in bpy.context.preferences.addons.keys() if "vrm" in k.lower()]) bpy.ops.import_scene.vrm(filepath=r"C:\work\vrm\avatar.vrm") ``` Оператор `import_scene.vrm` появляется только после включения аддона. Если его нет — версия аддона не совпадает с версией Blender. ```python for o in bpy.context.scene.objects: if o.type == 'MESH': tris = sum(len(p.vertices) - 2 for p in o.data.polygons) mats = [m.name for m in o.data.materials] print(f"{o.name}: tris={tris} mats={mats}") elif o.type == 'ARMATURE': print(f"{o.name}: костей={len(o.data.bones)}") ``` ### 3. Материалы MToon MToon — это toon-шейдер, заточенный под нелинейное освещение в VR-движках. В Blender его нет: аддон либо собирает node group по параметрам MToon, либо (в части версий аддона) умеет привести материал к стандартному Principled BSDF. ```python # Посмотреть, что вообще собрал аддон for o in bpy.data.objects: if o.type != 'MESH': continue for m in o.data.materials: if not m or not m.use_nodes: continue kinds = sorted({n.type for n in m.node_tree.nodes}) print(m.name, "->", kinds) ``` > [!NOTE] > Пункт «конвертировать MToon в Principled» существует не во всех сборках аддона и называется по-разному. Если в твоей версии такого пункта нет — это не баг, а ограничение: придётся собирать шейдер вручную, и toon-look придётся воспроизводить освещением (Flat/Toon шейдер Cycles), а не материалом. ### 4. Пружинные кости Волосы и некоторые элементы одежды анимируются spring bones — цепочка костей, колеблющихся от движения головы. В VRoid Studio это отдельная сущность; в Blender она либо приезжает как цепочка костей, либо теряется. ```python # Найти цепочки: кости без родителя, но с потомками — кандидаты на пружины arm = next((o for o in bpy.data.objects if o.type == 'ARMATURE'), None) if arm: for b in arm.data.bones: if b.parent is None or not b.use_connect: kids = [c.name for c in b.children] if kids: print("корень цепочки:", b.name, "-> потомки:", kids[:4]) ``` Что делать, если пружины не приехали: пересобрать волосы как цепочку костей вручную и анимировать через drivers либо через `Hook`/constraints. Это отдельная работа на несколько часов, и обратно в VRoid Studio она не экспортируется. ### 5. Обратный экспорт — что теряется ```python # Перед экспортом: что вообще можно выгрузить обратно в VRM print("mesh objects:", sum(1 for o in bpy.data.objects if o.type == 'MESH')) print("shape keys:", sum(len(o.data.shape_keys.key_blocks) for o in bpy.data.objects if o.type == 'MESH' and o.data.shape_keys)) # Экспорт через аддон VRM bpy.ops.export_scene.vrm(filepath=r"C:\work\vrm\avatar_out.vrm") ``` Реалистичный список потерь: любые нестандартные модификаторы, добавленные тобой меши, новые кости без привязки к стандартным именам VRM, изменения UV, материалы, которые не являются MToon. | Что делал в Blender | Вернётся ли в VRoid Studio | |---|---| | Подвинул вершины | нет | | Добавил shape keys | нет | | Новый материал | нет | | Удалил кость стандарта VRM | нет, файл может стать невалидным | | Переименовал кость | нет, сломает стандарт | | Ничего не менял | да, round-trip безопасен | ### 6. Для VRM-движка — не трогай Blender Если конечная цель — VRM-сцена в Unity или WebXR, правильный путь — экспортировать из VRoid Studio напрямую и не редактировать в Blender. Blender полезен, когда цель — рендер картинок или видео, а не интерактив. ```python # Проверка перед рендером: нормали, материалы, объём геометрии for o in bpy.data.objects: if o.type == 'MESH': tris = sum(len(p.vertices) - 2 for p in o.data.polygons) has_mat = any(m and m.use_nodes for m in o.data.materials) print(f"{o.name}: tris={tris} материалы={'да' if has_mat else 'НЕТ'}") ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```python # Сводка по модели: вершины, кости, ключи формы mesh = next(o for o in bpy.data.objects if o.type == 'MESH') print("вершин:", len(mesh.data.vertices)) print("shape keys:", len(mesh.data.shape_keys.key_blocks) if mesh.data.shape_keys else 0) print("UV-карт:", len(mesh.data.uv_layers)) ``` | Параметр | Значение | Когда менять | |---|---|---| | формат VRM | `1.0` | `0.x` — только если приёмник старый | | поза в VRoid Studio | A-Pose | T-Pose — если планируется риг в Blender | | материал в Blender | MToon node group | Principled — если рендеришь в Cycles/EEVEE, а не в VR | | шейдер Cycles для toon | `Flat` | `Toon` — когда нужен жёсткий контур | | `export_scene.vrm` | только из исходной модели | после любых правок результат гарантированно теряется | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | `import_scene.vrm` не найден | аддон не включён или несовместимая версия | Проверить `Preferences → Add-ons`, при необходимости поставить версию аддона от сообщества | | Материал выглядит как обычный пластик | MToon не переведён в поддерживаемый шейдер | Либо конвертировать в Principled, либо собрать toon-освещение в Cycles | | Пружинные волосы не двигаются | цепочки не импортировались или потеряли коллайдеры | Пересобрать вручную цепочку костей + drivers | | После экспорта в VRM Studio модель не открывается | нарушен стандарт VRM | Откатиться к исходному VRM и экспортировать без ручных правок | | Материалы потеряли цвет | texture slots переставлены | Не редактировать UV/slots, если нужен обратный путь | | Лицо без выражений | набор expressions не передан | Экспортировать с включёнными expressions из VRoid Studio | | Модель в 10 раз меньше | нормализация роста в формате | Масштабировать вручную; единицы Blender при этом ни при чём | ## 🖥 MCP-ПРОМПТ ```text RU: "Импортируй avatar.vrm, выведи по объектам треугольники, материалы и их типы нод, найди цепочки костей волос, проверь наличие shape keys" / EN: "Import avatar.vrm, print per-object triangles, materials and their node types, find hair bone chains, check for shape keys" ``` ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 32: DAZ → BLENDER (DIFFEOMORPHIC)](setup_32_daz_diffeomorphic.md), [SETUP 34: METAHUMAN → BLENDER](setup_34_metahuman_blender.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 07: CI/CD INTEGRATION](setup_07_ci_cd.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 09: BLENDERBOT / AI CODING AGENT](setup_09_blenderbot_ai.md), [SETUP 10: BLENDER + COMPOSITING (NUKE/AFTER EFFECTS ROUNDTRIP)](setup_10_compositing_roundtrip.md) - **Теги:** #blender, #character, #setup --- # SETUP 37: BLENDERKIT ASSETS Tags: #assets #setup # SETUP 37: BLENDERKIT ASSETS **ID:** SETUP-37 | **Type:** Asset Lib | **Difficulty:** ⭐ | **Time:** 15 min **RU/EN Bilingual** > Когда применять: нужна библиотека готовых моделей, материалов и HDRI, чтобы не тратить день на то, что уже сделали другие — и с понятной лицензией. ## 🎯 ЦЕЛЬ Настроить BlenderKit как рабочий источник ассетов внутри Blender: найти и отфильтровать бесплатное, скачать пакетом, подключить как Asset Browser и проверить лицензию перед тем, как ассет попадёт в коммерческий проект. Отдельный блок — выгрузка своего ассета в общую библиотеку. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Blender | 4.x | хост аддона | | Аддон BlenderKit | входит в поставку Blender, включается в Preferences → Add-ons | поиск, скачивание, загрузка своих ассетов | | Аккаунт BlenderKit | `blenderkit.com` | избранное, история, загрузка ассетов | | API-ключ | выдаётся в настройках аддона | доступ к REST API для батчей | > [!NOTE] > Точная схема авторизации в API менялась: ключ мог передаваться как query-параметр, как заголовок `Authorization`, или требовать отдельного токена. Не пиши запрос, пока не посмотришь, что именно показывает твоя панель настроек и что описано в документации по адресу `blenderkit.com/docs/`. Ниже примеры намеренно без конкретного способа подстановки ключа. ## 🛠️ НАСТРОЙКА ### 1. Включение аддона и авторизация ```python import bpy print("blenderkit загружен:", "blenderkit" in bpy.context.preferences.addons.keys()) # Локальная папка, куда аддон складывает скачанное bk = bpy.context.preferences.addons.get("blenderkit") if bk: prefs = bk.preferences for attr in dir(prefs): if not attr.startswith("_") and attr in ( "api_key", "blenderkit_directory", "username", "search_limit"): print(attr, "=", getattr(prefs, attr)) ``` Путь к библиотеке важен: если он внутри сетевого диска или в синхронизируемой папке, загрузка ассетов резко замедляется — файлы большие, и атомарные переименования по сети дорогие. ### 2. Поиск в UI Панель BlenderKit в N-панели (Sidebar) даёт поиск с фильтрами по типу ассета: models, materials, HDRIs, textures, scenes, brushes, node groups. Практический порядок работы: | Шаг | Что делаем | Зачем | |---|---|---| | 1 | Фильтр по типу | иначе в выдаче тонна того, что не нужно | | 2 | Галочка «Free» | убирает платные ассеты из результата | | 3 | Сортировка по популярности/оценке | ранний шум — внизу | | 4 | Смотрим лицензию и разрешение текстур | определяет, годится ли ассет в коммерческий проект | ### 3. Установка ассета в сцену Drag & drop из панели BlenderKit работает так: перетаскивание в 3D-вьюпорт создаёт объект, перетаскивание в Outliner импортирует ассет в сцену. Программный эквивалент — добавление через `bpy.ops.wm.append` по пути к файлу. ```python import bpy # Точный путь к библиотеке берём из настроек аддона, а не угадываем bk = bpy.context.preferences.addons.get("blenderkit") lib_dir = bk.preferences.blenderkit_directory if bk else r"C:\blenderkit_data" print("библиотека:", lib_dir) ``` > [!NOTE] > Имя свойства с путём к библиотеке — `blenderkit_directory`; выше оно используется как в примере, но проверь выводом `dir(prefs)`: поле могло быть переименовано. Путь — это фактически Asset Library, поэтому следующий шаг тоже требует его точного значения. ### 4. Подключение как Asset Browser ```python # Asset Browser читает библиотеки из настроек, а не из кода. # Здесь только проверяем, что каталог существует и не пуст. import os lib_dir = r"C:\blenderkit_data" print("существует:", os.path.isdir(lib_dir)) if os.path.isdir(lib_dir): print("элементов:", len(os.listdir(lib_dir))) ``` Саму библиотеку добавляют через `Edit → Preferences → File Paths → Asset Libraries`: указать имя и корневой путь. После этого ассеты BlenderKit видны в Asset Browser, и их можно перетаскивать в сцену. ```python # Проверка, что ассет помечен как asset-объект (иначе в браузере не появится) for o in bpy.data.objects[:10]: if o.asset_data(): print("asset:", o.name, "| catalog:", o.asset_data().catalog_id) ``` ### 5. Батч через API Для массовой загрузки (например, наполнить сцену 5 материалами + 3 моделями + 1 HDRI) удобнее REST API. ```python import json import urllib.request API = "https://www.blenderkit.com/api/v1/search/" def search(query, asset_type="model", free_only=True): """Поиск ассетов. Поле token подставь по инструкции из docs.""" payload = { "query": query, "asset_type": asset_type, # model / material / hdri / texture "is_free": free_only, } req = urllib.request.Request( API, data=json.dumps(payload).encode("utf-8"), headers={"Content-Type": "application/json", "User-Agent": "blender-pipeline/1.0"}, method="POST", ) with urllib.request.urlopen(req, timeout=60) as r: return json.loads(r.read().decode("utf-8")) # res = search("concrete wall", asset_type="material") # for item in res.get("results", [])[:5]: # print(item["name"], item.get("filesize")) ``` > [!NOTE] > Тело запроса и набор полей (`query`, `asset_type`, `is_free`) приведены по смыслу. Фактические имена полей и требуемый заголовок авторизации уточни в документации API — BlenderKit менял схему, и запрос без правильного ключа вернёт 401 либо пустой список. Это не повод переписывать код, а повод сначала открыть один запрос в браузере и посмотреть ответ. ### 6. Скачивание файлов ассета У ассета в ответе API есть несколько файлов (геометрия, текстуры, превью). Скачивать надо **все**, иначе материал придёт без карт. ```python # Типичная структура: у каждого файла есть url, size, checksum def iter_files(res): for result in res.get("results", []): for f in result.get("files", []): if f.get("download_type") in (None, "blend", "texture"): yield result["name"], f # for name, f in iter_files(res): # print(name, f["url"], f.get("file_size")) ``` ### 7. Загрузка своего ассета ```text 1. Отделить ассет: объект/материал помечается Asset (Object → Asset → Mark as Asset) 2. В N-панели BlenderKit: Upload → выбрать категорию и заполнить описание 3. Загрузить превью (опционально, но сильно повышает кликабельность) 4. Отправить на модерацию — ассет появляется в библиотеке после проверки ``` Приватные ассеты доступны только владельцу и требуют действующей подписки. ### 8. Лицензия — проверять всегда ```python # Подпись ассета содержит условия; печатаем, а не принимаем на веру res = search("wood", asset_type="material") for r in res.get("results", [])[:3]: print(r.get("name"), "|", r.get("license_short_name"), "|", r.get("is_free"), "| автор:", r.get("user", {}).get("username")) ``` Бесплатный ассет не значит «любая лицензия»: у части ассетов ограничения на коммерческое использование, а часть — CC0, часть — royalty-free с указанием автора. Проверять надо до того, как ассет окажется в рендере. ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```python # Сводка по локальной библиотеке: сколько весит и сколько внутри import os lib = r"C:\blenderkit_data" if os.path.isdir(lib): total = 0 files = 0 for root, _, names in os.walk(lib): for n in names: files += 1 total += os.path.getsize(os.path.join(root, n)) print(f"файлов: {files}, объём: {total/1024/1024:.0f} МБ") ``` | Параметр | Значение | Когда менять | |---|---|---| | `asset_type` | `model` / `material` / `hdri` / `texture` | подставлять под задачу, иначе в выдаче мусор | | фильтр Free | включён | выключать, когда нужен конкретный платный ассет | | путь библиотеки | локальный SSD | сетевой диск — только если ассетов мало | | `import_pack_images` | `False` | при батче, чтобы не раздувать blend-файл | | модерация загрузки | требуется | сроки модерации — от нескольких часов до нескольких дней | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Панели BlenderKit нет | аддон не включён | `Preferences → Add-ons`, включить и перезапустить панель | | Поиск возвращает пустоту | не авторизован либо фильтр Free слишком жёсткий | Войти в аккаунт, ослабить фильтры | | API отдаёт 401 | не подставлен токен | Сверяться с документацией по авторизации | | Скачался материал без карт | скачан только blend-файл | Качать все файлы ассета из ответа API | | Asset Browser пуст | библиотека не добавлена в `File Paths` | Добавить путь и перезапустить Blender | | Ассет не появляется в браузере | объект не помечен как Asset | `Object → Asset → Mark as Asset` | | Импорт микроскопический | ассет в сантиметрах, сцена в метрах | Масштабировать вручную либо применить трансформации | | Ассет «исчез» после переустановки Blender | путь к библиотеке сменился | Держать библиотеку в фиксированном месте и переподключать путь | | Скачивание подвисает | библиотека на сетевом диске | Перенести локально, скачать, потом раздать по местам | ## 🖥 MCP-ПРОМПТ ```text RU: "BlenderKit: 5 бесплатных материалов + 3 модели + 1 HDRI, только free, проверь лицензию каждого, вывеси размер и автора, импортируй в отдельную коллекцию" / EN: "BlenderKit: 5 free materials + 3 models + 1 HDRI, free filter only, verify each license, print size and author, import into a dedicated collection" ``` ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 38: FAB.COM (EPIC) PIPELINE](setup_38_fab_epic.md), [SETUP 39: QUIXEL BRIDGE STANDALONE](setup_39_quixel_bridge.md), [SETUP 40: AMBIENTCG + POLYHAVEN BATCH](setup_40_ambientcg_polyhaven.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 02: GENESISCORE LOCAL (OLLAMA)](setup_02_genesiscore_local.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [P27: POLY HAVEN ASSET PIPELINE](../prompts/27_polyhaven_pipeline.md), [P36: ASSET LICENSING (PIZZA/SKETCHFAB/HAVEN)](../prompts/36_asset_licensing.md) - **Теги:** #assets, #setup --- # SETUP 38: FAB.COM (EPIC) PIPELINE Tags: #assets #pipeline #setup # SETUP 38: FAB.COM (EPIC) PIPELINE **ID:** SETUP-38 | **Type:** Marketplace | **Difficulty:** ⭐⭐ | **Time:** 35-45 min > Когда применять: когда нужно завести в Blender контент с Fab (бывшие Megascans) — surface-комплекты, 3D-скан-ассеты, модели — и наладить повторяемую выгрузку, а не разовый ручной импорт. ## 🎯 ЦЕЛЬ Fab — маркетплейс Epic Games, в который переехали Megascans; отдельный Quixel Bridge больше не развивается. Настраиваем связку «учётная запись Fab → приложение-мост → Blender»: ставим аддон, забираем ассеты, приводим их к единому масштабу и раскладываем по коллекциям, чтобы дальше собирать сцену скриптом, а не руками. Отдельно разбираем поверхности UDIM — это главная причина, по которой «скачал и вставил» не работает: 8K-набор раскладывается по плиткам 1001…1008, и одной картинки недостаточно. > [!NOTE] > Способы установки аддона Fab для Blender, имя его пакета и набор маршрутов локального API менялись между сборками приложения Fab. Не выдумывай `module=` и `api/v1/...` — распечатай то, что реально лежит в скачанном `.zip` и в настройках приложения. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Аккаунт Fab | любая, бесплатная регистрация | вход в библиотеку, нужен для загрузок | | Приложение Fab (desktop) | текущая | локальный мост и менеджер загрузок | | Blender | 4.x LTS | цель пайплайна | | Аддон Fab для Blender | версия под твой Blender | импорт прямо в сцену | | glTF 2.0 / FBX / OBJ | — | форматы, которые Blender читает из коробки | | Python 3.11 из поставки Blender | 3.11 | встроенный интерпретатор, отдельный ставить не надо | ## 🛠️ НАСТРОЙКА ### 1. Аккаунт и приложение Регистрируешься на [fab.com](https://www.fab.com), скачиваешь desktop-приложение и входишь в него. Лицензия у ассетов разная, и она указана на странице самого ассета — читать надо там, а не по памяти: часть каталога доступна бесплатно, часть требует подписки или валюты в аккаунте. ### 2. Узнаём настоящее имя пакета аддона Аддон приезжает `.zip`-ом. Имя модуля для `addon_enable` — это имя папки внутри архива, а не имя файла: ```python import zipfile from pathlib import Path ADDON_ZIP = Path(r"C:/addons/fab_blender.zip") # путь к скачанному архиву with zipfile.ZipFile(ADDON_ZIP) as z: roots = sorted({n.split("/")[0] for n in z.namelist() if "/" in n}) print(roots) # первый элемент — то, что нужно в module= ``` ### 3. Ставим и включаем аддон ```python import bpy MODULE = roots[0] # подставь значение из шага 2 bpy.ops.preferences.addon_install(filepath=str(ADDON_ZIP), overwrite=True) bpy.ops.preferences.addon_enable(module=MODULE) # Проверка: аддон появился в списке и его модуль импортируется import addon_utils print(MODULE, addon_utils.check(MODULE)) print([m.__name__ for m in addon_utils.modules() if MODULE in m.__name__]) ``` ### 4. Пакетный импорт GLB в коллекцию ```python import bpy from pathlib import Path ROOT = Path(r"C:/assets/fab/batch01") def import_glb_folder(root: Path, coll_name: str = "FAB") -> bpy.types.Collection: coll = bpy.data.collections.get(coll_name) or bpy.data.collections.new(coll_name) if coll.name not in bpy.context.scene.collection.children: bpy.context.scene.collection.children.link(coll) for path in sorted(root.glob("*.glb")): before = set(bpy.data.objects) bpy.ops.import_scene.gltf(filepath=str(path)) for obj in set(bpy.data.objects) - before: for c in list(obj.users_collection): c.objects.unlink(obj) coll.objects.link(obj) return coll print(import_glb_folder(ROOT).name) ``` ### 5. Единый масштаб и имена Фабы отдают ассеты в разных единицах, поэтому «планка рядом с креслом» после импорта не работает. Приводим высоту по мировой Z к метру и фиксируем трансформ: ```python import bpy def normalize(obj, target_height=1.0): if obj.type != "MESH" or obj.dimensions.z <= 0: return s = target_height / obj.dimensions.z obj.scale = (s, s, s) obj.name = f"FAB_{obj.name}" bpy.ops.object.select_all(action="DESELECT") obj.select_set(True) bpy.context.view_layer.objects.active = obj bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY", center="BOUNDS") for obj in bpy.data.collections["FAB"].objects: normalize(obj, target_height=1.0) print([o.name for o in bpy.data.collections["FAB"].objects]) ``` ### 6. Surface-набор: UDIM 8K surface — это не одна 8192×8192, а сетка плиток. Грузим первую (`1001`), Blender распознаёт паттерн по имени: ```python import bpy from pathlib import Path TEX_DIR = Path(r"C:/assets/fab/surfaces/oak") img = bpy.data.images.load(str(TEX_DIR / "oak_color.1001.exr"), check_existing=True) print("source:", img.source, "size:", tuple(img.size), "tiles:", img.source == "TILED") # Значения enum менялись между 3.x и 4.x — печатаем, а не гадаем try: print([i.identifier for i in bpy.types.Image.bl_rna.properties["source"].enum_items]) except Exception as e: print("enum introspection failed:", e) ``` > [!NOTE] > `Image.source == "TILED"` — проверка, а не гарантия: в части версий Blender значение называется иначе. Если `False`, открой картинку через UI и переключи режим UDIM вручную; в любом случае проверяй, что в Image Editor видны плитки, а не одна. ### 7. Локальный сервис приложения Fab Приложение поднимает HTTP-сервис на `127.0.0.1` — это и есть «мостик» между клиентом Fab и Blender: ```python import socket, urllib.request, urllib.error for port in (2000, 2001, 8080): # типовой диапазон сборок s = socket.socket(); s.settimeout(0.4) print(port, "open" if s.connect_ex(("127.0.0.1", port)) == 0 else "closed") s.close() for path in ("/", "/health"): # проверяем, что сервис отвечает url = f"http://127.0.0.1:2000{path}" try: with urllib.request.urlopen(url, timeout=2) as r: print(url, r.status, r.read(120)) except urllib.error.HTTPError as e: print(url, "HTTP", e.code) except Exception as e: print(url, "ERR", type(e).__name__, e) ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```bash # Пакетная обработка без GUI: сцена-шаблон + скрипт blender --background template.blend --python import_fab_batch.py ``` | Параметр | Значение | Когда менять | |---|---|---| | `target_height` | `1.0` (метр) | для пропов ставь реальный размер, иначе ляжет масштаб | | `ADDON_ZIP` | путь к архиву аддона | одна установка на одну версию Blender | | `MODULE` | папка внутри архива | менять только при обновлении аддона | | `bpy.ops.import_scene.gltf` | оператор импорта GLB | для FBX — `bpy.ops.import_scene.fbx` | | `bpy.ops.wm.obj_import` | оператор импорта OBJ | в 3.3+ это новый C++-импортёр | | `img.source` | должно быть `TILED` | если нет — грузи через UI вручную | | порт локального сервиса | см. настройки приложения Fab | меняется между сборками приложения | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Плоская текстура, видны только квадратные плитки | грузится одна плитка, паттерн не распознан | грузи `..._1001.exr` из папки UDIM, проверь `img.source` | | Объект «микроскопический» или в километрах | ассет пришёл в другой единице | прогони `normalize()` из шага 5 | | Импорт не создаёт объектов | glTF-файл битый или это `.blend`-архив | открой файл, проверь `gltf` в заголовке файла | | `addon_enable` ругается на `module` | имя модуля ≠ имя zip-файла | распечатай `roots` из шага 2 | | Аддон есть, но кнопки в UI нет | аддон включён в preferences другого пользователя | `Edit → Preferences → Add-ons`, включить вручную | | `localhost` не отвечает | приложение Fab не запущено / другой порт | запусти приложение, порт бери из его настроек | | Текстуры в материале чёрные | материал не связан с Image Texture | проверь ноду `Image Texture → Base Color` и Color Management | | Модель вставлена в начало координат | не выставлен origin | `bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY")` | ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 08: ASSET PIPELINE](setup_08_asset_pipeline.md), [SETUP 14: BLENDER + GODOT PIPELINE](setup_14_godot_pipeline.md), [SETUP 37: BLENDERKIT ASSETS](setup_37_blenderkit_assets.md), [SETUP 39: QUIXEL BRIDGE STANDALONE](setup_39_quixel_bridge.md), [SETUP 40: AMBIENTCG + POLYHAVEN BATCH](setup_40_ambientcg_polyhaven.md) - **По темам:** [P27: POLY HAVEN ASSET PIPELINE](../prompts/27_polyhaven_pipeline.md), [P36: ASSET LICENSING (PIZZA/SKETCHFAB/HAVEN)](../prompts/36_asset_licensing.md), [SETUP 45: HUNYUAN3D 2.1 LOCAL](setup_45_hunyuan3d_21.md) - **Теги:** #assets, #pipeline, #setup --- # SETUP 39: QUIXEL BRIDGE STANDALONE Tags: #assets #setup # SETUP 39: QUIXEL BRIDGE STANDALONE **ID:** SETUP-39 | **Type:** Megascans | **Difficulty:** ⭐⭐ | **Time:** 30 min > Когда применять: когда нужен не сам Bridge (он уже неактуален), а воспроизводимый экспорт surface/скан-набора в набор карт, который предсказуемо ложится в Principled BSDF. ## 🎯 ЦЕЛЬ Quixel Bridge больше не качается отдельно: его роль перешла к приложению Fab, а Megascans-каталог живёт там же. Поэтому этот сетап — не про установку Bridge, а про то, что Bridge делал: подобрать набор карт (BaseColor, Normal, Roughness, Displacement, AO, Specular), разрешение, тайлинг по UDIM и LOD для сканов, а потом собрать это в Blender так, чтобы материал не «мылил» и чтобы displacement реально давал силуэт. Разбираем именно контракт экспорта — он не изменился от смены приложения. > [!NOTE] > Bridge не обновляется. Если в задаче написано «поставь Bridge» — скорее всего, имеется в виду приложение Fab (см. [SETUP 38](setup_38_fab_epic.md)). Старые инструкции из сети ведут в никуда. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Blender | 4.x LTS | сборка материала и рендер | | Приложение Fab | текущая | источник Megascans-ассетов | | EXR или PNG 16-bit | — | карты поверхности без потери в тёмных местах | | Каталог на ASCII-пути | `C:/assets/megascans` | часть обвязки не переваривает кириллицу | | Cycles | — | только он честно считает displacement | ## 🛠️ НАСТРОЙКА ### 1. Что именно выгружать Три разных типа ассетов требуют разных наборов карт, и путаница между ними — половина проблем: | Тип ассета | Обязательные карты | Что можно не брать | |---|---|---| | Surface (плитка, ткань, металл) | BaseColor, Normal, Roughness, Displacement | AO (считается из карт), Specular (legacy) | | 3D-скан | BaseColor, Normal, Roughness, Displacement + LOD | AO | | Билилборд/растительность | BaseColor + Alpha | всё остальное | ### 2. Скачиваем и раскладываем ```python from pathlib import Path ASSET = Path(r"C:/assets/megascans/wood_planks") # Ожидаемая структура: wood_planks_1001_color.exr, ..._1001_normal.exr, ... for p in sorted(ASSET.glob("*_1001_*")): print(p.name, p.stat().st_size // 1024, "KB") # Плитки нумеруются 1001..100N по одной оси UDIM — считаем, сколько реально пришло tiles = {p.name.split("_")[1] for p in ASSET.glob("*_color*")} print("tiles:", sorted(tiles), "| всего:", len(tiles)) ``` ### 3. Загружаем карты как UDIM ```python import bpy from pathlib import Path ASSET = Path(r"C:/assets/megascans/wood_planks") def load_udim(pattern: str) -> bpy.types.Image: img = bpy.data.images.load(str(ASSET / pattern), check_existing=True) print(pattern, "->", img.source, tuple(img.size)) return img color = load_udim("wood_planks_1001_color.exr") rough = load_udim("wood_planks_1001_roughness.exr") nrm = load_udim("wood_planks_1001_normal.exr") disp = load_udim("wood_planks_1001_displacement.exr") ``` > [!NOTE] > `img.source` должен стать `"TILED"`. Если печатает иное — плитки не склеились, и дальше ты получишь только первый квадрат. Проверяй это на шаге сразу, а не после рендера. ### 4. Собираем материал в Principled BSDF ```python import bpy mat = bpy.data.materials.get("MS_WoodPlanks") or bpy.data.materials.new("MS_WoodPlanks") mat.use_nodes = True nt = mat.node_tree nt.nodes.clear() out = nt.nodes.new("ShaderNodeOutputMaterial"); out.location = (600, 0) bsdf = nt.nodes.new("ShaderNodeBsdfPrincipled"); bsdf.location = (260, 0) tex = {} for name, img in (("Base Color", color), ("Roughness", rough), ("Normal", nrm)): n = nt.nodes.new("ShaderNodeTexImage") n.image = img n.interpolation = "Smart" # сглаживает мыло на далёких углах n.location = (-120, 200) n.label = name tex[name] = n nt.links.new(tex["Base Color"].outputs["Color"], bsdf.inputs["Base Color"]) nt.links.new(tex["Roughness"].outputs["Color"], bsdf.inputs["Roughness"]) # Normal map -> Normal Map node -> Normal nm = nt.nodes.new("ShaderNodeNormalMap") nm.location = (20, -220) nm.inputs["Strength"].default_value = 1.0 nt.links.new(tex["Normal"].outputs["Color"], nm.inputs["Color"]) nt.links.new(nm.outputs["Normal"], bsdf.inputs["Normal"]) # Displacement: отдельная нода, у Principled BSDF такого входа нет d = nt.nodes.new("ShaderNodeTexImage"); d.image = disp; d.location = (-120, -450) dtex = nt.nodes.new("ShaderNodeDisplacement"); dtex.location = (260, -450) dtex.inputs["Midlevel"].default_value = 0.5 dtex.inputs["Scale"].default_value = 0.1 nt.links.new(d.outputs["Color"], dtex.inputs["Height"]) nt.links.new(dtex.outputs["Displacement"], out.inputs["Displacement"]) nt.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"]) # Чтобы силуэт появился в геометрии, а не только в шейдинге print("blend_method enum:", [i.identifier for i in bpy.types.Material.bl_rna.properties["blend_method"].enum_items] if "blend_method" in bpy.types.Material.bl_rna.properties else "n/a (Blender 4.2+)") ``` ### 4a. Проверяем комплектность карт ```python import re from pathlib import Path ASSET = Path(r"C:/assets/megascans/wood_planks") KIND = ("color", "normal", "roughness", "displacement", "ao", "specular") present = {k: len(list(ASSET.glob(f"*_{k}_1001_*"))) for k in KIND} for k, v in present.items(): print(f"{k:15s} {'ok' if v else 'НЕТ'}") missing = [k for k, v in present.items() if not v] print("не хватает:", missing or "ничего") # Если не хватает roughness — материал будет выглядеть пластиковым, # если normal — поверхность потеряет микрорельеф. ``` ### 5. Включаем реальный displacement ```python import bpy scene = bpy.context.scene scene.render.engine = "CYCLES" mat = bpy.data.materials["MS_WoodPlanks"] # Метод смещения в настройках материала: читаем доступные значения, а не угадываем for key in ("surface_render_method",): if key in bpy.types.Material.bl_rna.properties: print(key, [i.identifier for i in bpy.types.Material.bl_rna.properties[key].enum_items]) # В UI это Render Properties -> Displacement: Displacement Only / Both # Сама настройка хранится на материале в UI; из скрипта надёжнее проверить рендер: scene.cycles.displacement_method = "BOTH" # 'BUMP' | 'DISPLACEMENT' | 'BOTH' print(scene.cycles.displacement_method, scene.cycles.max_bounces) ``` > [!NOTE] > Точное имя свойства метода смещения в Cycles и его значения отличаются между 3.x и 4.x. Прежде чем писать эту строку, напечатай `scene.cycles.bl_rna.properties.keys()` и найди нужное. Смысл остаётся один: смещение должно считаться в геометрии, иначе это просто параллакс-карта. ### 6. Масштаб и тайлинг ```python import bpy # Тайлинг задаётся нодой Mapping, а не в Image Texture nt = bpy.data.materials["MS_WoodPlanks"].node_tree mp = nt.nodes.new("ShaderNodeMapping") mp.location = (-360, 200) mp.inputs["Scale"].default_value = (1.0, 1.0, 1.0) src = next(n for n in nt.nodes if n.type == "TEX_IMAGE") nt.links.new(mp.outputs["Vector"], src.inputs["Vector"]) # Реальный размер плитки: displacement Scale в метрах = обратная величина тайлинга print("displacement scale (m):", 0.1, "-> тайлинг на 2 м материала:", 1 / 0.1 * 2) ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```bash # Проверить, что архив распаковался полностью (8K surface = 8 плиток на карту) python -c "from pathlib import Path; p=Path(r'C:/assets/megascans/wood_planks'); print(sorted({f.name.split('_')[1] for f in p.glob('*_color*')}))" # Батч-импорт всех скачанных .blend-бандлов в фоне blender --background --python-expr "import bpy;bpy.ops.wm.open_mainfile(filepath=r'C:/assets/megascans/surface.blend')" ``` | Параметр | Значение | Когда менять | |---|---|---| | Номер первой плитки | `1001` | всегда первая; в 10K-сетках бывает `1001` + отдельный набор | | `Normal Map → Strength` | `1.0` | меньше — площе, больше — артефакты на рёбрах | | `Displacement → Scale` | `0.1` м | это высота рельефа, а не процент | | `Displacement → Midlevel` | `0.5` | сдвигает карту вверх/вниз, если рельеф «утоплен» | | `Image Texture → Interpolation` | `Smart` | `Closest` для пиксельных масок, `Linear` для размытых карт | | `cycles.displacement_method` | `BOTH` | `BUMP` — быстрее, `DISPLACEMENT` — честная геометрия | | `cycles.max_bounces` | `4+` | для шершавых материалов | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Видна только первая плитка | UDIM не распознан | грузи `..._1001_...`, проверь `img.source == "TILED"` | | Рельеф «плывёт» вдали | слишком мелкая геометрия | `decimate` + `subdivision surface` перед рендером, либо `BUMP` | | Тени в нормалях инвертированы | карта в OpenGL-конвенции, а Blender ждёт DirectX | инвертируй зелёный канал через Separate/Combine XYZ | | Материал чёрный | Color Management: очень тёмная EXR-карта | `scene.view_settings.exposure` вверх, проверь `Filmic/AgX` | | Модель «плоская» при верном displacement | метод смещения = `BUMP` | выставить `BOTH`, смикрорельеф смикро-геометрией | | Roughness-карта не влияет | Color → Float подключён не туда | связывай `Color` карты в `Roughness` (Blender сам сконвертирует) | | Скан-лод без нормали | разный масштаб у LOD | прогони `normalize()` для каждого лода отдельно | | Кириллица в пути — ошибка чтения | ограничение части обвязки | переноси каталоги в ASCII-путь | ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 37: BLENDERKIT ASSETS](setup_37_blenderkit_assets.md), [SETUP 38: FAB.COM (EPIC) PIPELINE](setup_38_fab_epic.md), [SETUP 40: AMBIENTCG + POLYHAVEN BATCH](setup_40_ambientcg_polyhaven.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 02: GENESISCORE LOCAL (OLLAMA)](setup_02_genesiscore_local.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [P27: POLY HAVEN ASSET PIPELINE](../prompts/27_polyhaven_pipeline.md), [P36: ASSET LICENSING (PIZZA/SKETCHFAB/HAVEN)](../prompts/36_asset_licensing.md) - **Теги:** #assets, #setup --- # SETUP 40: AMBIENTCG + POLYHAVEN BATCH Tags: #assets #automation #setup # SETUP 40: AMBIENTCG + POLYHAVEN BATCH **ID:** SETUP-40 | **Type:** Free PBR | **Difficulty:** ⭐ | **Time:** 40-60 min > Когда применять: когда нужна пачка бесплатных CC0-материалов и HDRI без ручного скачивания по одной штуке — общий API-запрос, раскладка на диск, проверка лицензии и подключение к сцене одним скриптом. ## 🎯 ЦЕЛЬ ambientCG и Poly Haven — два CC0-источника, которые можно тянуть пачками: у Poly Haven есть документированный публичный API, у ambientCG — свой. Собираем один скрипт, который по списку ID тянет карты и HDRI, раскладывает их в предсказуемую структуру каталогов и отдаёт в Blender готовым материалом. Отдельно фиксируем главное про лицензию: оба источника CC0, но у ambientCG аттрибуция не обязательна, а у Poly Haven указание автора приветствуется — и то и другое менялось, поэтому проверяй страницу ассета. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Python | 3.10+ (можно встроенный в Blender) | скрипт пакетной загрузки | | `requests` | 2.x | HTTP-запросы к API | | Аккаунт Poly Haven | бесплатный | часть загрузок требует авторизации | | Blender | 4.x LTS | применение HDRI и создание материалов | | Диск | 5-20 ГБ на 10 материалов 2K | 8K-наборы весят сотни мегабайт | ## 🛠️ НАСТРОЙКА ### 1. Poly Haven: список ассетов через API ```python import requests BASE = "https://api.polyhaven.com" r = requests.get(f"{BASE}/assets", params={"t": "textures"}, timeout=30) r.raise_for_status() assets = r.json() print("textures:", len(assets)) print("первые id:", list(assets)[:5]) ``` > [!NOTE] > Хост `api.polyhaven.com` и пути `/assets`, `/info/`, `/files/` — те, что использовались до переезда Poly Haven на новый сайт. Если 404, открой документацию на polyhaven.com и возьми актуальный базовый URL: он менялся. Логику ниже правь в одном месте — в `BASE`. ### 2. Poly Haven: какие файлы качать ```python def list_files(asset_id: str, res: str = "2k") -> dict: r = requests.get(f"{BASE}/files/{asset_id}", timeout=30) r.raise_for_status() return r.json() info = list_files("wood_floor_deck") # Структура ответа: ключ — тип карты (Diffuse/Roughness/Normal/Displacement/...), # значение — словарь по разрешениям, в котором лежат вложенные dict'ы с "url" print(list(info.keys())) print(json.dumps(info, indent=2)[:600]) ``` ### 3. Скачиваем и раскладываем ```python import json from pathlib import Path OUT = Path(r"C:/assets/free_pbr") def download(url: str, dest: Path) -> int: dest.parent.mkdir(parents=True, exist_ok=True) with requests.get(url, stream=True, timeout=120) as r: r.raise_for_status() total = 0 with open(dest, "wb") as f: for chunk in r.iter_content(1 << 16): f.write(chunk); total += len(chunk) return total def grab_polyhaven(asset_id: str, res: str = "2k") -> int: files = list_files(asset_id, res) base = OUT / "polyhaven" / asset_id written = 0 for map_type, per_res in files.items(): # Diffuse, Normal, ... entry = per_res.get(res) or next(iter(per_res.values()), None) if not entry: continue # url лежит во вложенном слое: {"1k": {"HDRI": {"url": ...}}} и т.п. urls = [v["url"] for v in entry.values() if isinstance(v, dict) and "url" in v] for u in urls: written += download(u, base / f"{map_type}_{Path(u).name}") return written print(grab_polyhaven("wood_floor_deck", "2k") // 1024, "KB") ``` ### 4. ambientCG: карточка ассета и ссылка на скачивание ```python CG = "https://ambientcg.com" r = requests.get(f"{CG}/api/v2/full_json", params={"id": "Metal032"}, timeout=60) r.raise_for_status() data = r.json() print("имя:", data.get("displayName")) # В ответе есть поле с идентификатором набора файлов и структура папок загрузки print("ключи верхнего уровня:", sorted(data.keys())[:15]) ``` > [!NOTE] > Имя поля, в котором лежит ссылка на скачивание, у ambientCG менялось (`downloadFolders`, потом другие). Не хардкодь ключ — сначала напечатай `sorted(data.keys())` и достань нужный путь по факту. Формат самой ссылки `.../get?file=...` тоже сверь на странице ассета. ### 5. Скачиваем набор ambientCG ```python def grab_ambientcg(asset_id: str, res: str = "1K-JPG") -> int: r = requests.get(f"{CG}/api/v2/full_json", params={"id": asset_id}, timeout=60) r.raise_for_status() data = r.json() written = 0 for entry in data.get("downloadFolders", []): attr = entry.get("attribute", "") if res and res not in attr: continue # Downloadable в 1K-JPG: файлы лежат по одной ссылке на весь набор for f in entry.get("downloadable", {}).get("Resolution", []): for fl in f.get("downloadFilenames", []): url = f"{CG}/get?file={fl}" written += download(url, OUT / "ambientcg" / asset_id / f"{attr}.zip") return written print(grab_ambientcg("Metal032", "1K-JPG") // 1024, "KB") ``` ### 6. Создаём материал из скачанных карт ```python import bpy from pathlib import Path def pbr_material_from_folder(folder: Path, mat_name: str) -> bpy.types.Material: mat = bpy.data.materials.get(mat_name) or bpy.data.materials.new(mat_name) mat.use_nodes = True nt = mat.node_tree nodes, links = nt.nodes, nt.links links.clear() out = nodes.new("ShaderNodeOutputMaterial"); out.location = (600, 0) bsdf = nodes.new("ShaderNodeBsdfPrincipled"); bsdf.location = (300, 0) wanted = {"color": "Base Color", "roughness": "Roughness", "metal": "Metallic"} for stem, socket in wanted.items(): hit = next(folder.glob(f"*{stem}*.jpg"), None) or next(folder.glob(f"*{stem}*.png"), None) if not hit: continue tex = nodes.new("ShaderNodeTexImage"); tex.image = bpy.data.images.load(str(hit)) tex.location = (-100, 0) links.new(tex.outputs["Color"], bsdf.inputs[socket]) links.new(bsdf.outputs["BSDF"], out.inputs["Surface"]) return mat folder = next((OUT / "ambientcg" / "Metal032").glob("*.zip"), None) print("zip распакован в:", folder) ``` ### 7. Ставим HDRI как окружение ```python import bpy scene = bpy.context.scene world = scene.world or bpy.data.worlds.new("World") scene.world = world world.use_nodes = True bg = next(n for n in world.node_tree.nodes if n.type == "BACKGROUND") env = world.node_tree.nodes.new("ShaderNodeTexEnvironment") env.image = bpy.data.images.load(r"C:/assets/free_pbr/polyhaven/studio_small_08/HDRI_2k.hdr") world.node_tree.links.new(env.outputs["Color"], bg.inputs["Color"]) bg.inputs["Strength"].default_value = 1.0 print("view transform:", scene.view_settings.view_transform, "| exposure:", scene.view_settings.exposure) ``` ### 8. Лицензия: что можно, а что нет Оба источника CC0 — это значит «можно использовать в коммерции без обязательного указания авторства». Практическая проверка всё равно нужна, потому что условия переписывались и у ambientCG, и у Poly Haven: ```python # Собираем происхождение каждого файла в один отчёт — проще отвечать на вопросы клиента import json from pathlib import Path MANIFEST = Path(r"C:/assets/free_pbr/MANIFEST.json") entries = [ {"id": "wood_floor_deck", "source": "polyhaven", "license": "CC0-1.0", "res": "2k"}, {"id": "Metal032", "source": "ambientcg", "license": "CC0-1.0", "res": "1K-JPG"}, ] MANIFEST.write_text(json.dumps(entries, indent=2), encoding="utf-8") print(MANIFEST.read_text(encoding="utf-8")) ``` > [!NOTE] > Формулировки лицензий на сайтах менялись. Относись к CC0 как к рабочему предположению для черновиков и продакшена, но перед сдачей клиенту сверься со страницей конкретного ассета. Авторство указывать полезно всегда, даже когда оно не обязательно. ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```bash # Проверяем, что Poly Haven API вообще отвечает, до того как строить пайплайн curl -s -o NUL -w "%{http_code}\n" "https://api.polyhaven.com/assets?t=hdris" ``` | Параметр | Значение | Когда менять | |---|---|---| | `t` (тип ассета) | `textures`, `hdris`, `models` | брать из документации, значения менялись | | `res` | `1k` / `2k` / `4k` | 2K для превью, 4K для финального рендера | | `displacement_method` в Cycles | `BUMP` | на превью быстрее, `DISPLACEMENT` на финале | | `bg.inputs["Strength"]` | `1.0` | 0.3-0.5 для студийных HDRI, 2+ для уличных | | `view_transform` | `AgX` (Blender 4.x) | `Filmic` в 3.x, `Standard` для точных цветов | | пауза между запросами | `0.3-1.0` с | бережёт лимит и не ловит 429 | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | 404 на `/assets` | база переехала | открой документацию polyhaven.com, поправь `BASE` | | 401/403 на скачивании | нужна авторизация | войди в аккаунт и подставь токен в заголовок | | `KeyError` по ключу ответа | смена схемы API | сначала `print(list(info.keys()))`, потом читай ключ | | Скачался один файл вместо набора | вложенность `url` глубже одного уровня | рекурсивно обходи dict и собирай всё, что имеет `url` | | 429 при пакетной загрузке | слишком частые запросы | пауза между запросами, retry с задержкой | | HDRI-картинка не отображается | `view_transform` срезает диапазон | подними `exposure` или смени `view_transform` | | Цвета «кислотные» | Linear/Non-Color не выставлен | Color Data = sRGB для цветовых карт, Non-Color для roughness/normal | | Материал без roughness-карты | в наборе имя `Roughness` с большой буквы | поиск регистронезависимый | | Диск кончился на 8K | 8K UDIM = 64 плитки | качай 2K для библиотеки, 8K точечно под финал | ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 04: HEADLESS RENDER (CLI / CI/CD)](setup_04_headless_render.md), [SETUP 35: READY PLAYER ME AVATARS](setup_35_readyplayerme.md), [SETUP 37: BLENDERKIT ASSETS](setup_37_blenderkit_assets.md), [SETUP 38: FAB.COM (EPIC) PIPELINE](setup_38_fab_epic.md), [SETUP 39: QUIXEL BRIDGE STANDALONE](setup_39_quixel_bridge.md) - **По темам:** [P18: DRIVERS & CONSTRAINTS](../prompts/18_drivers_constraints.md), [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [P27: POLY HAVEN ASSET PIPELINE](../prompts/27_polyhaven_pipeline.md) - **Теги:** #assets, #automation, #setup --- # SETUP 41: HDRI LIGHTING LAB Tags: #lighting #setup # SETUP 41: HDRI LIGHTING LAB **ID:** SETUP-41 | **Type:** Lighting | **Difficulty:** ⭐⭐ | **Time:** 30 min > Когда применять: когда сцена «мылит» или наоборот слепит, и нужно за один прогон сравнить поворот HDRI, силу света, варианты Color Management и наличие запечённого GI — и выбрать вариант, а не угадывать. ## 🎯 ЦЕЛЬ Делаем в Blender повторяемый стенд для оценки освещения: одна HDRI, четыре поворота по Z, три значения силы, два варианта Color Management, сравнение с и без Light Probe. Всё это прогоняется скриптом и рендерится в превью-разрешении, чтобы решение принималось на картинках, а не на глазок в сцене. Заодно проверяем, как HDRI ведёт себя как фон (`film_transparent`) и как добавляет объём — потому что «просто HDRI» и «HDRI как видимый свет» — это разные задачи. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Blender | 4.x LTS | стенд | | HDRI | 2K или 4K EXR | источник и фон | | Cycles | — | честный GI и запекание | | GPU | 8+ ГБ VRAM | лестница рендеров | | Python `itertools` | stdlib | построение комбинаций | ## 🛠️ НАСТРОЙКА ### 1. Готовим стенд: HDR + нейтральная геометрия ```python import bpy from mathutils import Euler scene = bpy.context.scene scene.render.resolution_x, scene.render.resolution_y = 640, 360 scene.render.film_transparent = False # False = HDRI виден как фон def ensure_world() -> bpy.types.World: w = scene.world or bpy.data.worlds.new("World") scene.world = w w.use_nodes = True return w world = ensure_world() nt = world.node_tree nt.nodes.clear() env = nt.nodes.new("ShaderNodeTexEnvironment") env.image = bpy.data.images.load(r"C:/assets/hdri/studio_small_08_4k.hdr") env.location = (-600, 0) # Поворот HDRI: отдельная нода Mapping, Z-ось = азимут солнца mp = nt.nodes.new("ShaderNodeMapping") mp.name = "HDRI_ROT"; mp.location = (-380, 0) nt.links.new(mp.outputs["Vector"], env.inputs["Vector"]) bg = nt.nodes.new("ShaderNodeBackground"); bg.location = (-120, 0) out = nt.nodes.new("ShaderNodeOutputWorld"); out.location = (120, 0) nt.links.new(env.outputs["Color"], bg.inputs["Color"]) nt.links.new(bg.outputs["Background"], out.inputs["Surface"]) print("world готов:", world.name, env.image.name) ``` ### 2. Тестовые объекты Смысл набора в том, что три объекта показывают разные стороны освещения: матовый шар даёт общий уровень и мягкость, тёмный куб — контраст теней и насыщенность, а металлический шар — то, чего не видно больше нигде: блики и форму источников. Именно по металлу видно, что HDRI плохо работает в интерьере: в отражении сразу читаются её световые пятна, а не мягкий градиент. ```python import bpy def grey_material(name="M_Grey", v=0.5): m = bpy.data.materials.get(name) or bpy.data.materials.new(name) m.use_nodes = True b = next(n for n in m.node_tree.nodes if n.type == "BSDF_PRINCIPLED") b.inputs["Base Color"].default_value = (v, v, v, 1.0) b.inputs["Roughness"].default_value = 0.5 return m bpy.ops.mesh.primitive_uv_sphere_add(location=(-1, 0, 1), radius=1) sphere = bpy.context.object sphere.data.materials.append(grey_material("M_Sphere", 0.5)) bpy.ops.mesh.primitive_cube_add(location=(0.6, 0, 0.5), size=1) cube = bpy.context.object cube.data.materials.append(grey_material("M_Cube", 0.18)) bpy.ops.mesh.primitive_uv_sphere_add(location=(0, 2.5, 1), radius=1) mirror = bpy.context.object # металл-эталон: сразу видно блики HDRI mb = bpy.data.materials.new("M_Mirror"); mb.use_nodes = True mb.node_tree.nodes["Principled BSDF"].inputs["Metallic"].default_value = 1.0 mb.node_tree.nodes["Principled BSDF"].inputs["Roughness"].default_value = 0.02 mirror.data.materials.append(mb) print("стенд готов:", [o.name for o in (sphere, cube, mirror)]) ``` ### 3. Лестница поворотов и силы ```python import math import bpy def set_hdri(rot_z_deg: float, strength: float): nt = scene.world.node_tree nt.nodes["HDRI_ROT"].inputs["Rotation"].default_value = (0, 0, math.radians(rot_z_deg)) bg = next(n for n in nt.nodes if n.type == "BACKGROUND") bg.inputs["Strength"].default_value = strength return rot_z_deg, strength LADDER = [] for rot in (0, 90, 180, 270): for st in (0.5, 1.0, 2.0): LADDER.append(set_hdri(rot, st)) print("вариантов:", len(LADDER), LADDER[:3]) ``` ### 4. Рендерим лестницу в превью ```python import bpy from pathlib import Path OUT = Path(r"C:/renders/hdri_lab") OUT.mkdir(parents=True, exist_ok=True) scene.render.engine = "CYCLES" scene.cycles.samples = 64 scene.cycles.use_denoising = True scene.render.resolution_percentage = 100 scene.render.image_settings.file_format = "PNG" for rot, st in LADDER: set_hdri(rot, st) scene.render.filepath = str(OUT / f"rot{rot:03d}_st{st:g}.png") bpy.ops.render.render(write_still=True) print("готово:", len(list(OUT.glob("*.png"))), "кадров") ``` ### 5. Color Management: смотрим, а не угадываем ```python import bpy scene = bpy.context.scene vs = scene.view_settings print("view_transform:", vs.view_transform) print("доступные:", [i.identifier for i in type(vs).bl_rna.properties["view_transform"].enum_items]) print("look:", vs.look) print("available looks:", [i.identifier for i in type(vs).bl_rna.properties["look"].enum_items]) for transform in [i.identifier for i in type(vs).bl_rna.properties["view_transform"].enum_items][:6]: vs.view_transform = transform scene.render.filepath = str(OUT / f"vt_{transform}.png") bpy.ops.render.render(write_still=True) ``` ### 6. Light Probe и запекание ```python import bpy scene = bpy.context.scene probe = bpy.data.objects.get("LP_Sphere") if probe is None: # Типы LightProbe отличаются между версиями — создаём и смотрим, что получилось bpy.ops.object.lightprobe_add(type="SPHERE", location=(0, 0, 1)) probe = bpy.context.object probe.name = "LP_Sphere" print("probe:", probe.name, probe.data.type) scene.render.engine = "CYCLES" scene.cycles.samples = 128 bpy.ops.object.lightprobe_cache_bake(subset="ALL") # subset: 'ALL' | 'VISIBLE' | 'SELECTED' print("light cache baked") ``` ### 7. HDRI как видимый свет vs как только источник ```python import bpy scene = bpy.context.scene # 1) HDRI светит, фон — прозрачный (для композита поверх рендера в Nuke/AE) scene.render.film_transparent = True # 2) HDRI светит и виден в кадре scene.render.film_transparent = False # Объём: World Volume Scatter добавляет туман, который читает ту же HDRI vol = scene.world.node_tree.nodes.new("ShaderNodeVolumeScatter") vol.inputs["Color"].default_value = (0.8, 0.85, 0.95, 1.0) vol.inputs["Density"].default_value = 0.02 scene.world.node_tree.links.new( vol.outputs["Volume"], scene.world.node_tree.nodes["World Output"].inputs["Volume"]) scene.render.filepath = str(OUT / "volumetric.png") bpy.ops.render.render(write_still=True) print("volume on") ``` ### 8. Рендер-движок: выбираем осознанно ```python import bpy scene = bpy.context.scene # scene.render.engine — динамический enum, состав которого зависит от того, # какие аддоны включены. Поэтому читаем текущее значение и перебираем варианты # в try/except, а не полагаемся на список из памяти. print("сейчас:", scene.render.engine) for engine in ("CYCLES", "BLENDER_EEVEE_NEXT", "BLENDER_EEVEE", "BLENDER_WORKBENCH"): try: scene.render.engine = engine print("доступен:", engine) except TypeError: print("недоступен:", engine) scene.render.engine = "CYCLES" # EEVEE Next в 4.2+ сильно отличается от старого EEVEE: настройки лучей и AO # переехали, старые флаги исчезли. Если нужен быстрый превью — сначала Workbench. print("итог:", scene.render.engine) ``` > [!NOTE] > Имя `BLENDER_EEVEE_NEXT` существовало только в переходный период, а в 4.2+ движок снова называется `BLENDER_EEVEE`. Поэтому код выше перебирает оба и опирается на `try/except`, а не на зашитое имя. ### 9. Диапазон: почему картинка то «нормальная», то серая ```python import bpy scene = bpy.context.scene vs = scene.view_settings # Ступеньки экспозиции — самый быстрый способ понять, «серая» картинка из-за # экспозиции или из-за неверного view transform for e in (-2.0, -1.0, 0.0, 1.0, 2.0): vs.exposure = e print(f"exposure {e:+.1f} -> transform {vs.view_transform}, look {vs.look}") # Сброс и фиксация того, что подходит — иначе стенд невоспроизводим vs.exposure = 0.0 print("зафиксировано:", vs.view_transform, "|", vs.look) ``` Диапазон источника стоит проверить до рендера, а не после: у HDR-окружения он шире, чем у любого 8-битного формата, и часть яркостей срезается ещё на этапе HDRI, если карта была пережата в LDR. ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```bash # Прогнать лестницу без открытого Blender blender --background hdri_lab.blend --python hdri_ladder.py ``` | Параметр | Значение | Когда менять | |---|---|---| | `Mapping → Rotation.z` | `0…360°` | «позолотить» сцену — двигай азимут | | `Background → Strength` | `0.5 / 1 / 2` | больше 2 выжигает, меньше 0.3 даёт плоский свет | | `view_transform` | `AgX` в 4.x | `Filmic` в 3.x, `Standard` для контроля пересветов | | `view_settings.exposure` | `-2…+2` | в ступеньках по 0.5 | | `cycles.samples` | `64` превью / `512` финал | деноизер съедает шум | | `film_transparent` | `True` для композита | `False` когда HDRI должен быть в кадре | | `lightprobe_cache_bake` | `subset="ALL"` | `VISIBLE` быстрее, `SELECTED` для отладки одной зоны | | Volume Scatter `Density` | `0.005–0.05` | выше — «молоко» | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | HDRI не крутится | поворот повешен на объект, а не на Mapping | крути `ShaderNodeMapping`, а не Environment Texture | | Сцена «кислотная» | `view_transform` = `Standard` при яркой HDRI | `AgX` / `Filmic` | | Metal-эталон чёрный | HDRI без солнца, только мягкий свет | возьми HDRI с ярким источником в кадре | | Тени не появляются | HDRI-окружение, а не Area/Sun | HDRI даёт мягкий свет без теней; нужен отдельный источник | | `lightprobe_add` падает | тип `SPHERE` переименован | напечатай `[i.identifier for i in ...properties['type'].enum_items]` | | `lightprobe_cache_bake` не найден | запекание только в Cycles | переключи `render.engine` на `CYCLES` | | Один из 12 кадров чёрный | рендер прервался на долгом кадре | проверь `scene.cycles.max_bounces` и память | | Volume-туман не видно | нет ограничивающего объёма | добавь куб с Volume Scatter или World Volume | | Фон виден, а сцены нет | `film_transparent=True` | сохраняй PNG с альфа-каналом | ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 02: GENESISCORE LOCAL (OLLAMA)](setup_02_genesiscore_local.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 04: HEADLESS RENDER (CLI / CI/CD)](setup_04_headless_render.md), [SETUP 05: RENDER FARM (FLAMENCO)](setup_05_render_farm.md) - **По темам:** [CASE 03: PRODUCT VISUALIZATION](../cases/case_03_product_viz.md), [P13: LIGHTING & HDRI](../prompts/13_lighting_hdri.md), [P14: CAMERA & COMPOSITION](../prompts/14_camera_composition.md), [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md) - **Теги:** #lighting, #setup --- # SETUP 42: TRIPO AI API Tags: #ai-3d #api #setup # SETUP 42: TRIPO AI API **ID:** SETUP-42 | **Type:** Text/Image-to-3D | **Difficulty:** ⭐⭐ | **Time:** 30-40 min > Когда применять: когда нужен облачный text/image-to-3D по API с оплатой по кредитам, а не локальная модель — Tripo даёт готовый меш с PBR-картами, и задача состоит в том, чтобы корректно отправить задание, дождаться готовности и забрать результат в Blender. ## 🎯 ЦЕЛЬ Настраиваем облачный конвейер Tripo: ключ → постановка задачи (по тексту и/или по картинке) → `job id` → опрос статуса → URL результата → импорт в Blender. Ключевое, что надо понимать до начала: **это не локальный инструмент**. Нужен аккаунт Tripo, ключ API и кредиты — бесплатный лимит при регистрации есть, дальше платно, поэтому статья честно разделяет то, что работает локально (Blender, MCP-тулы импорта), и то, что требует денег (сама генерация). > [!NOTE] > Точные имена полей ответа Tripo (имя ключа, которое возвращает `job id`, точный путь эндпоинта создания задачи, имена форматов экспорта) я намеренно не привожу: они менялись между версиями API. Ниже приведён **контракт**, а не конкретные имена. Сверяй `BASE`, путь и ключи с текущей документацией Tripo перед первым запуском. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Аккаунт Tripo | — | кредиты и ключ | | Ключ API | — | авторизация запросов | | Python + `requests` | 3.10+ | HTTP-обвязка | | Blender | 4.x LTS | импорт результата | | blender-mcp с Tripo-инструментами | текущий | опрос статуса и импорт | | GLB / FBX / OBJ | — | форматы, которые Blender читает | ## 🛠️ НАСТРОЙКА ### 1. Проверяем, включён ли Tripo в этой сборке ```python # В MCP-сессии вызови get_tripo_status() — вернёт, включён ли провайдер и задан ли ключ. # Здесь — то же самое из Python: читаем переменные окружения, которые аддон видит. import os for var in ("BLENDERMCP_TRIPO_API_KEY", "TRIPO_API_KEY"): print(var, "set" if os.environ.get(var) else "missing") ``` > [!NOTE] > Имя переменной окружения для Tripo уточни в настройках аддона в своей сборке. В текущем MCP-каталоге Tripo-инструменты есть (`get_tripo_status`, `poll_tripo_job_status`, `import_generated_asset_tripo`), но переменная, которую аддон читает, в документации проекта не зафиксирована — не выдумывай её, посмотри в UI аддона. ### 2. Каркас клиента: submit → poll → download ```python import os, time, json, requests BASE = "https://platform.tripo3d.ai" # хост и префикс пути — сверь с документацией KEY = os.environ["TRIPO_API_KEY"] HDRS = {"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"} def submit(payload: dict) -> str: """Отправили промпт -> получили id задания. Имя ключа с id берём из ответа, не угадываем.""" r = requests.post(f"{BASE}/v2/openapi/task", headers=HDRS, json=payload, timeout=60) r.raise_for_status() body = r.json() print("ответ на submit:", json.dumps(body)[:400]) # сначала посмотри, потом правь return body["data"]["task_id"] # <-- путь по ответу: проверь фактический def poll(task_id: str, timeout_s: int = 600) -> str: """Опрашиваем статус, пока задание не дошло до терминального состояния.""" deadline = time.time() + timeout_s while time.time() < deadline: r = requests.get(f"{BASE}/v2/openapi/task/{task_id}", headers=HDRS, timeout=30) r.raise_for_status() data = r.json()["data"] status = data.get("status") print(status, data.get("progress")) if status in ("success", "failed", "expired"): return data time.sleep(5) raise TimeoutError(task_id) ``` ### 3. Ставим задачу из картинки + уточняющий текст ```python payload = { "type": "image_to_model", "file": {"type": "jpg", "file_type": "file_url", "url": "https://.../ref.jpg"}, "prompt": "worn leather satchel, stitched seams, soft edges", "face_limit": 1000, "texture": True, } # task_id = submit(payload) # data = poll(task_id) # print(json.dumps(data, indent=2)[:800]) # здесь придёт ссылка на модель ``` > [!NOTE] > Имена полей `type`, `file`, `prompt`, а также значения `image_to_model` / `text_to_model` и флаг текстурирования — сверяй с документацией: их переименовывали. Смысл запроса неизменен — картинка плюс текстовое уточнение, на выходе ссылка на файл модели. ### 4. Скачиваем результат ```python from pathlib import Path import requests def download_result(model_url: str, out_dir=Path(r"C:/assets/ai3d/tripo")) -> Path: out_dir.mkdir(parents=True, exist_ok=True) r = requests.get(model_url, stream=True, timeout=300) r.raise_for_status() dest = out_dir / Path(model_url.split("?")[0]).name with open(dest, "wb") as f: for chunk in r.iter_content(1 << 16): f.write(chunk) return dest # dest = download_result(data["model"]["pbr_model"]["url"]) # print(dest, dest.stat().st_size // 1024, "KB") ``` ### 5. Импорт в Blender ```python import bpy from pathlib import Path def import_ai3d(path: Path, coll_name="AI3D") -> bpy.types.Collection: coll = bpy.data.collections.get(coll_name) or bpy.data.collections.new(coll_name) if coll.name not in bpy.context.scene.collection.children: bpy.context.scene.collection.children.link(coll) before = set(bpy.data.objects) bpy.ops.import_scene.gltf(filepath=str(path)) # .glb / .gltf new = sorted(set(bpy.data.objects) - before, key=lambda o: o.name) for o in new: for c in list(o.users_collection): c.objects.unlink(o) coll.objects.link(o) return coll # coll = import_ai3d(dest) # print(len(coll.objects), "объектов") ``` > [!NOTE] > Ранг-файлы (скелет и анимация) приходят отдельными файлами в своём формате и в glTF не всегда переезжают. Если нужен риг — импортируй скелет отдельно, формат подбирай по документации; не рассчитывай, что GLB принесёт позу. ### 6. То же самое через MCP-тулы ```python # get_tripo_status() -> включён ли провайдер # poll_tripo_job_status(request_id="...") -> статус и прогресс # import_generated_asset_tripo(request_id="...", name="TripoSatchel") ``` Эти три вызова — контракт: id задания ты получаешь сам (шаг 2), MCP умеет опрашивать и импортировать. Создания задачи среди них нет. ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```bash # Проверка баланса/кредитов — запрос без изменений состояния curl -s -H "Authorization: Bearer $TRIPO_API_KEY" "https://platform.tripo3d.ai/v2/openapi/user/balance" ``` | Параметр | Значение | Когда менять | |---|---|---| | `BASE` | хост Tripo API | сверь с документацией, менялся | | таймаут опроса | `600` с | генерация сложной сцены дольше 5 мин | | интервал опроса | `5` с | меньше — упрёшься в лимит запросов | | текстура | `true` | `false` быстрее, но меш без PBR | | формат выгрузки | GLB | для игр; OBJ/FBX — если нужна совместимость | | лимит полигонов | `face_limit` | меньше = быстрее и дешевле по кредитам | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | 401/403 от API | ключ не задан или не передан | проверь заголовок `Authorization: Bearer <ключ>` | | `KeyError: 'task_id'` | путь к id в ответе другой | напечатай `json.dumps(body)`, поправь разбор | | Задание вечно `running` | счётчик кредитов кончился | проверь баланс; часть платформ «съедает» задание молча | | Импорт пустой | файл скачался как HTML-заглушка | проверь размер и первые байты до `gltf`-импорта | | Меш в неверном масштабе | облако не знает про метры | `normalize()` как в [SETUP 38](setup_38_fab_epic.md) | | Нет нормалей/руфнесса | заказывал без текстуры | перезакажи с текстурой либо запеки в Blender | | MCP: Tripo disabled | провайдер не включён в аддоне | включи в настройках аддона и перезапусти Blender | | Разные `request_id` форматы | MCP ждёт свой, API отдаёт свой | перескажи id точно, без обрезки | ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 43: MESHY API V4](setup_43_meshy_api_v4.md), [SETUP 16: BLENDER + AUTOMATIC1111 API](setup_16_automatic1111_api.md), [SETUP 44: RODIN / HYPER3D GEN](setup_44_rodin_hyper3d.md), [SETUP 45: HUNYUAN3D 2.1 LOCAL](setup_45_hunyuan3d_21.md), [SETUP 46: SHAP-E / POINT-E LOCAL](setup_46_shap_e_local.md) - **По темам:** [SETUP 47: ZERO123 / INSTANTMESH](setup_47_zero123_instantmesh.md), [SETUP 48: GAUSSIAN SPLATTING → BLENDER](setup_48_gaussian_splatting.md), [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md) - **Теги:** #ai-3d, #api, #setup --- # SETUP 43: MESHY API V4 Tags: #ai-3d #api #setup # SETUP 43: MESHY API V4 **ID:** SETUP-43 | **Type:** Text-to-3D/Texture | **Difficulty:** ⭐⭐ | **Time:** 40-50 min > Когда применять: когда нужен облачный цикл целиком — text-to-3D, потом text-to-texture, ремеш, анимация — через HTTP API Meshy, потому что в текущей сборке blender-mcp интеграции с Meshy нет. ## 🎯 ЦЕЛЬ Собираем полный цикл через публичный REST API Meshy: ключ → создание задачи любого типа → `task id` → опрос → ссылка на результат → импорт в Blender. Отдельные MCP-инструменты у Meshy в текущем MCP-каталоге **отсутствуют** — это факт, а не упущение: интеграции нет, всё делается HTTP-вызовами из `execute_blender_code` или из внешнего скрипта. Отдельно проговариваем деньги: Meshy даёт небольшой бесплатный лимит, дальше тарифы по кредитам, и цены пересматриваются. > [!NOTE] > Название «v4» в заголовке статьи — версия поколения API на момент написания. Имена полей ответа, точный базовый хост и префиксы путей менялись между мажорными версиями Meshy. Ниже везде приведён контракт, а конкретные ключи надо брать из текущей документации (docs.meshy.ai), а не из памяти. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Аккаунт Meshy | — | кредиты и ключ | | Ключ API | — | Bearer-авторизация | | Python + `requests` | 3.10+ | HTTP-обвязка | | Blender | 4.x LTS | импорт и чистка | | GLB / FBX | — | форматы выгрузки | ## 🛠️ НАСТРОЙКА ### 1. Ключ и базовый хост ```python import os KEY = os.environ["MESHY_API_KEY"] # Хост и префикс версионирования берём из документации: менялись и хост, и путь BASE = "https://api.meshy.ai" HDRS = {"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"} print("key len:", len(KEY), "| base:", BASE) ``` > [!NOTE] > `https://api.meshy.ai` — указан как база по умолчанию. Если отдаёт 404 или 401 сразу, открой docs.meshy.ai и возьми актуальный хост: у Meshy менялись и домен, и структура путей, и способ доставки результата (иногда результат приходит сразу в ответе, а не ссылкой). ### 2. Универсальный контракт: submit → poll → result ```python import time, json, requests def create_task(path: str, payload: dict) -> str: """Создали задачу -> получили id. Ключ с id смотри в ответе, не угадывай.""" r = requests.post(f"{BASE}{path}", headers=HDRS, json=payload, timeout=60) r.raise_for_status() body = r.json() print("submit ->", json.dumps(body)[:400]) return body["result"] # <-- путь по ответу: проверь фактический def poll(task_id: str, timeout_s: int = 600) -> dict: deadline = time.time() + timeout_s while time.time() < deadline: r = requests.get(f"{BASE}/textures/{task_id}", headers=HDRS, timeout=30) # ^ конкретный путь опроса свой у каждого типа задачи — смотри документацию r.raise_for_status() data = r.json()["result"] print(data.get("status"), data.get("progress")) if data.get("status") in ("SUCCEEDED", "FAILED", "CANCELED", "EXPIRED"): return data time.sleep(6) raise TimeoutError(task_id) ``` > [!NOTE] > Путь опроса зависит от типа задачи: text-to-3D, text-to-texture, remesh, rigging и анимация обслуживаются разными маршрутами. В примере это сделано параметром-шаблоном. Точные идентификаторы статусов (`SUCCEEDED` или `succeeded` — регистр тоже менялся) сверь в документации. ### 3. Text-to-3D ```python payload = { "mode": "preview", # preview быстрее и дешевле, preview=false — финал "prompt": "stylized orc axe, notched blade, leather wrap on haft", "should_remesh": True, "should_texture": True, "should_rig": False, } # task_id = create_task("/textures", payload) # путь для text-to-3D свой — сверь # data = poll(task_id) # print(data.get("model_urls")) ``` ### 4. Text-to-texture по готовому мешу ```python tex_payload = { "mode": "preview", "image_url": "https://.../your_axe.png", # рендер меша с белым фоном "prompt": "worn pitted steel, bloodstained wooden haft, stylized game asset", "base_mesh_id": "секция моделей в ответе на text-to-3D", } # tex_id = create_task("/textures", tex_payload) ``` ### 5. Ремеш и ретопология ```python # Ремеш задаётся либо флагом в text-to-3D (should_remesh), либо отдельной задачей, # которой скармливается URL модели из шага 3: remesh_payload = { "model_id": "секция моделей из ответа на text-to-3D", "topology": "quad", # tri | quad "target_face_count": 15000, } # remesh_id = create_task("/remesh", remesh_payload) # путь сверь ``` ### 6. Скачиваем и импортируем в Blender ```python from pathlib import Path import bpy, requests def fetch(url: str, out: Path) -> Path: out.parent.mkdir(parents=True, exist_ok=True) with requests.get(url, stream=True, timeout=300) as r: r.raise_for_status() with open(out, "wb") as f: for chunk in r.iter_content(1 << 16): f.write(chunk) return out def import_model(path: Path, coll="AI3D") -> bpy.types.Collection: c = bpy.data.collections.get(coll) or bpy.data.collections.new(coll) if c.name not in bpy.context.scene.collection.children: bpy.context.scene.collection.children.link(c) before = set(bpy.data.objects) bpy.ops.import_scene.gltf(filepath=str(path)) for o in set(bpy.data.objects) - before: for old in list(o.users_collection): old.objects.unlink(o) c.objects.link(o) return c # glb_url = [u for u in data["model_urls"] if u.endswith(".glb")][0] # path = fetch(glb_url, Path(r"C:/assets/ai3d/meshy/orc_axe.glb")) # print(len(import_model(path).objects), "объектов") ``` ### 7. Приводим меш в порядок после импорта ```python import bpy obj = bpy.data.objects.get("orc_axe") or next(iter(bpy.data.collections["AI3D"].objects)) bpy.ops.object.select_all(action="DESELECT") obj.select_set(True) bpy.context.view_layer.objects.active = obj dec = obj.modifiers.new("Decimate", "DECIMATE") dec.ratio = 0.5 sub = obj.modifiers.new("Subdiv", "SUBSURF") sub.levels, sub.render_levels = 1, 2 # Сглаживание по углу: имя оператора менялось между 4.0 и 4.1 smooth_ops = [o for o in dir(bpy.ops.object) if "smooth" in o or "auto" in o] print("available:", smooth_ops) ``` > [!NOTE] > В 4.1 оператор авто-сглаживания переименован (было `shade_auto_smooth`, стало `shade_smooth_by_angle`). Проверь список и вызови тот, что есть в твоей версии — точечный вызов надёжнее, чем зашитое имя. ### 7. Оценка стоимости до запуска ```python # Перед пакетной генерацией считаем, во сколько обойдётся партия. # Конкретную стоимость кредита бери из аккаунта — она меняется, и в статье # любой «примерный» тариф станет устаревшим быстрее, чем любой другой факт. PLAN = [ {"task": "text-to-3D preview", "count": 30}, {"task": "text-to-3D final", "count": 5}, {"task": "text-to-texture", "count": 5}, {"task": "remesh", "count": 5}, ] for row in PLAN: print(f"{row['task']:22s} x{row['count']:<3d} -> всего задач: {row['count']}") print("Итого операций:", sum(r["count"] for r in PLAN)) # Затем сверь расход: GET из документации ``` Прежде чем запускать пакет, прогони одну задачу в режиме предпросмотра и посмотри результат. Разница между дешёвым превью и платным финалом обычно окупает потраченные пять минут. ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```bash # Проверка кредитов/профиля перед пакетной генерацией curl -s -H "Authorization: Bearer $MESHY_API_KEY" "https://api.meshy.ai/users/meshy/balance" ``` | Параметр | Значение | Когда менять | |---|---|---| | `mode` | `preview` | `text`/`refine` для финального качества, дороже | | `should_remesh` | `True` | меш с нормалями вместо «капли» | | `should_texture` | `True` | без текстуры меш бесполезен для продакшена | | `should_rig` | `True` для персонажей | требует валидной топологии, чинит артефакты | | `topology` | `quad` | `tri` быстрее и дешевле | | `target_face_count` | `15000` | игровой проп; `50000+` для крупных планов | | интервал опроса | `6` с | меньше — лимит запросов | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | 401 сразу | ключ не тот или срок истёк | перевыпусти ключ в аккаунте | | 404 на любом пути | сменилась версия API | сверь `BASE` и префикс с документацией | | `KeyError` по ключу ответа | схема ответа другая | напечатай `json.dumps(body)[:800]` и поправь разбор | | Задача висит в `PENDING` дольше минуты | очередь платформы | это нормально; не перезапускай — создашь дубль | | Меш «капля» без текстуры | не заказан ремеш/текстура | повтори с `should_remesh` и `should_texture` | | UV растянуты | авто-UV на неправильной топологии | quad-ремеш перед текстурированием | | Скелет не импортировался | формат скелета не в GLB | импортируй скелет отдельным файлом | | Blender-mcp не знает про Meshy | интеграции нет в этой сборке | делай через HTTP, как выше | ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 42: TRIPO AI API](setup_42_tripo_ai_api.md), [SETUP 16: BLENDER + AUTOMATIC1111 API](setup_16_automatic1111_api.md), [SETUP 44: RODIN / HYPER3D GEN](setup_44_rodin_hyper3d.md), [SETUP 45: HUNYUAN3D 2.1 LOCAL](setup_45_hunyuan3d_21.md), [SETUP 46: SHAP-E / POINT-E LOCAL](setup_46_shap_e_local.md) - **По темам:** [SETUP 47: ZERO123 / INSTANTMESH](setup_47_zero123_instantmesh.md), [SETUP 48: GAUSSIAN SPLATTING → BLENDER](setup_48_gaussian_splatting.md), [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md) - **Теги:** #ai-3d, #api, #setup --- # SETUP 44: RODIN / HYPER3D GEN Tags: #ai-3d #setup # SETUP 44: RODIN / HYPER3D GEN **ID:** SETUP-44 | **Type:** Image-to-3D | **Difficulty:** ⭐⭐ | **Time:** 25-30 min > Когда применять: когда нужна быстрая облачная генерация меша по текстовому описанию с последующей чисткой в Blender — и когда хочется делать это прямо из MCP-сессии, а не отдельным HTTP-клиентом. ## 🎯 ЦЕЛЬ Rodin от Hyper3D — облачный генератор: ключ API, кредиты, очередь заданий. Настраиваем полный цикл через встроенные MCP-инструменты: проверить, что провайдер включён; отправить промпт с необязательным ограничением габаритов; опросить статус; импортировать результат в сцену. Дальше — обязательная чистка: облако возвращает плотный меш с непредсказуемой топологией, и без декаimation, авто-UV и запекания он не годится ни для рендера, ни для игры. > [!NOTE] > Имена MCP-тулов, как они выглядят сейчас: `get_hyper3d_status`, `generate_hyper3d_model_via_text`, `poll_rodin_job_status`, `import_generated_asset`. В справочниках проекта встречаются и другие (`create_rodin_job`, `generate_rodin_model`) — это версии постарше. Ориентируйся на свой каталог тулов, а не на архивные страницы. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Аккаунт Hyper3D | — | кредиты и подписка | | Ключ API | — | авторизация | | Переменная `BLENDERMCP_HYPER3D_API_KEY` | — | аддон читает её при старте | | Blender | 4.x LTS | импорт и чистка | | blender-mcp с Hyper3D-инструментами | текущий | весь цикл | | Спрайт нормалей | — | запечь после ремеша | ## 🛠️ НАСТРОЙКА ### 1. Ключ в окружении аддона ```python import os # Аддон читает ключ из окружения при старте Blender. Проверяем, что он есть — # иначе get_hyper3d_status вернёт «disabled». print("BLENDERMCP_HYPER3D_API_KEY:", "set" if os.environ.get("BLENDERMCP_HYPER3D_API_KEY") else "missing") ``` > [!NOTE] > `BLENDERMCP_HYPER3D_API_KEY` — имя из документации проекта. Если MCP-сервер запускается не из того окружения, где ты её задал, аддон её не увидит. Ключ можно занести и в настройки аддона в UI — проверь оба пути. ### 2. Проверка провайдера ```python # Вызов в MCP-сессии: # get_hyper3d_status() # Возвращает, включён ли провайдер и есть ли ключ. Пока здесь disabled — # все шаги дальше вернут ошибку авторизации. ``` ### 3. Отправляем промпт ```python # generate_hyper3d_model_via_text(text_prompt="...", bbox_condition=[...]) # 1) Минимальный вызов: # generate_hyper3d_model_via_text(text_prompt="weathered stone garden gnome holding a lantern") # 2) С ограничением габаритов — чтобы объект не вылез за пропорции: # generate_hyper3d_model_via_text(text_prompt="low poly wooden crate", # bbox_condition=[-0.5, -0.5, 0.0, 0.5, 0.5, 1.0]) ``` > [!NOTE] > `bbox_condition` — массив из шести чисел, и это **единственное**, что я не могу описать точно: непонятно, это ли центр+размер или минимум+максимум, и в каких единицах. Проверь в документации Hyper3D, прежде чем полагаться на масштаб. Как ограничение габаритов он очевидно полезен: без него модель приходит в произвольном масштабе. ### 4. Опрос статуса ```python # poll_rodin_job_status(request_id="...") # Поля: subscription_key (опционально), request_id. # Возвращает статус и прогресс; пока не терминальное состояние — жди и опрашивай снова. ``` ```text Ожидаемые состояния по ходу жизни задания (смысл, не точные строки): queued / running -> задание в работе, ждём succeeded -> готово, можно импортировать failed -> смотрим текст ошибки в ответе ``` > [!NOTE] > Точные строковые значения статусов не привожу: набор различается между сборками. Смотри на поле `status` в фактическом ответе `poll_rodin_job_status` и сравнивай с тем, что вернул предыдущий опрос. ### 5. Импорт результата ```python # import_generated_asset(name="RodinGnome", request_id="...") # Дополнительно доступен параметр task_uuid — заполняй его, если он есть в ответе: # import_generated_asset(name="RodinGnome", request_id="...", task_uuid="...") ``` ### 6. Чистка меша: декаimация ```python import bpy obj = bpy.data.objects.get("RodinGnome") or bpy.data.collections["AI3D"].objects[0] bpy.ops.object.select_all(action="DESELECT") obj.select_set(True) bpy.context.view_layer.objects.active = obj) print("до:", len(obj.data.polygons), "полигонов") dec = obj.modifiers.new("Decimate", "DECIMATE") # ratio считается от текущей плотности, а не задаёт целевое число current = len(obj.data.polygons) target = 15000 dec.ratio = max(0.0, min(1.0, target / current)) # Subdivision обратно сглаживает то, что декаimation сломала sub = obj.modifiers.new("Subdiv", "SUBSURF") sub.levels, sub.render_levels = 1, 2 print("ratio:", dec.ratio, "| целевой бюджет:", target) ``` ### 7. Авто-UV и запекание нормалей ```python import bpy bpy.ops.object.mode_set(mode="EDIT") bpy.ops.mesh.select_all(action="SELECT") bpy.ops.uv.smart_project(angle_limit=1.15192, island_margin=0.02) # ~66 градусов bpy.ops.object.mode_set(mode="OBJECT") print("UV islands:", len(obj.data.uv_layers)) # Запекаем нормали, чтобы они не ездили после изменения топологии bpy.ops.object.select_all(action="DESELECT") obj.select_set(True) bpy.context.view_layer.objects.active = obj bpy.context.view_layer.objects.active.data.use_auto_smooth = True # если есть в версии bpy.ops.object.bake(type="NORMAL", margin=8, use_clear=True) print("bake done") ``` > [!NOTE] > Поле `use_auto_smooth` на меш-данных удаляли в 4.1 вместе с переездом на шейдерные углы. Перед вызовом проверь: `hasattr(obj.data, "use_auto_smooth")`, иначе пропусти строку и полагайся на `shade_smooth_by_angle` / `bpy.ops.object.modifier_add(type="NODES")` с модификатором Smooth by Angle. ### 8. Масштаб и посадка на сцену ```python import bpy from mathutils import Vector bpy.context.view_layer.update() if obj.dimensions.z > 0: s = 0.6 / obj.dimensions.z obj.scale = (s, s, s) bpy.context.view_layer.update() ground_z = min((obj.matrix_world @ Vector(c)).z for c in obj.bound_box) obj.location.z -= ground_z print("посажено на Z=0, итог:", tuple(round(v, 3) for v in obj.dimensions)) ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```bash # Проверить, что аддон видит ключ, до того как ждать ошибку от облака echo $env:BLENDERMCP_HYPER3D_API_KEY | cut -c1-6 ``` | Параметр | Значение | Когда менять | |---|---|---| | `text_prompt` | конкретный объект + материал + стиль | «красивый проп» не работает, детализация важнее | | `bbox_condition` | 6 чисел | ограничить габарит; точный формат уточнить в документации | | `name` в `import_generated_asset` | `Rodin<Что>` | имя попадёт в сцену и в Outliner | | `task_uuid` | из ответа задания | заполняй, если поле есть — иначе импорт может не найти результат | | `dec.ratio` | `target / current` | пересчитывай после каждой новой генерации | | `smart_project angle_limit` | `1.15192` рад (~66°) | меньше — больше швов на сгибах | | `bake margin` | `8` px | увеличивай, если по краям UV видны артефакты | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | `get_hyper3d_status` = disabled | ключ не виден аддону | проверь `BLENDERMCP_HYPER3D_API_KEY` в окружении сервера, перезапусти Blender | | Ошибка авторизации при отправке | ключ неверный или истёк | перевыпусти на hyper3d.ai | | `poll` возвращает 404 по id | перепутан `request_id` и `task_uuid` | сверь поля в ответе на отправку | | Задание `failed` без текста | в ответе ошибка в другом поле | напечатай полный ответ, а не только `status` | | Модель в 100 раз больше сцены | не сработал `bbox_condition` / нет нормализации | прогони шаг 8 | | После декаимации кашица | слишком агрессивный `ratio` | подними ratio до 0.6-0.8, добавь Subdivision | | Нормали «плывут» после правки | не запечены | `bpy.ops.object.bake(type="NORMAL")` | | `use_auto_smooth` не существует | версия 4.1+ | проверь `hasattr` и используй Smooth by Angle | | Текстуры не на месте | GLB без встроенных карт | импортируй карты отдельными файлами | ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 42: TRIPO AI API](setup_42_tripo_ai_api.md), [SETUP 43: MESHY API V4](setup_43_meshy_api_v4.md), [SETUP 45: HUNYUAN3D 2.1 LOCAL](setup_45_hunyuan3d_21.md), [SETUP 46: SHAP-E / POINT-E LOCAL](setup_46_shap_e_local.md), [SETUP 47: ZERO123 / INSTANTMESH](setup_47_zero123_instantmesh.md) - **По темам:** [SETUP 48: GAUSSIAN SPLATTING → BLENDER](setup_48_gaussian_splatting.md), [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md) - **Теги:** #ai-3d, #setup --- # SETUP 45: HUNYUAN3D 2.1 LOCAL Tags: #ai-3d #blender #llm #mcp #pipeline #setup # SETUP 45: HUNYUAN3D 2.1 LOCAL **ID:** SETUP-45 | **Type:** Local 3D Gen | **Difficulty:** ⭐⭐⭐ | **Time:** 2-3 h > Когда применять: когда нужен 3D-генератор на своей машине — без отправки промптов и референсов в чужое облако. Hunyuan3D крутится локально, но весит и требует GPU, поэтому вся статья — про то, сколько места и VRAM нужно и как поднять сервис так, чтобы MCP его увидел. ## 🎯 ЦЕЛЬ Hunyuan3D — open-source генератор от Tencent с двухстадийным пайплайном: сначала форма (shapе), затем текстуры. В отличие от [Tripo](setup_42_tripo_ai_api.md) и [Meshy](setup_43_meshy_api_v4.md), здесь платить не надо: платит твоё железо. Настраиваем локальный запуск, поднимаем свой HTTP-эндпоинт и прописываем его аддон через `BLENDERMCP_HUNYUAN3D_API_URL`, чтобы MCP-инструменты опрашивали задание и импортировали результат в сцену. > [!NOTE] > Наличие отдельной переменной `BLENDERMCP_HUNYUAN3D_API_URL` — сам по себе факт, что аддон умеет ходить в **поднятый тобой** сервис, а не только в облако Tencent. Это и делает слово «local» в названии честным. Точное имя скрипта запуска и его флаги смотри в README репозитория: они меняются между коммитами. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Ориентир | Зачем | |---|---|---| | GPU | 12 ГБ VRAM минимум, 16+ комфортно | обе стадии; текстуры — тяжёлая | | CUDA | версия под твою карту | без неё будет CPU-режим и часы ожидания | | Диск | 20-30 ГБ | веса моделей + кэш | | RAM | 32 ГБ | выгрузка текстур | | Python | 3.10-3.11, окружение venv | изолированные зависимости | | Blender | 4.x LTS | импорт результата | > [!NOTE] > Требования к VRAM менялись от стадии к стадии. Ориентир «12 ГБ» — расхожая цифра из обсуждений, а не спецификация: точные значения для конкретного веса бери из model card в репозитории. На 8 ГБ текстурная стадия либо не запустится, либо уйдёт в очень долгий расчёт. ## 🛠️ НАСТРОЙКА ### 1. Клонируем и ставим зависимости ```bash git clone https://github.com/Tencent-Hunyuan/Hunyuan3D-2.git cd Hunyuan3D-2 # Отдельное окружение: зависимости тяжёлые и конфликтуют с системными python -m venv .venv # Windows: .venv\Scripts\activate # Linux/macOS: # source .venv/bin/activate pip install -r requirements.txt ``` ### 2. Проверяем, что видно GPU ```python # venv_activate; python import torch print("torch:", torch.__version__) print("cuda available:", torch.cuda.is_available()) if torch.cuda.is_available(): print("device:", torch.cuda.get_device_name(0)) free, total = torch.cuda.mem_get_info() print(f"VRAM: {free/2**30:.1f} / {total/2**30:.1f} GB free") ``` > [!NOTE] > Имя проверочного скрипта и флаг выбора стадии (форма / текстура / обе) в репозитории менялись. Открой README и список файлов в корне перед запуском, а не полагайся на имя из этой статьи. ### 3. Поднимаем локальный сервис ```bash # В репозитории Hunyuan3D-2 есть скрипт запуска с веб-интерфейсом. # Точное имя файла и флаги — в README; типичный вид такой: python app.py --server_port 7860 # Если нужен чистый HTTP-эндпоинт без UI — ищи в репозитории скрипт, # который отдаёт JSON по HTTP, и подними именно его. ``` ```python # Проверяем, что сервис слушает наш порт import socket s = socket.socket(); s.settimeout(0.5) print("7860:", "open" if s.connect_ex(("127.0.0.1", 7860)) == 0 else "closed") s.close() ``` ### 4. Прописываем аддон ```python import os for var in ("BLENDERMCP_HUNYUAN3D_API_URL", "BLENDERMCP_HUNYUAN3D_SECRET_ID", "BLENDERMCP_HUNYUAN3D_SECRET_KEY"): print(var, "=", os.environ.get(var, "<не задан>")) ``` Аддон должен ходить в `127.0.0.1:<порт твоего сервиса>`, а не в облако. Если оставишь адрес облака по умолчанию, отправишь референс наружу. ### 5. Ставим задание ```python # В текущем MCP-каталоге есть poll_hunyuan_job_status и import_generated_asset_hunyuan, # но нет тула «создать задание» — задание создаётся в локальном сервисе # (его веб-интерфейсом или HTTP-вызовом), а MCP его только опрашивает и импортирует. # # import_generated_asset_hunyuan(name="Cabin", zip_file_url="http://127.0.0.1:7860/.../out.zip") # ^ обрати внимание: аргумент называется zip_file_url, то есть результат # ожидается архивом .zip, а не «голым» .glb. ``` > [!NOTE] > Это не домыслы: имя параметра `zip_file_url` в сигнатуре инструмента прямо указывает, что ожидается zip-архив. Согласуй с этим свой локальный сервис — он должен отдавать ссылку на zip, внутри которого лежат меш и PBR-карты. ### 6. Опрашиваем и импортируем ```python # poll_hunyuan_job_status(job_id="...") # import_generated_asset_hunyuan(name="Cabin", zip_file_url="...") ``` ### 7. Импорт GLB вручную, если сервис отдаёт файл ```python import bpy from pathlib import Path def import_glb(path: Path, coll="AI3D") -> bpy.types.Collection: c = bpy.data.collections.get(coll) or bpy.data.collections.new(coll) if c.name not in bpy.context.scene.collection.children: bpy.context.scene.collection.children.link(c) before = set(bpy.data.objects) bpy.ops.import_scene.gltf(filepath=str(path)) for o in set(bpy.data.objects) - before: for old in list(o.users_collection): old.objects.unlink(o) c.objects.link(o) return c # print(len(import_glb(Path(r"C:/assets/ai3d/hunyuan/cabin.glb")).objects)) ``` ### 8. Доводим результат ```python import bpy obj = next(iter(bpy.data.collections["AI3D"].objects)) print("полигонов:", len(obj.data.polygons), "| UV-слоёв:", len(obj.data.uv_layers)) dec = obj.modifiers.new("Decimate", "DECIMATE") dec.ratio = 0.5 sub = obj.modifiers.new("Subdiv", "SUBSURF") sub.levels, sub.render_levels = 1, 2 smooth_ops = [o for o in dir(bpy.ops.object) if "smooth" in o] print("smoothing ops:", smooth_ops) ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```bash # Веса качаются отдельно от кода — проверь, сколько занято du -sh checkpoints 2>/dev/null || dir /s checkpoints # Держим локальный сервис живым между заданиями nvidia-smi --query-gpu=memory.used,memory.total --format=csv ``` | Параметр | Значение | Когда менять | |---|---|---| | `BLENDERMCP_HUNYUAN3D_API_URL` | `http://127.0.0.1:<порт>` | обязательно локальный адрес, если не хочешь отправлять наружу | | `SecretId` / `SecretKey` | из окружения | для локального сервиса часто не нужны, но аддон может требовать | | текстурная стадия | `2K` | `4K` кратно дольше по времени и VRAM | | `dec.ratio` | `0.3–0.6` | для игрового пропа | | `Subdiv render_levels` | `2` | выше — резче силуэт, дольше рендер | | порт сервиса | `7860` | любой свободный, аддон должен знать тот же | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | `CUDA out of memory` | не хватает VRAM | закрой другие GPU-процессы, снизь разрешение текстур, `PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True` | | Идёт на CPU, всё очень долго | `torch.cuda.is_available()` = False | поставь torch с CUDA-сборкой под свою карту | | `get_hunyuan3d_status` = disabled | аддон не видит `API_URL` | проверь переменную в окружении сервера, перезапусти Blender | | `poll` не находит job | сервис перезапустился и потерял состояние | состояние держи на диске, не в памяти процесса | | Импорт падает на zip | ожидался «голый» glb, пришёл архив | распакуй zip и импортируй содержимое | | Текстуры не подхватились | карты лежат в zip, а не встроены | после распаковки подключи Image Texture вручную | | Меш в форме «капли» | не отработала стадия формы | прогоняй обе стадии, а не только текстуры | | Веса скачались не полностью | оборвалась загрузка | сверь размер файлов с ожидаемым из model card | | Исходники в не-ASCII пути | часть загрузчиков не переваривает | держи checkpoints на латинском пути | ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 35: READY PLAYER ME AVATARS](setup_35_readyplayerme.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 02: GENESISCORE LOCAL (OLLAMA)](setup_02_genesiscore_local.md), [SETUP 09: BLENDERBOT / AI CODING AGENT](setup_09_blenderbot_ai.md), [SETUP 14: BLENDER + GODOT PIPELINE](setup_14_godot_pipeline.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 48: GAUSSIAN SPLATTING → BLENDER](setup_48_gaussian_splatting.md), [P27: POLY HAVEN ASSET PIPELINE](../prompts/27_polyhaven_pipeline.md) - **Теги:** #ai-3d, #blender, #llm, #mcp, #pipeline, #setup --- # SETUP 46: SHAP-E / POINT-E LOCAL Tags: #ai-3d #setup # SETUP 46: SHAP-E / POINT-E LOCAL **ID:** SETUP-46 | **Type:** Research Gen | **Difficulty:** ⭐⭐ | **Time:** 30-40 min > Когда применять: когда нужен полностью локальный, бесплатный и предсказуемый по зависимостям генератор — и когда результат нужен как «форма-подсказка» для моделирования, а не как финальный ассет. ## 🎯 ЦЕЛЬ Shap-E и Point-E — исследовательские модели OpenAI, которые генерируют 3D из текста или картинки. Они не для продакшена: геометрия грубая, часто «каплевидная», зато работают на CPU, ничего не стоят и не отправляют данные наружу. Статья даёт честный расклад: где Shap-E сильнее Point-E, как поставить его локально, как вытащить из неявного представления нормальный меш и что в Blender с ним делать, чтобы он перестал выглядеть генератом. > [!NOTE] > Shap-E и Point-E — модели 2022-2023 годов, и их репозитории не развиваются. Зато они до сих пор полезны как способ проверить идею «а что, если сгенерить форму», без ключей, без очередей и без кредитов. Не жди от них деталей и текстур уровня облачных сервисов. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Python | 3.10-3.11 | официальный репозиторий рассчитан на это | | PyTorch | CUDA-сборка, если есть GPU | базовая модель на CPU работает, но медленно | | Репозиторий | `openai/shap-e` | код и веса | | `trimesh` | ставится с репозиторием | запись и просмотр меша | | Blender | 4.x LTS | чистка результата | | Около 2 ГБ диска | — | веса базовой и текстурной модели | ## 🛠️ НАСТРОЙКА ### 1. Ставим ```bash git clone https://github.com/openai/shap-e.git cd shap-e python -m venv .venv # Windows: .venv\Scripts\activate # Linux/macOS: # source .venv/bin/activate pip install -e . ``` ### 2. Смотрим, что реально лежит в репозитории ```bash ls shap_e/assets/ ls *.py | head -20 python -m pip show shap-e ``` > [!NOTE] > Имена сэмпл-скриптов и файлов весов в репозитории менялись. Прежде чем писать команду, посмотри список: `python sample_text_to_3d.py --help` покажет реальные флаги, если скрипт их поддерживает. Никакого `sample_text_to_3d.py --text ... --outdir ...` не выдумывается — проверь выводом `--help`. ### 3. Текст → форма ```python # Минимальный скрипт по API модели (имена классов стабильны в ветке main): from pathlib import Path from shap_e.rendering import TextTo3DModel # Точное имя файла весов бери из ls shap_e/assets/ — оно менялось между коммитами model = TextTo3DModel("shap_e.assets/<файл_из_assets>.pkl") print("модель загружена") ``` ```python # Дальше — рендер сетки из неявного представления. # У Shap-E это NeRF-подобная сетка плюс marching cubes, поэтому шаг обязателен. import numpy as np import torch # seed фиксирует результат: без него каждый запуск даёт разный меш torch.manual_seed(42) np.random.seed(42) model.renderers.mesh.render_coarse_to_fine( 0, # индекс промпта в списке "a red wooden chair", # текстовое описание 0, # индекс seed verbose=True, ) print("рендер завершён") ``` > [!NOTE] > Список промптов задаётся при создании `TextTo3DModel` либо в настройках рендерера, и его форма в разных коммитах отличается. Сначала напечатай доступные атрибуты `model.renderers` и прочитай пример из `README` репозитория — это надёжнее, чем угадывать сигнатуру. ### 4. Сохраняем в файл ```python import trimesh from pathlib import Path OUT = Path(r"C:/assets/ai3d/shap_e"); OUT.mkdir(parents=True, exist_ok=True) # scene у Shap-E — объект trimesh; у scene.geometry список мешей scene = None # <- результат шага 3 print(type(scene), [type(g).__name__ for g in getattr(scene, "geometry", [])]) # scene.export(OUT / "chair.glb") ``` ### 5. Импорт в Blender и грубая чистка ```python import bpy from pathlib import Path def import_ai3d(path: Path, coll="AI3D") -> bpy.types.Collection: c = bpy.data.collections.get(coll) or bpy.data.collections.new(coll) if c.name not in bpy.context.scene.collection.children: bpy.context.scene.collection.children.link(c) before = set(bpy.data.objects) bpy.ops.import_scene.gltf(filepath=str(path)) for o in set(bpy.data.objects) - before: for old in list(o.users_collection): old.objects.unlink(o) c.objects.link(o) return c # coll = import_ai3d(OUT / "chair.glb") ``` ### 6. Voxel remesh — главный шаг для Shap-E Marching cubes даёт треугольный меш с рваным рельефом. Пересборка вокселами убирает шум сильнее, чем любой Decimate: ```python import bpy obj = next(iter(bpy.data.collections["AI3D"].objects)) bpy.ops.object.select_all(action="DESELECT") obj.select_set(True) bpy.context.view_layer.objects.active = obj # Voxel Remesh работает в режиме Object и требует замкнутой поверхности obj.data.remesh_voxel_size = 0.01 # в метрах объекта; меньше — детальнее и тяжелее bpy.ops.object.voxel_remesh() print("после remesh:", len(obj.data.polygons), "полигонов") sm = obj.modifiers.new("Smooth", "SMOOTH") sm.factor = 0.5 sm.iterations = 3 ``` > [!NOTE] > `remesh_voxel_size` — единственный параметр, который реально настраивают; подбирай его под масштаб объекта. Если меш «рассыпался», сначала проверь, что он замкнут: `remesh` требует watertight-поверхность, а marching cubes на плоских промптах даёт дырки. ### 7. Приводим к приличному виду ```python import bpy from mathutils import Vector smooth_ops = [o for o in dir(bpy.ops.object) if "smooth" in o or "auto" in o] print("smoothing ops:", smooth_ops) # shade_smooth_by_angle в 4.1+, shade_auto_smooth в 4.0 dec = obj.modifiers.new("Decimate", "DECIMATE") dec.ratio = 0.4 bpy.ops.object.shade_smooth() bpy.context.view_layer.update() if obj.dimensions.z > 0: s = 0.45 / obj.dimensions.z # посадить на реальный размер предмета obj.scale = (s, s, s) bpy.context.view_layer.update() ground = min((obj.matrix_world @ Vector(c)).z for c in obj.bound_box) obj.location.z -= ground print("итог:", tuple(round(v, 3) for v in obj.dimensions)) ``` ### 8. Point-E — только если совсем надо ```bash git clone https://github.com/openai/point-e.git cd point-e python -m venv .venv && .venv\Scripts\activate # Linux: source .venv/bin/activate pip install -e . ``` Point-E — предшественник Shap-E: ниже разрешение, меньше контроля над результатом, вывод — облако точек. Практический смысл один: если Shap-E не ставится по зависимостям, Point-E иногда ставится, и на нём можно хотя бы проверить идею формы. ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```bash # Список сэмпл-скриптов и их флагов — единственный честный источник имён ls *.py python sample_text_to_3d.py --help ``` | Параметр | Значение | Когда менять | |---|---|---| | `torch.manual_seed` | `42` | без него результат невоспроизводим | | `render_coarse_to_fine` | грубое → точное | грубое — быстрый превью-меш | | `remesh_voxel_size` | `0.01` м | масштабируй пропорционально размеру объекта | | `Smooth factor` / `iterations` | `0.5` / `3` | артефакты — увеличивай, форма плывёт — уменьшай | | `dec.ratio` | `0.4` | после remesh, не раньше | | базовая vs текстурная модель | базовая — быстрее | текстурная весит заметно больше | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | `ModuleNotFoundError: shap_e` | пакет не установлен / не тот Python | `pip install -e .` в активированном venv | | Веса не нашлись по имени | файл переименовали | `ls shap_e/assets/` и подставь реальное имя | | Каждый запуск даёт разный меш | не зафиксирован seed | `torch.manual_seed` + `np.random.seed` | | Меш в дырках, remesh падает | поверхность не замкнута | проверь замкнутость, увеличь `remesh_voxel_size` | | `KeyError` на аргументах рендера | сигнатура коммита другая | `python sample_text_to_3d.py --help`, потом пример из README | | Remesh «съел» объект | воксель крупнее геометрии | уменьши `remesh_voxel_size` вдвое | | Очень долго на CPU | базовая модель без GPU | поставь CUDA-сборку PyTorch, если есть карта | | В Blender импортируется пусто | экспорт прошёл в OBJ, а импортируем как GLB | проверь расширение и формат в `bpy.ops.import_scene` | | `bpy.ops.object.shade_smooth` отсутствует | не объектный режим | `bpy.ops.object.mode_set(mode="OBJECT")` перед вызовом | ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 42: TRIPO AI API](setup_42_tripo_ai_api.md), [SETUP 43: MESHY API V4](setup_43_meshy_api_v4.md), [SETUP 44: RODIN / HYPER3D GEN](setup_44_rodin_hyper3d.md), [SETUP 45: HUNYUAN3D 2.1 LOCAL](setup_45_hunyuan3d_21.md), [SETUP 47: ZERO123 / INSTANTMESH](setup_47_zero123_instantmesh.md) - **По темам:** [SETUP 48: GAUSSIAN SPLATTING → BLENDER](setup_48_gaussian_splatting.md), [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md) - **Теги:** #ai-3d, #setup --- # SETUP 47: ZERO123 / INSTANTMESH Tags: #ai-3d #setup # SETUP 47: ZERO123 / INSTANTMESH **ID:** SETUP-47 | **Type:** Multi-view 3D | **Difficulty:** ⭐⭐⭐ | **Time:** 1.5-2 h > Когда применять: когда есть одна фотография объекта и нужен текстурированный меш — а облачный API нельзя или не хочется. Цепочка: диффузионная модель дорисовывает ракурсы, InstantMesh собирает из них геометрию. ## 🎯 ЦЕЛЬ Zero123 / Zero123++ превращают одно фото в набор новых ракурсов. InstantMesh берёт эти ракурсы (или одно фото и делает ракурсы сам) и восстанавливает текстурированный меш за минуты на GPU. Настраиваем локальный запуск обеих частей и разбираем главную проблему: качество результата определяется не моделью, а согласованностью промежуточных ракурсов. Если они «разъезжаются» — меш будет сломан, и никакая чистка в Blender это не спасёт. > [!NOTE] > У Zero123 есть несколько репозиториев и весов (оригинальная работа «Zero-1-to-3» и улучшенные варианты). Имена скриптов, флаги и сами веса отличаются от репозитория к репозиторию. Прежде чем запускать, открой README того репозитория, который ставишь, и снимай имена оттуда. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Ориентир | Зачем | |---|---|---| | GPU | 16 ГБ VRAM | InstantMesh в дефолтных настройках; на 12 ГБ — со снижением разрешения | | CUDA | под твою карту | иначе всё уходит в CPU и часы вместо минут | | Диск | 15-25 ГБ | веса обеих моделей | | Python | 3.8-3.10 (репозитории старые) | свежий Python часто ломает зависимости | | Blender | 4.x LTS | импорт и чистка | | `trimesh` | для проверки геометрии | диагностика до импорта | > [!NOTE] > Требования по VRAM зависят от настроек. Ориентир «16 ГБ» — то, на чём InstantMesh стабильно отрабатывает дефолтные параметры; на 12 ГБ приходится резать разрешение входа и число итераций. Точные значения смотри в README репозитория. ## 🛠️ НАСТРОЙКА ### 1. Готовим референс ```python from pathlib import Path from PIL import Image SRC = Path(r"C:/assets/refs/chair.jpg") OUT_DIR = Path(r"C:/assets/ai3d/zero123"); OUT_DIR.mkdir(parents=True, exist_ok=True) img = Image.open(SRC).convert("RGB") # Модели ждут квадрат: центрируем кропом, а не ресайзом — иначе пропорции поплывут side = min(img.size) left, top = ((img.width - side) // 2, (img.height - side) // 2) img = img.crop((left, top, left + side, top + side)) img = img.resize((512, 512), Image.LANCZOS) img.save(OUT_DIR / "ref.png") print("референс:", img.size, "исходник:", Image.open(SRC).size) ``` > [!NOTE] > 512×512 — типичный вход для Zero123-подобных моделей. Некоторые варианты принимают 256. Точный допустимый размер выводит модель при загрузке — посмотри лог, а не угадывай. ### 2. Ставим Zero123 / Zero123++ ```bash git clone <репозиторий zero123> && cd zero123 python -m venv .venv # Windows: .venv\Scripts\activate # Linux/macOS: # source .venv/bin/activate pip install -r requirements.txt # Дальше смотри README репозитория: имя скрипта генерации ракурсов и его флаги ls *.py ``` ```bash # Проверяем GPU до того, как потратим час на установку python -c "import torch; print(torch.__version__, torch.cuda.is_available(), torch.cuda.get_device_name(0) if torch.cuda.is_available() else '')" ``` ### 3. Генерируем ракурсы ```bash # Типовая форма вызова: скрипт с промптом, числом ракурсов и папкой вывода. # Имя скрипта и точные флаги бери из README того репозитория, который клонировал. python <скрипт_генерации_ракурсов>.py \ --image ../ref.png \ --num-views 6 \ --output_dir ../views ``` ```python # Проверяем результат ДО того, как отдавать его InstantMesh from pathlib import Path import os from PIL import Image VIEWS = Path(r"C:/assets/ai3d/zero123/views") files = sorted(VIEWS.glob("*.png")) print("ракурсов:", len(files)) # Кадры должны быть одинакового размера и без чёрных/пустых — иначе InstantMesh их выкинет for f in files: im = Image.open(f) print(f.name, im.size, im.mode, "OK" if im.size[0] == im.size[1] and im.size[0] >= 256 else "ПРОБЛЕМА") ``` ### 4. Ставим InstantMesh ```bash git clone https://github.com/tencent/InstantMesh.git && cd InstantMesh python -m venv .venv # Windows: .venv\Scripts\activate # Linux/macOS: # source .venv/bin/activate pip install -r requirements.txt ``` ### 5. Собираем меш ```bash # Флаги читай в README: версии репозитория отличаются набором ключей bash run.sh inputs/case_rendering outputs/case # вариант с готовым набором ракурсов # или python run.py inputs/case.png outputs/case # вариант с одиночным изображением ``` ```python # Второй вариант существует и часто даёт лучший результат, чем ручные ракурсы: # модель сама дорисовывает вид. Сравни оба выхода. ``` ### 6. Проверяем геометрию до импорта ```python import trimesh from pathlib import Path MESH = Path(r"C:/assets/ai3d/zero123/outputs/case/mesh.obj") scene = trimesh.load(str(MESH), force="scene") geoms = list(scene.geometry.values()) print("мешей:", len(geoms)) for name, g in scene.geometry.items(): print(name, "verts:", len(g.vertices), "faces:", len(g.faces), "watertight:", g.is_watertight) ``` ### 7. Импорт в Blender и чистка ```python import bpy from pathlib import Path from mathutils import Vector def import_mesh(path: Path, coll="AI3D") -> bpy.types.Collection: c = bpy.data.collections.get(coll) or bpy.data.collections.new(coll) if c.name not in bpy.context.scene.collection.children: bpy.context.scene.collection.children.link(c) before = set(bpy.data.objects) bpy.ops.wm.obj_import(filepath=str(path)) # Blender 3.3+: C++-импортёр OBJ for o in set(bpy.data.objects) - before: for old in list(o.users_collection): old.objects.unlink(o) c.objects.link(o) return c # coll = import_mesh(MESH) # obj = next(iter(bpy.data.collections["AI3D"].objects)) # print(len(coll.objects), "объектов |", len(obj.data.polygons), "полигонов") bpy.ops.object.select_all(action="DESELECT") obj = bpy.context.view_layer.objects.active or next(iter(bpy.data.collections["AI3D"].objects)) obj.select_set(True) bpy.context.view_layer.objects.active = obj # Выборочное удаление мусорных граней, если меш «оброс» шумом bpy.ops.object.mode_set(mode="EDIT") bpy.ops.mesh.select_all(action="SELECT") bpy.ops.mesh.select_non_manifold() bpy.ops.mesh.delete(type="VERT") bpy.ops.object.mode_set(mode="OBJECT") # Масштаб и посадка на пол: без этого объект уезжает на 30 метров вверх bpy.context.view_layer.update() if obj.dimensions.z > 0: s = 0.5 / obj.dimensions.z obj.scale = (s, s, s) bpy.context.view_layer.update() obj.location.z -= min((obj.matrix_world @ Vector(c)).z for c in obj.bound_box) smooth_ops = [o for o in dir(bpy.ops.object) if "smooth" in o] print("итог:", tuple(round(v, 3) for v in obj.dimensions), "| smoothing ops:", smooth_ops) ``` > [!NOTE] > Удаление non-manifold-граней лечит типичную болезнь этой цепочки — «пузыри» и дырки от несогласованных ракурсов. Но если ракурсы разъехались принципиально, геометрия будет «смята» впустую: тогда правь вход, а не меш. ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```bash # Пакетно: 20 референсов через один запуск python <скрипт_ракурсов>.py --image ref.png --num-views 6 --output_dir views/i%03d ``` | Параметр | Значение | Когда менять | |---|---|---| | входной размер | `512×512` | 256 для быстрой проверки идеи | | `--num-views` | `6` | 4 хватит для простых форм, 8+ для сложных | | InstantMesh вход | папка ракурсов ИЛИ одно фото | сравни оба, результат разный | | `remesh_voxel_size` | не применяем | меши уже достаточно чистые | | `dec.ratio` | `0.4–0.6` | только если полигонов слишком много | | seed | фиксировать | без seed повторный запуск даст другой меш | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | `ImportError` на torchao / разных версиях torch | репозитории старые, конфликт версий | отдельный venv на каждый репозиторий, не смешивать | | Всё считается на CPU и очень долго | нет CUDA-сборки torch | поставь torch с CUDA под свою карту, проверь `torch.cuda.is_available()` | | Меш «смят» в гармошку | ракурсы геометрически несогласованы | подай в InstantMesh одно фото, чтобы он дорисовал виды сам | | InstantMesh падает на входе | кадры разного размера или есть пустые | проверь лог шага 3, перегенерируй | | Текстура размытая на рёбрах | мало ракурсов | увеличь `--num-views` до 8 | | UV перекрываются | реконструкция не гарантирует атлас | `bpy.ops.uv.smart_project()` | | `wm.obj_import` не найден | версия старше 3.3 | используй `bpy.ops.import_scene.obj` | | Меш в километрах | нет нормализации | прогони шаг 8 | | Веса скачались не полностью | оборвалась загрузка | сверь размер с ожидаемым из README | ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 42: TRIPO AI API](setup_42_tripo_ai_api.md), [SETUP 43: MESHY API V4](setup_43_meshy_api_v4.md), [SETUP 44: RODIN / HYPER3D GEN](setup_44_rodin_hyper3d.md), [SETUP 45: HUNYUAN3D 2.1 LOCAL](setup_45_hunyuan3d_21.md), [SETUP 46: SHAP-E / POINT-E LOCAL](setup_46_shap_e_local.md) - **По темам:** [SETUP 48: GAUSSIAN SPLATTING → BLENDER](setup_48_gaussian_splatting.md), [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md) - **Теги:** #ai-3d, #setup --- # SETUP 48: GAUSSIAN SPLATTING → BLENDER Tags: #ai-3d #blender #setup # SETUP 48: GAUSSIAN SPLATTING → BLENDER **ID:** SETUP-48 | **Type:** 3DGS | **Difficulty:** ⭐⭐⭐ | **Time:** 1.5-2 h > Когда применять: когда есть 3DGS-сцена (.ply на миллион сплатов) и нужно либо посмотреть её, либо частично перенести в Blender. Главное предупреждение сразу: **штатный Blender не рендерит 3DGS**, и миллион гауссианов в сцену не засунуть — дальше решается, что тебе на самом деле нужно. ## 🎯 ЦЕЛЬ 3D Gaussian Splatting хранит сцену как набор анизотропных эллипсоидов с цветом, прозрачностью и ковариациями. Blender про это понятия не имеет: у него полигоны. Поэтому статья разбирает три честных пути: (1) смотреть сплаты во внешнем просмотрщике и композить результат, (2) привести сплаты к мешу специализированным инструментом, (3) построить в Blender инстансированный меш из подмножества сплатов — для отсева фона, кадрирования и анализа, не для финального рендера. > [!NOTE] > Про конвертацию в `.mesh` через SuGaR и про веса `.ply` от нейросетевых реконструкций я говорю обобщённо: репозиторий SuGaR (Anttwo) действительно оптимизирует сплаты в меш с нормалями, но конкретные имена его скриптов и флагов я не привожу — сверяй README. Формат самого `.ply` для 3DGS стандартен, и ниже он разобран явно. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | `numpy` | 1.24+ | разбор `.ply` | | Blender | 4.x LTS | инстансы и композитинг | | Просмотрщик сплатов | — | посмотреть сцену целиком (см. шаг 1) | | 8+ ГБ VRAM | — | если рендеришь через GPU-бэкенд | | SuGaR (опционально) | — | честная конвертация сплатов в меш | ## 🛠️ НАСТРОЙКА ### 1. Сначала посмотри, не трогая Blender Прежде чем конвертировать, убедись, что сцена вообще стоит того: ```bash # Просмотрщик в браузере (PlayCanvas SuperSplat) или десктопный — на твой выбор. # Суть: открыть .ply и посмотреть ракурсы. # Суперсплат нужен для инспекции, а не для продакшн-рендера. echo "scene: C:/assets/gs/scene/point_cloud/iteration_30000/point_cloud.ply" ``` > [!NOTE] > Не подставляй сюда конкретные URL приложений: набор просмотрщиков меняется, и часть из них живёт как веб-приложения без стабильного API. Практический путь: найди актуальный просмотрщик 3DGS (их несколько, включая браузерные), открой им `.ply`. Ссылку на инструмент бери из репозитория, из которого ты получил `.ply`. ### 2. Читаем `.ply` 3DGS напрямую Формат — бинарный little-endian PLY, где каждая вершина несёт SH-коэффициенты, логитмасштаб, сигмоид прозрачности и кватернион. Читаем без посредников: ```python import numpy as np from pathlib import Path PLY = Path(r"C:/assets/gs/scene/point_cloud/iteration_30000/point_cloud.ply") def read_gsplat_ply(path: Path): with open(path, "rb") as f: header = [] while True: line = f.readline() if not line: raise ValueError("PLY header not terminated") s = line.decode("ascii").strip() header.append(s) if s == "end_header": break n, props, in_vertex = 0, [], False for s in header[1:]: t = s.split() if not t: continue if t[0] == "element": in_vertex = t[1] == "vertex" if in_vertex: n = int(t[2]) elif t[0] == "property" and in_vertex: props.append(t[-1]) dtype = np.dtype([(name, " [!NOTE] > Парсер выше рассчитан на бинарный little-endian PLY, где все вершинные свойства — `float32`. Если попался ASCII-PLY или счётчик граней (`element face`), он не сработает — сначала напечатай первые строки заголовка `print(header)` и адаптируй. Конвертеры, которые пишут сплаты, почти всегда делают бинарный вариант. ### 3. Раскладываем данные в понятные поля ```python # Константа SH нулевого порядка: 0.5 * sqrt(1/pi) SH_C0 = 0.28209479177387814 xyz = np.stack([data["x"], data["y"], data["z"]], axis=1) rgb = 0.5 + SH_C0 * np.stack([data["f_dc_0"], data["f_dc_1"], data["f_dc_2"]], axis=1) alpha = 1.0 / (1.0 + np.exp(-data["opacity"])) # сигмоид scale = np.exp(np.stack([data["scale_0"], data["scale_1"], data["scale_2"]], axis=1)) quat = np.stack([data["rot_0"], data["rot_1"], data["rot_2"], data["rot_3"]], axis=1) print("bounds:", xyz.min(axis=0).round(2), xyz.max(axis=0).round(2)) print("alpha mean:", float(alpha.mean()), "| scale median:", float(np.median(scale))) print("прозрачных (a<0.05):", int((alpha < 0.05).sum())) ``` ### 4. Отсекаем фон — это главный практический шаг ```python keep = alpha > 0.05 xyz, rgb, scale, quat = xyz[keep], rgb[keep], scale[keep], quat[keep] print("после отсева:", len(xyz)) # Дополнительно: отсечь по расстоянию от камеры/центра center = xyz.mean(axis=0) radius = np.linalg.norm(xyz - center, axis=1) keep2 = radius < np.percentile(radius, 98) xyz, rgb, scale, quat = xyz[keep2], rgb[keep2], scale[keep2], quat[keep2] print("после отсева по радиусу:", len(xyz), "| центр:", center.round(2)) ``` ### 5. Строим инстансы в Blender Один икосфер, миллион связанных копий — Blender не потянет. Работаем с подмножеством в десятки тысяч и режем дальше: ```python import bpy from mathutils import Vector, Quaternion MAX_SPLATS = 30000 # потолок: выше Blender встанет sel = np.linspace(0, len(xyz) - 1, min(MAX_SPLATS, len(xyz)), dtype=int) xyz, rgb, scale, quat = xyz[sel], rgb[sel], scale[sel], quat[sel] bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=1, radius=1.0) proto = bpy.context.object proto.name = "SplatProto" proto.data.name = "SplatProtoMesh" mat = bpy.data.materials.new("SplatColor") mat.use_nodes = True nt = mat.node_tree info = nt.nodes.new("ShaderNodeObjectInfo") # читает object.color emiss = nt.nodes.new("ShaderNodeEmission") out = nt.nodes.new("ShaderNodeOutputMaterial") nt.links.new(info.outputs["Color"], emiss.inputs["Color"]) nt.links.new(emiss.outputs["Emission"], out.inputs["Surface"]) proto.data.materials.append(mat) print("прототип готов, сплатов к инстансированию:", len(xyz)) ``` ### 6. Раскладываем инстансы ```python coll = bpy.data.collections.get("GSPLATS") or bpy.data.collections.new("GSPLATS") scene_coll = bpy.context.scene.collection if coll.name not in scene_coll.children: scene_coll.children.link(coll) for i in range(len(xyz)): dup = bpy.data.objects.new(f"s_{i:06d}", proto.data) # shared mesh = дёшево по памяти coll.objects.link(dup) p = xyz[i] dup.location = (float(p[0]), float(p[1]), float(p[2])) dup.rotation_mode = "QUATERNION" # в .ply порядок кватерниона (w, x, y, z) dup.rotation_quaternion = Quaternion((float(quat[i][0]), float(quat[i][1]), float(quat[i][2]), float(quat[i][3]))) s = scale[i] dup.scale = (float(s[0]), float(s[1]), float(s[2])) c = np.clip(rgb[i], 0.0, 1.0) dup.color = (float(c[0]), float(c[1]), float(c[2]), 1.0) print("инстансов:", len(coll.objects)) ``` > [!NOTE] > Это **визуализация**, а не рендер сплатов: у эллипсоидов нет анизотропного сплаттинга и альфа-смешивания, поэтому картинка будет грубой. Зато видно форму, отсеивается фон и можно работать с камерой. Для финального изображения нужен нормальный просмотрщик сплатов. ### 7. Экспорт подмножества в обычный меш ```python # Из инстансов можно получить обычный меш: объединяем и применяем модификаторы bpy.ops.object.select_all(action="DESELECT") for o in bpy.data.collections["GSPLATS"].objects: o.select_set(True) bpy.context.view_layer.objects.active = proto bpy.ops.object.join() merged = bpy.context.object merged.name = "GSplatMesh" bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) # Разумный полигон-бюджет: 30k сплатов по ~80 граней — это уже миллионы dec = merged.modifiers.new("Decimate", "DECIMATE") dec.ratio = 0.01 bpy.ops.object.modifier_apply(modifier=dec.name) print("после объединения:", len(merged.data.polygons), "полигонов") ``` ### 8. Честная конвертация через SuGaR ```bash # Репозиторий SuGaR оптимизирует сплаты в меш с нормалями — это настоящая конвертация, # а не инстансинг. Клонируй, ставь по README, имя скрипта и флаги смотри там. git clone https://github.com/Anttwo/SuGaR.git ``` > [!NOTE] > Внутренние имена скриптов SuGaR и набор флагов я не привожу: они менялись, а выдуманная команда хуже отсутствующей. Открой README репозитория и следуй описанному там запуску. Главное, что стоит запомнить: результат SuGaR — обычный меш, пригодный для рендера в Blender. ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```bash # Сколько сплатов в сцене — печатаем, а не гадаем python -c "import numpy as np;from pathlib import Path;p=Path(r'C:/assets/gs/point_cloud.ply');d=p.read_bytes();print('size MB:', round(len(d)/2**20,1))" ``` | Параметр | Значение | Когда менять | |---|---|---| | `MAX_SPLATS` | `30000` | выше — Blender начнёт тормозить и падать | | порог `alpha` | `0.05` | выше — чище фон, теряются полупрозрачные детали | | отсев по радиусу | `98` перцентиль | обрезает «хвосты» на краях сцены | | `SH_C0` | `0.28209479177387814` | константа, не параметр | | `subdivisions` прототипа | `1` | `2` — глаже, но втрое больше граней | | `dec.ratio` при join | `0.01` | подбирай под бюджет полигонов | | пропорции сплата | прямо из `exp(scale_*)` | не подгоняй вручную — иначе потеряется форма | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | PLY не читается, 0 вершин | ASCII-PLY или не тот формат | напечатай `print(header)`, проверь `format binary_little_endian` | | Цвета «кислотные» | не применил формулу SH→RGB | `rgb = 0.5 + SH_C0 * f_dc`, а не сырое `f_dc` | | Всё прозрачное | забыт сигмоид на прозрачности | `alpha = sigmoid(opacity)` | | Blender зависает при скрипте | слишком много объектов | срежь `MAX_SPLATS` до 10-20k | | Один сплат = гигантский эллипсоид | не применил `exp()` к scale | хранится логитм, в сцену идёт экспонента | | Повёрнутые сплаты «не туда» | порядок кватерниона | в 3DGS `rot_*` = (w, x, y, z) | | Текстуры на месте, объект не видно | меши связаны, но материал не задан | `ShaderNodeObjectInfo → Color` в Emission | | `.ply` весит 4 ГБ, RAM не хватает | читаешь всё в память | читай подмножество: сдвиг в заголовке + `np.fromfile(offset=...)` | | Хочу финальный рендер из Blender | сплаты не рендерятся нативно | SuGaR-конвертация или рендер во внешнем просмотрщике | ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 45: HUNYUAN3D 2.1 LOCAL](setup_45_hunyuan3d_21.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 07: CI/CD INTEGRATION](setup_07_ci_cd.md), [SETUP 09: BLENDERBOT / AI CODING AGENT](setup_09_blenderbot_ai.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 10: BLENDER + COMPOSITING (NUKE/AFTER EFFECTS ROUNDTRIP)](setup_10_compositing_roundtrip.md), [SETUP 11: BLENDER + TOUCHDESIGNER REALTIME VISUALS](setup_11_touchdesigner.md) - **Теги:** #ai-3d, #blender, #setup --- # SETUP 49: FFMPEG + VSE PIPELINE Tags: #pipeline #setup #video # SETUP 49: FFMPEG + VSE PIPELINE **ID:** SETUP-49 | **Type:** Video | **Difficulty:** ⭐⭐ | **Time:** 40 min > Когда применять: когда нужно доделать монтаж и отдать клиенту файл, а Blender используется как VSE: сборка стрипов, прокси, а потом финальное кодирование через ffmpeg с правильными CRF, битрейтом и метаданными цвета. ## 🎯 ЦЕЛЬ Blender умеет монтировать (Video Sequence Editor), но не лучший кодировщик под битрейт-контроль. Расклаиваем задачу на две части: VSE собирает таймлайн, прокси и финальную сборку; ffmpeg делает превью, инспекцию потоков, перекодирование в H.264/H.265 с заданным CRF и выгрузку мастер-файла. Отдельно разбираем HDR-метаданные — там главная ошибка в том, что перекодируешь в 8 бит и теряешь диапазон, даже если в команде написано «HDR». ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Blender | 4.x LTS | VSE | | ffmpeg / ffprobe | 4.x-7.x | кодирование и инспекция | | Прокси-директория | отдельная папка кэша | иначе VSE будет читать тяжёлые исходники | | Исходники | EXR-серия или ProRes | для HDR — только 10 бит | | Диск | 2-3× размер исходников | прокси + мастер + превью | ## 🛠️ НАСТРОЙКА ### 1. Проверяем, что ffmpeg доступен и что умеет ```bash ffmpeg -version ffprobe -version ffmpeg -encoders | grep -E "libx264|libx265|prores" ``` ### 2. Создаём VSE-редактор и кладём стрипы ```python import bpy from pathlib import Path SRC = Path(r"C:/edit/sources") scene = bpy.context.scene se = scene.sequence_editor_create() # В 4.4+ коллекция называется strips, раньше — sequences. Берём то, что есть. coll = se.strips if hasattr(se, "strips") else se.sequences print("API:", "strips" if hasattr(se, "strips") else "sequences") clips = sorted(SRC.glob("shot_*.mov")) for i, p in enumerate(clips): strip = coll.new_movie(name=p.stem, filepath=str(p), channel=1, frame_start=i * 48 + 1) print(strip.name, strip.frame_final_duration, "кадров") ``` > [!NOTE] > Переход `sequences` → `strips` в Blender 4.4 — ломающий для скриптов. Поэтому код выше сначала проверяет наличие атрибута, а не зашивает имя. Если пишешь скрипт под конкретную версию — зафиксируй её, иначе он сломается на следующем обновлении. ### 3. Кроссфейд между клипами ```python a = coll.strips["shot_001"] if hasattr(coll, "strips") else coll.sequences["shot_001"] b = coll.strips["shot_002"] if hasattr(coll, "strips") else coll.sequences["shot_002"] bl = se.strips if hasattr(se, "strips") else se.sequences bl.new_effect(type="CROSS", name="xf_001_002", channel=3, frame_start=a.frame_final_end - 12, frame_end=a.frame_final_end) # Реальный кроссфейд = смещение второго клипа и его прозрачности b.frame_start -= 12 b.frame_offset_start = 0 print("кроссфейд на кадрах:", a.frame_final_end - 12, "-", a.frame_final_end) ``` ### 4. Текстовая плашка ```python text = bl.new_effect(type="TEXT", name="slate", channel=4, frame_start=1, frame_end=48) text.text = "SHOT 01 / TAKE 2" print("text strip:", text.name, text.frame_final_duration) ``` ### 5. Прокси ```python scene.render.resolution_x, scene.render.resolution_y = 1920, 1080 scene.render.resolution_percentage = 50 scene.render.use_proxy = True # Каталог прокси задаётся в настройках Blender (Preferences -> File Paths -> # Temporary Files) и/или через оператор ниже — имя оператора менялось между версиями ops = [o for o in dir(bpy.ops.sequencer) if "proxy" in o] print("proxy ops:", ops) ``` > [!NOTE] > Набор операторов для прокси в VSE менялся: в разных версиях это `rebuild_proxy` / `proxy_build` / пересборка при импорте. Поэтому код печатает доступные имена, а не вызывает наугад. Включай `use_proxy` до добавления стрипов — иначе прокси не построятся. ### 6. Цвет и биты ```python scene = bpy.context.scene # 8 бит для SDR, 16 для HDR-последовательностей scene.render.image_settings.color_depth = "16" scene.render.image_settings.file_format = "OPEN_EXR" print("view transform:", scene.view_settings.view_transform, "| look:", scene.view_settings.look) # Стрипы читаются в своём colorspace — неверный здесь = мыло или пересветы for s in (scene.sequence_editor.strips if hasattr(scene.sequence_editor, "strips") else scene.sequence_editor.sequences): if s.type == "MOVIE": print(s.name, "->", s.colorspace_settings.name) ``` ### 7. Рендер сиквенции из Blender ```python scene.render.resolution_percentage = 100 scene.render.image_settings.file_format = "FFMPEG" ff = scene.render.ffmpeg print("format:", [i.identifier for i in type(ff).bl_rna.properties["format"].enum_items]) print("codec:", [i.identifier for i in type(ff).bl_rna.properties["codec"].enum_items]) print("crf:", [i.identifier for i in type(ff).bl_rna.properties["constant_rate_factor"].enum_items]) ``` ```bash # В фоне: кодирование всей секвенции силами Blender blender -b edit.blend -a ``` ### 8. Превью через ffmpeg ```bash ffmpeg -i master.mov -vf "fps=24,scale=1280:-2" -c:v libx264 -crf 23 -preset veryfast \ -c:a aac -b:a 128k preview.mp4 ``` ### 9. Мастер и раздача ```bash # ProRes 422 HQ — мастер без поколений: переживёт следующую перекодировку без потерь ffmpeg -i master.mov -c:v prores_ks -profile:v 3 -pix_fmt yuv422p10le -c:a pcm_s16le master_prores.mov # H.264 — раздача ffmpeg -i master.mov -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p \ -movflags +faststart -c:a aac -b:a 192k delivery_h264.mp4 # H.265 — меньше весит, хуже совместимость ffmpeg -i master.mov -c:v libx265 -crf 20 -preset slow -pix_fmt yuv420p \ -tag:v hvc1 delivery_h265.mp4 ``` ### 10. HDR-метаданные ```bash # Пометить как BT.2020 PQ без пережатия потока ffmpeg -i hdr.mov -c copy \ -color_primaries bt2020 -color_trc smpte2084 -colorspace bt2020nc \ -color_range tv hdr_tagged.mov # Проверить, что реально лежит внутри — всегда сверяй, а не верь флажкам ffprobe -v error -select_streams v:0 -show_entries \ stream=pix_fmt,color_space,color_transfer,color_primaries,bits_per_raw_sample \ -of default=noprint_wrappers=1 hdr_tagged.mov ``` > [!NOTE] > Строгое соответствие BT.2020 + PQ требует 10 бит. Если исходник 8-битный, теги будут правдой только наполовину: диапазон уже потерян при съёмке или конвертации. Проверяй `bits_per_raw_sample` в выводе `ffprobe` до, а не после того, как отдал файл. ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```bash # Превью-стрип 1:1, чтобы поймать битые кадры до полного рендера ffmpeg -i master.mov -vf "select='eq(n\,0)+not(mod(n\,48))'" -vsync vfr check_sheet_%03d.png ``` | Параметр | Значение | Когда менять | |---|---|---| | `-crf` | `18` раздача / `23` превью | ниже — больше файл, выше — артефакты | | `-preset` | `slow` финал / `veryfast` превью | влияет только на скорость и размер | | `-pix_fmt` | `yuv420p` для совместимости | `yuv422p10le` для мастеров и HDR | | `prores_ks -profile:v` | `3` = 422 HQ | `0` = Proxy для черновиков | | `-movflags +faststart` | для mp4 в вебе | без него не будет прогрессивного стрима | | `color_depth` в Blender | `16` для HDR | `8` для SDR хватает и быстрее | | `resolution_percentage` | `50` для прокси | `100` на финале | | `-b:a` | `192k` AAC | `-c:a copy` если исходник уже приемлем | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | `AttributeError: 'SequenceEditor' has no 'strips'` | версия до 4.4 | используй `sequences`; код с проверкой `hasattr` работает в обоих | | VSE тормозит при скрабе | прокси не построены | `render.use_proxy = True` **до** импорта стрипов | | Цвета мылят в VSE | неверный colorspace у стрипа | `strip.colorspace_settings.name` под реал. формат | | HDR-ролик после ffmpeg серый | 10 бит превратились в 8 | `-pix_fmt yuv422p10le`, проверь `bits_per_raw_sample` | | mp4 не играется в браузере | нет faststart | `-movflags +faststart` | | Звук рассинхронизирован | ffmpeg пережал аудио вместе с видео | `-c:a copy` для черновика, `-af aresample=async=1` если ресемплишь | | Прерывистый прогресс / не играется вовсе | H.265 без `hvc1` | добавь `-tag:v hvc1` | | «Файл повреждён» в монтажке | прерванная запись | `ffmpeg -i bad.mov -c copy fixed.mov` | | Blender не кодирует в ProRes | зависит от сборки ffmpeg в Blender | кодируй через системный ffmpeg, Blender отдай PNG/EXR-серию | | Клипы в разных FPS | разные исходники | приведи к одному FPS до сборки, иначе таймлайн разъедется | ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 08: ASSET PIPELINE](setup_08_asset_pipeline.md), [SETUP 14: BLENDER + GODOT PIPELINE](setup_14_godot_pipeline.md), [SETUP 38: FAB.COM (EPIC) PIPELINE](setup_38_fab_epic.md), [SETUP 45: HUNYUAN3D 2.1 LOCAL](setup_45_hunyuan3d_21.md), [SETUP 50: DAVINCI RESOLVE ROUNDTRIP](setup_50_davinci_roundtrip.md) - **По темам:** [SETUP 55: AI UPSCALE PIPELINE](setup_55_ai_upscale_pipeline.md), [SETUP 59: WEB VIEWER EXPORT](setup_59_web_viewer_export.md), [CASE 02: GAME PROPS PIPELINE](../cases/case_02_game_props.md) - **Теги:** #pipeline, #setup, #video --- # SETUP 50: DAVINCI RESOLVE ROUNDTRIP Tags: #setup #video # SETUP 50: DAVINCI RESOLVE ROUNDTRIP **ID:** SETUP-50 | **Type:** Color/Edit | **Difficulty:** ⭐⭐ | **Time:** 25 min **RU/EN Bilingual** > Когда применять: когда рендер из Blender нужно отдать в DaVinci Resolve на монтаж > или грейд — и вернуть результат обратно в 3D, не потеряв HDR-диапазон. --- ## 🎯 ЦЕЛЬ Настроить честный roundtrip: Blender отдаёт линейные EXR, Resolve работает с ними как с исходниками, а назад возвращается либо готовый кадр, либо LUT. Главное в этой связке — не допустить, чтобы Blender «сварил» тон-маппинг в пиксели: как только выход display-referred, грейд в Resolve теряет оригинальные света и тени. Всё ниже проверено на Blender 5.2.1 LTS в headless-режиме (`blender -b`). --- ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Blender | 5.x (проверено 5.2.1 LTS) | Рендер линейной EXR-последовательности | | DaVinci Resolve | 17+ | Монтаж и грейд; RCM с ACES-рабочим пространством | | OpenColorIO | в комплекте Blender | Разбор EXR без пере-тега | | ffmpeg | 7.x+ (проверено 9.0.1) | Сборка превью, прокси, финальный рендер | | Диск | 2× объём EXR | Мастер + превью | --- ## 🛠️ НАСТРОЙКА ### 1. Настроить выход Blender так, чтобы не было двойной конвертации Смысл: `View Transform = Standard` оставляет линейные значения как есть (в пределах 0..1), а OCIO-конфиг Blender при сохранении EXR кладёт в файл рабочее пространство сцены. Если оставить AgX/Filmic, на диск уйдёт уже «после тона» картинка. ```python # export_plate_config.py — запускается как blender -b scene.blend -P export_plate_config.py import bpy sc = bpy.context.scene # 1) Никакого display-referred тон-маппинга: только трансформация Standard. # Идентификаторы view_transform проверены в Blender 5.2: # Standard, Raw, AgX, Filmic, Filmic Log, False Color, ACES 1.3, ACES 2.0, # Khronos PBR Neutral. sc.view_settings.view_transform = 'Standard' sc.view_settings.look = 'None' # 2) FOLLOW_SCENE = сохранить в линейном пространстве сцены (без ODT). # OVERRIDE = применить view_transform при сохранении — для превью, не для мастера. sc.render.image_settings.color_management = 'FOLLOW_SCENE' # 3) EXR 16 бит, RGB, ZIP. Для HDR-мастера берите 32 бит и PIZ/DWAB. sc.render.image_settings.file_format = 'OPEN_EXR' sc.render.image_settings.color_mode = 'RGB' sc.render.image_settings.color_depth = '16' sc.render.image_settings.exr_codec = 'ZIP' sc.render.filepath = "//exr/plate_" sc.render.resolution_x, sc.render.resolution_y = 3840, 2160 sc.render.use_file_extension = True ``` ### 2. Отрендерить плату кадрами, а не одним файлом Resolve читает EXR-последовательность, и только она: одиночный multiview-файл он не подхватит как клип. ```bash # Проверено: -b, -E CYCLES, -o, -s/-e, -a, -F (движок), --render-frame blender -b scene.blend \ -E CYCLES \ -o "//exr/plate_####" \ -s 1 -e 250 -j 1 \ -a ``` ### 3. Собрать превью для монтажа Монтажёр не должен открывать 250 EXR по 300 МБ. Собираем прокси в H.264: `-c:v libx264` с `-crf 18`, обязательный `-pix_fmt yuv420p` (иначе плеер в браузере/Resolve не покажет) и `-movflags +faststart` (moov-атом в начало — файл начинает играть, не дожидаясь конца). ```bash # Из EXR-последовательности в превью (команда проверена на ffmpeg 9.0.1) ffmpeg -y -framerate 24 -start_number 1 -i exr/plate_%04d.exr \ -c:v libx264 -crf 18 -pix_fmt yuv420p -movflags +faststart \ preview/plate_v001.mp4 ``` ### 4. Забрать монтаж обратно без EDL Проверка факта: в Blender 5.2 встроенных импортёров `import_scene` ровно два — `fbx` и `gltf`. Импорта EDL/AAF/FCPXML нет. ```python # Воспроизводим монтаж по списку кадров из CSV, который отдал монтажёр. # API Blender 5.x: у секвенсора коллекция называется strips, а не sequences, # и класс полосы — Strip, а не Sequence. Это переименование 5.x. import bpy, csv, os sc = bpy.context.scene se = sc.sequence_editor_create() CUTS = [("plate_0001.exr", 1), ("render_0120.exr", 90), ("plate_0180.exr", 160)] SRC = "//exr/" with open("//edit/cutlist.csv", newline="", encoding="utf-8") as fh: rows = list(csv.reader(fh))[1:] # первая строка — заголовок channel = 1 for name, start in CUTS: st = se.strips.new_image(name=name, filepath=os.path.join(SRC, name), channel=channel, frame_start=int(start)) # colorspace_settings.name — реальное свойство полосы; список имён # (ACEScg, ACEScct, Linear Rec.709, sRGB, Non-Color, ...) проверен. st.colorspace_settings.name = 'ACEScg' st.duration = 40 channel += 1 sc.frame_start, sc.frame_end = 1, 250 sc.render.use_sequencer = True # рендер пойдёт через секвенсор, а не через 3D ``` > [!NOTE] > У полосы есть и `frame_final_duration`, но в Blender 5.2 он помечен как > устаревший и будет удалён в 6.0 — пиши `strip.duration`. Проверено на 5.2.1. ### 5. Разобрать кадр в Resolve Ручные шаги, порядок важен: 1. **File → Import → File** → папка с `plate_####.exr` (Resolve соберёт клип-последовательность). 2. Правый клик по клипу → **Attributes** → Input Color Space: выберите то, чем помечен файл. Для линейного ACEScg — `ACEScg`; если Blender писал в Rec.709-linear — `Linear Rec.709`. 3. **Project Settings → Color Management**: включите RCM, Timeline Working Space — ACEScct или ACES 1.3, Output — тот же ACES или sRGB-видео под вашу доставку. 4. Только теперь грейд: без настроенного Color Management любой корректор работает вслепую. ### 6. Вернуть результат Два варианта, и выбор между ними — не технический, а организационный. ```bash # Вариант A: Resolve отдаёт готовый кадр — забираем EXR обратно в Blender # как Image-датаблок (bpy.data.images.load понимает EXR) и подкладываем в композитор. # Вариант B: Resolve отдаёт LUT (.cube) — см. предупреждение в разделе ошибок. # # Финальная сборка ProRes-мастера из отрендеренных кадров: ffmpeg -y -framerate 24 -start_number 1 -i comp/comp_%04d.exr \ -c:v prores_ks -profile:v 3 -pix_fmt yuv422p10le \ delivery/master_v001.mov ``` Единственный способ натянуть `.cube` на сам Blender — это OCIO: сам Blender использует `.cube` внутри своего конфига (файлы `AgX_Base_sRGB.cube` и подобные лежат рядом с `config.ocio`). Значит, кастомный view transform — штатный механизм, а не хак: ```yaml # myconfig.ocio — копия datafiles/colormanagement/config.ocio + свой блок. # Запуск: OCIO=myconfig.ocio blender scene.blend displays: sRGB: - ! - ! active_displays: [sRGB] active_views: [Luminance] views: - ! name: Resolve_Grade from_scene_reference: ! children: - ! {src: ACES2065-1, dst: ACEScg} - ! {src: grade_v001.cube, interpolation: tetrahedral} ``` ```python # После этого view transform появляется в UI Color Management # и в scene.view_settings.view_transform как 'Resolve_Grade'. import bpy bpy.context.scene.view_settings.view_transform = 'Resolve_Grade' ``` > [!NOTE] > Список полей YAML-конфига OCIO и имена его `!<...>` блоков проверяй по > документации OpenColorIO — формат там версионируется. Смысловую проверку > можно сделать так: если после `OCIO=... blender` в Color Management появился > новый transform с нужным именем, конфиг прочитан верно. --- ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```bash # Диагностика возможностей конкретной сборки Blender перед пайплайном: blender -b --python-expr "import bpy;print(bpy.app.build_options.codec_ffmpeg, bpy.app.build_options.opencolorio)" ``` | Параметр | Значение | Когда менять | |---|---|---| | `view_transform` | `Standard` | Мастер EXR. Любое другое значение = двойной тон-маппинг | | `image_settings.color_management` | `FOLLOW_SCENE` | Мастер. `OVERRIDE` — только для быстрых превью | | `image_settings.color_depth` | `16` | 32 — если HDR-света выше 1.0 нужны с точностью | | `exr_codec` | `ZIP` | `PIZ`/`DWAB` — если важнее скорость записи, чем размер | | `-crf` (ffmpeg) | `18` | Превью. `16` для финальной сдачи, `23`+ для черновиков | | `-pix_fmt yuv420p` | обязателен | Совместимость плееров; для 10 бит — `yuv420p10le` | | `-movflags +faststart` | включать | Веб-доставка и превью по сети | | `prores_ks -profile:v 3` | 422 HQ | Мастер для монтажа; `4444` — если нужен альфа-канал | --- ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | В Resolve картинка «выжата», света в 0, тени в 1 | Blender отрендерил с `AgX`/`Filmic`, а не `Standard` | Перерендерить мастер с `view_transform='Standard'`; превью держать в OVERRIDE | | Resolve не видит папку с EXR как один клип | Импортирован одиночный multiview-файл | Импортировать папку с последовательностью `####` | | `Import → EDL` отсутствует | В Blender 5.2 встроенных импортёров только `fbx` и `gltf` | Вести монтаж через CSV-список кадров (шаг 4) либо сторонний аддон | | `.cube` из Resolve не грузится в Blender | Проверено: `bpy.data.images.load('x.cube')` → «unknown file-format» | Либо вернуть готовый кадр, либо собрать кастомный OCIO-конфиг (см. ниже) | | `enum "FFMPEG" not found` при `blender -b` | В background-режиме динамический enum форматов урезан (проверено на 5.2.1, при этом `bpy.app.build_options.codec_ffmpeg == True`) | Рендерить PNG-последовательность и кодировать ffmpeg-ом — шаг 3 | | Цвета «поплыли» после возврата в Blender | Resolve переписал файл в display-referred | Забирать EXR с `Color Space = Linear`, не с `sRGB`/`Gamma 2.2` | --- ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 49: FFMPEG + VSE PIPELINE](setup_49_ffmpeg_vse_pipeline.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 02: GENESISCORE LOCAL (OLLAMA)](setup_02_genesiscore_local.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 04: HEADLESS RENDER (CLI / CI/CD)](setup_04_headless_render.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 05: RENDER FARM (FLAMENCO)](setup_05_render_farm.md), [SETUP 06: CUSTOM MCP TOOLS](setup_06_custom_mcp_tools.md) - **Теги:** #setup, #video --- # SETUP 51: NOTION/OBSIDIAN DOCS SYNC Tags: #docs #setup # SETUP 51: NOTION/OBSIDIAN DOCS SYNC **ID:** SETUP-51 | **Type:** Docs | **Difficulty:** ⭐ | **Time:** 10 min **RU/EN Bilingual** > Когда применять: когда wiki растёт и её надо отдавать в Notion или Obsidian — > как чтение, а не как «обновлённое зеркало, о котором никто не знает». --- ## 🎯 ЦЕЛЬ Настроить односторонний экспорт базы знаний в два формата: Obsidian-vault (папка `.md` с вики-ссылками) и Notion (JSON для импорта или API). Ключевое решение — **не двусторонняя синхронизация**: источник правды один, markdown, экспорт read-only. Всё, что ниже, основано на реально существующих скриптах этого репозитория (`WIKI/scripts/build_database.py`, `WIKI/scripts/export_all.py`), а не на придуманных командах. --- ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Python | 3.10+ | Скрипты сборки базы и экспорта | | `WIKI/scripts/build_database.py` | из репозитория | `db/wiki_database.json` — источник для экспорта | | `WIKI/scripts/export_all.py` | из репозитория | CSV + Obsidian-vault + Notion JSON | | Obsidian | любая Desktop-версия | Просмотр `.md`-vault без плагинов | | Notion | аккаунт с правом создавать страницы | Импорт JSON либо API-загрузка | --- ## 🛠️ НАСТРОЙКА ### 1. Пересобрать базу — экспортировать из базы нельзя Экспортёр читает `db/wiki_database.json`. Если база устарела, экспорт разнесёт по Notion и Obsidian старые версии статей вместе с новыми. ```powershell # Перейдите в каталог проекта cd "<путь-к-проекту>" python "WIKI\scripts\validate.py" --strict python "WIKI\scripts\test_wiki.py" ``` ### 2. Запустить экспорт У `export_all.py` ровно два аргумента: `--chunk` (номер части) и `--chunks` (на сколько частей резать). Один файл на ~1000 статей — это десятки мегабайт JSON, поэтому дробить обязательно. ```powershell # Проверено по исходнику WIKI/scripts/export_all.py cd "<путь-к-проекту>\WIKI" python scripts\export_all.py --chunk 0 --chunks 4 python scripts\export_all.py --chunk 1 --chunks 4 python scripts\export_all.py --chunk 2 --chunks 4 python scripts\export_all.py --chunk 3 --chunks 4 ``` На выходе появляются три артефакта: ``` WIKI/db/wiki_export_0.csv # заголовок: id,title,category,type, # difficulty,language,tags,tools,file,description WIKI/export_obsidian/01_geometry_basics.md WIKI/export_obsidian/01_geometry_basics__ru.md WIKI/export_notion/notion_import_0.json ``` ### 3. Понять формат Obsidian-заметки Vault — это обычная папка с `.md`. Экспортёр пишет ровно такой файл: frontmatter, заголовок, описание, раздел `## Links` с `[[вики-ссылками]]` и служебная строка `_File:` с путём на оригинал. ```markdown --- id: 01_geometry_basics category: prompts tags: [basics, bpy, bpy.ops, geometry, modeling, primitives, prompt] lang: ru --- # P01: PRIMITIVE CREATION BASICS ## Links - [[07_instancing_arrays]] - [[23_geometry_nodes_basics]] _File: `prompts\01_geometry_basics.md`_ ``` Имя файла — это `id` из базы. Для переводов `id` имеет суффикс `__ru`, поэтому оригинал и перевод в одном vault не конфликтуют и оба остаются на связи. ### 4. Понять формат Notion-JSON Это не «выгрузка страницы», а список объектов `page`, готовый к импорту: у каждой страницы заданы свойства (Category, Lang, Difficulty, Tags, File) и один дочерний блок с описанием. ```json { "pages": [ { "object": "page", "properties": { "title": [{"text": {"content": "P01: PRIMITIVE CREATION BASICS"}}], "Category": {"select": {"name": "prompts"}}, "Lang": {"select": {"name": "ru"}}, "Difficulty": {"number": 1}, "Tags": {"multi_select": [{"name": "basics"}, {"name": "bpy"}]}, "File": {"rich_text": [{"text": {"content": "prompts\\01_geometry_basics.md"}}]} }, "children": [ {"object": "block", "type": "paragraph", "paragraph": {"rich_text": [{"text": {"content": ""}}]}} ] } ] } ``` Свойства из JSON **должны существовать в базе Notion заранее** с теми же именами и типами (`select` / `number` / `multi_select` / `rich_text`). Иначе импорт создаёт страницы без метаданных — и кросс-ссылки по тегам перестают работать. ### 5. Залить в Notion Два пути. Импорт через UI проще и не требует токена; API-путь нужен для автоматизации. ```powershell # Путь A: UI. Собрать все части в один zip и импортировать: # Notion → Import → Markdown & CSV → zip # Перед импортом разложить части, переименовав их в *.md, иначе Notion # не примет .json. # Путь B: API. Создать страницу одной командой: $headers = @{ "Authorization" = "Bearer $env:NOTION_TOKEN" "Notion-Version" = "2022-06-28" "Content-Type" = "application/json" } $body = Get-Content "WIKI\export_notion\notion_import_0.json" -Raw | ConvertFrom-Json Invoke-RestMethod -Method Post -Uri "https://api.notion.com/v1/pages" ` -Headers $headers -Body (@{ parent = @{ database_id = $env:NOTION_DATABASE_ID } properties = $body.pages[0].properties children = $body.pages[0].children } | ConvertTo-Json -Depth 20) ``` > [!NOTE] > `Notion-Version` — это версия API Notion, а не Blender и не wiki. Значение > `2022-06-28` использовалось здесь как пример; актуальную дату возьми из > документации Notion (https://developers.notion.com) и держи её в конфиге > проекта. Загрузка полного тела статьи (а не одного абзаца описания) > делается отдельным вызовом с блоками — в экспортере её нет намеренно, > чтобы JSON оставался разумного размера. ### 6. Договориться о direction of travel | Среда | Роль | Обратная синхронизация | |---|---|---| | `WIKI/*.md` | Единственный источник правды | — | | Obsidian vault | Чтение, поиск, граф вики-ссылок | Нет: правки в vault не возвращаются | | Notion | Ревью, комментарии, задачи | Только через экспорт заметок, вручную | | `db/wiki_database.json` | Производный артефакт | Перезаписывается сборкой | --- ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```bash # Полный цикл: пересобрать базу → разложить на 4 части → проверить cd "<путь-к-проекту>" python "WIKI\scripts\validate.py" --strict python "WIKI\scripts\build_database.py" python "WIKI\scripts\export_all.py" --chunk 0 --chunks 4 ``` | Параметр | Значение | Когда менять | |---|---|---| | `--chunks` | `4` | Больше статей — больше частей (Notion не любит гигантские импорты) | | `--chunk` | `0..N-1` | Прогоняется по кругу; забытый чанк = дыра в базе | | `id` в имени файла | `01_geometry_basics__ru` | Не менять: Obsidian-ссылки строятся по нему | | `Tags` в Notion | `multi_select` | Длиннее 10 тегов экспортёр не пишет — следить за ростом | | `description` | первые 1000 символов | Не хватает — править экспортёр, а не Notion | --- ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | В Notion страницы без тегов и категории | В базе Notion нет свойств `Category` / `Tags` нужного типа | Создать свойства заранее: `Category`=select, `Difficulty`=number, `Tags`=multi_select | | Заметки в Obsidian не связаны `[[...]]` | Экспортирована часть базы без поля `links` | Пересобрать базу и экспортировать заново, а не править vault руками | | Переводы перезаписали оригиналы | Имя файла строилось из заголовка, а не из `id` | Экспортёр уже использует `id` с суффиксом `__ru` — не откатывай это | | Notion отказался импортировать `.json` | Импорт через UI ждёт ZIP с `.md`/`.csv` | Переименовать части в `.md`, упаковать, импортировать | | В базе старая версия статьи | Экспорт шёл до `build_database.py` | Порядок строго: validate → build → export | | Части Obsidian «потерялись» | Экспортёр пишет по `m['id']`, коллизия перезатирает файл | Проверить, нет ли двух статей с одинаковым `id` — это баг, а не фича | --- ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 02: GENESISCORE LOCAL (OLLAMA)](setup_02_genesiscore_local.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 04: HEADLESS RENDER (CLI / CI/CD)](setup_04_headless_render.md), [SETUP 05: RENDER FARM (FLAMENCO)](setup_05_render_farm.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 06: CUSTOM MCP TOOLS](setup_06_custom_mcp_tools.md), [SETUP 07: CI/CD INTEGRATION](setup_07_ci_cd.md) - **Теги:** #docs, #setup --- # SETUP 52: GIT LFS + BLEND VERSIONING Tags: #setup #version-control # SETUP 52: GIT LFS + BLEND VERSIONING **ID:** SETUP-52 | **Type:** Versioning | **Difficulty:** ⭐⭐ | **Time:** 15 min **RU/EN Bilingual** > Когда применять: когда `.blend` уже весит десятки мегабайт и обычный git > начинает дублировать его в каждом коммите. --- ## 🎯 ЦЕЛЬ Настроить хранение бинарных артефактов Blender в Git LFS, уменьшить вес самих `.blend` и **честно обсудить, стоит ли вообще их класть в LFS**. Все команды ниже выполнены локально и дают ровно такой вывод (git-lfs 3.7.1, git 2.55.0). --- ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Git | 2.20+ | Поддержка `filter=lfs` в `.gitattributes` | | Git LFS | 3.x (проверено 3.7.1) | Хранение бинарников вне истории git | | Blender | 5.x (проверено 5.2.1) | `save_as_mainfile(compress=True)`, `pack_all()` | | Git-хостинг с LFS | любой | Квота на LFS-объекты считается отдельно от репозитория | --- ## ⚠️ СНАЧАЛА — ЧЕСТНЫЙ ВОПРОС: НУЖЕН ЛИ `.blend` В LFS? LFS решает ровно одну задачу: он не копирует содержимое файла в каждый коммит. Цена — вы теряете `git diff` по бинарнику (в `diff=lfs` видно только изменение указателя), платите квоту за LFS-объекты и **не получаете настоящих блокировок файлов**: `git lfs locks` работает только на LFS-сервере с API (GitHub/докер вариант), а не на пустом репозитории. Спорный случай именно `.blend`: он меняется каждый день, весит 50–500 МБ, и каждая версия физически другая — diff всё равно бесполезен. Альтернативы, которые чаще выигрывают: | Вариант | Что версионируем | Плюс | |---|---|---| | **Git + скрипты** | `.py`, `.json`, материалы, готовые рендеры | Настоящий diff, бесплатная история | | **Git + LFS** | `.blend` редких milestone'ов (`scene_v001.blend`) | Есть точка отката без лишних копий | | **Perforce / Helix** | Всё, с type map под `.blend` | Настоящие эксклюзивные check-out'ы — то, чего в git нет | | **Внешнее хранилище + хэши** | Версии в S3/NFS, в git — манифест с SHA-256 | Дёшево, но нужна дисциплина | | **Blender backup** | `bpy.ops.wm.save_version` | Локально, без VCS вообще | Практичное правило: **в LFS идут только замороженные версии и тяжёлые ассеты**, а ежедневная работа — это скрипты плюс `pack_all()`. --- ## 🛠️ НАСТРОЙКА ### 1. Инициализировать LFS в репозитории и объявить маски ```bash # Проверено: --local пишет настройки только в этот репозиторий git lfs install --local git lfs track "*.blend" "*.exr" "*.fbx" "*.png" ``` `git lfs track` сам дописывает правила в `.gitattributes`. Фактическое содержимое файла после этих четырёх команд (проверено): ``` *.blend filter=lfs diff=lfs merge=lfs -text *.exr filter=lfs diff=lfs merge=lfs -text *.fbx filter=lfs diff=lfs merge=lfs -text *.png filter=lfs diff=lfs merge=lfs -text ``` Читать построчно: `filter=lfs` — прогонять через LFS clean/smudge; `diff=lfs` — показывать в `git diff` только хэш-указатель вместо «бинарный файл»; `merge=lfs` — LFS умеет сливать указатели; `-text` — **запретить** перевод строк, иначе Windows-агент испортирует содержимое. ### 2. Уменьшить сам `.blend` до того, как он попадёт в LFS ```python # Сжимать и упаковывать сцену перед коммитом (проверено на 5.2.1). # Ключ: compress=True упаковывает сам .blend; pack_all() кладёт внутрь # текстуры и внешние данные, иначе они останутся рядом файлами. import bpy, os bpy.ops.file.pack_all() # реальный оператор в bpy.ops.file bpy.ops.wm.save_as_mainfile( filepath="//scenes/main_scene.blend", compress=True, # проверенный параметр оператора relative_remap=True, # пути сделать относительными ) print("saved", os.path.getsize("//scenes/main_scene.blend") // (1024 * 1024), "MB") ``` ### 3. Проверить, что LFS действительно работает ```bash # Проверено: выводит " * <путь>" для каждого LFS-файла git lfs ls-files # Диагностика окружения: пути к LFS-хранилищу и т.п. git lfs env # Что лежит в LFS прямо сейчас (не в истории) git lfs track ``` Если `git lfs ls-files` молчит, а `.blend` уже в истории — маска добавлена позже, чем файл закоммичен. Дальше нужен `migrate`. ### 4. Перевести уже существующую историю ```bash # Проверено: у migrate есть режимы import / export / info. # --include — маски, --above — только файлы больше N, --yes — без подтверждения. # Сначала обязательно посмотреть, что вообще тянется: git lfs migrate info --include="*.blend" --above=20m # Переписать историю локально (remote не трогается, SHA коммитов меняются): git lfs migrate import --include="*.blend" --above=20m --yes # Дальше — форс-пуш переписанных веток, и только его. git push --force-with-lease origin main ``` > [!NOTE] > `git lfs migrate` переписывает историю и меняет SHA всех коммитов после точки > миграции. Перед запуском закоммитьте или спрячьте всё незакоммиченное > (`git stash`), сделайте бэкап ветки и проверьте результат через > `git lfs migrate info` до пуша. Опция `--skip-smudge` и поведение с > `filter=lfs` менялись между версиями — сверяйтесь с `git lfs migrate -h` > на вашей машине (проверено на 3.7.1, где форма уже составная). ### 5. Не положиться на блокировки — договориться о дисциплине ```bash # Настоящей эксклюзивной блокировки .blend в чистом git нет. # Практика: main-сцены лежат в отдельной ветке, изменения только через PR, # а каждый дизайнер работает в своём экземпляре файла. git switch -c feat/shot_010 git add scenes/shot_010_main.blend git commit -m "shot_010: layout pass 2" git push -u origin feat/shot_010 ``` На GitHub/GitLab блокировки включаются отдельной настройкой репозитория; в локальном или self-hosted репозитории без LFS-сервера они не работают — не рассчитывайте на `git lfs locks` как на защиту от перезаписи. ### 6. Проверка в CI: LFS-указатели должны разворачиваться ```bash # .github/workflows/blend-check.yml — суть шага git lfs pull # скачать реальные объекты, а не указатели python - <<'PY' import bpy, sys bpy.ops.wm.open_mainfile(filepath="scenes/main_scene.blend") print("opened OK, objects:", len(bpy.data.objects)) PY ``` `git lfs pull` до `checkout` — обязателен. Без него в CI будет файл из трёх строк `version https://git-lfs.github.com/spec/v1`, и `open_mainfile` упадёт с невнятной ошибкой. --- ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```bash git lfs install --local # настройки LFS только в этом репозитории git lfs track "*.blend" # дописать маску в .gitattributes git lfs ls-files # что сейчас в LFS (текущая ветка) git lfs migrate info --include="*.blend" --above=20m # «покажи, не делая» git lfs migrate import --include="*.blend" --above=20m --yes git lfs pull # развернуть указатели в рабочую копию ``` | Параметр | Значение | Когда менять | |---|---|---| | `*.blend` в LFS | только milestone-версии | Ежедневные — мимо LFS, они съедают квоту | | `--above` | `20m` | Порог, ниже которого git и так дешевле | | `save_as_mainfile(compress=)` | `True` | Почти всегда: экономит единицы и десятки МБ | | `pack_all()` | перед коммитом | Иначе в LFS уедет `.blend`, а текстуры останутся рядом | | `diff=lfs` | по умолчанию | Оставить: реального diff по `.blend` всё равно нет | --- ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Репозиторий распух в 10 раз | `.blend` попали в обычный git **до** `git lfs track` | `git lfs migrate import --include="*.blend" --yes`, затем `--force-with-lease` | | Файл в CI — текст из трёх строк | `git lfs pull` не выполнили перед `checkout` | Добавить `git lfs pull` первым шагом в job | | «Файл изменился сам» при переносе репозитория | Без `-text` агент на Windows переписал окончания строк | Проверить `.gitattributes`: должна быть строка `filter=lfs diff=lfs merge=lfs -text` | | Текстуры не едут вместе со сценой | Не вызвали `pack_all()` | `bpy.ops.file.pack_all()` перед сохранением в репозиторий | | Два человека переписали `main_scene.blend` | Ожидали блокировку от LFS | Включить file locking на хостинге или перейти на Perforce; в чистом git блокировок нет | | `quota exceeded` на LFS | Квота считается по всем версиям всех LFS-файлов | `git lfs migrate export` для старых версий либо вынесите историю в архивный репозиторий | --- ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 02: GENESISCORE LOCAL (OLLAMA)](setup_02_genesiscore_local.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 04: HEADLESS RENDER (CLI / CI/CD)](setup_04_headless_render.md), [SETUP 05: RENDER FARM (FLAMENCO)](setup_05_render_farm.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 06: CUSTOM MCP TOOLS](setup_06_custom_mcp_tools.md), [SETUP 07: CI/CD INTEGRATION](setup_07_ci_cd.md) - **Теги:** #setup, #version-control --- # SETUP 53: FLAMENCO FARM CLUSTER Tags: #render-farm #setup # SETUP 53: FLAMENCO FARM CLUSTER **ID:** SETUP-53 | **Type:** Render Farm | **Difficulty:** ⭐⭐⭐ | **Time:** 40 min **RU/EN Bilingual** > Когда применять: когда одна машина не справляется и нужно собрать ферму из > нескольких узлов с общим диском. --- ## 🎯 ЦЕЛЬ Разобрать архитектуру Flamenco (менеджер + воркеры + общее хранилище) и собрать рабочий кластер. Статья намеренно **не выдумывает имена флагов**: конкретные ключи конфигурации менялись между релизами 3.x, поэтому ниже приведена проверяемая схема и то, что точно можно проверить самому. Проверяемые факты ниже относятся к Blender 5.2.1 LTS и shell-утилитам Linux/Windows. --- ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Flamenco | релиз 3.x (см. репозиторий проекта) | Менеджер и воркеры | | Blender | одна и та же версия на всех узлах | Иначе разный результат кадра | | Общее хранилище | NFS (Linux↔Linux) или SMB (с Windows) | Сцена на всех узлах | | GPU | NVIDIA + драйвер, `nvidia-smi` на каждом узле | Рендер на GPU, не CPU | | Сеть | 1 Gbps между узлами | Сцена и результаты ходят туда-сюда | --- ## 🛠️ НАСТРОЙКА ### 1. Понять архитектуру ``` ┌──────────────────────┐ HTTP ┌──────────────────────────┐ │ Браузер / API-клиент│──────────▶│ Flamenco manager │ └──────────────────────┘ │ очередь, деплой, логи │ └───────────┬──────────────┘ │ очередь задач ┌───────────────────────┼───────────────────────┐ ▼ ▼ ▼ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │ worker node 01 │ │ worker node 02 │ │ worker node N │ │ blender -b │ │ blender -b │ │ blender -b │ └────────┬───────┘ └────────┬───────┘ └────────┬───────┘ └───────────────────────┼───────────────────────┘ ▼ ┌──────────────────────────────────┐ │ Shared storage (NFS / SMB) │ │ сцены, текстуры, output, логи │ └──────────────────────────────────┘ ``` Ключевое: **менеджер не рендерит**. Он только раздаёт задания и собирает статусы. Если менеджер видит «idle workers» при полной очереди — проблема в сетевом хранилище или в том, что воркер не смог открыть файл сцены. ### 2. Поставить один менеджер ```bash # Источник сборок — официальный репозиторий проекта: # https://github.com/blender/flamenco # Конкретные имена релизов и файлов проверяйте в Releases, а не по памяти: curl -s https://api.github.com/repos/blender/flamenco/releases/latest \ | grep -E '"tag_name"|browser_download_url' ``` ```bash # Первый запуск: посмотреть справку своей сборки, а не гадать. ./flamenco --help ./flamenco --version ``` > [!NOTE] > Имя бинарника, набор подкоманд и ключи YAML-конфигурации менялись между > релизами Flamenco 3.x: в ранних версиях это были отдельные `flamenco-manager` > и `flamenco-worker`, позже роль воркера переименовали в «агент». Поэтому > здесь нет ни одной строки конфига — она была бы выдумкой. Правильный путь: > открыть README той версии, которую вы ставите, и сверить с выводом > `flamenco --help` на своей машине. ### 3. Поднять воркер и проверить его до подключения к ферме Префлайт-скрипт ловит 90 % проблем до того, как они станут «воркер молча падает на втором кадре». ```bash #!/usr/bin/env bash # node-preflight.sh — запускать на КАЖДОМ воркере set -euo pipefail echo "== blender ==" blender --version | head -3 echo "== gpu ==" command -v nvidia-smi >/dev/null && nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv || echo "NO NVIDIA GPU" echo "== storage write/read ==" STORAGE_MOUNT="${STORAGE_MOUNT:-/mnt/farm}" mkdir -p "$STORAGE_MOUNT/_selftest" dd if=/dev/urandom of="$STORAGE_MOUNT/_selftest/t.bin" bs=1M count=64 status=none sync # NFS: без sync кэш может не уехать до следующей проверки md5sum "$STORAGE_MOUNT/_selftest/t.bin" rm -f "$STORAGE_MOUNT/_selftest/t.bin" echo "== ok ==" ``` ### 4. Смонтировать общее хранилище ```bash # --- Сервер (экспорт NFS) --- sudo apt install -y nfs-kernel-server echo "/mnt/farm *(rw,sync,no_subtree_check)" >> /etc/exports sudo exportfs -ra systemctl restart nfs-server # --- Воркер (клиент NFS) --- sudo apt install -y nfs-common mkdir -p /mnt/farm echo "FARM_HOST:/mnt/farm /mnt/farm nfs defaults,vers=4.1 0 0" >> /etc/fstab sudo mount -a df -h /mnt/farm ``` С `sync` в экспорте NFS запись идёт сразу на диск — медленнее, но после перезагрузки узла не теряются готовые кадры. `no_root_squash` добавляйте осознанно: без него воркер, запущенный не от root, не сможет писать в директорию, созданную менеджером. ### 5. Разбить анимацию на чанки Менеджер сам раздаёт отдельные кадры, но рендер крупными блоками устойчивее к перезапускам: упал узел — потерян блок, а не 400 кадров. ```python # split_chunks.py — обычный Python, без зависимостей от Flamenco. def chunks(start: int, end: int, size: int): """Вернуть список (first, last) включительно. end >= start, size > 0.""" if end < start or size <= 0: raise ValueError("bad range") return [(f, min(f + size - 1, end)) for f in range(start, end + 1, size)] for first, last in chunks(1, 250, 25): print(f"-s {first} -e {last}") ``` ```bash # Команда, которую воркер выполнит на одном блоке. # Все флаги проверены: -b (background), -E (движок), -o (шаблон имени), # -s/-e (диапазон), -j (потоки), -a (анимация). blender -b //scenes/main.blend \ -E CYCLES \ -o "//output/anim_####" \ -s 1 -e 25 -j 1 \ -a ``` ### 6. Считать прогресс по логам Blender печатает в stdout построчный прогресс и строку сохранения кадра. Эти две подстроки стабильны и годятся как якоря для парсинга. ```bash # Следить за прогрессом на узле: tail -f worker.log | grep --line-buffered -E "Fra:|Saved:" # Сколько кадров реально готово на общем диске: ls //output/anim_####.exr 2>/dev/null | wc -l ``` --- ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```bash # Быстрая проверка, что менеджер и воркер одной версии — иначе они не договорятся flamenco --version # на менеджере flamenco --version # на каждом воркере: строки должны совпадать ``` | Параметр / понятие | Значение | Когда менять | |---|---|---| | Размер чанка | 25–50 кадров | Крупнее — больше потеряешь при падении узла | | `max_concurrent_tasks` | 1 на GPU | Два процесса на одной карте делят VRAM и оба падают | | Версия Blender | строго одна | Иначе часть кадров уйдёт в другой результат | | NFS `sync` | включён | Выключить только если готовы терять кадры при ребуте узла | | `-E CYCLES` | явно | Не полагаться на движок из сохранённой сцены | | Превью-рендер | 1 кадр на узел | Проверить, что GPU вообще считает, до отправки 250 кадров | > [!NOTE] > Список ключей конфигурации воркера (имя файла конфига, ключи GPU, лимит > задач) не приводится: он различается между релизами Flamenco 3.x. Откройте > README и `--help` вашей версии. Проверяемые вещи — `flamenco --version`, > `--help`, префлайт-скрипт выше и флаги самого Blender. --- ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Воркер подключается и сразу отваливается | Несовпадение версий менеджера и воркера | Сверить `flamenco --version` на обеих сторонах | | Очередь растёт, воркеры «idle» | Воркер не видит сцену на общем диске | Проверить `mount`, права на каталог, `no_root_squash` | | Кадры пересчитываются дважды | Два воркера взяли один и тот же блок | Уменьшить блок; проверить, что не запущено два менеджера на одном хранилище | | Часть кадров темнее остальных | Разные версии Blender или разные GPU-драйверы | Свести версии и драйверы к одному; перерендерить несогласованный блок | | OOM на тяжёлом кадре, узел уходит в swap | Нет лимита памяти на задачу | Ограничить RAM на воркере; упростить сцену (см. P30) | | NFS-монтирование есть, но файлы «долгие» | Забыт `sync` в экспорте, кэш на клиенте | Добавить `sync` в `/etc/exports`, `exportfs -ra`, перемонтировать | | Панель показывает статусы, которых нет в UI менеджера | Ожидание полей от другой версии | Не опираться на скриншоты: смотрите фактический веб-интерфейс своей версии | --- ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 05: RENDER FARM (FLAMENCO)](setup_05_render_farm.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 02: GENESISCORE LOCAL (OLLAMA)](setup_02_genesiscore_local.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 04: HEADLESS RENDER (CLI / CI/CD)](setup_04_headless_render.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [P29: BATCH PROCESSING & CLI](../prompts/29_batch_cli.md), [P47: ALEMBIC CACHE PIPELINE](../prompts/47_alembic_cache.md), [P49: RENDER FARM SUBMIT](../prompts/49_farm_submit.md) - **Теги:** #render-farm, #setup --- # SETUP 54: CLOUD RENDER AWS/GCP Tags: #cloud #render #setup # SETUP 54: CLOUD RENDER AWS/GCP **ID:** SETUP-54 | **Type:** Cloud | **Difficulty:** ⭐⭐⭐ | **Time:** 40 min **RU/EN Bilingual** > Когда применять: когда дедлайн не ждёт, а рендер занимает 20 часов на одной > машине. --- ## 🎯 ЦЕЛЬ Собрать повторяемый облачный рендер: сцена в объектном хранилище, контейнер с Blender на GPU-инстансе, результат возвращается обратно, счётчик стоимости не уезжает в космос. Главный принцип статьи: **сначала измерить, потом покупать**. Один кадр на своём железе — это честная цифра, из которой считается смета; без неё любой Spot-инстанс становится азартной игрой. --- ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | AWS CLI | v2 (проверено: команды `aws s3 sync`) | Синхронизация сцены и результатов | | Объектное хранилище | S3-совместимое | Промежуточное состояние между узлами | | Контейнерный образ | CUDA-база + Blender | Инстанс должен запускать то же, что и CI | | Blender | та же версия, что локально | Иначе другой результат кадра | | Бюджет-алерт | Billing → Budgets | Страховка от ночного разгона | --- ## 🛠️ НАСТРОЙКА ### 1. Измерить один кадр локально Единственная цифра, которая нужна до всех облачных трат. Рендерите кадр в фоне и засекаете время. ```bash # Проверено: -f рендерит один кадр; -E CYCLES задаёт движок явно. # Время в конце лога строки вида "Time: 00:12.34" — то, что нужно. time blender -b scene.blend -E CYCLES -o //bench/frame_#### -f 42 ``` Смета считается просто, без допущений о забытых множителях: ```python # estimate.py — калькулятор сметы. Подавайте ему реальные числа. def estimate(seconds_per_frame, frames, price_per_hour, startup_seconds=180, waste_ratio=0.15): """Вернуть (сек, стоимость) с запасом на холодный старт и переделки.""" compute = seconds_per_frame * frames total = compute * (1 + waste_ratio) + startup_seconds return total, total / 3600 * price_per_hour t, cost = estimate(seconds_per_frame=37.4, frames=250, price_per_hour=1.3) print(f"{t/3600:.2f} h, ${cost:.2f}") # 2.86 h, $3.72 — до первого Spot ``` Если смета на порядок выше сметы на «обычной машине» — проблема не в облаке, а в сцене: уменьшайте разрешение, samples, включайте адаптивный семплинг. ### 2. Упаковать сцену в контейнер ```dockerfile # Dockerfile.blender — проверенная база; версию CUDA подставь под свой драйвер. FROM nvidia/cuda:12.1.1-cudnn8-devel-ubuntu22.04 ARG BLENDER_VERSION=5.2.1 ARG BLENDER_SHA256=REPLACE_ME # обязательно: не качайте непроверенные сборки RUN apt-get update && apt-get install -y --no-install-recommends \ xz-utils wget python3 \ && wget -q "https://download.blender.org/release/Blender${BLENDER_VERSION%.*}/blender-${BLENDER_VERSION}-linux-x64.tar.xz" -O /tmp/b.tar.xz \ && echo "${BLENDER_SHA256} /tmp/b.tar.xz" | sha256sum -c - \ && tar -xJf /tmp/b.tar.xz -C /opt \ && ln -s /opt/blender-${BLENDER_VERSION}-linux-x64/blender /usr/local/bin/blender \ && rm /tmp/b.tar.xz ENV DEBIAN_FRONTEND=noninteractive WORKDIR /work ENTRYPOINT ["blender", "-b"] ``` ```bash docker build -f Dockerfile.blender -t blender-render:5.2.1 . # Проверка на локальной машине с GPU — обязательна до отправки в облако. # Несовпадение результатов здесь сэкономит деньги на инстансах. docker run --rm --gpus all \ -v "$(pwd)/scenes:/work" -v "$(pwd)/output:/work/output" \ blender-render:5.2.1 scene.blend -E CYCLES -o //output/anim_#### -s 1 -e 5 -a ``` ### 3. Положить и забрать через S3 ```bash # Проверено на AWS CLI: upload — положить, download — забрать. # --only-show-errors прячет поштучные строки на тысячах файлов. # 1) Сцена вверх (один раз; дальше меняется только .blend) aws s3 sync ./scenes s3://acme-render/in/scene_v001/ --only-show-errors # 2) Результаты вниз, с инкрементальной докачкой (докачка не перезаливает готовое) aws s3 sync s3://acme-render/out/scene_v001/ ./output/ --only-show-errors # 3) Что именно улетело — до отправки, а не после aws s3 ls s3://acme-render/in/scene_v001/ --recursive --summarize ``` Отдельная папка на версию сцены (`scene_v001`, `scene_v002`) — то, что спасает, когда два инстанса пишут в один префикс: результаты не смешиваются и можно досчитаться, какой именно набор кадров к какой версии относится. ### 4. Запустить инстанс через user-data ```bash #!/usr/bin/env bash # user-data.sh — выполняется один раз при старте инстанса. set -euo pipefail exec > >(tee /var/log/render-bootstrap.log | logger -t render -s 2>/dev/console) 2>&1 echo "== обновление пакетов ==" apt-get update -qq && apt-get install -y -qq awscli echo "== сцена из S3 ==" aws s3 sync s3://${S3_BUCKET}/in/scene_v001/ /work --only-show-errors echo "== рендер ==" blender -b /work/scene.blend -E CYCLES -o /work/output/anim_#### -s 1 -e 250 -a echo "== результат в S3 ==" aws s3 sync /work/output s3://${S3_BUCKET}/out/scene_v001/ --only-show-errors echo "== DONE ==" ``` Ключевое: скрипт **идемпотентен**. Инстанс можно перезапустить — `s3 sync` не перезальёт уже готовые кадры, а `blender -b` пересчитает недостающие. ### 5. Страховать бюджет ```bash # Личный счёт и регион: без этого все остальные команды уйдут не туда. aws sts get-caller-identity aws configure set region eu-central-1 # Бюджеты и пороговые алерты создаются в консоли Billing → Budgets # (или через CloudWatch Billing Alerts). Настраивайте ДО первого запуска: # лимит + оповещение на 50/80/100% — это разница между «инцидентом» # и «дорогим днём». aws budgets describe-budgets --account-id "$(aws sts get-caller-identity --query Account --output text)" ``` --- ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр / понятие | Значение | Когда менять | |---|---|---| | Тип инстанса | GPU-класс с 1 GPU | Две GPU на инстансе — сложнее считать VRAM и лимит памяти | | On-Demand → Spot | Spot в 2–6 раз дешевле | Spot только если задание умеет повторяться: `s3 sync` и `-b` это позволяют | | Прерываемость | Обязательно для Spot | Инстанс может забраться в любой момент; всё должно быть повторяемым | | Префикс S3 | На версию сцены | Иначе результаты двух запусков смешаются | | `--only-show-errors` | Всегда | Без него лог на 4000 объектов нечитаем | | Лимит бюджета | Ставить до запуска | Это единственная защита от петли «инстанс × бесконечный ретрай» | > [!NOTE] > Флаги запуска Spot-инстансов (`--instance-market-options`, > `--spot-options`, `--launch-template`) — это параметры AWS CLI, а не Blender. > Их набор менялся между версиями CLI; точный список берите из > `aws ec2 run-instances --help` на своей машине > (документация: https://docs.aws.amazon.com/cli/latest/reference/). > В GCP аналогично: `gcloud compute instances create --help`. --- ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Инстанс стартует и сразу `Exited` | В user-data ошибка, лог не в stdout | Писать в `/var/log/render-bootstrap.log`; ошибку искать в console output инстанса | | Результаты в S3 неполные | Прерванный Spot-запуск | Перезапустить: `s3 sync` докачает, `blender -b` пересчитает недостающее | | Кадры темнее, чем локально | GPU/driver или версия Blender отличается | Зафиксировать версию образа и драйвера; сверять 1 кадр | | Счёт вырос, а кадров нет | Бесконечный ретрай Spot | Ставить бюджет-алерт; ограничить число попыток в скрипте | | S3 `sync` залил пустую папку | Не тот префикс / опечатка в регионе | Проверять `aws s3 ls ... --recursive --summarize` до старта рендера | | Текстуры потерялись при переносе | `.blend` не упакован | `bpy.ops.file.pack_all()` перед `save_as_mainfile`, либо складывать `//textures` рядом | | `No space left on device` на инстансе | Местный диск мал, `output` локальный | Складывать вывод сразу в S3 или подключить том нужного размера | --- ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 04: HEADLESS RENDER (CLI / CI/CD)](setup_04_headless_render.md), [SETUP 05: RENDER FARM (FLAMENCO)](setup_05_render_farm.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 02: GENESISCORE LOCAL (OLLAMA)](setup_02_genesiscore_local.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md) - **По темам:** [CASE 03: PRODUCT VISUALIZATION](../cases/case_03_product_viz.md), [CASE 05: PROCEDURAL CITY](../cases/case_05_procedural_city.md), [CASE 07: STYLIZED BARREL (HAND-PAINT)](../cases/case_07_stylized_barrel.md), [CASE 08: SCI-FI CORRIDOR MODULAR](../cases/case_08_sci_fi_corridor.md) - **Теги:** #cloud, #render, #setup --- # SETUP 55: AI UPSCALE PIPELINE Tags: #pipeline #setup #upscale # SETUP 55: AI UPSCALE PIPELINE **ID:** SETUP-55 | **Type:** Upscale | **Difficulty:** ⭐ | **Time:** 15 min **RU/EN Bilingual** > Когда применять: когда кадр отрендерен в меньшем разрешении, чем нужно для > сдачи, — и остался выбор между апскейлом постфактум или повторным рендером. --- ## 🎯 ЦЕЛЬ Разложить апскейл по полочкам и выбрать правильный инструмент под задачу. Статья начинается с неприятной правды: **для 3D-рендера AI-апскейл почти всегда хуже, чем отрендерить в большем разрешении**. Апскейл имеет смысл там, где исходника больше нет, либо для черновых превью. Все команды ffmpeg проверены локально на версии 9.0.1. --- ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Blender | 5.x (проверено 5.2.1) | Рендер в большем разрешении — первый и лучший «апскейл» | | ffmpeg + ffprobe | 7.x+ (проверено 9.0.1) | Честный интерполяционный апскейл, сборка, верификация | | Real-ESRGAN | см. репозиторий проекта | Нейросетевой апскейл для превью и чужих материалов | | Cycles denoise | в комплекте Blender | Шум — причина, по которой хотят апскейл | | Диск | 2× от результата | Мастер не трогаем, работаем на копии | --- ## 🛠️ НАСТРОЙКА ### 1. Сначала — правильная развилка | Что лежит на входе | Что делать | Почему | |---|---|---| | Своя сцена, ещё не отрендерено | Рендерить ×2 по разрешению | Бесплатно, честно, детали настоящие | | Своя сцена, шумно | Поднять samples + denoise | Шум, а не разрешение, портит картинку | | Свой рендер, сдавать завтра | Lanczos-апскейл в ffmpeg | Предсказуемо, без галлюцинаций | | Чужой материал / превью | Real-ESRGAN | Дёшево по времени, качество непредсказуемо | | Композитинг из 8 слоёв | Апскейлить каждый слой **до** композита | Иначе края разъедутся между слоями | ### 2. Правильный путь: рендер в ×2 ```python # scale_render.py — resolution_percentage вместо ручного пересчёта. import bpy sc = bpy.context.scene sc.render.resolution_percentage = 200 # ровно ×2 к 100% sc.render.resolution_x, sc.render.resolution_y = 1920, 1080 # → фактический кадр 3840x2160. В панели Render это видно как # "Resolution 200%". # Шум лечится отдельно и почти всегда дешевле, чем лишние пиксели: sc.cycles.use_adaptive_sampling = True sc.cycles.adaptive_threshold = 0.01 sc.cycles.samples = 256 for vl in sc.view_layers: vl.cycles.use_denoising = True # проверено: реальный флаг во вью-слое print(sc.render.resolution_x * sc.render.resolution_percentage // 100) ``` > [!NOTE] > Идентификаторы движка денойзера (`OPENIMAGEDENOISE` / `OPTIX`) в фоне не > читаются — это динамический enum, и в `blender -b` список приходит пустым > (проверено на 5.2.1). Выбирайте движок в UI и проверяйте результат, либо > ставьте флаг `use_denoising = True` и полагайтесь на значение по умолчанию. ### 3. Честный интерполяционный апскейл ```bash # Lanczos по кадрам. -vf scale с флагом flags=lanczos — это то, чем # Blender увеличивает текстуру, и не выдумывает детали. # Проверено на ffmpeg 9.0.1: ffmpeg -y -framerate 24 -start_number 1 -i render/anim_%04d.png \ -vf "scale=iw*2:ih*2:flags=lanczos" \ -c:v libx264 -crf 16 -pix_fmt yuv420p -movflags +faststart \ delivery/anim_2x.mp4 ``` Сначала убедитесь, что вход действительно 1920×1080 — иначе «апскейл ×2» будет апскейлом с 640×480 и потеря спрячется в этом месте: ```bash ffprobe -v error -select_streams v:0 \ -show_entries stream=width,height,pix_fmt,r_frame_rate \ -of default=nw=1 render/anim_0001.png ``` ### 4. Нейросетевой апскейл ```bash # Репозиторий проекта: https://github.com/xinntao/Real-ESRGAN # Проверьте имена скрипта и ключей в README своей версии: python inference_realesrgan.py -h # Типичный вызов для одиночного кадра 2x: python inference_realesrgan.py -n realesrgan-x4plus -i plate.png -o plate_2x.png -s 2 ``` > [!NOTE] > Точные имена весов (`realesrgan-x4plus`, `-s`, `-i`, `-o`) и требуемая версия > PyTorch/Torchvision менялись между релизами Real-ESRGAN. Проверяйте > `inference_realesrgan.py -h` и README — приведённая строка это форма команды, > а не гарантия, что конкретная версия её примет без правок. Для последовательности — только батч, по кадру за вызов: ```bash # Один процесс на кадр. Тяжёлая модель в памяти не держится дольше вызова, # поэтому параллелизм упирается в RAM, а не в GPU. for f in render/anim_*.png; do python inference_realesrgan.py -n realesrgan-x4plus -i "$f" -o "upscale2x/$(basename "$f")" -s 2 done ``` ### 5. Денойз → шарпен → сборка ```bash # Шарпен умножением, без пересвета: unsharp с amount ниже 1.0 ffmpeg -y -framerate 24 -start_number 1 -i upscale2x/anim_%04d.png \ -vf "unsharp=5:5:0.4:5:5:0.0" \ -c:v libx264 -crf 16 -pix_fmt yuv420p -movflags +faststart \ delivery/anim_final.mp4 # ffprobe — обязательная финальная проверка перед сдачей: ffprobe -v error -select_streams v:0 \ -show_entries stream=codec_name,width,height,pix_fmt \ -of csv=p=0 delivery/anim_final.mp4 ``` ### 6. Не тронуть EXR-мастер ```bash # Последний рубеж: мастер должен остаться нетронутым. # Проверяем, что апскейлер писал только в delivery/: ls -la delivery/ master/exr/ | head -20 # и сверяем размеры: мастер не мог измениться git status --porcelain -- master/ # в git-репозитории ``` Мастер — линейный EXR. Прогоняя его через нейросеть или 8-битный H.264, вы теряете и HDR-диапазон, и возможность пере-грейдить позже. Апскейл — операция **на копии под сдачу**, и её результат никогда не поднимается в мастер. --- ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `resolution_percentage` | `200` | Вместо увеличения `resolution_x/y` — не расходится с UI | | `adaptive_threshold` | `0.01` | Шумно — снижать до `0.005`; медленно — поднимать до `0.02` | | `flags=lanczos` | обязательно | `bilinear` мыльнее, `neighbor` — лестница | | `-crf` | `16` для сдачи, `23` для превью | Ниже 14 размер растёт неоправданно | | `-pix_fmt yuv420p` | обязательно | Иначе часть плееров покажет чёрный экран | | `unsharp` amount | `0.4` | Выше `1.0` появляются ореолы на контрастных краях | | Мастер EXR | не трогать | Апскейл делается на копии под конкретный формат сдачи | --- ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Кадры «мягкие» в 2K, хотя рендер был 4K | Вход оказался 960×540 | `ffprobe` на первом кадре **до** апскейла | | Нейросеть дорисовала лишнее: изменились надписи, узоры | AI-апскейл не выдумывает — он галлюцинирует | Для финальной сдачи — Lanczos, не ESRGAN | | Ореолы вокруг контрастных краёв | `unsharp` amount > 1.0 | Снизить до `0.3–0.5` или убрать совсем | | Композитинг развалился: слои разного размера | Апскейлили готовый кадр, а не слои | Апскейлить каждый слой до композита (см. P15) | | Не открывается в плеере / на площадке | Не задан `-pix_fmt yuv420p` | Добавить флаг, перезалить | | Ресайз в Batch-рендере проигнорирован | Меняли `resolution_x` после включения пресета | Проверять итог: `resolution_x * resolution_percentage` | | Шум «уехал» после апскейла | Шум масштабируется вместе с картинкой | Денойз в рендере, а не после увеличения | --- ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 08: ASSET PIPELINE](setup_08_asset_pipeline.md), [SETUP 14: BLENDER + GODOT PIPELINE](setup_14_godot_pipeline.md), [SETUP 38: FAB.COM (EPIC) PIPELINE](setup_38_fab_epic.md), [SETUP 45: HUNYUAN3D 2.1 LOCAL](setup_45_hunyuan3d_21.md), [SETUP 49: FFMPEG + VSE PIPELINE](setup_49_ffmpeg_vse_pipeline.md) - **По темам:** [SETUP 59: WEB VIEWER EXPORT](setup_59_web_viewer_export.md), [CASE 02: GAME PROPS PIPELINE](../cases/case_02_game_props.md), [CASE 03: PRODUCT VISUALIZATION](../cases/case_03_product_viz.md) - **Теги:** #pipeline, #setup, #upscale --- # SETUP 56: MOCAP ROKOKO / MOVE AI Tags: #mocap #setup # SETUP 56: MOCAP ROKOKO / MOVE AI **ID:** SETUP-56 | **Type:** Mocap | **Difficulty:** ⭐⭐ | **Time:** 25 min **RU/EN Bilingual** > Когда применять: когда есть FBX с телом — запись смартфона, Rokoko Studio или > Move AI — и его нужно перенести на свой риг и привести в порядок. --- ## 🎯 ЦЕЛЬ Собрать повторяемый путь FBX → свой риг → запечённая анимация, с честным разбором двух мест, где ломается всё: оси FBX и доступ к F-кривым. Все вызовы API в статье проверены на Blender 5.2.1 LTS. --- ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Blender | 5.x (проверено 5.2.1) | Импорт FBX, риг, запекание | | Исходный FBX | экспорт из студии | Только FBX: `.fbx` встроенный, `io_fbx=True` | | Целевой риг | Armature | Куда ретаргетить (в примере — человеческая цепь) | | Текстовый редактор | любой | Держать таблицу соответствия костей | --- ## 🛠️ НАСТРОЙКА ### 1. Импортировать FBX с явными осями Здесь ловушка, на которой спотыкается половина импортов: **в Blender 5.x перечисления осей FBX — это `'X'`, `'Y'`, `'Z'`, `'-X'`, `'-Y'`, `'-Z'`**, а не старые имена вида `Y_FORWARD`. Проверено на 5.2.1: попытка передать `Y_FORWARD` падает с `enum "Y_FORWARD" not found`. ```python # import_mocap.py import bpy AXES = { # проверенные значения enum в Blender 5.2 'Y_FORWARD': '-Z', # Blender Y-up -> FBX Y-up: вперёд = -Z в Blender 'Z_FORWARD': '-Y', # Blender Z-up -> FBX Z-up } axis_forward, axis_up = '-Z', 'Y' bpy.ops.object.select_all(action='DESELECT') bpy.ops.import_scene.fbx( filepath="//mocap/take_01.fbx", use_anim=True, bake_anim=True, # кривые сразу в Action, без NLA-полос bake_anim_use_nla_strips=False, bake_anim_use_all_bones=True, bake_anim_force_startend_keying=True, object_types={'ARMATURE', 'MESH'}, # проверенный набор в 5.2 primary_bone_axis='Y', secondary_bone_axis='X', axis_forward=axis_forward, axis_up=axis_up, use_custom_props=False, ) ``` `bake_anim_use_nla_strips=False` — принципиально: иначе каждая полоса NLA станет отдельным источником, и дальнейшая чистка кривых превратится в неразбираемую кашу. ### 2. Сверить масштаб и ориентацию до ретаргета ```python def audit_rig(ob): """Напечатать то, из-за чего потом «плывут ноги».""" names = [b.name for b in ob.pose.bones] print("height_m:", max((ob.matrix_world @ b.tail).z for b in ob.pose.bones)) print("bone_count:", len(names)) return names src_bones = audit_rig(bpy.data.objects["take_01_armature"]) dst_bones = audit_rig(bpy.data.objects["MY_RIG"]) # Масштаб сцены правится один раз, до ретаргета: bpy.data.objects["take_01_armature"].scale = (0.01, 0.01, 0.01) # FBX в сантиметрах ``` Проверка, которую стоит делать всегда: рост скелета в метрах. Если скелет выше 2.2 м — у вас сантиметры или неверный `axis_up`, и никакой ретаргет это не починит. ### 3. Построить таблицу костей и проверить её Имена костей в FBX задаёт экспортирующая сторона: они отличаются у Rokoko, Move AI и любого другого инструмента. Поэтому маппинг — это **ваш файл**, а не свойство формата, и его надо проверять на реальном файле, а не писать по памяти. ```python # bone_map.py — маппинг хранится в файле, а не в коде скрипта BONE_MAP = { "Hips": "pelvis", "Spine": "spine_fk", "Chest": "spine_ik", "Neck": "neck", "Head": "head", "LeftUpLeg": "thigh_fk.L", "LeftLeg": "shin_fk.L", "LeftFoot": "foot_fk.L", "LeftToe": "ball_fk.L", "RightUpLeg":"thigh_fk.R", "RightLeg": "shin_fk.R", "RightFoot": "foot_fk.R", "RightToe": "ball_fk.R", "LeftArm": "upper_arm_fk.L", "LeftForeArm": "forearm_fk.L", "RightArm": "upper_arm_fk.R", "RightForeArm":"forearm_fk.R", } def verify_map(src_bones, dst_bones, mapping): missing_dst = sorted(set(mapping.values()) - set(dst_bones)) missing_src = sorted(set(mapping) - set(src_bones)) if missing_dst: print("НЕТ В ЦЕЛЕВОМ РИГЕ:", missing_dst) if missing_src: print("НЕТ В ИСХОДНОМ FBX:", missing_src) return len(mapping) - len(missing_src) print("mapped:", verify_map(src_bones, dst_bones, BONE_MAP)) ``` ### 4. Ретаргетить констрейнтами Классический способ: копировать **мировую** ориентацию костей, а не локальную. Локальное копирование ломается, когда у источника и цели разные длины костей. ```python def retarget(dst, src, mapping): bpy.ops.object.select_all(action='DESELECT') dst.select_set(True) bpy.context.view_layer.objects.active = dst bpy.ops.object.mode_set(mode='POSE') added = 0 for sbone, dbone in mapping.items(): if sbone not in src.pose.bones or dbone not in dst.pose.bones: continue c = dst.pose.bones[dbone].constraints.new('COPY_ROTATION') c.target, c.subtarget = src, sbone c.target_space = 'WORLD' # читать в мире... c.owner_space = 'WORLD' # ...и писать в мире added += 1 bpy.ops.object.mode_set(mode='OBJECT') return added print("constraints:", retarget(bpy.data.objects["MY_RIG"], bpy.data.objects["take_01_armature"], BONE_MAP)) ``` ### 5. Запечь и снять констрейнты ```python # Запекание в NLA с визуальным ключированием: контроллеры IK/FK # пересчитываются «как видит глаз», а не как считает формула. bpy.ops.object.select_all(action='DESELECT') dst = bpy.data.objects["MY_RIG"] dst.select_set(True) bpy.context.view_layer.objects.active = dst bpy.ops.nla.bake( frame_start=1, frame_end=int(dst.animation_data.action.frame_range[1]), step=1, only_selected=False, visual_keying=True, clear_constraints=True, # констрейнты уходят сразу после запекания clear_parents=False, clean_curves=False, # чистим сами, следующим шагом use_current_action=True, ) ``` ### 6. Почистить кривые В Blender 5.x F-кривые больше **не** лежат в `action.fcurves` — свойства нет, `AttributeError`. Правильный путь: `action.layers[0].strips[0].channelbags[0]`. Проверено на 5.2.1. ```python def action_fcurves(action): """Достать F-кривые из slotted Action в Blender 4.4+/5.x. Старый action.fcurves в 5.x удалён.""" for layer in action.layers: for strip in layer.strips: for cb in strip.channelbags: yield from cb.fcurves def clean(action): """Ровняем хэндлы: AUTO_CLAMPED не даёт кривой уйти в сторону между соседними ключами — это и есть главный источник одиночных «дёрганий» на коже.""" n = 0 for fc in action_fcurves(action): for kp in fc.keyframe_points: kp.handle_left_type = 'AUTO_CLAMPED' kp.handle_right_type = 'AUTO_CLAMPED' fc.update() n += 1 return n print("curves cleaned:", clean(dst.animation_data.action)) ``` Дополнительно стоит прогнать `bpy.ops.graph.smooth(type='SMOOTH')` с выделенными ключами — но порог подбирают вручную: у таза и у стопы он разный, и одна цифра для всей анимации либо съест движение, либо ничего не уберёт. ### 7. Зациклить ```python def make_loop(action, frame_start, frame_end): """Cycles-модификатор повторяет диапазон бесконечно. Проверенный набор типов в 5.2: NULL, GENERATOR, FNGENERATOR, ENVELOPE, CYCLES, NOISE, LIMITS, STEPPED, SMOOTH.""" frames = 0 for fc in action_fcurves(action): fc.modifiers.new('CYCLES') frames += 1 return frames print("looped:", make_loop(dst.animation_data.action, 1, 120)) ``` Вместо модификатора можно продублировать полосу в NLA — но тогда появятся швы на стыке, и их придётся править вручную. ### 8. Foot lock на кадрах контакта ```python # Простой, честный foot lock без плагинов: на кадрах контакта (их вы # определяете сами по детекции скорости стопы) ключи IK-контроллеров # фиксируются, а между ними интерполируются. import bpy ik = bpy.data.objects["MY_RIG"].pose.bones["foot_fk.L"] ik.lock_location = (True, True, True) # блокируем цель на кадрах контакта ik.lock_rotation = (True, True, True) # Затем вручную проставить ключи на кадрах контакта — либо снять блокировку # на кадрах переноса. Полностью автоматический foot lock требует плагина. ``` --- ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `axis_forward` / `axis_up` | `'-Z'` / `'Y'` | Другой источник — подбирается по одному кадру, а не «на глаз» | | `primary/secondary_bone_axis` | `'Y'` / `'X'` | Если кости цели лежат | | `bake_anim_use_nla_strips` | `False` | Всегда: NLA-полосы мешают чистке | | `bake_anim_force_startend_keying` | `True` | Нужны точные первый и последний кадры | | `target_space` / `owner_space` | `'WORLD'` / `'WORLD'` | Иначе длина костей влияет на результат | | `nla.bake(visual_keying=)` | `True` | IK/FK-риги: «как видит глаз» | | `clear_constraints` | `True` | Сразу после запекания — констрейнты мешают правке кривых | > [!NOTE] > Набор имён костей в FBX, пресеты экспорта в Rokoko Studio и Move AI и > формат их облачных выгрузок менялись. Таблицу `BONE_MAP` нужно печатать из > реально импортированного арматура (шаг 2) и править по факту, а не по > памяти. Метки на теле в облачном сервисе именуются по их собственному > словарю. --- ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | `enum "Y_FORWARD" not found` | Устаревшее имя оси в скрипте | В Blender 5.x передавать `'Y'`/`'-Z'` и т.п. | | Ноги «плывут», рост скелета 180 ед. | FBX в сантиметрах, сцена в метрах | Масштаб исходному объекту `0.01` **до** ретаргета | | `AttributeError: 'Action' object has no attribute 'fcurves'` | Blender 5.x: slotted Actions | Идти через `action.layers[0].strips[0].channelbags[0].fcurves` | | Один кадр из 1000 «дёргается» | Одиночный ключ, до него кривая уходила в сторону | `AUTO_CLAMPED` на хэндлах всех ключей | | Персонаж идёт боком / вверх ногами | Перепутаны `axis_forward` и `axis_up` | Проверить на первом кадре; смотреть на позу, а не на оси | | `enum "LIMIT_LOCATION" not found` у модификатора кривой | В 5.2 этот тип называется `LIMITS` | Перебор: `NULL, GENERATOR, FNGENERATOR, ENVELOPE, CYCLES, NOISE, LIMITS, STEPPED, SMOOTH` | | После запекания констрейнты остались | Не пересоздали риг / запекали не тот объект | Проверить `pose.bones[...].constraints` после `nla.bake` | --- ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 57: FACE MOCAP ARKIT](setup_57_face_mocap_arkit.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 02: GENESISCORE LOCAL (OLLAMA)](setup_02_genesiscore_local.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 04: HEADLESS RENDER (CLI / CI/CD)](setup_04_headless_render.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [P44: MOCAP RETARGETING](../prompts/44_mocap_retarget.md), [SETUP 05: RENDER FARM (FLAMENCO)](setup_05_render_farm.md) - **Теги:** #mocap, #setup --- # SETUP 57: FACE MOCAP ARKIT Tags: #mocap #setup #vr # SETUP 57: FACE MOCAP ARKIT **ID:** SETUP-57 | **Type:** Face Cap | **Difficulty:** ⭐⭐ | **Time:** 20 min **RU/EN Bilingual** > Когда применять: когда нужно записать лицо на iPhone с приложением для трекинга > и превратить поток blendshape-весов в анимацию Blender. --- ## 🎯 ЦЕЛЬ Собрать конвейер «iPhone → сеть → shape keys → запечённая анимация лица» и разобрать три места, где он ломается: несовпадение имён blendshape-ов, джиттер значений и рассинхрон звука. Все вызовы API проверены на Blender 5.2.1 LTS. --- ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | iPhone | с приложением лицевого трекинга ARKit | Источник blendshape-весов | | Blender | 5.x (проверено 5.2.1) | Приём по UDP, shape keys, запекание | | Сеть | одна подсеть с телефоном и машиной | UDP не ходит через NAT сам | | Лицо в Blender | меш с shape keys | Цель данных | --- ## 🛠️ НАСТРОЙКА ### 1. Создать shape keys под набор blendshape-ов ARKit отдаёт фиксированный набор именованных blendshape-локаций. Названия задаёт Apple, и это **ваша точка отказа**: если в сцене ключ называется иначе, значение придёт в никуда. Ключи создаются заранее, до записи. ```python # setup_face_shapes.py — создать shape keys по списку имён. import bpy # Подмножество имён, принятых в ARKit-совместимых пайплайнах. # Полный список — из документации Apple по ARKit (ARFaceAnchor); # он расширялся между версиями iOS, поэтому сверяйтесь с вашей версией. ARKIT_SHAPES = [ "browDownLeft", "browDownRight", "browInnerUp", "browOuterUpLeft", "browOuterUpRight", "jawOpen", "jawForward", "jawLeft", "jawRight", "mouthClose", "mouthDimpleLeft", "mouthDimpleRight", "mouthFrownLeft", "mouthFrownRight", "mouthFunnel", "mouthLeft", "mouthLowerDownLeft", "mouthLowerDownRight", "mouthPressLeft", "mouthPressRight", "mouthPucker", "mouthRight", "mouthRollLower", "mouthRollUpper", "mouthShrugLower", "mouthShrugUpper", "mouthSmileLeft", "mouthSmileRight", "mouthStretchLeft", "mouthStretchRight", "mouthUpperUpLeft", "mouthUpperUpRight", "noseSneerLeft", "noseSneerRight", "tongueOut", "eyeBlinkLeft", "eyeBlinkRight", "eyeLookDownLeft", "eyeLookInLeft", "eyeLookOutLeft", "eyeLookUpLeft", "eyeSquintLeft", "eyeWideLeft", "eyeLookDownRight", "eyeLookInRight", "eyeLookOutRight", "eyeLookUpRight", "eyeSquintRight", "eyeWideRight", ] def build_face_shapes(obj): if obj.type != 'MESH': raise ValueError("нужен меш, не арматура") if not obj.data.shape_keys: obj.shape_key_add(name="Basis", from_mix=False) # проверенная сигнатура keys = obj.data.shape_keys keys.use_relative = True created, missing = 0, [] for name in ARKIT_SHAPES: kb = keys.key_blocks.get(name) if kb is None: kb = obj.shape_key_add(name=name, from_mix=False) created += 1 kb.slider_min, kb.slider_max = 0.0, 1.0 kb.value = 0.0 if not kb.name: missing.append(name) return created, len(keys.key_blocks) print("created/total:", build_face_shapes(bpy.data.objects["FACE"])) ``` > [!NOTE] > Полный и актуальный перечень blendshape-локаций ARKit нужно взять из > документации Apple по `ARFaceAnchor` для вашей версии iOS: набор расширялся, > и статья не может быть источником истины здесь. Всё, что нужно от скрипта, — > это чтобы имена ключей в сцене совпадали с тем, что приходит по сети. ### 2. Принять поток по UDP ```python # face_receiver.py — запускать в фоне Blender: blender -b face_receiver.py import bpy, socket, json HOST, PORT = "0.0.0.0", 11111 # порт выбирается в приложении на телефоне sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind((HOST, PORT)) FACE = bpy.data.objects.get("FACE") if FACE is None: raise SystemExit("нет объекта FACE — запусти setup_face_shapes.py первым") def apply_payload(payload): """payload — словарь вида {"blendshapes": {"jawOpen": 0.42, ...}}.""" keys = FACE.data.shape_keys.key_blocks applied, unknown = 0, [] for name, value in payload.get("blendshapes", {}).items(): kb = keys.get(name) if kb is None: unknown.append(name) # тело без этого ключа continue kb.value = max(0.0, min(1.0, float(value))) applied += 1 return applied, unknown while True: data, _ = sock.recvfrom(65536) apply_payload(json.loads(data.decode("utf-8"))) ``` > [!NOTE] > Формат пакета задаёт приложение на телефоне: ключи, типы (float/percent) и > частоту отправки менялись между приложениями и версиями. Схема > `{"blendshapes": {...}}` — распространённая, но не гарантия. Приложите к > статье один реальный дамп пакета и печатайте `data[:200]`, чтобы увидеть > фактические ключи, вместо того чтобы угадывать их. ### 3. Запечь значения в ключи Живое значение в `key_blocks[...]` не переживёт перезагрузку файла. Запись в кривые — обязательный шаг. ```python # bake_face.py — после записи. Значения уже стоят в kb.value. FACE = bpy.data.objects["FACE"] keys = FACE.data.shape_keys scene = bpy.context.scene # Чистка лишних ключей: оставляем только те, где значение отличается от базы. for kb in keys.key_blocks: if kb.name == "Basis": continue if abs(kb.value) < 0.001: keys.key_blocks.remove(kb) else: kb.value = 0.0 action = keys.animation_data.action for layer in action.layers: for strip in layer.strips: for cb in strip.channelbags: for fc in cb.fcurves: print(fc.data_path, len(fc.keyframe_points)) ``` F-кривая shape key в Blender 5.x лежит по пути `key_blocks["<имя>"].value` — проверено на 5.2.1. Сам `action.fcurves` больше не существует. ### 4. Разогнать джиттер ```python def calm(action, factor=0.6): """Джиттер с телефона — это шум в каждом кадре. Лечится усреднением по соседним кадрам, а не 'сглаживанием' в UI.""" for layer in action.layers: for strip in layer.strips: for cb in strip.channelbags: for fc in cb.fcurves: kps = fc.keyframe_points prev = [kp.co.y for kp in kps] for i, kp in enumerate(kps): before = prev[i - 1] if i else kp.co.y after = prev[i + 1] if i + 1 < len(prev) else kp.co.y kp.co.y = before * (1 - factor) / 2 + kp.co.y * factor + after * (1 - factor) / 2 kp.handle_left_type = kp.handle_right_type = 'AUTO_CLAMPED' fc.update() ``` `factor` — не «сила сглаживания», а вес текущего кадра: меньше значение = плавнее, но с задержкой. Для рта `0.6` обычно достаточно, для `eyeWide*` значение ближе к `0.9`. ### 5. Проверить результат ```python # Диагностика, которую видно на графике: у каждой кривой есть min/max. for layer in action.layers: for strip in layer.strips: for cb in strip.channelbags: for fc in cb.fcurves: ys = [kp.co.y for kp in fc.keyframe_points] if min(ys) < -0.001 or max(ys) > 1.001: print("ВЫШЕЛ ЗА 0..1:", fc.data_path, min(ys), max(ys)) if len(ys) < 2: print("ОДИНОЧНЫЙ КЛЮЧ (джиттер):", fc.data_path) ``` Плюс визуальная проверка в UI: Graph Editor → Channels для `key_blocks["jawOpen"].value`. Плоская линия с одиночными пиками = джиттер, а не мимика. ### 6. Свести с аудио ```python # Звук кладётся в секвенсор, а не в сцену: в Blender 5.x коллекция полос # называется strips, класс полосы — Strip. import bpy sc = bpy.context.scene se = sc.sequence_editor_create() snd = se.strips.new_sound(name="vo_take01", filepath="//audio/vo_take01.wav", channel=1, frame_start=1) snd.volume = 1.0 # Метка на кадре, где актёр начинает говорить: дальше по ней # выравниваются глаза, если они пишутся отдельным проходом. sc.timeline_markers.new("lipsync_start", frame=42) ``` ### 7. Выгрузить в движок ```python # Morph targets уезжают вместе с анимацией в GLB/FBX. bpy.ops.export_scene.gltf( filepath="//exports/face_take01.glb", export_format='GLB', export_morph=True, # проверенный параметр в 5.2 export_morph_animation=True, export_morph_normal=True, export_animations=True, ) ``` --- ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```python # Диагностика: пришло ли что-то по сети blender -b --python-expr "import bpy;print('face keys:', len(bpy.data.objects['FACE'].data.shape_keys.key_blocks))" ``` | Параметр | Значение | Когда менять | |---|---|---| | `slider_min/max` | `0.0 / 1.0` | Всегда: ARKit отдаёт нормализованные 0..1 | | `use_relative` | `True` | Абсолютные ключи ломают экспорт morph-таргетов | | `from_mix=False` | Всегда | Иначе новый ключ создаётся из текущей смеси, а не из Basis | | `factor` в `calm()` | `0.6` для рта, `0.9` для глаз | Больше — задержка, меньше — остаточный джиттер | | Порт UDP | задаётся в приложении | Телефон и машина должны быть в одной подсети | | `new_sound` | в секвенсор | Голос — не часть 3D-сцены, а дорожка монтажа | --- ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Значения приходят, лицо не двигается | Имена ключей в сцене не совпадают с присылаемыми | Печатать `unknown` из `apply_payload` и переименовать ключи | | `AttributeError: 'Action' object has no attribute 'fcurves'` | Blender 5.x, slotted Actions | Путь `action.layers[0].strips[0].channelbags[0].fcurves` | | Литтер идёт позже голоса | Сглаживание добавило задержку, плюс запись не от синхрона | Снизить `factor`; метку ставить по звуку, а не по началу записи | | Челюсть «дрожит» в статике | Джиттер телефона не убран | `calm()` перед экспортом, либо `bpy.ops.graph.smooth` | | Морда «плывёт» между позами | Создавали ключи без `from_mix=False` | Удалить ключи и создать заново с Basis | | Приложение не подключается | Разные подсети / файрвол блокирует UDP | Проверить `netstat -an \| findstr :<порт>` и правило брандмауэра | | Морфы не доехали в движок | Не включён `export_morph` | Проверить экспорт заново с `export_morph=True` | --- ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 56: MOCAP ROKOKO / MOVE AI](setup_56_mocap_rokoko.md), [SETUP 58: VR SCENE INSPECTION](setup_58_vr_scene_inspection.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 02: GENESISCORE LOCAL (OLLAMA)](setup_02_genesiscore_local.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [P44: MOCAP RETARGETING](../prompts/44_mocap_retarget.md), [SETUP 04: HEADLESS RENDER (CLI / CI/CD)](setup_04_headless_render.md) - **Теги:** #mocap, #setup, #vr --- # SETUP 58: VR SCENE INSPECTION Tags: #setup #vr # SETUP 58: VR SCENE INSPECTION **ID:** SETUP-58 | **Type:** VR/XR | **Difficulty:** ⭐⭐ | **Time:** 20 min **RU/EN Bilingual** > Когда применять: когда масштаб сцены, высоты и перекрытия невозможно оценить > на мониторе — а проверить их надо до рендера. --- ## 🎯 ЦЕЛЬ Провести сцену в VR-сессии Blender, зафиксировать реальный масштаб и наложить аннотации, которые переживут сессию. Отдельно и важно: **API XR в Blender 5.x поменялся**. Проверено на 5.2.1 — оператора `bpy.ops.xr.start_session` и свойства `scene.xr` больше нет, есть `bpy.ops.wm.xr_session_toggle` и `wm.xr_session_settings`. Старые рецепты из интернета на этой версии не сработают. --- ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Blender | 5.x (проверено 5.2.1) | XR-сессия, навигация, аннотации | | Сборка с OpenXR | `bpy.app.build_options.xr_openxr == True` | Иначе сессия не запустится | | HMD и рантайм | OpenXR-совместимый (Quest Link / SteamVR / Meta XR) | Вывод картинки в шлем | | Комната с реальным масштабом | 2–3 м в длину | Сверять ощущения с метрами | --- ## 🛠️ НАСТРОЙКА ### 1. Проверить, что XR вообще собран ```python # Проверка до того, как надели шлем. В фоне не запускается — только читается. import bpy bo = bpy.app.build_options print("openxr:", bo.xr_openxr) print("gpu cycles:", bo.cycles, "| compositor:", bo.compositor_cpu) ``` Если `xr_openxr` — `False`, сессия не поднимется ни при каких настройках: нужен другой дистрибутив Blender. ### 2. Настроить масштаб и отсечение до входа в сессию ```python # xr_preflight.py — запускать в UI-сессии перед стартом XR. import bpy wm = bpy.context.window_manager xr = wm.xr_session_settings # проверенный тип: XrSessionSettings xr.use_absolute_tracking = True # world-scale: 1 юнит = 1 метр xr.use_positional_tracking = False # без room-scale, чтобы «пол» не прыгал xr.base_scale = 1.0 # базовая позиция пользователя относительно мира xr.view_scale = 1.0 # масштаб просмотра (не гейт) xr.clip_start = 0.01 # проверенное значение по умолчанию xr.clip_end = 1000.0 # проверенное значение по умолчанию xr.show_floor = True xr.show_controllers = True # проверенные значения enum в 5.2: DARK, LIGHT, DARK_RAY, LIGHT_RAY xr.controller_draw_style = 'DARK' xr.fly_speed = 2.0 print("scale ok:", xr.base_scale, xr.view_scale, xr.clip_start, xr.clip_end) ``` `clip_start = 0.01` обязателен: при стандартном «внутри» ближняя геометрия пропадает, и кажется, что в стене дыра. ### 3. Знать актуальные операторы навигации В 5.2.1 в `bpy.ops.wm` есть ровно это (проверено перечислением): | Оператор | Что делает | |---|---| | `wm.xr_session_toggle` | Войти/выйти из XR-сессии | | `wm.xr_navigation_teleport` | Телепорт по указателю; есть `range`, `selectable_only`, `force` | | `wm.xr_navigation_fly` | Полёт; есть `mode`, `fly_speed_factor`, `turn_speed_factor`, `snap_turn_threshold` | | `wm.xr_navigation_grab` | Сдвиг/поворот/масштаб мира; есть `lock_location`, `lock_rotation`, `lock_scale` | | `wm.xr_navigation_reset` | Сброс навигации; принимает `location`, `rotation`, `scale` | | `wm.xr_navigation_swap_hands` | Поменять руки местами | ```python # Включить сессию из скрипта/UI-кнопки: bpy.ops.wm.xr_session_toggle() # ...и выключить тем же вызовом. # Телепорт на расстояние до 10 м только по selectable-объектам: bpy.ops.wm.xr_navigation_teleport(range=10.0, selectable_only=True) ``` > [!NOTE] > На более старых версиях Blender (до 5.x) вход в сессию выглядел как > `bpy.ops.xr.start_session()`, а настройки жили в `scene.xr`. Если вы работаете > на такой версии — сверяйтесь со своей: точный набор операторов > перечисляется через `dir(bpy.ops.wm)`. ### 4. Поставить в сцену объекты известного размера Это единственный надёжный способ проверить масштаб: не «на глаз», а сравнением с эталоном. ```python # scale_reference.py — эталонные объекты в сцене. import bpy def make_reference(name, size, color=(1, 0, 0, 1)): bpy.ops.mesh.primitive_cube_add(size=1.0, location=(0, 0, 0)) ob = bpy.context.object ob.name = name ob.scale = (size, size, size) ob.location = (0, 0, size / 2) # стоит на полу ob.hide_render = True # в рендер не идёт mat = bpy.data.materials.new(f"{name}_mat") mat.diffuse_color = color ob.data.materials.append(mat) return ob make_reference("REF_door", 2.05) # дверной проём make_reference("REF_eye", 1.70) # рост человека make_reference("REF_step", 0.18) # ступенька def audit_scale(objects, tolerance=0.01): """Напечатать габариты, чтобы сравнить с замыслом.""" for ob in objects: dims = ob.dimensions flag = "OK" if abs(dims.z - 0.0) < tolerance else "" print(f"{ob.name}: {dims.x:.2f} x {dims.y:.2f} x {dims.z:.2f} {flag}") audit_scale([bpy.data.objects[n] for n in ("REF_door", "REF_eye", "REF_step")]) ``` Результат, который нужно увидеть в VR: дверь 2.05 м ощущается дверью, а не щелью; уровень глаз камеры — около 1.6–1.7 м; если объекты «плывут» при ходьбе, виноват не рендер, а `use_absolute_tracking` и единицы сцены. ### 5. Записывать находки, а не держать их в голове ```python # annotate.py — аннотация = текстовый блок в .blend + маркер на таймлайне. import bpy, datetime def annotate(text, frame=None, kind='note'): name = f"XR_{kind}_{datetime.datetime.now():%H%M%S}" txt = bpy.data.texts.get(name) or bpy.data.texts.new(name) txt.clear() txt.write(text) if frame is not None: bpy.context.scene.timeline_markers.new(name, frame=frame) return txt.name print(annotate("коридор: потолок ниже 2.4 m — опустить на 0.15", frame=120)) print(annotate("лестница: ступень 0.18 m, ок", kind='ok')) # Объектные заметки — чтобы подписать проблемный объект прямо в сцене: obj = bpy.data.objects.get("Corridor_Ceiling") if obj: obj["xr_review"] = "too low, -0.15" obj["xr_review_frame"] = 120 ``` Текстовый блок переживает перезагрузку файла и попадает в `.blend`, маркер — в таймлайн, а `custom property` висит на самом объекте: три разных носителя под три разных вопроса (что решено, где в таймлайне, какой объект виноват). ### 6. Забрать позицию пользователя программно ```python # Если нужно понять, где стоял оператор в момент находки: state = bpy.context.window_manager.xr_session_state # XrSessionState print("viewer:", state.viewer_pose_location) print("navigation:", state.navigation_location, state.navigation_rotation) print("scale:", state.navigation_scale) print("actionmaps:", [m.name for m in state.actionmaps]) ``` Полезно, чтобы приложить к отчёту «с этого ракурса» координату наблюдателя: иначе через неделю никто не вспомнит, о какой именно двери речь. ### 7. Снять стерео-кадр для архива ```python # Вариант для рендера, а не для OpenGL-захвата с экрана. # views_format — обычный enum; stereo_views — коллекция имён видов, # по умолчанию содержит 'left' и 'right' (проверено на 5.2.1). sc = bpy.context.scene sc.render.use_multiview = True sc.render.views_format = 'STEREO_3D' print("views:", [getattr(v, "name", v) for v in sc.render.stereo_views]) sc.render.filepath = "//xr_check/frame_" bpy.ops.render.render(write_still=True) ``` > [!NOTE] > `bpy.ops.render.opengl()` (захват вьюпорта) требует графического контекста и > в `blender -b` не работает — для headless-сценария используйте обычный > `bpy.ops.render.render()`. Порядок видов (`left`/`right`) и их имена > зависят от того, как настроен мульти-view в вашей сцене, поэтому печатайте > их, а не задавайте строкой. --- ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```bash # Проверка сборки без GUI: blender -b --python-expr "import bpy;print('openxr', bpy.app.build_options.xr_openxr)" ``` | Параметр | Значение | Когда менять | |---|---|---| | `use_absolute_tracking` | `True` | `False` — если нужно world-scale 1:100 для миниатюр | | `clip_start` | `0.01` | Ниже — z-fighting; выше `0.02` — исчезает геометрия у лица | | `clip_end` | `1000.0` | Для интерьеров хватит `100` — экономит глубину | | `base_scale` / `view_scale` | `1.0` | `view_scale` не трогать, если не собираете gizmo-режим | | `wm.xr_navigation_teleport(range=)` | `10.0` | Радиус телепорта: меньше — не упасть в пролёт | | `wm.xr_navigation_grab(lock_scale=)` | `True` | Запрет масштабирования мира при осмотре | | Габариты сцены | метры | Сцена в сантиметрах + XR = «всё в 100 раз меньше» | --- ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Кнопка XR в UI серая | Сборка без OpenXR | Проверить `bpy.app.build_options.xr_openxr`, поставить подходящий дистрибутив | | `bpy.ops.xr.start_session` → AttributeError | Старый рецепт для версии до 5.x | `bpy.ops.wm.xr_session_toggle()` | | Геометрия пропадает у лица и под ногами | `clip_start` слишком большой | `clip_start = 0.01` | | Сцена «микроскопическая» | Сцена в сантиметрах, а XR ждёт метры | Масштабировать сцену в 0.01 либо задать `use_absolute_tracking = False` | | Пол прыгает при ходьбе | Включён room-scale / позиционное отслеживание | `use_positional_tracking = False` | | Телепорт не срабатывает | Некуда телепортироваться: пустота | `selectable_only=False`, увеличить `range` | | Шлем не видит HMD | Рантайм не подхватил трекер | Проверить, что рантайм запущен и видит HMD **до** входа в сессию | | `render.opengl` падает в headless | Нужен графический контекст | Использовать `bpy.ops.render.render()` | --- ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 57: FACE MOCAP ARKIT](setup_57_face_mocap_arkit.md), [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 02: GENESISCORE LOCAL (OLLAMA)](setup_02_genesiscore_local.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 04: HEADLESS RENDER (CLI / CI/CD)](setup_04_headless_render.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 05: RENDER FARM (FLAMENCO)](setup_05_render_farm.md), [SETUP 06: CUSTOM MCP TOOLS](setup_06_custom_mcp_tools.md) - **Теги:** #setup, #vr --- # SETUP 59: WEB VIEWER EXPORT Tags: #pipeline #setup #web # SETUP 59: WEB VIEWER EXPORT **ID:** SETUP-59 | **Type:** Web 3D | **Difficulty:** ⭐⭐ | **Time:** 20 min **RU/EN Bilingual** > Когда применять: когда модель из Blender нужно показать на сайте — в > презентации, в портфолио, в документации. --- ## 🎯 ЦЕЛЬ Собрать минимальный веб-вьювер: GLB с компрессией Draco, локальный сервер для проверки, статическая публикация на GitHub Pages. Важное уточнение, проверенное на Blender 5.2.1: **встроенный glTF-экспортёр не умеет KTX2**. Перечисление `export_image_format` там ровно `AUTO, JPEG, WEBP, NONE`. KTX2 — это отдельный пост-проход. --- ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Blender | 5.x (проверено 5.2.1) | glTF-экспорт с Draco и WebP | | three.js | актуальный релиз | Лоадер и рендер в браузере | | Локальный HTTP-сервер | любой (`python -m http.server`) | Проверка до публикации | | glTF-Transform | по необходимости | Пост-оптимизация и KTX2 | | GitHub Pages | бесплатный тариф | Хостинг статики | --- ## 🛠️ НАСТРОЙКА ### 1. Подготовить сцену ```python # prepare_viewer.py — перед экспортом: без applied-модификаторов и мусора. import bpy sc = bpy.context.scene sc.unit_settings.system = 'METRIC' sc.unit_settings.scale_length = 1.0 # glTF ждёт метры # Ограничить то, что поедет на сайт: свет внутри glTF — отдельная история, # лучше оставить свет на стороне viewer'а. for ob in sc.objects: if ob.type not in {'MESH', 'EMPTY', 'ARMATURE', 'CAMERA'}: ob.hide_viewport = True ``` ### 2. Экспортировать GLB ```python # export_viewer.py — все параметры проверены вызовом в Blender 5.2.1. import bpy bpy.ops.object.select_all(action='DESELECT') for ob in bpy.context.scene.objects: if ob.type in {'MESH', 'EMPTY'}: ob.select_set(True) bpy.context.view_layer.objects.active = bpy.context.view_layer.objects.active bpy.ops.export_scene.gltf( filepath="//web/viewer.glb", export_format='GLB', # встроенный .glb, не .gltf + буфер use_selection=True, export_apply=True, # применить модификаторы в экспорт export_yup=True, # glTF: +Y вверх, Blender: +Z export_materials='EXPORT', # EXPORT / PLACEHOLDER / VIEWPORT / NONE export_image_format='WEBP', # AUTO / JPEG / WEBP / NONE — KTX2 тут нет export_image_quality=80, export_draco_mesh_compression_enable=True, export_draco_mesh_compression_level=6, # целое 0..10, 6 — разумный компромисс export_texture_dir="//web/textures", # текстуры рядом, а не внутри GLB ) ``` Проверить, что файл действительно стал меньше и что текстуры на месте: ```bash ls -lh web/viewer.glb web/textures/ python -c "import json,sys;print(open('web/viewer.glb','rb').read(4))" # должно быть 'glTF' ``` `glTF` в первых четырёх байтах — самая дешёвая проверка, что это не HTML страницы ошибки, скачанная под именем `.glb`. ### 3. Альтернатива Draco — Meshopt ```python # meshopt сжимает лучше Draco, но требует декодера на стороне браузера. bpy.ops.export_scene.gltf( filepath="//web/viewer_meshopt.glb", export_format='GLB', export_apply=True, export_meshopt_compression_enable=True, # проверенный параметр в 5.2 export_image_format='WEBP', ) ``` ### 4. KTX2 как пост-проход ```bash # KTX2 делается после экспорта Blender'ом — в самом экспортёре такой опции нет. # Инструменты: KTX-Software (toktx) либо glTF-Transform. # Точные ключи проверяйте в --help своих версий. npx @gltf-transform/cli optimize web/viewer.glb web/viewer.opt.glb --texture-compress webp ``` > [!NOTE] > Имена подкоманд и флагов glTF-Transform менялись между мажорными версиями > (команда `optimize` и параметры текстур особенно). Сверяйтесь с > `npx @gltf-transform/cli --help` и не принимайте эту строку за > гарантированно рабочую. KTX2-энкодер (KTX-Software) ставится отдельно: > https://github.com/KhronosGroup/KTX-Software ### 5. Страница вьювера ```html Viewer ``` ### 6. Проверить локально и поставить бюджет размера Открывать `index.html` двойным кликом нельзя: браузер запрещает загрузку модулей и fetch с `file://`. Нужен сервер — это не формальность, а причина «пустого экрана». ```bash cd web python -m http.server 8000 # затем http://localhost:8000/ и вкладка Network в devtools ``` | Модель | Размер | Способ | Примечание | |---|---|---|---| | 1–3 тыс. треугольников | 200–600 КБ | без сжатия | Достаточно лоу-поли, ничего не нужно | | 5–20 тыс. | 1–4 МБ | Draco | Бюджет: под 5 МБ на модель | | 20–100 тыс. | 4–15 МБ | Draco + WebP + KTX2 | KTX2 заметно жмёт текстуры | | > 100 тыс. | > 15 МБ | не грузить в браузер | Ссылка на демо-рендер или онлайн-просмотрщик | ### 7. Опубликовать ```yaml # .github/workflows/pages.yml name: Deploy viewer to Pages on: push: paths: ['web/**', '.github/workflows/pages.yml'] permissions: contents: read pages: write id-token: write concurrency: group: pages cancel-in-progress: true jobs: deploy: runs-on: ubuntu-latest environment: name: github-pages url: ${{ steps.deploy.outputs.page_url }} steps: - uses: actions/checkout@v4 - uses: actions/configure-pages@v5 - uses: actions/upload-pages-artifact@v3 with: path: web - id: deploy uses: actions/deploy-pages@v4 ``` > [!NOTE] > Версии actions (`@v4`, `@v5`, `@v3`) меняются; сверьтесь с текущими > релизами в документации GitHub Actions. Альтернатива без экшенов — > ветка `gh-pages` и `git push --force origin gh-pages`. --- ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ ```bash # Проверка, что GLB — валидный, а не HTML-страница ошибки python - <<'PY' from pathlib import Path p = Path("web/viewer.glb") head = p.read_bytes()[:4] print("magic:", head, "OK" if head == b"glTF" else "НЕ GLB", p.stat().st_size // 1024, "KB") PY ``` | Параметр | Значение | Когда менять | |---|---|---| | `export_image_format` | `WEBP` | `AUTO` — если часть текстур с альфой ломается | | `export_draco_mesh_compression_level` | `6` | Больше — меньше файл, дольше распаковка на клиенте | | `export_apply` | `True` | Модификаторы применятся только к экспорту, сцена останется живой | | `export_texture_dir` | отдельная папка | Для кеширования текстур отдельно от модели | | `unit_settings.scale_length` | `1.0` | Иначе модель приедет в сантиметрах и станет «игрушечной» | | Draco decoder path | тот же origin | Иначе CORS-ошибка и пустой экран без внятного сообщения | --- ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Чёрный экран, в консоли ошибка импорта | Открыт `file://`, а не http | Поднять `python -m http.server` | | `RuntimeError: Failed to fetch` на декодере | Декодер Draco лежит на другом домене | Положить `draco/` рядом со страницей и задать `setDecoderPath` | | Модель в 100 раз меньше ожидаемого | Сцена в сантиметрах при экспорте в метры | `unit_settings.scale_length = 1.0`, объекты в метрах | | Текстуры чёрные | Нет света во вьювере либо слишком яркий WebP | Добавить свет в сцену three.js; проверить альфу | | KTX2 не появился | Встроенный экспортёр его не поддерживает | Пост-проход через KTX-Software / glTF-Transform | | Модель «мигает» | `use_selection=True`, а выбор менялся | Экспортировать из чистой копии сцены | | Анимация не играет | `export_animations` выключен | Включить `export_animations=True` и проверить `export_animation_mode` | | Страница не обновилась после правки | Кеш CDN | Версия файла в имени или `?v=<дата>` | --- ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 08: ASSET PIPELINE](setup_08_asset_pipeline.md), [SETUP 14: BLENDER + GODOT PIPELINE](setup_14_godot_pipeline.md), [SETUP 38: FAB.COM (EPIC) PIPELINE](setup_38_fab_epic.md), [SETUP 45: HUNYUAN3D 2.1 LOCAL](setup_45_hunyuan3d_21.md), [SETUP 49: FFMPEG + VSE PIPELINE](setup_49_ffmpeg_vse_pipeline.md) - **По темам:** [P39: EXPORT PRESETS (GLB/FBX/USD)](../prompts/39_export_presets.md), [SETUP 55: AI UPSCALE PIPELINE](setup_55_ai_upscale_pipeline.md), [CASE 02: GAME PROPS PIPELINE](../cases/case_02_game_props.md) - **Теги:** #pipeline, #setup, #web --- # SETUP 60: SHOTGRID / FTRACK SYNC Tags: #production #setup # SETUP 60: SHOTGRID / FTRACK SYNC **ID:** SETUP-60 | **Type:** Production | **Difficulty:** ⭐⭐⭐ | **Time:** 35 min **RU/EN Bilingual** > Когда применять: когда над шотом работают несколько человек и нужно, чтобы > «версия 3», «Mount» и «в работе» значили одно и то же в трекере и в Blender. --- ## 🎯 ЦЕЛЬ Описать порядок работы между Blender и продакшн-трекером (ShotGrid / ftrack) так, чтобы он не зависел от конкретных полей и конкретной версии API. Статья **намеренно не приводит ни одного имени поля или метода API** трекера: схема сущностей и набор полей настраиваются в каждой студии по-своему, а имена endpoints меняются между версиями. Придуманное имя метода хуже, чем его отсутствие: по нему начнут писать интеграцию. --- ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Blender | 5.x (проверено 5.2.1) | Публикация версий, метаданные, аннотации | | ShotGrid или ftrack | версия вашей студии | Хранение шотов, задач, версий, заметок | | Python | 3.10+ | Скрипт публикации | | Сетевое хранилище | NFS/SMB | Куда кладутся версии `.blend` | | ffmpeg | 7.x+ (проверено 9.0.1) | Превью-ролик для ревью | --- ## 🛠️ НАСТРОЙКА ### 1. Разделить ответственность: что читаем, что пишем ``` Трекер (источник правды по статусам) │ pull: список моих задач, имена шотов, заметки ревью ▼ ┌──────────────────────┐ push: новая версия + метаданные + превью │ Blender │ ────────────────────────────────────────────▶ └──────────────────────┘ ``` Односторонняя запись — в трекер. Чтение — в обе стороны, но без конфликтов: люди не правят `.blend` через трекер, только статусы и комментарии. ### 2. Описать маппинг файлом, а не кодом Единственный артефакт, который реально переживёт смену версии трекера, — ваш собственный файл соответствия. Он не «API трекера», он — контракт вашего пайплайна. Обратите внимание: внутри описаны **роли**, а не имена полей студии. ```json { "tracker": "shotgrid", "roles": { "shot_code": {"from": "code of the shot entity", "note": "обычно код вроде sh010"}, "task_name": {"from": "name of the task entity", "note": "в студии может быть 'Mount' или 'Lighting'"}, "version_id": {"from": "human-readable version identifier", "note": "берём из Publish, не из имени файла"}, "review_notes": {"from": "annotation threads on the version", "note": "только текст, без вложений"}, "storage_root": {"from": "per-project storage root", "note": "куда публикуем .blend"} }, "publish": { "blend_subdir": "blend", "preview_subdir": "preview", "manifest_name": "manifest.json", "extensions": [".blend", ".exr", ".mp4"] }, "status_policy": { "push": ["in_progress", "ready_for_review", "final"], "pull": ["todo", "in_progress", "on_hold"] } } ``` ### 3. Опубликовать версию из Blender ```python # publish_version.py — Blender пишет файлы и манифест, трекеру достаётся # только этот манифест. Никаких полей трекера скрипт не знает. import bpy, json, os, hashlib, datetime, shutil def sha256(path, chunk=1 << 20): h = hashlib.sha256() with open(path, "rb") as fh: for block in iter(lambda: fh.read(chunk), b""): h.update(block) return h.hexdigest() def publish(shot_code, task_name, storage_root, frame_range): stamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") out = os.path.join(storage_root, "blend", shot_code, task_name, stamp) os.makedirs(out, exist_ok=True) # Упаковать и сжать: в трекер уходит маленький файл (проверено в 5.2) bpy.ops.file.pack_all() blend = os.path.join(out, f"{shot_code}_{task_name}.blend") bpy.ops.wm.save_as_mainfile(filepath=blend, compress=True) manifest = { "shot_code": shot_code, "task": task_name, "published": stamp, "blender": bpy.app.version_string, "frame_range": list(frame_range), "files": [{"name": os.path.basename(blend), "sha256": sha256(blend), "bytes": os.path.getsize(blend)}], } mpath = os.path.join(out, "manifest.json") with open(mpath, "w", encoding="utf-8") as fh: json.dump(manifest, fh, ensure_ascii=False, indent=2) return mpath print(publish("sh010", "Mount", "//storage/shots", (1, 250))) ``` `manifest.json` — это то, что делает публикацию проверяемой: трекеру не нужен доступ к вашим дискам, достаточно файла с хэшами. И когда версия «пропала», первый вопрос — совпадает ли `sha256`. ### 4. Сделать превью для ревью ```bash # Превью — отдельная папка версии, отдельным битрейтом. # Проверено на ffmpeg 9.0.1: ffmpeg -y -framerate 24 -start_number 1 -i exr/sh010_####.exr \ -c:v libx264 -crf 20 -pix_fmt yuv420p -movflags +faststart \ "preview/sh010_Mount_20261002_101500.mp4" ``` Ограничение по длине — организационное, а не техническое: ревьюер смотрит первые 15 секунд. Если превью длиннее, в нём почти никто не досмотрит до конца. ### 5. Забрать заметки ревью в Blender Заметки ревью — единственное, что должно приходить в 3D обратно, и только в одном-единственном виде: маркеры на таймлайне плюс текстовый блок. Так их видит animator, который не знает, что такое трекер. ```python # pull_notes.py — читает нормализованный JSON (его собрал скрипт трекера), # а не полезет в API трекера из Blender. import bpy, json, datetime def pull_notes(path): with open(path, encoding="utf-8") as fh: notes = json.load(fh) sc = bpy.context.scene # Маркер = место + короткий текст. Всё длинное — в текстовый блок. for m in list(sc.timeline_markers): if m.name.startswith("REV_"): sc.timeline_markers.remove(m) stamp = datetime.datetime.now().strftime("%H%M") txt = bpy.data.texts.get("REVIEW_NOTES") or bpy.data.texts.new("REVIEW_NOTES") txt.clear() for n in notes["items"]: frame = int(n.get("frame") or sc.frame_start) txt.write(f"{frame}\t{n['author']}\t{n['body']}\n") sc.timeline_markers.new(f"REV_{frame}_{n['author'][:8]}", frame=frame) return len(notes["items"]) print("notes:", pull_notes("//storage/inbox/sh010_review.json")) ``` ### 6. Отдать статус и версию обратно ```python # push_status.py — Blender не ходит в трекер. Он только пишет файл, # а на стороне трекера маленький скрипт его читает и обновляет сущности. import json, os, datetime def request_status(shot_code, task_name, state, blend_manifest_path, root): assert state in ("in_progress", "ready_for_review", "final") req = { "shot_code": shot_code, "task": task_name, "state": state, "version": datetime.datetime.now().isoformat(timespec="seconds"), "manifest": os.path.relpath(blend_manifest_path, root).replace("\\", "/"), } out_dir = os.path.join(root, "requests") os.makedirs(out_dir, exist_ok=True) path = os.path.join(out_dir, f"{shot_code}_{task_name}_{req['version'].replace(':','-')}.json") with open(path, "w", encoding="utf-8") as fh: json.dump(req, fh, ensure_ascii=False, indent=2) return path print(request_status("sh010", "Mount", "ready_for_review", "//storage/shots/blend/sh010/Mount/manifest.json", "//storage")) ``` Такой «файловый мост» медленнее прямого вызова API и полностью это окупает: он переживает смену трекера, его видно в логах, и его можно проверить вручную. ### 7. Обработать мост на стороне трекера Скрипт, который читает папку запросов, — единственное место, где живёт знание о конкретном трекере. Он маленький, и его легко заменить, когда студия сменит систему. ```python # apply_requests.py — запускается на стороне трекера (по cronу или вручную). # Здесь и только здесь появляются имена полей вашего инстанса. import json, os, glob, logging ROOT = "//storage" REQUESTS = os.path.join(ROOT, "requests") log = logging.getLogger("apply_requests") def read_requests(): for path in sorted(glob.glob(os.path.join(REQUESTS, "*.json"))): with open(path, encoding="utf-8") as fh: yield path, json.load(fh) def publish_to_tracker(req): """ЗАГЛУШКА. Реализация зависит от вашего трекера и его версии: найти сущность шота по req['shot_code'], найти задачу по req['task'], создать версию, приложить превью, выставить статус req['state'].""" raise NotImplementedError( "заполняется под ваш ShotGrid/ftrack: имена полей берите у админа студии" ) for path, req in read_requests(): try: publish_to_tracker(req) os.replace(path, path + ".done") # сначала переименовать, потом слать уведомление log.info("applied %s -> %s", req["shot_code"], req["state"]) except NotImplementedError: raise except Exception as exc: log.exception("failed %s: %s", path, exc) # Файл оставляем: он будет обработан снова после починки. ``` Порядок «сначала переименовать, потом слать уведомление» — не формальность: иначе при падении между публикацией и записью статуса вы получите версию в трекере без соответствующей записи в логе, и расхождение обнаружится через месяц. ### 8. Аутентификация и доступы — только через администратора студии | Что нужно | Как это обычно устроено | |---|---| | Адрес | URL сайта ShotGrid или сервера ftrack | | Идентификация скрипта | Имя приложения/скрипта, заведённое админом | | Секрет | Токен или пароль, выданный админом | | Права | Минимально необходимые: создавать версии, читать заметки | > [!NOTE] > Имена переменных окружения, параметров аутентификации и структура ответа > API у ShotGrid и ftrack свои и меняются между версиями; в ftrack доступ > обычно оформляется через персональный API-токен, в ShotGrid — через > пару «имя сайта + имя скрипта» и ключ. Берите эти значения у того, кто > администрирует ваш инстанс, и держите их в секрете окружения CI, а не в > репозитории. Официальные страницы продуктов: https://www.ftrack.com/ и > https://www.autodesk.com/products/flow-production-tracking/ --- ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр / артефакт | Значение | Когда менять | |---|---|---| | `pipeline_map.json` | роли, а не имена полей | Единственное, что правится при переезде на другой трекер | | `manifest.json` | sha256 + версия Blender | Обязателен: без него «версия пропала» не диагностируется | | `compress=True` при сохранении | включать | Уменьшает и загрузку, и размер версии в трекере | | Имя версии | `YYYYMMDD_HHMMSS` | Сортируется лексикографически — не нужно разбирать | | `status_policy.push` | `in_progress` / `ready_for_review` / `final` | Согласовать с продюсером: кто имеет право ставить `final` | | Маркер `REV_*` | кадр + первые 8 символов автора | Animator видит это в таймлайне без открытия трекера | > [!NOTE] > Конкретные имена сущностей и полей (код шота, имя задачи, идентификатор > версии) настраиваются в каждом проекте. Статья их не приводит намеренно: > выдуманное имя поля неотличимо от настоящего, и интеграция, написанная по > выдумке, падает на первой же авторизации. --- ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Интеграция работает у одного и падает у другого | Имена полей взяты из чужого проекта | Держать маппинг в `pipeline_map.json`, а не в коде | | Статус «в работе», версия в трекере есть | Двусторонняя запись: статус затирается | Сделать трекер единственным владельцем статусов | | Версия открылась, но битые пути | `.blend` не упакован | `bpy.ops.file.pack_all()` перед `save_as_mainfile(compress=True)` | | Заметки ревью не на таймлайне | Формат JSON изменился, скрипт молча прочитал 0 items | Проверять счётчик `notes:` в выводе, а не «посмотрел — вроде ок» | | `sha256` разошёлся при проверке | Файл перезаписан поверх версии | Версии иммутабельны: новая правка — новая папка с новым штампом | | Публикация едет по второму кругу | Скрипт читает свой же запрос | Файлы запросов живут в отдельной папке и очищаются после обработки | | CI падает на авторизации | Токен в репозитории протух | Секреты в CI-секретах, ротация по расписанию | --- ## 🔗 СВЯЗИ - **Прочие в категории «Сетапы»:** [SETUP 01: BLENDER-MCP BASIC](setup_01_blender_mcp_basic.md), [SETUP 02: GENESISCORE LOCAL (OLLAMA)](setup_02_genesiscore_local.md), [SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER)](setup_03_dream_textures.md), [SETUP 04: HEADLESS RENDER (CLI / CI/CD)](setup_04_headless_render.md), [SETUP 05: RENDER FARM (FLAMENCO)](setup_05_render_farm.md) - **По темам:** [P26: MCP BASICS (BLENDER-MCP COMMANDS)](../prompts/26_mcp_basics.md), [SETUP 06: CUSTOM MCP TOOLS](setup_06_custom_mcp_tools.md), [SETUP 07: CI/CD INTEGRATION](setup_07_ci_cd.md) - **Теги:** #production, #setup --- # WF 01: TEXT-TO-SCENE Tags: #assets #pipeline #render #workflow # WF 01: TEXT-TO-SCENE **ID:** WF-01 | **Тип:** End-to-End Pipeline | **Сложность:** ⭐⭐ | **Время:** 15-30 мин --- ## 🎯 ЦЕЛЬ Полный пайплайн: текстовое описание → 3D сцена (геометрия, материалы, свет, камера, рендер) --- ## 📋 АРХИТЕКТУРА ПАЙПЛАЙНА ``` USER PROMPT │ ▼ ┌─────────────────────────────────────┐ │ LLM AGENT (OpenCode/Claude) │ │ - Парсит описание │ │ - Планирует шаги │ │ - Вызывает MCP tools │ └─────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────┐ │ BLENDER-MCP SERVER (localhost:9876)│ │ - create_object │ │ - set_material │ │ - download_polyhaven_asset │ │ - execute_blender_code │ └─────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────┐ │ BLENDER (bpy execution) │ │ - Geometry creation │ │ - Material assignment │ │ - Lighting setup │ │ - Camera positioning │ │ - Render execution │ └─────────────────────────────────────┘ │ ▼ RESULT: Rendered image / .blend file / GLB export ``` --- ## 📝 ПРИМЕРЫ ПРОМПТОВ ### Простой: Комната ``` "Создай комнату 5x5x3м: пол, 4 стены, потолок. Добавь окно 1.5x1.5м в передней стене (boolean). Материалы: стены - белая штукатурка, пол - паркет, окно - стекло. Свет: HDRI 'studio' 2k от Poly Haven. Камера: в углу, направлена в центр, 35mm. Рендер: Cycles, 128 samples, 1920x1080, сохранить //renders/room.png" ``` ### Средний: Игровая локация ``` "Создай данжон-комнату 10x10x5м в стиле dark fantasy. Стены: каменная кладка с трещинами (PBR). Пол: каменные плиты с мхом в щелях. Реквизит: 3 колонны, 2 сундука, алтарь в центре. Освещение: 3 точечных источника (огонь) + слабое HDRI 'dungeon'. Частицы: пыль в лучах света (volume scatter). Камера: низкий ракурс, DOF на алтаре. Рендер: 256 samples, denoising ON" ``` ### Сложный: Процедурный город ``` "Сгенерируй процедурный квартал 200x200м: - Сетка 10x10 зданий, случайная высота 10-50м - Архитектура: модернизм / неоклассика / футуризм (веса 40/30/30) - Дороги между зданиями, тротуары, фонари каждые 20м - Парки в 15% случаев вместо зданий - HDRI 'city_day' 4k, солнечный свет - LOD система: близкие здания высокополигональные, дальние - lowpoly - Экспорт: GLB с LOD уровнями, текстуры 2k" ``` --- ## 🔧 MCP КОМАНДЫ ПОЭТАПНО ### Этап 1: Базовая геометрия ```bash # Очистка "Удали все объекты кроме камеры и света" # Пол "Создай плоскость 'Floor' 10x10м в (0,0,0)" # Стены (4 шт.) "Создай куб 'Wall_Front' 10x0.2x3м в (0, -5, 1.5)" "Создай куб 'Wall_Back' 10x0.2x3м в (0, 5, 1.5)" "Создай куб 'Wall_Left' 0.2x10x3м в (-5, 0, 1.5)" "Создай куб 'Wall_Right' 0.2x10x3м в (5, 0, 1.5)" # Потолок "Создай плоскость 'Ceiling' 10x10м в (0,0,3)" ``` ### Этап 2: Булевы операции (окна/двери) ```bash # Окно "Создай куб 'Window_Cut' 2x0.3x1.5м в (0, -5.1, 1.5) Вычти Window_Cut из Wall_Front (DIFFERENCE)" # Дверь "Создай куб 'Door_Cut' 1x0.3x2.2м в (-2, -5.1, 1.1) Вычти Door_Cut из Wall_Front (DIFFERENCE)" ``` ### Этап 3: Материалы ```bash # Пол - паркет "Создай материал 'Mat_Parquet': BaseColor #8B7355, Roughness 0.6, Metallic 0 Присвой к Floor" # Стены - штукатурка "Создай материал 'Mat_Plaster': BaseColor #F5F0E8, Roughness 0.85, Metallic 0 Присвой к Wall_Front, Wall_Back, Wall_Left, Wall_Right, Ceiling" # Стекло "Создай материал 'Mat_Glass': Transmission 1.0, IOR 1.45, Roughness 0 Присвой к окнам (если созданы отдельные объекты)" ``` ### Этап 4: Ассеты из Poly Haven ```bash # HDRI "Найди на Poly Haven HDRI 'studio' категорию 'indoor' 2k Скачай и поставь как World Environment" # Модели (опционально) "Найди на Poly Haven модель 'chair' категорию 'furniture' 1k Скачай и размести 3 стула вокруг стола" ``` ### Этап 5: Камера и рендер ```bash # Камера "Создай камеру 'Cam_Main' в (7, -7, 4) поворот (0.9, 0, 0.8) Сделай активной: bpy.context.scene.camera = bpy.data.objects['Cam_Main'] Настрой: 35mm, DOF на объект в центре" # Рендер "Настрой Cycles: 128 samples, denoising ON, 1920x1080 Рендер и сохрани в //renders/scene_01.png" ``` --- ## 🎯 ПАРАМЕТРЫ КАЧЕСТВА | Уровень | Samples | Denoising | Resolution | Time (RTX 3080) | |---------|---------|-----------|------------|-----------------| | **Draft** | 32 | ON | 1280x720 | ~15 сек | | **Preview** | 64 | ON | 1920x1080 | ~30 сек | | **Final** | 128 | ON | 1920x1080 | ~1 мин | | **Production** | 256 | ON + OptiX | 3840x2160 | ~5 мин | --- ## 💡 ПРО ТИПЫ ### Авто-сохранение промежуточных состояний ```python # В execute_blender_code import bpy bpy.ops.wm.save_as_mainfile(filepath="//wip/scene_step_01.blend") ``` ### Версионирование ```python import datetime version = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") bpy.ops.wm.save_as_mainfile(filepath=f"//versions/scene_v{version}.blend") ``` ### Пакетный рендер вариантов ```python variants = ["day", "night", "golden_hour"] for variant in variants: # Сменить HDRI # Рендер bpy.context.scene.render.filepath = f"//renders/scene_{variant}.png" bpy.ops.render.render(write_still=True) ``` --- ## ⚠️ ЧАСТЫЕ ПРОБЛЕМЫ И РЕШЕНИЯ | Проблема | Причина | Решение | |----------|---------|---------| | Рендер чёрный | Нет света / камера неактивна | Проверить `bpy.context.scene.camera` и свет | | Текстуры розовые | UV не развернуты | `Smart UV Project` перед материалами | | Boolean не работает | Неверные нормали / non-manifold | `Recalculate Normals`, `Select Non-Manifold` | | Память (OOM) | Слишком много полигонов | `Decimate`, уменьшить samples, tiling | | Рендер завис | Бесконечный цикл в драйверах | Проверить drivers, ограничить время | --- ## 🔗 СВЯЗИ - **Прочие в категории «Воркфлоу»:** [WF 17: STOP-MOTION FELT LOOK](wf_17_stop_motion_felt.md), [WF 23: VIRTUAL PRODUCTION LED](wf_23_virtual_production_led.md), [WF 24: PHOTOGRAMMETRY CLEANUP](wf_24_photogrammetry_cleanup.md), [WF 02: IMAGE-TO-3D](wf_02_image_to_3d.md), [WF 03: TEXTURE PIPELINE](wf_03_texture_pipeline.md) - **По темам:** [CASE 03: PRODUCT VISUALIZATION](../cases/case_03_product_viz.md), [CASE 05: PROCEDURAL CITY](../cases/case_05_procedural_city.md), [CASE 07: STYLIZED BARREL (HAND-PAINT)](../cases/case_07_stylized_barrel.md), [CASE 08: SCI-FI CORRIDOR MODULAR](../cases/case_08_sci_fi_corridor.md) - **Теги:** #assets, #pipeline, #render, #workflow --- # WF 02: IMAGE-TO-3D Tags: #ai-3d #pipeline #workflow # WF 02: IMAGE-TO-3D **ID:** WF-02 | **Type:** AI Generation Pipeline | **Difficulty:** ⭐⭐ | **Time:** 10-30 min per asset --- > Когда применять: когда есть одна картинка — фото предмета, скетч, кадр из концепта — и из неё нужно за минуты получить болванку формы, с которой уже можно работать. Не финальный ассет, а блокаут. ## 🎯 ЦЕЛЬ Провести 2D-изображение через три стадии: препроцессинг картинки, генерация сетки генератором, постобработка в Blender до состояния «можно показывать и можно отдавать дальше по пайплайну». Честная оговорка, без которой статья врёт: **image-to-3D отдаёт болванку, а не ассет**. Типичный результат — непропорциональные формы, произвольный масштаб относительно реального, зеркальные UV, иногда дырявая геометрия, а текстуры может не быть вовсе. Экономия времени реальна и она большая: ручной блокаут часового объекта занимает секунды. Доводка до ассета занимает столько же, сколько и без генератора. Поэтому ценность пайплайна не в генерации, а в дисциплине: одна и та же последовательность шагов, одинаковые проверки, одинаковый экспорт. Именно она превращает разовый эксперимент в повторяемый процесс. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия / состояние | Зачем | |---|---|---| | blender-mcp с генератором | проверить `get_addon_status` и статус конкретного генератора | генерация сетки | | Исходное изображение | 512×1024 на длинной стороне, лучше на прозрачном или однотонном фоне | вход генератора | | Python с Pillow | по необходимости | кадрирование, ресайз, замена фона | | Blender | текущий рабочий | импорт, чистка, ретопология, запекание | | Cycles | встроенный | запекание AO и нормалей требует рендер-движка | > [!NOTE] > Имена MCP-инструментов ниже взяты из подключённого сервера: `generate_hyper3d_model_via_text`, > `poll_rodin_job_status`, `import_generated_asset`, `get_tripo_status`, `poll_tripo_job_status`, > `import_generated_asset_tripo`, `get_hunyuan3d_status`, `poll_hunyuan_job_status`, > `import_generated_asset_hunyuan`. Набор и имена зависят от версии аддона — сначала вызови > `get_addon_status()` и посмотри, что доступно, вместо того чтобы идти по списку. ## 🛠️ НАСТРОЙКА ### Этап 1. Проверить, что генератор доступен Не вызывай генерацию до проверки статуса: иначе потратишь итерацию на заведомо неработающую интеграцию и решишь, что пайплайн сломан. ``` get_addon_status # версия Blender и состав аддона get_tripo_status # и/или get_hunyuan3d_status / get_hyper3d_status ``` ### Этап 2. Препроцессинг изображения Генераторы гораздо чувствительнее к фону, чем кажется: пёстрый фон превращается в пёструю геометрию, а объект, занимающий 20% кадра, получает 20% бюджета разрешения на себя. ```python # scripts/preprocess_image.py from PIL import Image def prepare(src, dst, size=1024, pad_ratio=0.85): """Квадратный холст, объект внутри него занимает pad_ratio по большей стороне.""" img = Image.open(src).convert("RGBA") # Если есть альфа-канал — используем его как маску, иначе белый угол # будет срезан и попадёт в геометрию if img.mode == "RGBA" and img.getchannel("A").getextrema()[0] < 255: bbox = img.getchannel("A").point(lambda a: 255 if a > 16 else 0).getbbox() else: bbox = Image.new("L", img.size, 255).getbbox() if bbox: img = img.crop(bbox) w, h = img.size side = int(max(w, h) / pad_ratio) canvas = Image.new("RGBA", (side, side), (0, 0, 0, 0)) canvas.paste(img, ((side - w) // 2, (side - h) // 2)) canvas = canvas.resize((size, size), Image.LANCZOS) canvas.save(dst) return dst prepare("product_photo.jpg", "input_1024.png") ``` > [!NOTE] > Про `rembg` (удаление фона нейросетью, `pip install rembg`) как про отдельный шаг: > пакет и его модель существуют и используются именно так, но весит модель сотни > мегабайт и тянет отдельные зависимости. Если фон однотонный, грубый кроп по > альфе из скрипта выше даёт результат не хуже и не требует ничего ставить. > Точные имена функций пакета сверяй по его документации — они менялись. ### Этап 3. Генерация ```bash # Вариант A: сервис с текстовым запросом, где картинка — подсказка generate_hyper3d_model_via_text( text_prompt="a weathered wooden barrel, hand painted stylised game asset", bbox_condition=[1.0, 1.2, 1.0] # пропорции габаритной коробки ) poll_rodin_job_status(request_id="<из ответа генерации>") import_generated_asset(request_id="<из ответа генерации>", name="Barrel_AI") # Вариант B: сервис, работающий именно с изображением # (Tripo / Hunyuan — в текущем MCP-каталоге есть get_*_status, poll_*_job_status, # import_generated_asset_*). Конкретный инструмент для загрузки картинки # смотри в своём каталоге тулов. ``` > [!NOTE] > Поля вроде `bbox_condition` и конкретные числовые строки статусов генераторов > здесь намеренно не выписаны: набор параметров и строковые значения статусов > отличаются между сборками аддона и между сервисами. Ориентируйся на фактический > ответ инструмента — в нём есть и идентификатор задачи, и её состояние. ### Этап 4. Импорт и чистка геометрии Первое, что делает пайплайн с результатом генерации, — выбрасывает мусор. Дырявые оболочки, висящие вершины и внутренние грани в сгенерированной сетке есть почти всегда. ```python import bpy, sys, os path = sys.argv[sys.argv.index("--") + 1:] if path: bpy.ops.wm.read_factory_settings(use_empty=True) bpy.ops.import_scene.gltf(filepath=path[0]) mesh = next(o for o in bpy.context.scene.objects if o.type == 'MESH') bpy.context.view_layer.objects.active = mesh bpy.ops.object.select_all(action='DESELECT') mesh.select_set(True) # Отдельные куски (например, «диск» и «ножка», если генератор не понял форму) # склеиваем: без этого UV и запекание ломаются на стыках bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.remove_doubles(threshold=0.0005) # порог в метрах bpy.ops.mesh.delete(type='LOOSE') bpy.ops.mesh.normals_make_consistent(inside=False) bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.dissolve_degenerate(dist=0.00001, edges=False) bpy.ops.object.mode_set(mode='OBJECT') print("clean:", len(mesh.data.vertices), "verts,", len(mesh.data.polygons), "faces") ``` > [!NOTE] > Набор операций нормалей менялся между версиями: до 4.1 «гладкие нормали по углу» > включались флагом на меше, а в 4.1 этот флаг убрали в пользу модификатора > *Smooth by Angle*. Оператор `normals_make_consistent` остаётся на месте, > но если его нет в твоей сборке, замени шаг на перебор полигонов с > `polygon.use_smooth` в Python. Имена операторов проверь так: > `hasattr(bpy.ops.mesh, "normals_make_consistent")`. ### Этап 5. Масштаб и пропорции Генератор не знает реального размера предмета. Если не задать масштаб вручную, «бочка» окажется двухметровой, а потом внезапно — сантиметровой в экспорте. ```python import bpy mesh = next(o for o in bpy.context.scene.objects if o.type == 'MESH') # Привести к реальному размеру: высота ~0.9 м у barrel mesh.dimensions.z = 0.9 bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) # Опустить на землю, чтобы ноль сцены совпадал с основанием for v in mesh.data.vertices: v.co.z -= min((mesh.matrix_world @ v.co).z for v in mesh.data.vertices) print("final size:", tuple(round(d, 3) for d in mesh.dimensions)) ``` ### Этап 6. Ретопология и UV Сырая сетка генератора почти всегда в неудобной топологии: неравномерные треугольники, нет петель, ничего не пригодно для ручной правки. ```python import bpy, math mesh = next(o for o in bpy.context.scene.objects if o.type == 'MESH') bpy.context.view_layer.objects.active = mesh bpy.ops.object.select_all(action='DESELECT') mesh.select_set(True) # Простой путь: децимация до бюджета. Минус — топология остаётся трёугольной dec = mesh.modifiers.new("Decimate", 'DECIMATE') dec.ratio = 0.25 bpy.ops.object.modifier_apply(modifier=dec.name) # Альтернатива для аккуратной топологии: Quad Remesher (аддон, платный, # не входит в стандартную поставку) или bpy.ops.object.voxel_remesh() # перед последующим сглаживанием. # UV: Smart Project даёт приемлемую развёртку на круглых предметах bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.uv.smart_project(angle_limit=math.radians(66.0), island_margin=0.01) bpy.ops.object.mode_set(mode='OBJECT') print("after retopo:", len(mesh.data.polygons), "faces,", len(mesh.data.uv_layers), "uv layers") ``` ### Этап 7. Материал PBR Текстура от генератора, если она есть, обычно непригодна для продакшена: разрешение низкое, UV зеркальные, карта нормалей отсутствует. Практичнее построить карты заново, запекая из геометрии. ```python import bpy mesh = next(o for o in bpy.context.scene.objects if o.type == 'MESH') bpy.context.view_layer.objects.active = mesh mat = bpy.data.materials.new("M_AI_Base") mat.use_nodes = True nt = mat.node_tree bsdf = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED') bsdf.inputs['Base Color'].default_value = (0.55, 0.42, 0.28, 1.0) bsdf.inputs['Roughness'].default_value = 0.65 if bsdf.inputs.get('Metallic'): bsdf.inputs['Metallic'].default_value = 0.0 mesh.data.materials.clear() mesh.data.materials.append(mat) # AO и кривизну запекаем в отдельные текстуры — это и есть «изображение» предмета ao_img = bpy.data.images.new("AO", width=2048, height=2048) ao_img.filepath_raw = "//textures/ao.png" ao_img.file_format = 'PNG' bpy.ops.object.bake(type='AO') ao_img.save() curv_img = bpy.data.images.new("Curvature", width=2048, height=2048) curv_img.filepath_raw = "//textures/curvature.png" curv_img.file_format = 'PNG' bpy.ops.object.bake(type='EMIT') curv_img.save() ``` Запекание требует активного движка рендера и работает через оператор `bpy.ops.object.bake`; типы карт (`AO`, `EMIT`, `NORMAL`, `ROUGHNESS`, `DIFFUSE`) берутся из списка допустимых значений этого оператора в твоей версии. ```python # Карта нормалей с разрешения с использованием High Poly-копии bpy.ops.object.bake(type='NORMAL', normal_space='TANGENT') ``` > [!NOTE] > Параметры запекания настолько зависят от версии и настроек (distance, cage, > margin, `selected_to_active`), что писать их значения в статье — > гарантированно устареть через месяц. Открывай панель запекания > (`Object Data → Texture` или вкладка запекания в свойствах объекта) и > выставляй руками; то, что нужно для нормалей с разрешения, — это > Target = Image Texture, Source = выбранный High Poly, Include: Only > Closest / Cage. Всё остальное в панели можно оставить по умолчанию, > пока не видно конкретных артефактов. ### Этап 8. Экспорт ```python import bpy, os os.makedirs(bpy.path.abspath("//exports/ai3d"), exist_ok=True) bpy.ops.export_scene.gltf( filepath=bpy.path.abspath("//exports/ai3d/barrel.glb"), export_format='GLB', export_apply=True, # модификаторы — в сетку, а не остаются в файле export_materials='EXPORT', export_image_format='AUTO', export_texcoords=True, export_normals=True, export_tangents=True, export_animations=False, ) print("saved:", bpy.path.abspath("//exports/ai3d/barrel.glb")) ``` ## ✅ РЕЗУЛЬТАТ **На выходе:** один `.glb` с геометрией, развёрткой UV, одним материалом и (если запекание прошло) текстурами AO и кривизны 2048×2048, плюс сам `.blend` со скриптами — чтобы результат можно было повторить, а не только получить. **Форматы:** `.glb` (основной), при необходимости `.fbx` и `.obj` для движков без glTF. **Кадров:** один ассет — это один результат, не анимация. Если в пайплайне несколько предметов, сцена при выводе состоит из N объектов и одного общего света. **Оценка времени по порядку величины, конкретных замеров нет:** препроцессинг картинки — секунды; сама генерация — от единиц секунд до нескольких минут, в зависимости от сервиса и очереди; чистка, ретопология и запекание карт 2k — десятки секунд на GPU, единицы минут на CPU. Замеры в этой статье не приводятся: сильно зависят от сервиса, железа и размера результата. ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | размер входа | 512–1024 по длинной стороне | меньше 512 — форма «мылится», больше 1024 время растёт без заметного выигрыша | | `pad_ratio` | 0.8–0.9 | при 1.0 объект касается краёв и обрезается по габаритам | | порог `remove_doubles` | 0.0005 (в метрах) | при крупном объекте поднять, иначе склеятся отдельные детали | | `Decimate.ratio` | 0.2–0.4 | ниже 0.1 форма начинает «схлопываться» в грани | | `smart_project angle_limit` | 66° | меньше — больше швов на цилиндрах | | размер запечённых карт | 2048 | 4096 оправдано только для hero-ассетов | | `export_apply` | `True` | без него модификаторы не попадут в файл | | `export_animations` | `False` | для статичного предмета; для персонажа — `True` | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Фон попал в геометрию | фон не отделён, а кроп не сработал | альфа-маска вместо белого угла, проверить результат визуально | | Модель «полая», видна изнанка | инвертированные нормали | `normals_make_consistent` на этапе чистки | | Модель в неудобном масштабе | генератор не знает реального размера | задать `dimensions` вручную на этапе 5 | | UV зеркальные, запекание едет | топология не пригодна для развёртки | ретопология до `Smart Project`, а не поверх сырой сетки | | Текстуры 512, мыльно | разрешение результата генератора | пережать AO и кривизну заново в 2048 из своей геометрии | | Карта нормалей вдавленная | запечатано из Low Poly вместо High Poly | запекать с High Poly в качестве Source | | Экспорт пустой или без материалов | скрипт отработал до того, как сцена была готова | порядок этапов фиксировать скриптом, а не вручную | | Генерация «не сработала» без ошибки | статус генератора был disabled | проверить `get_*_status` до генерации, а не после | ## 🔗 СВЯЗИ - **Прочие в категории «Воркфлоу»:** [WF 01: TEXT-TO-SCENE](wf_01_text_to_scene.md), [WF 03: TEXTURE PIPELINE](wf_03_texture_pipeline.md), [WF 05: PROCEDURAL ASSETS](wf_05_procedural_assets.md), [WF 07: GAME ASSET PIPELINE](wf_07_game_asset_pipeline.md), [WF 17: STOP-MOTION FELT LOOK](wf_17_stop_motion_felt.md) - **По темам:** [SETUP 45: HUNYUAN3D 2.1 LOCAL](../setups/setup_45_hunyuan3d_21.md), [SETUP 44: RODIN / HYPER3D](../setups/setup_44_rodin_hyper3d.md), [SETUP 42: TRIPO AI API](../setups/setup_42_tripo_ai_api.md), [WF 19: VOXEL ART PIPELINE](wf_19_voxel_art_pipeline.md), [WF 23: VIRTUAL PRODUCTION LED](wf_23_virtual_production_led.md) - **Теги:** #ai-3d, #pipeline, #workflow --- # WF 03: TEXTURE PIPELINE Tags: #pipeline #texturing #workflow # WF 03: TEXTURE PIPELINE **ID:** WF-03 | **Type:** PBR Texture Creation | **Difficulty:** ⭐⭐ | **Time:** 15-45 min per material --- ## 🎯 ЦЕЛЬ Полный пайплайн создания production-ready PBR материалов: от генерации до экспорта --- ## 📋 ПАЙПЛАЙН СТАДИИ ``` ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ SOURCE │───▶│ GENERATE │───▶│ REFINE │───▶│ BAKE │───▶│ EXPORT │ │ (Reference/│ │ (AI/Proc/ │ │ (Seamless, │ │ (PBR Maps) │ │ (Multi-fmt)│ │ Photo) │ │ Hand-paint)│ │ Tile, Color)│ │ (AO, Curv) │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ ``` --- ## 🎨 ИСТОЧНИКИ ТЕКСТУР | Источник | Качество | Скорость | Cost | Use Case | |----------|----------|----------|------|----------| | **Poly Haven** | ⭐⭐⭐⭐⭐ | Fast | Free | Production ready PBR | | **Dream Textures (SD)** | ⭐⭐⭐⭐ | Med | Free (local) | Unique/custom | | **Substance Designer** | ⭐⭐⭐⭐⭐ | Slow | $ | Procedural library | | **Quixel/Megascans** | ⭐⭐⭐⭐⭐ | Fast | Free (UE) | Photorealistic | | **Hand Paint (Blender)** | ⭐⭐⭐⭐ | Slow | Free | Artistic control | | **Photo → AI (CSM/Meshy)** | ⭐⭐⭐⭐ | Fast | $ | Photo-to-PBR | --- ## 🔧 MCP КОМАНДЫ ПО СТАДИЯМ ### СТАДИЯ 1: POLY HAVEN (Готовые PBR) ```bash # Поиск "Найди на Poly Haven текстуру 'weathered concrete' категория 'ground' 2k" # Скачивание + применение "Скачай 'concrete_plaster_01' 2k с Poly Haven и присвой к объекту 'Wall'" # Автопоиск по сцене "Найди на Poly Haven текстуры для всех объектов в сцене: стены, пол, потолок" ``` ### СТАДИЯ 2: DREAM TEXTURES (AI Генерация) ```bash # Генерация с нуля "Сгенерируй текстуру 'ancient stone wall, moss in cracks, seamless' Dream Textures: model=sd-2.1-base, size=1024, seamless=true" # Texture Projection (на модель) "Проектируй текстуру 'brick wall with moss' на выбранные грани стены Model: stable-diffusion-2-depth, seamless=true" # Inpainting (исправление швов) "Замаскируй швы на текстуре стены и сделай Inpainting: 'seamless continuation of stone texture'" # Outpainting (расширение) "Расширь текстуру пола на 2x с Outpainting: 'continuation of wooden floor boards'" ``` ### СТАДИЯ 3: PROCEDURAL (Node Groups) ```bash # Создание нод-группы для металла "Создай Node Group 'Procedural_Metal' с входами: - BaseColor (Color) - Roughness (Float 0-1) - Anisotropy (Float 0-1) - EdgeWear (Float 0-1) Выход: PBR материалы" # Использование "Присвой Node Group 'Procedural_Metal' к объекту 'Sword' с параметрами: color=gold, roughness=0.1, anisotropy=0.8" ``` ### СТАДИЯ 4: HAND PAINT (Texture Paint) ```bash # Настройка слоев "Настрой Texture Paint для 'Character_Body': Layers: BaseColor, Roughness, Metallic, Normal, Height, Subsurface Brushes: Draw, Soften, Smear, Clone, Fill, Mask Resolution: 4k BaseColor, 2k остальные" # Рисование "Нарисуй на слое BaseColor: кожу с порами, веснушками На слое Roughness: разную шероховатость (лицо/тело) На слое Subsurface: слуховой проход, кончики пальцев" ``` ### СТАДИЯ 5: BAKING (PBR Maps) ```bash # Bake все карты с High Poly на Low Poly "Выбери High Poly (Source) и Low Poly (Target) Bake: BaseColor, Roughness, Normal, Metallic, AO, Curvature, Height Resolution: 4k, Cage: 0.01, Margin: 16 Output: EXR 32-bit для Normal/Height, PNG 16-bit для остальных" # AO Bake "Bake Ambient Occlusion: 256 samples, Distance: 0.5m Output: AO_4k.exr" # Curvature Bake "Bake Curvature: Convex/Concave Output: Curvature_4k.exr (для Edge Wear масок)" ``` ### СТАДИЯ 6: EXPORT (Multi-format) ```bash # PBR Texture Sets "Экспортируй все карты текстуры 'Brick_Wall' в папку //textures/Brick_Wall/: Formats: PNG 16-bit (BaseColor, Roughness, Metallic, AO) EXR 32-bit (Normal, Height, Curvature) Naming: brick_basecolor_2k.png, brick_normal_2k.exr, etc." # Substance Painter Export "Экспорт проекта для Substance Painter: Export → SBSAR (Substance Material) + текстуры" ``` --- ## 📊 PBR MAP SPECIFICATIONS | Map | Format | Color Space | Bit Depth | Compression | Use Case | |-----|--------|-------------|-----------|-------------|----------| | **Base Color** | PNG | sRGB | 16-bit | Lossless | Albedo | | **Roughness** | PNG | Non-Color | 16-bit | Lossless | Microsurface | | **Metallic** | PNG | Non-Color | 8-bit | Lossless | Metalness | | **Normal** | EXR | Non-Color | 32-bit | ZIP | Surface detail | | **Height/Displacement** | EXR | Non-Color | 32-bit | ZIP | Tessellation | | **AO** | PNG | Non-Color | 16-bit | Lossless | Contact shadows | | **Curvature** | EXR | Non-Color | 16-bit | ZIP | Edge wear masks | | **Emission** | PNG | sRGB | 16-bit | Lossless | Self-illumination | | **Opacity/Alpha** | PNG | Non-Color | 8-bit | Lossless | Transparency | --- ## 📐 RESOLUTION GUIDELINES | Asset Type | Hero | Secondary | Background | Mobile/Web | |------------|------|-----------|------------|------------| | **Character** | 4k | 2k | 1k | 1k | | **Prop (Hero)** | 4k | 2k | 1k | 1k | | **Environment** | 4k | 2k | 2k | 1k | | **Vehicle** | 4k | 2k | 1k | 1k | | **UI/Icon** | 1k | 512 | 256 | 256 | --- ## 🔧 NAMING CONVENTION ``` {asset}_{map}_{resolution}.{ext} Examples: hero_basecolor_4k.png hero_roughness_4k.png hero_normal_4k.exr hero_height_4k.exr hero_ao_4k.png hero_curvature_4k.exr hero_metallic_4k.png hero_emission_4k.png hero_opacity_4k.png Variants: hero_basecolor_4k_v01.png hero_basecolor_4k_dirty.png hero_basecolor_4k_clean.png ``` --- ## 🔧 SUBSTANCE DESIGNER INTEGRATION ```bash # Export from Blender → Substance "Export mesh as FBX with UVs for Substance Designer: File → Export → FBX, Apply Transform, Selected Only" # Import SBSAR into Blender "Import Substance Material (.sbsar) into Blender: Preferences → Add-ons → Import-Export: Import SBSAR Shader Editor → Add → Group → Select .sbsar" ``` --- ## 📦 VERSION CONTROL (Git LFS) ### .gitattributes ```gitattributes *.png filter=lfs diff=lfs merge=lfs -text *.exr filter=lfs diff=lfs merge=lfs -text *.png filter=lfs diff=lfs merge=lfs -text *.jpg filter=lfs diff=lfs merge=lfs -text *.hdr filter=lfs diff=lfs merge=lfs -text *.exr filter=lfs diff=lfs merge=lfs -text *.glb filter=lfs diff=lfs merge=lfs -text *.gltf filter=lfs diff=lfs merge=lfs -text *.fbx filter=lfs diff=lfs merge=lfs -text *.usd filter=lfs diff=lfs merge=lfs -text *.usdz filter=lfs diff=lfs merge=lfs -text *.sbsar filter=lfs diff=lfs merge=lfs -text ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Воркфлоу»:** [WF 17: STOP-MOTION FELT LOOK](wf_17_stop_motion_felt.md), [WF 23: VIRTUAL PRODUCTION LED](wf_23_virtual_production_led.md), [WF 24: PHOTOGRAMMETRY CLEANUP](wf_24_photogrammetry_cleanup.md), [WF 01: TEXT-TO-SCENE](wf_01_text_to_scene.md), [WF 02: IMAGE-TO-3D](wf_02_image_to_3d.md) - **По темам:** [CASE 03: PRODUCT VISUALIZATION](../cases/case_03_product_viz.md), [P46: UDIM WORKFLOW](../prompts/46_udim_workflow.md), [WF 05: PROCEDURAL ASSETS](wf_05_procedural_assets.md) - **Теги:** #pipeline, #texturing, #workflow --- # WF 04: CHARACTER ANIMATION Tags: #animation #workflow # WF 04: CHARACTER ANIMATION **ID:** WF-04 | **Type:** Character Pipeline | **Difficulty:** ⭐⭐⭐ | **Time:** 2-6 hours --- ## 🎯 ЦЕЛЬ Полный пайплайн анимации персонажа: от риггинга до экспорта в игровые движки --- ## 📋 ПАЙПЛАЙН СТАДИИ ``` ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ BASE │───▶│ RETOPO │───▶│ UV & │───▶│ RIGGING │───▶│ SKINNING │───▶│ ANIMATION │ │ MESH │ │ (Game-ready)│ │ TEXTURE │ │ (Rigify/ │ │ (Weights) │ │ (Cycles/ │ │ (Sculpt) │ │ │ │ │ │ Custom) │ │ │ │ NLA) │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ ▼ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ EXPORT │◀───│ BATCH │◀───│ RENDER/ │◀───│ POLISH │◀───│ ANIMATION │ │ (FBX/GLTF/ │ │ EXPORT │ │ PREVIEW │ │ (Cleanup) │ │ LIBRARY │ │ USDZ/Alembic) (CI/CD) │ │ │ │ │ │ (NLA) │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ ``` --- ## 🔧 MCP КОМАНДЫ ПО СТАДИЯМ ### СТАДИЯ 1: BASE MESH (Скульптинг) ```bash # Базовые пропорции (8 голов) "Создай базовый меш персонажа: голова, торс, бедра, конечности Dyntopo: constant_detail=8, symmetry X Пропорции: рост=1.8м, голова=1/8, плечи=2 головы, талия=1.5 головы" # Детализация "Dyntopo sculpting: анатомия мышц, руки/ноги, лицо Детали: пальцы, ногти, зубы, глаза, волосы (базовые)" ``` ### СТАДИЯ 2: РЕТОПОЛОГИЯ (Game-ready ~15k tris) ```bash # Quad Remesher "Quad Remesher: target_quads=5000, adaptivity=0.3, smoothness=1.0 Проверить: только квады, нет полюсов >5, edge flow по анатомии" # Ручная ретопо (для лица/рук) "Ручная ретопология критических зон: лицо (edge flow по мимике), руки (edge flow по суставам), область таза/плеч" ``` ### СТАДИЯ 3: UV & ТЕКСТУРИРОВАНИЕ ```bash # UDIM Layout (4 тайла для 4k текстур) "UDIM Layout: 1001: Head/Neck (4k) 1002: Torso/Arms (2k) 1003: Legs (2k) 1004: Hands/Feet/Details (2k) # Texture Paint слои "BaseColor, Roughness, Normal, Subsurface, Specular, Displacement, Alpha" ``` ### СТАДИЯ 4: RIGGING (Rigify + Custom) ```bash # Meta-Rig "Add Armature → Human (Meta-Rig) Масштабировать под меш: Match to Mesh Generate Rig → Rigify # Дополнительные кости "Face Rig: 50+ костей (jaw, eyes, brows, lips, cheeks, tongue) Fingers: полная иерархия (3 фаланги на палец) Twist bones: forearm (2), upper arm (2), thigh (2), calf (2) Corrective shapes: knees, elbows, shoulders, hips" ``` ### СТАДИЯ 5: SKINNING ```bash # Автоматические веса + чистка "Parent Mesh → Rig (Automatic Weights) Weight Paint cleanup: - Head: жесткая привязка к skull/spine - Shoulders: градиент clavicle/upperarm/deltoid - Elbows: градиент upperarm/forearm + twist - Hips: плавный переход spine/leg + gluteus - Fingers: по фалангам (tip→base) Test: extreme poses, проверка деформаций коленей/локтей" ``` ### СТАДИЯ 6: ANIMATION LIBRARY (NLA) ```bash # Базовые циклы "Walk Cycle (24 frames @ 24fps): Frame 1: Contact R (Right heel, Left toe) Frame 7: Down (Right foot flat, Left passing) Frame 13: Contact L (Left heel, Right toe) Frame 19: Down (Left foot flat, Right passing) Frame 25: = Frame 1 (Loop) # Run Cycle (16 frames @ 24fps) # Idle/Breathing (120 frames) # Jump (30 frames): Anticipation → Jump → Landing → Recovery # Attack/Action (30-60 frames)" ### NLA Workflow ``` ```bash # NLA Editor Setup "Action Editor → Create Actions: Walk, Run, Idle, Jump, Attack NLA Editor → Push Down каждый Action → Track Blend Mode: Replace (базовый) / Add (аддитивные слои) Extrapolation: Cyclic (для циклов) / Hold (для поз) Layering: Base (Walk) + Additive (Arm swing) + Override (Head turn)" ``` --- ## 📊 РИГ СПЕЦИФИКАЦИИ | Компонент | Game-Ready | Film-Ready | |-----------|------------|------------| | **Bones** | 100-150 | 300-500 | | **Face Bones** | 30-50 | 80-150 | | **Twist Bones** | Forearm (2), Thigh (2) | Full chain | | **Corrective Shapes** | 15-25 | 50-100 | | **IK/FK** | Arms/Legs + Snap | Full body + Spine | | **Face Rig** | Basic (jaw, eyes, brows) | FACS (50+ shapes) | | **Cloth/Softbody** | Simulated separately | Integrated | --- ## 🔧 MCP КОМАНДЫ ДЛЯ АНИМАЦИИ ### Walk Cycle Creation ```bash "Создай Walk Cycle для ригнутого персонажа: 1. Frame 1: Contact R (Right heel down, Left toe up) 2. Frame 7: Down (Right foot flat, Left knee bent passing) 3. Frame 13: Contact L (Left heel down, Right toe up) 4. Frame 19: Down (Left foot flat, Right knee bent passing) 5. Frame 25: = Frame 1 (Loop) Graph Editor: All curves Bezier, Cyclic Extrapolation NLA: Push Down → Repeat infinitely" ``` ### Action Blending (NLA) ```bash "Setup NLA для смешивания анимаций: Track 0: Walk Cycle (Cyclic, Replace) Track 1: Arm Swing Additive (Add, Influence 0.5) Track 2: Head Turn Override (Replace, Influence 1.0) Track 3: Facial Expressions (Add, per shot) Blending: Crossfade 5 frames между действиями" ``` ### Procedural Animation (Drivers) ```bash # Breathing "Driver на Chest bone: Scale Z = 1.0 + 0.02 * sin(frame * 0.1)" # Foot Roll "Driver на Foot Roll: Custom Property 'Foot_Roll' → Driver: value * 45° (Degree to Radians)" # Eye Blink (Random) "Driver на Eyelid Bones: if random() < 0.001: blink = 1.0 else: blink = 0 Interpolation: Ease In/Out, Duration: 4 frames" ``` --- ## 📊 ANIMATION EXPORT SPECS | Target | Format | Settings | Notes | |--------|--------|----------|-------| | **Unity** | FBX | Apply Unit Scale, Bake Space Transform, Export Animations, NLA Strips | Loop poses = Frame 1 = Frame N+1 | | **Unreal** | FBX | Export Animations, Smoothing Groups, Tangents | Root Motion отдельно | | **Godot** | GLTF/GLB | Export Animations, Separate Animations | NLA Tracks → Separate Animations | | **Web (Three.js)** | GLB | Export Animations, Morph Targets | Compression: Draco | | **Cinematic** | Alembic | Full Geo Cache, 24fps | Для VFX пайплайна | | **Game Engine** | FBX/GLB | Bake Deformers, Remove Drivers | Оптимизация | --- ## 🔧 BATCH EXPORT (CI/CD) ### scripts/export_animations.py ```python import bpy import os import sys import argparse argv = sys.argv[sys.argv.index("--") + 1:] parser = argparse.ArgumentParser() parser.add_argument("--output-dir", default="exports/animations/") parser.add_argument("--formats", nargs="+", default=["FBX", "GLTF"]) parser.add_argument("--fps", type=int, default=30) args = parser.parse_args(argv) os.makedirs(args.output_dir, exist_ok=True) # Найти все Action'ы actions = [a for a in bpy.data.actions if a.users > 0] for action in actions: # Применить action к ригу rig = bpy.data.objects.get("Rig") if not rig: continue rig.animation_data.action = action # Экспорт name = action.name.replace(" ", "_") for fmt in args.formats: if fmt == "FBX": filepath = os.path.join(args.output_dir, f"{name}.fbx") bpy.ops.export_scene.fbx( filepath=filepath, use_selection=True, apply_unit_scale=True, bake_space_transform=True, object_types={'ARMATURE', 'MESH'}, use_armature_deform_only=True, add_leaf_bones=False, primary_bone_axis='Y_FORWARD', secondary_bone_axis='X_UP', bake_anim=True, bake_anim_use_all_bones=True, bake_anim_use_nla_strips=True, bake_anim_use_all_actions=False, bake_anim_force_startend_keying=True, bake_anim_step=1.0, bake_anim_simplify_factor=1.0, ) elif fmt == "GLTF": filepath = os.path.join(args.output_dir, f"{name}.glb") bpy.ops.export_scene.gltf( filepath=filepath, export_format='GLB', use_selection=True, export_animations=True, export_frame_range=True, export_frame_step=1, export_force_sampling=True, export_skins=True, export_morph=True, ) print(f"Exported: {name} → {fmt}") print(f"Done! Exported {len(actions)} animations to {args.output_dir}") ``` --- ## 📊 ANIMATION QUALITY CHECKLIST | Check | Target | Tool | |-------|--------|------| | **Frame Range** | Start=1, End=N, Loop=1=N+1 | Dope Sheet | | **Loop Seamless** | Frame 1 = Frame N+1 (pos/rot/scale) | Graph Editor | | **No Gimbal Lock** | Euler Filter, Quaternion interpolation | Graph Editor | | **IK/FK Snaps** | Smooth transitions, no pops | 3D View | | **Foot Sliding** | Zero velocity on contact frames | Graph Editor (Velocity) | | **Interpenetration** | No self-collision | Physics/Cloth sim | | **Frame Rate** | Constant (24/30/60 fps) | Scene Settings | | **Root Motion** | Clean translation/rotation | NLA/Action Editor | --- ## 🔗 СВЯЗИ - **Прочие в категории «Воркфлоу»:** [WF 09: REALTIME VJ LOOP](wf_09_realtime_vj_loop.md), [WF 14: CLOTH FASHION SHOW](wf_14_cloth_fashion_show.md), [WF 17: STOP-MOTION FELT LOOK](wf_17_stop_motion_felt.md), [WF 23: VIRTUAL PRODUCTION LED](wf_23_virtual_production_led.md), [WF 24: PHOTOGRAMMETRY CLEANUP](wf_24_photogrammetry_cleanup.md) - **По темам:** [CASE 04: CHARACTER RIG & ANIMATION](../cases/case_04_character_rig.md), [CASE 06: ANIMATION LOOP](../cases/case_06_animation_loop.md), [P14: CAMERA & COMPOSITION](../prompts/14_camera_composition.md) - **Теги:** #animation, #workflow --- # WF 05: PROCEDURAL ASSETS Tags: #geometry-nodes #modeling #pipeline #vegetation #workflow # WF 05: PROCEDURAL ASSETS **ID:** WF-05 | **Type:** Procedural Generation (Geometry Nodes) | **Difficulty:** ⭐⭐⭐ | **Time:** 30-90 min per asset --- ## 🎯 ЦЕЛЬ Создание бесконечных вариаций ассетов через Geometry Nodes: здания, растительность, реквизит, ландшафт --- ## 📋 ПАЙПЛАЙН СТАДИИ ``` ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ PARAMETERS │───▶│ GENERATE │───▶│ VARIATE │───▶│ LOD │───▶│ EXPORT │ │ (Seed, │ │ (GN Tree) │ │ (Seed, │ │ (Distance │ │ (Collection│ │ Style, │ │ Geometry │ │ Attr, │ │ Culling, │ │ Instancing)│ │ Constraints)│ │ Nodes) │ │ Noise) │ │ Decimate) │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ ``` --- ## 🌲 АССЕТ ТИПЫ И GN ТРЕЕ ### 1. PROCEDURAL BUILDING (Building Generator) ```python # GN Tree: Building_Generator # Inputs: # - Seed (Integer) # - Width, Depth, Height (Float) # - Floors (Integer) # - Style: "modern" | "classical" | "industrial" | "sci-fi" # - Window_Density (0-1) # - Roof_Type: "flat" | "pitched" | "dome" | "green" # Logic: # 1. Base Box → Extrude by Floors # 2. Facade Grid → Window Boolean/Array # 3. Roof Generator by Type # 4. Details: Balconies, HVAC, Antennas, Signage # 5. Material Assignment by Vertex Group # 6. LOD: Distance → Decimate (0.5 / 0.25 / 0.1) # 7. Collection Instancing → Realize Instances ``` ### 2. ROAD NETWORK ```python # GN Tree: Road_Network # Inputs: # - Curve Grid (Base) # - Road_Width: Main=12m, Local=8m, Alley=4m # - Sidewalk_Width: 2m # - Curb_Height: 0.15m # - Markings: Center, Crosswalk, Lanes # - Streetlights: Spacing=20m, Variants=3 # Traffic Animation: # Car Collection (10 variants) → Path Follow → Speed 30-60 km/h # Traffic Lights: Procedural Red/Yellow/Green cycle ``` ### 3. VEGETATION SCATTER ```python # GN Tree: Vegetation_Scatter # Inputs: # - Target Mesh (Terrain) # - Collections: Trees_Large, Trees_Small, Bushes, Grass, Flowers # - Density_Attribute (Weight Paint / Noise) # - Scale_Range: (0.8, 1.2) # - Rotation_Random: True # - Align_To_Normal: True # - Distance_Culling: Camera_Distance > 500m → Hide # Biome System: # - Forest: Trees_Large (Density 0.01) + Bushes (0.05) + Grass (0.5) # - Park: Trees_Small (0.02) + Benches (0.01) + Flowers (0.3) # - Urban: Streetlights (0.02) + Planters (0.01) ``` ### 4. PROP VARIATIONS (Fence, Crates, Barrels) ```python # GN Tree: Prop_Variations # Inputs: # - Source Collection (Base Props) # - Count (Integer) # - Scale_Range: (0.8, 1.3) # - Rotation_Random: True # - Position_Jitter: 0.1m # - Material_Variation: Random UV Offset / Color Ramp # LOD System: # Distance < 10m: Full Geo + 4k Textures # 10-50m: Decimate 0.5 + 2k Textures # 50-200m: Decimate 0.25 + 1k Textures # >200m: Billboard Impostor (8 angles) ``` --- ## 🔧 GN НОДЫ РЕФЕРЕНС ### Core Nodes | Node | Purpose | Key Settings | |------|---------|--------------| | **Distribute Points on Faces** | Рассеяние по поверхности | Poisson Disk, Density Attribute | | **Distribute Points in Volume** | Объемное рассеяние | Density, Seed | | **Instance on Points** | Инстансинг коллекций | Pick Instance, Scale, Rotation | | **Collection Info** | Доступ к коллекции | Separate Children, Reset Children | | **Realize Instances** | Конвертация в геометрию | Делает инстансы редактируемыми | ### Geometry Processing | Node | Purpose | |------|---------| | **Mesh Boolean** | Union/Difference/Intersect | | **Mesh to Points** | Конвертация в поинты | | **Points to Curves** | Линии из точек | | **Curve to Mesh** | Толщина линий | | **Resample Curve** | Уравнивание точек | | **Trim Curve** | Обрезка по фактору | ### Attributes | Node | Purpose | |------|---------| | **Store Named Attribute** | Сохранить кастомные данные | | **Named Attribute** | Чтение атрибутов | | **Random Value** | Случайные числа/вектора | | **Attribute Statistic** | Min/Max/Mean/Angle | | **Attribute Transfer** | Передача между геометриями | ### Math/Logic | Node | Purpose | |------|---------| | **Math** | Базовая математика | | **Vector Math** | Векторные операции | | **Compare** | Сравнения (Greater/Less/Equal) | | **Boolean Math** | AND/OR/NOT | | **Switch** | Переключение по условию | | **Index Switch** | Множественный выбор | --- ## 🎨 СТИЛИЗАЦИЯ (Style System) ### Building Styles (GN Input) ```python STYLES = { "modern": { "materials": ["glass", "concrete", "metal_panel"], "window_ratio": 0.7, "roof_type": "flat", "details": ["hvac", "glass_railing", "sunshade"] }, "classical": { "materials": ["brick", "stone", "wood_trim"], "window_ratio": 0.4, "roof_type": "pitched", "details": ["cornice", "balcony", "pilaster"] }, "industrial": { "materials": ["corrugated_metal", "concrete", "rusted_metal"], "window_ratio": 0.2, "roof_type": "flat", "details": ["vents", "pipes", "loading_dock"] }, "sci_fi": { "materials": ["metal_panel", "glass", "glowing_trim"], "window_ratio": 0.5, "roof_type": "dome", "details": ["antenna", "hologram", "force_field"] } } ``` --- ## 🎮 LOD SYSTEM (GN Implementation) ```python # Distance-based LOD в GN # Attribute "Distance" = length(Camera_Location - Position) # LOD Switch Node: # Distance < 50m: LOD0 (Full Geo, 4k textures) # 50-200m: LOD1 (Decimate 0.5, 2k textures) # 200-500m: LOD2 (Decimate 0.25, 1k textures) # 500m+: LOD3 (Billboard Impostor) # Impostor Generation (Pre-bake): # 1. Render 8 views (45° steps) → Sprite Sheet # 2. Shader: Billboard + Alpha + View Angle → Sprite Index ``` --- ## 📦 EXPORT PIPELINE ### Collection Instancing (Performance) ```python # Правило: 1 Draw Call на тип ассета # GN Tree Output → Collection Instancing # Blender автоматически батчит инстансы # Export для Game Engines: # 1. GN → Realize Instances → Apply Modifiers # 2. Separate by Material → Export Collections # 3. FBX/GLTF per Collection # 4. LODs как отдельные меши с суффиксами _LOD0, _LOD1 ``` --- ## 🔧 MCP КОМАНДЫ ДЛЯ PROCEDURAL ### Building Generation ```bash "Создай процедурный квартал 10x10 блоков: - Grid 10x10, spacing 100м - District Attribute: Random (0=Downtown, 1=Residential, 2=Industrial, 3=Park) - Building Generator: Height by District, Style by District - Road Network: Curves between blocks, Sidewalks 2m - Props Scatter: Streetlights (20m), Trees (Parks), Benches - LOD: Distance Culling + Decimate - Output: Collection Instancing для рендера / FBX per Building для экспорта" ``` ### Vegetation Scatter ```bash "Рассей биом 'Forest' на Terrain: - Collection: Trees_Large (Density 0.01, Scale 0.8-1.2) - Collection: Bushes (Density 0.05, Scale 0.5-1.0) - Collection: Grass (Density 0.5, Scale 0.1-0.3) - Weight Paint Density Attribute для путей/полян - LOD: Distance > 200m → Billboard Impostors - Wind Animation: Vertex Offset by Noise(Position + Time)" ``` ### Road Network ```bash "Создай дорожную сеть из Curve Grid: - Main Roads: 12m, 2 lanes + 2m sidewalks - Local Roads: 8m, 1 lane + 1.5m sidewalks - Markings: Center line, Crosswalks, Stop lines - Traffic Lights: Procedural на перекрестках (Cycle 60s) - Streetlights: Every 20m, 3 variants - Cars: Path Follow curves, 10 variants, Speed 30-60 km/h" ``` --- ## 📊 PERFORMANCE TIPS | Technique | Impact | When to Use | |-----------|--------|-------------| | **Collection Instancing** | -90% Draw Calls | Всегда для повторяющихся объектов | | **Distance Culling** | -50% Tris | Большие сцены | | **Decimate Modifier (GN)** | -75% Tris | LOD1/LOD2 | | **Billboard Impostors** | -99% Tris | >500m Distance | | **Texture Atlasing** | -Draw Calls | Много маленьких текстур | | **Instance Scale/Align in GN** | -CPU | Вместо Object Constraints | --- ## 🔗 СВЯЗИ - **Прочие в категории «Воркфлоу»:** [WF 01: TEXT-TO-SCENE](wf_01_text_to_scene.md), [WF 02: IMAGE-TO-3D](wf_02_image_to_3d.md), [WF 03: TEXTURE PIPELINE](wf_03_texture_pipeline.md), [WF 07: GAME ASSET PIPELINE](wf_07_game_asset_pipeline.md), [WF 12: CROWD SIM GN](wf_12_crowd_sim_gn.md) - **По темам:** [CASE 05: PROCEDURAL CITY](../cases/case_05_procedural_city.md), [P22: PARTICLE SYSTEMS](../prompts/22_particles.md), [P07: INSTANCING & ARRAYS](../prompts/07_instancing_arrays.md), [P23: GEOMETRY NODES BASICS](../prompts/23_geometry_nodes_basics.md) - **Теги:** #geometry-nodes, #modeling, #pipeline, #vegetation, #workflow --- # WF 06: ARCHVIZ WORKFLOW Tags: #archviz #workflow # WF 06: ARCHVIZ WORKFLOW **ID:** WF-06 | **Type:** Architectural Visualization | **Difficulty:** ⭐⭐⭐ | **Time:** 3-8 hours --- ## 🎯 ЦЕЛЬ Профессиональный ArchViz пайплайн: от CAD планов до фотореалистичного рендера --- ## 📋 ПАЙПЛАЙН СТАДИИ ``` ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ CAD/IMPORT│───▶│ MODELING │───▶│ MATERIALS │───▶│ LIGHTING │───▶│ CAMERA │ │ (DWG/IFC/ │ │ (Walls, │ │ (PBR, │ │ (HDRI + │ │ (Composition│ │ SketchUp) │ │ Windows, │ │ Displacement)│ │ Studio) │ │ DOF) │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │ ▼ ▼ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ FURNITURE │───▶│ LANDSCAPE │───▶│ RENDER │───▶│ COMPOSITING│───▶│ DELIVERY │ │ (Poly Pizza/│ │ (GN Scatter)│ │ (Cycles, │ │ (Color, │ │ (4K/8K, │ │ Sketchfab) │ │ Grass/Trees)│ │ 256-512 spp)│ │ Glare, LUT)│ │ Video, VR) │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ ``` --- ## 🔧 MCP КОМАНДЫ ПО СТАДИЯМ ### СТАДИЯ 1: ИМПОРТ CAD / ПЛАНЫ ```bash # Импорт DWG/DXF "Импорт DWG планов: File → Import → AutoCAD DXF/DWG Scale: 1 Unit = 1 Meter Layers: Walls, Doors, Windows, Dimensions, Text Clean: Remove duplicate vertices, Merge by Distance" # Или через SketchUp "Import SKP: File → Import → SketchUp Cleanup: Remove internal faces, Triangulate → Quads (Alt+J)" # Из IFC (BIM) "Import IFC: File → Import → IFC IfcOpenShell: элементы → Collections по IfcClass Walls/Slabs/Windows/Doors → Отдельные коллекции" ``` ### СТАДИЯ 2: МОДЕЛИРОВАНИЕ АРХИТЕКТУРЫ ```bash # Стены (из линий плана) "Выбери линии стен → Convert to Mesh → Extrude 3m → Solidify 0.2m Boolean: Windows/Doors cutouts (Difference) Window Frames: Separate object, Bevel 0.02m, Material 'Aluminum'" # Полы/Потолки "Floor: Plane → Scale to room → Solidify 0.15m Ceiling: Copy Floor → Move Z=3m → Modifier Array для плиток" # Лестницы "Curve → Bezier → Extrude → Bevel Depth 0.3m Array: Count по высоте, Relative Offset Z=0.18m Railing: Separate curve → Bevel Profile" ``` ### СТАДИЯ 3: ОКНА И ДВЕРИ (Parametric) ```bash # Окна (Geometry Nodes / Asset Browser) "Window Asset: Frame 0.08m, Glass 0.004m, Sill 0.05m Params: Width, Height, Divisions (2x2, 3x1, etc.), Handle Type Insert: Boolean Difference в стену + Frame Object" # Двери "Door Asset: Frame, Panel, Handle, Hinges Params: Width (0.9/1.0/1.2), Height (2.1/2.4), Swing Direction Hardware: Handle, Lock, Hinges (Separate objects для анимации)" ``` ### СТАДИЯ 4: МАТЕРИАЛЫ (ArchViz PBR Library) ```bash # Базовая библиотека "Создай Material Library Collection: Mat_Concrete_Polished: BaseColor #E0E0E0, Rough 0.1, Metal 0, Clearcoat 0.3 Mat_Concrete_Rough: BaseColor #B0B0B0, Rough 0.8, Metal 0 Mat_Brick_Red: BaseColor #8B3A2A, Rough 0.9, Normal Brick, Height 0.01 Mat_Wood_Floor: BaseColor #8B7355, Rough 0.6, Anisotropic 0.5 Mat_Glass_Clear: Transmission 1, IOR 1.52, Rough 0 Mat_Grass: BaseColor #4A7C2A, Rough 0.9, Displacement Grass Mat_Metal_Aluminum: BaseColor #CCCCCC, Rough 0.2, Metal 1, Aniso 0.3 Mat_Stone_Travertine: BaseColor #E8D5B7, Rough 0.7, Normal Stone" # Displacement Setup "Все материалы с Height: Material Settings → Displacement = Displacement Only Cycles: Experimental → Adaptive Subdivision → Dicings Rate 1-2" ``` ### СТАДИЯ 5: ОСВЕЩЕНИЕ (HDRI + Studio) ```bash # Дневной интерьер "HDRI: Poly Haven 'studio_small_01' 2k, Strength 0.4 Sun: 5000K, 5000W, Angle 45°, Size 0.5m (Soft Shadows) Portals: World → Portal на всех окнах (Cycles) # Вечерний интерьер HDRI: 'evening_road_01' 2k, Strength 0.8 Interior Lights: 2700K, 800-1500 lm per fixture Table Lamps: Emission 3000K, 500 lm Cove Lighting: LED Strip 3000K, Hidden in cove" # Дневной экстерьер "HDRI: 'kloppenheim_02' 4k, Strength 1.0 Sun: 5500K, 10000W, Angle 30° (Morning) / 60° (Noon) Volumetric: World Volume Scatter Density 0.01, Anisotropy 0.5" ``` ### СТАДИЯ 6: МЕБЕЛЬ И ДЕКОР (Asset Browser) ```bash # Poly Pizza (Low-poly, Free) "Search Poly Pizza: 'sofa', 'chair', 'table', 'lamp', 'plant', 'rug' License: CC0 preferred, Download → Normalize 1.0m → Apply Materials" # Sketchfab (High-poly, Realistic) "Search Sketchfab: 'modern sofa 3d model', 'dining table', 'floor lamp' Downloadable: Yes, License: CC-BY/CC0 Import: GLTF → Scale → Materials Fix → Collection 'Furniture'" # Custom Props (Custom Modeling) "Custom: Kitchen cabinets (GN parametric), Wardrobes, Bookshelves Plants: GN Scatter → Collection 'Plants_Indoor' (10 variants)" ``` ### СТАДИЯ 7: ЛАНДШАФТ И ОКРУЖЕНИЕ ```bash # Террейн "Plane → Subdivide 200x200 → Displace (Noise Texture, Strength 5m) Texture Paint: Grass, Dirt, Path, Grass/Flower Scatter (GN)" # Растительность (GN Scatter) "Collection 'Trees_Large': 5 variants, Density 0.005, Scale 0.8-1.3 Collection 'Bushes': 8 variants, Density 0.02 Collection 'Grass': 3 variants, Density 0.5, Weight Paint для путей Collection 'Flowers': 5 variants, Density 0.1, Random Color" # Вода "Plane → Shader: Glass + Noise Displacement + Foam (Geometry Nodes) River: Curve → Mesh → Flow Map → Animated UV Offset" ``` ### СТАДИЯ 8: КАМЕРЫ И КОМПОЗИЦИЯ ```bash # Камеры "Cam_Main: Wide 24mm, Eye Level 1.6m, Rule of Thirds Cam_Detail: 85mm, f/1.8, DOF on Detail Cam_Wide: 16mm, Low Angle, Dramatic Perspective Cam_Aerial: Drone View, 200m Height, Ortho/Perp # DOF Setup "Focus Object: Target Empty (Animated) F-Stop: 2.8 (Interior), 8 (Exterior), 1.8 (Portrait) Blades: 8-9, Ratio 1.0" # Composition Guides "Camera → Viewport Display → Composition Guides: Thirds / Golden Ratio" ``` ### СТАДИЯ 8: РЕНДЕР НАСТРОЙКИ ```bash # Production Quality "Cycles: 512 samples, Adaptive Threshold 0.01, Min Samples 64 Denoising: OPTIX (GPU) / OpenImageDenoise (CPU) Adaptive Sampling: ON, Min 32, Threshold 0.01 # Light Paths Max Bounces: 12, Diffuse 4, Glossy 4, Transmission 12, Volume 4 Caustics: Refractive ON, Reflective ON # Color Management Filmic, Look: Medium High Contrast Exposure: 0.0, Gamma: 1.0 # Output Resolution: 3840x2160 (4K) / 7680x4320 (8K) Format: EXR MultiLayer 32-bit + PNG 16-bit Color Mode: RGBA, Film Transparent: ON (для композинга)" ``` ### СТАДИЯ 9: КОМПОЗИТИНГ ```bash # Basic Compositor Tree "Render Layers → Color Balance (Lift/Gamma/Gain) → Hue/Sat (1.1) → Gamma (1.05) → Exposure (0.1) → Glare (Fog Glow, 0.2 mix) → Lens Distortion (0.01) → Vignette (0.15) → Color Matrix (Teal & Orange) → Vignette (0.1) → Film Grain (Overlay 0.02) → Composite + Viewer" # Cryptomatte для селекций "Enable Cryptomatte: Object + Material Compositor: Cryptomatte Node → Matte Output → Color Correct отдельные элементы" # Passes для гибкости "Enable: Combined, Depth, Normal, Mist, AO, Emission, Shadow, Cryptomatte File Output: EXR MultiLayer 32-bit (All Passes) + PNG 16-bit (Beauty)" ``` ### СТАДИЯ 10: ДЕЛИВЕРИ ```bash # Форматы "Still: 4K EXR 32-bit + PNG 16-bit + JPEG 95% (Web) Animation: 4K 30fps MP4 (H.264 High) + ProRes 422 HQ (Archive) VR/360: 8K Stereoscopic, Equirectangular, Stereo Side-by-Side # Naming project_scene_cam_v001_4k.exr project_scene_cam_v001_4k.png project_anim_v001_4k.mp4 project_anim_v001_4k_prores.mov" # Color Space Delivery "sRGB (Web), Rec.709 (Video), DCI-P3 (Cinema), Rec.2020 (HDR) ICC Profile embedded в EXR/PNG" ``` --- ## 📊 ARCHVIZ QUALITY PRESETS | Preset | Samples | Resolution | Time (RTX 4090) | Use Case | |--------|---------|------------|-----------------|----------| | **Draft** | 64 | 1920x1080 | 30 сек | Blocking, Layout | | **Preview** | 128 | 1920x1080 | 1 мин | Client Review | | **Standard** | 256 | 3840x2160 | 5 мин | Standard Delivery | | **High** | 512 | 3840x2160 | 12 мин | Competition | | **Ultra** | 1024 | 7680x4320 | 45 мин | Billboard/Print | --- ## 💡 PRO TIPS | Tip | Why | |-----|-----| | **Portals на окнах** | Ускоряет рендер интерьера в 3-5x | | **Adaptive Sampling** | Фокусирует сэмплы на шумных участках | | **Light Linking** | Контроль отражений в зеркалах/стекле | | **Cryptomatte** | Мгновенные маски в композинг без перерисовки | | **EXR MultiLayer** | Все проходы в одном файле, 32-bit precision | | **Adaptive Subdivision** | True displacement без тяжелого меша | | **Portals + HDRI** | Реалистичное GI за секунды | --- ## 🔗 СВЯЗИ - **Прочие в категории «Воркфлоу»:** [WF 11: ARCHVIZ DAY→NIGHT](wf_11_archviz_day_night.md), [WF 24: PHOTOGRAMMETRY CLEANUP](wf_24_photogrammetry_cleanup.md), [WF 01: TEXT-TO-SCENE](wf_01_text_to_scene.md), [WF 02: IMAGE-TO-3D](wf_02_image_to_3d.md), [WF 03: TEXTURE PIPELINE](wf_03_texture_pipeline.md) - **По темам:** [CASE 01: ARCHVIZ INTERIOR](../cases/case_01_archviz_interior.md), [CASE 08: SCI-FI CORRIDOR MODULAR](../cases/case_08_sci_fi_corridor.md), [WF 04: CHARACTER ANIMATION](wf_04_character_animation.md) - **Теги:** #archviz, #workflow --- # WF 07: GAME ASSET PIPELINE Tags: #pipeline #workflow # WF 07: GAME ASSET PIPELINE **ID:** WF-07 | **Type:** Game Asset Production | **Difficulty:** ⭐⭐ | **Time:** 1-3 hours per asset --- ## 🎯 ЦЕЛЬ Production-ready пайплайн для игровых ассетов: от концепта до интеграции в движок --- ## 📋 ПАЙПЛАЙН СТАДИИ ``` ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ CONCEPT │───▶│ BLOCKOUT │───▶│ HIGH POLY │───▶│ RETOPO │───▶│ UV & │───▶│ BAKING & │ │ /REFERENCE │ │ (Primitives)│ │ (Sculpt) │ │ (Low Poly) │ │ BAKING │ │ TEXTURING │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ ▼ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ ENGINE │◀───│ VALIDATION │◀───│ EXPORT │◀───│ LOD │◀───│ MATERIAL │◀───│ TEXTURE │ │ INTEGRATION│ │ (Auto QA) │ │ (Multi-fmt)│ │ Generation │ │ SETUP │ │ FINALIZE │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ ``` --- ## 📋 СТАДИЯ 1: CONCEPT & REFERENCE ### Reference Board ```bash "Создай Reference Board в PureRef / Milanote: - Orthographic views (Front/Side/Top) - обязательно для пропорций - Material close-ups (Metal wear, Fabric weave, Wood grain) - Lighting references (Studio, Outdoor, Mood) - Scale references (Human silhouette, Door height 2.1m) - Polycount targets: Hero 15k, Prop 5k, Foliage 500" ``` ### Scale Reference Setup ```bash "Создай Scale Reference в сцене: 1. Human proxy: Cylinder 1.8m height, 0.5m width 2. Door frame: 2.1m x 0.9m 3. Standard chair: 0.45m seat height 3. Grid floor: 1m x 1m squares Всё в Collection 'Reference' → Hide in Render" ``` --- ## 🔧 СТАДИЯ 2: BLOCKOUT (Primitives + Boolean) ```bash # Основные формы "Создай блокаут из примитивов: - Body: Cube → Subdivide → Shape - Head: Sphere → Shape - Limbs: Cylinders → Taper - Props: Primitives → Boolean для вырезов # Пропорции через Drivers "Driver на Scale: Body_Height = 1.8m (Driver Variable) Limb_Length = Body_Height * 0.45 Head_Size = Body_Height * 0.125" # Boolean для деталей "Boolean Difference: Вырезы для глаз, рта, карманов Boolean Union: Объединение частей оружия/аксессуаров" ``` --- ## 🔧 СТАДИЯ 3: HIGH POLY (Sculpting) ```bash # Dyntopo Sculpting "Dyntopo: Constant Detail 8-12, Symmetry X Brushes: Clay Strips, Draw Sharp, Smooth, Grab, Inflate, Crease Детали: Мышцы, Жесты, Ткани, Швы, Пуговицы, Застежки" # Multires для анимируемых частей "Multires Modifier: 4-5 уровней подделения Sculpt Level: 3-4 для деталей лица/рук Base Level: 0 для риггинга" ``` --- ## 🔧 СТАДИЯ 3: RETOPOLOGY (Game-Ready) ```bash # Auto Retopo (Quad Remesher) "Quad Remesher: Target Quads 5000 (Hero) / 2000 (Prop) Adaptivity: 0.3 (больше полигонов в деталях) Symmetry: X # Manual Retopo (Критичные зоны) "Ручная ретопология: - Лицо: Edge flow по мимическим мышцам (Лоопы вокруг глаз/рта) - Руки: Edge flow по суставам, Edge loops вокруг запястий - Деформационные зоны: Колени, Локти, Плечи, Таз - Edge Flow: Следует за анатомией, Квады только" ``` ### Polycount Targets | Asset Type | Tris (LOD0) | Tris (LOD1) | Tris (LOD2) | UV Tiles | |------------|-------------|-------------|-------------|----------| | **Hero Character** | 15k-25k | 8k-12k | 3k-5k | 2-4 (UDIM) | | **NPC/Enemy** | 8k-15k | 4k-8k | 2k-4k | 1-2 | | **Hero Prop** | 5k-10k | 2k-5k | 1k-2k | 1 | | **Environment Prop** | 2k-5k | 1k-3k | 500-1k | 1 | | **Foliage/Billboard** | 200-500 | 100-200 | 50-100 | 1 | --- ## 🔧 СТАДИЯ 4: UV & BAKING ### UV Unwrapping Strategy ```bash # Character UV (UDIM) "UDIM Layout: 1001: Head/Neck (4k) 1002: Torso/Arms (2k) 1003: Legs (2k) 1004: Hands/Feet/Details (2k) # Props/Environment "Smart UV Project: Angle Limit 66°, Island Margin 0.005 Lightmap Pack: Padding 4px (для Lightmap UV) Cube Projection: Для архитектуры/коробок" ``` ### Baking Setup (High → Low Poly) ```bash # Bake Settings (Cycles) "Bake Settings: Engine: Cycles, Samples: 16 (AO 256) Ray Distance: 0.01 (Cage) Margin: 16px Use Selected to Active: True # Maps to Bake: 1. Normal (Tangent Space, +X +Y +Z) → EXR 32-bit 2. AO (256 samples, Distance 0.5m) → PNG 16-bit 3. Curvature (Convex/Concave) → EXR 16-bit 4. Position (World Space) → EXR 32-bit 5. Thickness (Subsurface) → PNG 16-bit 5. Material ID → PNG 8-bit 6. Base Color (из High Poly материалов) → PNG 16-bit sRGB" ``` --- ## 🔧 СТАДИЯ 5: TEXTURING (Substance / Blender) ### Substance Painter Workflow ```bash # Export для SP "Export FBX: Apply Scale, Bake Space Transform, Selected Only Import в SP: Template 'PBR Metallic Roughness' Project Settings: 4k, Padding 16, Dilation 4" # Smart Materials "Smart Material Library: - Metal_Gold, Metal_Steel, Metal_Rusted - Fabric_Cotton, Fabric_Leather, Fabric_Denim - Wood_Oak, Wood_Pine, Wood_Painted - Stone_Marble, Stone_Concrete, Stone_Brick - Plastic_Glossy, Plastic_Matte, Rubber" # Layer Stack "Base: BaseColor + Normal + Roughness + Metallic Detail: Height/Normal Detail (Micro-detail) Wear: Edge Wear (Curvature Mask), Dirt (AO Mask) Damage: Scratches (Normal Map), Chips (Height Map) Story: Stickers, Decals, Graffiti (Alpha Blend)" ``` ### Blender Texture Paint (Alternative) ```bash "Blender Texture Paint Setup: Layers: BaseColor, Roughness, Metallic, Normal, Height, Subsurface Resolution: 4k Head, 2k Body Brushes: Draw, Soften, Smear, Clone, Fill, Mask Stencil: Для текстуры кожи, ткани, металла Export: Все карты → //textures/{asset}_v001/" ``` --- ## 🔧 СТАДИЯ 6: MATERIAL SETUP (Engine Ready) ### Unity URP/HDRP ```bash # Shader Graph / Standard Shader "Material Setup: - Shader: URP/Lit или HDRP/Lit - Base Map: BaseColor (sRGB) - Normal Map: Normal (BC5/BC7 compression) - Mask Map: R=Metallic, G=AO, B=Detail, A=Smoothness (1-Roughness) - Detail Maps: Detail Normal + Detail Albedo - Emission Map: Для свечения - UV2: Lightmap UV (Generate Lightmap UVs)" ``` ### Unreal Engine ```bash # Material Instance "Master Material: MF_PBR_Master Parameters: - BaseColor (Texture) - Normal (Texture, BC5) - ORM: R=AO, G=Roughness, B=Metallic - Emissive Color + Emissive Map - Subsurface Profile (для кожи) - Clearcoat (для лака/керамики) - Parallax/POM (для рельефа) # Texture Compression "Default: BC7 (BaseColor/Normal), BC4 (ORM) Normal: BC5n (Normal Map) HDR: BC6H (Emission/HDRI)" ``` --- ## 🔧 СТАДИЯ 7: LOD GENERATION ```bash # Auto LOD (Blender/Simplygon/Quad Remesher) "LOD0: Full Detail (Target Tris) LOD1: Decimate 0.5 (50% tris), UV сохраняются LOD2: Decimate 0.25 (25% tris) LOD3: Decimate 0.1 (10% tris) или Billboard # Billboard Impostor (Для дальних LOD) "8 angles (45° steps) → Sprite Sheet 2048x2048 Shader: Billboard + Alpha + View Angle → Sprite Index Material: Unlit/Transparent Cutout" ``` --- ## 🔧 СТАДИЯ 8: EXPORT & VALIDATION ### Export Settings per Engine ```bash # Unity (FBX) "FBX Export: - Apply Unit Scale, Bake Space Transform - Object Types: ARMATURE, MESH - Bake Animation: All Actions, NLA Strips - Forward: Y, Up: X - Embed Media: False (Textures отдельно)" # Unreal (FBX) "FBX Export: - Export Animations: True - Smoothing Groups: True - Tangents: True - Bake Animation: All, NLA Strips, Resample 1.0" # Godot / Web (GLTF/GLB) "GLTF Export: - Format: GLB (Binary) - Export Animations: All - Tangents: True - Draco Compression: Level 6-8 - Texture Compression: Basis Universal" # Web/Three.js (Optimized GLB) "GLTF Export: - Format: GLB - Draco Compression: Level 8 - Texture Compression: Basis Universal (KTX2) - Remove Unused: Nodes, Materials, Textures - Meshopt Compression: ON" ``` ### Validation Checklist (Auto QA) ```bash # Авто-проверка (Python Script) "Validation Rules: ✅ Tris within target ±10% ✅ No non-manifold edges ✅ No overlapping UVs (Margin > 2px) ✅ All UVs in 0-1 range (or UDIM tiles) ✅ No zero-area faces ✅ Normals outward ✅ Vertex Groups named correctly (for rigging) ✅ Material names match naming convention ✅ Texture names follow convention: {asset}_{map}_{res}.{ext} ✅ LODs: Smooth transition, Screen size thresholds ✅ Pivot at base (0,0,0) or logical center ✅ Scale = 1.0 (Applied) ✅ No unused materials/textures ✅ Naming Convention: {asset}_{type}_{variant}_LOD{num}" ``` --- ## 📦 ENGINE INTEGRATION ### Unity Pipeline ```bash # Asset Postprocessor (Auto) "Assets/Editor/AssetPostprocessor.cs: - OnPreprocessModel: Auto-scale, Generate Colliders - OnPostprocessModel: Setup Materials, LOD Groups - OnPreprocessTexture: sRGB/Linear, Compression, Mipmaps - Naming Convention Enforcement" # Prefab Structure "Prefab_Root/ ├── Mesh (LOD Group) │ ├── LOD0 (MeshRenderer) │ ├── LOD1 (MeshRenderer) │ └── LOD2 (MeshRenderer) ├── Rig (Animator + Avatar) ├── Colliders (Capsule/Box/Mesh) ├── Audio (AudioSource) └── Scripts (MonoBehaviour)" ``` ### Unreal Pipeline ```bash # DataAsset + Blueprint "Asset Structure: /Game/Assets/ ├── Meshes/ │ ├── SM_Asset_LOD0.uasset │ ├── SM_Asset_LOD1.uasset │ └── SM_Asset_LOD2.uasset ├── Materials/ │ ├── M_Asset_Base.uasset │ └── MI_Asset_Variant.uasset ├── Textures/ │ ├── T_Asset_BaseColor.uasset │ ├── T_Asset_Normal.uasset │ └── T_Asset_ORM.uasset ├── Blueprints/ │ └── BP_Asset.uasset └── Animations/ └── A_Asset_Action.uasset" ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Воркфлоу»:** [WF 01: TEXT-TO-SCENE](wf_01_text_to_scene.md), [WF 02: IMAGE-TO-3D](wf_02_image_to_3d.md), [WF 03: TEXTURE PIPELINE](wf_03_texture_pipeline.md), [WF 05: PROCEDURAL ASSETS](wf_05_procedural_assets.md), [WF 17: STOP-MOTION FELT LOOK](wf_17_stop_motion_felt.md) - **По темам:** [WF 19: VOXEL ART PIPELINE](wf_19_voxel_art_pipeline.md), [WF 23: VIRTUAL PRODUCTION LED](wf_23_virtual_production_led.md), [WF 24: PHOTOGRAMMETRY CLEANUP](wf_24_photogrammetry_cleanup.md) - **Теги:** #pipeline, #workflow --- # WF 08: BATCH PROCESSING Tags: #automation #workflow # WF 08: BATCH PROCESSING **ID:** WF-08 | **Type:** Automation / Pipeline | **Difficulty:** ⭐⭐ | **Time:** 15-60 min setup --- ## 🎯 ЦЕЛЬ Автоматизация рутинных операций: батч-рендер, конвертация, валидация, экспорт, деплой --- ## 📋 ТИПЫ БАТЧ-ОПЕРАЦИЙ | Категория | Операции | Типичное применение | |-----------|----------|---------------------| | **Rendering** | Frame range, Multi-camera, Multi-scene | Анимация, ArchViz, Product viz | | **Conversion** | Format convert, LOD gen, UV unwrap | Asset pipeline, Game export | | **Validation** | QA checks, Naming, Polycount | CI/CD, Asset library | | **Export** | Multi-format, Multi-LOD, Multi-scene | Game engine, Web, Pipeline | | **Asset Management** | Rename, Organize, Registry update | Library maintenance | | **Scene Assembly** | Batch import, Scatter, Layout | Procedural, Large scenes | --- ## 🔧 MCP КОМАНДЫ ДЛЯ БАТЧА ### Batch Render (Multi-camera, Multi-frame) ```bash "Батч-рендер всех камер в сцене: 1. Найди все камеры в коллекции 'Cameras_Render' 2. Для каждой камеры: - Set Active Camera - Set Output Path: //renders/{camera_name}_####.exr - Render Frame Range 1-250 - Compositor: Auto-save passes 3. Generate MP4 preview для каждой камеры" # Или через Python (см. скрипт ниже) ``` ### Batch Convert (Format + LOD) ```bash "Конвертация всех .blend в папке /assets/source/ в GLB + FBX + USDZ: 1. Найди все .blend файлы рекурсивно 2. Для каждого: - Открыть - Apply All Transforms - Generate LODs (Decimate 0.5, 0.25, 0.1) - Export GLB (Draco compression) - Export FBX (Game ready) - Export USDZ (AR/Web) - Save metadata JSON 3. Update Asset Registry" ``` ### Batch Validate (QA) ```bash "QA проверка всех ассетов в /assets/library/: Для каждого ассета: 1. Geometry: Non-manifold, Loose verts, Zero-area faces 2. UV: Overlaps, Out of bounds, Missing UVs 3. Materials: Missing textures, Wrong color space, Naming 4. Naming Convention: {asset}_{type}_{variant}_LOD{num} 5. Polycount: Within target ±10% 6. Scale: Applied (1.0, 1.0, 1.0) 6. Pivot: At base or center Generate QA Report: HTML + JSON" ``` ### Batch Rename & Organize ```bash "Реорганизация библиотеки /assets/: 1. Rename по конвенции: {category}_{name}_{variant}_v{ver} 2. Move to folders: /models/characters/, /models/props/, /models/env/ 3. Update Asset Registry (JSON/SQLite) 4. Generate Thumbnails (256x256) для каждого ассета 5. Generate LOD Preview (Wireframe + Shaded)" ``` --- ## 🐍 PYTHON СКРИПТЫ ДЛЯ БАТЧА ### batch_render.py ```python #!/usr/bin/env python3 """Batch Render Script для Blender CLI""" import bpy import os import sys import argparse from pathlib import Path def parse_args(): argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] parser = argparse.ArgumentParser(description="Batch Render") parser.add_argument("--scene", required=True, help="Path to .blend file") parser.add_argument("--output", default="//renders/", help="Output directory") parser.add_argument("--cameras", nargs="+", help="Camera names (default: all in 'Cameras_Render')") parser.add_argument("--frame-start", type=int, default=1) parser.add_argument("--frame-end", type=int, default=250) parser.add_argument("--resolution", nargs=2, type=int, default=[1920, 1080]) parser.add_argument("--samples", type=int, default=128) parser.add_argument("--format", choices=["PNG", "EXR", "JPEG"], default="EXR") parser.add_argument("--engine", choices=["CYCLES", "EEVEE"], default="CYCLES") parser.add_argument("--denoise", action="store_true") parser.add_argument("--compositor", action="store_true") return parser.parse_args(argv) def setup_scene(args): scene = bpy.context.scene scene.render.engine = args.engine scene.render.resolution_x = args.resolution[0] scene.render.resolution_y = args.resolution[1] scene.render.resolution_percentage = 100 scene.frame_start = args.frame_start scene.frame_end = args.frame_end if args.engine == 'CYCLES': scene.cycles.samples = args.samples scene.cycles.use_denoising = args.denoise if args.denoise: scene.cycles.denoiser = 'OPTIX' scene.cycles.denoising_prefilter = 'ACCURATE' scene.cycles.use_adaptive_sampling = True scene.cycles.adaptive_threshold = 0.01 # Output scene.render.filepath = args.output scene.render.image_settings.file_format = args.format scene.render.image_settings.color_mode = 'RGBA' scene.render.image_settings.color_depth = '32' if args.format == 'EXR' else '16' if args.format == 'EXR': scene.render.image_settings.exr_codec = 'ZIP' scene.render.image_settings.use_preview = True # Compositor if args.compositor: setup_compositor() def setup_compositor(): scene = bpy.context.scene scene.use_nodes = True tree = scene.node_tree nodes = tree.nodes links = tree.links nodes.clear() rl = nodes.new('CompositorNodeRLayers') rl.location = (-400, 0) # Basic color correction cc = nodes.new('CompositorNodeColorBalance') cc.location = (0, 200) cc.correction_method = 'LIFT_GAMMA_GAIN' cc.lift = (1.02, 1.0, 0.98) cc.gamma = (1.0, 1.0, 1.0) cc.gain = (0.98, 1.0, 1.02) glare = nodes.new('CompositorNodeGlare') glare.location = (200, 0) glare.glare_type = 'FOG_GLOW' glare.mix = 0.2 glare.threshold = 0.8 comp = nodes.new('CompositorNodeComposite') comp.location = (400, 0) viewer = nodes.new('CompositorNodeViewer') viewer.location = (400, -200) links = tree.links links.new(nodes['Render Layers'].outputs['Image'], cc.inputs['Image']) links.new(cc.outputs['Image'], glare.inputs['Image']) links.new(glare.outputs['Image'], comp.inputs['Image']) links.new(glare.outputs['Image'], viewer.inputs['Image']) def get_cameras(args): if args.cameras: return [bpy.data.objects.get(name) for name in args.cameras if bpy.data.objects.get(name)] # Auto-detect: cameras in 'Cameras_Render' collection coll = bpy.data.collections.get('Cameras_Render') if coll: return [obj for obj in coll.objects if obj.type == 'CAMERA'] # Fallback: all cameras return [obj for obj in bpy.data.objects if obj.type == 'CAMERA'] def render_camera(cam, args, cam_index): scene = bpy.context.scene scene.camera = cam scene.render.filepath = os.path.join(args.output, f"{cam.name}_####") # Render animation bpy.ops.render.render(animation=True, write_still=False) print(f"✅ Rendered camera: {cam.name}") def main(): args = parse_args() # Load scene bpy.ops.wm.open_mainfile(filepath=args.scene) # Setup setup_scene(args) # Get cameras cameras = get_cameras(args) if not cameras: print("❌ No cameras found!") return print(f"🎬 Rendering {len(cameras)} cameras, frames {args.frame_start}-{args.frame_end}") # Render each camera for i, cam in enumerate(cameras): if cam: render_camera(cam, args, i) print("✅ Batch render complete!") if __name__ == "__main__": main() ``` ### batch_convert.py ```python #!/usr/bin/env python3 """Batch Convert .blend → GLB/FBX/USDZ with LODs""" import bpy import os import sys import json import argparse from pathlib import Path def parse_args(): argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] parser = argparse.ArgumentParser() parser.add_argument("--input-dir", required=True, help="Source directory with .blend files") parser.add_argument("--output-dir", required=True, help="Output directory") parser.add_argument("--formats", nargs="+", default=["GLTF", "FBX", "USDZ"]) parser.add_argument("--lod-levels", type=int, default=3) parser.add_argument("--decimate-ratios", nargs="+", type=float, default=[1.0, 0.5, 0.25, 0.1]) parser.add_argument("--draco", action="store_true") parser.add_argument("--registry", default="assets_registry.json") return parser.parse_args(sys.argv[sys.argv.index("--") + 1:]) def create_lods(obj, ratios): """Create LOD meshes via Decimate modifier""" lods = [obj] for i, ratio in enumerate(ratios[1:], 1): lod = obj.copy() lod.data = obj.data.copy() lod.name = f"{obj.name}_LOD{i}" bpy.context.collection.objects.link(lod) mod = lod.modifiers.new(f"Decimate_LOD{i}", 'DECIMATE') mod.ratio = ratio mod.use_collapse_triangulate = True bpy.context.view_layer.objects.active = lod bpy.ops.object.modifier_apply(modifier=mod.name) lods.append(lod) return lods def export_gltf(objects, filepath, draco=False): bpy.ops.export_scene.gltf( filepath=filepath, export_format='GLB', use_selection=True, export_apply=True, export_tangents=True, export_materials='EXPORT', export_animations=True, export_draco_mesh_compression_enable=draco, export_draco_mesh_compression_level=6, ) def export_fbx(objects, filepath): bpy.ops.export_scene.fbx( filepath=filepath, use_selection=True, apply_unit_scale=True, apply_scale_options='FBX_SCALE_ALL', bake_space_transform=True, object_types={'ARMATURE', 'MESH'}, use_armature_deform_only=True, add_leaf_bones=False, primary_bone_axis='Y_FORWARD', secondary_bone_axis='X_UP', ) def export_usdz(objects, filepath): bpy.ops.wm.usd_export( filepath=filepath, export_selected=True, export_animation=True, export_materials=True, ) def process_blend(blend_path, args): # Load bpy.ops.wm.open_mainfile(filepath=str(blend_path)) # Select exportable objects objects = [obj for obj in bpy.data.objects if obj.get("export", True) and obj.type == 'MESH'] if not objects: print(f"⚠️ No exportable objects in {blend_path.name}") return # Create LODs all_objects = [] for obj in objects: lods = create_lods(obj, args.decimate_ratios) all_objects.extend(lods) # Output dir asset_name = blend_path.stem out_dir = Path(args.output_dir) / asset_name out_dir.mkdir(parents=True, exist_ok=True) metadata = {"asset": asset_name, "version": "1.0", "exports": {}} # Export each format if "GLTF" in args.formats: gltf_dir = out_dir / "gltf" gltf_dir.mkdir(exist_ok=True) for obj in all_objects: bpy.ops.object.select_all(action='DESELECT') obj.select_set(True) bpy.context.view_layer.objects.active = obj filepath = gltf_dir / f"{obj.name}.glb" export_gltf([obj], str(filepath), args.draco) metadata["exports"][f"gltf_{obj.name}"] = str(filepath) if "FBX" in args.formats: fbx_dir = out_dir / "fbx" fbx_dir.mkdir(exist_ok=True) for obj in all_objects: bpy.ops.object.select_all(action='DESELECT') obj.select_set(True) bpy.context.view_layer.objects.active = obj filepath = fbx_dir / f"{obj.name}.fbx" export_fbx([obj], str(filepath)) metadata["exports"][f"fbx_{obj.name}"] = str(filepath) if "USDZ" in args.formats: usdz_dir = out_dir / "usdz" usdz_dir.mkdir(exist_ok=True) for obj in all_objects: bpy.ops.object.select_all(action='DESELECT') obj.select_set(True) bpy.context.view_layer.objects.active = obj filepath = usdz_dir / f"{obj.name}.usdz" export_usdz([obj], str(filepath)) metadata["exports"][f"usdz_{obj.name}"] = str(filepath) # Save metadata (out_dir / "metadata.json").write_text(json.dumps(metadata, indent=2)) print(f"✅ Processed: {asset_name}") def main(): args = parse_args() input_dir = Path(args.input_dir) blend_files = list(input_dir.rglob("*.blend")) print(f"🔍 Found {len(blend_files)} .blend files") for blend_file in blend_files: try: process_blend(blend_file, args) except Exception as e: print(f"❌ Error processing {blend_file}: {e}") if __name__ == "__main__": main() ``` ### batch_validate.py ```python #!/usr/bin/env python3 """Batch QA Validation для библиотеки ассетов""" import bpy import os import sys import json import argparse from pathlib import Path def validate_object(obj): errors = [] warnings = [] # Geometry checks if obj.type == 'MESH': mesh = obj.data # Non-manifold import bmesh bm = bmesh.new() bm.from_mesh(mesh) non_manifold = [e for e in bm.edges if not e.is_manifold] if non_manifold: errors.append(f"{obj.name}: {len(non_manifold)} non-manifold edges") bm.free() # UV checks if not mesh.uv_layers: warnings.append(f"{obj.name}: No UV maps") else: # Check overlaps (simplified) uv_layer = mesh.uv_layers.active if uv_layer: uvs = [loop[uv_layer].uv for poly in mesh.polygons for loop in poly.loop_indices] if len(uvs) != len(set((round(u.x,4), round(u.y,4)) for u in uvs)): warnings.append(f"{obj.name}: Possible UV overlaps") # Scale if any(abs(s - 1.0) > 0.001 for s in obj.scale): warnings.append(f"{obj.name}: Scale not applied {obj.scale}") # Naming convention import re pattern = r'^[a-z]+_[a-z0-9_]+(_v\d+)?(_LOD\d+)?$' if not re.match(pattern, obj.name): warnings.append(f"{obj.name}: Naming convention mismatch") # Materials for mat_slot in obj.material_slots: if mat_slot.material: mat = mat_slot.material if mat.use_nodes: # Check for missing textures for node in mat.node_tree.nodes: if node.type == 'TEX_IMAGE' and not node.image: warnings.append(f"{obj.name}: Material '{mat.name}' has empty Image Texture node '{node.name}'") return {"errors": errors, "warnings": warnings} def main(): parser = argparse.ArgumentParser() parser.add_argument("--assets-dir", required=True) parser.add_argument("--output", default="qa_report.json") parser.add_argument("--naming-pattern", default=r'^[a-z]+_[a-z0-9_]+(_v\d+)?(_LOD\d+)?$') args = parser.parse_args(sys.argv[sys.argv.index("--") + 1:]) assets_dir = Path(args.assets_dir) blend_files = list(assets_dir.rglob("*.blend")) report = {"summary": {"total": 0, "passed": 0, "failed": 0}, "assets": {}} for blend_file in blend_files: print(f"🔍 Validating: {blend_file}") bpy.ops.wm.open_mainfile(filepath=str(blend_file)) asset_name = blend_file.stem asset_report = {"file": str(blend_file), "objects": {}, "passed": True} for obj in bpy.data.objects: if obj.type in {'MESH', 'ARMATURE', 'CAMERA', 'LIGHT'}: result = validate_object(obj) asset_report["objects"][obj.name] = result if result["errors"]: asset_report["passed"] = False if asset_report["passed"]: report["summary"]["passed"] += 1 else: report["summary"]["failed"] += 1 report["summary"]["total"] += 1 report["assets"][asset_name] = asset_report # Save report Path(args.output).write_text(json.dumps(report, indent=2, ensure_ascii=False)) print(f"\n📊 QA Report: {report['summary']['passed']}/{report['summary']['total']} passed") print(f"Report saved to: {args.output}") if __name__ == "__main__": main() ``` --- ## 🐚 BASH WRAPPERS ДЛЯ CI/CD ### render.sh ```bash #!/bin/bash # Usage: ./render.sh scene.blend [camera1 camera2...] SCENE=$1 shift CAMERAS=$@ blender -b "$SCENE" -P batch_render.py -- \ --cameras $CAMERAS \ --frame-start 1 --frame-end 250 \ --resolution 3840 2160 \ --samples 256 \ --format EXR \ --engine CYCLES \ --denoise \ --compositor \ --output //renders/ ``` ### convert.sh ```bash #!/bin/bash # Usage: ./convert.sh /path/to/assets /path/to/output blender -b -P batch_convert.py -- \ --input-dir "$1" \ --output-dir "$2" \ --formats GLTF FBX USDZ \ --lod-levels 3 \ --decimate-ratios 1.0 0.5 0.25 0.1 \ --draco ``` ### validate.sh ```bash #!/bin/bash # Usage: ./validate.sh /path/to/assets blender -b -P batch_validate.py -- \ --assets-dir "$1" \ --output qa_report.json ``` --- ## 🔄 CI/CD INTEGRATION (GitHub Actions) ### .github/workflows/batch.yml ```yaml name: Batch Processing on: schedule: - cron: '0 3 * * *' # Nightly workflow_dispatch: inputs: operation: type: choice options: [render, convert, validate, all] default: all jobs: batch: runs-on: ubuntu-latest container: image: nvidia/cuda:12.1-devel-ubuntu22.04 options: --gpus all steps: - uses: actions/checkout@v4 - name: Install Blender run: | apt-get update && apt-get install -y blender xvfb - name: Run Batch Operations run: | if [[ "${{ github.event.inputs.operation }}" == "render" || "${{ github.event.inputs.operation }}" == "all" ]]; then xvfb-run -a ./render.sh scenes/main.blend Cam_Main Cam_Wide Cam_Detail fi if [[ "${{ github.event.inputs.operation }}" == "convert" || "${{ github.event.inputs.operation }}" == "all" ]]; then ./convert.sh assets/source assets/exports fi if [[ "${{ github.event.inputs.operation }}" == "validate" || "${{ github.event.inputs.operation }}" == "all" ]]; then ./validate.sh assets/library fi - name: Upload Artifacts uses: actions/upload-artifact@v4 with: name: batch-results path: | renders/ exports/ qa_report.json retention-days: 7 ``` --- ## 📊 MONITORING & LOGGING ### Structured Logging ```python import logging import json from datetime import datetime class BatchLogger: def __init__(self, log_file="batch.log"): self.logger = logging.getLogger("batch") self.logger.setLevel(logging.INFO) handler = logging.FileHandler(log_file) handler.setFormatter(logging.Formatter('%(asctime)s | %(levelname)s | %(message)s')) self.logger.addHandler(handler) def log_start(self, operation, target, params): self.logger.info(json.dumps({ "event": "start", "operation": operation, "target": target, "params": params, "timestamp": datetime.now().isoformat() })) def log_end(self, operation, target, result, duration): self.logger.info(json.dumps({ "event": "end", "operation": operation, "target": target, "result": result, "duration_sec": duration, "timestamp": datetime.now().isoformat() })) def log_error(self, operation, target, error): self.logger.error(json.dumps({ "event": "error", "operation": operation, "target": target, "error": str(error), "timestamp": datetime.now().isoformat() })) ``` --- ## 🔗 СВЯЗИ - **Прочие в категории «Воркфлоу»:** [WF 01: TEXT-TO-SCENE](wf_01_text_to_scene.md), [WF 02: IMAGE-TO-3D](wf_02_image_to_3d.md), [WF 03: TEXTURE PIPELINE](wf_03_texture_pipeline.md), [WF 04: CHARACTER ANIMATION](wf_04_character_animation.md), [WF 05: PROCEDURAL ASSETS](wf_05_procedural_assets.md) - **По темам:** [P18: DRIVERS & CONSTRAINTS](../prompts/18_drivers_constraints.md), [P29: BATCH PROCESSING & CLI](../prompts/29_batch_cli.md), [SETUP 04: HEADLESS RENDER (CLI / CI/CD)](../setups/setup_04_headless_render.md), [SETUP 35: READY PLAYER ME AVATARS](../setups/setup_35_readyplayerme.md) - **Теги:** #automation, #workflow --- # WF 09: REALTIME VJ LOOP Tags: #animation #workflow # WF 09: REALTIME VJ LOOP **ID:** WF-09 | **Type:** Realtime | **Difficulty:** ⭐⭐ **RU/EN Bilingual** > Когда применять: когда нужен бесшовный цикл для выступления, студийного экрана или репетиции: 8 секунд, которые можно крутить в реальном времени, реагирующие на звук и управляемые извне. ## 🎯 ЦЕЛЬ Процедурная сцена с жёстким требованием **замкнутости цикла**: первый и последний кадр совпадают, поэтому 8-секундный ролик можно ставить на повтор без «щелчка». Реакция на звук и внешнее управление — надстройка, которая не ломает замкнутость. Главная ошибка в этой задаче — попытка делать это в Cycles. Cycles — движок офлайн-рендера. Реальное время — это EEVEE (в Blender 4.2+ он называется EEVEE Next) и вьюпорт. Всё, что ниже, проектируется под то, чтобы сцена **рендерилась во вьюпорте при 30+ fps**, а цикл потом рендерится офлайн с тем же результатом. > [!NOTE] > Идентификатор движка менялся: `BLENDER_EEVEE_NEXT` в Blender 4.2+ и `BLENDER_EEVEE` в более ранних. Никде не хардкодится — код читает текущий движок и подставляет подходящее имя. Точный список: `[i.identifier for i in scene.render.bl_rna.properties["engine"].enum_items]`. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия / состояние | Зачем | |---|---|---| | Blender с EEVEE | 4.2+ или старше, имя движка проверяем | рендер в реальном времени | | Видеомикшер / внешний пульт | опционально | управление извне | | UDP-сокет на localhost | стандартная библиотека Python | приём команд | | Огибающая звука | CSV из внешнего анализа | реакция на звук | ## 🛠️ НАСТРОЙКА ### Этап 1. Кадровая сетка цикла ```bash RU: "Цикл 8 секунд, 24 fps = 192 кадра. frame_start=1, frame_end=192 так, чтобы кадр 192 был дублем кадра 1 и при повторе не было скачка" EN: "8 s loop, 24 fps = 192 frames. frame_start=1, frame_end=192 so frame 192 duplicates frame 1 and looping has no jump" ``` ```python import bpy sc = bpy.context.scene FPS = 24 LOOP_FRAMES = 8 * FPS # 192 sc.render.fps = FPS sc.frame_start = 1 # последний кадр цикла = дубль первого, поэтому range на единицу короче sc.frame_end = LOOP_FRAMES sc.use_preview_range = False sc.sync_mode = 'FRAME_DROP' # лучше рваный темп, чем замедление def engine_evee(): names = [i.identifier for i in sc.render.bl_rna.properties["engine"].enum_items] for cand in ('BLENDER_EEVEE_NEXT', 'BLENDER_EEVEE'): if cand in names: return cand raise RuntimeError(f"EEVEE не найден среди движков: {names}") sc.render.engine = engine_evee() print("движок:", sc.render.engine, "кадров в цикле:", sc.frame_end) ``` ### Этап 2. Замкнутость: всё, что двигается, обязано вернуться Любой параметр, который не возвращается в исходное состояние, ломает цикл. Ротация — целое число оборотов, шум — периодический, фаза — по кадрам цикла. ```python import bpy, math LOOP_FRAMES = 192 def loop_rotation(obj, axis, turns): """Целое число оборотов за цикл = бесшовность гарантирована.""" obj.rotation_mode = 'XYZ' for f in range(sc.frame_start, sc.frame_end + 2): # +1: последний дубль первого phase = 2 * math.pi * turns * ((f - sc.frame_start) / LOOP_FRAMES) obj.rotation_euler[axis] = phase obj.keyframe_insert("rotation_euler", index=axis, frame=f) def loop_pingpong(obj, prop, index, lo, hi): """Треугольная волна: значение не скачет в конце цикла.""" half = LOOP_FRAMES / 2 for f in range(sc.frame_start, sc.frame_end + 2): t = ((f - sc.frame_start) % LOOP_FRAMES) / half val = lo + (hi - lo) * (1 - abs(1 - t)) getattr(obj, prop)[index] = val obj.keyframe_insert(prop, index=index, frame=f) pivot = bpy.data.objects.new("Loop_Pivot", None) sc.collection.objects.link(pivot) loop_rotation(pivot, 2, 1) # ровно один оборот за 8 секунд ``` Шум, который должен быть периодическим, считается через синус от номера кадра, а не через случайность. ```python import bpy, math driven = bpy.data.objects["Reactive_Core"] for f in range(sc.frame_start, sc.frame_end + 2): t = 2 * math.pi * (f - sc.frame_start) / LOOP_FRAMES # 0..2pi за цикл s = 1.0 + 0.15 * math.sin(3 * t) # период ровно один цикл driven.scale = (s, s, s) driven.keyframe_insert("scale", frame=f) ``` ### Этап 3. Проверка цикла — автоматическая, не на глаз ```python import bpy, os sc = bpy.context.scene out = bpy.path.abspath("//vj/check") os.makedirs(out, exist_ok=True) sc.render.resolution_x, sc.render.resolution_y = 640, 360 sc.render.image_settings.file_format = 'PNG' def render_frame(n): sc.frame_set(n) sc.render.filepath = os.path.join(out, f"chk_{n:04d}.png") bpy.ops.render.render(write_still=True) render_frame(sc.frame_start) render_frame(sc.frame_end) a = bpy.data.images.load(os.path.join(out, f"chk_{sc.frame_start:04d}.png")) b = bpy.data.images.load(os.path.join(out, f"chk_{sc.frame_end:04d}.png")) pa, pb = list(a.pixels), list(b.pixels) diff = sum(abs(x - y) for x, y in zip(pa, pb)) / max(len(pa), 1) print(f"средняя разница пикселей между 1 и {sc.frame_end}: {diff:.6f}") print("цикл замкнут" if diff < 0.002 else "цикл РАЗОМКНУТ — найди незамкнутую анимацию") ``` ### Этап 4. Реакция на звук Как и в [WF 27](wf_27_sound_sync_edit.md): огибающую считают вне Blender, здесь она приходит с частотой кадра цикла. ```python import bpy, os csv_path = r"C:\BLENDER ai\out\env_loop.csv" amps = [] if os.path.exists(csv_path): with open(csv_path, encoding="utf-8") as fh: for line in fh: line = line.strip() if line: amps.append(float(line.replace(",", "."))) print("окон огибающей:", len(amps)) ob = bpy.data.objects["Audio_Reactive_Array"] for f in range(sc.frame_start, sc.frame_end + 2): i = int((f - sc.frame_start) * len(amps) / LOOP_FRAMES) if amps else 0 amp = amps[min(i, len(amps) - 1)] if amps else 0.3 ob.scale = (1.0, 1.0, 0.4 + amp * 4.0) ob.keyframe_insert("scale", frame=f) ``` ### Этап 5. Внешнее управление (мост к пульту/TouchDesigner) Обмен идёт через UDP локально. Никаких внешних пакетов не нужно — стандартный `socket`. ```python import bpy, socket, threading, json def make_receiver(port=8765): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind(("127.0.0.1", port)) # только localhost: наружу не выставляем sock.settimeout(0.05) # не блокируем UI def loop(): while True: try: data, _ = sock.recvfrom(4096) except socket.timeout: continue except OSError: break try: msg = json.loads(data.decode("utf-8")) except (UnicodeDecodeError, json.JSONDecodeError): continue for target, val in msg.items(): ob = bpy.data.objects.get(target) if ob is None: continue setattr(ob, "rotation_euler", tuple(val) if isinstance(val, list) else ob.rotation_euler) t = threading.Thread(target=loop, daemon=True) t.start() return sock sock = make_receiver(8765) print("слушаю 127.0.0.1:8765") ``` Формат пакета: JSON вида `{"Loop_Pivot": [0.0, 0.0, 1.5708]}`. Отправить вручную: ```python import socket, json s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.sendto(json.dumps({"Loop_Pivot": [0, 0, 0.785]}).encode("utf-8"), ("127.0.0.1", 8765)) ``` > [!NOTE] > Приём в отдельном потоке и запись в `rotation_euler` из него — не идеальная схема: Blender не любит изменение данных из чужого потока. Рабочий компромисс — поток только складывает команды в очередь, а очередь разбирается из таймера на `bpy.app.timers` в главном потоке. Если начнутся артефакты, переходи на этот вариант. ### Этап 6. Воспроизведение в реальном времени и финальный рендер ```python import bpy, math sc = bpy.context.scene ob = bpy.data.objects["Reactive_Core"] rng = bpy.data.objects["Audio_Reactive_Array"] # быстрый превью-рендер вьюпорта во весь экран for area in bpy.context.screen.areas: if area.type == 'VIEW_3D': area.spaces.active.region_3d.view_perspective = 'CAMERA' area.spaces.active.shading.type = 'RENDERED' if not bpy.app.background: bpy.ops.screen.animation_play() # офлайн-рендер того же самого: EEVEE -> PNG-последовательность -> сборка в видео sc.render.resolution_x, sc.render.resolution_y = 1920, 1080 sc.render.filepath = r"//vj/loop_" bpy.ops.render.render(animation=True) ``` Сборка в видео делается внешним ffmpeg, потому что тайминг цикла должен совпасть с монтажным: ```bash ffmpeg -y -framerate 24 -i "vj/loop_0001.png" -c:v libx264 -crf 16 -pix_fmt yuv420p "vj/loop.mp4" ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `LOOP_FRAMES` | `fps × секунды` | длина цикла; 192 кадра — универсально монтируется | | `frame_end` | равен `LOOP_FRAMES`, не на единицу меньше | последний кадр намеренно дубль первого | | `sync_mode` | `'FRAME_DROP'` | лучше пропуск кадра, чем замедление | | `shading.type` | `'RENDERED'` | превью с реальным освещением; `'MATERIAL'` быстрее, но обманывает | | разница пикселей 1↔N | < 0.002 | порог, при котором цикл считается замкнутым | | оборотов за цикл | целое число | дробное число = щелчок на стыке | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Щелчок при повторе цикла | какой-то параметр не вернулся в начальное значение | прогнать проверку из этапа 3 и найти незамкнутую анимацию | | `ValueError` на `engine` | хардкод имени EEVEE | резолвер `engine_evee()` выше | | Вьюпорт 8 fps, сцена «тяжёлая» | слишком много объектов/материалов для EEVEE | инстансы вместо копий, один материал, отключить тени у мелких объектов | | Звук реагирует с задержкой | огибающая грубее кадра | считать огибающую с частотой не ниже частоты кадров | | Пропы при проигрывании | `sync_mode` не настроен | `FRAME_DROP`, уменьшить нагрузку | | Порты UDP не принимают | сокет слушает только localhost | это осознанное ограничение; наружу не выставлять без необходимости | | FFmpeg не собрал видео | кадры начинаются с 0001, а не с 0000 | сверить нумерацию и `-framerate` под фактический префикс | ## ✅ РЕЗУЛЬТАТ На выходе: `.blend` с 192-кадровым замкнутым циклом, PNG-последовательность `//vj/loop_####.png`, готовый `loop.mp4` и скрипт-приёмник команд на 127.0.0.1:8765. Характеристики — **оценка по порядко величины, а не измеренный результат**: сцена на 5–20 тыс. полигонов с одним-двумя материалами в EEVEE держит 30–60 fps на средней дискретной видеокарте. Рендер 192 кадров 1920×1080 в EEVEE — единицы секунд на кадр на старых картах, доли секунды на новых, то есть от десятков секунд до нескольких минут. Кадр PNG 1920×1080 — 2–6 МБ, весь цикл — сотни мегабайт, поэтому для видео сразу рендерят в H.264. **Это оценка.** Всё зависит от количества объектов, материала и GPU; измеряй у себя, и для превью ориентируйся на fps во вьюпорте, а не на время офлайн-рендера. Базовые сетапы: [SETUP 01: BLENDER MCP BASIC](../setups/setup_01_blender_mcp_basic.md) — транспорт команд в Blender, [SETUP 08: ASSET PIPELINE](../setups/setup_08_asset_pipeline.md) — общая схема работы с ассетами. ## 🔗 СВЯЗИ - **Прочие в категории «Воркфлоу»:** [WF 04: CHARACTER ANIMATION](wf_04_character_animation.md), [WF 14: CLOTH FASHION SHOW](wf_14_cloth_fashion_show.md), [WF 17: STOP-MOTION FELT LOOK](wf_17_stop_motion_felt.md), [WF 23: VIRTUAL PRODUCTION LED](wf_23_virtual_production_led.md), [WF 24: PHOTOGRAMMETRY CLEANUP](wf_24_photogrammetry_cleanup.md) - **По темам:** [CASE 04: CHARACTER RIG & ANIMATION](../cases/case_04_character_rig.md), [CASE 06: ANIMATION LOOP](../cases/case_06_animation_loop.md), [P14: CAMERA & COMPOSITION](../prompts/14_camera_composition.md) - **Теги:** #animation, #workflow --- # WF 10: PRODUCT TURNTABLE Tags: #product #workflow # WF 10: PRODUCT TURNTABLE **ID:** WF-10 | **Type:** Product | **Difficulty:** ⭐ **RU/EN Bilingual** ## 🎯 ЦЕЛЬ / Goal RU: 360° turntable 120f, studio HDRI, DOF EN: 360° turntable 120f, studio HDRI, DOF На выходе — ровный 360°-облёт товара на белом/нейтральном фоне, пригодный и для каталога, и для карточки на маркетплейсе. Ключевая сложность не в рендере, а в трёх вещах, которые обычно и делают turntable плохим: неравномерный шаг вращения (кадры «дёргаются»), пылающие блики от неудачного HDRI и уход формы к краям кадра из-за перспективы. Все три лечатся настройкой, а не постом. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Blender | 4.x | сцена, Cycles | | HDRI | Poly Haven, 1–2k | отражения на металле/лаке; фон перекрывается | | Модель товара | GLB/FBX/OBJ | единый меш или набор, вращается целиком | | GPU | желательно 8+ ГБ VRAM | 256 сэмплов по 1920×1920 | ## 🛠 НАСТРОЙКА ### Этап 1. Сцена: пол и «студийная» среда ```python import bpy, math scene = bpy.context.scene scene.render.fps = 24 scene.frame_start, scene.frame_end = 1, 120 # 120 кадров = ровно 5.0 с # Пол: не зеркало, но и не матовое стекло. Roughness 0.35 даёт мягкое # «подложное» отражение, на котором товар читается, а не тонет. bpy.ops.mesh.primitive_plane_add(size=20, location=(0, 0, 0)) floor = bpy.context.object floor.name = "StudioFloor" mat = bpy.data.materials.new("Mat_Floor") mat.use_nodes = True bsdf = mat.node_tree.nodes["Principled BSDF"] bsdf.inputs["Base Color"].default_value = (0.85, 0.85, 0.86, 1.0) bsdf.inputs["Roughness"].default_value = 0.35 floor.data.materials.append(mat) ``` ### Этап 2. HDRI в World ```python world = bpy.data.worlds.new("Studio") if not bpy.data.worlds else bpy.data.worlds[0] scene.world = world world.use_nodes = True nt = world.node_tree nt.nodes.clear() env = nt.nodes.new("ShaderNodeTexEnvironment") bg = nt.nodes.new("ShaderNodeBackground") out = nt.nodes.new("ShaderNodeOutputWorld") # strength низкий: HDRI здесь работает источником отражений, а не светом. # Основной свет —Area-источники ниже, иначе будет двойное освещение. bg.inputs["Strength"].default_value = 0.35 nt.links.new(env.outputs["Color"], bg.inputs["Color"]) nt.links.new(bg.outputs["Background"], out.inputs["Surface"]) env.image = bpy.data.images.load("//hdri/studio_small_08_1k.hdr") env.location = (math.radians(90), 0, math.radians(180)) # свет падает сзади-слева ``` ### Этап 3. Три источника: key / fill / rim ```python def area_light(name, loc, rot, energy, size, size_y=None): bpy.ops.object.light_add(type='AREA', location=loc, rotation=rot) ob = bpy.context.object ob.name = name ob.data.energy = energy ob.data.size = size if size_y: ob.data.shape = 'RECTANGLE' # узкая полоса = длинный блик на цилиндре ob.data.size_y = size_y return ob # key: основной, мягкий, сверху-слева area_light("Key", (1.2, -1.6, 1.8), (math.radians(-45), 0, math.radians(35)), 400, 1.5, 1.5) # fill: в 4 раза слабее, без теней по краям area_light("Fill", (-1.6, -1.0, 0.9), (math.radians(-70), 0, math.radians(-60)), 100, 2.0) # rim: узкая полоса сзади, рисует контур против фона area_light("Rim", (0.0, 1.8, 1.4), (math.radians(-60), 0, 0), 250, 2.0, 0.35) ``` ### Этап 4. Камера и DOF ```python cam_data = bpy.data.cameras.new("Cam") cam = bpy.data.objects.new("Cam", cam_data) scene.collection.objects.link(cam) scene.camera = cam # 85 мм — телеобъектив: минимальная перспективная дисторсия, товар «прижат» к кадру cam_data.lens = 85 cam.location = (0.0, -1.9, 0.32) # Направить на центр товара. aim — это track-to без лишнего пустого объекта. import mathutils direction = mathutils.Vector((0, 0, 0.09)) - cam.location cam.rotation_euler = direction.to_track_quat('-Z', 'Y').to_euler() cam_data.dof.use_dof = True cam_data.dof.focus_distance = (mathutils.Vector((0, 0, 0.09)) - cam.location).length cam_data.dof.aperture_fstop = 8.0 # f/8 — лёгкое размытие только по краям ``` ### Этап 5. Вращение — драйвер, а не keyframe Keyframe с интерполяцией даёт рывки на стыке и требует ручной правки easing. Драйвер от `frame` даёт идеально линейный, математически бесшовный оборот. ```python pivot = bpy.data.objects.new("Turntable", None) # пустой — точка вращения scene.collection.objects.link(pivot) for ob in bpy.data.objects: if ob.type == 'MESH' and ob is not pivot: ob.parent = pivot d = pivot.driver_add("rotation_euler", 2).driver d.type = 'SCRIPTED' d.expression = "2*pi*frame/120" # 3° на кадр, кадр 121 ≡ кадр 1 → шва нет ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `frame_end` | 120 | 90 кадров = 4°/кадр (быстрее), 240 = 1.5°/кадр (плавнее) | | `render.fps` | 24 | 30 → 4 с при тех же 120 кадрах | | `cycles.samples` | 256 | 128 для превью, 512 для глянца | | `view_settings.view_transform` | `'AgX'` | `'Filmic'` в версиях до 4.0 | | `dof.aperture_fstop` | 8.0 | меньше — сильнее размытие краёв | ```python scene.render.engine = 'CYCLES' scene.cycles.device = 'GPU' scene.cycles.samples = 256 scene.cycles.use_adaptive_sampling = True scene.cycles.adaptive_threshold = 0.01 scene.cycles.use_denoising = True scene.view_settings.view_transform = 'AgX' # Разрешение под карточку товара — квадрат scene.render.resolution_x = scene.render.resolution_y = 1920 scene.render.film_transparent = False scene.render.image_settings.file_format = 'PNG' scene.render.image_settings.color_mode = 'RGBA' scene.render.image_settings.color_depth = '16' # 16 бит — потом не жалко сводить в монтаже scene.render.filepath = "//render/turntable_" bpy.ops.render.render(animation=True) # Сборка в видео scene.render.image_settings.file_format = 'FFMPEG' scene.render.ffmpeg.format = 'MPEG4' scene.render.ffmpeg.codec = 'H264' scene.render.ffmpeg.constant_rate_factor = 'HIGH' scene.render.filepath = "//render/turntable.mp4" bpy.ops.render.render(animation=True) ``` > [!NOTE] > Идентификаторы в `view_transform` зависят от версии: `AgX` появился в Blender 4.0, > до этого был `Filmic`. Если присваивание падает с TypeError — перечисли доступные > значения `[i.identifier for i in scene.view_settings.bl_rna.properties['view_transform'].enum_items]`. ## 📤 РЕЗУЛЬТАТ | Параметр | Значение | |---|---| | Объём | 120 кадров × 3° = полный оборот, 5 с при 24 fps | | Файлы | `turntable_0001.png … turntable_0120.png` (16 бит) + `turntable.mp4` | | Разрешение | 1920 × 1920 (квадрат под карточку) | | Шов | нет: кадр 121 математически совпадает с кадром 1 | | Рендер | ~1.5–4 с/кадр на GPU среднего класса → **3–8 минут** на 120 кадров | Побочный, но полезный выход: `.blend` с драйвером и тремя источниками — это же заготовка для WF 26 (LOOKDEV TURNTABLE LAB), где тот же риг крутится для подбора материала. ## ✅ QA перед сдачей Ротация «проходит» в трёх местах, и все три ловятся глазом на нескольких кадрах: момент, когда товар стоит боком (виден самый толстый блик), момент с отражением в полу, и момент, когда блик уходит за контур силуэта. Если блик не «съезжает» с изделия между кадрами — свет стоит слишком широко или HDRI слишком мягкий. ```python import bpy, glob # Контрольный скрипт: все кадры на месте, ни один не чёрный out = sorted(glob.glob("//render/turntable_*.png")) missing = [i for i in range(1, 121) if f"turntable_{i:04d}.png" not in out] print("кадров:", len(out), "| пропущено:", missing) for path in out[::20]: # каждый 20-й, а не все 120 img = bpy.data.images.load(path) px = list(img.pixels[:4000]) # угол кадра: там всегда фон if max(px) < 0.02: print("ПУСТОЙ/ЧЁРНЫЙ:", path) ``` Второй контрольный скрипт — про сам риг, а не про файлы. Он ловит две поломки, которых не видно на превью: ключи поверх драйвера (тогда шаг не постоянен) и незамкнутый оборот (тогда стык всплывёт при сведении). Заодно печатает реальную стоимость геометрии — это и есть то, из чего складывается время рендера. ```python import bpy, math, glob, os scene = bpy.context.scene pivot = bpy.data.objects["Turntable"] prod = next(o for o in scene.objects if o.type == 'MESH' and o.name != "StudioFloor") # 1. Пять СОСЕДНИХ кадров должны дать пять одинаковых приращений seq = [] for f in range(1, 6): scene.frame_set(f) bpy.context.view_layer.update() seq.append(pivot.rotation_euler.z) steps = [round(b - a, 6) for a, b in zip(seq, seq[1:])] print("шаг/кадр, рад:", steps, "| шагов:", len(steps)) # 2. Замкнутость: за frame_end кадров должно набежать ровно 2*pi scene.frame_set(scene.frame_end + 1) bpy.context.view_layer.update() sweep = pivot.rotation_euler.z - seq[0] print("оборот за", scene.frame_end, "кадров, рад:", round(sweep, 6), "| 2*pi =", round(2 * math.pi, 6)) # 3. Полигоны против треугольников: считает движок вторые, поэтому # calc_loop_triangles() — честная мера тяжести сцены me = prod.data me.calc_loop_triangles() print(prod.name, "| вершин:", len(me.vertices), "| полигонов:", len(me.polygons), "| треугольников:", len(me.loop_triangles)) # 4. Кадры на диске files = sorted(glob.glob(os.path.join(bpy.path.abspath("//render"), "turntable_*.png"))) print("кадров на диске:", len(files), "| ожидалось:", scene.frame_end) ``` Если шаги не равны — в сцене остались keyframe поверх драйвера. Если `sweep` меньше `2*pi` — в выражении стоит не тот знаменатель: должно быть `2*pi*frame/120` при 120 кадрах, а не `2*pi*frame/24`. Расхождение треугольников и полигонов (968 против 500 на monkey Suzanne) нормально: n-угольник — это n−2 треугольника, и движок раскладывает так всю геометрию. Проверка на стыке цикла: открой кадры `turntable_0001.png` и `turntable_0120.png` рядом. Между ними должен быть ровно один шаг (3°) — такой же, как между 0001 и 0002. Если между ними заметный «перескок», в сцене остались keyframe поверх драйвера. ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Товар «прыгает» в кадре по высоте | пивот не в центре масс, сетка смещена по Z | совмести центр `pivot` с центром габаритов: `bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY')` | | Стык дёргается | keyframe-анимация с easing вместо драйвера | заменить на `driver` с выражением `2*pi*frame/N` | | Пятна на металле | HDRI со «съеденными» деталями в отражении | взять HDRI с резкими источниками, снизить `Strength` до 0.3 | | Товар обрезан по краям | камера слишком близко | отдали камеру или уменьши `lens` | | Блик полоской по всему изделию | источник слишком узкий и яркий | увеличить `size`, снизить `energy`, добавить fill | | Тёмные углы, плоская картинка | `world` с пустым `Background` вместо HDRI | проверь, что `env.image` назначен и `Strength > 0` | | Размыто всё, а не только края | слишком малое расстояние фокуса | `focus_distance` = расстояние до центра товара, `fstop` ≥ 5.6 | ## 🔗 СВЯЗИ - **Прочие в категории «Воркфлоу»:** [WF 26: LOOKDEV TURNTABLE LAB](wf_26_lookdev_turntable_lab.md), [WF 01: TEXT-TO-SCENE](wf_01_text_to_scene.md), [WF 02: IMAGE-TO-3D](wf_02_image_to_3d.md), [WF 03: TEXTURE PIPELINE](wf_03_texture_pipeline.md), [WF 04: CHARACTER ANIMATION](wf_04_character_animation.md) - **По темам:** [CASE 10: SNEAKER PRODUCT HERO](../cases/case_10_sneaker_product.md), [CASE 14: FOOD RENDER (RAMEN)](../cases/case_14_food_r visualisation.md), [CASE 15: CAR STUDIO CINEMATIC](../cases/case_15_car_studio_cinematic.md), [P50: PORTFOLIO TURNTABLE & QA](../prompts/50_turntable_qa.md) - **Теги:** #product, #workflow --- # WF 11: ARCHVIZ DAY→NIGHT Tags: #archviz #workflow # WF 11: ARCHVIZ DAY→NIGHT **ID:** WF-11 | **Type:** ArchViz | **Difficulty:** ⭐⭐ **RU/EN Bilingual** ## 🎯 ЦЕЛЬ / Goal RU: Sun+HDRI morph, lightmix, 2 renders blend EN: Sun+HDRI morph, lightmix, 2 renders blend Один `.blend`, который даёт **два состояния** — яркий день и ночь — без дублирования сцены, и один параметр `daynight` от 0 (ночь) до 1 (день), который переключает их. Ключевая мысль: день и ночь различаются не «яркостью», а **составом света**. Днём источник один — солнце, и всё остальное почти не работает. Ночью источников много (фонари, окна, интерьер), они разного цвета и стоят на разной высоте. Если просто потушить `Sun.energy`, получится не ночь, а выключенный свет. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Blender | 4.x | Cycles (EEVEE годится для превью) | | HDRI day + HDRI night | Poly Haven, 1–2k | рефлексы в стекле и на полу | | Модель помещения/экстерьера | GLB/FBX | геометрия | | Эмиссивные материалы на окнах | — | «живые» окна ночью | ## 🛠 НАСТРОЙКА ### Этап 1. Одна ручка на весь свет Все источники, HDRIs и эмиссия завязываются на одно свойство сцены. Это и есть «lightmix» из постановки — не микс-нода, а один параметр. ```python import bpy, math scene = bpy.context.scene scene["daynight"] = 1.0 # 1 = день, 0 = ночь scene.id_properties_ui("daynight").update( min=0.0, max=1.0, description="0 = ночь, 1 = день") def drive(owner, path, index, expr): """Драйвер от scene['daynight'] с именем переменной t.""" fc = owner.driver_add(path, index) if index >= 0 else owner.driver_add(path) d = fc.driver d.type = 'SCRIPTED' v = d.variables.new() v.name, v.type = "t", 'SINGLE_PROP' v.targets[0].id = scene v.targets[0].data_path = '["daynight"]' d.expression = expr return d ``` > [!NOTE] > Шаг с `id` — не формальность. У `DriverTarget` есть отдельное поле `id_type`, и его > значение по умолчанию — `'OBJECT'`: присваивание сцены проверяется по нему и падает с > `TypeError: DriverTarget.id expected a Object type, not Scene` (проверено на 5.2.1 LTS). > Если в твоей версии ругается именно на эту строку, поставь рядом > `v.targets[0].id_type = 'SCENE'` **до** присваивания `id`. Список допустимых значений: > `[i.identifier for i in v.targets[0].bl_rna.properties["id_type"].enum_items]`. ### Этап 2. Солнце: высота, энергия, температура ```python bpy.ops.object.light_add(type='SUN', location=(0, 0, 10)) sun = bpy.context.object sun.name = "Sun" # Половина углового диаметра Солнца (~0.53°) — иначе тени неестественно резкие sun.data.angle = math.radians(0.53) sun.rotation_euler = (math.radians(52), 0, math.radians(-70)) # Ночью солнце уходит под горизонт и гаснет. Квадрат — чтобы в сумеречный # момент не было вспышки на линейной шкале. drive(sun, "rotation_euler", 0, "0.12 + 0.79*t") # 7° → 52° drive(sun.data, "energy", -1, "0.02 + 4.8*t*t") # Цвет: ночью холодный и синий, днём тёплый. Три драйвера — по одному на канал. drive(sun.data, "color", 0, "0.45 + 0.55*t") drive(sun.data, "color", 1, "0.60 + 0.36*t") drive(sun.data, "color", 2, "1.00 - 0.18*t") ``` ### Этап 3. World: небо днём, HDRI ночью ```python world = bpy.data.worlds.new("ArchViz") if not bpy.data.worlds else bpy.data.worlds[0] scene.world = world world.use_nodes = True nt = world.node_tree nt.nodes.clear() tex_env = nt.nodes.new("ShaderNodeTexEnvironment") tex_sky = nt.nodes.new("ShaderNodeTexSky") mix = nt.nodes.new("ShaderNodeMixShader") bg = nt.nodes.new("ShaderNodeBackground") bg_day = nt.nodes.new("ShaderNodeBackground") # почти прозрачный — свет даёт Sun bg_night= nt.nodes.new("ShaderNodeBackground") out = nt.nodes.new("ShaderNodeOutputWorld") tex_sky.sky_type = 'NISHITA' tex_sky.sun_elevation = math.radians(40) tex_sky.sun_rotation = math.radians(-70) tex_env.image = bpy.data.images.load("//hdri/night_street_1k.hdr") bg_night.inputs["Strength"].default_value = 0.25 bg_day.inputs["Strength"].default_value = 0.0 # днём небо рисует Sun, а не фон nt.links.new(tex_sky.outputs["Color"], bg.inputs["Color"]) nt.links.new(tex_env.outputs["Color"], bg_night.inputs["Color"]) nt.links.new(tex_sky.outputs["Color"], mix.inputs[1]) nt.links.new(bg_night.outputs["Background"], mix.inputs[2]) nt.links.new(bg.outputs["Background"], mix.inputs[1]) nt.links.new(mix.outputs["Shader"], out.inputs["Surface"]) ``` > [!NOTE] > У `ShaderNodeTexSky` в разных версиях менялись имена входов (`sun_elevation`, > `sun_rotation`, `sun_intensity`) и набор режимов `sky_type`. Проверь список входов > у ноды в своей версии — если поле называется иначе, код упадёт на `tex_sky.xxx`. ### Этап 4. Ночной свет: окна и интерьер Окна — не лампы, а эмиссия на материале. Это даёт мягкое свечение, которое видно и в отражениях, и на фасаде. ```python # Окна mat_win = bpy.data.materials["Mat_Window"] pbsdf = mat_win.node_tree.nodes["Principled BSDF"] drive(pbsdf.inputs["Emission Strength"], "default_value", -1, "6.0*(1.0 - t)") # Уличные фонари: Point/Area, гаснут днём for name, energy_day in (("Lamp_01", 180.0), ("Lamp_02", 140.0)): ob = bpy.data.objects.get(name) if ob: drive(ob.data, "energy", -1, f"{energy_day}*(1.0 - t)") # Интерьерный свет — самый слабый по мощности, но самый заметный для глаза drive(bpy.data.objects["Interior_Fill"].data, "energy", -1, "45.0*(1.0 - t)") ``` ### Этап 5. Рендер двух состояний ```python scene.render.engine = 'CYCLES' scene.cycles.samples = 256 scene.cycles.use_denoising = True scene.render.resolution_x, scene.render.resolution_y = 1920, 1080 scene.render.image_settings.file_format = 'PNG' scene.render.image_settings.color_depth = '16' for name, t in (("day", 1.0), ("night", 0.0)): scene["daynight"] = t bpy.context.view_layer.update() # без этого драйверы не пересчитаются scene.render.filepath = f"//render/interior_{name}.png" bpy.ops.render.render(write_still=True) ``` ### Этап 6. Сведение в композит-нодах ```python scene.use_nodes = True nt = scene.node_tree nt.nodes.clear() img_day = nt.nodes.new("CompositorNodeImage"); img_day.image = bpy.data.images.load("//render/interior_day.png") img_night = nt.nodes.new("CompositorNodeImage"); img_night.image= bpy.data.images.load("//render/interior_night.png") glare = nt.nodes.new("CompositorNodeGlare") glare.glare_type = 'FOG_GLOW' # только ночью; для дня отключи ноду mixc = nt.nodes.new("CompositorNodeMixRGB") comp = nt.nodes.new("CompositorNodeComposite") nt.links.new(img_day.outputs["Image"], mixc.inputs[1]) nt.links.new(img_night.outputs["Image"], mixc.inputs[2]) nt.links.new(glare.outputs["Image"], mixc.inputs[2]) nt.links.new(mixc.outputs["Image"], comp.inputs["Image"]) drive(mixc.inputs[0], "default_value", -1, "1.0 - t") # t=1 → день, t=0 → ночь scene.render.filepath = "//render/interior_dn_" bpy.ops.render.render(animation=True) ``` > [!NOTE] > В свежих версиях Blender нода Glare получила порты вместо полей: `Threshold`, > `Strength` и `Quality` могут быть входами. Если `glare.glare_type` не пишется — > открой ноду в композиторе и посмотри, что осталось свойством, а что стало входом. ### Этап 7. Анимация перехода «день → ночь» Сам переход делается анимацией одного ключа: 48 кадров (2 с при 24 fps) с линейной интерполяцией. Все источники разъезжаются сами, потому что завязаны на `daynight`. ```python # Ключ на самом свойстве сцены — единственное, что нужно анимировать scene["daynight"] = 1.0 scene.keyframe_insert('["daynight"]', frame=1) scene["daynight"] = 0.0 scene.keyframe_insert('["daynight"]', frame=48) # Тип интерполяции задаётся в Graph Editor (F-кривая сцены). # Линейная обязательна, иначе между ключами будет «плато». scene.frame_start, scene.frame_end = 1, 48 scene.render.filepath = "//render/dn_transition_" bpy.ops.render.render(animation=True) ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `scene["daynight"]` | 0…1 | единственная ручка; 0.3–0.7 даёт «сумерки» | | `sun.data.angle` | 0.53° | до 2° = мягкие тени, «пасмурно» | | `bg_night.Strength` | 0.25 | поднимать, если ночью не видно отражений | | `Emission Strength` | 6.0 | выше 10 — окна «сгорают» в чистом виде | | `cycles.samples` | 256 | 512 для финального света с окнами | ## 📤 РЕЗУЛЬТАТ | Параметр | Значение | |---|---| | Файлы | `interior_day.png`, `interior_night.png` (1920×1080, 16 бит) + `interior_dn_####.png` | | Сегмент анимации | 2 состояния × 1 кадр, либо 24-кадровый переход при анимации `daynight` | | `.blend` | один файл, в котором слайдер переключает свет без пересборки | | Рендер | 256 сэмплов, 1920×1080, interior: **4–12 мин/кадр**; два состояния ≈ 10–25 мин | Переход «день→ночь» как видео делается анимацией одного ключа на `scene["daynight"]` с линейным интерпейсом: 48 кадров (2 с при 24 fps) — и микшёр сам разедет свет, HDRIs, эмиссию и фонари синхронно. Проверяется одним скриптом. Он делает три вещи: пересчитывает драйверы и печатает, что каждый источник действительно поехал; считает драйверы по всем объектам сцены (ползунок есть, а половина света его не слушает — самая частая поломка этой схемы); сверяет, что на диске лежат оба состояния и вся серия перехода. ```python import bpy, os, glob scene = bpy.context.scene sun = bpy.data.objects["Sun"] # 1. Пересчёт драйверов. В фоновом режиме (-b) одного # view_layer.update() мало: сцену надо пометить update_tag, иначе # значения остаются от предыдущего вызова. def apply_daynight(t): scene["daynight"] = t scene.update_tag() bpy.context.evaluated_depsgraph_get().update() for t in (1.0, 0.5, 0.0): apply_daynight(t) print("t=%.1f наклон=%.3f рад энергия=%.3f красный=%.3f" % (t, sun.rotation_euler.x, sun.data.energy, sun.data.color[0])) # 2. Все ли источники подписаны на ползунок def driven(owner): ad = getattr(owner, "animation_data", None) return len(ad.drivers) if ad else 0 lamps = [n for n in ("Lamp_01", "Lamp_02") if n in bpy.data.objects] print("драйверов Sun.data:", driven(sun.data), "| фонарей:", sum(driven(bpy.data.objects[n].data) for n in lamps), "из", len(lamps)) # Драйвер на «Emission Strength» висит на дереве нод, а не на самой ноде win = bpy.data.materials["Mat_Window"] pb = next(n for n in win.node_tree.nodes if n.type == 'BSDF_PRINCIPLED') print("драйверов на 'Emission Strength':", driven(pb.inputs["Emission Strength"].id_data)) # 3. Файлы на диске out = bpy.path.abspath("//render") print("day:", os.path.exists(os.path.join(out, "interior_day.png")), "| night:", os.path.exists(os.path.join(out, "interior_night.png"))) seq = sorted(glob.glob(os.path.join(out, "interior_dn_*.png"))) print("кадров перехода:", len(seq), "| ожидалось:", scene.frame_end) ``` `энергия` на `t=0.0` должна упасть практически до нуля (в примере выше — 0.02), а `красный` канал — до 0.45: если оба остались дневными, драйверы не считаются и ты рендеришь два одинаковых кадра. Ноль драйверов на фонаре означает, что имя объекта в цикле не совпало с тем, что лежит в сцене — проверка по `n in bpy.data.objects` именно для этого и сделана. ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Ночью сцена чёрная | `Sun` всё ещё даёт энергию либо HDRI не назначен | проверить `scene.world` и `tex_env.image`; при `t=0` энергия солнца должна быть ≈ 0 | | Днём небо плоское | в Background дневного неба `Strength = 0` | это нормально: небо рисует `Sun`; если нужно и небо — верни небо в Background | | Переключил `daynight`, картинка не изменилась | драйверы не пересчитались | `bpy.context.view_layer.update()` или `scene.frame_set(frame + 1)` | | Слишком жёлтый ночной свет | не разделён цвет фонарей и окон | температуру задавать в `light.data.color`, а не тонировать кадр в композитe | | Окна светятся днём | забыл `Emission Strength` в драйвер | `(1.0 - t)` обязателен на каждой эмиссии | | Тени «плывут» при анимации | солнце ключевано без `Rotation`/`Rotation Mode` | проверить `sun.rotation_mode` и единицы (`DEGREES`) | ## 🔗 СВЯЗИ - **Прочие в категории «Воркфлоу»:** [WF 06: ARCHVIZ WORKFLOW](wf_06_archviz_workflow.md), [WF 24: PHOTOGRAMMETRY CLEANUP](wf_24_photogrammetry_cleanup.md), [WF 01: TEXT-TO-SCENE](wf_01_text_to_scene.md), [WF 02: IMAGE-TO-3D](wf_02_image_to_3d.md), [WF 03: TEXTURE PIPELINE](wf_03_texture_pipeline.md) - **По темам:** [CASE 01: ARCHVIZ INTERIOR](../cases/case_01_archviz_interior.md), [CASE 08: SCI-FI CORRIDOR MODULAR](../cases/case_08_sci_fi_corridor.md), [WF 04: CHARACTER ANIMATION](wf_04_character_animation.md) - **Теги:** #archviz, #workflow --- # WF 12: CROWD SIM GN Tags: #geometry-nodes #simulation #workflow # WF 12: CROWD SIM GN **ID:** WF-12 | **Type:** Simulation | **Difficulty:** ⭐⭐⭐ **RU/EN Bilingual** ## 🎯 ЦЕЛЬ / Goal RU: GN points+instances, avoidance, LOD EN: GN points+instances, avoidance, LOD Толпа из сотен и тысяч агентов, которые идут по сцене, не наезжая друг на друга, при 60 fps во вьюпорте и при этом не съедают память. Реальный путь здесь один: **не** объекты с физикой, а точки + инстансы в Geometry Nodes. Тысяча объектов — это тысяча итераций зависимостей и провал по времени кадра; тысяча инстансов — это один вызов отрисовки. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Blender | 4.0+ | интерфейс нод через `node_group.interface` | | Geometry Nodes | встроенный | вся логика толпы | | Коллекция с мешами агента | `bpy.data.collections` | источник инстансов | | Коллекция `LOD_High` / `LOD_Low` | — | два уровня детализации | > [!NOTE] > API интерфейса нодового дерева сменился в Blender 4.0: вместо `group.inputs.new(...)` / > `group.outputs.new(...)` используется `group.interface.new_socket(...)`. Если пишешь > под 3.x — проверь по руководству своей версии, старые вызовы падают. ## 🛠 НАСТРОЙКА ### Этап 1. Каркас: группа нод и выход ```python import bpy ground = bpy.data.objects["Ground"] mod = ground.modifiers.new("Crowd", 'NODES') ng = bpy.data.node_groups.new("CrowdGN", 'GeometryNodeTree') mod.node_group = ng # Blender 4.0+ — интерфейс создаётся явно. Без выхода дерево не запустится. ng.interface.new_socket(name="Geometry", in_out='INPUT', socket_type='NodeSocketGeometry') ng.interface.new_socket(name="Geometry", in_out='OUTPUT', socket_type='NodeSocketGeometry') gin = ng.nodes.new("NodeGroupInput"); gin.location = (-800, 0) gout = ng.nodes.new("NodeGroupOutput"); gout.location = (1200, 0) # Удобный хелпер: нода + линк одной строкой def add(idname, x, y, **props): n = ng.nodes.new(idname) n.location = (x, y) for k, v in props.items(): setattr(n, k, v) return n def link(a, sa, b, sb): return ng.links.new(a.outputs[sa], b.inputs[sb]) ``` ### Этап 2. Точки на полу с «слепыми зонами» ```python dist = add("GeometryNodeDistributePointsOnFaces", -500, 0) dist.distribute_method = 'POISSON' # равномернее, чем RANDOM, и быстрее рендерится dist.inputs["Density"].default_value = 0.8 # точек на м² # Слепые зоны: Delete Geometry по маске «точка попала в камеру/в стену» delete = add("GeometryNodeDeleteGeometry", -200, -250) delete.domain = 'POINT' delete.mode = 'ALL' link(gin, "Geometry", dist, "Mesh") link(dist, "Points", delete, "Geometry") ``` Маска «не ставить людей в этих зонах» делается отдельной нодой `GeometryNodeProximity` (входы `Target` и `Source Position`, выход `Distance`) плюс `Math (Less Than)` — то есть расстояние до объекта-исключения меньше порога ⇒ удалить. Тот же приём оставляет «коридор» для прохода и пустое пятно перед камерой. ### Этап 3. Два уровня детализации ```python col_hi = add("GeometryNodeCollectionInfo", -200, 250) col_hi.transform_space = 'ORIGINAL' col_hi.inputs["Collection"].default_value = bpy.data.collections["LOD_High"] col_hi.inputs["Separate Children"].default_value = True col_lo = add("GeometryNodeCollectionInfo", -200, -50) col_lo.transform_space = 'ORIGINAL' col_lo.inputs["Collection"].default_value = bpy.data.collections["LOD_Low"] col_lo.inputs["Separate Children"].default_value = True # Первые N индексов — дорогой меш, остальные — дешёвый силуэт idx = add("GeometryNodeInputIndex", -200, 400) cmp = add("ShaderNodeMath", 0, 400, operation='LESS_THAN') cmp.inputs[1].default_value = 150 # порог близости, подбирай на глаз link(idx, "Index", cmp, "Value") lod = add("GeometryNodeSwitch", 200, 150, input_type='GEOMETRY') link(cmp, "Value", lod, "Switch") link(col_lo, "Instances", lod, "False") link(col_hi, "Instances", lod, "True") ``` ### Этап 4. Движение по траектории — без кэша Ключ ко всему: `Scene Time` даёт секунды, умноженные на скорость, дают позицию вдоль кривой. Это детерминированно, считается за кадр и не требует запекания. ```python curve = add("GeometryNodeCurveToPoints", -500, -500) curve.inputs["Count"].default_value = 400 resample = add("GeometryNodeResampleCurve", -300, -500) resample.mode = 'COUNT' resample.inputs["Count"].default_value = 400 link(gin, "Geometry", resample, "Curve") t = add("GeometryNodeInputSceneTime", -500, -700) mul = add("ShaderNodeMath", -300, -700, operation='MULTIPLY') mul.inputs[1].default_value = 0.6 # скорость «единиц кривой в секунду» frac = add("ShaderNodeMath", -100, -700, operation='WRAP') frac.inputs[1].default_value = 1.0 # бесшовность: ходим по кругу link(t, "Seconds", mul, "Value") link(mul, "Value", frac, "Value") # инстансы двигаем Set Position вдоль оси Y, а поворот ставим через tangent setpos = add("GeometryNodeSetPosition", 600, 0) setpos.inputs["Selection"].default_value = True ``` ### Этап 5. Инстансы ```python inst = add("GeometryNodeInstanceOnPoints", 850, 0) link(delete, "Geometry", inst, "Points") link(lod, "Output", inst, "Instance") link(resample, "Mesh", inst, "Rotation") # Rotation-порт принимает и вектор, и поле link(setpos, "Geometry", gout, "Geometry") # Разброс по высоте/повороту, чтобы толпа не была «сеткой» rnd = add("FunctionNodeRandomValue", 400, -400, data_type='FLOAT_VECTOR') rnd.inputs["Min"].default_value = (-0.02, 0.0, -0.02) rnd.inputs["Max"].default_value = ( 0.02, 0.0, 0.02) link(rnd, "Value", inst, "Scale") ``` ### Этап 6. Производительность и проверка ```python # Тени от инстансов — самая дорогая часть; если не нужны, гасим на объекте ground.visible_shadow = False # Счётчик агентов в сцене — единственный честный способ проверить «сколько людей» bpy.context.view_layer.update() def count_points(obj, gname): ev = obj.evaluated_get(bpy.context.evaluated_depsgraph_get()) for m in ev.modifiers: if m.name == gname: return len(m.evaluated_get(bpy.data.meshes.new("tmp"))) return 0 ``` > [!NOTE] > Точный способ достать количество точек из модификатора зависит от того, где стоит > Realize Instances. Практичный вариант для QA — временно добавить `Realize Instances` > и посмотреть счётчик вершин в Edit Mode, либо использовать `Statistics` в системной > панели. Не рассчитывай на точный API-запрос: реализации отличаются между версиями. ### Этап 7. Раскладка по кадрам в композиторе Для QA толпу удобно смотреть не в 3D, а в виде линейки кадров: сразу видно, где агенты стоят стеной, а где разбежались. ```python scene.render.engine = 'BLENDER_EEVEE_NEXT' if 'BLENDER_EEVEE_NEXT' in \ {i.identifier for i in bpy.types.RenderSettings.bl_rna.properties['engine'].enum_items} \ else 'BLENDER_EEVEE' scene.render.resolution_x, scene.render.resolution_y = 1920, 1080 scene.render.fps = 24 scene.frame_start, scene.frame_end = 1, 120 scene.render.image_settings.file_format = 'PNG' scene.render.filepath = "//crowd/prev_" bpy.ops.render.render(animation=True) ``` ## ⚙️ КЛЮЧЕВЫЕ ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `Distribute Points density` | 0.8 /м² | выше 3 — упрётесь во время кадра | | `Distribute Points method` | `'POISSON'` | `'RANDOM'` быстрее, но даёт «сгустки» | | Порог LOD (`LESS_THAN`) | 150 | подбирай по расстоянию до камеры | | `Scene Time → Speed` | 0.6 | скорость шага | | `visible_shadow` | `False` | включай, когда тени нужны в кадре | ## 📤 РЕЗУЛЬТАТ | Параметр | Значение | |---|---| | Агентов | 400–3 000 на одну сцену без кэша | | Что получается | **не файл**, а живая сцена: вьюпорт 30–60 fps на средней видеокарте | | Кэш | не нужен — вся анимация детерминированная от `Scene Time` | | Превью-рендер | 120 кадров (5 с при 24 fps), 1920×1080, EEVEE, **~1–3 мин** | | Что можно забрать дальше | `Realize Instances` + Alembic — если нужно отдать в движок | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Модификатор красный, дерево не считается | не создан выход через `interface` | добавить `OUTPUT`-сокет `NodeSocketGeometry` | | `AttributeError` на `node_group.inputs` | API 4.0+ | использовать `node_group.interface.new_socket(...)` | | Агенты стоят столбом в ряд | точки распределены по сетке, а не случайно | `Distribute Points` с `Density` вместо ручного массива, увеличить `Seed` | | FPS падает на 1–2 объектах рядом с камерой | они попали в ветку `LOD_High` | снизить порог `LESS_THAN`, вынести тяжёлые детали в отдельную коллекцию | | Толпа «едет» рывками | скорость не согласована с длиной кривой | `Speed` = длина кривой / длительность цикла | | Экспорт в движок пустой | инстансы не realizes | `Realize Instances` перед Alembic, иначе движок не увидит геометрию | | Пропали агенты при смене коллекции | `Collection Info` не назначен | проверить `Separate Children` и путь к коллекции | > [!NOTE] > Полноценное избегание «агент-агент» в GN требует состояния между кадрами > (Simulation Zone) и поиска соседей. Встроенной пространственной хеш-таблицы у GN нет: > либо считают соседей вручную по сетке, либо выносят расталкивание в скриптовый > handler. Проверь, какие ноды зоны симуляции доступны в твоей версии, прежде чем > строить на них продакшн-пайплайн. ## 🔗 СВЯЗИ - **Прочие в категории «Воркфлоу»:** [WF 05: PROCEDURAL ASSETS](wf_05_procedural_assets.md), [WF 13: DESTRUCTION RBD](wf_13_destruction_rbd.md), [WF 15: FLUID PRODUCT SPLASH](wf_15_fluid_product_splash.md), [WF 01: TEXT-TO-SCENE](wf_01_text_to_scene.md), [WF 02: IMAGE-TO-3D](wf_02_image_to_3d.md) - **По темам:** [P45: GEOMETRY NODES SIMULATION ZONES](../prompts/45_gn_simulation.md), [SIMULATION & PHYSICS: GEOMETRY NODES 4.0+](../simulation/rigid_body_gn.md), [CASE 05: PROCEDURAL CITY](../cases/case_05_procedural_city.md), [P07: INSTANCING & ARRAYS](../prompts/07_instancing_arrays.md) - **Теги:** #geometry-nodes, #simulation, #workflow --- # WF 13: DESTRUCTION RBD Tags: #simulation #workflow # WF 13: DESTRUCTION RBD **ID:** WF-13 | **Type:** FX | **Difficulty:** ⭐⭐⭐ **RU/EN Bilingual** ## 🎯 ЦЕЛЬ / Goal RU: Cell fracture + RBD + dust, cache Alembic EN: Cell fracture + RBD + dust, cache Alembic Разрушаемая стена: разлетается на куски под ударом, куски физически падают и сталкиваются, в воздухе стоит пыль. И — кэш в Alembic, чтобы рендер считался много раз без повторной физики. Разрушение и пыль — это два разных рейдера с разными требованиями, и главная ошибка новичка — пытаться сделать обе части в один проход. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Blender | 4.x | встроенная физика и Fracture-модификатор | | Модель разрушаемого объекта | GLB/FBX | манifold-геометрия обязательна | | Alembic | встроенный | формат кэша симуляции | | VRAM | 8+ ГБ | 500 активных тел | > [!NOTE] > Fracture-модификатор требует **замкнутой (manifold) геометрии**. Модель с > неразрешёнными нормалями, двойными оболостями или незамкнутыми краями разобьётся > «в молоко» либо вообще не даст трещин. Проверяй `Select → All by Trait → Non Manifold` > до начала, а не после. ## 🛠 НАСТРОЙКА ### Этап 1. Fracture-модификатор: разбивка на ячейки ```python import bpy wall = bpy.data.objects["Wall"] mod = wall.modifiers.new("Fracture", 'FRACTURE') mod.fracture_type = 'CELL_FRACTURE' # вариантное разбиение: дёшево, стабильно mod.cell_type = 'RANDOM' mod.cell_divide = 12 # 12 = примерно 1728 ячеек на куб; начни с 6–8 mod.cell_size = 0.4 mod.use_separate_verts = True # критично: без этого куски не разделятся mod.show_viewport = True mod.show_render = True ``` Типы `fracture_type` и их поведение: | Тип | Когда использовать | Цена | |---|---|---| | `CELL_FRACTURE` | стены, панели, «кирпич» | дёшево, но видно регулярность | | `VORONOI_FRACTURE` | бетон, камень — нужна неправильная форма | дороже, сильнее зависит от топологии | | `RADIAL_FRACTURE` | взрыв от точки: `Number of Cuts` + `Range` | дёшево, хорош для стекла/зеркал | | `WEIGHTED_FRACTURE` | разрушение по весам из карты плотности | требует подготовки весов | ### Этап 2. Тела физики ```python bpy.ops.rigidbody.world_add() # не bpy.ops.object.rigidbody: там такого оператора нет rbw = bpy.context.scene.rigidbody_world rbw.substeps_per_frame = 10 # растёт вместе с числом тел rbw.solver_iterations = 20 pc = rbw.point_cache pc.frame_start, pc.frame_end = 1, 120 pc.use_disk_cache = True # кэш на диск — переживёт перезапуск Blender pc.compression = 'LIGHT' # Полу нужна коллизия. Добавь её вручную: Physics → Collision, # либо bpy.ops.object.modifier_add(type='COLLISION') на выделенном объекте. # Пол — пассивное тело: неподвижно, но участвует в столкновениях. floor = bpy.data.objects["Floor"] floor.rigid_body.type = 'PASSIVE' # Оригинал стены — источник удара, он сам не падает wall.rigid_body.type = 'KINEMATIC' wall.rigid_body.kinematic = True # включить анимацию/ключ в момент удара wall.rigid_body.use_deactivation = True ``` ### Этап 3. Получить куски как настоящие объекты У модификатора Fracture есть два режима работы: интерактивный (через панель Physics, где куски создаются как внутренний объект) и «честный» — когда геометрия реально становится объектами. Второй надёжнее для скриптов и для Alembic. ```python # Путь A: внутренний объект раскола (имя зависит от версии — ищем по шаблону) frac = None for ob in bpy.data.objects: if ob.type == 'MESH' and "fracture" in ob.name.lower(): frac = ob break if frac is None: # Путь B: применяем модификатор и разделяем на отдельные объекты. # Минус: трещины больше не появятся позже, все куски существуют с кадра 1. bpy.context.view_layer.objects.active = wall bpy.ops.object.modifier_apply(modifier="Fracture") bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.separate(type='LOOSE') # каждая несвязанная часть → свой объект bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.select_all(action='DESELECT') chunks = [o for o in bpy.context.scene.objects if o.type == 'MESH' and o is not wall] for o in chunks: o.select_set(True) bpy.ops.rigidbody.objects_add(type='ACTIVE') # одним вызовом на всё выделенное bpy.ops.object.select_all(action='DESELECT') # Активация по очереди — иначе 500 тел стартуют в один кадр и «выстреливают» for i, o in enumerate(chunks): o.rigid_body.use_start_deactivated = True o.rigid_body.use_deactivation = True ``` > [!NOTE] > Имя объекта раскола (`<имя>_fracture` и его варианты) — внутренняя деталь Blender и > менялась между версиями. Поэтому в коде выше путь A ищет объект по подстроке, а если > не находит — откатывается на путь B. Не закладывайся на конкретное имя. ### Этап 4. Пыль: объёмный куб, а не частицы Для пыли в одном кадре объёмный куб дешевле частиц на порядок и не требует кэша. ```python bpy.ops.mesh.primitive_cube_add(size=6, location=(0, 0, 2)) dust = bpy.context.object dust.name = "DustVolume" dust.display_type = 'WIRE' # в сцене видно только гало, в рендере — объём mat = bpy.data.materials.new("Mat_Dust") mat.use_nodes = True nt = mat.node_tree nt.nodes.clear() vol = nt.nodes.new("ShaderNodeVolumeScatter") # одностороннее рассеяние: дешево и мягко vol.inputs["Color"].default_value = (0.62, 0.58, 0.52, 1.0) vol.inputs["Density"].default_value = 0.0 out = nt.nodes.new("ShaderNodeOutputMaterial") nt.links.new(vol.outputs["Volume"], out.inputs["Volume"]) dust.data.materials.append(mat) # Плотность появляется в момент удара и оседает for frame, val in ((1, 0.0), (28, 0.35), (75, 0.06), (120, 0.0)): vol.inputs["Density"].default_value = val vol.inputs["Density"].keyframe_insert("default_value", frame=frame) ``` Куб обязательно должен быть **замкнутым** и достаточно мелко разбит по сетке: взрослее объём выглядит «ватой». Проверь `bpy.context.scene.render.engine` — в Cycles объёмы считаются точно, в EEVEE быстрее, но с другими ограничениями на разрешение. ### Этап 5. Прогрев кэша Кэш считается только если сцена реально «проиграна», а не прокручена на кадрах. ```python scene = bpy.context.scene scene.frame_set(1) for f in range(1, 121): # покадровый прогон — сбрасывает инерцию и промахи scene.frame_set(f) bpy.ops.ptcache.bake_all(bake=True) ``` ### Этап 6. Экспорт в Alembic ```python bpy.ops.object.select_all(action='DESELECT') for o in chunks: o.select_set(True) bpy.ops.wm.alembic_export( filepath="//cache/wall_destruction.abc", start=1, end=120, selected=True, # только куски, без стены и пыли flatten=True, # общая трансформация — файл переносится без иерархии uvs=True, normals=True, apply_modifiers=True, ) ``` > [!NOTE] > Полный список флагов `wm.alembic_export` менялся; в частности, применение > модификаторов при экспорте управляется параметром, который в разных версиях называется > по-разному. Проверь актуальный список: наведи курсор на оператор в Python-консоли или > вызови `bpy.ops.wm.alembic_export.get_rna_type().properties.keys()`. ### Этап 7. Удар: кинематика как «пушка» Стена не падает сама — её нужно толкнуть в нужном кадре. Кинематическое тело с анимированным ключом позиции работает как импульс. ```python wall = bpy.data.objects["Wall"] wall.location.z = 0.0 wall.keyframe_insert("location", index=2, frame=1) wall.location.z = -0.8 # за 1 кадр вниз = резкий удар wall.keyframe_insert("location", index=2, frame=3) wall.location.z = -0.8 # дальше стоит: пока не пересчитаешь кэш wall.keyframe_insert("location", index=2, frame=4) # Порядок обязателен: удар -> прогрев -> запекание -> экспорт. # Если запечь до удара, кэш будет пустым. for f in range(1, 121): bpy.context.scene.frame_set(f) bpy.ops.ptcache.bake_all(bake=True) ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `cell_divide` | 6–8 | выше 12 — каждый кусок тоньше пыли и тормозит | | `rbw.substeps_per_frame` | 10 | 20 при >300 активных тел | | `rbw.solver_iterations` | 20 | выше при «протекающих» столкновениях | | `pc.compression` | `'LIGHT'` | `'NO'` при артефактах, `'HEAVY'` для экономии места | | `DustVolume density` | 0.35 в пике | выше — стена уходит в молоко | ## 📤 РЕЗУЛЬТАТ | Параметр | Значение | |---|---| | Симуляция | 120 кадров (5 с при 24 fps), 200–800 активных кусков | | Кэш | `//cache/wall_destruction.abc`, ориентировочно 50–400 МБ | | Пыль | отдельный объект `DustVolume` — в Alembic не входит, рендерится отдельно | | Рендер | 1920×1080, Cycles 256 сэмплов, **15–45 мин** (объём доминирует) | | Повторный прогон | физика не пересчитывается: рендер идёт по кэшу | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Трещин нет, стена целая | не manifold-геометрия | `Select Non Manifold`, либо Decimate до цельности | | Куски не падают | куски не получили `rigid_body` | выделить и `rigb.objects_add(type='ACTIVE')` заново | | Взрыв на первом кадре | нет `use_start_deactivated` | включить у всех активных тел | | Симуляция каждый кадр разная | кэш не запечён, `use_disk_cache` выключен | `ptcache.bake_all(bake=True)` + дисковый кэш | | Кусок проваливается сквозь пол | `substeps_per_frame` мало при быстрых телах | поднять до 15–20 | | Пыль в кубе, а не в воздухе | куб не замкнут | применить `Scale`-одну операцию, проверить нормали | | Alembic пустой на выходе | не выделены куски | `selected=True` требует явного `select_set` для каждого | ## 🔗 СВЯЗИ - **Прочие в категории «Воркфлоу»:** [WF 12: CROWD SIM GN](wf_12_crowd_sim_gn.md), [WF 15: FLUID PRODUCT SPLASH](wf_15_fluid_product_splash.md), [WF 01: TEXT-TO-SCENE](wf_01_text_to_scene.md), [WF 02: IMAGE-TO-3D](wf_02_image_to_3d.md), [WF 03: TEXTURE PIPELINE](wf_03_texture_pipeline.md) - **По темам:** [P21: PHYSICS SIMULATION](../prompts/21_physics_simulation.md), [P45: GEOMETRY NODES SIMULATION ZONES](../prompts/45_gn_simulation.md), [P47: ALEMBIC CACHE PIPELINE](../prompts/47_alembic_cache.md), [SIMULATION & PHYSICS: GEOMETRY NODES 4.0+](../simulation/rigid_body_gn.md) - **Теги:** #simulation, #workflow --- # WF 14: CLOTH FASHION SHOW Tags: #animation #clothing #workflow # WF 14: CLOTH FASHION SHOW **ID:** WF-14 | **Type:** Cloth | **Difficulty:** ⭐⭐ **RU/EN Bilingual** ## 🎯 ЦЕЛЬ / Goal RU: Marvelous import + wind + runway loop EN: Marvelous import + wind + runway loop Коллекция одежды, симулируемая тканью, с дефиле в 8-секундном цикле. Смысл воркфлоу — в стыке двух систем: **Marvelous Designer** считает покрой (лекала, сетка, швы, вытачки), **Blender** считает физику поверх уже готовой сетки. Ошибка номер один — симулировать в том же файле, где лекала редактируются. Это невозможно: у Blender нет модификатора, который считает покрой, а Designer не считает столкновения с телом. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Marvelous Designer | любая своя | лекала, посадка, швы | | Blender | 4.x | Cloth, Collision, ветер | | Rig (Armature) | FBX/GLB | посадка ткани на тело | | Окружение с коллизией | сцена | модель тела + подиум как Collision | > [!NOTE] > Родной формат Marvelous Designer — `.mcx`, и Blender его **не импортирует**. Обмен > идёт через экспорт из Designer в OBJ, FBX или Alembic. Импорт на стороне Blender > переезжал между операторами: `bpy.ops.wm.obj_import` (актуально) против старого > `bpy.ops.import_scene.obj`. Если первый недоступен — проверь, какой из них есть > в твоей версии, через поиск операторов в Python-консоли. ## 🛠 НАСТРОЙКА ### Этап 1. Импорт сетки и рига ```python import bpy # Актуальный оператор Blender 3.3+. Fallback для старых версий — см. примечание выше. try: bpy.ops.wm.obj_import(filepath=r"C:\lookbook\dress.obj") except AttributeError: bpy.ops.import_scene.obj(filepath=r"C:\lookbook\dress.obj") bpy.ops.import_scene.fbx(filepath=r"C:\lookbook\rig.fbx") garment = bpy.data.objects["Dress"] rig = bpy.data.objects["Rig"] bpy.context.view_layer.objects.active = rig rig.select_set(True) bpy.ops.object.mode_set(mode='POSE') bpy.ops.pose.select_all(action='SELECT') bpy.ops.pose.transforms_clear() # сбросить позу перед симуляцией: A/T-поза, без выгибов bpy.ops.object.mode_set(mode='OBJECT') ``` ### Этап 2. Порядок модификаторов Порядок в стеке решает всё: ткань считается на **низкополигональной** сетке, а гладкость добавляется после. Если поставить Subdivision перед Cloth, физика станет в разы дороже и не изменится качественно. ```python # стек снизу вверх: Cloth -> Smooth/Subdivision cloth = garment.modifiers.new("Cloth", 'CLOTH') subd = garment.modifiers.new("Subdivision", 'SUBSURF') subd.levels, subd.render_levels = 1, 2 subd.show_only_control_edges = True # видно исходную сетку — по ней и судишь о качестве ``` ### Этап 3. Настройки ткани ```python cs = cloth.settings cs.quality = 10 # 5 хватит для превью, 10 — рабочая цифра cs.mass = 0.25 # кг на вершину; меньше = легче, «шёлк» cs.tension_stiffness = 12 cs.compression_stiffness = 12 cs.shear_stiffness = 5 cs.bending_stiffness = 0.35 # ключ к виду: плащ vs дублёнка cs.air_damping = 1.2 # Кэш: пока не испечён, физика живёт только до конца диапазона кэша cloth.point_cache.frame_start = 1 cloth.point_cache.frame_end = 240 cloth.point_cache.use_disk_cache = True col = cloth.collision_settings col.use_collision = True col.distance_min = 0.015 # половина толщины ткани; меньше = больше «слипания» col.collision_quality = 3 col.use_self_collision = True # обязательно для юбок: без этого ткань проходит сквозь себя col.self_distance_min = 0.015 col.friction = 5.0 ``` ### Этап 4. Приколотка к костям Классический приём: ткань не «приклеивается» к кости напрямую — ей назначают vertex group, веса которой берутся из весов деформации арматуры через Data Transfer. ```python # 1) На арматуре подняты группы деформации (созданы при импорте FBX) # 2) На одежде создаём пустую группу и переливаем в неё веса vgroup = garment.vertex_groups.new(name="Pin") dt = garment.modifiers.new("PinFromArmature", 'DATA_TRANSFER') dt.object = rig dt.data_types = {'VERTEX_GROUP'} dt.vertex_group = "Pin" dt.mix_mode = 'REPLACE' # не смешивать со старыми весами # Порядок: Cloth должен идти ПОСЛЕ Data Transfer, иначе он увидит пустую группу garment.modifiers.move(garment.modifiers.find("Data Transfer") + 1, garment.modifiers.find("Data Transfer")) cs.vertex_group_mass = "Pin" # это и есть «Pin Group» в UI cs.pin_stiffness = 1.0 # 1.0 = полностью закреплено; 0.5 = слегка проседает ``` > [!NOTE] > Точное имя поля для переноса данных вершины называется `vertex_group`, а режим > сопоставления выбирается в `mapping` — распространённые варианты: `TOPOLOGY`, > `NEAREST_FACE_INTERPOLATED`. Проверь доступные значения: у Data Transfer список полей > длинный и часть названий отличается от подписей в UI. ### Этап 5. Коллизии тела и подиума ```python for name in ("Body", "Podium"): ob = bpy.data.objects.get(name) if ob and ob.modifiers.get("Collision") is None: ob.modifiers.new("Collision", 'COLLISION') ob.collision.thickness_outer = 0.02 # запас под сетку одежды ob.collision.damping = 0.3 ob.collision.cloth_friction = 6.0 ``` ### Этап 6. Ветер ```python bpy.ops.object.effector_add(type='WIND', location=(0, 4, 2), rotation=(1.5708, 0, 3.1416)) wind = bpy.context.object wind.name = "RunwayWind" f = wind.field f.shape = 'PLANE' f.strength = 900.0 # порыв дефиле: единицы привязаны к масштабу сцены f.noise = 1.6 # без noise ветер «уставит» ткань как флаг f.flow = 0.6 # flow создаёт «ветер по потоку» — струится вдоль шлейфа f.falloff_power = 1.5 f.use_max_distance = True f.distance_max = 14.0 f.seed = 7 # Зациклить ветер по кадрам цикла, иначе бесшовности не будет for frame, val in ((1, 400.0), (60, 1200.0), (120, 500.0), (180, 1100.0), (241, 400.0)): f.strength = val f.keyframe_insert("strength", frame=frame) fcu = wind.animation_data.action.fcurves[0] mod = fcu.modifiers.new('CYCLES') # 1-й и 241-й кадр совпадают → шва не видно ``` > [!NOTE] > Строка `action.fcurves[0]` рассчитана на ветку до 4.4. В 4.4+ Actions стали > многослойными, и кривые лежат внутри слота, а `action.fcurves` в 5.2.1 LTS просто > отсутствует (проверено: `hasattr(act, "fcurves")` → `False`, `act.fcurves[0]` падает > с IndexError). Рабочий доступ в новой ветке — обойти дерево: > > ```python > ad = wind.animation_data > bag = ad.action.layers[0].strips[0].channelbag(ad.action_slot) > fcu = bag.fcurves[0] > mod = fcu.modifiers.new('CYCLES') > ``` > > Это тот же приём, что и в WF 17 ([этап 1](wf_17_stop_motion_felt.md)), только для > другой сущности. ### Этап 7. Прогрев и запекание ```python scene = bpy.context.scene scene.frame_start, scene.frame_end = 1, 240 # Пока не испечено, симуляция живёт только до конца кэша; прокрутка # кадрами физику не считает — нужен bake. for f in range(1, 241): scene.frame_set(f) bpy.ops.ptcache.bake_all(bake=True) ``` ## ⚙️ КЛЮЧЕВЫЕ ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `bending_stiffness` | 0.35 | 0.05 — шёлк/шифон, 2.0 — кожа/деним | | `mass` | 0.25 | выше — «тяжёлая» ткань, быстрее оседает | | `self_distance_min` | 0.015 | при проколах юбки — увеличить вдвое | | `quality` | 10 | выше 15 — только финальный прогон | | `f.noise` | 1.6 | 0 = ровный поток, >3 = хаос | | `pin_stiffness` | 1.0 | 0.3–0.7, если плечо должно слегка «плыть» | ## 📤 РЕЗУЛЬТАТ | Параметр | Значение | |---|---| | Цикл | 240 кадров при 24 fps = **10 с** дефиле, бесшовно за счёт `CYCLES` на ветре | | Что получается | запечённая симуляция Cloth в `.blend` + кэш на диске | | Геометрия | 20–80 тыс. вершин до Subdivision, 200–600 тыс. после | | Bake | **3–20 мин** (зависит от `quality` и числа коллизий) | | Рендер | 1920×1080 Cycles 128 сэмплов: **40–120 мин** на 240 кадров | Проверяется одним скриптом. Важное отличие bake от «просто прокрутить кадры» видно по `point_cache.is_baked`: без запекания физика живёт только до конца кэша, и любое изменение сцены пересчитывает её заново — картинка «плывёт» между прогонами. Геометрию смотрят на **последнем** кадре кэша: на первом ткань ещё стоит по исходным координатам и не сказала ничего. Нулевое число полигонов — тоже результат: значит, на одежде не построено ни одной грани и симулировать нечего. ```python import bpy scene = bpy.context.scene garment = bpy.data.objects["Dress"] cloth = garment.modifiers["Cloth"] pc = cloth.point_cache # 1. Кэш: испечён или просто посчитан в памяти print("is_baked:", pc.is_baked, "| is_baking:", pc.is_baking, "| is_outdated:", pc.is_outdated) print("диапазон:", pc.frame_start, "-", pc.frame_end, "| на диске:", pc.use_disk_cache, "| info:", repr(pc.info)) # 2. Что получилось на последнем кадре кэша scene.frame_set(pc.frame_end) dg = bpy.context.evaluated_depsgraph_get() ev = garment.evaluated_get(dg).data ev.calc_loop_triangles() print("после симуляции:", len(ev.vertices), "вершин,", len(ev.polygons), "полигонов,", len(ev.loop_triangles), "треугольников") def z_span(verts): zs = [v.co.z for v in verts] return round(min(zs), 3), round(max(zs), 3) print("Z покой :", z_span(garment.data.vertices)) print("Z симуляция:", z_span(ev.vertices)) # 3. Коллизии на месте? for name in ("Body", "Podium"): ob = bpy.data.objects.get(name) print(name, "| Collision:", (ob.modifiers.get("Collision") is not None) if ob else "нет объекта") # 4. Сколько вершин реально приколоты к скелету pin = garment.vertex_groups.get("Pin") if pin: idx = pin.index pinned = [v.index for v in garment.data.vertices if any(g.group == idx and g.weight > 0.5 for g in v.groups)] print("приколот вершин:", len(pinned), "из", len(garment.data.vertices)) print("Pin Group в настройках:", repr(cloth.settings.vertex_group_mass)) ``` Пустой `Pin Group` в настройках при непустой группе `Pin` на объекте — почти всегда Data Transfer в стеке ниже Cloth: ткань ещё не видела весов, когда считалась. > [!NOTE] > Путь к весам эффекторов менялся. На объекте (`garment.effector_weights.wind`) он был > в ветке 4.x; в 5.2.1 LTS `Object.effector_weights` убран, и веса лежат на настройках > самого модификатора — `cloth.settings.effector_weights.wind`. Проверено: у `Object` > в этой сборке нет такого поля вообще. Поэтому строка из таблицы ошибок про ветер > безопаснее в виде > `(getattr(garment, "effector_weights", None) or cloth.settings.effector_weights).wind` > — она работает в обеих ветвях. ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Ткань «прилипает» к телу | `distance_min` слишком мал либо нет коллизии на Body | увеличить `distance_min`, добавить Collision на тело | | Юбка проходит сквозь себя | выключен `use_self_collision` | включить и поднять `self_distance_min` | | Одежда не следует за костями | Data Transfer стоит после Cloth | поднять его выше Cloth в стеке | | Ткань «взрывается» на 1-м кадре | `mass` слишком мал при высокой жёсткости | снизить `tension_stiffness`, поднять `mass` до 0.3 | | Ткань провисает ниже колен | `bending_stiffness` слишком низкий | 0.5–1.0 для костюма, 0.1 для плаща | | Физика обрывается на кадре 240 | не испечён кэш | `ptcache.bake_all(bake=True)` | | Ветер не двигает ткань | effector не попал в веса объекта | проверить `garment.effector_weights.wind` (должно быть 1.0) | ## 🔗 СВЯЗИ - **Прочие в категории «Воркфлоу»:** [WF 04: CHARACTER ANIMATION](wf_04_character_animation.md), [WF 09: REALTIME VJ LOOP](wf_09_realtime_vj_loop.md), [WF 17: STOP-MOTION FELT LOOK](wf_17_stop_motion_felt.md), [WF 23: VIRTUAL PRODUCTION LED](wf_23_virtual_production_led.md), [WF 24: PHOTOGRAMMETRY CLEANUP](wf_24_photogrammetry_cleanup.md) - **По темам:** [CASE 04: CHARACTER RIG & ANIMATION](../cases/case_04_character_rig.md), [CASE 06: ANIMATION LOOP](../cases/case_06_animation_loop.md), [P03: MODIFIERS STACK](../prompts/03_modifiers_stack.md) - **Теги:** #animation, #clothing, #workflow --- # WF 15: FLUID PRODUCT SPLASH Tags: #simulation #workflow # WF 15: FLUID PRODUCT SPLASH **ID:** WF-15 | **Type:** Fluid | **Difficulty:** ⭐⭐⭐ **RU/EN Bilingual** ## 🎯 ЦЕЛЬ / Goal RU: FLIP splash around product, meshing, whitewater EN: FLIP splash around product, meshing, whitewater Бьющая вверх струя вокруг товара: капли расходятся по форме изделия, превращаются в меш, от него отрывается аэрозоль (whitewater). Три разных рендера в одном кадре — жидкость, меш и пена — это три разных требования к памяти, и именно на этом воркфлоу обычно кончается RAM. Здесь обязательна дисциплина: маленький домен, честный масштаб, `cache_type = 'ALL'` до конца расчёта и запись кэша на диск. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Blender | 4.x | Mantaflow (встроенный) | | ОЗУ | 16 ГБ+ | при `resolution_max = 256` домен — это ~16 млн ячеек | | Меш товара | замкнутый | иначе эффектор не считается | | Диск | 20 ГБ+ | кэш жидкости весит больше, чем сам рендер | > [!NOTE] > Названия полей Mantaflow менялись заметно (`cache_data_format`, `simulation_method`, > `resolution_max`, `spray_scale` и соседние). Ниже приведены те имена, которые > использовались в ветке 4.x, но перед продакшном обязательно сверь список: в Python-консоли > набери `dir(bpy.data.objects["Domain"].fluid_settings)` — выведет реальные поля твоей > сборки, и это надёжнее, чем любая документация. ## 🛠 НАСТРОЙКА ### Этап 1. Масштаб решает всё Домен должен быть **плотным**: 1 unit = 1 метр, товар внутри, а не «комната 10 метров вокруг банки». Каждая лишняя единица сетки — это куб памяти вчетверо. ```python import bpy, math scene = bpy.context.scene scene.render.fps = 24 scene.frame_start, scene.frame_end = 1, 90 # 90 кадров ≈ 3.75 с # Домен: куб 2×2×2 м вокруг товара. Максимальный габарит товара # должен занимать не меньше трети ширины домена, иначе сетка грубая. bpy.ops.object.fluid_add(type='DOMAIN', location=(0, 0, 1.0)) dom = bpy.context.object dom.name = "Domain" dom.scale = (1.0, 1.0, 1.0) fs = dom.fluid_settings fs.domain_type = 'LIQUID' fs.resolution_max = 256 # 128 — черновик, 256 — рабочая, 512 — уже 64 ГБ RAM ``` ### Этап 2. Влияние: струя снизу вверх ```python # Цилиндр-источник под товаром: жидкость выходит из-под изделия, # расходится по его форме и срывается каплями — классический «waterfall» bpy.ops.mesh.primitive_cylinder_add(vertices=32, radius=0.35, depth=0.4, location=(0, 0, 0.12)) src = bpy.context.object src.name = "Source" bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) bpy.ops.object.fluid_add(type='FLOW', location=(0, 0, 0)) flw = bpy.context.object flw.name = "Flow" fset = flw.fluid_settings fset.flow_type = 'LIQUID' fset.flow_behavior = 'INFLOW' fset.use_inflow = True fset.use_plane_init = False # False = непрерывный поток, True = разовый «поршень» ``` ### Этап 3. Товар как эффектор ```python product = bpy.data.objects["Product"] bpy.ops.object.fluid_add(type='EFFECTOR', location=product.location) eff = bpy.context.object eff.name = "ProductEffector" eset = eff.fluid_settings eset.effector_type = 'COLLISION' eset.use_effector = True # Насколько жидкость «обтекает» форму до того, как считает себя внутри. # Больше = объёмнее жидкость вокруг изделия, меньше = тонкая плёнка. eset.surface_distance = 0.02 eset.use_plane_init = False # Подвижное тело: если товар падает/вращается — эффектор должен это знать eset.subframes = 1 # промежуточные кадры: без них быстрые объекты «протыкают» воду ``` ### Этап 4. Параметры домена и whitewater ```python fs.cache_type = 'ALL' # 'ALL' = сохраняем весь кэш. Иначе после смены кадра # физика пересчитается и результат «поплывёт» fs.cache_frame_start = 1 fs.cache_frame_end = 90 fs.cache_directory = "//cache/fluid" fs.cache_data_format = 'OPENVDB' # Аэрозоль: то, что читается как брызги и пена. Без этого будет # гладкая «желевая» вода без капель. ew = fs.effector_weights ew.spray_emission = 12.0 # капли в воздухе ew.foam_emission = 6.0 # пена на поверхности ew.surface_distance = 0.02 ``` ### Этап 5. Меширование жидкости Домен создаёт частицы «Liquid Mesh»; из них строится поверхность, а не из объёма сетки. ```python # Настройки частиц меша живут в particle system домена for ps in dom.particle_systems: if "mesh" in ps.name.lower(): ps.settings.liquid_render = 'MESH' # иначе на выходе будет пузырьки, а не поверхность ps.settings.sphere_scale = 1.0 ps.settings.sphere_radius = 1.0 # >1 = поверхность «натягивается», меньше капель ``` > [!NOTE] > Имя частичной системы, создаваемой доменом, и поля `liquid_render` / `sphere_scale` > внутренние и версионно-зависимые. Надёжнее найти её в интерфейсе: вкладка Physics → > Type: Liquid → раздел Liquid Mesh. Если в твоей версии поля называются иначе — > сверься с руководством, а не с этим списком. ### Этап 6. Материал жидкости и пены ```python mat = bpy.data.materials.new("Mat_Liquid") mat.use_nodes = True nt = mat.node_tree bsdf = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED') bsdf.inputs["Base Color"].default_value = (0.75, 0.85, 0.9, 1.0) bsdf.inputs["Transmission Weight"].default_value = 1.0 bsdf.inputs["Roughness"].default_value = 0.02 bsdf.inputs["IOR"].default_value = 1.333 # вода, не стекло: 1.45 было бы «стеклянной» product.data.materials.append(mat) # Пена — объёмный Principled Volume: цвет, плотность, эмиссия и # два входа whitewater, которые появляются у ноды в контексте флюида foam = bpy.data.materials.new("Mat_Foam") foam.use_nodes = True ntf = foam.node_tree ntf.nodes.clear() pv = ntf.nodes.new("ShaderNodeVolumePrincipled") out = ntf.nodes.new("ShaderNodeOutputMaterial") ntf.links.new(pv.outputs["Volume"], out.inputs["Volume"]) pv.inputs["Color"].default_value = (1.0, 1.0, 1.0, 1.0) pv.inputs["Density"].default_value = 0.35 dom.data.materials.append(foam) ``` ### Этап 7. Расчёт ```python # Порядок: сначала белкастер, потом рендер. Порог в память — 16 ГБ bpy.ops.fluid.bake_all() # Проверка: кэш лежит на диске и весит сотни мегабайт — это нормально # ls -lh cache/fluid/ ``` ### Этап 8. Контроль: что реально лежит на диске Bake может прерваться по памяти и не сообщить об этом — сцена просто останется без кэша, а рендер возьмёт кадр 1 для всего ролика. Поэтому проверять надо не флаги, а файлы: сколько их, сколько они весят и совпадает ли это с `cache_frame_start` / `cache_frame_end`. Заодно печатаются те поля домена, которые определяют и память, и картинку: разрешение, число системных частиц и состояние меширования. ```python import bpy, os, glob scene = bpy.context.scene dom = bpy.data.objects["Domain"] # Настройки домена лежат в разных местах в зависимости от версии: # на объекте (`dom.fluid_settings`) либо на модификаторе Fluid (`md.domain_settings`). # Поэтому берём первое, что реально нашлось, — иначе скрипт упадёт на AttributeError # там, где в прошлой версии всё работало. fs = getattr(dom, "fluid_settings", None) if fs is None: fs = next((md.domain_settings for md in dom.modifiers if md.type == 'FLUID'), None) if fs is None: print("Домен не найден — проверь имя объекта и как он создан") else: # 1. Параметры, по которым считается объём работы print("тип:", fs.domain_type, "| resolution_max:", fs.resolution_max, "| кэш:", fs.cache_type, "| формат:", fs.cache_data_format) print("кадры кэша:", fs.cache_frame_start, "…", fs.cache_frame_end, "| печётся сейчас:", getattr(fs, "is_cache_baking_data", "нет поля")) # 2. Кэш на диске: считаем настоящие файлы, а не доверяем флажку. # cache_directory — путь относительно .blend, абсолютизируем. cache_dir = bpy.path.abspath(fs.cache_directory) files = sorted(glob.glob(os.path.join(cache_dir, "*"))) size_mb = sum(os.path.getsize(f) for f in files if os.path.isfile(f)) / 1048576 print("каталог:", cache_dir, "| файлов:", len(files), "| МБ:", round(size_mb, 1), "| ожидалось:", fs.cache_frame_end - fs.cache_frame_start + 1) # 3. Системы частиц: их создаёт сам домен, и их отсутствие — # главный признак того, что bake не отработал print("particle systems:", [ps.name for ps in dom.particle_systems]) # 4. Whitewater: имена полей менялись, поэтому читаем через getattr. # 0 означает «брызги выключены» — кадр получится гладким, как желе. ew = fs.effector_weights if fs is not None else None if ew is not None: print("spray_emission:", getattr(ew, "spray_emission", "нет поля"), "| foam_emission:", getattr(ew, "foam_emission", "нет поля")) ``` > [!NOTE] > Блок написан устойчиво к версиям и это не косметика: проверено на 5.2.1 LTS, что > `bpy.ops.object.fluid_add` там **не существует** вообще (`object.effector_add` есть, > `quick_liquid` есть, а fluid-оператора нет), домен создаётся как объект с > модификатором `FLUID` и полем `fluid_type` (`'NONE' / 'DOMAIN' / 'FLOW' / 'EFFECTOR'`), > настройки лежат в `md.domain_settings`, а не в `ob.fluid_settings`. Имя системы частиц > там — `Liquid` с типом `'FLIP'`, и полей `liquid_render` / `sphere_scale` у неё уже > нет. Поэтому всё, что переезжало между версиями, в скрипте читается через `getattr` > с запасным вариантом: в своей сборке сначала набери > `dir(bpy.data.objects["Domain"].fluid_settings)` и сверь список. `is_cache_baking_data` во время проверки должен быть `False` — если `True`, bake ещё идёт и подсчёт файлов покажет недобакенный объём. Расхождение «файлов заметно меньше, чем кадров кэша» — самый частый признак вылета по памяти на середине; лечится снижением `resolution_max` или габарита домена, а не повторным bake в том же разрешении. ## ⚙️ КЛЮЧЕВЫЕ ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `resolution_max` | 128 / 256 / 512 | черновик / рабочий / премиум | | `cache_type` | `'ALL'` | иначе результат нестабилен между прогонами | | `spray_emission` | 8–20 | выше 25 — кадр в молоке | | `foam_emission` | 3–10 | для мыла/молока | | `subframes` | 1–3 | обязательно 3, если товар быстро движется | | `IOR` | 1.333 | стекло — 1.45 | ## 📤 РЕЗУЛЬТАТ | Параметр | Значение | |---|---| | Симуляция | 90 кадров (3.75 с при 24 fps), домен 2×2×2 м, `resolution_max = 256` | | Кэш | `//cache/fluid/` на диске, ориентировочно 1–8 ГБ | | Меш | поверхность жидкости из частиц Liquid Mesh, отдельный меш для пены | | Рендер | 1920×1080 Cycles 256 сэмплов: **40–120 мин** (жидкость с преломлением дороже всего) | | Экспорт | Alembic жидкости для последующего композитинга | Разрешение кадра — 1920×1080, 16 бит на канал (`color_depth = '16'`), контейнер `FFMPEG` / `H264` с `constant_rate_factor = 'HIGH'`. Порядок вывода: сначала PNG-серия с тем же именем, что и кадры кэша, потом сборка в видео вторым проходом с `image_settings.file_format = 'FFMPEG'`. Оценки времени даны для `resolution_max = 256` и одной жидкости на кадр; на 128 они примерно вчетверо меньше, на 512 — вылет по памяти раньше, чем начнётся рендер. Это ориентир для планирования, а не обещание: реальное время определяется GPU, числом источников света и тем, считается ли ворс/аэрозоль. ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Жидкость проходит сквозь товар | товар не EFFECTOR либо `subframes = 0` | добавить эффектор, поднять `subframes` до 3 | | Картинка «плывёт» между прогонами | `cache_type` не `'ALL'` | выставить `'ALL'` и перезапечь | | Вылет по памяти | домен слишком большой или `resolution_max` велик | уменьшить габарит домена, `resolution_max` до 128 | | Капли выглядят как пузырьки | не переключено `liquid_render` в `'MESH'` | переключить в интерфейсе Liquid Mesh | | Нет брызг, только «желе» | `spray_emission = 0` и нет whitewater | поднять `spray_emission`, назначить пеноматериал | | Поверхность в сетку | мало итераций меширования | увеличить `surface_distance`, проверить итерации в UI | | Товар «висит» в воздухе после удара | нет гравитации для эффектора | эффектор по умолчанию статичен — либо кинематика, либо внешняя анимация | ## 🔗 СВЯЗИ - **Прочие в категории «Воркфлоу»:** [WF 12: CROWD SIM GN](wf_12_crowd_sim_gn.md), [WF 13: DESTRUCTION RBD](wf_13_destruction_rbd.md), [WF 01: TEXT-TO-SCENE](wf_01_text_to_scene.md), [WF 02: IMAGE-TO-3D](wf_02_image_to_3d.md), [WF 03: TEXTURE PIPELINE](wf_03_texture_pipeline.md) - **По темам:** [P21: PHYSICS SIMULATION](../prompts/21_physics_simulation.md), [P45: GEOMETRY NODES SIMULATION ZONES](../prompts/45_gn_simulation.md), [P47: ALEMBIC CACHE PIPELINE](../prompts/47_alembic_cache.md), [SIMULATION & PHYSICS: GEOMETRY NODES 4.0+](../simulation/rigid_body_gn.md) - **Теги:** #simulation, #workflow --- # WF 16: NPR ANIME RENDER Tags: #render #stylized #workflow # WF 16: NPR ANIME RENDER **ID:** WF-16 | **Type:** Stylized | **Difficulty:** ⭐⭐ **RU/EN Bilingual** ## 🎯 ЦЕЛЬ / Goal RU: Lineart + toon shader, paint texture, grease pencil FX EN: Lineart + toon shader, paint texture, grease pencil FX Аниме-кадр из трёх независимых слоёв, каждый из которых можно перерисовывать отдельно: **цвет** (2–3 тональных плана, без градиентов), **линия** (контур, крест, перекрытие), **краска** (hand-painted текстура поверх цвета) плюс Grease Pencil для FX. Такой разбор нужен не для красоты: линия и цвет в аниме живут в разных темпах — перекрасили небо, контур трогать не надо. Ключевой технический факт, который определяет всю сборку: **`Shader to RGB` работает только в EEVEE**. На Cycles cel-шейдер через `Shader to RGB` не считается — там нужен либо Freestyle-обводка, либо инвертированная оболочка (inverted hull). ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Blender | 4.x | EEVEE Next + Freestyle | | Grease Pencil (3D) | встроенный | FX-штрихи поверх 3D | | Hand-painted текстура | 2k PNG | слой краски | | Freestyle | встроенный | контур, отдельный рендер-пасс | ## 🛠 НАСТРОЙКА ### Этап 1. Cel-шейдер: три тона, без градиента ```python import bpy scene = bpy.context.scene scene.render.engine = 'BLENDER_EEVEE_NEXT' # если нет — см. примечание про имя движка # Плоские цвета «ломаются» при AgX/Filmic: тёмный тон уезжает в серый, # светлый — в белый. Для сел-шейдера нужен Standard. scene.view_settings.view_transform = 'Standard' scene.view_settings.look = 'None' scene.view_settings.exposure = 0.0 mat = bpy.data.materials.new("Mat_Toon") mat.use_nodes = True nt = mat.node_tree nt.nodes.clear() diff = nt.nodes.new("ShaderNodeBsdfDiffuse") s2rgb = nt.nodes.new("ShaderNodeShaderToRGB") # EEVEE ONLY ramp = nt.nodes.new("ShaderNodeValToRGB") emis = nt.nodes.new("ShaderNodeEmission") out = nt.nodes.new("ShaderNodeOutputMaterial") diff.inputs["Color"].default_value = (1.0, 1.0, 1.0, 1.0) # белый: свет считает Shader to RGB ramp.color_ramp.interpolation = 'CONSTANT' # ровные ступени, не градиент ramp.color_ramp.elements[0].position = 0.0 ramp.color_ramp.elements[0].color = (0.12, 0.10, 0.16, 1.0) # тень ramp.color_ramp.elements[1].position = 0.42 # свет ramp.color_ramp.elements[1].color = (0.72, 0.60, 0.66, 1.0) el = ramp.color_ramp.elements.new(0.78) # блик el.color = (1.0, 0.96, 0.90, 1.0) nt.links.new(diff.outputs["BSDF"], s2rgb.inputs["Shader"]) nt.links.new(s2rgb.outputs["Color"], ramp.inputs["Fac"]) nt.links.new(ramp.outputs["Color"], emis.inputs["Color"]) nt.links.new(emis.outputs["Emission"], out.inputs["Surface"]) ``` Пороги 0.42 / 0.78 подбираются под сцену: слишком низкие — свет «съедает» тень, слишком высокие — всё в один тон. Для персонажа обычно берут 3 ступени, для фона 2. ### Этап 2. Слой краски поверх цвета Краска должна влиять только на цвет, не на свет. Поэтому её подключают к `Emission Color`, а `Light Path → Is Camera Ray` позволяет отличить «для зрителя» от «для отражений». ```python paint = nt.nodes.new("ShaderNodeTexImage") paint.image = bpy.data.images.load("//textures/handpaint_face.png") paint.extension = 'CLIP' texco = nt.nodes.new("ShaderNodeTexCoord") lp = nt.nodes.new("ShaderNodeLightPath") mixrgb = nt.nodes.new("ShaderNodeMixRGB") mixrgb.blend_type = 'MULTIPLY' mixrgb.inputs["Fac"].default_value = 0.85 # 0.85 — краска слегка просвечивает nt.links.new(texco.outputs["UV"], paint.inputs["Vector"]) nt.links.new(ramp.outputs["Color"], mixrgb.inputs[1]) nt.links.new(paint.outputs["Color"], mixrgb.inputs[2]) nt.links.new(lp.outputs["Is Camera Ray"], mixrgb.inputs[0]) nt.links.new(mixrgb.outputs["Color"], emis.inputs["Color"]) ``` ### Этап 3. Обводка: Freestyle ```python scene.render.use_freestyle = True scene.render.line_thickness = 1.6 scene.render.line_thickness_mode = 'ABSOLUTE' # ABSOLUTE = пиксели, не « world units» fs = bpy.context.view_layer.freestyle_settings fs.mode = 'EDITOR' fs.as_render_pass = True # вынести линии в отдельный пасс — см. этап 5 ls = fs.linesets[0] if len(fs.linesets) else fs.linesets.new("Main") ls.select_by_edge_types = True ls.select_silhouette = True # силуэт объекта ls.select_border = True # край меша: глаза, рот, разрез ls.select_crease = True # внутренние сгибы ls.select_external_contour = True ls.select_material_boundary = False # иначе каждая грань обведётся ls.select_edge_mark = False style = ls.linestyle style.color = (0.05, 0.04, 0.07) style.thickness = 1.8 style.alpha = 1.0 style.caps = 'ROUND' # круглый конец штриха вместо плоского ``` > [!NOTE] > Список `select_*` у FreestyleLineSet расширялся между версиями: часть флагов появилась > позже и в старых сборках отсутствует. Перед продакшном вызови > `[p.identifier for p in ls.bl_rna.properties if p.identifier.startswith("select")]` — > это покажет ровно те переключатели, которые есть в твоей версии. ### Этап 4. Grease Pencil FX ```python # 3D-объект Grease Pencil живёт поверх сцены и рендерится вместе с ней try: bpy.ops.object.gpencil_add(type='3D', location=(0, 0, 0.5)) except TypeError: bpy.ops.object.gpencil_add(type='GREASEPENCIL', location=(0, 0, 0.5)) fx = bpy.context.object fx.name = "FX_Speedlines" gmat = bpy.data.materials.new("Mat_FX") gmat.use_nodes = True fx.data.materials.append(gmat) # Анимация штрихов: появляются по кадрам, а не существуют все сразу try: sa = fx.data.stroke_animation sa.draw_on_anim = True sa.frame_offset = 3 # задержка отрисовки на кадр except AttributeError: pass # в части версий API анимации штрихов другой ``` > [!NOTE] > Блок Grease Pencil активно переписывался между 4.0 и 4.3 (появились слои материалов, > слои данных, новая система хранения). Имена вроде `stroke_animation`, `draw_on_anim`, > `frame_offset` и способ включения рендера GP-объекта в сцену (`scene.render.use_grease_pencil` > в старых версиях) — не то, на что стоит закладываться. Открой Grease Pencil в своей > сборке и проверь по интерфейсу: Data-панель → Animation, Material-панель → Line Art. ### Этап 5. Два прохода: цвет дешевле, линия в полном разрешении Это главное практическое отличие от фотореализма: **линия рисуется в два раза дороже цвета**, потому что Freestyle обходит всю геометрию. Отсюда схема «цвет в 1/2, линия в 1:1». ```python scene.render.resolution_x, scene.render.resolution_y = 1920, 1080 # Проход 1 — цвет без линий fs.as_render_pass = False scene.render.use_freestyle = False scene.render.filepath = "//render/anime_color_" scene.render.image_settings.file_format = 'PNG' bpy.ops.render.render(animation=True) # Проход 2 — только Freestyle-пасс, в полном разрешении fs.as_render_pass = True scene.render.use_freestyle = True scene.render.filepath = "//render/anime_lines_" bpy.ops.render.render(animation=True) ``` ### Этап 6. Сведение в композиторе ```python scene.use_nodes = True ntc = scene.node_tree ntc.nodes.clear() img_c = ntc.nodes.new("CompositorNodeImage"); img_c.image = bpy.data.images["anime_color_0001.png"] img_l = ntc.nodes.new("CompositorNodeImage"); img_l.image = bpy.data.images["anime_lines_0001.png"] over = ntc.nodes.new("CompositorNodeAlphaOver") comp = ntc.nodes.new("CompositorNodeComposite") ntc.links.new(img_c.outputs["Image"], over.inputs[2]) # база — цвет ntc.links.new(img_l.outputs["Image"], over.inputs[1]) # сверху — линия ntc.links.new(over.outputs["Image"], comp.inputs["Image"]) ``` ### Этап 7. Инвертированная оболочка — запасной контур Если линия нужна «жирная» и с переменной толщиной (обводка от руки), Freestyle не умеет: он рисует в экранных пикселях, и толщина постоянная. Тогда контур делается геометрией — второй оболочкой с вывернутыми нормалями. ```python hero = bpy.data.objects["Hero"] # Оболочка: копия геометрии, вывернутая наружу, с чёрным самосветящимся материалом shell = hero.copy() shell.data = hero.data.copy() shell.name = "Hero_Outline" hero.users_collection[0].objects.link(shell) solid = shell.modifiers.new("Solidify", 'SOLIDIFY') solid.thickness = 0.006 # толщина обводки в мире, НЕ в пикселях solid.offset = 1.0 solid.use_flip_normals = True # нормали наружу — иначе оболочка «съест» модель solid.use_rim = True om = bpy.data.materials.new("Mat_Outline") om.use_nodes = True oe = om.node_tree.nodes["Principled BSDF"] oe.inputs["Base Color"].default_value = (0.0, 0.0, 0.0, 1.0) oe.inputs["Emission Color"].default_value = (0.02, 0.02, 0.03, 1.0) oe.inputs["Emission Strength"].default_value = 1.0 om.use_backface_culling = True # видны только «наружные» грани оболочки shell.data.materials.clear() shell.data.materials.append(om) ``` > [!NOTE] > Метод прорисовки прозрачности в EEVEE менялся: в версиях до 4.2 это > `material.blend_method` (плюс `shadow_method`), начиная с 4.2 (EEVEE Next) — > `material.surface_render_method`. Если присваивание не сработало, посмотри, что > доступно: `[p.identifier for p in bpy.types.Material.bl_rna.properties if "blend" in p.identifier or "render_method" in p.identifier]`. ## ⚙️ КЛЮЧЕВЫЕ ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `view_transform` | `'Standard'` | AgX «съедает» плоские тона | | Ramp positions | 0.42 / 0.78 | 3 тона для героя, 2 (0.55) для фона | | `line_thickness` | 1.6 | для «крупного плана» 2.2, для общего плана 1.0 | | `line_thickness_mode` | `'ABSOLUTE'` | `'RELATIVE'` — толщина зависит от расстояния | | `crease_angle` | ~134° | больше — меньше лишних штрихов на сгибах | ## 📤 РЕЗУЛЬТАТ | Параметр | Значение | |---|---| | Цикл | 96 кадров при 24 fps = **4 с**, 1080p | | Файлы | `anime_color_####.png` + `anime_lines_####.png` + `anime_####.png` после сведения | | Слои | цвет (EEVEE cel), Freestyle-пасс, Grease Pencil FX, hand-paint текстура | | Рендер | EEVEE: цвет 3–8 с/кадр; Freestyle 15–40 с/кадр. Итого **40–90 мин** на 96 кадров | | Особенность | линия и цвет разделены → перекраска не трогает обводку | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Cel-шейдер чёрный | `Shader to RGB` в Cycles | переключить на EEVEE; в Cycles использовать инвертированную оболочку | | Тона «плывут» при смене кадра | `ColorRamp` в шейдере не привязан к реальному освещению | добавить Ambient Occlusion node в diffuse-свет | | Линия обводит каждую грань | включён `select_material_boundary` | выключить, оставить только silhouette/crease/border | | Толщина линии зависит от кадра | `line_thickness_mode = 'RELATIVE'` | переключить на `'ABSOLUTE'` | | GP-объект не виден в рендере | слой/видимость GP в новой версии переехала | проверить Data-панель → слои и рендер-флаги Grease Pencil | | Краска не ложится | нет UV или текстура не в диапазоне 0–1 | `UV → Smart UV Project`, `image.source = 'SEAMLESS'` для бесшовной | | Рендер в разы медленнее ожидаемого | Freestyle считается по всей геометрии | упростить сетку для прохода линий, отключить Freestyle на невидимых объектах | ## 🔗 СВЯЗИ - **Прочие в категории «Воркфлоу»:** [WF 01: TEXT-TO-SCENE](wf_01_text_to_scene.md), [WF 17: STOP-MOTION FELT LOOK](wf_17_stop_motion_felt.md), [WF 18: ISOMETRIC GAME ART](wf_18_isometric_game_art.md), [WF 19: VOXEL ART PIPELINE](wf_19_voxel_art_pipeline.md), [WF 20: KITBASH GREEBLE CITY](wf_20_kitbash_greeble.md) - **По темам:** [CASE 08: SCI-FI CORRIDOR MODULAR](../cases/case_08_sci_fi_corridor.md), [WF 23: VIRTUAL PRODUCTION LED](wf_23_virtual_production_led.md), [WF 24: PHOTOGRAMMETRY CLEANUP](wf_24_photogrammetry_cleanup.md) - **Теги:** #render, #stylized, #workflow --- # WF 17: STOP-MOTION FELT LOOK Tags: #animation #pipeline #render #shading #texturing #workflow # WF 17: STOP-MOTION FELT LOOK **ID:** WF-17 | **Type:** Stylized | **Difficulty:** ⭐⭐ **RU/EN Bilingual** ## 🎯 ЦЕЛЬ / Goal RU: Stepped interpolation, fuzz texture, fingerprint normal EN: Stepped interpolation, fuzz texture, fingerprint normal Пластилиновый кукольный мультфильм: кадры «встают» вместо плавного движения, поверх материала торчит ворс, а вмятины от пальцев читаются в бликах. Три приёма дают 80 % узнаваемости, остальное — свет. Пластилин в stop motion — это не «серый материал»: у него **очень высокая шероховатость, сильный sheen по краям и лёгкое подповерхностное рассеяние**. Ворс даёт мягкий ореол вокруг силуэта. Вмятины читаются только если свет скользящий — при frontal-свете их не видно. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Blender | 4.x | Cycles, Hair-частицы | | Модель скульпта | GLB/FBX | герой | | Normal-карта вмятин | 2k, снятая со скульпта | отпечатки пальцев | | HDRI «студия» | 1k | скользящий ключевой свет | ## 🛠 НАСТРОЙКА ### Этап 1. Тайминг: не 24 кадра в секунду, а позы Стоп-моушен снимают на 12–15 кадров в секунду, и в каждом кадре поза **держится**. Реализуется это двумя настройками: рендерим через кадр, а интерполяция — CONSTANT. ```python import bpy scene = bpy.context.scene scene.render.fps = 24 scene.frame_start, scene.frame_end = 1, 72 # Каждый второй кадр => на выходе 12 fps. Это и есть «пластилиновый» темп. scene.frame_step = 2 def set_constant_interpolation(objs): """Все ключи объектов -> CONSTANT: между позами нет перехода.""" for ob in objs: ad = ob.animation_data if ad is None or ad.action is None: continue for fc in ad.action.fcurves: for kp in fc.keyframe_points: kp.interpolation = 'CONSTANT' fc.update() set_constant_interpolation([o for o in bpy.context.scene.objects if o.animation_data]) ``` > [!NOTE] > В Blender 4.4+ Actions стали многослойными (layered/slotted): `action.fcurves` может > вернуть пустой список, потому что кривые лежат внутри `layer → strip → channelbag` > слота. Если после вызова выше анимация всё ещё плавная — обойди дерево через > `action.layers[*].strips[*].channelbag(ad.action_slot).fcurves` либо примени > оператор из меню Animation вручную. Проверь структуру своего Action перед > использованием функции в конвейере. ### Этап 2. Пластилиновый материал ```python mat = bpy.data.materials.new("Mat_Felt") mat.use_nodes = True nt = mat.node_tree p = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED') p.inputs["Base Color"].default_value = (0.72, 0.30, 0.22, 1.0) # терракота p.inputs["Roughness"].default_value = 0.92 # почти мат, но не «мел» p.inputs["Specular IOR Level"].default_value = 0.25 # минимум: пластик не «мокрый» p.inputs["Sheen Weight"].default_value = 0.55 # ореол по краям — ключ к «войлоку» p.inputs["Sheen Roughness"].default_value = 0.25 p.inputs["Subsurface Weight"].default_value = 0.12 # тонкие уши/пальцы просвечивают p.inputs["Subsurface Radius"].default_value = (0.02, 0.01, 0.008) p.inputs["Subsurface Scale"].default_value = 0.01 # Мелкая зернистость: без неё пластилин выглядит как крашеный пластик noise = nt.nodes.new("ShaderNodeTexNoise") noise.inputs["Scale"].default_value = 340.0 noise.inputs["Detail"].default_value = 6.0 bump = nt.nodes.new("ShaderNodeBump") bump.inputs["Strength"].default_value = 0.15 bump.inputs["Distance"].default_value = 0.002 nt.links.new(noise.outputs["Fac"], bump.inputs["Height"]) nt.links.new(bump.outputs["Normal"], p.inputs["Normal"]) ``` > [!NOTE] > Названия сокетов Principled BSDF менялись в 4.0: `Sheen` стал `Sheen Weight`, > `Specular` — `Specular IOR Level`, `Subsurface` — `Subsurface Weight`. > В версиях до 4.0 использовать старые имена, иначе код упадёт на > `p.inputs["Sheen Weight"]` с KeyError. Список актуальных имён: > `[s.name for s in p.inputs]`. ### Этап 3. Ворс: hair-частицы с детьми ```python hero = bpy.data.objects["Hero"] bpy.ops.object.particle_system_add() ps = hero.particle_systems[-1] st = ps.settings st.type = 'HAIR' st.count = 4000 # на объекте ~100k — уже тяжело st.hair_length = 0.035 # ворс 3.5 см на фигуре 30 см st.render_type = 'PATH' st.use_advanced_hair = True # Дети дают плотность без роста числа частиц st.child_type = 'INTERPOLATED' st.child_percent = 5 # % от родителей st.rendered_child_count = 60 st.child_radius = 0.05 st.child_roundness = 2.0 st.kink = 'CURL' # ворс завивается, как на войлочной игрушке st.kink_amplitude = 0.004 st.kink_frequency = 3.0 st.root_radius = 0.0002 st.tip_radius = 0.00005 # кончик тоньше — силуэт «дымит» ``` Материал ворса: полупрозрачный Principled с `Sheen`, а не чёрный — иначе по краям появится грязная кайма. ```python fmat = bpy.data.materials.new("Mat_Fuzz") fmat.use_nodes = True fp = next(n for n in fmat.node_tree.nodes if n.type == 'BSDF_PRINCIPLED') fp.inputs["Base Color"].default_value = (0.85, 0.55, 0.45, 1.0) fp.inputs["Roughness"].default_value = 0.9 fp.inputs["Sheen Weight"].default_value = 0.8 # Рендер с задержкой в несколько кадров: strand-ов очень много, лучше просчитать один раз st.render_step = 3 ``` ### Этап 4. Вмятины от пальцев Отпечатки — это **микрорельеф**, а не цвет. Поэтому он делается либо нормаль-картой, либо впечатывается в геометрию. ```python # Вариант A: нормаль-карта со скульпта tex = bpy.data.textures.new("Fingerprints", type='IMAGE') tex.image = bpy.data.images.load("//textures/fingerprints_n.png") nm = fmat.node_tree.nodes.new("ShaderNodeNormalMap") nm.inputs["Strength"].default_value = 0.6 fmat.node_tree.links.new(nm.outputs["Color"], fp.inputs["Normal"]) # Вариант B: вмятины в геометрии через Displace bpy.ops.object.modifier_add(type='DISPLACE') # на выделенном объекте disp = bpy.context.object.modifiers["Displace"] disp.texture = tex disp.texture_coords = 'LOCAL' disp.direction = 'NORMAL' disp.strength = -0.004 # отрицательный = вдавливаем, а не выдавливаем disp.mid_level = 0.5 ``` ### Этап 5. Свет: скользящий, иначе вмятины пропадут ```python import math bpy.ops.object.light_add(type='AREA', location=(-0.9, -0.7, 1.1), rotation=(math.radians(-40), 0, math.radians(-55))) key = bpy.context.object key.name = "KeySlide" key.data.shape = 'RECTANGLE' key.data.size, key.data.size_y = 0.35, 1.2 # узкая полоса = скользящий блик key.data.energy = 120 # Заполняющий очень слабый: контраст = читаемость микрорельефа bpy.ops.object.light_add(type='AREA', location=(1.2, -1.0, 0.5)) fill = bpy.context.object fill.data.energy = 15 fill.data.size = 1.5 # Камера: макро-объектив и малая глубина резкости — «кукольность» cam = bpy.data.objects["Camera"] cam.data.lens = 85 cam.data.dof.use_dof = True cam.data.dof.focus_distance = 1.4 cam.data.dof.aperture_fstop = 2.8 ``` ### Этап 6. Дрожание камеры Рукотворность дают не только позы, но и «дыхание» камеры между дублями. ```python import math, random rng = random.Random(11) for pose, frame in enumerate(range(1, 73, 2), start=1): cam.location = (rng.uniform(-0.004, 0.004), rng.uniform(-0.004, 0.004), rng.uniform(-0.002, 0.002)) cam.rotation_euler.z = math.radians(rng.uniform(-0.4, 0.4)) cam.keyframe_insert("location", frame=frame) cam.keyframe_insert("rotation_euler", index=2, frame=frame) set_constant_interpolation([cam]) ``` ## ⚙️ КЛЮЧЕВЫЕ ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `scene.frame_step` | 2 | 3 при 8 fps, 1 для 24 fps | | `Sheen Weight` | 0.55 | выше 0.8 — «бархад», а не войлок | | `Roughness` | 0.92 | 1.0 — крашеный мел | | `hair_length` | 0.035 | для крупных кукол 0.08 | | `rendered_child_count` | 60 | выше 150 — рендер встаёт колом | | `aperture_fstop` | 2.8 | 5.6, если ворс не в фокусе | ## 📤 РЕЗУЛЬТАТ | Параметр | Значение | |---|---| | Объём | 36 отрендеренных кадров = **3 с при 12 fps** (72 кадра диапазона через один) | | Файлы | `felt_0001.png`, `felt_0003.png`, … — номера идут через один, 1920×1080, Cycles | | Рендер | 40–180 с/кадр с ворсом и SSS → **25–110 минут** на 36 кадров | | Проход | можно рендерить в 4 раза ниже и апскейлить — стоп-моушен это прощает | Про «4 раза ниже» стоит сказать точнее, потому что это экономия, а не потеря качества. Рендерить вчетверо ниже по каждой оси (960×540 вместо 1920×1080) безопасно: в пластилиновом кадре нет ни мелких надписей, ни тонких бликов, которые развалились бы апскейлом. А вот рендер втрое ниже по **числу сэмплов** бьёт по ворсу сильнее — там главный источник шума не амплитуда, а подповерхностное рассеяние на тонких пальцах, и после 64 сэмплов кайма по краям остаётся даже с денойзером. Практическая схема: рабочий проход на 128 сэмплов с `use_denoising` и `adaptive_threshold = 0.01`, финальный — на 512 с тем же порогом, разница между ними видна на щеке и на кончиках пальцев, а не в общем поле кадра. Проверяется одним скриптом. Он отвечает на три вопроса, которые ломают stop-motion незаметно: сколько кадров реально уйдёт в рендер, не осталось ли где-нибудь сглаженной интерполяции (плавная анимация вместо поз) и назначен ли ворсу материал. ```python import bpy, os, glob scene = bpy.context.scene # 1. Сколько кадров реально уйдёт в рендер: frame_step режет диапазон shots = list(range(scene.frame_start, scene.frame_end + 1, scene.frame_step)) fps_out = scene.render.fps / scene.frame_step print("кадров в диапазоне:", scene.frame_end - scene.frame_start + 1, "| после frame_step:", len(shots)) print("темп на выходе:", fps_out, "fps | длительность:", round(len(shots) / fps_out, 2), "с") # 2. Главная проверка: между позами не должно быть НИЧЕГО, кроме CONSTANT. # В 4.4+ кривые лежат в слотах, а не в action.fcurves — обходим дерево. def fcurves_of(ob): ad = ob.animation_data if ad is None or ad.action is None: return [] act = ad.action if getattr(act, "layers", None): out = [] for layer in act.layers: for strip in layer.strips: cb = strip.channelbag(ad.action_slot) if cb: out.extend(cb.fcurves) return out return list(act.fcurves) bad = [] for ob in [o for o in scene.objects if o.animation_data]: for fc in fcurves_of(ob): kinds = sorted({kp.interpolation for kp in fc.keyframe_points}) if kinds != ['CONSTANT']: bad.append((ob.name, fc.data_path, kinds)) print("кривых с интерполяцией:", len(bad)) for row in bad[:8]: print(" ", row) # 3. Ворс: без материала strands рендерятся чёрным карандашом по краю силуэта hero = bpy.data.objects["Hero"] ps = hero.particle_systems[-1] print("частиц:", ps.settings.count, "| детей:", ps.settings.rendered_child_count, "| длина:", round(ps.settings.hair_length, 3)) print("материал ворса:", repr(ps.settings.material_slot), "| слот:", ps.settings.material) # 4. Кадры на диске files = sorted(glob.glob(os.path.join(bpy.path.abspath("//render"), "felt_*.png"))) print("отрендерено:", len(files), "| ожидалось:", len(shots)) ``` Ненулевой счётчик в п. 2 — это ровно тот случай, когда stop-motion тихо превратился в обычную анимацию. Пустой `material_slot` в п. 3 означает, что strands останутся без шейдера и дадут грязную кайму, а не мягкий ореол. ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Движение всё равно плавное | `frame_step` не влияет на анимацию, только на рендер | интерполяция должна быть `CONSTANT` | | `KeyError: "Sheen Weight"` | версия до 4.0 | использовать имя `Sheen` | | Ворс не рендерится | не задан материал или слишком мал `tip_radius` | назначить материал, поднять `root_radius` до 0.0005 | | Рендер считается вечно | `rendered_child_count` завышен | 30–60 достаточно, а `child_percent` поднять | | Вмятины не видны | свет frontal | поставить key сбоку-сверху, узкой полосой | | Материал выглядит как резина | нет подповерхностного слоя | `Subsurface Weight` 0.1–0.15 | | Модель «плывёт» между дублями | не применён CONSTANT к камере | прогнать `set_constant_interpolation` и по камере | ## 🔗 СВЯЗИ - **Прочие в категории «Воркфлоу»:** [WF 23: VIRTUAL PRODUCTION LED](wf_23_virtual_production_led.md), [WF 24: PHOTOGRAMMETRY CLEANUP](wf_24_photogrammetry_cleanup.md), [WF 01: TEXT-TO-SCENE](wf_01_text_to_scene.md), [WF 03: TEXTURE PIPELINE](wf_03_texture_pipeline.md), [WF 02: IMAGE-TO-3D](wf_02_image_to_3d.md) - **По темам:** [CASE 03: PRODUCT VISUALIZATION](../cases/case_03_product_viz.md), [P46: UDIM WORKFLOW](../prompts/46_udim_workflow.md), [CASE 05: PROCEDURAL CITY](../cases/case_05_procedural_city.md), [CASE 07: STYLYZED BARREL (HAND-PAINT)](../cases/case_07_stylized_barrel.md) - **Теги:** #animation, #pipeline, #render, #shading, #texturing, #workflow --- # WF 18: ISOMETRIC GAME ART Tags: #stylized #workflow # WF 18: ISOMETRIC GAME ART **ID:** WF-18 | **Type:** Game | **Difficulty:** ⭐⭐ **RU/EN Bilingual** ## 🎯 ЦЕЛЬ / Goal RU: Ortho 54.7°, tileable, atlas, Godot export EN: Ortho 54.7°, tileable, atlas, Godot export Изометрические тайлы и ассеты для игры: тайл земли, который повторяется без шва, набор спрайтов/моделей под одну и ту же проекцию, всё выверено под одну камеру и экспортировано в движок. Главная сложность — **не геометрия, а согласованность**: если один ассет снят при 54.736°, а второй при 60°, они не совпадут ни при каком движении камеры, и переделать потом дорого. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия | Зачем | |---|---|---| | Blender | 4.x | ortho-камера, тайлинг, glTF-экспорт | | Бесшовные текстуры | Poly Haven / CC0 | тайл земли | | Godot | 4.x | приём glTF / PNG | | Спрайт-лист (атлас) | — | 1 текстура на много объектов | ## 🛠 НАСТРОЙКА ### Шаг 0. Сначала математика: 54.736° и 60° — это разное Это главная путаница, и она не косметическая — от угла зависит сетка пикселей. | Проекция | Наклон камеры (X) | След плитки на экране | Когда берут | |---|---|---|---| | True isometric | `54.7356°` | √3 : 1 | 3D-рендер без пиксель-арта | | Dimetric 2:1 | `60°` | **2 : 1** | пиксель-арт и тайлы, «классическая изометрия» | Закон простой: при повороте по Z на 45° отношение ширины к высоте следа плитки равно `1 : cos(X)`. Отсюда `cos X = 0.5` → `X = 60°` для 2:1, а `cos 54.736° ≈ 0.577` → √3:1. ```python import bpy, math TRUE_ISO = math.radians(54.7356) # atan(sqrt(2)) PIXEL_ISO = math.radians(60.0) # 2:1 def make_iso_camera(angle=PIXEL_ISO, ortho_scale=10.0, distance=40.0): cam_data = bpy.data.cameras.new("IsoCam") cam = bpy.data.objects.new("IsoCam", cam_data) bpy.context.scene.collection.objects.link(cam) cam_data.type = 'ORTHO' # именно ортокамера: перспектива «плывёт» по глубине cam_data.ortho_scale = ortho_scale cam.rotation_euler = (angle, 0.0, math.radians(45.0)) # Точку схода держим на Z = 0: сетка плиток уходит в горизонт, а не «в небо». # Для true iso направление взгляда единичное = (0.5774, -0.5774, 0.8165). v = (math.sin(angle) * math.sqrt(0.5), -math.sin(angle) * math.sqrt(0.5), math.cos(angle)) cam.location = (v[0] * distance, v[1] * distance, v[2] * distance) cam_data.clip_end = distance * 4.0 bpy.context.scene.camera = cam return cam ``` ### Этап 1. Тайл земли: сетка 1×1 и UV вне диапазона ```python import bpy scene = bpy.context.scene scene.unit_settings.system = 'METRIC' scene.unit_settings.scale_length = 1.0 # 1 юнит = 1 метр = 1 тайл. Это обязательное условие: иначе тайл «поедет» # при масштабировании коллизий в движке. bpy.ops.mesh.primitive_grid_add(x_subdivisions=2, y_subdivisions=2, size=1.0) tile = bpy.context.object tile.name = "Tile_Ground" ``` Тайл делается **больше 1×1 в UV**, а Image Texture ставится в `REPEAT`. Так он бесшовный при любом масштабе и не зависит от того, как разложены UV. ```python uv = tile.data.uv_layers.active for loop in tile.data.loops: uv.data[loop.index].uv = (loop.vertex.co.x * 2.0, loop.vertex.co.y * 2.0) mat = bpy.data.materials.new("Mat_Tile") mat.use_nodes = True nt = mat.node_tree p = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED') tex = nt.nodes.new("ShaderNodeTexImage") tex.image = bpy.data.images.load("//textures/grass_bare.png") tex.extension = 'REPEAT' # повтор по краям кадра текстуры nt.links.new(tex.outputs["Color"], p.inputs["Base Color"]) ``` > [!NOTE] > Бесшовность берётся либо из флага источника изображения, либо из подготовки > самого файла. В свойствах Image (Properties → Image → Source) есть вариант > `SEAMLESS`, который пересобирает текстуру в tile-паттерн; альтернатива — > `extension = 'MIRROR'` у ноды, дающая бесшовный повтор за счёт зеркальных > половин. Оба способа реальны, но дают разный рисунок: выбирай осознанно. ### Этап 2. Атлас: padding обязателен В атласе билинейная интерполяция соседних UV тянет цвет из соседней плитки, и по краям каждого спрайта появляется светлая кайма. Лечится только отступами. ```python # Плитка 2x2 с отступом 4 px при атласе 1024 => 4 тайла по 256 без каймы PAD, TILE = 4, 256 ATLAS = 1024 uv = tile.data.uv_layers.active def atlas_uv(col, row, cells=2): step = TILE / ATLAS off = (PAD / ATLAS) * cells uv.data[loop.index].uv = ( col * step + off, row * step + off) for loop in tile.data.loops: atlas_uv(0, 0) # пример: один тайл в левом нижнем углу атласа ``` > [!NOTE] > Увеличение шага и добавление рамки — упрощённая схема; в продакшне атлас либо > собирают внешним инструментом, либо выносят тайлы в отдельные материалы. > Альтернатива атласу — текстурный массив: в нём нет соседних плиток, поэтому > каймы не бывает by design. Проверь, поддерживает ли твой экспортёр/движок > текстурные массивы, прежде чем выбирать этот путь. ### Этап 3. Свет и схема: три направления, никаких новых При изометрии источник света «виден» только если он даёт ровно три градации на каждой из 6 граней куба — иначе тайлы читаются как разноцветные ромбы, а не как объёмные блоки. ```python import math bpy.ops.object.light_add(type='SUN', rotation=(math.radians(50), 0, math.radians(35))) sun = bpy.context.object sun.data.energy = 3.0 sun.data.angle = math.radians(3.0) # мягче = меньше «каши» на мелких тайлах scene.view_settings.view_transform = 'Standard' # плоские цвета, как в игре scene.render.engine = 'BLENDER_EEVEE_NEXT' if 'BLENDER_EEVEE_NEXT' in \ {i.identifier for i in bpy.types.RenderSettings.bl_rna.properties['engine'].enum_items} \ else 'BLENDER_EEVEE' ``` ### Этап 4. Рендер тайла и ассет-прототипов ```python scene.render.film_transparent = True # обязательно: фон должен быть альфой scene.render.image_settings.file_format = 'PNG' scene.render.image_settings.color_mode = 'RGBA' scene.render.resolution_x = scene.render.resolution_y = 1024 scene.render.resolution_percentage = 100 scene.render.filepath = "//game/tiles/iso_tile_" bpy.ops.render.render(write_still=True) # Спрайт-лист персонажа: 4 направления × 4 кадра scene.render.resolution_x, scene.render.resolution_y = 2048, 2048 scene.frame_start, scene.frame_end = 1, 4 scene.render.filepath = "//game/sprites/hero_" bpy.ops.render.render(animation=True) ``` Тени на изометрии — отдельный трюк: включи **shadow catcher** на плоскости под объектом, чтобы тень стала альфой в том же файле, что и спрайт. ```python bpy.ops.mesh.primitive_plane_add(size=20, location=(0, 0, 0.001)) shadow = bpy.context.object shadow.is_shadow_catcher = True # Cycles; в EEVEE это не работает ``` ### Этап 5. Экспорт в движок ```python bpy.ops.object.select_all(action='DESELECT') for ob in scene.objects: if ob.type == 'MESH': ob.select_set(True) bpy.ops.export_scene.gltf( filepath="//game/export/iso_level.glb", export_format='GLB', use_selection=True, export_apply=True, # применить модификаторы на экспорт export_texcoords=True, export_normals=True, export_yup=True, # Blender Z-up -> glTF Y-up. Не выключай. ) ``` > [!NOTE] > Список параметров glTF-экспортёра менялся между версиями: часть флагов переехала > в другую вкладку и переименована. Точный набор для твоей сборки смотри так: > `[p.identifier for p in bpy.ops.export_scene.gltf.get_rna_type().properties]`. > Универсально безопасны только `filepath`, `export_format`, `use_selection`. ## ⚙️ КЛЮЧЕВЫЕ ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | Наклон камеры | 60° (2:1) или 54.7356° (true iso) | менять только до начала ассетов | | `ortho_scale` | ширина тайла в пикселях / 1024 | подгонять под сетку | | `film_transparent` | `True` | для спрайтов обязательно | | `view_transform` | `'Standard'` | AgX «приближает» цвета к реализму | | Атлас padding | 4 px на 256 | при мыльном рендере — до 8 px | ## 📤 РЕЗУЛЬТАТ | Параметр | Значение | |---|---| | Тайл земли | `iso_tile.png`, 1024×1024, RGBA, бесшовный | | Спрайт-лист | `hero_0001…0004.png`, 2048×2048 × 4 кадра, альфа в отдельном канале | | 3D-экспорт | `iso_level.glb`, ~2–20 МБ | | Рендер | тайл — **секунды**, спрайт-лист 4 кадра — **1–4 мин** EEVEE | | Тайминг | один тайл + один спрайт-лист = рабочий атом ассет-пака | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Тайл видно швом | UV ровно 0–1 и `extension = 'CLIP'` | `extension = 'REPEAT'` + UV 0–2 | | Светлая кайма по краям спрайта | нет padding в атласе | 4–8 px отступа либо текстурный массив | | Соседние тайлы «плывут» | не совпадает масштаб UV и единицы сцены | `unit_settings.scale_length = 1.0`, UV = миры × N | | Тайлы не стыкуются в игре | экспорт с неприменённым скейлом | `export_apply=True`, в движке не масштабировать | | Всё «плоское», нет объёма | источник света фронтально | поднять угол солнца до 45–60°, убрать ambient | | Взлетело не туда при импорте | Z-up против Y-up | `export_yup=True` при экспорте | | Часть ассетов не совпала | их снимали с другим углом камеры | переснять всё с одной камеры | ## 🔗 СВЯЗИ - **Прочие в категории «Воркфлоу»:** [WF 16: NPR ANIME RENDER](wf_16_npr_anime_render.md), [WF 19: VOXEL ART PIPELINE](wf_19_voxel_art_pipeline.md), [WF 20: KITBASH GREEBLE CITY](wf_20_kitbash_greeble.md), [WF 01: TEXT-TO-SCENE](wf_01_text_to_scene.md), [WF 02: IMAGE-TO-3D](wf_02_image_to_3d.md) - **По темам:** [CASE 08: SCI-FI CORRIDOR MODULAR](../cases/case_08_sci_fi_corridor.md), [CASE 09: ANIME HEAD BUST](../cases/case_09_anime_head_bust.md), [CASE 11: COZY ROOM DIORAMA](../cases/case_11_cozy_room_diorama.md) - **Теги:** #stylized, #workflow --- # WF 19: VOXEL ART PIPELINE Tags: #pipeline #stylized #workflow # WF 19: VOXEL ART PIPELINE **ID:** WF-19 | **Type:** Game | **Difficulty:** ⭐ **RU/EN Bilingual** > Когда применять: когда нужно превратить любую форму — модель, скан, AI-генерацию — в узнаваемую блочную графику с ограниченной палитрой: для игровых ассетов, иконок, диорам. ## 🎯 ЦЕЛЬ Любой объект пересобирается по регулярной сетке в объём из кубических ячеек, цвет ограничивается палитрой из 8–16 цветов, и на выходе получается ассет, который одинаково читается в рендере, в игровом движке и на иконке 64×64. Ключевая мысль: вокселизация — это **пересборка формы**, а не декорация. Если поверх гладкой модели просто наложить кубики, получится «каша», потому что оригинальная поверхность никуда не делась. Сначала объём должен быть пересобран воксельным ремешем, и только потом раскрашен. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия / состояние | Зачем | |---|---|---| | Blender с модификатором Remesh | проверь: `Add Modifier → Remesh`, режимы `Blocks / Smooth / Sharp / Voxel` | пересборка геометрии по сетке | | Исходная геометрия | своя, из библиотеки или из AI-генератора | форма, которую пересобираем | | Image Editor | встроенный | уменьшение палитры до 8–16 цветов | | Cycles или EEVEE | любой; для превью хватит EEVEE | финальный кадр | | Оценка, а не замер | — | время рендера в таблице ниже — порядок величины | > [!NOTE] > Встроенного импортёра формата `.vox` (MagicaVoxel) в Blender нет. Обмен с MagicaVoxel идёт через OBJ + текстуру, либо через сторонний аддон. Если нужен именно `.vox` — сначала экспортируй OBJ с развёрткой, импортируй обратно и собери палитру через Remesh + ColorRamp. ## 🛠️ НАСТРОЙКА ### Этап 1. Подготовка: выбрать размер ячейки Шаг ячейки (`voxel size`) — это главное решение всего пайплайна: он задаёт и объём файла, и читаемость силуэта. ```bash RU: "Воксельная сетка 0.1 м. Возьми объект 'Sword' из blend, уменьши его до высоты 1.2 м, примени трансформации, сохрани копию как 'Sword_Orig'" EN: "Voxel grid 0.1 m. Take object 'Sword', scale to 1.2 m tall, apply transforms, keep a copy as 'Sword_Orig'" ``` Проверка до ремеша: посчитай, сколько ячеек уйдёт. Объём габаритного параллелепипеда, делённый на размер ячейки в кубе, даёт верхнюю оценку. ```python import bpy obj = bpy.data.objects["Sword"] cell = 0.10 dims = [d / cell for d in obj.dimensions] # ячеек по каждой оси print("габарит в ячейках:", [round(d) for d in dims], "верхняя оценка объёма:", int(dims[0] * dims[1] * dims[2])) # Если оценка > 500 000 — ячейка слишком мелкая, уменьшай детализацию, а не размер ячейки. ``` ### Этап 2. Генерация: воксельный ремеш Два пути. Быстрый — оператор, который сразу переписывает меш. Контролируемый — модификатор, который можно крутить не генерируя результат. ```python import bpy obj = bpy.data.objects["Sword"] bpy.ops.object.select_all(action='DESELECT') obj.select_set(True) bpy.context.view_layer.objects.active = obj bpy.ops.object.mode_set(mode='OBJECT') # ремеш не работает в Edit Mode; mode_set — оператор, не метод # --- путь A: оператор, переписывает меш на месте --- obj.data.remesh_voxel_size = 0.10 # размер ячейки в единицах сцены obj.data.remesh_voxel_adaptivity = 0.0 # 0 = строгая сетка, больше = экономнее obj.data.remesh_preserve_volume = True # не давать объёму «схлопнуться» в местах сближения bpy.ops.object.voxel_remesh() ``` Путь B — тот же результат, но через модификатор: у тебя остаётся исходная геометрия в стеке, можно менять параметры без перегенерации. ```python mod = obj.modifiers.new("Voxel", 'REMESH') mod.mode = 'BLOCKS' # именно Blocks даёт кубические ступеньки mod.octree_depth = 5 # больше = мельче ячейка; логарифмическая шкала mod.use_remove_disconnected = True # выкинуть мелкие островки mod.use_smooth_shade = False # для вокселей нужны плоские грани bpy.ops.object.modifier_apply(modifier=mod.name) ``` ### Этап 3. Look: палитра и раскраска Квантование цвета делается штатно: ColorRamp с интерполяцией `Constant` — это и есть палитра. Важная деталь: стопы ставятся **на границах** между цветами (середина между соседними палитрами), иначе крайние значения интерполируются и получается каша. ```python import bpy mat = bpy.data.materials.new("Mat_VoxelAtlas") mat.use_nodes = True nt = mat.node_tree bsdf = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED') bsdf.inputs['Roughness'].default_value = 0.75 tex = nt.nodes.new("ShaderNodeTexImage") tex.image = bpy.data.images.load(r"//palette_ref.png") tex.interpolation = 'Closest' # без этогоimage сглаживает между вокселями ramp = nt.nodes.new("ShaderNodeValToRGB") ramp.color_ramp.interpolation = 'CONSTANT' # 6 цветов палитры -> 7 стопов: 6 центров + границы stops = [(0.00, (0.12, 0.10, 0.09, 1)), (0.18, (0.55, 0.20, 0.14, 1)), (0.36, (0.85, 0.62, 0.30, 1)), (0.55, (0.35, 0.55, 0.28, 1)), (0.74, (0.20, 0.35, 0.60, 1)), (0.90, (0.92, 0.90, 0.86, 1))] cr = ramp.color_ramp cr.elements[0].position, cr.elements[0].color = stops[0][0], stops[0][1] cr.elements[1].position, cr.elements[1].color = stops[-1][0], stops[-1][1] for pos, col in stops[1:-1]: cr.elements.new(pos).color = col nt.links.new(tex.outputs['Color'], ramp.inputs['Fac']) nt.links.new(ramp.outputs['Color'], bsdf.inputs['Base Color']) obj.data.materials.clear(); obj.data.materials.append(mat) ``` Если воксели должны быть разноцветными (не одним материалом), крась по правилам — высота, нормаль или случай: ```python import bpy, random random.seed(7) # повторяемость: без сида каждый запуск даёт новый результат me = obj.data attr = me.color_attributes.new(name="Col", type='FLOAT_COLOR', domain='POINT') zmin, zmax = min(v.co.z for v in me.vertices), max(v.co.z for v in me.vertices) palette = [(0.12,0.10,0.09,1), (0.55,0.20,0.14,1), (0.85,0.62,0.30,1), (0.35,0.55,0.28,1), (0.92,0.90,0.86,1)] for item in attr.data: t = (item.vector.z - zmin) / max(zmax - zmin, 1e-6) if t < 0.25: # низ — тень item.color = palette[0] elif random.random() < 0.15: # редкий блик-«шум» item.color = palette[random.randint(1, 4)] else: item.color = palette[2 if t < 0.6 else 4] ``` Материал для чтения атрибута — `Attribute`, имя атрибута `Col`. Плоские грани обязательны, иначе кубики скругляются и стиль пропадает: ```python for p in me.polygons: p.use_smooth = False # воксель — всегда плоская грань mat2 = bpy.data.materials.new("Mat_VoxelVertexColor") mat2.use_nodes = True nt = mat2.node_tree bsdf = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED') a = nt.nodes.new("ShaderNodeAttribute") a.attribute_type = 'GEOMETRY' a.attribute_name = "Col" nt.links.new(a.outputs['Color'], bsdf.inputs['Base Color']) obj.data.materials.clear(); obj.data.materials.append(mat2) ``` > [!NOTE] > `mesh.color_attributes.new(...)` с типом `FLOAT_COLOR` появился в Blender 3.2+. В более старых версиях цвет лежит в вершинных цветах, и доступ другой — проверь в `Help → Manual` раздел про цветовые атрибуты для своей версии. ### Этап 4. Deliver: рендер и экспорт ```python import bpy sc = bpy.context.scene sc.render.engine = 'CYCLES' sc.cycles.samples = 64 sc.cycles.use_denoising = True sc.render.resolution_x, sc.render.resolution_y = 1024, 1024 sc.render.film_transparent = False sc.render.filepath = r"//renders/voxel_sword.png" bpy.ops.render.render(write_still=True) bpy.ops.export_scene.gltf(filepath=r"//export/sword_voxel.glb", export_format='GLB', use_selection=True, export_apply=True) ``` Считать геометрию надо через evaluated-объект: модификаторы в исходном меше не лежат. ```python dg = bpy.context.evaluated_depsgraph_get() ev = obj.evaluated_get(dg) me = ev.to_mesh() tris = sum(len(p.vertices) - 2 for p in me.polygons) print(f"треугольников: {tris:,}, вершин: {len(me.vertices):,}") ev.to_mesh_clear() # без этого копия меша остаётся в памяти и утекает при цикле ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `remesh_voxel_size` / `octree_depth` | 0.1 м / 5 | мельче — детализация растёт, файл и память растут кубически | | `mod.mode` | `'BLOCKS'` | `'SMOOTH'` — если нужен не блочный, а сглаженный воксельный ремеш | | `remesh_voxel_adaptivity` | 0.0 | поднимать, если память кончается; растёт «шаг» в пустых местах | | `color_ramp.interpolation` | `'CONSTANT'` | ровно это и даёт квантование в палитру | | `tex.interpolation` | `'Closest'` | `Linear` размоет границы вокселей | | `octree_depth` | +1 | ≈ в 2 раза мельче ячейка по линейной оси | | `use_remove_disconnected` | `True` | выкидывает мусорные островки меньше порога | | Шаг ячейки | Что читается | Порядок объёма | |---|---|---| | 0.02–0.05 м | детали механизма, оружие | десятки–сотни тысяч ячеек | | 0.1 м | персонаж, проп, мебель | десятки тысяч ячеек | | 0.25–0.5 м | окружение, здания, landscape | тысячи ячеек | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Поверх кубиков видны следы исходной модели | вокселизация сделана как декорация, ремеш не применён | применить Remesh до раскраски, проверить `octree_depth` | | Всё слито в одну кашу | ячейка мельче, чем самая тонкая деталь | увеличить `voxel_size`, упростить исходную форму | | Файл весит сотни мегабайт | `remesh_voxel_adaptivity = 0` на большой сцене | поднять адаптивность, ограничить габарит | | Цвета «в грязь», ступенек не видно | стопы ColorRamp поставлены на сами значения палитры, а не на границы | ставить стопы в середине между соседними цветами | | Грани вокселя скруглены | сглаженные нормали после ремеша | `polygon.use_smooth = False` для всех граней | ## ✅ РЕЗУЛЬТАТ На выходе: один объект из воксельных ячеек + один материал (атлас-текстура с 6–16 цветами либо чтение цветового атрибута), плюс превью 1024×1024 и GLB. Характеристики типового ассета — **оценка, а не измеренный результат**: проп высотой 1.2 м при ячейке 0.1 м даёт порядка 20–60 тыс. треугольников (шесть граней на ячейку), один материал, одна текстура 1024×1024. Рендер одного кадра 1024×1024 в Cycles на 64 сэмплах с шумоподавлением — секунды, десятки секунд; на 256 сэмплах без шумоподавления — минуты. Размер GLB с текстурой в PNG/JPEG — единицы мегабайт. **Это оценка по порядку величины.** Точное время рендера зависит от железа, версии Cycles, числа материалов и объёма; измеряй на своей машине, а не по этой таблице. Базовые сетапы: [SETUP 01: BLENDER MCP BASIC](../setups/setup_01_blender_mcp_basic.md) — транспорт команд в Blender, [SETUP 08: ASSET PIPELINE](../setups/setup_08_asset_pipeline.md) — общая схема работы с ассетами. ## 🔗 СВЯЗИ - **Прочие в категории «Воркфлоу»:** [WF 01: TEXT-TO-SCENE](wf_01_text_to_scene.md), [WF 02: IMAGE-TO-3D](wf_02_image_to_3d.md), [WF 03: TEXTURE PIPELINE](wf_03_texture_pipeline.md), [WF 05: PROCEDURAL ASSETS](wf_05_procedural_assets.md), [WF 07: GAME ASSET PIPELINE](wf_07_game_asset_pipeline.md) - **По темам:** [CASE 08: SCI-FI CORRIDOR MODULAR](../cases/case_08_sci_fi_corridor.md), [WF 16: NPR ANIME RENDER](wf_16_npr_anime_render.md), [WF 17: STOP-MOTION FELT LOOK](wf_17_stop_motion_felt.md) - **Теги:** #pipeline, #stylized, #workflow --- # WF 20: KITBASH GREEBLE CITY Tags: #stylized #workflow # WF 20: KITBASH GREEBLE CITY **ID:** WF-20 | **Type:** Kitbash | **Difficulty:** ⭐⭐ **RU/EN Bilingual** > Когда применять: когда крупная форма уже собрана из блоков, но выглядит «пустой»: нужно заполнить поверхности мелкими деталями — трубами, панелями, антеннами, решётками — не моделируя их вручную. ## 🎯 ЦЕЛЬ Мы берём готовую китбаш-коробку и автоматически расставляем по её граням библиотеку мелких модулей — греблей (greebles). Форма перестаёт читаться как набор чистых прямоугольников и начинает читаться как техника или здание. Принцип, который отличает нормальный китбаш от мусора: **детали разного масштаба, разной плотности и с разной ориентацией**. Модули не должны повторяться рядами и не должны одинаково ориентироваться. Поэтому три параметра, которые настраивают в первую очередь: плотность, минимальное расстояние между модулями и фильтр по размеру грани. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия / состояние | Зачем | |---|---|---| | Blender с Geometry Nodes | 3.0+; интерфейс нод менялся в 4.0 | автоматическая расстановка модулей | | Коллекция модулей | 20–80 мелких объектов, единый масштаб | источник деталей | | Базовая форма (коробка) | из китбаш-библиотеки или собрана вручную | поверхность, на которую сажаем модули | | Процедурные материалы | без текстур можно обойтись | материал читается лучше с вариативностью | | HDRI | Poly Haven | окружение для рендера | > [!NOTE] > Имена нод Geometry Nodes (например `GeometryNodeDistributePointsOnFaces`) — это внутренние идентификаторы, а не то, что видишь в интерфейсе. В поиске нод в Blender они находятся по человекочитаемому имени: `Distribute Points on Faces`, `Instance on Points`, `Pick Instance`, `Collection Info`. Полный список идентификаторов твоей версии смотри из Python-консоли: `[t for t in dir(bpy.types) if t.startswith("GeometryNode")]`. ## 🛠️ НАСТРОЙКА ### Этап 1. Подготовка: привести модули к общему виду Модули должны быть одного масштаба, иначе «гребля» будет выглядеть как случайный мусор. К каждому модулю: origin в основании (не в центре), нормали наружу, без оставшихся модификаторов. ```bash RU: "Собери коллекцию 'GreebleLib': импортируй 24 модуля в 1 unit = 1 m, для каждого origin_set(type='ORIGIN_CURSOR', center='MEDIAN') в нижнюю точку, вычисти модификаторы, применяй трансформации" EN: "Build collection 'GreebleLib': 24 modules at 1 unit = 1 m, origin_set(type='ORIGIN_CURSOR', center='MEDIAN') to the base, clear modifiers, apply transforms" ``` ```python import bpy lib = bpy.data.collections["GreebleLib"] bpy.ops.object.select_all(action='DESELECT') for ob in lib.objects: ob.select_set(True) bpy.context.view_layer.objects.active = lib.objects[0] for ob in lib.objects: # origin внизу: так модуль «садится» на поверхность, а не висит в воздухе zs = [ (ob.matrix_world @ v.co).z for v in ob.data.vertices ] cursor_z = min(zs) scene_cursor = bpy.context.scene.cursor.location.copy() bpy.context.scene.cursor.location = (ob.location.x, ob.location.y, cursor_z) bpy.ops.object.origin_set(type='ORIGIN_CURSOR', center='MEDIAN') bpy.context.scene.cursor.location = scene_cursor ob.scale = [s / 1.0 for s in ob.scale] # нормализация к 1 m bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) for m in list(ob.modifiers): ob.modifiers.remove(m) ``` ### Этап 2. Расстановка: Geometry Nodes Схема: `Distribute Points on Faces` → `Instance on Points` (по коллекции) → `Pick Instance` (случайный ребёнок на точку) → `Realize Instances` (опционально). ```python import bpy ng = bpy.data.node_groups.new("GN_Greeble", 'GeometryNodeTree') # --- интерфейс группы. В Blender 4.0+ это ng.interface.new_socket(...), # --- в более старых версиях — ng.inputs.new("NodeSocketFloat", "Name"). def iface(name, stype, in_out): try: return ng.interface.new_socket(name, in_out=in_out, socket_type=stype) except AttributeError: coll = ng.outputs if in_out == 'OUTPUT' else ng.inputs return coll.new(stype, name) iface("Geometry", "NodeSocketGeometry", 'INPUT') iface("Geometry", "NodeSocketGeometry", 'OUTPUT') iface("Density", "NodeSocketFloat", 'INPUT') iface("Seed", "NodeSocketInt", 'INPUT') gi_in = next(s for s in ng.interface.items_tree if s.item_type=='SOCKET' and s.in_out=='INPUT' and s.name=="Geometry") gi_out = next(s for s in ng.interface.items_tree if s.item_type=='SOCKET' and s.in_out=='OUTPUT' and s.name=="Geometry") def N(t, **kw): n = ng.nodes.new(t) for k, v in kw.items(): setattr(n, k, v) return n def S(n, name): s = n.inputs.get(name) if s is None: raise KeyError(f"{n.bl_idname}: нет входа '{name}' — проверь имена в своей версии") return s def L(a, b): ng.links.new(a, b) grp_in = N("NodeGroupInput"); grp_out = N("NodeGroupOutput") dist = N("GeometryNodeDistributePointsOnFaces") inst = N("GeometryNodeInstanceOnPoints") pick = N("GeometryNodePickInstance") coll = N("GeometryNodeCollectionInfo") rnd = N("FunctionNodeRandomValue"); rnd.data_type = 'INT' real = N("GeometryNodeRealizeInstances") coll.inputs["Collection"].default_value = bpy.data.collections["GreebleLib"] coll.inputs["Separate Children"].default_value = True coll.inputs["Reset Children"].default_value = True rnd.inputs["Min"].default_value = 0 rnd.inputs["Max"].default_value = len(coll.inputs["Collection"].default_value.objects) - 1 rnd.inputs["Seed"].default_value = 7 L(grp_in.outputs["Geometry"], dist.inputs["Mesh"]) L(dist.outputs["Points"], inst.inputs["Points"]) L(grp_in.outputs["Density"], dist.inputs["Density"]) L(coll.outputs["Geometry"], inst.inputs["Instance"]) L(inst.outputs["Instances"], pick.inputs["Geometry"]) L(grp_in.outputs["Seed"], pick.inputs["Instance Index"]) L(rnd.outputs["Value"], pick.inputs["Instance Index"]) L(pick.outputs["Geometry"], real.inputs["Geometry"]) L(real.outputs["Geometry"], grp_out.inputs["Geometry"]) base = bpy.data.objects["Block_Main"] mod = base.modifiers.new("Greeble", 'NODES') mod.node_group = ng base.modifiers["Greeble"]["Socket_2"] = 6.0 # Density: модулей на м² base.modifiers["Greeble"]["Socket_3"] = 7 # Seed ``` > [!NOTE] > Значения `Socket_N` в списке модификаторов зависят от того, в каком порядке интерфейс группы создаёт сокеты. Надёжнее после создания группы пройти по `mod` через интерфейс и записать значения по имени, либо задавать их руками в панели модификатора. Порядок и имена сокетов проверь в UI на своей версии. ### Этап 3. Фильтр по размеру грани и по ориентации Модули одного размера на всех гранях дают «гвозди». Два приёма: (1) отбирать модули по площади грани через `Distribute Points on Faces` + `Delete Geometry` по полю, (2) добавить рандомный поворот через `Random Value` (Vector) → `Instance on Points.Rotation`. ```python # Продолжение графа: случайный поворот каждого экземпляра rot = ng.nodes.new("FunctionNodeRandomValue") rot.data_type = 'FLOAT_VECTOR' for name, v in (("Min", (-3.14, -3.14, -3.14)), ("Max", (3.14, 3.14, 3.14))): for s in rot.inputs: if s.name == name and s.type == 'VECTOR': s.default_value = v break rot.inputs["Seed"].default_value = 11 ng.links.new(rot.outputs["Value"], inst.inputs["Rotation"]) ``` ### Этап 3a. Проверка плотности, а не «на глаз» Плотность в модулях на м² нельзя прикидывать: можно получить и гвозди, и кашу. Считаем фактическое число инстансов и площадь покрытия на грани. ```python import bpy dg = bpy.context.evaluated_depsgraph_get() base = bpy.data.objects["Block_Main"] ev = base.evaluated_get(dg) me = ev.to_mesh() me.calc_loop_triangles() area = 0.0 for t in me.loop_triangles: p = [ev.matrix_world @ me.vertices[i].co for i in t.vertices[:3]] area += ((p[1] - p[0]).cross(p[2] - p[0])).length / 2.0 mods = base.modifiers["Greeble"] density = float(mods.get("Socket_2", 0.0) or 0.0) est = area * density print(f"площадь граней: {area:.1f} м², density={density:.1f}, модулей примерно {est:.0f}") ev.to_mesh_clear() # ориентир: меньше 3 на м² — пусто, больше 12 — начинается каша if est and area: print("плотность в норме" if 3 <= est / area <= 12 else "плотность вне диапазона 3..12") ``` ### Этап 4. Вариативность без новых объектов Object Info → `Random` даёт каждому объекту стабильное число 0..1. Через ColorRamp оно превращается в сдвиг цвета и шероховатости. Это самый дешёвый способ сделать 40 одинаковых модулей неодинаковыми. ```python import bpy mat = bpy.data.materials.new("Mat_Kitbash") mat.use_nodes = True nt = mat.node_tree bsdf = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED') oi = nt.nodes.new("ShaderNodeObjectInfo") ramp = nt.nodes.new("ShaderNodeValToRGB") ramp.color_ramp.elements[0].color = (0.18, 0.19, 0.21, 1) # «грязный» вариант ramp.color_ramp.elements[1].color = (0.42, 0.44, 0.47, 1) # «чистый» вариант nt.links.new(oi.outputs["Random"], ramp.inputs["Fac"]) nt.links.new(ramp.outputs["Color"], bsdf.inputs["Base Color"]) rough = nt.nodes.new("ShaderNodeMapRange") rough.inputs["To Min"].default_value = 0.25 rough.inputs["To Max"].default_value = 0.65 nt.links.new(oi.outputs["Random"], rough.inputs["Value"]) nt.links.new(rough.outputs["Result"], bsdf.inputs["Roughness']) ``` ### Этап 5. Декали и грязь Декаль — тонкая плоскость с альфа-текстурой, прижатая к поверхности. Грязь — Attribute/Vertex Color, покрашенный по высоте. ```python import bpy # декаль-плоскость с альфой bpy.ops.mesh.primitive_plane_add(size=0.8, location=(0, -2.0, 1.2)) dec = bpy.context.active_object dec.name = "Decal_07" m = bpy.data.materials.new("Mat_Decal"); m.use_nodes = True nt = m.node_tree bsdf = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED') tex = nt.nodes.new("ShaderNodeTexImage") tex.image = bpy.data.images.load(r"//textures/decal_07.png") # PNG с альфой tex.extension = 'CLIP' nt.links.new(tex.outputs["Color"], bsdf.inputs["Base Color"]) nt.links.new(tex.outputs["Alpha"], bsdf.inputs["Alpha"]) # в Cycles можно вместо этого использовать ноду Decal — она есть только в Cycles dec.data.materials.append(m) ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `Distribute Points on Faces.Density` | 4–8 модулей на м² | ниже 3 — пусто, выше 12 — каша | | `Distribute Points on Faces.Seed` | любое целое | менять, чтобы получить другой расклад, а не чтобы «починить» | | `Collection Info.Separate Children` | `True` | иначе все модули слипнутся в один инстанс | | `Collection Info.Reset Children` | `True` | сбрасывает трансформации детей к нулю | | `Pick Instance.Instance Index` | Random Value (Int) | без него все точки получат одинаковый модуль | | `Random Value` rotation range | ±π по всем осям | ±π/6 по Z — чтобы модули выглядели «прикрученными» | | `ObjectInfo.Random → ColorRamp` | 2–3 стопа | больше стопов — больше разброса | | Масштаб формы | Плотность | Размер модулей | |---|---|---| | Панель 1×1 м | 20–40 | 2–8 см | | Стена здания 10×5 м | 3–6 | 10–40 см | | Корпус аппарата 3×2×2 м | 6–12 | 5–20 см | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Все точки получили один и тот же модуль | нет `Pick Instance` с рандомным индексом | добавить `Random Value` (Int) → `Pick Instance` | | Модули торчат из стены наполовину | origin не в основании | `origin_set` в нижнюю точку перед расстановкой | | Модули «плывут» при движении камеры | точки считаются в локальных координатах | проверить систему координат в группе и `Reset Children` | | Density не меняется после правки | сокет не тот, либо значение не применилось | проверить `Socket_N` в панели модификатора, сверить имена сокетов | | Материал на модулях чёрный | нет UV после расстановки | инстансы наследуют UV коробки — добавь `UV Unwrap` или `Store Named Attribute` | | Сцена встала при 60 модулях | инстансы не реализованы, движок считает каждый | `Realize Instances` только если правда нужно, иначе оставить инстансы | ## ✅ РЕЗУЛЬТАТ На выходе: одна коробка с одним модификатором Geometry Nodes, на гранях которой сидит библиотека модулей, и один материал с вариативностью по `Object Info → Random`. Плюс превью 1600×900 и GLB. Характеристики — **оценка по порядку величины, а не измеренный результат**: коробка 10×5×3 м при плотности 6 даёт порядка 900–1500 модулей; если каждый модуль — 200–600 треугольников, это от 0.2 до 0.9 млн треугольников **инстансами** (то есть один модуль в памяти, а не тысяча копий). После `Realize Instances` тот же объём распухает до десятков мегабайт в памяти. Рендер 1600×900 в Cycles, 128 сэмплов — минуты на средней видеокарте; в EEVEE — секунды. **Это оценка.** Реальное время рендера зависит от числа уникальных материалов, объёма инстансов и железа — измеряй у себя. Базовые сетапы: [SETUP 01: BLENDER MCP BASIC](../setups/setup_01_blender_mcp_basic.md) — транспорт команд в Blender, [SETUP 08: ASSET PIPELINE](../setups/setup_08_asset_pipeline.md) — общая схема работы с ассетами. ## 🔗 СВЯЗИ - **Прочие в категории «Воркфлоу»:** [WF 16: NPR ANIME RENDER](wf_16_npr_anime_render.md), [WF 18: ISOMETRIC GAME ART](wf_18_isometric_game_art.md), [WF 19: VOXEL ART PIPELINE](wf_19_voxel_art_pipeline.md), [WF 01: TEXT-TO-SCENE](wf_01_text_to_scene.md), [WF 02: IMAGE-TO-3D](wf_02_image_to_3d.md) - **По темам:** [CASE 08: SCI-FI CORRIDOR MODULAR](../cases/case_08_sci_fi_corridor.md), [CASE 09: ANIME HEAD BUST](../cases/case_09_anime_head_bust.md), [CASE 11: COZY ROOM DIORAMA](../cases/case_11_cozy_room_diorama.md) - **Теги:** #stylized, #workflow --- # WF 21: MOTION GRAPHICS TITLES Tags: #motion #workflow # WF 21: MOTION GRAPHICS TITLES **ID:** WF-21 | **Type:** MoGraph | **Difficulty:** ⭐⭐ **RU/EN Bilingual** > Когда применять: когда нужны заставки, логотипы, нижние трети и бегущие строки — текст и простые формы, которые живут 1–5 секунд и должны выглядеть отрендеренными, а не «дефолтными». ## 🎯 ЦЕЛЬ Текстовый объект превращается в анимированную заставку: движение по кривой, появление по буквам, затухание, зацикливание — и всё это рендерится с прозрачным фоном, чтобы накладываться поверх видео. Главная специфика моушн-графики по сравнению с 3D-сценой: **результат всегда временной**. Значит, главные настройки — это кадровая сетка (`fps`, длительность), тип интерполяции между ключами и то, чтобы первый и последний кадр совпали, если ролик зацикливается. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия / состояние | Зачем | |---|---|---| | Blender | версия с Curve Modifier и F-кривыми | основа | | Шрифт | `.ttf`/`.otf`, желательно с кириллицей | сам текст; системные шрифты выглядят случайно | | Кривая (Bezier) | создаётся в сцене | траектория движения | | Композитор | встроенный, Alpha Over | склейка слоёв и прозрачность | | VSE | встроенный | обрезка по таймингу, проверка цикла | | Стартовый файл кадров | — | экспорт анимации | > [!NOTE] > Имена сокетов нод compositor (в частности у `Alpha Over`) различаются между версиями: где-то входы называются `Image`, `Image.001`, где-то `Background`/`Foreground`. Ниже код использует порядок входов и явные имена с проверкой — если имя не нашлось, код упадёт с понятной ошибкой, а не сделает тихо не то. ## 🛠️ НАСТРОЙКА ### Этап 1. Кадровая сетка Всё дальше — в кадрах, не в секундах. Частая ошибка: анимировать в секундах при `fps = 24` и получать сдвиг на треть длительности. ```bash RU: "Моушн-графика: 24 fps, длительность 2 секунды (48 кадров), рендер с прозрачным фоном, кадры в //renders/titles/" EN: "Motion graphics: 24 fps, 2 s duration (48 frames), transparent film, frames to //renders/titles/" ``` ```python import bpy sc = bpy.context.scene FPS, SECONDS = 24, 2.0 sc.render.fps = FPS sc.frame_start = 1 sc.frame_end = int(FPS * SECONDS) # 48 кадров при 24 fps и 2 секундах sc.frame_set(sc.frame_start) for name, at in (("appear", 0.10), ("settle", 0.45), ("hold", 1.60), ("exit", 1.90)): sc.timeline_markers.new(name, frame=int(at * FPS)) print("кадров:", sc.frame_end, "маркеров:", len(sc.timeline_markers)) ``` ### Этап 2. Текст: объём, форма, ритм ```python import bpy bpy.ops.object.text_add(location=(0, 0, 0)) txt = bpy.context.active_object txt.name = "Title_Text" td = txt.data td.body = "BLENDER AI" td.align_x = 'CENTER' td.align_y = 'CENTER' td.size = 0.6 td.extrude = 0.03 # глубина букв td.bevel_depth = 0.006 # скругление кромки — ключ к «премиальному» виду td.bevel_resolution = 3 td.space_character = 1.05 # трекинг: 1.0 = по умолчанию шрифта td.resolution_u = 8 # больше = глаже кривые букв td.materials.append(bpy.data.materials["Mat_Title"]) ``` ### Этап 3. Движение по кривой Кривая + `Follow Path` — надёжнее, чем ключевые кадры на каждом объекте: траекторию можно править интерактивно, а текст остаётся одним объектом. ```python import bpy, math # дуга, по которой проезжает заголовок bpy.ops.curve.primitive_bezier_curve_add(radius=4.0, location=(0, 0, 0)) path = bpy.context.active_object path.name = "Path_Title" path.data.dimensions = '3D' con = txt.constraints.new('FOLLOW_PATH') con.target = path con.use_curve_follow = True # объект поворачивается по касательной, а не едет боком con.forward_axis = 'FORWARD_Y' con.up_axis = 'UP_Z' con.use_fixed_location = True # появляется на 15% пути, уезжает на 100% for frame, off in ((1, 0.0), (7, 0.15), (40, 0.92), (48, 1.0)): con.offset_factor = off con.keyframe_insert(data_path="offset_factor", frame=frame) ``` ### Этап 4. Появление по буквам Встроенного «выводить текст по буквам» у текстового объекта нет — текст появляется целиком. Два рабочих способа: дублировать по букве со сдвигом, либо обрезать кривую (Trim Curve в Geometry Nodes). Первый способ предсказуем и его видно на любой версии. ```python import bpy src = bpy.data.objects["Title_Text"] src_body = src.data.body # убираем оригинал, делаем по объекту на букву bpy.data.objects.remove(src, do_unlink=True) letters = [] for i, ch in enumerate(src_body): if ch == " ": continue bpy.ops.object.text_add(location=(0, 0, 0)) ob = bpy.context.active_object ob.name = f"Ch_{i:02d}_{ch}" ob.data.body = ch ob.data.align_x = 'CENTER' ob.data.align_y = 'CENTER' ob.data.size = 0.6 ob.data.extrude = 0.03 ob.data.materials.append(bpy.data.materials["Mat_Title"]) letters.append((i, ob)) STAGGER = 1.5 # кадров между буквами for i, ob in letters: start = 4 + i * STAGGER ob.location = (0, 0, -1.2) # из-под маски ob.keyframe_insert(data_path="location", frame=1) ob.location = (0, 0, 0.0) for f, v in ((start, -1.2), (start + 6, 0.0)): ob.location.z = v ob.keyframe_insert(data_path="location", frame=f) ``` > [!NOTE] > Более чистый вариант — одна кривая вместо 10 объектов, обрезанная нодой `Trim Curve` в Geometry Nodes с параметром Factor. Идентификатор ноды и набор входов (`Curve`, `Start`, `End`, режим `FACTOR`/`LENGTH`/`SPLINE`) менялись между версиями — проверь в поиске нод на своей версии. ### Этап 5. Интерполяция и easing Дефолтный Bezier даёт «резиновое» движение. Для заставок обычно хотят резкий вход (back/out) и мягкое торможение. ```python import bpy act = bpy.data.objects["Title_Text"].animation_data.action # В Blender 4.4+ действия стали слотамистыми: act.fcurves может быть пустым, # тогда F-кривые лежат в act.layers[..].strips[..].channelbag(slot).fcurves. fcurves = list(getattr(act, "fcurves", [])) if not fcurves and hasattr(act, "layers"): slot = bpy.data.objects["Title_Text"].animation_data.action_slot for layer in act.layers: for strip in layer.strips: cb = strip.channelbag(slot) if cb: fcurves.extend(cb.fcurves) for fc in fcurves: for kp in fc.keyframe_points: kp.interpolation = 'BEZIER' kp.easing = 'EASE_OUT' kp.handle_left_type = kp.handle_right_type = 'AUTO_CLAMPED' # без перелётов fc.update() ``` > [!NOTE] > Значения `kp.easing` и набор `easing_property` зависят от версии: список (AUTO, EASE_IN, EASE_OUT, EASE_IN_OUT, LINEAR, CONSTANT, QUAD, CUBIC, BACK, BOUNCE, ELASTIC, SINE…) приводится по одной версии и менялся. Если поле называется иначе — открой F-Curve в редакторе и выбери руками. ### Этап 6. Склейка и прозрачный вывод ```python import bpy sc = bpy.context.scene sc.render.film_transparent = True # альфа-канал в PNG 16-bit или EXR sc.use_nodes = True nt = sc.node_tree for n in list(nt.nodes): nt.nodes.remove(n) rl = nt.nodes.new("CompositorNodeRLayers") ao = nt.nodes.new("CompositorNodeAlphaOver") comp = nt.nodes.new("CompositorNodeComposite") # входы Alpha Over: [0]=Fac, [1]=нижний слой, [2]=верхний слой ao.inputs[0].default_value = 1.0 nt.links.new(rl.outputs["Image"], ao.inputs[1]) nt.links.new(rl.outputs["Alpha"], ao.inputs[2]) nt.links.new(ao.outputs["Image"], comp.inputs["Image"]) ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `render.fps` | 24 / 25 / 30 | под требования площадки | | `frame_end` | `fps × секунды` | длина ролика | | `bevel_depth` | 0.004–0.01 от `size` | больше — «резиновый» пластик, меньше — плоская печать | | `space_character` | 0.95–1.15 | трекинг: ниже 0.9 текст слипается, выше 1.3 разваливается | | `Follow Path.offset_factor` | 0 → 1 | 1 = конец пути, а не «длина» | | `use_curve_follow` | `True` | объект доворачивается по касательной | | `interpolation` | `'BEZIER'` | `'LINEAR'` для механических проездов, `'CONSTANT'` для ступеней | | `film_transparent` | `True` | для наложения поверх видео | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Текст целиком, без появления по буквам | у Text-объекта нет пер-буквенной анимации | дублировать по буквам со сдвигом либо Trim Curve в GN | | Анимация «на полкадра позади» | считали секунды при `fps=24` | пересчитать всё в кадры: `frames = seconds × fps` | | Движение с перелётом, значение улетает за пределы | `AUTO_CLAMPED` не задан | `handle_left_type = handle_right_type = 'AUTO_CLAMPED'` | | `action.fcurves` пуст | версия 4.4+ со слотистыми действиями | брать F-кривые из `act.layers[..].strips[..].channelbag(slot)` | | Цикл дёргается на стыке | кадр 1 и кадр N не совпадают | сделать `frame_end = N`, а последний кадр сдвинуть на один назад, либо F-кривая должна иметь период ровно `N-1` | | При переносе в монтажку фон чёрный | `film_transparent` выключен | включить и сохранять PNG 16-bit / EXR с альфой | | Шрифт выглядит «не тем» | системный шрифт без нужного начертания | подключить свой `.ttf` в `Preferences → File Paths → Fonts` | ## ✅ РЕЗУЛЬТАТ На выходе: 48 кадров 1920×1080 (или под требуемое соотношение) с прозрачным фоном — PNG 16-bit с альфой либо EXR, плюс `.blend` с одним текстовым объектом (или набором букв) и кривой-траекторией. Характеристики — **оценка по порядку величины**: сцена из 10 текстовых объектов с экструзией и bevel — единицы тысяч треугольников, один материал, ноль текстур. Рендер 48 кадров 1920×1080 в Cycles на 64 сэмплах — секунды на кадр, то есть десятки секунд на весь ролик; в EEVEE — доли секунды на кадр. Объём PNG 16-bit с альфой — десятки мегабайт на 48 кадров. **Это оценка, а не измеренный результат.** Реальное время зависит от разрешения, сэмплов, наличия DOF/motion blur и железа. Движение (motion blur) включается в рендере, но конкретные свойства blur в разных версиях лежат в разных местах — проверь в `Render Properties` своей версии, а не по этому тексту. Базовые сетапы: [SETUP 01: BLENDER MCP BASIC](../setups/setup_01_blender_mcp_basic.md) — транспорт команд в Blender, [SETUP 08: ASSET PIPELINE](../setups/setup_08_asset_pipeline.md) — общая схема работы с ассетами. ## 🔗 СВЯЗИ - **Прочие в категории «Воркфлоу»:** [WF 28: LOCALIZATION TITLES](wf_28_localization_dub_titles.md), [WF 01: TEXT-TO-SCENE](wf_01_text_to_scene.md), [WF 02: IMAGE-TO-3D](wf_02_image_to_3d.md), [WF 03: TEXTURE PIPELINE](wf_03_texture_pipeline.md), [WF 04: CHARACTER ANIMATION](wf_04_character_animation.md) - **По темам:** [WF 05: PROCEDURAL ASSETS](wf_05_procedural_assets.md), [WF 06: ARCHVIZ WORKFLOW](wf_06_archviz_workflow.md), [WF 07: GAME ASSET PIPELINE](wf_07_game_asset_pipeline.md) - **Теги:** #motion, #workflow --- # WF 22: DATAVIZ 3D CHARTS Tags: #dataviz #workflow # WF 22: DATAVIZ 3D CHARTS **ID:** WF-22 | **Type:** Info | **Difficulty:** ⭐⭐ **RU/EN Bilingual** > Когда применять: когда заказчик просит «красивые 3D-столбики» из CSV: демо-график, презентационный ролик, экран для лобби. Сознание о том, что 3D искажает сравнение длин, здесь — часть работы, а не формальность. ## 🎯 ЦЕЛЬ Из CSV получается 3D-столбчатая диаграмма: столбцы по одной на строку, высота пропорциональна значению, цвет кодирует ту же величину, снизу подписи категорий, камера делает медленный облёт. Первое, что нужно сказать честно: **3D-график хуже читается, чем плоский**. Перспектива сокращает дальние столбцы, и глаз сравнивает не длину, а площадь и положение. Если 3D обязателен (обычно это требование маркетинга, а не аналитика), компенсируй: ортогональная камера убирает перспективное сокращение, монотонная светлота палитры даёт порядок, а числовые подписи снимают задачу сравнения с глазу. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия / состояние | Зачем | |---|---|---| | Blender | базовый | сцена и рендер | | CSV-файл | разделитель `;` или `,`, кодировка UTF-8 | данные | | Модуль `csv` | входит в Python внутри Blender | разбор без внешних пакетов | | Модуль `numpy` | входит в поставку Blender, но в коде ниже не нужен | — | | Шрифт для подписей | встроенный Bfont достаточно | читаемость | > [!NOTE] > Не используй pandas: его нет в стандартной поставке Blender, и если он не установлен через pip в интерпретатор Blender, скрипт упадёт. `csv`, `json`, `math`, `random` — стандартные, работают всегда. ## 🛠️ НАСТРОЙКА ### Этап 1. Прочитать данные ```bash RU: "Построй 3D-столбчатую диаграмму из //data/sales.csv: столбцы по месяцам, высота = выручка, цвет по тем же значениям, подписи снизу, камера — ортогональная, медленный облёт" EN: "Build a 3D bar chart from //data/sales.csv: one bar per month, height = revenue, color by the same value, labels below, camera orthographic, slow orbit" ``` ```python import csv, os path = bpy.path.abspath("//data/sales.csv") rows = [] with open(path, newline='', encoding='utf-8') as fh: for r in csv.DictReader(fh, delimiter=','): rows.append((r['month'].strip(), float(r['revenue'].replace(' ', '')))) if not rows: raise SystemExit("CSV пуст или нет заголовков month/revenue") values = [v for _, v in rows] vmin, vmax = min(values), max(values) print(f"строк: {len(rows)}, диапазон: {vmin:.0f}..{vmax:.0f}") ``` ### Этап 2. Построить столбцы Столбцы строятся из одного «прототипа», который затем множится — так дешевле, чем создавать каждый отдельно, и вся группа остаётся одной сущностью. ```python import bpy, math from mathutils import Vector BAR_W, BAR_D, GAP, MAX_H = 0.6, 0.6, 0.25, 4.0 values_max = max(values) or 1.0 def box_mesh(w, d, h, cx, cy): """Единичный параллелепипед с центром в основании — так проще масштабировать по Y.""" hw, hd = w / 2, d / 2 v = [(-hw, -hd, 0), (hw, -hd, 0), (hw, hd, 0), (-hw, hd, 0), (-hw, -hd, h), (hw, -hd, h), (hw, hd, h), (-hw, hd, h)] f = [(0,3,2,1), (4,5,6,7), (0,1,5,4), (1,2,6,5), (2,3,7,6), (3,0,4,7)] me = bpy.data.meshes.new("BarMesh") me.from_pydata(v, [], f) me.update() return me bars = [] x = 0.0 for i, (label, value) in enumerate(rows): h = MAX_H * value / values_max ob = bpy.data.objects.new(f"Bar_{i:02d}_{label}", box_mesh(BAR_W, BAR_D, h, x, 0)) ob.location = (x, 0, 0) bpy.context.collection.objects.link(ob) # offset в custom property -> Object Info Random в шейдере (см. этап 3) ob["v_norm"] = (value - vmin) / (vmax - vmin) if vmax > vmin else 0.5 bars.append((ob, label, value)) x += BAR_W + GAP bpy.ops.object.select_all(action='DESELECT') for ob, _, _ in bars: ob.select_set(True) bpy.context.view_layer.objects.active = bars[0][0] bpy.ops.object.join() # все столбцы — один объект, но с сохранёнными пропорциями ``` > [!NOTE] > После `join()` все столбцы — одна геометрия, и `Object Info → Random` вернёт одно и то же число на весь объект. Поэтому цвет по значению надо раздавать **до** join: крась по вершинам (attribute), либо вообще не join'ить. Здесь join сделан только ради удобства трансформации — если нужен цвет по значению, оставь столбцы отдельными объектами. ### Этап 3. Цвет по значению Через `Object Info → Random` столбцы красятся сами: `Random` у каждого объекта стабильное число 0..1, ColorRamp с монотонной светлотой даёт читаемый порядок. ```python import bpy mat = bpy.data.materials.new("Mat_Data") mat.use_nodes = True nt = mat.node_tree bsdf = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED') bsdf.inputs['Roughness'].default_value = 0.45 oi = nt.nodes.new("ShaderNodeObjectInfo") ramp = nt.nodes.new("ShaderNodeValToRGB") cr = ramp.color_ramp cr.interpolation = 'LINEAR' cr.elements[0].position = 0.0 cr.elements[0].color = (0.05, 0.16, 0.30, 1) # тёмно-синий: мало cr.elements[1].position = 1.0 cr.elements[1].color = (0.95, 0.55, 0.18, 1) # оранжевый: много # средний стоп светлее обоих краёв -> порядок читается и по светлоте mid = cr.elements.new(0.5); mid.color = (0.30, 0.52, 0.58, 1) nt.links.new(oi.outputs["Random"], ramp.inputs["Fac"]) nt.links.new(ramp.outputs["Color"], bsdf.inputs["Base Color"]) for ob, _, _ in bars: ob.data.materials.append(mat) ``` Если всё-таки нужен цвет при join'е — пиши в вершинный атрибут до объединения: ```python # по нижней грани столбца читаем z его вершины -> нормированное значение for ob, label, value in bars: t = (value - vmin) / (vmax - vmin) if vmax > vmin else 0.5 attr = ob.data.attributes.new(name="val", type='FLOAT', domain='POINT') for d in attr.data: d.value = t # в шейдере вместо ObjectInfo.Random: Attribute(attribute_name="val") ``` ### Этап 4. Подписи категорий Подписи всегда ставятся в плоскости кадры и всегда с числами: если график 3D, читатель обязан иметь возможность сверить глазом. ```python import bpy cam = bpy.data.objects["Cam_Chart"] labels = [] for i, (ob, label, value) in enumerate(bars): bpy.ops.object.text_add() t = bpy.context.active_object t.name = f"Lbl_{label}" t.data.body = f"{label}\n{value:,.0f}".replace(",", " ") # разделитель тысяч принято ставить пробелом t.data.align_x = 'CENTER' t.data.size = 0.22 t.data.extrude = 0.0 t.location = (ob.location.x, -0.75, 0.0) c = t.constraints.new('TRACK_TO') c.target = cam; c.track_axis = 'TRACK_NEGATIVE_Z'; c.up_axis = 'UP_Y' labels.append(t) ``` ### Этап 5. Ось и сетка ```python import bpy axis_mat = bpy.data.materials.new("Mat_Axis") axis_mat.use_nodes = True nt = axis_mat.node_tree b = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED') b.inputs['Base Color'].default_value = (0.12, 0.12, 0.14, 1) b.inputs['Roughness'].default_value = 0.7 extent = (len(bars) * (BAR_W + GAP)) + 1.0 for name, loc, dims in (("Axis_X", (extent/2 - GAP/2, -0.45, 0), (extent, 0.02, 0.02)), ("Axis_Y", (extent/2 - GAP/2, 0.0, 0), (extent, 0.02, 0.02))): bpy.ops.mesh.primitive_cube_add(size=1, location=loc) o = bpy.context.active_object; o.name = name o.scale = (dims[0], dims[1], dims[2]) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) o.data.materials.append(axis_mat) ``` ### Этап 6. Камера и облёт Для 3D-графика ортогональная камера — не упрощение, а условие читаемости: она убирает перспективное сокращение, из-за которого дальние столбцы кажутся короче. ```python import bpy, math bpy.ops.object.camera_add(location=(0, -14, 5.2), rotation=(math.radians(68), 0, 0)) cam = bpy.context.active_object; cam.name = "Cam_Chart" cam.data.type = 'ORTHO' cam.data.ortho_scale = len(bars) * (BAR_W + GAP) + 1.5 bpy.context.scene.camera = cam # медленный облёт: дуга в 40 кадров и обратно bpy.ops.curve.primitive_bezier_curve_add(location=(0, -14, 5.2), radius=8.0) path = bpy.context.active_object for p in path.data.splines[0].bezier_points: p.handle_left_type = p.handle_right_type = 'AUTO' con = cam.constraints.new('FOLLOW_PATH') con.target = path con.use_fixed_location = True for frame, off in ((1, 0.0), (40, 0.5), (80, 0.0)): con.offset_factor = off con.keyframe_insert(data_path="offset_factor", frame=frame) sc = bpy.context.scene sc.frame_start, sc.frame_end, sc.frame_set = 1, 80, 1 ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `camera.data.type` | `'ORTHO'` для диаграмм | перспектива допустима только для «рекламного» кадра | | `ortho_scale` | ширина графика + запас | подгоняется под число столбцов | | `ColorRamp` палитра | монотонная светлота | цветом кодируется порядок, а не категория | | `MAX_H` | 3–5 м | при коротких столбцах подписи наезжают | | `GAP` | 0.2–0.35 от ширины столбца | меньше — сливается, больше — теряется связь | | `view_transform` | `'Standard'` | для графика: AgX/Filmic сжимают контраст и врут в контрасте палитры | > [!NOTE] > Список значений `view_settings.view_transform` зависит от версии и включённых OCIO-конфигов: встречаются `Standard`, `Filmic`, `AgX`, `Raw`, `False Color`. Прочитай доступные у себя так: `[i.identifier for i in scene.view_settings.bl_rna.properties["view_transform"].enum_items]`. ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Все столбцы одного цвета | `join()` слил столбцы, `Object Info → Random` дал одно число | красить до join либо использовать вершинный атрибут | | CSV читается, но `KeyError: 'month'` | разделитель `;` при `delimiter=','` | подобрать `delimiter` под файл | | `OverflowError` при `float()` | в значении пробелы, запятые или неразрывный пробел | чистить строку (`replace`, `strip`) до `float` | | Дальние столбцы «короче» | перспективная камера | `ortho` + увеличение ракурса сверху | | Подписи не читаются | они не в фокальной плоскости и не в кадре | `TRACK_TO` на камеру, вынести вперёд по Y | | Цвета «не тем» на финальном кадре | `AgX`/`Filmic` сжали контраст | для проверки палитры рендерить с `view_transform='Standard'` | ## ✅ РЕЗУЛЬТАТ На выходе: сцена со столбцами, осями и подписями, плюс 80 кадров 1920×1080 облёта (рендер одного кадра — основной результат; 80 кадров нужны только если делается ролик). Характеристики — **оценка по порядку величины, а не измеренный результат**: 12 столбцов по 12 треугольников на столбец — единицы сотен треугольников на всю диаграмму, один материал столбцов + один материал осей, ноль текстур (подписи — геометрия текста, а не текстура). Один кадр 1920×1080 в Cycles, 64 сэмпла — секунды; в EEVEE — доли секунды. Восемьдесят кадров — от десятков секунд до нескольких минут. **Это оценка.** Точное время зависит от числа объектов в сцене и железа; измеряй у себя. Базовые сетапы: [SETUP 01: BLENDER MCP BASIC](../setups/setup_01_blender_mcp_basic.md) — транспорт команд в Blender, [SETUP 08: ASSET PIPELINE](../setups/setup_08_asset_pipeline.md) — общая схема работы с ассетами. ## 🔗 СВЯЗИ - **Прочие в категории «Воркфлоу»:** [WF 01: TEXT-TO-SCENE](wf_01_text_to_scene.md), [WF 02: IMAGE-TO-3D](wf_02_image_to_3d.md), [WF 03: TEXTURE PIPELINE](wf_03_texture_pipeline.md), [WF 04: CHARACTER ANIMATION](wf_04_character_animation.md), [WF 05: PROCEDURAL ASSETS](wf_05_procedural_assets.md) - **По темам:** [WF 06: ARCHVIZ WORKFLOW](wf_06_archviz_workflow.md), [WF 07: GAME ASSET PIPELINE](wf_07_game_asset_pipeline.md), [WF 08: BATCH PROCESSING](wf_08_batch_processing.md) - **Теги:** #dataviz, #workflow --- # WF 23: VIRTUAL PRODUCTION LED Tags: #animation #pipeline #render #texturing #workflow # WF 23: VIRTUAL PRODUCTION LED **ID:** WF-23 | **Type:** VP | **Difficulty:** ⭐⭐⭐ **RU/EN Bilingual** > Когда применять: когда снимают в павильоне с LED-стеной: нужно, чтобы 3D-сцена реально светила на актёров, отражалась в их глазах и совпадала с траекторией камеры. ## 🎯 ЦЕЛЬ Blender в виртуальном продакшене играет **две разные роли**, и их путают чаще всего: 1. **Источник контента для стены.** Рендер с «виртуальной» камеры, отправленный на видеопроцессор LED-стены. Здесь важны: UV-развёртка 1:1, эмиссивный материал без засветки сцены, скорость кадра. 2. **Движок финального кадра.** Рендер с реальной камеры площадки, где стена уже отрендерена и вставлена композитом. Здесь важны: траектория камеры, тени от актёров, «shadow catcher», чистый альфа-кадр без стены. Статья про оба режима. Физическое железо (стена, видеопроцессор, трекер, genlock) — вне Blender, поэтому здесь только то, что делается в сцене. > [!NOTE] > nDisplay и подобные системы виртуального продакшена — закрытые технологии с плагинами под конкретную версию Blender. Имена нод, панели и требуемые настройки у них свои и меняются; здесь описан общий принцип «камера + эмиссивная стена + трек», который работает с любым из этих решений и с самодельной связкой из нескольких инстансов и видеомикшера. Точные шаги настройки nDisplay бери из документации NVIDIA под свою версию плагина, не отсюда. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия / состояние | Зачем | |---|---|---| | Blender с Cycles | шейдер `Light Path` обязателен | разделение «камера / отражение / засветка» | | LED-стена | разрешение, напр. 2560×1360 | определяет шаг пикселя и требования к рендеру | | Видеопроцессор стены | — | принимает кадры/поток от Blender | | Система трекинга камеры | оптический трекер или моушен-капчер | траектория реальной камеры в сцене | | HDRI площадки | с реальными приборами | чтобы свет стены совпадал с освещением павильона | ## 🛠️ НАСТРОЙКА ### Этап 1. Геометрия стены и развёртка 1:1 Стена — это не «декорация», а эмиттер с заданным числом пикселей. Развёртка обязана быть пиксель-в-пиксель: если в 2560 пикселях по горизонтали приходится 2561 UV-координата, изображение смазывается на швах. ```bash RU: "LED-стена 2560x1360 пикселей, шаг пикселя 2.6 мм => плоскость 6.656 x 3.536 м. Создай плоскость 'LED_Wall' ровно такого размера, UV 1:1 без Scale и Crop, назначь эмиссивный материал 'Mat_LED'" EN: "LED wall 2560x1360 px, 2.6 mm pitch => plane 6.656 x 3.536 m. Create plane 'LED_Wall' exactly that size, UV 1:1 with no Scale/Crop, assign emissive material 'Mat_LED'" ``` ```python import bpy W_PX, H_PX, PITCH = 2560, 1360, 0.0026 # разрешение стены и шаг пикселя в метрах wall_w, wall_h = W_PX * PITCH, H_PX * PITCH # 6.656 x 3.536 м print(f"геометрия стены: {wall_w:.3f} x {wall_h:.3f} м, " f"{wall_w/wall_h:.4f} против {W_PX/H_PX:.4f} — должно совпасть") bpy.ops.mesh.primitive_plane_add(size=1, location=(0, -4.0, 1.75)) wall = bpy.context.active_object wall.name = "LED_Wall" wall.scale = (wall_w, wall_h, 1.0) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) uv = wall.data.uv_layers.new(name="UVMap") # ровно одна UV-карта, 0..1, без перекрытий for poly in wall.data.polygons: for li in poly.loop_indices: co = wall.data.vertices[wall.data.loops[li].vertex_index].co uv.data[li].uv = (co.x / wall_w + 0.5, co.y / wall_h + 0.5) ``` ### Этап 2. Материал: светит в камеру, но не засвечивает сцену Ключевой приём — разделение по типу луча через `Light Path`. В камеру стена выглядит как источник изображения; в отражениях она даёт слабый диффузный отклик; в диффузном освещении сцены она почти не участвует, иначе свет «из стены» заливает павильон. ```python import bpy mat = bpy.data.materials.new("Mat_LED") mat.use_nodes = True nt = mat.node_tree out = next(n for n in nt.nodes if n.type == 'OUTPUT_MATERIAL') em = nt.nodes.new("ShaderNodeEmission") lp = nt.nodes.new("ShaderNodeLightPath") tex = nt.nodes.new("ShaderNodeTexImage") mix = nt.nodes.new("ShaderNodeMixShader") glow = nt.nodes.new("ShaderNodeEmission") # «диффузный» отклик для отражений tex.image = bpy.data.images.load(r"//renders/wall_0001.png") tex.extension = 'EXTEND' em.inputs['Strength'].default_value = 3.0 # пересвеченность стены в кадре glow.inputs['Color'].default_value = (0.05, 0.06, 0.08, 1) glow.inputs['Strength'].default_value = 0.25 # насколько «виден» экран в глазах актёра # Mix Shader: Is Camera Ray = 1 -> верхний сокет (изображение), иначе нижний (glow) nt.links.new(lp.outputs['Is Camera Ray'], mix.inputs[0]) nt.links.new(tex.outputs['Color'], em.inputs['Color']) nt.links.new(glow.outputs['Emission'], mix.inputs[2]) nt.links.new(mix.outputs['Shader'], out.inputs['Surface']) wall.data.materials.append(mat) ``` ### Этап 3. Стены не должно быть в финальном кадре Для финального рендера стена — источник света, но не видимая геометрия. Иначе получишь изображение стены поверх записанного видео. ```python import bpy wall = bpy.data.objects["LED_Wall"] # в финальном кадре стена не рисуется, но продолжает освещать wall.visible_camera = False # не видно в primary rays wall.visible_glossy = True # отражения в глазах/на одежде оставляем # пол в павильоне — shadow catcher: принимает тень, не рисуется сам bpy.ops.mesh.primitive_plane_add(size=30, location=(0, 0, 0)) floor = bpy.context.active_object; floor.name = "Stage_Floor" floor.is_shadow_catcher = True ``` > [!NOTE] > Флаги видимости по типам лучей и `is_shadow_catcher` относятся к Cycles и переезжали между версиями (в некоторых имена менялись). Надёжно проверить наличие у себя так: `hasattr(bpy.types.Object, "visible_camera")` и `hasattr(bpy.types.Object, "is_shadow_catcher")`. Если поля нет — см. `Object Properties → Visibility → Ray Visibility` в своей версии. ### Этап 4. Трекинг реальной камеры Трекер отдаёт последовательность положений и ориентаций камеры. Большинство систем умеют экспортировать текст/CSV; в Blender Camera Motion есть аддон-модуль Camera Solver, который умеет решать такое прямо в VSE. ```bash RU: "Импортируй solves камеры из //trk/cam_solve.txt в VSE как полосу Movie, запусти Camera Motion -> Camera Motion, примени solve к камере Cam_OnStage" EN: "Import camera solves from //trk/cam_solve.txt as a Movie strip in the VSE, run Camera Motion -> Camera Motion, apply the solve to Cam_OnStage" ``` ```python import bpy, math from mathutils import Euler, Vector cam = bpy.data.objects["Cam_OnStage"] keys = [] with open(bpy.path.abspath("//trk/cam_solve.txt"), encoding="utf-8") as fh: for line in fh: if not line.strip() or line.lstrip().startswith("#"): continue p = line.split() if len(p) < 6: continue keys.append((float(p[0]), Vector(map(float, p[1:4])), Euler(tuple(map(math.radians, map(float, p[4:7]))), 'XYZ').to_quaternion())) if not keys: raise SystemExit("не разобрано ни одной строки — проверь формат файла solves") # Blender всегда едет с 0 по возрастанию, а таймкод трекера может начинаться с чего угодно keys.sort(key=lambda k: k[0]) sc = bpy.context.scene t0 = int(keys[0][0]) cam.rotation_mode = 'QUATERNION' for i, (tc, loc, rot) in enumerate(keys): frame = sc.frame_start + i cam.location = loc cam.quaternion = rot cam.keyframe_insert("location", frame=frame) cam.keyframe_insert("quaternion", frame=frame) sc.frame_start = sc.frame_start sc.frame_end = sc.frame_start + len(keys) - 1 print(f"решено кадров: {len(keys)}, таймкод трекера начинался с {t0}, " f"в сцене: {sc.frame_start}..{sc.frame_end}") # камера должна попасть в кадр cam.data.clip_start = 0.1 cam.data.clip_end = 100.0 cam.data.dof.use_dof = False # трекер не даёт дистанцию: DOF по фокусу сработает неверно ``` ### Этап 5. Два режима рендера в одной сцене ```python import bpy sc = bpy.context.scene sc.render.engine = 'CYCLES' sc.cycles.samples = 32 # для стены хватает немного: картинка плоская sc.cycles.use_denoising = True # режим 1: контент стены — отдельная камера, отдельный файл, свой набор объектов sc.camera = bpy.data.objects["Cam_OnWall"] sc.render.resolution_x, sc.render.resolution_y = 2560, 1360 sc.render.image_settings.file_format = 'PNG' sc.render.image_settings.color_mode = 'RGB' sc.render.filepath = r"//led/frame_" sc.frame_start, sc.frame_end = 1, 600 bpy.ops.render.render(animation=True) # 600 кадров — чаще всего очередью, не вручную # режим 2: финальный кадр — камера площадки, стена выключена, вывод с альфой sc.camera = bpy.data.objects["Cam_OnStage"] sc.render.film_transparent = True sc.render.image_settings.color_mode = 'RGBA' sc.render.filepath = r"//final/plate_" ``` > [!NOTE] > Genlock (синхронизация по внешнему тактовому сигналу) и реальный вывод в стену — это уровень сервера рендера и видеопроцессора, а не Blender. В Blender нет функции «принимать внешний синхроимпульс». Настройкой частоты кадров в сцене (`fps`) и перезапуском рендера по таймеру управляют снаружи. Протоколы и конкретные утилиты бери из документации твоего оборудования. ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | Геометрия стены | `px × pitch` по каждой оси | всегда; расхождение с соотношением пикселей даёт «резиновую» картинку | | `Emission.Strength` | 2–4 | яркость стены для камеры | | `Light Path.Is Camera Ray` | ключ ко всему | без него стена засвечивает сцену | | `visible_camera = False` | для финального кадра | иначе стена попадёт в кадр поверх видео | | `is_shadow_catcher` | на полу павильона | тени от актёров без рисования пола | | `fps` сцены | ровно тот же, что у трекера и камеры | расхождение = рассинхрон нарастающий по времени | | `samples` | 16–64 для контента стены | стена почти не имеет шума | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Швы и «мыло» на стене | UV не 1:1, растянут Aspect Ratio | перегенерировать UV ровно по формуле `px × pitch` | | Сцена засвечена, контраст «поплыл» | стена светит во все стороны | `Light Path` + разделение по лучам, понизить `glow` | | Синхрон уходит к концу съёмки | `fps` сцены не равен `fps` камеры/трекера | выставить одинаково, кадр-в-кадр | | Камера «прыгает» в моменты проходов | solves с дырами, нет интерполяции | интерполировать ключи, дублировать соседние при пропусках | | DOF едет не туда | трекер не даёт дистанцию до объекта | фокус ставить вручную или по маркеру, `use_dof` гасить на превью | | Финальный кадр с «двойной стеной» | стена не выключена из камеры | `visible_camera = False` для финального прохода | | Цвет стены и реальный свет не совпадают | HDRI павильона не соответствует приборам | снять HDRI на пустой площадке и использовать её | ## ✅ РЕЗУЛЬТАТ На выходе: сцена с эмиссивной стеной, одной камерой «на стену» и одной камерой «на площадке», плюс две последовательности: контент стены (RGB PNG, 2560×1360, 600 кадров) и финальные плашки с альфой (RGBA, 1600×900 или выше). Характеристики — **оценка по порядку величины, а не измеренный результат**: 600 кадров 2560×1360 в Cycles на 32 сэмплах с одной плоскостью-эмиттером — от долей секунды до нескольких секунд на кадр на дискретной видеокарте, то есть от единиц минут до десятков минут на всю смену. Реальный стенд с полноценной сценой в 100–300 тыс. полигонов идёт в десятки раз дольше. Объём: один кадр контента стены — единицы мегабайт в PNG; 600 кадров — сотни мегабайт. **Это оценка.** В реальном продакшене сцену рендерят не в Cycles в одиночку, а распределённо, и скорость определяется узким местом конвейера, а не Blender. Замеряй на своём железе. Базовые сетапы: [SETUP 01: BLENDER MCP BASIC](../setups/setup_01_blender_mcp_basic.md) — транспорт команд в Blender, [SETUP 08: ASSET PIPELINE](../setups/setup_08_asset_pipeline.md) — общая схема работы с ассетами. ## 🔗 СВЯЗИ - **Прочие в категории «Воркфлоу»:** [WF 17: STOP-MOTION FELT LOOK](wf_17_stop_motion_felt.md), [WF 24: PHOTOGRAMMETRY CLEANUP](wf_24_photogrammetry_cleanup.md), [WF 01: TEXT-TO-SCENE](wf_01_text_to_scene.md), [WF 03: TEXTURE PIPELINE](wf_03_texture_pipeline.md), [WF 02: IMAGE-TO-3D](wf_02_image_to_3d.md) - **По темам:** [CASE 03: PRODUCT VISUALIZATION](../cases/case_03_product_viz.md), [CASE 05: PROCEDURAL CITY](../cases/case_05_procedural_city.md), [CASE 07: STYLIZED BARREL (HAND-PAINT)](../cases/case_07_stylized_barrel.md), [CASE 08: SCI-FI CORRIDOR MODULAR](../cases/case_08_sci_fi_corridor.md) - **Теги:** #animation, #pipeline, #render, #texturing, #workflow --- # WF 24: PHOTOGRAMMETRY CLEANUP Tags: #animation #archviz #photogrammetry #pipeline #render #texturing #workflow # WF 24: PHOTOGRAMMETRY CLEANUP **ID:** WF-24 | **Type:** Scan | **Difficulty:** ⭐⭐ **RU/EN Bilingual** > Когда применять: когда приехал скан — с реального объекта или из открытой библиотеки — и его надо превратить в ассет, который можно анимировать, поставить в сцену и отрендерить. ## 🎯 ЦЕЛЬ Три миллиона полигонов из фотограмметрии превращаются в 20–60 тысяч чистых квадов с одной запечённой текстурой 2k, тремя уровнями детализации и готовым экспортом. Логика та же, что в любом ретопо: **разделить форму и материал**. Форма живёт в низкополигональной геометрии, детали поверхности (надписи, сколы, патина) живут в текстуре, запечённой с исходного скана. Это единственный способ сохранить детализацию фотограмметрии, не таская её миллион полигонов в сцену. > [!NOTE] > Фотограмметрию (фото → 3D) в Blender не делают: это работа сторонних программ — RealityCapture и Agisoft Metashape, у которых свои экспортные опции и свои форматы (`.obj`, `.ply`, `.fbx`). Blender умеет импортировать OBJ и PLY штатно; FBX зависит от того, включён ли аддон. Точные параметры экспорта смотри в документации той программы, которой снимаешь. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия / состояние | Зачем | |---|---|---| | Blender с Cycles | запекание текстур — только в Cycles | bake карт | | RealityCapture или Agisoft Metashape | сторонняя программа | сама фотограмметрия | | Quad Remesher или другой ремешер quads | аддон, платный/сторонний | равномерная квадратная топология | | Масштабная линейка в кадре | — | реальный размер скана | | Папка `//tex/` под карты | — | base color, normal, AO | > [!NOTE] > Встроенного в Blender «ремешера квадами с равной длиной ребра» нет. `Decimate` в режиме `COLLAPSE` даёт нерегулярную сетку — она годится для LOD, но не для анимации. Если нужен ровный quad-flow, нужен аддон (например Quad Remesher) либо отдельная программа ретопологии. Ровный квад — обязательное условие для скиннинга и для нормалей, сглаженных по углу. ## 🛠️ НАСТРОЙКА ### Этап 1. Импорт и приведение к реальному размеру ```bash RU: "Импортируй //scan/statue.obj как 'Scan_Hi', переведи в метры по линейке 1.42 м, поставь нижнюю точку в Z=0, удали дублирующие вершины и выверни нормали" EN: "Import //scan/statue.obj as 'Scan_Hi', scale to metres using a 1.42 m tape reference, put the base at Z=0, remove duplicate verts and recalculate normals" ``` ```python import bpy, bmesh from mathutils import Vector scan = bpy.data.objects["Scan_Hi"] # приводим к метрам по референсу из съёмки MEASURED_M = 1.42 current_dz = max(v.co.z for v in scan.data.vertices) - min(v.co.z for v in scan.data.vertices) scan.scale = [MEASURED_M / current_dz] * 3 bpy.ops.object.select_all(action='DESELECT') scan.select_set(True); bpy.context.view_layer.objects.active = scan bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) zs = [(scan.matrix_world @ v.co).z for v in scan.data.vertices] scan.location.z -= min(zs) # основание в нуле # чистка меша: дубли, вырожденные ребра, нормали bm = bmesh.new(); bm.from_mesh(scan.data) bmesh.ops.remove_doubles(bm, verts=bm.verts[:], dist=1e-5) bmesh.ops.dissolve_degenerate(bm, dist=1e-5, edges=bm.edges[:]) loose = [v for v in bm.verts if not v.link_faces] bmesh.ops.delete(bm, geom=loose, context='VERTS') bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:]) bm.to_mesh(scan.data); bm.free() scan.data.update() print("после чистки:", len(scan.data.polygons), "полигонов") ``` ### Этап 2. Базовый LOD сразу Скан стоит упростить **до** ретопологии: ретопо на 3 миллионах полигонов занимает часы, на 200 тысячах — минуты. ```python import bpy def make_lod(src, ratio, name): """Копия объекта с упрощением; детализация падает, силуэт сохраняется.""" dup = src.copy() # копия сохраняет и данные меша dup.data = src.data.copy() # иначе все LOD делят один меш dup.name = name bpy.context.collection.objects.link(dup) m = dup.modifiers.new("Decimate", 'DECIMATE') m.decimate_type = 'COLLAPSE' m.ratio = ratio bpy.ops.object.select_all(action='DESELECT') dup.select_set(True); bpy.context.view_layer.objects.active = dup bpy.ops.object.modifier_apply(modifier=m.name) return dup lod0 = make_lod(scan, 0.10, "Scan_LOD0") # ~100 тыс. полигонов lod1 = make_lod(scan, 0.03, "Scan_LOD1") # ~30 тыс. lod2 = make_lod(scan, 0.01, "Scan_LOD2") # ~10 тыс. ``` ### Этап 3. Ретопология под анимацию Рабочая последовательность без сторонних ремешеров: сгладить форму → упростить в нерегулярную сетку → **это база для переразвёртки**, а равномерность достигается либо аддоном, либо вручную в режиме Edge Slide / To Sphere. ```python import bpy, math base = bpy.data.objects["Scan_LOD1"] bpy.ops.object.select_all(action='DESELECT') base.select_set(True); bpy.context.view_layer.objects.active = base sub = base.modifiers.new("Smooth", 'SUBSURF') # база для переноса деталей в текстуру sub.levels = 1; sub.render_levels = 1 bpy.ops.object.modifier_apply(modifier=sub.name) sm = base.modifiers.new("SmoothCorrective", 'CORRECTIVE_SMOOTH') # если нужен слайдер коррекции формы bpy.ops.object.modifier_apply(modifier=sm.name) bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.uv.smart_project(angle_limit=math.radians(66), island_margin=0.02) bpy.ops.object.mode_set(mode='OBJECT') for p in base.data.polygons: p.use_smooth = True ``` > [!NOTE] > `Smart UV Project` разворачивает всё в пару UV-карт с равными островами — это не то, что нужно для запекания. Для bake нужен **один** развёрнутый вручную или раскроенный вручную лист (лапки, спинка, бок), иначе карта сдвинется на каждом острове. Делай лист сам, запекание проверяй визуально. ### Этап 4. Запекание текстур с исходного скана Суть: низкополигональная форма получает карты, запечённые с высокополигонального скана. Дальше исходный скан можно удалить. ```python import bpy hi = bpy.data.objects["Scan_Hi"] # источник данных lo = bpy.data.objects["Scan_LOD1"] # приёмник # временно даём обоим одинаковый материал с нужными картами mat = bpy.data.materials.new("Mat_ScanBaked") mat.use_nodes = True nt = mat.node_tree bsdf = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED') tex = nt.nodes.new("ShaderNodeTexImage") tex.image = bpy.data.images.load(bpy.path.abspath("//tex/statue_basecolor.png")) nt.links.new(tex.outputs['Color'], bsdf.inputs['Base Color']) for ob in (hi, lo): ob.data.materials.clear(); ob.data.materials.append(mat) bpy.ops.object.select_all(action='DESELECT') lo.select_set(True); hi.select_set(True) bpy.context.view_layer.objects.active = lo bpy.ops.object.mode_set(mode='OBJECT') bake = bpy.context.scene.render.bake # Bake Settings сцены bake.use_pass_direct = True # AO-подобное затемнение в затенённых зонах bake.use_pass_indirect = False # экономит время, обычно не нужно bake.margin = 16 # отступ, чтобы не было чёрных кромок bake.cycles_samples = 32 bpy.ops.object.bake(type='EMIT', use_clear=True) # EMIT = diffuse albedo, шероховатость не трогается # normal-карта — вторая запекание, с включённым displacement исходника bpy.ops.object.bake(type='NORMAL') ``` > [!NOTE] > `type` в `bpy.ops.object.bake` принимает значения вроде `EMIT`, `DIFFUSE`, `NORMAL`, `AO`, `SHADOW`, `ROUGHNESS`, `DISPLACEMENT` — точный набор зависит от версии. Второй аргумент важнее самого списка: если он у тебя называется иначе, открой `Render Properties → Bake` и посмотри путь в Python-консоли через `bpy.ops.object.bake.get_rna_type().properties`. ### Этап 5. Нормали и материал Скан после ретопо — это «мягкая» форма: нормали нужно сглаживать по углу, а не по касательной, иначе по кромкам появляется лестница. ```python import bpy, math mat = bpy.data.materials.new("Mat_ScanFinal") mat.use_nodes = True nt = mat.node_tree bsdf = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED') base_tex = nt.nodes.new("ShaderNodeTexImage") base_tex.image = bpy.data.images.load(bpy.path.abspath("//tex/statue_basecolor.png")) norm_tex = nt.nodes.new("ShaderNodeTexImage") norm_tex.image = bpy.data.images.load(bpy.path.abspath("//tex/statue_normal.png")) norm_tex.colorspace_settings.name = 'Non-Color' # для карты нормалей цвет не интерпретируется nt.links.new(base_tex.outputs['Color'], bsdf.inputs['Base Color']) nt.links.new(norm_tex.outputs['Color'], bsdf.inputs['Normal']) bsdf.inputs['Roughness'].default_value = 0.7 for ob in bpy.data.objects: if ob.name.startswith("Scan_LOD"): ob.data.materials.clear(); ob.data.materials.append(mat) ``` Сглаживание по углу: в Blender 4.1+ это отдельная операция, до 4.1 — флаги меша. ```python import bpy, math bpy.ops.object.select_all(action='DESELECT') lo = bpy.data.objects["Scan_LOD1"]; lo.select_set(True) bpy.context.view_layer.objects.active = lo try: bpy.ops.object.shade_auto_smooth(angle=math.radians(40)) except AttributeError: lo.data.use_auto_smooth = True lo.data.auto_smooth_angle = math.radians(40) ``` ### Этап 6. Экспорт ```python import bpy bpy.ops.export_scene.gltf(filepath=r"//export/statue.glb", export_format='GLB', export_apply=True, export_texcoords=True, export_normals=True) bpy.ops.wm.save_as_mainfile(filepath=r"//export/statue.blend") ``` > [!NOTE] > До 4.x экспорт OBJ делался через `bpy.ops.export_scene.obj`, в 4.x он переехал в `bpy.ops.wm.obj_export`. Если первый вариант у тебя не существует — это не ошибка сцены, а смена имени. Проверь: `hasattr(bpy.ops.wm, "obj_export")`. ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `Decimate.ratio` для LOD0 | 0.10 | насколько сильно проседать до ретопо | | `Decimate.decimate_type` | `'COLLAPSE'` | для упрощения формы; `'UNSUBDIV'` сглаживает, но не снижает число полигонов столько | | `bake.margin` | 8–32 | больше = меньше «чёрных кромок» на швах | | `bake.use_pass_direct` | `True` | добавляет мягкое затемнение; выключай, если результат слишком грязный | | `bake.cycles_samples` | 16–64 | качество AO/нормалей | | `colorspace_settings` нормали | `'Non-Color'` | обязательно, иначе карта «поплывёт» | | `texture` | 2048×2048, одна на объект | 8k на скан почти никогда не нужно | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Модель в 100 раз меньше, чем ожидалось | скан в произвольных единицах | привести по референсу-линейке в кадре съёмки | | Чёрные кромки на запечённой карте | `margin` мал или UV-швы за пределами карты | увеличить `margin`, сдвинуть UV внутрь листа | | Нормали «поплыли», рельеф стал плоским | карта нормалей в sRGB вместо `Non-Color` | выставить `colorspace_settings.name = 'Non-Color'` | | Запекается чёрное | приёмник и источник в разных слоях видимости или нет общей карты | оба объекта — в одной коллекции, материал с картой общий | | Камера проваливается внутрь модели при анимации | топология нерегулярная, нет edge loops | ретопо квадами (аддон) либо хотя бы ручная правка рёбер | | Файл весит гигабайты | 8k карты × 5 карт × 5 LOD | одна карта 2k, три LOD, остальное вырезано | | Разные LOD «прыгают» при переключении | упрощение сдвигает силуэт | упрощать с сохранением объёма и крайних точек, не постепенно | ## ✅ РЕЗУЛЬТАТ На выходе: `.blend` с тремя уровнями детализации, один материал с картами base color 2k и normal 2k, плюс GLB. Характеристики — **оценка по порядку величины, а не измеренный результат**: исходный скан — миллионы полигонов; LOD0 порядка 100 тыс., LOD1 порядка 30 тыс., LOD2 порядка 10 тыс. Две текстуры 2048×2048 — порядка 30–60 МБ в памяти (RGB + RGBA, сжатие PNG/JPEG уменьшает файл вдвое-втрое, но не память). Запекание двух карт на объекте среднего размера — минуты; в реальности дольше, если источник с миллионами полигонов. Рендер итоговой сцены — обычный рендер, минуты. **Это оценка.** Замеры зависят от объёма исходного скана и железа; измеряй на своём материале. Базовые сетапы: [SETUP 01: BLENDER MCP BASIC](../setups/setup_01_blender_mcp_basic.md) — транспорт команд в Blender, [SETUP 08: ASSET PIPELINE](../setups/setup_08_asset_pipeline.md) — общая схема работы с ассетами. ## 🔗 СВЯЗИ - **Прочие в категории «Воркфлоу»:** [WF 17: STOP-MOTION FELT LOOK](wf_17_stop_motion_felt.md), [WF 23: VIRTUAL PRODUCTION LED](wf_23_virtual_production_led.md), [WF 01: TEXT-TO-SCENE](wf_01_text_to_scene.md), [WF 03: TEXTURE PIPELINE](wf_03_texture_pipeline.md), [WF 02: IMAGE-TO-3D](wf_02_image_to_3d.md) - **По темам:** [CASE 03: PRODUCT VISUALIZATION](../cases/case_03_product_viz.md), [CASE 08: SCI-FI CORRIDOR MODULAR](../cases/case_08_sci_fi_corridor.md), [CASE 05: PROCEDURAL CITY](../cases/case_05_procedural_city.md), [CASE 07: STYLIZED BARREL (HAND-PAINT)](../cases/case_07_stylized_barrel.md) - **Теги:** #animation, #archviz, #photogrammetry, #pipeline, #render, #texturing, #workflow --- # WF 25: AI CONCEPT→FINAL Tags: #ai-3d #workflow # WF 25: AI CONCEPT→FINAL **ID:** WF-25 | **Type:** AI Pipe | **Difficulty:** ⭐⭐⭐ **RU/EN Bilingual** > Когда применять: когда есть концепт-картинка или текстовое описание, и нужен финальный 3D-рендер быстрее, чем моделирование вручную. Честно: генератор даёт форму, а не ассет. ## 🎯 ЦЕЛЬ Концепт → сгенерированная 3D-модель → приведённая к масштабу и нормалям → ретопология → PBR-материалы → рендер. Ключевая честная оговорка, без которой статья врёт: **text/image-to-3D даёт болванку**. Почти всегда — непропорциональные формы, зеркальные UV, произвольный масштаб, иногда дырявая геометрия и текстура, которой нет. Генератор экономит время на блокауте; финальное качество всё равно делается руками. Этот пайплайн честно разделён на «то, что машина» и «то, что человек». ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия / состояние | Зачем | |---|---|---| | blender-mcp с генератором | проверить `get_addon_status` и статус конкретного генератора | генерация | | Концепт-изображение 1024×1024 | — | вход для image-to-3D | | Blender с Quad Remesher | аддон | равномерная топология | | Материалы | процедурные или из набора | PBR поверх формы | | HDRI + 3-point схема света | Poly Haven | рендер | > [!NOTE] > Имена MCP-инструментов ниже взяты из подключённого сервера (`generate_hyper3d_model_via_text`, `poll_rodin_job_status`, `import_generated_asset`, `get_tripo_status`, `poll_tripo_job_status`, `import_generated_asset_tripo`, `get_hunyuan3d_status`, `poll_hunyuan_job_status`, `import_generated_asset_hunyuan`). Набор и имена зависят от версии аддона: сначала вызови `get_addon_status()` и посмотри, что реально доступно, вместо того чтобы идти по списку. ## 🛠️ НАСТРОЙКА ### Этап 1. Проверить, что генератор вообще доступен Не вызывай генерацию до проверки статуса: иначе потратишь итерацию на заведомо нерабочую интеграцию. ```bash # 1) состояние аддона и версия Blender get_addon_status # 2) статусы конкретных генераторов — вызывай те, что есть в твоей сборке get_tripo_status get_hyper3d_status get_hunyuan3d_status ``` Если статус показывает, что генератор помечен как платный/премиум — сначала спроси пользователя, прежде чем запускать платные задачи. Проверка лимитов и стоимости — часть работы, а не деталь. ### Этап 2. Генерация ```bash # генерация по описанию; bbox_condition — желаемый габарит (в метрах, 6 чисел) generate_hyper3d_model_via_text( text_prompt="stylized knight helmet, faceted plates, closed visor, dark steel", bbox_condition=[0.4, 0.35, 0.45, 0, 0, 0] # ширина, глубина, высота, центр ) # генерация по картинке концепта generate_hyper3d_model_via_text( text_prompt="same asset as the reference image, single object, clean background", bbox_condition=[0.4, 0.35, 0.45, 0, 0, 0] ) # дальше — опрос задачи и импорт результата по request_id poll_rodin_job_status(request_id="<из ответа генерации>") import_generated_asset(name="Helmet_Raw", task_uuid="") ``` Опрос задачи возвращает статус; импорт выполняется отдельно и только после того, как задача дошла до готового состояния. Вызывать импорт раньше — самая частая ошибка. ### Этап 3. Привести к масштабу и посадить на пол Сразу после импорта — габаритный проверка и посадка. Генераторы не понимают метры. ```python import bpy from mathutils import Vector ob = bpy.data.objects.get("Helmet_Raw") if ob is None: raise SystemExit("объект не импортирован — проверь статус задачи") def bbox_world(ob): cs = [ob.matrix_world @ Vector(c) for c in ob.bound_box] lo = Vector((min(c.x for c in cs), min(c.y for c in cs), min(c.z for c in cs))) hi = Vector((max(c.x for c in cs), max(c.y for c in cs), max(c.z for c in cs))) return lo, hi lo, hi = bbox_world(ob) size = hi - lo print(f"импортирован габарит: {size.x:.3f} x {size.y:.3f} x {size.z:.3f} (в единицах сцены)") TARGET_H = 0.45 # шлем = 45 см s = TARGET_H / max(size.z, 1e-6) ob.scale = [s] * 3 bpy.ops.object.select_all(action='DESELECT') ob.select_set(True); bpy.context.view_layer.objects.active = ob bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) lo, hi = bbox_world(ob) # после применения трансформации пересчитываем ob.location.z -= lo.z # нижняя точка в Z=0 print(f"после нормализации: {hi.x-lo.x:.3f} x {hi.y-lo.y:.3f} x {hi.z-lo.z:.3f} м") ``` Отдельно проверь зеркальность: сгенерированные модели часто приходят вывернуты наизнанку. ```python # площадь нормалей наружу: если объём «вывернут», normal.dot(center - p) должен быть > 0 import bmesh me = ob.data bm = bmesh.new(); bm.from_mesh(me) bad = 0 for f in bm.faces: c = f.calc_center_median() if f.normal.dot(c) < 0: bad += 1 bm.free() if bad > len(me.polygons) * 0.5: bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.normals_make_consistent(inside=False) # доступность менялась между версиями bpy.ops.object.mode_set(mode='OBJECT') print("нормали были вывернуты — исправлено") ``` > [!NOTE] > Операция пересчёта нормалей в наружу переименовывалась (`normals_make_consistent`, в более старых версиях `editnormals_make_consistent`). Если оператор не найден — выдели всё в Edit Mode и сделай `Mesh → Normals → Recalculate Outside` вручную; логика та же. ### Этап 4. Ретопология и UV ```python import bpy, math bpy.ops.object.select_all(action='DESELECT') ob.select_set(True); bpy.context.view_layer.objects.active = ob # грубое упрощение до рабочего объёма d = ob.modifiers.new("Decimate", 'DECIMATE') d.decimate_type = 'COLLAPSE'; d.ratio = 0.12 bpy.ops.object.modifier_apply(modifier=d.name) # гладкие нормали: без этого ретопо-поверхность будет «гранёной» try: bpy.ops.object.shade_auto_smooth(angle=math.radians(35)) except AttributeError: bpy.ops.object.shade_smooth() bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.uv.smart_project(angle_limit=math.radians(66), island_margin=0.01) bpy.ops.object.mode_set(mode='OBJECT') ``` Равномерные квады даёт аддон-ремешер — см. [WF 24](wf_24_photogrammetry_cleanup.md): встроенного в Blender квад-ремешера с ровным ребром нет. ### Этап 5. Материалы Если генератор принёс текстуру, сначала проверь, что она вообще есть и в том ли цветовом пространстве. ```python import bpy img = bpy.data.images.get("Helmet_Raw texture") print("текстуры на объекте:", [i.name for i in bpy.data.images if i.users]) if img: print("размер:", tuple(img.size), "colorspace:", img.colorspace_settings.name) mat = bpy.data.materials.new("Mat_Helmet") mat.use_nodes = True nt = mat.node_tree bsdf = next(n for n in nt.nodes if n.type == 'BSDF_PRINCIPLED') bsdf.inputs['Base Color'].default_value = (0.06, 0.07, 0.09, 1) bsdf.inputs['Metallic'].default_value = 1.0 bsdf.inputs['Roughness'].default_value = 0.38 # потёртости без текстуры: Noise -> Roughness, иначе металл выглядит пластиковым noise = nt.nodes.new("ShaderNodeTexNoise") noise.inputs['Scale'].default_value = 40.0 noise.inputs['Detail'].default_value = 6.0 mr = nt.nodes.new("ShaderNodeMapRange") mr.inputs['To Min'].default_value = 0.25 mr.inputs['To Max'].default_value = 0.55 nt.links.new(noise.outputs['Fac'], mr.inputs['Value']) nt.links.new(mr.outputs['Result'], bsdf.inputs['Roughness']) ob.data.materials.clear(); ob.data.materials.append(mat) ``` ### Этап 6. Камера, свет, рендер ```python import bpy, math sc = bpy.context.scene # плоский продуктовый свет: две софтбокс-панели + заполняющий for name, loc, energy, size in (("Key", (1.2, -1.6, 1.6), 400, 1.5), ("Fill", (-1.6, -0.8, 0.9), 120, 2.0)): bpy.data.lights.new(name, type='AREA') l = bpy.data.objects.new(f"L_{name}", bpy.data.lights[name]) sc.collection.objects.link(l); l.location = loc l.data.energy = energy; l.data.size = size c = l.constraints.new('TRACK_TO'); c.target = ob c.track_axis = 'TRACK_NEGATIVE_Z'; c.up_axis = 'UP_Y' bpy.ops.object.camera_add(location=(0.9, -1.5, 0.55)) cam = bpy.context.active_object; cam.data.lens = 85 c = cam.constraints.new('TRACK_TO'); c.target = ob c.track_axis = 'TRACK_NEGATIVE_Z'; c.up_axis = 'UP_Y' sc.camera = cam sc.render.engine = 'CYCLES' sc.cycles.samples = 256 sc.cycles.use_denoising = True sc.render.resolution_x, sc.render.resolution_y = 1600, 1600 sc.render.filepath = r"//renders/helmet_hero.png" bpy.ops.render.render(write_still=True) ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `bbox_condition` при генерации | желаемый габарит | задавай сразу: экономит одну итерацию нормализации | | `Decimate.ratio` | 0.10–0.20 | сильнее — теряются мелкие пластины шлема | | `Metallic` / `Roughness` | 1.0 / 0.25–0.55 | металл без вариации шероховатости читается как пластик | | `lens` камеры | 50–100 мм для продукта | широкий объектив растягивает объект | | `samples` | 128 черновик / 256–512 финал | шумоподавление обязательно на обоих | | `RESOLUTION` | 1024 черновик / 1600–2048 финал | — | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Импорт вернул объект, которого нет в сцене | задача ещё не готова, `request_id` от другого запуска | опросить статус, дождаться готового, импортировать по правильному id | | Объект «утоплен» в полу или висит | не посажен на нулевую Z | пересчитать `bound_box` в мировых координатах и сдвинуть | | Модель в 10 раз больше комнаты | генератор не знает про метры | привести к нужной высоте через `scale` + `transform_apply` | | Форма вывернута, рендер чёрный | нормали наружу | `Recalculate Outside` или вывернуть объект | | Текстуры нет, материал серый | генератор не отдаёт карту | процедурный материал (см. этап 5), UV развернуть вручную | | Сцена «в пятне», форма не читается | слишком мягкий свет | жёстче ключевой, ниже фон, добавить контровой | ## ✅ РЕЗУЛЬТАТ На выходе: нормализованный объект на Z=0 с одним PBR-материалом и рендер 1600×1600 в PNG, плюс `.blend`. Характеристики — **оценка по порядку величины, а не измеренный результат**: сгенерированная болванка — обычно сотни тысяч треугольников; после `Decimate 0.12` — десятки тысяч, после квад-ремешера с ребром 2–3 см на модели 45 см — 20–60 тыс. Рендер 1600×1600 в Cycles, 256 сэмплов, шумоподавление включено, сцена из одного объекта с процедурным материалом — от десятков секунд до единиц минут на средней дискретной видеокарте. **Это оценка.** Генерация сама занимает от десятков секунд до нескольких минут на задачу в зависимости от сервиса; измеряй на своём ключе и учитывай лимиты. Базовые сетапы: [SETUP 01: BLENDER MCP BASIC](../setups/setup_01_blender_mcp_basic.md) — транспорт команд в Blender, [SETUP 08: ASSET PIPELINE](../setups/setup_08_asset_pipeline.md) — общая схема работы с ассетами. ## 🔗 СВЯЗИ - **Прочие в категории «Воркфлоу»:** [WF 02: IMAGE-TO-3D](wf_02_image_to_3d.md), [WF 01: TEXT-TO-SCENE](wf_01_text_to_scene.md), [WF 03: TEXTURE PIPELINE](wf_03_texture_pipeline.md), [WF 04: CHARACTER ANIMATION](wf_04_character_animation.md), [WF 05: PROCEDURAL ASSETS](wf_05_procedural_assets.md) - **По темам:** [SETUP 42: TRIPO AI API](../setups/setup_42_tripo_ai_api.md), [SETUP 43: MESHY API V4](../setups/setup_43_meshy_api_v4.md), [SETUP 44: RODIN / HYPER3D GEN](../setups/setup_44_rodin_hyper3d.md), [SETUP 45: HUNYUAN3D 2.1 LOCAL](../setups/setup_45_hunyuan3d_21.md) - **Теги:** #ai-3d, #workflow --- # WF 26: LOOKDEV TURNTABLE LAB Tags: #lookdev #product #workflow # WF 26: LOOKDEV TURNTABLE LAB **ID:** WF-26 | **Type:** Lookdev | **Difficulty:** ⭐⭐ **RU/EN Bilingual** > Когда применять: когда модель готова, но непонятно, как она будет выглядеть — при каком свете, в каком материале и не ломается ли силуэт при повороте. Это диагностика, а не продакшн-рендер. ## 🎯 ЦЕЛЬ Собирается матрица превью: объект × несколько HDRI × несколько световых схем × несколько вариантов материала, плюс turntable-облёт на 360°. На выходе — папка с именованными кадрами, по которым видно, какой свет врал, какой материал читается, и где модель «сдувается» сбоку. Принцип, который отличает лабораторию от «посмотреть в рендере»: **все кадры делаются за один прогон одним скриптом с неизменной камерой**. Если каждый кадр снимается отдельно, камера и экспозиция «уплывают», сравнивать нечего. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия / состояние | Зачем | |---|---|---| | Blender с Cycles | базовый | предсказуемый свет и отражения | | Набор HDRI (`.hdr`) | 4–6 штук в `//hdri/` | лестница условий | | Объект на чистом фоне | без реквизита | иначе непонятно, что видно | | Плавный фон | плоскость или world | камешка не «висит в пустоте» | | Таблица вариантов | можно прямо в статье как шаблон | воспроизводимость | > [!NOTE] > HDRIs с Poly Haven приходят файлами `.hdr`/`.exr` — их нужно скачать и положить в проект вручную; отдельного MCP-инструмента для скачивания HDRI в этом сервере нет. Имена файлов в примерах ниже условные. ## 🛠️ НАСТРОЙКА ### Этап 1. Лабораторная сцена Ничего лишнего: объект, камера, пол. Никаких декораций — они съедают контраст и прячут ошибки материала. ```bash RU: "Собери лабораторную сцену: плоскость 'Backdrop' 20x20 м, объект 'Product' по центру, камера 50 мм на расстоянии 2.2 м, рендер 1000x1000, без DOF, view_transform = Standard" EN: "Build a lab scene: plane 'Backdrop' 20x20 m, object 'Product' at origin, 50 mm camera at 2.2 m, render 1000x1000, no DOF, view_transform = Standard" ``` ```python import bpy, math sc = bpy.context.scene bpy.ops.object.select_all(action='DESELECT') bpy.ops.mesh.primitive_plane_add(size=20, location=(0, 0, -0.001)) back = bpy.context.active_object; back.name = "Backdrop" m = bpy.data.materials.new("Mat_Backdrop"); m.use_nodes = True b = next(n for n in m.node_tree.nodes if n.type == 'BSDF_PRINCIPLED') b.inputs['Base Color'].default_value = (0.18, 0.19, 0.21, 1) b.inputs['Roughness'].default_value = 0.55 # не зеркало: иначе фон даёт лишний блик back.data.materials.append(m) bpy.ops.object.camera_add(location=(1.55, -1.55, 1.0)) cam = bpy.context.active_object; cam.data.lens = 50 cam.data.dof.use_dof = False # DOF выключен намеренно: размытие мешает оценке c = cam.constraints.new('TRACK_TO'); c.target = bpy.data.objects["Product"] c.track_axis = 'TRACK_NEGATIVE_Z'; c.up_axis = 'UP_Y' sc.camera = cam sc.render.resolution_x = sc.render.resolution_y = 1000 sc.render.engine = 'CYCLES' sc.cycles.samples = 64 sc.cycles.use_denoising = True sc.view_settings.view_transform = 'Standard' # честная экспозиция, без плёночной сжалки sc.view_settings.exposure = 0.0 ``` > [!NOTE] > Список доступных `view_transform` зависит от версии и цветовой конфигурации: `Standard`, `Filmic`, `AgX`, `Raw`, `False Color` встречаются в разных наборах. Узнать свои так: `[i.identifier for i in sc.view_settings.bl_rna.properties["view_transform"].enum_items]`. ### Этап 2. HDRI-лестница Лестница строится по смыслу: нейтральная студия → жёсткий дневной свет → тёплый закат → холодное ночное окружение. Плюс один кадр с намеренно переэкспонированной средой — он показывает, где форма «разваливается» по светлоте. ```python import bpy LADDER = [ # (файл, strength, подпись) ("//hdri/studio_soft.hdr", 0.6, "studio"), ("//hdri/sun_hard.hdr", 1.0, "sun"), ("//hdri/sunset.hdr", 0.8, "sunset"), ("//hdri/night_city.hdr", 0.5, "night"), ] world = bpy.data.worlds.new("LabWorld") sc = bpy.context.scene sc.world = world world.use_nodes = True nt = world.node_tree out = next(n for n in nt.nodes if n.type == 'OUTPUT_WORLD') env = nt.nodes.new("ShaderNodeTexEnvironment") bg = next(n for n in nt.nodes if n.type == 'BACKGROUND') env.projection = 'EQUIRECTANGULAR' nt.links.new(env.outputs['Color'], bg.inputs['Color']) nt.links.new(bg.outputs['Background'], out.inputs['Surface']) def use_hdri(path, strength): img = bpy.data.images.load(bpy.path.abspath(path), check_existing=True) env.image = img bg.inputs['Strength'].default_value = strength return img ``` ### Этап 3. Световые лестницы Четыре схемы: чистый HDRI, студийная трёхточка, один жёсткий источник, верхний свет. Разница между ними и есть ответ на вопрос «почему модель выглядит плохо». ```python import bpy from mathutils import Vector def clear_lights(): for ob in list(bpy.data.objects): if ob.type == 'LIGHT': bpy.data.objects.remove(ob, do_unlink=True) def add_area(name, loc, energy, size, target, color=(1, 1, 1)): ld = bpy.data.lights.new(name, type='AREA') ld.energy = energy; ld.size = size; ld.color = color ob = bpy.data.objects.new(name, ld); bpy.context.scene.collection.objects.link(ob) ob.location = loc c = ob.constraints.new('TRACK_TO'); c.target = target c.track_axis = 'TRACK_NEGATIVE_Z'; c.up_axis = 'UP_Y' return ob def rig(kind, target): clear_lights() if kind == "three_point": add_area("K", (1.8, -1.8, 2.2), 500, 1.5, target) add_area("F", (-2.0, -1.2, 1.0), 150, 2.5, target) add_area("R", (-1.0, 2.2, 1.8), 300, 0.6, target, (0.85, 0.9, 1.0)) elif kind == "hard": add_area("K", (2.2, -2.2, 2.6), 900, 0.25, target) # маленький источник = резкая тень elif kind == "top": add_area("T", (0, 0, 3.0), 700, 2.0, target) elif kind == "bounce": add_area("K", (1.6, -1.6, 1.6), 400, 1.2, target) add_area("B", (0, 0.5, 0.15), 120, 4.0, target, (1.0, 0.95, 0.9)) # отражение снизу ``` ### Этап 4. Варианты материала Меняй только то, что реально меняет восприятие. Десять «почти одинаковых» вариантов — это десять зря потраченных кадров. ```python import bpy VARIANTS = { "matte": dict(metallic=0.0, rough=0.65, coat=0.0), "satin": dict(metallic=0.0, rough=0.25, coat=0.0), "gloss": dict(metallic=0.0, rough=0.08, coat=0.0), "coat": dict(metallic=0.0, rough=0.15, coat=0.8), # лак: автоприрода, посуда, мебель "metal_raw": dict(metallic=1.0, rough=0.35, coat=0.0), "metal_pol": dict(metallic=1.0, rough=0.12, coat=0.0), } def apply_variant(obj, name): v = VARIANTS[name] for slot in obj.material_slots: mat = slot.material if mat is None or not mat.use_nodes: continue b = next(n for n in mat.node_tree.nodes if n.type == 'BSDF_PRINCIPLED') for sock, key in (('Metallic', 'metallic'), ('Roughness', 'rough')): s = b.inputs.get(sock) if s: s.default_value = v[key] coat = b.inputs.get('Coat Weight') or b.inputs.get('Clearcoat') if coat: coat.default_value = v['coat'] return True return False ``` > [!NOTE] > Сокеты `Coat Weight` / `Clearcoat` — это одно и то же поле в разных версиях: в 4.x Principal BSDF переименован и получил `Coat Weight`, `Coat Roughness`, `Specular IOR Level`. Код выше проверяет оба имени; на других переименованиях (например `Transmission Weight` вместо `Transmission`) может понадобиться добавить вариант — смотри фактические имена сокетов, а не этот список. ### Этап 5. Прогон матрицы Имена файлов кодируют всё, что нужно для сравнения. Это дешевле, чем «какой это был кадр». ```python import bpy, os sc = bpy.context.scene obj = bpy.data.objects["Product"] target = obj MATRIX = [(hd, st, rig_k, var) for hd, (_, st, _) in LADDER for rig_k in ("hdri_only", "three_point", "hard") for var in ("matte", "satin", "coat")] # это 4x3x3 = 36 кадров — много; для первого прохода бери 2 HDRI x 2 схемы x 3 материала = 12 os.makedirs(bpy.path.abspath("//lab"), exist_ok=True) sc.cycles.samples = 32 # черновик лестницы: быстро и шумно done = 0 for hdri_path, strength, hdri_name in LADDER[:2]: for rig_kind in ("hdri_only", "three_point"): if rig_kind == "hdri_only": use_hdri(hdri_path, strength) clear_lights() else: rig(rig_kind, target) bg.inputs['Strength'].default_value = 0.15 # лампы главные, HDRI — заполнение for var in ("matte", "satin", "coat"): apply_variant(obj, var) sc.render.filepath = f"//lab/{hdri_name}__{rig_kind}__{var}.png" bpy.ops.render.render(write_still=True) done += 1 print("кадров:", done) ``` ### Этап 6. Turntable Поворот объекта, а не камеры: свет остаётся тем же, и ты видишь, как форма ведёт себя при смене ракурса к источнику. ```python import bpy, math pivot = bpy.data.objects.new("Turntable_Pivot", None) bpy.context.scene.collection.objects.link(pivot) obj = bpy.data.objects["Product"] obj.parent = pivot obj.matrix_parent_inverse = pivot.matrix_world.inverted() FRAMES = 24 # 15° на кадр sc = bpy.context.scene sc.frame_start, sc.frame_end = 1, FRAMES pivot.rotation_mode = 'XYZ' for f in range(1, FRAMES + 1): pivot.rotation_euler = (0.0, 0.0, math.radians(360.0 * (f - 1) / FRAMES)) pivot.keyframe_insert("rotation_euler", frame=f) # цикл замыкается без скачка: последний кадр не доводим до 360 pivot.rotation_euler = (0.0, 0.0, 0.0) pivot.keyframe_insert("rotation_euler", frame=FRAMES + 1) sc.render.filepath = r"//lab/turntable_" bpy.ops.render.render(animation=True) ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `view_transform` | `'Standard'` для оценки | в продакшн-кадре — по вкусу | | `DOF` | выключен | DOF прячет ошибки силуэта | | HDRI `Strength` | 0.4–1.0 | насколько окружение сильнее ламп | | Площадь источника | 0.2 м = жёсткий, 2.5 м = мягкий | размер источника, а не расстояние, даёт жёсткость тени | | `samples` лестницы | 32 | лестница нужна для сравнения, не для красоты | | `samples` финала | 256–512 | только отобранные варианты | | Кадров turntable | 24 (15°) | 12 кадров — слишком редко, 48 — избыточно | | Что смотрим | Где видно ошибку | |---|---| | Силуэт | turntable: провалы, «пузыри», вывернутые участки | | Материал | жёсткий источник: блик должен быть узким и иметь форму, а не пятно | | Экспозиция | переэкспонированный кадр: где форма теряет границы | | Отражения | coat-вариант: лак должен давать двойной блик, а не один | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | Кадры не сравнимы между собой | экспозиция/камера менялись вручную | один скрипт, неизменная камера, фиксированный `view_transform` | | Форма «пластиковая» при нулевом HDRI | только HDRI без ламп: нет бликов с формой | добавить жёсткий источник маленького размера | | Всё чёрное | HDRI не загрузился, `Strength` = 0 | проверить `env.image` не `None` | | Turntable дёргается на стыке | последний кадр = 360° и дублирует первый | последний кадр ставить в 0°, а не в 360° | | Матрица заняла час | 36 кадров по 256 сэмплов | сначала черновик 32 сэмпла, потом 3–5 финальных | | Цвета на кадрах «прыгают» | включён авто-экспотер/авто-баланс | отключить авто-настройки, фиксировать экспозицию вручную | ## ✅ РЕЗУЛЬТАТ На выходе: папка `//lab/` с 12–36 превью 1000×1000 (имена вида `sun__three_point__coat.png`) и 24 кадра turntable 1000×1000. Характеристики — **оценка по порядку величины, а не измеренный результат**: сцена из одного объекта, пола и источников — десятки тысяч полигонов, 1–3 материала, одна HDRI-карта (2k HDR — десятки мегабайт в памяти). Один кадр 1000×1000 в Cycles на 32 сэмплах — секунды; 36 кадров лестницы — минуты на средней видеокарте. 24 кадра turntable на 64 сэмплах — те же единицы минут. Кадр PNG 1000×1000 — 1–3 МБ. **Это оценка.** Реальное время определяется числом источников, объёмом геометрии и железа; измеряй у себя. Базовые сетапы: [SETUP 01: BLENDER MCP BASIC](../setups/setup_01_blender_mcp_basic.md) — транспорт команд в Blender, [SETUP 08: ASSET PIPELINE](../setups/setup_08_asset_pipeline.md) — общая схема работы с ассетами. ## 🔗 СВЯЗИ - **Прочие в категории «Воркфлоу»:** [WF 10: PRODUCT TURNTABLE](wf_10_product_turntable.md), [WF 01: TEXT-TO-SCENE](wf_01_text_to_scene.md), [WF 02: IMAGE-TO-3D](wf_02_image_to_3d.md), [WF 03: TEXTURE PIPELINE](wf_03_texture_pipeline.md), [WF 04: CHARACTER ANIMATION](wf_04_character_animation.md) - **По темам:** [CASE 10: SNEAKER PRODUCT HERO](../cases/case_10_sneaker_product.md), [CASE 14: FOOD RENDER (RAMEN)](../cases/case_14_food_r visualisation.md), [CASE 15: CAR STUDIO CINEMATIC](../cases/case_15_car_studio_cinematic.md), [P50: PORTFOLIO TURNTABLE & QA](../prompts/50_turntable_qa.md) - **Теги:** #lookdev, #product, #workflow --- # WF 27: SOUND-SYNC EDIT Tags: #audio #workflow # WF 27: SOUND-SYNC EDIT **ID:** WF-27 | **Type:** Audio | **Difficulty:** ⭐⭐ **RU/EN Bilingual** > Когда применять: когда кадры рендера уже есть и нужно их подмонтировать под музыку: нарезка по биту, синхронизация сцены, нормализация громкости, экспорт с аудио. ## 🎯 ЦЕЛЬ Музыка становится **сеткой монтажа**, а не украшением: длительность сцены выводится из трека, смена плана ставится на доли, громкость приводится к целевой, а на выходе идёт ролик с синхронной звуковой дорожкой. Единственный надёжный способ синхронизации — считать кадры, а не «слушать и подкручивать». Всё остальное (визуальный бит, реакция света, движение камеры) — надстройка поверх сетки, построенной по темпу. > [!NOTE] > В Blender **нет** встроенного аудио-анализа для шейдеров и геометрии: ноды шейдеров не видят звук, а драйверы не читают огибающую. Реакция на звук делается так: огибающую считают вне Blender, записывают в CSV или в F-кривые, и уже ими управляют через драйверы. Инструменты анализа — DAW или `ffmpeg` (фильтры `astats`, `ebur128`); в самой статье используется только то, что гарантированно есть. ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия / состояние | Зачем | |---|---|---| | Blender с VSE (Sequence Editor) | встроенный | монтаж и синхронизация | | Аудиофайл | WAV/MP3, BPM известен или измерен | сетка монтажа | | `ffmpeg` в PATH | версия любая recent | нормализация громкости и проверка | | Готовые кадры/ролики | PNG-последовательность или видео | материал для монтажа | ## 🛠️ НАСТРОЙКА ### Этап 1. Сцена под трек, а не наоборот ```bash RU: "Монтаж под музыку: трек //audio/theme.mp3, 128 BPM, 24 fps. Сделай маркеры на каждый бит и на каждую долю, длительность сцены = трек, полосы в VSE, экспорт //renders/reel_" EN: "Music-synced edit: track //audio/theme.mp3, 128 BPM, 24 fps. Mark every beat and every subdivision, scene length = track, strips in the VSE, export to //renders/reel_" ``` ```python import bpy, math BPM, FPS = 128.0, 24 SEC = 212.4 # длительность трека, сек — измерь, не угадывай sc = bpy.context.scene sc.render.fps = FPS sc.frame_start = 0 # 0-based: так маркеры и кадры совпадают без смещения sc.frame_end = int(SEC * FPS) sc.sync_mode = 'AUDIO_SYNC' # проигрывание вьюпорта по звуку, а не по таймеру def frames_per_beat(bpm, fps, division=1): """Кадров на одну долю. division=2 -> восьмая, 4 -> шестнадцатая.""" return 60.0 / bpm * fps / division F = frames_per_beat(BPM, FPS) print(f"128 BPM @ {FPS} fps -> бит = {F:.3f} кадра, 1/4 = {F/4:.3f} кадра") ``` ### Этап 2. Маркеры по биту и по долям Маркеры — это и есть редакционная карта: по ним и режут, и потом сверяются. ```python import bpy sc = bpy.context.scene for mk in list(sc.timeline_markers): sc.timeline_markers.remove(mk) def mark_grid(fpb, fps, frame_end, prefix): n = 0 f = 0.0 while f <= frame_end: sc.timeline_markers.new(f"{prefix}_{n:03d}", frame=int(round(f))) f += fpb n += 1 return n beat_fpb = 60.0 / BPM * FPS nb = mark_grid(beat_fpb, FPS, sc.frame_end, "beat") ns = mark_grid(beat_fpb / 4, FPS, sc.frame_end, "sub") # шестнадцатые print(f"маркеров: битов {nb}, долей {ns}, всего {len(sc.timeline_markers)}") ``` ### Этап 3. Аудио в VSE Имя коллекции API для полос менялось в Blender 4.4: было `sequence_editor.sequences`, стало `sequence_editor.strips`. Код ниже пробует оба и падает с понятным сообщением, а не молча. ```python import bpy sc = bpy.context.scene se = sc.sequence_editor or sc.sequence_editor_create() def strips_container(se): for attr in ("strips", "sequences"): coll = getattr(se, attr, None) if coll is not None: return coll raise AttributeError( "не нашёл ни sequence_editor.strips, ни sequence_editor.sequences — " "проверь имя в API своей версии") strips = strips_container(se) audio = bpy.data.sounds.load(bpy.path.abspath("//audio/theme.mp3")) s1 = strips.new_sound(name="Theme", filepath=audio.filepath, channel=1, frame_start=0) s1.volume = 1.0 s1.sound = audio print("полос:", [getattr(s, "name", "?") for s in strips]) ``` > [!NOTE] > Сигнатуры `new_sound` / `new_image` / `new_movie` тоже менялись (добавлялись необязательные параметры вроде `fit_method`). Если твой вызов ругается на неизвестный аргумент — вызови `bpy.ops.sequencer` со списком доступных операторов или посмотри `getattr(strips, "new_sound").__doc__` в Python-консоли. ### Этап 4. Резать по биту Не режем вручную: составляем монтажный план (EDL) как обычный CSV, который потом можно и в монтажку отдать, и скриптом разложить по полосам. ```python import csv, bpy beat_fpb = 60.0 / BPM * FPS SHOTS = ["open_hall", "closeup_face", "wide_room", "insert_hands", "outro_title"] # длительность каждого плана — целое число битов: 8, 16, 8, 16, 32 plan = [8, 16, 8, 16, 32] assert len(plan) == len(SHOTS), "число планов должно совпадать с числом кадров" edl, cursor = [], 0.0 for shot, beats in zip(SHOTS, plan): length = beats * beat_fpb edl.append(dict(shot=shot, start_frame=int(round(cursor)), end_frame=int(round(cursor + length)), beats=beats, sec_start=round(cursor / FPS, 3), sec_len=round(length / FPS, 3))) cursor += length with open(bpy.path.abspath("//renders/edl.csv"), "w", newline="", encoding="utf-8") as fh: w = csv.DictWriter(fh, fieldnames=list(edl[0])) w.writeheader(); w.writerows(edl) for e in edl: print(f"{e['shot']:<16} {e['start_frame']:>6} .. {e['end_frame']:<6} " f"({e['beats']} бит, {e['sec_len']} с)") ``` ### Этап 5. Громкость Стриминговые площадки требуют интегрированной громкости около **−14 LUFS** и пиков не выше −1 dBTP. Проверяется и приводится внешним инструментом, не на глаз. ```bash # измерить текущую громкость (фильтр ebur128 печатает итог в конце) ffmpeg -hide_banner -i "audio/theme.mp3" -af ebur128=peak=true -f null - # привести к целевой громкости: I=-14 LUFS, TP=-1 dBTP, LRA=11 ffmpeg -hide_banner -i "audio/theme.mp3" -af loudnorm=I=-14:TP=-1.0:LRA=11 \ -ar 48000 "audio/theme_norm.wav" ``` ```python # вернуть нормализованный трек в сцену import bpy se = bpy.context.scene.sequence_editor strips = strips_container(se) old = next((s for s in strips if s.type == 'SOUND'), None) start = old.frame_start if old else 0 if old: strips.remove(old) norm = bpy.data.sounds.load(bpy.path.abspath("//audio/theme_norm.wav")) strips.new_sound(name="Theme_norm", filepath=norm.filepath, channel=1, frame_start=start) ``` ### Этап 6. Реакция на звук: огибающая снаружи, драйверы внутри Огибающую считают вне Blender и кладут в CSV. Дальше Blender просто читает числа. ```bash # пример: RMS-огибающая в CSV (амплитуда в dB на каждое окно). # Пути здесь уже настоящие, относительные к каталогу проекта — ffmpeg запускается из оболочки, # а не из Blender, и префикс // (относительный путь Blender) для него не действует. ffmpeg -hide_banner -i "audio/theme.wav" -ac 1 -ar 1000 \ -af "astats=metadata=1:reset=24,ametadata=print:key=lavfi.astats.Overall.RMS_level" \ -f null - 2>&1 | grep RMS_level > out/env_raw.txt ``` ```python import bpy, re, math def load_envelope(path): """Парсит вывод ametadata в список float. Формат примитивный, поэтому честно: если формат изменится, load_envelope вернёт пустой список и ты это увидишь по печати.""" out = [] rx = re.compile(r"RMS_level=(-?\d+(?:\.\d+)?)") with open(path, encoding="utf-8", errors="replace") as fh: for line in fh: m = rx.search(line) if m: v = float(m.group(1)) out.append(0.0 if v <= -70 else 10 ** (v / 20)) # dB -> линейная амплитуда return out env = load_envelope(r"<путь-к-проекту>\out\env_raw.txt") print("окон огибающей:", len(env), "макс:", round(max(env), 3) if env else None) # реакция: масштаб объекта по огибающей через ключи (надёжнее драйвера по внешнему файлу) ob = bpy.data.objects["PulseCube"] STEP = max(1, len(env) // (bpy.context.scene.frame_end or 1)) for f in range(1, (bpy.context.scene.frame_end or 1) + 1, STEP): amp = env[min(f // STEP, len(env) - 1)] if env else 0.0 ob.scale = (1.0 + amp * 0.4,) * 3 ob.keyframe_insert("scale", frame=f) ``` ### Этап 7. Экспорт с аудио ```python import bpy sc = bpy.context.scene sc.render.engine = 'CYCLES' sc.cycles.samples = 64 sc.render.resolution_x, sc.render.resolution_y = 1920, 1080 sc.render.image_settings.file_format = 'FFMPEG' sc.render.ffmpeg.format = 'MPEG4' sc.render.ffmpeg.codec = 'H264' sc.render.ffmpeg.constant_rate_factor = 'HIGH' sc.render.ffmpeg.audio_codec = 'AAC' sc.render.filepath = r"//renders/reel_" bpy.ops.render.render(animation=True) ``` > [!NOTE] > Набор кодеков в `ffmpeg` зависит от того, как Blender собран (и от того, включён ли ffmpeg вообще). Если `file_format = 'FFMPEG'` не принимается — сначала проверь доступные форматы: `[i.identifier for i in sc.render.image_settings.bl_rna.properties["file_format"].enum_items]`. Надёжная запасная схема — отрендерить PNG-последовательность и собрать видео внешним ffmpeg. ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `render.fps` | 24 / 25 / 30 | под частоту кадров трека и площадки | | кадров на бит | `60/bpm × fps` | базовая единица монтажа | | `sync_mode` | `'AUDIO_SYNC'` | проигрывание по звуку в реальном времени | | `frame_start` | `0` | так маркеры и кадры не разъезжаются на единицу | | `loudnorm I` | `−14` LUFS | целевая интегрированная громкость для стриминга | | `loudnorm TP` | `−1.0` dBTP | пик, чтобы не было клиппинга на транскодере | | `constant_rate_factor` | `'HIGH'` | ниже — меньше файл, выше — меньше артефактов | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | `AttributeError` на `sequences` / `strips` | переименование API в 4.4 | резолвер выше: пробует оба имени | | Монтаж «едет» на доли | длительность плана не кратна биту | длительности задавать целым числом битов (8/16/32) | | Видео без звука | `audio_codec` не задан или полоса не в кадре | `ffmpeg.audio_codec='AAC'`, проверить `frame_start` полосы | | Звук «хрипящий» после транскодера | пик выше −1 dBTP | нормализация `loudnorm` до −1 dBTP | | Сцена проигрывается рывками | `sync_mode` выключен или сцена тяжелее реального времени | `AUDIO_SYNC` и облегчить сцену | | Реакция на звук не работает | ждали, что шейдер услышит аудио | огибающую считать вне Blender и вести через ключи/драйверы | | Кадры и звук расходятся к концу | fps рендера и fps трека разные | одинаковый `fps` + рендер ровно `frame_end` кадров | ## ✅ РЕЗУЛЬТАТ На выходе: `.blend` со сценой длиной ровно в трек, маркерами на биты и доли, аудиополосой в VSE, монтажным планом `//renders/edl.csv`, и готовый файл `//renders/reel_*.mp4` с AAC-дорожкой. Характеристики — **оценка по порядку величины, а не измеренный результат**: ролик 3.5 минуты при 24 fps — это порядка 5000 кадров. В VSE-рендере (то есть пересборка из готовых клипов) это секунды-десятки секунд. Рендер 3D-сцены под 24 кода — совсем другая история, см. [WF 01](wf_01_text_to_scene.md). Файл 1920×1080, 3.5 минуты, H264 CRF `HIGH` с AAC — порядка 50–150 МБ. **Это оценка.** Время зависит от того, что именно рендерится (VSE-пересборка или 3D), и от битрейта; измеряй у себя. Базовые сетапы: [SETUP 01: BLENDER MCP BASIC](../setups/setup_01_blender_mcp_basic.md) — транспорт команд в Blender, [SETUP 08: ASSET PIPELINE](../setups/setup_08_asset_pipeline.md) — общая схема работы с ассетами. ## 🔗 СВЯЗИ - **Прочие в категории «Воркфлоу»:** [WF 01: TEXT-TO-SCENE](wf_01_text_to_scene.md), [WF 02: IMAGE-TO-3D](wf_02_image_to_3d.md), [WF 03: TEXTURE PIPELINE](wf_03_texture_pipeline.md), [WF 04: CHARACTER ANIMATION](wf_04_character_animation.md), [WF 05: PROCEDURAL ASSETS](wf_05_procedural_assets.md) - **По темам:** [WF 06: ARCHVIZ WORKFLOW](wf_06_archviz_workflow.md), [WF 07: GAME ASSET PIPELINE](wf_07_game_asset_pipeline.md), [WF 08: BATCH PROCESSING](wf_08_batch_processing.md) - **Теги:** #audio, #workflow --- # WF 28: LOCALIZATION TITLES Tags: #localization #motion #workflow # WF 28: LOCALIZATION TITLES **ID:** WF-28 | **Type:** Publish | **Difficulty:** ⭐ **RU/EN Bilingual** > Когда применять: когда ролик с титрами надо отдать на пять языков или в четыре вертикальных формата, и перерисовывать каждое слово вручную — не вариант. ## 🎯 ЦЕЛЬ Из одного исходного SRT или списка реплик получаются титры на N языков, автоматически расставленные по таймкодам, проверенные по **safe area** под конкретный формат кадра, и экспортированные отдельными файлами. Два свойства, которые делают такую систему пригодной к повторному использованию: титры живут **как данные** (текст + таймкод в таблице), а не как нарисованные объекты в сцене; и проверка safe area — это код, а не «посмотрел глазами». ## 📋 ЧТО ПОНАДОБИТСЯ | Компонент | Версия / состояние | Зачем | |---|---|---| | Blender с VSE | встроенный | полосы-титры и таймкоды | | Файл SRT или CSV | UTF-8 | источник текста и таймкодов | | Шрифт с нужными наборами символов | `.ttf`/`.otf`, Latin + CJK + Cyrillic | иначе будут «квадратики» | | Таблица safe area | считается в коде | проверка попадания под обрезку площадок | > [!NOTE] > Импорт субтитров как готовых полос в VSE появился в Blender 4.x (`Text Import` в меню VSE → Add, оператор вроде `bpy.ops.sequencer.text_import`). В более старых версиях этого нет. Код ниже **не зависит** от него: он создаёт полосы-текст сам, поэтому работает везде, где есть `new_effect(type='TEXT')`. ## 🛠️ НАСТРОЙКА ### Этап 1. Разбор SRT ```bash RU: "Локализация титров: исходный //subs/ru.srt, языки en, de, es, zh. Создай полосы-текст по таймкодам исходника, проверь safe area под 16:9 и 9:16, экспортируй рендеры //subs/out/_" EN: "Subtitle localization: source //subs/ru.srt, languages en, de, es, zh. Create text strips on the source timings, check safe area for 16:9 and 9:16, export renders to //subs/out/_" ``` ```python import re TS = re.compile(r"(\d{2}):(\d{2}):(\d{2})[,.](\d{3})\s*-->\s*(\d{2}):(\d{2}):(\d{2})[,.](\d{3})") def parse_srt(path): def ms(h, m, s, f): return ((int(h) * 60 + int(m)) * 60 + int(s)) * 1000 + int(f) cues, idx, t0, t1, buf = [], 0, None, None, [] with open(path, encoding="utf-8-sig") as fh: for raw in fh.read().splitlines() + [""]: line = raw.strip() if not line: if t0 is not None: cues.append((t0, t1, "\n".join(buf))) t0, t1, buf = None, None, [] continue m = TS.match(line) if m: g = m.groups() t0, t1 = ms(*g[:4]), ms(*g[4:]) elif t0 is not None and not line.isdigit(): buf.append(line) return cues cues = parse_srt(r"<путь-к-проекту>\subs\ru.srt") print(f"реплик: {len(cues)}") for (a, b, txt) in cues[:3]: print(f"{a:>7} .. {b:<7} | {txt[:40]!r}") ``` ### Этап 2. Перевод как данные, не как сцена Пока перевода нет, таймкоды живут отдельно от текста. Это позволяет генерировать языки в любом порядке и не трогать сцену. ```python import json, os # translations.json: { "en": [[start_ms, end_ms, "text"], ...], ... } # Ключ времени — не индекс: сдвиг на одну строку в переводе не должен ломать синхронизацию. path = r"<путь-к-проекту>\subs\translations.json" if os.path.exists(path): with open(path, encoding="utf-8") as fh: TRANSLATIONS = json.load(fh) else: TRANSLATIONS = {"en": [[a, b, "[перевод]"] for a, b, _ in cues]} for lang, rows in TRANSLATIONS.items(): bad = [i for i, (a, b, _) in enumerate(rows) if b <= a] print(f"{lang}: {len(rows)} реплик, некорректных таймкодов {len(bad)}") ``` > [!NOTE] > Реальной автоматической трансляции в Blender нет. Переводы приходят из внешней TMS, из таблицы или из GPT-подсказок, которую человек ревьюит. Всё, что делает Blender, — раскладка уже готового текста по таймкодам. Не выдавай перевод за автоматический, если это не так. ### Этап 3. Полосы-текст в VSE ```python import bpy sc = bpy.context.scene sc.render.fps = 24 sc.frame_start, sc.frame_end = 0, int(200 * 24) se = sc.sequence_editor or sc.sequence_editor_create() def strips_container(se): for attr in ("strips", "sequences"): c = getattr(se, attr, None) if c is not None: return c raise AttributeError("не найдено ни strips, ни sequences в sequence_editor") strips = strips_container(se) def add_subtitle(text, start_ms, end_ms, channel=2): f0 = int(start_ms / 1000 * sc.render.fps) f1 = max(f0 + 1, int(end_ms / 1000 * sc.render.fps)) st = strips.new_effect(name=f"sub_{f0:05d}", type='TEXT', channel=channel, frame_start=f0) st.frame_final_end = f1 for prop, val in (("text", text), ("font_size", 0.052), ("align_x", 'CENTER'), ("align_y", 'BOTTOM'), ("use_shadow", True), ("box_color", (0.0, 0.0, 0.0, 0.55)), ("use_box", True)): try: setattr(st, prop, val) except (AttributeError, TypeError): pass # свойства текстовых полос различаются между версиями — не молчим, а продолжаем return st lang = "en" made = 0 for start_ms, end_ms, text in TRANSLATIONS[lang]: add_subtitle(text, start_ms, end_ms) made += 1 print(f"полос-титров: {made}, всего полос в VSE: {len(strips)}") ``` > [!NOTE] > Свойства текстовых полос (`text`, `font_size`, `align_x`, `align_y`, `use_box`, `box_color`, `use_shadow`, `shadow_color`) и их точные имена менялись. Код выше ставит их через `setattr` в `try/except` — если имя не подошло, полоса создастся без этого свойства, и это будет видно визуально. Список актуальных имён смотри так: `[p.identifier for p in bpy.types.TextSequence.bl_rna.properties]` (или `bpy.types.TextEffect`, если так называется тип в твоей версии). ### Этап 4. Safe area: считаем, а не смотрим Зоны: для 16:9 обычно 90% по действию и 80% для названий; вертикальные форматы площадок режут по бокам гораздо сильнее. Считать надо в пикселях финального кадра. ```python def safe_area(res_x, res_y, action=0.90, title=0.80): """Прямоугольники в пикселях, внутри которых текст обязателен.""" def rect(f): w = res_x * f h = res_y * f x0, y0 = (res_x - w) / 2, (res_y - h) / 2 return (x0, y0, x0 + w, y0 + h) return {"action": rect(action), "title": rect(title)} for name, (rx, ry) in (("16:9", (1920, 1080)), ("9:16", (1080, 1920)), ("1:1", (1080, 1080))): sa = safe_area(rx, ry) a, t = sa["action"], sa["title"] print(f"{name:<5} action={tuple(round(v) for v in a)} title={tuple(round(v) for v in t)}") def text_fits(n_chars, font_px, box_w_px): """Грубая оценка: средняя ширина глифа ≈ 0.5em. Точнее не сделать без шрифта.""" return n_chars * font_px * 0.5 <= box_w_px box = safe_area(1920, 1080)["action"] for line in ("TRANSFORMERS ARE WATCHING", " transformers are watching "): ok = text_fits(len(line), 34, box[2] - box[0]) print(f"{line.strip():<28} влезает в action-safe 16:9: {ok}") ``` ### Этап 5. Вертикальный формат Тот же монтаж, другая геометрия кадра. Перекомпоновка — это не «обрезать по бокам», а сдвинуть смысловой центр в новый центр. ```python import bpy, math sc = bpy.context.scene def set_format(name, rx, ry): sc.render.resolution_x, sc.render.resolution_y = rx, ry sc.render.resolution_percentage = 100 cam = sc.camera if cam is None: return cam.data.sensor_fit = 'VERTICAL' # иначе FoV считается по другой оси base_fov = cam.data.lens # для вертикального кадра угол по ширине должен вырасти if name == "9:16": cam.data.lens = base_fov * (9 / 16) * (16 / 9) cam.location = (cam.location.x, cam.location.y * 1.15, cam.location.z) cam.keyframe_insert("location", frame=sc.frame_start) set_format("9:16", 1080, 1920) print(sc.render.resolution_x, sc.render.resolution_y, sc.camera.data.sensor_fit if sc.camera else "камеры нет") ``` ### Этап 6. Экспорт по языкам ```python import bpy, os, shutil sc = bpy.context.scene se = sc.sequence_editor strips = strips_container(se) os.makedirs(bpy.path.abspath("//subs/out"), exist_ok=True) all_subs = [s for s in strips if s.type == 'TEXT'] def only_language(lang, rows, channel=2): """Оставляем только полосы одного языка на своём канале.""" wanted = {int(a / 1000 * sc.render.fps) for a, _, _ in rows} for s in all_subs: s.channel = channel if s.frame_start in wanted else 1 sc.render.use_sequencer = True for lang, rows in TRANSLATIONS.items(): only_language(lang, rows) sc.render.filepath = bpy.path.abspath(f"//subs/out/{lang}_") sc.render.image_settings.file_format = 'PNG' bpy.ops.render.render(animation=True) shutil.copy(bpy.path.abspath("//subs/translations.json"), bpy.path.abspath("//subs/out/translations.json")) ``` ## ⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ | Параметр | Значение | Когда менять | |---|---|---| | `render.fps` | совпадает с исходным таймкодом | смена fps ломает тайминги субтитров | | `font_size` | 3.5–5.5% высоты кадра | для 9:16 обычно крупнее по проценту ширины | | safe area action | 90% | большинство площадок | | safe area title | 80% | названия и логотипы | | `align_y` | `'BOTTOM'` | нижние трети по умолчанию | | `use_box` | `True` при тёмном фоне | без подложки текст пропадает на светлом | | канал полосы | отдельный на язык | позволяет держать все языки в одной сцене | ## ⚠️ ТИПОВЫЕ ОШИБКИ | Симптом | Причина | Решение | |---|---|---| | `AttributeError` на `sequences` | переименование API в 4.4 | резолвер `strips_container` выше | | Текст выводится, но не с тем шрифтом/размером | часть свойств не применилась (типы полос разные) | вызвать `setattr` в `try/except` и проверить визуально; список имён — из `bl_rna` | | Субтитры «уехали» на полсекунды | fps сцены не совпал с исходником | таймкоды пересчитываются как `ms/1000*fps` при каждом изменении fps | | Текст обрезан площадкой | не попал в safe area | прогнать `safe_area` и подвинуть `align_y`/кегль | | «Квадратики» в CJK | в шрифте нет нужных глифов | подключить шрифт с полным набором в `Preferences → File Paths → Fonts` | | Длинная реплика не влезает | не включён перенос по словам | включить перенос строк и/или уменьшить `font_size` для длинных реплик | | Все языки наложились друг на друга | полосы на одном канале | развести по каналам, включать нужный при экспорте | ## ✅ РЕЗУЛЬТАТ На выходе: один `.blend` со всеми языками на разных каналах, файл `//subs/out/translations.json` с таймкодами и текстами, и набор отрендеренных прогонов — по одному на язык и формат (например 4 языка × 2 формата = 8 прогонов). Характеристики — **оценка по порядку величины, а не измеренный результат**: 3-минутный ролик на 24 fps — около 4300 кадров на прогон. VSE-рендер (пересборка готовых клипов с наложением текста, без 3D) — секунды на кадр, то есть единицы минут на прогон; 8 прогонов — десятки минут. Полоса текста в VSE рендерится композитором видеоредактора и почти не стоит ничего по ресурсам. Кадр 1920×1080 PNG — 1–4 МБ. **Это оценка.** Если под титрами идёт 3D-сцена, а не готовые клипы, время определяется рендером 3D, а не VSE. Замеряй у себя. Базовые сетапы: [SETUP 01: BLENDER MCP BASIC](../setups/setup_01_blender_mcp_basic.md) — транспорт команд в Blender, [SETUP 08: ASSET PIPELINE](../setups/setup_08_asset_pipeline.md) — общая схема работы с ассетами. ## 🔗 СВЯЗИ - **Прочие в категории «Воркфлоу»:** [WF 21: MOTION GRAPHICS TITLES](wf_21_motion_graphics_titles.md), [WF 01: TEXT-TO-SCENE](wf_01_text_to_scene.md), [WF 02: IMAGE-TO-3D](wf_02_image_to_3d.md), [WF 03: TEXTURE PIPELINE](wf_03_texture_pipeline.md), [WF 04: CHARACTER ANIMATION](wf_04_character_animation.md) - **По темам:** [WF 05: PROCEDURAL ASSETS](wf_05_procedural_assets.md), [WF 06: ARCHVIZ WORKFLOW](wf_06_archviz_workflow.md), [WF 07: GAME ASSET PIPELINE](wf_07_game_asset_pipeline.md) - **Теги:** #localization, #motion, #workflow --- # P01:基础体素创建 Tags: #basics #bpy #geometry #primitives # P01:基础体素创建 **ID:** P01 | **类别:** Geometry Basics | **难度:** ⭐ | **标签:** #geometry #primitives #basics #bpy.ops --- ## 🎯 目标 通过 MCP 创建基础体素,精确控制参数 --- ## 📝 提示词 ``` 创建名为 "[NAME]" 的 [TYPE] 体素,位置 [X, Y, Z], 缩放 [SX, SY, SZ],旋转 [RX, RY, RZ](弧度)。 类型: CUBE, SPHERE, CYLINDER, PLANE, CONE, TORUS, EMPTY, CAMERA, LIGHT 示例: "在 (2, 0, 1) 创建立方体 'Box_01',缩放 (1, 2, 1),旋转 (0, 0, 0.785)" ``` --- ## 🔧 MCP 工具: `create_object` ```json { "type": "create_object", "params": { "entity_type": "CUBE", "name": "Box_01", "location": [2.0, 0.0, 1.0], "rotation": [0.0, 0.0, 0.785], "scale": [1.0, 2.0, 1.0] } } ``` --- ## 📊 各类型参数 | 类型 | 必需 | 可选 | |------|------|------| | CUBE | size | location, rotation, scale | | SPHERE | radius | segments, ring_count, location, rotation, scale | | CYLINDER | radius, depth | vertices, cap_ends, location, rotation, scale | | PLANE | size | location, rotation, scale | | CONE | radius1, radius2, depth | vertices, location, rotation, scale | | TORUS | major_radius, minor_radius | major_segments, minor_segments, location, rotation, scale | | EMPTY | — | location, rotation, scale, type | | CAMERA | — | location, rotation, lens, sensor_width | | LIGHT | — | location, rotation, type, energy, color | --- ## ⚠️ 常见错误 | 错误 | 原因 | 解决 | |------|------|------| | `Object not found` | 名称被占用 | 用唯一名称或检查 `get_scene_info` | | `Invalid rotation` | 用了角度而非弧度 | 换算: `radians = degrees * π / 180` | | `Primitive not created` | 上下文是 EDIT 模式 | 始终保持 OBJECT 模式 | --- ## 🔗 相关链接 - **案例:** [Case 02: Game Props](../cases/case_02_game_props.md) - **工作流:** [WF 01: Text-to-Scene](../workflows/wf_01_text_to_scene.md) - **下一个:** [P02: Boolean Operations](./02_boolean_operations.md) --- # P02:布尔运算 Tags: #boolean #difference #intersect #modeling #modifiers #union # P02:布尔运算 **ID:** P02 | **类别:** Geometry Basics | **难度:** ⭐⭐ | **标签:** #boolean #modeling #modifiers #difference #union #intersect --- ## 🎯 目标 通过 MCP 在对象间执行布尔运算(Difference、Union、Intersect) --- ## 📝 提示词 ``` 在 "[OBJECT_A]"(基础)和 "[OBJECT_B]"(工具)之间执行布尔运算 [OPERATION]。 运算:DIFFERENCE(相减)、UNION(合并)、INTERSECT(相交)。 结果:对象 A 被修改,对象 B 可以删除。 ``` --- ## 🔧 MCP: `execute_blender_code` ```python import bpy # 设置 obj_a = bpy.data.objects.get("Wall") obj_b = bpy.data.objects.get("Window_Cutout") if not obj_a or not obj_b: raise ValueError("Objects not found") # 确保 OBJECT 模式 if bpy.context.mode != 'OBJECT': bpy.ops.object.mode_set(mode='OBJECT') # 选择对象 bpy.ops.object.select_all(action='DESELECT') obj_a.select_set(True) obj_b.select_set(True) bpy.context.view_layer.objects.active = obj_a # 添加布尔修改器 bool_mod = obj_a.modifiers.new(name="Bool_Window", type='BOOLEAN') bool_mod.operation = 'DIFFERENCE' # 'UNION', 'INTERSECT' bool_mod.object = obj_b bool_mod.solver = 'EXACT' # 简单几何用 'FAST' # 应用 bpy.ops.object.modifier_apply(modifier="Bool_Window") # 删除工具对象(可选) bpy.data.objects.remove(obj_b, do_unlink=True) print(f"Boolean DIFFERENCE applied. {obj_a.name} modified.") ``` --- ## 📊 运算类型 | 运算 | 可视结果 | 用例 | |------|----------|------| | **DIFFERENCE** | A 减 B | 门窗、开孔、凹槽 | | **UNION** | A 加 B | 合并部件、融合网格 | | **INTERSECT** | 仅相交部分 | 遮罩、形状裁剪 | --- ## 💡 高级模式 ### 多个布尔(一次完成) ```python # 一次多个相减 cutters = [bpy.data.objects.get(f"Cutter_{i}") for i in range(5)] for cutter in cutters: mod = wall.modifiers.new(f"Bool_{cutter.name}", 'BOOLEAN') mod.operation = 'DIFFERENCE' mod.object = cutter mod.solver = 'EXACT' # 全部应用 for mod in list(wall.modifiers): if mod.type == 'BOOLEAN': bpy.context.view_layer.objects.active = wall bpy.ops.object.modifier_apply(modifier=mod.name) # 清理 for c in cutters: bpy.data.objects.remove(c, do_unlink=True) ``` ### 注意法线 ```python # 布尔运算前检查并修复法线 bpy.context.view_layer.objects.active = obj_a bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.normals_make_consistent(inside=False) bpy.ops.object.mode_set(mode='OBJECT') ``` --- ## ⚠️ 常见问题 | 问题 | 原因 | 解决 | |------|------|------| | 结果为空 | 法线内外反了 | `Mesh > Normals > Recalculate Outside` | | 几何伪影 | 非流形边 | `Select > Select All by Trait > Non-Manifold` → 修复 | | 复杂网格很慢 | 面数太多 | 先用 `DECIMATE`,或用 `FAST` solver | | 对象消失 | 相交为空 | 检查对象位置 | --- ## 🔗 相关链接 - **案例:** [Case 01: ArchViz Interior](../cases/case_01_archviz_interior.md) — 墙体门窗 - **工作流:** [WF 01: Text-to-Scene](../workflows/wf_01_text_to_scene.md) - **相关提示词:** [P01 Primitives](./01_geometry_basics.md), [P03 Modifiers Stack](./03_modifiers_stack.md) - **下一个:** [P03: Modifiers Stack](./03_modifiers_stack.md) --- # P09:PRINCIPLED BSDF 设置 Tags: #materials #pbr #principled-bshf #shader-nodes # P09:PRINCIPLED BSDF 设置 **ID:** P09 | **类别:** Materials PBR | **提示词:** ⭐ | **Tags:** #materials #pbr #principled-bshf #shader-nodes --- ## 🎯 目标 创建并配置带完整 PBR 贴图集的 Principled BSDF 材质 --- ## 📝 提示词 ``` 创建 "[MAT_NAME]" 材质,类型 Principled BSDF: Base Color: [R, G, B] 或 hex #RRGGBB Roughness: [0.0-1.0] Metallic: [0.0-1.0] [可选] Transmission, IOR, Clearcoat, Sheen, Emission 指定给对象 "[OBJECT_NAME]" ``` --- ## 🔧 MCP: `set_material`(基础) ```json { "type": "set_material", "params": { "object_name": "Cube", "material_name": "Mat_Red", "color": [1.0, 0.0, 0.0, 1.0] } } ``` --- ## 🔧 MCP: `execute_blender_code`(完整 PBR) ```python import bpy from pathlib import Path def create_pbr_material(name, maps_dict, obj_name=None): """ maps_dict keys: 'base_color', 'roughness', 'metallic', 'normal', 'height', 'ao', 'emission' values: 文件路径或颜色元组 (R,G,B,A) """ mat = bpy.data.materials.new(name) mat.use_nodes = True nodes = mat.node_tree.nodes links = mat.node_tree.links nodes.clear() # Principled BSDF principled = nodes.new('ShaderNodeBsdfPrincipled') principled.location = (0, 0) # Material Output output = nodes.new('ShaderNodeOutputMaterial') output.location = (400, 0) links.new(principled.outputs['BSDF'], output.inputs['Surface']) # Displacement disp = nodes.new('ShaderNodeDisplacement') disp.location = (200, -200) links.new(disp.outputs['Displacement'], output.inputs['Displacement']) mat.displacement_method = 'BOTH' # Texture Coordinate + Mapping tex_coord = nodes.new('ShaderNodeTexCoord') tex_coord.location = (-600, 0) mapping = nodes.new('ShaderNodeMapping') mapping.location = (-400, 0) links.new(tex_coord.outputs['UV'], mapping.inputs['Vector']) def add_texture(key, input_name, colorspace='sRGB', use_normal_map=False, use_disp=False): if key not in maps_dict: return val = maps_dict[key] if isinstance(val, (tuple, list)): # 颜色 principled.inputs[input_name].default_value = val + (1.0,) if len(val)==3 else val return # 图像文件 tex = nodes.new('ShaderNodeTexImage') tex.image = bpy.data.images.load(str(val)) tex.image.colorspace_settings.name = colorspace tex.location = (-200, 0) links.new(mapping.outputs['Vector'], tex.inputs['Vector']) if use_normal_map: nmap = nodes.new('ShaderNodeNormalMap') nmap.location = (0, -100) links.new(tex.outputs['Color'], nmap.inputs['Color']) links.new(nmap.outputs['Normal'], principled.inputs[input_name]) elif use_disp: links.new(tex.outputs['Color'], disp.inputs['Height']) else: links.new(tex.outputs['Color'], principled.inputs[input_name]) # 连接贴图 add_texture('base_color', 'Base Color', 'sRGB') add_texture('roughness', 'Roughness', 'Non-Color') add_texture('metallic', 'Metallic', 'Non-Color') add_texture('normal', 'Normal', 'Non-Color', use_normal_map=True) add_texture('height', 'Height', 'Non-Color', use_disp=True) add_texture('emission', 'Emission Color', 'sRGB') # AO -> 与 Base Color 混合 if 'ao' in maps_dict: ao_tex = nodes.new('ShaderNodeTexImage') ao_tex.image = bpy.data.images.load(str(maps_dict['ao'])) ao_tex.image.colorspace_settings.name = 'Non-Color' ao_tex.location = (-200, 200) links.new(mapping.outputs['Vector'], ao_tex.inputs['Vector']) mix = nodes.new('ShaderNodeMixRGB') mix.blend_type = 'MULTIPLY' mix.location = (0, 100) # 通过 Mix 重连 Base Color if principled.inputs['Base Color'].links: from_node = principled.inputs['Base Color'].links[0].from_node from_socket = principled.inputs['Base Color'].links[0].from_socket links.new(from_socket, mix.inputs[1]) links.new(ao_tex.outputs['Color'], mix.inputs[2]) links.new(mix.outputs['Color'], principled.inputs['Base Color']) # 指定给对象 if obj_name: obj = bpy.data.objects.get(obj_name) if obj: if obj.data.materials: obj.data.materials[0] = mat else: obj.data.materials.append(mat) return mat # === 使用示例 === # 1. 纯色 create_pbr_material("Mat_Red", { 'base_color': (1.0, 0.2, 0.2, 1.0), 'roughness': 0.5, 'metallic': 0.0 }, "Cube") # 2. 金色 create_pbr_material("Mat_Gold", { 'base_color': (1.0, 0.843, 0.0, 1.0), 'roughness': 0.1, 'metallic': 1.0 }, "Ring") # 3. 玻璃 create_pbr_material("Mat_Glass", { 'base_color': (1.0, 1.0, 1.0, 1.0), 'roughness': 0.0, 'transmission': 1.0, # 需要时加到函数中 'ior': 1.45 }, "Glass_Obj") # 4. 来自文件的完整 PBR create_pbr_material("Mat_Brick", { 'base_color': Path("//textures/brick_basecolor.png"), 'roughness': Path("//textures/brick_roughness.png"), 'normal': Path("//textures/brick_normal.png"), 'height': Path("//textures/brick_height.png"), 'ao': Path("//textures/brick_ao.png"), }, "Wall") ``` --- ## 📊 PBR 参数速查 | 材质 | Base Color | Roughness | Metallic | Transmission | IOR | Clearcoat | |----------|------------|-----------|----------|--------------|-----|-----------| | **塑料** | 颜色 | 0.3-0.5 | 0.0 | 0.0 | 1.5 | 0.0 | | **金属(抛光)** | 金属色 | 0.05-0.15 | 1.0 | 0.0 | — | 0.0 | | **金属(锈蚀/哑光)** | 金属色 | 0.6-0.9 | 1.0 | 0.0 | — | 0.0 | | **玻璃** | 白色 | 0.0 | 0.0 | 1.0 | 1.45-1.5 | 0.0 | | **水** | 浅蓝 | 0.05 | 0.0 | 1.0 | 1.33 | 0.0 | | **皮肤** | 肤色 | 0.6-0.8 | 0.0 | 0.0 | 1.5 | 0.1-0.3 | | **布料** | 布色 | 0.8-1.0 | 0.0 | 0.0 | 1.5 | 0.0 | | **陶瓷** | 颜色 | 0.1-0.3 | 0.0 | 0.0 | 1.5 | 0.2-0.5 | | **石头/混凝土** | 灰/米 | 0.7-1.0 | 0.0 | 0.0 | 1.5 | 0.0 | | **木材** | 木色 | 0.6-0.9 | 0.0 | 0.0 | 1.5 | 0.0 | --- ## 🎨 特殊效果 ### Emission(发光) ```python principled.inputs['Emission Color'].default_value = (1.0, 0.8, 0.2, 1.0) # 暖光 principled.inputs['Emission Strength'].default_value = 5.0 # 强度 ``` ### Clearcoat(清漆/陶瓷) ```python principled.inputs['Coat Weight'].default_value = 1.0 principled.inputs['Coat Roughness'].default_value = 0.03 ``` ### Sheen(布料/丝绒) ```python principled.inputs['Sheen Weight'].default_value = 0.5 principled.inputs['Sheen Roughness'].default_value = 0.3 principled.inputs['Sheen Tint'].default_value = 0.5 ``` ### Subsurface(皮肤、蜡、大理石) ```python principled.inputs['Subsurface Weight'].default_value = 0.3 principled.inputs['Subsurface Radius'].default_value = (1.0, 0.2, 0.1) # RGB 穿透半径 principled.inputs['Subsurface Color'].default_value = (1.0, 0.3, 0.1, 1.0) ``` ### Anisotropic(拉丝表面) ```python principled.inputs['Anisotropic'].default_value = 0.8 principled.inputs['Anisotropic Rotation'].default_value = 0.25 # 90° # 需要 Tangent 输入(UV 或 Geometry > Tangent) ``` --- ## 🔗 СВЯЗИ - **案例:** [Case 01: ArchViz](../cases/case_01_archviz_interior.md), [Case 03: Product Viz](../cases/case_03_product_viz.md) - **工作流:** [WF 03: Texture Pipeline](../workflows/wf_03_texture_pipeline.md) - **提示词:** [P10 Node Groups](./10_node_groups.md), [P11 Procedural Textures](./11_procedural_textures.md) - **MCP 工具:** `set_material`(基础),`execute_blender_code`(完整) - **下一个:** [P10: Node Group Creation](./10_node_groups.md) --- # 配置 01:BLENDER-MCP 基础 # 配置 01:BLENDER-MCP 基础 **ID:** SETUP-01 | **类型:** 核心基础设施 | **难度:** ⭐ | **用时:** 10-15 分钟 --- ## 🎯 目标 安装并配置 blender-mcp,使其与 IDE 智能体协作(OpenCode、Cursor、VS Code、Codex、Claude Desktop) --- ## 📋 前置要求 | 组件 | 版本 | 状态 | |------|------|------| | **Blender** | 4.0+ | ✅ 必需 | | **Python** | 3.10+ | ✅ Blender 自带 | | **uv** | latest | ✅ 必需 | | **Git** | latest | ⚠️ 可选 | --- ## 🪟 WINDOWS 安装(分步) ### 1. 安装 uv ```powershell # 以管理员身份运行 PowerShell irm https://astral.sh/uv/install.ps1 | iex # 重启终端 # 验证 uv --version uvx --version ``` ### 2. 安装 blender-mcp 插件 ```powershell # 自动安装到 Blender uvx blender-mcp install-addon # 检查路径 uvx blender-mcp addon-paths # 应显示: C:\Users\\AppData\Roaming\Blender Foundation\Blender\4.x\scripts\addons\blender_mcp.py ``` ### 3. 配置 PATH(如果找不到 uvx) ```powershell # 查找 uvx where uvx # 通常: C:\Users\\.local\bin\uvx.exe # 添加到用户 PATH $localBin = "$env:USERPROFILE\.local\bin" $userPath = [Environment]::GetEnvironmentVariable("Path", "User") [Environment]::SetEnvironmentVariable("Path", "$userPath;$localBin", "User") # 重启终端/IDE ``` --- ## 🔧 BLENDER 设置 ### 1. 启用插件 ``` Edit → Preferences → Add-ons 搜索: "MCP for Blender" ☑ Interface: MCP for Blender ``` ### 2. 启动 MCP 服务器 ``` N 面板(按 N 键)→ "MCP for Blender" 选项卡 ☑ Poly Haven ☑ Poly Pizza(粘贴 API 密钥) ☐ Sketchfab(可选) ☐ Hyper3D / Hunyuan3D(可选) 🟢 按钮: "Start MCP Server" ``` ### 3. 在系统控制台中检查 ``` Window → Toggle System Console 应出现: "BlenderMCP server started on localhost:9876" ``` --- ## 🤖 IDE 智能体配置 ### OpenCode (`~/.config/opencode/opencode.jsonc`) ```jsonc { "mcp": { "blender-mcp": { "type": "local", "command": ["uvx", "blender-mcp"], "enabled": true, "environment": { "BLENDER_HOST": "localhost", "BLENDER_PORT": "9876" } } } } ``` ### Cursor (Settings → MCP → Add Server) ```json { "mcpServers": { "blender": { "command": "cmd", "args": ["/c", "uvx", "blender-mcp"] } } } ``` ### VS Code (MCP 扩展) ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["blender-mcp"] } } } ``` ### Codex ```bash codex mcp add blender -- uvx blender-mcp ``` ### Claude Desktop (`claude_desktop_config.json`) ```json { "mcpServers": { "blender": { "command": "uvx", "args": ["blender-mcp"] } } } ``` --- ## ✅ 连接测试 ### 在 OpenCode / 终端中: ```bash # 简单测试 "获取当前场景信息" ``` ### 预期回复: ```json { "name": "Scene", "object_count": 2, "objects": [ {"name": "Cube", "type": "MESH", "location": [0, 0, 0]}, {"name": "Light", "type": "LIGHT", "location": [0, 0, 5]} ], "materials_count": 1 } ``` ### 创建对象测试: ```bash "在位置 (2, 0, 0) 创建立方体 'TestCube',缩放 (1, 2, 1)" ``` --- ## 🔑 API 密钥(可选) | 服务 | 获取地址 | 粘贴位置 | |------|----------|----------| | **Poly Pizza** | https://poly.pizza/settings/api | Blender Preferences → Add-ons → MCP for Blender → Poly Pizza API Key | | **Sketchfab** | https://sketchfab.com/developers/api | Blender Preferences → Add-ons → MCP for Blender → Sketchfab API Key | | **Hyper3D** | https://hyper3d.ai/ | Blender Preferences → Add-ons → MCP for Blender → Hyper3D API Key | | **Hunyuan3D** | https://3d.hunyuan.tencent.com/ | Blender Preferences → Add-ons → MCP for Blender → SecretId/SecretKey | --- ## ⚠️ 常见问题 | 问题 | 解决方法 | |------|----------| | `spawn uvx ENOENT` | 将 `~/.local/bin` 加入 PATH,重启 IDE | | `Connection refused` | Blender 未运行或 MCP 服务器未启动(N 面板 → Start) | | `Timeout` | 请求太复杂 → 拆分成小步骤 | | `Poly Pizza 403` | Cloudflare 拦截 → 手动下载 .glb → File → Import → glTF | | `ModuleNotFoundError: mcp` | 以管理员身份运行一次 Blender 以自动安装依赖 | --- ## 🔗 相关链接 - **提示词:** [P01-P05](../prompts/) — 基础命令 - **案例:** [Case 02: Game Props](../cases/case_02_game_props.md) - **工作流:** [WF 01: Text-to-Scene](../workflows/wf_01_text_to_scene.md) - **下一个:** [Setup 02: GenesisCore Local](../setups/setup_02_genesiscore_local.md) --- # 配置 02:本地 GENESISCORE (OLLAMA) # 配置 02:本地 GENESISCORE (OLLAMA) **ID:** SETUP-02 | **类型:** 本地 LLM 集成 | **难度:** ⭐⭐ | **用时:** 15-30 分钟 --- ## 🎯 目标 配置 GenesisCore 使用本地 LLM(Ollama):隐私、免费、离线 --- ## 📋 前置要求 | 组件 | 版本 | 状态 | |------|------|------| | **Blender** | 4.0+ | ✅ 必需 | | **Ollama** | latest | ✅ 必需 | | **RAM** | 16GB+ | 🟡 推荐 | | **VRAM** | 8GB+ | 🟡 7B+ 模型需要 | --- ## 🪟 WINDOWS 安装 ### 1. 安装 Ollama ```powershell # 下载并安装 winget install Ollama.Ollama # 或从官网下载 # https://ollama.com/download/windows # 检查 ollama --version ollama list ``` ### 2. 下载代码/Blender 模型 ```powershell # 最适合 Blender Python 的模型(按优先级) ollama pull codellama:13b # 最适合代码,7.3GB ollama pull deepseek-coder:6.7b # Python 极佳,3.8GB ollama pull qwen2.5-coder:7b # 新,速度快,4.7GB ollama pull llama3.2:3b # 轻量,速度快,2.0GB # 检查 ollama list ``` ### 3. 启动 Ollama 服务 ```powershell # 应以后台运行(安装时自动启动) # 检查: curl http://localhost:11434/api/tags # 如果不行: ollama serve ``` --- ## 🔧 GENESISCORE 设置 ### 1. 安装插件(如尚未安装) ```bash # 在 Blender: Edit → Preferences → Add-ons → Install from ZIP # 选择: GenesisCore-main.zip # 启用: GenesisCore ``` ### 2. 配置服务商 ``` N 面板 → GenesisCore 选项卡: 1. Provider: ▼ Ollama 2. Base URL: http://localhost:11434/v1 (重要:末尾要有 /v1!) 3. API Key: (Ollama 留空) 4. 点击 "Fetch Models" 4. 选择模型: codellama:13b / deepseek-coder:6.7b / qwen2.5-coder:7b 5. Tool Modules: 全部启用(Shift+Click) 6. Save Config 7. Connect ``` ### 3. 模型设置(推荐) ```json { "provider": "Ollama", "base_url": "http://localhost:11434/v1", "model": "codellama:13b", "temperature": 0.1, "top_p": 0.9, "max_tokens": 4096, "use_history": true } ``` --- ## 🔧 性能优化 ### GPU Offloading (NVIDIA) ```powershell # 有 CUDA 时 Ollama 自动使用 GPU # 检查: ollama run codellama:13b "test" # 日志中应显示 "using GPU" # 强制启用 GPU layers: # 在环境变量中: OLLAMA_NUM_GPU=999 OLLAMA_GPU_LAYERS=999 ``` ### 内存优化 ```yaml # 在 GenesisCore 设置中: use_history: false # 关闭历史以节省 token max_tokens: 2048 # 限制回答长度 temperature: 0.1 # 确定性回答 ``` ### 量化(显存不足时) ```powershell # 使用量化版本: ollama pull codellama:13b-q4_K_M # 4-bit,7.3GB → 4.7GB ollama pull deepseek-coder:6.7b-q4_K_M # 3.8GB → 2.5GB ``` --- ## 🧪 测试 ### 在 GenesisCore 聊天中快速测试 ``` "在位置 (1, 2, 3) 创建立方体 'TestCube',红色材质" ``` ### 预期结果 1. 智能体分析请求 2. 调用 `create_object` + `set_material` 3. Blender 中在 (1, 2, 3) 出现红色立方体 --- ## 🔧 故障排除 | 问题 | 解决方法 | |------|----------| | `Connection refused` | Ollama 未运行 → `ollama serve` | | `Model not found` | `ollama pull ` | | `Out of memory` | 用更小的模型 (3b/7b) 或 q4 量化 | | `Slow response` | 启用 GPU offloading,减小 `max_tokens` | | `Base URL error` | 检查 `http://localhost:11434/v1`(要有 /v1!) | --- ## 🔗 相关链接 - **Ollama 模型:** https://ollama.com/library - **GenesisCore 仓库:** https://github.com/AIGODLIKE/GenesisCore - **配置 01:** [blender-mcp Basic](../setups/setup_01_blender_mcp_basic.md) - **下一个:** [Setup 03: Dream Textures](../setups/setup_03_dream_textures.md) --- # WF 01:文本生成场景 # WF 01:文本生成场景 **ID:** WF-01 | **类型:** 端到端管线 | **难度:** ⭐⭐ | **用时:** 15-30 分钟 --- ## 🎯 目标 完整管线:文字描述 → 3D 场景(几何、材质、灯光、相机、渲染) --- ## 📋 管线架构 ``` USER PROMPT │ ▼ ┌─────────────────────────────────────┐ │ LLM AGENT (OpenCode/Claude) │ │ - 解析描述 │ │ - 规划步骤 │ │ - 调用 MCP 工具 │ └─────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────┐ │ BLENDER-MCP SERVER (localhost:9876)│ │ - create_object │ │ - set_material │ │ - download_polyhaven_asset │ │ - execute_blender_code │ └─────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────┐ │ BLENDER (bpy 执行) │ │ - 创建几何 │ │ - 指定材质 │ │ - 布置灯光 │ │ - 放置相机 │ │ - 执行渲染 │ └─────────────────────────────────────┘ │ ▼ 结果: 渲染图 / .blend 文件 / GLB 导出 ``` --- ## 📝 提示词示例 ### 简单:房间 ``` "创建 5x5x3 米房间:地板、4 面墙、天花板。 在前墙开 1.5x1.5 米窗(boolean)。 材质:墙-白色灰泥,地板-拼花木,窗-玻璃。 灯光:Poly Haven HDRI 'studio' 2k。 相机:在角落看向中心,35mm。 渲染:Cycles 128 samples,1920x1080,保存到 //renders/room.png" ``` ### 中等:地牢房间 ``` "创建 10x10x5 米暗黑幻想地牢房间。 墙:开裂石砌 (PBR)。地板:缝隙带苔藓的石板。 道具:3 根柱子、2 个箱子、中央祭坛。 灯光:3 个点光源(火焰)+ 弱 HDRI 'dungeon'。 相机:低角度,祭坛 DOF。 渲染:256 samples,denoising ON" ``` --- ## 🔧 按阶段的 MCP 命令 ### 阶段 1:基础几何 ```bash "创建平面 'Floor' 10x10 米于 (0,0,0)" "创建立方体 'Wall_Front' 10x0.2x3 米于 (0, -5, 1.5)" ``` ### 阶段 2:布尔(窗/门) ```bash "创建立方体 'Window_Cut' 2x0.3x1.5 米于 (0, -5.1, 1.5) 从 Wall_Front 减去 Window_Cut (DIFFERENCE)" ``` ### 阶段 3:材质 ```bash "创建材质 'Mat_Parquet': BaseColor #8B7355, Roughness 0.6,赋给 Floor" ``` ### 阶段 4:Poly Haven ```bash "在 Poly Haven 搜索 HDRI 'studio' indoor 类 2k,下载并设为 World" ``` ### 阶段 5:相机和渲染 ```bash "创建相机 'Cam_Main' 于 (7, -7, 4) 看向中心,35mm,设为活动相机,中心 DOF" "设置 Cycles: 128 samples, denoising ON, 1920x1080,渲染到 //renders/scene_01.png" ``` --- ## 🔗 相关链接 - **配置:** [Setup 01: blender-mcp](../setups/setup_01_blender_mcp_basic.md) - **下一个:** [WF 02: Image-to-3D](../workflows/wf_02_image_to_3d.md) --- # WF 02:图片转 3D # WF 02:图片转 3D **ID:** WF-02 | **AI:** AI 生成管线 | **⭐⭐** | **每个资产 10-30 分钟** --- ## 🎯 目标 通过 AI 将 2D 图像(照片、草图、概念图)转为 3D 模型 --- ## 📋 管线架构 ``` INPUT IMAGE │ ▼ ┌─────────────────────────────────────┐ │ PREPROCESSING │ │ • Background removal (RMBG) │ │ • Normalize aspect ratio │ │ • Multiple views generation │ └─────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────┐ │ AI 3D GENERATION │ │ • TripoSR / LRM / Zero123++ │ │ • Hunyuan3D / Hyper3D (cloud) │ │ • Mesh + Textures + UV │ └─────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────┐ │ POST-PROCESSING │ │ • Retopology (Quad Remesher) │ │ • UV Unwrap │ │ • PBR Texture Baking │ │ • LOD Generation │ └─────────────────────────────────────┘ │ ▼ OUTPUT: .glb / .fbx / .obj + PBR textures ``` --- ## 🤖 AI 模型(2024/2025) ### 本地(免费,需要 GPU) | 模型 | 质量 | 速度 | 显存 | 安装 | |--------|----------|----------|------|-----------| | **TripoSR** | ⭐⭐⭐⭐ | ~0.5s | 6GB | `pip install triposr` | | **LRM(Large Reconstruction Model)** | ⭐⭐⭐⭐⭐ | ~2s | 8GB | `pip install lrm` | | **Zero123++** | ⭐⭐⭐ | ~5s | 6GB | `pip install zero123pp` | | **InstantMesh** | ⭐⭐⭐⭐ | ~10s | 8GB | `pip install instant-mesh` | ### 云端(API,质量更高) | 服务 | 质量 | 费用 | API | |--------|----------|-----------|-----| | **Hunyuan3D 2.0** | ⭐⭐⭐⭐⭐ | 免费层 | Tencent Cloud | | **Hyper3D Rodin** | ⭐⭐⭐⭐⭐ | $0.5/次 | hyper3d.ai | | **Meshy.ai** | ⭐⭐⭐⭐ | $0.2/次 | meshy.ai | | **CSM (Common Sense Machines)** | ⭐⭐⭐⭐ | $0.3/次 | csm.ai | | **Tripo AI** | ⭐⭐⭐⭐ | $0.1/次 | tripo3d.ai | --- ## 🔧 MCP 命令 ### 本地生成(TripoSR) ```bash # 安装 pip install triposr torch torchvision # 通过 Python 生成 python -c " from triposr import TripoSR model = TripoSR.from_pretrained('VAST-AI/TripoSR') output = model.generate('input_image.png', num_inference_steps=50) # output: 含 vertices、faces、textures 的 mesh 字典 " ``` ### MCP:HYPER3D / HUNYUAN3D(经由 blender-mcp) ```bash # 在 OpenCode / Cursor 中: "用 Hyper3D Rodin 从图像 'concept_art.png' 生成 3D 模型 1. 加载图像 2. 启动生成 3. 等待完成 4. 导入结果到 Blender 4. 自动重拓扑到 10k tris 5. UV 展开 + PBR 纹理烘焙 6. 导出为 GLB" ``` ### MCP:MESHY.AI ```bash "用 Meshy.ai 从 'product_photo.jpg' 生成 3D: 1. 加载照片 2. 参数:quality=high, texture_resolution=2048 3. 下载结果为 GLB 4. 导入 Blender 5. 检查 UV 和材质" ``` --- ## 📝 分步管线(本地 TripoSR) ### 1. 图像预处理 ```python # preprocess.py from PIL import Image from rembg import remove def preprocess_image(input_path, output_path, size=512): img = Image.open(input_path) # 去除背景 img_no_bg = remove(img) # 带填充的方形裁剪 img_square = make_square(img_no_bg, size) # 保存 img_square.save(output_path, "PNG") return output_path def make_square(img, size): # 为正方形添加白色/透明背景 w, h = img.size max_dim = max(w, h) new_img = Image.new('RGBA', (max_dim, max_dim), (0,0,0,0)) new_img.paste(img, ((max_dim-w)//2, (max_dim-h)//2)) return new_img.resize((size, size), Image.LANCZOS) ``` ### 2. 网格生成(TripoSR) ```python # generate_mesh.py from triposr import TripoSR import numpy as np import trimesh def generate_3d(image_path, output_dir, steps=50): model = TripoSR.from_pretrained('VAST-AI/TripoSR') # 生成 output = model.generate(image_path, num_inference_steps=steps) # 转为 trimesh vertices = output['vertices'] faces = output['faces'] mesh = trimesh.Trimesh(vertices=vertices, faces=faces) # 导出 mesh.export(f"{output_dir}/model.obj") mesh.export(f"{output_dir}/model.glb") # 纹理(如有) if 'texture' in output: Image.fromarray(output['texture']).save(f"{output_dir}/texture.png") return mesh ``` ### 3. 在 BLENDER 中后处理(MCP) ```bash # 在 OpenCode 中: "把生成的 model.glb 导入 Blender: 1. File → Import → glTF 2.0 2. 自动重拓扑:Quad Remesher target=10000 tris 3. UV 展开:Smart Project angle=66 island_margin=0.01 4. PBR 纹理烘焙:BaseColor, Roughness, Normal, Metallic, AO 5. 材质:Principled BSDF + Texture Nodes 5. 导出:GLB + 独立 2k 纹理" ``` --- ## 📊 质量与规格 | 方法 | Tris | 纹理分辨率 | UV 质量 | 用时 (RTX 3080) | |-------|------|-------------|------------|-----------------| | **TripoSR (raw)** | 10k-50k | 512x512 | Auto UV | 0.5s | | **TripoSR + Retopo** | 5k-15k | 2k | Clean UV | 30s | | **LRM** | 20k-100k | 1024x1024 | Good UV | 2s | | **Hunyuan3D 2.0** | 10k-50k | 2k-4k | Excellent UV | 30s (API) | | **Hyper3D Rodin** | 10k-30k | 4k | Production UV | 60s (API) | --- ## 💡 技巧 | 技巧 | 原因 | |-----|-----| | 用 `rembg` 去背景 | 干净剪影 = 更好几何 | | 多视角(front/side/back) | LRM/Hunyuan 支持多视角 | | 方形输入 512x512 | 多数模型的标准 | | 后重拓扑是必须的 | 原始网格不适合游戏/动画 | | UV 之后做 PBR 烘焙 | 法线/腔体来自几何 | --- ## 🔗 LINKS - **TripoSR:** https://github.com/VAST-AI-Research/TripoSR - **LRM:** https://github.com/lrm-dev/LRM - **InstantMesh:** https://github.com/TencentARC/InstantMesh - **Hunyuan3D:** https://3d.hunyuan.tencent.com/ - **Hyper3D:** https://hyper3d.ai/ - **Meshy:** https://www.meshy.ai/ - **配置 03:** [Dream Textures](../setups/setup_03_dream_textures.md) - **下一个:** [WF 03: Texture Pipeline](../workflows/wf_03_texture_pipeline.md) ---